@sableui/react 0.5.0 → 0.6.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 (82) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +148 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/THIRD_PARTY_NOTICES.md +11 -0
  5. package/bin/sable-lint.mjs +2 -1
  6. package/bin/sable.mjs +3 -2
  7. package/dist/{_contract-Cn3lUpih.d.ts → _contract-C2M1SjBf.d.ts} +20 -2
  8. package/dist/blocks/index.js +19 -51
  9. package/dist/{chunk-RCLKXF2N.js → chunk-7QSDYBXO.js} +610 -30
  10. package/dist/{chunk-GU3T7SBA.js → chunk-EDXLM7YL.js} +1 -1
  11. package/dist/chunk-FKZXAFIY.js +12 -0
  12. package/dist/{chunk-OQACYT2D.js → chunk-NETUFI34.js} +23 -1
  13. package/dist/{chunk-ZBZMKOO7.js → chunk-VVW4UCDI.js} +3 -3
  14. package/dist/foundations/index.js +6 -0
  15. package/dist/index.d.ts +127 -11
  16. package/dist/index.js +23 -13
  17. package/dist/ios/index.d.ts +6 -1
  18. package/dist/ios/index.js +4 -3
  19. package/dist/ios/styles.css +9 -0
  20. package/dist/patterns/index.js +22 -28
  21. package/dist/presets/index.d.ts +108 -5
  22. package/dist/presets/index.js +239 -6
  23. package/dist/styles.css +50 -0
  24. package/dist/{theme-provider-CGeiusgu.d.ts → theme-provider-kQszHYji.d.ts} +1 -1
  25. package/dist/themes/atelier/atelier.css +31 -3
  26. package/dist/themes/atelier/index.d.ts +1 -1
  27. package/dist/themes/ember/ember.css +38 -7
  28. package/dist/themes/ember/index.d.ts +1 -1
  29. package/dist/themes/index.d.ts +2 -2
  30. package/dist/themes/meridian/index.d.ts +1 -1
  31. package/dist/themes/meridian/meridian.css +33 -4
  32. package/dist/themes/poppy/index.d.ts +1 -1
  33. package/dist/themes/poppy/poppy.css +28 -0
  34. package/dist/themes/prism/index.d.ts +1 -1
  35. package/dist/themes/prism/prism.css +26 -0
  36. package/dist/themes/sable/index.d.ts +1 -1
  37. package/dist/themes/sable/sable.css +24 -11
  38. package/dist/themes/verdant/index.d.ts +1 -1
  39. package/dist/themes/verdant/verdant.css +34 -4
  40. package/llms/blocks/danger-zone-block.md +1 -1
  41. package/llms/blocks/event-card-block.md +1 -1
  42. package/llms/blocks/feedback-form-block.md +1 -1
  43. package/llms/blocks/invite-team-block.md +1 -1
  44. package/llms/blocks/media-player-block.md +3 -16
  45. package/llms/blocks/notification-list-block.md +3 -3
  46. package/llms/blocks/product-card-block.md +3 -17
  47. package/llms/blocks/profile-card-block.md +4 -17
  48. package/llms/blocks/profile-form-block.md +2 -2
  49. package/llms/blocks/team-members-block.md +1 -1
  50. package/llms/blocks/testimonial-block.md +1 -1
  51. package/llms/components/avatar.md +13 -1
  52. package/llms/components/card.md +1 -1
  53. package/llms/components/gradient-art.md +80 -0
  54. package/llms/components/sheet.md +1 -1
  55. package/llms/foundations/accessibility.md +1 -0
  56. package/llms/patterns/auth-page.md +5 -19
  57. package/llms/patterns/brand-landing-page.md +1 -1
  58. package/llms/patterns/chat-panel.md +1 -1
  59. package/llms/patterns/dashboard-page.md +4 -4
  60. package/llms/patterns/feed-page.md +4 -4
  61. package/llms/patterns/landing-page.md +1 -1
  62. package/llms/patterns/list-detail-page.md +3 -2
  63. package/llms/patterns/settings-page.md +3 -3
  64. package/llms/reference/token-intent.md +9 -1
  65. package/llms/tasks/auditing-an-app.md +6 -0
  66. package/llms/tasks/building-an-ios-prototype.md +3 -1
  67. package/llms/tasks/converting-an-app.md +1 -1
  68. package/llms/tasks/creating-a-preset.md +24 -0
  69. package/llms/tasks/reviewing-a-design.md +9 -0
  70. package/llms/tasks/using-presets.md +6 -1
  71. package/llms-full.txt +201 -104
  72. package/llms.txt +3 -2
  73. package/package.json +9 -7
  74. package/registry.json +283 -30
  75. package/scripts/design-presets.mjs +27 -4
  76. package/scripts/grade.mjs +2 -1
  77. package/scripts/label-contrast.mjs +174 -0
  78. package/scripts/lint-rules.mjs +39 -2
  79. package/skills/sable/SKILL.md +3 -1
  80. package/targets/ios.json +18 -2
  81. package/third-party/paper-shaders-LICENSE.txt +201 -0
  82. package/third-party/paper-shaders-NOTICE.txt +5 -0
package/AGENTS.md CHANGED
@@ -45,11 +45,11 @@ These are short on purpose. Each links to a foundations doc with the full vocabu
45
45
  6. **Motion has a reason and a recipe.** Apply a named `motion-*` recipe; drop to `duration-(--motion-duration-*)` + `ease-*` tokens only when none fits. Never Tailwind's `animate-*`, never a raw `duration-150`. Exits are faster than entrances and never overshoot. → `sable docs motion`
46
46
  7. **Icons by semantic name.** `<Icon name="check" />`; size with `text-{size}`, never type utilities. A mark attached to a line of type (dot, bullet, leading icon) sits in an `h-[1lh] items-center` box and is sized in em, never nudged with px. → `sable docs typography`
47
47
  8. **Every scroll container declares a policy:** `scrollbar-quiet` (vertical) or `scrollbar-hidden` (horizontal, with an edge fade carrying the affordance).
48
- 9. **Accessible by construction.** WCAG AA contrast, 44px hit areas, visible focus, logical properties, live regions that exist before they speak. → `sable docs accessibility`
48
+ 9. **Accessible by construction.** WCAG AA contrast (and a label on a brand or status fill also clears APCA Lc 60 — `label-contrast`), 44px hit areas, visible focus, logical properties, live regions that exist before they speak. → `sable docs accessibility`
49
49
 
50
50
  ## Customizing: themes are presets
51
51
 
52
- Pick a theme as a starting point, then override tokens on top of it. Every token a theme sets can be reset by the app on `:root` (or any subtree) after the theme CSS loads — radius, density, type scale, motion scale, accent — and components follow, because they only read tokens. The docs site's **Customize** panel does exactly this live and exports the result as CSS; paste it after your Sable imports. Retrieve the overridable set with `npx sable token --list`.
52
+ Pick a theme as a starting point, then override tokens on top of it. Every token a theme sets can be reset by the app on `:root` (or any subtree) after the theme CSS loads — radius, density, type scale, motion scale, accent — and components follow, because they only read tokens. The docs site's **Customize** panel does exactly this live and exports the result as CSS; paste it after your Sable imports. Retrieve the overridable set with `npx sable token --list`. When you retune a filled role (`--primary`, `--sidebar-primary`, `--highlight`, `--destructive`, `--success`, `--tone-warning`, `--tone-info`), check the label on it: `sable lint` measures the pair against the installed theme and suggests one that reads. If the pair is a deliberate brand choice, keep it with `/* design-lint-disable-next-line label-contrast -- <reason> */`. Status colours have a second token for the colour drawn on a surface — `--destructive-text`, `--success-text`, `--tone-warning-text`, `--tone-info-text` — which `text-`, `border-` and `ring-<status>` read; only `bg-<status>` reads the fill. Retune both.
53
53
 
54
54
  ## Wire it up
55
55
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,153 @@
1
1
  # What's new
2
2
 
3
+ ## 0.6.1
4
+
5
+ - **`sable lint` catches a status fill retuned without its text.** Since 0.6,
6
+ `text-destructive`, `border-destructive`, `ring-destructive` and the rest read
7
+ `--destructive-text`; only `bg-destructive` reads the fill. An app that set
8
+ `--destructive` (or `--success`, `--tone-warning`, `--tone-info`) and stopped
9
+ there painted its buttons in its own red and its error text, borders and rings
10
+ in the theme's, and nothing said so. The new consumer rule `status-text-drift`
11
+ reads the app's stylesheets for a status fill set without its `-text` in the
12
+ same theme and mode, and names the mode that is missing. It needs only the
13
+ stylesheet, so it reports even where `label-contrast` cannot measure. `sable
14
+ grade` runs it too.
15
+
16
+ ## 0.6.0
17
+
18
+ ### Art from the theme
19
+
20
+ - **`GradientArt`, placeholder artwork painted by a shader.** A static WebGL
21
+ mesh gradient (Paper Shaders' static mesh gradient; `variant="grain"` for
22
+ their grain gradient) painted from the theme, for covers, banners, tiles and
23
+ avatars with no image yet. `seed` fixes each item's composition and leading
24
+ colour, so a row of covers or avatars stays individually recognisable in
25
+ every theme; children render above the art. It paints once on mount, on
26
+ resize and when the theme changes, through one WebGL context shared by every
27
+ instance, so a page of avatars costs one context. A CSS gradient stays
28
+ underneath for prerendered pages and browsers without WebGL2.
29
+ - **Each preset has its own art direction.** New optional `--art-*` tokens
30
+ (`art-1` … `art-6`, `art-ground`, `art-warp`, `art-softness`, `art-grain`;
31
+ `ART_TOKENS` in the contract) set the art's palette and character, through
32
+ `--theme-art-*` hooks like the workspace surfaces. Ember burns (fire falling
33
+ off into warm black), Poppy is candy blobs, Prism airbrushed pastels, Verdant
34
+ stained glass, Meridian grainy monochrome, Atelier a dark studio with one
35
+ amber glow; Sable keeps its tones. The colours derive from each theme's own
36
+ tokens where its art direction allows, so a brand change carries into the
37
+ art. Presets may declare the hooks too.
38
+ - **`Avatar variant="art"`.** The fallback paints the initials over
39
+ `GradientArt` seeded by the person (the initials, then `alt`, or `seed`), so
40
+ everyone without a photo gets their own colour and keeps it wherever they
41
+ appear. The default stays `neutral`.
42
+ - **Blocks and patterns use it.** The product card, media player and profile
43
+ card blocks and the auth page paint their placeholder art with
44
+ `GradientArt` instead of hand-written CSS gradients, and the avatars across
45
+ the blocks and page patterns use `variant="art"`.
46
+ - **New dependency:** `@paper-design/shaders` 0.0.81 (Apache-2.0), pinned
47
+ exactly while it is pre-1.0. Only the two fragment shaders and the noise
48
+ texture are imported; the notice is in `THIRD_PARTY_NOTICES.md`.
49
+
50
+ ### Labels on filled colours
51
+
52
+ - **Labels on brand and status fills now have to pass APCA as well as WCAG.** A
53
+ filled control's label is held to 4.5:1 and APCA Lc 60 in both modes, for
54
+ `primary`, `sidebar-primary`, `highlight`, `destructive`, `success`,
55
+ `tone-warning` and `tone-info`. The ratio alone let dark ink on a bright red
56
+ through at Lc 38–46, which reads muddy. Every shipped theme is tested against
57
+ both floors, with no exceptions.
58
+ - **Status colours have a separate token for text.** `--destructive-text`,
59
+ `--success-text`, `--tone-warning-text` and `--tone-info-text`.
60
+ `text-destructive`, `border-destructive`, `ring-destructive`, `outline-`,
61
+ `decoration-`, `fill-` and `stroke-` now read the `-text` token; only
62
+ `bg-destructive` reads the fill. The class names are unchanged. A dark-mode
63
+ fill can now sit low enough for a white label while error text stays light
64
+ enough to read on a dark card. Badge's soft status tones and the destructive
65
+ Menu item mix their ink from the `-text` token too: from the fill, Meridian's
66
+ dark destructive menu item measured 3.8:1; every theme now clears 4.5:1.
67
+ **Migration:** if your app retunes `--destructive` (or `--success`,
68
+ `--tone-warning`, `--tone-info`), set its `-text` as well, or text, borders
69
+ and rings keep the theme's colour. Presets saved on 0.5 keep working: a
70
+ missing `-text` token takes its fill, which is what text read before.
71
+ - **Destructive buttons have white labels in dark mode.** In Sable, Atelier,
72
+ Ember, Meridian and Verdant the dark destructive fill moved down (Sable
73
+ 68%→59%, Meridian 66%→55%, Atelier 66%→58.5%, Verdant 64%→57.5%, Ember
74
+ 62%→58%) and its label went from dark to white. Error text reads the lighter
75
+ `-text` value, so it is no darker than before.
76
+ - **Dark highlights have white labels in Sable and Ember.** Sable's dark
77
+ `--highlight` went from 70% to 57% and Ember's from 65.2% to 58%; dark ink on
78
+ the brighter fill read at Lc 42–46.
79
+ - **Small status fixes.** In Ember, Atelier, Meridian and Verdant, dark
80
+ `success` and `tone-info` are a little lighter so the dark label clears Lc
81
+ 60. In light mode, Sable's `text-tone-info` is one step darker and Verdant's
82
+ `text-tone-warning` is 51%; both measured just under 4.5:1 on `muted`.
83
+
84
+ ### Lint and CLI
85
+
86
+ - **New `sable lint` rule: `label-contrast`.** It finds an app's overrides of a
87
+ brand or status fill or its `-foreground`, completes the pair from the
88
+ installed theme and measures both modes. Findings carry file:line and the
89
+ nearest pair that reads: the fill's lightness moves, the hue stays. Keep a
90
+ deliberate brand pair with `/* design-lint-disable-next-line label-contrast
91
+ -- <reason> */`. `sable grade` runs the same rule. Apps that passed lint on
92
+ 0.5 can fail it now.
93
+ - **`sable preset --check` measures labels on fills.** A failing pair stops the
94
+ check with `ERR_PRESET_CONTRAST`, the suggested pair and a ready-to-paste
95
+ `exceptions` entry. Presets accept an optional `exceptions` array (`rule`,
96
+ `mode`, `token`, `reason`); an accepted pair is reported with its reason on
97
+ every check instead of failing. `@sableui/react/presets` exports
98
+ `presetLabelContrast()` and the measuring helpers (`checkLabelledFills`,
99
+ `fitFillToInk`, `inkFor`, `inkPolarity`, `labelContrast`, `LABELLED_FILLS`,
100
+ `BRAND_FILLS`, `STATUS_FILLS`).
101
+
102
+ ### Sheets own their actions
103
+
104
+ - **A sheet lays out the actions inside it.** `CardFooter` has an `actions`
105
+ prop for a footer that holds only buttons. Inside a bottom `SheetContent` or
106
+ an `IosSheet`, that footer becomes a full-width stack pinned to the bottom:
107
+ the primary on top, every button at the cta height. On a page nothing
108
+ changes. `SheetFooter` stacks the same way in a bottom sheet, capped at
109
+ `max-w-sm` and centred; side sheets keep the right-aligned row.
110
+ DangerZoneBlock, FeedbackFormBlock and ProfileFormBlock mark their footers.
111
+ - **Only the sheet's own content.** A Dialog, Popover or side Sheet opened from
112
+ inside a bottom sheet presents a page again, so its footer is a row. Only the
113
+ outermost flat Card in a sheet bleeds into the sheet's padding (lining its
114
+ content up with the sheet's title and close button), and only that card's
115
+ footer stacks; a card nested inside it is left alone.
116
+ - **`PresentationProvider`, `usePresentation`** and the `Presentation` type let
117
+ a custom container declare itself a sheet.
118
+ - **`IosSheet background="opaque"`** keeps the floating medium-detent shape but
119
+ fills it solid, like SwiftUI's `.presentationBackground`, for forms whose
120
+ fields would otherwise sit on whatever the glass blurs. In an iOS sheet, a
121
+ form or grid between the content and its action bar keeps its own layout.
122
+ The iOS target is marked beta, and `sable target ios` prints its layout rules.
123
+
124
+ ### Components
125
+
126
+ - **Tab triggers space a badge or icon from the label.** Pill and underline
127
+ `TabsTrigger` use `--control-gap`, as Button does; the label's trimmed span
128
+ had dropped a typed space, so a badge touched the text.
129
+ - **NotificationListBlock puts the unread dot before the time**, so the times
130
+ line up on one right edge.
131
+
132
+ ### On sableui.com
133
+
134
+ - **Customize was redesigned.** Simple is gestures over previews: the character
135
+ pad, one shape preview for corners and depth together, a live palette strip
136
+ of what the page is painted with, single-line font rows. Advanced is a
137
+ property grid in the workspace register (compact density, `PanelSection`,
138
+ `PropertyRow`/`PropertyPair`, `ScrubField`, `Select`): one exact anchor per
139
+ domain with the ladder it resolves to beneath it. Mode, Reset all and Export
140
+ sit in a footer both views share, and every section resets on its own.
141
+ - **Status colours lean toward the brand within their meaning.** Destructive,
142
+ warning and success each move toward the brand inside a hue range the
143
+ presets already ship (destructive 5–30°, warning 50–90°, success 140–175°),
144
+ warning keeps 30° from destructive, and saturation follows the brand at half
145
+ strength with a floor. Advanced has a Status lean control.
146
+ - **Customize fits labels to brand and status fills**: the label ink is chosen
147
+ once, from the brand as picked, and each mode's fill moves to meet it.
148
+ - The homepage now says what is true: Sable is open source under MIT and
149
+ installs from npm.
150
+
3
151
  ## 0.5.0
4
152
 
5
153
  - **Stacked toasts.** `<ToastProvider stacked>` stacks toasts like a deck of
package/CONTRIBUTING.md CHANGED
@@ -117,7 +117,7 @@ deliberately reverses: `DESIGN.md → Registers`. What a contributor needs to kn
117
117
 
118
118
  ## Theme contract
119
119
 
120
- A theme is a CSS file that defines every token in `themes/_contract.ts` for `:root` (light) and `.dark` (dark), plus an optional TS manifest. Components depend on the **semantic** layer; each theme supplies its own palette underneath. Alternate themes activate through the root `data-sable-theme` attribute; consuming apps should usually let `<SableThemeProvider>` own that wiring and read/update it through `useSableTheme()`. To make a new theme: copy `themes/sable/`, swap the palette + semantic values, keep the token names identical, add the manifest to `themes/registry.ts`, import its CSS from `src/themes.css`, add `./theme/<id>` + `./theme/<id>.css` to both `exports` and `publishConfig.exports`, and cover it in `themes.test.ts`. `build-registry` fails on a theme missing from any of those, and generates `registry.json → themes.available` from the manifests — never hand-edit that list. Re-skinning never touches component code.
120
+ A theme is a CSS file that defines every token in `themes/_contract.ts` for `:root` (light) and `.dark` (dark), plus an optional TS manifest. Components depend on the **semantic** layer; each theme supplies its own palette underneath. Alternate themes activate through the root `data-sable-theme` attribute; consuming apps should usually let `<SableThemeProvider>` own that wiring and read/update it through `useSableTheme()`. To make a new theme: copy `themes/sable/`, swap the palette + semantic values, keep the token names identical, add the manifest to `themes/registry.ts`, import its CSS from `src/themes.css`, add `./theme/<id>` + `./theme/<id>.css` to both `exports` and `publishConfig.exports`, and cover it in `themes.test.ts` — which holds every label on a brand or status fill (`primary`, `sidebar-primary`, `highlight`, `destructive`, `success`, `tone-warning`, `tone-info`) to 4.5:1 **and** APCA Lc 60 with no exceptions; apps and presets may record exceptions, Sable's own themes may not. Each status colour also has a `-text` token (the colour on a surface, read by `text-`/`border-`/`ring-<status>` through Tailwind's per-property namespaces in styles.css), gated as text on `background`, `card`, `popover` and `muted`. `build-registry` fails on a theme missing from any of those, and generates `registry.json → themes.available` from the manifests — never hand-edit that list. Re-skinning never touches component code.
121
121
 
122
122
  **Themes are presets, not the unit of customization.** Every token a theme sets can also be set by an app (or by the docs customizer) on `:root` after the theme loads. A theme is the starting point a consumer picks; the customizer exports the deltas on top of it. Keep that working: a theme must not set a token with `!important` or under a selector more specific than `[data-sable-theme="<id>"]`, or a consumer override cannot win.
123
123
 
@@ -16,10 +16,21 @@ Third-party software, fonts, and trademarks retain their own terms.
16
16
  | [React](https://github.com/facebook/react) | MIT | Component runtime |
17
17
  | [Reicon](https://github.com/dqev/reicon) | MIT | Default interface icons and bundled brand icons |
18
18
  | [Konsta UI](https://github.com/konstaui/konsta) | MIT | Optional native controls for iOS prototypes |
19
+ | [Paper Shaders](https://github.com/paper-design/shaders) | Apache-2.0 | GradientArt's mesh and grain fragment shaders |
19
20
 
20
21
  Library dependencies are external to Sable's library build and carry their
21
22
  license notices in their distributions.
22
23
 
24
+ ## Paper Shaders
25
+
26
+ GradientArt renders fragment shaders from `@paper-design/shaders` 0.0.81, a
27
+ dependency. Its vertex shader is adapted from that package's
28
+ `dist/vertex-shader.js` (outputs the two fragment shaders do not read are
29
+ removed) and lives in `src/components/gradient-art/renderer.ts`. Copyright 2026
30
+ Paper, "Powered by Paper Shaders" (https://shaders.paper.design); the license
31
+ and NOTICE are included as `third-party/paper-shaders-LICENSE.txt` and
32
+ `third-party/paper-shaders-NOTICE.txt`.
33
+
23
34
  ## Brand icons
24
35
 
25
36
  The 23 bundled brand marks come from `reicon-brands` 1.0.2. Sable normalizes them
@@ -27,6 +27,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
27
27
  import { dirname, join, relative, resolve, sep } from "node:path"
28
28
  import { fileURLToPath } from "node:url"
29
29
  import { ignoreGlob, lintText, makeRules, report } from "../scripts/lint-rules.mjs"
30
+ import { labelContrastScanner, statusTextDrift } from "../scripts/label-contrast.mjs"
30
31
 
31
32
  const HERE = dirname(fileURLToPath(import.meta.url))
32
33
  const PKG_ROOT = join(HERE, "..")
@@ -53,7 +54,7 @@ const motionTokens = new Set([
53
54
  REGISTRY.tokens?.motion?.scale,
54
55
  ].filter(Boolean))
55
56
 
56
- const rules = makeRules({ semanticTokens, motionTokens }).filter((r) => r.scope !== "package")
57
+ const rules = makeRules({ semanticTokens, motionTokens, labelContrast: await labelContrastScanner(PKG_ROOT, REGISTRY.themes?.default), statusText: (css) => statusTextDrift(css, REGISTRY.themes?.default) }).filter((r) => r.scope !== "package")
57
58
 
58
59
  /* ── Args. ─────────────────────────────────────────────────────────────────── */
59
60
  const args = process.argv.slice(2)
package/bin/sable.mjs CHANGED
@@ -84,7 +84,7 @@ const COMMANDS = [
84
84
  arg: null,
85
85
  flags: [
86
86
  { name: "--template", value: null, describe: "Print a complete editable Sable preset JSON record." },
87
- { name: "--check", value: "file.json", describe: "Validate the schema and Sable token contract." },
87
+ { name: "--check", value: "file.json", describe: "Validate the schema, the Sable token contract, and labels on brand and status fills (accept a pair with `exceptions`)." },
88
88
  { name: "--css", value: "file.json", describe: "Generate scoped theme CSS from a validated preset." },
89
89
  ],
90
90
  returns: ["preset.template", "preset.check", "preset.css"],
@@ -282,6 +282,7 @@ const COMMANDS = [
282
282
  const ERROR_CODES = [
283
283
  { code: "ERR_TARGET_MISSING", when: "The installed package is missing its optional target mapping artifact." },
284
284
  { code: "ERR_PRESET_INVALID", when: "A preset is missing required metadata, semantic values, or uses unsupported tokens or CSS." },
285
+ { code: "ERR_PRESET_CONTRAST", when: "A label on a brand or status fill (primary, sidebar-primary, highlight, destructive, success, tone-warning, tone-info) misses 4.5:1 or APCA Lc 60. Apply the suggested pair, or record an `exceptions` entry with a reason." },
285
286
  { code: "ERR_AGENT_SETUP", when: "The packaged skill is missing, the destination is linked, or existing local edits would be overwritten." },
286
287
  { code: "ERR_UNKNOWN_COMMAND", when: "The first argument is not a command." },
287
288
  { code: "ERR_MISSING_ARGUMENT", when: "A command that requires an argument got none." },
@@ -1213,7 +1214,7 @@ const HANDLERS = {
1213
1214
  const contexts = (match.contexts ?? []).map(c => `${c.role}: ${c.strategy} → ${c.importFrom} (${c.components.join(" + ")})`).join("\n")
1214
1215
  ok("target.component", match, `${match.source}: ${match.strategy}\n${contexts || `${match.importFrom ?? "app"} → ${match.components.join(" + ") || match.strategy}`}\n${match.guidance}`)
1215
1216
  }
1216
- ok("target", target, `iOS ${target.platformVersion}: ${target.designPolicy.principle}\n${target.designPolicy.rule}\n` + Object.values(target.components).map(c => `${c.source}: ${c.strategy} → ${c.strategy === "contextual" ? "choose by role" : c.components.join(" + ") || "redesign for phone"}`).join("\n"))
1217
+ ok("target", target, `iOS ${target.platformVersion}${target.status ? ` (${target.status})` : ""}: ${target.designPolicy.principle}\n${target.statusNote ? `${target.statusNote}\n` : ""}${target.designPolicy.rule}\n${(target.designPolicy.layout ?? []).map((l) => `Layout: ${l}\n`).join("")}` + Object.values(target.components).map(c => `${c.source}: ${c.strategy} → ${c.strategy === "contextual" ? "choose by role" : c.components.join(" + ") || "redesign for phone"}`).join("\n"))
1217
1218
  },
1218
1219
  preset: () => cmdDesignTool("design-presets.mjs"),
1219
1220
  setup: () => {
@@ -7,7 +7,7 @@
7
7
  * To author a new theme: copy `themes/sable/`, keep these token names, and
8
8
  * supply your own palette + values. See CONTRIBUTING.md → "Theme contract".
9
9
  */
10
- declare const SEMANTIC_TOKENS: readonly ["background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "muted", "muted-foreground", "border", "border-strong", "input", "control-border", "ring", "primary", "primary-foreground", "secondary", "secondary-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "success", "success-foreground", "tone-standard", "tone-collectible", "tone-edition", "tone-info", "tone-warning", "highlight", "tone-standard-foreground", "tone-collectible-foreground", "tone-edition-foreground", "tone-info-foreground", "tone-warning-foreground", "highlight-foreground", "overlay", "overlay-foreground", "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground", "sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring"];
10
+ declare const SEMANTIC_TOKENS: readonly ["background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "muted", "muted-foreground", "border", "border-strong", "input", "control-border", "ring", "primary", "primary-foreground", "secondary", "secondary-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "success", "success-foreground", "tone-standard", "tone-collectible", "tone-edition", "tone-info", "tone-warning", "highlight", "tone-standard-foreground", "tone-collectible-foreground", "tone-edition-foreground", "tone-info-foreground", "tone-warning-foreground", "highlight-foreground", "destructive-text", "success-text", "tone-warning-text", "tone-info-text", "overlay", "overlay-foreground", "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground", "sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring"];
11
11
  type SemanticToken = (typeof SEMANTIC_TOKENS)[number];
12
12
  /**
13
13
  * Motion tokens (mode-independent). A theme MAY override these to retune its
@@ -106,6 +106,24 @@ type DensityToken = (typeof DENSITY_TOKENS)[number];
106
106
  */
107
107
  declare const WORKSPACE_SURFACE_TOKENS: readonly ["canvas", "canvas-foreground", "panel", "panel-foreground", "panel-raised", "panel-field", "panel-field-hover", "hairline", "hairline-strong"];
108
108
  type WorkspaceSurfaceToken = (typeof WORKSPACE_SURFACE_TOKENS)[number];
109
+ /**
110
+ * Art direction for generated imagery (GradientArt) — like the workspace
111
+ * surfaces, every theme gets them from styles.css with a `--theme-*` hook, so a
112
+ * theme declares only what makes its art its own.
113
+ *
114
+ * - `art-1` … `art-6` are the colours the art is painted from, in placement
115
+ * order; unset slots are transparent and skipped. The default is the four
116
+ * decorative tones, lifted to an art strength.
117
+ * - `art-ground` is what the colours glow over (grain's ground, the CSS
118
+ * fallback's base). The default is the brand steeped in the first tone.
119
+ * - `art-warp` (0–1) bends the field; `art-softness` (0–1) is how far colours
120
+ * melt into each other, 0 crisp blobs to 1 one wash; `art-grain` (0–1) is a
121
+ * film-grain overlay for themes whose imagery carries one.
122
+ *
123
+ * Derive the colours from the theme's own tokens (`oklch(from var(--primary)
124
+ * …)`) where the art direction allows, so a brand change carries into the art.
125
+ */
126
+ declare const ART_TOKENS: readonly ["art-1", "art-2", "art-3", "art-4", "art-5", "art-6", "art-ground", "art-warp", "art-softness", "art-grain"];
109
127
  /**
110
128
  * UI type tiers (mode-independent) — the chrome typography register.
111
129
  *
@@ -152,4 +170,4 @@ interface ThemeManifest {
152
170
  };
153
171
  }
154
172
 
155
- export { DENSITY_TOKENS as D, MOTION_TOKENS as M, RADIUS_TOKENS as R, SEMANTIC_TOKENS as S, type ThemeManifest as T, WORKSPACE_SURFACE_TOKENS as W, type ThemeMode as a, type DensityToken as b, type MotionToken as c, type RadiusToken as d, type SemanticToken as e, TYPE_OVERLINE_TOKENS as f, TYPE_REFINEMENT_TOKENS as g, TYPE_SCALE_TOKENS as h, TYPE_UI_TOKENS as i, type TypeOverlineToken as j, type TypeRefinementToken as k, type TypeScaleToken as l, type TypeUiToken as m, type WorkspaceSurfaceToken as n };
173
+ export { ART_TOKENS as A, DENSITY_TOKENS as D, MOTION_TOKENS as M, RADIUS_TOKENS as R, SEMANTIC_TOKENS as S, type ThemeManifest as T, WORKSPACE_SURFACE_TOKENS as W, type ThemeMode as a, type DensityToken as b, type MotionToken as c, type RadiusToken as d, type SemanticToken as e, TYPE_OVERLINE_TOKENS as f, TYPE_REFINEMENT_TOKENS as g, TYPE_SCALE_TOKENS as h, TYPE_UI_TOKENS as i, type TypeOverlineToken as j, type TypeRefinementToken as k, type TypeScaleToken as l, type TypeUiToken as m, type WorkspaceSurfaceToken as n };
@@ -1,5 +1,6 @@
1
- import { Kbd, CommandMenuPanel, ToggleGroup, Toggle, BarChart, DateTile, Accordion, AccordionItem, AccordionTrigger, AccordionPanel, RadialChart, Sparkline, Slider, Stepper, StepperItem, SegmentedBar, Checkbox, PieChart, AreaChart, OtpField } from '../chunk-GU3T7SBA.js';
2
- import { Card, CardHeader, CardTitle, CardDescription, CardAction, Button, CardList, Entity, OverflowMenu, MenuItem, MenuSeparator, Code, CardContent, Message, Bubble, CardFooter, DestructiveActionModal, Badge, AvatarGroup, Avatar, Textarea, CodeBlock, Input, Table, TableHeader, TableRow, TableHead, TableBody, TableCell, StatusDot, Switch, Form, Field, Select, SelectTrigger, SelectContent, SelectItem, Separator, Label, Attachment } from '../chunk-RCLKXF2N.js';
1
+ import { Kbd, CommandMenuPanel, ToggleGroup, Toggle, BarChart, DateTile, Accordion, AccordionItem, AccordionTrigger, AccordionPanel, RadialChart, Sparkline, Slider, Stepper, StepperItem, SegmentedBar, Checkbox, PieChart, AreaChart, OtpField } from '../chunk-EDXLM7YL.js';
2
+ import { Card, CardHeader, CardTitle, CardDescription, CardAction, Button, CardList, Entity, OverflowMenu, MenuItem, MenuSeparator, Code, CardContent, Message, Bubble, CardFooter, DestructiveActionModal, Badge, AvatarGroup, Avatar, Textarea, CodeBlock, Input, Table, TableHeader, TableRow, TableHead, TableBody, TableCell, GradientArt, StatusDot, Switch, Form, Field, Select, SelectTrigger, SelectContent, SelectItem, Separator, Label, Attachment } from '../chunk-7QSDYBXO.js';
3
+ import '../chunk-FKZXAFIY.js';
3
4
  import { Region, Stack, Row, Grid } from '../chunk-3IWZYBGH.js';
4
5
  import { cn } from '../chunk-SRQWQUTW.js';
5
6
  import { Icon } from '../chunk-QJ6FWTUG.js';
@@ -162,7 +163,7 @@ function DangerZoneBlock({ className, framed = true, onDelete }) {
162
163
  /* @__PURE__ */ jsx(CardTitle, { render: /* @__PURE__ */ jsx("h2", {}), children: "Delete account" }),
163
164
  /* @__PURE__ */ jsx(CardDescription, { children: "Permanently remove your profile, editions, and sales history. Collectors keep the editions they own." })
164
165
  ] }),
165
- /* @__PURE__ */ jsx(CardFooter, { className: "justify-end", children: /* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: () => setOpen(true), children: "Delete account" }) })
166
+ /* @__PURE__ */ jsx(CardFooter, { actions: true, className: "justify-end", children: /* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: () => setOpen(true), children: "Delete account" }) })
166
167
  ] }),
167
168
  /* @__PURE__ */ jsx(
168
169
  DestructiveActionModal,
@@ -300,7 +301,7 @@ function EventCardBlock({ className, framed = true, onRespond }) {
300
301
  ] })
301
302
  ] }),
302
303
  /* @__PURE__ */ jsxs(CardFooter, { className: "flex-wrap justify-between gap-3", children: [
303
- /* @__PURE__ */ jsx(AvatarGroup, { "aria-label": `${event.total} attendees`, total: event.total, size: "sm", surface: framed ? "card" : void 0, fit: true, children: event.attendees.map((initials) => /* @__PURE__ */ jsx(Avatar, { fallback: initials, alt: initials }, initials)) }),
304
+ /* @__PURE__ */ jsx(AvatarGroup, { "aria-label": `${event.total} attendees`, total: event.total, size: "sm", surface: framed ? "card" : void 0, fit: true, children: event.attendees.map((initials) => /* @__PURE__ */ jsx(Avatar, { variant: "art", fallback: initials, alt: initials }, initials)) }),
304
305
  /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
305
306
  /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => onRespond?.("decline"), children: "Decline" }),
306
307
  /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => onRespond?.("join"), children: "Join" })
@@ -434,7 +435,7 @@ function FeedbackFormBlock({ className, framed = true, onDone }) {
434
435
  /* @__PURE__ */ jsx(ToggleGroup, { value: [score], onValueChange: (v) => v[0] && setScore(String(v[0])), "aria-label": "Rating", className: "w-full", children: scale.map((n) => /* @__PURE__ */ jsx(Toggle, { value: n, className: "flex-1", children: n }, n)) }),
435
436
  /* @__PURE__ */ jsx(Textarea, { placeholder: "What worked, what didn\u2019t\u2026", "aria-label": "Notes", rows: 3, value: notes, onChange: (e) => setNotes(e.target.value) })
436
437
  ] }),
437
- /* @__PURE__ */ jsxs(CardFooter, { className: "justify-end", children: [
438
+ /* @__PURE__ */ jsxs(CardFooter, { actions: true, className: "justify-end", children: [
438
439
  /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => onDone?.(false), children: "Skip" }),
439
440
  /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => onDone?.(true, { score: Number(score), notes }), children: "Send feedback" })
440
441
  ] })
@@ -543,7 +544,7 @@ function InviteTeamBlock({ className, framed = true, onSend }) {
543
544
  /* @__PURE__ */ jsx(CardDescription, { children: "Collaborators can edit every project." })
544
545
  ] }),
545
546
  /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col gap-4", children: [
546
- /* @__PURE__ */ jsx(AvatarGroup, { "aria-label": `${collaborators.length} collaborators`, size: "sm", surface: framed ? "card" : void 0, children: collaborators.map((initials) => /* @__PURE__ */ jsx(Avatar, { fallback: initials, alt: initials }, initials)) }),
547
+ /* @__PURE__ */ jsx(AvatarGroup, { "aria-label": `${collaborators.length} collaborators`, size: "sm", surface: framed ? "card" : void 0, children: collaborators.map((initials) => /* @__PURE__ */ jsx(Avatar, { variant: "art", fallback: initials, alt: initials }, initials)) }),
547
548
  /* @__PURE__ */ jsxs(
548
549
  "form",
549
550
  {
@@ -665,17 +666,6 @@ var kpiSparklineBlockMeta = {
665
666
  ]
666
667
  };
667
668
  var track = { title: "Low tide, high noon", artist: "Juno Avery", position: 38, elapsed: "1:24", remaining: "-2:16" };
668
- var ART_TONES = ["info", "collectible", "edition"];
669
- var ART_SPOTS = [[20, 20, 70], [85, 30, 60], [60, 90, 65]];
670
- var cover = [
671
- ...ART_SPOTS.map(
672
- ([x, y, reach], i) => (
673
- // design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
674
- `radial-gradient(in oklab at ${x}% ${y}%, oklch(from var(--tone-${ART_TONES[i % ART_TONES.length]}) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent ${reach}%)`
675
- )
676
- ),
677
- "var(--primary)"
678
- ].join(", ");
679
669
  function MediaPlayerBlock({ className, framed = true, onPlayingChange, onPrevious, onNext }) {
680
670
  const [playing, setPlaying] = useState(true);
681
671
  return /* @__PURE__ */ jsx(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-xs", className), children: /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col gap-4", children: [
@@ -683,7 +673,7 @@ function MediaPlayerBlock({ className, framed = true, onPlayingChange, onPreviou
683
673
  Entity,
684
674
  {
685
675
  className: "p-0",
686
- leading: /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "size-14 rounded-md", style: { background: cover } }),
676
+ leading: /* @__PURE__ */ jsx(GradientArt, { "aria-hidden": true, seed: track.title, className: "size-14 rounded-md" }),
687
677
  title: track.title,
688
678
  subtitle: track.artist,
689
679
  action: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Like", children: /* @__PURE__ */ jsx(Icon, { name: "heart" }) })
@@ -821,15 +811,15 @@ function NotificationListBlock({ className, framed = true }) {
821
811
  return /* @__PURE__ */ jsx(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-xs overflow-hidden", className), children: /* @__PURE__ */ jsx(CardList, { children: notifications.map((n) => /* @__PURE__ */ jsx(
822
812
  Entity,
823
813
  {
824
- leading: /* @__PURE__ */ jsx(Avatar, { size: "sm", fallback: n.initials, alt: n.name }),
814
+ leading: /* @__PURE__ */ jsx(Avatar, { variant: "art", size: "sm", fallback: n.initials, alt: n.name }),
825
815
  title: /* @__PURE__ */ jsxs(Fragment, { children: [
826
816
  n.name,
827
817
  n.unread ? /* @__PURE__ */ jsx("span", { className: "sr-only", children: " (unread)" }) : null
828
818
  ] }),
829
819
  subtitle: n.what,
830
820
  action: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-caption text-muted-foreground", children: [
831
- n.when,
832
- n.unread ? /* @__PURE__ */ jsx(StatusDot, { tone: "info" }) : null
821
+ n.unread ? /* @__PURE__ */ jsx(StatusDot, { tone: "info" }) : null,
822
+ n.when
833
823
  ] })
834
824
  },
835
825
  n.name
@@ -846,7 +836,7 @@ var notificationListBlockMeta = {
846
836
  intent: "Reach for this for a notifications popover, a recent-activity card or an inbox preview. A full inbox with selection and bulk actions is a ListDetailPage.",
847
837
  notes: [
848
838
  "Rows are Card + CardList + Entity, inset from the card's padding at any density.",
849
- "Unread is a quiet info dot after the time \u2014 the list stays calm when most items are unread.",
839
+ "Unread is a quiet info dot before the time, so the times keep one right edge \u2014 the list stays calm when most items are unread.",
850
840
  "framed={false} drops the surface for a Popover, which already is one."
851
841
  ],
852
842
  a11y: [
@@ -1071,20 +1061,9 @@ var pricingTableBlockMeta = {
1071
1061
  ]
1072
1062
  };
1073
1063
  var product = { title: "Untitled (gold rush)", maker: "Mara Okafor", edition: "12 of 50", price: "0.42 SOL" };
1074
- var ART_TONES2 = ["info", "collectible", "edition", "standard"];
1075
- var ART_SPOTS2 = [[18, 22, 58], [82, 18, 55], [70, 88, 60], [12, 92, 55]];
1076
- var artwork = [
1077
- ...ART_SPOTS2.map(
1078
- ([x, y, reach], i) => (
1079
- // design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
1080
- `radial-gradient(in oklab at ${x}% ${y}%, oklch(from var(--tone-${ART_TONES2[i % ART_TONES2.length]}) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent ${reach}%)`
1081
- )
1082
- ),
1083
- "var(--primary)"
1084
- ].join(", ");
1085
1064
  function ProductCardBlock({ className, framed = true, onCollect }) {
1086
1065
  return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-xs overflow-hidden", className), children: [
1087
- /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "aspect-[4/3]", style: { background: artwork } }),
1066
+ /* @__PURE__ */ jsx(GradientArt, { "aria-hidden": true, seed: product.title, className: "aspect-[4/3]" }),
1088
1067
  /* @__PURE__ */ jsxs(CardHeader, { children: [
1089
1068
  /* @__PURE__ */ jsx(CardTitle, { children: product.title }),
1090
1069
  /* @__PURE__ */ jsx(CardDescription, { children: product.maker }),
@@ -1125,24 +1104,13 @@ var profile = {
1125
1104
  { value: "4.2k", label: "followers" }
1126
1105
  ]
1127
1106
  };
1128
- var ART_TONES3 = ["info", "collectible", "edition", "standard"];
1129
- var ART_SPOTS3 = [[10, 50, 60], [50, 0, 55], [90, 60, 60], [40, 100, 50]];
1130
- var banner = [
1131
- ...ART_SPOTS3.map(
1132
- ([x, y, reach], i) => (
1133
- // design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
1134
- `radial-gradient(in oklab at ${x}% ${y}%, oklch(from var(--tone-${ART_TONES3[i % ART_TONES3.length]}) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent ${reach}%)`
1135
- )
1136
- ),
1137
- "var(--primary)"
1138
- ].join(", ");
1139
1107
  function ProfileCardBlock({ className, framed = true, onFollowChange, onShare }) {
1140
1108
  const [following, setFollowing] = useState(false);
1141
1109
  return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-xs overflow-hidden", className), children: [
1142
- /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "h-20", style: { background: banner } }),
1110
+ /* @__PURE__ */ jsx(GradientArt, { "aria-hidden": true, seed: profile.handle, className: "h-20" }),
1143
1111
  /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col gap-3 pt-0", children: [
1144
1112
  /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between", children: [
1145
- /* @__PURE__ */ jsx(Avatar, { size: "lg", fallback: profile.initials, alt: profile.name, className: "-mt-6 ring-4 ring-card" }),
1113
+ /* @__PURE__ */ jsx(Avatar, { variant: "art", size: "lg", fallback: profile.initials, alt: profile.name, className: "-mt-6 ring-4 ring-card" }),
1146
1114
  /* @__PURE__ */ jsxs("div", { className: "flex gap-1.5 pt-3", children: [
1147
1115
  /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Share profile", onClick: onShare, children: /* @__PURE__ */ jsx(Icon, { name: "share" }) }),
1148
1116
  /* @__PURE__ */ jsx(
@@ -1222,7 +1190,7 @@ function ProfileFormBlock({ className, framed = true, onSave }) {
1222
1190
  children: [
1223
1191
  /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
1224
1192
  /* @__PURE__ */ jsxs(Row, { gap: 2, children: [
1225
- /* @__PURE__ */ jsx(Avatar, { size: "lg", fallback: profile2.initials, alt: profile2.name }),
1193
+ /* @__PURE__ */ jsx(Avatar, { variant: "art", size: "lg", fallback: profile2.initials, alt: profile2.name }),
1226
1194
  /* @__PURE__ */ jsxs(Stack, { gap: 0.5, align: "start", children: [
1227
1195
  /* @__PURE__ */ jsxs(Row, { gap: 1, children: [
1228
1196
  /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", children: "Upload photo" }),
@@ -1241,7 +1209,7 @@ function ProfileFormBlock({ className, framed = true, onSave }) {
1241
1209
  /* @__PURE__ */ jsx(SelectContent, { children: timezones.map((zone) => /* @__PURE__ */ jsx(SelectItem, { value: zone.value, children: zone.label }, zone.value)) })
1242
1210
  ] }) }) })
1243
1211
  ] }) }),
1244
- /* @__PURE__ */ jsx(CardFooter, { className: "justify-end", children: /* @__PURE__ */ jsx(Button, { type: "submit", children: "Save profile" }) })
1212
+ /* @__PURE__ */ jsx(CardFooter, { actions: true, className: "justify-end", children: /* @__PURE__ */ jsx(Button, { type: "submit", children: "Save profile" }) })
1245
1213
  ]
1246
1214
  }
1247
1215
  )
@@ -1639,7 +1607,7 @@ function TeamMembersBlock({ className, framed = true, onInvite, onMemberAction }
1639
1607
  /* @__PURE__ */ jsx(CardList, { children: members.map((m) => /* @__PURE__ */ jsx(
1640
1608
  Entity,
1641
1609
  {
1642
- leading: /* @__PURE__ */ jsx(Avatar, { size: "sm", fallback: m.initials, alt: m.name }),
1610
+ leading: /* @__PURE__ */ jsx(Avatar, { variant: "art", size: "sm", fallback: m.initials, alt: m.name }),
1643
1611
  title: m.name,
1644
1612
  subtitle: m.email,
1645
1613
  action: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
@@ -1697,7 +1665,7 @@ function TestimonialBlock({ className }) {
1697
1665
  ] })
1698
1666
  ] }),
1699
1667
  /* @__PURE__ */ jsxs("figcaption", { className: "flex items-center gap-3", children: [
1700
- /* @__PURE__ */ jsx(Avatar, { size: "lg", fallback: story.initials, alt: story.name }),
1668
+ /* @__PURE__ */ jsx(Avatar, { variant: "art", size: "lg", fallback: story.initials, alt: story.name }),
1701
1669
  /* @__PURE__ */ jsxs("div", { children: [
1702
1670
  /* @__PURE__ */ jsx("p", { className: "text-title-sm", children: story.name }),
1703
1671
  /* @__PURE__ */ jsx("p", { className: "text-body-sm text-muted-foreground", children: story.role })