@saasflare/ui 3.1.2 → 3.2.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 (56) hide show
  1. package/README.md +68 -2
  2. package/dist/{button-0Bdl7Nqm.d.ts → button-BA7OcXqy.d.mts} +12 -17
  3. package/dist/{button-Brb4BhPO.d.mts → button-Bfg2Tnvx.d.ts} +12 -17
  4. package/dist/{chunk-D5LKWKG7.js → chunk-2GOPD64T.js} +117 -89
  5. package/dist/{chunk-56PMDC5F.mjs → chunk-2ONA6OMO.mjs} +29 -40
  6. package/dist/{chunk-RW2S3KNB.mjs → chunk-5C65JNGY.mjs} +7 -6
  7. package/dist/{chunk-NPNSPYTX.js → chunk-7UD3SGPP.js} +28 -39
  8. package/dist/chunk-GI6VN7XU.mjs +2143 -0
  9. package/dist/{chunk-FT66KYRN.js → chunk-ITALEYDI.js} +2 -2
  10. package/dist/{chunk-4BOMMZEY.js → chunk-JC7EIEGI.js} +14 -13
  11. package/dist/chunk-N65HIOBD.js +234 -0
  12. package/dist/{chunk-EJHYM2HP.mjs → chunk-OZAWULTM.mjs} +1 -1
  13. package/dist/chunk-R3AVBLJ3.js +2207 -0
  14. package/dist/{chunk-WRONFPRI.mjs → chunk-RMQBB72G.mjs} +118 -91
  15. package/dist/chunk-XNDTCYSO.mjs +211 -0
  16. package/dist/{dialog-BmY55WSX.d.ts → dialog-CZRwrqDa.d.ts} +2 -2
  17. package/dist/{dialog-CcaHMAsS.d.mts → dialog-Cr0becOL.d.mts} +2 -2
  18. package/dist/entries/calendar.d.mts +3 -3
  19. package/dist/entries/calendar.d.ts +3 -3
  20. package/dist/entries/calendar.js +13 -214
  21. package/dist/entries/calendar.mjs +5 -196
  22. package/dist/entries/carousel.d.mts +3 -3
  23. package/dist/entries/carousel.d.ts +3 -3
  24. package/dist/entries/carousel.js +17 -14
  25. package/dist/entries/carousel.mjs +10 -7
  26. package/dist/entries/chart.d.mts +1 -1
  27. package/dist/entries/chart.d.ts +1 -1
  28. package/dist/entries/chart.js +11 -11
  29. package/dist/entries/chart.mjs +1 -1
  30. package/dist/entries/command.d.mts +3 -3
  31. package/dist/entries/command.d.ts +3 -3
  32. package/dist/entries/command.js +21 -19
  33. package/dist/entries/command.mjs +8 -6
  34. package/dist/entries/drawer.d.mts +1 -1
  35. package/dist/entries/drawer.d.ts +1 -1
  36. package/dist/entries/drawer.js +9 -9
  37. package/dist/entries/drawer.mjs +2 -2
  38. package/dist/entries/input-otp.d.mts +2 -2
  39. package/dist/entries/input-otp.d.ts +2 -2
  40. package/dist/entries/input-otp.js +10 -8
  41. package/dist/entries/input-otp.mjs +6 -4
  42. package/dist/entries/resizable.d.mts +3 -2
  43. package/dist/entries/resizable.d.ts +3 -2
  44. package/dist/entries/resizable.js +8 -6
  45. package/dist/entries/resizable.mjs +6 -4
  46. package/dist/index.d.mts +974 -31
  47. package/dist/index.d.ts +974 -31
  48. package/dist/index.js +2992 -554
  49. package/dist/index.mjs +2486 -199
  50. package/dist/{use-saasflare-props-NrM2Glmp.d.mts → use-saasflare-props-BrjMhU0U.d.mts} +53 -4
  51. package/dist/{use-saasflare-props-NrM2Glmp.d.ts → use-saasflare-props-BrjMhU0U.d.ts} +53 -4
  52. package/package.json +4 -3
  53. package/styles/aurora.css +47 -0
  54. package/styles/palettes.css +483 -3
  55. package/styles/surfaces.css +89 -10
  56. package/styles/theme.css +11 -5
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @fileoverview 16 preset brand palettes — activate via [data-palette="id"] on <html>.
2
+ * @fileoverview 20 preset brand palettes — activate via [data-palette="id"] on <html>.
3
3
  * @module packages/ui/styles/palettes
4
4
  * @package ui
5
5
  * @reviewed 2026-04-19
@@ -134,18 +134,498 @@
134
134
  :root[data-palette="ruby"] { --primary-h: 10; --primary-c: 0.21; --primary-l: 0.58; }
135
135
  :root[data-palette="ruby"].dark { --primary-l: 0.68; }
136
136
 
137
+ /* ─── Sky (HeroUI-style pastel blue) ────────────── */
138
+ :root[data-palette="sky"] { --primary-h: 210; --primary-c: 0.12; --primary-l: 0.72; }
139
+ :root[data-palette="sky"].dark { --primary-l: 0.80; }
140
+
141
+ /* ─── Lavender (HeroUI-style pastel violet) ─────── */
142
+ :root[data-palette="lavender"] { --primary-h: 280; --primary-c: 0.10; --primary-l: 0.72; }
143
+ :root[data-palette="lavender"].dark { --primary-l: 0.80; }
144
+
145
+ /* ─── Mint (HeroUI-style pastel green) ──────────── */
146
+ :root[data-palette="mint"] { --primary-h: 155; --primary-c: 0.10; --primary-l: 0.72; }
147
+ :root[data-palette="mint"].dark { --primary-l: 0.80; }
148
+
149
+ /* ─── Snow (near-white, ultra-low chroma) ───────── */
150
+ /* For an outline-forward, low-contrast feel. Primary lands near-white so the
151
+ * UI leans on borders/dividers instead of filled accent surfaces. */
152
+ :root[data-palette="snow"] {
153
+ --primary-h: 0;
154
+ --primary-c: 0.005;
155
+ --primary-l: 0.93;
156
+ --neutral-h: 0;
157
+ --neutral-c: 0;
158
+ --primary-foreground: oklch(0.2 0 0); /* dark text on near-white */
159
+ }
160
+ :root[data-palette="snow"].dark {
161
+ --primary-l: 0.95;
162
+ --primary-foreground: oklch(0.2 0 0);
163
+ }
164
+
165
+ /* ─── Craivo (cool cyan → magenta brand) ─────────── */
166
+ /* Brand hue anchored in cool cyan-blue; gradient-text / progress should
167
+ * layer to magenta via component-level from/to props. Neutrals are pure
168
+ * grey (no brand tint). Dark canvas is pushed below the system default
169
+ * to land near-black, matching the Craivo landing page. */
170
+ :root[data-palette="craivo"] {
171
+ --primary-h: 200;
172
+ --primary-c: 0.18;
173
+ --primary-l: 0.62;
174
+ --neutral-h: 200;
175
+ --neutral-c: 0;
176
+ }
177
+ :root[data-palette="craivo"].dark {
178
+ --primary-l: 0.72;
179
+ --background: oklch(0.12 0 0); /* near-black canvas */
180
+ }
181
+
182
+ /* Chart palette: spans cyan → magenta with a lime success-accent, matching
183
+ * the electric-cool spectrum used across Craivo's metric/progress visuals. */
184
+ :root[data-palette="craivo"] {
185
+ --chart-1: oklch(0.70 0.18 200); /* cyan (brand) */
186
+ --chart-2: oklch(0.68 0.22 330); /* magenta */
187
+ --chart-3: oklch(0.78 0.20 140); /* lime */
188
+ --chart-4: oklch(0.72 0.20 60); /* amber */
189
+ --chart-5: oklch(0.66 0.22 290); /* violet */
190
+ }
191
+
137
192
  /* ============================================
138
193
  * Chart palette overrides for achromatic palettes
139
194
  *
140
195
  * With --primary-c: 0, the derived chart colors collapse to grayscale.
141
- * Give Ink and Stone a fixed distinguishable 5-hue palette.
196
+ * Give Ink, Stone, Black and Snow a fixed distinguishable 5-hue palette.
142
197
  * ============================================ */
143
198
  :root[data-palette="ink"],
144
199
  :root[data-palette="stone"],
145
- :root[data-palette="black"] {
200
+ :root[data-palette="black"],
201
+ :root[data-palette="snow"] {
146
202
  --chart-1: oklch(0.60 0.18 230); /* blue */
147
203
  --chart-2: oklch(0.65 0.17 55); /* orange */
148
204
  --chart-3: oklch(0.60 0.15 155); /* green */
149
205
  --chart-4: oklch(0.58 0.20 25); /* red */
150
206
  --chart-5: oklch(0.60 0.18 290); /* violet */
151
207
  }
208
+
209
+ /* ─── Colorful (minimal at rest, soft pastel-gradient glow on hover) ─── */
210
+ /* Inspired by htgf.de — surfaces are quiet and outline-forward at rest,
211
+ * then on hover bloom with a SOFT pastel gradient (peach → pink → lavender
212
+ * → cyan) wrapped in a matching color-aura glow. Text stays dark; the
213
+ * effect reads as "warm cloud appears behind the button." Token --primary
214
+ * is kept as a warm red so swatches in palette pickers stay legible; the
215
+ * pastel sweep lives only in the hover rules below. */
216
+ :root[data-palette="colorful"] {
217
+ --primary-h: 25;
218
+ --primary-c: 0.21;
219
+ --primary-l: 0.58;
220
+ --neutral-h: 0;
221
+ --neutral-c: 0;
222
+
223
+ /* --primary itself is transparent — primary surfaces don't carry a
224
+ * solid color. Instead, the resting state gets the gradient as an
225
+ * almost-transparent background-image wash (see rules below). On
226
+ * hover/active, the full-opacity gradient kicks in. */
227
+ --primary: transparent;
228
+ --primary-foreground: oklch(0.2 0 0);
229
+
230
+ /* Full-opacity pastel sweep — peach, blush, lavender, cyan. Used on
231
+ * hover and on active/checked states. */
232
+ --colorful-gradient: linear-gradient(95deg, #ffd4a3 0%, #ffb8c5 35%, #d4bce8 65%, #b8d8e5 100%);
233
+
234
+ /* Almost-transparent variant of the same sweep for resting "primary
235
+ * fill" surfaces (~18% alpha). Visible enough to identify the
236
+ * surface, quiet enough not to compete with surrounding content. */
237
+ --colorful-gradient-faint: linear-gradient(95deg,
238
+ rgba(255, 212, 163, 0.20) 0%,
239
+ rgba(255, 184, 197, 0.20) 35%,
240
+ rgba(212, 188, 232, 0.20) 65%,
241
+ rgba(184, 216, 229, 0.20) 100%);
242
+ }
243
+ :root[data-palette="colorful"].dark {
244
+ --primary-l: 0.68;
245
+ --primary: transparent;
246
+ --primary-foreground: oklch(0.95 0 0);
247
+ /* Slightly stronger alpha in dark mode so the faint wash still
248
+ * reads against the darker canvas. */
249
+ --colorful-gradient-faint: linear-gradient(95deg,
250
+ rgba(255, 212, 163, 0.14) 0%,
251
+ rgba(255, 184, 197, 0.14) 35%,
252
+ rgba(212, 188, 232, 0.14) 65%,
253
+ rgba(184, 216, 229, 0.14) 100%);
254
+ }
255
+
256
+ /* Apply the faint gradient as the resting "primary fill". Covers both
257
+ * the token-driven path (Buttons use bg-[var(--intent)]) and the direct
258
+ * utility-class path (Tooltip / Calendar selected days / PricingCard
259
+ * pill / scroll-to-top / etc. use bare bg-primary). Hover and active
260
+ * states win via higher-specificity rules further down. */
261
+ :root[data-palette="colorful"] [data-slot="button"][data-intent="primary"][data-variant="solid"]:not(:hover):not(:disabled),
262
+ :root[data-palette="colorful"] [data-slot="button"][data-intent="primary"][data-variant="soft"]:not(:hover):not(:disabled),
263
+ :root[data-palette="colorful"] [data-slot="button"][data-intent="primary"][data-variant="shadow"]:not(:hover):not(:disabled),
264
+ :root[data-palette="colorful"] .bg-primary:not(:hover) {
265
+ background-image: var(--colorful-gradient-faint);
266
+ }
267
+
268
+ /* Palette-scoped hover retrofits. Specificity 0,2,0 from the
269
+ * :root[data-palette] prefix beats the variant utility's 0,1,0 — same
270
+ * mechanism every preset above uses to override base tokens. Scoped via
271
+ * the data-slot + data-variant attributes the components already emit. */
272
+
273
+ /* With --primary: transparent, the primary-intent token chain resolves
274
+ * --intent-text = --primary = transparent, which would render invisible
275
+ * text on every non-filled variant (outline, ghost, link, glass). Route
276
+ * primary-intent text through --foreground so it always reads against
277
+ * the page background. The fill (--intent) stays transparent so solid
278
+ * surfaces still behave as the user requested. */
279
+ :root[data-palette="colorful"] [data-intent="primary"] {
280
+ --intent-text: var(--foreground);
281
+ }
282
+
283
+ /* Discoverability for transparent primary buttons. Solid / soft / shadow
284
+ * variants of primary intent would otherwise be invisible at rest — the
285
+ * fill is transparent and they ship without a border. Add a hairline
286
+ * border + soft drop shadow so the button's footprint reads against the
287
+ * page without compromising the "quiet at rest" feel. Outline already
288
+ * has its own border; glass has the surface treatment; ghost / link are
289
+ * intentionally chromeless and stay so. */
290
+ :root[data-palette="colorful"] [data-slot="button"][data-variant="solid"][data-intent="primary"]:not(:hover):not(:disabled),
291
+ :root[data-palette="colorful"] [data-slot="button"][data-variant="soft"][data-intent="primary"]:not(:hover):not(:disabled),
292
+ :root[data-palette="colorful"] [data-slot="button"][data-variant="shadow"][data-intent="primary"]:not(:hover):not(:disabled) {
293
+ border: 1px solid oklch(0 0 0 / 0.12);
294
+ box-shadow:
295
+ 0 1px 2px oklch(0 0 0 / 0.06),
296
+ 0 4px 12px -2px oklch(0 0 0 / 0.05);
297
+ }
298
+ :root[data-palette="colorful"].dark [data-slot="button"][data-variant="solid"][data-intent="primary"]:not(:hover):not(:disabled),
299
+ :root[data-palette="colorful"].dark [data-slot="button"][data-variant="soft"][data-intent="primary"]:not(:hover):not(:disabled),
300
+ :root[data-palette="colorful"].dark [data-slot="button"][data-variant="shadow"][data-intent="primary"]:not(:hover):not(:disabled) {
301
+ border: 1px solid oklch(1 0 0 / 0.14);
302
+ box-shadow:
303
+ 0 1px 2px oklch(0 0 0 / 0.25),
304
+ 0 4px 12px -2px oklch(0 0 0 / 0.20);
305
+ }
306
+
307
+ /* Colorful hover bloom — shared across every Button variant when the
308
+ * intent is primary, plus every outline-variant button regardless of
309
+ * intent. Variants targeted here:
310
+ * - outline (all intents): minimal-at-rest is already the variant's
311
+ * resting style; hover swaps the passive tint for the pastel sweep.
312
+ * - solid / soft / ghost / shadow / glass with intent="primary": the
313
+ * filled/tinted resting state is invisible under --primary:transparent,
314
+ * so the gradient is what gives these variants a hover personality.
315
+ * (link stays a pure-text variant — the underline is its own affordance.)
316
+ * Multi-layer box-shadow simulates the diffused warm/cool color aura
317
+ * around the button seen on htgf.de. */
318
+ :root[data-palette="colorful"] [data-slot="button"][data-variant="outline"]:hover:not(:disabled),
319
+ :root[data-palette="colorful"] [data-slot="button"][data-variant="solid"][data-intent="primary"]:hover:not(:disabled),
320
+ :root[data-palette="colorful"] [data-slot="button"][data-variant="soft"][data-intent="primary"]:hover:not(:disabled),
321
+ :root[data-palette="colorful"] [data-slot="button"][data-variant="ghost"][data-intent="primary"]:hover:not(:disabled),
322
+ :root[data-palette="colorful"] [data-slot="button"][data-variant="shadow"][data-intent="primary"]:hover:not(:disabled),
323
+ :root[data-palette="colorful"] [data-slot="button"][data-variant="glass"][data-intent="primary"]:hover:not(:disabled) {
324
+ background-image: var(--colorful-gradient);
325
+ background-color: transparent;
326
+ border-color: transparent;
327
+ color: #1a1a1a;
328
+ box-shadow:
329
+ -8px 0 24px -4px rgba(255, 180, 140, 0.55),
330
+ 8px 0 24px -4px rgba(160, 210, 230, 0.55),
331
+ 0 4px 16px -2px rgba(220, 180, 230, 0.40);
332
+ }
333
+
334
+ /* Rest-state safety net: with --primary: transparent, any utility that
335
+ * resolves to var(--primary) (bg-primary, text-primary, border-primary)
336
+ * would render as invisible. Add a neutral fallback only where the
337
+ * components ship with primary-tinted resting surfaces, so icons and
338
+ * chips remain visible until the hover gradient takes over. */
339
+ :root[data-palette="colorful"] [data-slot="feature-card"] > div:first-of-type {
340
+ background-color: oklch(0 0 0 / 0.05);
341
+ color: oklch(0.2 0 0);
342
+ }
343
+ :root[data-palette="colorful"].dark [data-slot="feature-card"] > div:first-of-type {
344
+ background-color: oklch(1 0 0 / 0.08);
345
+ color: oklch(0.95 0 0);
346
+ }
347
+
348
+ /* Cards: pastel gradient border ring on hover. background-clip trick keeps
349
+ * the inner fill as the card surface and lets only the 1px border bloom
350
+ * into the gradient. Mirrors the same warm-aura glow used on buttons. */
351
+ :root[data-palette="colorful"] [data-slot="feature-card"]:hover,
352
+ :root[data-palette="colorful"] [data-slot="testimonial-card"]:hover,
353
+ :root[data-palette="colorful"] [data-slot="team-card"]:hover,
354
+ :root[data-palette="colorful"] [data-slot="spotlight-card"]:hover {
355
+ border-color: transparent;
356
+ background-image:
357
+ linear-gradient(var(--card), var(--card)),
358
+ var(--colorful-gradient);
359
+ background-origin: border-box;
360
+ background-clip: padding-box, border-box;
361
+ box-shadow:
362
+ -12px 0 32px -6px rgba(255, 180, 140, 0.30),
363
+ 12px 0 32px -6px rgba(160, 210, 230, 0.30),
364
+ 0 8px 24px -4px rgba(220, 180, 230, 0.25);
365
+ }
366
+
367
+ /* Feature-card icon chip: soft tint at rest, pastel gradient fill on card hover.
368
+ * Resting style at feature-card.tsx:64 is bg-primary/10 text-primary; this
369
+ * swaps the chip surface to the pastel sweep and keeps the icon dark. */
370
+ :root[data-palette="colorful"] [data-slot="feature-card"]:hover > div:first-of-type {
371
+ background-image: var(--colorful-gradient);
372
+ color: #1a1a1a;
373
+ }
374
+
375
+ /* ── "On / checked / filled" state identity ──────────────────────────────
376
+ * Every form control that ships with bg-primary as its active-state fill
377
+ * would render invisibly under --primary: transparent. Instead, use the
378
+ * pastel gradient as the brand identity for the active state — switches
379
+ * on, checkboxes checked, radio dots, sliders filled, progress bars,
380
+ * avatar badges, the pricing card's "popular" pill. Resting/empty states
381
+ * fall back to a neutral muted surface so the control is still readable. */
382
+
383
+ /* Switch — checked = gradient, thumb tinted dark for contrast. */
384
+ :root[data-palette="colorful"] [data-slot="switch"][data-state="checked"] {
385
+ background-image: var(--colorful-gradient);
386
+ background-color: transparent;
387
+ }
388
+ :root[data-palette="colorful"] [data-slot="switch"][data-state="checked"] [data-slot="switch-thumb"] {
389
+ background-color: oklch(0.2 0 0);
390
+ }
391
+ :root[data-palette="colorful"].dark [data-slot="switch"][data-state="checked"] [data-slot="switch-thumb"] {
392
+ background-color: oklch(0.98 0 0);
393
+ }
394
+
395
+ /* Checkbox — checked = gradient, indicator (the tick) flips to dark/light. */
396
+ :root[data-palette="colorful"] [data-slot="checkbox"][data-state="checked"] {
397
+ background-image: var(--colorful-gradient);
398
+ background-color: transparent;
399
+ border-color: transparent;
400
+ color: oklch(0.2 0 0);
401
+ }
402
+ :root[data-palette="colorful"].dark [data-slot="checkbox"][data-state="checked"] {
403
+ color: oklch(0.2 0 0);
404
+ }
405
+
406
+ /* Radio group — checked ring border + indicator dot need a visible color. */
407
+ :root[data-palette="colorful"] [data-slot="radio-group-item"][data-state="checked"] {
408
+ border-color: oklch(0.5 0.15 25);
409
+ }
410
+ :root[data-palette="colorful"] [data-slot="radio-group-item"] {
411
+ color: oklch(0.5 0.15 25);
412
+ }
413
+ :root[data-palette="colorful"].dark [data-slot="radio-group-item"][data-state="checked"] {
414
+ border-color: oklch(0.7 0.15 25);
415
+ }
416
+ :root[data-palette="colorful"].dark [data-slot="radio-group-item"] {
417
+ color: oklch(0.7 0.15 25);
418
+ }
419
+
420
+ /* Slider — filled range = gradient, thumb gets a visible border. */
421
+ :root[data-palette="colorful"] [data-slot="slider-range"] {
422
+ background-image: var(--colorful-gradient);
423
+ background-color: transparent;
424
+ }
425
+ :root[data-palette="colorful"] [data-slot="slider-thumb"] {
426
+ border-color: oklch(0.5 0.15 25 / 0.6);
427
+ }
428
+ :root[data-palette="colorful"].dark [data-slot="slider-thumb"] {
429
+ border-color: oklch(0.7 0.15 25 / 0.7);
430
+ }
431
+
432
+ /* Progress — track = soft muted, fill = gradient. */
433
+ :root[data-palette="colorful"] [data-slot="progress"] {
434
+ background-color: oklch(0 0 0 / 0.08);
435
+ }
436
+ :root[data-palette="colorful"].dark [data-slot="progress"] {
437
+ background-color: oklch(1 0 0 / 0.12);
438
+ }
439
+ :root[data-palette="colorful"] [data-slot="progress-indicator"] {
440
+ background-image: var(--colorful-gradient);
441
+ background-color: transparent;
442
+ }
443
+
444
+ /* Avatar status badge — solid dark/light dot since the surface is tiny
445
+ * and a gradient on it would be visually noisy at this scale. */
446
+ :root[data-palette="colorful"] [data-slot="avatar-badge"] {
447
+ background-color: oklch(0.2 0 0);
448
+ color: oklch(0.98 0 0);
449
+ }
450
+ :root[data-palette="colorful"].dark [data-slot="avatar-badge"] {
451
+ background-color: oklch(0.98 0 0);
452
+ color: oklch(0.2 0 0);
453
+ }
454
+
455
+ /* Pricing card "Popular" pill — the only badge that uses bg-primary
456
+ * directly. Render as the pastel gradient pill with dark text. */
457
+ :root[data-palette="colorful"] [data-slot="pricing-card"] > div:first-child {
458
+ background-image: var(--colorful-gradient);
459
+ background-color: transparent;
460
+ color: oklch(0.2 0 0);
461
+ }
462
+
463
+ /* Tooltip — bg-primary text-primary-foreground would be invisible. A
464
+ * dark slate surface reads cleanly without needing the gradient
465
+ * (gradients on tiny tooltip surfaces look noisy at this scale). */
466
+ :root[data-palette="colorful"] [data-slot="tooltip-content"] {
467
+ background-color: oklch(0.2 0 0);
468
+ color: oklch(0.98 0 0);
469
+ }
470
+ :root[data-palette="colorful"].dark [data-slot="tooltip-content"] {
471
+ background-color: oklch(0.98 0 0);
472
+ color: oklch(0.2 0 0);
473
+ }
474
+
475
+ /* Calendar — selected single, range-start, range-end days use bg-primary.
476
+ * Promote the selection to the pastel gradient so it stays consistent
477
+ * with switches / progress / checkboxes. */
478
+ :root[data-palette="colorful"] [data-slot="calendar"] [data-selected-single="true"],
479
+ :root[data-palette="colorful"] [data-slot="calendar"] [data-range-start="true"],
480
+ :root[data-palette="colorful"] [data-slot="calendar"] [data-range-end="true"] {
481
+ background-image: var(--colorful-gradient);
482
+ background-color: transparent;
483
+ color: oklch(0.2 0 0);
484
+ }
485
+
486
+ /* Scroll-to-top button — pinned floating action that uses bg-primary as
487
+ * its filled surface. Promote to gradient + dark icon. */
488
+ :root[data-palette="colorful"] [data-slot="scroll-to-top-button"],
489
+ :root[data-palette="colorful"] [data-slot="scroll-to-top"] {
490
+ background-image: var(--colorful-gradient);
491
+ background-color: transparent;
492
+ color: oklch(0.2 0 0);
493
+ }
494
+
495
+ /* Generic text-primary safety net — Tailwind's text-primary utility
496
+ * resolves to var(--primary), which is transparent under this palette
497
+ * and would render invisible text wherever it's used directly (link
498
+ * hovers in <Empty>, custom prose, downstream apps, etc.). Route it
499
+ * through --foreground so the text stays readable. Specific data-slot
500
+ * rules above still win for active states that need the gradient. */
501
+ :root[data-palette="colorful"] .text-primary {
502
+ color: var(--foreground);
503
+ }
504
+
505
+ /* ─────────────────────────────────────────────────────────────────────
506
+ * Audit pass — close remaining utility + intent gaps so no element
507
+ * renders invisibly under --primary: transparent. See the audit table
508
+ * in the plan file for the per-component reasoning.
509
+ * ───────────────────────────────────────────────────────────────────── */
510
+
511
+ /* .border-primary — Steps circles, Card hover ring, Field checked,
512
+ * Timeline dots, PricingCard featured border. */
513
+ :root[data-palette="colorful"] .border-primary {
514
+ border-color: oklch(0.5 0.15 25 / 0.4);
515
+ }
516
+ :root[data-palette="colorful"].dark .border-primary {
517
+ border-color: oklch(0.7 0.15 25 / 0.5);
518
+ }
519
+
520
+ /* .fill-primary — inline SVG fills (e.g. icons.tsx success glyph). */
521
+ :root[data-palette="colorful"] .fill-primary {
522
+ fill: var(--foreground);
523
+ }
524
+
525
+ /* Text-selection color — input / textarea / native-select apply
526
+ * selection:bg-primary selection:text-primary-foreground, which is
527
+ * invisible without an explicit color. Use a soft warm-tinted highlight. */
528
+ :root[data-palette="colorful"] ::selection {
529
+ background-color: oklch(0.85 0.10 25 / 0.5);
530
+ color: oklch(0.2 0 0);
531
+ }
532
+ :root[data-palette="colorful"].dark ::selection {
533
+ background-color: oklch(0.55 0.12 25 / 0.55);
534
+ color: oklch(0.98 0 0);
535
+ }
536
+
537
+ /* PricingCard featured: ring-primary/20 uses --color-primary and goes
538
+ * invisible. Restore the elevated featured look with a soft pastel
539
+ * ring + ambient shadow. Targets the .border-primary class the
540
+ * component applies when featured (pricing-card.tsx:100). */
541
+ :root[data-palette="colorful"] [data-slot="pricing-card"].border-primary {
542
+ box-shadow:
543
+ 0 1px 2px oklch(0 0 0 / 0.06),
544
+ 0 0 0 1px oklch(0.5 0.15 25 / 0.25),
545
+ 0 8px 24px -4px oklch(0.5 0.15 25 / 0.10);
546
+ }
547
+
548
+ /* Typewriter cursor — 2px wide blinking caret. The faint gradient is
549
+ * invisible at this scale; solid foreground keeps the cursor readable. */
550
+ :root[data-palette="colorful"] [data-slot="typewriter-text"] .bg-primary,
551
+ :root[data-palette="colorful"] .animate-pulse.bg-primary {
552
+ background-color: var(--foreground);
553
+ background-image: none;
554
+ }
555
+
556
+ /* Alert primary intent — all three derived utilities (bg/text/border)
557
+ * resolve through transparent --intent. Replace with a coherent
558
+ * gradient-tinted surface that reads as the primary-intent tone. */
559
+ :root[data-palette="colorful"] [data-slot="alert"][data-intent="primary"] {
560
+ background-image: var(--colorful-gradient-faint);
561
+ background-color: transparent;
562
+ border-color: oklch(0.5 0.15 25 / 0.3);
563
+ color: var(--foreground);
564
+ }
565
+ :root[data-palette="colorful"] [data-slot="alert"][data-intent="primary"] [data-slot="alert-description"] {
566
+ color: oklch(0.4 0 0);
567
+ }
568
+ :root[data-palette="colorful"].dark [data-slot="alert"][data-intent="primary"] [data-slot="alert-description"] {
569
+ color: oklch(0.75 0 0);
570
+ }
571
+
572
+ /* Badge primary intent — uses the full hover-color gradient
573
+ * (--colorful-gradient: peach → blush → lavender → cyan) as the brand
574
+ * identity, matching what the buttons + cards bloom into on hover.
575
+ * - solid: full gradient pill + dark text + soft drop shadow
576
+ * - soft: faint version of the same gradient + foreground text
577
+ * - outline: gradient border ring (background-clip trick) + dark text
578
+ * All three carry the same identity so the user can pick the weight
579
+ * (heavy / soft / outline) without losing the "colorful" feel. */
580
+
581
+ :root[data-palette="colorful"] [data-slot="badge"][data-intent="primary"][data-variant="solid"] {
582
+ /* Mid-alpha pastel sweep (~60%) — quieter than the full gradient
583
+ * used on buttons, so the badge doesn't compete with surrounding
584
+ * content. Dark mode keeps the full gradient since the dark canvas
585
+ * already absorbs intensity. */
586
+ background-image: linear-gradient(95deg,
587
+ rgba(255, 212, 163, 0.60) 0%,
588
+ rgba(255, 184, 197, 0.60) 35%,
589
+ rgba(212, 188, 232, 0.60) 65%,
590
+ rgba(184, 216, 229, 0.60) 100%);
591
+ background-color: transparent;
592
+ color: oklch(0.2 0 0);
593
+ border-color: transparent;
594
+ /* Warm-left / cool-right color aura, also softened for light mode. */
595
+ box-shadow:
596
+ inset 0 1px 0 oklch(1 0 0 / 0.40),
597
+ -6px 0 18px -4px rgba(255, 180, 140, 0.35),
598
+ 6px 0 18px -4px rgba(160, 210, 230, 0.35),
599
+ 0 3px 12px -2px rgba(220, 180, 230, 0.25);
600
+ letter-spacing: 0.01em;
601
+ }
602
+ :root[data-palette="colorful"].dark [data-slot="badge"][data-intent="primary"][data-variant="solid"] {
603
+ color: oklch(0.2 0 0);
604
+ box-shadow:
605
+ inset 0 1px 0 oklch(1 0 0 / 0.25),
606
+ -6px 0 18px -4px rgba(255, 180, 140, 0.35),
607
+ 6px 0 18px -4px rgba(160, 210, 230, 0.35),
608
+ 0 3px 12px -2px rgba(220, 180, 230, 0.25);
609
+ }
610
+
611
+ :root[data-palette="colorful"] [data-slot="badge"][data-intent="primary"][data-variant="soft"] {
612
+ background-image: var(--colorful-gradient-faint);
613
+ background-color: transparent;
614
+ color: var(--foreground);
615
+ border-color: oklch(0.5 0.12 25 / 0.18);
616
+ letter-spacing: 0.01em;
617
+ }
618
+ :root[data-palette="colorful"].dark [data-slot="badge"][data-intent="primary"][data-variant="soft"] {
619
+ border-color: oklch(0.7 0.10 25 / 0.20);
620
+ }
621
+
622
+ :root[data-palette="colorful"] [data-slot="badge"][data-intent="primary"][data-variant="outline"] {
623
+ background-image:
624
+ linear-gradient(var(--background), var(--background)),
625
+ var(--colorful-gradient);
626
+ background-origin: border-box;
627
+ background-clip: padding-box, border-box;
628
+ border-color: transparent;
629
+ color: var(--foreground);
630
+ letter-spacing: 0.01em;
631
+ }
@@ -1,20 +1,26 @@
1
1
  /**
2
- * @fileoverview Surface system — flat / glass as semantic overlay.
2
+ * @fileoverview Surface system — flat / glass / clay as semantic overlay.
3
3
  * @module packages/ui/styles/surfaces
4
4
  * @package ui
5
- * @reviewed 2026-04-19
5
+ * @reviewed 2026-05-26
6
6
  *
7
7
  * Components bind to four surface tokens:
8
- * --surface-bg background color or gradient
8
+ * --surface-bg background color or gradient (resolved via `background:`)
9
9
  * --surface-border border color
10
10
  * --surface-backdrop backdrop-filter value (blur/saturate) or "none"
11
- * --surface-shadow box-shadow value
11
+ * --surface-shadow box-shadow value (may stack multiple layers)
12
12
  *
13
- * Switching [data-style="…"] on <html> swaps the tokens without touching
14
- * any component. Extend by adding a new selector block:
13
+ * Two attributes activate the surface tokens:
14
+ * - [data-style="…"] — used on <html> by SaasflareShell (page-wide default)
15
+ * - [data-surface="…"] — used on individual components for per-element override
15
16
  *
16
- * [data-style="neumorphic"] { --surface-bg: …; }
17
+ * Both feed the same rule blocks so swapping at either level swaps the tokens
18
+ * without touching any component. A component-level `data-surface` re-defines
19
+ * the tokens for its subtree, overriding the cascaded page-wide value — that's
20
+ * how `<Button surface="glass">` wins over `<html data-style="flat">`.
17
21
  *
22
+ * Extend by adding a new selector block:
23
+ * [data-style="neumorphic"], [data-surface="neumorphic"] { --surface-bg: …; … }
18
24
  * and registering the id in the StyleVariant union (types.ts).
19
25
  */
20
26
 
@@ -25,14 +31,16 @@
25
31
  --surface-shadow: 0 1px 2px oklch(0 0 0 / 0.05);
26
32
  }
27
33
 
28
- [data-style="flat"] {
34
+ [data-style="flat"],
35
+ [data-surface="flat"] {
29
36
  --surface-bg: var(--card);
30
37
  --surface-border: var(--border);
31
38
  --surface-backdrop: none;
32
39
  --surface-shadow: 0 1px 2px oklch(0 0 0 / 0.05);
33
40
  }
34
41
 
35
- [data-style="glass"] {
42
+ [data-style="glass"],
43
+ [data-surface="glass"] {
36
44
  --surface-bg: oklch(0.99 0.005 var(--neutral-h) / 0.6);
37
45
  --surface-border: oklch(1 0 0 / 0.2);
38
46
  --surface-backdrop: blur(12px) saturate(140%);
@@ -42,10 +50,81 @@
42
50
  }
43
51
 
44
52
  [data-style="glass"].dark,
45
- .dark [data-style="glass"] {
53
+ .dark [data-style="glass"],
54
+ [data-surface="glass"].dark,
55
+ .dark [data-surface="glass"] {
46
56
  --surface-bg: oklch(0.2 0.015 var(--neutral-h) / 0.5);
47
57
  --surface-border: oklch(1 0 0 / 0.1);
48
58
  --surface-shadow:
49
59
  0 8px 32px oklch(0 0 0 / 0.4),
50
60
  inset 0 1px 0 oklch(1 0 0 / 0.08);
51
61
  }
62
+
63
+ /* ============================================
64
+ * Clay — palette-agnostic pillow finish. Color comes from the active palette
65
+ * via --card (surface base) and --accent (shadow tint). Four-layer shadow
66
+ * stack creates the 3D pillow effect:
67
+ * 1. Outer drop — accent-tinted soft elevation
68
+ * 2. Outer ambient — neutral contact shadow
69
+ * 3. Inner top — bright highlight, like top-lit edge
70
+ * 4. Inner bottom — accent-tinted shade, like underside
71
+ * Background is a vertical micro-gradient so the surface feels rounded rather
72
+ * than printed-flat. NO transparency, NO backdrop-filter.
73
+ * ============================================ */
74
+ [data-style="clay"],
75
+ [data-surface="clay"] {
76
+ --surface-bg: linear-gradient(
77
+ 180deg,
78
+ oklch(from var(--card) calc(l + 0.02) c h),
79
+ oklch(from var(--card) calc(l - 0.02) c h)
80
+ );
81
+ --surface-border: transparent;
82
+ --surface-backdrop: none;
83
+ --surface-shadow:
84
+ 0 8px 24px -8px oklch(from var(--accent) 0.4 c h / 0.35),
85
+ 0 2px 4px oklch(0 0 0 / 0.06),
86
+ inset 0 2px 3px oklch(1 0 0 / 0.6),
87
+ inset 0 -2px 4px oklch(from var(--accent) 0.3 c h / 0.15);
88
+ }
89
+
90
+ [data-style="clay"].dark,
91
+ .dark [data-style="clay"],
92
+ [data-surface="clay"].dark,
93
+ .dark [data-surface="clay"] {
94
+ --surface-bg: linear-gradient(
95
+ 180deg,
96
+ oklch(from var(--card) calc(l + 0.025) c h),
97
+ oklch(from var(--card) calc(l - 0.015) c h)
98
+ );
99
+ --surface-border: transparent;
100
+ --surface-backdrop: none;
101
+ --surface-shadow:
102
+ 0 8px 24px -8px oklch(from var(--accent) 0.5 c h / 0.4),
103
+ 0 2px 4px oklch(0 0 0 / 0.3),
104
+ inset 0 1px 0 oklch(1 0 0 / 0.06),
105
+ inset 0 -2px 4px oklch(from var(--accent) 0.4 c h / 0.18);
106
+ }
107
+
108
+ /* ============================================
109
+ * Surface utility classes — let any component opt into the active surface
110
+ * tokens with one className, no useSaasflareProps roundtrip required.
111
+ *
112
+ * Use `.surface-card` on the root of card-like elements (FeatureCard,
113
+ * TestimonialCard, StatCard, …). The class consumes whichever
114
+ * `--surface-*` values are currently active in the cascade — `flat`
115
+ * defaults from :root, `glass` overrides from <html data-style="glass">,
116
+ * or a local `data-surface` attribute on an ancestor.
117
+ *
118
+ * Pair with Tailwind `border` to make `--surface-border` visible. Border
119
+ * width is intentionally not baked in so consumers can pick `border`,
120
+ * `border-2`, or `border-0` per design.
121
+ * ============================================ */
122
+ @layer utilities {
123
+ .surface-card {
124
+ background: var(--surface-bg);
125
+ border-color: var(--surface-border);
126
+ backdrop-filter: var(--surface-backdrop);
127
+ -webkit-backdrop-filter: var(--surface-backdrop);
128
+ box-shadow: var(--surface-shadow);
129
+ }
130
+ }
package/styles/theme.css CHANGED
@@ -26,6 +26,7 @@
26
26
  @import "tailwindcss";
27
27
  @import "./motion.css";
28
28
  @import "./surfaces.css";
29
+ @import "./aurora.css";
29
30
  @import "./palettes.css";
30
31
 
31
32
  /* Tailwind v4 source discovery — scan the package itself so consumers
@@ -330,20 +331,25 @@ textarea,
330
331
  }
331
332
 
332
333
  /* ============================================
333
- * Radius axis — discrete preset switched via [data-radius] on <html>.
334
+ * Radius axis — discrete preset switched via [data-radius].
334
335
  *
335
336
  * Baseline default is "rounded" (set in :root above as --radius: 0.625rem).
336
337
  * "pill" overrides the entire derived scale explicitly: relying on calc()
337
338
  * would give 9995–10003px for sm/lg, which is visually pill on tall elements
338
339
  * but NOT pill on a 200px-wide component. Lock the whole scale instead.
339
340
  *
341
+ * The selectors deliberately drop the `:root` prefix so a per-component
342
+ * `data-radius` attribute (e.g. `<Button radius="pill">`) creates its own
343
+ * cascade scope and overrides the inherited page-wide value. Without that
344
+ * scope a component-level `radius` prop would have no visible effect.
345
+ *
340
346
  * Custom per-palette override (CustomPalette.radius) sets --radius inline
341
347
  * and wins by specificity — intentional escape hatch.
342
348
  * ============================================ */
343
- :root[data-radius="sharp"] { --radius: 0; }
344
- :root[data-radius="soft"] { --radius: 0.35rem; }
345
- :root[data-radius="rounded"] { --radius: 0.625rem; }
346
- :root[data-radius="pill"] {
349
+ [data-radius="sharp"] { --radius: 0; }
350
+ [data-radius="soft"] { --radius: 0.35rem; }
351
+ [data-radius="rounded"] { --radius: 0.625rem; }
352
+ [data-radius="pill"] {
347
353
  --radius: 9999px;
348
354
  --radius-sm: 9999px;
349
355
  --radius-md: 9999px;