@titan-design/react-ui 0.6.0 → 0.8.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 +12 -7
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +12 -7
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +956 -128
- package/dist/index.d.ts +956 -128
- package/dist/index.js +2873 -981
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2826 -982
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-MO0JCySL.d.mts → pages-DCFBqp79.d.mts} +289 -57
- package/dist/{pages-D7Jlssvp.d.ts → pages-_fI3-x7Z.d.ts} +289 -57
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1659 -799
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1660 -800
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +16 -82
- package/dist/theme/index.d.ts +16 -82
- package/dist/theme/index.js +13 -115
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +13 -115
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +14 -6
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +14 -6
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +1 -1
- package/src/arch/Architecture.stories.tsx +870 -0
- package/src/arch/annotations.json +48 -0
- package/src/arch/arch-graph.json +4301 -0
- package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +90 -0
- package/src/components/custom/CircularTimer/CircularTimer.test.tsx +62 -0
- package/src/components/custom/CircularTimer/CircularTimer.tsx +112 -0
- package/src/components/custom/CircularTimer/index.ts +1 -0
- package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
- package/src/components/custom/Metric/Metric.stories.tsx +1 -1
- package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
- package/src/components/custom/Table/Table.stories.tsx +1 -1
- package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
- package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
- package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
- package/src/components/custom/TimerReadout/index.ts +1 -0
- package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
- package/src/components/custom/Typography/Typography.stories.tsx +1 -1
- package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
- package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +9 -15
- package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
- package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
- package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
- package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
- package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
- package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
- package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
- package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
- package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
- package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -5
- package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
- package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
- package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
- package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
- package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
- package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
- package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
- package/src/components/custom/Workout/FatigueMeter.test.tsx +85 -0
- package/src/components/custom/Workout/FatigueMeter.tsx +86 -0
- package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
- package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
- package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
- package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
- package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
- package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
- package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
- package/src/components/custom/Workout/MetricTiles.tsx +48 -0
- package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
- package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
- package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -6
- package/src/components/custom/Workout/README.md +193 -4
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.stories.tsx +133 -3
- package/src/components/custom/Workout/RestTimer.test.tsx +73 -0
- package/src/components/custom/Workout/RestTimer.tsx +155 -59
- package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
- package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
- package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
- package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
- package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
- package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
- package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
- package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
- package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
- package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
- package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
- package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
- package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
- package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
- package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
- package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
- package/src/components/custom/Workout/SessionHeader.tsx +152 -0
- package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
- package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
- package/src/components/custom/Workout/SessionRail.tsx +126 -0
- package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
- package/src/components/custom/Workout/SetBar.test.tsx +123 -0
- package/src/components/custom/Workout/SetBar.tsx +171 -0
- package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
- package/src/components/custom/Workout/SetRow.test.tsx +94 -166
- package/src/components/custom/Workout/SetRow.tsx +169 -232
- package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
- package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
- package/src/components/custom/Workout/SetStrip.tsx +71 -0
- package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
- package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
- package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
- package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
- package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
- package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
- package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
- package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
- package/src/components/custom/Workout/StatusPill.tsx +88 -0
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
- package/src/components/custom/Workout/TempoBar.stories.tsx +70 -1
- package/src/components/custom/Workout/TempoBar.test.tsx +119 -4
- package/src/components/custom/Workout/TempoBar.tsx +107 -22
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
- package/src/components/custom/Workout/TempoDisplay.tsx +23 -33
- package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
- package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
- package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +262 -56
- package/src/components/custom/Workout/VelocityStrip.test.tsx +440 -141
- package/src/components/custom/Workout/VelocityStrip.tsx +776 -141
- package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
- package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
- package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
- package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
- package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
- package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
- package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
- package/src/components/custom/Workout/ZoneTrack.stories.tsx +139 -0
- package/src/components/custom/Workout/ZoneTrack.test.tsx +234 -0
- package/src/components/custom/Workout/ZoneTrack.tsx +373 -0
- package/src/components/custom/Workout/index.ts +55 -2
- package/src/components/custom/Workout/metricText.stories.tsx +69 -0
- package/src/components/custom/Workout/metricText.tsx +34 -0
- package/src/components/custom/Workout/paceTone.test.ts +29 -0
- package/src/components/custom/Workout/paceTone.ts +27 -0
- package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
- package/src/components/custom/index.ts +2 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
- package/src/components/icons/icons.stories.tsx +67 -2
- package/src/components/icons/icons.test.tsx +32 -2
- package/src/components/icons/icons.tsx +108 -0
- package/src/components/shell/BrandLockup.stories.tsx +3 -3
- package/src/components/shell/DashboardShell.stories.tsx +65 -0
- package/src/components/shell/DashboardShell.test.tsx +42 -0
- package/src/components/shell/DashboardShell.tsx +74 -0
- package/src/components/shell/DeviceIndicator.stories.tsx +2 -2
- package/src/components/shell/DeviceMenu.stories.tsx +5 -5
- package/src/components/shell/DeviceRow.stories.tsx +3 -3
- package/src/components/shell/NavItem.stories.tsx +52 -0
- package/src/components/shell/NavItem.test.tsx +35 -0
- package/src/components/shell/NavItem.tsx +75 -0
- package/src/components/shell/README.md +30 -0
- package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
- package/src/components/shell/SessionStatePill.stories.tsx +3 -3
- package/src/components/shell/SessionStatePill.tsx +1 -1
- package/src/components/shell/SideNav.stories.tsx +81 -0
- package/src/components/shell/SideNav.test.tsx +51 -0
- package/src/components/shell/SideNav.tsx +74 -0
- package/src/components/shell/TopBar.stories.tsx +5 -5
- package/src/components/shell/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +86 -3
- package/src/components/ui/alert/Alert.test.tsx +52 -0
- package/src/components/ui/alert/Alert.tsx +54 -20
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
- package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
- package/src/components/ui/badge/Badge.stories.tsx +1 -1
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
- package/src/components/ui/button/Button.stories.tsx +1 -1
- package/src/components/ui/card/Card.stories.tsx +1 -1
- package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
- package/src/components/ui/chip/Chip.stories.tsx +1 -1
- package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
- package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
- package/src/components/ui/divider/Divider.stories.tsx +1 -1
- package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
- package/src/components/ui/form-field/FormField.stories.tsx +1 -1
- package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
- package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/indicator/Indicator.stories.tsx +2 -2
- package/src/components/ui/indicator/Indicator.test.tsx +1 -0
- package/src/components/ui/indicator/Indicator.tsx +6 -1
- package/src/components/ui/input/Input.stories.tsx +1 -1
- package/src/components/ui/link/Link.stories.tsx +1 -1
- package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
- package/src/components/ui/menu/Menu.stories.tsx +1 -1
- package/src/components/ui/modal/Modal.stories.tsx +1 -1
- package/src/components/ui/pill/Pill.stories.tsx +1 -1
- package/src/components/ui/popover/Popover.stories.tsx +1 -1
- package/src/components/ui/progress/Progress.stories.tsx +1 -1
- package/src/components/ui/progress/Progress.tsx +47 -22
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/section/Section.stories.tsx +1 -1
- package/src/components/ui/select/Select.stories.tsx +1 -1
- package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
- package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
- package/src/components/ui/stack/Stack.stories.tsx +1 -1
- package/src/components/ui/surface/Surface.stories.tsx +1 -1
- package/src/components/ui/switch/Switch.stories.tsx +1 -1
- package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
- package/src/components/ui/tile/Tile.stories.tsx +79 -0
- package/src/components/ui/tile/Tile.test.tsx +48 -0
- package/src/components/ui/tile/Tile.tsx +64 -0
- package/src/components/ui/tile/index.ts +2 -0
- package/src/components/ui/toast/Toast.stories.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
- package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
- package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useTimer.test.ts +150 -0
- package/src/hooks/useTimer.ts +77 -0
- package/src/index.ts +3 -0
- package/src/pages.ts +1 -1
- package/src/stories/PresetShowcase.stories.tsx +1 -1
- package/src/stories/ThemePresets.stories.tsx +1 -1
- package/src/theme/ColorSystem.stories.tsx +1 -1
- package/src/theme/Colors.stories.tsx +5 -8
- package/src/theme/config.ts +4 -0
- package/src/theme/elevation.stories.tsx +1 -1
- package/src/theme/global.css +5 -1
- package/src/theme/shadows.stories.tsx +1 -1
- package/src/theme/tokens/primitives.ts +11 -107
- package/src/theme/tokens/semantic.ts +8 -0
- package/src/theme/workout-tokens.ts +11 -7
- package/tailwind.config.js +5 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHEET · Session Rail — set-type encodings (R&D). Throwaway specimen at real rail width, on real tokens.
|
|
3
|
+
* Explores the cases the flat `sets × reps @ load` strip doesn't model:
|
|
4
|
+
* 1. Variable rep-range — range-max segments; the variable zone (floor→max) upcoming part gets its OWN color.
|
|
5
|
+
* 2. Drop set — ONE bar, sub-loads separated by thin NOTCHES (< the between-set gap); reps velocity-colored.
|
|
6
|
+
* 3. Myo-rep / rest-pause — activation chunk + mini-clusters separated by small cluster gaps.
|
|
7
|
+
* Gap hierarchy: butted reps (0) < notch (2, drop sub-load) < cluster gap (3, myo rest-pause) < SET_GAP (5, sets).
|
|
8
|
+
* Hardens into a SetStrip model extension (SetState variants: range/drop/myo) — a post-harden follow-up.
|
|
9
|
+
*/
|
|
10
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
11
|
+
import { Fragment } from 'react'
|
|
12
|
+
import { primitiveColors, primitiveRamps as ramp } from '../../../theme/tokens/primitives'
|
|
13
|
+
import { GREY, RAIL_W, SET_GAP, T_PRIMARY, T_SECONDARY, T_TERTIARY, INSET, velColor, reps, Page, sectionTitle } from './setHeadingKit'
|
|
14
|
+
|
|
15
|
+
// ---- variable-zone (floor→max) upcoming color — real ramp candidates (no invented hex)
|
|
16
|
+
const VAR_GREY = primitiveColors.charcoal[200] // #3C3C3C — lighter charcoal (the paler-grey reference)
|
|
17
|
+
const VAR_BLUE_800 = ramp.blue[800] // #14407E — clearly blue, a touch brighter
|
|
18
|
+
const VAR_BLUE_900 = ramp.blue[900] // #112C5A — dark muted navy, recessive
|
|
19
|
+
const VAR_CYAN_900 = ramp.cyan[900] // #0B3149 — teal alt, sidesteps info-blue adjacency
|
|
20
|
+
const NOTCH = 2 // drop sub-load divider
|
|
21
|
+
const CLUSTER_GAP = 3 // myo rest-pause divider
|
|
22
|
+
|
|
23
|
+
type Chunk = { colors: string[]; gapAfter?: number }
|
|
24
|
+
function Bar({ chunks, h }: { chunks: Chunk[]; h: number }) {
|
|
25
|
+
return (
|
|
26
|
+
<div style={{ display: 'flex', width: '100%', height: h, borderRadius: 2, overflow: 'hidden' }}>
|
|
27
|
+
{chunks.map((ch, ci) => (
|
|
28
|
+
<Fragment key={ci}>
|
|
29
|
+
{ch.colors.map((c, i) => (
|
|
30
|
+
<div key={i} style={{ flex: 1, minWidth: 0, height: '100%', background: c }} />
|
|
31
|
+
))}
|
|
32
|
+
{ci < chunks.length - 1 && <div style={{ flex: `0 0 ${ch.gapAfter ?? NOTCH}px`, height: '100%', background: INSET }} />}
|
|
33
|
+
</Fragment>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---- 1. variable rep-range: `max` segments; committed 0..floor, variable floor..max; variable-todo gets VAR color
|
|
40
|
+
function rangeColors(floor: number, max: number, doneVels: number[], varColor: string): string[] {
|
|
41
|
+
return Array.from({ length: max }, (_, i) => {
|
|
42
|
+
if (i < doneVels.length) return velColor(doneVels[i])
|
|
43
|
+
return i < floor ? GREY : varColor
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
function RangeBar({ floor, max, done, varColor, h }: { floor: number; max: number; done: number[]; varColor: string; h: number }) {
|
|
47
|
+
return <Bar chunks={[{ colors: rangeColors(floor, max, done, varColor) }]} h={h} />
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---- 2. drop set: one bar, sub-loads butted internally, separated by thin notches
|
|
51
|
+
function DropBar({ subloads, h }: { subloads: number[][]; h: number }) {
|
|
52
|
+
const chunks: Chunk[] = subloads.map((vels, i) => ({ colors: vels.map(velColor), gapAfter: NOTCH, ...(i === subloads.length - 1 ? { gapAfter: undefined } : {}) }))
|
|
53
|
+
return <Bar chunks={chunks} h={h} />
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- 3. myo-rep: activation chunk + mini-clusters, separated by cluster gaps
|
|
57
|
+
function MyoBar({ activation, clusters, h }: { activation: number[]; clusters: number[][]; h: number }) {
|
|
58
|
+
const chunks: Chunk[] = [{ colors: activation.map(velColor), gapAfter: CLUSTER_GAP }, ...clusters.map((c) => ({ colors: c.map(velColor), gapAfter: CLUSTER_GAP }))]
|
|
59
|
+
return <Bar chunks={chunks} h={h} />
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---- 3b. UPCOMING myo, unknown length: grey activation (expected) + a blue "clusters-to-failure" trail
|
|
63
|
+
// that fades and leaves the right edge open — signalling "N more clusters, count unknown".
|
|
64
|
+
function MyoUpcomingBar({ activationLen, blue, h }: { activationLen: number; blue: string; h: number }) {
|
|
65
|
+
const fades = [1, 0.6, 0.32] // decreasing opacity per expected cluster → "and maybe more"
|
|
66
|
+
return (
|
|
67
|
+
<div style={{ display: 'flex', width: '100%', height: h, gap: 0 }}>
|
|
68
|
+
<div style={{ display: 'flex', flex: activationLen, height: '100%', borderRadius: '2px 0 0 2px', overflow: 'hidden' }}>
|
|
69
|
+
{Array.from({ length: activationLen }, (_, i) => (
|
|
70
|
+
<div key={i} style={{ flex: 1, height: '100%', background: GREY }} />
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
{fades.map((op, ci) => (
|
|
74
|
+
<Fragment key={ci}>
|
|
75
|
+
<div style={{ flex: `0 0 ${CLUSTER_GAP}px`, height: '100%', background: INSET }} />
|
|
76
|
+
<div style={{ display: 'flex', flex: 5, height: '100%', overflow: 'hidden', opacity: op }}>
|
|
77
|
+
{Array.from({ length: 5 }, (_, i) => (
|
|
78
|
+
<div key={i} style={{ flex: 1, height: '100%', background: blue }} />
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
</Fragment>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---- row scaffold at real rail width, on the sunk inset surface
|
|
88
|
+
function Row({ name, meta, children }: { name: string; meta: string; children: React.ReactNode }) {
|
|
89
|
+
return (
|
|
90
|
+
<div style={{ width: RAIL_W, background: INSET, padding: '9px 12px', borderRadius: 4 }}>
|
|
91
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
|
|
92
|
+
<span style={{ fontSize: 14, fontWeight: 800, color: T_PRIMARY }}>{name}</span>
|
|
93
|
+
<div style={{ flex: 1 }} />
|
|
94
|
+
<span style={{ fontSize: 10, fontFamily: 'monospace', color: T_TERTIARY }}>{meta}</span>
|
|
95
|
+
</div>
|
|
96
|
+
{children}
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
function Col({ label, children }: { label: string; children: React.ReactNode }) {
|
|
101
|
+
return (
|
|
102
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
103
|
+
<span style={{ ...sectionTitle, color: T_SECONDARY }}>{label}</span>
|
|
104
|
+
{children}
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const H = 8
|
|
110
|
+
|
|
111
|
+
function Sheet() {
|
|
112
|
+
return (
|
|
113
|
+
<Page title="Session Rail — set-type encodings" note="R&D for prescriptions the flat sets×reps strip doesn't model. Rail width, real velocity ramp pins. Rep-range: range-max segments, the variable zone (floor→max) upcoming part gets its own color. Drop: one bar, sub-loads split by thin notches. Myo: activation + mini-clusters split by cluster gaps.">
|
|
114
|
+
<div style={{ display: 'flex', gap: 40, flexWrap: 'wrap', alignItems: 'flex-start' }}>
|
|
115
|
+
{/* 1. variable rep-range — real ramp candidates for the variable zone */}
|
|
116
|
+
<Col label="1 · variable rep-range — LOCKED: cyan 900">
|
|
117
|
+
{([
|
|
118
|
+
['grey · charcoal 200', VAR_GREY],
|
|
119
|
+
['blue 800 · #14407E', VAR_BLUE_800],
|
|
120
|
+
['blue 900 · #112C5A', VAR_BLUE_900],
|
|
121
|
+
['cyan 900 · #0B3149', VAR_CYAN_900],
|
|
122
|
+
] as const).map(([label, c]) => (
|
|
123
|
+
<Row key={label} name="Face Pull" meta="3 × 15–20 @ 40">
|
|
124
|
+
<span style={{ fontSize: 9, color: T_TERTIARY, display: 'block', marginBottom: 5 }}>{label}</span>
|
|
125
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: SET_GAP }}>
|
|
126
|
+
<RangeBar floor={15} max={20} done={[]} varColor={c} h={H} />
|
|
127
|
+
<RangeBar floor={15} max={20} done={reps(12, 0.9)} varColor={c} h={H} />
|
|
128
|
+
<RangeBar floor={15} max={20} done={reps(17, 0.8)} varColor={c} h={H} />
|
|
129
|
+
</div>
|
|
130
|
+
</Row>
|
|
131
|
+
))}
|
|
132
|
+
<span style={{ fontSize: 10, color: T_TERTIARY, maxWidth: RAIL_W }}>Each block: todo · 12 done (committed) · 17 done (into the variable zone). Variable-todo = the swatch; committed-todo = grey.</span>
|
|
133
|
+
</Col>
|
|
134
|
+
|
|
135
|
+
{/* 2. drop set */}
|
|
136
|
+
<Col label="2 · drop set">
|
|
137
|
+
<Row name="Lat Pulldown" meta="1 drop · 10→8→6 @ 120→80">
|
|
138
|
+
<DropBar subloads={[reps(10, 0.85), reps(8, 0.7), reps(6, 0.6)]} h={H} />
|
|
139
|
+
</Row>
|
|
140
|
+
<Row name="Leg Extension" meta="1 drop · 12→10→8→6 @ 90→45">
|
|
141
|
+
<DropBar subloads={[reps(12, 0.8), reps(10, 0.7), reps(8, 0.62), reps(6, 0.55)]} h={H} />
|
|
142
|
+
</Row>
|
|
143
|
+
<span style={{ fontSize: 10, color: T_TERTIARY, maxWidth: RAIL_W }}>One set, no rest; notches mark each load drop. Notch (2px) < set gap (5px) so drops read as sub-structure of a single set.</span>
|
|
144
|
+
</Col>
|
|
145
|
+
|
|
146
|
+
{/* 3. myo-rep */}
|
|
147
|
+
<Col label="3 · myo-rep / rest-pause">
|
|
148
|
+
<Row name="Cable Curl" meta="myo · done · 15 + 5 + 5 + 5">
|
|
149
|
+
<MyoBar activation={reps(15, 0.75)} clusters={[reps(5, 0.6), reps(5, 0.55), reps(5, 0.5)]} h={H} />
|
|
150
|
+
</Row>
|
|
151
|
+
<Row name="Triceps Pushdown" meta="myo · done · 12 + 4 + 4">
|
|
152
|
+
<MyoBar activation={reps(12, 0.7)} clusters={[reps(4, 0.55), reps(4, 0.5)]} h={H} />
|
|
153
|
+
</Row>
|
|
154
|
+
<span style={{ fontSize: 10, color: T_TERTIARY, maxWidth: RAIL_W }}>Done: activation to near-failure, then rest-pause clusters. Cluster gap (3px) between notch and set gap.</span>
|
|
155
|
+
<span style={{ ...sectionTitle, color: T_SECONDARY, marginTop: 6 }}>upcoming · length unknown</span>
|
|
156
|
+
<Row name="Cable Curl" meta="myo · ~15 + clusters to failure">
|
|
157
|
+
<MyoUpcomingBar activationLen={15} blue={VAR_CYAN_900} h={H} />
|
|
158
|
+
</Row>
|
|
159
|
+
<Row name="Triceps Pushdown" meta="myo · ~12 + clusters to failure">
|
|
160
|
+
<MyoUpcomingBar activationLen={12} blue={VAR_CYAN_900} h={H} />
|
|
161
|
+
</Row>
|
|
162
|
+
<span style={{ fontSize: 10, color: T_TERTIARY, maxWidth: RAIL_W }}>{'Grey activation (expected) + blue "clusters-to-failure" trail that fades and leaves the right edge open — reuses the variable/opportunity blue, signalling "more coming, count unknown."'}</span>
|
|
163
|
+
</Col>
|
|
164
|
+
</div>
|
|
165
|
+
</Page>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const meta: Meta = {
|
|
170
|
+
title: 'Lab/Explorations/Set Types',
|
|
171
|
+
parameters: { layout: 'fullscreen' },
|
|
172
|
+
}
|
|
173
|
+
export default meta
|
|
174
|
+
type Story = StoryObj
|
|
175
|
+
|
|
176
|
+
export const Encodings: Story = { render: () => <Sheet /> }
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHEET · Workout Expansion (page 2) — what a rail item reveals when expanded.
|
|
3
|
+
* Anchored on the REAL unexpanded rail row (`ExerciseCardHeading`) as the persistent header, then the
|
|
4
|
+
* revealed body in two REAL-component directions so the fork is concrete:
|
|
5
|
+
* A — Table (real `SetTableHeader` + real `SetRow`s — the current expanded body).
|
|
6
|
+
* C — Embed (real `VelocityStrip` full/expanded per set + a compact caption — the bar IS the set).
|
|
7
|
+
* B (drop PREV) is a trivial prop on the table, noted but not forked here. Same data across both: set 1 done
|
|
8
|
+
* · set 2 LIVE 5/10 · set 3 upcoming, with velocities that DECAY within a set so the "shape" reading is real.
|
|
9
|
+
* OPEN: A-vs-C · live-set treatment in the body · read-only density (drop TEMPO row? per-row strips?). DECISIONS doc.
|
|
10
|
+
*/
|
|
11
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
12
|
+
import { View, Text } from 'react-native'
|
|
13
|
+
import { ExerciseCardHeading } from './ExerciseCardHeading'
|
|
14
|
+
import { SetTableHeader } from './SetTableHeader'
|
|
15
|
+
import { SetRow, type SetRowProps } from './SetRow'
|
|
16
|
+
import { VelocityStrip } from './VelocityStrip'
|
|
17
|
+
import { type SetStripSet } from './SetStrip'
|
|
18
|
+
import {
|
|
19
|
+
INSET,
|
|
20
|
+
INSET_SHADOW,
|
|
21
|
+
RAISED,
|
|
22
|
+
BORDER_SUBTLE,
|
|
23
|
+
GREEN,
|
|
24
|
+
T_PRIMARY,
|
|
25
|
+
T_SECONDARY,
|
|
26
|
+
T_TERTIARY,
|
|
27
|
+
Page,
|
|
28
|
+
sectionTitle,
|
|
29
|
+
monoTag,
|
|
30
|
+
} from './setHeadingKit'
|
|
31
|
+
|
|
32
|
+
// Per-rep mean velocity that DECAYS across a set (fast → slow) so bar height/color carries shape.
|
|
33
|
+
const decay = (n: number, start: number, span = 0.5) =>
|
|
34
|
+
Array.from({ length: n }, (_, r) => +(start - (span * r) / Math.max(1, n - 1)).toFixed(3))
|
|
35
|
+
|
|
36
|
+
// ---- shared data (real prop shapes) ------------------------------------------------
|
|
37
|
+
// Heading strip (SetStripSet[]): set1 done · set2 active 5/10 · set3 todo.
|
|
38
|
+
const HEADING_STATES: SetStripSet[] = [
|
|
39
|
+
{ status: 'done', velocities: decay(10, 1.05) },
|
|
40
|
+
{ status: 'active', velocities: decay(5, 0.95, 0.3), planned: 10 },
|
|
41
|
+
{ status: 'todo', planned: 10 },
|
|
42
|
+
]
|
|
43
|
+
// Expanded-body per-set rows (real SetRowProps): set 1 done · set 2 LIVE · set 3 todo.
|
|
44
|
+
const BODY_SETS: SetRowProps[] = [
|
|
45
|
+
{
|
|
46
|
+
state: 'done',
|
|
47
|
+
setNumber: 1,
|
|
48
|
+
reps: 10,
|
|
49
|
+
weight: 90,
|
|
50
|
+
rpe: 7.5,
|
|
51
|
+
unit: 'lbs',
|
|
52
|
+
velocities: decay(10, 1.05),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
state: 'live',
|
|
56
|
+
setNumber: 2,
|
|
57
|
+
unit: 'lbs',
|
|
58
|
+
target: { reps: 10, weight: 90 },
|
|
59
|
+
reps: 10,
|
|
60
|
+
weight: 90,
|
|
61
|
+
velocities: decay(5, 0.95, 0.3),
|
|
62
|
+
},
|
|
63
|
+
{ state: 'todo', setNumber: 3, unit: 'lbs', target: { reps: 10, weight: 90 } },
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
// ---- the real unexpanded rail row, used as the persistent expansion header ---------
|
|
67
|
+
function RailHeader() {
|
|
68
|
+
return (
|
|
69
|
+
<View style={{ position: 'relative', zIndex: 2, backgroundColor: RAISED }}>
|
|
70
|
+
<ExerciseCardHeading
|
|
71
|
+
name="Cable Chest Press"
|
|
72
|
+
sets={3}
|
|
73
|
+
reps={10}
|
|
74
|
+
load={90}
|
|
75
|
+
unit="lbs"
|
|
76
|
+
tempo={[2, 1, 2, 0]}
|
|
77
|
+
indicator="info"
|
|
78
|
+
setStates={HEADING_STATES}
|
|
79
|
+
/>
|
|
80
|
+
</View>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---- A · table body: real SetTableHeader + real SetRow (LOCKED: drop PREV, keep strips)
|
|
85
|
+
function TableBody() {
|
|
86
|
+
return (
|
|
87
|
+
<View>
|
|
88
|
+
<SetTableHeader unit="lbs" showPrevious={false} />
|
|
89
|
+
{BODY_SETS.map((s, i) => (
|
|
90
|
+
<SetRow key={i} {...s} />
|
|
91
|
+
))}
|
|
92
|
+
</View>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---- C · embed body: real VelocityStrip (full/expanded) per set + compact caption ---
|
|
97
|
+
const capText = {
|
|
98
|
+
fontFamily: 'Inter, sans-serif',
|
|
99
|
+
fontSize: 11,
|
|
100
|
+
fontWeight: '600' as const,
|
|
101
|
+
color: T_PRIMARY,
|
|
102
|
+
}
|
|
103
|
+
const dimText = { ...capText, color: T_SECONDARY, fontWeight: '500' as const }
|
|
104
|
+
function EmbedSetRow({ s }: { s: SetRowProps }) {
|
|
105
|
+
const live = s.state === 'live'
|
|
106
|
+
const todo = s.state === 'todo'
|
|
107
|
+
const vels = s.state === 'done' || s.state === 'live' ? s.velocities : []
|
|
108
|
+
const rpe = s.state === 'todo' ? null : s.rpe
|
|
109
|
+
return (
|
|
110
|
+
<View
|
|
111
|
+
style={{
|
|
112
|
+
paddingVertical: 7,
|
|
113
|
+
paddingHorizontal: 10,
|
|
114
|
+
marginHorizontal: 2,
|
|
115
|
+
marginBottom: 4,
|
|
116
|
+
borderRadius: 8,
|
|
117
|
+
opacity: todo ? 0.55 : 1,
|
|
118
|
+
...(live
|
|
119
|
+
? {
|
|
120
|
+
backgroundColor: 'rgba(46,213,115,0.06)',
|
|
121
|
+
borderWidth: 1,
|
|
122
|
+
borderColor: 'rgba(46,213,115,0.30)',
|
|
123
|
+
}
|
|
124
|
+
: {}),
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
<View style={{ flexDirection: 'row', alignItems: 'baseline', marginBottom: 6 }}>
|
|
128
|
+
<Text style={{ ...dimText, width: 22, color: T_TERTIARY }}>{s.setNumber}</Text>
|
|
129
|
+
{s.state === 'live' ? (
|
|
130
|
+
<Text style={{ ...capText, color: GREEN, fontWeight: '700' }}>
|
|
131
|
+
{vels.length}/{s.target.reps} × {s.target.weight}
|
|
132
|
+
</Text>
|
|
133
|
+
) : s.state === 'done' ? (
|
|
134
|
+
<Text style={capText}>
|
|
135
|
+
{s.reps} × {s.weight}
|
|
136
|
+
</Text>
|
|
137
|
+
) : (
|
|
138
|
+
<Text style={dimText}>
|
|
139
|
+
{s.target.reps} × {s.target.weight}
|
|
140
|
+
</Text>
|
|
141
|
+
)}
|
|
142
|
+
<View style={{ flex: 1 }} />
|
|
143
|
+
{rpe != null && <Text style={dimText}>RPE {rpe}</Text>}
|
|
144
|
+
{live && <Text style={{ ...capText, color: GREEN, fontWeight: '700' }}>live</Text>}
|
|
145
|
+
</View>
|
|
146
|
+
{vels.length > 0 ? (
|
|
147
|
+
<VelocityStrip velocities={vels} variant="expanded" showInfo={false} />
|
|
148
|
+
) : (
|
|
149
|
+
<View style={{ height: 24, flexDirection: 'row', gap: 2 }}>
|
|
150
|
+
{Array.from({ length: s.state === 'todo' ? s.target.reps : 10 }).map((_, i) => (
|
|
151
|
+
<View
|
|
152
|
+
key={i}
|
|
153
|
+
style={{
|
|
154
|
+
flex: 1,
|
|
155
|
+
height: 6,
|
|
156
|
+
alignSelf: 'flex-end',
|
|
157
|
+
backgroundColor: '#2C2C2C',
|
|
158
|
+
borderRadius: 1,
|
|
159
|
+
opacity: 0.5,
|
|
160
|
+
}}
|
|
161
|
+
/>
|
|
162
|
+
))}
|
|
163
|
+
</View>
|
|
164
|
+
)}
|
|
165
|
+
</View>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
function EmbedBody() {
|
|
169
|
+
return (
|
|
170
|
+
<View style={{ paddingTop: 6 }}>
|
|
171
|
+
{BODY_SETS.map((s, i) => (
|
|
172
|
+
<EmbedSetRow key={i} s={s} />
|
|
173
|
+
))}
|
|
174
|
+
</View>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---- expanded item = real rail header + revealed body ------------------------------
|
|
179
|
+
function ExpandedItem({ label, children }: { label: string; children: React.ReactNode }) {
|
|
180
|
+
return (
|
|
181
|
+
<View style={{ gap: 8 }}>
|
|
182
|
+
<Text style={{ ...monoTag, color: T_SECONDARY }}>{label}</Text>
|
|
183
|
+
<View
|
|
184
|
+
style={
|
|
185
|
+
{
|
|
186
|
+
width: 260,
|
|
187
|
+
backgroundColor: INSET,
|
|
188
|
+
boxShadow: INSET_SHADOW,
|
|
189
|
+
borderRadius: 4,
|
|
190
|
+
overflow: 'hidden',
|
|
191
|
+
} as object
|
|
192
|
+
}
|
|
193
|
+
>
|
|
194
|
+
<RailHeader />
|
|
195
|
+
<View
|
|
196
|
+
style={{
|
|
197
|
+
backgroundColor: INSET,
|
|
198
|
+
paddingBottom: 8,
|
|
199
|
+
borderTopWidth: 1,
|
|
200
|
+
borderTopColor: BORDER_SUBTLE,
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
{children}
|
|
204
|
+
</View>
|
|
205
|
+
</View>
|
|
206
|
+
</View>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const meta: Meta = {
|
|
211
|
+
title: 'Lab/Explorations/Workout Expansion',
|
|
212
|
+
parameters: { layout: 'fullscreen' },
|
|
213
|
+
}
|
|
214
|
+
export default meta
|
|
215
|
+
type Story = StoryObj
|
|
216
|
+
|
|
217
|
+
export const DirectionStudy: Story = {
|
|
218
|
+
name: 'Expanded item · A table / C embed-in-bar (real components)',
|
|
219
|
+
render: () => (
|
|
220
|
+
<Page
|
|
221
|
+
title="Workout expansion — direction study (page 2)"
|
|
222
|
+
note="LOCKED (2026-07-10): body = direction A, the SetTableHeader+SetRow table, with PREV dropped (real showPrevious={false}) and the per-row velocity strips kept. C (per-rep VelocityStrip becomes the set row) shown for the record — rejected as too novel; the table already carries velocity shape via the per-row strip. Anchored on the REAL unexpanded rail row (ExerciseCardHeading) as the persistent header; data: set 1 done · set 2 live 5/10 · set 3 upcoming, velocities decaying within a set."
|
|
223
|
+
>
|
|
224
|
+
<View style={{ flexDirection: 'row', gap: 44, alignItems: 'flex-start', flexWrap: 'wrap' }}>
|
|
225
|
+
<View style={{ gap: 12 }}>
|
|
226
|
+
<Text style={sectionTitle}>A · Table (current body)</Text>
|
|
227
|
+
<ExpandedItem label="real SetTableHeader + SetRow">
|
|
228
|
+
<TableBody />
|
|
229
|
+
</ExpandedItem>
|
|
230
|
+
</View>
|
|
231
|
+
<View style={{ gap: 12 }}>
|
|
232
|
+
<Text style={sectionTitle}>C · Embed in the bar</Text>
|
|
233
|
+
<ExpandedItem label="real VelocityStrip (full) + caption">
|
|
234
|
+
<EmbedBody />
|
|
235
|
+
</ExpandedItem>
|
|
236
|
+
</View>
|
|
237
|
+
</View>
|
|
238
|
+
</Page>
|
|
239
|
+
),
|
|
240
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { ScheduleTiles } from './ScheduleTiles'
|
|
3
|
+
|
|
4
|
+
const HOUR = 60 * 60 * 1000
|
|
5
|
+
const DAY = 24 * HOUR
|
|
6
|
+
|
|
7
|
+
// Deterministic reference: fix `now` and offset `when` from it so the `soon`
|
|
8
|
+
// accent (and the relative Until text) is stable across renders.
|
|
9
|
+
const NOW = Date.now()
|
|
10
|
+
|
|
11
|
+
const meta: Meta<typeof ScheduleTiles> = {
|
|
12
|
+
title: 'Workout/ScheduleTiles',
|
|
13
|
+
component: ScheduleTiles,
|
|
14
|
+
tags: ['autodocs'],
|
|
15
|
+
parameters: {
|
|
16
|
+
docs: {
|
|
17
|
+
description: {
|
|
18
|
+
component:
|
|
19
|
+
'**Molecule.** An HStack of [Tile](?path=/docs/components-atoms-tile--docs); Until reuses [DateTime](?path=/docs/custom-datetime--docs) relative. Used-by ↑ SessionHeader.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default meta
|
|
26
|
+
type Story = StoryObj<typeof ScheduleTiles>
|
|
27
|
+
|
|
28
|
+
export const TwoDaysOut: Story = {
|
|
29
|
+
args: {
|
|
30
|
+
now: NOW,
|
|
31
|
+
when: NOW + 2 * DAY,
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const SoonUnderADay: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
now: NOW,
|
|
38
|
+
when: NOW + 5 * HOUR,
|
|
39
|
+
},
|
|
40
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { ScheduleTiles, compactUntil } from './ScheduleTiles'
|
|
4
|
+
import { primitiveRamps } from '../../../theme/tokens/primitives'
|
|
5
|
+
|
|
6
|
+
const HOUR = 60 * 60 * 1000
|
|
7
|
+
const DAY = 24 * HOUR
|
|
8
|
+
const ORANGE = primitiveRamps.orange[400]
|
|
9
|
+
|
|
10
|
+
// A fixed reference; the compact Until label is deterministic from (when - now).
|
|
11
|
+
const NOW = Date.now()
|
|
12
|
+
|
|
13
|
+
describe('ScheduleTiles', () => {
|
|
14
|
+
it('renders Date, Time, and Until tiles', () => {
|
|
15
|
+
render(<ScheduleTiles now={NOW} when={NOW + 2 * DAY} />)
|
|
16
|
+
expect(screen.getByText('Date')).toBeInTheDocument()
|
|
17
|
+
expect(screen.getByText('Time')).toBeInTheDocument()
|
|
18
|
+
expect(screen.getByText('Until')).toBeInTheDocument()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('does not accent the Until value when more than a day away', () => {
|
|
22
|
+
const when = NOW + 2 * DAY
|
|
23
|
+
render(<ScheduleTiles now={NOW} when={when} />)
|
|
24
|
+
const value = screen.getByText(compactUntil(when - NOW))
|
|
25
|
+
expect(value.style.color).toBe('')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('accents the Until value orange when less than a day away', () => {
|
|
29
|
+
const when = NOW + 5 * HOUR
|
|
30
|
+
render(<ScheduleTiles now={NOW} when={when} />)
|
|
31
|
+
const value = screen.getByText(compactUntil(when - NOW))
|
|
32
|
+
expect(value).toHaveStyle({ color: ORANGE })
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { type ViewProps } from 'react-native'
|
|
3
|
+
import { primitiveRamps } from '../../../theme/tokens/primitives'
|
|
4
|
+
import { HStack } from '../../ui/stack'
|
|
5
|
+
import { Tile } from '../../ui/tile/Tile'
|
|
6
|
+
import { formatDateTime } from '../DateTime/DateTime'
|
|
7
|
+
|
|
8
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
9
|
+
|
|
10
|
+
/** Accent applied to the Until value when the session is < 1 day away. */
|
|
11
|
+
const SOON_ACCENT = primitiveRamps.orange[400]
|
|
12
|
+
|
|
13
|
+
/** Compact time-until label: `30m` / `5h` / `2d` / `3w`. Deterministic from the delta. */
|
|
14
|
+
export function compactUntil(ms: number): string {
|
|
15
|
+
if (ms <= 0) return 'now'
|
|
16
|
+
const min = Math.round(ms / 60000)
|
|
17
|
+
if (min < 60) return `${min}m`
|
|
18
|
+
const hr = Math.round(min / 60)
|
|
19
|
+
if (hr < 24) return `${hr}h`
|
|
20
|
+
const d = Math.round(hr / 24)
|
|
21
|
+
if (d < 7) return `${d}d`
|
|
22
|
+
return `${Math.round(d / 7)}w`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type Gap = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12
|
|
26
|
+
|
|
27
|
+
export interface ScheduleTilesProps extends ViewProps {
|
|
28
|
+
/** The scheduled moment (timestamp in ms or Date object). */
|
|
29
|
+
when: number | Date
|
|
30
|
+
/** Reference "now"; injectable so the until/accent calc is deterministic in tests. Defaults to the mount time. */
|
|
31
|
+
now?: number
|
|
32
|
+
/** Gap between the tiles (default 1). */
|
|
33
|
+
gap?: Gap
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ScheduleTiles — Date / Time / Until row for an upcoming session.
|
|
38
|
+
*
|
|
39
|
+
* An HStack of three {@link Tile}s derived from a single scheduled moment. The
|
|
40
|
+
* Until value reuses {@link formatDateTime} relative formatting and turns
|
|
41
|
+
* accent-orange only when the session is less than a day away.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* <ScheduleTiles when={Date.now() + 5 * 60 * 60 * 1000} />
|
|
45
|
+
*/
|
|
46
|
+
export function ScheduleTiles({ when, now, gap = 1, ...props }: ScheduleTilesProps) {
|
|
47
|
+
// Capture a stable mount-time "now" when the prop is omitted (Date.now() is
|
|
48
|
+
// impure, so it lives in a hook initializer per the render-purity rule).
|
|
49
|
+
const [mountNow] = useState(() => Date.now())
|
|
50
|
+
const reference = now ?? mountNow
|
|
51
|
+
const whenMs = when instanceof Date ? when.getTime() : when
|
|
52
|
+
const soon = whenMs - reference < DAY_MS
|
|
53
|
+
const time = new Date(whenMs).toLocaleTimeString(undefined, {
|
|
54
|
+
hour: 'numeric',
|
|
55
|
+
minute: '2-digit',
|
|
56
|
+
hour12: true,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<HStack gap={gap} {...props}>
|
|
61
|
+
<Tile label="Date" value={formatDateTime(when, 'short')} />
|
|
62
|
+
<Tile label="Time" value={time} />
|
|
63
|
+
<Tile
|
|
64
|
+
label="Until"
|
|
65
|
+
value={compactUntil(whenMs - reference)}
|
|
66
|
+
valueColor={soon ? SOON_ACCENT : undefined}
|
|
67
|
+
/>
|
|
68
|
+
</HStack>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { SegmentedBar } from './SegmentedBar'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `SegmentedBar` — a horizontal track split into weighted, individually-fillable
|
|
7
|
+
* segments. Each segment is a flex slot holding a left-aligned colour fill; `pulse`
|
|
8
|
+
* segments breathe on a shared active-pulse loop and an optional `marker` drops a
|
|
9
|
+
* vertical line at a fraction of the width. Colours are passed-in literal hex.
|
|
10
|
+
*/
|
|
11
|
+
const meta: Meta<typeof SegmentedBar> = {
|
|
12
|
+
title: 'Workout/SegmentedBar',
|
|
13
|
+
component: SegmentedBar,
|
|
14
|
+
tags: ['autodocs'],
|
|
15
|
+
parameters: {
|
|
16
|
+
docs: {
|
|
17
|
+
description: {
|
|
18
|
+
component:
|
|
19
|
+
'**Atom.** A horizontal track split into weighted, individually-fillable ' +
|
|
20
|
+
'segments (weight · fill · colour · pulse) with an optional marker line — the ' +
|
|
21
|
+
'presentational primitive the per-set strips build on. ' +
|
|
22
|
+
'Used-by ↑ [SetStrip](?path=/docs/workout-setstrip--docs).',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
argTypes: {
|
|
27
|
+
height: { control: { type: 'range', min: 2, max: 24, step: 1 } },
|
|
28
|
+
gap: { control: { type: 'range', min: 0, max: 12, step: 1 } },
|
|
29
|
+
radius: { control: { type: 'range', min: 0, max: 12, step: 1 } },
|
|
30
|
+
},
|
|
31
|
+
decorators: [
|
|
32
|
+
(Story) => (
|
|
33
|
+
<View style={{ width: 240, padding: 16, backgroundColor: '#131313' }}>
|
|
34
|
+
<Story />
|
|
35
|
+
</View>
|
|
36
|
+
),
|
|
37
|
+
],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default meta
|
|
41
|
+
type Story = StoryObj<typeof SegmentedBar>
|
|
42
|
+
|
|
43
|
+
export const EqualSegments: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
height: 8,
|
|
46
|
+
segments: [
|
|
47
|
+
{ color: '#D14343' },
|
|
48
|
+
{ color: '#FF7900' },
|
|
49
|
+
{ color: '#F9B415' },
|
|
50
|
+
{ color: '#2ED573' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const Weighted: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
height: 12,
|
|
58
|
+
gap: 4,
|
|
59
|
+
radius: 6,
|
|
60
|
+
segments: [
|
|
61
|
+
{ color: '#2ED573', weight: 3 },
|
|
62
|
+
{ color: '#F9B415', weight: 2 },
|
|
63
|
+
{ color: '#D14343', weight: 1 },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
parameters: {
|
|
67
|
+
docs: { description: { story: 'Segments sized by `weight`, with a gap and rounded slots.' } },
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const PartialFills: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
height: 12,
|
|
74
|
+
gap: 4,
|
|
75
|
+
radius: 6,
|
|
76
|
+
segments: [
|
|
77
|
+
{ color: '#2ED573', fill: 1 },
|
|
78
|
+
{ color: '#F9B415', fill: 0.6 },
|
|
79
|
+
{ color: '#01B5D1', fill: 0.3, pulse: true },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
parameters: {
|
|
83
|
+
docs: {
|
|
84
|
+
description: { story: 'Each slot fills a fraction of its width; the last one pulses.' },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const WithMarker: Story = {
|
|
90
|
+
args: {
|
|
91
|
+
height: 10,
|
|
92
|
+
radius: 5,
|
|
93
|
+
segments: [{ color: '#01B5D1', fill: 0.7 }],
|
|
94
|
+
marker: { position: 0.5, color: '#FFFFFF' },
|
|
95
|
+
},
|
|
96
|
+
parameters: {
|
|
97
|
+
docs: { description: { story: 'A single fill with a target marker at the halfway point.' } },
|
|
98
|
+
},
|
|
99
|
+
}
|