@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.
- 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 +416 -18
- package/dist/index.d.ts +416 -18
- package/dist/index.js +2512 -1307
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2480 -1299
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
- package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1644 -1575
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1645 -1576
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +409 -415
- package/dist/theme/index.d.ts +409 -415
- 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/package.json +8 -3
- 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 +258 -0
- package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
- package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
- package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
- package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
- package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
- package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
- package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
- package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
- package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
- package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
- package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
- package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
- package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
- package/src/components/custom/Fatigue/README.md +93 -0
- package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
- package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
- package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
- package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
- package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
- package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
- package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
- package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
- package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
- package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
- package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
- package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
- package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
- package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
- package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
- package/src/components/custom/Fatigue/index.ts +41 -0
- package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
- package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
- 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/DualVelocityStrip.test.tsx +685 -0
- package/src/components/custom/Workout/ExerciseCard.test.tsx +7 -7
- package/src/components/custom/Workout/ExerciseCard.tsx +5 -3
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +11 -5
- 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 +62 -16
- 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/SegmentedBar.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.test.tsx +43 -0
- package/src/components/custom/Workout/SessionRail.tsx +63 -18
- package/src/components/custom/Workout/SetBar.tsx +3 -3
- package/src/components/custom/Workout/SetRow.test.tsx +12 -9
- package/src/components/custom/Workout/SetRow.tsx +9 -2
- 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.compact.stories.tsx +191 -0
- package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
- package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
- package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
- package/src/components/custom/Workout/VelocityStrip.test.tsx +199 -79
- package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
- 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/index.ts +3 -0
- package/src/components/custom/Workout/setHeadingKit.tsx +14 -8
- package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
- package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
- package/src/components/custom/charts/SetBarChart.tsx +651 -0
- package/src/components/custom/charts/live-rep-growth.ts +91 -0
- package/src/components/custom/index.ts +1 -0
- 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/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 +57 -44
- 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/test/raw-color-patterns.test.ts +71 -0
- package/src/theme/ColorPalettes.stories.tsx +874 -0
- package/src/theme/ColorPrimitives.stories.tsx +348 -0
- 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 +125 -0
- package/src/theme/color-story-kit.tsx +170 -0
- 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/tokens/primitives.ts +62 -115
- package/src/theme/tokens/semantic.ts +130 -82
- package/src/utils/colors.ts +1 -1
- package/tailwind.config.js +2 -5
- package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
- package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
- package/src/theme/ColorSystem.stories.tsx +0 -508
- package/src/theme/Colors.stories.tsx +0 -323
- package/src/theme/shadows.stories.tsx +0 -523
- package/src/theme/shadows.ts +0 -439
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface MATERIALS — the treatments layered on top of the flat grey ramp.
|
|
3
|
+
*
|
|
4
|
+
* The ramp says how deep a plane sits. This says what it is made of. Two
|
|
5
|
+
* materials, deliberately: a matte PAPER sheet for hero surfaces, and a recessed
|
|
6
|
+
* INSET WELL for grouped or awaiting-data regions.
|
|
7
|
+
*
|
|
8
|
+
* SCOPE, and it is a hard rule rather than a preference: paper goes on hero
|
|
9
|
+
* surfaces only — the live card, the LOAD/verdict chip, the rest hero. The
|
|
10
|
+
* "everything is a paper card" look is explicitly NOT the model; it was an
|
|
11
|
+
* over-application built to find the ceiling. There are no LEVELS of paper. One
|
|
12
|
+
* treatment, used sparingly, or the texture stops meaning anything.
|
|
13
|
+
*
|
|
14
|
+
* Second hard rule: paper fills take DEEP/MUTED tones only (a ramp step, or a
|
|
15
|
+
* categorical `dark` variant). Vivid and neon colours are ink ON paper — never
|
|
16
|
+
* the paper itself.
|
|
17
|
+
*
|
|
18
|
+
* PLATFORM: `backgroundImage` and multi-layer `boxShadow` are web/RNW only. On
|
|
19
|
+
* native these are ignored and the surface falls back to its flat fill, which is
|
|
20
|
+
* why every material here keeps `backgroundColor` load-bearing on its own. A
|
|
21
|
+
* material must never be the only thing carrying meaning.
|
|
22
|
+
*
|
|
23
|
+
* ── THE TONAL FILL IS GONE (VW-99, S-6 wall calibration) ───────────────────
|
|
24
|
+
*
|
|
25
|
+
* `paperSheet` used to carry a vertical ΔL* 3 gradient, sized "just under seen
|
|
26
|
+
* as a gradient". On the wall panel at ~3 m it could not be told from a flat
|
|
27
|
+
* sheet of the same tone, and the full material still read as — the operator's
|
|
28
|
+
* words — "a flat digital rectangle". It failed its own stated purpose, which
|
|
29
|
+
* was precisely to stop that.
|
|
30
|
+
*
|
|
31
|
+
* Raising it was not available. The design ceiling is ~ΔL* 4 (past that it reads
|
|
32
|
+
* decorative, which the exploration rejected), and the same run showed why the
|
|
33
|
+
* headroom would not have helped: the 0.025 ELEVATION step — a comparable ΔL*
|
|
34
|
+
* — was called instantly, because it sits at a hard edge between two flat
|
|
35
|
+
* fields. The fill spread the same amount smoothly over ~300 px, where vision is
|
|
36
|
+
* genuinely poor. It is a spatial-frequency problem, not an amplitude one, so
|
|
37
|
+
* the band between "invisible" and "decorative" may simply not exist here.
|
|
38
|
+
*
|
|
39
|
+
* ── AND THE DITHER WENT WITH IT (VW-99 run 2) ──────────────────────────────
|
|
40
|
+
*
|
|
41
|
+
* `ditherTile` was held back one run on the grounds that "probably dead weight"
|
|
42
|
+
* is not a measurement. Run 1 could not measure it: with the fill invisible
|
|
43
|
+
* there was no gradient to band, so the panel returned a guaranteed clean sheet.
|
|
44
|
+
*
|
|
45
|
+
* Run 2 settled it by adding a hand-quantised REFERENCE beside the smooth
|
|
46
|
+
* control — the same tone span drawn as five hard-edged 1-level steps, which is
|
|
47
|
+
* what the artefact looks like when it happens. The reference banded plainly and
|
|
48
|
+
* the smooth gradient did not. So the panel can resolve banding; this render
|
|
49
|
+
* path simply does not produce it. A mitigation for an artefact that does not
|
|
50
|
+
* occur is cost with no benefit, and it is gone.
|
|
51
|
+
*
|
|
52
|
+
* That is a claim about THIS display, which is the one the product ships on and
|
|
53
|
+
* the reason `paperSheet` is hero-surface-only. A phone is a separate question.
|
|
54
|
+
*
|
|
55
|
+
* Kept: grain, rim-light, contact shadow.
|
|
56
|
+
*
|
|
57
|
+
* See coordination/design-explorations/surface-system-north-star.md §4 and
|
|
58
|
+
* coordination/validation-runbooks/VW-99-depth-wall-calibration.md.
|
|
59
|
+
*/
|
|
60
|
+
import type { ViewStyle } from 'react-native'
|
|
61
|
+
import { getSemanticColors } from './tokens/semantic'
|
|
62
|
+
|
|
63
|
+
const c = getSemanticColors('dark')
|
|
64
|
+
|
|
65
|
+
// ── colour math ─────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function parseHex(hex: string): [number, number, number] | null {
|
|
68
|
+
const h = hex.replace('#', '')
|
|
69
|
+
const full = h.length === 3 ? h.split('').map((ch) => ch + ch).join('') : h
|
|
70
|
+
const rgb: [number, number, number] = [
|
|
71
|
+
parseInt(full.slice(0, 2), 16),
|
|
72
|
+
parseInt(full.slice(2, 4), 16),
|
|
73
|
+
parseInt(full.slice(4, 6), 16),
|
|
74
|
+
]
|
|
75
|
+
return rgb.some(Number.isNaN) ? null : rgb
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** sRGB perceived luminance (0 dark … 1 bright), Rec.601 weights. */
|
|
79
|
+
function perceivedLuminance(hex: string): number {
|
|
80
|
+
const rgb = parseHex(hex)
|
|
81
|
+
if (!rgb) return 0.5
|
|
82
|
+
const [r, g, b] = rgb
|
|
83
|
+
return (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The CIELAB conversion chain (srgb2lin/lin2srgb/fLab/fInv, hexToLab/labToHex,
|
|
87
|
+
// shiftL) lived here so the tonal fill could be specified in ΔL*. It had no
|
|
88
|
+
// other caller, so it went with the fill — see the module header.
|
|
89
|
+
|
|
90
|
+
// ── grain ───────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Grain opacity for a tone, scaled by perceived brightness.
|
|
94
|
+
*
|
|
95
|
+
* A fixed opacity reads hot on a dark plane and vanishes on a bright one, so
|
|
96
|
+
* darker tones get a whisper and brighter tones keep full texture. Anchored so a
|
|
97
|
+
* dark chip (lum ≈ 0.13) lands ≈ 0.06 and a bright bar (lum ≈ 0.58) lands ≈ 0.20.
|
|
98
|
+
* Above roughly 0.05α on a mid tone it starts reading as "dirty" rather than
|
|
99
|
+
* matte, which is what the clamp is protecting.
|
|
100
|
+
*
|
|
101
|
+
* This is the ONE curve all grain goes through — a card and a bar carry
|
|
102
|
+
* proportional, not identical, texture. That is what makes them read as the same
|
|
103
|
+
* material.
|
|
104
|
+
*/
|
|
105
|
+
export function grainOpacityForTone(baseColor: string): number {
|
|
106
|
+
return Math.min(0.24, Math.max(0.04, 0.02 + 0.31 * perceivedLuminance(baseColor)))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The matte grain tile as a `backgroundImage` url — one shared `feTurbulence`
|
|
111
|
+
* fractal-noise tile, painted once, NEVER animated. Pair with
|
|
112
|
+
* `backgroundColor: baseColor`.
|
|
113
|
+
*/
|
|
114
|
+
export function grainForTone(baseColor: string): string {
|
|
115
|
+
const op = grainOpacityForTone(baseColor).toFixed(3)
|
|
116
|
+
return (
|
|
117
|
+
`url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E` +
|
|
118
|
+
`%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E` +
|
|
119
|
+
`%3Crect width='120' height='120' filter='url(%23n)' opacity='${op}'/%3E%3C/svg%3E")`
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// `tonalFill` and `ditherTile` both lived here until VW-99 measured them on the
|
|
124
|
+
// wall. Deleted rather than left exported-but-unused; the reasoning is in the
|
|
125
|
+
// module header, which is the part worth keeping.
|
|
126
|
+
|
|
127
|
+
// ── the materials ───────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A matte PAPER hero sheet: brightness-scaled grain + a crisp top rim-light +
|
|
131
|
+
* one soft contact shadow.
|
|
132
|
+
*
|
|
133
|
+
* The contact shadow is the ONE place a drop-shadow is still correct on a dark
|
|
134
|
+
* surface: it is large, soft, and describes a sheet lying on a plane rather than
|
|
135
|
+
* trying to encode hierarchy. Inline depth is the hairline's job.
|
|
136
|
+
*
|
|
137
|
+
* Defaults to `surface-raised` so a same-toned card laid inside reads as one
|
|
138
|
+
* continuous sheet rather than a seam.
|
|
139
|
+
*
|
|
140
|
+
* THE RIM-LIGHT IS A HAIRLINE. Run 2 of VW-99 called it "weak at distance" on
|
|
141
|
+
* both the card and the hero sheet, and the reason is visible in the number: it
|
|
142
|
+
* was `rgba(255,255,255,0.10)`, a 1px white line on a dark plane — physically
|
|
143
|
+
* the same cue as `hairline-*`, but set BELOW `hairline-default`, which the same
|
|
144
|
+
* run had just had to raise twice. It is not a subtler species of edge, it was
|
|
145
|
+
* simply mistuned. Raised to `0.20`, between `hairline-default` and
|
|
146
|
+
* `hairline-strong`: this edge defines a hero surface, so it earns more than the
|
|
147
|
+
* default separator, and unlike a separator there is no fallback behind it.
|
|
148
|
+
*
|
|
149
|
+
* Deliberately NOT wired to the token, though the values now agree in spirit: a
|
|
150
|
+
* box-shadow rim and a border hairline are retuned by different evidence, and
|
|
151
|
+
* coupling them means a hairline run silently restyles every hero sheet.
|
|
152
|
+
*
|
|
153
|
+
* HERO SURFACES ONLY, MUTED TONES ONLY — see the module header.
|
|
154
|
+
*/
|
|
155
|
+
export function paperSheet(tone: string = c['surface-raised']): ViewStyle {
|
|
156
|
+
return {
|
|
157
|
+
backgroundColor: tone,
|
|
158
|
+
// Grain alone. The dither went with the gradient it protected — see header.
|
|
159
|
+
backgroundImage: grainForTone(tone),
|
|
160
|
+
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.20), 0 8px 22px rgba(0,0,0,0.50)',
|
|
161
|
+
} as unknown as ViewStyle
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A recessed INSET WELL: a tone pressed BELOW the surrounding plane, cut in by an
|
|
166
|
+
* inner top shadow with a faint light line on the floor.
|
|
167
|
+
*
|
|
168
|
+
* The bottom rim is the load-bearing half. A dark inset alone just reads as a
|
|
169
|
+
* darker rectangle; it is the light catching the floor that says "this is below
|
|
170
|
+
* the surface" — which is why the treatment survives at 16px where a full paper
|
|
171
|
+
* treatment does not.
|
|
172
|
+
*
|
|
173
|
+
* The floor light was `0.04` and is now `0.12`, chosen on the wall in VW-99 run
|
|
174
|
+
* 2 against a `.04`-vs-`.12`-vs-none forced choice. Worth recording that `.04`
|
|
175
|
+
* was NOT invisible — the no-line decoy was correctly rejected and both real
|
|
176
|
+
* values were seen, so this is a preference between two working values, not a
|
|
177
|
+
* rescue. The rim-light next door was a genuine miss; this one was only thin.
|
|
178
|
+
*
|
|
179
|
+
* Note this is a MATERIAL, and independent of the surface LEVEL named `inset`
|
|
180
|
+
* that was retired in TD-07.14. A well is something you apply; a level is
|
|
181
|
+
* somewhere you sit.
|
|
182
|
+
*/
|
|
183
|
+
export function insetWell(tone: string = c['surface-input']): ViewStyle {
|
|
184
|
+
return {
|
|
185
|
+
backgroundColor: tone,
|
|
186
|
+
boxShadow: 'inset 0 2px 6px rgba(0,0,0,0.55), inset 0 -1px 0 rgba(255,255,255,0.12)',
|
|
187
|
+
} as unknown as ViewStyle
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Paper for a COLORED BAR — the same material as {@link paperSheet}, tuned for a
|
|
192
|
+
* small saturated element: grain + top rim-light + a contact shadow cast past the
|
|
193
|
+
* bar's leading edge, so a filled bar reads as a physical block that has travelled.
|
|
194
|
+
*
|
|
195
|
+
* No tonal fill or dither here: a bar is too narrow for a ΔL* 3 gradient to read,
|
|
196
|
+
* and too narrow to band.
|
|
197
|
+
*
|
|
198
|
+
* `flip` = a vertically mirrored (`scaleY(-1)`) plot, i.e. the `down` wing of a
|
|
199
|
+
* diverging chart. The contact shadow's y-offset is pre-inverted so the mirror
|
|
200
|
+
* lands it pointing AWAY from the shared axis rather than up across it, and the
|
|
201
|
+
* top rim-light is DROPPED — mirrored it would sit on the axis edge and read
|
|
202
|
+
* wrong, so grain and shadow carry the material alone.
|
|
203
|
+
*/
|
|
204
|
+
export function barPaper(color: string, flip = false): ViewStyle {
|
|
205
|
+
return {
|
|
206
|
+
backgroundImage: grainForTone(color),
|
|
207
|
+
boxShadow: flip
|
|
208
|
+
? '0 -6px 16px rgba(0,0,0,0.45)'
|
|
209
|
+
: 'inset 0 1.5px 0 rgba(255,255,255,0.22), 0 6px 16px rgba(0,0,0,0.45)',
|
|
210
|
+
} as unknown as ViewStyle
|
|
211
|
+
}
|
|
@@ -49,7 +49,7 @@ export const audiobookPreset: ThemePreset = {
|
|
|
49
49
|
'text-secondary': '#b5b3af',
|
|
50
50
|
'text-tertiary': '#7a7875',
|
|
51
51
|
|
|
52
|
-
// Surfaces: warm
|
|
52
|
+
// Surfaces: warm greys (not pure gray)
|
|
53
53
|
'surface-base': '#101114',
|
|
54
54
|
'surface-elevated': '#15171c',
|
|
55
55
|
'surface-raised': '#1a1d24',
|
|
@@ -62,9 +62,9 @@ export const audiobookPreset: ThemePreset = {
|
|
|
62
62
|
'background-subtle': '#1a1d24',
|
|
63
63
|
|
|
64
64
|
// Borders
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
65
|
+
'hairline-default': 'rgba(255, 255, 255, 0.06)',
|
|
66
|
+
'hairline-subtle': 'rgba(255, 255, 255, 0.03)',
|
|
67
|
+
'hairline-strong': '#2d323d',
|
|
68
68
|
'border-focus': '#d4782a',
|
|
69
69
|
'border-input': '#2d323d',
|
|
70
70
|
'border-input-hover': '#454c5c',
|
|
@@ -28,9 +28,9 @@ export interface ThemePresetColors {
|
|
|
28
28
|
'background-base'?: string
|
|
29
29
|
'background-default'?: string
|
|
30
30
|
'background-subtle'?: string
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
31
|
+
'hairline-default'?: string
|
|
32
|
+
'hairline-subtle'?: string
|
|
33
|
+
'hairline-strong'?: string
|
|
34
34
|
'border-focus'?: string
|
|
35
35
|
'divider'?: string
|
|
36
36
|
[key: `${string}`]: string | undefined
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TD-07.15 — on-surface text contrast, as a gate rather than a document.
|
|
3
|
+
*
|
|
4
|
+
* The warm grey migration was validated for ΔE, L* and warmth long before it
|
|
5
|
+
* was validated for CONTRAST, and the contrast check is the one that found a
|
|
6
|
+
* real defect: `text-tertiary` was failing WCAG large-text on three of the six
|
|
7
|
+
* dark planes and had been for as long as the surface ramp had existed. Nobody
|
|
8
|
+
* had run the numbers because the greys "looked fine".
|
|
9
|
+
*
|
|
10
|
+
* So the numbers run in CI now. If a text token is retuned into a failure, this
|
|
11
|
+
* fails first.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect } from 'vitest'
|
|
14
|
+
import { greyRamp } from './tokens/primitives'
|
|
15
|
+
import { semanticColorsDark } from './tokens/semantic'
|
|
16
|
+
|
|
17
|
+
const hex2rgb = (h: string): [number, number, number] => {
|
|
18
|
+
const n = parseInt(h.slice(1), 16)
|
|
19
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
|
|
20
|
+
}
|
|
21
|
+
const srgb2lin = (c: number) => {
|
|
22
|
+
const s = c / 255
|
|
23
|
+
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4
|
|
24
|
+
}
|
|
25
|
+
const relLum = (hex: string) => {
|
|
26
|
+
const [r, g, b] = hex2rgb(hex).map(srgb2lin)
|
|
27
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
28
|
+
}
|
|
29
|
+
const contrast = (a: string, b: string) => {
|
|
30
|
+
const [hi, lo] = [relLum(a), relLum(b)].sort((x, y) => y - x)
|
|
31
|
+
return (hi + 0.05) / (lo + 0.05)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Every plane text can land on, darkest first. */
|
|
35
|
+
const PLANES = [975, 950, 925, 900, 875, 850] as const
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Floors per role, not one blanket 4.5. `text-tertiary` is by definition the
|
|
39
|
+
* de-emphasised role — captions, units, disabled labels — so WCAG's large-text
|
|
40
|
+
* 3:1 is the honest bar for it. Holding it to 4.5 would either be a lie we
|
|
41
|
+
* suppress or a change that erases the distinction between the three roles.
|
|
42
|
+
*/
|
|
43
|
+
const TEXT_ROLES = [
|
|
44
|
+
{ role: 'text-primary', min: 7, note: 'body copy — AAA' },
|
|
45
|
+
{ role: 'text-secondary', min: 4.5, note: 'supporting copy — AA' },
|
|
46
|
+
{ role: 'text-tertiary', min: 3, note: 'de-emphasised — AA large' },
|
|
47
|
+
] as const
|
|
48
|
+
|
|
49
|
+
describe('semantic text on the surface planes (dark)', () => {
|
|
50
|
+
for (const { role, min, note } of TEXT_ROLES) {
|
|
51
|
+
describe(`${role} (${note})`, () => {
|
|
52
|
+
it.each(PLANES)(`clears ${min}:1 on grey-%s`, (plane) => {
|
|
53
|
+
const fg = semanticColorsDark[role as keyof typeof semanticColorsDark] as string
|
|
54
|
+
const ratio = contrast(fg, greyRamp[plane])
|
|
55
|
+
expect(
|
|
56
|
+
ratio,
|
|
57
|
+
`${role} ${fg} on grey-${plane} ${greyRamp[plane]} is ${ratio.toFixed(2)}:1, needs ${min}`,
|
|
58
|
+
).toBeGreaterThanOrEqual(min)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The roles must stay ORDERED as well as legible. A migration that lifted
|
|
65
|
+
* tertiary until it out-contrasted secondary would pass every check above
|
|
66
|
+
* while destroying the hierarchy the three roles exist to express.
|
|
67
|
+
*/
|
|
68
|
+
it('keeps primary > secondary > tertiary in contrast on every plane', () => {
|
|
69
|
+
for (const plane of PLANES) {
|
|
70
|
+
const [pri, sec, ter] = TEXT_ROLES.map(({ role }) =>
|
|
71
|
+
contrast(semanticColorsDark[role as keyof typeof semanticColorsDark] as string, greyRamp[plane]),
|
|
72
|
+
)
|
|
73
|
+
expect(pri, `on grey-${plane}: primary ${pri.toFixed(2)} vs secondary ${sec.toFixed(2)}`).toBeGreaterThan(sec)
|
|
74
|
+
expect(sec, `on grey-${plane}: secondary ${sec.toFixed(2)} vs tertiary ${ter.toFixed(2)}`).toBeGreaterThan(ter)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Border visibility is NOT checked here. The separators are alpha now, and a
|
|
79
|
+
// WCAG ratio needs two opaque colours — feeding `rgba(…)` to a hex parser
|
|
80
|
+
// silently yields black, so such a check passes without measuring anything.
|
|
81
|
+
// Alpha separators are validated by compositing them over each plane in
|
|
82
|
+
// `surface.contract.test.ts` (R3 for the hairlines, R4 for border-prominent).
|
|
83
|
+
})
|
|
@@ -5,124 +5,82 @@
|
|
|
5
5
|
* and should rarely be used directly in components. Use semantic tokens instead.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Absolute colour keywords.
|
|
10
|
+
*
|
|
11
|
+
* This used to hold five scales — `blue`, `red`, `redVivid`, `neutral` and
|
|
12
|
+
* `charcoal`. All five are gone (TD-07.14): the greys folded into `greyRamp`
|
|
13
|
+
* below and the chromatics into the OKLCH `primitiveRamps`. What is left has no
|
|
14
|
+
* scale because these three are not colours you tune, they are keywords.
|
|
15
|
+
*/
|
|
8
16
|
export const primitiveColors = {
|
|
9
|
-
// Blue scale
|
|
10
|
-
blue: {
|
|
11
|
-
50: '#EFF6FF',
|
|
12
|
-
100: '#DBEAFE',
|
|
13
|
-
200: '#BFDBFE',
|
|
14
|
-
300: '#93C5FD',
|
|
15
|
-
400: '#60A5FA',
|
|
16
|
-
500: '#3B82F6',
|
|
17
|
-
600: '#5048E5',
|
|
18
|
-
700: '#3832A0',
|
|
19
|
-
800: '#1E40AF',
|
|
20
|
-
900: '#1E3A8A',
|
|
21
|
-
},
|
|
22
|
-
|
|
23
|
-
// Red scale
|
|
24
|
-
red: {
|
|
25
|
-
50: '#FEF2F2',
|
|
26
|
-
100: '#FEE2E2',
|
|
27
|
-
200: '#FECACA',
|
|
28
|
-
300: '#FCA5A5',
|
|
29
|
-
400: '#DA6868',
|
|
30
|
-
500: '#EF4444',
|
|
31
|
-
600: '#D14343',
|
|
32
|
-
700: '#922E2E',
|
|
33
|
-
800: '#991B1B',
|
|
34
|
-
900: '#7F1D1D',
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
// Vivid red scale
|
|
38
|
-
redVivid: {
|
|
39
|
-
50: '#FFECEE',
|
|
40
|
-
100: '#FFD6DB',
|
|
41
|
-
200: '#FFB3BC',
|
|
42
|
-
300: '#FF8593',
|
|
43
|
-
400: '#FF6070',
|
|
44
|
-
500: '#FF4757',
|
|
45
|
-
600: '#E63548',
|
|
46
|
-
700: '#C42539',
|
|
47
|
-
800: '#9E1C2C',
|
|
48
|
-
900: '#7A1520',
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
// Neutral/Gray scale
|
|
52
|
-
neutral: {
|
|
53
|
-
50: '#FAFAFA',
|
|
54
|
-
100: '#F3F4F6',
|
|
55
|
-
200: '#E5E7EB',
|
|
56
|
-
300: '#D1D5DB',
|
|
57
|
-
400: '#9CA3AF',
|
|
58
|
-
500: '#6B7280',
|
|
59
|
-
600: '#4B5563',
|
|
60
|
-
700: '#374151',
|
|
61
|
-
800: '#1F2937',
|
|
62
|
-
900: '#111827',
|
|
63
|
-
950: '#030712',
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
// Charcoal scale (dark backgrounds)
|
|
67
|
-
charcoal: {
|
|
68
|
-
0: '#6E6E6E',
|
|
69
|
-
50: '#5D5D5D',
|
|
70
|
-
100: '#4C4C4C',
|
|
71
|
-
200: '#3C3C3C',
|
|
72
|
-
300: '#2C2C2C',
|
|
73
|
-
400: '#1F1F1F',
|
|
74
|
-
500: '#1C1C1C',
|
|
75
|
-
600: '#191919',
|
|
76
|
-
700: '#161616',
|
|
77
|
-
800: '#131313',
|
|
78
|
-
900: '#101010',
|
|
79
|
-
},
|
|
80
|
-
|
|
81
|
-
// Pure colors
|
|
82
17
|
white: '#FFFFFF',
|
|
83
18
|
black: '#000000',
|
|
84
19
|
transparent: 'transparent',
|
|
85
20
|
} as const
|
|
86
21
|
|
|
87
22
|
/**
|
|
88
|
-
*
|
|
23
|
+
* Unified warm-neutral grey ramp (TD-07.14)
|
|
24
|
+
*
|
|
25
|
+
* The ONE achromatic scale. Regenerate with `scripts/generate-grey-ramp.mjs` —
|
|
26
|
+
* preserve the generator, not just the hexes.
|
|
27
|
+
*
|
|
28
|
+
* The surface planes ARE steps here rather than a parallel object: `850` is
|
|
29
|
+
* overlay, `975` is the bezel. That is the whole point of the ramp. `grey`,
|
|
30
|
+
* `neutral`, `surfaceRampDark` and `backgroundFrameDark` all resolve into it,
|
|
31
|
+
* which is why every plane step below is byte-identical to the value that
|
|
32
|
+
* shipped in v0.10.0 — the surfaces do not move, only their names.
|
|
33
|
+
*
|
|
34
|
+
* TWO THINGS ABOUT THE NUMBERING, both deliberate and both previously re-derived
|
|
35
|
+
* from scratch more than once because they were never written down:
|
|
89
36
|
*
|
|
90
|
-
*
|
|
91
|
-
* `
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* the
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* change is additive and doesn't ripple into those consumers.
|
|
106
|
-
* See coordination/design-explorations/surface-system-north-star.md.
|
|
37
|
+
* 1. `975` sits BELOW the standard grid. `canonL` — the median L* of the seven
|
|
38
|
+
* chromatic `primitiveRamps` at each step — ends at L*10.3, which is step
|
|
39
|
+
* `950`. The bezel is L*3.8, darker than the grid's last rung, so there is
|
|
40
|
+
* simply no step number left for it and `975` extends past the end.
|
|
41
|
+
* 2. `850`/`875` are quarter-steps because overlay/raised/elevated are only
|
|
42
|
+
* ~2.7 L* apart and all three collide on `900` at normal resolution. `800`
|
|
43
|
+
* is a generated bridge between `700` (L*37.9) and overlay (L*22.7) so the
|
|
44
|
+
* spacing stays even across the join.
|
|
45
|
+
*
|
|
46
|
+
* There is no `inset`. It was retired, not renamed: at ΔE 1.10 from `975` it was
|
|
47
|
+
* an imperceptible duplicate of the frame, which is exactly why `<Surface pressed>`
|
|
48
|
+
* (surface − 1) kept collapsing into it. `975` is the floor.
|
|
49
|
+
*
|
|
50
|
+
* Spec, generator and the rejected v1/v2 cuts:
|
|
51
|
+
* coordination/design-explorations/foundations/warm-grey-ramp/
|
|
107
52
|
*/
|
|
108
|
-
export const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
53
|
+
export const greyRamp = {
|
|
54
|
+
50: '#F9F6F3', // L*97.0 W6
|
|
55
|
+
100: '#EDEAE7', // L*92.8 W6
|
|
56
|
+
200: '#D4D1CE', // L*84.0 W6 — warm silver
|
|
57
|
+
300: '#BDBAB7', // L*75.7 W6
|
|
58
|
+
400: '#A29F9D', // L*65.7 W5
|
|
59
|
+
500: '#888684', // L*56.0 W4
|
|
60
|
+
600: '#72716F', // L*47.7 W3
|
|
61
|
+
700: '#5A5958', // L*37.9 W2
|
|
62
|
+
800: '#424140', // L*27.6 W2 — generated bridge
|
|
63
|
+
850: '#373635', // L*22.7 W2 — surface overlay
|
|
64
|
+
875: '#31302F', // L*19.9 W2 — surface raised
|
|
65
|
+
900: '#2C2A28', // L*17.2 W4 — surface elevated
|
|
66
|
+
925: '#252321', // L*13.9 W4 — surface base
|
|
67
|
+
950: '#1C1916', // L*9.0 W6 — background base / shell
|
|
68
|
+
975: '#100D0A', // L*3.8 W6 — background frame / bezel
|
|
115
69
|
} as const
|
|
116
70
|
|
|
117
71
|
/**
|
|
118
|
-
*
|
|
72
|
+
* The surface planes, as ramp steps — the mapping `<Surface level>` resolves through.
|
|
119
73
|
*
|
|
120
|
-
*
|
|
121
|
-
* shell. Sits ONE STEP BELOW `surfaceRampDark.background` (L*9), darker even
|
|
122
|
-
* than `surfaceRampDark.inset` (L*4.5) — it isn't a plane IN the ramp, it's
|
|
123
|
-
* the bezel the ramp sits inside. Literal, not `deriveSurfaceRamp()` output.
|
|
74
|
+
* Ordered darkest -> lightest, which is the order `pressedLevel()` walks.
|
|
124
75
|
*/
|
|
125
|
-
export const
|
|
76
|
+
export const SURFACE_PLANE_STEPS = {
|
|
77
|
+
frame: 975,
|
|
78
|
+
background: 950,
|
|
79
|
+
base: 925,
|
|
80
|
+
elevated: 900,
|
|
81
|
+
raised: 875,
|
|
82
|
+
overlay: 850,
|
|
83
|
+
} as const
|
|
126
84
|
|
|
127
85
|
/**
|
|
128
86
|
* Derived tonal ramps (TD-05.09 color foundations)
|
|
@@ -507,17 +465,6 @@ export const primitiveBorderRadius = {
|
|
|
507
465
|
full: '9999px',
|
|
508
466
|
} as const
|
|
509
467
|
|
|
510
|
-
export const primitiveShadows = {
|
|
511
|
-
none: 'none',
|
|
512
|
-
sm: '0px 1px 2px rgba(100, 116, 139, 0.12)',
|
|
513
|
-
DEFAULT: '0px 1px 3px rgba(100, 116, 139, 0.12), 0px 1px 2px rgba(100, 116, 139, 0.24)',
|
|
514
|
-
md: '0px 4px 6px rgba(100, 116, 139, 0.12)',
|
|
515
|
-
lg: '0px 10px 15px rgba(100, 116, 139, 0.12)',
|
|
516
|
-
xl: '0px 20px 25px rgba(100, 116, 139, 0.12)',
|
|
517
|
-
'2xl': '0px 25px 50px rgba(100, 116, 139, 0.25)',
|
|
518
|
-
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
|
|
519
|
-
} as const
|
|
520
|
-
|
|
521
468
|
export const primitiveBreakpoints = {
|
|
522
469
|
xs: 0,
|
|
523
470
|
sm: 600,
|