@sableui/react 0.8.1 → 0.9.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 +2 -2
- package/CHANGELOG.md +97 -0
- package/bin/sable.mjs +26 -11
- package/dist/blocks/index.d.ts +1 -1
- package/dist/components/app-shell/app-shell.js +2 -12
- package/dist/components/app-shell/app-shell.meta.js +22 -0
- package/dist/components/app-shell/index.js +2 -0
- package/dist/components/app-shell/skip-link.js +28 -0
- package/dist/components/button/button.js +35 -8
- package/dist/components/button/button.meta.js +6 -1
- package/dist/components/card/card.meta.js +1 -1
- package/dist/components/command-menu/command-menu.js +27 -2
- package/dist/components/data-table/data-table.meta.js +4 -0
- package/dist/components/date-tile/date-tile.meta.js +1 -0
- package/dist/components/feedback/feedback.meta.js +1 -0
- package/dist/components/fieldset/fieldset.js +22 -13
- package/dist/components/fieldset/fieldset.meta.js +25 -1
- package/dist/components/form/form.js +45 -9
- package/dist/components/form/form.meta.js +12 -3
- package/dist/components/media-tile/media-tile.js +16 -2
- package/dist/components/media-tile/media-tile.meta.js +8 -0
- package/dist/components/radio-card/radio-card.js +6 -2
- package/dist/components/radio-card/radio-card.meta.js +5 -0
- package/dist/components/sheet/sheet.js +1 -0
- package/dist/components/stepper/stepper.js +7 -2
- package/dist/components/stepper/stepper.meta.js +1 -1
- package/dist/components/toast/toast.js +2 -0
- package/dist/file-hashes.json +29 -28
- package/dist/index.d.ts +109 -22
- package/dist/index.js +1 -1
- package/dist/{meta-CcE_SeFu.d.ts → meta-DVG8ceze.d.ts} +9 -0
- package/dist/patterns/index.d.ts +1 -1
- package/dist/styles.css +30 -0
- package/llms/components/app-shell.md +26 -0
- package/llms/components/button.md +2 -1
- package/llms/components/card.md +1 -1
- package/llms/components/data-table.md +1 -0
- package/llms/components/fieldset.md +21 -1
- package/llms/components/form.md +11 -2
- package/llms/components/media-tile.md +32 -0
- package/llms/components/radio-card.md +1 -0
- package/llms/components/stepper.md +1 -1
- package/llms/tasks/creating-a-preset.md +4 -1
- package/llms/tasks/getting-started.md +6 -0
- package/llms/tasks/tables-with-tanstack.md +157 -0
- package/llms-full.txt +266 -7
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/registry.json +94 -10
- package/scripts/grade.mjs +14 -3
- package/scripts/sources.mjs +10 -2
- package/skills/sable/SKILL.md +32 -7
package/registry.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://sable.design/registry.schema.json",
|
|
3
3
|
"name": "@sableui/react",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"description": "Themeable React components, semantic tokens, layout patterns, presets, and package-owned agent skills. Default theme: Sable.",
|
|
6
6
|
"engine": {
|
|
7
7
|
"behavior": "@base-ui/react",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"@import \"@sableui/react/styles.css\";",
|
|
29
29
|
"@source \"<node_modules>/@sableui/react/dist/**/*.{js,mjs}\";"
|
|
30
30
|
],
|
|
31
|
+
"stack": "Sable depends on no router, data or form library; an existing app keeps its own, and links take any router's Link through render. For a blank-slate app the suggested stack is Vite + TypeScript, TanStack Router, TanStack Query, TanStack Form for forms with validation logic, and TanStack Table once a table outgrows DataTable (sable docs tables-with-tanstack).",
|
|
31
32
|
"cssSources": "That @source ships every component's classes (about 32 KB gzipped). Once the app is built, `sable sources --write src/sable-sources.css` lists just the modules it imports; @import that file in place of the @source. sable check fails while it is stale.",
|
|
32
33
|
"fonts": {
|
|
33
34
|
"sans": "Figtree",
|
|
@@ -2956,6 +2957,13 @@
|
|
|
2956
2957
|
"description": "Floating overlays — a message launcher, FAB, or modal mount points."
|
|
2957
2958
|
}
|
|
2958
2959
|
],
|
|
2960
|
+
"subcomponents": [
|
|
2961
|
+
{
|
|
2962
|
+
"name": "SkipLink",
|
|
2963
|
+
"type": "ComponentProps<\"a\">",
|
|
2964
|
+
"description": "AppShell's skip link on its own, for a layout that is not an AppShell (a storefront with a top header). Put it first in the body and give the page's <main> the matching id: `<SkipLink />` then `<main id=\"main-content\" tabIndex={-1}>`. `href` defaults to #main-content, the label to \"Skip to content\"."
|
|
2965
|
+
}
|
|
2966
|
+
],
|
|
2959
2967
|
"tokensUsed": [
|
|
2960
2968
|
"background",
|
|
2961
2969
|
"foreground",
|
|
@@ -2974,6 +2982,11 @@
|
|
|
2974
2982
|
"code": "<AppShell\n banners={<Banner tone=\"warning\">Network congested — mints may be slow.</Banner>}\n header={<AppHeader title=\"Studio\" />}\n sidebar={\n <Sidebar>\n <SidebarNav>\n <SidebarItem icon={<Icon name=\"home\" />} label=\"Home\" active render={<Link to=\"/\" />} />\n <SidebarItem icon={<Icon name=\"bell\" />} label=\"Notifications\" render={<Link to=\"/notifications\" />} />\n <SidebarItem icon={<Icon name=\"gear\" />} label=\"Settings\" render={<Link to=\"/settings\" />} />\n </SidebarNav>\n </Sidebar>\n }\n bottomNav={\n <BottomNav>\n <BottomNavItem icon={<Icon name=\"home\" />} label=\"Home\" active render={<Link to=\"/\" />} />\n <BottomNavItem icon={<Icon name=\"bell\" />} label=\"Alerts\" render={<Link to=\"/notifications\" />} />\n </BottomNav>\n }\n>\n <Region max=\"feed\">\n <PageHeader title=\"Home\" description=\"Latest posts from artists you follow.\" />\n </Region>\n</AppShell>",
|
|
2975
2983
|
"imports": "import { AppHeader, AppShell, Banner, BottomNav, BottomNavItem, Icon, PageHeader, Region, Sidebar, SidebarItem, SidebarNav } from \"@sableui/react\"\n// <Link> = your router's link (react-router, next/link, @tanstack/router, …)"
|
|
2976
2984
|
},
|
|
2985
|
+
{
|
|
2986
|
+
"label": "SkipLink in a layout of your own",
|
|
2987
|
+
"code": "<>\n <SkipLink />\n <header className=\"flex items-center justify-between px-(--page-inset) py-4\">\n <Link to=\"/\">Alder & Lune</Link>\n <Button variant=\"ghost\" size=\"icon\" aria-label=\"Search\"><Icon name=\"search\" /></Button>\n </header>\n <main id=\"main-content\" tabIndex={-1}>\n <Region max=\"wide\">\n <PageHeader title=\"Lighting\" />\n </Region>\n </main>\n</>",
|
|
2988
|
+
"imports": "import { Button, Icon, PageHeader, Region, SkipLink } from \"@sableui/react\"\n// <Link> = your router's link (react-router, next/link, @tanstack/router, …)"
|
|
2989
|
+
},
|
|
2977
2990
|
{
|
|
2978
2991
|
"label": "Minimal (sidebar + content only)",
|
|
2979
2992
|
"code": "<AppShell\n sidebar={\n <Sidebar>\n <SidebarNav>\n <SidebarItem icon={<Icon name=\"home\" />} label=\"Dashboard\" active render={<Link to=\"/\" />} />\n </SidebarNav>\n </Sidebar>\n }\n>\n <Region max=\"wide\">\n <PageHeader title=\"Dashboard\" />\n </Region>\n</AppShell>",
|
|
@@ -4921,7 +4934,12 @@
|
|
|
4921
4934
|
"name": "loading",
|
|
4922
4935
|
"type": "boolean",
|
|
4923
4936
|
"default": "false",
|
|
4924
|
-
"description": "
|
|
4937
|
+
"description": "Show a leading Spinner while an action is in flight and refuse presses. Sets aria-busy and aria-disabled, never the native disabled: disabling the focused button sent focus to <body>, so a sheet the action opened had nowhere to return it."
|
|
4938
|
+
},
|
|
4939
|
+
{
|
|
4940
|
+
"name": "onClick",
|
|
4941
|
+
"type": "(event: MouseEvent<HTMLButtonElement>) => unknown",
|
|
4942
|
+
"description": "May return a promise: the button shows `loading` until it settles and ignores further presses meanwhile, from the first click on. Prefer it to a `busy` flag in state, which takes effect a render late (three fast clicks ran three times)."
|
|
4925
4943
|
},
|
|
4926
4944
|
{
|
|
4927
4945
|
"name": "badge",
|
|
@@ -5353,7 +5371,7 @@
|
|
|
5353
5371
|
"name": "variant",
|
|
5354
5372
|
"type": "\"default\" | \"flat\"",
|
|
5355
5373
|
"default": "\"default\"",
|
|
5356
|
-
"description": "\"default\" raises with bg-card + border + soft shadow; \"flat\" is transparent."
|
|
5374
|
+
"description": "\"default\" raises with bg-card + border + soft shadow; \"flat\" is transparent, for a card inside a section that already frames it. A flat card made a link or button shows its shape only on hover (the muted fill), so over a photo with square bottom corners it reads as a cropped image at rest: give a linked media card the default variant."
|
|
5357
5375
|
},
|
|
5358
5376
|
{
|
|
5359
5377
|
"name": "density",
|
|
@@ -7471,6 +7489,10 @@
|
|
|
7471
7489
|
"guidance": "do",
|
|
7472
7490
|
"description": "Pair server-side datasets with Pagination and pass already-sorted/paged rows."
|
|
7473
7491
|
},
|
|
7492
|
+
{
|
|
7493
|
+
"guidance": "do",
|
|
7494
|
+
"description": "Move to TanStack Table, rendered with Sable's Table parts, once the table needs server-side sorting or filtering, column visibility or resizing, grouping, or virtualised rows. DataTable stays dependency-free and does not grow into a data grid; `sable docs tables-with-tanstack` maps one onto the other."
|
|
7495
|
+
},
|
|
7474
7496
|
{
|
|
7475
7497
|
"guidance": "dont",
|
|
7476
7498
|
"description": "Make every row clickable and also fill it with competing buttons. Choose a clear row interaction model."
|
|
@@ -7795,6 +7817,7 @@
|
|
|
7795
7817
|
{
|
|
7796
7818
|
"name": "DateTile",
|
|
7797
7819
|
"status": "stable",
|
|
7820
|
+
"composition": "a <time> element with the type tiers, on a surface utility",
|
|
7798
7821
|
"group": "Data display",
|
|
7799
7822
|
"register": "page",
|
|
7800
7823
|
"path": "components/date-tile",
|
|
@@ -8854,6 +8877,7 @@
|
|
|
8854
8877
|
{
|
|
8855
8878
|
"name": "Feedback",
|
|
8856
8879
|
"status": "stable",
|
|
8880
|
+
"composition": "Popover, Textarea, ToggleGroup, Button",
|
|
8857
8881
|
"group": "Overlays",
|
|
8858
8882
|
"register": "page",
|
|
8859
8883
|
"path": "components/feedback",
|
|
@@ -8967,7 +8991,7 @@
|
|
|
8967
8991
|
"group": "Forms",
|
|
8968
8992
|
"register": "either",
|
|
8969
8993
|
"path": "components/fieldset",
|
|
8970
|
-
"summary": "Form section panel (Card-like bordered surface) over Base UI Fieldset, with Legend/Description/Content/Footer parts.",
|
|
8994
|
+
"summary": "Form section panel (Card-like bordered surface) over Base UI Fieldset, with Legend/Description/Content/Footer parts; `variant=\"plain\"` is the bare group, for a radio group's shared name.",
|
|
8971
8995
|
"keywords": [
|
|
8972
8996
|
"fieldset",
|
|
8973
8997
|
"form group",
|
|
@@ -8989,6 +9013,10 @@
|
|
|
8989
9013
|
"guidance": "dont",
|
|
8990
9014
|
"description": "Do not use Fieldset as a generic spacing wrapper outside forms."
|
|
8991
9015
|
},
|
|
9016
|
+
{
|
|
9017
|
+
"guidance": "do",
|
|
9018
|
+
"description": "Use `variant=\"plain\"` to name a radio group or a run of checkboxes inside a form or card that is already framed: the legend reads as a field label and nothing draws a second box. Do not strip the card variant with classes instead."
|
|
9019
|
+
},
|
|
8992
9020
|
{
|
|
8993
9021
|
"guidance": "dont",
|
|
8994
9022
|
"description": "Do not use a Legend that is really a heading. A legend names the group's question for assistive technology; using it as a section title puts a question mark where the outline should be."
|
|
@@ -9029,6 +9057,12 @@
|
|
|
9029
9057
|
]
|
|
9030
9058
|
},
|
|
9031
9059
|
"props": [
|
|
9060
|
+
{
|
|
9061
|
+
"name": "variant",
|
|
9062
|
+
"type": "\"card\" | \"plain\"",
|
|
9063
|
+
"default": "\"card\"",
|
|
9064
|
+
"description": "`card` is a bordered, raised section with padded parts. `plain` is the grouping alone (no border, surface or inset) with the legend in the field-label style: the shared name a radio group needs, inside a form that is already framed."
|
|
9065
|
+
},
|
|
9032
9066
|
{
|
|
9033
9067
|
"name": "disabled",
|
|
9034
9068
|
"type": "boolean",
|
|
@@ -9090,6 +9124,11 @@
|
|
|
9090
9124
|
"label": "Why a fieldset, and when it is wrong",
|
|
9091
9125
|
"code": "// Fieldset groups inputs that are ONE question with multiple parts, and gives\n// the group a single accessible name — which is the part you cannot do with a\n// div and a heading.\n\n<Fieldset>\n <FieldsetLegend>Digest frequency</FieldsetLegend>\n <FieldsetContent>\n <RadioGroup defaultValue=\"weekly\">\n <Field label=\"Daily\" orientation=\"horizontal\"><Radio value=\"daily\" /></Field>\n <Field label=\"Weekly\" orientation=\"horizontal\"><Radio value=\"weekly\" /></Field>\n </RadioGroup>\n </FieldsetContent>\n</Fieldset>\n\n// ✓ Required for radio groups: without a fieldset the options have no shared\n// name, so a screen reader announces \"Weekly, radio\" with no question.\n// ✗ Not for visual grouping of unrelated fields. Use PanelSection on a panel or\n// a Card section on a page; a legend that is really a heading lies to\n// assistive technology about the form's structure.",
|
|
9092
9126
|
"imports": "import { Field, Fieldset, FieldsetContent, FieldsetLegend, Radio, RadioGroup } from \"@sableui/react\""
|
|
9127
|
+
},
|
|
9128
|
+
{
|
|
9129
|
+
"label": "A radio group's name, without a second box",
|
|
9130
|
+
"code": "<Card>\n <CardContent>\n <Fieldset variant=\"plain\">\n <FieldsetLegend>Delivery</FieldsetLegend>\n <RadioGroup defaultValue=\"standard\">\n <RadioCard value=\"standard\" title=\"Standard\" description=\"3–5 working days\" trailing=\"Free\" />\n <RadioCard value=\"express\" title=\"Express\" description=\"Next working day\" trailing=\"$12.00\" />\n </RadioGroup>\n </Fieldset>\n </CardContent>\n</Card>",
|
|
9131
|
+
"imports": "import { Card, CardContent, Fieldset, FieldsetLegend, RadioCard, RadioGroup } from \"@sableui/react\""
|
|
9093
9132
|
}
|
|
9094
9133
|
]
|
|
9095
9134
|
},
|
|
@@ -9570,7 +9609,12 @@
|
|
|
9570
9609
|
{
|
|
9571
9610
|
"name": "Form",
|
|
9572
9611
|
"type": "{ validationMode?: \"onSubmit\" | \"onBlur\" | \"onChange\"; errors?: Record<string, string | string[]>; onFormSubmit?: (values, details) => void; actionsRef?: RefObject<Form.Actions> } + ComponentPropsWithRef<\"form\">",
|
|
9573
|
-
"description": "A <form> laid out as a vertical column with comfortable field spacing (override with className, e.g. gap-4). On submit it validates every Field (native constraints plus Field validate), shows each message under its field, focuses the first invalid control, and only then calls onSubmit. validationMode defaults to \"onSubmit\" (then live re-validation on change). errors shows external (e.g. server) messages keyed by Field name and clears one when its field is edited. onFormSubmit receives the named Field values and prevents the native submit. A control outside any Field is still checked by the browser before onSubmit runs. Validating in your own code (react-hook-form, a schema, state)? Pass noValidate: onSubmit always runs and each Field shows the error you set; without it an empty required Field stops the submit before your handler can set that error."
|
|
9612
|
+
"description": "A <form> laid out as a vertical column with comfortable field spacing (override with className, e.g. gap-4). On submit it validates every Field (native constraints plus Field validate), shows each message under its field, focuses the first invalid control, and only then calls onSubmit. validationMode defaults to \"onSubmit\" (then live re-validation on change). errors shows external (e.g. server) messages keyed by Field name and clears one when its field is edited. onFormSubmit receives the named Field values and prevents the native submit. A control outside any Field is still checked by the browser before onSubmit runs. Validating in your own code (react-hook-form, a schema, state)? Pass noValidate: onSubmit always runs and each Field shows the error you set; without it an empty required Field stops the submit before your handler can set that error. Either way, once onSubmit returns (or the promise it returns settles), the first control a Field marks invalid takes focus."
|
|
9613
|
+
},
|
|
9614
|
+
{
|
|
9615
|
+
"name": "onSubmit",
|
|
9616
|
+
"type": "(event: FormEvent<HTMLFormElement>) => unknown",
|
|
9617
|
+
"description": "The native submit handler. It may return a promise. When it returns, or its promise settles, the first control a Field marks invalid takes focus, so errors your own code sets get the same treatment as Sable's validation."
|
|
9574
9618
|
},
|
|
9575
9619
|
{
|
|
9576
9620
|
"name": "validationMode",
|
|
@@ -9603,8 +9647,8 @@
|
|
|
9603
9647
|
"subcomponents": [
|
|
9604
9648
|
{
|
|
9605
9649
|
"name": "Field",
|
|
9606
|
-
"type": "{ label?: ReactNode; description?: ReactNode; error?: ReactNode; required?: boolean; messages?: Partial<Record<\"valueMissing\" | \"typeMismatch\" | \"patternMismatch\" | \"tooShort\" | \"tooLong\" | \"rangeUnderflow\" | \"rangeOverflow\" | \"stepMismatch\" | \"badInput\", ReactNode>>; disabled?: boolean; name?: string; validate?: (value, formValues) => string | string[] | null | Promise<…>; validationMode?: \"onSubmit\" | \"onBlur\" | \"onChange\"; validationDebounceTime?: number; orientation?: \"vertical\" | \"horizontal\"; className?: string; children: ReactElement }",
|
|
9607
|
-
"description": "Wraps a single control element (e.g. <Input />) on Base UI Field and wires label (htmlFor + aria-labelledby), helper text and error (aria-describedby), aria-invalid, and required onto it automatically. Base UI-backed controls (Input, Textarea, Select, Combobox, NumberField, Checkbox, Switch, Slider, OtpField…) register through context; the child is also cloned so plain elements keep the same wiring. error renders a destructive message and takes precedence; without it the slot shows the field's messages entry for the native constraint that failed (messages={{ valueMissing: \"Enter a subject.\" }}), else Base UI's validation message (the browser's wording for a native constraint, validate, or Form errors). required adds the native attribute plus a visual asterisk (defaults to false). disabled disables the control and dims the label. name keys the field in Form errors / onFormSubmit. validationMode overrides the Form's. orientation defaults to \"vertical\" (label → control); \"horizontal\" places control then label inline for a single checkbox / switch / radio — the control centres on the label's first line (it stays there when the label wraps) and description/error sit in the label's column. The root, label, description, error and control carry data-invalid / data-valid, data-touched, data-dirty, data-filled, data-focused and data-disabled for state styling."
|
|
9650
|
+
"type": "{ label?: ReactNode; description?: ReactNode; error?: ReactNode; required?: boolean; messages?: Partial<Record<\"valueMissing\" | \"typeMismatch\" | \"patternMismatch\" | \"tooShort\" | \"tooLong\" | \"rangeUnderflow\" | \"rangeOverflow\" | \"stepMismatch\" | \"badInput\", ReactNode>>; disabled?: boolean; name?: string; validate?: (value, formValues) => string | string[] | null | Promise<…>; validationMode?: \"onSubmit\" | \"onBlur\" | \"onChange\"; validationDebounceTime?: number; orientation?: \"vertical\" | \"horizontal\"; trailing?: ReactNode; className?: string; children: ReactElement } + other div props on the root",
|
|
9651
|
+
"description": "Wraps a single control element (e.g. <Input />) on Base UI Field and wires label (htmlFor + aria-labelledby), helper text and error (aria-describedby), aria-invalid, and required onto it automatically. Base UI-backed controls (Input, Textarea, Select, Combobox, NumberField, Checkbox, Switch, Slider, OtpField…) register through context; the child is also cloned so plain elements keep the same wiring. error renders a destructive message and takes precedence; without it the slot shows the field's messages entry for the native constraint that failed (messages={{ valueMissing: \"Enter a subject.\" }}), else Base UI's validation message (the browser's wording for a native constraint, validate, or Form errors). required adds the native attribute plus a visual asterisk (defaults to false). disabled disables the control and dims the label. name keys the field in Form errors / onFormSubmit. validationMode overrides the Form's. orientation defaults to \"vertical\" (label → control); \"horizontal\" places control then label inline for a single checkbox / switch / radio — the control centres on the label's first line (it stays there when the label wraps) and description/error sit in the label's column. trailing puts something at the end of the control's row: vertically, beside the control (a promo code's Apply button, the control taking the rest); horizontally, at the end of the label's first line (a facet count). Other props (data-*, id, aria-*) land on the root. The root, label, description, error and control carry data-invalid / data-valid, data-touched, data-dirty, data-filled, data-focused and data-disabled for state styling."
|
|
9608
9652
|
}
|
|
9609
9653
|
],
|
|
9610
9654
|
"tokensUsed": [
|
|
@@ -9628,6 +9672,11 @@
|
|
|
9628
9672
|
"code": "<Field label=\"Password\" error=\"Must be at least 8 characters.\"><Input type=\"password\" /></Field>",
|
|
9629
9673
|
"imports": "import { Field, Input } from \"@sableui/react\""
|
|
9630
9674
|
},
|
|
9675
|
+
{
|
|
9676
|
+
"label": "An input with its action",
|
|
9677
|
+
"code": "<Field label=\"Promo code\" error={promoError} trailing={<Button variant=\"secondary\" onClick={applyPromo}>Apply</Button>}><Input name=\"promo\" autoCapitalize=\"characters\" /></Field>",
|
|
9678
|
+
"imports": "import { Button, Field, Input } from \"@sableui/react\""
|
|
9679
|
+
},
|
|
9631
9680
|
{
|
|
9632
9681
|
"label": "Your own wording for a missing value",
|
|
9633
9682
|
"code": "<Form onSubmit={handleSubmit}><Field label=\"Subject\" required messages={{ valueMissing: \"Enter a subject.\" }}><Input name=\"subject\" /></Field><Button type=\"submit\">Send</Button></Form>",
|
|
@@ -11102,6 +11151,7 @@
|
|
|
11102
11151
|
{
|
|
11103
11152
|
"name": "MediaTile",
|
|
11104
11153
|
"status": "beta",
|
|
11154
|
+
"composition": "AspectBox, MediaImage, a stretched link (or Card render), Badge appearance=\"overlay\", Button",
|
|
11105
11155
|
"group": "Media & mockups",
|
|
11106
11156
|
"register": "page",
|
|
11107
11157
|
"path": "components/media-tile",
|
|
@@ -11203,11 +11253,26 @@
|
|
|
11203
11253
|
"type": "ReactNode",
|
|
11204
11254
|
"description": "The second line — an artist, a channel, a year."
|
|
11205
11255
|
},
|
|
11256
|
+
{
|
|
11257
|
+
"name": "meta",
|
|
11258
|
+
"type": "ReactNode",
|
|
11259
|
+
"description": "Lines under the subtitle, in its muted type: a price beside its sale price, a count of finishes, a status Badge. A wrapping block, where the subtitle is one truncated line. Use it for a product tile instead of building one."
|
|
11260
|
+
},
|
|
11206
11261
|
{
|
|
11207
11262
|
"name": "src",
|
|
11208
11263
|
"type": "string",
|
|
11209
11264
|
"description": "Cover image URL. Without one, or when it fails, the cover is GradientArt seeded by the item."
|
|
11210
11265
|
},
|
|
11266
|
+
{
|
|
11267
|
+
"name": "srcSet",
|
|
11268
|
+
"type": "string",
|
|
11269
|
+
"description": "Responsive cover candidates, passed to the `<img>`."
|
|
11270
|
+
},
|
|
11271
|
+
{
|
|
11272
|
+
"name": "sizes",
|
|
11273
|
+
"type": "string",
|
|
11274
|
+
"description": "The cover's layout width for `srcSet`, e.g. `\"(min-width: 64rem) 25vw, 50vw\"`."
|
|
11275
|
+
},
|
|
11211
11276
|
{
|
|
11212
11277
|
"name": "alt",
|
|
11213
11278
|
"type": "string",
|
|
@@ -11333,6 +11398,11 @@
|
|
|
11333
11398
|
"Truncated titles keep their full text in the link's accessible name and in a `title` tooltip."
|
|
11334
11399
|
],
|
|
11335
11400
|
"examples": [
|
|
11401
|
+
{
|
|
11402
|
+
"label": "Product tile",
|
|
11403
|
+
"code": "<Grid minItemWidth=\"14rem\" gap={4}>\n\t{products.map((product) => (\n\t\t<MediaTile\n\t\t\tkey={product.id}\n\t\t\taspect={4 / 5}\n\t\t\ttitle={product.name}\n\t\t\tsubtitle={product.material}\n\t\t\tsrc={product.image}\n\t\t\tsrcSet={product.imageSet}\n\t\t\tsizes=\"(min-width: 64rem) 25vw, 50vw\"\n\t\t\trender={<Link to={`/products/${product.slug}`} />}\n\t\t\tbadge={product.isNew ? <Badge appearance=\"overlay\">New</Badge> : undefined}\n\t\t\tmeta={\n\t\t\t\t<>\n\t\t\t\t\t<span className=\"text-foreground\">{formatPrice(product.price)}</span>\n\t\t\t\t\t<span>{product.finishes.length} finishes</span>\n\t\t\t\t</>\n\t\t\t}\n\t\t/>\n\t))}\n</Grid>",
|
|
11404
|
+
"imports": "import { Badge, Grid, MediaTile } from \"@sableui/react\"\n// <Link> = your router's link (react-router, next/link, @tanstack/router, …)"
|
|
11405
|
+
},
|
|
11336
11406
|
{
|
|
11337
11407
|
"label": "Album grid",
|
|
11338
11408
|
"code": "<Grid minItemWidth=\"10rem\" gap={3}>\n\t{albums.map((album) => (\n\t\t<MediaTile\n\t\t\tkey={album.id}\n\t\t\ttitle={album.title}\n\t\t\tsubtitle={album.artist}\n\t\t\tsrc={album.cover}\n\t\t\trender={<Link to={`/albums/${album.id}`} />}\n\t\t\tsubtitleRender={<Link to={`/artists/${album.artistId}`} />}\n\t\t\taction={\n\t\t\t\t<Button size=\"icon\" aria-label={`Play ${album.title}`} onClick={() => play(album)}>\n\t\t\t\t\t<Icon name=\"play\" variant=\"solid\" />\n\t\t\t\t</Button>\n\t\t\t}\n\t\t/>\n\t))}\n</Grid>",
|
|
@@ -14796,6 +14866,11 @@
|
|
|
14796
14866
|
"type": "ReactNode",
|
|
14797
14867
|
"description": "Optional supporting copy beneath the title (text-body-sm, muted)."
|
|
14798
14868
|
},
|
|
14869
|
+
{
|
|
14870
|
+
"name": "trailing",
|
|
14871
|
+
"type": "ReactNode",
|
|
14872
|
+
"description": "End-aligned on the title's line: a price, a delivery date, a count. The title takes the rest of the row."
|
|
14873
|
+
},
|
|
14799
14874
|
{
|
|
14800
14875
|
"name": "icon",
|
|
14801
14876
|
"type": "ReactNode",
|
|
@@ -17682,7 +17757,7 @@
|
|
|
17682
17757
|
"name": "orientation",
|
|
17683
17758
|
"type": "\"horizontal\" | \"vertical\"",
|
|
17684
17759
|
"default": "\"horizontal\"",
|
|
17685
|
-
"description": "Horizontal for a short flow across the top of a form; vertical for long flows, narrow columns or steps with descriptions."
|
|
17760
|
+
"description": "Horizontal for a short flow across the top of a form; vertical for long flows, narrow columns or steps with descriptions. A horizontal stepper narrower than 28rem (a phone) compacts itself: every disc stays, only the current step's title shows, and the other titles stay for screen readers. No breakpoint code needed."
|
|
17686
17761
|
},
|
|
17687
17762
|
{
|
|
17688
17763
|
"name": "indicator",
|
|
@@ -23869,6 +23944,7 @@
|
|
|
23869
23944
|
"iconStart",
|
|
23870
23945
|
"loading",
|
|
23871
23946
|
"loadingLabel",
|
|
23947
|
+
"onClick",
|
|
23872
23948
|
"render",
|
|
23873
23949
|
"size",
|
|
23874
23950
|
"variant"
|
|
@@ -24284,6 +24360,7 @@
|
|
|
24284
24360
|
"disabled",
|
|
24285
24361
|
"hint",
|
|
24286
24362
|
"icon",
|
|
24363
|
+
"leading",
|
|
24287
24364
|
"onSelect",
|
|
24288
24365
|
"stacked"
|
|
24289
24366
|
],
|
|
@@ -24616,6 +24693,7 @@
|
|
|
24616
24693
|
"name",
|
|
24617
24694
|
"orientation",
|
|
24618
24695
|
"required",
|
|
24696
|
+
"trailing",
|
|
24619
24697
|
"validate",
|
|
24620
24698
|
"validationDebounceTime",
|
|
24621
24699
|
"validationMode"
|
|
@@ -24623,7 +24701,8 @@
|
|
|
24623
24701
|
"Fieldset": [
|
|
24624
24702
|
"className",
|
|
24625
24703
|
"render",
|
|
24626
|
-
"style"
|
|
24704
|
+
"style",
|
|
24705
|
+
"variant"
|
|
24627
24706
|
],
|
|
24628
24707
|
"FieldsetLegend": [
|
|
24629
24708
|
"className",
|
|
@@ -24671,6 +24750,7 @@
|
|
|
24671
24750
|
"errors",
|
|
24672
24751
|
"noValidate",
|
|
24673
24752
|
"onFormSubmit",
|
|
24753
|
+
"onSubmit",
|
|
24674
24754
|
"validationMode"
|
|
24675
24755
|
],
|
|
24676
24756
|
"GlidingHighlight": [
|
|
@@ -24785,10 +24865,13 @@
|
|
|
24785
24865
|
"badge",
|
|
24786
24866
|
"href",
|
|
24787
24867
|
"loading",
|
|
24868
|
+
"meta",
|
|
24788
24869
|
"render",
|
|
24789
24870
|
"seed",
|
|
24790
24871
|
"size",
|
|
24872
|
+
"sizes",
|
|
24791
24873
|
"src",
|
|
24874
|
+
"srcSet",
|
|
24792
24875
|
"subtitle",
|
|
24793
24876
|
"subtitleHref",
|
|
24794
24877
|
"subtitleRender",
|
|
@@ -25347,6 +25430,7 @@
|
|
|
25347
25430
|
"title",
|
|
25348
25431
|
"titleClassName",
|
|
25349
25432
|
"tone",
|
|
25433
|
+
"trailing",
|
|
25350
25434
|
"value"
|
|
25351
25435
|
],
|
|
25352
25436
|
"RadioGroup": [
|
|
@@ -26372,7 +26456,7 @@
|
|
|
26372
26456
|
"name": "themeStorageKey",
|
|
26373
26457
|
"type": "string | null",
|
|
26374
26458
|
"default": "SABLE_THEME_STORAGE_KEY",
|
|
26375
|
-
"description": "localStorage key for the design theme. null disables persistence."
|
|
26459
|
+
"description": "localStorage key for the design theme. null disables persistence. The default key is shared by every Sable app on an origin (several apps on localhost:5173 in development), and a stored id another app set wins over `defaultThemeId` when this app knows it too. An app with one fixed preset passes `themeId` (or `themeStorageKey={null}`); an app that lets users switch themes passes its own key."
|
|
26376
26460
|
},
|
|
26377
26461
|
{
|
|
26378
26462
|
"name": "target",
|
package/scripts/grade.mjs
CHANGED
|
@@ -304,6 +304,9 @@ const UNIVERSAL_PROPS = new Set([
|
|
|
304
304
|
"minLength", "min", "max", "step", "rows", "cols", "open", "selected", "multiple", "loading",
|
|
305
305
|
// Media attributes, for components that spread onto <video>/<audio>/<img> (Video, Image).
|
|
306
306
|
"autoPlay", "muted", "loop", "playsInline", "preload", "crossOrigin", "decoding", "fetchPriority", "srcSet", "sizes",
|
|
307
|
+
// Input attributes the form contract asks for (the numeric keypad, the
|
|
308
|
+
// return key's label), on components that spread onto <input>.
|
|
309
|
+
"inputMode", "enterKeyHint", "autoCapitalize", "autoCorrect", "pattern", "list", "accept", "capture",
|
|
307
310
|
])
|
|
308
311
|
const isEventProp = (p) => /^on[A-Z]/.test(p)
|
|
309
312
|
const isPassthrough = (p) => p.startsWith("data-") || p.startsWith("aria-") || p.startsWith("--")
|
|
@@ -500,6 +503,14 @@ for (const file of files) {
|
|
|
500
503
|
/* 7. Surfaces the app built itself (informational). */
|
|
501
504
|
if (/\.(tsx|jsx)$/.test(file)) {
|
|
502
505
|
for (const { index, ...s } of surfaceInventory(text)) inventory.push({ file: rel, line: lineOf(text, index), ...s })
|
|
506
|
+
// Arrow-key handling written by hand: list navigation is behaviour Sable
|
|
507
|
+
// owns (roving focus, active descendant, typeahead), and the storefront
|
|
508
|
+
// search wrote its own. A note, since a canvas or a game has its own keys.
|
|
509
|
+
const arrows = /\bonKeyDown\b[\s\S]{0,600}?["'`]Arrow(?:Up|Down)["'`]|["'`]Arrow(?:Up|Down)["'`][\s\S]{0,600}?\bonKeyDown\b/.exec(text)
|
|
510
|
+
if (arrows) {
|
|
511
|
+
const at = text.indexOf("Arrow", arrows.index)
|
|
512
|
+
inventory.push({ file: rel, line: lineOf(text, at), tag: "onKeyDown", kind: "keyboard", classes: "ArrowUp / ArrowDown handled by hand", consider: "CommandMenu, Combobox, Menu, Select, Toolbar or Tabs, whose keyboard model is built and tested" })
|
|
513
|
+
}
|
|
503
514
|
}
|
|
504
515
|
|
|
505
516
|
/* 8. Composition. Anything exporting a component that returns a big tree is
|
|
@@ -679,10 +690,10 @@ if (conformance.length) {
|
|
|
679
690
|
L(" composition no route-like files found")
|
|
680
691
|
}
|
|
681
692
|
if (inventory.length) {
|
|
682
|
-
const kinds = ["surface", "media frame", "round"].map((k) => [k, inventory.filter((s) => s.kind === k).length]).filter(([, n]) => n)
|
|
693
|
+
const kinds = ["surface", "media frame", "round", "keyboard"].map((k) => [k, inventory.filter((s) => s.kind === k).length]).filter(([, n]) => n)
|
|
683
694
|
L()
|
|
684
|
-
L(` app-owned surfaces (
|
|
685
|
-
for (const s of inventory.slice(0, 12)) L(` ${s.file}:${s.line} <${s.tag}> ${s.kind} (${s.classes}) → ${s.consider}`)
|
|
695
|
+
L(` app-owned surfaces (notes, not scored; keeping your own is fine) — ${inventory.length}: ${kinds.map(([k, n]) => `${n} ${k}`).join(" · ")}`)
|
|
696
|
+
for (const s of inventory.slice(0, 12)) L(` ${s.file}:${s.line} <${s.tag}> ${s.kind} (${s.classes}) → Sable has ${s.consider}`)
|
|
686
697
|
if (inventory.length > 12) L(` … ${inventory.length - 12} more (--json lists every one)`)
|
|
687
698
|
}
|
|
688
699
|
if (cliUsage) {
|
package/scripts/sources.mjs
CHANGED
|
@@ -195,13 +195,21 @@ export function sourcesCss(pkgRoot, root, outFile) {
|
|
|
195
195
|
* that component unstyled, silently — so it fails. A whole-package @source is
|
|
196
196
|
* correct, just heavy, so it only gets a note.
|
|
197
197
|
*/
|
|
198
|
+
const WHOLE_DIST = /@source[^;]*(?:@sableui\/react|packages\/sable)\/dist(?:\/\*\*)?[^;]*;/
|
|
199
|
+
|
|
198
200
|
export function sourcesCheck(pkgRoot, root, cssFiles) {
|
|
199
201
|
const generated = cssFiles.find((f) => f.text.includes(SOURCES_MARKER))
|
|
200
202
|
if (!generated) {
|
|
201
|
-
const whole = cssFiles.some((f) =>
|
|
202
|
-
return whole ? [{ status: "info", name: "css:size", detail: "@source scans all of Sable's dist, so every component's classes ship (about 32 KB gzipped).", fix: "sable sources --write src/sable-sources.css, then import it in place of that @source." }] : []
|
|
203
|
+
const whole = cssFiles.some((f) => WHOLE_DIST.test(f.text))
|
|
204
|
+
return whole ? [{ status: "info", name: "css:size", detail: "@source scans all of Sable's dist, so every component's classes ship (about 32 KB gzipped; an app using half the catalog lands near 20 KB).", fix: "sable sources --write src/sable-sources.css, then import it in place of that @source." }] : []
|
|
203
205
|
}
|
|
204
206
|
const rel = relative(root, generated.path)
|
|
207
|
+
// The generated list only trims anything once the whole-package @source is
|
|
208
|
+
// gone: left beside it, Tailwind still scans every module, and the CSS stays
|
|
209
|
+
// the size it was.
|
|
210
|
+
const leftover = cssFiles.find((f) => f !== generated && WHOLE_DIST.test(f.text))
|
|
211
|
+
if (leftover)
|
|
212
|
+
return [{ status: "warn", name: "css:sources", detail: `${relative(root, leftover.path)} still has the whole-package @source, so ${rel} trims nothing: Tailwind scans all of Sable either way.`, fix: `Delete that @source line; ${rel} replaces it.` }]
|
|
205
213
|
const { modules, unresolved } = sableModules(pkgRoot, appSources(root))
|
|
206
214
|
if (unresolved.length && !/\/dist\/\*\*\//.test(generated.text))
|
|
207
215
|
return [{ status: "fail", name: "css:sources", detail: `${rel} lists modules, but these imports could not be traced: ${unresolved.slice(0, 4).join(", ")}.`, fix: `sable sources --write ${rel}` }]
|
package/skills/sable/SKILL.md
CHANGED
|
@@ -29,9 +29,11 @@ References inform an original direction. A reference or preset is optional.
|
|
|
29
29
|
`"reduced"`), state still changes and nothing keeps moving.
|
|
30
30
|
6. Write the reuse inventory: the Sable components, parts, utilities, and token
|
|
31
31
|
groups used, and every app-owned UI or CSS addition with the catalog search
|
|
32
|
-
that justified it. Start from the app-owned surfaces `sable check` lists
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
that justified it. Start from the app-owned surfaces `sable check` lists,
|
|
33
|
+
which are notes, not failures: for each, write one line saying you used the
|
|
34
|
+
Sable part named beside it or kept your own, and why ("composed our own
|
|
35
|
+
product tile from AspectBox + Badge: the brief wants the price over the
|
|
36
|
+
photo"). Keeping your own layout is fine. Behaviour still comes from Sable.
|
|
35
37
|
7. Record what you learned: `sable docs learning-from-builds`. Keep conditional
|
|
36
38
|
lessons with evidence; never fabricate human approval or preference records.
|
|
37
39
|
|
|
@@ -93,6 +95,15 @@ and registry `consume` block. Install from npm with the project's package
|
|
|
93
95
|
manager, for example `pnpm add @sableui/react`; no registry setup or token is
|
|
94
96
|
needed. Do not substitute an unrelated package with a similar name.
|
|
95
97
|
|
|
98
|
+
A project that already has a router, data layer or form library keeps it: Sable
|
|
99
|
+
depends on none, and its links, menus and nav take any router's link through
|
|
100
|
+
`render`. For a blank-slate app, start from Sable's suggested stack: React with
|
|
101
|
+
TypeScript on Vite, **TanStack Router** for routes and links, **TanStack Query**
|
|
102
|
+
for server data, **TanStack Form** when a form needs validation logic, and
|
|
103
|
+
**TanStack Table** once a table outgrows `DataTable` (`sable docs
|
|
104
|
+
tables-with-tanstack`). Say so in the brief, and use something else only when the
|
|
105
|
+
user or the product asks for it.
|
|
106
|
+
|
|
96
107
|
Complete setup before implementing UI:
|
|
97
108
|
|
|
98
109
|
- Match the installed package's React, React DOM, Tailwind, and Node requirements.
|
|
@@ -133,9 +144,18 @@ The installed catalog and guides are authoritative for API and token names.
|
|
|
133
144
|
|
|
134
145
|
1. Search the installed catalog for every recognizable element or behavior:
|
|
135
146
|
`sable search "<need>" --json`. Inspect the matching
|
|
136
|
-
`sable component <Name> --props` and `--examples` before using it.
|
|
137
|
-
|
|
138
|
-
|
|
147
|
+
`sable component <Name> --props` and `--examples` before using it. What a
|
|
148
|
+
result asks of you depends on what it is:
|
|
149
|
+
- **Behaviour** (controls, fields, overlays, menus, tabs, anything with focus,
|
|
150
|
+
keyboard or ARIA): always use it, never rebuild it. A native element wearing
|
|
151
|
+
a component's classes, or arrow keys handled by hand, is a reinvention even
|
|
152
|
+
if it uses valid tokens.
|
|
153
|
+
- **Parts** (Card, Entity, AspectBox, Badge, Avatar, MediaImage, layout
|
|
154
|
+
primitives): compose them freely, in whatever arrangement the design needs.
|
|
155
|
+
- **References** (patterns, blocks, and compositions marked ◇ such as
|
|
156
|
+
MediaTile): use one when its layout fits the brief. When it doesn't, compose
|
|
157
|
+
your own from parts; that is not a gap and needs no apology. Apps on Sable
|
|
158
|
+
should not all look alike.
|
|
139
159
|
2. Resolve styling in this order: Sable component/variant, Sable utility or recipe,
|
|
140
160
|
semantic token, then app-owned CSS for a genuinely uncovered need. Retrieve
|
|
141
161
|
`sable utility <name>` and `sable token <group>` rather than guessing names.
|
|
@@ -161,7 +181,12 @@ The installed catalog and guides are authoritative for API and token names.
|
|
|
161
181
|
Save a short brief in the project's existing design location: audience, primary
|
|
162
182
|
task, content, constraints, selected identity, and meaningful states, plus each
|
|
163
183
|
route's width (a Region `max`, or fluid) and why, chosen by what the person does
|
|
164
|
-
there (`sable docs building-a-page`).
|
|
184
|
+
there (`sable docs building-a-page`). Name the product's signature surfaces: the
|
|
185
|
+
one to three places its identity lives (a shop's product card and search, a music
|
|
186
|
+
app's player and album tile). Design those from the brief's direction first,
|
|
187
|
+
composing Sable's parts and behaviour; take a Sable reference for one only when
|
|
188
|
+
it already matches that direction. Everything else (forms, settings, menus,
|
|
189
|
+
tables) takes Sable's defaults. Label invented sample content. A supplied preset is an explicit input: read its exact record and
|
|
165
190
|
revision, validate it through `sable preset --check <path> --json`, and read
|
|
166
191
|
`sable docs using-presets`. Preserve its tokens and guidance unless the user requests
|
|
167
192
|
a change or the new task requires a documented adaptation.
|