@sableui/react 0.7.0 → 0.8.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.
Files changed (77) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +115 -0
  3. package/README.md +2 -0
  4. package/bin/sable.mjs +34 -0
  5. package/dist/blocks/traffic-sources-block.js +1 -1
  6. package/dist/blocks/traffic-trend-block.js +3 -3
  7. package/dist/components/breadcrumbs/breadcrumbs.js +10 -9
  8. package/dist/components/breadcrumbs/breadcrumbs.meta.js +2 -2
  9. package/dist/components/button/button.meta.js +4 -0
  10. package/dist/components/card/card.js +7 -4
  11. package/dist/components/chat-composer/chat-composer.js +4 -2
  12. package/dist/components/chat-composer/chat-composer.meta.js +8 -2
  13. package/dist/components/command-menu/command-menu.js +9 -2
  14. package/dist/components/command-menu/command-menu.meta.js +3 -3
  15. package/dist/components/data-table/data-table.js +117 -64
  16. package/dist/components/data-table/data-table.meta.js +27 -1
  17. package/dist/components/form/form.js +30 -9
  18. package/dist/components/form/form.meta.js +17 -3
  19. package/dist/components/message-scroller/message-scroller.meta.js +1 -1
  20. package/dist/components/page-header/page-header.js +1 -0
  21. package/dist/components/page-header/page-header.meta.js +1 -1
  22. package/dist/components/pie-chart/pie-chart.js +3 -3
  23. package/dist/components/pie-chart/pie-chart.meta.js +3 -3
  24. package/dist/components/radial-chart/radial-chart.js +3 -3
  25. package/dist/components/radial-chart/radial-chart.meta.js +2 -2
  26. package/dist/components/settings-menu/settings-menu.js +10 -1
  27. package/dist/components/settings-menu/settings-menu.meta.js +1 -1
  28. package/dist/components/side-nav/side-nav.variants.js +3 -2
  29. package/dist/components/sidebar/sidebar.js +1 -1
  30. package/dist/components/sidebar/sidebar.meta.js +5 -0
  31. package/dist/components/switch/switch.js +1 -1
  32. package/dist/components/switch/switch.meta.js +2 -2
  33. package/dist/components/table/table.js +1 -1
  34. package/dist/components/toggle/toggle.js +2 -1
  35. package/dist/file-hashes.json +603 -0
  36. package/dist/icon-sets/reicon.js +13 -1
  37. package/dist/index.d.ts +88 -15
  38. package/dist/index.js +1 -1
  39. package/dist/layout/index.d.ts +5 -2
  40. package/dist/lib/chart/format.js +5 -0
  41. package/dist/lib/component-guidance.js +7 -2
  42. package/dist/lib/heading-row.js +3 -2
  43. package/dist/lib/trim-label.js +17 -4
  44. package/dist/presets/index.d.ts +7 -1
  45. package/dist/presets/index.js +10 -0
  46. package/dist/styles.css +27 -2
  47. package/llms/blocks/traffic-sources-block.md +1 -1
  48. package/llms/blocks/traffic-trend-block.md +5 -3
  49. package/llms/components/breadcrumbs.md +1 -1
  50. package/llms/components/button.md +1 -0
  51. package/llms/components/chat-composer.md +3 -2
  52. package/llms/components/command-menu.md +2 -2
  53. package/llms/components/data-table.md +33 -1
  54. package/llms/components/form.md +19 -2
  55. package/llms/components/message-scroller.md +1 -1
  56. package/llms/components/page-header.md +1 -1
  57. package/llms/components/pie-chart.md +2 -2
  58. package/llms/components/radial-chart.md +1 -1
  59. package/llms/components/settings-menu.md +1 -1
  60. package/llms/components/sidebar.md +1 -0
  61. package/llms/components/switch.md +2 -2
  62. package/llms/layout/region.md +16 -3
  63. package/llms/tasks/building-a-page.md +34 -3
  64. package/llms/tasks/creating-a-preset.md +4 -1
  65. package/llms/tasks/patching-a-component.md +48 -0
  66. package/llms/tasks/reviewing-a-design.md +9 -0
  67. package/llms/tasks/using-presets.md +19 -0
  68. package/llms-full.txt +207 -27
  69. package/llms.txt +1 -1
  70. package/package.json +4 -2
  71. package/registry.json +132 -30
  72. package/scripts/design-memory.mjs +7 -2
  73. package/scripts/design-presets.mjs +15 -4
  74. package/scripts/lint-rules.mjs +32 -1
  75. package/scripts/patches.mjs +219 -0
  76. package/scripts/sources.mjs +247 -0
  77. package/skills/sable/SKILL.md +16 -4
package/AGENTS.md CHANGED
@@ -36,7 +36,7 @@ These are short on purpose. Each links to a foundations doc with the full vocabu
36
36
 
37
37
  **The same goes for everything below a component. Resolve in this order, and stop at the first that fits:** (1) a **component** → (2) a Sable **utility or recipe** — relative surfaces (`sable-surface*`, `sable-well`, `sable-surface-interactive`), state variants (`sable-selected:`, `sable-highlighted:`…), geometry (`rounded-concentric-*`, `popup-inset`, `rounded-popup-item`, `popup-bleed`), scroll policies, `motion-*` recipes → (3) a **semantic token** → (4) hand-written CSS, last, and reported as a gap. `npx sable search "<what you need>"` searches all of these; `registry.json → utilities` lists the utilities with intent and what each replaces. Mixing ink into a surface by hand (`color-mix(… var(--foreground) …)`) is the tell that you skipped step 2 — that is what the relative-surface utilities compute, in every theme and mode.
38
38
 
39
- **Never restyle a component with override classes to get a look it lacks.** `className` on a Sable component is for layout (width, margin, flex and grid placement, position); its look — fill, edge, corner, ring, shadow, ink — comes from a variant. Use the one that exists, and if none fits, that is a library gap to report, not a `className`. The common cases: a control floating over content is `<Button variant="floating">`, one on a coloured band `variant="on-color"`; a quiet chip is `<Pill appearance="outline" size="sm">`; a status label is `<Badge appearance="plain" tone="…" dot>`; a destructive menu row is `<MenuItem variant="destructive">` (never `className="text-destructive"`); a list of `Entity` rows in a card is `<CardList>`, and a table that runs edge to edge in a card is `<CardContent flush>`, which insets the outer columns to the card's padding (never `pl-6`/`pr-6` on cells). `sable-lint` flags these (`component-restyle`, `destructive-item-ink`, `table-edge-padding`, `popup-separator-bleed`).
39
+ **Never restyle a component with override classes to get a look it lacks.** `className` on a Sable component is for layout (width, margin, flex and grid placement, position); its look — fill, edge, corner, ring, shadow, ink — comes from a variant. Use the one that exists, and if none fits, that is a library gap to report, not a `className`. The common cases: a control floating over content is `<Button variant="floating">`, one on a coloured band `variant="on-color"`; a quiet chip is `<Pill appearance="outline" size="sm">`; a status label is `<Badge appearance="plain" tone="…" dot>`; a destructive menu row is `<MenuItem variant="destructive">` (never `className="text-destructive"`); a list of `Entity` rows in a card is `<CardList>`, and a table that runs edge to edge in a card is `<CardContent flush>`, which insets the outer columns to the card's padding (never `pl-6`/`pr-6` on cells). `sable-lint` flags these (`component-restyle`, `destructive-item-ink`, `table-edge-padding`, `popup-separator-bleed`). A component that is *broken*, not just missing a look, is patched in place with `sable patch <Component> --reason … --repro …` while the fix ships: the record is re-checked on every upgrade (`sable docs patching-a-component`).
40
40
  1. **Semantic tokens only.** `bg-card`, `text-muted-foreground`, `border-border`, `bg-primary` — never a palette step, never inline hex/`oklch()`. Tone fills take their paired ink (`bg-tone-success` + `text-tone-success-foreground`). → `sable docs token-intent`
41
41
  2. **Type is a closed vocabulary.** One paired utility per text node (`text-heading-2`, `text-body-md`, `text-label-lg`, `text-mono-md`); never compose `text-* font-* tracking-*` by hand. Pick by **role**, not size. One `heading-1` per route. Two colors: `foreground` and `muted-foreground` — never opacity on text. → `sable docs typography`
42
42
  3. **Space on the 4px scale, by role.** Inset, stack, inline; related things closer than unrelated things. No off-scale `p-[15px]`. Padding, gaps and insets stay on the scale (a step, or `--spacing(n)` inside an arbitrary value) so the density control moves them; fixed `rem`/`px` only for what must not scale, like a hairline. Single-column content in a card is a flex column, not a grid. → `sable docs spacing`
@@ -57,7 +57,7 @@ The machine-readable version of this section is the `consume` block in `registry
57
57
 
58
58
  1. **Install** from npm: `pnpm add @sableui/react` (or the project's package manager). No registry configuration or token is needed.
59
59
  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
- 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.
60
+ 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.
61
61
 
62
62
  `styles.css` carries **Sable only** — the default theme, and the layout floor every other theme inherits (`--page-inset`, `--region-*`, `--rhythm-unit` on a bare `:root`). The bundled **alternates are opt-in**, because they are `[data-sable-theme]`-scoped and an app that never sets the attribute ships CSS that can never match: 5.5 KB gzip of permanently-dead weight, which is exactly how a real app's build audit found this. Using one? `@import "@sableui/react/theme/prism.css";`. Offering the whole set through a `<ThemeSelector>`? `@import "@sableui/react/themes.css";`. **Symptom of forgetting:** `data-sable-theme` is set, nothing matches, and the page silently renders as the Sable theme — no error, just the wrong theme.
63
63
  4. **Fonts:** load Figtree (sans) + DM Mono (mono) — the theme declares the stacks, the host provides the files. Apply `antialiased` (`-webkit-font-smoothing: antialiased` / `-moz-osx-font-smoothing: grayscale`) **once on the app root**: Sable's type scale was drawn against smoothed text, and without it macOS renders every weight heavier than designed. The package deliberately doesn't set it — a library has no business styling the host's whole page — so it has to happen here.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,120 @@
1
1
  # What's new
2
2
 
3
+ ## 0.8.0
4
+
5
+ The same support-workspace app was built twice, on Sable and on shadcn, and this
6
+ release closes every gap the comparison found: two silent layout and form bugs,
7
+ the component defects, row selection, a sanctioned way to patch a component
8
+ while its fix ships, and CSS that only carries the components an app imports.
9
+
10
+ ### Upgrading from 0.7
11
+
12
+ - **`<Form noValidate>` now means your code owns validation.** `onSubmit` always
13
+ runs and each Field shows the `error` you set; nothing checks the Fields or
14
+ the browser first. It used to skip only Sable's extra browser check, while
15
+ Form still blocked an empty `required` Field. `validationMode`, `errors`,
16
+ `onFormSubmit` and `actionsRef` do nothing with it. Drop `noValidate` to keep
17
+ Sable's validation, and word its messages with Field's new `messages`.
18
+ - **A Switch's knob stays white when on.** It took the primary's label ink, so
19
+ a mid-light brand colour gave a dark knob. It now turns dark only on a track
20
+ lighter than L 0.9 (a dark mode's near-white primary). Set
21
+ `--theme-switch-thumb-on` to choose your own.
22
+ - **PageHeader holds 20rem for its title** before actions may share its row, so
23
+ on a phone the actions sit under the title instead of squeezing it to one
24
+ word a line.
25
+
26
+ ### Everything in this release
27
+
28
+ - **Nested `Grid`, `Col` and `SplitPane` no longer inherit their parent's
29
+ breakpoints.** A breakpoint a nested one left unset read its ancestor's value,
30
+ so a `cols={2}` Grid inside a 12-column `lg` Columns drew 12 tracks, and a
31
+ nested `span={6}` Col spanned 9. Each now resets its own.
32
+ - **`Field messages`** words a failed native constraint:
33
+ `messages={{ valueMissing: "Enter a subject." }}`, instead of the browser's
34
+ "Please fill out this field."
35
+ - **Text around an expression stays one label.** `All ({count})` rendered as
36
+ "All ( 12 )" in Button, Pill, Badge, NotificationBadge, Toolbar and pill Tabs:
37
+ each text child was its own flex item, spaced by the gap. Adjacent text now
38
+ joins into one.
39
+ - **Toggle spaces an icon from its label**, like Button.
40
+ - **SidebarHeader grows to hold a second row** (a search field under the brand)
41
+ instead of clipping it at 64px. Give that row `basis-full`.
42
+ - **`BreadcrumbLink` takes `render`**, so a router link navigates in the app
43
+ instead of reloading the page.
44
+ - **DataTable checkbox selection.** `selectedKeys` and `onSelectedKeysChange`
45
+ add a checkbox column, each box named by `rowSelectionLabel`, and a
46
+ select-all box that acts on the rows shown and goes mixed when some are
47
+ checked. Checked rows take the selected fill; a box never opens the row. The
48
+ docs show a bulk-action bar with a live status.
49
+ - **`sable patch`** fixes a component in place while the library fix ships.
50
+ `sable patch Card --reason … --repro …` records why and how to check it, and
51
+ prints your package manager's patch steps (`pnpm patch`, `yarn patch`, or
52
+ patch-package). Every build now ships `dist/file-hashes.json`, and `sable
53
+ doctor` re-checks each patch on upgrade: it fails when Sable changed a
54
+ patched file, quoting the changelog for that component, until the repro says
55
+ `--keep` or `--drop`. See `sable docs patching-a-component`.
56
+ - **`sable sources`** writes `@source` lines for just the Sable modules an app
57
+ imports. The whole-package `@source` ships every component's classes (about
58
+ 32 KB gzipped); an app on Card, Button and DataTable measured 12 KB. `sable
59
+ doctor` fails when the file goes stale, since an unlisted component renders
60
+ unstyled.
61
+ - **New lint rule `form-required-error`:** a `required` Field with an `error`
62
+ inside a validating Form, where that error can never show for an empty field.
63
+ - **Icons:** `tag`, `paperclip`, `chart`, `flag` and `at-sign`.
64
+ - **`CommandMenu` stacked rows:** `stacked` puts `detail` under the label, for
65
+ results only their detail tells apart.
66
+ - **A one-setting `SettingsMenu`** opens straight to its options under the
67
+ row's label: one click, not two.
68
+ - **`ChatComposer sendOn="mod-enter"`** for long-form replies: Enter breaks the
69
+ line, Cmd/Ctrl+Enter sends.
70
+ - **Docs:** `sable learn --template` states its evidence rules (inside the
71
+ project; different before and after files); MessageScroller names its scroll
72
+ options (`{ animate }`, not `behavior`); Button explains `useCopy(text)`; and
73
+ `reviewing-a-design` says to scroll through, or capture with reduced motion,
74
+ before calling a below-the-fold chart empty.
75
+
76
+ ## 0.7.1
77
+
78
+ Fixes from building a customer-support app on 0.7.0 with an app-owned preset,
79
+ plus guidance for choosing a page width. Nothing to migrate.
80
+
81
+ - **A table in a flush card keeps the card's corners.** `Card` now clips to its
82
+ rounded corners when it holds a `CardContent flush`, as it already did for a
83
+ `CardList`. The hover fill of an edge-to-edge `Table` or `DataTable` row and a
84
+ `TableFooter`'s tint no longer poke square corners out past the card's.
85
+ - **Only body rows hover.** `TableRow` lifts on hover inside `TableBody`; header
86
+ and footer rows stay still, since nothing there responds to a click.
87
+ - **SideNav and Sidebar icons stay level with their labels at any spacing
88
+ step.** The icon box was sized in spacing steps around an icon sized in type,
89
+ so a theme with a step other than 4px shrank the box and dropped every icon
90
+ about 2px below its label. The box is now sized in the icon's own unit and
91
+ centred with flex.
92
+ - **A donut's number holds the centre.** In `PieChart` and a full-ring
93
+ `RadialChart`, `centerLabel`'s value sits on the centre and its caption hangs
94
+ below, instead of the pair splitting the centre between them.
95
+ - **`centerLabel.value` takes a number.** It is written in full up to 9,999 and
96
+ shortened from 10,000 on (`10.2K`, `102.1K`, `1.2M`), in the chart's
97
+ `locale`. A string still shows as written, for units.
98
+ - **Presets can set page widths.** `page-inset` and the role caps
99
+ (`region-form`, `-feed`, `-content`, `-wide`, `-full`) are preset tokens, so a
100
+ product's widths live in its preset rather than app CSS. They are opt-in:
101
+ `sable preset --template` and Canvas exports leave them out, so a preset never
102
+ overrides widths an app already sets. `PAGE_WIDTH_TOKENS` lists them.
103
+ - **Guidance for choosing a page width.** `sable docs building-a-page` has a
104
+ table of which `max` to use by what the person does on the page, and when to
105
+ go fluid. The Region docs say `full` is the largest cap (edge to edge is
106
+ `bleed`) and show a fluid workspace; the old "Fluid dashboard" example was
107
+ capped and is retitled. `reviewing-a-design` asks for a 1920px capture of a
108
+ capped or fluid route, where the choice shows, and the skill's brief records
109
+ each route's width.
110
+ - **A pasted preset gets applied.** Canvas's "Copy for agent" message now asks
111
+ the agent to apply the preset and build the app, not only save it. The skill
112
+ treats a preset supplied for an existing app as a restyle, `sable docs
113
+ using-presets` has an "Apply to an existing app" section, and a passing
114
+ `sable preset --check` lists the steps that make an app use the preset.
115
+ - **`TrafficTrendBlock` fits a narrow card.** Below 24rem its range toggles
116
+ move under the title instead of squeezing it.
117
+
3
118
  ## 0.7.0
4
119
 
5
120
  A dogfooding release. An agent built a music streaming app on Sable with
package/README.md CHANGED
@@ -72,6 +72,8 @@ export default defineConfig({ resolve: { dedupe: ["react", "react-dom"] } })
72
72
  @source "../node_modules/@sableui/react/dist/**/*.{js,mjs}";
73
73
  ```
74
74
 
75
+ That `@source` generates the classes of every Sable component, about 32 KB of CSS gzipped. Once the app is built, `npx sable sources --write src/sable-sources.css` writes `@source` lines for just the modules it imports (12 KB for an app using Card, Button and DataTable); `@import "./sable-sources.css";` in place of the line above. Re-run it after importing something new: `sable check` fails while the file is stale, since an unlisted component renders unstyled.
76
+
75
77
  **4. Fonts** — the default theme expects **Figtree** (sans) + **DM Mono** (mono). Each alternate preset declares its own families in its manifest (`registry.json → themes.available[].fonts`). Load whichever stacks you plan to use via `<link>` or `@font-face`; the theme manifests declare the stacks but ship no font files.
76
78
 
77
79
  **5. Providers + color mode / theme selection:**
package/bin/sable.mjs CHANGED
@@ -37,6 +37,8 @@ import { dirname, join, relative, resolve } from "node:path"
37
37
  import { fileURLToPath } from "node:url"
38
38
  import { skeletonize } from "../scripts/skeleton.mjs"
39
39
  import { claudeStopHook, installAgentSkill, installClaudeStopHook } from "../scripts/agent-setup.mjs"
40
+ import { patchChecks } from "../scripts/patches.mjs"
41
+ import { sourcesCheck } from "../scripts/sources.mjs"
40
42
 
41
43
  const HERE = dirname(fileURLToPath(import.meta.url))
42
44
  const PKG_ROOT = join(HERE, "..")
@@ -89,6 +91,28 @@ const COMMANDS = [
89
91
  ],
90
92
  returns: ["preset.template", "preset.check", "preset.css"],
91
93
  },
94
+ {
95
+ name: "patch",
96
+ summary: "Fix a Sable component in place while the library fix ships: record why and how to check it, patch with your package manager, and have doctor re-check it on every upgrade.",
97
+ arg: { name: "Component", required: false },
98
+ flags: [
99
+ { name: "--reason", value: "text", describe: "What is wrong. Required to record a patch." },
100
+ { name: "--repro", value: "text", describe: "How to see it, so a later upgrade can be checked against it. Required to record a patch." },
101
+ { name: "--issue", value: "id", describe: "Where it was reported upstream." },
102
+ { name: "--file", value: "dist/lib/<module>.js", describe: "Also cover a shared module the component uses, or patch one on its own." },
103
+ { name: "--list", value: null, describe: "Each recorded patch and its state against the installed Sable." },
104
+ { name: "--keep", value: null, describe: "After an upgrade: the repro still fails on Sable's own files, so keep the patch against this version." },
105
+ { name: "--drop", value: null, describe: "The repro passes on Sable's own files: drop the record (and the patch)." },
106
+ ],
107
+ returns: ["patch.recorded", "patch.list", "patch.kept", "patch.dropped"],
108
+ },
109
+ {
110
+ name: "sources",
111
+ summary: "Write the @source lines for just the Sable modules this app imports, so Tailwind generates their classes and no others (about 32 KB gzipped for all of Sable, a fraction for a typical app). doctor fails while the file is stale.",
112
+ arg: null,
113
+ flags: [{ name: "--write", value: "file.css", describe: "Write the stylesheet there (e.g. src/sable-sources.css) instead of printing it." }],
114
+ returns: ["sources"],
115
+ },
92
116
  {
93
117
  name: "setup",
94
118
  summary: "Install package-backed skill entry files for coding agents, preserving local edits.",
@@ -1265,6 +1289,9 @@ function doctorReport() {
1265
1289
  '@source "<path-to>/@sableui/react/dist/**/*.{js,mjs}";',
1266
1290
  )
1267
1291
  else if (hasSable) add("pass", "css:source", "@source covers the package.")
1292
+ // A generated per-module list (sable sources) must cover every module the
1293
+ // app imports, or those components render unstyled.
1294
+ if (hasSable) checks.push(...sourcesCheck(PKG_ROOT, CWD, expandedCss.map(({ path }) => cssByPath.get(path))))
1268
1295
  }
1269
1296
 
1270
1297
  /* The theme trap: opting into theme switching without importing theme CSS. */
@@ -1300,6 +1327,11 @@ function doctorReport() {
1300
1327
  )
1301
1328
  }
1302
1329
 
1330
+ /* Local patches (sable patch): each one is re-checked against the installed
1331
+ * Sable, and fails when Sable changed a patched file, so an upgrade tells the
1332
+ * agent to re-run the repro instead of keeping or losing a fix silently. */
1333
+ checks.push(...patchChecks(PKG_ROOT, CWD, R.version))
1334
+
1303
1335
  if (pkg && !/resolve\s*:\s*\{[^}]*dedupe|dedupe/.test(readConfigs()))
1304
1336
  add("info", "dedupe", "Could not confirm React dedupe in a bundler config.", 'resolve.dedupe: ["react","react-dom"] — a second React is an invalid-hook blank page.')
1305
1337
 
@@ -1627,6 +1659,8 @@ const HANDLERS = {
1627
1659
  ok("target", target, `iOS ${target.platformVersion}${target.status ? ` (${target.status})` : ""}: ${target.designPolicy.principle}\n${target.statusNote ? `${target.statusNote}\n` : ""}${target.designPolicy.rule}\n${(target.designPolicy.layout ?? []).map((l) => `Layout: ${l}\n`).join("")}` + Object.values(target.components).map(c => `${c.source}: ${c.strategy} → ${c.strategy === "contextual" ? "choose by role" : c.components.join(" + ") || "redesign for phone"}`).join("\n"))
1628
1660
  },
1629
1661
  preset: () => cmdDesignTool("design-presets.mjs"),
1662
+ patch: () => cmdDesignTool("patches.mjs"),
1663
+ sources: () => cmdDesignTool("sources.mjs"),
1630
1664
  setup: cmdSetup,
1631
1665
  check: cmdCheck,
1632
1666
  provider: cmdProvider,
@@ -24,7 +24,7 @@ function TrafficSourcesBlock({ className, framed = true }) {
24
24
  index: "source",
25
25
  valueKey: "visitors",
26
26
  donut: true,
27
- centerLabel: { value: total.toLocaleString("en-US"), label: "Visitors" }
27
+ centerLabel: { value: total, label: "Visitors" }
28
28
  }
29
29
  ) }),
30
30
  /* @__PURE__ */ jsxs(CardFooter, { className: "text-body-sm text-muted-foreground", children: [
@@ -27,11 +27,11 @@ var shortDate = (iso) => (/* @__PURE__ */ new Date(`${iso}T00:00:00`)).toLocaleD
27
27
  function TrafficTrendBlock({ className, framed = true }) {
28
28
  const [range, setRange] = useState("90d");
29
29
  const shown = days.slice(-(ranges.find((r) => r.id === range)?.days ?? 90));
30
- return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-3xl", className), children: [
31
- /* @__PURE__ */ jsxs(CardHeader, { children: [
30
+ return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("@container/trend w-full max-w-3xl", className), children: [
31
+ /* @__PURE__ */ jsxs(CardHeader, { className: "@max-sm/trend:has-data-[slot=card-action]:grid-cols-1", children: [
32
32
  /* @__PURE__ */ jsx(CardTitle, { render: /* @__PURE__ */ jsx("h2", {}), children: "Visitors" }),
33
33
  /* @__PURE__ */ jsx(CardDescription, { children: "Desktop and mobile, by day." }),
34
- /* @__PURE__ */ jsx(CardAction, { children: /* @__PURE__ */ jsx(ToggleGroup, { value: [range], onValueChange: (v) => v[0] && setRange(String(v[0])), "aria-label": "Time range", children: ranges.map((r) => /* @__PURE__ */ jsx(Toggle, { value: r.id, children: r.label }, r.id)) }) })
34
+ /* @__PURE__ */ jsx(CardAction, { className: "@max-sm/trend:col-start-1 @max-sm/trend:row-span-1 @max-sm/trend:row-start-auto @max-sm/trend:mt-2 @max-sm/trend:justify-self-start", children: /* @__PURE__ */ jsx(ToggleGroup, { value: [range], onValueChange: (v) => v[0] && setRange(String(v[0])), "aria-label": "Time range", children: ranges.map((r) => /* @__PURE__ */ jsx(Toggle, { value: r.id, children: r.label }, r.id)) }) })
35
35
  ] }),
36
36
  /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx(
37
37
  AreaChart,
@@ -1,4 +1,5 @@
1
1
  // src/components/breadcrumbs/breadcrumbs.tsx
2
+ import { useRender } from "@base-ui/react/use-render";
2
3
  import { Icon } from "../../icon/index.js";
3
4
  import { cn } from "../../lib/cn.js";
4
5
  import { jsx } from "react/jsx-runtime";
@@ -28,20 +29,20 @@ function BreadcrumbItem({
28
29
  BreadcrumbItem.displayName = "BreadcrumbItem";
29
30
  function BreadcrumbLink({
30
31
  className,
31
- ref,
32
+ render,
32
33
  ...props
33
34
  }) {
34
- return /* @__PURE__ */ jsx(
35
- "a",
36
- {
37
- ref,
35
+ return useRender({
36
+ render: render ?? /* @__PURE__ */ jsx("a", {}),
37
+ props: {
38
+ ...props,
38
39
  className: cn(
39
40
  "rounded-sm text-muted-foreground motion-interactive hover:text-foreground focus-visible:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
40
41
  className
41
- ),
42
- ...props
43
- }
44
- );
42
+ )
43
+ },
44
+ defaultTagName: "a"
45
+ });
45
46
  }
46
47
  BreadcrumbLink.displayName = "BreadcrumbLink";
47
48
  function BreadcrumbPage({
@@ -74,8 +74,8 @@ var breadcrumbsMeta = {
74
74
  },
75
75
  {
76
76
  name: "BreadcrumbLink",
77
- type: "ComponentPropsWithRef<'a'>",
78
- description: "An interactive ancestor crumb (<a>). Muted by default, brightens to foreground on hover/focus."
77
+ type: "ComponentPropsWithRef<'a'> & { render?: useRender.RenderProp }",
78
+ description: `An interactive ancestor crumb (<a>). Muted by default, brightens to foreground on hover/focus. render composes another element with the crumb's styling, e.g. a router link: render={<Link to="/library" />}, so navigating stays in the app.`
79
79
  },
80
80
  {
81
81
  name: "BreadcrumbPage",
@@ -31,6 +31,10 @@ var buttonMeta = {
31
31
  guidance: "do",
32
32
  description: "Use a single primary button in a view or dense panel so the action hierarchy stays obvious."
33
33
  },
34
+ {
35
+ guidance: "do",
36
+ description: "For a copy control, pair an icon Button with `useCopy(text, { timeout? })`. It takes the text when the hook is called and returns `{ copy, copied }`; `copy()` takes no argument, so pass the value the button copies to the hook, not to `copy`."
37
+ },
34
38
  {
35
39
  guidance: "do",
36
40
  description: 'Write labels as actions, such as "Save changes" or "Delete draft", instead of vague labels like "OK".'
@@ -44,10 +44,13 @@ function Card({
44
44
  "[--card-inner-radius:max(var(--radius-sm),calc(var(--radius-lg)-var(--card-p)-1px))]",
45
45
  // Declares its surface so sable-well / sable-surface-interactive inside derive from the card.
46
46
  variant === "default" && "border border-border bg-card shadow-sm [--sable-surface-bg:var(--card)]",
47
- // A CardList runs its rows to the card's edges, and an interactive
48
- // row's hover fill would otherwise poke square-ish corners out past
49
- // the card's rounded ones — clip to the card's own curve instead.
50
- "has-data-[slot=card-list]:overflow-clip",
47
+ // A CardList or a flush CardContent (an edge-to-edge Table) runs its
48
+ // rows to the card's edges, and a row's hover fill or a TableFooter's
49
+ // tint would otherwise poke square corners out past the card's
50
+ // rounded ones — clip to the card's own curve instead. Nothing
51
+ // overflows on purpose here: the table insets its edge cells by
52
+ // --table-edge-px, so focus rings stay inside, and menus portal out.
53
+ "has-data-[slot=card-list]:overflow-clip has-[[data-slot=card-content][data-flush]]:overflow-clip",
51
54
  INTERACTIVE_BASE,
52
55
  INTERACTIVE_HOVER[variant],
53
56
  className
@@ -16,6 +16,7 @@ function ChatComposer({
16
16
  onValueChange,
17
17
  onSend,
18
18
  placeholder = "Send a message\u2026",
19
+ sendOn = "enter",
19
20
  busy = false,
20
21
  onStop,
21
22
  disabled = false,
@@ -69,11 +70,11 @@ function ChatComposer({
69
70
  (event) => {
70
71
  if (event.key !== "Enter") return;
71
72
  if (event.nativeEvent.isComposing) return;
72
- if (event.shiftKey) return;
73
+ if (sendOn === "mod-enter" ? !(event.metaKey || event.ctrlKey) : event.shiftKey) return;
73
74
  event.preventDefault();
74
75
  send();
75
76
  },
76
- [send]
77
+ [send, sendOn]
77
78
  );
78
79
  const showStop = busy && Boolean(onStop);
79
80
  return /* @__PURE__ */ jsxs(
@@ -103,6 +104,7 @@ function ChatComposer({
103
104
  autoResize: true,
104
105
  rows: 1,
105
106
  "aria-label": label,
107
+ "aria-keyshortcuts": sendOn === "mod-enter" ? "Meta+Enter Control+Enter" : "Enter",
106
108
  "aria-invalid": invalid || void 0,
107
109
  disabled,
108
110
  placeholder,
@@ -1,7 +1,7 @@
1
1
  // src/components/chat-composer/chat-composer.meta.ts
2
2
  var chatComposerMeta = {
3
3
  name: "ChatComposer",
4
- summary: "The input half of a conversation: one surface holding attachments, an auto-growing field, and its controls. Enter sends, Shift+Enter breaks the line, and send becomes stop while a reply streams.",
4
+ summary: 'The input half of a conversation: one surface holding attachments, an auto-growing field, and its controls. Enter sends, Shift+Enter breaks the line (sendOn="mod-enter" swaps them for long-form replies), and send becomes stop while a reply streams.',
5
5
  status: "stable",
6
6
  keywords: [
7
7
  "chat input",
@@ -46,7 +46,7 @@ var chatComposerMeta = {
46
46
  },
47
47
  {
48
48
  guidance: "dont",
49
- description: "Do not use it as a general multi-line input. That is `Textarea` \u2014 this one submits on Enter, which is wrong for any field inside a larger form."
49
+ description: 'Do not use it as a general multi-line input inside a larger form. That is `Textarea`. A standalone reply box that sends multi-paragraph messages (email, support replies) is this one with sendOn="mod-enter".'
50
50
  },
51
51
  {
52
52
  guidance: "dont",
@@ -118,6 +118,12 @@ var chatComposerMeta = {
118
118
  default: '"Send a message\u2026"',
119
119
  description: "Placeholder for the field."
120
120
  },
121
+ {
122
+ name: "sendOn",
123
+ type: '"enter" | "mod-enter"',
124
+ default: '"enter"',
125
+ description: 'Which key sends. enter for chat (Shift+Enter breaks the line). mod-enter for long-form replies such as an email or a support answer: Enter breaks the line and Cmd/Ctrl+Enter sends, so a paragraph break never mails a draft. Say so in footer ("Cmd+Enter to send").'
126
+ },
121
127
  {
122
128
  name: "busy",
123
129
  type: "boolean",
@@ -201,6 +201,7 @@ function CommandMenuBody({
201
201
  id: item.id ? `${listboxId}-${item.id}` : void 0,
202
202
  icon: item.icon,
203
203
  detail: item.detail,
204
+ stacked: item.stacked,
204
205
  hint: item.hint,
205
206
  disabled: item.disabled,
206
207
  onSelect: () => {
@@ -371,6 +372,7 @@ function CommandMenuItem({
371
372
  onSelect,
372
373
  icon,
373
374
  detail,
375
+ stacked,
374
376
  hint,
375
377
  id: idProp,
376
378
  className,
@@ -408,8 +410,13 @@ function CommandMenuItem({
408
410
  ...props,
409
411
  children: [
410
412
  icon && /* @__PURE__ */ jsx(Icon, { name: icon, className: "text-base text-muted-foreground" }),
411
- /* @__PURE__ */ jsx("span", { className: "truncate", children }),
412
- detail && /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-body-sm text-muted-foreground", children: detail }),
413
+ stacked ? /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
414
+ /* @__PURE__ */ jsx("span", { className: "truncate", children }),
415
+ detail && /* @__PURE__ */ jsx("span", { className: "truncate text-body-sm text-muted-foreground", children: detail })
416
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
417
+ /* @__PURE__ */ jsx("span", { className: "truncate", children }),
418
+ detail && /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-body-sm text-muted-foreground", children: detail })
419
+ ] }),
413
420
  hint && /* @__PURE__ */ jsx("span", { className: "ms-auto flex min-w-0 max-w-1/2 items-center ps-4 text-body-sm text-muted-foreground", children: /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: hint }) })
414
421
  ]
415
422
  }
@@ -132,7 +132,7 @@ var commandMenuMeta = {
132
132
  {
133
133
  name: "items",
134
134
  type: "CommandItem[]",
135
- description: "Data-driven commands ({ group, label, onSelect, id?, icon?, detail?, hint?, disabled? }). Ignored when children are provided. id is optional identity: filter is handed these same objects, so it is how a caller that ranks over its own richer model maps the array back onto it \u2014 and it becomes the row's DOM id (namespaced to the palette) and React key, so two rows may share a label."
135
+ description: "Data-driven commands ({ group, label, onSelect, id?, icon?, detail?, stacked?, hint?, disabled? }). stacked puts detail on its own line under the label, for results only their detail tells apart (two tickets with one subject); beside a long label an inline detail truncates first. Ignored when children are provided. id is optional identity: filter is handed these same objects, so it is how a caller that ranks over its own richer model maps the array back onto it \u2014 and it becomes the row's DOM id (namespaced to the palette) and React key, so two rows may share a label."
136
136
  },
137
137
  {
138
138
  name: "query",
@@ -213,8 +213,8 @@ var commandMenuMeta = {
213
213
  },
214
214
  {
215
215
  name: "CommandMenuItem",
216
- type: "{ onSelect?: () => void; icon?: string; detail?: ReactNode; hint?: ReactNode; disabled?: boolean; id?: string; children }",
217
- description: "A selectable row. Self-filters by its text (unless the root was given a filter), runs onSelect, then closes the palette. detail is secondary text after the label; hint is the end-aligned trailing slot \u2014 a shortcut, a count, or a disabled row's reason. A disabled row stays listed, stays announced, and stays reachable by arrow key so its reason can be read, but is inert to Enter and to a click."
216
+ type: "{ onSelect?: () => void; icon?: string; detail?: ReactNode; stacked?: boolean; hint?: ReactNode; disabled?: boolean; id?: string; children }",
217
+ description: "A selectable row. Self-filters by its text (unless the root was given a filter), runs onSelect, then closes the palette. detail is secondary text after the label, or under it with stacked; hint is the end-aligned trailing slot \u2014 a shortcut, a count, or a disabled row's reason. A disabled row stays listed, stays announced, and stays reachable by arrow key so its reason can be read, but is inert to Enter and to a click."
218
218
  }
219
219
  ],
220
220
  tokensUsed: [