From bbc8cb33d36ee664ca58513c7598f07a3dbf11d3 Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 25 Jul 2026 18:47:25 +0700 Subject: [PATCH 1/3] docs: add Claude-facing usage, internals and analysis references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three references written against v1.2.11 that were sitting untracked: - CLAUDE.md — usage reference: component API, theming, common patterns. - CLAUDE-INTERNALS.md — how the library is built, for modifying it. - ANALYSIS.md — analysis of the repo as cloned on 2026-07-18. Committing so they survive and can be updated alongside the code rather than being regenerated from scratch each time. Co-Authored-By: Claude Fable 5 --- ANALYSIS.md | 68 ++++++++++++++++++++ CLAUDE-INTERNALS.md | 144 ++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 149 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 361 insertions(+) create mode 100644 ANALYSIS.md create mode 100644 CLAUDE-INTERNALS.md create mode 100644 CLAUDE.md diff --git a/ANALYSIS.md b/ANALYSIS.md new file mode 100644 index 0000000..cf841a9 --- /dev/null +++ b/ANALYSIS.md @@ -0,0 +1,68 @@ +# @pathscale/ui — Internals Analysis + +*Analysis of the repo at `github.com/pathscale/ui`, v1.2.11, cloned 2026-07-18.* + +## What it is + +A highly opinionated **SolidJS component library** (~104 component directories, 419 TS/TSX source files) targeting **HeroUI API parity** in Solid, built on a daisyUI-flavored CSS-variable theming system. It is the product of a large migration (documented in `docs/component-migration-map.md`): ~40 old components were removed, several renamed (`Loading→Spinner`, `DropdownSelect→Select`, `RadialProgress→ProgressCircle`), and the styling layer was refactored into per-component `.classes.ts` + `.css` files. + +Toolchain: **Bun** (runtime, package manager, test runner), **rslib** (bundleless ESM build), **Biome** (lint/format), **Tailwind v4** (consumer-side; the library authors plain CSS + tokens), **TypeScript 6 strict**. + +## Architecture at a glance + +``` +src/ + index.ts # 922-line barrel — the single public surface + index.css # imports base + themes + icons, declares Tailwind v4 @theme tokens + components/ # ~104 folders, each: X.tsx + X.classes.ts + X.css + index.ts + _shared/ # createOverlayPosition — bespoke floating-ui replacement + hooks/ # date/ (internal date engine), form/ (TanStack Form wrapper), + # table/ (TanStack Table state slices), layout/ (useDesktop) + primitives/ # virtualizer/ (TanStack Virtual wrapper), streaming/ (buffer + subscription) + motion/ # driver-based tween engine + Solid components (MotionDiv, Presence, AnimatedCollapse) + lib/ # mergeRefs, chain, OverrideProps, homegrown cva, tag helpers + styles/ # base/, themes/{light,dark}.css, icons/generated-icons.css +``` + +### The component contract (enforced by CI) + +Every component is a four-file quartet: `X.tsx` (implementation), `X.classes.ts` (a `CLASSES` const mapping semantic slots/variants to BEM-ish class strings like `button--primary`), `X.css` (actual styles in `@layer components`, driven by theme CSS variables), and `index.ts` (barrel). `scripts/check-contracts.ts` runs before every build and CI, and fails the build if a component lacks an `index.ts` with a type export, lacks `splitProps` or `twMerge` in its main file, or contains a purely static inline `style={{}}`. + +Conventions worth knowing: +- **Class merging**: `twMerge(CLASSES.base, CLASSES.variant[v], flag && CLASSES.flag.x, local.class, local.className)` — consumer classes always win. Both `class` and `className` are accepted (React-refugee ergonomics). +- **Boolean props are `is*`** (`isDisabled`, `isOpen`, `isInvalid`), HeroUI-style, reconciled with native attrs (`isDisabled ?? disabled`). +- **`data-slot` on every part** plus state mirrored to `data-open`/`data-selected`/`data-invalid` etc., so CSS and tests target data attributes. +- **Compound components** via `Object.assign(Root, {Trigger, Content, ...})` + Solid context holding accessors/setters; controlled/uncontrolled via the `value/defaultValue/onChange` triple pattern. +- **No polymorphic `as` prop** — element types are hardcoded per part. + +### Notable subsystems + +- **Overlay positioning is bespoke** (`src/components/_shared/overlayPosition.ts`) — no floating-ui. Auto-flip, viewport clamping, rAF-batched, used by Dropdown/Select/pickers. All positioning bugs live in this one file. +- **Motion** (`src/motion/`): a driver-based tween system. The default driver is **immediate** — everything snaps to its end state until the consumer installs a real driver via `enablePopmotion(animate)` (popmotion is an optional peer; `popmotion.ts` is a 17-line adapter, not vendored code). Presets (`route`, `fade`, `toast`…) are built from tokens; `Presence`/`MotionDiv`/`AnimatedCollapse` are the Solid layer. Note: most components (Modal, Toast, Drawer) animate with **CSS**, not this system. +- **Metal-border** (newest feature): a WebGL "liquid metal" plasma border. All instances share **one offscreen GL context and one ~15fps RAF loop**; each instance blits a cropped region to its own 2D canvas and punches out the interior. Includes an SVG glow subsystem driven by GPU pixel readback with a hotspot state machine. Graceful degradation when WebGL is unavailable; pauses offscreen (IntersectionObserver) and under reduced-motion. +- **Forms**: `createForm` wraps TanStack Form with Standard Schema (Zod/Valibot/Arktype) validation wired to change+blur+submit; `Form`/`FormField`/`FormSubmitButton` compose it. Field errors are touch-gated (invisible until blur). +- **Dates**: a **fully internal date engine** (native `Date` + `Intl`, all dates constructed at noon to dodge DST) powering Calendar/RangeCalendar/DatePicker/DateRangePicker. The segmented `date-field`/`time-field` inputs are a separate, unrelated code path. +- **Table**: headless — thin presentational compound parts over `src/hooks/table` state-slice hooks wrapping TanStack Table. Virtualization pieces (`useVirtualRows`, `VirtualSpacerRow`) ship separately and are **not wired in**; integration is the consumer's job. +- **Icons**: build-time Iconify pipeline. `pluginIconify` scans source for `icon-[set--name]` classes and emits `src/styles/icons/generated-icons.css` (committed, currently 14 icons) with data-URI SVG masks. The `Icon` component just merges the icon class name onto a span. +- **Theming**: 2 themes (`light` default, `dark`) switched via `data-theme` on ``. Tokens are daisyUI-compatible (`--color-primary`, `--color-base-100`, `--b1`…) plus HeroUI-style (`--color-default/-foreground`) plus a ~35-variable glass-surface set. `src/index.css`'s `@theme` block re-exposes them as Tailwind v4 utilities. + +### Build & CI + +`bun run build` = contract check → `rslib build` (bundleless ESM, file-per-module, d.ts, CSS copied verbatim) → purge-manifest generation (`dist/purge-manifest.json`, consumed by `@pathscale/rsbuild-plugin-ui-css-purge` so apps can drop CSS for components they don't import). CI (GitHub Actions on `master`) runs contract check, `tsc --noEmit`, and build; PR previews are built from the playground and deployed to Surge. Versioning/publishing is manual (bare version-number commits). The playground (`playground/`, Vite + Tailwind v4) aliases `@pathscale/ui` to local `src/` for instant hot-reload — its `App.tsx` is a single ~7,300-line demo page. + +## Issues found (worth acting on) + +1. **🔴 Leaked credential**: `.github/workflows/preview.yml` hardcodes a Surge token in plaintext (`SURGE_TOKEN: 256124bc...`). Anyone can deploy/teardown sites on that account. Rotate the token and move it to repo secrets. +2. **Broken `./stores` export**: `package.json` exports `./stores` → `dist/stores/index.js`, but there is no `src/stores/` — the build never produces that file, so importing `@pathscale/ui/stores` throws. Remove the entry or add the module. +3. **Stale README**: it tells consumers to import `dist/styles/compat/daisy-primitives.css` (no `compat/` exists in source) and points to `docs/motion.md` (doesn't exist; only `component-migration-map.md` does). +4. **Tests exist but CI never runs them**: 4 test files under `tests/` (bun:test, pure-function coverage only) and no `test` script in package.json; CI doesn't invoke `bun test`. +5. **CI depends on a sibling checkout**: the build symlinks `pathscale/rsbuild-plugin-ui-css-purge` to `../rsbuild-plugin-ui-css-purge` — local builds may behave differently from CI if you don't replicate that layout. +6. **Duplicated helpers**: `invokeEventHandler` is copy-pasted into ~6 components (Card, Dropdown, Tabs, Toast, Select, Table), as is `sortItemsByDomOrder` — a bug fix must touch all copies. +7. **Dead/legacy code**: the homegrown `cva` in `src/lib/style/classes.ts` is imported by no component (the real styling engine is `CLASSES` + `twMerge`); `scripts/copy-css.js` is superseded by rslib's copy step; `cally` is an unused devDependency; `useFieldNew.ts` is legacy naming (exports `useField`). +8. **Minor internal inconsistencies**: metal-border's header comment says ~30fps but the constant is 66ms (~15fps); its `GLOW_READBACK_INTERVAL_MS` throttle constant isn't actually applied in the loop; `AnimatedCollapse` has an intentionally no-op ResizeObserver placeholder. + +## Overall assessment + +The library is coherent and unusually disciplined for its size: one enforced component anatomy, one styling contract, one state-management idiom, and heavy leverage of TanStack for the hard generic problems (form/table/virtual) while keeping bespoke code where it pays off (overlay positioning, date engine, motion seam, WebGL effects). The main risks are operational rather than architectural: the leaked CI token, the untested-in-CI test suite, dead config (`./stores`), and copy-pasted micro-helpers. The design choice that most often surprises newcomers: **nothing in the motion system animates until a driver is installed**, and **the virtualizer is intentionally not integrated into Table**. + +Companion docs (for Claude's future sessions, also human-readable): [CLAUDE.md](CLAUDE.md) (usage reference) and [CLAUDE-INTERNALS.md](CLAUDE-INTERNALS.md) (modification guide). diff --git a/CLAUDE-INTERNALS.md b/CLAUDE-INTERNALS.md new file mode 100644 index 0000000..55a8b92 --- /dev/null +++ b/CLAUDE-INTERNALS.md @@ -0,0 +1,144 @@ +# @pathscale/ui — Internals & Modification Guide (for Claude) + +How the library works inside and how to change it safely. Usage reference: [CLAUDE.md](CLAUDE.md). Human analysis: [ANALYSIS.md](ANALYSIS.md). + +## Repo map + +``` +src/index.ts # 922-line barrel = the ONLY public surface; every new export goes here +src/index.css # imports styles/base, themes/{dark,light}, icons; Tailwind v4 @theme token block +src/components// # one dir per component (quartet, see below) +src/components/_shared/ # overlayPosition.ts (createOverlayPosition) — bespoke floating-ui +src/components/types.ts # IComponentBaseProps {dataTheme, class, className, style}, ComponentSize/Color/Variant/Position/Shape, ResponsiveProp +src/components/utils.tsx # wrapWithElementIfInvalid (Dynamic wrapper for raw children) +src/hooks/{date,form,layout,table}/ +src/primitives/{virtualizer,streaming}/ +src/motion/ # tween engine + solid/ components +src/lib/{iterable,refs,props,style,tag}/ +src/styles/{base,themes,icons}/ +scripts/check-contracts.ts # build gate (see Contract section) +tests/ # bun:test, pure functions only, NOT run in CI +playground/ # Vite app aliasing @pathscale/ui -> ../src (hot dev) +docs/component-migration-map.md +``` + +## Build & toolchain + +- `bun run build` = `check-contracts.ts` → `rslib build` → `postbuild:manifest` (purge-manifest generator from `@pathscale/rsbuild-plugin-ui-css-purge` → `dist/purge-manifest.json`). +- rslib (`rslib.config.ts`): entry glob `./src/**/*.{ts,tsx}`, `bundle:false` (dist mirrors src, `outBase: "./src"`), ESM only, d.ts unbundled, deps/peers auto-external. CSS is **copied verbatim** (not processed): `src/styles/**/*.css → dist/styles`, `src/components/**/*.css → dist/components`, `src/index.css → dist/`. Plugins: babel(+solid) and `pluginIconify` (targetDir `src/styles/icons`, scans for `icon-[set--name]`, resolves from @iconify/json, writes committed `generated-icons.css` — only icons actually referenced). +- Tailwind directives in the CSS (`@theme`, `@layer`, `@source`) are resolved by the **consumer's** Tailwind v4 pipeline, not this build. +- Dev: `bun run dev` (rslib watch) or, better, `bun run playground:dev` (Vite alias → instant HMR of src). Lint/format: Biome (`bun run lint`, `bun run format`; 2-space, 80 cols, double quotes). Types: `npx tsc --noEmit` (TS strict, `~/*` and `@src/*` → `src/*`). Tests: `bun test` (no npm script; NOT in CI). +- CI (`.github/workflows/ci.yml`, master): checks out `pathscale/rsbuild-plugin-ui-css-purge` and symlinks it to `../rsbuild-plugin-ui-css-purge`, then `bun install`, `bun run check`, `tsc --noEmit`, `bun run build`. Preview workflow deploys playground to Surge per-PR (⚠️ hardcoded Surge token in the workflow file — known leak). Versioning: manual bare-version commits (e.g. `1.2.11`); publish is manual; `files: ["dist"]`. + +## The contract (enforced — build fails otherwise) + +`scripts/check-contracts.ts` scans every `src/components/*` dir (skip-list: `types.ts, utils.tsx, showcase, showcase-section, props-table, icon, form`) and requires: +1. `index.ts` barrel exists and exports a type (must contain the string `type `). +2. `PascalCase.tsx` (derived from kebab dir name), if present, contains **both `splitProps` and `twMerge`**. +3. No purely-static inline `style={{...}}` (dynamic markers `${`, backtick, spread, call, ternary are allowed). + +CONTRIBUTING.md adds (not machine-checked): `is*` booleans default false, `ComponentSize`/`ComponentColor` enums, events pass values not events, aria/keyboard requirements, `function` components with explicit `: JSX.Element`, no hardcoded English strings, comments explain why, `class` canonical (`className` compat only). + +## Component anatomy (the quartet) + +``` +src/components/foo-bar/ + FooBar.tsx # implementation + FooBar.classes.ts # export const CLASSES = { base: "foo-bar", variant: {...}, flag: {...} } as const — plain BEM-ish strings + FooBar.css # @layer components { .foo-bar { ... } } — styles via theme CSS vars, often color-mix(in oklab, ...) + index.ts # export { default as FooBar, type FooBarProps } from "./FooBar"; +``` +Then add exports to `src/index.ts`. + +Canonical implementation skeleton (from Button): +```tsx +const [local, others] = splitProps(props, ["children","class","className","dataTheme","style","variant","size", ...]); +const variant = () => local.variant ?? parentCtx?.variant() ?? "primary"; +const classes = () => twMerge( + CLASSES.base, CLASSES.variant[variant()], CLASSES.size[size()], + local.isIconOnly && CLASSES.flag.isIconOnly, + local.class, local.className, // consumer overrides LAST, always both +); +return + +``` + +## Component inventory (by family) + +- **Layout/primitives**: Flex, Grid, Join, Surface, Card, GlassPanel, Separator, ScrollShadow, Skeleton, EmptyState, Footer, Header, Navbar, Toolbar, FloatingDock +- **Typography/misc**: Text, Link, Kbd, Badge, Chip, Tag/TagGroup, Avatar, Icon, Tooltip, Breadcrumbs, Pagination, Meter, ProgressBar, ProgressCircle, Spinner (alias: Loading) +- **Inputs**: Input, InputGroup, InputOTP, TextField, TextArea (and textarea), NumberField, SearchField, PasswordField (+ password-requirements/rules, `passwordRules.ts`), ColorField, Checkbox(+Group), Radio(+Group), Toggle, Slider, Select, ComboBox, ListBox, SizePicker, Form pieces (Label, Description, ErrorMessage, FieldError, Fieldset) +- **Dates**: Calendar, RangeCalendar, DatePicker, DateRangePicker (internal date engine); DateField, TimeField (separate segmented editors) +- **Color**: ColorPicker, ColorArea, ColorSlider, ColorSwatch(+Picker), ColorWheelFlower, ThemeColorPicker +- **Overlays**: Modal, Drawer, Popover, Dropdown, Menu, Toast, Disclosure(+Group), Accordion +- **Data**: Table (headless compound + hooks), plus primitives `useVirtualRows`, `useStreamingBuffer`, `useStreamingSubscription` +- **Auth kit**: AuthForm, AuthCard, AuthFieldGroup, AuthSubmitButton, AuthFooterLinks, AuthPoweredBy, AuthErrorMessage, AuthSuccessMessage — thin Tailwind-utility wrappers composing Button/Card/fields +- **Visual FX**: MetalBorder (WebGL liquid-metal border; presets `chromatic|silver|gold`, `kind="pill"|"circle"`, `glow`, `strength` 0-100, `theme="dark"|"light"|"auto"`), GlowCard (mouse-tracking glow), NoiseBackground (animated gradient blobs), ImmersiveLanding (full mini-app w/ PWA widgets), VideoPreview, LiveChat, ChatBubble, LanguageSwitcher + +Renames from old versions (see `docs/component-migration-map.md`): Loading→Spinner, DropdownSelect→Select, RadialProgress→ProgressCircle, RangeSlider→Slider, Progress→ProgressBar/ProgressCircle. ~40 components removed outright (Carousel, Rating, Steps, Stats, FileInput, …). + +## Forms (TanStack Form + Standard Schema) + +```tsx +import { createForm, Form, FormField, FormSubmitButton } from "@pathscale/ui"; + +const form = createForm({ + defaultValues: { email: "", password: "" }, + schema: loginSchema, // any Standard Schema: Zod v4 / Valibot / ArkType + onSubmit: async (values) => { await login(values); }, +}); + +
+ + + Log in + +``` + +- `Form` without a `form` prop = plain styled `
` (`FormRoot`). With `form` = context provider + wired submit. +- Inside a ``: `useField(name)` → `{value, error, touched, invalid, handleChange, handleBlur}`. **Errors are touch-gated** — `error()` is `undefined` until the field blurs. `FormSubmitButton` disables on `!canSubmit` (not touch-gated), so the button can be disabled with no visible error. +- Escape hatch: `form._tsForm` is the raw TanStack form API (typed `any` on purpose). +- Schema validation runs on change+blur+submit; blur errors clear immediately on change once valid. + +## Table (headless assembly) + +```tsx +import { useTableModel, useTableSorting, useTablePagination, TableRoot, TableContent, ... } from "@pathscale/ui"; + +const sorting = useTableSorting(); +const pagination = useTablePagination(); // default page sizes [10,25,50,100] +const table = useTableModel({ + data: () => rows(), columns, + sorting: sorting.sorting, setSorting: sorting.setSorting, + pagination: pagination.pagination, setPagination: pagination.setPagination, + enableSorting: true, enablePagination: true, +}); +// render table.getHeaderGroups()/getRowModel().rows into: +// … +``` + +- State-slice hooks (all controlled-or-uncontrolled): `useTableSorting`, `useTableSelection`, `useTableFiltering` (per-column popovers + `getColumnFilterProps`), `useTablePagination` (⚠️ `nextPage(max)`/`lastPage(max)` need caller-supplied max page index), `useTableExpansion`. +- Parts: TableRoot/ScrollContainer/Content/Header/Column/Body/Row/Cell/ExpandedRow/Footer/PageSize/ResizableContainer/ColumnResizer/LoadMore(+Content), plus SortIcon, ExpandToggle, InlineConfirm, MobileListView (responsive card fallback), VirtualSpacerRow. +- **Virtualization is not built in**: combine `useVirtualRows` (wraps @tanstack/solid-virtual) + `VirtualSpacerRow` yourself. Playground has examples: `playground/src/examples/Table*.tsx`. + +## Motion + +```ts +import { enablePopmotion } from "@pathscale/ui"; +import { animate } from "popmotion"; +enablePopmotion((opts) => animate({ ...opts })); // ⚠️ WITHOUT this, all JS animations SNAP to end state +``` + +- `runMotion(el, from, to, transition?, onComplete?)` — animates `opacity/x/y/scale`; **durations in seconds**. +- Presets via `getPreset/resolvePreset` (`route`, `routeAuth`, `authSwap`, `fade`, `fadeUp`, `scaleIn`, `toast`, `routeDashboard`); `registerPreset` mutates the global set; `createMotionSystem()` for isolated instances; `createRouteTransitionResolver({rules, fallback})` for route rules. +- Solid components: `{(isExiting, onExitComplete) => …` — Presence force-unmounts after 800ms if `onExitComplete` never fires. `` for height collapse. +- `resolvePreset(name, {reduceMotion})` returns the `noMotion` preset under prefers-reduced-motion. Note Modal/Toast/Drawer animate via CSS, not this system. + +## Streaming + +```ts +const buf = useStreamingBuffer({ strategy: "upsert", maxSize: 500, getKey: r => r.id }); +useStreamingSubscription({ + subscribe: (o) => { const es = new EventSource(url); es.onmessage = e => o.next(JSON.parse(e.data)); return () => es.close(); }, + onData: buf.add, +}); +// buf.rows() is the reactive, capped, deduped array +``` +Strategies: `append` (ignore duplicate keys) | `upsert` (replace in place) | `replace`. Subscription returns `{isLive, isConnecting, error, eventCount, start, stop}`; auto-starts unless `enabled` is false. + +## Toast (imperative singleton) + +```ts +import { toast, ToastProvider } from "@pathscale/ui"; +// mount once, then anywhere: +toast.success("Saved"); toast.danger("Failed"); toast.promise(p, {loading, success, error}); +``` + +## Icons + +Icons are Iconify classes: `` or bare `class="icon-[lucide--search]"`. In this repo they're baked at build time into `src/styles/icons/generated-icons.css` (only icons actually used get emitted). Consumer apps with Tailwind v4 can use `@plugin "@iconify/tailwind4"` for arbitrary icons (playground does this). + +## Dates + +Calendar/DatePicker/RangeCalendar/DateRangePicker use the internal engine (native Date + Intl; no date lib). Values are `Date` objects; ranges are `{start: Date, end: Date}`. Controlled via `value/defaultValue/onChange`. `DateField`/`TimeField` are separate segmented text editors, not calendar-backed. + +## Playground (fastest way to try things) + +```sh +bun install && cd playground && bun install && cd .. +bun run playground:dev # Vite; @pathscale/ui aliased to local src/ — edits hot-reload, no rebuild +``` +`playground/src/App.tsx` (~7,300 lines) demos every component; examples in `playground/src/examples/` (Form, Motion, Streaming, Table×3). Playground forces `data-theme="dark"` at runtime in `playground/src/index.tsx`. From a5552b74bea460746b7198171904ac70875335c8 Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 25 Jul 2026 20:21:01 +0700 Subject: [PATCH 2/3] docs: adopt the AGENTS.md agent standard + guardrails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings this repo onto the same agent-instruction standard as the rest of the pathscale codebase, following PakhomovAlexander/project-hub. AGENTS.md is canonical; CLAUDE.md is a thin `@AGENTS.md` import. Codex, Cursor and Gemini CLI read AGENTS.md natively and Claude Code loads it through the import, so every agent lands on the same runbook instead of a per-vendor fork. Contents are stack-aware rather than boilerplate: invariants, build/test commands and release discipline reflect what this repo actually is (Rust crate vs binary vs workspace; JS library vs application; which package manager the lockfile says). Shared across every repo: Verification ("run what you build"; compare against the base branch instead of asserting; a suspiciously fast build was cached), PR discipline, docs-are-shared-memory-not-private-memory, and the git workflow. Guardrails are enforced, not just documented: .claude/settings.json pre-allows read-only commands for this stack and prompts before pushes, publishing, `gh pr merge`, cloud CLIs and deploys. The PreToolUse hook catches wrapper forms a permission glob misses (`env X=y git push`, `git -C dir push`, `bash -c '…'`) and includes `bun` in the publish watchlist, since that is the package manager in use here. The CI-green rule ships commented out with its reason and enabling steps: CI does not reliably attach checks to pull requests yet, so "wait for green" would teach an agent to wait on nothing. It is a separate project. .gitignore drops superseded blocks on CLAUDE.md, AGENTS.md and /.claude so this guidance is tracked; /.claude/settings.local.json stays ignored, since personal overrides should not be shared. Co-Authored-By: Claude Fable 5 --- .claude/hooks/ask-before-risky-commands.sh | 69 +++++++++ .claude/settings.json | 43 ++++++ AGENTS.md | 84 +++++++++++ CLAUDE.md | 155 ++------------------- 4 files changed, 205 insertions(+), 146 deletions(-) create mode 100755 .claude/hooks/ask-before-risky-commands.sh create mode 100644 .claude/settings.json create mode 100644 AGENTS.md diff --git a/.claude/hooks/ask-before-risky-commands.sh b/.claude/hooks/ask-before-risky-commands.sh new file mode 100755 index 0000000..4f8788c --- /dev/null +++ b/.claude/hooks/ask-before-risky-commands.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Claude Code PreToolUse(Bash) gate — pathscale backend service. +# +# Prompts before prod-affecting / destructive commands in ANY wrapper form +# (env VAR=val …, tool -C …, chained with ; && |, multi-line, quoted via +# bash -c '…'). For everything else it stays SILENT (exit 0, no decision), so the +# normal permission rules — permissions.allow / ask / deny in .claude/settings.json +# and the session's permission mode — decide as usual. +# +# This is one layer of defense, not a replacement for the permission system: a +# pattern match over a command string is best-effort (quoting and indirection can +# evade any blocklist). It backs up the declarative permissions.ask list in +# .claude/settings.json — KEEP THE TWO IN SYNC — and for fully autonomous runs, +# prefer OS-level sandboxing on top. +# +# Adapted from PakhomovAlexander/project-hub. Edit RISKY_WORDS for this repo; +# tooling; further branches below gate `git`/`docker push`, `git clean`, recursive +# `rm`, `find -delete`, package publishing, PR-merge/release via `gh`, and a deploy +# script run by path. Add your own command families (e.g. `ssh`, a bespoke deploy +# CLI) to RISKY_WORDS, or trim what you don't use — and mirror the change in +# permissions.ask in .claude/settings.json. +set -u + +# --- the watchlist: command words that should prompt before running ---------------- +RISKY_WORDS="aws|gcloud|az|kubectl|helm|terraform|terragrunt|flyctl|fly" +# ----------------------------------------------------------------------------------- + +# Pull the command out of the hook's stdin JSON (jq if available, else python3). +if command -v jq >/dev/null 2>&1; then + cmd="$(jq -r '.tool_input.command // ""' 2>/dev/null)" +else + cmd="$(python3 -c 'import sys, json; print(json.load(sys.stdin).get("tool_input", {}).get("command", ""))' 2>/dev/null)" +fi + +# Couldn't read the command → stay neutral, let normal permission rules decide. +[ -z "$cmd" ] && exit 0 + +# A command word counts as "at a command position" after start-of-line, whitespace, +# ; & | ( or a quote — and ends before whitespace, a quote, ) ; & | or end-of-line — +# so `bash -c 'git push'` is still seen. +b="(^|[[:space:];&|(\"'\`])" +e="([[:space:];&|)\"'\`]|$)" +# Global options that may sit between a tool and its subcommand (git -C dir push). +opts='([[:space:]]+(-[A-Za-z-]+|[-_A-Za-z0-9]+=[^[:space:]]+|-C[[:space:]]+[^[:space:]]+))*' + +re="${b}(${RISKY_WORDS})${e}" +# git push / git clean, docker push — allowing global options before the subcommand. +re="$re|${b}git${opts}[[:space:]]+(push|clean)${e}" +re="$re|${b}docker${opts}[[:space:]]+push${e}" +# recursive rm (-r / -R / -fr / --recursive), with flags/paths in any order. +re="$re|${b}rm([[:space:]]+[^[:space:]]+)*[[:space:]]+(-[A-Za-z]*[rR]|--recursive)" +# find … -delete — irreversible bulk delete. +re="$re|${b}find([[:space:]]+[^[:space:]]+)*[[:space:]]+-delete${e}" +# package publishing — ships artifacts to a registry. +re="$re|${b}(npm|pnpm|yarn|bun|cargo|gem)([[:space:]]+[^[:space:]]+)*[[:space:]]+publish${e}" +re="$re|${b}twine([[:space:]]+[^[:space:]]+)*[[:space:]]+upload${e}" +# gh mutations that merge, ship, or destroy. +re="$re|${b}gh[[:space:]]+(pr[[:space:]]+merge|repo[[:space:]]+delete|release[[:space:]]+(create|delete))${e}" +re="$re|${b}gh[[:space:]]+api([[:space:]]+[^[:space:]]+)*[[:space:]]+(-X|--method)[[:space:]]+(POST|PUT|PATCH|DELETE)${e}" +# a deploy script invoked by path, e.g. ./scripts/deploy.sh — a word-list can't see it. +re="$re|${b}([./A-Za-z0-9_-]*/)?deploy(\.[A-Za-z]+)?${e}" +# WorkTable data migrations and endpoint regeneration rewrite committed artifacts. +re="$re|${b}([./A-Za-z0-9_-]*/)?regenerate_endpoints(\.[A-Za-z]+)?${e}" + +if printf '%s\n' "$cmd" | grep -Eq "$re"; then + printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"Prod-affecting / destructive command — confirm before running."}}' +fi +# No match → no output: fall through to the normal permission flow. +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..33e2ea1 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,43 @@ +{ + "permissions": { + "allow": [ + "Bash(bun run build:*)", + "Bash(bun run test:*)", + "Bash(bun run typecheck:*)", + "Bash(bun run lint:*)", + "Bash(bun install:*)", + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(git log:*)" + ], + "ask": [ + "Bash(git push:*)", + "Bash(cargo publish:*)", + "Bash(npm publish:*)", + "Bash(bun publish:*)", + "Bash(docker push:*)", + "Bash(gh pr merge:*)", + "Bash(gh release:*)", + "Bash(aws:*)", + "Bash(gcloud:*)", + "Bash(az:*)", + "Bash(kubectl:*)", + "Bash(helm:*)", + "Bash(terraform:*)", + "Bash(flyctl:*)" + ] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/ask-before-risky-commands.sh\"" + } + ] + } + ] + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..56a9fb5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,84 @@ +# Working agreement — UI + +The operating contract for **any** coding agent working in this repository. This file is +the single source of truth for the rules: Codex, Cursor and Gemini CLI read `AGENTS.md` +natively, and Claude Code loads it through the `@AGENTS.md` import in +[`CLAUDE.md`](CLAUDE.md). **Never fork these rules into a per-vendor file.** + +**JavaScript/TypeScript library** (`@pathscale/ui`), built with `bun`. + +## Invariants (don't break these) + +- **Publishing to npm is irreversible.** A version can never be reused. Check the built artifact before releasing. +- **`bun` is the package manager** — its lockfile is authoritative. Don't introduce a second one by running npm/yarn/pnpm here. +- **Docs describe what is true now.** If you change behaviour, update the README and any affected doc in the same change. + +## Build & run + +```bash +bun install +bun run dev +bun run build +bun run lint +``` + +## Verification + +Run what you build before reporting it done. Type-checks and tests verify code correctness, +not feature correctness — **if you can't run it, say so explicitly** rather than implying +success. + +- Compare against the base branch rather than asserting: a pre-existing failing test or lint + error is not something you introduced, and saying so requires checking. +- A build that finishes suspiciously fast was cached, not rebuilt. Force a real rebuild when + the rebuild is the thing you're verifying. + +## PR discipline + +**Always paste the full PR URL** (`https://github.com/pathscale/UI/pull/`), not just the number, so it's +clickable. + + + +## Keeping docs honest + +Hit a factual error here — a stale path, a wrong command, a moved status? Fix it in the same +change. Don't open cosmetic rewording PRs. + +Learned something durable — a gotcha, a decision, a constraint? It belongs **in this repo's +docs**, not in your agent's private memory. Repo docs are versioned, reviewable, and visible +to every agent and human; private memory dies with your machine. + +## Git workflow + +- **Always specify the branch when pushing**: `git push origin branch-name` +- **Branch naming**: `fix/issue-description` or `feat/issue-description` +- **No force-pushing** during PR review + +## Guardrails + +[`.claude/settings.json`](.claude/settings.json) and [`.claude/hooks/`](.claude/hooks/) make +Claude Code prompt a human before prod-affecting or destructive commands — pushes, publishing +to a registry, `gh pr merge`, cloud CLIs, recursive deletes, deploy scripts. + +**Other agents don't get that net automatically.** Apply the same rule yourself: ask before +running any command family listed in +[`.claude/hooks/ask-before-risky-commands.sh`](.claude/hooks/ask-before-risky-commands.sh). +It is one layer of defence, not a guarantee — a pattern match over a command string is +best-effort. diff --git a/CLAUDE.md b/CLAUDE.md index e811c14..bd54075 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,149 +1,12 @@ -# @pathscale/ui — Usage Reference (for Claude) +@AGENTS.md -SolidJS component library, HeroUI-parity API, daisyUI-style theming. v1.2.11. ~104 components. -This file = **how to USE the library** (as a consumer or when writing demos/examples). -For **modifying the library itself**, read [CLAUDE-INTERNALS.md](CLAUDE-INTERNALS.md). Human analysis: [ANALYSIS.md](ANALYSIS.md). +# Claude Code notes — UI -## Install & setup +The import above is binding: [`AGENTS.md`](AGENTS.md) is the **working agreement** for this +repository, and every Claude Code session loads it automatically. Don't copy rules here — +one source of truth, no drift. Only genuinely Claude-specific wiring belongs below. -```sh -bun add @pathscale/ui # peer deps: solid-js ^1.9, @solid-primitives/*, @tanstack/solid-form|table, popmotion (optional), @standard-schema/spec (optional) -``` - -```ts -import { Button, Flex, Modal, toast } from "@pathscale/ui"; // everything is in the root barrel -import "@pathscale/ui/index.css"; // tokens + themes + base + icons -``` - -Subpath exports also exist: `./components/*`, `./primitives/*`, `./hooks/*`, `./motion`, `./styles/*`. -⚠️ `@pathscale/ui/stores` is declared in package.json but **broken** (no source backs it) — never import it. -⚠️ README mentions `dist/styles/compat/daisy-primitives.css` and `docs/motion.md` — both **stale/nonexistent**. - -## Theming - -- Two themes: `light` (default when no attribute) and `dark`. Switch: `document.documentElement.setAttribute("data-theme", "dark")`. -- Tokens are CSS vars: `--color-primary(/-content)`, `--color-secondary`, `--color-accent`, `--color-neutral`, `--color-info/success/warning/error`, `--color-danger`, surfaces `--color-base-100/200/300`, `--color-base-content`, HeroUI-style `--color-default(/-foreground/-hover)`, `--color-background/foreground`, daisy short aliases `--b1/--b2/--b3/--bc`, radii `--radius-selector/-field/-box`, and a ~35-var `--glass-*` set (runtime-tweakable on `documentElement.style`). -- `src/index.css` has a Tailwind v4 `@theme` block, so in a Tailwind v4 app `bg-primary`, `text-base-content`, `bg-base-100` etc. work. -- Any component accepts `dataTheme` prop → rendered as `data-theme` attr (scoped theming). - -## Component conventions (consumer-facing) - -- Booleans are HeroUI-style `is*`: `isDisabled`, `isOpen`, `isInvalid`, `isPending`, `isIconOnly`, `isHoverable`, `isPressable`. Native `disabled` also honored. -- Sizes: `xs | sm | md | lg | xl` (`ComponentSize`). Colors: `neutral | primary | secondary | accent | info | success | warning | error | ghost` (`ComponentColor`). -- Both `class` and `className` accepted everywhere; consumer classes win (merged last via twMerge). -- Controlled/uncontrolled triples: `isOpen/defaultOpen/onOpenChange`, `value/defaultValue/onChange`, `selectedKey/defaultSelectedKey/onSelectionChange`. Event callbacks pass **values, not events**. -- Compound components: `Modal.Trigger`, `Tabs.List`, `Select.Option`, etc. (`Object.assign` statics; also exported flat: `AccordionRoot`, `AlertTitle`, …). Parts are styleable/testable via `data-slot="..."` and state attrs (`data-open`, `data-selected`, `data-invalid`). -- No polymorphic `as` prop. - -```tsx - - - -``` - -## Component inventory (by family) - -- **Layout/primitives**: Flex, Grid, Join, Surface, Card, GlassPanel, Separator, ScrollShadow, Skeleton, EmptyState, Footer, Header, Navbar, Toolbar, FloatingDock -- **Typography/misc**: Text, Link, Kbd, Badge, Chip, Tag/TagGroup, Avatar, Icon, Tooltip, Breadcrumbs, Pagination, Meter, ProgressBar, ProgressCircle, Spinner (alias: Loading) -- **Inputs**: Input, InputGroup, InputOTP, TextField, TextArea (and textarea), NumberField, SearchField, PasswordField (+ password-requirements/rules, `passwordRules.ts`), ColorField, Checkbox(+Group), Radio(+Group), Toggle, Slider, Select, ComboBox, ListBox, SizePicker, Form pieces (Label, Description, ErrorMessage, FieldError, Fieldset) -- **Dates**: Calendar, RangeCalendar, DatePicker, DateRangePicker (internal date engine); DateField, TimeField (separate segmented editors) -- **Color**: ColorPicker, ColorArea, ColorSlider, ColorSwatch(+Picker), ColorWheelFlower, ThemeColorPicker -- **Overlays**: Modal, Drawer, Popover, Dropdown, Menu, Toast, Disclosure(+Group), Accordion -- **Data**: Table (headless compound + hooks), plus primitives `useVirtualRows`, `useStreamingBuffer`, `useStreamingSubscription` -- **Auth kit**: AuthForm, AuthCard, AuthFieldGroup, AuthSubmitButton, AuthFooterLinks, AuthPoweredBy, AuthErrorMessage, AuthSuccessMessage — thin Tailwind-utility wrappers composing Button/Card/fields -- **Visual FX**: MetalBorder (WebGL liquid-metal border; presets `chromatic|silver|gold`, `kind="pill"|"circle"`, `glow`, `strength` 0-100, `theme="dark"|"light"|"auto"`), GlowCard (mouse-tracking glow), NoiseBackground (animated gradient blobs), ImmersiveLanding (full mini-app w/ PWA widgets), VideoPreview, LiveChat, ChatBubble, LanguageSwitcher - -Renames from old versions (see `docs/component-migration-map.md`): Loading→Spinner, DropdownSelect→Select, RadialProgress→ProgressCircle, RangeSlider→Slider, Progress→ProgressBar/ProgressCircle. ~40 components removed outright (Carousel, Rating, Steps, Stats, FileInput, …). - -## Forms (TanStack Form + Standard Schema) - -```tsx -import { createForm, Form, FormField, FormSubmitButton } from "@pathscale/ui"; - -const form = createForm({ - defaultValues: { email: "", password: "" }, - schema: loginSchema, // any Standard Schema: Zod v4 / Valibot / ArkType - onSubmit: async (values) => { await login(values); }, -}); - - - - - Log in - -``` - -- `Form` without a `form` prop = plain styled `
` (`FormRoot`). With `form` = context provider + wired submit. -- Inside a ``: `useField(name)` → `{value, error, touched, invalid, handleChange, handleBlur}`. **Errors are touch-gated** — `error()` is `undefined` until the field blurs. `FormSubmitButton` disables on `!canSubmit` (not touch-gated), so the button can be disabled with no visible error. -- Escape hatch: `form._tsForm` is the raw TanStack form API (typed `any` on purpose). -- Schema validation runs on change+blur+submit; blur errors clear immediately on change once valid. - -## Table (headless assembly) - -```tsx -import { useTableModel, useTableSorting, useTablePagination, TableRoot, TableContent, ... } from "@pathscale/ui"; - -const sorting = useTableSorting(); -const pagination = useTablePagination(); // default page sizes [10,25,50,100] -const table = useTableModel({ - data: () => rows(), columns, - sorting: sorting.sorting, setSorting: sorting.setSorting, - pagination: pagination.pagination, setPagination: pagination.setPagination, - enableSorting: true, enablePagination: true, -}); -// render table.getHeaderGroups()/getRowModel().rows into: -// … -``` - -- State-slice hooks (all controlled-or-uncontrolled): `useTableSorting`, `useTableSelection`, `useTableFiltering` (per-column popovers + `getColumnFilterProps`), `useTablePagination` (⚠️ `nextPage(max)`/`lastPage(max)` need caller-supplied max page index), `useTableExpansion`. -- Parts: TableRoot/ScrollContainer/Content/Header/Column/Body/Row/Cell/ExpandedRow/Footer/PageSize/ResizableContainer/ColumnResizer/LoadMore(+Content), plus SortIcon, ExpandToggle, InlineConfirm, MobileListView (responsive card fallback), VirtualSpacerRow. -- **Virtualization is not built in**: combine `useVirtualRows` (wraps @tanstack/solid-virtual) + `VirtualSpacerRow` yourself. Playground has examples: `playground/src/examples/Table*.tsx`. - -## Motion - -```ts -import { enablePopmotion } from "@pathscale/ui"; -import { animate } from "popmotion"; -enablePopmotion((opts) => animate({ ...opts })); // ⚠️ WITHOUT this, all JS animations SNAP to end state -``` - -- `runMotion(el, from, to, transition?, onComplete?)` — animates `opacity/x/y/scale`; **durations in seconds**. -- Presets via `getPreset/resolvePreset` (`route`, `routeAuth`, `authSwap`, `fade`, `fadeUp`, `scaleIn`, `toast`, `routeDashboard`); `registerPreset` mutates the global set; `createMotionSystem()` for isolated instances; `createRouteTransitionResolver({rules, fallback})` for route rules. -- Solid components: `{(isExiting, onExitComplete) => …` — Presence force-unmounts after 800ms if `onExitComplete` never fires. `` for height collapse. -- `resolvePreset(name, {reduceMotion})` returns the `noMotion` preset under prefers-reduced-motion. Note Modal/Toast/Drawer animate via CSS, not this system. - -## Streaming - -```ts -const buf = useStreamingBuffer({ strategy: "upsert", maxSize: 500, getKey: r => r.id }); -useStreamingSubscription({ - subscribe: (o) => { const es = new EventSource(url); es.onmessage = e => o.next(JSON.parse(e.data)); return () => es.close(); }, - onData: buf.add, -}); -// buf.rows() is the reactive, capped, deduped array -``` -Strategies: `append` (ignore duplicate keys) | `upsert` (replace in place) | `replace`. Subscription returns `{isLive, isConnecting, error, eventCount, start, stop}`; auto-starts unless `enabled` is false. - -## Toast (imperative singleton) - -```ts -import { toast, ToastProvider } from "@pathscale/ui"; -// mount once, then anywhere: -toast.success("Saved"); toast.danger("Failed"); toast.promise(p, {loading, success, error}); -``` - -## Icons - -Icons are Iconify classes: `` or bare `class="icon-[lucide--search]"`. In this repo they're baked at build time into `src/styles/icons/generated-icons.css` (only icons actually used get emitted). Consumer apps with Tailwind v4 can use `@plugin "@iconify/tailwind4"` for arbitrary icons (playground does this). - -## Dates - -Calendar/DatePicker/RangeCalendar/DateRangePicker use the internal engine (native Date + Intl; no date lib). Values are `Date` objects; ranges are `{start: Date, end: Date}`. Controlled via `value/defaultValue/onChange`. `DateField`/`TimeField` are separate segmented text editors, not calendar-backed. - -## Playground (fastest way to try things) - -```sh -bun install && cd playground && bun install && cd .. -bun run playground:dev # Vite; @pathscale/ui aliased to local src/ — edits hot-reload, no rebuild -``` -`playground/src/App.tsx` (~7,300 lines) demos every component; examples in `playground/src/examples/` (Form, Motion, Streaming, Table×3). Playground forces `data-theme="dark"` at runtime in `playground/src/index.tsx`. +- Guardrails live in [`.claude/settings.json`](.claude/settings.json): safe read-only + commands are pre-allowed; pushes, publishing, `gh pr merge`, cloud CLIs and deploys prompt + first (`permissions.ask` plus the `PreToolUse` hook in [`.claude/hooks/`](.claude/hooks/)). +- Keep the hook's `RISKY_WORDS` and `permissions.ask` **in sync** — they back each other up. From e6871667aff0c41937248ee61f772ed45c18c043 Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 25 Jul 2026 20:37:14 +0700 Subject: [PATCH 3/3] docs: add frontend conventions reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds docs/frontend-conventions.md — the frontend working agreement — and links it as the first invariant in AGENTS.md so it is read before implementation files are opened. Covers the non-negotiables (SolidJS not React; signals///class=; reuse @pathscale/ui and verify props from existing usage; Bun and Biome; mirror an existing pattern before inventing one), a context-efficient workflow (classify before reading, search narrowly, at most five files to start, don't repeat known searches, validate incrementally, report only what changed), and this repo's actual validation commands read from package.json. Content is tiered to what this repo actually is, verified against the manifests rather than assumed: - Full frontend apps also get the backend-contract rules: docs/*.services.json is authoritative, don't invent endpoints/params/returns/roles, never hand-edit generated DTOs under src/models/**, and don't introduce REST/fetch where the app uses the @pathscale/wss-adapter WebSocket RPC. - Solid apps with no backend contract get the stack rules only — quoting a services JSON at them would point at a file that does not exist. - Packages that are not Solid apps get Bun/Biome and mirror-existing-patterns only. Two repo-specific adjustments: @pathscale/ui is told it *is* the library, so a prop change is an API change for every consumer, rather than being told to "reuse @pathscale/ui"; and the i18n rule is stated only where an i18n system exists, phrased conditionally elsewhere. The References section (project map / services contract / UI conventions / validation) is included as the intended split but marked TODO — those documents do not exist yet, and pointing at missing files is worse than not pointing. Co-Authored-By: Claude Fable 5 --- AGENTS.md | 4 +++ docs/frontend-conventions.md | 64 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 docs/frontend-conventions.md diff --git a/AGENTS.md b/AGENTS.md index 56a9fb5..009ac4c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,10 @@ natively, and Claude Code loads it through the `@AGENTS.md` import in ## Invariants (don't break these) +- **Read [`docs/frontend-conventions.md`](docs/frontend-conventions.md) before opening + implementation files.** It is the frontend working agreement: SolidJS/`@pathscale/ui` + conventions, and a context-efficient workflow. Reading it first keeps + context small and avoids re-deriving patterns that already exist. - **Publishing to npm is irreversible.** A version can never be reused. Check the built artifact before releasing. - **`bun` is the package manager** — its lockfile is authoritative. Don't introduce a second one by running npm/yarn/pnpm here. - **Docs describe what is true now.** If you change behaviour, update the README and any affected doc in the same change. diff --git a/docs/frontend-conventions.md b/docs/frontend-conventions.md new file mode 100644 index 0000000..068b1d6 --- /dev/null +++ b/docs/frontend-conventions.md @@ -0,0 +1,64 @@ +# Frontend conventions — UI + +Read this **before** opening implementation files, so context stays small and existing +conventions are followed. It covers implementing, debugging, reviewing and refactoring pages, hooks, stores, services, routing and `@pathscale/ui` usage. + +Stack: **SolidJS**, **rsbuild**, **Bun**, **Biome**, **`@pathscale/ui`**. + +## Non-negotiables + +- **SolidJS, not React.** No `useState`/`useEffect` reflexes, no virtual-DOM + assumptions. Use signals, ``, ``, and `class=` (not `className=`). +- Follow the existing hooks, stores, routes, guards and feature structure **before** + introducing a new pattern. Find one analogous implementation and mirror it. +- **This repo *is* `@pathscale/ui`.** You are building the components, not consuming + them — so any change to a component's props or behaviour is an API change for every + consuming app. Check `docs/component-migration-map.md` and existing usage patterns + before altering a public surface. +- **User-facing strings:** this repo has no i18n system today. If one is added, route + every user-facing string through it — don't hand-roll a second mechanism alongside it. +- **Use Bun and Biome**, and this repository's actual validation commands (below) — + not a remembered command from another project. + +## Context-efficient workflow + +1. **Classify the task before reading files:** auth · data/hooks · feature page · + routing · stores · UI/styling. +2. **Search narrowly before reading.** Prefer symbols, exact strings and matching line + ranges over opening whole files. +3. **Start with at most five directly relevant files.** Expand only when there is a + concrete unanswered question. +4. **Mirror an analogous implementation** before creating a new pattern. +5. **This repo has no backend services contract** — there is no services JSON to + consult. Don't look for one. +6. **Don't repeat a search whose result you already have**, and don't reread unchanged + files without a reason. +7. **Verify existing `@pathscale/ui` usage** before assuming a component API. +8. **Validate incrementally:** smallest relevant check first, broader checks only when + needed. +9. **Report only:** changed files · validation results · remaining risks · contract + limitations. + +## Validation + +```bash +bun run lint +bun run format +bun run check +bun run build +``` + +Run the smallest relevant check first; widen only if it passes or the failure is unclear. + +## References + +Load only the reference needed for the current task — not all of them automatically: + +- **project map** — feature structure, routes, stores and app flow +- **services contract** — endpoint wiring, hooks and generated DTO rules +- **UI conventions** — SolidJS and `@pathscale/ui` usage +- **validation** — exact commands for this repository (see above) + +> **TODO — these reference docs do not exist yet.** The names above are the intended +> split; writing them is a follow-up project. Until they exist, this file plus the +> services JSON is the reference. Don't go looking for files that aren't there.