@sableui/react 0.11.0 → 0.13.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 +10 -4
- package/CHANGELOG.md +199 -0
- package/DESIGN.md +3 -3
- package/README.md +15 -1
- package/bin/sable.mjs +254 -10
- package/dist/components/app-header/app-header.meta.js +2 -1
- package/dist/components/button/button.js +3 -0
- package/dist/components/button/button.meta.js +1 -1
- package/dist/components/button/button.variants.js +1 -1
- package/dist/components/button-group/button-group.js +17 -0
- package/dist/components/button-group/button-group.meta.js +72 -0
- package/dist/components/button-group/index.js +7 -0
- package/dist/components/caret-menu/caret-menu.js +221 -0
- package/dist/components/caret-menu/caret-menu.meta.js +119 -0
- package/dist/components/caret-menu/index.js +8 -0
- package/dist/components/chat-composer/chat-composer.js +30 -7
- package/dist/components/chat-composer/chat-composer.meta.js +27 -8
- package/dist/components/combobox/combobox.meta.js +26 -0
- package/dist/components/input-group/index.js +7 -0
- package/dist/components/input-group/input-group.js +66 -0
- package/dist/components/input-group/input-group.meta.js +106 -0
- package/dist/components/menu/index.js +2 -0
- package/dist/components/menu/menu.js +51 -9
- package/dist/components/menu/menu.meta.js +11 -2
- package/dist/components/menubar/index.js +8 -0
- package/dist/components/menubar/menubar.js +42 -0
- package/dist/components/menubar/menubar.meta.js +99 -0
- package/dist/components/message-scroller/message-scroller.meta.js +4 -0
- package/dist/components/navigation-menu/index.js +19 -0
- package/dist/components/navigation-menu/navigation-menu.js +108 -0
- package/dist/components/navigation-menu/navigation-menu.meta.js +141 -0
- package/dist/components/overflow-menu/overflow-menu.meta.js +13 -1
- package/dist/components/pagination/pagination.js +11 -5
- package/dist/components/pagination/pagination.meta.js +21 -3
- package/dist/components/scroller/scroller.js +8 -4
- package/dist/components/scroller/scroller.meta.js +6 -0
- package/dist/components/sheet/sheet.js +82 -9
- package/dist/components/sheet/sheet.meta.js +41 -4
- package/dist/components/sheet/sheet.variants.js +8 -1
- package/dist/components/sidebar/sidebar.js +13 -2
- package/dist/components/sidebar/sidebar.meta.js +10 -0
- package/dist/components/streaming-text/streaming-text.meta.js +5 -1
- package/dist/file-hashes.json +49 -33
- package/dist/icon-sets/reicon.js +2 -0
- package/dist/index.d.ts +660 -164
- package/dist/index.js +12 -1
- package/dist/lib/palette.js +46 -6
- package/dist/lib/taxonomy.js +5 -0
- package/dist/lib/use-caret-trigger.js +187 -0
- package/dist/lib/use-copy.js +6 -1
- package/dist/lib/use-smooth-wheel.js +164 -0
- package/dist/palette.css +243 -165
- package/dist/patterns/chat-panel.js +1 -1
- package/dist/patterns/chat-panel.meta.js +1 -0
- package/dist/styles.css +25 -0
- package/llms/components/app-header.md +1 -1
- package/llms/components/button-group.md +55 -0
- package/llms/components/button.md +1 -1
- package/llms/components/caret-menu.md +100 -0
- package/llms/components/chat-composer.md +24 -7
- package/llms/components/combobox.md +12 -0
- package/llms/components/input-group.md +78 -0
- package/llms/components/menu.md +16 -1
- package/llms/components/menubar.md +110 -0
- package/llms/components/message-scroller.md +1 -0
- package/llms/components/navigation-menu.md +113 -0
- package/llms/components/overflow-menu.md +26 -1
- package/llms/components/pagination.md +15 -3
- package/llms/components/scroller.md +1 -0
- package/llms/components/sheet.md +18 -3
- package/llms/components/sidebar.md +2 -0
- package/llms/components/streaming-text.md +2 -1
- package/llms/patterns/chat-panel.md +4 -1
- package/llms/reference/token-intent.md +1 -1
- package/llms/tasks/building-a-chat.md +158 -0
- package/llms/tasks/building-with-sable.md +4 -1
- package/llms/tasks/converting-an-app.md +7 -0
- package/llms/tasks/extending-a-component.md +170 -0
- package/llms/tasks/getting-started.md +22 -4
- package/llms/tasks/migrating-from-shadcn.md +351 -0
- package/llms-full.txt +1322 -27
- package/llms.txt +13 -4
- package/package.json +3 -2
- package/registry.json +979 -23
- package/scripts/lint-rules.mjs +35 -0
- package/scripts/wiring.mjs +541 -0
- package/skills/entry/SKILL.md +3 -2
- package/skills/sable/SKILL.md +26 -5
- package/targets/ios.json +45 -0
- package/dist/lib/use-wheel-to-horizontal.js +0 -65
package/AGENTS.md
CHANGED
|
@@ -51,10 +51,14 @@ These are short on purpose. Each links to a foundations doc with the full vocabu
|
|
|
51
51
|
|
|
52
52
|
Pick a theme as a starting point, then override tokens on top of it. Every token a theme sets can be reset by the app on `:root` (or any subtree) after the theme CSS loads — radius, density, type scale, motion scale, accent — and components follow, because they only read tokens. The docs site's **Customize** panel does exactly this live and exports the result as CSS; paste it after your Sable imports. Retrieve the overridable set with `npx sable token --list`. When you retune a filled role (`--primary`, `--sidebar-primary`, `--highlight`, `--destructive`, `--success`, `--tone-warning`, `--tone-info`), check the label on it: `sable lint` measures the pair against the installed theme and suggests one that reads. If the pair is a deliberate brand choice, keep it with `/* design-lint-disable-next-line label-contrast -- <reason> */`. Status colours have a second token for the colour drawn on a surface — `--destructive-text`, `--success-text`, `--tone-warning-text`, `--tone-info-text` — which `text-`, `border-` and `ring-<status>` read; only `bg-<status>` reads the fill. Retune both.
|
|
53
53
|
|
|
54
|
+
When a component doesn't do something you need — be a link, carry a control, take a key, offer @mentions or /commands — read `sable docs extending-a-component` before restyling or copying it: `render`, slots, controlled state and refs, `useCaretTrigger` + `CaretMenu`, and how to tell a library gap from app-owned work.
|
|
55
|
+
|
|
54
56
|
## Wire it up
|
|
55
57
|
|
|
56
58
|
The machine-readable version of this section is the `consume` block in `registry.json`.
|
|
57
59
|
|
|
60
|
+
**Start with `sable init`.** After `pnpm add @sableui/react`, run `pnpm exec sable init` (`npx sable init` once the package is installed — never before, when `npx sable` would fetch an unrelated package) from the app root. It runs steps 2–5 and 7 below: installs missing packages with the project's package manager, puts the CSS imports in order, adds dedupe and the Tailwind plugin to a Vite config (or a PostCSS config for Next.js), loads the fonts, mounts `IconProvider` in a Vite starter's entry, installs the agent skill, and finishes with `sable doctor`. What it cannot change safely it prints as a snippet with the file it belongs in; apply those, then run `sable doctor` again. `--dry-run` previews every change. In an app that is already wired, `sable doctor --fix` applies the same fixes to whatever doctor flags. The numbered steps are the manual route and what `init` checks against.
|
|
61
|
+
|
|
58
62
|
1. **Install** from npm: `pnpm add @sableui/react` (or the project's package manager). No registry configuration or token is needed. Sable depends on no router, data or form library, and keeps whatever the project already uses: links, menus and nav items take any router's link through `render` (`render={<Link to="/inbox" />}` is TanStack Router's or React Router's `Link` alike). For a blank-slate app, the suggested stack is Vite + TypeScript with TanStack Router, TanStack Query, TanStack Form for forms with validation logic, and TanStack Table once a table outgrows `DataTable` (`sable docs tables-with-tanstack`).
|
|
59
63
|
2. **Dedupe React** in the bundler (`resolve.dedupe: ["react","react-dom"]`) — Base UI must not resolve a second React, or you get an "invalid hook call" blank page.
|
|
60
64
|
3. **CSS, in order:** `@import "tailwindcss";` → `@import "@sableui/react/styles.css";` → `@source "<node_modules>/@sableui/react/dist/**/*.{js,mjs}";` so Tailwind scans the package's classes. That ships every component's classes (about 32 KB gzipped); before shipping, `sable sources --write src/sable-sources.css` lists just the modules the app imports, imported in place of that `@source`, and `sable check` fails when it goes stale.
|
|
@@ -73,7 +77,8 @@ The machine-readable version of this section is the `consume` block in `registry
|
|
|
73
77
|
npx sable pattern SettingsPage --skeleton # composition tree, fixtures stripped
|
|
74
78
|
npx sable block --list # section-sized blocks: cards, forms, lists
|
|
75
79
|
npx sable component DataTable --props
|
|
76
|
-
npx sable search "empty state" # across components, patterns, layout, tokens, docs
|
|
80
|
+
npx sable search "empty state" # across components, examples, patterns, blocks, layout, tokens, docs
|
|
81
|
+
npx sable component Form --example "Server errors" # one example a search hit names, imports included
|
|
77
82
|
npx sable docs converting-an-app # the task walkthroughs
|
|
78
83
|
npx sable manifest --json # the whole command surface, machine-readable
|
|
79
84
|
```
|
|
@@ -150,9 +155,10 @@ package with `--root` naming the repository root (for example
|
|
|
150
155
|
there and resolve Sable through the package.
|
|
151
156
|
Each entry resolves `skills/sable/SKILL.md` from the installed package, so package
|
|
152
157
|
upgrades also update the workflow. Other agents can read that file directly.
|
|
153
|
-
Use `$sable build <idea>`, `inspire <references>`, `audit <target>`,
|
|
154
|
-
`polish <target>` in Codex; use `/sable` with the same modes in
|
|
155
|
-
Cursor. `create` is a build alias.
|
|
158
|
+
Use `$sable build <idea>`, `inspire <references>`, `audit <target>`,
|
|
159
|
+
`polish <target>`, or `fix <target>` in Codex; use `/sable` with the same modes in
|
|
160
|
+
Claude Code or Cursor. `create` is a build alias. `fix` takes the short path for
|
|
161
|
+
one scoped defect and escalates to `polish` when the change grows. These are agent modes, not shell commands.
|
|
156
162
|
|
|
157
163
|
Upgrade with `pnpm update @sableui/react --latest`, then
|
|
158
164
|
`pnpm exec sable setup --agent --update`. Setup only replaces managed files
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,204 @@
|
|
|
1
1
|
# What's new
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
Tone scales that keep their steps apart, whatever the tone.
|
|
6
|
+
|
|
7
|
+
- **Breaking: tone scales are one ladder, and the tone sits on its nearest
|
|
8
|
+
step.** Every tone scale (`tone-1-*` … `tone-3-*`, `tone-info-*`,
|
|
9
|
+
`tone-warning-*`, `success-*`, `destructive-*`) now uses one shared
|
|
10
|
+
ladder of depths per mode, so `bg-tone-1-100` and `bg-success-100` are
|
|
11
|
+
the same depth. The tone lands exactly on the step nearest its own
|
|
12
|
+
lightness, not always on 500. Before, a tone near one end of the range
|
|
13
|
+
squeezed five steps into the little room left past it: in dark mode,
|
|
14
|
+
light status colours (Poppy, Prism) had 600–950 almost
|
|
15
|
+
indistinguishable, and deep tones in light mode crowded 800–950. Now the
|
|
16
|
+
steps either side stretch to keep the ladder's ends. Migration: where you
|
|
17
|
+
used `*-500` to mean the tone, use the tone token (`bg-success`,
|
|
18
|
+
`text-tone-1`). The other steps keep their roles (100 a wash, 700/800
|
|
19
|
+
text), but may be a little lighter or deeper than before.
|
|
20
|
+
- **Tone steps stay in sRGB for any hue.** A tone scale's chroma falls from
|
|
21
|
+
the tone toward each end of the ladder, under a bound fitted per step
|
|
22
|
+
over the whole hue circle, so a customised or app-defined tone can't push
|
|
23
|
+
a step out of gamut. This replaces the fixed caps on the light steps.
|
|
24
|
+
- **Yellow sits a little deeper in dark mode** (500 at L 0.78, was 0.82), so
|
|
25
|
+
its 800–950 no longer crowd against the ink.
|
|
26
|
+
- **The palette tests hold spacing.** Neighbouring steps of every scale, in
|
|
27
|
+
every theme and mode, are at least 3 apart in OKLab (the 50→100 washes
|
|
28
|
+
1.4), and every tone sits exactly on its nearest step. Before, the only
|
|
29
|
+
check was that each step moved 0.005 in lightness, and 73 of 224 scales
|
|
30
|
+
had a pair of neighbouring steps under 3 apart.
|
|
31
|
+
|
|
32
|
+
## 0.12.0
|
|
33
|
+
|
|
34
|
+
Getting to a working first component, finding the right example, moving over from shadcn, and the components that move called for: Menubar, NavigationMenu, InputGroup, ButtonGroup, CaretMenu and bottom-sheet snap points.
|
|
35
|
+
|
|
36
|
+
- **New: `sable init` wires an app in one step.** Run it after `pnpm add
|
|
37
|
+
@sableui/react`. It installs what is missing with your package manager
|
|
38
|
+
(React, Tailwind and its build plugin), puts the CSS imports in order, adds
|
|
39
|
+
React dedupe and the Tailwind plugin to a Vite config (a
|
|
40
|
+
`postcss.config.mjs` for Next.js), loads Figtree and DM Mono, and wraps a
|
|
41
|
+
Vite starter's `<App />` in `IconProvider`. Then it installs the agent
|
|
42
|
+
skill and runs `sable doctor`. What it can't change safely, such as a
|
|
43
|
+
Next.js layout or an unfamiliar config, it prints as the snippet and the
|
|
44
|
+
file it belongs in. `--dry-run` shows every change first.
|
|
45
|
+
- **New: `sable doctor --fix`** applies the same fixes to whatever doctor
|
|
46
|
+
flags in an app that's already set up, then checks again. A fix that
|
|
47
|
+
didn't take is reported as unverified, not done. Doctor also gains two
|
|
48
|
+
warnings: Tailwind missing from the Vite or Next.js build (`css:build`),
|
|
49
|
+
and the theme's fonts never loaded (`fonts`).
|
|
50
|
+
- **`sable search` finds examples.** Every component example is searchable
|
|
51
|
+
by what it shows: `sable search "multiple selection"` or `"server errors"`.
|
|
52
|
+
A hit names its own retrieve command, `sable component Combobox --example
|
|
53
|
+
"Multiple selection"`, which prints that one example with its imports.
|
|
54
|
+
Examples are capped per component and per list, so a component name still
|
|
55
|
+
lands on the component.
|
|
56
|
+
- **New: a migration guide from shadcn/ui and Radix.** `sable docs
|
|
57
|
+
migrating-from-shadcn` maps what changes everywhere (`asChild` becomes
|
|
58
|
+
`render`, values become arrays, menu rows take `onClick`,
|
|
59
|
+
`data-[state=open]:` becomes `sable-open:`), the tokens, lucide icons, and
|
|
60
|
+
every shadcn component to its Sable counterpart, or what to compose where
|
|
61
|
+
there isn't one. `converting-an-app` points to it.
|
|
62
|
+
- **New lint rule: `shadcn-leftover`.** Flags imports still coming from the
|
|
63
|
+
old stack (`@radix-ui/*`, `@/components/ui`, `lucide-react`, `sonner`,
|
|
64
|
+
`vaul`, `cmdk`, `input-otp`, `next-themes`) and any `asChild` prop, which a
|
|
65
|
+
Sable trigger silently ignores. A deliberate keeper takes a reasoned disable.
|
|
66
|
+
- **The agent skill has a `fix` mode.** `$sable fix <target>` (or `/sable
|
|
67
|
+
fix`) repairs one scoped defect on a short path: it reads only the
|
|
68
|
+
contracts the change touches, then runs `sable check` on the changed files
|
|
69
|
+
and looks at the affected state. It escalates to `polish`, and its full
|
|
70
|
+
finish checklist, when the change grows. Run `sable setup --agent --update`
|
|
71
|
+
to refresh installed entries.
|
|
72
|
+
- **ChatComposer can send attachments without text.** Pass
|
|
73
|
+
`allowEmpty={files.length > 0}` and send enables on the files alone;
|
|
74
|
+
`onSend` receives `""`. Tie it to the pending files: a constant
|
|
75
|
+
`allowEmpty` leaves send enabled with nothing to send.
|
|
76
|
+
- **ChatComposer: on a phone or tablet, Return starts a new line.** A soft
|
|
77
|
+
keyboard has no Shift+Return, so on a touch device (`pointer: coarse`) the
|
|
78
|
+
default `sendOn="enter"` now behaves as `mod-enter`: Return breaks the
|
|
79
|
+
line, the send button sends, and ⌘/Ctrl+Enter still sends from a hardware
|
|
80
|
+
keyboard. `aria-keyshortcuts` reports whichever is in effect. `footer` now
|
|
81
|
+
also takes a function of the send key in effect, so a keyboard hint can
|
|
82
|
+
stay true: `footer={({ sendOn }) => sendOn === "enter" ? "Enter to send" :
|
|
83
|
+
null}` (null removes the row). *Migration:* a fixed "Enter to send" footer
|
|
84
|
+
is wrong on touch; switch it to the function form, as ChatPanel has.
|
|
85
|
+
- **Combobox documents server-backed search.** A new "Async search" example
|
|
86
|
+
and the props it uses (`filter={null}`, `onInputValueChange`,
|
|
87
|
+
`itemToStringLabel`) show how to show results a server already filtered.
|
|
88
|
+
- **New: `Menubar`.** An application's row of menus (File, Edit, View) over
|
|
89
|
+
Base UI's Menubar. Each menu is an ordinary `Menu` with a `MenubarTrigger`
|
|
90
|
+
as its title. The bar is one tab stop with ←/→ between titles, and once a
|
|
91
|
+
menu is open, pointing at a neighbour opens it.
|
|
92
|
+
- **New: `NavigationMenu`.** A site header's primary nav: a bar of links and
|
|
93
|
+
triggers, where a trigger opens a panel of links on hover, click or Enter.
|
|
94
|
+
One shared panel moves along the bar and resizes to each trigger's
|
|
95
|
+
content. Links take your router's link through `render` and mark the
|
|
96
|
+
current page with `active`; panel rows take a `description`. Below `md`,
|
|
97
|
+
hand the same destinations to a Sheet with a SideNav, as the docs show.
|
|
98
|
+
- **Pagination can render real links.** `pageLink={(page) => <Link
|
|
99
|
+
to={`?page=${page}`} />}` makes every page control a link that crawlers
|
|
100
|
+
follow and readers can open in a new tab. The current page keeps
|
|
101
|
+
`aria-current`; the arrows at the ends stay disabled buttons.
|
|
102
|
+
`onPageChange` is optional with `pageLink`.
|
|
103
|
+
- **New: `InputGroup`.** An Input with addons inside the same field: a
|
|
104
|
+
prefix or suffix ("https://", ".com"), an icon, or a borderless Button
|
|
105
|
+
such as copy or reveal. One shell, one focus ring, one error state; inside
|
|
106
|
+
a Field, the label, description and error reach the Input.
|
|
107
|
+
- **OverflowMenu documents responsive actions.** Secondary actions inline
|
|
108
|
+
when the container is wide, folded into the menu when it's narrow, the
|
|
109
|
+
primary action always inline. `sable search "responsive action menu"`
|
|
110
|
+
finds it.
|
|
111
|
+
- **Sheet: snap points for bottom sheets.** `<Sheet snapPoints={[0.4, 1]}>`
|
|
112
|
+
gives a bottom sheet stops between closed and fully open; a swipe moves
|
|
113
|
+
between them, and `snapPoint` / `onSnapPointChange` control the active
|
|
114
|
+
one (new `SheetSnapPoint` type). With stops the sheet shows a drag handle,
|
|
115
|
+
and the part below the screen becomes padding, so content scrolls and
|
|
116
|
+
`SheetFooter` stays visible. Other sides ignore stops and a development
|
|
117
|
+
build says so. Every bottom sheet now also rides above the on-screen
|
|
118
|
+
keyboard (measured from `visualViewport`).
|
|
119
|
+
- **New: `CaretMenu` and `useCaretTrigger`, mentions and slash commands on
|
|
120
|
+
any text field.** The hook watches a Textarea, Input or ChatComposer
|
|
121
|
+
(`fieldRef`) for a trigger character at the caret (`@`, `/` at the start,
|
|
122
|
+
`#`) and reports the query. CaretMenu shows suggestions there, driven by
|
|
123
|
+
↑/↓, Enter, Tab and Escape while focus stays in the field. The app
|
|
124
|
+
supplies the items and what choosing one does: `trigger.replace("@Sam
|
|
125
|
+
Rivera ")` inserts a mention as text, and a command runs and `replace("")`
|
|
126
|
+
clears it. The value stays a plain string; chips inside the field need a
|
|
127
|
+
rich-text editor, which Sable doesn't ship.
|
|
128
|
+
- **ChatComposer: an Enter someone else handled doesn't send.** A listener
|
|
129
|
+
on `fieldRef` that handles Enter and calls `preventDefault()` owns it. It's
|
|
130
|
+
how CaretMenu picks a suggestion instead of sending, and how an app adds
|
|
131
|
+
key behaviour of its own.
|
|
132
|
+
- **ChatComposer keeps focus after Stop.** Stop turns back into send in
|
|
133
|
+
place, and with an empty field that send is disabled, which dropped focus
|
|
134
|
+
to the page. Focus now moves to the field, except on touch, where it would
|
|
135
|
+
raise the keyboard unasked.
|
|
136
|
+
- **Scroller eases the mouse wheel.** New `smoothWheel` (on by default): a
|
|
137
|
+
notch eases toward its target instead of jumping, on either axis, the
|
|
138
|
+
inertia a library like Lenis adds but scoped to the region. Only a mouse
|
|
139
|
+
wheel is eased: trackpads and touch keep their own momentum, keys and
|
|
140
|
+
scrollbar drags stay native, it releases at the ends so the page scrolls
|
|
141
|
+
on, a nested area that can still scroll (a code block, a textarea) keeps
|
|
142
|
+
the wheel, a region that smooths or snaps in CSS is left to the browser,
|
|
143
|
+
and anything else that moves the region (`scrollIntoView`, a scrollbar
|
|
144
|
+
drag) stops it at once. Reduced motion turns it off. New
|
|
145
|
+
`useSmoothWheel()` gives a scroll container of your own the same thing.
|
|
146
|
+
- **New guide: `sable docs building-a-chat`.** Wires Sable's chat parts to
|
|
147
|
+
any streaming source: which part owns what and what stays with the app, a
|
|
148
|
+
turn's lifecycle mapped onto `busy`, `onStop`, `streaming` and `disabled`,
|
|
149
|
+
where each failure goes, announcing a turn once rather than per token, and
|
|
150
|
+
rendering model output without injecting HTML.
|
|
151
|
+
- **New guide: `sable docs extending-a-component`.** What to do when a
|
|
152
|
+
component doesn't do what you need: compose with `render`, fill a slot,
|
|
153
|
+
hold the state and reach the element, add behaviour on top (mentions as
|
|
154
|
+
the worked example), change the look through tokens and presets rather
|
|
155
|
+
than overrides, patch a defect, and tell a library gap from app work.
|
|
156
|
+
- **The shadcn migration guide now maps every component.** Menubar,
|
|
157
|
+
NavigationMenu, InputGroup, ButtonGroup, link Pagination and drawer snap
|
|
158
|
+
points all have counterparts.
|
|
159
|
+
- **Fix: a horizontal Scroller no longer widens its container.** Its outer
|
|
160
|
+
box kept the default `min-width: auto`, so inside a grid or flex column a
|
|
161
|
+
strip of cards grew to its full length and never scrolled. It now shrinks
|
|
162
|
+
to the column and scrolls with its edge fade. The Scrolling page's own
|
|
163
|
+
example was showing exactly this, and also lacked `orientation="horizontal"`.
|
|
164
|
+
- **`useCopy` takes a function.** `useCopy(() => fetch(url).then((r) =>
|
|
165
|
+
r.text()))` produces the text only on click; a pending promise goes to the
|
|
166
|
+
clipboard as a ClipboardItem so Safari still counts it as part of the click.
|
|
167
|
+
New `CopySource` type.
|
|
168
|
+
- **New: `ButtonGroup`.** Buttons joined into one control, such as a split
|
|
169
|
+
button (an action beside the chevron that opens its menu) or a short run of
|
|
170
|
+
related actions. The children stay ordinary Buttons with their own variants
|
|
171
|
+
and sizes. The group squares their inner corners, collapses bordered seams
|
|
172
|
+
into one hairline and draws a seam between filled ones. Button now reads a
|
|
173
|
+
per-side radius (`--button-radius-s` / `--button-radius-e`, falling back to
|
|
174
|
+
`--button-radius`) and renders `data-variant`, which is how the group
|
|
175
|
+
does it without overriding a class.
|
|
176
|
+
- **Menu rows can carry an icon and a description.** `MenuItem` takes `icon`
|
|
177
|
+
and `description`, a quieter second line saying what the action does; with
|
|
178
|
+
a description the icon sits in a small tile spanning both lines.
|
|
179
|
+
- **New: `MenuLinkItem`.** A menu row that navigates: a real `<a>`, so it opens
|
|
180
|
+
in a new tab, middle-clicks and copies as a link. With `target="_blank"`
|
|
181
|
+
it adds `rel="noopener noreferrer"` and a trailing ↗.
|
|
182
|
+
- **Icon contract: `arrow-up-right`.** Added for links that leave the app.
|
|
183
|
+
An app with its own icon set should map it; Reicon's set already does.
|
|
184
|
+
- **sableui.com: a Copy page button on every page.** Each page with a
|
|
185
|
+
Markdown version has a split button in its header. Copy page copies the
|
|
186
|
+
page as Markdown, fetched only when you click. The menu behind the chevron
|
|
187
|
+
opens the Markdown file, starts a ChatGPT or Claude chat that reads the
|
|
188
|
+
page (Claude tries the desktop app first and falls back to claude.ai), and
|
|
189
|
+
copies the setup command (`pnpm add @sableui/react && pnpm exec
|
|
190
|
+
sable init`). The Markdown files are the same ones the site publishes at
|
|
191
|
+
`/<page>.md`; both now come from one table
|
|
192
|
+
(`apps/docs/src/markdown-twins.json`).
|
|
193
|
+
- **New: `Sidebar surface="page"`.** The rail paints the page's own surface
|
|
194
|
+
and ink, with no border, instead of the theme's `--sidebar-*` set. Use it
|
|
195
|
+
when a full-width header sits above the rail: a tinted or dark rail
|
|
196
|
+
(Verdant, Meridian) starting under a page-coloured bar reads as pasted on.
|
|
197
|
+
The default, `surface="rail"`, is unchanged and still right for a
|
|
198
|
+
full-height rail. Overriding `bg-background text-foreground` on a Sidebar
|
|
199
|
+
never worked, because the rail re-points those tokens at its own;
|
|
200
|
+
use the prop instead. sableui.com's docs rail now uses it.
|
|
201
|
+
|
|
3
202
|
## 0.11.0
|
|
4
203
|
|
|
5
204
|
- **New: `PropertyGroup`, a collapsible group inside a PanelSection.** The
|
package/DESIGN.md
CHANGED
|
@@ -85,9 +85,9 @@ Roles are the contract, and roles are not enough for a product: tags, avatars, l
|
|
|
85
85
|
- **Nine families:** `neutral`, `red`, `orange`, `yellow`, `green`, `teal`, `blue`, `purple` and `pink`, each 50–950, as Tailwind utilities (`bg-blue-100`, `text-pink-700`) and variables (`--teal-500`). Tailwind's other families are switched off.
|
|
86
86
|
- **The lean.** A family keeps its own hue and leans toward the brand by half the gap, at most 15°, and only as far as the brand has colour (an ink-black primary pulls nothing). It's Material 3's harmonize, and the same lean the customizer gives status colours. `--theme-palette-harmony` scales it: 0 keeps the families' own hues, 2 doubles the pull.
|
|
87
87
|
- **Neutral** is the theme's own `--background` mixed toward its `--foreground`, so it carries the surfaces' tint.
|
|
88
|
-
- **Tone scales.** Every tone (tone-1…3, info, warning, success, destructive) gets a scale
|
|
88
|
+
- **Tone scales.** Every tone (tone-1…3, info, warning, success, destructive) gets a scale on one shared ladder of depths, so `bg-tone-1-100` and `bg-success-100` are the same depth. The tone sits exactly on the step nearest its own lightness: a mid tone is 500, a light status colour in dark mode is 800. The steps either side stretch to keep the ladder's ends, so a tone near one end does not crush five steps into the little room left past it. Use `--success` itself for the tone, and the steps for its washes and deeper inks. Chroma falls from the tone toward each end, under an sRGB bound fitted per step over the whole hue circle, since a tone can be any hue.
|
|
89
89
|
- **Steps follow the mode.** A low step is always near the page and a high step always far from it, so a pair like `bg-blue-100 text-blue-800` reads in both modes with no `dark:`.
|
|
90
|
-
- **Held, not hoped.** It's all relative-colour CSS computed where it is declared, so a preset, a customizer look or an app's `--primary` carries into it with no regeneration. Chroma is fitted under sRGB across the whole lean (piecewise, a knot every 5°), and `palette.test.ts` evaluates the shipped CSS for every built-in theme and mode: in gamut, steadily away from the page, `700` readable on the page, `800` readable on its own `100`, and neighbouring families apart.
|
|
90
|
+
- **Held, not hoped.** It's all relative-colour CSS computed where it is declared, so a preset, a customizer look or an app's `--primary` carries into it with no regeneration. Chroma is fitted under sRGB across the whole lean (piecewise, a knot every 5°), and `palette.test.ts` evaluates the shipped CSS for every built-in theme and mode: in gamut, steadily away from the page, `700` readable on the page, `800` readable on its own `100`, neighbouring steps at least 3 apart in OKLab (the 50→100 washes 1.4), every tone exactly on its nearest step, and neighbouring families apart.
|
|
91
91
|
|
|
92
92
|
Components never name a step. They paint roles, which is what lets a theme restyle them. Where a component takes a family (Badge, Pill and StatusDot accept `tone="blue"`, a category with no meaning attached), it sets `data-palette` to the family and paints from the slots that fills: `palette-wash` (100), `palette-wash-strong` (200), `palette-edge` (300), `palette-dot` (500), `palette-solid` (700), `palette-text` (800) and `palette-solid-ink` (50). The classes are the same for every family, and each ink-on-fill pair is held to 4.5:1. An app can use `data-palette` the same way.
|
|
93
93
|
|
|
@@ -469,7 +469,7 @@ Reference via `z-(--z-overlay)`, never a raw `z-[9999]`. Base UI overlays portal
|
|
|
469
469
|
|
|
470
470
|
**Vertical gets a bar; horizontal gets a carousel.** A vertical list needs position feedback — how far through a long transcript am I — and a thin bar is the only thing that gives it. A horizontal strip does not: what a reader needs there is "there is more this way", which an **edge fade says better than a 4px rail**, and which the rail obscures by adding chrome under the content. So `Scroller` draws a bar on its vertical axis and none on its horizontal one, where its alpha-masked edges already do the work. Reach for the same split in anything strip-shaped: media rows, chip rows, tab overflow.
|
|
471
471
|
|
|
472
|
-
**A horizontal region must be operable without a horizontal input.** Hiding the bar removed the last non-drag affordance on that axis, and most mice have no horizontal wheel — so `Scroller` maps a vertical wheel to sideways movement, and is focusable with arrow / Home / End / PageUp / PageDown handling. Both are on by default; the wheel mapping is `wheelScroll`, the name is `label`. The four things that make a naive wheel mapping feel broken, all handled in `
|
|
472
|
+
**A horizontal region must be operable without a horizontal input.** Hiding the bar removed the last non-drag affordance on that axis, and most mice have no horizontal wheel — so `Scroller` maps a vertical wheel to sideways movement, and is focusable with arrow / Home / End / PageUp / PageDown handling. Both are on by default; the wheel mapping is `wheelScroll`, the name is `label`. The four things that make a naive wheel mapping feel broken, all handled in `useSmoothWheel` (lib/use-smooth-wheel.ts): it **releases at both ends** so it never traps a reader scrolling the page, it **ignores trackpad `deltaX` gestures** (`|deltaX| > |deltaY|` means the browser is already doing it, and handling `deltaY` too double-scrolls a diagonal swipe), it **ignores `shift+wheel`** (already native), and it **normalises `deltaMode`** (mice report *lines*, so the raw value moves ~3px per notch). The same hook eases a mouse wheel on either axis (`smoothWheel`, on by default): only notches, never trackpads or touch, it gives way to a nested area that can still scroll, it leaves regions that smooth or snap in CSS to the browser, and anything else that moves the region stops the ease. Keyboard focus is declared rather than inherited: Chrome 128+ and Firefox make scroll regions focusable on their own, Safari does not.
|
|
473
473
|
|
|
474
474
|
**Reveal is driven by scrolling, not by hover.** Hover is a decent proxy and a bad one for the cases that matter: a touch drag hovers nothing, and neither does a programmatic scroll — a chat auto-following a streamed reply, a jump-to-message, a restored read position — which are exactly the moments a reader needs to see where they now are. `useScrollActivity` sets `data-scrolling` on the element for ~600ms after each scroll event and `scrollbar-quiet` keys off it. Hover and `:focus-within` remain in the CSS as the no-JS baseline, so the bare utility still behaves for a consumer who never reaches for the hook. The fade honours `--motion-scale`, so reduced motion collapses it to an instant swap.
|
|
475
475
|
|
package/README.md
CHANGED
|
@@ -62,6 +62,17 @@ retrieval is the default. These tools do not update model weights. See
|
|
|
62
62
|
|
|
63
63
|
> **Agents:** the machine-readable version of these steps is the `consume` block in [`registry.json`](./registry.json).
|
|
64
64
|
|
|
65
|
+
**Quick start** — install, then let `sable init` do steps 2–5:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pnpm add @sableui/react
|
|
69
|
+
pnpm exec sable init # or: npx sable init — once the package is installed
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`init` reads your package manager from the lockfile and your framework from the project. It installs what is missing (React, Tailwind and its build plugin), puts the CSS imports below in order, adds React dedupe and the Tailwind plugin to a Vite config, loads the fonts, mounts `IconProvider` in a Vite starter's entry, installs the agent skill (`sable setup --agent`), then runs `sable doctor`. Anything it cannot change safely — an unfamiliar config, a Next.js layout — it prints as the exact snippet and the file it belongs in. Vite and Next.js (App Router) get every step; other setups get the packages and the stylesheet. `--dry-run` shows every change first; `--no-agent` skips the skill. Already set up? `sable doctor --fix` applies the same fixes to whatever doctor flags.
|
|
73
|
+
|
|
74
|
+
The steps below are what `init` does, for doing it by hand or checking its work.
|
|
75
|
+
|
|
65
76
|
**1. Install:**
|
|
66
77
|
|
|
67
78
|
```bash
|
|
@@ -138,8 +149,11 @@ npx sable pattern --list # the eight canonical page shells
|
|
|
138
149
|
npx sable pattern DashboardPage --skeleton # its composition tree — 127 lines of source, 45 of structure
|
|
139
150
|
npx sable block PricingCardBlock --code # a section-sized block's source, imports included
|
|
140
151
|
npx sable component Button --props
|
|
141
|
-
npx sable search "date range" # one ranked set across components, patterns, layout, tokens, docs
|
|
152
|
+
npx sable search "date range" # one ranked set across components, examples, patterns, layout, tokens, docs
|
|
153
|
+
npx sable component Calendar --example Range # just the example a search hit names
|
|
154
|
+
npx sable init # wire a new app: packages, CSS, fonts, IconProvider, agent skill, then doctor
|
|
142
155
|
npx sable doctor # is this project wired correctly? exits 1 if not
|
|
156
|
+
npx sable doctor --fix # apply the fixes doctor can make safely; print the rest
|
|
143
157
|
npx sable docs designing-a-surface # brief, direction, composition, interaction, adaptation
|
|
144
158
|
npx sable docs developing-taste # references, rendered directions, scoped judgment
|
|
145
159
|
npx sable atlas --search "editorial culture" --contrast --limit 3 --json # a project's own atlas, if it has one
|