@titan-design/react-ui 0.12.1 → 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 +60 -47
- package/dist/bodymap.js +856 -736
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +858 -738
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +1431 -237
- package/dist/index.d.ts +1431 -237
- package/dist/index.js +6506 -4781
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6280 -4642
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-B1hqnGIC.d.ts → pages-CsjsUn0G.d.ts} +51 -16
- package/dist/{pages-CvsFqNtx.d.mts → pages-ZbT_pD_U.d.mts} +51 -16
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1919 -1937
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1927 -1945
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +225 -140
- package/dist/theme/index.d.ts +225 -140
- package/dist/theme/index.js +298 -319
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +283 -315
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +145 -24
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +145 -24
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/dist/theme/tokens.d.mts +128 -11
- package/dist/theme/tokens.d.ts +128 -11
- package/dist/theme/tokens.js +126 -26
- package/dist/theme/tokens.js.map +1 -1
- package/dist/theme/tokens.mjs +124 -27
- package/dist/theme/tokens.mjs.map +1 -1
- package/docs/audits/2026-09-08-critique-addenda.md +51 -0
- package/docs/audits/2026-09-08-library-critique.md +212 -0
- package/docs/library-roadmap.md +99 -0
- 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.freshness.test.ts +52 -0
- package/src/arch/arch-graph.json +4989 -1583
- package/src/components/custom/ActiveWork/CoChangeChip.stories.tsx +44 -0
- package/src/components/custom/ActiveWork/CoChangeChip.test.tsx +29 -0
- package/src/components/custom/ActiveWork/CoChangeChip.tsx +48 -0
- package/src/components/custom/ActiveWork/Eyebrow.stories.tsx +25 -0
- package/src/components/custom/ActiveWork/Eyebrow.test.tsx +16 -0
- package/src/components/custom/ActiveWork/Eyebrow.tsx +26 -0
- package/src/components/custom/ActiveWork/FileActivityDetail.stories.tsx +50 -0
- package/src/components/custom/ActiveWork/FileActivityDetail.test.tsx +66 -0
- package/src/components/custom/ActiveWork/FileActivityDetail.tsx +202 -0
- package/src/components/custom/ActiveWork/FileActivityRow.stories.tsx +70 -0
- package/src/components/custom/ActiveWork/FileActivityRow.test.tsx +67 -0
- package/src/components/custom/ActiveWork/FileActivityRow.tsx +130 -0
- package/src/components/custom/ActiveWork/FileHistoryExplorer.stories.tsx +84 -0
- package/src/components/custom/ActiveWork/FileHistoryExplorer.test.tsx +99 -0
- package/src/components/custom/ActiveWork/FileHistoryExplorer.tsx +138 -0
- package/src/components/custom/ActiveWork/FilePathLabel.stories.tsx +39 -0
- package/src/components/custom/ActiveWork/FilePathLabel.test.tsx +53 -0
- package/src/components/custom/ActiveWork/FilePathLabel.tsx +63 -0
- package/src/components/custom/ActiveWork/InitiativeBrief.stories.tsx +50 -0
- package/src/components/custom/ActiveWork/InitiativeBrief.test.tsx +62 -0
- package/src/components/custom/ActiveWork/InitiativeBrief.tsx +182 -0
- package/src/components/custom/ActiveWork/InitiativeCard.stories.tsx +73 -0
- package/src/components/custom/ActiveWork/InitiativeCard.test.tsx +68 -0
- package/src/components/custom/ActiveWork/InitiativeCard.tsx +136 -0
- package/src/components/custom/ActiveWork/InitiativeHeader.stories.tsx +59 -0
- package/src/components/custom/ActiveWork/InitiativeHeader.test.tsx +40 -0
- package/src/components/custom/ActiveWork/InitiativeHeader.tsx +73 -0
- package/src/components/custom/ActiveWork/InitiativeReader.stories.tsx +145 -0
- package/src/components/custom/ActiveWork/OpenLoops.stories.tsx +58 -0
- package/src/components/custom/ActiveWork/OpenLoops.test.tsx +61 -0
- package/src/components/custom/ActiveWork/OpenLoops.tsx +107 -0
- package/src/components/custom/ActiveWork/PortfolioOverview.stories.tsx +101 -0
- package/src/components/custom/ActiveWork/PortfolioOverview.test.tsx +81 -0
- package/src/components/custom/ActiveWork/PortfolioOverview.tsx +83 -0
- package/src/components/custom/ActiveWork/README.md +305 -0
- package/src/components/custom/ActiveWork/SessionDetail.stories.tsx +71 -0
- package/src/components/custom/ActiveWork/SessionDetail.test.tsx +84 -0
- package/src/components/custom/ActiveWork/SessionDetail.tsx +180 -0
- package/src/components/custom/ActiveWork/SessionList.stories.tsx +69 -0
- package/src/components/custom/ActiveWork/SessionList.test.tsx +78 -0
- package/src/components/custom/ActiveWork/SessionList.tsx +103 -0
- package/src/components/custom/ActiveWork/SessionListItem.stories.tsx +64 -0
- package/src/components/custom/ActiveWork/SessionListItem.test.tsx +62 -0
- package/src/components/custom/ActiveWork/SessionListItem.tsx +152 -0
- package/src/components/custom/ActiveWork/SessionReader.stories.tsx +113 -0
- package/src/components/custom/ActiveWork/SeverityLabel.stories.tsx +51 -0
- package/src/components/custom/ActiveWork/SeverityLabel.test.tsx +68 -0
- package/src/components/custom/ActiveWork/SeverityLabel.tsx +97 -0
- package/src/components/custom/ActiveWork/TaskRow.stories.tsx +63 -0
- package/src/components/custom/ActiveWork/TaskRow.test.tsx +112 -0
- package/src/components/custom/ActiveWork/TaskRow.tsx +176 -0
- package/src/components/custom/ActiveWork/TaskTable.stories.tsx +123 -0
- package/src/components/custom/ActiveWork/TaskTable.test.tsx +233 -0
- package/src/components/custom/ActiveWork/TaskTable.tsx +321 -0
- package/src/components/custom/ActiveWork/active-work-depth.test.tsx +102 -0
- package/src/components/custom/ActiveWork/file-history-fixture.ts +91 -0
- package/src/components/custom/ActiveWork/format-time.test.ts +23 -0
- package/src/components/custom/ActiveWork/format-time.ts +32 -0
- package/src/components/custom/ActiveWork/index.ts +19 -0
- package/src/components/custom/ActiveWork/initiative-fixture.ts +104 -0
- package/src/components/custom/ActiveWork/session-fixture.ts +267 -0
- package/src/components/custom/ActiveWork/session-linkers.ts +56 -0
- package/src/components/custom/ActiveWork/task-list-fixture.ts +97 -0
- package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +12 -6
- package/src/components/custom/DateTime/DateTime.stories.tsx +2 -1
- package/src/components/custom/DateTime/DateTime.test.tsx +3 -1
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +22 -9
- package/src/components/custom/EmptyState/EmptyState.test.tsx +7 -13
- package/src/components/custom/EmptyState/EmptyState.tsx +4 -18
- package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +4 -3
- package/src/components/custom/Fatigue/FatigueLights.stories.tsx +2 -2
- package/src/components/custom/Fatigue/GhostSpark.stories.tsx +4 -3
- package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +2 -2
- package/src/components/custom/Fatigue/LiveFatigueCard.tsx +8 -4
- package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +116 -6
- package/src/components/custom/Fatigue/LiveFatiguePanel.test.tsx +134 -0
- package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +54 -83
- package/src/components/custom/Fatigue/README.md +40 -3
- package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +2 -2
- package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +2 -1
- package/src/components/custom/Fatigue/VelocityHero.stories.tsx +4 -3
- package/src/components/custom/Fatigue/VerdictHero.stories.tsx +2 -2
- package/src/components/custom/Fatigue/fatigue-mock.test.ts +0 -5
- package/src/components/custom/Fatigue/fatigue-mock.ts +2 -10
- package/src/components/custom/Fatigue/fatigue-tokens.ts +12 -0
- package/src/components/custom/Fatigue/index.ts +17 -5
- package/src/components/custom/Fatigue/panel-layout.test.ts +149 -0
- package/src/components/custom/Fatigue/panel-layout.ts +185 -0
- package/src/components/custom/Gauge/Gauge.stories.tsx +6 -3
- package/src/components/custom/Gauge/Gauge.test.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.tsx +7 -2
- package/src/components/custom/Metric/Metric.stories.tsx +10 -1
- package/src/components/custom/Metric/Metric.test.tsx +1 -3
- package/src/components/custom/Metric/Metric.tsx +5 -20
- package/src/components/custom/Prose/MarkdownProse.stories.tsx +88 -0
- package/src/components/custom/Prose/MarkdownProse.test.tsx +87 -0
- package/src/components/custom/Prose/MarkdownProse.tsx +201 -0
- package/src/components/custom/Prose/README.md +58 -0
- package/src/components/custom/Prose/index.ts +1 -0
- package/src/components/custom/Scatter/Scatter.stories.tsx +55 -14
- package/src/components/custom/Scatter/Scatter.test.tsx +2 -2
- package/src/components/custom/Scatter/Scatter.tsx +122 -18
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +41 -8
- package/src/components/custom/Sidebar/Sidebar.test.tsx +5 -1
- package/src/components/custom/Sidebar/Sidebar.tsx +16 -33
- package/src/components/custom/Table/Table.stories.tsx +99 -16
- package/src/components/custom/Table/Table.test.tsx +273 -86
- package/src/components/custom/Table/Table.tsx +159 -67
- package/src/components/custom/Table/column-fit.test.ts +75 -0
- package/src/components/custom/Table/column-fit.ts +116 -0
- package/src/components/custom/Table/index.ts +9 -0
- package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +8 -6
- package/src/components/custom/Treemap/Treemap.stories.tsx +15 -12
- package/src/components/custom/Treemap/Treemap.test.tsx +1 -1
- package/src/components/custom/Treemap/Treemap.tsx +11 -3
- package/src/components/custom/Typography/Typography.stories.tsx +28 -1
- package/src/components/custom/Typography/Typography.test.tsx +34 -0
- package/src/components/custom/Typography/Typography.tsx +6 -0
- package/src/components/custom/Typography/index.ts +1 -8
- package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +11 -2
- package/src/components/custom/Workout/BaseBadge.stories.tsx +27 -11
- package/src/components/custom/Workout/BaseBadge.test.tsx +31 -11
- package/src/components/custom/Workout/BaseBadge.tsx +6 -2
- package/src/components/custom/Workout/BodyMap.stories.tsx +4 -1
- package/src/components/custom/Workout/BodyMap.tsx +8 -4
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +28 -6
- package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +11 -15
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +50 -12
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +12 -9
- package/src/components/custom/Workout/CapacityBandChart.test.tsx +24 -51
- package/src/components/custom/Workout/CapacityBandChart.tsx +144 -111
- package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
- package/src/components/custom/Workout/DeviationBar.test.tsx +10 -4
- package/src/components/custom/Workout/DeviationBar.tsx +24 -16
- package/src/components/custom/Workout/DualVelocityStrip.stories.tsx +183 -0
- package/src/components/custom/Workout/DualVelocityStrip.test.tsx +32 -18
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +12 -11
- package/src/components/custom/Workout/ExerciseCard.test.tsx +52 -22
- package/src/components/custom/Workout/ExerciseCard.tsx +99 -183
- package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +95 -12
- 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.stories.tsx +9 -1
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +3 -2
- package/src/components/custom/Workout/ExerciseHeading.stories.tsx +8 -8
- package/src/components/custom/Workout/ExerciseHeading.tsx +194 -46
- package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +12 -9
- package/src/components/custom/Workout/ExerciseIndicator.test.tsx +27 -11
- package/src/components/custom/Workout/ExerciseIndicator.tsx +14 -15
- package/src/components/custom/Workout/FatigueMeter.stories.tsx +5 -5
- package/src/components/custom/Workout/InputBar.stories.tsx +4 -5
- package/src/components/custom/Workout/InputBar.tsx +4 -2
- package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
- package/src/components/custom/Workout/IntensityBar.test.tsx +5 -1
- package/src/components/custom/Workout/IntensityBar.tsx +8 -3
- package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +5 -4
- package/src/components/custom/Workout/MesoCard.stories.tsx +4 -1
- package/src/components/custom/Workout/MesoCard.test.tsx +7 -18
- package/src/components/custom/Workout/MesoCard.tsx +14 -10
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoProgressBar.test.tsx +18 -11
- package/src/components/custom/Workout/MesoProgressBar.tsx +15 -13
- package/src/components/custom/Workout/MesoStatusCard.stories.tsx +2 -1
- package/src/components/custom/Workout/MesoStatusCard.test.tsx +14 -44
- package/src/components/custom/Workout/MesoStatusCard.tsx +20 -33
- package/src/components/custom/Workout/MetricTiles.stories.tsx +3 -2
- package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +2 -8
- package/src/components/custom/Workout/MuscleGroupChip.test.tsx +23 -31
- package/src/components/custom/Workout/MuscleGroupChip.tsx +28 -67
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +9 -4
- package/src/components/custom/Workout/PlaceholderStrip.test.tsx +17 -10
- package/src/components/custom/Workout/PlaceholderStrip.tsx +9 -8
- package/src/components/custom/Workout/PrBadge.stories.tsx +2 -1
- package/src/components/custom/Workout/PrBadge.test.tsx +3 -9
- package/src/components/custom/Workout/PrBadge.tsx +13 -17
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +16 -3
- package/src/components/custom/Workout/PrHistoryModal.test.tsx +13 -36
- package/src/components/custom/Workout/PrHistoryModal.tsx +8 -4
- package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +6 -1
- package/src/components/custom/Workout/README.md +68 -17
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +3 -2
- package/src/components/custom/Workout/ReadinessCheck.test.tsx +12 -18
- package/src/components/custom/Workout/ReadinessCheck.tsx +22 -4
- package/src/components/custom/Workout/RestTimer.stories.tsx +16 -4
- package/src/components/custom/Workout/RestTimer.tsx +4 -2
- package/src/components/custom/Workout/ScheduleTiles.stories.tsx +3 -2
- package/src/components/custom/Workout/ScheduleTiles.test.tsx +3 -4
- package/src/components/custom/Workout/ScheduleTiles.tsx +4 -7
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +21 -17
- package/src/components/custom/Workout/SegmentedBar.tsx +10 -5
- package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +5 -5
- package/src/components/custom/Workout/SessionHeader.stories.tsx +6 -6
- package/src/components/custom/Workout/SessionHeader.tsx +12 -12
- package/src/components/custom/Workout/SessionRail.stories.tsx +12 -11
- package/src/components/custom/Workout/SessionRail.tsx +7 -9
- package/src/components/custom/Workout/SetBar.stories.tsx +10 -6
- package/src/components/custom/Workout/SetBar.test.tsx +49 -4
- package/src/components/custom/Workout/SetBar.tsx +38 -12
- package/src/components/custom/Workout/SetRow.stories.tsx +3 -2
- package/src/components/custom/Workout/SetRow.test.tsx +16 -2
- package/src/components/custom/Workout/SetRow.tsx +61 -23
- package/src/components/custom/Workout/SetStrip.stories.tsx +63 -7
- package/src/components/custom/Workout/SetStrip.test.tsx +25 -0
- package/src/components/custom/Workout/SetStrip.tsx +24 -4
- package/src/components/custom/Workout/SetTableHeader.stories.tsx +6 -6
- package/src/components/custom/Workout/SetTableHeader.tsx +9 -12
- package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +7 -7
- package/src/components/custom/Workout/SetsRepsLoad.tsx +24 -12
- package/src/components/custom/Workout/Sparkline.stories.tsx +14 -11
- package/src/components/custom/Workout/Sparkline.test.tsx +37 -19
- package/src/components/custom/Workout/Sparkline.tsx +14 -18
- package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusDot.test.tsx +44 -5
- package/src/components/custom/Workout/StatusDot.tsx +93 -63
- package/src/components/custom/Workout/StatusPill.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusPill.test.tsx +6 -15
- package/src/components/custom/Workout/StatusPill.tsx +27 -38
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +11 -4
- package/src/components/custom/Workout/StrengthTrendChart.test.tsx +15 -37
- package/src/components/custom/Workout/StrengthTrendChart.tsx +39 -36
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +27 -64
- package/src/components/custom/Workout/SupersetWrapper.test.tsx +16 -14
- package/src/components/custom/Workout/SupersetWrapper.tsx +17 -21
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +26 -10
- package/src/components/custom/Workout/TempoDisplay.test.tsx +18 -4
- package/src/components/custom/Workout/TempoDisplay.tsx +2 -0
- package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +9 -1
- package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +4 -1
- package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +10 -6
- package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +5 -2
- package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +44 -1
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +12 -6
- package/src/components/custom/Workout/VelocityStrip.test.tsx +48 -6
- package/src/components/custom/Workout/VelocityStrip.tsx +26 -14
- package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +6 -7
- package/src/components/custom/Workout/VolumeLandmarkBar.tsx +15 -6
- package/src/components/custom/Workout/WeekRow.stories.tsx +4 -1
- package/src/components/custom/Workout/WeekRow.test.tsx +18 -15
- package/src/components/custom/Workout/WeekRow.tsx +23 -16
- package/src/components/custom/Workout/WeightBadge.stories.tsx +2 -1
- package/src/components/custom/Workout/WeightBadge.test.tsx +50 -19
- package/src/components/custom/Workout/WeightBadge.tsx +28 -27
- package/src/components/custom/Workout/WorkoutCard.stories.tsx +6 -1
- package/src/components/custom/Workout/WorkoutCard.test.tsx +18 -0
- package/src/components/custom/Workout/WorkoutCard.tsx +35 -57
- package/src/components/custom/Workout/WorkoutPill.stories.tsx +5 -2
- package/src/components/custom/Workout/WorkoutPill.test.tsx +39 -7
- package/src/components/custom/Workout/WorkoutPill.tsx +88 -57
- package/src/components/custom/Workout/ZoneTrack.stories.tsx +10 -7
- package/src/components/custom/Workout/ZoneTrack.tsx +10 -9
- package/src/components/custom/Workout/exerciseRowState.ts +29 -0
- package/src/components/custom/Workout/index.ts +17 -2
- package/src/components/custom/Workout/metricText.stories.tsx +15 -13
- package/src/components/custom/Workout/metricText.tsx +8 -2
- package/src/components/custom/Workout/setHeadingKit.tsx +219 -36
- package/src/components/custom/Workout/velocity-story-kit.tsx +2 -2
- package/src/components/custom/charts/README.md +66 -0
- package/src/components/custom/charts/SetBarChart.test.tsx +46 -1
- package/src/components/custom/charts/SetBarChart.tsx +56 -9
- package/src/components/custom/charts/SparkBars.stories.tsx +55 -0
- package/src/components/custom/charts/SparkBars.test.tsx +69 -0
- package/src/components/custom/charts/SparkBars.tsx +85 -0
- package/src/components/custom/charts/flatBarGeometry.test.ts +41 -0
- package/src/components/custom/charts/flatBarGeometry.ts +32 -0
- package/src/components/custom/charts/index.ts +3 -0
- package/src/components/custom/index.ts +3 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +13 -10
- package/src/components/custom/stepper/Stepper.test.tsx +1 -7
- package/src/components/custom/stepper/Stepper.tsx +21 -34
- package/src/components/icons/icons.stories.tsx +1 -1
- package/src/components/icons/icons.tsx +51 -0
- package/src/components/shell/AppShell.stories.tsx +150 -0
- package/src/components/shell/AppShell.test.tsx +70 -0
- package/src/components/shell/AppShell.tsx +90 -0
- package/src/components/shell/BrandLockup.stories.tsx +66 -5
- package/src/components/shell/BrandLockup.test.tsx +34 -1
- package/src/components/shell/BrandLockup.tsx +27 -11
- package/src/components/shell/NavItem.stories.tsx +4 -4
- package/src/components/shell/NavItem.test.tsx +17 -0
- package/src/components/shell/NavItem.tsx +17 -5
- package/src/components/shell/README.md +89 -26
- package/src/components/shell/SideNav.stories.tsx +38 -7
- package/src/components/shell/SideNav.test.tsx +16 -8
- package/src/components/shell/SideNav.tsx +17 -17
- package/src/components/shell/TopBar.stories.tsx +44 -19
- package/src/components/shell/TopBar.test.tsx +34 -12
- package/src/components/shell/TopBar.tsx +61 -40
- package/src/components/shell/brands.tsx +66 -0
- package/src/components/shell/index.ts +9 -8
- package/src/components/shell/{DeviceIndicator.stories.tsx → workout/DeviceIndicator.stories.tsx} +2 -2
- package/src/components/shell/{DeviceIndicator.tsx → workout/DeviceIndicator.tsx} +2 -2
- package/src/components/shell/{DeviceMenu.stories.tsx → workout/DeviceMenu.stories.tsx} +2 -2
- package/src/components/shell/{DeviceMenu.tsx → workout/DeviceMenu.tsx} +3 -3
- package/src/components/shell/{DeviceRow.stories.tsx → workout/DeviceRow.stories.tsx} +2 -2
- package/src/components/shell/{DeviceRow.tsx → workout/DeviceRow.tsx} +3 -3
- package/src/components/shell/{SessionStatePill.stories.tsx → workout/SessionStatePill.stories.tsx} +2 -2
- package/src/components/shell/{SessionStatePill.tsx → workout/SessionStatePill.tsx} +5 -3
- package/src/components/shell/workout/WorkoutShell.stories.tsx +71 -0
- package/src/components/shell/{DashboardShell.test.tsx → workout/WorkoutShell.test.tsx} +19 -8
- package/src/components/shell/workout/WorkoutShell.tsx +77 -0
- package/src/components/shell/workout/WorkoutTopBar.stories.tsx +58 -0
- package/src/components/shell/workout/WorkoutTopBar.test.tsx +39 -0
- package/src/components/shell/workout/WorkoutTopBar.tsx +52 -0
- package/src/components/shell/workout/index.ts +9 -0
- package/src/components/shell/workout/workoutNavItems.tsx +16 -0
- package/src/components/ui/README.md +110 -0
- package/src/components/ui/alert/Alert.stories.tsx +15 -5
- package/src/components/ui/alert/Alert.tsx +24 -3
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +11 -13
- package/src/components/ui/autocomplete/Autocomplete.test.tsx +7 -25
- package/src/components/ui/autocomplete/Autocomplete.tsx +21 -28
- package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
- package/src/components/ui/avatar/Avatar.tsx +2 -10
- package/src/components/ui/badge/Badge.stories.tsx +83 -28
- package/src/components/ui/badge/Badge.test.tsx +19 -3
- package/src/components/ui/badge/Badge.tsx +48 -55
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
- package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +4 -16
- package/src/components/ui/button/Button.stories.tsx +46 -12
- package/src/components/ui/button/Button.tsx +44 -37
- package/src/components/ui/card/Card.stories.tsx +90 -94
- package/src/components/ui/card/Card.test.tsx +80 -6
- package/src/components/ui/card/Card.tsx +150 -203
- package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
- package/src/components/ui/checkbox/Checkbox.tsx +7 -27
- package/src/components/ui/chip/Chip.stories.tsx +37 -13
- package/src/components/ui/chip/Chip.test.tsx +10 -6
- package/src/components/ui/chip/Chip.tsx +76 -91
- package/src/components/ui/collapse/Collapse.stories.tsx +23 -23
- package/src/components/ui/collapse/Collapse.tsx +6 -11
- package/src/components/ui/data-row/DataRow.stories.tsx +14 -3
- package/src/components/ui/data-row/DataRow.test.tsx +5 -15
- package/src/components/ui/data-row/DataRow.tsx +18 -7
- package/src/components/ui/divider/Divider.stories.tsx +1 -1
- package/src/components/ui/divider/Divider.tsx +1 -5
- package/src/components/ui/drawer/Drawer.stories.tsx +5 -16
- package/src/components/ui/drawer/Drawer.tsx +13 -27
- package/src/components/ui/floating-overlay-depth.test.tsx +143 -0
- package/src/components/ui/form-field/FormField.stories.tsx +6 -20
- package/src/components/ui/form-field/FormField.test.tsx +1 -5
- package/src/components/ui/form-field/FormField.tsx +10 -46
- package/src/components/ui/help-tip/HelpTip.stories.tsx +8 -19
- package/src/components/ui/help-tip/HelpTip.test.tsx +1 -3
- package/src/components/ui/help-tip/HelpTip.tsx +21 -22
- package/src/components/ui/help-tip/index.ts +7 -1
- package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
- package/src/components/ui/icon-box/IconBox.test.tsx +1 -7
- package/src/components/ui/icon-box/IconBox.tsx +10 -18
- package/src/components/ui/indicator/Indicator.stories.tsx +5 -4
- package/src/components/ui/indicator/Indicator.tsx +20 -14
- package/src/components/ui/input/Input.stories.tsx +3 -11
- package/src/components/ui/input/Input.test.tsx +2 -10
- package/src/components/ui/input/Input.tsx +10 -25
- package/src/components/ui/input/index.ts +7 -1
- package/src/components/ui/link/Link.stories.tsx +19 -7
- package/src/components/ui/list-item/ListItem.stories.tsx +17 -5
- package/src/components/ui/list-item/ListItem.tsx +2 -9
- package/src/components/ui/menu/Menu.stories.tsx +21 -15
- package/src/components/ui/menu/Menu.test.tsx +2 -11
- package/src/components/ui/menu/Menu.tsx +12 -14
- package/src/components/ui/menu/index.ts +8 -1
- package/src/components/ui/modal/Modal.stories.tsx +2 -2
- package/src/components/ui/modal/Modal.tsx +31 -19
- package/src/components/ui/pill/Pill.stories.tsx +111 -23
- package/src/components/ui/pill/Pill.test.tsx +101 -3
- package/src/components/ui/pill/Pill.tsx +168 -49
- package/src/components/ui/pill/index.ts +1 -1
- package/src/components/ui/popover/Popover.stories.tsx +6 -8
- package/src/components/ui/popover/Popover.test.tsx +1 -6
- package/src/components/ui/popover/Popover.tsx +20 -15
- package/src/components/ui/popover/index.ts +7 -1
- package/src/components/ui/progress/Progress.stories.tsx +3 -12
- package/src/components/ui/progress/Progress.test.tsx +1 -5
- package/src/components/ui/progress/Progress.tsx +9 -6
- package/src/components/ui/radio/Radio.stories.tsx +42 -21
- package/src/components/ui/radio/Radio.test.tsx +6 -2
- package/src/components/ui/radio/Radio.tsx +23 -22
- package/src/components/ui/section/Section.stories.tsx +12 -21
- package/src/components/ui/section/Section.test.tsx +1 -6
- package/src/components/ui/section/Section.tsx +1 -3
- package/src/components/ui/section/index.ts +1 -5
- package/src/components/ui/select/Select.stories.tsx +3 -13
- package/src/components/ui/select/Select.test.tsx +2 -11
- package/src/components/ui/select/Select.tsx +16 -15
- package/src/components/ui/skeleton/Skeleton.stories.tsx +3 -11
- package/src/components/ui/skeleton/Skeleton.tsx +3 -14
- package/src/components/ui/skeleton/index.ts +1 -7
- package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
- package/src/components/ui/spinner/Spinner.tsx +4 -5
- package/src/components/ui/stack/Stack.stories.tsx +8 -8
- package/src/components/ui/stack/Stack.tsx +1 -1
- package/src/components/ui/surface/Surface.stories.tsx +92 -33
- package/src/components/ui/surface/Surface.test.tsx +88 -28
- package/src/components/ui/surface/Surface.tsx +37 -87
- package/src/components/ui/surface/SurfaceContext.ts +22 -62
- package/src/components/ui/surface/index.ts +8 -0
- package/src/components/ui/surface/resolveSurface.ts +98 -0
- package/src/components/ui/surface/surface.contract.test.ts +6 -1
- package/src/components/ui/switch/Switch.stories.tsx +1 -1
- package/src/components/ui/switch/Switch.tsx +7 -6
- package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
- package/src/components/ui/tabs/Tabs.tsx +8 -21
- package/src/components/ui/tabs/index.ts +1 -7
- package/src/components/ui/tile/Tile.stories.tsx +1 -1
- package/src/components/ui/tile/Tile.tsx +3 -0
- package/src/components/ui/toast/Toast.stories.tsx +1 -1
- package/src/components/ui/toast/Toast.test.tsx +2 -8
- package/src/components/ui/toast/Toast.tsx +49 -57
- package/src/components/ui/toast/index.ts +0 -1
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +92 -71
- package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +3 -18
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +38 -49
- package/src/components/ui/tooltip/Tooltip.stories.tsx +12 -3
- package/src/components/ui/tooltip/Tooltip.test.tsx +21 -3
- package/src/components/ui/tooltip/Tooltip.tsx +55 -36
- package/src/components/ui/tooltip/index.ts +1 -1
- package/src/components/ui/trigger/TriggerSurface.test.tsx +215 -0
- package/src/components/ui/trigger/TriggerSurface.tsx +81 -0
- package/src/components/ui/trigger/index.ts +2 -0
- package/src/stories/PresetShowcase.stories.tsx +101 -48
- package/src/test/no-device-internals.test.ts +3 -3
- package/src/test/no-raw-color.test.ts +39 -0
- package/src/theme/Alpha.stories.tsx +103 -0
- package/src/theme/ChoosingTokens.stories.tsx +48 -0
- package/src/theme/ColorPalettes.stories.tsx +62 -45
- package/src/theme/ColorPrimitives.stories.tsx +35 -35
- package/src/theme/Depth.stories.tsx +132 -65
- package/src/theme/DepthCalibration.stories.tsx +23 -21
- package/src/theme/ProposedTokensVW82.stories.tsx +414 -0
- package/src/theme/ThemeProvider.test.tsx +4 -4
- package/src/theme/barrel.ts +7 -6
- package/src/theme/color-stories.coverage.test.ts +1 -1
- package/src/theme/color-utils.ts +41 -29
- package/src/theme/config.completeness.test.ts +1 -1
- package/src/theme/config.ts +40 -0
- package/src/theme/elevation.test.ts +82 -56
- package/src/theme/elevation.ts +77 -561
- package/src/theme/global.css +45 -5
- package/src/theme/grey-ramp.test.ts +24 -8
- package/src/theme/lift.test.ts +39 -0
- package/src/theme/lift.ts +102 -0
- package/src/theme/materials.test.ts +5 -2
- package/src/theme/materials.ts +38 -4
- package/src/theme/native-theming-guard.test.ts +5 -5
- package/src/theme/presets/apply.ts +4 -0
- package/src/theme/presets/audiobook.ts +5 -2
- package/src/theme/presets/default.ts +4 -0
- package/src/theme/presets/index.ts +14 -1
- package/src/theme/presets/types.ts +1 -1
- package/src/theme/semantic-contrast.test.ts +13 -4
- package/src/theme/status-distinctness.test.ts +66 -0
- package/src/theme/subtle-tone-weight.test.ts +163 -0
- package/src/theme/surface-planes.test.ts +37 -0
- package/src/theme/surface-planes.ts +68 -0
- package/src/theme/tailwind-var-opacity.test.ts +163 -0
- package/src/theme/token-export-guard.test.ts +1 -3
- package/src/theme/tokens/primitives.test.ts +15 -7
- package/src/theme/tokens/primitives.ts +98 -33
- package/src/theme/tokens/semantic-hex-guard.test.ts +322 -0
- package/src/theme/tokens/semantic.ts +124 -54
- package/src/theme/wash-ladder.test.ts +58 -0
- package/src/theme/workout-tokens.ts +1 -7
- package/src/types/jest-axe.d.ts +2 -6
- package/src/types/raw-md.d.ts +5 -0
- package/src/utils/colors.alpha-adoption.test.ts +408 -0
- package/src/utils/colors.ts +4 -4
- package/src/utils/form.ts +1 -3
- package/src/utils/index.ts +2 -7
- package/src/utils/number-format.ts +17 -0
- package/src/utils/workout-format.ts +15 -6
- package/src/web-jsx/jsx-runtime.ts +1 -5
- package/tailwind.config.js +55 -9
- package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +0 -151
- package/src/components/custom/Workout/RowInventory.stories.tsx +0 -408
- package/src/components/custom/Workout/S3FullRail.stories.tsx +0 -46
- package/src/components/custom/Workout/S3SessionPace.stories.tsx +0 -95
- package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +0 -61
- package/src/components/custom/Workout/S3SetTypes.stories.tsx +0 -176
- package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +0 -240
- package/src/components/shell/DashboardShell.stories.tsx +0 -65
- package/src/components/shell/DashboardShell.tsx +0 -75
- package/src/components/shell/SessionRailSpecimen.stories.tsx +0 -753
- package/src/theme/elevation.stories.tsx +0 -346
- package/src/utils/useTheme.ts +0 -72
- /package/src/components/shell/{DeviceIndicator.test.tsx → workout/DeviceIndicator.test.tsx} +0 -0
- /package/src/components/shell/{DeviceMenu.test.tsx → workout/DeviceMenu.test.tsx} +0 -0
- /package/src/components/shell/{DeviceRow.test.tsx → workout/DeviceRow.test.tsx} +0 -0
- /package/src/components/shell/{SessionStatePill.test.tsx → workout/SessionStatePill.test.tsx} +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import { View } from 'react-native'
|
|
4
|
+
import { cn } from '../../../utils/cn'
|
|
5
|
+
import { Card } from '../../ui/card'
|
|
6
|
+
import { Collapse, CollapseButton, CollapseContent } from '../../ui/collapse'
|
|
7
|
+
import { Divider } from '../../ui/divider'
|
|
8
|
+
import { Pill } from '../../ui/pill'
|
|
9
|
+
import { Tooltip, useHoverState } from '../../ui/tooltip'
|
|
10
|
+
import { DateTime } from '../DateTime'
|
|
11
|
+
import { MarkdownProse } from '../Prose'
|
|
12
|
+
import { Typography } from '../Typography'
|
|
13
|
+
import { formatSessionDuration, formatTaskAge } from './format-time'
|
|
14
|
+
import { extractTaskRefs, sessionLinkers, type SessionLinkHandlers } from './session-linkers'
|
|
15
|
+
import { ExactTime, type SessionSummary } from './SessionListItem'
|
|
16
|
+
import { TaskTable, type TaskColumnKey } from './TaskTable'
|
|
17
|
+
import type { TaskListItem } from './TaskRow'
|
|
18
|
+
|
|
19
|
+
/** Task pills beyond this collapse into a `+N`, so a sprawling session cannot push the log off screen. */
|
|
20
|
+
const MAX_TASK_PILLS = 14
|
|
21
|
+
|
|
22
|
+
// The card already names the initiative context, and tags would crowd the title; the id column keeps the prefix.
|
|
23
|
+
const EMBEDDED_HIDDEN_COLUMNS: TaskColumnKey[] = ['slug', 'tags']
|
|
24
|
+
|
|
25
|
+
export interface SessionDetailProps extends SessionLinkHandlers {
|
|
26
|
+
session: SessionSummary
|
|
27
|
+
/** Reference instant for the age label, injected so renders are deterministic. */
|
|
28
|
+
now: number
|
|
29
|
+
/**
|
|
30
|
+
* Task rows for the ids the log mentions, resolved by the host from its task
|
|
31
|
+
* store. When given, the tasks-touched strip can expand into a table.
|
|
32
|
+
*/
|
|
33
|
+
tasks?: TaskListItem[]
|
|
34
|
+
className?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The body without its leading h1: the card header carries the title. */
|
|
38
|
+
export function stripLeadingHeading(body: string): string {
|
|
39
|
+
const lines = body.split('\n')
|
|
40
|
+
const first = lines.findIndex((line) => line.trim() !== '')
|
|
41
|
+
if (first === -1 || !/^#\s/.test(lines[first]!.trim())) return body
|
|
42
|
+
return lines.slice(first + 1).join('\n')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function AgeHover({ session, now }: { session: SessionSummary; now: number }) {
|
|
46
|
+
const { hovered, hoverProps } = useHoverState()
|
|
47
|
+
return (
|
|
48
|
+
<Tooltip usePortal isOpen={hovered} content={<ExactTime session={session} />}>
|
|
49
|
+
<View {...hoverProps} className="shrink-0" testID="session-age">
|
|
50
|
+
<Typography variant="caption" className="leading-none text-text-tertiary">
|
|
51
|
+
{formatTaskAge(session.ended, now)}
|
|
52
|
+
</Typography>
|
|
53
|
+
</View>
|
|
54
|
+
</Tooltip>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function SessionMeta({ session, now }: { session: SessionSummary; now: number }) {
|
|
59
|
+
const duration = formatSessionDuration(session.started, session.ended)
|
|
60
|
+
return (
|
|
61
|
+
<View className="flex-row items-center gap-2.5">
|
|
62
|
+
<DateTime
|
|
63
|
+
value={session.ended}
|
|
64
|
+
format="medium"
|
|
65
|
+
isUTC
|
|
66
|
+
variant="mono"
|
|
67
|
+
className="shrink-0 text-text-secondary"
|
|
68
|
+
/>
|
|
69
|
+
<AgeHover session={session} now={now} />
|
|
70
|
+
{duration ? (
|
|
71
|
+
<Typography variant="caption" className="shrink-0 leading-none text-text-tertiary">
|
|
72
|
+
{`· ${duration}`}
|
|
73
|
+
</Typography>
|
|
74
|
+
) : null}
|
|
75
|
+
<Typography variant="mono" numberOfLines={1} className="shrink text-text-tertiary">
|
|
76
|
+
{session.id}
|
|
77
|
+
</Typography>
|
|
78
|
+
</View>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface TasksTouchedProps {
|
|
83
|
+
refs: string[]
|
|
84
|
+
tasks: TaskListItem[]
|
|
85
|
+
now: number
|
|
86
|
+
onPressTask?: (id: string) => void
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The strip of task pills; with task rows supplied it expands into a table. */
|
|
90
|
+
function TasksTouched({ refs, tasks, now, onPressTask }: TasksTouchedProps) {
|
|
91
|
+
if (refs.length === 0) return null
|
|
92
|
+
const shown = refs.slice(0, MAX_TASK_PILLS)
|
|
93
|
+
const hidden = refs.length - shown.length
|
|
94
|
+
return (
|
|
95
|
+
<Collapse>
|
|
96
|
+
<View className="flex-row flex-wrap items-center gap-1.5" testID="session-tasks-touched">
|
|
97
|
+
<Typography variant="caption" className="leading-none text-text-tertiary">
|
|
98
|
+
tasks touched
|
|
99
|
+
</Typography>
|
|
100
|
+
{shown.map((id) => (
|
|
101
|
+
<Pill
|
|
102
|
+
key={id}
|
|
103
|
+
variant="subtle"
|
|
104
|
+
color="primary"
|
|
105
|
+
size="xs"
|
|
106
|
+
onPress={onPressTask ? () => onPressTask(id) : undefined}
|
|
107
|
+
>
|
|
108
|
+
{id}
|
|
109
|
+
</Pill>
|
|
110
|
+
))}
|
|
111
|
+
{hidden > 0 ? (
|
|
112
|
+
<Typography variant="caption" className="leading-none text-text-tertiary">
|
|
113
|
+
{`+${hidden}`}
|
|
114
|
+
</Typography>
|
|
115
|
+
) : null}
|
|
116
|
+
{tasks.length > 0 ? (
|
|
117
|
+
<CollapseButton
|
|
118
|
+
className="ml-auto px-1 py-0"
|
|
119
|
+
accessibilityLabel={`Show the ${tasks.length} tasks touched as a table`}
|
|
120
|
+
/>
|
|
121
|
+
) : null}
|
|
122
|
+
</View>
|
|
123
|
+
{tasks.length > 0 ? (
|
|
124
|
+
<CollapseContent className="pt-2">
|
|
125
|
+
<TaskTable
|
|
126
|
+
tasks={tasks}
|
|
127
|
+
now={now}
|
|
128
|
+
hideLegend
|
|
129
|
+
hideColumns={EMBEDDED_HIDDEN_COLUMNS}
|
|
130
|
+
label={`${tasks.length} ${tasks.length === 1 ? 'task' : 'tasks'} touched`}
|
|
131
|
+
/>
|
|
132
|
+
</CollapseContent>
|
|
133
|
+
) : null}
|
|
134
|
+
</Collapse>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* SessionDetail — one session log, readable: its title, when it ran and for
|
|
140
|
+
* how long, the tasks it touched (as pills, or as a table when the host
|
|
141
|
+
* supplies the rows), then the markdown body with task ids, `[[name]]` links
|
|
142
|
+
* and PR numbers auto-linked.
|
|
143
|
+
*
|
|
144
|
+
* Holds no selection state; pair it with {@link SessionList} in the host.
|
|
145
|
+
* Composes {@link Card}, {@link Divider}, {@link Pill}, {@link Collapse},
|
|
146
|
+
* {@link DateTime}, {@link TaskTable} and {@link MarkdownProse}.
|
|
147
|
+
*/
|
|
148
|
+
export function SessionDetail({
|
|
149
|
+
session,
|
|
150
|
+
now,
|
|
151
|
+
tasks = [],
|
|
152
|
+
onPressTask,
|
|
153
|
+
onPressLink,
|
|
154
|
+
onPressPr,
|
|
155
|
+
className,
|
|
156
|
+
}: SessionDetailProps) {
|
|
157
|
+
const refs = useMemo(() => extractTaskRefs(session.body), [session.body])
|
|
158
|
+
const touched = useMemo(
|
|
159
|
+
() => refs.map((id) => tasks.find((t) => t.id === id)).filter((t): t is TaskListItem => !!t),
|
|
160
|
+
[refs, tasks]
|
|
161
|
+
)
|
|
162
|
+
const body = useMemo(() => stripLeadingHeading(session.body), [session.body])
|
|
163
|
+
const linkers = useMemo(
|
|
164
|
+
() => sessionLinkers({ onPressTask, onPressLink, onPressPr }),
|
|
165
|
+
[onPressTask, onPressLink, onPressPr]
|
|
166
|
+
)
|
|
167
|
+
return (
|
|
168
|
+
<Card className={cn('gap-3 p-5', className)} testID="session-detail">
|
|
169
|
+
<View className="gap-2">
|
|
170
|
+
<Typography variant="h5" className="text-text-primary">
|
|
171
|
+
{session.title}
|
|
172
|
+
</Typography>
|
|
173
|
+
<SessionMeta session={session} now={now} />
|
|
174
|
+
<TasksTouched refs={refs} tasks={touched} now={now} onPressTask={onPressTask} />
|
|
175
|
+
</View>
|
|
176
|
+
<Divider />
|
|
177
|
+
<MarkdownProse body={body} linkers={linkers} testID="session-body" />
|
|
178
|
+
</Card>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { useArgs } from 'storybook/preview-api'
|
|
3
|
+
import { Surface } from '../../ui/surface'
|
|
4
|
+
import { SessionList } from './SessionList'
|
|
5
|
+
import { SESSION_FIXTURE, SESSION_NOW } from './session-fixture'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* **SessionList** — the selectable list half of the session reader. The host
|
|
9
|
+
* holds `selectedId`; this story keeps it in Storybook args so clicking a row
|
|
10
|
+
* updates the Controls panel.
|
|
11
|
+
*
|
|
12
|
+
* Composes `Eyebrow` · `SessionListItem`.
|
|
13
|
+
*/
|
|
14
|
+
const meta: Meta<typeof SessionList> = {
|
|
15
|
+
title: 'Custom/ActiveWork/SessionList',
|
|
16
|
+
component: SessionList,
|
|
17
|
+
tags: ['autodocs', 'status:candidate', '!status:review'],
|
|
18
|
+
args: {
|
|
19
|
+
sessions: SESSION_FIXTURE,
|
|
20
|
+
now: SESSION_NOW,
|
|
21
|
+
selectedId: SESSION_FIXTURE[0]!.id,
|
|
22
|
+
},
|
|
23
|
+
argTypes: {
|
|
24
|
+
sessions: { table: { disable: true } },
|
|
25
|
+
now: { table: { disable: true } },
|
|
26
|
+
selectedId: { control: 'select', options: SESSION_FIXTURE.map((s) => s.id) },
|
|
27
|
+
},
|
|
28
|
+
render: function Render(args) {
|
|
29
|
+
const [, updateArgs] = useArgs()
|
|
30
|
+
return <SessionList {...args} onSelect={(s) => updateArgs({ selectedId: s.id })} />
|
|
31
|
+
},
|
|
32
|
+
decorators: [
|
|
33
|
+
(Story) => (
|
|
34
|
+
<Surface level="base" className="min-h-screen w-72 p-6" testID="page-surface">
|
|
35
|
+
<Story />
|
|
36
|
+
</Surface>
|
|
37
|
+
),
|
|
38
|
+
],
|
|
39
|
+
parameters: {
|
|
40
|
+
layout: 'fullscreen',
|
|
41
|
+
docs: {
|
|
42
|
+
description: {
|
|
43
|
+
component:
|
|
44
|
+
'Composes **Eyebrow** · **Divider** · **SessionListItem**. Used-by ↑ the Session Reader composition. Selection is controlled by the host; sessions group under a month divider once the list spans more than one.',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
export default meta
|
|
50
|
+
|
|
51
|
+
type Story = StoryObj<typeof SessionList>
|
|
52
|
+
|
|
53
|
+
/** Six sessions across two months, newest selected. */
|
|
54
|
+
export const Default: Story = {}
|
|
55
|
+
|
|
56
|
+
/** One month only: no period dividers. */
|
|
57
|
+
export const SingleMonth: Story = {
|
|
58
|
+
args: { sessions: SESSION_FIXTURE.filter((s) => s.ended.startsWith('2026-07')) },
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Nothing selected yet. */
|
|
62
|
+
export const NoSelection: Story = {
|
|
63
|
+
args: { selectedId: undefined },
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A custom heading in place of the count. */
|
|
67
|
+
export const CustomLabel: Story = {
|
|
68
|
+
args: { label: 'Recent sessions' },
|
|
69
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { SessionList, groupByPeriod, sessionPeriod } from './SessionList'
|
|
5
|
+
import { SESSION_FIXTURE, SESSION_NOW } from './session-fixture'
|
|
6
|
+
|
|
7
|
+
const july = SESSION_FIXTURE.filter((s) => s.ended.startsWith('2026-07'))
|
|
8
|
+
|
|
9
|
+
describe('SessionList', () => {
|
|
10
|
+
it('renders a row per session under a count heading, inside a listbox', () => {
|
|
11
|
+
render(<SessionList sessions={SESSION_FIXTURE} now={SESSION_NOW} />)
|
|
12
|
+
expect(screen.getByText('6 sessions')).toBeInTheDocument()
|
|
13
|
+
expect(screen.getByRole('listbox', { name: '6 sessions' })).toBeInTheDocument()
|
|
14
|
+
expect(screen.getAllByRole('option')).toHaveLength(SESSION_FIXTURE.length)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('divides the list by month once it spans more than one, and not before', () => {
|
|
18
|
+
const { rerender } = render(<SessionList sessions={SESSION_FIXTURE} now={SESSION_NOW} />)
|
|
19
|
+
expect(screen.getAllByTestId('session-period').map((p) => p.textContent)).toEqual([
|
|
20
|
+
'July 2026',
|
|
21
|
+
'May 2026',
|
|
22
|
+
])
|
|
23
|
+
rerender(<SessionList sessions={july} now={SESSION_NOW} />)
|
|
24
|
+
expect(screen.queryByTestId('session-period')).not.toBeInTheDocument()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('marks only the selected id and reports the pressed session to the host', () => {
|
|
28
|
+
const onSelect = vi.fn()
|
|
29
|
+
render(
|
|
30
|
+
<SessionList
|
|
31
|
+
sessions={SESSION_FIXTURE}
|
|
32
|
+
now={SESSION_NOW}
|
|
33
|
+
selectedId={SESSION_FIXTURE[1]!.id}
|
|
34
|
+
onSelect={onSelect}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
const options = screen.getAllByRole('option')
|
|
38
|
+
expect(options.filter((o) => o.getAttribute('aria-selected') === 'true')).toHaveLength(1)
|
|
39
|
+
expect(options[1]).toHaveAttribute('aria-selected', 'true')
|
|
40
|
+
fireEvent.click(options[3]!)
|
|
41
|
+
expect(onSelect).toHaveBeenCalledWith(SESSION_FIXTURE[3])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('singularises the heading and accepts a custom label', () => {
|
|
45
|
+
const { rerender } = render(
|
|
46
|
+
<SessionList sessions={SESSION_FIXTURE.slice(0, 1)} now={SESSION_NOW} />
|
|
47
|
+
)
|
|
48
|
+
expect(screen.getByText('1 session')).toBeInTheDocument()
|
|
49
|
+
rerender(<SessionList sessions={SESSION_FIXTURE} now={SESSION_NOW} label="Recent" />)
|
|
50
|
+
expect(screen.getByRole('listbox', { name: 'Recent' })).toBeInTheDocument()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('has no a11y violations', async () => {
|
|
54
|
+
const { container } = render(
|
|
55
|
+
<SessionList
|
|
56
|
+
sessions={SESSION_FIXTURE}
|
|
57
|
+
now={SESSION_NOW}
|
|
58
|
+
selectedId={SESSION_FIXTURE[0]!.id}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('period grouping', () => {
|
|
66
|
+
it('labels a session by the calendar month it ended in, in UTC', () => {
|
|
67
|
+
expect(sessionPeriod('2026-07-01T00:30:00Z')).toBe('July 2026')
|
|
68
|
+
expect(sessionPeriod('nope')).toBe('Undated')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('groups consecutive sessions and keeps the given order', () => {
|
|
72
|
+
const groups = groupByPeriod(SESSION_FIXTURE)
|
|
73
|
+
expect(groups.map((g) => [g.label, g.sessions.length])).toEqual([
|
|
74
|
+
['July 2026', 5],
|
|
75
|
+
['May 2026', 1],
|
|
76
|
+
])
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import { View, type ViewProps } from 'react-native'
|
|
4
|
+
import { cn } from '../../../utils/cn'
|
|
5
|
+
import { Divider } from '../../ui/divider'
|
|
6
|
+
import { Typography } from '../Typography'
|
|
7
|
+
import { Eyebrow } from './Eyebrow'
|
|
8
|
+
import { SessionListItem, type SessionSummary } from './SessionListItem'
|
|
9
|
+
|
|
10
|
+
// React Native's `Role` union omits `'listbox'` and `'group'`, even though RNW
|
|
11
|
+
// passes both straight through to the DOM.
|
|
12
|
+
const LISTBOX_ROLE = 'listbox' as ViewProps['role']
|
|
13
|
+
const GROUP_ROLE = 'group' as ViewProps['role']
|
|
14
|
+
|
|
15
|
+
export interface SessionListProps {
|
|
16
|
+
/** Newest first is the expected order; the list renders what it is given. */
|
|
17
|
+
sessions: SessionSummary[]
|
|
18
|
+
/** Reference instant for the age labels, injected so renders are deterministic. */
|
|
19
|
+
now: number
|
|
20
|
+
selectedId?: string
|
|
21
|
+
onSelect?: (session: SessionSummary) => void
|
|
22
|
+
/** Heading over the list. Defaults to the session count. */
|
|
23
|
+
label?: string
|
|
24
|
+
className?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Period {
|
|
28
|
+
label: string
|
|
29
|
+
sessions: SessionSummary[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The calendar month a session ended in, as the list's period label. */
|
|
33
|
+
export function sessionPeriod(iso: string): string {
|
|
34
|
+
const date = new Date(iso)
|
|
35
|
+
if (Number.isNaN(date.getTime())) return 'Undated'
|
|
36
|
+
return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Consecutive sessions in the same month, in the order given. */
|
|
40
|
+
export function groupByPeriod(sessions: SessionSummary[]): Period[] {
|
|
41
|
+
const periods: Period[] = []
|
|
42
|
+
for (const session of sessions) {
|
|
43
|
+
const label = sessionPeriod(session.ended)
|
|
44
|
+
const last = periods[periods.length - 1]
|
|
45
|
+
if (last && last.label === label) last.sessions.push(session)
|
|
46
|
+
else periods.push({ label, sessions: [session] })
|
|
47
|
+
}
|
|
48
|
+
return periods
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function PeriodDivider({ label }: { label: string }) {
|
|
52
|
+
return (
|
|
53
|
+
<View className="flex-row items-center gap-2 pt-2" testID="session-period">
|
|
54
|
+
<Typography variant="caption" className="leading-none text-text-tertiary">
|
|
55
|
+
{label}
|
|
56
|
+
</Typography>
|
|
57
|
+
<Divider className="flex-1" />
|
|
58
|
+
</View>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* SessionList — the selectable list half of the session reader. Owns no
|
|
64
|
+
* selection state: the host holds `selectedId` and pairs the list with a
|
|
65
|
+
* `SessionDetail`, so the two can be laid out however the surface needs.
|
|
66
|
+
* Sessions are grouped under a hairline divider per calendar month once the
|
|
67
|
+
* list spans more than one.
|
|
68
|
+
*
|
|
69
|
+
* Composes {@link Eyebrow}, {@link Divider} and {@link SessionListItem}.
|
|
70
|
+
*/
|
|
71
|
+
export function SessionList({
|
|
72
|
+
sessions,
|
|
73
|
+
now,
|
|
74
|
+
selectedId,
|
|
75
|
+
onSelect,
|
|
76
|
+
label,
|
|
77
|
+
className,
|
|
78
|
+
}: SessionListProps) {
|
|
79
|
+
const heading = label ?? `${sessions.length} ${sessions.length === 1 ? 'session' : 'sessions'}`
|
|
80
|
+
const periods = useMemo(() => groupByPeriod(sessions), [sessions])
|
|
81
|
+
const showPeriods = periods.length > 1
|
|
82
|
+
return (
|
|
83
|
+
<View className={cn('gap-2', className)}>
|
|
84
|
+
<Eyebrow>{heading}</Eyebrow>
|
|
85
|
+
<View className="gap-1" role={LISTBOX_ROLE} aria-label={heading}>
|
|
86
|
+
{periods.map((period) => (
|
|
87
|
+
<View key={period.label} className="gap-1" role={GROUP_ROLE} aria-label={period.label}>
|
|
88
|
+
{showPeriods ? <PeriodDivider label={period.label} /> : null}
|
|
89
|
+
{period.sessions.map((session) => (
|
|
90
|
+
<SessionListItem
|
|
91
|
+
key={session.id}
|
|
92
|
+
session={session}
|
|
93
|
+
now={now}
|
|
94
|
+
selected={session.id === selectedId}
|
|
95
|
+
onSelect={onSelect ? () => onSelect(session) : undefined}
|
|
96
|
+
/>
|
|
97
|
+
))}
|
|
98
|
+
</View>
|
|
99
|
+
))}
|
|
100
|
+
</View>
|
|
101
|
+
</View>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { Surface } from '../../ui/surface'
|
|
3
|
+
import { SessionListItem } from './SessionListItem'
|
|
4
|
+
import { SESSION_FIXTURE, SESSION_NOW } from './session-fixture'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* **SessionListItem** — one session in the reader's list, led by its title,
|
|
8
|
+
* then age · duration · tasks touched · track. Hover for the full title, the
|
|
9
|
+
* exact end time and the task ids.
|
|
10
|
+
*
|
|
11
|
+
* Composes `Tooltip` · `DateTime` · `Typography`.
|
|
12
|
+
*/
|
|
13
|
+
const meta: Meta<typeof SessionListItem> = {
|
|
14
|
+
title: 'Custom/ActiveWork/SessionListItem',
|
|
15
|
+
component: SessionListItem,
|
|
16
|
+
tags: ['autodocs', 'status:candidate', '!status:review'],
|
|
17
|
+
args: {
|
|
18
|
+
session: SESSION_FIXTURE[0],
|
|
19
|
+
now: SESSION_NOW,
|
|
20
|
+
selected: false,
|
|
21
|
+
},
|
|
22
|
+
argTypes: {
|
|
23
|
+
session: { table: { disable: true } },
|
|
24
|
+
now: { table: { disable: true } },
|
|
25
|
+
onSelect: { action: 'onSelect' },
|
|
26
|
+
},
|
|
27
|
+
decorators: [
|
|
28
|
+
(Story) => (
|
|
29
|
+
<Surface level="base" className="min-h-screen w-72 p-6" testID="page-surface">
|
|
30
|
+
<Story />
|
|
31
|
+
</Surface>
|
|
32
|
+
),
|
|
33
|
+
],
|
|
34
|
+
parameters: {
|
|
35
|
+
layout: 'fullscreen',
|
|
36
|
+
docs: {
|
|
37
|
+
description: {
|
|
38
|
+
component:
|
|
39
|
+
'Composes **Tooltip** · **DateTime** · **Typography**. Used-by ↑ SessionList. Selection is a prop; the row renders the raised fill and accent bar. Hover a row for the full title, exact end time and task ids.',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
export default meta
|
|
45
|
+
|
|
46
|
+
type Story = StoryObj<typeof SessionListItem>
|
|
47
|
+
|
|
48
|
+
/** A canonical session touching six tasks. */
|
|
49
|
+
export const Default: Story = {}
|
|
50
|
+
|
|
51
|
+
/** The selected treatment: raised fill and a leading accent bar. */
|
|
52
|
+
export const Selected: Story = {
|
|
53
|
+
args: { selected: true },
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** An ad-hoc session: the track reads in the footer. */
|
|
57
|
+
export const AdHoc: Story = {
|
|
58
|
+
args: { session: SESSION_FIXTURE[1] },
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A six-minute session mentioning one task. */
|
|
62
|
+
export const Short: Story = {
|
|
63
|
+
args: { session: SESSION_FIXTURE[5] },
|
|
64
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { SessionListItem, sessionRowMeta } from './SessionListItem'
|
|
5
|
+
import { SESSION_FIXTURE, SESSION_NOW } from './session-fixture'
|
|
6
|
+
|
|
7
|
+
const session = SESSION_FIXTURE[0]!
|
|
8
|
+
|
|
9
|
+
describe('SessionListItem', () => {
|
|
10
|
+
it('leads with the title and reads age · duration · tasks · track beneath it', () => {
|
|
11
|
+
render(<SessionListItem session={session} now={SESSION_NOW} />)
|
|
12
|
+
expect(screen.getByText(session.title)).toBeInTheDocument()
|
|
13
|
+
expect(screen.getByRole('option')).toHaveAccessibleName(/1d ago · 1h 4m · 6 tasks · canonical$/)
|
|
14
|
+
expect(screen.getByText('6 tasks')).toBeInTheDocument()
|
|
15
|
+
expect(screen.queryByText('Jul 12')).not.toBeInTheDocument()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('shows the exact end time when the age is hovered', () => {
|
|
19
|
+
render(<SessionListItem session={session} now={SESSION_NOW} />)
|
|
20
|
+
expect(screen.queryByText('07/12/2026, 02:52 PM')).not.toBeInTheDocument()
|
|
21
|
+
fireEvent.pointerEnter(screen.getByTestId('session-age'))
|
|
22
|
+
expect(screen.getByText('07/12/2026, 02:52 PM')).toBeInTheDocument()
|
|
23
|
+
fireEvent.pointerLeave(screen.getByTestId('session-age'))
|
|
24
|
+
expect(screen.queryByText('07/12/2026, 02:52 PM')).not.toBeInTheDocument()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('lists the task ids when the count is hovered', () => {
|
|
28
|
+
render(<SessionListItem session={session} now={SESSION_NOW} />)
|
|
29
|
+
fireEvent.pointerEnter(screen.getByTestId('session-task-count'))
|
|
30
|
+
expect(screen.getByText(/^AW-17 AW-18/)).toBeInTheDocument()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('exposes selection to assistive tech and shows the accent bar only when selected', () => {
|
|
34
|
+
const { rerender } = render(<SessionListItem session={session} now={SESSION_NOW} />)
|
|
35
|
+
expect(screen.getByRole('option')).toHaveAttribute('aria-selected', 'false')
|
|
36
|
+
expect(screen.queryByTestId('session-list-item-accent')).not.toBeInTheDocument()
|
|
37
|
+
rerender(<SessionListItem session={session} now={SESSION_NOW} selected />)
|
|
38
|
+
expect(screen.getByRole('option')).toHaveAttribute('aria-selected', 'true')
|
|
39
|
+
expect(screen.getByTestId('session-list-item-accent')).toBeInTheDocument()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('selects on press, including a press on a hover field', () => {
|
|
43
|
+
const onSelect = vi.fn()
|
|
44
|
+
render(<SessionListItem session={session} now={SESSION_NOW} onSelect={onSelect} />)
|
|
45
|
+
fireEvent.click(screen.getByText('6 tasks'))
|
|
46
|
+
expect(onSelect).toHaveBeenCalledTimes(1)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('singularises one task and drops the count when no task is mentioned', () => {
|
|
50
|
+
expect(sessionRowMeta(session, SESSION_NOW, 1)).toBe('1d ago · 1h 4m · 1 task · canonical')
|
|
51
|
+
expect(sessionRowMeta(session, SESSION_NOW, 0)).toBe('1d ago · 1h 4m · canonical')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('has no a11y violations', async () => {
|
|
55
|
+
const { container } = render(
|
|
56
|
+
<div role="listbox" aria-label="Sessions">
|
|
57
|
+
<SessionListItem session={session} now={SESSION_NOW} selected />
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
61
|
+
})
|
|
62
|
+
})
|