@titan-design/react-ui 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +14 -0
  2. package/dist/bodymap.js +129 -76
  3. package/dist/bodymap.js.map +1 -1
  4. package/dist/bodymap.mjs +129 -76
  5. package/dist/bodymap.mjs.map +1 -1
  6. package/dist/index.d.mts +117 -26
  7. package/dist/index.d.ts +117 -26
  8. package/dist/index.js +677 -450
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +673 -451
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/{pages-_3Ajpiqu.d.ts → pages-CsjsUn0G.d.ts} +30 -13
  13. package/dist/{pages-CpW3OB3g.d.mts → pages-ZbT_pD_U.d.mts} +30 -13
  14. package/dist/pages.d.mts +1 -1
  15. package/dist/pages.d.ts +1 -1
  16. package/dist/pages.js +676 -701
  17. package/dist/pages.js.map +1 -1
  18. package/dist/pages.mjs +676 -701
  19. package/dist/pages.mjs.map +1 -1
  20. package/dist/theme/index.d.mts +31 -7
  21. package/dist/theme/index.d.ts +31 -7
  22. package/dist/theme/index.js +84 -20
  23. package/dist/theme/index.js.map +1 -1
  24. package/dist/theme/index.mjs +83 -21
  25. package/dist/theme/index.mjs.map +1 -1
  26. package/dist/theme/tokens-css.js +84 -20
  27. package/dist/theme/tokens-css.js.map +1 -1
  28. package/dist/theme/tokens-css.mjs +84 -20
  29. package/dist/theme/tokens-css.mjs.map +1 -1
  30. package/dist/theme/tokens.d.mts +56 -7
  31. package/dist/theme/tokens.d.ts +56 -7
  32. package/dist/theme/tokens.js +72 -20
  33. package/dist/theme/tokens.js.map +1 -1
  34. package/dist/theme/tokens.mjs +71 -21
  35. package/dist/theme/tokens.mjs.map +1 -1
  36. package/docs/render-testing-pattern.md +130 -0
  37. package/package.json +1 -1
  38. package/src/arch/Architecture.stories.tsx +3 -1
  39. package/src/arch/arch-graph.json +74 -72
  40. package/src/components/custom/ActiveWork/InitiativeBrief.tsx +2 -1
  41. package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +8 -3
  42. package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
  43. package/src/components/custom/Gauge/Gauge.stories.tsx +4 -1
  44. package/src/components/custom/Gauge/Gauge.tsx +3 -1
  45. package/src/components/custom/Scatter/Scatter.stories.tsx +10 -7
  46. package/src/components/custom/Scatter/Scatter.tsx +6 -4
  47. package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
  48. package/src/components/custom/Table/Table.stories.tsx +1 -1
  49. package/src/components/custom/Table/Table.tsx +2 -2
  50. package/src/components/custom/Treemap/Treemap.stories.tsx +13 -10
  51. package/src/components/custom/Treemap/Treemap.tsx +4 -1
  52. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +2 -1
  53. package/src/components/custom/Workout/BaseBadge.stories.tsx +11 -7
  54. package/src/components/custom/Workout/BaseBadge.tsx +3 -2
  55. package/src/components/custom/Workout/BodyMap.tsx +5 -3
  56. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +13 -3
  57. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +5 -3
  58. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +2 -1
  59. package/src/components/custom/Workout/CapacityBandChart.tsx +5 -3
  60. package/src/components/custom/Workout/DeviationBar.test.tsx +5 -2
  61. package/src/components/custom/Workout/DeviationBar.tsx +5 -2
  62. package/src/components/custom/Workout/DualVelocityStrip.stories.tsx +7 -6
  63. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +29 -16
  64. package/src/components/custom/Workout/ExerciseCard.stories.tsx +8 -8
  65. package/src/components/custom/Workout/ExerciseCard.test.tsx +39 -19
  66. package/src/components/custom/Workout/ExerciseCard.tsx +92 -167
  67. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +89 -6
  68. package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +189 -1
  69. package/src/components/custom/Workout/ExerciseCardHeading.tsx +167 -18
  70. package/src/components/custom/Workout/ExerciseDetailPage.tsx +3 -2
  71. package/src/components/custom/Workout/ExerciseHeading.tsx +192 -47
  72. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +6 -5
  73. package/src/components/custom/Workout/InputBar.tsx +4 -2
  74. package/src/components/custom/Workout/IntensityBar.tsx +4 -1
  75. package/src/components/custom/Workout/MesoCard.tsx +2 -1
  76. package/src/components/custom/Workout/MesoStatusCard.tsx +11 -10
  77. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +8 -3
  78. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +12 -2
  79. package/src/components/custom/Workout/README.md +58 -12
  80. package/src/components/custom/Workout/ReadinessCheck.tsx +4 -2
  81. package/src/components/custom/Workout/RestTimer.stories.tsx +13 -2
  82. package/src/components/custom/Workout/RestTimer.tsx +4 -2
  83. package/src/components/custom/Workout/SegmentedBar.stories.tsx +16 -12
  84. package/src/components/custom/Workout/SegmentedBar.tsx +4 -3
  85. package/src/components/custom/Workout/SetStrip.stories.tsx +3 -2
  86. package/src/components/custom/Workout/SetsRepsLoad.tsx +23 -9
  87. package/src/components/custom/Workout/Sparkline.stories.tsx +13 -10
  88. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +2 -1
  89. package/src/components/custom/Workout/StrengthTrendChart.tsx +8 -6
  90. package/src/components/custom/Workout/TempoDisplay.stories.tsx +4 -3
  91. package/src/components/custom/Workout/TempoDisplay.tsx +2 -0
  92. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +3 -2
  93. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +1 -1
  94. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +40 -0
  95. package/src/components/custom/Workout/VelocityStrip.test.tsx +48 -6
  96. package/src/components/custom/Workout/VelocityStrip.tsx +23 -14
  97. package/src/components/custom/Workout/ZoneTrack.stories.tsx +3 -1
  98. package/src/components/custom/Workout/exerciseRowState.ts +29 -0
  99. package/src/components/custom/Workout/index.ts +12 -2
  100. package/src/components/custom/Workout/metricText.stories.tsx +9 -8
  101. package/src/components/custom/Workout/metricText.tsx +4 -2
  102. package/src/components/custom/Workout/setHeadingKit.tsx +12 -10
  103. package/src/components/custom/Workout/velocity-story-kit.tsx +2 -2
  104. package/src/components/custom/charts/README.md +5 -0
  105. package/src/components/custom/charts/SetBarChart.test.tsx +16 -0
  106. package/src/components/custom/charts/SetBarChart.tsx +14 -5
  107. package/src/components/custom/charts/flatBarGeometry.test.ts +41 -0
  108. package/src/components/custom/charts/flatBarGeometry.ts +32 -0
  109. package/src/components/custom/stepper/Stepper.tsx +8 -6
  110. package/src/components/ui/alert/Alert.stories.tsx +14 -4
  111. package/src/components/ui/alert/Alert.tsx +24 -3
  112. package/src/components/ui/button/Button.tsx +12 -12
  113. package/src/components/ui/checkbox/Checkbox.tsx +2 -2
  114. package/src/components/ui/data-row/DataRow.stories.tsx +6 -2
  115. package/src/components/ui/drawer/Drawer.tsx +4 -1
  116. package/src/components/ui/icon-box/IconBox.tsx +8 -6
  117. package/src/components/ui/indicator/Indicator.stories.tsx +4 -3
  118. package/src/components/ui/list-item/ListItem.stories.tsx +7 -4
  119. package/src/components/ui/modal/Modal.tsx +2 -0
  120. package/src/components/ui/pill/Pill.stories.tsx +53 -0
  121. package/src/components/ui/pill/Pill.tsx +10 -6
  122. package/src/components/ui/popover/Popover.stories.tsx +1 -1
  123. package/src/components/ui/progress/Progress.tsx +9 -6
  124. package/src/components/ui/radio/Radio.tsx +1 -1
  125. package/src/components/ui/section/Section.stories.tsx +10 -10
  126. package/src/components/ui/select/Select.tsx +2 -1
  127. package/src/components/ui/spinner/Spinner.tsx +3 -1
  128. package/src/components/ui/stack/Stack.stories.tsx +7 -7
  129. package/src/components/ui/surface/Surface.stories.tsx +75 -28
  130. package/src/components/ui/switch/Switch.tsx +1 -1
  131. package/src/components/ui/tabs/Tabs.tsx +4 -2
  132. package/src/components/ui/toast/Toast.tsx +6 -4
  133. package/src/components/ui/toolbar-button/ToolbarButton.tsx +13 -6
  134. package/src/stories/PresetShowcase.stories.tsx +1 -1
  135. package/src/test/no-raw-color.test.ts +39 -0
  136. package/src/theme/Alpha.stories.tsx +103 -0
  137. package/src/theme/ProposedTokensVW82.stories.tsx +414 -0
  138. package/src/theme/config.ts +12 -0
  139. package/src/theme/global.css +15 -3
  140. package/src/theme/subtle-tone-weight.test.ts +163 -0
  141. package/src/theme/tailwind-var-opacity.test.ts +163 -0
  142. package/src/theme/tokens/primitives.ts +33 -0
  143. package/src/theme/tokens/semantic-hex-guard.test.ts +322 -0
  144. package/src/theme/tokens/semantic.ts +56 -20
  145. package/src/utils/colors.alpha-adoption.test.ts +408 -0
  146. package/tailwind.config.js +50 -9
@@ -0,0 +1,39 @@
1
+ import { RuleTester } from 'eslint'
2
+ import rule from '../../eslint-rules/no-raw-color'
3
+
4
+ const ruleTester = new RuleTester({
5
+ languageOptions: {
6
+ ecmaVersion: 2022,
7
+ sourceType: 'module',
8
+ parserOptions: { ecmaFeatures: { jsx: true } },
9
+ },
10
+ })
11
+
12
+ describe('no-raw-color', () => {
13
+ it('skips false positives while still flagging real raw colour', () => {
14
+ ruleTester.run('no-raw-color', rule as never, {
15
+ valid: [
16
+ // (a) enum-typed prop value: a bare colour word passed as color/variant/tone.
17
+ { code: '<Spinner color="white" />' },
18
+ // (a) the same enum's members listed in a Storybook argTypes options array.
19
+ {
20
+ code: "const meta = { argTypes: { color: { control: 'select', options: ['primary', 'white'] } } }",
21
+ },
22
+ // (b) { value, label } demo data reusing colour words as IDs.
23
+ { code: "const colorOptions = [{ value: 'red', label: 'Red' }]" },
24
+ // (c) documentation text in a <Text> element's children.
25
+ { code: '<Text>{`background-color: #3C3C3C;`}</Text>' },
26
+ ],
27
+ invalid: [
28
+ // Positive control: a real raw hex in a style prop must still be flagged,
29
+ // including on a <Text> element — only its *children* are exempt.
30
+ {
31
+ code: "<Text style={{ backgroundColor: '#123456' }} />",
32
+ errors: [{ messageId: 'hex' }],
33
+ },
34
+ // Positive control: a bare colour word outside any exempt shape is still named.
35
+ { code: "const bg = 'white'", errors: [{ messageId: 'named' }] },
36
+ ],
37
+ })
38
+ })
39
+ })
@@ -0,0 +1,103 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { alpha } from '../utils/colors'
4
+ import { getSemanticColors } from './tokens/semantic'
5
+ import { primitiveColors } from './tokens/primitives'
6
+ import { SectionIntro, SectionTitle, SWATCH_BORDER } from './color-story-kit'
7
+
8
+ const t = getSemanticColors('dark')
9
+
10
+ /**
11
+ * Foundations/Color/Alpha — deriving a translucent colour from a token instead
12
+ * of hardcoding an `rgba(...)` triple.
13
+ *
14
+ * `alpha(color, opacity)` (`packages/ui/src/utils/colors.ts`) takes a RESOLVED
15
+ * colour — a hex string or an `rgb()`/`rgba()` string, e.g. a semantic token's
16
+ * value or a primitive — and returns it as `rgba(r, g, b, opacity)`. It does
17
+ * NOT take a token name: `alpha('brand-primary', 0.1)` would try to parse the
18
+ * literal string `'brand-primary'` as a colour and fail. Resolve the token
19
+ * first (`t['brand-primary']`, `primitiveColors.white`, …), then wrap it.
20
+ *
21
+ * The payoff over a hand-picked `rgba(255, 121, 0, 0.1)`: when a ramp gets
22
+ * re-spaced or a semantic mapping moves, every `alpha()` call built on that
23
+ * token moves with it. A literal triple silently drifts from the token it was
24
+ * copied from and stays wrong until someone notices — the failure mode VW-78
25
+ * was opened to burn down (see `eslint-rules/raw-color-baseline.json`,
26
+ * enforced by `no-raw-color`'s `functional` rule).
27
+ */
28
+ const meta: Meta = {
29
+ title: 'Foundations/Color/Alpha',
30
+ tags: ['autodocs'],
31
+ }
32
+
33
+ export default meta
34
+
35
+ const EXAMPLES = [
36
+ { name: 'brand-primary @ 0.12', value: alpha(t['brand-primary'], 0.12) },
37
+ { name: 'status-success @ 0.15', value: alpha(t['status-success'], 0.15) },
38
+ { name: 'status-warning @ 0.25', value: alpha(t['status-warning'], 0.25) },
39
+ { name: 'status-error @ 0.3', value: alpha(t['status-error'], 0.3) },
40
+ { name: 'primitiveColors.white @ 0.08', value: alpha(primitiveColors.white, 0.08) },
41
+ ] as const
42
+
43
+ function AlphaSwatch({ name, value }: { name: string; value: string }) {
44
+ return (
45
+ <View style={{ alignItems: 'center', width: 96 }}>
46
+ <View
47
+ style={{
48
+ width: 96,
49
+ height: 64,
50
+ borderRadius: 8,
51
+ backgroundColor: t['surface-elevated'],
52
+ borderWidth: 1,
53
+ borderColor: SWATCH_BORDER,
54
+ overflow: 'hidden',
55
+ }}
56
+ >
57
+ <View style={{ flex: 1, backgroundColor: value }} />
58
+ </View>
59
+ <Text className="text-text-secondary text-xs mt-1" style={{ textAlign: 'center' }}>
60
+ {name}
61
+ </Text>
62
+ <Text className="text-text-tertiary" style={{ fontSize: 9, marginTop: 1 }}>
63
+ {value}
64
+ </Text>
65
+ </View>
66
+ )
67
+ }
68
+
69
+ export const Default: StoryObj = {
70
+ render: () => (
71
+ <View style={{ padding: 24 }}>
72
+ <Text className="text-2xl font-bold text-text-primary mb-2">Alpha</Text>
73
+ <SectionIntro>
74
+ `alpha(color, opacity)` derives a translucent colour from a RESOLVED token or primitive
75
+ instead of a hand-picked `rgba(...)` triple, so the wash tracks the token it came from. Each
76
+ swatch below is rendered over the elevated surface plane so the translucency is visible.
77
+ </SectionIntro>
78
+
79
+ <SectionTitle>Usage</SectionTitle>
80
+ <View
81
+ style={{
82
+ backgroundColor: t['surface-raised'],
83
+ borderRadius: 8,
84
+ padding: 12,
85
+ marginBottom: 20,
86
+ }}
87
+ >
88
+ <Text className="text-text-secondary" style={{ fontFamily: 'monospace', fontSize: 12 }}>
89
+ {"import { alpha } from '@/utils/colors'\n\n"}
90
+ {"const t = getSemanticColors('dark')\n"}
91
+ {"const wash = alpha(t['brand-primary'], 0.12)"}
92
+ </Text>
93
+ </View>
94
+
95
+ <SectionTitle>Examples</SectionTitle>
96
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 16 }}>
97
+ {EXAMPLES.map((example) => (
98
+ <AlphaSwatch key={example.name} {...example} />
99
+ ))}
100
+ </View>
101
+ </View>
102
+ ),
103
+ }
@@ -0,0 +1,414 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { alpha } from '../utils/colors'
4
+ import { getSemanticColors } from './tokens/semantic'
5
+ import { greyRamp, primitiveColors, primitiveRamps } from './tokens/primitives'
6
+ import { SectionIntro, SectionTitle, SWATCH_BORDER } from './color-story-kit'
7
+ import { ToolbarButton } from '../components/ui/toolbar-button'
8
+ import { Treemap } from '../components/custom/Treemap'
9
+
10
+ const t = getSemanticColors('dark')
11
+
12
+ /**
13
+ * Foundations/Color/Proposed VW-82 tokens — a DECISION story, not documentation.
14
+ *
15
+ * VW-82 (#200) migrated component source off raw colours. Every literal that had
16
+ * an exact token was swapped; the ones with no token were deliberately left raw
17
+ * and proposed rather than invented, because adding a token is a design call.
18
+ * This story renders those candidates in the context they would actually ship
19
+ * in, so the proposal can be accepted or rejected by eye instead of by hex.
20
+ *
21
+ * WHY THE SCRIMS ARE TOKENS AND NOT `bg-black/50`: Tailwind v3 cannot apply an
22
+ * opacity modifier to a `var()` colour — it fails to parse the value and emits
23
+ * NO rule at all. So a translucent role has to ship as its own rgba token, which
24
+ * is exactly what `hairline-*` already does (`rgba(255, 255, 255, 0.15)`, not
25
+ * `white/15`). The four scrims below are rendered through
26
+ * `alpha(primitiveColors.black, …)`, which is the value each token would carry.
27
+ *
28
+ * The two candidates that have NO token and no primitive — `on-control-idle`
29
+ * (#D1D1D1) and `on-data-strong` (#0B0B0B) — cannot be written as literals here:
30
+ * `no-raw-color` allots this file zero, and funding a new allowance to document
31
+ * a proposal would push the ratchet the wrong way. They are instead shown by
32
+ * rendering the REAL components that carry them today, side by side with the
33
+ * existing tokens that might replace them. That is stronger evidence anyway.
34
+ */
35
+ const meta: Meta<ProposedTokensArgs> = {
36
+ title: 'Foundations/Color/Proposed VW-82 tokens',
37
+ tags: ['autodocs'],
38
+ argTypes: {
39
+ showScrims: {
40
+ control: 'boolean',
41
+ description: 'Lift all four scrims off the backdrop, to judge their impact.',
42
+ },
43
+ backdrop: {
44
+ control: 'radio',
45
+ options: ['pattern', 'gradient', 'surface'],
46
+ description:
47
+ 'What the scrims sit on. `pattern` is the honest worst case — a scrim over a flat dark surface always looks fine.',
48
+ },
49
+ },
50
+ args: { showScrims: true, backdrop: 'pattern' },
51
+ }
52
+
53
+ export default meta
54
+
55
+ interface ProposedTokensArgs {
56
+ showScrims: boolean
57
+ backdrop: 'pattern' | 'gradient' | 'surface'
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // 1 — scrims
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /**
65
+ * The four candidates, as the value each token would carry. Derived through
66
+ * `alpha()` from the black primitive rather than written as rgba triples, so
67
+ * they track the primitive the way the real token would.
68
+ */
69
+ const SCRIMS = [
70
+ {
71
+ name: 'scrim-press',
72
+ opacity: 0.1,
73
+ usedBy: 'Alert close button — web:hover',
74
+ },
75
+ {
76
+ name: 'scrim-press-strong',
77
+ opacity: 0.2,
78
+ usedBy: 'Alert close button — active',
79
+ },
80
+ {
81
+ name: 'scrim-subtle',
82
+ opacity: 0.3,
83
+ usedBy: 'Modal backdrop (blurred), Select filled fill',
84
+ },
85
+ {
86
+ name: 'scrim-default',
87
+ opacity: 0.5,
88
+ usedBy: 'Modal backdrop, Drawer overlay',
89
+ },
90
+ ] as const
91
+
92
+ const CHECKER_LIGHT = greyRamp[50]
93
+ const CHECKER_DARK = greyRamp[950]
94
+ const CHECKER_STEP = 20
95
+ const PANEL_W = 196
96
+ const PANEL_H = 140
97
+
98
+ /**
99
+ * A deliberately hostile backdrop. A scrim over a flat dark plane reads as
100
+ * "fine" no matter what its opacity is; the only way to judge one is to put
101
+ * something with real tonal range under it.
102
+ */
103
+ function Checkerboard({ rows, columns }: { rows: number; columns: number }) {
104
+ return (
105
+ <View style={{ position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 }}>
106
+ {Array.from({ length: rows }, (_, row) => (
107
+ <View key={row} style={{ flexDirection: 'row' }}>
108
+ {Array.from({ length: columns }, (_, column) => (
109
+ <View
110
+ key={column}
111
+ style={{
112
+ width: CHECKER_STEP,
113
+ height: CHECKER_STEP,
114
+ backgroundColor: (row + column) % 2 === 0 ? CHECKER_LIGHT : CHECKER_DARK,
115
+ }}
116
+ />
117
+ ))}
118
+ </View>
119
+ ))}
120
+ </View>
121
+ )
122
+ }
123
+
124
+ /** Stand-in for the photographic mid-tones a real app backdrop has. */
125
+ function GradientWash() {
126
+ return (
127
+ <View
128
+ style={{
129
+ position: 'absolute',
130
+ top: 0,
131
+ right: 0,
132
+ bottom: 0,
133
+ left: 0,
134
+ // react-native-web renders backgroundImage at runtime (see DeviationBar).
135
+ backgroundImage: `linear-gradient(135deg, ${primitiveRamps.blue[300]} 0%, ${primitiveRamps.magenta[300]} 38%, ${primitiveRamps.amber[200]} 72%, ${greyRamp[950]} 100%)`,
136
+ }}
137
+ />
138
+ )
139
+ }
140
+
141
+ /** Cards + copy on top of the backdrop, so the scrim has content to obscure. */
142
+ function BackdropContent() {
143
+ return (
144
+ <View style={{ flexDirection: 'row', gap: 6, padding: 10 }}>
145
+ {[primitiveRamps.orange[400], primitiveRamps.green[300], primitiveRamps.blue[300]].map(
146
+ (fill) => (
147
+ <View
148
+ key={fill}
149
+ style={{
150
+ width: 56,
151
+ height: 48,
152
+ borderRadius: 6,
153
+ backgroundColor: fill,
154
+ padding: 5,
155
+ }}
156
+ >
157
+ <Text style={{ color: greyRamp[975], fontSize: 10, fontWeight: '700' }}>Card</Text>
158
+ </View>
159
+ )
160
+ )}
161
+ </View>
162
+ )
163
+ }
164
+
165
+ function ScrimPanel({ showScrims, backdrop }: ProposedTokensArgs) {
166
+ return (
167
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 14 }}>
168
+ {SCRIMS.map((scrim) => (
169
+ <View key={scrim.name} style={{ width: PANEL_W }}>
170
+ <View
171
+ style={{
172
+ height: PANEL_H,
173
+ borderRadius: 8,
174
+ overflow: 'hidden',
175
+ borderWidth: 1,
176
+ borderColor: SWATCH_BORDER,
177
+ backgroundColor: t['surface-base'],
178
+ }}
179
+ >
180
+ {backdrop === 'pattern' && (
181
+ <Checkerboard
182
+ rows={Math.ceil(PANEL_H / CHECKER_STEP)}
183
+ columns={Math.ceil(PANEL_W / CHECKER_STEP)}
184
+ />
185
+ )}
186
+ {backdrop === 'gradient' && <GradientWash />}
187
+ <BackdropContent />
188
+ {showScrims && (
189
+ <View
190
+ style={{
191
+ position: 'absolute',
192
+ top: 0,
193
+ right: 0,
194
+ bottom: 0,
195
+ left: 0,
196
+ backgroundColor: alpha(primitiveColors.black, scrim.opacity),
197
+ alignItems: 'center',
198
+ justifyContent: 'center',
199
+ }}
200
+ >
201
+ <Text style={{ color: primitiveColors.white, fontSize: 12, fontWeight: '700' }}>
202
+ {scrim.name}
203
+ </Text>
204
+ </View>
205
+ )}
206
+ </View>
207
+ <Text className="text-text-primary text-xs font-semibold mt-2">{scrim.name}</Text>
208
+ <Text className="text-text-tertiary" style={{ fontSize: 10 }}>
209
+ {alpha(primitiveColors.black, scrim.opacity)}
210
+ </Text>
211
+ <Text className="text-text-secondary" style={{ fontSize: 10, marginTop: 2 }}>
212
+ {scrim.usedBy}
213
+ </Text>
214
+ </View>
215
+ ))}
216
+ </View>
217
+ )
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // 2 — on-control
222
+ // ---------------------------------------------------------------------------
223
+
224
+ const TOOLBAR_FACE = greyRamp[800]
225
+
226
+ /**
227
+ * A toolbar-button face rendered with a chosen label colour, matching the real
228
+ * `ToolbarButton` geometry so the comparison beside it is like-for-like.
229
+ */
230
+ function LabelChip({ label, color, caption }: { label: string; color: string; caption: string }) {
231
+ return (
232
+ <View style={{ alignItems: 'center', width: 116 }}>
233
+ <View
234
+ style={{
235
+ backgroundColor: TOOLBAR_FACE,
236
+ borderRadius: 4,
237
+ paddingHorizontal: 10,
238
+ paddingVertical: 4,
239
+ minHeight: 30,
240
+ justifyContent: 'center',
241
+ }}
242
+ >
243
+ <Text style={{ color, fontSize: 14, fontWeight: '700' }}>{label}</Text>
244
+ </View>
245
+ <Text className="text-text-secondary" style={{ fontSize: 10, marginTop: 4 }}>
246
+ {caption}
247
+ </Text>
248
+ <Text className="text-text-tertiary" style={{ fontSize: 9 }}>
249
+ {color.toUpperCase()}
250
+ </Text>
251
+ </View>
252
+ )
253
+ }
254
+
255
+ function OnControlPanel() {
256
+ return (
257
+ <View className="bg-surface-elevated" style={{ borderRadius: 8, padding: 16 }}>
258
+ <Text className="text-text-secondary text-xs mb-3">
259
+ Left pair: the LIVE ToolbarButton, which is what the proposal is about. Right: the existing
260
+ tokens, drawn on the same face at the same size — does one of them already do the job?
261
+ </Text>
262
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 14, alignItems: 'flex-start' }}>
263
+ <View style={{ alignItems: 'center', width: 116 }}>
264
+ <ToolbarButton label="Idle" isActive={false} variant="raised" />
265
+ <Text className="text-text-secondary" style={{ fontSize: 10, marginTop: 4 }}>
266
+ live · on-control-idle
267
+ </Text>
268
+ <Text className="text-text-tertiary" style={{ fontSize: 9 }}>
269
+ #D1D1D1
270
+ </Text>
271
+ </View>
272
+ <View style={{ alignItems: 'center', width: 116 }}>
273
+ <ToolbarButton label="Active" isActive variant="raised" />
274
+ <Text className="text-text-secondary" style={{ fontSize: 10, marginTop: 4 }}>
275
+ live · on-control-active
276
+ </Text>
277
+ <Text className="text-text-tertiary" style={{ fontSize: 9 }}>
278
+ #FFFFFF
279
+ </Text>
280
+ </View>
281
+ <LabelChip label="Idle" color={greyRamp[200]} caption="greyRamp[200]" />
282
+ <LabelChip label="Idle" color={t['text-secondary']} caption="text-secondary" />
283
+ <LabelChip label="Idle" color={t['text-primary']} caption="text-primary" />
284
+ <LabelChip label="Active" color={primitiveColors.white} caption="primitiveColors.white" />
285
+ </View>
286
+ <Text className="text-text-tertiary text-xs mt-3">
287
+ `on-control-active` is exactly `primitiveColors.white`, so the only real question is whether
288
+ it deserves a NAME — the toolbar face is a grey plane, not a brand fill, so borrowing
289
+ `on-brand-primary` for it would be the wrong role at the right value.
290
+ </Text>
291
+ </View>
292
+ )
293
+ }
294
+
295
+ // ---------------------------------------------------------------------------
296
+ // 3 — on-data-strong
297
+ // ---------------------------------------------------------------------------
298
+
299
+ const LIGHT_TILES = [
300
+ { id: 'chest', value: 34, color: primitiveRamps.amber[200], label: 'Chest' },
301
+ { id: 'back', value: 28, color: primitiveRamps.green[200], label: 'Back' },
302
+ { id: 'quads', value: 21, color: primitiveRamps.blue[200], label: 'Quads' },
303
+ { id: 'delts', value: 14, color: primitiveRamps.orange[200], label: 'Delts' },
304
+ { id: 'arms', value: 9, color: primitiveRamps.magenta[200], label: 'Arms' },
305
+ ]
306
+
307
+ function TileRow({ color, caption }: { color: string; caption: string }) {
308
+ return (
309
+ <View>
310
+ <View style={{ flexDirection: 'row', gap: 2 }}>
311
+ {LIGHT_TILES.map((tile) => (
312
+ <View
313
+ key={tile.id}
314
+ style={{
315
+ width: 62,
316
+ height: 44,
317
+ borderRadius: 3,
318
+ backgroundColor: tile.color,
319
+ opacity: 0.9,
320
+ padding: 4,
321
+ }}
322
+ >
323
+ <Text style={{ color, fontSize: 10, fontWeight: '600' }}>{tile.label}</Text>
324
+ </View>
325
+ ))}
326
+ </View>
327
+ <Text className="text-text-secondary" style={{ fontSize: 10, marginTop: 4 }}>
328
+ {caption} · {color.toUpperCase()}
329
+ </Text>
330
+ </View>
331
+ )
332
+ }
333
+
334
+ function OnDataPanel() {
335
+ return (
336
+ <View className="bg-surface-elevated" style={{ borderRadius: 8, padding: 16 }}>
337
+ <Text className="text-text-secondary text-xs mb-3">
338
+ Top: the LIVE Treemap, whose label is the proposed `on-data-strong`. Below: the same tiles
339
+ with the nearest existing dark tokens, so the difference (or lack of one) is visible at
340
+ label size rather than as a swatch.
341
+ </Text>
342
+ <View style={{ gap: 12 }}>
343
+ <View>
344
+ <Treemap data={LIGHT_TILES} width={320} height={92} maxTiles={5} />
345
+ <Text className="text-text-secondary" style={{ fontSize: 10, marginTop: 4 }}>
346
+ live Treemap · on-data-strong · #0B0B0B
347
+ </Text>
348
+ </View>
349
+ <TileRow color={t['background-frame']} caption="background-frame" />
350
+ <TileRow color={greyRamp[950]} caption="greyRamp[950]" />
351
+ <TileRow color={primitiveColors.black} caption="primitiveColors.black" />
352
+ </View>
353
+ </View>
354
+ )
355
+ }
356
+
357
+ // ---------------------------------------------------------------------------
358
+
359
+ function ApprovedSnaps() {
360
+ return (
361
+ <View
362
+ style={{
363
+ backgroundColor: t['surface-raised'],
364
+ borderRadius: 8,
365
+ padding: 12,
366
+ marginBottom: 20,
367
+ }}
368
+ >
369
+ <Text className="text-text-primary text-sm font-semibold mb-1">
370
+ Already approved — the six grey snaps (follow-up, NOT in #200)
371
+ </Text>
372
+ <Text className="text-text-secondary text-xs">
373
+ These need no new token: they are drifted Tailwind and ad-hoc neutrals that snap onto the
374
+ existing warm ramp. Approved for the follow-up PR because each one moves pixels.
375
+ {'\n\n'}
376
+ #6B7280 (Tailwind gray-500) → result-neutral, at IntensityBar, TempoDisplay, setHeadingKit,
377
+ Spinner · #9CA3AF (Tailwind gray-400) → text-secondary, at TempoDisplay ×4 · #333333 →
378
+ surface-raised · #2C2C2C → surface-elevated · #3A3A3A → surface-overlay · #0A0A0A →
379
+ background-frame
380
+ </Text>
381
+ </View>
382
+ )
383
+ }
384
+
385
+ type Story = StoryObj<ProposedTokensArgs>
386
+
387
+ export const Default: Story = {
388
+ render: (args) => (
389
+ <View style={{ padding: 24, maxWidth: 900 }}>
390
+ <Text className="text-2xl font-bold text-text-primary mb-2">Proposed VW-82 tokens</Text>
391
+ <SectionIntro>
392
+ Seven candidates from #200, rendered where they would ship. Nothing here is in the token
393
+ layer — `semantic.ts`, `primitives.ts`, `global.css` and `tailwind.config.js` are untouched
394
+ until these are approved.
395
+ </SectionIntro>
396
+
397
+ <ApprovedSnaps />
398
+
399
+ <SectionTitle>1 · Scrims (4 candidates)</SectionTitle>
400
+ <Text className="text-text-secondary text-xs mb-3">
401
+ Toggle `showScrims` off to see what each one is hiding, and switch `backdrop` between the
402
+ checkerboard, a photographic gradient, and a plain surface. The plain surface is the case
403
+ that flatters every opacity equally — judge on the other two.
404
+ </Text>
405
+ <ScrimPanel {...args} />
406
+
407
+ <SectionTitle>2 · on-control (2 candidates)</SectionTitle>
408
+ <OnControlPanel />
409
+
410
+ <SectionTitle>3 · on-data-strong (1 candidate)</SectionTitle>
411
+ <OnDataPanel />
412
+ </View>
413
+ ),
414
+ }
@@ -54,6 +54,8 @@ export const lightThemeCSSVars = {
54
54
 
55
55
  '--color-on-brand-primary': semanticColorsLight['on-brand-primary'],
56
56
  '--color-on-brand-secondary': semanticColorsLight['on-brand-secondary'],
57
+ '--color-on-brand-primary-subtle': semanticColorsLight['on-brand-primary-subtle'],
58
+ '--color-on-brand-secondary-subtle': semanticColorsLight['on-brand-secondary-subtle'],
57
59
 
58
60
  '--color-status-success': semanticColorsLight['status-success'],
59
61
  '--color-status-success-subtle': semanticColorsLight['status-success-subtle'],
@@ -126,6 +128,10 @@ export const lightThemeCSSVars = {
126
128
  '--color-on-status-error': semanticColorsLight['on-status-error'],
127
129
  '--color-on-status-warning': semanticColorsLight['on-status-warning'],
128
130
  '--color-on-status-info': semanticColorsLight['on-status-info'],
131
+ '--color-on-status-success-subtle': semanticColorsLight['on-status-success-subtle'],
132
+ '--color-on-status-error-subtle': semanticColorsLight['on-status-error-subtle'],
133
+ '--color-on-status-warning-subtle': semanticColorsLight['on-status-warning-subtle'],
134
+ '--color-on-status-info-subtle': semanticColorsLight['on-status-info-subtle'],
129
135
 
130
136
  '--color-result-improve': semanticColorsLight['result-improve'],
131
137
  '--color-result-improve-light': semanticColorsLight['result-improve-light'],
@@ -191,6 +197,8 @@ export const darkThemeCSSVars = {
191
197
 
192
198
  '--color-on-brand-primary': semanticColorsDark['on-brand-primary'],
193
199
  '--color-on-brand-secondary': semanticColorsDark['on-brand-secondary'],
200
+ '--color-on-brand-primary-subtle': semanticColorsDark['on-brand-primary-subtle'],
201
+ '--color-on-brand-secondary-subtle': semanticColorsDark['on-brand-secondary-subtle'],
194
202
 
195
203
  '--color-status-success': semanticColorsDark['status-success'],
196
204
  '--color-status-success-subtle': semanticColorsDark['status-success-subtle'],
@@ -263,6 +271,10 @@ export const darkThemeCSSVars = {
263
271
  '--color-on-status-error': semanticColorsDark['on-status-error'],
264
272
  '--color-on-status-warning': semanticColorsDark['on-status-warning'],
265
273
  '--color-on-status-info': semanticColorsDark['on-status-info'],
274
+ '--color-on-status-success-subtle': semanticColorsDark['on-status-success-subtle'],
275
+ '--color-on-status-error-subtle': semanticColorsDark['on-status-error-subtle'],
276
+ '--color-on-status-warning-subtle': semanticColorsDark['on-status-warning-subtle'],
277
+ '--color-on-status-info-subtle': semanticColorsDark['on-status-info-subtle'],
266
278
 
267
279
  '--color-result-improve': semanticColorsDark['result-improve'],
268
280
  '--color-result-improve-light': semanticColorsDark['result-improve-light'],
@@ -24,7 +24,7 @@
24
24
  --color-brand-secondary: #307B9B;
25
25
  --color-brand-secondary-light: #2697B7;
26
26
  --color-brand-secondary-dark: #2A617F;
27
- --color-brand-secondary-subtle: rgba(48, 123, 155, 0.12);
27
+ --color-brand-secondary-subtle: rgba(34, 211, 238, 0.12);
28
28
  --color-brand-secondary-muted: rgba(48, 123, 155, 0.30);
29
29
  --color-brand-secondary-strong: rgba(48, 123, 155, 0.50);
30
30
  --color-brand-primary-hover: #FFA063;
@@ -34,6 +34,8 @@
34
34
 
35
35
  --color-on-brand-primary: #FFFFFF;
36
36
  --color-on-brand-secondary: #FFFFFF;
37
+ --color-on-brand-primary-subtle: #FF7900;
38
+ --color-on-brand-secondary-subtle: #22D3EE;
37
39
 
38
40
  --color-status-success: #2ED573;
39
41
  --color-status-success-light: #58F69E;
@@ -46,7 +48,7 @@
46
48
  --color-status-error: #D14343;
47
49
  --color-status-error-light: #E05254;
48
50
  --color-status-error-dark: #A4221C;
49
- --color-status-error-subtle: rgba(209, 67, 67, 0.12);
51
+ --color-status-error-subtle: rgba(247, 113, 117, 0.08);
50
52
  --color-status-error-muted: rgba(209, 67, 67, 0.30);
51
53
  --color-status-error-strong: rgba(209, 67, 67, 0.50);
52
54
  --color-status-error-vivid: #FF4757;
@@ -66,7 +68,7 @@
66
68
  --color-status-info: #2196F3;
67
69
  --color-status-info-light: #78C2FF;
68
70
  --color-status-info-dark: #1072CB;
69
- --color-status-info-subtle: rgba(33, 150, 243, 0.12);
71
+ --color-status-info-subtle: rgba(120, 194, 255, 0.12);
70
72
  --color-status-info-muted: rgba(33, 150, 243, 0.30);
71
73
  --color-status-info-strong: rgba(33, 150, 243, 0.50);
72
74
 
@@ -74,6 +76,10 @@
74
76
  --color-on-status-error: #FFFFFF;
75
77
  --color-on-status-warning: #FFFFFF;
76
78
  --color-on-status-info: #FFFFFF;
79
+ --color-on-status-success-subtle: #2ED573;
80
+ --color-on-status-error-subtle: #F77175;
81
+ --color-on-status-warning-subtle: #F9B415;
82
+ --color-on-status-info-subtle: #78C2FF;
77
83
 
78
84
  --color-result-improve: #4caf50;
79
85
  --color-result-improve-light: rgba(76, 175, 80, 0.16);
@@ -200,6 +206,8 @@
200
206
 
201
207
  --color-on-brand-primary: #FFFFFF;
202
208
  --color-on-brand-secondary: #FFFFFF;
209
+ --color-on-brand-primary-subtle: #FF7900;
210
+ --color-on-brand-secondary-subtle: #307B9B;
203
211
 
204
212
  --color-status-success: #2ED573;
205
213
  --color-status-success-light: #58F69E;
@@ -240,6 +248,10 @@
240
248
  --color-on-status-error: #FFFFFF;
241
249
  --color-on-status-warning: #FFFFFF;
242
250
  --color-on-status-info: #FFFFFF;
251
+ --color-on-status-success-subtle: #2ED573;
252
+ --color-on-status-error-subtle: #D14343;
253
+ --color-on-status-warning-subtle: #F9B415;
254
+ --color-on-status-info-subtle: #2196F3;
243
255
 
244
256
  --color-result-improve: #4caf50;
245
257
  --color-result-improve-light: rgba(76, 175, 80, 0.12);