@fanfare-io/fanfare-sdk-react 0.21.0 → 0.23.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.
@@ -55,6 +55,23 @@
55
55
 
56
56
  [data-fanfare-variant="retro"] {
57
57
  --ff-variant: "retro";
58
+ /* Inconsolata's advance is 0.5; 0.58 also covers the fallback monospace stack a host that
59
+ * does not load the face renders with. */
60
+ --ff-numeral-advance: 0.58;
61
+ --ff-countdown-cell-padding: 0.5rem 0.75rem;
62
+ --ff-countdown-cell-border-width: 2px;
63
+ --ff-countdown-reserve-cell: 1.75rem;
64
+ --ff-countdown-reserve-gap: 1rem;
65
+ --ff-countdown-cell-bg: var(--ff-color-background);
66
+ --ff-countdown-cell-shadow: 3px 3px 0 var(--ff-color-foreground);
67
+ --ff-progress-bar-shadow: inset 0 -3px 0 rgba(0, 0, 0, 0.2);
68
+ --ff-card-shadow: 6px 6px 0 var(--ff-color-foreground);
69
+ --ff-card-shadow-elevated: var(--ff-card-shadow);
70
+ --ff-countdown-separator-display: none;
71
+ --ff-countdown-inline-weight: 800;
72
+ --ff-countdown-tone-critical-bg: #ff3a00;
73
+ --ff-countdown-tone-critical-fg: #ffffff;
74
+ --ff-countdown-numeral-font: var(--ff-font-mono);
58
75
  --ff-color-background: #f4f1ea;
59
76
  --ff-color-foreground: #000000;
60
77
  --ff-color-muted: #444444;
@@ -63,7 +80,7 @@
63
80
  /*
64
81
  * Note: retro intentionally does NOT override --ff-color-danger. The
65
82
  * brutalist `#ff3a00` orange is a Countdown-critical-urgency accent only
66
- * (applied directly in the countdown cva compound), not a global semantic
83
+ * (scoped to the countdown unit rule below), not a global semantic
67
84
  * destructive color. Destructive buttons / error views keep the customer's
68
85
  * (or @theme default) danger token under retro.
69
86
  */
@@ -74,15 +91,51 @@
74
91
 
75
92
  [data-fanfare-variant="rounded"] {
76
93
  --ff-variant: "rounded";
94
+ --ff-countdown-cell-radius: 14px;
95
+ --ff-countdown-cell-padding: 0.75rem 1rem;
96
+ --ff-countdown-cell-border-width: 1px;
97
+ --ff-countdown-cell-bg: var(--ff-color-background);
98
+ --ff-countdown-cell-shadow: 0 2px 12px -3px rgba(0, 0, 0, 0.08);
99
+ --ff-countdown-cell-shadow-hover: 0 4px 16px -4px rgba(0, 0, 0, 0.12);
100
+ --ff-countdown-separator-display: none;
101
+ --ff-countdown-inline-weight: 600;
102
+ --ff-countdown-reserve-cell: 2.125rem;
103
+ --ff-countdown-reserve-gap: 1rem;
104
+ --ff-card-shadow: 0 2px 12px -3px rgba(0, 0, 0, 0.08);
105
+ --ff-card-shadow-hover: 0 4px 16px -4px rgba(0, 0, 0, 0.12);
106
+ --ff-card-shadow-elevated: var(--ff-card-shadow);
107
+ --ff-card-shadow-elevated-hover: var(--ff-card-shadow-hover);
108
+ --ff-progress-shadow: inset 0 1px 4px -1px rgba(0, 0, 0, 0.06);
109
+ --ff-progress-bar-shadow: 0 0 10px -2px var(--ff-color-primary);
110
+ --ff-loading-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.08);
111
+ --ff-button-shadow: 0 2px 12px -3px rgba(0, 0, 0, 0.08);
112
+ --ff-button-shadow-hover: 0 4px 16px -4px rgba(0, 0, 0, 0.12);
113
+ --ff-button-shadow-active: inset 0 2px 4px rgba(0, 0, 0, 0.1);
114
+ --ff-button-shadow-primary: 0 2px 12px -3px rgba(0, 0, 0, 0.08), 0 0 12px -6px var(--ff-color-primary);
115
+ --ff-input-shadow: inset 0 1px 4px -1px rgba(0, 0, 0, 0.06);
116
+ --ff-input-shadow-focus: inset 0 1px 4px -1px rgba(0, 0, 0, 0.06), 0 0 0 3px rgba(var(--ff-color-primary-rgb), 0.12);
117
+ --ff-otp-shadow: inset 0 1px 4px -1px rgba(0, 0, 0, 0.06);
118
+ --ff-otp-shadow-focus: inset 0 1px 4px -1px rgba(0, 0, 0, 0.06), 0 0 0 3px rgba(var(--ff-color-primary-rgb), 0.12);
119
+ --ff-otp-shadow-focus-error:
120
+ inset 0 1px 4px -1px rgba(0, 0, 0, 0.06), 0 0 0 3px rgba(var(--ff-color-danger-rgb), 0.12);
77
121
  }
78
122
 
79
123
  [data-fanfare-variant="clean"] {
80
124
  --ff-variant: "clean";
125
+ --ff-numeral-advance: 0.58;
126
+ --ff-countdown-cell-gap: 0.125rem;
127
+ --ff-countdown-inline-weight: 500;
128
+ --ff-countdown-inline-tracking: 0.02em;
129
+ --ff-countdown-reserve-gap: 2rem;
130
+ --ff-countdown-numeral-font: var(--ff-font-mono);
81
131
  --ff-color-background: #ffffff;
82
132
  --ff-color-foreground: #0a0a0a;
83
133
  --ff-color-muted: #737373;
84
134
  --ff-color-muted-foreground: #737373;
85
135
  --ff-color-border: #e8e8e8;
136
+ --ff-card-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 8px 24px rgba(0, 0, 0, 0.06);
137
+ --ff-card-shadow-elevated: var(--ff-card-shadow);
138
+ --ff-progress-bar-shadow: 0 0 #0000;
86
139
  --ff-font-sans: "Figtree", system-ui, sans-serif;
87
140
  --ff-font-heading: "Figtree", system-ui, sans-serif;
88
141
  --ff-font-mono: "Inconsolata", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
@@ -98,6 +151,154 @@
98
151
  font-family: var(--ff-font-sans);
99
152
  color: var(--ff-color-foreground);
100
153
  background-color: var(--ff-color-surface);
154
+ --ff-widget-max-width: 24rem;
155
+
156
+ /* The advance width of one digit as a fraction of the font size, for the
157
+ * variant's numeral typeface. Fitting a fixed-width glyph run to a
158
+ * container needs it; over-stating it only shrinks the numerals. */
159
+ --ff-numeral-advance: 0.65;
160
+
161
+ /* Display text that fits its container: a `cqi`-driven scale between a
162
+ * legible floor and a ceiling the design allows. */
163
+ --ff-stat-min: 1.5rem;
164
+ --ff-stat-scale: 9cqi;
165
+ --ff-stat-max: 2.25rem;
166
+ --ff-stat-weight: 700;
167
+ --ff-title-min: 1rem;
168
+ --ff-title-scale: 6cqi;
169
+ --ff-title-max: 1.25rem;
170
+ --ff-title-weight: 600;
171
+
172
+ /* Countdown: geometry, colour, label placement, separator and inline type,
173
+ * overridden per variant in the blocks above. */
174
+ --ff-countdown-cell-gap: 0.75rem;
175
+ --ff-countdown-cell-padding: 0;
176
+ --ff-countdown-cell-radius: 0;
177
+ --ff-countdown-cell-border-width: 0px;
178
+ --ff-countdown-cell-bg: transparent;
179
+ --ff-countdown-cell-shadow: none;
180
+ --ff-countdown-cell-shadow-hover: var(--ff-countdown-cell-shadow);
181
+ --ff-countdown-numeral-color: var(--ff-color-foreground);
182
+ --ff-countdown-numeral-font: var(--ff-font-heading);
183
+ --ff-countdown-label-color: var(--ff-color-muted-foreground);
184
+ --ff-countdown-label-size: 0.625rem;
185
+ --ff-countdown-label-order: 1;
186
+ --ff-countdown-separator-glyph: "/";
187
+ --ff-countdown-separator-display: inline;
188
+ --ff-countdown-separator-color: var(--ff-color-muted-foreground);
189
+ --ff-countdown-inline-size: inherit;
190
+ --ff-countdown-inline-weight: 600;
191
+ --ff-countdown-inline-tracking: 0;
192
+
193
+ /* Numeral fitting: the bounds the fitted numeral size is clamped to, and
194
+ * the horizontal space the numerals cannot have — the panel's own padding
195
+ * plus, per variant, each cell's padding and border and each gap's width
196
+ * and separator glyph. Rounded up: over-reserving shrinks the numerals,
197
+ * under-reserving overflows the widget. */
198
+ --ff-countdown-numeral-min: 1.25rem;
199
+ --ff-countdown-numeral-max: 2.5rem;
200
+ --ff-countdown-reserve-outer: 2.5rem;
201
+ --ff-countdown-reserve-cell: 0rem;
202
+ --ff-countdown-reserve-gap: 3rem;
203
+
204
+ /* Countdown urgency: the colours a warning or critical cell takes. */
205
+ --ff-countdown-tone-warning-color: var(--ff-color-warning);
206
+ --ff-countdown-tone-critical-color: var(--ff-color-danger);
207
+ --ff-countdown-tone-critical-bg: transparent;
208
+ --ff-countdown-tone-critical-fg: inherit;
209
+
210
+ /* Panel geometry: the padding around a panel's stack and the gap between
211
+ * the rows of that stack and of a panel body. */
212
+ --ff-panel-gap: 1.25rem;
213
+ --ff-panel-padding: 1.25rem;
214
+
215
+ /* Surface elevation, one token per surface and per interactive state it
216
+ * distinguishes. A variant that raises a surface overrides these. */
217
+ --ff-card-shadow: none;
218
+ --ff-card-shadow-hover: var(--ff-card-shadow);
219
+
220
+ /* The `elevated` card is raised above its aesthetic's resting surface, so it
221
+ * carries its own pair rather than reading the resting one. The default is
222
+ * the elevation the variant-less card has always drawn. */
223
+ --ff-card-shadow-elevated: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
224
+ --ff-card-shadow-elevated-hover: var(--ff-card-shadow-elevated);
225
+ --ff-progress-shadow: none;
226
+ --ff-progress-bar-shadow: none;
227
+ --ff-loading-shadow: none;
228
+
229
+ /* Surfaces that also carry a focus ring compose their shadow with it
230
+ * through Tailwind's own shadow utility, so the empty value here is the
231
+ * transparent shadow that composes rather than the `none` that would
232
+ * invalidate the whole list — and the ring with it. */
233
+ --ff-button-shadow: 0 0 #0000;
234
+ --ff-button-shadow-hover: var(--ff-button-shadow);
235
+ --ff-button-shadow-active: var(--ff-button-shadow);
236
+ --ff-button-shadow-primary: var(--ff-button-shadow);
237
+ --ff-input-shadow: 0 0 #0000;
238
+ --ff-input-shadow-focus: var(--ff-input-shadow);
239
+ --ff-otp-shadow: 0 0 #0000;
240
+ --ff-otp-shadow-focus: var(--ff-otp-shadow);
241
+ --ff-otp-shadow-focus-error: var(--ff-otp-shadow-focus);
242
+
243
+ /* Selector rows (a product, a payment method): the box a row draws and the
244
+ * square a product thumbnail occupies inside it. */
245
+ --ff-row-gap: 0.75rem;
246
+ --ff-row-padding: 0.75rem;
247
+ --ff-row-radius: 0.375rem;
248
+ --ff-row-thumb-size: 2.75rem;
249
+ --ff-row-thumb-size-narrow: 2.5rem;
250
+
251
+ /* Ledger rows: the rule drawn between them, and the colour each tone
252
+ * carries on the marker glyph and the value. */
253
+ --ff-ledger-row-padding: 0.375rem;
254
+ --ff-ledger-border-color: color-mix(in oklab, var(--ff-color-border) 40%, transparent);
255
+ --ff-ledger-tone-default: var(--ff-color-foreground);
256
+ --ff-ledger-tone-muted: var(--ff-color-muted);
257
+ --ff-ledger-tone-live: var(--ff-color-primary);
258
+ --ff-ledger-tone-danger: var(--ff-color-danger);
259
+ --ff-ledger-tone-success: var(--ff-color-success);
260
+ }
261
+
262
+ /**
263
+ * Hero is the same countdown at a larger scale: a taller numeral ceiling, a
264
+ * wider cell gap, and a gap reserve that counts that gap either side of the
265
+ * larger separator glyph. The variants that draw no separator reserve the
266
+ * gap alone.
267
+ */
268
+ .fanfare-themed [data-slot="countdown"][data-size="hero"] {
269
+ --ff-countdown-numeral-max: 3.75rem;
270
+ --ff-countdown-cell-gap: 1rem;
271
+ --ff-countdown-reserve-gap: 3.75rem;
272
+ }
273
+
274
+ .fanfare-themed[data-fanfare-variant="retro"] [data-slot="countdown"][data-size="hero"],
275
+ .fanfare-themed[data-fanfare-variant="rounded"] [data-slot="countdown"][data-size="hero"] {
276
+ --ff-countdown-reserve-gap: 1rem;
277
+ }
278
+
279
+ /**
280
+ * Four unit labels plus three separators need more width than the widget
281
+ * has, so a countdown showing four cells drops its separators at every size
282
+ * and reserves the cell gap alone between cells.
283
+ */
284
+ .fanfare-themed [data-slot="countdown"][data-units="4"] {
285
+ --ff-countdown-separator-display: none;
286
+ --ff-countdown-reserve-gap: var(--ff-countdown-cell-gap);
287
+ }
288
+
289
+ /**
290
+ * The boxed variants' cell padding alone would leave four cells no room for
291
+ * numerals at the narrowest widget width the cells layout renders at, so a
292
+ * four-cell row narrows each cell's side padding; the cell reserve follows.
293
+ */
294
+ .fanfare-themed[data-fanfare-variant="rounded"] [data-slot="countdown"][data-units="4"] {
295
+ --ff-countdown-cell-padding: 0.75rem 0.5rem;
296
+ --ff-countdown-reserve-cell: 1.125rem;
297
+ }
298
+
299
+ .fanfare-themed[data-fanfare-variant="retro"] [data-slot="countdown"][data-units="4"] {
300
+ --ff-countdown-cell-padding: 0.5rem 0.5rem;
301
+ --ff-countdown-reserve-cell: 1.5rem;
101
302
  }
102
303
 
103
304
  /**
@@ -141,3 +342,384 @@
141
342
  }
142
343
  }
143
344
  }
345
+
346
+ @layer components {
347
+ /**
348
+ * Countdown token consumers. The layer keeps a one-off Tailwind utility
349
+ * winning over these rules, and the `.fanfare-themed` prefix keeps them from
350
+ * reaching a host page's own markup once the stylesheet is imported.
351
+ */
352
+ /**
353
+ * Cells are a two-row grid: every unit spans both rows through a subgrid, so
354
+ * the numerals share the first row and each separator centres on that row
355
+ * whatever size the numerals fit to.
356
+ */
357
+ .fanfare-themed [data-slot="countdown-cells"] {
358
+ column-gap: var(--ff-countdown-cell-gap);
359
+ }
360
+
361
+ .fanfare-themed [data-slot="countdown-unit"] {
362
+ padding: var(--ff-countdown-cell-padding);
363
+ border-radius: var(--ff-countdown-cell-radius);
364
+ border-width: var(--ff-countdown-cell-border-width);
365
+ border-style: solid;
366
+ background-color: var(--ff-countdown-cell-bg);
367
+ box-shadow: var(--ff-countdown-cell-shadow);
368
+ }
369
+
370
+ /* Hover elevation is gated on a real pointer, so a tap does not leave a
371
+ * cell raised. */
372
+ @media (hover: hover) {
373
+ .fanfare-themed [data-slot="countdown-unit"]:hover {
374
+ box-shadow: var(--ff-countdown-cell-shadow-hover);
375
+ }
376
+ }
377
+
378
+ /* Per-digit boxes inherit the numeral face from their unit. */
379
+ .fanfare-themed [data-slot="countdown-unit-digits"] {
380
+ color: var(--ff-countdown-numeral-color);
381
+ font-family: var(--ff-countdown-numeral-font);
382
+ }
383
+
384
+ /**
385
+ * Per-unit numerals size themselves to the widget: the container's inline
386
+ * size less the reserved chrome, divided across every digit on screen
387
+ * (`--ff-countdown-digits`, across `--ff-countdown-cells` cells, at the
388
+ * variant's advance ratio). The per-digit granularity is excluded — its cells are fixed boxes
389
+ * whose numerals are sized by utility to match.
390
+ *
391
+ * `cqi` cannot name a container, so it binds to the nearest one. Every
392
+ * countdown the SDK renders sits under the widget root and outside
393
+ * `ProductSelector`'s own container, so this reads the widget's width.
394
+ */
395
+ .fanfare-themed [data-cells="unit"] [data-slot="countdown-unit-digits"] {
396
+ font-size: clamp(
397
+ var(--ff-countdown-numeral-min),
398
+ calc(
399
+ (
400
+ 100cqi - var(--ff-countdown-reserve-outer) - var(--ff-countdown-cells) * var(--ff-countdown-reserve-cell) -
401
+ (var(--ff-countdown-cells) - 1) * var(--ff-countdown-reserve-gap)
402
+ ) /
403
+ (var(--ff-countdown-digits) * var(--ff-numeral-advance))
404
+ ),
405
+ var(--ff-countdown-numeral-max)
406
+ );
407
+ }
408
+
409
+ .fanfare-themed [data-slot="countdown-unit-label"] {
410
+ order: var(--ff-countdown-label-order);
411
+ font-size: var(--ff-countdown-label-size);
412
+ color: var(--ff-countdown-label-color);
413
+ }
414
+
415
+ .fanfare-themed [data-slot="countdown-separator"] {
416
+ display: var(--ff-countdown-separator-display);
417
+ color: var(--ff-countdown-separator-color);
418
+ }
419
+
420
+ .fanfare-themed [data-slot="countdown-separator"]::before {
421
+ content: var(--ff-countdown-separator-glyph);
422
+ }
423
+
424
+ .fanfare-themed [data-layout="inline"][data-slot="countdown"],
425
+ .fanfare-themed [data-slot="countdown-inline"] {
426
+ font-size: var(--ff-countdown-inline-size);
427
+ font-weight: var(--ff-countdown-inline-weight);
428
+ letter-spacing: var(--ff-countdown-inline-tracking);
429
+ }
430
+
431
+ /**
432
+ * Both layouts publish the resolved tone on their root: the cells layout
433
+ * colours each unit's numerals, the inline layout colours the single line,
434
+ * and the cells layout's narrow-container inline fallback picks up the same
435
+ * rule.
436
+ */
437
+ .fanfare-themed [data-slot="countdown-unit"][data-tone="warning"] [data-slot="countdown-unit-digits"],
438
+ .fanfare-themed [data-slot="countdown"][data-tone="warning"] [data-slot="countdown-inline"] {
439
+ color: var(--ff-countdown-tone-warning-color);
440
+ }
441
+
442
+ .fanfare-themed [data-slot="countdown-unit"][data-tone="critical"] [data-slot="countdown-unit-digits"],
443
+ .fanfare-themed [data-slot="countdown"][data-tone="critical"] [data-slot="countdown-inline"] {
444
+ color: var(--ff-countdown-tone-critical-color);
445
+ }
446
+
447
+ /**
448
+ * Retro's urgency accent is part of that variant's aesthetic rather than the
449
+ * global danger semantic, so the filled cell is scoped to the retro variant
450
+ * instead of moving `--ff-color-danger`. Other variants keep the cell surface
451
+ * their geometry tokens give them and carry urgency in the numeral colour
452
+ * alone.
453
+ */
454
+ .fanfare-themed[data-fanfare-variant="retro"] [data-slot="countdown-unit"][data-tone="critical"] {
455
+ background-color: var(--ff-countdown-tone-critical-bg);
456
+ color: var(--ff-countdown-tone-critical-fg);
457
+ }
458
+
459
+ /**
460
+ * The global critical rule sets the numeral colour directly, so the filled
461
+ * cell's foreground has to be restated on the numerals and label rather than
462
+ * relied on through inheritance.
463
+ */
464
+ .fanfare-themed[data-fanfare-variant="retro"]
465
+ [data-slot="countdown-unit"][data-tone="critical"]
466
+ [data-slot="countdown-unit-digits"],
467
+ .fanfare-themed[data-fanfare-variant="retro"]
468
+ [data-slot="countdown-unit"][data-tone="critical"]
469
+ [data-slot="countdown-unit-label"] {
470
+ color: var(--ff-countdown-tone-critical-fg);
471
+ }
472
+
473
+ /**
474
+ * Display text that is prose rather than a fixed glyph run: it scales with
475
+ * the widget between a legible floor and the design's ceiling, so a narrow
476
+ * embed shrinks the words instead of wrapping or clipping them.
477
+ */
478
+ .fanfare-themed [data-slot="stat-value"] {
479
+ font-size: clamp(var(--ff-stat-min), var(--ff-stat-scale), var(--ff-stat-max));
480
+ font-weight: var(--ff-stat-weight);
481
+ line-height: 1.2;
482
+ }
483
+
484
+ .fanfare-themed [data-slot="panel-title"] {
485
+ font-size: clamp(var(--ff-title-min), var(--ff-title-scale), var(--ff-title-max));
486
+ font-weight: var(--ff-title-weight);
487
+ line-height: 1.4;
488
+ }
489
+
490
+ /**
491
+ * The payment panel's section heading reads at body scale under its badge
492
+ * and hero stat. A retry moves focus to it so a screen reader starts from
493
+ * the explanation; it is not a control, and the Retry badge already marks
494
+ * the state, so the programmatic focus draws no ring.
495
+ */
496
+ .fanfare-themed [data-slot="checkout-title"] {
497
+ color: var(--ff-color-foreground);
498
+ font-size: 0.875rem;
499
+ font-weight: 600;
500
+ line-height: 1.25rem;
501
+ outline: none;
502
+ }
503
+
504
+ .fanfare-themed [data-slot="panel-body"],
505
+ .fanfare-themed [data-slot="panel-stack"] {
506
+ gap: var(--ff-panel-gap);
507
+ }
508
+
509
+ .fanfare-themed [data-slot="panel-stack"] {
510
+ padding: var(--ff-panel-padding);
511
+ }
512
+
513
+ /**
514
+ * Every Card carries `data-fanfare-card` whatever `data-slot` its consumer
515
+ * publishes on it, so the surface rules key on that marker rather than on a
516
+ * slot name a panel is free to replace.
517
+ */
518
+ .fanfare-themed [data-fanfare-card] {
519
+ box-shadow: var(--ff-card-shadow);
520
+ }
521
+
522
+ /**
523
+ * A raised card publishes `data-elevated`, so its elevation is a token the
524
+ * variant reaches rather than a utility that outranks every variant's
525
+ * aesthetic.
526
+ */
527
+ .fanfare-themed [data-fanfare-card][data-elevated] {
528
+ box-shadow: var(--ff-card-shadow-elevated);
529
+ }
530
+
531
+ @media (hover: hover) {
532
+ .fanfare-themed [data-fanfare-card]:hover {
533
+ box-shadow: var(--ff-card-shadow-hover);
534
+ }
535
+
536
+ .fanfare-themed [data-fanfare-card][data-elevated]:hover {
537
+ box-shadow: var(--ff-card-shadow-elevated-hover);
538
+ }
539
+ }
540
+
541
+ .fanfare-themed [data-slot="progress-track"] {
542
+ box-shadow: var(--ff-progress-shadow);
543
+ }
544
+
545
+ .fanfare-themed [data-slot="progress-bar"] {
546
+ box-shadow: var(--ff-progress-bar-shadow);
547
+ }
548
+
549
+ .fanfare-themed [data-slot="loading-spinner"] {
550
+ box-shadow: var(--ff-loading-shadow);
551
+ }
552
+
553
+ .fanfare-themed [data-slot="product-selector-row"],
554
+ .fanfare-themed [data-slot="payment-method-row"] {
555
+ gap: var(--ff-row-gap);
556
+ padding: var(--ff-row-padding);
557
+ border-radius: var(--ff-row-radius);
558
+ }
559
+
560
+ /**
561
+ * A payment method row draws the product selector's row, so a payment choice
562
+ * reads as the same kind of choice as the item it pays for. The native radio
563
+ * stays in the row: it carries the group's keyboard model, and the row takes
564
+ * its checked and focus states from it.
565
+ */
566
+ .fanfare-themed [data-slot="payment-method-row"] {
567
+ display: flex;
568
+ align-items: center;
569
+ width: 100%;
570
+ cursor: pointer;
571
+ border: 1px solid color-mix(in oklab, var(--ff-color-border) 40%, transparent);
572
+ background-color: var(--ff-color-background);
573
+ transition:
574
+ background-color 150ms,
575
+ border-color 150ms,
576
+ box-shadow 150ms;
577
+ }
578
+
579
+ @media (hover: hover) {
580
+ .fanfare-themed [data-slot="payment-method-row"]:hover {
581
+ background-color: var(--ff-color-secondary);
582
+ }
583
+ }
584
+
585
+ .fanfare-themed [data-slot="payment-method-row"]:has(:checked) {
586
+ border-color: var(--ff-color-foreground);
587
+ box-shadow: inset 0 0 0 1px var(--ff-color-foreground);
588
+ }
589
+
590
+ .fanfare-themed [data-slot="payment-method-row"]:has(:focus-visible) {
591
+ outline: 2px solid var(--ff-color-primary);
592
+ outline-offset: 2px;
593
+ }
594
+
595
+ .fanfare-themed [data-slot="payment-method-row"] > input {
596
+ flex-shrink: 0;
597
+ width: 1rem;
598
+ height: 1rem;
599
+ margin: 0;
600
+ outline: none;
601
+ accent-color: var(--ff-color-foreground);
602
+ }
603
+
604
+ .fanfare-themed [data-slot="payment-method-row"] > svg {
605
+ flex-shrink: 0;
606
+ width: 1.25rem;
607
+ height: 1.25rem;
608
+ color: var(--ff-color-muted);
609
+ fill: none;
610
+ stroke: currentColor;
611
+ stroke-width: 1.5;
612
+ stroke-linecap: round;
613
+ stroke-linejoin: round;
614
+ }
615
+
616
+ /**
617
+ * The card field's frame draws the variant's text input around Stripe's
618
+ * iframe. Focus and validity live inside that iframe, where no selector
619
+ * reaches, so the element's own events publish them on the frame.
620
+ */
621
+ .fanfare-themed [data-fanfare-field="stripe-card-frame"][data-framed] {
622
+ padding: 0.625rem 0.75rem;
623
+ border: 1px solid var(--ff-color-border);
624
+ border-radius: 0.375rem;
625
+ background-color: var(--ff-color-background);
626
+ transition:
627
+ border-color 200ms,
628
+ box-shadow 200ms,
629
+ background-color 200ms;
630
+ }
631
+
632
+ .fanfare-themed [data-fanfare-field="stripe-card-frame"][data-focused] {
633
+ border-color: var(--ff-color-primary);
634
+ box-shadow: 0 0 0 2px color-mix(in oklab, var(--ff-color-primary) 20%, transparent);
635
+ }
636
+
637
+ .fanfare-themed[data-fanfare-variant="retro"] [data-fanfare-field="stripe-card-frame"][data-framed] {
638
+ border: 3px solid var(--ff-color-foreground);
639
+ border-radius: 0;
640
+ box-shadow: inset 3px 3px 0 var(--ff-color-secondary);
641
+ }
642
+
643
+ .fanfare-themed[data-fanfare-variant="retro"] [data-fanfare-field="stripe-card-frame"][data-focused] {
644
+ border-color: var(--ff-color-primary);
645
+ box-shadow: inset 3px 3px 0 var(--ff-color-primary);
646
+ }
647
+
648
+ .fanfare-themed[data-fanfare-variant="rounded"] [data-fanfare-field="stripe-card-frame"][data-framed] {
649
+ border-color: color-mix(in oklab, var(--ff-color-foreground) 12%, transparent);
650
+ border-radius: 0.75rem;
651
+ background-color: color-mix(in oklch, var(--ff-color-foreground) 3%, var(--ff-color-background));
652
+ box-shadow: var(--ff-input-shadow);
653
+ }
654
+
655
+ .fanfare-themed[data-fanfare-variant="rounded"] [data-fanfare-field="stripe-card-frame"][data-focused] {
656
+ border-color: color-mix(in oklab, var(--ff-color-primary) 50%, transparent);
657
+ background-color: var(--ff-color-background);
658
+ box-shadow: var(--ff-input-shadow-focus);
659
+ }
660
+
661
+ .fanfare-themed[data-fanfare-variant="clean"] [data-fanfare-field="stripe-card-frame"][data-framed] {
662
+ padding-inline: 0;
663
+ border-width: 0 0 1px;
664
+ border-color: color-mix(in oklab, var(--ff-color-foreground) 20%, transparent);
665
+ border-radius: 0;
666
+ background-color: transparent;
667
+ }
668
+
669
+ .fanfare-themed[data-fanfare-variant="clean"] [data-fanfare-field="stripe-card-frame"][data-focused] {
670
+ border-color: var(--ff-color-primary);
671
+ box-shadow: none;
672
+ }
673
+
674
+ /* Stated after the variants, at their specificity, so every variant shows it. */
675
+ .fanfare-themed [data-fanfare-field="stripe-card-frame"][data-framed][data-invalid] {
676
+ border-color: var(--ff-color-danger);
677
+ }
678
+
679
+ .fanfare-themed [data-slot="product-thumb"] {
680
+ width: var(--ff-row-thumb-size);
681
+ height: var(--ff-row-thumb-size);
682
+ }
683
+
684
+ /**
685
+ * The thumbnail takes its narrow side length once the picker's own container
686
+ * is too tight for the wide one. The query is unnamed so it binds to that
687
+ * container — the nearest one — rather than to the widget, which a picker
688
+ * rendered on its own need not sit inside.
689
+ */
690
+ @container not (min-width: 400px) {
691
+ .fanfare-themed [data-slot="product-thumb"] {
692
+ width: var(--ff-row-thumb-size-narrow);
693
+ height: var(--ff-row-thumb-size-narrow);
694
+ }
695
+ }
696
+
697
+ .fanfare-themed [data-slot="ledger-row"] {
698
+ padding-block: var(--ff-ledger-row-padding);
699
+ border-bottom-color: var(--ff-ledger-border-color);
700
+ }
701
+
702
+ /**
703
+ * Tone is published on the row and inherited by the marker glyph and the
704
+ * value; the label states its own muted colour and is unaffected.
705
+ */
706
+ .fanfare-themed [data-slot="ledger-row"][data-tone="default"] {
707
+ color: var(--ff-ledger-tone-default);
708
+ }
709
+
710
+ .fanfare-themed [data-slot="ledger-row"][data-tone="muted"] {
711
+ color: var(--ff-ledger-tone-muted);
712
+ }
713
+
714
+ .fanfare-themed [data-slot="ledger-row"][data-tone="live"] {
715
+ color: var(--ff-ledger-tone-live);
716
+ }
717
+
718
+ .fanfare-themed [data-slot="ledger-row"][data-tone="danger"] {
719
+ color: var(--ff-ledger-tone-danger);
720
+ }
721
+
722
+ .fanfare-themed [data-slot="ledger-row"][data-tone="success"] {
723
+ color: var(--ff-ledger-tone-success);
724
+ }
725
+ }
@@ -0,0 +1,58 @@
1
+ import { WidgetVariant } from '@fanfare-io/fanfare-sdk-core/theme';
2
+ /** The catalogue categories, each with its own fallback stack. */
3
+ export type FontCategory = "Display" | "Handwriting" | "Monospace" | "Sans Serif" | "Serif";
4
+ export interface FontCatalogueEntry {
5
+ category: FontCategory;
6
+ /** The weights the family publishes; requesting one it lacks fails the whole font request. */
7
+ weights: number[];
8
+ }
9
+ /**
10
+ * Generic stacks a browser can render without a network request, so a family that fails to load
11
+ * degrades to a face of the same flavour rather than to the default serif.
12
+ */
13
+ export declare const CATEGORY_FALLBACKS: Record<FontCategory, string>;
14
+ /**
15
+ * CSS generic families and CSS-wide keywords. A font value that is one of these names no family at
16
+ * all, so it is neither fetchable nor quotable: quoting it would make the browser look for a family
17
+ * literally called `serif`. Kept lowercase because family names are case-insensitive in CSS.
18
+ */
19
+ export declare const GENERIC_FONT_KEYWORDS: ReadonlySet<string>;
20
+ /** The families the admin font picker offers, in the picker's own order. */
21
+ export declare const FONT_CATALOGUE: Record<string, FontCatalogueEntry>;
22
+ /** Whether a family resolves without a network request. Family names are case-insensitive in CSS. */
23
+ export declare const isSystemFont: (family: string) => boolean;
24
+ /**
25
+ * The catalogue's own spelling of a family. CSS matches family names case-insensitively, but the
26
+ * Google Fonts css2 endpoint does not — it rejects `figtree` outright — so a host's casing is
27
+ * resolved to the catalogue's before anything is requested.
28
+ */
29
+ export declare const catalogueFamilyName: (family: string) => string | undefined;
30
+ /**
31
+ * The Google Fonts stylesheet for a family, asking for exactly the weights it publishes — the
32
+ * css2 endpoint rejects the whole request when it names a weight the family lacks, and several
33
+ * curated families publish fewer than the usual 400/500/600/700. A family outside the catalogue
34
+ * has no known weight list, so it is requested without a weight axis at all.
35
+ */
36
+ export declare const googleFontStylesheetHref: (family: string) => string;
37
+ /** The faces a variant's own token block names: its text face, and its mono face where it has one. */
38
+ export interface VariantFamilies {
39
+ body: string;
40
+ mono?: string;
41
+ }
42
+ /** The families each variant's own token block names, used when a theme names none of its own. */
43
+ export declare const VARIANT_DEFAULT_FAMILIES: Record<WidgetVariant, VariantFamilies>;
44
+ /**
45
+ * The faces to fetch for a variant. A variant name the build does not know still reaches here — a
46
+ * theme served by a newer admin, or a hand-written element attribute — and takes the default
47
+ * variant's faces, which are the ones its token block falls back to as well.
48
+ */
49
+ export declare const variantFamilies: (variant: string | undefined) => VariantFamilies;
50
+ /**
51
+ * Requests a family's stylesheet at most once per page. The link is inserted for print only and
52
+ * promoted to every medium once it has loaded, so a slow or unreachable font origin never holds
53
+ * first paint. A request that fails leaves nothing behind and is never retried — the theme's
54
+ * fallback stack carries the text, and a second attempt would spend the same latency again.
55
+ */
56
+ export declare function ensureFontStylesheet(family: string): void;
57
+ /** Test-only seam: forgets which families were requested. Never exported from the package entry. */
58
+ export declare function resetFontLoaderCache(): void;