@primitiv-ui/tokens 0.1.27 → 0.1.29

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.
@@ -0,0 +1,156 @@
1
+ import palette from './palette.json'
2
+ import intent from './intent.json'
3
+
4
+ type Mode = 'light' | 'dark'
5
+ type DtcgNode = { $type?: string; $value?: unknown; [key: string]: unknown }
6
+
7
+ /**
8
+ * Resolve a DTCG alias (`{color.neutral.900}`) or literal value against a
9
+ * mode's palette tree, following chained aliases.
10
+ */
11
+ function resolveRef(mode: Mode, ref: string): string {
12
+ if (!ref.startsWith('{')) return ref
13
+ const path = ref.slice(1, -1).split('.')
14
+ let node: DtcgNode = (palette as Record<Mode, DtcgNode>)[mode]
15
+ for (const key of path) node = node[key] as DtcgNode
16
+ return resolveRef(mode, node.$value as string)
17
+ }
18
+
19
+ /** Resolve a slash-separated Intent token (`"surface/raised"`) to its hex colour in `mode`. */
20
+ function intentColor(mode: Mode, token: string): string {
21
+ let node: DtcgNode = (intent as Record<Mode, DtcgNode>)[mode]
22
+ for (const key of token.split('/')) node = node[key] as DtcgNode
23
+ const value = node.$value as string
24
+ return value.startsWith('{') ? resolveRef(mode, value) : value
25
+ }
26
+
27
+ /** WCAG relative luminance (sRGB, 0-1). */
28
+ function luminance(hex: string): number {
29
+ const channel = (value: number) => {
30
+ const c = value / 255
31
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
32
+ }
33
+ const r = parseInt(hex.slice(1, 3), 16)
34
+ const g = parseInt(hex.slice(3, 5), 16)
35
+ const b = parseInt(hex.slice(5, 7), 16)
36
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
37
+ }
38
+
39
+ /** WCAG contrast ratio between two colours (1-21). */
40
+ function contrastRatio(a: string, b: string): number {
41
+ const [lighter, darker] = [luminance(a), luminance(b)].sort((x, y) => y - x)
42
+ return (lighter + 0.05) / (darker + 0.05)
43
+ }
44
+
45
+ // Regression guard for the swapped-index bug (verified against the Figma
46
+ // Intent collection, which always resolves color/neutral/* through the
47
+ // Light-mode ramp — Dark-mode Intent variables pick a *different* neutral
48
+ // step, not a different ramp). Our codebase instead carries a separately
49
+ // generated Dark ramp that is already the Light ramp's near-mirror
50
+ // (`dark.neutral.50 ≈ light.neutral.900`), so reproducing the same visual
51
+ // role in Dark mode means picking the same or a hex-nearest step in *our*
52
+ // Dark ramp — not re-swapping the index a second time. Several tokens were
53
+ // swapped a second time, cancelling the intended flip and landing both
54
+ // modes on the same (or a same-toned) colour.
55
+ describe.each(['content/primary', 'content/secondary'] as const)('%s', (token) => {
56
+ it('resolves to a different colour in light vs dark mode', () => {
57
+ expect(intentColor('light', token)).not.toBe(intentColor('dark', token))
58
+ })
59
+
60
+ it('meets WCAG AA (4.5:1) against surface/default in both modes', () => {
61
+ for (const mode of ['light', 'dark'] as const) {
62
+ const ratio = contrastRatio(intentColor(mode, token), intentColor(mode, 'surface/default'))
63
+ expect(ratio).toBeGreaterThanOrEqual(4.5)
64
+ }
65
+ })
66
+ })
67
+
68
+ // The neutral alpha ramp (Path A): the mode's veil colour — the neutral ramp's
69
+ // 900 end (soft-black in light, soft-white in dark) — held constant across all
70
+ // ten steps while only the alpha byte climbs. Guards both the anchor (the rgb
71
+ // part must be the veil, so the ramp inverts with the theme) and the curve
72
+ // (strictly increasing opacity).
73
+ describe('color/neutral-alpha ramp', () => {
74
+ const steps = ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900'] as const
75
+
76
+ it.each(['light', 'dark'] as const)("%s mode anchors every step on the mode's veil colour", (mode) => {
77
+ const veil = resolveRef(mode, '{color.neutral.900}')
78
+ for (const step of steps) {
79
+ expect(resolveRef(mode, `{color.neutral-alpha.${step}}`).slice(0, 7)).toBe(veil)
80
+ }
81
+ })
82
+
83
+ it.each(['light', 'dark'] as const)('%s mode climbs a strictly increasing alpha curve', (mode) => {
84
+ const alphas = steps.map((step) => parseInt(resolveRef(mode, `{color.neutral-alpha.${step}}`).slice(7, 9), 16))
85
+ for (let i = 1; i < alphas.length; i++) {
86
+ expect(alphas[i]).toBeGreaterThan(alphas[i - 1])
87
+ }
88
+ })
89
+
90
+ // The inverse companion: every step is the OTHER theme's neutral-alpha step,
91
+ // so each mode carries the opposite veil (soft-white in light, soft-black in
92
+ // dark). It exists for Figma's dark step-swap idiom — the file resolves the
93
+ // Palette collection through Light mode even on dark frames, so dark Intent
94
+ // variables alias neutral-alpha-inverse to reach the white veil — and the
95
+ // exact mirror keeps a variables backup a no-op.
96
+ it.each([
97
+ ['light', 'dark'],
98
+ ['dark', 'light'],
99
+ ] as const)("neutral-alpha-inverse in %s mode mirrors the %s mode's neutral-alpha ramp exactly", (mode, other) => {
100
+ for (const step of steps) {
101
+ expect(resolveRef(mode, `{color.neutral-alpha-inverse.${step}}`)).toBe(
102
+ resolveRef(other, `{color.neutral-alpha.${step}}`),
103
+ )
104
+ }
105
+ })
106
+ })
107
+
108
+ // Surfaces and borders that are supposed to track the theme (paler in light,
109
+ // darker in dark) rather than stay fixed.
110
+ describe.each([
111
+ 'surface/subtle',
112
+ 'surface/raised',
113
+ 'surface/overlay',
114
+ 'surface/floating',
115
+ 'surface/inverse',
116
+ 'surface/sunken',
117
+ 'border/subtle',
118
+ 'border/default',
119
+ 'table/row/stripe',
120
+ 'table/row/hover',
121
+ 'action/secondary/default',
122
+ 'action/secondary/hover',
123
+ 'action/secondary/active',
124
+ 'action/secondary/disabled',
125
+ 'action/secondary/foreground/default',
126
+ 'action/secondary/border/default',
127
+ 'action/secondary/border/hover',
128
+ 'action/secondary/border/disabled',
129
+ 'action/ghost/hover',
130
+ 'action/ghost/active',
131
+ ] as const)('%s', (token) => {
132
+ it('resolves to a different colour in light vs dark mode', () => {
133
+ expect(intentColor('light', token)).not.toBe(intentColor('dark', token))
134
+ })
135
+ })
136
+
137
+ // `surface/selected` (the ToggleGroup thumb) and `content/on-selected` (its
138
+ // label) are the deliberate exception: RFC 0017 requires them to read as
139
+ // light-surface-with-dark-label in *both* themes, so the thumb keeps lifting
140
+ // off a track that itself goes dark in dark mode. A near-1 contrast ratio
141
+ // between the two modes confirms neither one flipped.
142
+ describe('surface/selected and content/on-selected', () => {
143
+ it('stay visually consistent (not flipped) between light and dark mode', () => {
144
+ for (const token of ['surface/selected', 'content/on-selected'] as const) {
145
+ const ratio = contrastRatio(intentColor('light', token), intentColor('dark', token))
146
+ expect(ratio).toBeLessThan(1.25)
147
+ }
148
+ })
149
+
150
+ it('content/on-selected meets WCAG AA (4.5:1) against surface/selected in both modes', () => {
151
+ for (const mode of ['light', 'dark'] as const) {
152
+ const ratio = contrastRatio(intentColor(mode, 'content/on-selected'), intentColor(mode, 'surface/selected'))
153
+ expect(ratio).toBeGreaterThanOrEqual(4.5)
154
+ }
155
+ })
156
+ })
package/src/intent.json CHANGED
@@ -93,6 +93,16 @@
93
93
  }
94
94
  }
95
95
  },
96
+ "ghost": {
97
+ "hover": {
98
+ "$type": "color",
99
+ "$value": "{color.neutral-alpha.200}"
100
+ },
101
+ "active": {
102
+ "$type": "color",
103
+ "$value": "{color.neutral-alpha.300}"
104
+ }
105
+ },
96
106
  "danger": {
97
107
  "foreground": {
98
108
  "default": {
@@ -164,11 +174,81 @@
164
174
  }
165
175
  }
166
176
  },
177
+ "feedback": {
178
+ "neutral": {
179
+ "soft": {
180
+ "background": { "$type": "color", "$value": "{color.neutral.100}" },
181
+ "foreground": { "$type": "color", "$value": "{color.neutral.700}" }
182
+ }
183
+ },
184
+ "success": {
185
+ "soft": {
186
+ "background": { "$type": "color", "$value": "{color.success.100}" },
187
+ "foreground": { "$type": "color", "$value": "{color.success.700}" },
188
+ "border": { "$type": "color", "$value": "{color.success.700}" },
189
+ "hover": { "$type": "color", "$value": "{color.success.200}" },
190
+ "active": { "$type": "color", "$value": "{color.success.300}" }
191
+ },
192
+ "solid": {
193
+ "background": { "$type": "color", "$value": "{color.success.500}" },
194
+ "foreground": { "$type": "color", "$value": "{color.absolute-black}" }
195
+ }
196
+ },
197
+ "warning": {
198
+ "soft": {
199
+ "background": { "$type": "color", "$value": "{color.warning.100}" },
200
+ "foreground": { "$type": "color", "$value": "{color.warning.700}" },
201
+ "border": { "$type": "color", "$value": "{color.warning.700}" },
202
+ "hover": { "$type": "color", "$value": "{color.warning.200}" },
203
+ "active": { "$type": "color", "$value": "{color.warning.300}" }
204
+ },
205
+ "solid": {
206
+ "background": { "$type": "color", "$value": "{color.warning.500}" },
207
+ "foreground": { "$type": "color", "$value": "{color.absolute-black}" }
208
+ }
209
+ },
210
+ "info": {
211
+ "soft": {
212
+ "background": { "$type": "color", "$value": "{color.info.100}" },
213
+ "foreground": { "$type": "color", "$value": "{color.info.700}" },
214
+ "border": { "$type": "color", "$value": "{color.info.700}" },
215
+ "hover": { "$type": "color", "$value": "{color.info.200}" },
216
+ "active": { "$type": "color", "$value": "{color.info.300}" }
217
+ },
218
+ "solid": {
219
+ "background": { "$type": "color", "$value": "{color.info.500}" },
220
+ "foreground": { "$type": "color", "$value": "{color.absolute-black}" }
221
+ }
222
+ },
223
+ "danger": {
224
+ "soft": {
225
+ "background": { "$type": "color", "$value": "{color.danger.100}" },
226
+ "foreground": { "$type": "color", "$value": "{color.danger.700}" },
227
+ "border": { "$type": "color", "$value": "{color.danger.700}" },
228
+ "hover": { "$type": "color", "$value": "{color.danger.200}" },
229
+ "active": { "$type": "color", "$value": "{color.danger.300}" }
230
+ },
231
+ "solid": {
232
+ "background": { "$type": "color", "$value": "{color.danger.500}" },
233
+ "foreground": { "$type": "color", "$value": "{color.absolute-white}" }
234
+ }
235
+ }
236
+ },
237
+ "choice-card": {
238
+ "selected": {
239
+ "background": { "$type": "color", "$value": "{color.brand.100}" },
240
+ "border": { "$type": "color", "$value": "{color.brand.500}" }
241
+ }
242
+ },
167
243
  "surface": {
168
244
  "default": {
169
245
  "$type": "color",
170
246
  "$value": "{color.absolute-white}"
171
247
  },
248
+ "floating": {
249
+ "$type": "color",
250
+ "$value": "{color.absolute-white}"
251
+ },
172
252
  "subtle": {
173
253
  "$type": "color",
174
254
  "$value": "{color.neutral.100}"
@@ -342,24 +422,24 @@
342
422
  "secondary": {
343
423
  "default": {
344
424
  "$type": "color",
345
- "$value": "{color.neutral.800}"
425
+ "$value": "{color.neutral.100}"
346
426
  },
347
427
  "hover": {
348
428
  "$type": "color",
349
- "$value": "{color.neutral.700}"
429
+ "$value": "{color.neutral.200}"
350
430
  },
351
431
  "active": {
352
432
  "$type": "color",
353
- "$value": "{color.neutral.600}"
433
+ "$value": "{color.neutral.400}"
354
434
  },
355
435
  "disabled": {
356
436
  "$type": "color",
357
- "$value": "{color.neutral.900}"
437
+ "$value": "{color.neutral.50}"
358
438
  },
359
439
  "foreground": {
360
440
  "default": {
361
441
  "$type": "color",
362
- "$value": "{color.neutral.50}"
442
+ "$value": "{color.neutral.900}"
363
443
  },
364
444
  "disabled": {
365
445
  "$type": "color",
@@ -369,11 +449,11 @@
369
449
  "border": {
370
450
  "default": {
371
451
  "$type": "color",
372
- "$value": "{color.neutral.700}"
452
+ "$value": "{color.neutral.200}"
373
453
  },
374
454
  "hover": {
375
455
  "$type": "color",
376
- "$value": "{color.neutral.600}"
456
+ "$value": "{color.neutral.400}"
377
457
  },
378
458
  "active": {
379
459
  "$type": "color",
@@ -381,10 +461,20 @@
381
461
  },
382
462
  "disabled": {
383
463
  "$type": "color",
384
- "$value": "{color.neutral.800}"
464
+ "$value": "{color.neutral.100}"
385
465
  }
386
466
  }
387
467
  },
468
+ "ghost": {
469
+ "hover": {
470
+ "$type": "color",
471
+ "$value": "{color.neutral-alpha.200}"
472
+ },
473
+ "active": {
474
+ "$type": "color",
475
+ "$value": "{color.neutral-alpha.300}"
476
+ }
477
+ },
388
478
  "danger": {
389
479
  "foreground": {
390
480
  "default": {
@@ -456,34 +546,104 @@
456
546
  }
457
547
  }
458
548
  },
549
+ "feedback": {
550
+ "neutral": {
551
+ "soft": {
552
+ "background": { "$type": "color", "$value": "{color.neutral.100}" },
553
+ "foreground": { "$type": "color", "$value": "{color.neutral.700}" }
554
+ }
555
+ },
556
+ "success": {
557
+ "soft": {
558
+ "background": { "$type": "color", "$value": "{color.success.100}" },
559
+ "foreground": { "$type": "color", "$value": "{color.success.700}" },
560
+ "border": { "$type": "color", "$value": "{color.success.700}" },
561
+ "hover": { "$type": "color", "$value": "{color.success.200}" },
562
+ "active": { "$type": "color", "$value": "{color.success.300}" }
563
+ },
564
+ "solid": {
565
+ "background": { "$type": "color", "$value": "{color.success.500}" },
566
+ "foreground": { "$type": "color", "$value": "{color.absolute-black}" }
567
+ }
568
+ },
569
+ "warning": {
570
+ "soft": {
571
+ "background": { "$type": "color", "$value": "{color.warning.100}" },
572
+ "foreground": { "$type": "color", "$value": "{color.warning.700}" },
573
+ "border": { "$type": "color", "$value": "{color.warning.700}" },
574
+ "hover": { "$type": "color", "$value": "{color.warning.200}" },
575
+ "active": { "$type": "color", "$value": "{color.warning.300}" }
576
+ },
577
+ "solid": {
578
+ "background": { "$type": "color", "$value": "{color.warning.500}" },
579
+ "foreground": { "$type": "color", "$value": "{color.absolute-black}" }
580
+ }
581
+ },
582
+ "info": {
583
+ "soft": {
584
+ "background": { "$type": "color", "$value": "{color.info.100}" },
585
+ "foreground": { "$type": "color", "$value": "{color.info.700}" },
586
+ "border": { "$type": "color", "$value": "{color.info.700}" },
587
+ "hover": { "$type": "color", "$value": "{color.info.200}" },
588
+ "active": { "$type": "color", "$value": "{color.info.300}" }
589
+ },
590
+ "solid": {
591
+ "background": { "$type": "color", "$value": "{color.info.500}" },
592
+ "foreground": { "$type": "color", "$value": "{color.absolute-black}" }
593
+ }
594
+ },
595
+ "danger": {
596
+ "soft": {
597
+ "background": { "$type": "color", "$value": "{color.danger.100}" },
598
+ "foreground": { "$type": "color", "$value": "{color.danger.700}" },
599
+ "border": { "$type": "color", "$value": "{color.danger.700}" },
600
+ "hover": { "$type": "color", "$value": "{color.danger.200}" },
601
+ "active": { "$type": "color", "$value": "{color.danger.300}" }
602
+ },
603
+ "solid": {
604
+ "background": { "$type": "color", "$value": "{color.danger.500}" },
605
+ "foreground": { "$type": "color", "$value": "{color.absolute-white}" }
606
+ }
607
+ }
608
+ },
609
+ "choice-card": {
610
+ "selected": {
611
+ "background": { "$type": "color", "$value": "{color.brand.100}" },
612
+ "border": { "$type": "color", "$value": "{color.brand.500}" }
613
+ }
614
+ },
459
615
  "surface": {
460
616
  "default": {
461
617
  "$type": "color",
462
618
  "$value": "{color.black}"
463
619
  },
620
+ "floating": {
621
+ "$type": "color",
622
+ "$value": "{color.neutral.100}"
623
+ },
464
624
  "subtle": {
465
625
  "$type": "color",
466
- "$value": "{color.neutral.800}"
626
+ "$value": "{color.neutral.100}"
467
627
  },
468
628
  "raised": {
469
629
  "$type": "color",
470
- "$value": "{color.neutral.900}"
630
+ "$value": "{color.neutral.50}"
471
631
  },
472
632
  "overlay": {
473
633
  "$type": "color",
474
- "$value": "{color.neutral.50}"
634
+ "$value": "{color.neutral.900}"
475
635
  },
476
636
  "inverse": {
477
637
  "$type": "color",
478
- "$value": "{color.neutral.100}"
638
+ "$value": "{color.neutral.800}"
479
639
  },
480
640
  "sunken": {
481
641
  "$type": "color",
482
- "$value": "{color.neutral.800}"
642
+ "$value": "{color.neutral.100}"
483
643
  },
484
644
  "selected": {
485
645
  "$type": "color",
486
- "$value": "{color.neutral.50}"
646
+ "$value": "{color.neutral.900}"
487
647
  }
488
648
  },
489
649
  "scrim": {
@@ -493,11 +653,11 @@
493
653
  "content": {
494
654
  "primary": {
495
655
  "$type": "color",
496
- "$value": "{color.neutral.50}"
656
+ "$value": "{color.neutral.900}"
497
657
  },
498
658
  "secondary": {
499
659
  "$type": "color",
500
- "$value": "{color.neutral.200}"
660
+ "$value": "{color.neutral.700}"
501
661
  },
502
662
  "muted": {
503
663
  "$type": "color",
@@ -517,7 +677,7 @@
517
677
  },
518
678
  "on-selected": {
519
679
  "$type": "color",
520
- "$value": "{color.neutral.900}"
680
+ "$value": "{color.neutral.50}"
521
681
  },
522
682
  "error": {
523
683
  "$type": "color",
@@ -536,11 +696,11 @@
536
696
  "row": {
537
697
  "stripe": {
538
698
  "$type": "color",
539
- "$value": "{color.neutral.900}"
699
+ "$value": "{color.neutral.50}"
540
700
  },
541
701
  "hover": {
542
702
  "$type": "color",
543
- "$value": "{color.neutral.800}"
703
+ "$value": "{color.neutral.100}"
544
704
  },
545
705
  "selected": {
546
706
  "$type": "color",
@@ -557,11 +717,11 @@
557
717
  "border": {
558
718
  "subtle": {
559
719
  "$type": "color",
560
- "$value": "{color.neutral.700}"
720
+ "$value": "{color.neutral.200}"
561
721
  },
562
722
  "default": {
563
723
  "$type": "color",
564
- "$value": "{color.neutral.600}"
724
+ "$value": "{color.neutral.400}"
565
725
  },
566
726
  "strong": {
567
727
  "$type": "color",