@fanfare-io/fanfare-sdk-react 0.21.0 → 0.22.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
+ /* Product selector row: the box a row draws and the square its thumbnail
244
+ * 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,250 @@
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
+ .fanfare-themed [data-slot="panel-body"],
491
+ .fanfare-themed [data-slot="panel-stack"] {
492
+ gap: var(--ff-panel-gap);
493
+ }
494
+
495
+ .fanfare-themed [data-slot="panel-stack"] {
496
+ padding: var(--ff-panel-padding);
497
+ }
498
+
499
+ /**
500
+ * Every Card carries `data-fanfare-card` whatever `data-slot` its consumer
501
+ * publishes on it, so the surface rules key on that marker rather than on a
502
+ * slot name a panel is free to replace.
503
+ */
504
+ .fanfare-themed [data-fanfare-card] {
505
+ box-shadow: var(--ff-card-shadow);
506
+ }
507
+
508
+ /**
509
+ * A raised card publishes `data-elevated`, so its elevation is a token the
510
+ * variant reaches rather than a utility that outranks every variant's
511
+ * aesthetic.
512
+ */
513
+ .fanfare-themed [data-fanfare-card][data-elevated] {
514
+ box-shadow: var(--ff-card-shadow-elevated);
515
+ }
516
+
517
+ @media (hover: hover) {
518
+ .fanfare-themed [data-fanfare-card]:hover {
519
+ box-shadow: var(--ff-card-shadow-hover);
520
+ }
521
+
522
+ .fanfare-themed [data-fanfare-card][data-elevated]:hover {
523
+ box-shadow: var(--ff-card-shadow-elevated-hover);
524
+ }
525
+ }
526
+
527
+ .fanfare-themed [data-slot="progress-track"] {
528
+ box-shadow: var(--ff-progress-shadow);
529
+ }
530
+
531
+ .fanfare-themed [data-slot="progress-bar"] {
532
+ box-shadow: var(--ff-progress-bar-shadow);
533
+ }
534
+
535
+ .fanfare-themed [data-slot="loading-spinner"] {
536
+ box-shadow: var(--ff-loading-shadow);
537
+ }
538
+
539
+ .fanfare-themed [data-slot="product-selector-row"] {
540
+ gap: var(--ff-row-gap);
541
+ padding: var(--ff-row-padding);
542
+ border-radius: var(--ff-row-radius);
543
+ }
544
+
545
+ .fanfare-themed [data-slot="product-thumb"] {
546
+ width: var(--ff-row-thumb-size);
547
+ height: var(--ff-row-thumb-size);
548
+ }
549
+
550
+ /**
551
+ * The thumbnail takes its narrow side length once the picker's own container
552
+ * is too tight for the wide one. The query is unnamed so it binds to that
553
+ * container — the nearest one — rather than to the widget, which a picker
554
+ * rendered on its own need not sit inside.
555
+ */
556
+ @container not (min-width: 400px) {
557
+ .fanfare-themed [data-slot="product-thumb"] {
558
+ width: var(--ff-row-thumb-size-narrow);
559
+ height: var(--ff-row-thumb-size-narrow);
560
+ }
561
+ }
562
+
563
+ .fanfare-themed [data-slot="ledger-row"] {
564
+ padding-block: var(--ff-ledger-row-padding);
565
+ border-bottom-color: var(--ff-ledger-border-color);
566
+ }
567
+
568
+ /**
569
+ * Tone is published on the row and inherited by the marker glyph and the
570
+ * value; the label states its own muted colour and is unaffected.
571
+ */
572
+ .fanfare-themed [data-slot="ledger-row"][data-tone="default"] {
573
+ color: var(--ff-ledger-tone-default);
574
+ }
575
+
576
+ .fanfare-themed [data-slot="ledger-row"][data-tone="muted"] {
577
+ color: var(--ff-ledger-tone-muted);
578
+ }
579
+
580
+ .fanfare-themed [data-slot="ledger-row"][data-tone="live"] {
581
+ color: var(--ff-ledger-tone-live);
582
+ }
583
+
584
+ .fanfare-themed [data-slot="ledger-row"][data-tone="danger"] {
585
+ color: var(--ff-ledger-tone-danger);
586
+ }
587
+
588
+ .fanfare-themed [data-slot="ledger-row"][data-tone="success"] {
589
+ color: var(--ff-ledger-tone-success);
590
+ }
591
+ }
@@ -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;
@@ -2,8 +2,16 @@ import { BrandTheme } from '@fanfare-io/fanfare-sdk-core/theme';
2
2
  import { ResolvedTheme, WidgetVariant } from './theme.types';
3
3
  /**
4
4
  * Context value for theme state.
5
+ *
6
+ * `theme` is the merged value a component renders with, every field filled from the SDK defaults.
7
+ * `overrides` answers the different question of what the host actually asked for: the raw fields
8
+ * the providers above this point set, accumulated but never merged with defaults, and `{}` where no
9
+ * provider set anything. A merged value cannot answer that — a host naming the default family is
10
+ * indistinguishable there from a host naming nothing.
5
11
  */
6
12
  export interface ThemeContextValue extends ResolvedTheme {
13
+ /** The raw fields providers up the tree explicitly set, without defaults */
14
+ overrides: BrandTheme;
7
15
  /** Whether this is the root theme provider */
8
16
  isRoot: boolean;
9
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanfare-io/fanfare-sdk-react",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "React adapter for Fanfare SDK",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",
@@ -43,8 +43,8 @@
43
43
  "nanostores": "^1.1.0",
44
44
  "tailwind-merge": "^2.6.0",
45
45
  "valibot": "^1.1.0",
46
- "@fanfare-io/fanfare-sdk-core": "0.21.0",
47
- "@fanfare-io/fanfare-sdk-i18n": "0.21.0"
46
+ "@fanfare-io/fanfare-sdk-core": "0.22.0",
47
+ "@fanfare-io/fanfare-sdk-i18n": "0.22.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@iconify-json/heroicons": "^1.2.3",
@@ -55,7 +55,6 @@
55
55
  "@size-limit/preset-small-lib": "^11.1.8",
56
56
  "@storybook/addon-a11y": "^10.4.0",
57
57
  "@storybook/addon-docs": "^10.4.0",
58
- "@storybook/addon-themes": "^10.4.0",
59
58
  "@storybook/addon-vitest": "~10.2.16",
60
59
  "@storybook/react-vite": "^10.4.0",
61
60
  "@stripe/stripe-js": "^7.3.1",
@@ -91,8 +90,8 @@
91
90
  "vite-plugin-dts": "^4.5.4",
92
91
  "vite-tsconfig-paths": "^5.1.4",
93
92
  "vitest": "^5.0.0",
94
- "@fanfare-io/fanfare-sdk-core": "0.21.0",
95
- "@fanfare-io/fanfare-sdk-solid": "0.21.0"
93
+ "@fanfare-io/fanfare-sdk-core": "0.22.0",
94
+ "@fanfare-io/fanfare-sdk-solid": "0.22.0"
96
95
  },
97
96
  "sideEffects": [
98
97
  "**/*.css"
@@ -110,7 +109,7 @@
110
109
  {
111
110
  "name": "React adapter",
112
111
  "path": "dist/index.js",
113
- "limit": "67 KB",
112
+ "limit": "70 KB",
114
113
  "ignore": [
115
114
  "react",
116
115
  "react-dom",