@titan-design/react-ui 0.10.0 → 0.12.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 (190) hide show
  1. package/dist/bodymap.js +173 -343
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +173 -343
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +416 -18
  6. package/dist/index.d.ts +416 -18
  7. package/dist/index.js +2512 -1307
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2480 -1299
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
  12. package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1644 -1575
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1645 -1576
  18. package/dist/pages.mjs.map +1 -1
  19. package/dist/theme/index.d.mts +409 -415
  20. package/dist/theme/index.d.ts +409 -415
  21. package/dist/theme/index.js +234 -408
  22. package/dist/theme/index.js.map +1 -1
  23. package/dist/theme/index.mjs +227 -399
  24. package/dist/theme/index.mjs.map +1 -1
  25. package/dist/theme/tokens-css.js +141 -140
  26. package/dist/theme/tokens-css.js.map +1 -1
  27. package/dist/theme/tokens-css.mjs +141 -140
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +8 -3
  30. package/src/arch/Architecture.stories.tsx +9 -9
  31. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  32. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
  33. package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
  34. package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
  35. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
  36. package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
  37. package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
  38. package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
  39. package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
  40. package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
  41. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
  42. package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
  43. package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
  44. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
  45. package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
  46. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
  47. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
  48. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
  49. package/src/components/custom/Fatigue/README.md +93 -0
  50. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
  51. package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
  52. package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
  53. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
  54. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
  55. package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
  56. package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
  57. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
  58. package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
  59. package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
  60. package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
  61. package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
  62. package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
  63. package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
  64. package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
  65. package/src/components/custom/Fatigue/index.ts +41 -0
  66. package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
  67. package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
  68. package/src/components/custom/Sidebar/Sidebar.tsx +4 -4
  69. package/src/components/custom/Table/Table.tsx +2 -2
  70. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +3 -3
  71. package/src/components/custom/Workout/BaseBadge.test.tsx +6 -1
  72. package/src/components/custom/Workout/BaseBadge.tsx +1 -1
  73. package/src/components/custom/Workout/BodyMap.tsx +2 -2
  74. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +2 -2
  75. package/src/components/custom/Workout/DeviationBar.test.tsx +2 -1
  76. package/src/components/custom/Workout/DeviationBar.tsx +3 -2
  77. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
  78. package/src/components/custom/Workout/ExerciseCard.test.tsx +7 -7
  79. package/src/components/custom/Workout/ExerciseCard.tsx +5 -3
  80. package/src/components/custom/Workout/ExerciseDetailPage.tsx +11 -5
  81. package/src/components/custom/Workout/InputBar.tsx +2 -2
  82. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +1 -1
  83. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +1 -1
  84. package/src/components/custom/Workout/LiveAuraFrame.tsx +3 -3
  85. package/src/components/custom/Workout/MesoCard.tsx +3 -1
  86. package/src/components/custom/Workout/MuscleGroupChip.tsx +4 -3
  87. package/src/components/custom/Workout/PrHistoryModal.tsx +2 -2
  88. package/src/components/custom/Workout/ProgramPlanningPage.tsx +1 -1
  89. package/src/components/custom/Workout/README.md +62 -16
  90. package/src/components/custom/Workout/ReadinessCheck.tsx +2 -2
  91. package/src/components/custom/Workout/RestTimer.tsx +2 -2
  92. package/src/components/custom/Workout/S3SetTypes.stories.tsx +3 -3
  93. package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
  94. package/src/components/custom/Workout/SessionHeader.stories.tsx +2 -2
  95. package/src/components/custom/Workout/SessionHeader.tsx +1 -1
  96. package/src/components/custom/Workout/SessionRail.stories.tsx +2 -2
  97. package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
  98. package/src/components/custom/Workout/SessionRail.tsx +63 -18
  99. package/src/components/custom/Workout/SetBar.tsx +3 -3
  100. package/src/components/custom/Workout/SetRow.test.tsx +12 -9
  101. package/src/components/custom/Workout/SetRow.tsx +9 -2
  102. package/src/components/custom/Workout/StatusDot.tsx +5 -4
  103. package/src/components/custom/Workout/StrengthTrendChart.tsx +1 -1
  104. package/src/components/custom/Workout/TrainingStatusPage.tsx +2 -2
  105. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
  106. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
  107. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
  108. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
  109. package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
  110. package/src/components/custom/Workout/VelocityStrip.test.tsx +199 -79
  111. package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
  112. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +3 -3
  113. package/src/components/custom/Workout/WeightBadge.test.tsx +6 -1
  114. package/src/components/custom/Workout/WeightBadge.tsx +2 -1
  115. package/src/components/custom/Workout/WorkoutCard.tsx +3 -1
  116. package/src/components/custom/Workout/WorkoutPill.tsx +5 -2
  117. package/src/components/custom/Workout/ZoneTrack.tsx +4 -4
  118. package/src/components/custom/Workout/index.ts +3 -0
  119. package/src/components/custom/Workout/setHeadingKit.tsx +14 -8
  120. package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
  121. package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
  122. package/src/components/custom/charts/SetBarChart.tsx +651 -0
  123. package/src/components/custom/charts/live-rep-growth.ts +91 -0
  124. package/src/components/custom/index.ts +1 -0
  125. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  126. package/src/components/custom/stepper/Stepper.tsx +3 -3
  127. package/src/components/shell/DeviceMenu.tsx +2 -2
  128. package/src/components/shell/SideNav.tsx +1 -1
  129. package/src/components/shell/TopBar.tsx +1 -1
  130. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  131. package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
  132. package/src/components/ui/avatar/Avatar.tsx +2 -2
  133. package/src/components/ui/badge/Badge.tsx +2 -2
  134. package/src/components/ui/card/Card.tsx +3 -3
  135. package/src/components/ui/checkbox/Checkbox.tsx +1 -1
  136. package/src/components/ui/collapse/Collapse.tsx +1 -1
  137. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  138. package/src/components/ui/drawer/Drawer.tsx +3 -3
  139. package/src/components/ui/form-field/FormField.tsx +1 -1
  140. package/src/components/ui/help-tip/HelpTip.tsx +1 -1
  141. package/src/components/ui/menu/Menu.tsx +2 -2
  142. package/src/components/ui/pill/Pill.tsx +2 -2
  143. package/src/components/ui/popover/Popover.stories.tsx +2 -2
  144. package/src/components/ui/popover/Popover.tsx +1 -1
  145. package/src/components/ui/progress/Progress.tsx +3 -3
  146. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  147. package/src/components/ui/radio/Radio.tsx +4 -4
  148. package/src/components/ui/select/Select.tsx +3 -3
  149. package/src/components/ui/surface/Surface.stories.tsx +3 -3
  150. package/src/components/ui/surface/Surface.test.tsx +32 -19
  151. package/src/components/ui/surface/Surface.tsx +3 -3
  152. package/src/components/ui/surface/SurfaceContext.ts +19 -29
  153. package/src/components/ui/surface/surface.contract.test.ts +57 -44
  154. package/src/components/ui/switch/Switch.tsx +1 -1
  155. package/src/components/ui/tabs/Tabs.tsx +2 -2
  156. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +5 -5
  157. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +1 -1
  158. package/src/components/ui/toolbar-button/ToolbarButton.tsx +36 -22
  159. package/src/stories/PresetShowcase.stories.tsx +21 -21
  160. package/src/test/raw-color-patterns.test.ts +71 -0
  161. package/src/theme/ColorPalettes.stories.tsx +874 -0
  162. package/src/theme/ColorPrimitives.stories.tsx +348 -0
  163. package/src/theme/Depth.stories.tsx +312 -0
  164. package/src/theme/DepthCalibration.stories.tsx +655 -0
  165. package/src/theme/ThemeProvider.tsx +1 -1
  166. package/src/theme/barrel.ts +7 -14
  167. package/src/theme/color-stories.coverage.test.ts +125 -0
  168. package/src/theme/color-story-kit.tsx +170 -0
  169. package/src/theme/color-utils.ts +197 -0
  170. package/src/theme/config.ts +0 -8
  171. package/src/theme/depth-calibration.test.tsx +97 -0
  172. package/src/theme/elevation.test.ts +62 -1
  173. package/src/theme/elevation.ts +34 -8
  174. package/src/theme/global.css +47 -53
  175. package/src/theme/grey-ramp.test.ts +166 -0
  176. package/src/theme/materials.test.ts +106 -0
  177. package/src/theme/materials.ts +211 -0
  178. package/src/theme/presets/audiobook.ts +4 -4
  179. package/src/theme/presets/types.ts +3 -3
  180. package/src/theme/semantic-contrast.test.ts +83 -0
  181. package/src/theme/tokens/primitives.ts +62 -115
  182. package/src/theme/tokens/semantic.ts +130 -82
  183. package/src/utils/colors.ts +1 -1
  184. package/tailwind.config.js +2 -5
  185. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
  186. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
  187. package/src/theme/ColorSystem.stories.tsx +0 -508
  188. package/src/theme/Colors.stories.tsx +0 -323
  189. package/src/theme/shadows.stories.tsx +0 -523
  190. package/src/theme/shadows.ts +0 -439
@@ -0,0 +1,348 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import {
4
+ primitiveColors,
5
+ primitiveRamps,
6
+ greyRamp,
7
+ SURFACE_PLANE_STEPS,
8
+ categoricalPalette,
9
+ CATEGORICAL_CVD_SAFE_MAX,
10
+ } from './tokens/primitives'
11
+ import {
12
+ SWATCH_BORDER,
13
+ SectionIntro,
14
+ SectionTitle,
15
+ ScaleRow,
16
+ contrast,
17
+ CVD_MATRICES,
18
+ simulateCvd,
19
+ } from './color-story-kit'
20
+
21
+ /**
22
+ * Foundations/Color/Primitives — the raw material, and how it is validated.
23
+ *
24
+ * This half holds the scales themselves and nothing about what they mean. The
25
+ * organisations OF these scales — surfaces, categorical, status, brand,
26
+ * fatigue and the rest — live in the sibling `Foundations/Color/Palettes`.
27
+ *
28
+ * This page is a MENU: everything on it is something you may legitimately reach
29
+ * for. It is now also the complete list — TD-07.14 deleted the five older
30
+ * `primitiveColors` scales (`charcoal`, `neutral`, `redVivid`, and the support
31
+ * `blue`/`red`) rather than leaving them shelved, so there is no longer a
32
+ * category of "real but don't pick these". The greys folded into `greyRamp`
33
+ * below; the chromatics into the OKLCH `primitiveRamps`.
34
+ */
35
+ const meta: Meta = {
36
+ title: 'Foundations/Color/Primitives',
37
+ tags: ['autodocs'],
38
+ }
39
+
40
+ export default meta
41
+
42
+ // ============================================================================
43
+ // 1. Ramps
44
+ // ============================================================================
45
+
46
+ const CHROMATIC_RAMPS: Array<[keyof typeof primitiveRamps, string]> = [
47
+ ['red', 'Red'],
48
+ ['orange', 'Orange'],
49
+ ['amber', 'Amber'],
50
+ ['green', 'Green'],
51
+ ['cyan', 'Cyan'],
52
+ ['blue', 'Blue'],
53
+ ['magenta', 'Magenta'],
54
+ ]
55
+
56
+ const KEYWORDS = [
57
+ { name: 'white', value: primitiveColors.white },
58
+ { name: 'black', value: primitiveColors.black },
59
+ ] as const
60
+
61
+ /** Which surface plane each ramp step backs, for the annotation row. */
62
+ const PLANE_BY_STEP: Record<number, string> = Object.fromEntries(
63
+ Object.entries(SURFACE_PLANE_STEPS).map(([plane, step]) => [step, plane])
64
+ )
65
+
66
+ /** Steps whose value is byte-identical to what shipped in v0.10.0. */
67
+ const ANCHOR_STEPS = new Set<number>(Object.values(SURFACE_PLANE_STEPS))
68
+
69
+ const GREY_STEPS = Object.keys(greyRamp).map(Number).sort((a, b) => a - b)
70
+
71
+ /**
72
+ * The grey ramp, rendered as a numbered scale with its plane aliases.
73
+ *
74
+ * This story used to show six unnumbered plane NAMES here, which was the visible
75
+ * symptom of the greys not actually being a ramp. The numbers are the content:
76
+ * they are what let a border, a plane and a text colour be compared on one scale.
77
+ */
78
+ function GreyRampRow() {
79
+ return (
80
+ <View style={{ flexDirection: 'row', gap: 4, marginBottom: 4 }}>
81
+ {GREY_STEPS.map((step) => {
82
+ const hex = greyRamp[step as keyof typeof greyRamp]
83
+ const plane = PLANE_BY_STEP[step]
84
+ return (
85
+ <View key={step} style={{ flex: 1, alignItems: 'stretch' }}>
86
+ <View
87
+ style={{
88
+ height: 76,
89
+ borderRadius: 6,
90
+ backgroundColor: hex,
91
+ borderWidth: ANCHOR_STEPS.has(step) ? 1.5 : 1,
92
+ borderColor: ANCHOR_STEPS.has(step) ? '#FF7900' : SWATCH_BORDER,
93
+ }}
94
+ />
95
+ <Text className="text-text-primary" style={{ fontSize: 10, fontWeight: '600', marginTop: 3 }}>
96
+ {step}
97
+ {ANCHOR_STEPS.has(step) ? ' ★' : ''}
98
+ </Text>
99
+ <Text className="text-text-tertiary" style={{ fontSize: 8 }}>
100
+ {hex.toUpperCase()}
101
+ </Text>
102
+ {plane ? (
103
+ <Text style={{ fontSize: 8, color: '#FF9628', marginTop: 1 }}>{plane}</Text>
104
+ ) : null}
105
+ </View>
106
+ )
107
+ })}
108
+ </View>
109
+ )
110
+ }
111
+
112
+ export const Ramps: StoryObj = {
113
+ name: '1. Ramps — Source of Truth',
114
+ render: () => (
115
+ <View style={{ padding: 24 }}>
116
+ <Text className="text-2xl font-bold text-text-primary mb-2">Ramps</Text>
117
+ <SectionIntro>
118
+ Every raw colour value titan ships, as ordered scales. Nothing here carries meaning — a
119
+ ramp step becomes a border or a status only when a semantic token or a palette references
120
+ it, which is what{' '}
121
+ <Text className="font-semibold">Foundations/Color/Palettes</Text> documents. Everything on
122
+ this page is current and safe to pick.
123
+ </SectionIntro>
124
+
125
+ <Text className="text-text-tertiary text-xs mb-6">
126
+ Looking for <Text className="font-semibold">charcoal</Text>,{' '}
127
+ <Text className="font-semibold">neutral</Text>,{' '}
128
+ <Text className="font-semibold">redVivid</Text> or the support{' '}
129
+ <Text className="font-semibold">blue</Text>/<Text className="font-semibold">red</Text>{' '}
130
+ scales? All five were DELETED in TD-07.14, not shelved. The two grey scales folded into
131
+ the warm ramp below — they were cold (neutral ran R−B down to −26) while the surfaces had
132
+ been warm since v0.10.0, and nothing ever reconciled the two. The chromatics moved to the
133
+ OKLCH ramps above. Spec and generator:
134
+ coordination/design-explorations/foundations/warm-grey-ramp/.
135
+ </Text>
136
+
137
+ <SectionTitle>Chromatic — OKLCH tonal ramps</SectionTitle>
138
+ <Text className="text-text-secondary text-xs mb-4">
139
+ Seven OKLCH-generated hue ramps, 11 perceptual steps each (50 → 950), built with
140
+ hue-torsion and a chroma arc anchored through the brand/semantic hexes. These are the
141
+ source of truth for chromatic hexes: the categorical, diverging, effort and fatigue
142
+ palettes are all references into these steps rather than duplicated values, so re-tuning a
143
+ ramp moves them together. Amber absorbs the former yellow (lemon → warm body) and cyan
144
+ absorbs the former steel.
145
+ </Text>
146
+ {CHROMATIC_RAMPS.map(([key, name]) => (
147
+ <ScaleRow key={key} name={name} scale={primitiveRamps[key]} />
148
+ ))}
149
+
150
+ <SectionTitle>Achromatic — the warm grey ramp</SectionTitle>
151
+ <Text className="text-text-secondary text-xs mb-3">
152
+ The ONE grey scale. Fifteen steps, warm at every one (R−B never below +2), spaced by
153
+ perceptual lightness so a step is the same visual distance anywhere in the stack. It
154
+ replaced three separate scales — a cold <Text className="font-semibold">charcoal</Text>, a
155
+ cold <Text className="font-semibold">neutral</Text>, and a standalone surface ramp — which
156
+ is why the surface planes appear here as ordinary numbered steps rather than as a parallel
157
+ set of names.
158
+ </Text>
159
+ <Text className="text-text-secondary text-xs mb-4">
160
+ Two things about the numbering, both deliberate.{' '}
161
+ <Text className="font-semibold">975</Text> sits past the end of the shared grid: the
162
+ canonical L* ladder — the median of the seven chromatic ramps — stops at L*10.3, which is
163
+ step 950, and the bezel is L*3.8. There was no number left for it.{' '}
164
+ <Text className="font-semibold">850</Text> and <Text className="font-semibold">875</Text>{' '}
165
+ are quarter-steps because overlay, raised and elevated sit only ~2.7 L* apart and all three
166
+ collide on 900 at normal resolution. There is deliberately no{' '}
167
+ <Text className="font-semibold">inset</Text>: at ΔE 1.10 from the frame it was an
168
+ imperceptible duplicate, which is why <Text className="font-semibold">&lt;Surface
169
+ pressed&gt;</Text> kept collapsing into it.
170
+ </Text>
171
+ <GreyRampRow />
172
+ <Text className="text-text-tertiary text-xs mb-4">
173
+ <Text style={{ color: '#FF9628' }}>★ / orange outline</Text> = a step that also names a
174
+ surface plane, byte-identical to the value that shipped in v0.10.0 — the surfaces did not
175
+ move, only their names. Reach for planes with{' '}
176
+ <Text className="font-semibold">&lt;Surface level&gt;</Text> rather than by reading a step;
177
+ the depth semantics and per-plane token aliases are in the Palettes surface story.
178
+ </Text>
179
+
180
+ <SectionTitle>Keywords</SectionTitle>
181
+ <Text className="text-text-secondary text-xs mb-4">
182
+ Absolute values with no scale. `transparent` is the third and encodes absence — it has no
183
+ swatch because there is nothing to show.
184
+ </Text>
185
+ <View style={{ flexDirection: 'row', gap: 12 }}>
186
+ {KEYWORDS.map(({ name, value }) => (
187
+ <View key={name} style={{ alignItems: 'center' }}>
188
+ <View
189
+ style={{
190
+ width: 62,
191
+ height: 46,
192
+ borderRadius: 6,
193
+ backgroundColor: value,
194
+ borderWidth: 1,
195
+ borderColor: SWATCH_BORDER,
196
+ }}
197
+ />
198
+ <Text className="text-text-secondary text-xs mt-1">{name}</Text>
199
+ <Text className="text-text-tertiary" style={{ fontSize: 8 }}>
200
+ {value.toUpperCase()}
201
+ </Text>
202
+ </View>
203
+ ))}
204
+ </View>
205
+ </View>
206
+ ),
207
+ }
208
+
209
+ // ============================================================================
210
+ // 2. Accessibility — contrast + CVD
211
+ // ============================================================================
212
+
213
+ /** The mid-step of each chromatic ramp — the band most fills and strokes land on. */
214
+ const RAMP_MIDS = CHROMATIC_RAMPS.map(([key, name]) => ({
215
+ name,
216
+ hex: primitiveRamps[key][500],
217
+ }))
218
+
219
+ /**
220
+ * Width of the mode-label gutter. The swatch rows are indented by the same
221
+ * amount so each simulation strip sits directly under the colour it simulates —
222
+ * the comparison is worthless if the reader has to count across to pair them.
223
+ */
224
+ const CVD_LABEL_WIDTH = 96
225
+
226
+ function CvdStrips({ colors, width }: { colors: readonly string[]; width: number }) {
227
+ return (
228
+ <>
229
+ {(['deuteranopia', 'protanopia'] as const).map((mode) => (
230
+ <View key={mode} style={{ flexDirection: 'row', alignItems: 'center', marginTop: 3 }}>
231
+ <Text className="text-text-secondary" style={{ fontSize: 9, width: CVD_LABEL_WIDTH }}>
232
+ {mode}
233
+ </Text>
234
+ <View style={{ flexDirection: 'row', gap: 6 }}>
235
+ {colors.map((hex, i) => (
236
+ <View
237
+ key={i}
238
+ style={{
239
+ width,
240
+ height: 14,
241
+ borderRadius: 3,
242
+ backgroundColor: simulateCvd(CVD_MATRICES[mode], hex),
243
+ }}
244
+ />
245
+ ))}
246
+ </View>
247
+ </View>
248
+ ))}
249
+ </>
250
+ )
251
+ }
252
+
253
+ export const Accessibility: StoryObj = {
254
+ name: '2. Accessibility — Contrast + CVD',
255
+ render: () => (
256
+ <View style={{ padding: 24 }}>
257
+ <Text className="text-2xl font-bold text-text-primary mb-2">Accessibility</Text>
258
+ <SectionIntro>
259
+ How the raw material behaves for viewers who cannot separate the hues, and under text.
260
+ Dichromacy is simulated with the Machado-2009 matrices at severity 1.0. Converging strips
261
+ across two swatches mean those two are the same colour to that viewer — which is a
262
+ problem only where hue is carrying the meaning on its own.
263
+ </SectionIntro>
264
+
265
+ <SectionTitle>Ramp mid-steps (500)</SectionTitle>
266
+ <Text className="text-text-secondary text-xs mb-3">
267
+ The seven hues at the step most fills land on. Red/orange/amber and green/cyan compress
268
+ toward each other under both dichromacies — which is exactly why the categorical palette
269
+ below fans its picks across lightness instead of taking all seven at 500, and why the
270
+ ordered scales in Palettes encode magnitude in lightness rather than hue.
271
+ </Text>
272
+ <View
273
+ style={{ flexDirection: 'row', gap: 6, marginBottom: 6, paddingLeft: CVD_LABEL_WIDTH }}
274
+ >
275
+ {RAMP_MIDS.map(({ name, hex }) => (
276
+ <View key={name} style={{ alignItems: 'center' }}>
277
+ <View
278
+ style={{
279
+ width: 76,
280
+ height: 40,
281
+ borderRadius: 6,
282
+ backgroundColor: hex,
283
+ borderWidth: 1,
284
+ borderColor: SWATCH_BORDER,
285
+ }}
286
+ />
287
+ <Text className="text-text-secondary" style={{ fontSize: 9, marginTop: 2 }}>
288
+ {name}
289
+ </Text>
290
+ </View>
291
+ ))}
292
+ </View>
293
+ <CvdStrips colors={RAMP_MIDS.map((r) => r.hex)} width={76} />
294
+
295
+ <View style={{ height: 28 }} />
296
+
297
+ <SectionTitle>Categorical series — in-place text contrast</SectionTitle>
298
+ <Text className="text-text-secondary text-xs mb-3">
299
+ The palette that has to survive this test in production. All pairs hold ΔE ≥ 8 through the
300
+ first {CATEGORICAL_CVD_SAFE_MAX}; the 7th is extended and wants a legend. The palette
301
+ itself is documented in Palettes — this is the validation of it.
302
+ </Text>
303
+ {(['default', 'dark'] as const).map((variant) => {
304
+ const colors = categoricalPalette[variant]
305
+ const textColor = variant === 'dark' ? primitiveColors.white : primitiveColors.black
306
+ return (
307
+ <View key={variant} style={{ marginBottom: 22 }}>
308
+ <Text
309
+ className="font-semibold text-text-primary text-sm mb-2"
310
+ style={{ textTransform: 'capitalize' }}
311
+ >
312
+ {variant}
313
+ </Text>
314
+ <View
315
+ style={{
316
+ flexDirection: 'row',
317
+ gap: 6,
318
+ marginBottom: 6,
319
+ paddingLeft: CVD_LABEL_WIDTH,
320
+ }}
321
+ >
322
+ {colors.map((hex, i) => (
323
+ <View key={i} style={{ alignItems: 'center' }}>
324
+ <View
325
+ style={{
326
+ width: 56,
327
+ height: 40,
328
+ borderRadius: 6,
329
+ backgroundColor: hex,
330
+ alignItems: 'center',
331
+ justifyContent: 'center',
332
+ }}
333
+ >
334
+ <Text style={{ color: textColor, fontSize: 11, fontWeight: '700' }}>Aa</Text>
335
+ </View>
336
+ <Text className="text-text-secondary" style={{ fontSize: 8, marginTop: 2 }}>
337
+ {contrast(hex, textColor).toFixed(1)}:1
338
+ </Text>
339
+ </View>
340
+ ))}
341
+ </View>
342
+ <CvdStrips colors={colors} width={56} />
343
+ </View>
344
+ )
345
+ })}
346
+ </View>
347
+ ),
348
+ }
@@ -0,0 +1,312 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { greyRamp } from './tokens/primitives'
4
+ import { getSemanticColors } from './tokens/semantic'
5
+ import {
6
+ getElevationSurface,
7
+ getElevationShadow,
8
+ getBaseSurfaceColor,
9
+ FLOATING_ELEVATION_MIN,
10
+ type ElevationLevel,
11
+ } from './elevation'
12
+ import { paperSheet, insetWell, barPaper } from './materials'
13
+ import { SectionIntro, SectionTitle } from './color-story-kit'
14
+
15
+ const t = getSemanticColors('dark')
16
+
17
+ /**
18
+ * Foundations/Depth — how a surface says how far away it is.
19
+ *
20
+ * There are exactly three mechanisms, and which one applies is not a taste call:
21
+ *
22
+ * TONE how deep a plane sits — the grey ramp
23
+ * HAIRLINE where one plane ends — alpha, self-normalising
24
+ * MATERIAL what a plane is made of — paper, well
25
+ *
26
+ * Drop-shadow is deliberately not on that list for anything inline. This page
27
+ * exists because the system previously had a fourth mechanism — a neumorphic
28
+ * dual-opposing shadow — that could not work here and was removed in TD-07.16.
29
+ */
30
+ const meta: Meta = {
31
+ title: 'Foundations/Depth',
32
+ tags: ['autodocs'],
33
+ }
34
+ export default meta
35
+
36
+ const CONTENT_LEVELS: ElevationLevel[] = [0, 1, 2, 3]
37
+ const FLOATING_LEVELS: ElevationLevel[] = [4, 5]
38
+ const base = getBaseSurfaceColor('dark')
39
+
40
+ function Swatch({
41
+ label,
42
+ note,
43
+ style,
44
+ }: {
45
+ label: string
46
+ note: string
47
+ style: Record<string, unknown>
48
+ }) {
49
+ return (
50
+ <View style={{ width: 190 }}>
51
+ <View
52
+ style={{
53
+ height: 92,
54
+ borderRadius: 10,
55
+ padding: 10,
56
+ justifyContent: 'flex-end',
57
+ ...style,
58
+ }}
59
+ >
60
+ <Text className="text-text-primary" style={{ fontSize: 12, fontWeight: '600' }}>
61
+ {label}
62
+ </Text>
63
+ </View>
64
+ <Text className="text-text-tertiary" style={{ fontSize: 9, marginTop: 4, lineHeight: 13 }}>
65
+ {note}
66
+ </Text>
67
+ </View>
68
+ )
69
+ }
70
+
71
+ export const Mechanisms: StoryObj = {
72
+ name: '1. The three mechanisms',
73
+ render: () => (
74
+ <View style={{ padding: 24, backgroundColor: greyRamp[975] }}>
75
+ <Text className="text-2xl font-bold text-text-primary mb-2">Depth</Text>
76
+ <SectionIntro>
77
+ Three mechanisms, in the order you should reach for them. A surface that needs to read as
78
+ deeper takes a TONE; one that needs an edge takes a HAIRLINE; one that needs to feel
79
+ physical takes a MATERIAL. Drop-shadow is not on the list for anything that is not
80
+ floating — see section 3 for why.
81
+ </SectionIntro>
82
+
83
+ <SectionTitle>Tone — the grey ramp</SectionTitle>
84
+ <Text className="text-text-secondary text-xs mb-3">
85
+ The primary cue and the only one that works identically on web and native. Every plane is
86
+ a ramp step.
87
+ </Text>
88
+ <View style={{ flexDirection: 'row', gap: 10, marginBottom: 22 }}>
89
+ {[975, 950, 925, 900, 875, 850].map((step) => (
90
+ <Swatch
91
+ key={step}
92
+ label={`grey-${step}`}
93
+ note=""
94
+ style={{ backgroundColor: greyRamp[step as keyof typeof greyRamp] }}
95
+ />
96
+ ))}
97
+ </View>
98
+
99
+ <SectionTitle>Hairline — where a plane ends</SectionTitle>
100
+ <Text className="text-text-secondary text-xs mb-3">
101
+ Alpha-white, so it composites toward white by the same amount on any background and holds
102
+ a near-constant ΔL* wherever it lands. A solid border swings wildly across the ramp; this
103
+ does not. It is the default separator, and it is what replaced the solid dark border
104
+ tokens.
105
+ </Text>
106
+ <View style={{ flexDirection: 'row', gap: 10, marginBottom: 22 }}>
107
+ {(['hairline-subtle', 'hairline-default', 'hairline-strong'] as const).map((token) => (
108
+ <Swatch
109
+ key={token}
110
+ label={token.replace('hairline-', '')}
111
+ note={t[token]}
112
+ style={{
113
+ backgroundColor: greyRamp[925],
114
+ borderWidth: 1,
115
+ borderColor: t[token],
116
+ }}
117
+ />
118
+ ))}
119
+ </View>
120
+
121
+ <SectionTitle>Material — what a plane is made of</SectionTitle>
122
+ <Text className="text-text-secondary text-xs mb-3">
123
+ Used sparingly. Paper is for HERO surfaces only and takes muted tones only — vivid colour
124
+ is ink ON paper, never the paper itself. There are no levels of paper; one treatment, or
125
+ the texture stops meaning anything.
126
+ </Text>
127
+ <View style={{ flexDirection: 'row', gap: 10, marginBottom: 8 }}>
128
+ <Swatch
129
+ label="paperSheet"
130
+ note="tonal fill (ΔL* ≤ 3) + grain + dither + rim + contact shadow. Hero surfaces."
131
+ style={paperSheet(greyRamp[875]) as Record<string, unknown>}
132
+ />
133
+ <Swatch
134
+ label="insetWell"
135
+ note="inner top cut + a light line on the floor. The floor rim is the half that says 'below'."
136
+ style={insetWell(greyRamp[950]) as Record<string, unknown>}
137
+ />
138
+ <Swatch
139
+ label="barPaper"
140
+ note="the same grain curve, tuned for a small saturated element."
141
+ style={{ ...(barPaper(t['status-info']) as Record<string, unknown>), backgroundColor: t['status-info'] }}
142
+ />
143
+ </View>
144
+ <Text className="text-text-tertiary text-xs">
145
+ Materials are web/RNW only — `backgroundImage` and multi-layer `boxShadow` are ignored on
146
+ native. Every material keeps a solid `backgroundColor` underneath so a surface never
147
+ depends on one, and a material must never be the only thing carrying meaning.
148
+ </Text>
149
+ </View>
150
+ ),
151
+ }
152
+
153
+ export const Elevation: StoryObj = {
154
+ name: '2. Elevation — tone below, shadow only when floating',
155
+ render: () => (
156
+ <View style={{ padding: 24, backgroundColor: greyRamp[975] }}>
157
+ <Text className="text-2xl font-bold text-text-primary mb-2">Elevation</Text>
158
+ <SectionIntro>
159
+ Levels 0–3 are separated by TONE ALONE. They cast no shadow, because a dark-on-dark drop
160
+ shadow is inert — it costs a composite layer and shows nothing. From{' '}
161
+ <Text className="font-semibold">{FLOATING_ELEVATION_MIN}</Text> up the element is
162
+ genuinely floating over a backdrop, where a large soft shadow describes separation rather
163
+ than rank, and there the shadow returns.
164
+ </SectionIntro>
165
+
166
+ <SectionTitle>Content levels — tone only</SectionTitle>
167
+ <View style={{ flexDirection: 'row', gap: 10, marginBottom: 22 }}>
168
+ {CONTENT_LEVELS.map((lv) => (
169
+ <Swatch
170
+ key={lv}
171
+ label={`elevation ${lv}`}
172
+ note={`${getElevationSurface(base, lv, 'dark')} · no shadow`}
173
+ style={{
174
+ backgroundColor: getElevationSurface(base, lv, 'dark'),
175
+ borderWidth: 1,
176
+ borderColor: t['hairline-default'],
177
+ ...(getElevationShadow(base, lv, 'dark') as Record<string, unknown>),
178
+ }}
179
+ />
180
+ ))}
181
+ </View>
182
+
183
+ <SectionTitle>Floating levels — shadow returns, always with a ring</SectionTitle>
184
+ <View style={{ flexDirection: 'row', gap: 24, marginBottom: 8 }}>
185
+ {FLOATING_LEVELS.map((lv) => (
186
+ <Swatch
187
+ key={lv}
188
+ label={`elevation ${lv}`}
189
+ note={`${getElevationSurface(base, lv, 'dark')} · casts a shadow`}
190
+ style={{
191
+ backgroundColor: getElevationSurface(base, lv, 'dark'),
192
+ borderWidth: 1,
193
+ borderColor: t['hairline-strong'],
194
+ ...(getElevationShadow(base, lv, 'dark') as Record<string, unknown>),
195
+ }}
196
+ />
197
+ ))}
198
+ </View>
199
+ <Text className="text-text-tertiary text-xs">
200
+ A floating surface should carry a hairline ring as well as its shadow — the shadow places
201
+ it above the page, the ring gives it an edge that survives on a panel where the shadow is
202
+ washed out by glare.
203
+ </Text>
204
+ </View>
205
+ ),
206
+ }
207
+
208
+ export const WhyNotNeumorphism: StoryObj = {
209
+ name: '3. Why not neumorphism',
210
+ render: () => (
211
+ <View style={{ padding: 24, backgroundColor: greyRamp[975] }}>
212
+ <Text className="text-2xl font-bold text-text-primary mb-2">Why not neumorphism</Text>
213
+ <SectionIntro>
214
+ The system used to ship a neumorphic builder — a dark shadow bottom-right paired with a
215
+ light rim top-left. It was removed rather than retuned, and the reason is a lightness
216
+ budget rather than a preference, so it is worth being able to see.
217
+ </SectionIntro>
218
+
219
+ <Text className="text-text-secondary text-xs mb-4">
220
+ Neumorphism needs a MID-TONE ground (roughly 75–90% lightness) so the dark half has
221
+ somewhere to fall and the light half has somewhere to rise. Our surfaces sit near 10%.
222
+ Below them there is almost nothing left — so the dark half disappears and the treatment
223
+ collapses to a one-sided rim, which reads as a smudge rather than as depth.
224
+ </Text>
225
+
226
+ <View style={{ flexDirection: 'row', gap: 24 }}>
227
+ <View>
228
+ <Text className="text-text-secondary text-xs mb-2">
229
+ On a mid-tone ground — both halves have room
230
+ </Text>
231
+ <View
232
+ style={{
233
+ backgroundColor: greyRamp[300],
234
+ padding: 26,
235
+ borderRadius: 12,
236
+ flexDirection: 'row',
237
+ gap: 16,
238
+ }}
239
+ >
240
+ <View
241
+ style={
242
+ {
243
+ width: 110,
244
+ height: 66,
245
+ borderRadius: 10,
246
+ backgroundColor: greyRamp[300],
247
+ boxShadow: '3px 3px 6px rgba(0,0,0,0.35), -3px -3px 6px rgba(255,255,255,0.55)',
248
+ } as Record<string, unknown>
249
+ }
250
+ />
251
+ </View>
252
+ </View>
253
+
254
+ <View>
255
+ <Text className="text-text-secondary text-xs mb-2">
256
+ On our ground — the dark half has nowhere to go
257
+ </Text>
258
+ <View
259
+ style={{
260
+ backgroundColor: greyRamp[925],
261
+ padding: 26,
262
+ borderRadius: 12,
263
+ flexDirection: 'row',
264
+ gap: 16,
265
+ }}
266
+ >
267
+ <View
268
+ style={
269
+ {
270
+ width: 110,
271
+ height: 66,
272
+ borderRadius: 10,
273
+ backgroundColor: greyRamp[925],
274
+ boxShadow: '3px 3px 6px rgba(0,0,0,0.35), -3px -3px 6px rgba(255,255,255,0.12)',
275
+ } as Record<string, unknown>
276
+ }
277
+ />
278
+ </View>
279
+ </View>
280
+
281
+ <View>
282
+ <Text className="text-text-secondary text-xs mb-2">What we do instead</Text>
283
+ <View
284
+ style={{
285
+ backgroundColor: greyRamp[925],
286
+ padding: 26,
287
+ borderRadius: 12,
288
+ flexDirection: 'row',
289
+ gap: 16,
290
+ }}
291
+ >
292
+ <View
293
+ style={{
294
+ width: 110,
295
+ height: 66,
296
+ borderRadius: 10,
297
+ backgroundColor: greyRamp[900],
298
+ borderWidth: 1,
299
+ borderColor: t['hairline-default'],
300
+ }}
301
+ />
302
+ </View>
303
+ </View>
304
+ </View>
305
+
306
+ <Text className="text-text-tertiary text-xs mt-4">
307
+ Same treatment, same offsets, different ground. The middle panel is the one that shipped.
308
+ The right panel is one tone step plus a hairline — cheaper, and it survives glare.
309
+ </Text>
310
+ </View>
311
+ ),
312
+ }