@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.
Files changed (50) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +112 -0
  3. package/bin/sable.mjs +26 -11
  4. package/dist/blocks/index.d.ts +1 -1
  5. package/dist/components/app-shell/app-shell.js +2 -12
  6. package/dist/components/app-shell/app-shell.meta.js +22 -0
  7. package/dist/components/app-shell/index.js +2 -0
  8. package/dist/components/app-shell/skip-link.js +28 -0
  9. package/dist/components/button/button.js +46 -8
  10. package/dist/components/button/button.meta.js +6 -1
  11. package/dist/components/card/card.meta.js +1 -1
  12. package/dist/components/command-menu/command-menu.js +29 -2
  13. package/dist/components/date-tile/date-tile.meta.js +1 -0
  14. package/dist/components/feedback/feedback.meta.js +1 -0
  15. package/dist/components/fieldset/fieldset.js +22 -13
  16. package/dist/components/fieldset/fieldset.meta.js +25 -1
  17. package/dist/components/form/form.js +50 -9
  18. package/dist/components/form/form.meta.js +12 -3
  19. package/dist/components/media-tile/media-tile.js +16 -2
  20. package/dist/components/media-tile/media-tile.meta.js +8 -0
  21. package/dist/components/radio-card/radio-card.js +6 -2
  22. package/dist/components/radio-card/radio-card.meta.js +5 -0
  23. package/dist/components/sheet/sheet.js +16 -1
  24. package/dist/components/stepper/stepper.js +17 -3
  25. package/dist/components/stepper/stepper.meta.js +1 -1
  26. package/dist/components/toast/toast.js +3 -0
  27. package/dist/file-hashes.json +29 -27
  28. package/dist/index.d.ts +109 -22
  29. package/dist/index.js +1 -1
  30. package/dist/lib/is-promise.js +5 -0
  31. package/dist/{meta-CcE_SeFu.d.ts → meta-DVG8ceze.d.ts} +9 -0
  32. package/dist/patterns/index.d.ts +1 -1
  33. package/dist/styles.css +73 -0
  34. package/llms/components/app-shell.md +26 -0
  35. package/llms/components/button.md +2 -1
  36. package/llms/components/card.md +1 -1
  37. package/llms/components/fieldset.md +21 -1
  38. package/llms/components/form.md +11 -2
  39. package/llms/components/media-tile.md +32 -0
  40. package/llms/components/radio-card.md +1 -0
  41. package/llms/components/stepper.md +1 -1
  42. package/llms/tasks/building-with-sable.md +4 -0
  43. package/llms/tasks/creating-a-preset.md +4 -1
  44. package/llms-full.txt +103 -7
  45. package/llms.txt +2 -2
  46. package/package.json +1 -1
  47. package/registry.json +89 -10
  48. package/scripts/grade.mjs +14 -3
  49. package/scripts/sources.mjs +13 -2
  50. package/skills/sable/SKILL.md +23 -7
@@ -1,7 +1,7 @@
1
1
  // src/components/fieldset/fieldset.meta.ts
2
2
  var fieldsetMeta = {
3
3
  name: "Fieldset",
4
- summary: "Form section panel (Card-like bordered surface) over Base UI Fieldset, with Legend/Description/Content/Footer parts.",
4
+ 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.',
5
5
  status: "stable",
6
6
  keywords: [
7
7
  "fieldset",
@@ -24,6 +24,10 @@ var fieldsetMeta = {
24
24
  guidance: "dont",
25
25
  description: "Do not use Fieldset as a generic spacing wrapper outside forms."
26
26
  },
27
+ {
28
+ guidance: "do",
29
+ 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.'
30
+ },
27
31
  {
28
32
  guidance: "dont",
29
33
  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."
@@ -64,6 +68,12 @@ var fieldsetMeta = {
64
68
  ]
65
69
  },
66
70
  props: [
71
+ {
72
+ name: "variant",
73
+ type: '"card" | "plain"',
74
+ default: '"card"',
75
+ 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."
76
+ },
67
77
  {
68
78
  name: "disabled",
69
79
  type: "boolean",
@@ -152,6 +162,20 @@ var fieldsetMeta = {
152
162
  // \u2717 Not for visual grouping of unrelated fields. Use PanelSection on a panel or
153
163
  // a Card section on a page; a legend that is really a heading lies to
154
164
  // assistive technology about the form's structure.`
165
+ },
166
+ {
167
+ label: "A radio group's name, without a second box",
168
+ code: `<Card>
169
+ <CardContent>
170
+ <Fieldset variant="plain">
171
+ <FieldsetLegend>Delivery</FieldsetLegend>
172
+ <RadioGroup defaultValue="standard">
173
+ <RadioCard value="standard" title="Standard" description="3\u20135 working days" trailing="Free" />
174
+ <RadioCard value="express" title="Express" description="Next working day" trailing="$12.00" />
175
+ </RadioGroup>
176
+ </Fieldset>
177
+ </CardContent>
178
+ </Card>`
155
179
  }
156
180
  ]
157
181
  };
@@ -10,6 +10,7 @@ import {
10
10
  useRef
11
11
  } from "react";
12
12
  import { cn } from "../../lib/cn.js";
13
+ import { isPromise } from "../../lib/is-promise.js";
13
14
  import { Label } from "../label/index.js";
14
15
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
15
16
  function Form({
@@ -23,16 +24,18 @@ function Form({
23
24
  ...props
24
25
  }) {
25
26
  const blockedRef = useRef(false);
27
+ const formRef = useRef(null);
26
28
  const handleSubmit = (event) => {
27
29
  blockedRef.current = false;
28
30
  const form = event.currentTarget;
31
+ formRef.current = form;
29
32
  if (!form.checkValidity()) {
30
33
  event.preventDefault();
31
34
  blockedRef.current = true;
32
35
  form.reportValidity();
33
36
  return;
34
37
  }
35
- onSubmit?.(event);
38
+ afterSubmit(form, onSubmit?.(event));
36
39
  };
37
40
  if (noValidate) {
38
41
  return /* @__PURE__ */ jsx(
@@ -40,7 +43,10 @@ function Form({
40
43
  {
41
44
  noValidate: true,
42
45
  className: cn("flex flex-col gap-6", className),
43
- onSubmit,
46
+ onSubmit: (event) => {
47
+ const form = event.currentTarget;
48
+ afterSubmit(form, onSubmit?.(event));
49
+ },
44
50
  ...props
45
51
  }
46
52
  );
@@ -54,13 +60,37 @@ function Form({
54
60
  actionsRef,
55
61
  onSubmit: handleSubmit,
56
62
  onFormSubmit: onFormSubmit ? (values, details) => {
57
- if (!blockedRef.current) onFormSubmit(values, details);
63
+ if (blockedRef.current) return;
64
+ const result = onFormSubmit(values, details);
65
+ if (formRef.current && isPromise(result)) afterSubmit(formRef.current, result);
58
66
  } : void 0,
59
67
  ...props
60
68
  }
61
69
  );
62
70
  }
63
71
  Form.displayName = "Form";
72
+ var SETTLE_FRAMES = 10;
73
+ function afterSubmit(form, result) {
74
+ const attempt = (framesLeft) => requestAnimationFrame(() => {
75
+ if (!form.isConnected) return;
76
+ const active = document.activeElement;
77
+ if (active && active !== document.body && !form.contains(active)) return;
78
+ if (active && form.contains(active) && active.getAttribute("aria-invalid") === "true") return;
79
+ const first = [...form.querySelectorAll('[aria-invalid="true"]')].find(
80
+ (el) => !(el instanceof HTMLInputElement && el.type === "hidden") && !el.closest("[aria-hidden='true'],[inert]")
81
+ );
82
+ if (!first) {
83
+ if (framesLeft > 0) attempt(framesLeft - 1);
84
+ return;
85
+ }
86
+ first.focus();
87
+ if (first instanceof HTMLInputElement && /^(?:text|search|email|tel|url|password|number)?$/.test(first.type)) {
88
+ first.select();
89
+ }
90
+ });
91
+ if (isPromise(result)) result.finally(() => attempt(SETTLE_FRAMES));
92
+ else attempt(SETTLE_FRAMES);
93
+ }
64
94
  function Field({
65
95
  label,
66
96
  description,
@@ -73,8 +103,10 @@ function Field({
73
103
  validationMode,
74
104
  validationDebounceTime,
75
105
  orientation = "vertical",
106
+ trailing,
76
107
  className,
77
- children
108
+ children,
109
+ ...props
78
110
  }) {
79
111
  const generatedId = useId();
80
112
  const child = Children.only(children);
@@ -137,22 +169,31 @@ function Field({
137
169
  validate,
138
170
  validationMode,
139
171
  validationDebounceTime,
172
+ ...props,
140
173
  children: orientation === "horizontal" ? (
141
174
  // Control beside a text column. The control sits in a box one LABEL
142
175
  // line tall (`h-[1lh]` in the label's own type step), so it centres
143
176
  // on the first line and stays there when the label wraps. Helper and
144
177
  // error text live in the label's column, not under the control.
145
- /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
146
- /* @__PURE__ */ jsx("div", { className: "flex h-[1lh] shrink-0 items-center text-label-lg", children: control }),
147
- /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1.5", children: [
178
+ // A Switch (the tallest, --spacing(6)) is taller than that line, so
179
+ // the row is padded by the difference: the control stays inside the
180
+ // Field's box, where an AccordionPanel or any clipping parent keeps
181
+ // it whole.
182
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 py-[max(0px,calc((--spacing(6)_-_1lh)/2))] text-label-lg", children: [
183
+ /* @__PURE__ */ jsx("div", { className: "flex h-[1lh] shrink-0 items-center", children: control }),
184
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [
148
185
  labelNode,
149
186
  descriptionNode,
150
187
  errorNode
151
- ] })
188
+ ] }),
189
+ trailing != null ? /* @__PURE__ */ jsx("div", { className: "flex h-[1lh] shrink-0 items-center", children: trailing }) : null
152
190
  ] })
153
191
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
154
192
  labelNode,
155
- control,
193
+ trailing != null ? /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
194
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: control }),
195
+ trailing
196
+ ] }) : control,
156
197
  descriptionNode,
157
198
  errorNode
158
199
  ] })
@@ -72,7 +72,12 @@ var formMeta = {
72
72
  {
73
73
  name: "Form",
74
74
  type: '{ validationMode?: "onSubmit" | "onBlur" | "onChange"; errors?: Record<string, string | string[]>; onFormSubmit?: (values, details) => void; actionsRef?: RefObject<Form.Actions> } + ComponentPropsWithRef<"form">',
75
- 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.'
75
+ 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.'
76
+ },
77
+ {
78
+ name: "onSubmit",
79
+ type: "(event: FormEvent<HTMLFormElement>) => unknown",
80
+ 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."
76
81
  },
77
82
  {
78
83
  name: "validationMode",
@@ -105,8 +110,8 @@ var formMeta = {
105
110
  subcomponents: [
106
111
  {
107
112
  name: "Field",
108
- 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<\u2026>; validationMode?: "onSubmit" | "onBlur" | "onChange"; validationDebounceTime?: number; orientation?: "vertical" | "horizontal"; className?: string; children: ReactElement }',
109
- 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\u2026) 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 \u2192 control); "horizontal" places control then label inline for a single checkbox / switch / radio \u2014 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.`
113
+ 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<\u2026>; validationMode?: "onSubmit" | "onBlur" | "onChange"; validationDebounceTime?: number; orientation?: "vertical" | "horizontal"; trailing?: ReactNode; className?: string; children: ReactElement } + other div props on the root',
114
+ 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\u2026) 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 \u2192 control); "horizontal" places control then label inline for a single checkbox / switch / radio \u2014 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.`
110
115
  }
111
116
  ],
112
117
  tokensUsed: ["muted-foreground", "destructive"],
@@ -125,6 +130,10 @@ var formMeta = {
125
130
  label: "Field with an error",
126
131
  code: '<Field label="Password" error="Must be at least 8 characters."><Input type="password" /></Field>'
127
132
  },
133
+ {
134
+ label: "An input with its action",
135
+ code: '<Field label="Promo code" error={promoError} trailing={<Button variant="secondary" onClick={applyPromo}>Apply</Button>}><Input name="promo" autoCapitalize="characters" /></Field>'
136
+ },
128
137
  {
129
138
  label: "Your own wording for a missing value",
130
139
  code: '<Form onSubmit={handleSubmit}><Field label="Subject" required messages={{ valueMissing: "Enter a subject." }}><Input name="subject" /></Field><Button type="submit">Send</Button></Form>'
@@ -28,7 +28,10 @@ function TileLink({
28
28
  function MediaTile({
29
29
  title,
30
30
  subtitle,
31
+ meta,
31
32
  src,
33
+ srcSet,
34
+ sizes,
32
35
  alt,
33
36
  seed,
34
37
  aspect = "square",
@@ -84,7 +87,18 @@ function MediaTile({
84
87
  ),
85
88
  children: subtitle
86
89
  }
87
- ) : /* @__PURE__ */ jsx("p", { className: mediaTileSubtitleVariants({ size }), children: subtitle }) : null
90
+ ) : /* @__PURE__ */ jsx("p", { className: mediaTileSubtitleVariants({ size }), children: subtitle }) : null,
91
+ meta != null ? /* @__PURE__ */ jsx(
92
+ "div",
93
+ {
94
+ "data-slot": "media-tile-meta",
95
+ className: cn(
96
+ mediaTileSubtitleVariants({ size }),
97
+ "flex flex-wrap items-center gap-x-2 gap-y-1 overflow-visible whitespace-normal"
98
+ ),
99
+ children: meta
100
+ }
101
+ ) : null
88
102
  ] }),
89
103
  /* @__PURE__ */ jsxs(
90
104
  AspectBox,
@@ -93,7 +107,7 @@ function MediaTile({
93
107
  "data-slot": "media-tile-cover",
94
108
  className: "order-first rounded-(--media-tile-radius)",
95
109
  children: [
96
- /* @__PURE__ */ jsx(MediaImage, { src, alt, seed: seed ?? titleText, loading }),
110
+ /* @__PURE__ */ jsx(MediaImage, { src, srcSet, sizes, alt, seed: seed ?? titleText, loading }),
97
111
  linked || action != null ? (
98
112
  // The tile answers the pointer and the keyboard alike: a light scrim
99
113
  // deepens the cover so the action reads over any artwork. Hover only
@@ -3,6 +3,7 @@ var mediaTileMeta = {
3
3
  name: "MediaTile",
4
4
  summary: "A cover-first tile \u2014 square, 16:9 or poster artwork with an optional play-style action over it, then a title and a subtitle that can each be links \u2014 that is one link as a whole without nesting its other controls inside one.",
5
5
  status: "beta",
6
+ composition: 'AspectBox, MediaImage, a stretched link (or Card render), Badge appearance="overlay", Button',
6
7
  keywords: [
7
8
  "album",
8
9
  "album art",
@@ -63,7 +64,10 @@ var mediaTileMeta = {
63
64
  props: [
64
65
  { name: "title", type: "ReactNode", description: "The item's name. Seeds the fallback art when it is a string." },
65
66
  { name: "subtitle", type: "ReactNode", description: "The second line \u2014 an artist, a channel, a year." },
67
+ { name: "meta", type: "ReactNode", 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." },
66
68
  { name: "src", type: "string", description: "Cover image URL. Without one, or when it fails, the cover is GradientArt seeded by the item." },
69
+ { name: "srcSet", type: "string", description: "Responsive cover candidates, passed to the `<img>`." },
70
+ { name: "sizes", type: "string", description: 'The cover\'s layout width for `srcSet`, e.g. `"(min-width: 64rem) 25vw, 50vw"`.' },
67
71
  { name: "alt", type: "string", default: '""', description: "Cover alt text. Empty by default: the title below already names the item." },
68
72
  { name: "seed", type: "string | number", description: "Seeds the fallback art. Defaults to `title` when it is a string." },
69
73
  {
@@ -110,6 +114,10 @@ var mediaTileMeta = {
110
114
  "Truncated titles keep their full text in the link's accessible name and in a `title` tooltip."
111
115
  ],
112
116
  examples: [
117
+ {
118
+ label: "Product tile",
119
+ code: '<Grid minItemWidth="14rem" gap={4}>\n {products.map((product) => (\n <MediaTile\n key={product.id}\n aspect={4 / 5}\n title={product.name}\n subtitle={product.material}\n src={product.image}\n srcSet={product.imageSet}\n sizes="(min-width: 64rem) 25vw, 50vw"\n render={<Link to={`/products/${product.slug}`} />}\n badge={product.isNew ? <Badge appearance="overlay">New</Badge> : undefined}\n meta={\n <>\n <span className="text-foreground">{formatPrice(product.price)}</span>\n <span>{product.finishes.length} finishes</span>\n </>\n }\n />\n ))}\n</Grid>'
120
+ },
113
121
  {
114
122
  label: "Album grid",
115
123
  code: '<Grid minItemWidth="10rem" gap={3}>\n {albums.map((album) => (\n <MediaTile\n key={album.id}\n title={album.title}\n subtitle={album.artist}\n src={album.cover}\n render={<Link to={`/albums/${album.id}`} />}\n subtitleRender={<Link to={`/artists/${album.artistId}`} />}\n action={\n <Button size="icon" aria-label={`Play ${album.title}`} onClick={() => play(album)}>\n <Icon name="play" variant="solid" />\n </Button>\n }\n />\n ))}\n</Grid>'
@@ -15,6 +15,7 @@ function RadioCard({
15
15
  className,
16
16
  title,
17
17
  description,
18
+ trailing,
18
19
  icon,
19
20
  tone = "neutral",
20
21
  titleClassName,
@@ -40,8 +41,11 @@ function RadioCard({
40
41
  )
41
42
  }
42
43
  );
43
- const content = /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col gap-0.5", children: [
44
- /* @__PURE__ */ jsx("span", { className: cn("text-title-sm", titleClassName), children: title }),
44
+ const content = /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
45
+ trailing != null ? /* @__PURE__ */ jsxs("span", { className: "flex items-baseline justify-between gap-3", children: [
46
+ /* @__PURE__ */ jsx("span", { className: cn("min-w-0 text-title-sm", titleClassName), children: title }),
47
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-body-md text-foreground tabular-nums", children: trailing })
48
+ ] }) : /* @__PURE__ */ jsx("span", { className: cn("text-title-sm", titleClassName), children: title }),
45
49
  description ? /* @__PURE__ */ jsx(
46
50
  "span",
47
51
  {
@@ -65,6 +65,11 @@ var radioCardMeta = {
65
65
  type: "ReactNode",
66
66
  description: "Optional supporting copy beneath the title (text-body-sm, muted)."
67
67
  },
68
+ {
69
+ name: "trailing",
70
+ type: "ReactNode",
71
+ description: "End-aligned on the title's line: a price, a delivery date, a count. The title takes the rest of the row."
72
+ },
68
73
  {
69
74
  name: "icon",
70
75
  type: "ReactNode",
@@ -42,7 +42,21 @@ function SheetContent({
42
42
  reportSide?.(resolvedSide);
43
43
  }, [reportSide, resolvedSide]);
44
44
  const popupRef = useRef(null);
45
- const mergedRef = useMemo(() => mergeRefs(popupRef, ref), [ref]);
45
+ const [popup, setPopup] = useState(null);
46
+ const mergedRef = useMemo(() => mergeRefs(popupRef, ref, setPopup), [ref]);
47
+ useIsomorphicLayoutEffect(() => {
48
+ if (!popup || resolvedSide !== "left" && resolvedSide !== "right") return;
49
+ const root = document.documentElement;
50
+ const name = `--sable-sheet-${resolvedSide}-w`;
51
+ const publish = () => root.style.setProperty(name, `${popup.offsetWidth}px`);
52
+ publish();
53
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(publish);
54
+ observer?.observe(popup);
55
+ return () => {
56
+ observer?.disconnect();
57
+ root.style.removeProperty(name);
58
+ };
59
+ }, [popup, resolvedSide]);
46
60
  return /* @__PURE__ */ jsxs(SheetPortal, { children: [
47
61
  /* @__PURE__ */ jsx(Base.Backdrop, { className: "fixed inset-0 z-(--z-overlay) bg-overlay/(--scrim-alpha) opacity-[calc(1_-_var(--drawer-swipe-progress))] motion-fade data-[swiping]:transition-none" }),
48
62
  /* @__PURE__ */ jsx(Base.Viewport, { className: "fixed inset-0 z-(--z-overlay)", children: /* @__PURE__ */ jsx(
@@ -50,6 +64,7 @@ function SheetContent({
50
64
  {
51
65
  ref: mergedRef,
52
66
  initialFocus: initialFocus === void 0 ? (type) => type === "touch" ? popupRef.current : true : initialFocus,
67
+ "data-sable-sheet": resolvedSide,
53
68
  className: cn(sheetVariants({ side }), className),
54
69
  ...props,
55
70
  children: /* @__PURE__ */ jsx(Base.Content, { className: "contents", children: /* @__PURE__ */ jsx(PresentationProvider, { value: resolvedSide === "bottom" ? "sheet" : "page", children }) })
@@ -33,6 +33,10 @@ function Stepper({
33
33
  }) {
34
34
  const items = Children.toArray(children).filter(isValidElement);
35
35
  const labels = { ...DEFAULT_STATUS_LABELS, ...statusLabels };
36
+ const hasCurrent = items.some((child, index) => {
37
+ const own = child.props.status;
38
+ return (own ?? (index < current ? "complete" : index === current ? "current" : "upcoming")) === "current";
39
+ });
36
40
  return /* @__PURE__ */ jsx(
37
41
  "ol",
38
42
  {
@@ -41,7 +45,8 @@ function Stepper({
41
45
  // isolate: the indicators' z-10 (above the connector) stays inside the
42
46
  // stepper instead of rising over sticky headers and nav bars.
43
47
  "isolate flex w-full",
44
- orientation === "horizontal" ? "flex-row items-start gap-3" : "flex-col",
48
+ // A container, so the steps compact on the stepper's own width.
49
+ orientation === "horizontal" ? "@container/stepper flex-row items-start gap-3" : "flex-col",
45
50
  className
46
51
  ),
47
52
  ...props,
@@ -54,7 +59,8 @@ function Stepper({
54
59
  current,
55
60
  orientation,
56
61
  indicator,
57
- statusLabels: labels
62
+ statusLabels: labels,
63
+ hasCurrent
58
64
  },
59
65
  children: child
60
66
  },
@@ -83,6 +89,7 @@ function StepperItem({
83
89
  const isLast = index === count - 1;
84
90
  const isCurrent = index === current;
85
91
  const statusLabel = (ctx?.statusLabels ?? DEFAULT_STATUS_LABELS)[status];
92
+ const hasCurrent = ctx?.hasCurrent ?? true;
86
93
  const body = /* @__PURE__ */ jsxs(Fragment, { children: [
87
94
  dot ? (
88
95
  // One title line tall (the step carries the title's type), so the dot
@@ -94,7 +101,14 @@ function StepperItem({
94
101
  {
95
102
  className: cn(
96
103
  "flex min-w-0 flex-col gap-0.5 text-start",
97
- !dot && "min-h-(--control-xs) justify-center"
104
+ !dot && "min-h-(--control-xs) justify-center",
105
+ // Narrow and horizontal: three two-word titles wrapped under their
106
+ // connectors at 390px. The current step keeps its title; the rest
107
+ // are discs on screen and full titles to a screen reader.
108
+ // Keyed on the resolved status, not the index: a flow driven by each
109
+ // step's `status`, or finished (`current` past the end), still keeps
110
+ // the right title, or every title when no step is current.
111
+ orientation === "horizontal" && status !== "current" && hasCurrent && "@max-md/stepper:sr-only"
98
112
  ),
99
113
  children: [
100
114
  /* @__PURE__ */ jsxs(
@@ -73,7 +73,7 @@ var stepperMeta = {
73
73
  name: "orientation",
74
74
  type: '"horizontal" | "vertical"',
75
75
  default: '"horizontal"',
76
- description: "Horizontal for a short flow across the top of a form; vertical for long flows, narrow columns or steps with descriptions."
76
+ 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."
77
77
  },
78
78
  {
79
79
  name: "indicator",
@@ -103,6 +103,8 @@ function ToastViewport({
103
103
  return /* @__PURE__ */ jsx(ToastViewportContext.Provider, { value: { position, stacked }, children: /* @__PURE__ */ jsx(ToastPortal, { children: /* @__PURE__ */ jsx(
104
104
  Base.Viewport,
105
105
  {
106
+ "data-sable-toast-viewport": "",
107
+ "data-position": position,
106
108
  className: cn(
107
109
  "fixed z-(--z-toast) w-95 max-w-[90vw] outline-none",
108
110
  // Stacked toasts are positioned against the viewport, not laid out in it.
@@ -119,6 +121,7 @@ function ToastRoot({ className, ...props }) {
119
121
  return /* @__PURE__ */ jsx(
120
122
  Base.Root,
121
123
  {
124
+ "data-sable-toast-stacked": stacked ? "" : void 0,
122
125
  className: cn(
123
126
  "flex w-full gap-3 rounded-md border border-border-floating bg-popover p-4 text-popover-foreground shadow-xl",
124
127
  stacked ? [STACKED_ROOT, STACKED_EDGE[edgeOf(position)]] : ENTER_MOTION[position],
@@ -24,7 +24,7 @@
24
24
  "dist/blocks/goal-progress-block.meta.js": "3e5b376bf58bdb4b",
25
25
  "dist/blocks/hero-block.js": "ac74e80d4d7d2ad9",
26
26
  "dist/blocks/hero-block.meta.js": "2dd2e5be2472a563",
27
- "dist/blocks/index.d.ts": "3a17da01eaa6b5f4",
27
+ "dist/blocks/index.d.ts": "7dc3a0506334de38",
28
28
  "dist/blocks/index.js": "8116f4581aad860e",
29
29
  "dist/blocks/invite-team-block.js": "be4df15518252535",
30
30
  "dist/blocks/invite-team-block.meta.js": "fd57b632f0f32276",
@@ -89,9 +89,10 @@
89
89
  "dist/components/app-header/app-header.meta.js": "708f75abae1bb376",
90
90
  "dist/components/app-header/index.js": "012804273e56f06d",
91
91
  "dist/components/app-shell/app-shell-dock.js": "1038582b11567a8a",
92
- "dist/components/app-shell/app-shell.js": "983f2c34bf5e8d75",
93
- "dist/components/app-shell/app-shell.meta.js": "d7fc13df89305638",
94
- "dist/components/app-shell/index.js": "54cfab2dbacb0216",
92
+ "dist/components/app-shell/app-shell.js": "88d2d0d1f4e11374",
93
+ "dist/components/app-shell/app-shell.meta.js": "77c4bc3e2fa16ec0",
94
+ "dist/components/app-shell/index.js": "976c14a186f290ec",
95
+ "dist/components/app-shell/skip-link.js": "59f149e155942e69",
95
96
  "dist/components/area-chart/area-chart.js": "ddcc0a0f65328dea",
96
97
  "dist/components/area-chart/area-chart.meta.js": "ba9ac76647282b0c",
97
98
  "dist/components/area-chart/index.js": "51abe421742a4b41",
@@ -127,15 +128,15 @@
127
128
  "dist/components/bubble/bubble.meta.js": "2202d4226cf79eb6",
128
129
  "dist/components/bubble/bubble.variants.js": "b1eefabd9f0bb425",
129
130
  "dist/components/bubble/index.js": "dfdc5326d5050d12",
130
- "dist/components/button/button.js": "66868eaaaed21718",
131
- "dist/components/button/button.meta.js": "dca1d77177e5d2aa",
131
+ "dist/components/button/button.js": "95d4cea99b77a48f",
132
+ "dist/components/button/button.meta.js": "aa62fe875a12ac8a",
132
133
  "dist/components/button/button.variants.js": "d3054d6389644df0",
133
134
  "dist/components/button/index.js": "1ef5e4068111496a",
134
135
  "dist/components/calendar/calendar.js": "91321f1ea8843a11",
135
136
  "dist/components/calendar/calendar.meta.js": "46565db6017d2706",
136
137
  "dist/components/calendar/index.js": "24c1d513b60c972a",
137
138
  "dist/components/card/card.js": "0b864f24d922a0c9",
138
- "dist/components/card/card.meta.js": "e2b8107ece1ca7ca",
139
+ "dist/components/card/card.meta.js": "2cee6606ed8720cc",
139
140
  "dist/components/card/index.js": "d6b4b33cd2a0c862",
140
141
  "dist/components/carousel/carousel.js": "09aef818b8130d57",
141
142
  "dist/components/carousel/carousel.meta.js": "d45f0086b2e40632",
@@ -169,7 +170,7 @@
169
170
  "dist/components/combobox/combobox.meta.js": "38ef70bf96a3cf87",
170
171
  "dist/components/combobox/index.js": "f8f9863cf4f7e59b",
171
172
  "dist/components/command-menu/command-menu-trigger.js": "30b1606fb611c758",
172
- "dist/components/command-menu/command-menu.js": "c126d5a07b936311",
173
+ "dist/components/command-menu/command-menu.js": "3478c25118653d88",
173
174
  "dist/components/command-menu/command-menu.meta.js": "dde6c79e36a55f25",
174
175
  "dist/components/command-menu/index.js": "c49dd188ea788ba5",
175
176
  "dist/components/context-card/context-card.js": "ab28de9684e32eea",
@@ -185,7 +186,7 @@
185
186
  "dist/components/date-picker/date-picker.meta.js": "a56f356cf633d7e8",
186
187
  "dist/components/date-picker/index.js": "44e1227972891934",
187
188
  "dist/components/date-tile/date-tile.js": "7458bf733bc64ab2",
188
- "dist/components/date-tile/date-tile.meta.js": "c3e119d069bf66e6",
189
+ "dist/components/date-tile/date-tile.meta.js": "0bc0081c8cb73535",
189
190
  "dist/components/date-tile/index.js": "684eef3a72b11651",
190
191
  "dist/components/destructive-action-modal/destructive-action-modal.js": "3806d39cb3fb0cdf",
191
192
  "dist/components/destructive-action-modal/destructive-action-modal.meta.js": "e81a94de90f8eccf",
@@ -204,10 +205,10 @@
204
205
  "dist/components/entity/entity.variants.js": "8838fb5cf6f304f1",
205
206
  "dist/components/entity/index.js": "1397602aa7068da8",
206
207
  "dist/components/feedback/feedback.js": "af27a7be7d8e0552",
207
- "dist/components/feedback/feedback.meta.js": "286f3b2331e02636",
208
+ "dist/components/feedback/feedback.meta.js": "a37f92831e6e24f2",
208
209
  "dist/components/feedback/index.js": "fd1ab40c9d879286",
209
- "dist/components/fieldset/fieldset.js": "b3618d9987ad4dcb",
210
- "dist/components/fieldset/fieldset.meta.js": "84842564127c6629",
210
+ "dist/components/fieldset/fieldset.js": "08d02302b6866f8e",
211
+ "dist/components/fieldset/fieldset.meta.js": "64be6d633de27e04",
211
212
  "dist/components/fieldset/index.js": "9def974be7511a5c",
212
213
  "dist/components/file-dropzone/file-dropzone.js": "668f2325feba6b6d",
213
214
  "dist/components/file-dropzone/file-dropzone.meta.js": "6f7f0601319576ae",
@@ -216,8 +217,8 @@
216
217
  "dist/components/font-picker/font-picker.js": "1d76108197a03759",
217
218
  "dist/components/font-picker/font-picker.meta.js": "846401404a0e03c5",
218
219
  "dist/components/font-picker/index.js": "eb8b97fcda8ff592",
219
- "dist/components/form/form.js": "3b6497305d155da8",
220
- "dist/components/form/form.meta.js": "071442ffd103c43c",
220
+ "dist/components/form/form.js": "af2fa2d3cb54d125",
221
+ "dist/components/form/form.meta.js": "1e377418406a019d",
221
222
  "dist/components/form/index.js": "6c3047e437b10fba",
222
223
  "dist/components/gliding-highlight/gliding-highlight.js": "c424c85af1cc5665",
223
224
  "dist/components/gliding-highlight/gliding-highlight.meta.js": "4aecad0e1aabea90",
@@ -253,8 +254,8 @@
253
254
  "dist/components/media-image/media-image.js": "f68499e9f84b6e64",
254
255
  "dist/components/media-image/media-image.meta.js": "0778fd790511a701",
255
256
  "dist/components/media-tile/index.js": "00281f6f8cc246d1",
256
- "dist/components/media-tile/media-tile.js": "9396be50ea93808d",
257
- "dist/components/media-tile/media-tile.meta.js": "2229f0c98e1cfea8",
257
+ "dist/components/media-tile/media-tile.js": "faf79c27ec15c8d4",
258
+ "dist/components/media-tile/media-tile.meta.js": "6a21970f9b463812",
258
259
  "dist/components/media-tile/media-tile.variants.js": "e22c08004b4a55ad",
259
260
  "dist/components/menu/index.js": "44a0f949be1272fb",
260
261
  "dist/components/menu/menu.js": "55d24cd48752830a",
@@ -333,8 +334,8 @@
333
334
  "dist/components/radio/radio.js": "dce8ff6dd0deaa5e",
334
335
  "dist/components/radio/radio.meta.js": "0b0419bde53f857d",
335
336
  "dist/components/radio-card/index.js": "ff815b13ce46e2bb",
336
- "dist/components/radio-card/radio-card.js": "4f8c35a3b44b67e6",
337
- "dist/components/radio-card/radio-card.meta.js": "3941f904085e471b",
337
+ "dist/components/radio-card/radio-card.js": "29c459d88339f035",
338
+ "dist/components/radio-card/radio-card.meta.js": "7370c85b8f32ffad",
338
339
  "dist/components/radio-card/radio-card.variants.js": "86778fe9e4888cd1",
339
340
  "dist/components/relative-time/index.js": "02e9b7b91ccad470",
340
341
  "dist/components/relative-time/relative-time.js": "db5cd286b36dee74",
@@ -366,7 +367,7 @@
366
367
  "dist/components/settings-menu/settings-menu.meta.js": "46bdda712ce13d85",
367
368
  "dist/components/settings-menu/settings-menu.variants.js": "1eefc629793ed7b3",
368
369
  "dist/components/sheet/index.js": "b19aacba55e7de80",
369
- "dist/components/sheet/sheet.js": "131b7f06ec9a35c9",
370
+ "dist/components/sheet/sheet.js": "61e0c11831dfa940",
370
371
  "dist/components/sheet/sheet.meta.js": "28cd51d5ba9872db",
371
372
  "dist/components/sheet/sheet.variants.js": "672d2fd7f746bd57",
372
373
  "dist/components/show-more/index.js": "e7c7e37ffedde2c2",
@@ -399,8 +400,8 @@
399
400
  "dist/components/status-dot/status-dot.meta.js": "90200e97feb25c97",
400
401
  "dist/components/status-dot/status-dot.variants.js": "d65605a0376e7d3b",
401
402
  "dist/components/stepper/index.js": "2346537b50c95017",
402
- "dist/components/stepper/stepper.js": "4bcb5ea9b5515449",
403
- "dist/components/stepper/stepper.meta.js": "2e9cb9935eb21282",
403
+ "dist/components/stepper/stepper.js": "8e1bdfc654e5aa3e",
404
+ "dist/components/stepper/stepper.meta.js": "6d17c7521e2aa31e",
404
405
  "dist/components/stepper/stepper.variants.js": "3c343c6e38e39d80",
405
406
  "dist/components/streaming-text/index.js": "c46856aa7c0718f5",
406
407
  "dist/components/streaming-text/streaming-text.js": "e450c4d5e312a6c5",
@@ -428,7 +429,7 @@
428
429
  "dist/components/thinking-orb/thinking-orb.meta.js": "dcc1982c0f9e9d01",
429
430
  "dist/components/thinking-orb/thinking-orb.render.js": "1d561ac1ffcd1e97",
430
431
  "dist/components/toast/index.js": "d13be0a6c335931d",
431
- "dist/components/toast/toast.js": "23041aac9b57a88b",
432
+ "dist/components/toast/toast.js": "f58c5e39eb3158a3",
432
433
  "dist/components/toast/toast.meta.js": "493dec13b784b6bc",
433
434
  "dist/components/toggle/index.js": "84a0a4bf596a1b7d",
434
435
  "dist/components/toggle/toggle.js": "90d0deb9c6af0d61",
@@ -470,8 +471,8 @@
470
471
  "dist/icon-sets/brands.js": "9a37d8739f14f2a9",
471
472
  "dist/icon-sets/reicon.d.ts": "52f6e41dc8137b4e",
472
473
  "dist/icon-sets/reicon.js": "89a930f17cc90cde",
473
- "dist/index.d.ts": "5412b556b1d05aa9",
474
- "dist/index.js": "3f9d4da26572073f",
474
+ "dist/index.d.ts": "b3c6e9a4124867da",
475
+ "dist/index.js": "b7e51e26dc0b1e6c",
475
476
  "dist/ios/chrome.css": "a7ca9e3735d8658e",
476
477
  "dist/ios/chrome.js": "37091fef7b68bd28",
477
478
  "dist/ios/frame.css": "dd2703c944ce575a",
@@ -524,6 +525,7 @@
524
525
  "dist/lib/heading-row.js": "ef1faf4955e0fec6",
525
526
  "dist/lib/highlight-glide.js": "04a077a17dad7e52",
526
527
  "dist/lib/hit-target.js": "b2ad19bc6d3271e0",
528
+ "dist/lib/is-promise.js": "cc36f1bd7d4d4461",
527
529
  "dist/lib/meta.js": "e3b0c44298fc1c14",
528
530
  "dist/lib/portal-theme-scope.js": "2c0d22d961e51174",
529
531
  "dist/lib/presentation.js": "8e4f69cd7205bd55",
@@ -540,7 +542,7 @@
540
542
  "dist/lib/use-resize-observer.js": "4f08e280d64be22c",
541
543
  "dist/lib/use-scroll-activity.js": "793394a8c952e697",
542
544
  "dist/lib/use-wheel-to-horizontal.js": "5f886b1033db9129",
543
- "dist/meta-CcE_SeFu.d.ts": "c1a10b5cf75438de",
545
+ "dist/meta-DVG8ceze.d.ts": "60cdb0adc726bc03",
544
546
  "dist/motion/MotionProvider.js": "feeab4e6ce816a5e",
545
547
  "dist/motion/Presence.js": "21fc6f1c68207718",
546
548
  "dist/motion/Reveal.js": "b80396095aa2de96",
@@ -559,7 +561,7 @@
559
561
  "dist/patterns/dashboard-page.meta.js": "da27758c17fe15a2",
560
562
  "dist/patterns/feed-page.js": "0fabf456c32dbbad",
561
563
  "dist/patterns/feed-page.meta.js": "ab079074b87ad23b",
562
- "dist/patterns/index.d.ts": "ed15cd27eeb74f4f",
564
+ "dist/patterns/index.d.ts": "7511ee8632e26bbf",
563
565
  "dist/patterns/index.js": "3ff5fabb1afbf8dc",
564
566
  "dist/patterns/landing-page.js": "4f3294839ac36cd6",
565
567
  "dist/patterns/landing-page.meta.js": "95d34273ee999eea",
@@ -570,7 +572,7 @@
570
572
  "dist/presets/index.d.ts": "143528fa9cbec492",
571
573
  "dist/presets/index.js": "51babe07a2424380",
572
574
  "dist/registry-CyL28OzJ.d.ts": "b068fc89571517d8",
573
- "dist/styles.css": "ce0dc2926e952ef3",
575
+ "dist/styles.css": "5a8e45c86522c82f",
574
576
  "dist/theme-provider-S0NVJs2Y.d.ts": "ae7728dddd10f563",
575
577
  "dist/themes/_contract.js": "cacc81c9295d55a2",
576
578
  "dist/themes/atelier/atelier.css": "e693fec4a6ae71a8",