@saasflare/ui 3.1.1 → 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.
- package/README.md +68 -2
- package/dist/{button-0Bdl7Nqm.d.ts → button-BA7OcXqy.d.mts} +12 -17
- package/dist/{button-Brb4BhPO.d.mts → button-Bfg2Tnvx.d.ts} +12 -17
- package/dist/{chunk-D5LKWKG7.js → chunk-2GOPD64T.js} +117 -89
- package/dist/{chunk-WPOOC2FX.mjs → chunk-2ONA6OMO.mjs} +33 -44
- package/dist/{chunk-RW2S3KNB.mjs → chunk-5C65JNGY.mjs} +7 -6
- package/dist/{chunk-DNLCSV5M.js → chunk-7UD3SGPP.js} +32 -43
- package/dist/chunk-GI6VN7XU.mjs +2143 -0
- package/dist/{chunk-FT66KYRN.js → chunk-ITALEYDI.js} +2 -2
- package/dist/{chunk-4BOMMZEY.js → chunk-JC7EIEGI.js} +14 -13
- package/dist/chunk-N65HIOBD.js +234 -0
- package/dist/{chunk-EJHYM2HP.mjs → chunk-OZAWULTM.mjs} +1 -1
- package/dist/chunk-R3AVBLJ3.js +2207 -0
- package/dist/{chunk-WRONFPRI.mjs → chunk-RMQBB72G.mjs} +118 -91
- package/dist/chunk-XNDTCYSO.mjs +211 -0
- package/dist/{dialog-BmY55WSX.d.ts → dialog-CZRwrqDa.d.ts} +2 -2
- package/dist/{dialog-CcaHMAsS.d.mts → dialog-Cr0becOL.d.mts} +2 -2
- package/dist/entries/calendar.d.mts +3 -3
- package/dist/entries/calendar.d.ts +3 -3
- package/dist/entries/calendar.js +13 -214
- package/dist/entries/calendar.mjs +5 -196
- package/dist/entries/carousel.d.mts +3 -3
- package/dist/entries/carousel.d.ts +3 -3
- package/dist/entries/carousel.js +17 -14
- package/dist/entries/carousel.mjs +10 -7
- package/dist/entries/chart.d.mts +1 -1
- package/dist/entries/chart.d.ts +1 -1
- package/dist/entries/chart.js +11 -11
- package/dist/entries/chart.mjs +1 -1
- package/dist/entries/command.d.mts +3 -3
- package/dist/entries/command.d.ts +3 -3
- package/dist/entries/command.js +21 -19
- package/dist/entries/command.mjs +8 -6
- package/dist/entries/drawer.d.mts +1 -1
- package/dist/entries/drawer.d.ts +1 -1
- package/dist/entries/drawer.js +9 -9
- package/dist/entries/drawer.mjs +2 -2
- package/dist/entries/input-otp.d.mts +2 -2
- package/dist/entries/input-otp.d.ts +2 -2
- package/dist/entries/input-otp.js +10 -8
- package/dist/entries/input-otp.mjs +6 -4
- package/dist/entries/resizable.d.mts +3 -2
- package/dist/entries/resizable.d.ts +3 -2
- package/dist/entries/resizable.js +8 -6
- package/dist/entries/resizable.mjs +6 -4
- package/dist/index.d.mts +974 -31
- package/dist/index.d.ts +974 -31
- package/dist/index.js +2994 -556
- package/dist/index.mjs +2488 -201
- package/dist/{use-saasflare-props-NrM2Glmp.d.ts → use-saasflare-props-BrjMhU0U.d.mts} +53 -4
- package/dist/{use-saasflare-props-NrM2Glmp.d.mts → use-saasflare-props-BrjMhU0U.d.ts} +53 -4
- package/package.json +4 -3
- package/styles/aurora.css +47 -0
- package/styles/palettes.css +487 -3
- package/styles/surfaces.css +89 -10
- package/styles/theme.css +41 -19
package/styles/palettes.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
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
|
|
@@ -122,6 +122,10 @@
|
|
|
122
122
|
:root[data-palette="teal"] { --primary-h: 185; --primary-c: 0.15; --primary-l: 0.60; }
|
|
123
123
|
:root[data-palette="teal"].dark { --primary-l: 0.70; }
|
|
124
124
|
|
|
125
|
+
/* ─── Sage (dusty teal, Saasflare alt-brand) ────── */
|
|
126
|
+
:root[data-palette="sage"] { --primary-h: 169.28; --primary-c: 0.082; --primary-l: 0.586; }
|
|
127
|
+
:root[data-palette="sage"].dark { --primary-l: 0.70; }
|
|
128
|
+
|
|
125
129
|
/* ─── Iris ──────────────────────────────────────── */
|
|
126
130
|
:root[data-palette="iris"] { --primary-h: 255; --primary-c: 0.16; --primary-l: 0.60; }
|
|
127
131
|
:root[data-palette="iris"].dark { --primary-l: 0.70; }
|
|
@@ -130,18 +134,498 @@
|
|
|
130
134
|
:root[data-palette="ruby"] { --primary-h: 10; --primary-c: 0.21; --primary-l: 0.58; }
|
|
131
135
|
:root[data-palette="ruby"].dark { --primary-l: 0.68; }
|
|
132
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
|
+
|
|
133
192
|
/* ============================================
|
|
134
193
|
* Chart palette overrides for achromatic palettes
|
|
135
194
|
*
|
|
136
195
|
* With --primary-c: 0, the derived chart colors collapse to grayscale.
|
|
137
|
-
* Give Ink and
|
|
196
|
+
* Give Ink, Stone, Black and Snow a fixed distinguishable 5-hue palette.
|
|
138
197
|
* ============================================ */
|
|
139
198
|
:root[data-palette="ink"],
|
|
140
199
|
:root[data-palette="stone"],
|
|
141
|
-
:root[data-palette="black"]
|
|
200
|
+
:root[data-palette="black"],
|
|
201
|
+
:root[data-palette="snow"] {
|
|
142
202
|
--chart-1: oklch(0.60 0.18 230); /* blue */
|
|
143
203
|
--chart-2: oklch(0.65 0.17 55); /* orange */
|
|
144
204
|
--chart-3: oklch(0.60 0.15 155); /* green */
|
|
145
205
|
--chart-4: oklch(0.58 0.20 25); /* red */
|
|
146
206
|
--chart-5: oklch(0.60 0.18 290); /* violet */
|
|
147
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
|
+
}
|
package/styles/surfaces.css
CHANGED
|
@@ -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-
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
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
|
+
}
|