@sableui/react 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +157 -0
- package/CHANGELOG.md +50 -0
- package/CONTRIBUTING.md +221 -0
- package/DESIGN.md +548 -0
- package/LICENSE +21 -0
- package/README.md +194 -2
- package/THIRD_PARTY_NOTICES.md +44 -0
- package/bin/sable-lint.mjs +0 -0
- package/bin/sable.mjs +1223 -0
- package/dist/_contract-CYScOdf5.d.ts +150 -0
- package/dist/chunk-3IWZYBGH.js +398 -0
- package/dist/chunk-5Z5KLOAU.js +167 -0
- package/dist/chunk-7MG4SHYX.js +15 -0
- package/dist/chunk-ADLLDWLW.js +15 -0
- package/dist/chunk-EP55KSYA.js +14 -0
- package/dist/chunk-J23ELQQZ.js +9396 -0
- package/dist/chunk-JI7WJP2L.js +286 -0
- package/dist/chunk-K7RUHUGC.js +14 -0
- package/dist/chunk-KO3NAEBF.js +15 -0
- package/dist/chunk-L4IEIQQA.js +14 -0
- package/dist/chunk-L6Y46C62.js +270 -0
- package/dist/chunk-NEQNXI2G.js +25 -0
- package/dist/chunk-PAOZBZME.js +40 -0
- package/dist/chunk-RWP4XBSK.js +139 -0
- package/dist/chunk-SRQWQUTW.js +63 -0
- package/dist/chunk-YXBOYWBN.js +15 -0
- package/dist/foundations/index.d.ts +88 -0
- package/dist/foundations/index.js +792 -0
- package/dist/icon/index.d.ts +60 -0
- package/dist/icon/index.js +2 -0
- package/dist/icon-sets/brands.d.ts +44 -0
- package/dist/icon-sets/brands.js +147 -0
- package/dist/icon-sets/reicon.d.ts +8 -0
- package/dist/icon-sets/reicon.js +1 -0
- package/dist/index.d.ts +7013 -0
- package/dist/index.js +15462 -0
- package/dist/ios/chrome.css +83 -0
- package/dist/ios/frame.css +5 -0
- package/dist/ios/index.d.ts +70 -0
- package/dist/ios/index.js +313 -0
- package/dist/ios/preview.d.ts +64 -0
- package/dist/ios/preview.js +133 -0
- package/dist/ios/styles.css +77 -0
- package/dist/layout/index.d.ts +280 -0
- package/dist/layout/index.js +2 -0
- package/dist/meta-6dUzgC6M.d.ts +205 -0
- package/dist/patterns/index.d.ts +36 -0
- package/dist/patterns/index.js +934 -0
- package/dist/presets/index.d.ts +37 -0
- package/dist/presets/index.js +117 -0
- package/dist/registry-CyL28OzJ.d.ts +28 -0
- package/dist/styles.css +1893 -0
- package/dist/theme-provider-BWcizhAt.d.ts +105 -0
- package/dist/themes/atelier/atelier.css +213 -0
- package/dist/themes/atelier/index.d.ts +9 -0
- package/dist/themes/atelier/index.js +1 -0
- package/dist/themes/base/base.css +443 -0
- package/dist/themes/base/index.d.ts +9 -0
- package/dist/themes/base/index.js +1 -0
- package/dist/themes/ember/ember.css +270 -0
- package/dist/themes/ember/index.d.ts +9 -0
- package/dist/themes/ember/index.js +1 -0
- package/dist/themes/index.d.ts +10 -0
- package/dist/themes/index.js +8 -0
- package/dist/themes/meridian/index.d.ts +9 -0
- package/dist/themes/meridian/index.js +1 -0
- package/dist/themes/meridian/meridian.css +255 -0
- package/dist/themes/poppy/index.d.ts +6 -0
- package/dist/themes/poppy/index.js +1 -0
- package/dist/themes/poppy/poppy.css +174 -0
- package/dist/themes/prism/index.d.ts +6 -0
- package/dist/themes/prism/index.js +1 -0
- package/dist/themes/prism/prism.css +183 -0
- package/dist/themes/verdant/index.d.ts +9 -0
- package/dist/themes/verdant/index.js +1 -0
- package/dist/themes/verdant/verdant.css +210 -0
- package/dist/themes.css +30 -0
- package/llms/components/accordion.md +93 -0
- package/llms/components/app-header.md +70 -0
- package/llms/components/app-shell.md +117 -0
- package/llms/components/attachment.md +105 -0
- package/llms/components/avatar.md +141 -0
- package/llms/components/badge.md +117 -0
- package/llms/components/banner.md +119 -0
- package/llms/components/bottom-nav.md +80 -0
- package/llms/components/breadcrumbs.md +75 -0
- package/llms/components/browser.md +67 -0
- package/llms/components/bubble.md +119 -0
- package/llms/components/button.md +199 -0
- package/llms/components/calendar.md +75 -0
- package/llms/components/card.md +135 -0
- package/llms/components/carousel.md +87 -0
- package/llms/components/chat-composer.md +126 -0
- package/llms/components/checkbox-card.md +98 -0
- package/llms/components/checkbox.md +90 -0
- package/llms/components/code-block.md +81 -0
- package/llms/components/code.md +62 -0
- package/llms/components/collapsible.md +83 -0
- package/llms/components/color-field.md +100 -0
- package/llms/components/combobox.md +119 -0
- package/llms/components/command-menu.md +158 -0
- package/llms/components/context-card.md +73 -0
- package/llms/components/context-menu.md +79 -0
- package/llms/components/data-table.md +99 -0
- package/llms/components/date-picker.md +97 -0
- package/llms/components/description.md +96 -0
- package/llms/components/destructive-action-modal.md +76 -0
- package/llms/components/dialog.md +127 -0
- package/llms/components/dots-menu.md +64 -0
- package/llms/components/empty-state.md +113 -0
- package/llms/components/entity.md +67 -0
- package/llms/components/feedback.md +63 -0
- package/llms/components/fieldset.md +106 -0
- package/llms/components/file-dropzone.md +103 -0
- package/llms/components/form.md +111 -0
- package/llms/components/gliding-highlight.md +84 -0
- package/llms/components/inline-status.md +62 -0
- package/llms/components/input.md +127 -0
- package/llms/components/kbd.md +57 -0
- package/llms/components/label.md +73 -0
- package/llms/components/marker.md +99 -0
- package/llms/components/marquee.md +66 -0
- package/llms/components/menu.md +122 -0
- package/llms/components/message-scroller.md +110 -0
- package/llms/components/message.md +114 -0
- package/llms/components/meter.md +124 -0
- package/llms/components/middle-truncate.md +62 -0
- package/llms/components/note.md +81 -0
- package/llms/components/notification-badge.md +67 -0
- package/llms/components/number-field.md +70 -0
- package/llms/components/otp-field.md +101 -0
- package/llms/components/page-header.md +76 -0
- package/llms/components/page-loader.md +67 -0
- package/llms/components/pagination.md +92 -0
- package/llms/components/panel-group.md +120 -0
- package/llms/components/panel-section.md +122 -0
- package/llms/components/phone.md +70 -0
- package/llms/components/pill.md +73 -0
- package/llms/components/popover.md +79 -0
- package/llms/components/progress.md +98 -0
- package/llms/components/radio-card.md +90 -0
- package/llms/components/radio.md +86 -0
- package/llms/components/relative-time.md +60 -0
- package/llms/components/scroller.md +88 -0
- package/llms/components/scrub-field.md +97 -0
- package/llms/components/search-input.md +84 -0
- package/llms/components/select.md +106 -0
- package/llms/components/separator.md +70 -0
- package/llms/components/settings-menu.md +139 -0
- package/llms/components/sheet.md +97 -0
- package/llms/components/show-more.md +59 -0
- package/llms/components/side-nav.md +106 -0
- package/llms/components/sidebar.md +87 -0
- package/llms/components/skeleton.md +62 -0
- package/llms/components/slider.md +114 -0
- package/llms/components/spinner.md +87 -0
- package/llms/components/status-bar.md +91 -0
- package/llms/components/status-dot.md +70 -0
- package/llms/components/stepper.md +125 -0
- package/llms/components/streaming-text.md +86 -0
- package/llms/components/switch.md +87 -0
- package/llms/components/table.md +112 -0
- package/llms/components/tabs.md +84 -0
- package/llms/components/textarea.md +88 -0
- package/llms/components/theme-selector.md +95 -0
- package/llms/components/theme-switcher.md +71 -0
- package/llms/components/thinking-orb.md +91 -0
- package/llms/components/toast.md +130 -0
- package/llms/components/toggle.md +95 -0
- package/llms/components/toolbar.md +168 -0
- package/llms/components/tooltip.md +93 -0
- package/llms/components/tree.md +227 -0
- package/llms/components/video.md +66 -0
- package/llms/components/workspace.md +138 -0
- package/llms/foundations/accessibility.md +60 -0
- package/llms/foundations/motion.md +96 -0
- package/llms/foundations/scrolling.md +41 -0
- package/llms/foundations/shape.md +73 -0
- package/llms/foundations/spacing.md +82 -0
- package/llms/foundations/surfaces.md +69 -0
- package/llms/foundations/typography.md +120 -0
- package/llms/layout/aspect-box.md +37 -0
- package/llms/layout/bleed.md +31 -0
- package/llms/layout/center.md +37 -0
- package/llms/layout/columns.md +51 -0
- package/llms/layout/grid-overlay.md +40 -0
- package/llms/layout/grid.md +47 -0
- package/llms/layout/page.md +39 -0
- package/llms/layout/region.md +49 -0
- package/llms/layout/row.md +40 -0
- package/llms/layout/safe-area.md +37 -0
- package/llms/layout/spacer.md +39 -0
- package/llms/layout/split-pane.md +41 -0
- package/llms/layout/stack.md +39 -0
- package/llms/patterns/auth-page.md +68 -0
- package/llms/patterns/brand-landing-page.md +423 -0
- package/llms/patterns/chat-panel.md +210 -0
- package/llms/patterns/dashboard-page.md +170 -0
- package/llms/patterns/feed-page.md +103 -0
- package/llms/patterns/landing-page.md +122 -0
- package/llms/patterns/list-detail-page.md +100 -0
- package/llms/patterns/settings-page.md +121 -0
- package/llms/reference/token-intent.md +324 -0
- package/llms/reference/utilities.md +129 -0
- package/llms/tasks/adding-a-component.md +113 -0
- package/llms/tasks/auditing-an-app.md +216 -0
- package/llms/tasks/building-a-page.md +109 -0
- package/llms/tasks/building-a-tool.md +141 -0
- package/llms/tasks/building-an-ios-prototype.md +105 -0
- package/llms/tasks/building-with-sable.md +73 -0
- package/llms/tasks/choosing-a-component.md +109 -0
- package/llms/tasks/converting-an-app.md +139 -0
- package/llms/tasks/creating-a-preset.md +43 -0
- package/llms/tasks/creating-design-assets.md +96 -0
- package/llms/tasks/designing-a-surface.md +115 -0
- package/llms/tasks/developing-taste.md +157 -0
- package/llms/tasks/getting-started.md +63 -0
- package/llms/tasks/learning-from-builds.md +74 -0
- package/llms/tasks/reviewing-a-design.md +114 -0
- package/llms/tasks/reviewing-a-journey.md +84 -0
- package/llms/tasks/reviewing-motion.md +92 -0
- package/llms/tasks/using-presets.md +87 -0
- package/llms/taste/asset-plan.template.json +53 -0
- package/llms/taste/composition-follows-content.md +20 -0
- package/llms/taste/hierarchy-is-a-relationship.md +22 -0
- package/llms/taste/lessons.json +57 -0
- package/llms/taste/motion-needs-a-reason.md +21 -0
- package/llms/taste/surface-states-travel-together.md +20 -0
- package/llms-full.txt +14906 -0
- package/llms.txt +405 -0
- package/package.json +185 -3
- package/registry.json +18812 -0
- package/scripts/agent-setup.mjs +54 -0
- package/scripts/design-atlas.mjs +221 -0
- package/scripts/design-directions.mjs +123 -0
- package/scripts/design-memory.mjs +166 -0
- package/scripts/design-preferences.mjs +227 -0
- package/scripts/design-presets.mjs +48 -0
- package/scripts/grade.mjs +509 -0
- package/scripts/lint-rules.mjs +1065 -0
- package/scripts/review-evidence.mjs +120 -0
- package/scripts/skeleton.mjs +248 -0
- package/skills/entry/SKILL.md +34 -0
- package/skills/sable/SKILL.md +187 -0
- package/skills/sable/agents/openai.yaml +6 -0
- package/targets/ios.json +982 -0
- package/third-party/reicon-LICENSE.txt +20 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Task: adding a component to Sable
|
|
2
|
+
|
|
3
|
+
Contributing *to the library*, not consuming it. Every step below is enforced by
|
|
4
|
+
`pnpm build:registry` or `pnpm design-lint` — the checks are the specification,
|
|
5
|
+
so run them early rather than at the end.
|
|
6
|
+
|
|
7
|
+
## The files
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/components/<kebab-name>/
|
|
11
|
+
<kebab-name>.tsx the component
|
|
12
|
+
<kebab-name>.meta.ts the machine-readable contract ← the important one
|
|
13
|
+
<kebab-name>.test.tsx behaviour, not snapshots
|
|
14
|
+
index.ts named exports + the meta
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then: export from `src/index.ts` (the single public barrel), file it in
|
|
18
|
+
`src/lib/taxonomy.ts`, and add `apps/docs/src/pages/components/<Name>Page.tsx`
|
|
19
|
+
plus its route in `apps/docs/src/App.tsx`.
|
|
20
|
+
|
|
21
|
+
## The meta is the deliverable
|
|
22
|
+
|
|
23
|
+
`registry.json` is generated from it, and that catalog is what every agent reads.
|
|
24
|
+
A component whose meta is thin is a component nobody can use correctly.
|
|
25
|
+
|
|
26
|
+
Enforced minima — the build fails below them:
|
|
27
|
+
|
|
28
|
+
- `usage.description` — why it exists and when to reach for it
|
|
29
|
+
- **≥1 `do`, ≥2 `dont`, ≥2 examples, ≥2 a11y notes, ≥1 related component**
|
|
30
|
+
- every prop described; `props` holds the ROOT's props only (a capitalised entry
|
|
31
|
+
that is not the root is a subcomponent hiding in `props`)
|
|
32
|
+
- a component with `subcomponents` must show the composed JSX in an example
|
|
33
|
+
- examples must be **complete and copy-pasteable** — no `…` elisions, no phantom
|
|
34
|
+
components, only on-contract `<Icon name>` values
|
|
35
|
+
- `tokensUsed` entries must exist in the token contract
|
|
36
|
+
|
|
37
|
+
A second `dont` must name a **different** failure, not restate the first. The
|
|
38
|
+
highest-value guidance answers *which of these near-identical components do I
|
|
39
|
+
actually want* — AppHeader vs PageHeader, Code vs CodeBlock (`prompt` for a
|
|
40
|
+
command), Meter vs Progress.
|
|
41
|
+
|
|
42
|
+
## Declare how it moves
|
|
43
|
+
|
|
44
|
+
`meta.motion` answers four questions, one entry per slot. Fill the ones that
|
|
45
|
+
apply and **decline the rest explicitly** — `recipe: "none"` with a `why`. Use
|
|
46
|
+
`recipe: "custom"` when a rAF loop or canvas fills the slot and no CSS utility
|
|
47
|
+
could (ThinkingOrb draws a point cloud per frame).
|
|
48
|
+
|
|
49
|
+
| slot | the question |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `arrival` | how does it enter, or does it belong to a layout that already staged it? |
|
|
52
|
+
| `response` | what happens under the pointer? |
|
|
53
|
+
| `transition` | how does it move between its own states? |
|
|
54
|
+
| `activity` | what does it do while working, indeterminately? |
|
|
55
|
+
|
|
56
|
+
The `why` states what the motion is *for* — what a reader would misread without
|
|
57
|
+
it. `"motion-pop on the panel"` restates the recipe and teaches nothing; *"the
|
|
58
|
+
transform-origin follows the anchor, so on a form with several selects in a
|
|
59
|
+
column the origin is the only thing identifying which field the open list belongs
|
|
60
|
+
to"* is the sentence worth writing, because it also tells the next person when
|
|
61
|
+
**not** to copy it.
|
|
62
|
+
|
|
63
|
+
This exists because every other motion rule is prohibitive — `raw-duration`,
|
|
64
|
+
`builtin-animate`, `unknown-motion` all fire on motion that is present and wrong,
|
|
65
|
+
and none of them can fire on motion that is absent. A component with no motion at
|
|
66
|
+
all used to clear the entire contract, which is exactly what gets written when
|
|
67
|
+
nobody is asked for it by name. `pnpm build:registry -- --motion-report` shows
|
|
68
|
+
current coverage; `static-interactive` catches the element-level half in lint.
|
|
69
|
+
|
|
70
|
+
## Taxonomy: two orthogonal axes
|
|
71
|
+
|
|
72
|
+
`src/lib/taxonomy.ts` declares a **group** (role) and a **register**
|
|
73
|
+
(`page` / `workspace` / `either`). Keep them separate: a register-based *group*
|
|
74
|
+
was tried and made every workspace form control have two valid homes. Group by
|
|
75
|
+
KIND, not by co-occurrence — the `related` graph tracks composition as much as
|
|
76
|
+
kinship, and grouping by it measurably made cohesion worse.
|
|
77
|
+
|
|
78
|
+
Guidance lives in the component's own meta. `lib/component-guidance.ts` is for
|
|
79
|
+
the 13 layout primitives only; a component in both places fails the build,
|
|
80
|
+
because the meta wins *whole* and silently discards the other.
|
|
81
|
+
|
|
82
|
+
## The hard rules
|
|
83
|
+
|
|
84
|
+
1. **Semantic tokens only** — never hex/oklch, never a raw palette step.
|
|
85
|
+
2. **Paired typography utilities** — never `text-{size}` + `font-{weight}`.
|
|
86
|
+
3. **`primitives/` is behaviour, `components/` is the public API.** Base UI is
|
|
87
|
+
wrapped and re-exported; consumers import only from the root barrel.
|
|
88
|
+
4. **No icon library in the package.** `<Icon name>` resolves through the
|
|
89
|
+
injected provider. Two narrow amendments exist (`src/icon-sets/`, and
|
|
90
|
+
size-matched structural marks) — read them before assuming you qualify.
|
|
91
|
+
5. **Tailwind v4 packaging discipline** — `src/styles.css` must not
|
|
92
|
+
`@import "tailwindcss"`.
|
|
93
|
+
|
|
94
|
+
## Two failure modes that do not announce themselves
|
|
95
|
+
|
|
96
|
+
- **A custom `text-*` utility unregistered in `lib/cn.ts`** lands in
|
|
97
|
+
tailwind-merge's *colour* group and silently evicts the real colour class. This
|
|
98
|
+
shipped white text on a white button. Register the tier, or do not name it
|
|
99
|
+
`text-*`. The same applies to `bg-*`.
|
|
100
|
+
- **An unmapped icon name renders nothing** — no error, no warning.
|
|
101
|
+
|
|
102
|
+
## Verify
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pnpm typecheck && pnpm design-lint && pnpm build:registry && pnpm test:run
|
|
106
|
+
pnpm build:registry -- --doc-report # guidance depth, weakest entries first
|
|
107
|
+
pnpm build:registry -- --motion-report # motion slots: filled, declined, absent
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Read next
|
|
111
|
+
|
|
112
|
+
- `CONTRIBUTING.md` — the hard rules, the recipe, theme + icon + motion contracts
|
|
113
|
+
- `DESIGN.md` — the system spec, with intent and do/don't per layer
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Task: auditing existing work against Sable
|
|
2
|
+
|
|
3
|
+
This is Sable's native contract-and-taste audit, used by `$sable audit <target>`.
|
|
4
|
+
`$sable critique <target>` emphasizes design judgment. These are agent skill
|
|
5
|
+
modes, not shell subcommands. No other design skill is required.
|
|
6
|
+
|
|
7
|
+
## Establish the comparison basis
|
|
8
|
+
|
|
9
|
+
Identify source paths, rendered routes, installed Sable version, audience, primary
|
|
10
|
+
task, selected identity and supported device/color modes. Reuse the existing brief;
|
|
11
|
+
label assumptions. For a whole app, inventory routes and shared controls, inspect
|
|
12
|
+
each distinct layout and critical journey, and state which routes were sampled.
|
|
13
|
+
Audit the installed version using its registry, AGENTS.md and DESIGN.md. Library
|
|
14
|
+
contribution rules do not all apply to consumers: original themes, fonts, layouts
|
|
15
|
+
and native elements are not automatically violations.
|
|
16
|
+
|
|
17
|
+
An audit reports findings by default. An explicit "audit and fix" request already
|
|
18
|
+
authorizes scoped repairs and verification. Do not automatically upgrade packages,
|
|
19
|
+
migrate the app, change its identity, generate variants or alter CI for a review.
|
|
20
|
+
Resolve the project's installed CLI before using `npx`; in this repository use
|
|
21
|
+
`node packages/sable/bin/sable.mjs`. Below, `sable` means that resolved CLI.
|
|
22
|
+
|
|
23
|
+
Inspect the interface before reading previous verdicts. Follow
|
|
24
|
+
[reviewing-a-design.md](./reviewing-a-design.md) for desktop/mobile, full-route,
|
|
25
|
+
keyboard, interaction and motion evidence. A screenshot-only critique leaves code
|
|
26
|
+
and live behavior untested. Compare three layers explicitly:
|
|
27
|
+
|
|
28
|
+
| Layer | Evidence and judgment |
|
|
29
|
+
|---|---|
|
|
30
|
+
| System contract | Component props/anatomy/states, semantic tokens, layout primitives, pattern intent, icons, motion and accessibility behavior. |
|
|
31
|
+
| Taste and craft | Brief fit, hierarchy, typography, spacing/rhythm, color/material, imagery, story, interaction, motion and mobile composition. |
|
|
32
|
+
| Learned context | Applicable human preferences, separately identified agent observations, conditional lessons and comparable previous builds. |
|
|
33
|
+
|
|
34
|
+
If the project keeps a visual atlas, retrieve a close analogue and a useful
|
|
35
|
+
contrast through `sable atlas --search "<audience and design problem>" --contrast
|
|
36
|
+
--limit 3 --json`. Open useful entries
|
|
37
|
+
with `sable atlas --show <id> --json` and actually inspect their images. Explain
|
|
38
|
+
which relationships transfer; resemblance to Sable's default theme is not the goal.
|
|
39
|
+
Use `sable prefer --search "<decision>" --scope project:<project-id> --json` in an
|
|
40
|
+
applicable scope and `sable learn --search "<problem>" --json`. Agent preferences
|
|
41
|
+
require explicit `--reviewer agent` retrieval and agent attribution. Empty memory
|
|
42
|
+
is valid. When prior builds matter, inspect their actual captures for repeated
|
|
43
|
+
compositions and range; state when comparison evidence is absent.
|
|
44
|
+
|
|
45
|
+
Inventory imported Sable components, locally recreated controls, native elements,
|
|
46
|
+
consumer tokens and repeated overrides. Query the installed catalog before
|
|
47
|
+
recommending a replacement. Check the API, composed anatomy, supported states,
|
|
48
|
+
keyboard/focus behavior, semantic token roles and motion policy. Explain which
|
|
49
|
+
behavior a replacement gains and what must remain. An unused page pattern is not
|
|
50
|
+
drift; repeated overrides can indicate a missing system hook rather than app misuse.
|
|
51
|
+
Run relevant `sable doctor`, `sable lint <paths> --json` and `sable grade <paths>
|
|
52
|
+
--json` checks. Lint is lexical; source grades and pattern similarity do not judge
|
|
53
|
+
taste. Deliberately static motion slots remain valid with appropriate feedback.
|
|
54
|
+
|
|
55
|
+
## Deliver evidence-backed findings
|
|
56
|
+
|
|
57
|
+
Save the audit under `design/<surface>/audit/` or the existing review location.
|
|
58
|
+
Record source/version, routes/states inspected, commands, reference/memory IDs and
|
|
59
|
+
untested areas. Use the source/artifact binding and review template described in
|
|
60
|
+
[reviewing-a-design.md](./reviewing-a-design.md) for structured review records.
|
|
61
|
+
|
|
62
|
+
For each finding, record location, observed consequence, severity, evidence,
|
|
63
|
+
comparison basis, concrete repair, owner (app or Sable) and verification method.
|
|
64
|
+
Distinguish **contract violation**, **taste/craft issue**, **intentional choice**
|
|
65
|
+
and **Sable system gap**. Separate objective failures from agent judgments and
|
|
66
|
+
suspected gaps from verified library defects. Keep component/token findings
|
|
67
|
+
separate from visual quality; report the eight review dimensions as `pass`,
|
|
68
|
+
`revise` or `untested`, never a fabricated premium score. Retain what already works.
|
|
69
|
+
|
|
70
|
+
For authorized fixes, repair observed issues, run relevant checks and recapture
|
|
71
|
+
affected states. Preserve before/after evidence and resolved/remaining findings.
|
|
72
|
+
Use [reviewing-a-journey.md](./reviewing-a-journey.md) for connected flows and
|
|
73
|
+
[reviewing-motion.md](./reviewing-motion.md) for temporal behavior. Save useful
|
|
74
|
+
conditional lessons through [learning-from-builds.md](./learning-from-builds.md).
|
|
75
|
+
Completing an audit supplies no human preference and does not authorize inventing
|
|
76
|
+
or automatically saving one.
|
|
77
|
+
|
|
78
|
+
## Additional checks after an upgrade
|
|
79
|
+
|
|
80
|
+
An app converted to Sable is correct against the contract *as it stood on the day
|
|
81
|
+
it was converted*. The contract keeps moving — new rules, new components,
|
|
82
|
+
deprecations, new doctrine — and none of that reaches an app that merely has the
|
|
83
|
+
package installed. This is the pass that closes the gap.
|
|
84
|
+
|
|
85
|
+
Use the following drift checks after an actual upgrade, or when the user asks to
|
|
86
|
+
compare versions. For an audit-only request, report suggested changes rather than
|
|
87
|
+
performing the fixes or CI edits illustrated below.
|
|
88
|
+
|
|
89
|
+
## The failure this prevents
|
|
90
|
+
|
|
91
|
+
Enforcement travels with the package: `sable-lint` reads the token vocabulary and
|
|
92
|
+
rule table out of the `registry.json` of the version you installed, so upgrading
|
|
93
|
+
Sable silently widens what counts as a violation. That is the design — it means a
|
|
94
|
+
new rule reaches every app without anyone republishing a checklist — but it has a
|
|
95
|
+
predictable failure mode. An app upgrades, CI goes red, the findings are a mix of
|
|
96
|
+
brand-new requirements and drift that has been sitting there for months, nobody
|
|
97
|
+
can tell which is which, and the team pins the version. A pinned version is a
|
|
98
|
+
contract that stopped applying.
|
|
99
|
+
|
|
100
|
+
So audit in the order below: **find out what changed, then measure, then fix.**
|
|
101
|
+
|
|
102
|
+
## 0. Know what moved before you look at your code
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npx sable-lint --since 0.54.2 # the version you were on
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`--since` lists only the rules added after that version, with the reason each
|
|
109
|
+
exists. Everything else `sable-lint` reports is pre-existing drift — still worth
|
|
110
|
+
fixing, but it is not what the upgrade did to you, and separating the two is what
|
|
111
|
+
keeps the upgrade reviewable.
|
|
112
|
+
|
|
113
|
+
For changes that are not lint rules, diff the catalog:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Components that gained a deprecation, and what replaced them.
|
|
117
|
+
node -e "const r=require('@sableui/react/registry.json');
|
|
118
|
+
r.components.filter(c=>c.status==='deprecated')
|
|
119
|
+
.forEach(c=>console.log(c.name,'→',c.supersededBy))"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Deprecation is structured, not prose — `status: "deprecated"` always carries
|
|
123
|
+
`supersededBy`, and the build refuses a deprecation without a forward path. Grep
|
|
124
|
+
your app for each name on the left.
|
|
125
|
+
|
|
126
|
+
## 1. Run the linter, and read the new rules first
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npx sable-lint # ./src by default
|
|
130
|
+
npx sable-lint app components # or pass your dirs
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Fix the `--since` rules first: those are the ones your reviewers have not seen
|
|
134
|
+
before, and they are the ones with a fresh rationale worth reading. A genuine
|
|
135
|
+
exception takes an auditable
|
|
136
|
+
`// design-lint-disable-next-line <ruleId> -- <reason>`; a reasonless disable is
|
|
137
|
+
itself a violation.
|
|
138
|
+
|
|
139
|
+
## 2. Motion — the part the linter can only half-see
|
|
140
|
+
|
|
141
|
+
`static-interactive` (new in 0.55.0) is the first **prescriptive** rule in the
|
|
142
|
+
system. Every other motion rule fires on motion that is present and wrong;
|
|
143
|
+
none of them could ever fire on motion that is *absent*, so an app that simply
|
|
144
|
+
snaps between every state passed the entire contract. Most apps converted before
|
|
145
|
+
0.55.0 will have findings here, and they are real: they are the places the
|
|
146
|
+
interface has no reaction.
|
|
147
|
+
|
|
148
|
+
The linter catches static interactive **elements**. It cannot see the three
|
|
149
|
+
motion slots that are about a component's behaviour over time, so check those by
|
|
150
|
+
hand, per surface:
|
|
151
|
+
|
|
152
|
+
| slot | ask | usual fix |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| arrival | does new content appear, or cut in? | `motion-fade`, `motion-slide-*`, `motion-pop`, `Reveal` |
|
|
155
|
+
| response | does anything happen under the pointer? | `motion-interactive`, `motion-press` |
|
|
156
|
+
| transition | does it move between its own states, or teleport? | `motion-disclosure`, or ease the transform |
|
|
157
|
+
| activity | can you tell "working" from "frozen"? | `motion-spin`, `motion-pulse`, `motion-shimmer` |
|
|
158
|
+
|
|
159
|
+
Sable's own components declare their slots — the `## Motion` section of each
|
|
160
|
+
entry in `llms/components/` says which slots are filled, which were *declined*,
|
|
161
|
+
and why. Read the declined ones too: "this does not animate, and here is the
|
|
162
|
+
reason" is what stops you adding motion somewhere it was ruled out.
|
|
163
|
+
|
|
164
|
+
Anything you compose by hand owns its own slots. The question is not "should this
|
|
165
|
+
animate" but "which of the four does this need, and what fills them".
|
|
166
|
+
|
|
167
|
+
## 3. Hand-rolled components that Sable now ships
|
|
168
|
+
|
|
169
|
+
The most valuable finding in an audit is usually a deletion. Search for the
|
|
170
|
+
shapes that tend to get built locally before the library has them, then check the
|
|
171
|
+
catalog:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
grep -rEl "role=\"(dialog|listbox|tablist|menu)\"|aria-expanded" src/ | head -40
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Every conversion so far has deleted more than it added at this step. One tool's
|
|
178
|
+
layers panel lost 174 lines by adopting `Tree`; its whole local
|
|
179
|
+
`components/workspace/` directory went away.
|
|
180
|
+
|
|
181
|
+
## 4. Review the rendered product
|
|
182
|
+
|
|
183
|
+
Contract drift is only one part of an audit. A valid token can still be used in
|
|
184
|
+
the wrong hierarchy; a page full of correct cards can still obscure the task.
|
|
185
|
+
Follow [reviewing-a-design.md](./reviewing-a-design.md) across purpose/story,
|
|
186
|
+
typography, spacing, color/material, interaction, motion, responsive behavior,
|
|
187
|
+
and accessibility. Compare against the surface brief, not the default theme.
|
|
188
|
+
Inspect actual desktop/mobile states and record repairs with before/after evidence.
|
|
189
|
+
Capture a reusable finding through [learning-from-builds.md](./learning-from-builds.md).
|
|
190
|
+
|
|
191
|
+
## 5. Make the contract audit a gate
|
|
192
|
+
|
|
193
|
+
An audit you have to remember to run is an audit that happens once. Two lines:
|
|
194
|
+
|
|
195
|
+
```jsonc
|
|
196
|
+
// package.json
|
|
197
|
+
"scripts": { "lint:design": "sable-lint src" }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```yaml
|
|
201
|
+
# CI, alongside tsc and tests
|
|
202
|
+
- run: pnpm lint:design
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
With that in place the loop closes itself: the dependency bot opens the Sable
|
|
206
|
+
upgrade PR, the new rules ship inside it, CI fails on that PR rather than three
|
|
207
|
+
weeks later, and the diff that introduced the requirement is the diff that has to
|
|
208
|
+
satisfy it. That is the only arrangement where "we should audit the apps" stops
|
|
209
|
+
being a thing anyone has to say.
|
|
210
|
+
|
|
211
|
+
## Read next
|
|
212
|
+
|
|
213
|
+
- [converting-an-app.md](./converting-an-app.md) — the first-pass migration
|
|
214
|
+
- [building-a-page.md](./building-a-page.md) ·
|
|
215
|
+
[building-a-tool.md](./building-a-tool.md)
|
|
216
|
+
- `DESIGN.md → Motion`
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Task: building a page (page register)
|
|
2
|
+
|
|
3
|
+
Marketing, docs, settings, a feed, a dashboard — anything whose primary
|
|
4
|
+
interaction is reading and navigating. This is the default register; if you are
|
|
5
|
+
building an editor or a canvas app, see
|
|
6
|
+
[building-a-tool.md](./building-a-tool.md).
|
|
7
|
+
|
|
8
|
+
## Start from the brief, then choose composition
|
|
9
|
+
|
|
10
|
+
Read [designing-a-surface.md](./designing-a-surface.md) for the user's outcome,
|
|
11
|
+
content, visual direction, story, interaction, and adaptation. Retrieve relevant
|
|
12
|
+
lessons with `sable learn --search "<context>"`.
|
|
13
|
+
|
|
14
|
+
`sable pattern --list` offers complete canonical compositions with imports.
|
|
15
|
+
Inspect a matching skeleton and reuse it when its information architecture fits.
|
|
16
|
+
For an original page, compose layout primitives and semantic HTML around the
|
|
17
|
+
brief. Patterns encode useful defaults; they do not define the space of good
|
|
18
|
+
designs. Record the reason for the chosen composition.
|
|
19
|
+
|
|
20
|
+
## Layout, in the order it matters
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<AppShell header={<AppHeader title="…" />} sidebar={<Sidebar>…</Sidebar>}>
|
|
24
|
+
<Region max="content"> {/* width capped BY ROLE, never globally */}
|
|
25
|
+
<PageHeader title="Invoices" description="…" actions={<Button>Export</Button>} />
|
|
26
|
+
<Stack gap={3}> {/* gap counts 8px rhythm units, not pixels */}
|
|
27
|
+
…
|
|
28
|
+
</Stack>
|
|
29
|
+
</Region>
|
|
30
|
+
</AppShell>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- **Never a global `max-width`.** Width is capped per region by role:
|
|
34
|
+
`prose` / `form` / `feed` / `content` / `wide` / `full`. A form region is narrow
|
|
35
|
+
because a 700px text input is harder to use, not more generous.
|
|
36
|
+
- **Measure ≠ width.** Cap prose line length in `ch` (`--measure-prose`)
|
|
37
|
+
independently of the region's layout width.
|
|
38
|
+
- **The 12-column grid lives *inside* a region**, for genuinely multi-column
|
|
39
|
+
content. A column span is not a page width.
|
|
40
|
+
- **Chrome is off the grid.** `Sidebar` and rails are fixed-width, outside the
|
|
41
|
+
content grid.
|
|
42
|
+
|
|
43
|
+
## Typography: pick the tier by ROLE, not by size
|
|
44
|
+
|
|
45
|
+
| tier | for | not |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `text-display-*` | a hero, one per view at most | page titles — that is `heading-1` |
|
|
48
|
+
| `text-heading-1..4` | the document outline; pick by depth | size — if it belongs in a table of contents it is a heading |
|
|
49
|
+
| `text-title-*` | a *component's* title: card, dialog, panel section | a heading tier — a card title is not part of the page outline |
|
|
50
|
+
| `text-body-*` | prose you read a sentence of | labels |
|
|
51
|
+
| `text-label-*` | field labels, column headers, button text | body at the same size |
|
|
52
|
+
| `text-caption` | metadata *beneath* something | `text-overline`, which sits *above* and categorises |
|
|
53
|
+
|
|
54
|
+
Always the paired utility. Never `text-sm font-medium` — CI rejects it.
|
|
55
|
+
|
|
56
|
+
## Colour: role, not appearance
|
|
57
|
+
|
|
58
|
+
`accent`, `secondary` and `muted` are near-identical in most themes and are
|
|
59
|
+
chosen by what the surface *does*:
|
|
60
|
+
|
|
61
|
+
- `muted` — recessed and inert (a well, a disabled field, a zebra stripe)
|
|
62
|
+
- `secondary` — a control fill (something you can press)
|
|
63
|
+
- `accent` — a transient state (hover, selected, current)
|
|
64
|
+
|
|
65
|
+
Pick by appearance and it works in Base and breaks in Meridian. Every token's
|
|
66
|
+
intent, and what it gets confused with, is in
|
|
67
|
+
`llms/reference/token-intent.md`.
|
|
68
|
+
|
|
69
|
+
Ink is never chosen independently of its surface: `bg-card text-card-foreground`,
|
|
70
|
+
as a pair.
|
|
71
|
+
|
|
72
|
+
## The traps
|
|
73
|
+
|
|
74
|
+
- **A `Card` is a container, not a heading or a landmark.** If its title is the
|
|
75
|
+
only heading in the region, render a real heading element.
|
|
76
|
+
- **One `h1` per route** — `PageHeader` renders it. A second breaks the outline a
|
|
77
|
+
screen-reader user navigates by.
|
|
78
|
+
- **`AppHeader` is app chrome; `PageHeader` is content.** Route-specific actions
|
|
79
|
+
in `AppHeader` persist onto routes where they mean nothing.
|
|
80
|
+
- **Tone is never the only signal.** Pair a tone with an icon or text; colour
|
|
81
|
+
alone fails for colour-blind users.
|
|
82
|
+
|
|
83
|
+
## Verify
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx sable-lint # lints ./src; pass dirs to change (npx sable-lint app components)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`sable-lint` ships with the package and reads the token vocabulary from the
|
|
90
|
+
`registry.json` of the version you installed, so it checks your app against your
|
|
91
|
+
exact tokens. It exits 1 on any violation. `--rules` explains each one; `--ignore
|
|
92
|
+
'<glob>'` skips a data/fixture layer, where a hex value is content rather than a
|
|
93
|
+
style. A rare, genuine exception takes an auditable
|
|
94
|
+
`// design-lint-disable-next-line <ruleId> -- <reason>`.
|
|
95
|
+
|
|
96
|
+
Then your own `tsc` and tests. (`pnpm design-lint && pnpm build:registry &&
|
|
97
|
+
pnpm test:run` are the *in-repo* gates — they only run inside the Sable repo.)
|
|
98
|
+
|
|
99
|
+
Then inspect the rendered page on desktop and mobile, operate its primary flow,
|
|
100
|
+
use the keyboard, and check reduced motion. Follow
|
|
101
|
+
[reviewing-a-design.md](./reviewing-a-design.md) for evidence and repair. Passing
|
|
102
|
+
source checks alone says nothing about hierarchy, story, or the resulting polish.
|
|
103
|
+
|
|
104
|
+
## Read next
|
|
105
|
+
|
|
106
|
+
- `sable pattern --list` — the current compositions
|
|
107
|
+
- `registry.json → layoutPrimitives` — full contracts plus the layout doctrine
|
|
108
|
+
- `DESIGN.md` — spacing, dimensions, shape, elevation, layering, motion
|
|
109
|
+
- `llms/reference/token-intent.md`
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Task: building a tool (workspace register)
|
|
2
|
+
|
|
3
|
+
An editor, a canvas app, a console — anything whose primary interaction is
|
|
4
|
+
*operating on something* rather than reading and navigating.
|
|
5
|
+
|
|
6
|
+
If the primary interaction is reading down a page, stop: you want
|
|
7
|
+
[building-a-page.md](./building-a-page.md). A dashboard that scrolls is a page
|
|
8
|
+
with dense content, not a workspace.
|
|
9
|
+
|
|
10
|
+
## The shell
|
|
11
|
+
|
|
12
|
+
First read [designing-a-surface.md](./designing-a-surface.md). Define the operator's
|
|
13
|
+
primary task, selection/navigation model, most frequent actions, and narrower
|
|
14
|
+
viewport behavior before choosing panel furniture. Retrieve applicable precedents
|
|
15
|
+
with `sable learn --search "workspace interaction"`; editorial motion and display
|
|
16
|
+
hierarchy do not automatically transfer to dense operating surfaces.
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
<Workspace {/* h-dvh, sets data-density="compact" */}
|
|
20
|
+
toolbar={<WorkspaceBar>…</WorkspaceBar>}
|
|
21
|
+
statusBar={<StatusBar>…</StatusBar>}
|
|
22
|
+
>
|
|
23
|
+
<PanelGroup>
|
|
24
|
+
<Panel label="Layers" width={leftWidth}>…</Panel>
|
|
25
|
+
<PanelResizer side="left" width={leftWidth} onWidthChange={setLeftWidth}
|
|
26
|
+
label="Resize layers panel" />
|
|
27
|
+
<WorkspaceCanvas>{/* the work surface — clips, never scrolls */}</WorkspaceCanvas>
|
|
28
|
+
</PanelGroup>
|
|
29
|
+
</Workspace>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`Workspace` sets the compact density scope for its whole subtree. Nothing
|
|
33
|
+
hardcodes a height: every control reads `--control-*`, so the same
|
|
34
|
+
`<Button size="md">` is 40px on a page and 32px here. A `Dialog` opened from
|
|
35
|
+
inside resets to comfortable, because a modal is a page-register surface even
|
|
36
|
+
when a tool launched it.
|
|
37
|
+
|
|
38
|
+
## What you will need, and what to avoid reaching for
|
|
39
|
+
|
|
40
|
+
| you want | use | NOT |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| a property grid | `PanelSection` + `PropertyRow` / `PropertyPair` | `Card` — in 240px it spends its border and inset on nothing |
|
|
43
|
+
| a numeric property | `ScrubField` | `NumberField` — its two steppers put a ~112px floor on a ~44px slot |
|
|
44
|
+
| a layers / object list | `Tree` | a hand-nested list — no selection model or keyboard |
|
|
45
|
+
| an empty panel section | `TreeEmpty` | `EmptyState` — `px-6 py-12` around a `size-12` disc |
|
|
46
|
+
| a colour property | `ColorField` | `<input type="color">` — OS dialog, ignores the theme, wrong size |
|
|
47
|
+
| ambient document facts | `StatusBar` | a toolbar — status is glanced at, not operated |
|
|
48
|
+
| chrome typography | `text-ui-md` / `-sm` / `-body` / `-section` / `-micro` / `-num` | `text-body-*` — page tiers, ~1/3 too large here |
|
|
49
|
+
|
|
50
|
+
## Menus, selects and popovers adapt on their own
|
|
51
|
+
|
|
52
|
+
`Menu`, `Select`, `Popover`, `Tooltip` and `ContextMenu` pick up the workspace's
|
|
53
|
+
density automatically — including **across the portal**. Their popups mount at
|
|
54
|
+
the document root, so CSS could never reach them; they re-apply `data-density`
|
|
55
|
+
from React context instead.
|
|
56
|
+
|
|
57
|
+
**Do not size them per call site.** That was necessary before v0.41 — Glitch
|
|
58
|
+
carried 14 such overrides and had them inconsistent — and it is now drift.
|
|
59
|
+
|
|
60
|
+
A **modal** is deliberately different: `Dialog`, `Sheet` and
|
|
61
|
+
`DestructiveActionModal` stay page-register even when a tool opens them, because
|
|
62
|
+
a modal is its own surface rather than part of the control that opened it.
|
|
63
|
+
|
|
64
|
+
## The traps, all of which have shipped
|
|
65
|
+
|
|
66
|
+
- **A portaled surface still fires your canvas's pointer handlers.** Every Sable
|
|
67
|
+
popup — `Dialog`, `Menu`, `Select`, `Popover`, `Tooltip`, `ContextMenu` —
|
|
68
|
+
portals to `document.body`. But React propagates events through the **component
|
|
69
|
+
tree, not the DOM tree**, so if the component that renders your canvas also
|
|
70
|
+
renders the dialog, every press inside that dialog re-enters your pan/drag/
|
|
71
|
+
marquee handler.
|
|
72
|
+
|
|
73
|
+
The symptom is specific and misleading: **pointerdown-driven controls work while
|
|
74
|
+
click-driven ones do not.** A combobox opens (pointerdown) but you cannot pick
|
|
75
|
+
an option (click); buttons, switches and Cancel are dead; typing and Escape are
|
|
76
|
+
fine. That is a global handler calling `setPointerCapture` on pointerdown, which
|
|
77
|
+
retargets `pointerup` away from the button, so the browser never synthesises a
|
|
78
|
+
`click`. Nothing is prevented and nothing errors — it reads exactly like a
|
|
79
|
+
broken component.
|
|
80
|
+
|
|
81
|
+
Guard by **containment in your own surface**, not by a marker on ancestors:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
// ✗ walks DOM ancestors — a portaled popup's ancestors are <body>/<html>,
|
|
85
|
+
// so the marker is never found and the guard passes everything through
|
|
86
|
+
if ((e.target as Element).closest("[data-no-pan]")) return
|
|
87
|
+
|
|
88
|
+
// ✓ a pan is a press inside the thing being panned. Portals fail this by
|
|
89
|
+
// construction, and so does every future popup you have not written yet
|
|
90
|
+
if (!viewportRef.current?.contains(e.target as Node)) return
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Test it with a popup **open over a live document**, not on an empty canvas — a
|
|
94
|
+
disabled pan cannot exhibit the bug, so a green run there proves nothing.
|
|
95
|
+
|
|
96
|
+
- **`min-h-0` is the precondition for scrolling.** `overflow-y-auto` only
|
|
97
|
+
activates if every flex ancestor can shrink. One missing `min-h-0` up the chain
|
|
98
|
+
and the panel grows instead of scrolling — silently.
|
|
99
|
+
- **Never override a control height with a selector.** `[data-density] button {
|
|
100
|
+
height: … }` loses to any equal-specificity utility. Redefine `--control-*` in a
|
|
101
|
+
density scope instead. CI-enforced (`raw-control-height`).
|
|
102
|
+
- **Tree geometry has one invariant:** `--tree-indent` must equal
|
|
103
|
+
`--tree-column`. A smaller step puts a child's content LEFT of its parent's and
|
|
104
|
+
the nesting reads inverted. Use `treeIndent(depth)`; never hand-write an inset.
|
|
105
|
+
- **No pills in chrome.** The compact scope flattens `--button-radius` to ~5px —
|
|
106
|
+
one of two page-register rules the workspace register deliberately reverses.
|
|
107
|
+
- **Drag with window listeners, not `setPointerCapture`.** Capture throws for an
|
|
108
|
+
inactive pointer id and the throw aborts the handler mid-gesture, silently.
|
|
109
|
+
- **An unmapped `<Icon name>` renders nothing.** No error. Check the contract.
|
|
110
|
+
|
|
111
|
+
## Verify
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx sable-lint # lints ./src; pass dirs to change (npx sable-lint app components)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`sable-lint` ships with the package and reads the token vocabulary from the
|
|
118
|
+
`registry.json` of the version you installed, so it checks your app against your
|
|
119
|
+
exact tokens. It exits 1 on any violation. `--rules` explains each one; `--ignore
|
|
120
|
+
'<glob>'` skips a data/fixture layer, where a hex value is content rather than a
|
|
121
|
+
style. A rare, genuine exception takes an auditable
|
|
122
|
+
`// design-lint-disable-next-line <ruleId> -- <reason>`.
|
|
123
|
+
|
|
124
|
+
Then your own `tsc` and tests. (`pnpm design-lint && pnpm build:registry &&
|
|
125
|
+
pnpm test:run` are the *in-repo* gates — they only run inside the Sable repo.)
|
|
126
|
+
|
|
127
|
+
Then measure in the browser. Every number in the workspace register came from
|
|
128
|
+
measuring Figma and Paper in-page, and every register bug found so far was found
|
|
129
|
+
by reading computed values, not by looking at a screenshot.
|
|
130
|
+
|
|
131
|
+
Also inspect the rendered hierarchy and complete a real flow with a live document,
|
|
132
|
+
keyboard navigation, a narrow viewport, and reduced motion. Use
|
|
133
|
+
[reviewing-a-design.md](./reviewing-a-design.md) to record evidence, repair defects,
|
|
134
|
+
and distinguish observed behavior from untested claims.
|
|
135
|
+
|
|
136
|
+
## Read next
|
|
137
|
+
|
|
138
|
+
- `DESIGN.md → Registers` — doctrine, measured numbers, the two reversals
|
|
139
|
+
- `llms/components/workspace.md`, `panel-group.md`, `panel-section.md`,
|
|
140
|
+
`tree.md`, `scrub-field.md`, `status-bar.md`, `color-field.md`
|
|
141
|
+
- `llms/reference/token-intent.md` — which token to reach for
|