@titan-design/react-ui 0.11.0 → 0.12.1
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.
- package/dist/bodymap.js +173 -343
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +173 -343
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +27 -22
- package/dist/index.d.ts +27 -22
- package/dist/index.js +366 -561
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +360 -553
- package/dist/index.mjs.map +1 -1
- package/dist/pages.js +242 -407
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +242 -407
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +250 -1157
- package/dist/theme/index.d.ts +250 -1157
- package/dist/theme/index.js +234 -408
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +227 -399
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +141 -140
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +141 -140
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/dist/theme/tokens.d.mts +904 -0
- package/dist/theme/tokens.d.ts +904 -0
- package/dist/theme/tokens.js +856 -0
- package/dist/theme/tokens.js.map +1 -0
- package/dist/theme/tokens.mjs +833 -0
- package/dist/theme/tokens.mjs.map +1 -0
- package/package.json +7 -1
- package/src/arch/Architecture.stories.tsx +9 -9
- package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
- package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +3 -3
- package/src/components/custom/Fatigue/FatigueLights.stories.tsx +2 -2
- package/src/components/custom/Fatigue/GhostSpark.stories.tsx +3 -3
- package/src/components/custom/Fatigue/LiveFatigueCard.tsx +1 -1
- package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +2 -2
- package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +2 -2
- package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +4 -4
- package/src/components/custom/Fatigue/VelocityHero.stories.tsx +3 -3
- package/src/components/custom/Fatigue/VerdictHero.stories.tsx +2 -2
- package/src/components/custom/Fatigue/fatigue-tokens.ts +12 -12
- package/src/components/custom/Sidebar/Sidebar.tsx +4 -4
- package/src/components/custom/Table/Table.tsx +2 -2
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +3 -3
- package/src/components/custom/Workout/BaseBadge.test.tsx +6 -1
- package/src/components/custom/Workout/BaseBadge.tsx +1 -1
- package/src/components/custom/Workout/BodyMap.tsx +2 -2
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +2 -2
- package/src/components/custom/Workout/DeviationBar.test.tsx +2 -1
- package/src/components/custom/Workout/DeviationBar.tsx +3 -2
- package/src/components/custom/Workout/ExerciseCard.test.tsx +3 -3
- package/src/components/custom/Workout/ExerciseCard.tsx +2 -2
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -4
- package/src/components/custom/Workout/InputBar.tsx +2 -2
- package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +1 -1
- package/src/components/custom/Workout/LiveAuraFrame.test.tsx +1 -1
- package/src/components/custom/Workout/LiveAuraFrame.tsx +3 -3
- package/src/components/custom/Workout/MesoCard.tsx +3 -1
- package/src/components/custom/Workout/MuscleGroupChip.tsx +4 -3
- package/src/components/custom/Workout/PrHistoryModal.tsx +2 -2
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +1 -1
- package/src/components/custom/Workout/README.md +1 -1
- package/src/components/custom/Workout/ReadinessCheck.tsx +2 -2
- package/src/components/custom/Workout/RestTimer.tsx +2 -2
- package/src/components/custom/Workout/S3SetTypes.stories.tsx +3 -3
- package/src/components/custom/Workout/SessionHeader.stories.tsx +2 -2
- package/src/components/custom/Workout/SessionHeader.tsx +1 -1
- package/src/components/custom/Workout/SessionRail.stories.tsx +2 -2
- package/src/components/custom/Workout/SessionRail.tsx +13 -5
- package/src/components/custom/Workout/SetBar.tsx +3 -3
- package/src/components/custom/Workout/SetRow.test.tsx +6 -3
- package/src/components/custom/Workout/StatusDot.tsx +5 -4
- package/src/components/custom/Workout/StrengthTrendChart.tsx +1 -1
- package/src/components/custom/Workout/TrainingStatusPage.tsx +2 -2
- package/src/components/custom/Workout/VelocityStrip.test.tsx +5 -2
- package/src/components/custom/Workout/VelocityStrip.tsx +4 -4
- package/src/components/custom/Workout/VolumeLandmarkBar.tsx +3 -3
- package/src/components/custom/Workout/WeightBadge.test.tsx +6 -1
- package/src/components/custom/Workout/WeightBadge.tsx +2 -1
- package/src/components/custom/Workout/WorkoutCard.tsx +3 -1
- package/src/components/custom/Workout/WorkoutPill.tsx +5 -2
- package/src/components/custom/Workout/ZoneTrack.tsx +4 -4
- package/src/components/custom/Workout/setHeadingKit.tsx +14 -8
- package/src/components/custom/Workout/velocity-story-kit.tsx +2 -2
- package/src/components/custom/charts/SetBarChart.tsx +3 -3
- package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
- package/src/components/custom/stepper/Stepper.tsx +3 -3
- package/src/components/shell/DeviceMenu.tsx +2 -2
- package/src/components/shell/README.md +1 -1
- package/src/components/shell/SessionRailSpecimen.stories.tsx +1 -1
- package/src/components/shell/SideNav.tsx +1 -1
- package/src/components/shell/TopBar.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
- package/src/components/ui/avatar/Avatar.tsx +2 -2
- package/src/components/ui/badge/Badge.tsx +2 -2
- package/src/components/ui/card/Card.tsx +3 -3
- package/src/components/ui/checkbox/Checkbox.tsx +1 -1
- package/src/components/ui/collapse/Collapse.tsx +1 -1
- package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
- package/src/components/ui/drawer/Drawer.tsx +3 -3
- package/src/components/ui/form-field/FormField.tsx +1 -1
- package/src/components/ui/help-tip/HelpTip.tsx +1 -1
- package/src/components/ui/menu/Menu.tsx +2 -2
- package/src/components/ui/pill/Pill.tsx +2 -2
- package/src/components/ui/popover/Popover.stories.tsx +2 -2
- package/src/components/ui/popover/Popover.tsx +1 -1
- package/src/components/ui/progress/Progress.tsx +3 -3
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/radio/Radio.tsx +4 -4
- package/src/components/ui/select/Select.tsx +3 -3
- package/src/components/ui/surface/Surface.stories.tsx +3 -3
- package/src/components/ui/surface/Surface.test.tsx +32 -19
- package/src/components/ui/surface/Surface.tsx +3 -3
- package/src/components/ui/surface/SurfaceContext.ts +19 -29
- package/src/components/ui/surface/surface.contract.test.ts +58 -45
- package/src/components/ui/switch/Switch.tsx +1 -1
- package/src/components/ui/tabs/Tabs.tsx +2 -2
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +5 -5
- package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +36 -22
- package/src/stories/PresetShowcase.stories.tsx +21 -21
- package/src/theme/ColorPalettes.stories.tsx +41 -38
- package/src/theme/ColorPrimitives.stories.tsx +89 -41
- package/src/theme/Depth.stories.tsx +312 -0
- package/src/theme/DepthCalibration.stories.tsx +655 -0
- package/src/theme/ThemeProvider.tsx +1 -1
- package/src/theme/barrel.ts +7 -14
- package/src/theme/color-stories.coverage.test.ts +15 -20
- package/src/theme/color-story-kit.tsx +1 -1
- package/src/theme/color-utils.ts +197 -0
- package/src/theme/config.ts +0 -8
- package/src/theme/depth-calibration.test.tsx +97 -0
- package/src/theme/elevation.test.ts +62 -1
- package/src/theme/elevation.ts +34 -8
- package/src/theme/global.css +47 -53
- package/src/theme/grey-ramp.test.ts +166 -0
- package/src/theme/materials.test.ts +106 -0
- package/src/theme/materials.ts +211 -0
- package/src/theme/presets/audiobook.ts +4 -4
- package/src/theme/presets/types.ts +3 -3
- package/src/theme/semantic-contrast.test.ts +83 -0
- package/src/theme/token-export-guard.test.ts +104 -0
- package/src/theme/tokens/primitives.ts +63 -116
- package/src/theme/tokens/semantic.ts +130 -82
- package/src/utils/colors.ts +1 -1
- package/tailwind.config.js +2 -5
- package/src/theme/bar-paper.ts +0 -59
- package/src/theme/shadows.stories.tsx +0 -523
- package/src/theme/shadows.ts +0 -439
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
3
|
+
import { View, Text, Pressable } from 'react-native'
|
|
4
|
+
import { greyRamp } from './tokens/primitives'
|
|
5
|
+
import { getSemanticColors } from './tokens/semantic'
|
|
6
|
+
import { getBaseSurfaceColor, getElevationSurface, type ElevationLevel } from './elevation'
|
|
7
|
+
import { grainForTone, paperSheet } from './materials'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Foundations/Depth Calibration — the S-6 wall gate (VW-99).
|
|
11
|
+
*
|
|
12
|
+
* `Foundations/Depth` explains the depth model. This file MEASURES it, and the
|
|
13
|
+
* two are deliberately shaped differently:
|
|
14
|
+
*
|
|
15
|
+
* Depth small labelled swatches, captions that give the answer
|
|
16
|
+
* Depth Calibration full-bleed fields, no labels, decoys, a hidden key
|
|
17
|
+
*
|
|
18
|
+
* Every threshold in the depth model (TD-07.16) is desk-verified only. The
|
|
19
|
+
* north-star doc set its floors explicitly "to survive glare + a mediocre
|
|
20
|
+
* panel", and none of them have been looked at on the panel. That matters more
|
|
21
|
+
* than the numbers suggest: solid dark borders were DELETED, so if the hairlines
|
|
22
|
+
* wash out there is no fallback, and elevation levels 1-3 are separated by tone
|
|
23
|
+
* alone now that shadow is gated behind FLOATING_ELEVATION_MIN.
|
|
24
|
+
*
|
|
25
|
+
* WHY FORCED-CHOICE. "Does this look OK?" is close to unfalsifiable at
|
|
26
|
+
* threshold — you are asking someone to report the absence of a faint thing they
|
|
27
|
+
* already know is supposed to be there. So each panel asks a question with a
|
|
28
|
+
* checkable answer, hides the key behind a toggle, and seeds decoys (cells with
|
|
29
|
+
* NO hairline, pairs that are genuinely identical). A run that "sees" a decoy is
|
|
30
|
+
* a run whose PASSes mean nothing — re-run it at the real viewing distance.
|
|
31
|
+
*
|
|
32
|
+
* HOW TO RUN. On the wall panel, at demo brightness, at demo viewing distance
|
|
33
|
+
* (~3 m), with the room lit the way it will be lit. Answer every panel BEFORE
|
|
34
|
+
* touching Reveal. Record on the sheet:
|
|
35
|
+
* `voltras-workspace/sources/runbooks/VW-99-depth-wall-calibration.md`
|
|
36
|
+
*/
|
|
37
|
+
const meta: Meta = {
|
|
38
|
+
title: 'Foundations/Depth Calibration',
|
|
39
|
+
parameters: {
|
|
40
|
+
// Global preview layout is 'centered'; every panel here needs the full
|
|
41
|
+
// panel width, because half the question is whether a thing survives at size.
|
|
42
|
+
layout: 'fullscreen',
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
export default meta
|
|
46
|
+
|
|
47
|
+
const t = getSemanticColors('dark')
|
|
48
|
+
const base = getBaseSurfaceColor('dark')
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The rig's ground sits BELOW the ramp, at true black.
|
|
52
|
+
*
|
|
53
|
+
* Not a style choice. `grey-975` is a plane the hairlines have to work on, and
|
|
54
|
+
* if the page were also 975 those cells would be invisible against it — you
|
|
55
|
+
* would be judging a line with no plane around it. Nothing in the product ships
|
|
56
|
+
* on true black; this is a test surface, and it is deliberately off the ramp so
|
|
57
|
+
* that every step OF the ramp is itself testable.
|
|
58
|
+
*/
|
|
59
|
+
const GROUND = '#000000'
|
|
60
|
+
|
|
61
|
+
// ── furniture ───────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Panel chrome: the task prompt, and a Reveal toggle that is the only thing
|
|
65
|
+
* standing between the operator and a primed judgement. Children receive
|
|
66
|
+
* `revealed` so each panel decides what its key looks like.
|
|
67
|
+
*/
|
|
68
|
+
function Panel({
|
|
69
|
+
title,
|
|
70
|
+
task,
|
|
71
|
+
children,
|
|
72
|
+
}: {
|
|
73
|
+
title: string
|
|
74
|
+
task: string
|
|
75
|
+
children: (revealed: boolean) => React.ReactNode
|
|
76
|
+
}) {
|
|
77
|
+
const [revealed, setRevealed] = useState(false)
|
|
78
|
+
return (
|
|
79
|
+
<View style={{ backgroundColor: GROUND, padding: 28, minHeight: '100vh' as never }}>
|
|
80
|
+
<View
|
|
81
|
+
style={{
|
|
82
|
+
flexDirection: 'row',
|
|
83
|
+
alignItems: 'flex-start',
|
|
84
|
+
justifyContent: 'space-between',
|
|
85
|
+
gap: 24,
|
|
86
|
+
marginBottom: 18,
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<View style={{ flex: 1 }}>
|
|
90
|
+
<Text className="text-text-primary" style={{ fontSize: 22, fontWeight: '700' }}>
|
|
91
|
+
{title}
|
|
92
|
+
</Text>
|
|
93
|
+
<Text
|
|
94
|
+
className="text-text-secondary"
|
|
95
|
+
style={{ fontSize: 15, marginTop: 6, maxWidth: 900 }}
|
|
96
|
+
>
|
|
97
|
+
{task}
|
|
98
|
+
</Text>
|
|
99
|
+
</View>
|
|
100
|
+
<Pressable
|
|
101
|
+
onPress={() => setRevealed((r) => !r)}
|
|
102
|
+
style={{
|
|
103
|
+
paddingVertical: 10,
|
|
104
|
+
paddingHorizontal: 18,
|
|
105
|
+
borderRadius: 8,
|
|
106
|
+
borderWidth: 1,
|
|
107
|
+
borderColor: t['hairline-strong'],
|
|
108
|
+
backgroundColor: getElevationSurface(base, 2, 'dark'),
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<Text className="text-text-primary" style={{ fontSize: 14, fontWeight: '600' }}>
|
|
112
|
+
{revealed ? 'Hide key' : 'Reveal key'}
|
|
113
|
+
</Text>
|
|
114
|
+
</Pressable>
|
|
115
|
+
</View>
|
|
116
|
+
{children(revealed)}
|
|
117
|
+
</View>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function KeyLine({ children }: { children: string }) {
|
|
122
|
+
return (
|
|
123
|
+
<Text className="text-text-secondary" style={{ fontSize: 13, marginTop: 10, lineHeight: 19 }}>
|
|
124
|
+
{children}
|
|
125
|
+
</Text>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── C1 · hairlines ──────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
type HairlineToken = 'hairline-subtle' | 'hairline-default' | 'hairline-strong'
|
|
132
|
+
type HairlineCondition = HairlineToken | 'none'
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A fixed 4×4 assignment, not a random one — a story that shuffles on each
|
|
136
|
+
* render cannot be re-run, screenshotted, or visually regression-tested. It is
|
|
137
|
+
* unprimed because the KEY is hidden, not because the layout is unpredictable.
|
|
138
|
+
*
|
|
139
|
+
* Rows are the ground planes a hairline actually lands on in production. Each
|
|
140
|
+
* row contains all four conditions in a different order, so no column is "the
|
|
141
|
+
* blank column" and no row can be answered by pattern rather than by looking.
|
|
142
|
+
*/
|
|
143
|
+
const HAIRLINE_GROUNDS = [975, 950, 925, 900] as const
|
|
144
|
+
const HAIRLINE_GRID: HairlineCondition[][] = [
|
|
145
|
+
['hairline-default', 'none', 'hairline-strong', 'hairline-subtle'],
|
|
146
|
+
['hairline-subtle', 'hairline-strong', 'none', 'hairline-default'],
|
|
147
|
+
['none', 'hairline-default', 'hairline-subtle', 'hairline-strong'],
|
|
148
|
+
['hairline-strong', 'hairline-subtle', 'hairline-default', 'none'],
|
|
149
|
+
]
|
|
150
|
+
const CELL_LABELS = ['A', 'B', 'C', 'D']
|
|
151
|
+
|
|
152
|
+
export const Hairlines: StoryObj = {
|
|
153
|
+
name: 'C1 · Hairlines — the only separator left',
|
|
154
|
+
render: () => (
|
|
155
|
+
<Panel
|
|
156
|
+
title="C1 · Hairlines"
|
|
157
|
+
task={
|
|
158
|
+
'Each cell is one flat plane cut in half by a horizontal divider — or by nothing at all. ' +
|
|
159
|
+
'Four of the sixteen have NO divider. Write down every cell where you can see a line, ' +
|
|
160
|
+
'from the real viewing distance, without leaning in. Then reveal.'
|
|
161
|
+
}
|
|
162
|
+
>
|
|
163
|
+
{(revealed) => (
|
|
164
|
+
<View>
|
|
165
|
+
{HAIRLINE_GRID.map((row, r) => (
|
|
166
|
+
<View
|
|
167
|
+
key={HAIRLINE_GROUNDS[r]}
|
|
168
|
+
style={{ flexDirection: 'row', gap: 14, marginBottom: 22, alignItems: 'flex-start' }}
|
|
169
|
+
>
|
|
170
|
+
{/* Row gutter. Labels used to sit UNDER their tiles, which made every
|
|
171
|
+
row number ambiguous the moment one row was hard to see — the
|
|
172
|
+
first real-operator run misnumbered the whole grid because of it.
|
|
173
|
+
The label now leads its tile, and the row has a fixed marker. */}
|
|
174
|
+
<View style={{ width: 30, paddingTop: 22 }}>
|
|
175
|
+
<Text className="text-text-tertiary" style={{ fontSize: 15, fontWeight: '700' }}>
|
|
176
|
+
{r + 1}
|
|
177
|
+
</Text>
|
|
178
|
+
</View>
|
|
179
|
+
{row.map((condition, cIdx) => {
|
|
180
|
+
const ground = greyRamp[HAIRLINE_GROUNDS[r]]
|
|
181
|
+
return (
|
|
182
|
+
<View key={cIdx} style={{ flex: 1 }}>
|
|
183
|
+
<Text
|
|
184
|
+
className="text-text-tertiary"
|
|
185
|
+
style={{ fontSize: 13, marginBottom: 5, fontWeight: '600' }}
|
|
186
|
+
>
|
|
187
|
+
{`${CELL_LABELS[cIdx]}${r + 1}`}
|
|
188
|
+
{revealed
|
|
189
|
+
? condition === 'none'
|
|
190
|
+
? ' · NONE (decoy)'
|
|
191
|
+
: ` · ${condition.replace('hairline-', '')} on grey-${HAIRLINE_GROUNDS[r]}`
|
|
192
|
+
: ''}
|
|
193
|
+
</Text>
|
|
194
|
+
<View style={{ height: 128, backgroundColor: ground, borderRadius: 6 }}>
|
|
195
|
+
<View style={{ flex: 1 }} />
|
|
196
|
+
<View
|
|
197
|
+
style={{
|
|
198
|
+
height: 1,
|
|
199
|
+
backgroundColor: condition === 'none' ? 'transparent' : t[condition],
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
<View style={{ flex: 1 }} />
|
|
203
|
+
</View>
|
|
204
|
+
</View>
|
|
205
|
+
)
|
|
206
|
+
})}
|
|
207
|
+
</View>
|
|
208
|
+
))}
|
|
209
|
+
|
|
210
|
+
{revealed ? (
|
|
211
|
+
<View style={{ marginTop: 6, maxWidth: 1000 }}>
|
|
212
|
+
<KeyLine>
|
|
213
|
+
{'Decoys: B1, C2, A3, D4. Reporting a line in any of those invalidates the run — ' +
|
|
214
|
+
'move back to the real distance and go again.'}
|
|
215
|
+
</KeyLine>
|
|
216
|
+
<KeyLine>
|
|
217
|
+
{'PASS: every `default` and `strong` cell seen on every ground. `subtle` is allowed ' +
|
|
218
|
+
'to disappear on grey-975 — it is a whisper by design — but if `default` is ' +
|
|
219
|
+
'missing anywhere, that is the failure that has no fallback.'}
|
|
220
|
+
</KeyLine>
|
|
221
|
+
<KeyLine>
|
|
222
|
+
{'Retune lever: raise the alphas in tokens/semantic.ts (`hairline-*`, currently ' +
|
|
223
|
+
'.06 / .09 / .14 white). Raise the whole family together — the ratio between the ' +
|
|
224
|
+
'three is what makes them read as one scale.'}
|
|
225
|
+
</KeyLine>
|
|
226
|
+
</View>
|
|
227
|
+
) : null}
|
|
228
|
+
</View>
|
|
229
|
+
)}
|
|
230
|
+
</Panel>
|
|
231
|
+
),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── C2 · tone steps ─────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
const STAIRCASE: ElevationLevel[] = [0, 1, 2, 3, 4, 5]
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Adjacent-step pairs, with the lighter side pre-assigned to a fixed side and
|
|
240
|
+
* one deliberately IDENTICAL pair seeded in. Without that decoy, "I can see a
|
|
241
|
+
* difference" is a question nobody answers no to.
|
|
242
|
+
*/
|
|
243
|
+
const STEP_PAIRS: { left: ElevationLevel; right: ElevationLevel; label: string }[] = [
|
|
244
|
+
{ left: 1, right: 0, label: '1' },
|
|
245
|
+
{ left: 2, right: 3, label: '2' },
|
|
246
|
+
{ left: 2, right: 2, label: '3' },
|
|
247
|
+
{ left: 3, right: 4, label: '4' },
|
|
248
|
+
{ left: 1, right: 2, label: '5' },
|
|
249
|
+
{ left: 5, right: 4, label: '6' },
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
export const ToneSteps: StoryObj = {
|
|
253
|
+
name: 'C2 · Tone steps — levels 1-3 have nothing else',
|
|
254
|
+
render: () => (
|
|
255
|
+
<Panel
|
|
256
|
+
title="C2 · Tone steps"
|
|
257
|
+
task={
|
|
258
|
+
'TOP: one continuous field with no borders anywhere. Count the vertical edges you can see. ' +
|
|
259
|
+
'BOTTOM: six split fields. For each, say LEFT lighter, RIGHT lighter, or SAME — one pair ' +
|
|
260
|
+
'genuinely is the same. Answer both before revealing.'
|
|
261
|
+
}
|
|
262
|
+
>
|
|
263
|
+
{(revealed) => (
|
|
264
|
+
<View>
|
|
265
|
+
<View style={{ flexDirection: 'row', height: 200, borderRadius: 6, overflow: 'hidden' }}>
|
|
266
|
+
{STAIRCASE.map((lv) => (
|
|
267
|
+
<View
|
|
268
|
+
key={lv}
|
|
269
|
+
style={{ flex: 1, backgroundColor: getElevationSurface(base, lv, 'dark') }}
|
|
270
|
+
/>
|
|
271
|
+
))}
|
|
272
|
+
</View>
|
|
273
|
+
<Text className="text-text-tertiary" style={{ fontSize: 13, marginTop: 8 }}>
|
|
274
|
+
{revealed
|
|
275
|
+
? `6 bands / 5 edges — elevation 0…5, colorAdjustment 0 · .025 · .05 · .075 · .10 · .13`
|
|
276
|
+
: 'Edges visible: ______'}
|
|
277
|
+
</Text>
|
|
278
|
+
|
|
279
|
+
<View style={{ flexDirection: 'row', gap: 14, marginTop: 26 }}>
|
|
280
|
+
{STEP_PAIRS.map((pair) => (
|
|
281
|
+
<View key={pair.label} style={{ flex: 1 }}>
|
|
282
|
+
<View
|
|
283
|
+
style={{ flexDirection: 'row', height: 150, borderRadius: 6, overflow: 'hidden' }}
|
|
284
|
+
>
|
|
285
|
+
<View
|
|
286
|
+
style={{
|
|
287
|
+
flex: 1,
|
|
288
|
+
backgroundColor: getElevationSurface(base, pair.left, 'dark'),
|
|
289
|
+
}}
|
|
290
|
+
/>
|
|
291
|
+
<View
|
|
292
|
+
style={{
|
|
293
|
+
flex: 1,
|
|
294
|
+
backgroundColor: getElevationSurface(base, pair.right, 'dark'),
|
|
295
|
+
}}
|
|
296
|
+
/>
|
|
297
|
+
</View>
|
|
298
|
+
<Text className="text-text-tertiary" style={{ fontSize: 12, marginTop: 5 }}>
|
|
299
|
+
{pair.label}
|
|
300
|
+
{revealed
|
|
301
|
+
? pair.left === pair.right
|
|
302
|
+
? ' · SAME (decoy)'
|
|
303
|
+
: ` · ${pair.left > pair.right ? 'LEFT' : 'RIGHT'} (lv ${pair.left}|${pair.right})`
|
|
304
|
+
: ''}
|
|
305
|
+
</Text>
|
|
306
|
+
</View>
|
|
307
|
+
))}
|
|
308
|
+
</View>
|
|
309
|
+
|
|
310
|
+
{revealed ? (
|
|
311
|
+
<View style={{ marginTop: 14, maxWidth: 1000 }}>
|
|
312
|
+
<KeyLine>
|
|
313
|
+
{'Pair 1 is the one that matters: it is the 0.025 step from level 0 to level 1, the ' +
|
|
314
|
+
'smallest in the ladder and the one carrying the most UI. Pair 3 is the decoy.'}
|
|
315
|
+
</KeyLine>
|
|
316
|
+
<KeyLine>
|
|
317
|
+
{'PASS: all five edges counted on the staircase, pairs 1/2/4/5/6 called correctly, ' +
|
|
318
|
+
'pair 3 called SAME. Missing pair 1 means level 1 is not a level — it is level 0 ' +
|
|
319
|
+
'with extra bookkeeping, and no shadow is coming to save it below level 4.'}
|
|
320
|
+
</KeyLine>
|
|
321
|
+
<KeyLine>
|
|
322
|
+
{'Retune lever: widen `colorAdjustment` on elevation levels 1-3 in elevation.ts. ' +
|
|
323
|
+
'Level 5 is the ceiling the ladder has to fit under, so widening the bottom ' +
|
|
324
|
+
'compresses the top — re-space the whole ladder rather than nudging one entry.'}
|
|
325
|
+
</KeyLine>
|
|
326
|
+
</View>
|
|
327
|
+
) : null}
|
|
328
|
+
</View>
|
|
329
|
+
)}
|
|
330
|
+
</Panel>
|
|
331
|
+
),
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── C3 · paper material ─────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
const PAPER_TONE = greyRamp[875]
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* REWRITTEN FOR RUN 2. Run 1 asked "is the tonal fill visible?" and the answer
|
|
340
|
+
* was no, so the fill was deleted — which left this panel with nothing to
|
|
341
|
+
* compare. The question that survives the deletion is broader and more useful:
|
|
342
|
+
* with the fill gone, does what REMAINS of the paper material read at all?
|
|
343
|
+
*
|
|
344
|
+
* `paperSheet` is now grain + dither + a top rim-light + a contact shadow. If
|
|
345
|
+
* that stack cannot be told from a flat `backgroundColor` at 3 m, it is three
|
|
346
|
+
* paint layers buying nothing and it should follow the fill out.
|
|
347
|
+
*
|
|
348
|
+
* The forced choice is odd-one-out with a genuine identical pair, rather than
|
|
349
|
+
* "can you see it" — the operator has to say WHICH and HOW, and "they are all
|
|
350
|
+
* the same" is an available, unembarrassing answer. That matters here more than
|
|
351
|
+
* on C1: grain lifts apparent lightness, so a two-way comparison can be won on
|
|
352
|
+
* a brightness cue without the material ever reading as a material.
|
|
353
|
+
*/
|
|
354
|
+
const PAPER_SHEETS = ['flat', 'paper', 'flat'] as const
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* ADDED IN RUN 2. `insetWell` is the other material TD-07.16 shipped, and it has
|
|
358
|
+
* never been on a panel. Run 2 flagged `paperSheet`'s rim-light as weak at .10 —
|
|
359
|
+
* and the well's floor light, which its own docstring calls "the load-bearing
|
|
360
|
+
* half", is at .04. That is below every hairline value the same run rejected as
|
|
361
|
+
* too faint, so it is very likely inert on the wall and nobody has looked.
|
|
362
|
+
*
|
|
363
|
+
* Forced choice rather than "can you see it": the SHIPPED value, a candidate,
|
|
364
|
+
* and a well with no floor line at all. Reporting a line on the decoy invalidates
|
|
365
|
+
* the row. Answering it here costs one glance and saves a whole extra sitting.
|
|
366
|
+
*/
|
|
367
|
+
const WELL_TONE = greyRamp[950]
|
|
368
|
+
const WELL_FLOORS: { label: string; floorAlpha: number | null; answer: string }[] = [
|
|
369
|
+
{ label: '1', floorAlpha: 0.12, answer: 'CANDIDATE — floor light raised to .12' },
|
|
370
|
+
{ label: '2', floorAlpha: null, answer: 'NONE (decoy) — inner top shadow only, no floor line' },
|
|
371
|
+
{ label: '3', floorAlpha: 0.04, answer: 'SHIPPED — floor light at .04' },
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
export const PaperMaterial: StoryObj = {
|
|
375
|
+
name: 'C3 · Paper material — what is left after the fill',
|
|
376
|
+
render: () => (
|
|
377
|
+
<Panel
|
|
378
|
+
title="C3 · Paper material"
|
|
379
|
+
task={
|
|
380
|
+
'TOP: three sheets on the same tone. Two are identical. Which one differs, and in what way ' +
|
|
381
|
+
'— lighter, textured, edged? "All three the same" is a real answer. BOTTOM: the same ' +
|
|
382
|
+
'material at hero size. Does it read as a sheet with light on it, as visible noise, or as ' +
|
|
383
|
+
'nothing at all?'
|
|
384
|
+
}
|
|
385
|
+
>
|
|
386
|
+
{(revealed) => (
|
|
387
|
+
<View>
|
|
388
|
+
<View style={{ flexDirection: 'row', gap: 22 }}>
|
|
389
|
+
{PAPER_SHEETS.map((kind, i) => (
|
|
390
|
+
<Card
|
|
391
|
+
key={i}
|
|
392
|
+
label={String(i + 1)}
|
|
393
|
+
revealed={revealed}
|
|
394
|
+
answer={
|
|
395
|
+
kind === 'paper'
|
|
396
|
+
? 'paperSheet() — grain + dither + rim-light + contact shadow'
|
|
397
|
+
: 'FLAT (decoy) — backgroundColor alone'
|
|
398
|
+
}
|
|
399
|
+
style={
|
|
400
|
+
kind === 'paper'
|
|
401
|
+
? { ...(paperSheet(PAPER_TONE) as Record<string, unknown>) }
|
|
402
|
+
: { backgroundColor: PAPER_TONE }
|
|
403
|
+
}
|
|
404
|
+
/>
|
|
405
|
+
))}
|
|
406
|
+
</View>
|
|
407
|
+
|
|
408
|
+
<Text
|
|
409
|
+
className="text-text-secondary"
|
|
410
|
+
style={{ fontSize: 15, marginTop: 26, marginBottom: 10 }}
|
|
411
|
+
>
|
|
412
|
+
The same material, at hero size — lit sheet, visible noise, or nothing?
|
|
413
|
+
</Text>
|
|
414
|
+
<View
|
|
415
|
+
style={{
|
|
416
|
+
height: 260,
|
|
417
|
+
borderRadius: 12,
|
|
418
|
+
...(paperSheet(PAPER_TONE) as Record<string, unknown>),
|
|
419
|
+
}}
|
|
420
|
+
/>
|
|
421
|
+
<Text className="text-text-tertiary" style={{ fontSize: 12, marginTop: 8 }}>
|
|
422
|
+
{revealed ? 'paperSheet() at hero width' : 'reads as: ______'}
|
|
423
|
+
</Text>
|
|
424
|
+
|
|
425
|
+
<Text
|
|
426
|
+
className="text-text-secondary"
|
|
427
|
+
style={{ fontSize: 15, marginTop: 26, marginBottom: 10 }}
|
|
428
|
+
>
|
|
429
|
+
Inset wells — which of these have a light line along the FLOOR (bottom inner edge)?
|
|
430
|
+
</Text>
|
|
431
|
+
<View style={{ flexDirection: 'row', gap: 22 }}>
|
|
432
|
+
{WELL_FLOORS.map((w) => (
|
|
433
|
+
<View key={w.label} style={{ flex: 1 }}>
|
|
434
|
+
<View
|
|
435
|
+
style={
|
|
436
|
+
{
|
|
437
|
+
height: 120,
|
|
438
|
+
borderRadius: 10,
|
|
439
|
+
backgroundColor: WELL_TONE,
|
|
440
|
+
boxShadow: w.floorAlpha
|
|
441
|
+
? `inset 0 2px 6px rgba(0,0,0,0.55), inset 0 -1px 0 rgba(255,255,255,${w.floorAlpha})`
|
|
442
|
+
: 'inset 0 2px 6px rgba(0,0,0,0.55)',
|
|
443
|
+
} as Record<string, unknown>
|
|
444
|
+
}
|
|
445
|
+
/>
|
|
446
|
+
<Text className="text-text-tertiary" style={{ fontSize: 12, marginTop: 6 }}>
|
|
447
|
+
{w.label}
|
|
448
|
+
{revealed ? ` · ${w.answer}` : ' · floor line? ______'}
|
|
449
|
+
</Text>
|
|
450
|
+
</View>
|
|
451
|
+
))}
|
|
452
|
+
</View>
|
|
453
|
+
|
|
454
|
+
{revealed ? (
|
|
455
|
+
<View style={{ marginTop: 14, maxWidth: 1000 }}>
|
|
456
|
+
<KeyLine>
|
|
457
|
+
{'Sheet 2 is the material; 1 and 3 are the identical decoy pair. PASS: 2 picked out, ' +
|
|
458
|
+
'AND the hero sheet described as a surface rather than as noise. Invisible and ' +
|
|
459
|
+
'obvious are both failures — the same two-sided bar the fill was held to.'}
|
|
460
|
+
</KeyLine>
|
|
461
|
+
<KeyLine>
|
|
462
|
+
{'Levers. Reads as NOTHING: the honest move is to drop grain and the dither from ' +
|
|
463
|
+
'paperSheet and keep only the rim-light and contact shadow, which are edge cues ' +
|
|
464
|
+
'and sit at a spatial frequency the wall demonstrably resolves — that is the ' +
|
|
465
|
+
'lesson C2 taught and the fill failed. Reads as NOISE: lower the grain opacity in ' +
|
|
466
|
+
'grainForTone (materials.ts) before touching its frequency.'}
|
|
467
|
+
</KeyLine>
|
|
468
|
+
<KeyLine>
|
|
469
|
+
{'If 1 and 3 were reported as different from each other, the run is invalid at this ' +
|
|
470
|
+
'distance — they are byte-identical. Step back to 3 m and go again.'}
|
|
471
|
+
</KeyLine>
|
|
472
|
+
</View>
|
|
473
|
+
) : null}
|
|
474
|
+
</View>
|
|
475
|
+
)}
|
|
476
|
+
</Panel>
|
|
477
|
+
),
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function Card({
|
|
481
|
+
label,
|
|
482
|
+
answer,
|
|
483
|
+
revealed,
|
|
484
|
+
style,
|
|
485
|
+
}: {
|
|
486
|
+
label: string
|
|
487
|
+
answer: string
|
|
488
|
+
revealed: boolean
|
|
489
|
+
style: Record<string, unknown>
|
|
490
|
+
}) {
|
|
491
|
+
return (
|
|
492
|
+
<View style={{ flex: 1 }}>
|
|
493
|
+
<View style={{ height: 300, borderRadius: 12, ...style }} />
|
|
494
|
+
<Text className="text-text-tertiary" style={{ fontSize: 12, marginTop: 8, lineHeight: 17 }}>
|
|
495
|
+
{label}
|
|
496
|
+
{revealed ? ` · ${answer}` : ''}
|
|
497
|
+
</Text>
|
|
498
|
+
</View>
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ── C4 · banding ────────────────────────────────────────────────────────────
|
|
503
|
+
|
|
504
|
+
const BAND_TONE = greyRamp[900]
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* REWRITTEN FOR RUN 2, and this panel is why the rewrite could not be skipped.
|
|
508
|
+
*
|
|
509
|
+
* Run 1 compared dither-on against dither-off over the tonal fill. Neither field
|
|
510
|
+
* banded — but the fill was already established as invisible, so there was no
|
|
511
|
+
* gradient to band and the panel could not have failed. That is a VOID result,
|
|
512
|
+
* not a pass, and it is the exact failure mode a calibration rig exists to catch
|
|
513
|
+
* in itself.
|
|
514
|
+
*
|
|
515
|
+
* Two changes:
|
|
516
|
+
*
|
|
517
|
+
* 1. Both material fields are now the SHIPPED stack ± dither, derived from
|
|
518
|
+
* `paperSheet` rather than reassembled by hand, so they cannot drift from
|
|
519
|
+
* what actually ships.
|
|
520
|
+
* 2. A POSITIVE CONTROL is seeded: a bare low-contrast vertical gradient with no
|
|
521
|
+
* dither and no grain, which on an 8-bit panel must band. If it does not, the
|
|
522
|
+
* panel cannot detect banding under these viewing conditions and the row is
|
|
523
|
+
* VOID AGAIN — now with evidence, instead of being mistaken for a pass.
|
|
524
|
+
*
|
|
525
|
+
* The control cannot be disguised as one of the material fields, and does not
|
|
526
|
+
* need to be: it is not part of a forced choice. Which field it is stays hidden
|
|
527
|
+
* with the rest of the key, so the operator still reports what they see rather
|
|
528
|
+
* than what they expect.
|
|
529
|
+
*/
|
|
530
|
+
const SHIPPED_PAPER = paperSheet(BAND_TONE) as Record<string, unknown>
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* ADDED IN RUN 2, and it is the whole reason this panel is worth re-running.
|
|
534
|
+
*
|
|
535
|
+
* Run 2 reported NO banding anywhere — including on the control that was
|
|
536
|
+
* supposed to guarantee it. By the sheet that is another VOID, but "the control
|
|
537
|
+
* did not fire" has two very different causes and they demand opposite actions:
|
|
538
|
+
*
|
|
539
|
+
* a. the panel/viewing setup cannot resolve banding -> VOID, fix the setup
|
|
540
|
+
* b. the render path genuinely does not band here -> ditherTile is dead
|
|
541
|
+
* (10-bit panel, or the browser dithers gradients itself)
|
|
542
|
+
*
|
|
543
|
+
* A smooth control cannot tell those apart, because both produce "no steps".
|
|
544
|
+
* So: the same tone span, quantised BY HAND into five hard-edged stripes. This
|
|
545
|
+
* is banding, drawn deliberately. C2 already proved hard tone edges at this
|
|
546
|
+
* spacing are visible on the wall, so if these stripes read and the smooth
|
|
547
|
+
* gradient beside them does not, cause (b) is established — the pipeline is not
|
|
548
|
+
* producing the artefact, and the mitigation for it is unearned.
|
|
549
|
+
*/
|
|
550
|
+
const BAND_STEPS = (() => {
|
|
551
|
+
const from = [0x2c, 0x2a, 0x28] // grey-900
|
|
552
|
+
const to = [0x31, 0x30, 0x2f] // grey-875
|
|
553
|
+
return Array.from({ length: 5 }, (_, i) => {
|
|
554
|
+
const t = i / 4
|
|
555
|
+
const ch = from.map((f, k) => Math.round(f + (to[k] - f) * t))
|
|
556
|
+
return `#${ch.map((v) => v.toString(16).padStart(2, '0')).join('')}`
|
|
557
|
+
})
|
|
558
|
+
})()
|
|
559
|
+
|
|
560
|
+
const BAND_FIELDS: { key: string; answer: string; style: Record<string, unknown> }[] = [
|
|
561
|
+
{
|
|
562
|
+
key: '1',
|
|
563
|
+
answer: 'the SHIPPED stack — dither over grain',
|
|
564
|
+
style: SHIPPED_PAPER,
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
key: '2',
|
|
568
|
+
answer: 'CONTROL — a bare grey-900 → grey-875 gradient, no dither, no grain',
|
|
569
|
+
style: {
|
|
570
|
+
backgroundColor: BAND_TONE,
|
|
571
|
+
backgroundImage: `linear-gradient(180deg, ${greyRamp[900]} 0%, ${greyRamp[875]} 100%)`,
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
key: '2b',
|
|
576
|
+
answer:
|
|
577
|
+
'REFERENCE — hard-edged stripes, grey-900 → grey-875 in 5 steps. This is what banding LOOKS like.',
|
|
578
|
+
style: {
|
|
579
|
+
backgroundImage: `linear-gradient(180deg, ${BAND_STEPS.map(
|
|
580
|
+
(hex, i) =>
|
|
581
|
+
`${hex} ${(i / BAND_STEPS.length) * 100}%, ${hex} ${((i + 1) / BAND_STEPS.length) * 100}%`,
|
|
582
|
+
).join(', ')})`,
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
key: '3',
|
|
587
|
+
answer: 'the shipped stack MINUS the dither — grain alone',
|
|
588
|
+
style: { ...SHIPPED_PAPER, backgroundImage: grainForTone(BAND_TONE) },
|
|
589
|
+
},
|
|
590
|
+
]
|
|
591
|
+
|
|
592
|
+
export const Banding: StoryObj = {
|
|
593
|
+
name: 'C4 · Banding — does the dither earn its place',
|
|
594
|
+
render: () => (
|
|
595
|
+
<Panel
|
|
596
|
+
title="C4 · Banding"
|
|
597
|
+
task={
|
|
598
|
+
'Four wide fields. Look for horizontal STEPS — flat stripes with a visible edge between ' +
|
|
599
|
+
'them, rather than a smooth fall. Say BANDS or SMOOTH for each. This is the one check that ' +
|
|
600
|
+
'cannot be done anywhere but on the panel: banding is an 8-bit artefact of this display.'
|
|
601
|
+
}
|
|
602
|
+
>
|
|
603
|
+
{(revealed) => (
|
|
604
|
+
<View>
|
|
605
|
+
{BAND_FIELDS.map((field) => (
|
|
606
|
+
<View key={field.key}>
|
|
607
|
+
<View
|
|
608
|
+
style={
|
|
609
|
+
{
|
|
610
|
+
height: 240,
|
|
611
|
+
borderRadius: 8,
|
|
612
|
+
marginTop: field.key === '1' ? 0 : 20,
|
|
613
|
+
...field.style,
|
|
614
|
+
} as Record<string, unknown>
|
|
615
|
+
}
|
|
616
|
+
/>
|
|
617
|
+
<Text className="text-text-tertiary" style={{ fontSize: 12, marginTop: 6 }}>
|
|
618
|
+
{`Field ${field.key}`}
|
|
619
|
+
{revealed ? ` · ${field.answer}` : ' · bands? ______'}
|
|
620
|
+
</Text>
|
|
621
|
+
</View>
|
|
622
|
+
))}
|
|
623
|
+
|
|
624
|
+
{revealed ? (
|
|
625
|
+
<View style={{ marginTop: 14, maxWidth: 1000 }}>
|
|
626
|
+
<KeyLine>
|
|
627
|
+
{'READ FIELDS 2 AND 2b FIRST — they decide whether the rest of the panel means ' +
|
|
628
|
+
'anything. 2b is banding drawn by hand: the same tone span quantised into five ' +
|
|
629
|
+
'hard-edged 1-level steps, which is exactly what the artefact looks like when it ' +
|
|
630
|
+
'occurs. Sanity-check the setup (brightness, no browser zoom) before calling it.'}
|
|
631
|
+
</KeyLine>
|
|
632
|
+
<KeyLine>
|
|
633
|
+
{'2b reads, 2 (smooth) does not -> this render path does not band: 10-bit panel, or ' +
|
|
634
|
+
'the browser dithers gradients itself. 2b ALSO invisible -> the artefact is below ' +
|
|
635
|
+
'threshold here at this tone span. Either way ditherTile buys nothing on this ' +
|
|
636
|
+
'display and comes out of paperSheet — a real result, not a null one. The one ' +
|
|
637
|
+
'caveat is that it is a claim about THIS panel; the same recipe on a phone is a ' +
|
|
638
|
+
'separate question, and paperSheet is hero-surface-only for that reason.'}
|
|
639
|
+
</KeyLine>
|
|
640
|
+
<KeyLine>
|
|
641
|
+
{'If 2 DOES band: compare 1 (shipped) against 3 (no dither). 3 banding and 1 not is ' +
|
|
642
|
+
'the dither earning its place — keep it.'}
|
|
643
|
+
</KeyLine>
|
|
644
|
+
<KeyLine>
|
|
645
|
+
{'If field 1 bands too, raise the 0.02α in ditherTile (materials.ts) until it stops; ' +
|
|
646
|
+
'past ~0.03 it stops being invisible and becomes texture, at which point it is ' +
|
|
647
|
+
'grain, not dither.'}
|
|
648
|
+
</KeyLine>
|
|
649
|
+
</View>
|
|
650
|
+
) : null}
|
|
651
|
+
</View>
|
|
652
|
+
)}
|
|
653
|
+
</Panel>
|
|
654
|
+
),
|
|
655
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Registers titan's semantic color tokens as runtime CSS variables via
|
|
4
4
|
// nativewind's `vars()` so that className color tokens (`text-text-secondary`,
|
|
5
|
-
// `bg-surface-raised`, `border-
|
|
5
|
+
// `bg-surface-raised`, `border-hairline`, …) resolve on NATIVE React Native — the
|
|
6
6
|
// native equivalent of the `:root {}` block `global.css` provides on web.
|
|
7
7
|
//
|
|
8
8
|
// Why this exists: titan's Workout organisms reference semantic colors as CSS
|