@sableui/react 0.8.2 → 0.9.1
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 +1 -1
- package/CHANGELOG.md +112 -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 +46 -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 +29 -2
- 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 +50 -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 +16 -1
- package/dist/components/stepper/stepper.js +17 -3
- package/dist/components/stepper/stepper.meta.js +1 -1
- package/dist/components/toast/toast.js +3 -0
- package/dist/file-hashes.json +29 -27
- package/dist/index.d.ts +109 -22
- package/dist/index.js +1 -1
- package/dist/lib/is-promise.js +5 -0
- 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 +73 -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/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/building-with-sable.md +4 -0
- package/llms/tasks/creating-a-preset.md +4 -1
- package/llms-full.txt +103 -7
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/registry.json +89 -10
- package/scripts/grade.mjs +14 -3
- package/scripts/sources.mjs +13 -2
- package/skills/sable/SKILL.md +23 -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.1",
|
|
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",
|
|
@@ -2957,6 +2957,13 @@
|
|
|
2957
2957
|
"description": "Floating overlays — a message launcher, FAB, or modal mount points."
|
|
2958
2958
|
}
|
|
2959
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
|
+
],
|
|
2960
2967
|
"tokensUsed": [
|
|
2961
2968
|
"background",
|
|
2962
2969
|
"foreground",
|
|
@@ -2975,6 +2982,11 @@
|
|
|
2975
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>",
|
|
2976
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, …)"
|
|
2977
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
|
+
},
|
|
2978
2990
|
{
|
|
2979
2991
|
"label": "Minimal (sidebar + content only)",
|
|
2980
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>",
|
|
@@ -4922,7 +4934,12 @@
|
|
|
4922
4934
|
"name": "loading",
|
|
4923
4935
|
"type": "boolean",
|
|
4924
4936
|
"default": "false",
|
|
4925
|
-
"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)."
|
|
4926
4943
|
},
|
|
4927
4944
|
{
|
|
4928
4945
|
"name": "badge",
|
|
@@ -5354,7 +5371,7 @@
|
|
|
5354
5371
|
"name": "variant",
|
|
5355
5372
|
"type": "\"default\" | \"flat\"",
|
|
5356
5373
|
"default": "\"default\"",
|
|
5357
|
-
"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."
|
|
5358
5375
|
},
|
|
5359
5376
|
{
|
|
5360
5377
|
"name": "density",
|
|
@@ -7800,6 +7817,7 @@
|
|
|
7800
7817
|
{
|
|
7801
7818
|
"name": "DateTile",
|
|
7802
7819
|
"status": "stable",
|
|
7820
|
+
"composition": "a <time> element with the type tiers, on a surface utility",
|
|
7803
7821
|
"group": "Data display",
|
|
7804
7822
|
"register": "page",
|
|
7805
7823
|
"path": "components/date-tile",
|
|
@@ -8859,6 +8877,7 @@
|
|
|
8859
8877
|
{
|
|
8860
8878
|
"name": "Feedback",
|
|
8861
8879
|
"status": "stable",
|
|
8880
|
+
"composition": "Popover, Textarea, ToggleGroup, Button",
|
|
8862
8881
|
"group": "Overlays",
|
|
8863
8882
|
"register": "page",
|
|
8864
8883
|
"path": "components/feedback",
|
|
@@ -8972,7 +8991,7 @@
|
|
|
8972
8991
|
"group": "Forms",
|
|
8973
8992
|
"register": "either",
|
|
8974
8993
|
"path": "components/fieldset",
|
|
8975
|
-
"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.",
|
|
8976
8995
|
"keywords": [
|
|
8977
8996
|
"fieldset",
|
|
8978
8997
|
"form group",
|
|
@@ -8994,6 +9013,10 @@
|
|
|
8994
9013
|
"guidance": "dont",
|
|
8995
9014
|
"description": "Do not use Fieldset as a generic spacing wrapper outside forms."
|
|
8996
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
|
+
},
|
|
8997
9020
|
{
|
|
8998
9021
|
"guidance": "dont",
|
|
8999
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."
|
|
@@ -9034,6 +9057,12 @@
|
|
|
9034
9057
|
]
|
|
9035
9058
|
},
|
|
9036
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
|
+
},
|
|
9037
9066
|
{
|
|
9038
9067
|
"name": "disabled",
|
|
9039
9068
|
"type": "boolean",
|
|
@@ -9095,6 +9124,11 @@
|
|
|
9095
9124
|
"label": "Why a fieldset, and when it is wrong",
|
|
9096
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.",
|
|
9097
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\""
|
|
9098
9132
|
}
|
|
9099
9133
|
]
|
|
9100
9134
|
},
|
|
@@ -9575,7 +9609,12 @@
|
|
|
9575
9609
|
{
|
|
9576
9610
|
"name": "Form",
|
|
9577
9611
|
"type": "{ validationMode?: \"onSubmit\" | \"onBlur\" | \"onChange\"; errors?: Record<string, string | string[]>; onFormSubmit?: (values, details) => void; actionsRef?: RefObject<Form.Actions> } + ComponentPropsWithRef<\"form\">",
|
|
9578
|
-
"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."
|
|
9579
9618
|
},
|
|
9580
9619
|
{
|
|
9581
9620
|
"name": "validationMode",
|
|
@@ -9608,8 +9647,8 @@
|
|
|
9608
9647
|
"subcomponents": [
|
|
9609
9648
|
{
|
|
9610
9649
|
"name": "Field",
|
|
9611
|
-
"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 }",
|
|
9612
|
-
"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."
|
|
9613
9652
|
}
|
|
9614
9653
|
],
|
|
9615
9654
|
"tokensUsed": [
|
|
@@ -9633,6 +9672,11 @@
|
|
|
9633
9672
|
"code": "<Field label=\"Password\" error=\"Must be at least 8 characters.\"><Input type=\"password\" /></Field>",
|
|
9634
9673
|
"imports": "import { Field, Input } from \"@sableui/react\""
|
|
9635
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
|
+
},
|
|
9636
9680
|
{
|
|
9637
9681
|
"label": "Your own wording for a missing value",
|
|
9638
9682
|
"code": "<Form onSubmit={handleSubmit}><Field label=\"Subject\" required messages={{ valueMissing: \"Enter a subject.\" }}><Input name=\"subject\" /></Field><Button type=\"submit\">Send</Button></Form>",
|
|
@@ -11107,6 +11151,7 @@
|
|
|
11107
11151
|
{
|
|
11108
11152
|
"name": "MediaTile",
|
|
11109
11153
|
"status": "beta",
|
|
11154
|
+
"composition": "AspectBox, MediaImage, a stretched link (or Card render), Badge appearance=\"overlay\", Button",
|
|
11110
11155
|
"group": "Media & mockups",
|
|
11111
11156
|
"register": "page",
|
|
11112
11157
|
"path": "components/media-tile",
|
|
@@ -11208,11 +11253,26 @@
|
|
|
11208
11253
|
"type": "ReactNode",
|
|
11209
11254
|
"description": "The second line — an artist, a channel, a year."
|
|
11210
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
|
+
},
|
|
11211
11261
|
{
|
|
11212
11262
|
"name": "src",
|
|
11213
11263
|
"type": "string",
|
|
11214
11264
|
"description": "Cover image URL. Without one, or when it fails, the cover is GradientArt seeded by the item."
|
|
11215
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
|
+
},
|
|
11216
11276
|
{
|
|
11217
11277
|
"name": "alt",
|
|
11218
11278
|
"type": "string",
|
|
@@ -11338,6 +11398,11 @@
|
|
|
11338
11398
|
"Truncated titles keep their full text in the link's accessible name and in a `title` tooltip."
|
|
11339
11399
|
],
|
|
11340
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
|
+
},
|
|
11341
11406
|
{
|
|
11342
11407
|
"label": "Album grid",
|
|
11343
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>",
|
|
@@ -14801,6 +14866,11 @@
|
|
|
14801
14866
|
"type": "ReactNode",
|
|
14802
14867
|
"description": "Optional supporting copy beneath the title (text-body-sm, muted)."
|
|
14803
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
|
+
},
|
|
14804
14874
|
{
|
|
14805
14875
|
"name": "icon",
|
|
14806
14876
|
"type": "ReactNode",
|
|
@@ -17687,7 +17757,7 @@
|
|
|
17687
17757
|
"name": "orientation",
|
|
17688
17758
|
"type": "\"horizontal\" | \"vertical\"",
|
|
17689
17759
|
"default": "\"horizontal\"",
|
|
17690
|
-
"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."
|
|
17691
17761
|
},
|
|
17692
17762
|
{
|
|
17693
17763
|
"name": "indicator",
|
|
@@ -23874,6 +23944,7 @@
|
|
|
23874
23944
|
"iconStart",
|
|
23875
23945
|
"loading",
|
|
23876
23946
|
"loadingLabel",
|
|
23947
|
+
"onClick",
|
|
23877
23948
|
"render",
|
|
23878
23949
|
"size",
|
|
23879
23950
|
"variant"
|
|
@@ -24289,6 +24360,7 @@
|
|
|
24289
24360
|
"disabled",
|
|
24290
24361
|
"hint",
|
|
24291
24362
|
"icon",
|
|
24363
|
+
"leading",
|
|
24292
24364
|
"onSelect",
|
|
24293
24365
|
"stacked"
|
|
24294
24366
|
],
|
|
@@ -24621,6 +24693,7 @@
|
|
|
24621
24693
|
"name",
|
|
24622
24694
|
"orientation",
|
|
24623
24695
|
"required",
|
|
24696
|
+
"trailing",
|
|
24624
24697
|
"validate",
|
|
24625
24698
|
"validationDebounceTime",
|
|
24626
24699
|
"validationMode"
|
|
@@ -24628,7 +24701,8 @@
|
|
|
24628
24701
|
"Fieldset": [
|
|
24629
24702
|
"className",
|
|
24630
24703
|
"render",
|
|
24631
|
-
"style"
|
|
24704
|
+
"style",
|
|
24705
|
+
"variant"
|
|
24632
24706
|
],
|
|
24633
24707
|
"FieldsetLegend": [
|
|
24634
24708
|
"className",
|
|
@@ -24676,6 +24750,7 @@
|
|
|
24676
24750
|
"errors",
|
|
24677
24751
|
"noValidate",
|
|
24678
24752
|
"onFormSubmit",
|
|
24753
|
+
"onSubmit",
|
|
24679
24754
|
"validationMode"
|
|
24680
24755
|
],
|
|
24681
24756
|
"GlidingHighlight": [
|
|
@@ -24790,10 +24865,13 @@
|
|
|
24790
24865
|
"badge",
|
|
24791
24866
|
"href",
|
|
24792
24867
|
"loading",
|
|
24868
|
+
"meta",
|
|
24793
24869
|
"render",
|
|
24794
24870
|
"seed",
|
|
24795
24871
|
"size",
|
|
24872
|
+
"sizes",
|
|
24796
24873
|
"src",
|
|
24874
|
+
"srcSet",
|
|
24797
24875
|
"subtitle",
|
|
24798
24876
|
"subtitleHref",
|
|
24799
24877
|
"subtitleRender",
|
|
@@ -25352,6 +25430,7 @@
|
|
|
25352
25430
|
"title",
|
|
25353
25431
|
"titleClassName",
|
|
25354
25432
|
"tone",
|
|
25433
|
+
"trailing",
|
|
25355
25434
|
"value"
|
|
25356
25435
|
],
|
|
25357
25436
|
"RadioGroup": [
|
|
@@ -26377,7 +26456,7 @@
|
|
|
26377
26456
|
"name": "themeStorageKey",
|
|
26378
26457
|
"type": "string | null",
|
|
26379
26458
|
"default": "SABLE_THEME_STORAGE_KEY",
|
|
26380
|
-
"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."
|
|
26381
26460
|
},
|
|
26382
26461
|
{
|
|
26383
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,24 @@ 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
|
+
// The whole-package scan only: `dist`, `dist/` or a `dist/**` glob. A single
|
|
199
|
+
// module (`dist/components/chart/chart.js`, a lazy route's) and an exclusion
|
|
200
|
+
// (`@source not …`) are deliberate and trim nothing away.
|
|
201
|
+
const WHOLE_DIST = /@source\s+(?!not\b)["'][^"']*(?:@sableui\/react|packages\/sable)\/dist\/?(?:\*\*(?:\/[^"']*)?)?["']/
|
|
202
|
+
|
|
198
203
|
export function sourcesCheck(pkgRoot, root, cssFiles) {
|
|
199
204
|
const generated = cssFiles.find((f) => f.text.includes(SOURCES_MARKER))
|
|
200
205
|
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." }] : []
|
|
206
|
+
const whole = cssFiles.some((f) => WHOLE_DIST.test(f.text))
|
|
207
|
+
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
208
|
}
|
|
204
209
|
const rel = relative(root, generated.path)
|
|
210
|
+
// The generated list only trims anything once the whole-package @source is
|
|
211
|
+
// gone: left beside it, Tailwind still scans every module, and the CSS stays
|
|
212
|
+
// the size it was.
|
|
213
|
+
const leftover = cssFiles.find((f) => f !== generated && WHOLE_DIST.test(f.text))
|
|
214
|
+
if (leftover)
|
|
215
|
+
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
216
|
const { modules, unresolved } = sableModules(pkgRoot, appSources(root))
|
|
206
217
|
if (unresolved.length && !/\/dist\/\*\*\//.test(generated.text))
|
|
207
218
|
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
|
|
|
@@ -142,9 +144,18 @@ The installed catalog and guides are authoritative for API and token names.
|
|
|
142
144
|
|
|
143
145
|
1. Search the installed catalog for every recognizable element or behavior:
|
|
144
146
|
`sable search "<need>" --json`. Inspect the matching
|
|
145
|
-
`sable component <Name> --props` and `--examples` before using it.
|
|
146
|
-
|
|
147
|
-
|
|
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.
|
|
148
159
|
2. Resolve styling in this order: Sable component/variant, Sable utility or recipe,
|
|
149
160
|
semantic token, then app-owned CSS for a genuinely uncovered need. Retrieve
|
|
150
161
|
`sable utility <name>` and `sable token <group>` rather than guessing names.
|
|
@@ -170,7 +181,12 @@ The installed catalog and guides are authoritative for API and token names.
|
|
|
170
181
|
Save a short brief in the project's existing design location: audience, primary
|
|
171
182
|
task, content, constraints, selected identity, and meaningful states, plus each
|
|
172
183
|
route's width (a Region `max`, or fluid) and why, chosen by what the person does
|
|
173
|
-
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
|
|
174
190
|
revision, validate it through `sable preset --check <path> --json`, and read
|
|
175
191
|
`sable docs using-presets`. Preserve its tokens and guidance unless the user requests
|
|
176
192
|
a change or the new task requires a documented adaptation.
|