@titan-design/react-ui 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/bodymap.js +129 -76
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +129 -76
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +117 -26
- package/dist/index.d.ts +117 -26
- package/dist/index.js +677 -450
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +673 -451
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-_3Ajpiqu.d.ts → pages-CsjsUn0G.d.ts} +30 -13
- package/dist/{pages-CpW3OB3g.d.mts → pages-ZbT_pD_U.d.mts} +30 -13
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +676 -701
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +676 -701
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +31 -7
- package/dist/theme/index.d.ts +31 -7
- package/dist/theme/index.js +84 -20
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +83 -21
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +84 -20
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +84 -20
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/dist/theme/tokens.d.mts +56 -7
- package/dist/theme/tokens.d.ts +56 -7
- package/dist/theme/tokens.js +72 -20
- package/dist/theme/tokens.js.map +1 -1
- package/dist/theme/tokens.mjs +71 -21
- package/dist/theme/tokens.mjs.map +1 -1
- package/docs/render-testing-pattern.md +130 -0
- package/package.json +1 -1
- package/src/arch/Architecture.stories.tsx +3 -1
- package/src/arch/arch-graph.json +74 -72
- package/src/components/custom/ActiveWork/InitiativeBrief.tsx +2 -1
- package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +8 -3
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +4 -1
- package/src/components/custom/Gauge/Gauge.tsx +3 -1
- package/src/components/custom/Scatter/Scatter.stories.tsx +10 -7
- package/src/components/custom/Scatter/Scatter.tsx +6 -4
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
- package/src/components/custom/Table/Table.stories.tsx +1 -1
- package/src/components/custom/Table/Table.tsx +2 -2
- package/src/components/custom/Treemap/Treemap.stories.tsx +13 -10
- package/src/components/custom/Treemap/Treemap.tsx +4 -1
- package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +2 -1
- package/src/components/custom/Workout/BaseBadge.stories.tsx +11 -7
- package/src/components/custom/Workout/BaseBadge.tsx +3 -2
- package/src/components/custom/Workout/BodyMap.tsx +5 -3
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +13 -3
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +5 -3
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +2 -1
- package/src/components/custom/Workout/CapacityBandChart.tsx +5 -3
- package/src/components/custom/Workout/DeviationBar.test.tsx +5 -2
- package/src/components/custom/Workout/DeviationBar.tsx +5 -2
- package/src/components/custom/Workout/DualVelocityStrip.stories.tsx +7 -6
- package/src/components/custom/Workout/DualVelocityStrip.test.tsx +29 -16
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +8 -8
- package/src/components/custom/Workout/ExerciseCard.test.tsx +39 -19
- package/src/components/custom/Workout/ExerciseCard.tsx +92 -167
- package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +89 -6
- package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +189 -1
- package/src/components/custom/Workout/ExerciseCardHeading.tsx +167 -18
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +3 -2
- package/src/components/custom/Workout/ExerciseHeading.tsx +192 -47
- package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +6 -5
- package/src/components/custom/Workout/InputBar.tsx +4 -2
- package/src/components/custom/Workout/IntensityBar.tsx +4 -1
- package/src/components/custom/Workout/MesoCard.tsx +2 -1
- package/src/components/custom/Workout/MesoStatusCard.tsx +11 -10
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +8 -3
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +12 -2
- package/src/components/custom/Workout/README.md +58 -12
- package/src/components/custom/Workout/ReadinessCheck.tsx +4 -2
- package/src/components/custom/Workout/RestTimer.stories.tsx +13 -2
- package/src/components/custom/Workout/RestTimer.tsx +4 -2
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +16 -12
- package/src/components/custom/Workout/SegmentedBar.tsx +4 -3
- package/src/components/custom/Workout/SetStrip.stories.tsx +3 -2
- package/src/components/custom/Workout/SetsRepsLoad.tsx +23 -9
- package/src/components/custom/Workout/Sparkline.stories.tsx +13 -10
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +2 -1
- package/src/components/custom/Workout/StrengthTrendChart.tsx +8 -6
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +4 -3
- package/src/components/custom/Workout/TempoDisplay.tsx +2 -0
- package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +3 -2
- package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +1 -1
- package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +40 -0
- package/src/components/custom/Workout/VelocityStrip.test.tsx +48 -6
- package/src/components/custom/Workout/VelocityStrip.tsx +23 -14
- package/src/components/custom/Workout/ZoneTrack.stories.tsx +3 -1
- package/src/components/custom/Workout/exerciseRowState.ts +29 -0
- package/src/components/custom/Workout/index.ts +12 -2
- package/src/components/custom/Workout/metricText.stories.tsx +9 -8
- package/src/components/custom/Workout/metricText.tsx +4 -2
- package/src/components/custom/Workout/setHeadingKit.tsx +12 -10
- package/src/components/custom/Workout/velocity-story-kit.tsx +2 -2
- package/src/components/custom/charts/README.md +5 -0
- package/src/components/custom/charts/SetBarChart.test.tsx +16 -0
- package/src/components/custom/charts/SetBarChart.tsx +14 -5
- package/src/components/custom/charts/flatBarGeometry.test.ts +41 -0
- package/src/components/custom/charts/flatBarGeometry.ts +32 -0
- package/src/components/custom/stepper/Stepper.tsx +8 -6
- package/src/components/ui/alert/Alert.stories.tsx +14 -4
- package/src/components/ui/alert/Alert.tsx +24 -3
- package/src/components/ui/button/Button.tsx +12 -12
- package/src/components/ui/checkbox/Checkbox.tsx +2 -2
- package/src/components/ui/data-row/DataRow.stories.tsx +6 -2
- package/src/components/ui/drawer/Drawer.tsx +4 -1
- package/src/components/ui/icon-box/IconBox.tsx +8 -6
- package/src/components/ui/indicator/Indicator.stories.tsx +4 -3
- package/src/components/ui/list-item/ListItem.stories.tsx +7 -4
- package/src/components/ui/modal/Modal.tsx +2 -0
- package/src/components/ui/pill/Pill.stories.tsx +53 -0
- package/src/components/ui/pill/Pill.tsx +10 -6
- package/src/components/ui/popover/Popover.stories.tsx +1 -1
- package/src/components/ui/progress/Progress.tsx +9 -6
- package/src/components/ui/radio/Radio.tsx +1 -1
- package/src/components/ui/section/Section.stories.tsx +10 -10
- package/src/components/ui/select/Select.tsx +2 -1
- package/src/components/ui/spinner/Spinner.tsx +3 -1
- package/src/components/ui/stack/Stack.stories.tsx +7 -7
- package/src/components/ui/surface/Surface.stories.tsx +75 -28
- package/src/components/ui/switch/Switch.tsx +1 -1
- package/src/components/ui/tabs/Tabs.tsx +4 -2
- package/src/components/ui/toast/Toast.tsx +6 -4
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +13 -6
- package/src/stories/PresetShowcase.stories.tsx +1 -1
- package/src/test/no-raw-color.test.ts +39 -0
- package/src/theme/Alpha.stories.tsx +103 -0
- package/src/theme/ProposedTokensVW82.stories.tsx +414 -0
- package/src/theme/config.ts +12 -0
- package/src/theme/global.css +15 -3
- package/src/theme/subtle-tone-weight.test.ts +163 -0
- package/src/theme/tailwind-var-opacity.test.ts +163 -0
- package/src/theme/tokens/primitives.ts +33 -0
- package/src/theme/tokens/semantic-hex-guard.test.ts +322 -0
- package/src/theme/tokens/semantic.ts +56 -20
- package/src/utils/colors.alpha-adoption.test.ts +408 -0
- package/tailwind.config.js +50 -9
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getSemanticColors, type ThemeMode } from '../../../theme/tokens/semantic'
|
|
2
|
+
|
|
3
|
+
/** The interaction washes an exercise row can wear, beyond its resting look. */
|
|
4
|
+
export type ExerciseRowState = 'hovered' | 'selected' | 'pressed'
|
|
5
|
+
|
|
6
|
+
const ROW_STATE_TOKEN = {
|
|
7
|
+
hovered: 'interactive-hover',
|
|
8
|
+
selected: 'interactive-selected',
|
|
9
|
+
pressed: 'interactive-active',
|
|
10
|
+
} as const satisfies Record<ExerciseRowState, keyof ReturnType<typeof getSemanticColors>>
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Literal-hex wash for one interaction state, in the same shape as
|
|
14
|
+
* `onSurfaceColors`. Uses `getSemanticColors` rather than `resolveColor` so the
|
|
15
|
+
* value survives where there are no CSS custom properties (the raw-RN wall SPA,
|
|
16
|
+
* native) and stays assertable in tests.
|
|
17
|
+
*/
|
|
18
|
+
export function exerciseRowStateColor(state: ExerciseRowState, mode: ThemeMode = 'dark'): string {
|
|
19
|
+
return getSemanticColors(mode)[ROW_STATE_TOKEN[state]]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The tone for an exercise being performed right now. Static by construction —
|
|
24
|
+
* a live row sits on a wall display, where titan's "the grain never animates"
|
|
25
|
+
* rule applies just as much to a heading.
|
|
26
|
+
*/
|
|
27
|
+
export function exerciseLiveColor(mode: ThemeMode = 'dark'): string {
|
|
28
|
+
return getSemanticColors(mode)['status-live']
|
|
29
|
+
}
|
|
@@ -68,8 +68,18 @@ export {
|
|
|
68
68
|
type ExerciseIndicatorProps,
|
|
69
69
|
type ExerciseIndicatorKind,
|
|
70
70
|
} from './ExerciseIndicator'
|
|
71
|
-
export {
|
|
72
|
-
|
|
71
|
+
export {
|
|
72
|
+
ExerciseHeading,
|
|
73
|
+
exerciseHeadingLabel,
|
|
74
|
+
type ExerciseHeadingProps,
|
|
75
|
+
type ExerciseHeadingLayout,
|
|
76
|
+
} from './ExerciseHeading'
|
|
77
|
+
export {
|
|
78
|
+
ExerciseCardHeading,
|
|
79
|
+
type ExerciseCardHeadingProps,
|
|
80
|
+
type ExerciseRowDensity,
|
|
81
|
+
} from './ExerciseCardHeading'
|
|
82
|
+
export { exerciseLiveColor, exerciseRowStateColor, type ExerciseRowState } from './exerciseRowState'
|
|
73
83
|
export { ExerciseCard, type ExerciseCardProps } from './ExerciseCard'
|
|
74
84
|
export { SetTableHeader, type SetTableHeaderProps } from './SetTableHeader'
|
|
75
85
|
export {
|
|
@@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
|
2
2
|
import { View } from 'react-native'
|
|
3
3
|
import { MetricCell } from './metricText'
|
|
4
4
|
import { Surface } from '../../ui/surface'
|
|
5
|
+
import { greyRamp } from '../../../theme/tokens/primitives'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* `MetricCell` — the shared "segmented metric" typography primitive (Inter · 600 ·
|
|
@@ -41,23 +42,23 @@ export default meta
|
|
|
41
42
|
type Story = StoryObj<typeof MetricCell>
|
|
42
43
|
|
|
43
44
|
export const ValueCell: Story = {
|
|
44
|
-
args: { children: '145', color:
|
|
45
|
+
args: { children: '145', color: greyRamp[50], fontSize: 11 },
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export const Separator: Story = {
|
|
48
|
-
args: { children: '×', color:
|
|
49
|
+
args: { children: '×', color: greyRamp[600], fontSize: 11 },
|
|
49
50
|
parameters: { docs: { description: { story: 'A muted separator cell (× / @ / -).' } } },
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export const InContext: Story = {
|
|
53
54
|
render: () => (
|
|
54
55
|
<View style={{ flexDirection: 'row', alignItems: 'baseline' }}>
|
|
55
|
-
<MetricCell color=
|
|
56
|
-
<MetricCell color=
|
|
57
|
-
<MetricCell color=
|
|
58
|
-
<MetricCell color=
|
|
59
|
-
<MetricCell color=
|
|
60
|
-
<MetricCell color=
|
|
56
|
+
<MetricCell color={greyRamp[50]}>5</MetricCell>
|
|
57
|
+
<MetricCell color={greyRamp[600]}> × </MetricCell>
|
|
58
|
+
<MetricCell color={greyRamp[50]}>8</MetricCell>
|
|
59
|
+
<MetricCell color={greyRamp[600]}> @ </MetricCell>
|
|
60
|
+
<MetricCell color={greyRamp[50]}>145</MetricCell>
|
|
61
|
+
<MetricCell color={greyRamp[600]}> lb</MetricCell>
|
|
61
62
|
</View>
|
|
62
63
|
),
|
|
63
64
|
parameters: {
|
|
@@ -15,20 +15,22 @@ export interface MetricCellProps {
|
|
|
15
15
|
/** Cell text color — value cells take a primary color, separators a muted one. */
|
|
16
16
|
color: string
|
|
17
17
|
fontSize?: number
|
|
18
|
+
/** Cell font weight. Default 600 (the bold "digital readout" look). */
|
|
19
|
+
weight?: 400 | 600
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* @deprecated Use `<Metric size="…" align="…">` (roadmap decision 11) — removed
|
|
22
24
|
* after AW-127 consumer migration.
|
|
23
25
|
*/
|
|
24
|
-
export function MetricCell({ children, color, fontSize = 11 }: MetricCellProps) {
|
|
26
|
+
export function MetricCell({ children, color, fontSize = 11, weight = 600 }: MetricCellProps) {
|
|
25
27
|
return (
|
|
26
28
|
<Text
|
|
27
29
|
style={{
|
|
28
30
|
fontFamily: METRIC_FONT,
|
|
29
31
|
fontSize,
|
|
30
32
|
color,
|
|
31
|
-
fontWeight: '600',
|
|
33
|
+
fontWeight: String(weight) as '400' | '600',
|
|
32
34
|
letterSpacing: 1,
|
|
33
35
|
}}
|
|
34
36
|
>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { ReactNode } from 'react'
|
|
7
7
|
import { TempoDisplay } from './TempoDisplay'
|
|
8
|
-
import { greyRamp } from '../../../theme/tokens/primitives'
|
|
8
|
+
import { greyRamp, primitiveRamps } from '../../../theme/tokens/primitives'
|
|
9
9
|
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
10
10
|
|
|
11
11
|
// ---- geometry
|
|
@@ -29,19 +29,19 @@ export const INSET_SHADOW =
|
|
|
29
29
|
'inset 0 9px 12px -6px rgba(0,0,0,0.85), inset 7px 0 10px -6px rgba(0,0,0,0.7), inset 0 -1px 0 rgba(255,255,255,0.02)'
|
|
30
30
|
|
|
31
31
|
// ---- zone colors = real titan ramp pins (primitiveRamps)
|
|
32
|
-
export const RED =
|
|
33
|
-
export const ORANGE =
|
|
34
|
-
export const AMBER =
|
|
35
|
-
export const GREEN =
|
|
32
|
+
export const RED = primitiveRamps.red[600]
|
|
33
|
+
export const ORANGE = primitiveRamps.orange[400]
|
|
34
|
+
export const AMBER = primitiveRamps.amber[300]
|
|
35
|
+
export const GREEN = primitiveRamps.green[300]
|
|
36
36
|
export const velColor = (v: number): string =>
|
|
37
37
|
v < 0.5 ? RED : v < 0.75 ? ORANGE : v < 1.0 ? AMBER : GREEN
|
|
38
38
|
|
|
39
39
|
// active-set pulse: each segment eases along its own ramp (adjacent lighter/darker steps) through the pin.
|
|
40
40
|
export const PULSE_CSS = `
|
|
41
|
-
@keyframes s3pRed{0%,100%{background
|
|
42
|
-
@keyframes s3pOrange{0%,100%{background
|
|
43
|
-
@keyframes s3pAmber{0%,100%{background
|
|
44
|
-
@keyframes s3pGreen{0%,100%{background
|
|
41
|
+
@keyframes s3pRed{0%,100%{background:${primitiveRamps.red[500]}}50%{background:${primitiveRamps.red[700]}}}
|
|
42
|
+
@keyframes s3pOrange{0%,100%{background:${primitiveRamps.orange[300]}}50%{background:${primitiveRamps.orange[500]}}}
|
|
43
|
+
@keyframes s3pAmber{0%,100%{background:${primitiveRamps.amber[200]}}50%{background:${primitiveRamps.amber[400]}}}
|
|
44
|
+
@keyframes s3pGreen{0%,100%{background:${primitiveRamps.green[200]}}50%{background:${primitiveRamps.green[400]}}}`
|
|
45
45
|
export const pulseAnim = (c: string): string | undefined => {
|
|
46
46
|
const name =
|
|
47
47
|
c === RED
|
|
@@ -58,6 +58,7 @@ export const pulseAnim = (c: string): string | undefined => {
|
|
|
58
58
|
|
|
59
59
|
// ---- sets/reps/load in the TempoDisplay visual language (Inter · 600 · letter-spacing 1 · gray separators)
|
|
60
60
|
const INTER = 'Inter, sans-serif'
|
|
61
|
+
// VW-82: Tailwind gray-500; no ramp step matches. Left raw, proposed in the PR.
|
|
61
62
|
const SRL_SEP = '#6B7280'
|
|
62
63
|
function SRLCell({ children, color = T_PRIMARY }: { children: ReactNode; color?: string }) {
|
|
63
64
|
return (
|
|
@@ -136,7 +137,7 @@ export function SetStrip({ sets, h }: { sets: SetState[]; h: number }) {
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
export function Indicator({ kind }: { kind: 'pr' | 'issue' | 'info' }) {
|
|
139
|
-
const map = { pr: ['★', ORANGE], issue: ['!', RED], info: ['i', '
|
|
140
|
+
const map = { pr: ['★', ORANGE], issue: ['!', RED], info: ['i', T['status-info']] } as const
|
|
140
141
|
const [glyph, color] = map[kind]
|
|
141
142
|
return (
|
|
142
143
|
<span
|
|
@@ -336,6 +337,7 @@ export function Page({
|
|
|
336
337
|
<div
|
|
337
338
|
style={{
|
|
338
339
|
minHeight: '100vh',
|
|
340
|
+
// VW-82: no token at this value (nearest background-frame #100D0A).
|
|
339
341
|
background: '#0A0A0A',
|
|
340
342
|
color: T_PRIMARY,
|
|
341
343
|
padding: 28,
|
|
@@ -116,8 +116,8 @@ export const VIEW_HEIGHT: Record<StripView, { single: number; dual: number }> =
|
|
|
116
116
|
|
|
117
117
|
export type StripView = 'compact' | 'expanded' | 'hero'
|
|
118
118
|
|
|
119
|
-
/** The dual renderer for a view. `expanded`'s dual is the lean `
|
|
120
|
-
export const dualVariantFor = (view: StripView) => (view === 'expanded' ? '
|
|
119
|
+
/** The dual renderer for a view. `expanded`'s dual is the lean `dual-expanded` renderer. */
|
|
120
|
+
export const dualVariantFor = (view: StripView) => (view === 'expanded' ? 'dual-expanded' : view)
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* One scenario, rendered single above dual on the same dataset.
|
|
@@ -16,6 +16,7 @@ come from [`src/arch/arch-graph.json`](../../../arch/arch-graph.json).
|
|
|
16
16
|
| `SparkBars` | atom | `resolveColor`, `cn` | FileActivityDetail, FileActivityRow (ActiveWork) | yes |
|
|
17
17
|
| `SetBarChart` | atom | `barPaper`, `SurfaceContext`, `live-rep-growth` | RomProgressionChart (Fatigue), VelocityStrip (Workout) | no — by path |
|
|
18
18
|
| `live-rep-growth` | hook | `Animated`, `Easing` | SetBarChart, VelocityStrip | no — by path |
|
|
19
|
+
| `flatBarGeometry` | module | — | SegmentedBar (Workout), SetBarChart | no — by path |
|
|
19
20
|
|
|
20
21
|
`SetBarChart` and `live-rep-growth` are deliberately absent from `index.ts`: they
|
|
21
22
|
are workout-internal and imported by path, so the public barrel stays one mark
|
|
@@ -39,6 +40,10 @@ as a series, and `maxBars` because a sparkline is a recent-history glance.
|
|
|
39
40
|
so every value-height family animates the live rep identically. It carries the
|
|
40
41
|
12% `PEAK_OVERSHOOT` for a new-peak bar and honours `prefers-reduced-motion`.
|
|
41
42
|
|
|
43
|
+
**`flatBarGeometry`** is the single source for the default height/gap/radius
|
|
44
|
+
`SegmentedBar` (set-level) and `SetBarChart` (rep-level) each declare, documented
|
|
45
|
+
as intentionally different sizes rather than left to drift apart unnoticed (VW-86).
|
|
46
|
+
|
|
42
47
|
## Reuse audit
|
|
43
48
|
|
|
44
49
|
| Leaf | Should compose | Status |
|
|
@@ -7,12 +7,28 @@ import {
|
|
|
7
7
|
valueLabelFontSize,
|
|
8
8
|
estimateValueLabelWidth,
|
|
9
9
|
shouldFlipEdgeLabel,
|
|
10
|
+
scaleDenominator,
|
|
11
|
+
FIXED_MAX_VALUE,
|
|
12
|
+
PEAK_HEADROOM,
|
|
10
13
|
type SetSlot,
|
|
11
14
|
} from './SetBarChart'
|
|
12
15
|
|
|
13
16
|
const reps = (values: number[]): SetSlot[] => values.map((v) => ({ kind: 'rep', value: v }))
|
|
14
17
|
const silver = () => '#C7CBD1'
|
|
15
18
|
|
|
19
|
+
describe('scaleDenominator', () => {
|
|
20
|
+
it('gives `fixed` the same headroom band above its ceiling as `peak` gets above its max', () => {
|
|
21
|
+
// Pinned: without this headroom, a rep AT the fixed ceiling fills the plot with zero margin
|
|
22
|
+
// above it, unlike an equally-tall `peak` bar (which always keeps PEAK_HEADROOM's margin).
|
|
23
|
+
expect(scaleDenominator('fixed', 0)).toBeCloseTo(FIXED_MAX_VALUE * PEAK_HEADROOM)
|
|
24
|
+
expect(scaleDenominator('peak', 1)).toBeCloseTo(1 * PEAK_HEADROOM)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('ignores the performed max for `fixed` — the ceiling stays constant across sets', () => {
|
|
28
|
+
expect(scaleDenominator('fixed', 0.5)).toBe(scaleDenominator('fixed', 100))
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
16
32
|
describe('valueLabelFontSize', () => {
|
|
17
33
|
it('reads the full 12px at the standard single-hero (220) and dual-wing (110) scales', () => {
|
|
18
34
|
expect(valueLabelFontSize(220)).toBe(12)
|
|
@@ -23,6 +23,7 @@ import { primitiveRamps } from '../../../theme/tokens/primitives'
|
|
|
23
23
|
import { barPaper } from '../../../theme/materials'
|
|
24
24
|
import { useOnSurfaceColor, useSurface, surfaceBackground } from '../../ui/surface/SurfaceContext'
|
|
25
25
|
import { useLiveRepGrowth } from './live-rep-growth'
|
|
26
|
+
import { REP_LEVEL_FLAT_BAR } from './flatBarGeometry'
|
|
26
27
|
|
|
27
28
|
/** Linear-blend two #RRGGBB hexes (`t`=0 → a, 1 → b) — the surface-relative solid to-do tone. */
|
|
28
29
|
function mixHex(a: string, b: string, t: number): string {
|
|
@@ -188,13 +189,21 @@ export interface SetBarChartProps {
|
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
/** The `scale="fixed"` velocity ceiling (m/s) — a bar height reads the same absolute value across sets. */
|
|
191
|
-
const FIXED_MAX_VALUE = 1.15
|
|
192
|
-
/**
|
|
192
|
+
export const FIXED_MAX_VALUE = 1.15
|
|
193
|
+
/**
|
|
194
|
+
* Headroom above the scaling ceiling: just enough to seat the peak bar's value label without a big
|
|
195
|
+
* empty band on top. Applied to BOTH scales — `peak` already multiplies the performed max by this,
|
|
196
|
+
* but `fixed` used the bare ceiling with none: a rep AT or PAST `FIXED_MAX_VALUE` (a fast lift on a
|
|
197
|
+
* live wall, not just a theoretical edge) filled the plot to its very top with no room left for that
|
|
198
|
+
* bar's own label, while an equally-tall `peak` bar always kept this margin. Multiplying `fixed`'s
|
|
199
|
+
* denominator by the same constant gives both scales the identical breathing room above their tallest
|
|
200
|
+
* bar, so a lift at the ceiling reads the same on either scale.
|
|
201
|
+
*/
|
|
193
202
|
export const PEAK_HEADROOM = 1.03
|
|
194
203
|
|
|
195
204
|
/** The height-scaling denominator for `scale` at the given performed max (guarded ≥ 0 by callers). */
|
|
196
205
|
export function scaleDenominator(scale: 'peak' | 'fixed', maxValue: number): number {
|
|
197
|
-
return scale === 'fixed' ? FIXED_MAX_VALUE : maxValue * PEAK_HEADROOM
|
|
206
|
+
return (scale === 'fixed' ? FIXED_MAX_VALUE : maxValue) * PEAK_HEADROOM
|
|
198
207
|
}
|
|
199
208
|
|
|
200
209
|
/**
|
|
@@ -235,7 +244,7 @@ const LABEL_GAP = 3
|
|
|
235
244
|
/** Inter-bar gap as a fraction of bar width — the locked dense default (near expanded density). */
|
|
236
245
|
export const GAP_RATIO = 0.08
|
|
237
246
|
/** Floor on the proportional inter-bar gap (px) — narrow plots tighten to ~expanded density. */
|
|
238
|
-
const MIN_BAR_GAP =
|
|
247
|
+
const MIN_BAR_GAP = REP_LEVEL_FLAT_BAR.gapFloor
|
|
239
248
|
/**
|
|
240
249
|
* The EXTRA margin at a chunk boundary (drop sub-load / myo cluster / cluster intra-rest), as a
|
|
241
250
|
* fraction of bar width — proportional so it scales with the dense spacing and reads as a clear
|
|
@@ -250,7 +259,7 @@ export const BAR_MAX_WIDTH = 120
|
|
|
250
259
|
/** Below this per-bar width the value labels collide, so all but the peak + live rep are dropped. */
|
|
251
260
|
const LABEL_MIN_BAR_WIDTH = 30
|
|
252
261
|
/** Default top-corner radius on bars (px). */
|
|
253
|
-
const DEFAULT_BAR_RADIUS =
|
|
262
|
+
const DEFAULT_BAR_RADIUS = REP_LEVEL_FLAT_BAR.radius
|
|
254
263
|
/** Minimum drawn height of a performed bar (px) — a near-zero rep still reads as a rep. */
|
|
255
264
|
const MIN_BAR_HEIGHT = 4
|
|
256
265
|
/** In `flat` (compact) mode every rep bar is this fraction of the plot height — a uniform short bar. */
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
import { SET_LEVEL_FLAT_BAR, REP_LEVEL_FLAT_BAR } from './flatBarGeometry'
|
|
6
|
+
|
|
7
|
+
const DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const segmentedBarSource = readFileSync(path.join(DIR, '../Workout/SegmentedBar.tsx'), 'utf8')
|
|
9
|
+
const setBarChartSource = readFileSync(path.join(DIR, 'SetBarChart.tsx'), 'utf8')
|
|
10
|
+
|
|
11
|
+
describe('flat-bar geometry (VW-86)', () => {
|
|
12
|
+
it('documents intentionally different set-level and rep-level sizes', () => {
|
|
13
|
+
expect(SET_LEVEL_FLAT_BAR).toEqual({ height: 8, gap: 5, radius: 2 })
|
|
14
|
+
expect(REP_LEVEL_FLAT_BAR).toEqual({ height: null, gapFloor: 2, radius: 5 })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('SegmentedBar imports its geometry from the shared module rather than its own literals', () => {
|
|
18
|
+
expect(segmentedBarSource).toMatch(
|
|
19
|
+
/import\s*\{\s*SET_LEVEL_FLAT_BAR\s*\}\s*from\s*'\.\.\/charts\/flatBarGeometry'/
|
|
20
|
+
)
|
|
21
|
+
// The old hardcoded defaults must be gone from the component's own prop destructuring.
|
|
22
|
+
expect(segmentedBarSource).not.toMatch(/height\s*=\s*8\b/)
|
|
23
|
+
expect(segmentedBarSource).not.toMatch(/radius\s*=\s*2\b/)
|
|
24
|
+
expect(segmentedBarSource).toMatch(/height\s*=\s*SET_LEVEL_FLAT_BAR\.height/)
|
|
25
|
+
expect(segmentedBarSource).toMatch(/radius\s*=\s*SET_LEVEL_FLAT_BAR\.radius/)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('SetBarChart imports its geometry from the shared module rather than its own literals', () => {
|
|
29
|
+
expect(setBarChartSource).toMatch(
|
|
30
|
+
/import\s*\{\s*REP_LEVEL_FLAT_BAR\s*\}\s*from\s*'\.\/flatBarGeometry'/
|
|
31
|
+
)
|
|
32
|
+
expect(setBarChartSource).not.toMatch(/const MIN_BAR_GAP = 2\b/)
|
|
33
|
+
expect(setBarChartSource).not.toMatch(/const DEFAULT_BAR_RADIUS = 5\b/)
|
|
34
|
+
expect(setBarChartSource).toMatch(/const MIN_BAR_GAP = REP_LEVEL_FLAT_BAR\.gapFloor/)
|
|
35
|
+
expect(setBarChartSource).toMatch(/const DEFAULT_BAR_RADIUS = REP_LEVEL_FLAT_BAR\.radius/)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('SegmentedBar keeps SEGMENTED_BAR_GAP derived from the shared set-level gap', () => {
|
|
39
|
+
expect(segmentedBarSource).toMatch(/const SEGMENTED_BAR_GAP = SET_LEVEL_FLAT_BAR\.gap/)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flat-bar geometry `SegmentedBar` (set-level: one row per SET, e.g. SetStrip/SessionRail)
|
|
3
|
+
* and `SetBarChart` (rep-level: one bar per REP within a set) each declare as their own component
|
|
4
|
+
* defaults. VW-86 named the fact that these lived in two files with no shared source as the drift
|
|
5
|
+
* risk: a future edit to one could silently diverge from the other without anyone noticing they
|
|
6
|
+
* were ever meant to be compared.
|
|
7
|
+
*
|
|
8
|
+
* They are NOT unified into one set of numbers — a strip of SETS across a session and a chart of
|
|
9
|
+
* REPS within one set are genuinely different sizes by design. This module exists so that
|
|
10
|
+
* difference is a documented, single-sourced choice instead of two numbers that happen to match
|
|
11
|
+
* or drift by accident.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** `SegmentedBar`'s own default geometry (set-level: one row per SET). */
|
|
15
|
+
export const SET_LEVEL_FLAT_BAR = {
|
|
16
|
+
/** Track height (px). */
|
|
17
|
+
height: 8,
|
|
18
|
+
/** Gap between segment slots (px) — also SetStrip's between-set gap, see `SEGMENTED_BAR_GAP`. */
|
|
19
|
+
gap: 5,
|
|
20
|
+
/** Corner radius (px). */
|
|
21
|
+
radius: 2,
|
|
22
|
+
} as const
|
|
23
|
+
|
|
24
|
+
/** `SetBarChart`'s own default geometry (rep-level: one bar per REP within a set). */
|
|
25
|
+
export const REP_LEVEL_FLAT_BAR = {
|
|
26
|
+
/** No fixed height default — SetBarChart's `height` is a required prop set by the caller. */
|
|
27
|
+
height: null,
|
|
28
|
+
/** Inter-bar gap is proportional (`gapRatio · barWidth`), floored at this many px. */
|
|
29
|
+
gapFloor: 2,
|
|
30
|
+
/** Corner radius (px) default. */
|
|
31
|
+
radius: 5,
|
|
32
|
+
} as const
|
|
@@ -161,6 +161,13 @@ export function StepIndicator({
|
|
|
161
161
|
error: 'bg-status-error border-status-error',
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
const glyphStyles: Record<StepStatus, string> = {
|
|
165
|
+
completed: 'text-on-brand-primary',
|
|
166
|
+
active: 'text-on-brand-primary',
|
|
167
|
+
upcoming: 'text-text-secondary',
|
|
168
|
+
error: 'text-on-status-error',
|
|
169
|
+
}
|
|
170
|
+
|
|
164
171
|
return (
|
|
165
172
|
<View
|
|
166
173
|
className={cn(
|
|
@@ -170,12 +177,7 @@ export function StepIndicator({
|
|
|
170
177
|
)}
|
|
171
178
|
>
|
|
172
179
|
{icon || (
|
|
173
|
-
<Text
|
|
174
|
-
className={cn(
|
|
175
|
-
'text-sm font-semibold',
|
|
176
|
-
status === 'upcoming' ? 'text-text-secondary' : 'text-white'
|
|
177
|
-
)}
|
|
178
|
-
>
|
|
180
|
+
<Text className={cn('text-sm font-semibold', glyphStyles[status])}>
|
|
179
181
|
{status === 'completed' ? '✓' : index + 1}
|
|
180
182
|
</Text>
|
|
181
183
|
)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Decorator, Meta, StoryObj } from '@storybook/react-vite'
|
|
2
2
|
import { View, Text } from 'react-native'
|
|
3
3
|
import { Alert, AlertTitle, AlertDescription } from './Alert'
|
|
4
|
+
import { greyRamp } from '../../../theme/tokens/primitives'
|
|
4
5
|
|
|
5
6
|
const meta: Meta<typeof Alert> = {
|
|
6
7
|
title: 'Components/Molecules/Alert',
|
|
@@ -178,7 +179,7 @@ export const DescriptionOnly: Story = {
|
|
|
178
179
|
|
|
179
180
|
/** Wall-background frame for the compact cue stories. */
|
|
180
181
|
const wall: Decorator = (Story) => (
|
|
181
|
-
<View style={{ width: 460, padding: 28, backgroundColor: '
|
|
182
|
+
<View style={{ width: 460, padding: 28, backgroundColor: 'var(--color-background-frame)' }}>
|
|
182
183
|
<Story />
|
|
183
184
|
</View>
|
|
184
185
|
)
|
|
@@ -219,13 +220,22 @@ export const CompactLiveCue: Story = {
|
|
|
219
220
|
/** Default vs compact, side by side, for the density comparison. */
|
|
220
221
|
export const DefaultVsCompact: Story = {
|
|
221
222
|
render: () => (
|
|
222
|
-
<View
|
|
223
|
-
|
|
223
|
+
<View
|
|
224
|
+
style={{
|
|
225
|
+
gap: 16,
|
|
226
|
+
width: 460,
|
|
227
|
+
padding: 28,
|
|
228
|
+
backgroundColor: 'var(--color-background-frame)',
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
<Text style={{ color: greyRamp[700], fontSize: 10, fontWeight: '700' }}>DEFAULT</Text>
|
|
224
232
|
<Alert status="warning" variant="subtle">
|
|
225
233
|
<AlertTitle>Approaching threshold</AlertTitle>
|
|
226
234
|
<AlertDescription>18% velocity loss — one or two reps left in the band.</AlertDescription>
|
|
227
235
|
</Alert>
|
|
228
|
-
<Text style={{ color:
|
|
236
|
+
<Text style={{ color: greyRamp[700], fontSize: 10, fontWeight: '700' }}>
|
|
237
|
+
COMPACT (cue pill)
|
|
238
|
+
</Text>
|
|
229
239
|
<Alert
|
|
230
240
|
status="warning"
|
|
231
241
|
variant="subtle"
|
|
@@ -44,42 +44,54 @@ const statusColors: Record<
|
|
|
44
44
|
subtle: string
|
|
45
45
|
outline: string
|
|
46
46
|
solid: string
|
|
47
|
+
/** Text/glyph colour on the `solid` fill — the status's on-colour. */
|
|
48
|
+
onSolid: string
|
|
47
49
|
border: string
|
|
48
50
|
icon: string
|
|
49
51
|
text: string
|
|
52
|
+
/** Label ON the `subtle` fill — see AW-133; the base token is too deep to read there. */
|
|
53
|
+
subtleText: string
|
|
50
54
|
}
|
|
51
55
|
> = {
|
|
52
56
|
success: {
|
|
53
57
|
subtle: 'bg-status-success-subtle',
|
|
54
58
|
outline: 'border-2 border-status-success bg-transparent',
|
|
55
59
|
solid: 'bg-status-success',
|
|
60
|
+
onSolid: 'text-on-status-success',
|
|
56
61
|
border: 'border-status-success',
|
|
57
62
|
icon: 'text-status-success',
|
|
58
63
|
text: 'text-status-success',
|
|
64
|
+
subtleText: 'text-on-status-success-subtle',
|
|
59
65
|
},
|
|
60
66
|
info: {
|
|
61
67
|
subtle: 'bg-status-info-subtle',
|
|
62
68
|
outline: 'border-2 border-status-info bg-transparent',
|
|
63
69
|
solid: 'bg-status-info',
|
|
70
|
+
onSolid: 'text-on-status-info',
|
|
64
71
|
border: 'border-status-info',
|
|
65
72
|
icon: 'text-status-info',
|
|
66
73
|
text: 'text-status-info',
|
|
74
|
+
subtleText: 'text-on-status-info-subtle',
|
|
67
75
|
},
|
|
68
76
|
warning: {
|
|
69
77
|
subtle: 'bg-status-warning-subtle',
|
|
70
78
|
outline: 'border-2 border-status-warning bg-transparent',
|
|
71
79
|
solid: 'bg-status-warning',
|
|
80
|
+
onSolid: 'text-on-status-warning',
|
|
72
81
|
border: 'border-status-warning',
|
|
73
82
|
icon: 'text-status-warning',
|
|
74
83
|
text: 'text-status-warning',
|
|
84
|
+
subtleText: 'text-on-status-warning-subtle',
|
|
75
85
|
},
|
|
76
86
|
error: {
|
|
77
87
|
subtle: 'bg-status-error-subtle',
|
|
78
88
|
outline: 'border-2 border-status-error bg-transparent',
|
|
79
89
|
solid: 'bg-status-error',
|
|
90
|
+
onSolid: 'text-on-status-error',
|
|
80
91
|
border: 'border-status-error',
|
|
81
92
|
icon: 'text-status-error',
|
|
82
93
|
text: 'text-status-error',
|
|
94
|
+
subtleText: 'text-on-status-error-subtle',
|
|
83
95
|
},
|
|
84
96
|
}
|
|
85
97
|
|
|
@@ -142,7 +154,7 @@ export function Alert({
|
|
|
142
154
|
className={cn(
|
|
143
155
|
'font-bold',
|
|
144
156
|
isCompact ? 'text-base' : 'text-lg',
|
|
145
|
-
isSolid ? '
|
|
157
|
+
isSolid ? colors.onSolid : variant === 'subtle' ? colors.subtleText : colors.icon
|
|
146
158
|
)}
|
|
147
159
|
>
|
|
148
160
|
{defaultIcons[status]}
|
|
@@ -154,7 +166,10 @@ export function Alert({
|
|
|
154
166
|
<View className="flex-1">
|
|
155
167
|
{message != null && (
|
|
156
168
|
<Text
|
|
157
|
-
className={cn(
|
|
169
|
+
className={cn(
|
|
170
|
+
'text-sm font-semibold',
|
|
171
|
+
isSolid ? colors.onSolid : variant === 'subtle' ? colors.subtleText : colors.text
|
|
172
|
+
)}
|
|
158
173
|
testID="alert-message"
|
|
159
174
|
>
|
|
160
175
|
{message}
|
|
@@ -168,9 +183,15 @@ export function Alert({
|
|
|
168
183
|
onPress={onClose}
|
|
169
184
|
accessibilityRole="button"
|
|
170
185
|
accessibilityLabel="Close alert"
|
|
186
|
+
/* VW-82: press-state scrims stay raw (see Modal.tsx). */
|
|
171
187
|
className="ml-2 p-1 rounded web:hover:bg-black/10 active:bg-black/20"
|
|
172
188
|
>
|
|
173
|
-
<Text
|
|
189
|
+
<Text
|
|
190
|
+
className={cn(
|
|
191
|
+
'text-lg',
|
|
192
|
+
isSolid ? cn(colors.onSolid, 'opacity-70') : 'text-text-secondary'
|
|
193
|
+
)}
|
|
194
|
+
>
|
|
174
195
|
×
|
|
175
196
|
</Text>
|
|
176
197
|
</Pressable>
|
|
@@ -10,12 +10,12 @@ export type ButtonColor = 'primary' | 'secondary' | 'success' | 'error' | 'warni
|
|
|
10
10
|
/** Inline color map for RNW where Tailwind text classes get dropped */
|
|
11
11
|
const textColorMap: Record<ButtonVariant, Record<ButtonColor, string>> = {
|
|
12
12
|
solid: {
|
|
13
|
-
primary: '
|
|
14
|
-
secondary: '
|
|
15
|
-
success: '
|
|
16
|
-
error: '
|
|
17
|
-
warning: '
|
|
18
|
-
info: '
|
|
13
|
+
primary: semanticColorsDark['on-brand-primary'],
|
|
14
|
+
secondary: semanticColorsDark['on-brand-secondary'],
|
|
15
|
+
success: semanticColorsDark['on-status-success'],
|
|
16
|
+
error: semanticColorsDark['on-status-error'],
|
|
17
|
+
warning: semanticColorsDark['on-status-warning'],
|
|
18
|
+
info: semanticColorsDark['on-status-info'],
|
|
19
19
|
},
|
|
20
20
|
outline: {
|
|
21
21
|
primary: semanticColorsDark['brand-primary'],
|
|
@@ -128,12 +128,12 @@ const variantStyles: Record<ButtonVariant, Record<ButtonColor, string>> = {
|
|
|
128
128
|
|
|
129
129
|
const textStyles: Record<ButtonVariant, Record<ButtonColor, string>> = {
|
|
130
130
|
solid: {
|
|
131
|
-
primary: 'text-
|
|
132
|
-
secondary: 'text-
|
|
133
|
-
success: 'text-
|
|
134
|
-
error: 'text-
|
|
135
|
-
warning: 'text-
|
|
136
|
-
info: 'text-
|
|
131
|
+
primary: 'text-on-brand-primary',
|
|
132
|
+
secondary: 'text-on-brand-secondary',
|
|
133
|
+
success: 'text-on-status-success',
|
|
134
|
+
error: 'text-on-status-error',
|
|
135
|
+
warning: 'text-on-status-warning',
|
|
136
|
+
info: 'text-on-status-info',
|
|
137
137
|
},
|
|
138
138
|
outline: {
|
|
139
139
|
primary: 'text-brand-primary',
|
|
@@ -93,12 +93,12 @@ export const Checkbox = forwardRef<View, CheckboxProps>(function Checkbox(
|
|
|
93
93
|
)}
|
|
94
94
|
>
|
|
95
95
|
{isChecked && !isIndeterminate && (
|
|
96
|
-
<View className={cn('bg-
|
|
96
|
+
<View className={cn('bg-on-brand-primary rounded-sm', styles.icon)}>
|
|
97
97
|
{/* Checkmark icon - using a simple view as placeholder */}
|
|
98
98
|
<Text className="text-brand-primary font-bold text-center leading-none">✓</Text>
|
|
99
99
|
</View>
|
|
100
100
|
)}
|
|
101
|
-
{isIndeterminate && <View className={cn('bg-
|
|
101
|
+
{isIndeterminate && <View className={cn('bg-on-brand-primary h-0.5 w-2/3 rounded')} />}
|
|
102
102
|
</View>
|
|
103
103
|
|
|
104
104
|
{(label || helperText) && (
|
|
@@ -42,13 +42,17 @@ export const WithCustomValue: Story = {
|
|
|
42
42
|
value={
|
|
43
43
|
<View
|
|
44
44
|
style={{
|
|
45
|
-
backgroundColor: '
|
|
45
|
+
backgroundColor: 'var(--color-status-success)',
|
|
46
46
|
borderRadius: 9999,
|
|
47
47
|
paddingHorizontal: 8,
|
|
48
48
|
paddingVertical: 2,
|
|
49
49
|
}}
|
|
50
50
|
>
|
|
51
|
-
<Text
|
|
51
|
+
<Text
|
|
52
|
+
style={{ color: 'var(--color-on-status-success)', fontSize: 12, fontWeight: '600' }}
|
|
53
|
+
>
|
|
54
|
+
Active
|
|
55
|
+
</Text>
|
|
52
56
|
</View>
|
|
53
57
|
}
|
|
54
58
|
/>
|
|
@@ -103,7 +103,10 @@ export function Drawer({
|
|
|
103
103
|
<Modal visible={isOpen} transparent animationType="fade" onRequestClose={onClose}>
|
|
104
104
|
<View className="flex-1">
|
|
105
105
|
{/* Backdrop */}
|
|
106
|
-
<Pressable
|
|
106
|
+
<Pressable
|
|
107
|
+
onPress={handleOverlayPress}
|
|
108
|
+
className="absolute inset-0 bg-black/50" /* VW-82: scrim, see Modal.tsx */
|
|
109
|
+
/>
|
|
107
110
|
|
|
108
111
|
{/* Drawer Panel — floating: overlay plane + lift, no ring. */}
|
|
109
112
|
<Surface
|
|
@@ -13,13 +13,15 @@ export interface IconBoxProps extends ViewProps {
|
|
|
13
13
|
className?: string
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// `-subtle` is the wash ladder's lightest rung. It replaces a `/10` opacity
|
|
17
|
+
// modifier, which emitted no rule at all against a var()-backed token (VW-308).
|
|
16
18
|
const colorClasses: Record<IconBoxColor, { bg: string; icon: string }> = {
|
|
17
|
-
primary: { bg: 'bg-brand-primary
|
|
18
|
-
secondary: { bg: 'bg-brand-secondary
|
|
19
|
-
success: { bg: 'bg-status-success
|
|
20
|
-
error: { bg: 'bg-status-error
|
|
21
|
-
warning: { bg: 'bg-status-warning
|
|
22
|
-
info: { bg: 'bg-status-info
|
|
19
|
+
primary: { bg: 'bg-brand-primary-subtle', icon: 'text-brand-primary' },
|
|
20
|
+
secondary: { bg: 'bg-brand-secondary-subtle', icon: 'text-brand-secondary' },
|
|
21
|
+
success: { bg: 'bg-status-success-subtle', icon: 'text-status-success' },
|
|
22
|
+
error: { bg: 'bg-status-error-subtle', icon: 'text-status-error' },
|
|
23
|
+
warning: { bg: 'bg-status-warning-subtle', icon: 'text-status-warning' },
|
|
24
|
+
info: { bg: 'bg-status-info-subtle', icon: 'text-status-info' },
|
|
23
25
|
neutral: { bg: 'bg-surface-elevated', icon: 'text-text-secondary' },
|
|
24
26
|
}
|
|
25
27
|
|