@sableui/react 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +189 -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 +21 -53
  9. package/dist/{chunk-CCUDFLU3.js → chunk-7QSDYBXO.js} +652 -38
  10. package/dist/{chunk-FBSL2LZQ.js → chunk-EDXLM7YL.js} +11 -30
  11. package/dist/chunk-FKZXAFIY.js +12 -0
  12. package/dist/{chunk-OQACYT2D.js → chunk-NETUFI34.js} +23 -1
  13. package/dist/{chunk-NGBDP3BM.js → chunk-QJ6FWTUG.js} +1 -1
  14. package/dist/{chunk-D2ETQBHV.js → chunk-VVW4UCDI.js} +5 -5
  15. package/dist/{chunk-TH563WLW.js → chunk-XAZGMY73.js} +10 -3
  16. package/dist/foundations/index.js +6 -0
  17. package/dist/icon/index.js +2 -2
  18. package/dist/icon-sets/brands.d.ts +0 -27
  19. package/dist/icon-sets/brands.js +7 -1
  20. package/dist/icon-sets/reicon.js +1 -1
  21. package/dist/index.d.ts +164 -23
  22. package/dist/index.js +116 -29
  23. package/dist/ios/index.d.ts +6 -1
  24. package/dist/ios/index.js +4 -3
  25. package/dist/ios/styles.css +9 -0
  26. package/dist/patterns/index.js +24 -30
  27. package/dist/presets/index.d.ts +108 -5
  28. package/dist/presets/index.js +239 -6
  29. package/dist/styles.css +57 -0
  30. package/dist/{theme-provider-CGeiusgu.d.ts → theme-provider-kQszHYji.d.ts} +1 -1
  31. package/dist/themes/atelier/atelier.css +31 -3
  32. package/dist/themes/atelier/index.d.ts +1 -1
  33. package/dist/themes/ember/ember.css +38 -7
  34. package/dist/themes/ember/index.d.ts +1 -1
  35. package/dist/themes/index.d.ts +2 -2
  36. package/dist/themes/meridian/index.d.ts +1 -1
  37. package/dist/themes/meridian/meridian.css +33 -4
  38. package/dist/themes/poppy/index.d.ts +1 -1
  39. package/dist/themes/poppy/poppy.css +28 -0
  40. package/dist/themes/prism/index.d.ts +1 -1
  41. package/dist/themes/prism/prism.css +26 -0
  42. package/dist/themes/sable/index.d.ts +1 -1
  43. package/dist/themes/sable/sable.css +24 -11
  44. package/dist/themes/verdant/index.d.ts +1 -1
  45. package/dist/themes/verdant/verdant.css +34 -4
  46. package/llms/blocks/danger-zone-block.md +1 -1
  47. package/llms/blocks/event-card-block.md +1 -1
  48. package/llms/blocks/feedback-form-block.md +1 -1
  49. package/llms/blocks/invite-team-block.md +1 -1
  50. package/llms/blocks/media-player-block.md +3 -16
  51. package/llms/blocks/notification-list-block.md +3 -3
  52. package/llms/blocks/product-card-block.md +3 -17
  53. package/llms/blocks/profile-card-block.md +4 -17
  54. package/llms/blocks/profile-form-block.md +2 -2
  55. package/llms/blocks/team-members-block.md +1 -1
  56. package/llms/blocks/testimonial-block.md +1 -1
  57. package/llms/components/avatar.md +13 -1
  58. package/llms/components/card.md +1 -1
  59. package/llms/components/gradient-art.md +80 -0
  60. package/llms/components/sheet.md +1 -1
  61. package/llms/components/tabs.md +1 -1
  62. package/llms/components/toast.md +12 -1
  63. package/llms/foundations/accessibility.md +1 -0
  64. package/llms/patterns/auth-page.md +5 -19
  65. package/llms/patterns/brand-landing-page.md +1 -1
  66. package/llms/patterns/chat-panel.md +1 -1
  67. package/llms/patterns/dashboard-page.md +4 -4
  68. package/llms/patterns/feed-page.md +4 -4
  69. package/llms/patterns/landing-page.md +1 -1
  70. package/llms/patterns/list-detail-page.md +3 -2
  71. package/llms/patterns/settings-page.md +3 -3
  72. package/llms/reference/token-intent.md +9 -1
  73. package/llms/tasks/auditing-an-app.md +6 -0
  74. package/llms/tasks/building-an-ios-prototype.md +3 -1
  75. package/llms/tasks/converting-an-app.md +1 -1
  76. package/llms/tasks/creating-a-preset.md +24 -0
  77. package/llms/tasks/reviewing-a-design.md +9 -0
  78. package/llms/tasks/using-presets.md +6 -1
  79. package/llms-full.txt +214 -106
  80. package/llms.txt +4 -3
  81. package/package.json +9 -7
  82. package/registry.json +310 -32
  83. package/scripts/design-presets.mjs +27 -4
  84. package/scripts/grade.mjs +2 -1
  85. package/scripts/label-contrast.mjs +138 -0
  86. package/scripts/lint-rules.mjs +23 -2
  87. package/skills/sable/SKILL.md +3 -1
  88. package/targets/ios.json +18 -2
  89. package/third-party/paper-shaders-LICENSE.txt +201 -0
  90. package/third-party/paper-shaders-NOTICE.txt +5 -0
@@ -143,14 +143,18 @@
143
143
  --accent: oklch(57.5% 0.2 31.5);
144
144
  --accent-foreground: oklch(99% 0.004 91.5);
145
145
  --destructive: oklch(48% 0.2 25);
146
+ --destructive-text: oklch(48% 0.2 25);
146
147
  --destructive-foreground: oklch(94.6% 0.0083 91.48);
147
148
  --success: oklch(43% 0.1 165);
149
+ --success-text: oklch(43% 0.1 165);
148
150
  --success-foreground: oklch(94.6% 0.0083 91.48);
149
151
  --tone-standard: oklch(40% 0.07 130);
150
152
  --tone-collectible: oklch(46% 0.14 285);
151
153
  --tone-edition: oklch(45% 0.15 309);
152
154
  --tone-info: oklch(48% 0.13 250);
155
+ --tone-info-text: oklch(48% 0.13 250);
153
156
  --tone-warning: oklch(50% 0.12 52);
157
+ --tone-warning-text: oklch(50% 0.12 52);
154
158
  --highlight: oklch(57.5% 0.2 31.5);
155
159
  /* Ember as TEXT (eyebrow numbers, arrows). The fill ember carries white ink
156
160
  but is too light to be read as ink itself (4.1:1 on the sand); this is the
@@ -230,15 +234,20 @@
230
234
  --secondary-foreground: oklch(94.3% 0.0042 91.45);
231
235
  --accent: oklch(57.5% 0.2 31.5);
232
236
  --accent-foreground: oklch(99% 0.004 91.5);
233
- --destructive: oklch(66% 0.21 25);
234
- --destructive-foreground: oklch(22.46% 0.0094 107.13);
235
- --success: oklch(72% 0.12 165);
237
+ /* Status colours come in two: the fill carries a label (destructive sits low under white ink), and `-text` is the same colour drawn on a surface — text, borders, rings — kept light enough to read on a dark card. */
238
+ --destructive: oklch(55% 0.21 25);
239
+ --destructive-text: oklch(78.5% 0.122 25);
240
+ --destructive-foreground: oklch(94.6% 0.0083 91.48);
241
+ --success: oklch(75.5% 0.12 165);
242
+ --success-text: oklch(75.5% 0.12 165);
236
243
  --success-foreground: oklch(22.46% 0.0094 107.13);
237
244
  --tone-standard: oklch(70% 0.09 130);
238
245
  --tone-collectible: oklch(70% 0.12 285);
239
246
  --tone-edition: oklch(68% 0.14 309);
240
- --tone-info: oklch(72% 0.11 250);
247
+ --tone-info: oklch(76.5% 0.11 250);
248
+ --tone-info-text: oklch(76.5% 0.11 250);
241
249
  --tone-warning: oklch(78% 0.13 60);
250
+ --tone-warning-text: oklch(78% 0.13 60);
242
251
  --highlight: oklch(57.5% 0.2 31.5);
243
252
  --meridian-ember-ink: oklch(72% 0.17 33); /* 6.4:1 on the dark ground */
244
253
  --tone-standard-foreground: oklch(22.46% 0.0094 107.13);
@@ -258,3 +267,23 @@
258
267
  --sidebar-border: oklch(34.69% 0.0186 98.75);
259
268
  --sidebar-ring: oklch(65.11% 0.2063 32.56);
260
269
  }
270
+
271
+ /* Art direction for generated imagery (GradientArt; ART_TOKENS in
272
+ themes/_contract.ts). Grainy monochrome photographic grounds (art-direction.md): soft
273
+ mid-grey tones, slightly lifted blacks, film grain — and none of the orange
274
+ accent, which belongs to typography and controls. Greys carry the ink's
275
+ warm cast.
276
+ Declared on every arm so a .dark or .light island derives from its own
277
+ tokens rather than inheriting this scope's resolved colours. */
278
+ [data-sable-theme="meridian"],
279
+ [data-sable-theme="meridian"] .dark,
280
+ [data-sable-theme="meridian"] .light {
281
+ --theme-art-1: oklch(from var(--foreground) 0.88 0.008 h);
282
+ --theme-art-2: oklch(from var(--foreground) 0.62 0.006 h);
283
+ --theme-art-3: oklch(from var(--foreground) 0.38 0.008 h);
284
+ --theme-art-4: oklch(from var(--foreground) 0.74 0.006 h);
285
+ --theme-art-ground: oklch(from var(--foreground) 0.28 0.008 h);
286
+ --theme-art-warp: 0.4;
287
+ --theme-art-softness: 0.92;
288
+ --theme-art-grain: 0.55;
289
+ }
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-Cn3lUpih.js';
1
+ import { T as ThemeManifest } from '../../_contract-C2M1SjBf.js';
2
2
 
3
3
  /** Original Sable preset; no third-party artwork or reference-site code. */
4
4
  declare const poppyTheme: ThemeManifest;
@@ -79,14 +79,18 @@
79
79
  --accent: oklch(87.83223% 0.046910 333.7803);
80
80
  --accent-foreground: oklch(27.91553% 0.070219 323.8141);
81
81
  --destructive: oklch(51.71923% 0.183122 13.5471);
82
+ --destructive-text: oklch(51.71923% 0.183122 13.5471);
82
83
  --destructive-foreground: oklch(100.00000% 0.000000 89.8756);
83
84
  --success: oklch(44.82304% 0.085811 158.9570);
85
+ --success-text: oklch(44.82304% 0.085811 158.9570);
84
86
  --success-foreground: oklch(100.00000% 0.000000 89.8756);
85
87
  --tone-standard: oklch(45.04846% 0.050543 265.7140);
86
88
  --tone-collectible: oklch(44.74226% 0.160756 301.6122);
87
89
  --tone-edition: oklch(48.37678% 0.151106 344.8805);
88
90
  --tone-info: oklch(47.10428% 0.117665 253.7666);
91
+ --tone-info-text: oklch(47.10428% 0.117665 253.7666);
89
92
  --tone-warning: oklch(48.46763% 0.102195 75.1153);
93
+ --tone-warning-text: oklch(48.46763% 0.102195 75.1153);
90
94
  --highlight: oklch(86.64637% 0.118528 86.6791);
91
95
  --tone-standard-foreground: oklch(100.00000% 0.000000 89.8756);
92
96
  --tone-collectible-foreground: oklch(100.00000% 0.000000 89.8756);
@@ -162,14 +166,18 @@
162
166
  --accent: oklch(45.76059% 0.076204 319.6398);
163
167
  --accent-foreground: oklch(96.99788% 0.031195 84.5922);
164
168
  --destructive: oklch(80.09751% 0.118613 6.8786);
169
+ --destructive-text: oklch(80.09751% 0.118613 6.8786);
165
170
  --destructive-foreground: oklch(27.91553% 0.070219 323.8141);
166
171
  --success: oklch(83.08052% 0.080357 158.1865);
172
+ --success-text: oklch(83.08052% 0.080357 158.1865);
167
173
  --success-foreground: oklch(27.91553% 0.070219 323.8141);
168
174
  --tone-standard: oklch(82.64273% 0.059760 270.2687);
169
175
  --tone-collectible: oklch(81.83406% 0.094036 306.4836);
170
176
  --tone-edition: oklch(82.56509% 0.096677 343.4480);
171
177
  --tone-info: oklch(85.19509% 0.074997 252.9332);
178
+ --tone-info-text: oklch(85.19509% 0.074997 252.9332);
172
179
  --tone-warning: oklch(86.64637% 0.118528 86.6791);
180
+ --tone-warning-text: oklch(86.64637% 0.118528 86.6791);
173
181
  --highlight: oklch(86.64637% 0.118528 86.6791);
174
182
  --highlight-foreground: oklch(27.91553% 0.070219 323.8141);
175
183
  --overlay: oklch(13.55719% 0.032037 309.1234);
@@ -188,3 +196,23 @@
188
196
  --tone-info-foreground: oklch(27.91553% 0.070219 323.8141);
189
197
  --tone-warning-foreground: oklch(27.91553% 0.070219 323.8141);
190
198
  }
199
+
200
+ /* Art direction for generated imagery (GradientArt; ART_TOKENS in
201
+ themes/_contract.ts). Playful: candy colours in crisp, wobbly blobs — sunny yellow,
202
+ bubblegum, grape, sky and a mint the palette lacks. Low softness keeps each
203
+ blob its own colour instead of melting into one.
204
+ Declared on every arm so a .dark or .light island derives from its own
205
+ tokens rather than inheriting this scope's resolved colours. */
206
+ [data-sable-theme="poppy"],
207
+ [data-sable-theme="poppy"] .dark,
208
+ [data-sable-theme="poppy"] .light {
209
+ --theme-art-1: oklch(from var(--highlight) 0.88 0.15 h);
210
+ --theme-art-2: oklch(from var(--tone-edition) 0.72 0.19 h);
211
+ --theme-art-3: oklch(from var(--tone-collectible) 0.64 0.2 h);
212
+ --theme-art-4: oklch(from var(--tone-info) 0.76 0.13 h);
213
+ --theme-art-5: oklch(0.85 0.12 168);
214
+ --theme-art-ground: oklch(from var(--tone-collectible) 0.45 0.16 h);
215
+ --theme-art-warp: 1;
216
+ --theme-art-softness: 0.5;
217
+ --theme-art-grain: 0;
218
+ }
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-Cn3lUpih.js';
1
+ import { T as ThemeManifest } from '../../_contract-C2M1SjBf.js';
2
2
 
3
3
  /** Original Sable preset; no third-party artwork or reference-site code. */
4
4
  declare const prismTheme: ThemeManifest;
@@ -79,14 +79,18 @@
79
79
  --accent: oklch(90.43693% 0.027753 253.2434);
80
80
  --accent-foreground: oklch(26.55994% 0.047330 255.4106);
81
81
  --destructive: oklch(51.62734% 0.183834 19.4551);
82
+ --destructive-text: oklch(51.62734% 0.183834 19.4551);
82
83
  --destructive-foreground: oklch(100.00000% 0.000000 89.8756);
83
84
  --success: oklch(45.54384% 0.081395 166.9049);
85
+ --success-text: oklch(45.54384% 0.081395 166.9049);
84
86
  --success-foreground: oklch(100.00000% 0.000000 89.8756);
85
87
  --tone-standard: oklch(44.17719% 0.043085 257.4210);
86
88
  --tone-collectible: oklch(45.52411% 0.167785 299.9667);
87
89
  --tone-edition: oklch(46.78672% 0.141265 343.5094);
88
90
  --tone-info: oklch(46.18475% 0.139719 259.8084);
91
+ --tone-info-text: oklch(46.18475% 0.139719 259.8084);
89
92
  --tone-warning: oklch(48.72795% 0.103396 73.7587);
93
+ --tone-warning-text: oklch(48.72795% 0.103396 73.7587);
90
94
  --highlight: oklch(87.60451% 0.060193 266.4847);
91
95
  --tone-standard-foreground: oklch(100.00000% 0.000000 89.8756);
92
96
  --tone-collectible-foreground: oklch(100.00000% 0.000000 89.8756);
@@ -158,14 +162,18 @@
158
162
  --accent: oklch(44.66150% 0.052212 253.9323);
159
163
  --accent-foreground: oklch(96.50786% 0.016545 259.4182);
160
164
  --destructive: oklch(79.47983% 0.122016 8.7083);
165
+ --destructive-text: oklch(79.47983% 0.122016 8.7083);
161
166
  --destructive-foreground: oklch(25.20350% 0.047984 259.9119);
162
167
  --success: oklch(82.23776% 0.085532 165.8233);
168
+ --success-text: oklch(82.23776% 0.085532 165.8233);
163
169
  --success-foreground: oklch(25.20350% 0.047984 259.9119);
164
170
  --tone-standard: oklch(82.02131% 0.046953 256.4806);
165
171
  --tone-collectible: oklch(77.28561% 0.118760 301.0442);
166
172
  --tone-edition: oklch(80.44093% 0.102099 341.6738);
167
173
  --tone-info: oklch(80.57243% 0.097939 259.4939);
174
+ --tone-info-text: oklch(80.57243% 0.097939 259.4939);
168
175
  --tone-warning: oklch(84.66478% 0.108333 84.1636);
176
+ --tone-warning-text: oklch(84.66478% 0.108333 84.1636);
169
177
  --highlight: oklch(82.28684% 0.088371 261.1846);
170
178
  --highlight-foreground: oklch(25.20350% 0.047984 259.9119);
171
179
  --overlay: oklch(14.80476% 0.014876 256.6891);
@@ -184,3 +192,21 @@
184
192
  --tone-info-foreground: oklch(25.20350% 0.047984 259.9119);
185
193
  --tone-warning-foreground: oklch(25.20350% 0.047984 259.9119);
186
194
  }
195
+
196
+ /* Art direction for generated imagery (GradientArt; ART_TOKENS in
197
+ themes/_contract.ts). Long soft airbrushed fields, pink to violet to periwinkle, with one
198
+ vivid indigo (art-direction.md). Diffuse, sourceless glow; no grain.
199
+ Declared on every arm so a .dark or .light island derives from its own
200
+ tokens rather than inheriting this scope's resolved colours. */
201
+ [data-sable-theme="prism"],
202
+ [data-sable-theme="prism"] .dark,
203
+ [data-sable-theme="prism"] .light {
204
+ --theme-art-1: oklch(from var(--tone-edition) 0.83 0.1 h);
205
+ --theme-art-2: oklch(from var(--tone-collectible) 0.68 0.16 h);
206
+ --theme-art-3: oklch(from var(--tone-info) 0.82 0.09 h);
207
+ --theme-art-4: oklch(from var(--primary) 0.55 0.2 h);
208
+ --theme-art-ground: oklch(from var(--primary) 0.35 0.14 h);
209
+ --theme-art-warp: 0.45;
210
+ --theme-art-softness: 1;
211
+ --theme-art-grain: 0;
212
+ }
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-Cn3lUpih.js';
1
+ import { T as ThemeManifest } from '../../_contract-C2M1SjBf.js';
2
2
 
3
3
  /**
4
4
  * Sable — the default theme. Concrete token values live in
@@ -220,8 +220,12 @@
220
220
  --tone-standard-dark: oklch(78% 0.205 168);
221
221
  --tone-info-dark: oklch(76% 0.165 235);
222
222
  --tone-warning-dark: oklch(82% 0.165 65);
223
- --highlight-dark: oklch(70% 0.285 318);
224
- --destructive-dark: oklch(68% 0.225 25);
223
+ /* A labelled fill: dropped to where white reads (4.8:1, Lc 72) rather than
224
+ lifted to where dark ink reads muddy (Lc 46). */
225
+ --highlight-dark: oklch(57% 0.28 318);
226
+ /* Status colours come in two: the fill carries a label (destructive sits low under white ink), and `-text` is the same colour drawn on a surface — text, borders, rings — kept light enough to read on a dark card. */
227
+ --destructive-dark: oklch(77.5% 0.129 25);
228
+ --destructive-fill-dark: oklch(59% 0.225 25);
225
229
  /* Success twins the standard tone's dark value today (same caribbean-green),
226
230
  but is a SEPARATE token a theme can retune independently of the post-type. */
227
231
  --success-dark: oklch(78% 0.205 168);
@@ -245,8 +249,10 @@
245
249
  --accent: var(--zinc-100);
246
250
  --accent-foreground: var(--zinc-950);
247
251
  --destructive: var(--torch-red-600);
252
+ --destructive-text: var(--torch-red-600);
248
253
  --destructive-foreground: var(--white);
249
254
  --success: var(--caribbean-green-600);
255
+ --success-text: var(--caribbean-green-600);
250
256
  --success-foreground: var(--zinc-50);
251
257
 
252
258
  --border: var(--zinc-200);
@@ -263,7 +269,9 @@
263
269
  --tone-collectible: var(--blue-gem-600);
264
270
  --tone-edition: var(--purple-heart-700);
265
271
  --tone-warning: var(--flush-orange-700);
272
+ --tone-warning-text: var(--flush-orange-700);
266
273
  --tone-info: var(--azure-radiance-600);
274
+ --tone-info-text: var(--azure-radiance-700); /* a step under the fill: 600 is 4.46:1 as text on muted */
267
275
  --highlight: var(--purple-heart-700);
268
276
 
269
277
  /* Tone ink — light mode the tone fills resolve to dark 600/700 steps, so the
@@ -422,15 +430,18 @@
422
430
  --muted-foreground: var(--zinc-400);
423
431
  --accent: var(--zinc-800);
424
432
  --accent-foreground: var(--zinc-50);
425
- --destructive: var(--destructive-dark);
426
- /* Dark `--destructive` is a light, vibrant red (so `text-destructive` reads on
427
- the near-black canvas). White ink on that fill is only ~3.3:1 (fails AA), so
428
- the legible ink flips to the dark neutral — exactly like the tone fills below.
429
- Dark ink on the light-red fill is ~5.9:1. */
430
- --destructive-foreground: var(--zinc-950);
431
- /* Like the tone fills, dark `--success` is a light, vibrant green, so the
432
- legible ink flips to the dark neutral (same model as --destructive above). */
433
+ --destructive: var(--destructive-fill-dark);
434
+ --destructive-text: var(--destructive-dark);
435
+ /* Dark destructive is two colours. The fill is a saturated red low enough for
436
+ white (4.6:1, Lc 75); dark ink on the old light-red fill passed the ratio but
437
+ read muddy (Lc 44). `text-destructive` and its borders and rings read
438
+ `--destructive-text`, the light, vibrant red that reads on the near-black
439
+ canvas. */
440
+ --destructive-foreground: var(--white);
441
+ /* Dark `--success` is a light, vibrant green that works as both fill and text,
442
+ so the legible ink flips to the dark neutral. */
433
443
  --success: var(--success-dark);
444
+ --success-text: var(--success-dark);
434
445
  --success-foreground: var(--zinc-950);
435
446
 
436
447
  --border: oklch(from var(--zinc-700) l c h / 0.4);
@@ -444,7 +455,9 @@
444
455
  --tone-collectible: var(--tone-collectible-dark);
445
456
  --tone-edition: var(--tone-edition-dark);
446
457
  --tone-warning: var(--tone-warning-dark);
458
+ --tone-warning-text: var(--tone-warning-dark);
447
459
  --tone-info: var(--tone-info-dark);
460
+ --tone-info-text: var(--tone-info-dark);
448
461
  --highlight: var(--highlight-dark);
449
462
 
450
463
  /* Tone ink — dark mode the tone fills resolve to lighter dark-tuned values,
@@ -455,7 +468,7 @@
455
468
  --tone-edition-foreground: var(--zinc-950);
456
469
  --tone-warning-foreground: var(--zinc-950);
457
470
  --tone-info-foreground: var(--zinc-950);
458
- --highlight-foreground: var(--zinc-950);
471
+ --highlight-foreground: var(--zinc-50);
459
472
  --overlay: var(--zinc-950);
460
473
  --overlay-foreground: var(--zinc-50);
461
474
 
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-Cn3lUpih.js';
1
+ import { T as ThemeManifest } from '../../_contract-C2M1SjBf.js';
2
2
 
3
3
  /**
4
4
  * Verdant — a Sable theme. Concrete token values live in
@@ -111,8 +111,10 @@
111
111
  --accent: oklch(91.5% 0.014 170);
112
112
  --accent-foreground: oklch(24% 0.035 175);
113
113
  --destructive: oklch(51% 0.19 22);
114
+ --destructive-text: oklch(51% 0.19 22);
114
115
  --destructive-foreground: oklch(98.6% 0.004 106);
115
116
  --success: oklch(50% 0.11 173);
117
+ --success-text: oklch(50% 0.11 173);
116
118
  --success-foreground: oklch(98.6% 0.004 106);
117
119
  --tone-standard: oklch(49% 0.018 175);
118
120
  --tone-standard-foreground: oklch(98.6% 0.004 106);
@@ -121,8 +123,10 @@
121
123
  --tone-edition: oklch(50% 0.11 173);
122
124
  --tone-edition-foreground: oklch(98.6% 0.004 106);
123
125
  --tone-info: oklch(51% 0.12 250);
126
+ --tone-info-text: oklch(51% 0.12 250);
124
127
  --tone-info-foreground: oklch(98.6% 0.004 106);
125
128
  --tone-warning: oklch(53% 0.12 65);
129
+ --tone-warning-text: oklch(51% 0.12 65); /* a step under the fill: 53% is 4.44:1 as text on muted */
126
130
  --tone-warning-foreground: oklch(98.6% 0.004 106);
127
131
  --highlight: oklch(90% 0.05 100);
128
132
  --highlight-foreground: oklch(24% 0.035 175);
@@ -186,9 +190,12 @@
186
190
  --secondary-foreground: oklch(98.6% 0.004 106);
187
191
  --accent: oklch(33% 0.045 175); /* above --popover: a hover row or chip inside a menu/dialog must still read */
188
192
  --accent-foreground: oklch(98.6% 0.004 106);
189
- --destructive: oklch(64% 0.19 22);
190
- --destructive-foreground: oklch(16% 0.03 175);
191
- --success: oklch(72% 0.13 173);
193
+ /* Status colours come in two: the fill carries a label (destructive sits low under white ink), and `-text` is the same colour drawn on a surface — text, borders, rings — kept light enough to read on a dark card. */
194
+ --destructive: oklch(57.5% 0.19 22);
195
+ --destructive-text: oklch(78% 0.125 22);
196
+ --destructive-foreground: oklch(98.6% 0.004 106);
197
+ --success: oklch(74% 0.13 173);
198
+ --success-text: oklch(74% 0.13 173);
192
199
  --success-foreground: oklch(16% 0.03 175);
193
200
  --tone-standard: oklch(72% 0.015 175);
194
201
  --tone-standard-foreground: oklch(16% 0.03 175);
@@ -196,9 +203,11 @@
196
203
  --tone-collectible-foreground: oklch(16% 0.03 175);
197
204
  --tone-edition: oklch(76% 0.12 173);
198
205
  --tone-edition-foreground: oklch(16% 0.03 175);
199
- --tone-info: oklch(74% 0.11 250);
206
+ --tone-info: oklch(76% 0.11 250);
207
+ --tone-info-text: oklch(76% 0.11 250);
200
208
  --tone-info-foreground: oklch(16% 0.03 175);
201
209
  --tone-warning: oklch(80% 0.13 75);
210
+ --tone-warning-text: oklch(80% 0.13 75);
202
211
  --tone-warning-foreground: oklch(16% 0.03 175);
203
212
  --highlight: oklch(40% 0.06 90);
204
213
  --highlight-foreground: oklch(98.6% 0.004 106);
@@ -213,3 +222,24 @@
213
222
  --sidebar-border: oklch(30% 0.03 175);
214
223
  --sidebar-ring: oklch(85% 0.015 170);
215
224
  }
225
+
226
+ /* Art direction for generated imagery (GradientArt; ART_TOKENS in
227
+ themes/_contract.ts). Out-of-focus stained glass (art-direction.md): saturated teal,
228
+ orange, magenta, blue and green zones melting into each other, glowing from
229
+ within, each zone keeping its own colour. Colour lives in imagery only, so the art is far brighter than the
230
+ two-tone chrome. No grain.
231
+ Declared on every arm so a .dark or .light island derives from its own
232
+ tokens rather than inheriting this scope's resolved colours. */
233
+ [data-sable-theme="verdant"],
234
+ [data-sable-theme="verdant"] .dark,
235
+ [data-sable-theme="verdant"] .light {
236
+ --theme-art-1: oklch(from var(--tone-edition) 0.76 0.15 h);
237
+ --theme-art-2: oklch(from var(--tone-warning) 0.76 0.16 h);
238
+ --theme-art-3: oklch(0.66 0.24 355);
239
+ --theme-art-4: oklch(from var(--tone-info) 0.62 0.17 h);
240
+ --theme-art-5: oklch(0.8 0.19 140);
241
+ --theme-art-ground: oklch(from var(--tone-collectible) 0.24 0.05 h);
242
+ --theme-art-warp: 0.9;
243
+ --theme-art-softness: 0.62;
244
+ --theme-art-grain: 0;
245
+ }
@@ -44,7 +44,7 @@ export function DangerZoneBlock({ className, framed = true, onDelete }: DangerZo
44
44
  Permanently remove your profile, editions, and sales history. Collectors keep the editions they own.
45
45
  </CardDescription>
46
46
  </CardHeader>
47
- <CardFooter className="justify-end">
47
+ <CardFooter actions className="justify-end">
48
48
  <Button variant="destructive" onClick={() => setOpen(true)}>
49
49
  Delete account
50
50
  </Button>
@@ -66,7 +66,7 @@ export function EventCardBlock({ className, framed = true, onRespond }: EventCar
66
66
  <CardFooter className="flex-wrap justify-between gap-3">
67
67
  <AvatarGroup aria-label={`${event.total} attendees`} total={event.total} size="sm" surface={framed ? "card" : undefined} fit>
68
68
  {event.attendees.map((initials) => (
69
- <Avatar key={initials} fallback={initials} alt={initials} />
69
+ <Avatar variant="art" key={initials} fallback={initials} alt={initials} />
70
70
  ))}
71
71
  </AvatarGroup>
72
72
  <div className="flex gap-2">
@@ -52,7 +52,7 @@ export function FeedbackFormBlock({ className, framed = true, onDone }: Feedback
52
52
  </ToggleGroup>
53
53
  <Textarea placeholder="What worked, what didn’t…" aria-label="Notes" rows={3} value={notes} onChange={(e) => setNotes(e.target.value)} />
54
54
  </CardContent>
55
- <CardFooter className="justify-end">
55
+ <CardFooter actions className="justify-end">
56
56
  <Button variant="ghost" size="sm" onClick={() => onDone?.(false)}>
57
57
  Skip
58
58
  </Button>
@@ -41,7 +41,7 @@ export function InviteTeamBlock({ className, framed = true, onSend }: InviteTeam
41
41
  <CardContent className="flex flex-col gap-4">
42
42
  <AvatarGroup aria-label={`${collaborators.length} collaborators`} size="sm" surface={framed ? "card" : undefined}>
43
43
  {collaborators.map((initials) => (
44
- <Avatar key={initials} fallback={initials} alt={initials} />
44
+ <Avatar variant="art" key={initials} fallback={initials} alt={initials} />
45
45
  ))}
46
46
  </AvatarGroup>
47
47
  <form
@@ -21,24 +21,10 @@ A now-playing card: cover, title and artist as an Entity with a Like action, a p
21
21
 
22
22
  ```tsx
23
23
  import { useState } from "react"
24
- import { Button, Card, CardContent, cn, Entity, Icon, Slider } from "@sableui/react"
24
+ import { Button, Card, CardContent, cn, Entity, GradientArt, Icon, Slider } from "@sableui/react"
25
25
 
26
26
  const track = { title: "Low tide, high noon", artist: "Juno Avery", position: 38, elapsed: "1:24", remaining: "-2:16" }
27
27
 
28
- /**
29
- * Placeholder cover painted from the palette — the theme's decorative tones
30
- * over the brand, so it follows every look. Replace with the cover image.
31
- */
32
- const ART_TONES = ["info", "collectible", "edition"]
33
- const ART_SPOTS = [[20, 20, 70], [85, 30, 60], [60, 90, 65]]
34
- const cover = [
35
- ...ART_SPOTS.map(
36
- ([x, y, reach], i) =>
37
- // design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
38
- `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}%)`,
39
- ),
40
- "var(--primary)",
41
- ].join(", ")
42
28
 
43
29
  export interface MediaPlayerBlockProps {
44
30
  className?: string
@@ -59,7 +45,8 @@ export function MediaPlayerBlock({ className, framed = true, onPlayingChange, on
59
45
  card already pads it. */}
60
46
  <Entity
61
47
  className="p-0"
62
- leading={<div aria-hidden className="size-14 rounded-md" style={{ background: cover }} />}
48
+ // Placeholder cover painted from the theme. Replace with the cover image.
49
+ leading={<GradientArt aria-hidden seed={track.title} className="size-14 rounded-md" />}
63
50
  title={track.title}
64
51
  subtitle={track.artist}
65
52
  action={
@@ -9,7 +9,7 @@ An activity list: Entity rows with the actor's Avatar, who did what, and a compa
9
9
  ## Notes
10
10
 
11
11
  - Rows are Card + CardList + Entity, inset from the card's padding at any density.
12
- - Unread is a quiet info dot after the time — the list stays calm when most items are unread.
12
+ - Unread is a quiet info dot before the time, so the times keep one right edge — the list stays calm when most items are unread.
13
13
  - framed={false} drops the surface for a Popover, which already is one.
14
14
 
15
15
  ## Accessibility
@@ -40,7 +40,7 @@ export function NotificationListBlock({ className, framed = true }: Notification
40
40
  {notifications.map((n) => (
41
41
  <Entity
42
42
  key={n.name}
43
- leading={<Avatar size="sm" fallback={n.initials} alt={n.name} />}
43
+ leading={<Avatar variant="art" size="sm" fallback={n.initials} alt={n.name} />}
44
44
  title={
45
45
  <>
46
46
  {n.name}
@@ -50,8 +50,8 @@ export function NotificationListBlock({ className, framed = true }: Notification
50
50
  subtitle={n.what}
51
51
  action={
52
52
  <span className="flex items-center gap-1.5 text-caption text-muted-foreground">
53
- {n.when}
54
53
  {n.unread ? <StatusDot tone="info" /> : null}
54
+ {n.when}
55
55
  </span>
56
56
  }
57
57
  />
@@ -20,25 +20,10 @@ A product card: edge-to-edge artwork, title and maker, an edition Badge, and the
20
20
  ## Source
21
21
 
22
22
  ```tsx
23
- import { Badge, Button, Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle, cn } from "@sableui/react"
23
+ import { Badge, Button, Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle, cn, GradientArt } from "@sableui/react"
24
24
 
25
25
  const product = { title: "Untitled (gold rush)", maker: "Mara Okafor", edition: "12 of 50", price: "0.42 SOL" }
26
26
 
27
- /**
28
- * Placeholder artwork painted from the palette — a soft mesh of the theme's
29
- * decorative tones over the brand, so it follows every look. Replace the art
30
- * <div> with your product's image.
31
- */
32
- const ART_TONES = ["info", "collectible", "edition", "standard"]
33
- const ART_SPOTS = [[18, 22, 58], [82, 18, 55], [70, 88, 60], [12, 92, 55]]
34
- const artwork = [
35
- ...ART_SPOTS.map(
36
- ([x, y, reach], i) =>
37
- // design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
38
- `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}%)`,
39
- ),
40
- "var(--primary)",
41
- ].join(", ")
42
27
 
43
28
  export interface ProductCardBlockProps {
44
29
  className?: string
@@ -51,7 +36,8 @@ export interface ProductCardBlockProps {
51
36
  export function ProductCardBlock({ className, framed = true, onCollect }: ProductCardBlockProps) {
52
37
  return (
53
38
  <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-xs overflow-hidden", className)}>
54
- <div aria-hidden className="aspect-[4/3]" style={{ background: artwork }} />
39
+ {/* Placeholder art painted from the theme, so it follows every look. Replace with the product image. */}
40
+ <GradientArt aria-hidden seed={product.title} className="aspect-[4/3]" />
55
41
  <CardHeader>
56
42
  <CardTitle>{product.title}</CardTitle>
57
43
  <CardDescription>{product.maker}</CardDescription>
@@ -21,7 +21,7 @@ A profile card: a cover banner, an avatar rising into it, Share and Follow, the
21
21
 
22
22
  ```tsx
23
23
  import { useState } from "react"
24
- import { Avatar, Button, Card, CardContent, CardDescription, CardTitle, cn, Icon } from "@sableui/react"
24
+ import { Avatar, Button, Card, CardContent, CardDescription, CardTitle, cn, GradientArt, Icon } from "@sableui/react"
25
25
 
26
26
  const profile = {
27
27
  name: "Juno Avery",
@@ -34,20 +34,6 @@ const profile = {
34
34
  ],
35
35
  }
36
36
 
37
- /**
38
- * Placeholder banner painted from the palette — the theme's decorative tones
39
- * over the brand, so it follows every look. Replace with the person's cover image.
40
- */
41
- const ART_TONES = ["info", "collectible", "edition", "standard"]
42
- const ART_SPOTS = [[10, 50, 60], [50, 0, 55], [90, 60, 60], [40, 100, 50]]
43
- const banner = [
44
- ...ART_SPOTS.map(
45
- ([x, y, reach], i) =>
46
- // design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
47
- `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}%)`,
48
- ),
49
- "var(--primary)",
50
- ].join(", ")
51
37
 
52
38
  export interface ProfileCardBlockProps {
53
39
  className?: string
@@ -62,12 +48,13 @@ export function ProfileCardBlock({ className, framed = true, onFollowChange, onS
62
48
  const [following, setFollowing] = useState(false)
63
49
  return (
64
50
  <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-xs overflow-hidden", className)}>
65
- <div aria-hidden className="h-20" style={{ background: banner }} />
51
+ {/* Placeholder banner painted from the theme, so it follows every look. Replace with the person's cover image. */}
52
+ <GradientArt aria-hidden seed={profile.handle} className="h-20" />
66
53
  {/* pt-0: the avatar rises into the banner above, so the content
67
54
  starts at the banner's edge rather than a full pad below it. */}
68
55
  <CardContent className="flex flex-col gap-3 pt-0">
69
56
  <div className="flex items-start justify-between">
70
- <Avatar size="lg" fallback={profile.initials} alt={profile.name} className="-mt-6 ring-4 ring-card" />
57
+ <Avatar variant="art" size="lg" fallback={profile.initials} alt={profile.name} className="-mt-6 ring-4 ring-card" />
71
58
  <div className="flex gap-1.5 pt-3">
72
59
  <Button variant="ghost" size="icon-sm" aria-label="Share profile" onClick={onShare}>
73
60
  <Icon name="share" />
@@ -64,7 +64,7 @@ export function ProfileFormBlock({ className, framed = true, onSave }: ProfileFo
64
64
  <CardContent>
65
65
  <Stack gap={3}>
66
66
  <Row gap={2}>
67
- <Avatar size="lg" fallback={profile.initials} alt={profile.name} />
67
+ <Avatar variant="art" size="lg" fallback={profile.initials} alt={profile.name} />
68
68
  <Stack gap={0.5} align="start">
69
69
  <Row gap={1}>
70
70
  <Button type="button" variant="outline" size="sm">
@@ -104,7 +104,7 @@ export function ProfileFormBlock({ className, framed = true, onSave }: ProfileFo
104
104
  </Grid>
105
105
  </Stack>
106
106
  </CardContent>
107
- <CardFooter className="justify-end">
107
+ <CardFooter actions className="justify-end">
108
108
  <Button type="submit">Save profile</Button>
109
109
  </CardFooter>
110
110
  </Form>
@@ -54,7 +54,7 @@ export function TeamMembersBlock({ className, framed = true, onInvite, onMemberA
54
54
  {members.map((m) => (
55
55
  <Entity
56
56
  key={m.id}
57
- leading={<Avatar size="sm" fallback={m.initials} alt={m.name} />}
57
+ leading={<Avatar variant="art" size="sm" fallback={m.initials} alt={m.name} />}
58
58
  title={m.name}
59
59
  subtitle={m.email}
60
60
  action={
@@ -50,7 +50,7 @@ export function TestimonialBlock({ className }: TestimonialBlockProps) {
50
50
  <blockquote className="text-balance text-heading-3 sm:text-heading-2">“{story.quote}”</blockquote>
51
51
  </Stack>
52
52
  <figcaption className="flex items-center gap-3">
53
- <Avatar size="lg" fallback={story.initials} alt={story.name} />
53
+ <Avatar variant="art" size="lg" fallback={story.initials} alt={story.name} />
54
54
  <div>
55
55
  <p className="text-title-sm">{story.name}</p>
56
56
  <p className="text-body-sm text-muted-foreground">{story.role}</p>
@@ -12,6 +12,7 @@ Use Avatar to represent a person, team, or account with a compact visual identit
12
12
 
13
13
  ### Best practices
14
14
 
15
+ - Do: Use `variant="art"` where people without photos sit side by side — a member list, an activity feed, a chat. Seeded by the person, each gets their own colour, so faces stay tellable apart at a glance; the neutral fallback makes everyone the same grey circle.
15
16
  - Do: Provide meaningful alt text for profile images, or an empty alt when the adjacent text already names the person.
16
17
  - Do: Use initials or a simple glyph fallback so missing images still communicate identity.
17
18
  - Do: Use consistent sizes within a group; reserve lg for profile headers or major identity moments.
@@ -50,14 +51,17 @@ Use Avatar to represent a person, team, or account with a compact visual identit
50
51
  | fallbackDelay | number | — | Delay before showing the fallback, in ms — avoids a flash on fast loads. |
51
52
  | status | ReactNode | — | A presence indicator pinned to the bottom-end edge, usually <StatusDot tone="success" />. It sits outside the clipped circle and wears a ring in the surface colour (--avatar-group-ring, else the page background). Use this instead of absolutely positioning a dot over an Avatar. |
52
53
  | size | "sm" \| "md" \| "lg" | "md" | Square footprint: 32px / 40px / 48px. |
54
+ | variant | "neutral" \| "art" | "neutral" | What the fallback looks like. neutral: initials on the muted surface. art: initials over GradientArt seeded by the person, so each person without a photo gets their own stable colour from the theme. |
55
+ | seed | string \| number | — | The art variant's seed. Defaults to the fallback text, then `alt`, so a person keeps one colour wherever they appear; pass a user id to tell apart two people who share initials. |
53
56
 
54
57
  ## Variants
55
58
 
56
59
  - **size**: sm, md, lg
60
+ - **variant**: neutral, art
57
61
 
58
62
  ## Tokens used
59
63
 
60
- `muted`, `muted-foreground`, `background`, `card`, `popover`
64
+ `muted`, `muted-foreground`, `background`, `card`, `popover`, `overlay`, `overlay-foreground`
61
65
 
62
66
  ## Accessibility
63
67
 
@@ -90,6 +94,14 @@ import { Avatar } from "@sableui/react"
90
94
  <Avatar fallback="AM" alt="Alex Morgan" />
91
95
  ```
92
96
 
97
+ ### Art fallback
98
+
99
+ ```tsx
100
+ import { Avatar } from "@sableui/react"
101
+
102
+ <Avatar variant="art" fallback="AM" alt="Alex Morgan" />
103
+ ```
104
+
93
105
  ### Sizes
94
106
 
95
107
  ```tsx