@titan-design/react-ui 0.12.0 → 0.13.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 +46 -47
- package/dist/bodymap.js +768 -701
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +770 -703
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +1320 -215
- package/dist/index.d.ts +1320 -215
- package/dist/index.js +5996 -4498
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5773 -4357
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-CvsFqNtx.d.mts → pages-CpW3OB3g.d.mts} +21 -3
- package/dist/{pages-B1hqnGIC.d.ts → pages-_3Ajpiqu.d.ts} +21 -3
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1435 -1428
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1443 -1436
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +202 -1042
- package/dist/theme/index.d.ts +202 -1042
- package/dist/theme/index.js +215 -300
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +202 -296
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +62 -5
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +62 -5
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/dist/theme/tokens.d.mts +972 -0
- package/dist/theme/tokens.d.ts +972 -0
- package/dist/theme/tokens.js +904 -0
- package/dist/theme/tokens.js.map +1 -0
- package/dist/theme/tokens.mjs +880 -0
- package/dist/theme/tokens.mjs.map +1 -0
- 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/package.json +7 -1
- package/src/arch/arch-graph.freshness.test.ts +52 -0
- package/src/arch/arch-graph.json +5021 -1617
- 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 +181 -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 +4 -3
- 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 +2 -2
- package/src/components/custom/Gauge/Gauge.test.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.tsx +4 -1
- 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 +46 -8
- package/src/components/custom/Scatter/Scatter.test.tsx +2 -2
- package/src/components/custom/Scatter/Scatter.tsx +116 -14
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +40 -7
- 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 +98 -15
- 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 +2 -2
- package/src/components/custom/Treemap/Treemap.test.tsx +1 -1
- package/src/components/custom/Treemap/Treemap.tsx +7 -2
- 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 +9 -1
- package/src/components/custom/Workout/BaseBadge.stories.tsx +19 -7
- package/src/components/custom/Workout/BaseBadge.test.tsx +31 -11
- package/src/components/custom/Workout/BaseBadge.tsx +3 -0
- package/src/components/custom/Workout/BodyMap.stories.tsx +4 -1
- package/src/components/custom/Workout/BodyMap.tsx +3 -1
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +16 -4
- package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +11 -15
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +45 -9
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +10 -8
- package/src/components/custom/Workout/CapacityBandChart.test.tsx +24 -51
- package/src/components/custom/Workout/CapacityBandChart.tsx +139 -108
- package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
- package/src/components/custom/Workout/DeviationBar.test.tsx +5 -2
- package/src/components/custom/Workout/DeviationBar.tsx +21 -16
- package/src/components/custom/Workout/DualVelocityStrip.stories.tsx +182 -0
- package/src/components/custom/Workout/DualVelocityStrip.test.tsx +3 -2
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +8 -7
- package/src/components/custom/Workout/ExerciseCard.test.tsx +13 -3
- package/src/components/custom/Workout/ExerciseCard.tsx +42 -51
- package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +7 -7
- package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +9 -1
- package/src/components/custom/Workout/ExerciseHeading.stories.tsx +8 -8
- package/src/components/custom/Workout/ExerciseHeading.tsx +8 -5
- package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +6 -4
- 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/IntensityBar.stories.tsx +1 -1
- package/src/components/custom/Workout/IntensityBar.test.tsx +5 -1
- package/src/components/custom/Workout/IntensityBar.tsx +4 -2
- 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 +12 -9
- 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 +9 -23
- 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 +1 -1
- 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 +4 -1
- 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 +12 -7
- 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 +19 -3
- package/src/components/custom/Workout/RestTimer.stories.tsx +3 -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 +5 -5
- package/src/components/custom/Workout/SegmentedBar.tsx +6 -2
- 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 +60 -5
- 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 +3 -5
- package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
- 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 +9 -3
- package/src/components/custom/Workout/StrengthTrendChart.test.tsx +15 -37
- package/src/components/custom/Workout/StrengthTrendChart.tsx +31 -30
- 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 +22 -7
- package/src/components/custom/Workout/TempoDisplay.test.tsx +18 -4
- 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 +7 -4
- package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +4 -1
- package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +4 -1
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +12 -6
- package/src/components/custom/Workout/VelocityStrip.tsx +3 -0
- 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 +7 -6
- package/src/components/custom/Workout/ZoneTrack.tsx +10 -9
- package/src/components/custom/Workout/index.ts +5 -0
- package/src/components/custom/Workout/metricText.stories.tsx +6 -5
- package/src/components/custom/Workout/metricText.tsx +4 -0
- package/src/components/custom/Workout/setHeadingKit.tsx +207 -26
- package/src/components/custom/charts/README.md +61 -0
- package/src/components/custom/charts/SetBarChart.test.tsx +30 -1
- package/src/components/custom/charts/SetBarChart.tsx +42 -4
- 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/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 +13 -28
- 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 +90 -27
- 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 +1 -1
- 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 +32 -25
- 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 +6 -26
- 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 +9 -2
- 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 -30
- 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 +2 -12
- package/src/components/ui/indicator/Indicator.stories.tsx +1 -1
- 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 +11 -2
- 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 +29 -19
- package/src/components/ui/pill/Pill.stories.tsx +58 -23
- package/src/components/ui/pill/Pill.test.tsx +101 -3
- package/src/components/ui/pill/Pill.tsx +164 -49
- package/src/components/ui/pill/index.ts +1 -1
- package/src/components/ui/popover/Popover.stories.tsx +5 -7
- 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/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 +5 -14
- 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 +14 -14
- 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 +1 -4
- package/src/components/ui/stack/Stack.stories.tsx +1 -1
- package/src/components/ui/stack/Stack.tsx +1 -1
- package/src/components/ui/surface/Surface.stories.tsx +17 -5
- 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 +7 -22
- 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 +43 -53
- 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 +25 -43
- 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 +100 -47
- package/src/test/no-device-internals.test.ts +3 -3
- 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 +24 -22
- 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 +28 -0
- package/src/theme/elevation.test.ts +82 -56
- package/src/theme/elevation.ts +77 -561
- package/src/theme/global.css +30 -2
- 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/surface-planes.test.ts +37 -0
- package/src/theme/surface-planes.ts +68 -0
- package/src/theme/token-export-guard.test.ts +102 -0
- package/src/theme/tokens/primitives.test.ts +15 -7
- package/src/theme/tokens/primitives.ts +67 -35
- package/src/theme/tokens/semantic.ts +71 -37
- 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.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 +5 -0
- 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,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The {@link LiveFatiguePanel}'s responsive layout, as one pure function (TD-03.58).
|
|
3
|
+
*
|
|
4
|
+
* The panel is container-responsive, not prop-sized (SIZE-D01): it measures its own width
|
|
5
|
+
* in `onLayout` and feeds it here. Everything the two columns need — whether they stack,
|
|
6
|
+
* the padding, the gap, the card's width, and the split of the ONE body height — comes out
|
|
7
|
+
* of this one call, so the hero and the card cannot disagree about any of it.
|
|
8
|
+
*
|
|
9
|
+
* Pure, with no `react-native` import, so the breakpoint behaviour is unit-testable
|
|
10
|
+
* without `onLayout` (which never fires under jsdom).
|
|
11
|
+
*
|
|
12
|
+
* SOURCED vs CHOSEN. The breakpoint edges are titan's own `primitiveBreakpoints` — not
|
|
13
|
+
* invented here. The per-tier padding/gap/card numbers below the `md` edge, the `xl`
|
|
14
|
+
* card-width ratio and its cap, and the stacked height share ARE chosen; each is a single
|
|
15
|
+
* named constant and each is called out in the PR body.
|
|
16
|
+
*/
|
|
17
|
+
import { primitiveBreakpoints } from '../../../theme/tokens/primitives'
|
|
18
|
+
|
|
19
|
+
/** titan's breakpoint scale, re-exported as the panel's tier edges. Sourced, not chosen. */
|
|
20
|
+
export const PANEL_BREAKPOINTS = primitiveBreakpoints
|
|
21
|
+
|
|
22
|
+
export type PanelTier = keyof typeof PANEL_BREAKPOINTS
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The `md` edge (1000) is where the two columns fit side by side: the card's own default
|
|
26
|
+
* 318 plus the hero's usable minimum plus the padding and gap. Below it they stack.
|
|
27
|
+
*/
|
|
28
|
+
const ROW_TIER_MIN = PANEL_BREAKPOINTS.md
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The tier for a measured container width. An unmeasured container (0, before the first
|
|
32
|
+
* `onLayout`) resolves to `md` — today's side-by-side geometry — so the first paint matches
|
|
33
|
+
* what the panel has always rendered rather than flashing a stacked layout.
|
|
34
|
+
*/
|
|
35
|
+
export function panelTier(width: number): PanelTier {
|
|
36
|
+
if (width <= 0) return 'md'
|
|
37
|
+
if (width >= PANEL_BREAKPOINTS.xl) return 'xl'
|
|
38
|
+
if (width >= PANEL_BREAKPOINTS.lg) return 'lg'
|
|
39
|
+
if (width >= PANEL_BREAKPOINTS.md) return 'md'
|
|
40
|
+
if (width >= PANEL_BREAKPOINTS.sm) return 'sm'
|
|
41
|
+
return 'xs'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** titan's long-standing card column width, and the floor every wider tier clamps to. */
|
|
45
|
+
export const CARD_WIDTH_BASE = 318
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* CHOSEN. At wall width a 318px card is ~17% of the content row against ~34% at the `md`
|
|
49
|
+
* edge, so it reads as a sliver beside the hero. 0.22 pulls it back toward the `md`
|
|
50
|
+
* proportion without letting the hero lose its lead.
|
|
51
|
+
*/
|
|
52
|
+
export const CARD_WIDTH_XL_RATIO = 0.22
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* CHOSEN. The cap. The card's internal charts (`RomProgressionChart`, `GhostSpark`) were
|
|
56
|
+
* drawn around ~318 and go sparse rather than richer past roughly 1.5x that, so the
|
|
57
|
+
* expansion stops at 460 (reached at 2091px) instead of tracking the container forever.
|
|
58
|
+
*/
|
|
59
|
+
export const CARD_WIDTH_MAX = 460
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* CHOSEN. Stacked, the CARD takes the larger share of the one body height. Its sections
|
|
63
|
+
* have hard floors — the verdict hero, the three lights, the ROM chart and the ghost spark —
|
|
64
|
+
* while the velocity hero is a bar plot that scales continuously, so squeezing the hero
|
|
65
|
+
* degrades gracefully and squeezing the card overflows it.
|
|
66
|
+
*/
|
|
67
|
+
export const CARD_HEIGHT_SHARE_STACKED = 0.55
|
|
68
|
+
|
|
69
|
+
/** What the hero reserves above its plot for the `VELOCITY · this set` eyebrow, px. */
|
|
70
|
+
export const HERO_EYEBROW_ALLOWANCE = 26
|
|
71
|
+
|
|
72
|
+
// --- the card's own height arithmetic ------------------------------------------------
|
|
73
|
+
// It lives here rather than inside LiveFatigueCard so the panel can reason about the card's
|
|
74
|
+
// floor without importing a component (this module stays react-native-free and testable).
|
|
75
|
+
|
|
76
|
+
/** Floor for the ghost-spark plot inside the card, px. */
|
|
77
|
+
export const CARD_MIN_CHART_HEIGHT = 168
|
|
78
|
+
/** Ceiling for the ghost-spark plot inside the card, px. */
|
|
79
|
+
export const CARD_MAX_CHART_HEIGHT = 240
|
|
80
|
+
/** Share of a pinned card height the ghost-spark plot takes, before the clamp. */
|
|
81
|
+
export const CARD_CHART_HEIGHT_RATIO = 0.4
|
|
82
|
+
/** What an unpinned card gives the ghost-spark plot, px. */
|
|
83
|
+
export const CARD_NATURAL_CHART_HEIGHT = 172
|
|
84
|
+
|
|
85
|
+
/** The ghost-spark plot height for a card height, or the natural height when unpinned. */
|
|
86
|
+
export function cardChartHeight(cardHeight?: number): number {
|
|
87
|
+
if (cardHeight == null) return CARD_NATURAL_CHART_HEIGHT
|
|
88
|
+
const scaled = cardHeight * CARD_CHART_HEIGHT_RATIO
|
|
89
|
+
return Math.round(Math.min(CARD_MAX_CHART_HEIGHT, Math.max(CARD_MIN_CHART_HEIGHT, scaled)))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* MEASURED, not chosen: everything in the card that is not the ghost-spark plot — the 18px
|
|
94
|
+
* padding top and bottom, the verdict hero, the three lights, the two 16px minimum spacers
|
|
95
|
+
* and the ROM chart. Taken from the rendered card in Storybook (`scrollHeight` 384 at the
|
|
96
|
+
* {@link CARD_MIN_CHART_HEIGHT} floor), so a change to those sections needs re-measuring.
|
|
97
|
+
*/
|
|
98
|
+
export const CARD_CHROME_HEIGHT = 216
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The shortest a stacked card can be drawn without its content spilling past the rounded
|
|
102
|
+
* edge. Before this floor existed, an `xs` panel at `bodyHeight` 560 gave the card 301px
|
|
103
|
+
* against a 384px content height and the ghost spark rendered outside the card.
|
|
104
|
+
*/
|
|
105
|
+
export const CARD_MIN_HEIGHT_STACKED = CARD_CHROME_HEIGHT + CARD_MIN_CHART_HEIGHT
|
|
106
|
+
|
|
107
|
+
/** Padding and column gap per tier. `md` and up hold today's values exactly. */
|
|
108
|
+
const TIER_SPACING: Record<PanelTier, { padding: number; gap: number }> = {
|
|
109
|
+
xs: { padding: 16, gap: 12 },
|
|
110
|
+
sm: { padding: 20, gap: 14 },
|
|
111
|
+
md: { padding: 24, gap: 18 },
|
|
112
|
+
lg: { padding: 24, gap: 18 },
|
|
113
|
+
xl: { padding: 24, gap: 18 },
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface PanelLayout {
|
|
117
|
+
tier: PanelTier
|
|
118
|
+
/** Card below the hero rather than beside it. */
|
|
119
|
+
stacked: boolean
|
|
120
|
+
/** Uniform padding around the body row, px. */
|
|
121
|
+
padding: number
|
|
122
|
+
/** Gap between the hero and the card, px — column gap when row, row gap when stacked. */
|
|
123
|
+
gap: number
|
|
124
|
+
/**
|
|
125
|
+
* Card width, px. Stacked, this is the full content width, so the card takes a real
|
|
126
|
+
* number rather than needing a second `onLayout` of its own to size its charts.
|
|
127
|
+
*/
|
|
128
|
+
cardWidth: number
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The card column width for a row tier, before any explicit `cardWidth` override. */
|
|
132
|
+
function rowCardWidth(tier: PanelTier, width: number): number {
|
|
133
|
+
if (tier !== 'xl') return CARD_WIDTH_BASE
|
|
134
|
+
const fluid = Math.round(width * CARD_WIDTH_XL_RATIO)
|
|
135
|
+
return Math.min(CARD_WIDTH_MAX, Math.max(CARD_WIDTH_BASE, fluid))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The whole layout for a measured container width. */
|
|
139
|
+
export function panelLayout(width: number): PanelLayout {
|
|
140
|
+
const tier = panelTier(width)
|
|
141
|
+
const { padding, gap } = TIER_SPACING[tier]
|
|
142
|
+
const stacked = width > 0 && width < ROW_TIER_MIN
|
|
143
|
+
const cardWidth = stacked
|
|
144
|
+
? Math.max(0, Math.round(width - padding * 2))
|
|
145
|
+
: rowCardWidth(tier, width)
|
|
146
|
+
return { tier, stacked, padding, gap, cardWidth }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface PanelBodySplit {
|
|
150
|
+
/** Height handed to the velocity hero's plot, px. */
|
|
151
|
+
heroHeight: number
|
|
152
|
+
/** Height handed to the fatigue card, px. */
|
|
153
|
+
cardHeight: number
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* CHOSEN. The shortest the velocity plot is still worth drawing. Below this the bars stop
|
|
158
|
+
* being readable across a room, so the stacked panel grows past `bodyHeight` rather than
|
|
159
|
+
* flattening the hero any further.
|
|
160
|
+
*/
|
|
161
|
+
export const HERO_MIN_PLOT_HEIGHT = 140
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The ONE body height split across the hero and the card (TD-03.60). Side by side both
|
|
165
|
+
* take the full height; stacked they share it by {@link CARD_HEIGHT_SHARE_STACKED} with the
|
|
166
|
+
* gap taken out first, and neither drops below its floor. Either way both numbers come from
|
|
167
|
+
* this one call, so changing `bodyHeight` moves both or neither.
|
|
168
|
+
*/
|
|
169
|
+
export function panelBodySplit(bodyHeight: number, layout: PanelLayout): PanelBodySplit {
|
|
170
|
+
if (!layout.stacked) {
|
|
171
|
+
return {
|
|
172
|
+
heroHeight: Math.max(0, bodyHeight - HERO_EYEBROW_ALLOWANCE),
|
|
173
|
+
cardHeight: Math.max(0, bodyHeight),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const usable = Math.max(0, bodyHeight - layout.gap)
|
|
177
|
+
const cardHeight = Math.max(
|
|
178
|
+
CARD_MIN_HEIGHT_STACKED,
|
|
179
|
+
Math.round(usable * CARD_HEIGHT_SHARE_STACKED)
|
|
180
|
+
)
|
|
181
|
+
return {
|
|
182
|
+
heroHeight: Math.max(HERO_MIN_PLOT_HEIGHT, usable - cardHeight - HERO_EYEBROW_ALLOWANCE),
|
|
183
|
+
cardHeight,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -2,9 +2,9 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
|
2
2
|
import { Gauge } from './Gauge'
|
|
3
3
|
|
|
4
4
|
const meta: Meta<typeof Gauge> = {
|
|
5
|
-
title: '
|
|
5
|
+
title: 'Custom/Charts/Gauge',
|
|
6
6
|
component: Gauge,
|
|
7
|
-
tags: ['autodocs'],
|
|
7
|
+
tags: ['autodocs', 'status:candidate', '!status:review'],
|
|
8
8
|
argTypes: {
|
|
9
9
|
value: { control: { type: 'range', min: 0, max: 100, step: 1 } },
|
|
10
10
|
size: { control: { type: 'range', min: 100, max: 320, step: 10 } },
|
|
@@ -152,7 +152,10 @@ export function Gauge({
|
|
|
152
152
|
))}
|
|
153
153
|
|
|
154
154
|
{/* Center readout — Metric visual language. */}
|
|
155
|
-
<View
|
|
155
|
+
<View
|
|
156
|
+
style={{ position: 'absolute', top: 0, left: 0, width: size, height: size }}
|
|
157
|
+
className="items-center justify-center"
|
|
158
|
+
>
|
|
156
159
|
<View className="flex-row items-baseline gap-0.5">
|
|
157
160
|
<Text testID="gauge-value" className="text-3xl font-bold" style={{ color: displayColor }}>
|
|
158
161
|
{formatValue(value)}
|
|
@@ -5,7 +5,16 @@ import { Metric, MetricGroup } from './Metric'
|
|
|
5
5
|
const meta: Meta<typeof Metric> = {
|
|
6
6
|
title: 'Components/Molecules/Metric',
|
|
7
7
|
component: Metric,
|
|
8
|
-
tags: ['autodocs'],
|
|
8
|
+
tags: ['autodocs', 'status:candidate', '!status:review'],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'Composes RN primitives only. `MetricGroup` arranges several with a divider rule ' +
|
|
14
|
+
'between them.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
9
18
|
argTypes: {
|
|
10
19
|
value: {
|
|
11
20
|
control: 'text',
|
|
@@ -60,9 +60,7 @@ describe('Metric', () => {
|
|
|
60
60
|
|
|
61
61
|
describe('accessibility', () => {
|
|
62
62
|
it('has no accessibility violations', async () => {
|
|
63
|
-
const { container } = render(
|
|
64
|
-
<Metric value="42" label="Reps" unit="reps" trend="up" />
|
|
65
|
-
)
|
|
63
|
+
const { container } = render(<Metric value="42" label="Reps" unit="reps" trend="up" />)
|
|
66
64
|
const results = await axe(container)
|
|
67
65
|
expect(results).toHaveNoViolations()
|
|
68
66
|
})
|
|
@@ -46,18 +46,10 @@ export function Metric({
|
|
|
46
46
|
<View className={cn('items-center', className)} {...props}>
|
|
47
47
|
<View className="flex-row items-baseline gap-1">
|
|
48
48
|
<Text className={cn(styles.value, 'text-text-primary')}>{value}</Text>
|
|
49
|
-
{unit && (
|
|
50
|
-
|
|
51
|
-
)}
|
|
52
|
-
{trend && (
|
|
53
|
-
<Text className={cn(styles.unit, trendColors[trend])}>
|
|
54
|
-
{trendArrows[trend]}
|
|
55
|
-
</Text>
|
|
56
|
-
)}
|
|
49
|
+
{unit && <Text className={cn(styles.unit, 'text-text-tertiary')}>{unit}</Text>}
|
|
50
|
+
{trend && <Text className={cn(styles.unit, trendColors[trend])}>{trendArrows[trend]}</Text>}
|
|
57
51
|
</View>
|
|
58
|
-
<Text className={cn(styles.label, 'text-text-secondary mt-1')}>
|
|
59
|
-
{label}
|
|
60
|
-
</Text>
|
|
52
|
+
<Text className={cn(styles.label, 'text-text-secondary mt-1')}>{label}</Text>
|
|
61
53
|
</View>
|
|
62
54
|
)
|
|
63
55
|
}
|
|
@@ -67,11 +59,7 @@ export interface MetricGroupProps extends ViewProps {
|
|
|
67
59
|
children: React.ReactNode
|
|
68
60
|
}
|
|
69
61
|
|
|
70
|
-
export function MetricGroup({
|
|
71
|
-
className,
|
|
72
|
-
children,
|
|
73
|
-
...props
|
|
74
|
-
}: MetricGroupProps) {
|
|
62
|
+
export function MetricGroup({ className, children, ...props }: MetricGroupProps) {
|
|
75
63
|
const items = React.Children.toArray(children)
|
|
76
64
|
|
|
77
65
|
return (
|
|
@@ -80,10 +68,7 @@ export function MetricGroup({
|
|
|
80
68
|
<React.Fragment key={i}>
|
|
81
69
|
<View className="flex-1 items-center">{child}</View>
|
|
82
70
|
{i < items.length - 1 && (
|
|
83
|
-
<View
|
|
84
|
-
className="w-px h-8 bg-divider mx-2"
|
|
85
|
-
testID="metric-divider"
|
|
86
|
-
/>
|
|
71
|
+
<View className="w-px h-8 bg-divider mx-2" testID="metric-divider" />
|
|
87
72
|
)}
|
|
88
73
|
</React.Fragment>
|
|
89
74
|
))}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { MarkdownProse, type ProseLinker } from './MarkdownProse'
|
|
4
|
+
|
|
5
|
+
const SAMPLE = `# Shipped AW-17 and AW-18
|
|
6
|
+
|
|
7
|
+
Resolved the cwd to an initiative via the registered worktree, then folded the
|
|
8
|
+
result into [[bootstrap-prompt]]. See #42 for the file-history explorer.
|
|
9
|
+
|
|
10
|
+
## What changed
|
|
11
|
+
|
|
12
|
+
- \`active-work open\` now resolves **worktree-registered** cwds first
|
|
13
|
+
- A missing worktree no longer aborts the launch; AW-116 tracks the prune
|
|
14
|
+
- TD-07.14 unified the greys, so the shell planes moved with it
|
|
15
|
+
|
|
16
|
+
### Follow-ups
|
|
17
|
+
|
|
18
|
+
Nothing blocks AW-22; the reader is next.`
|
|
19
|
+
|
|
20
|
+
const TASK_LINKER: ProseLinker = {
|
|
21
|
+
id: 'task',
|
|
22
|
+
pattern: /\b[A-Z]{2,}-\d+(?:\.\d+)?\b/,
|
|
23
|
+
tone: 'brand',
|
|
24
|
+
}
|
|
25
|
+
const WIKI_LINKER: ProseLinker = {
|
|
26
|
+
id: 'wiki',
|
|
27
|
+
pattern: /\[\[[^\]]+\]\]/,
|
|
28
|
+
tone: 'link',
|
|
29
|
+
label: (ref) => ref.slice(2, -2),
|
|
30
|
+
}
|
|
31
|
+
const PR_LINKER: ProseLinker = { id: 'pr', pattern: /#\d+\b/, tone: 'muted' }
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* **MarkdownProse** — a small markdown subset (headings, bullets, paragraphs,
|
|
35
|
+
* bold, code) rendered as themed prose, with caller-described references
|
|
36
|
+
* auto-linked in one of three tones.
|
|
37
|
+
*
|
|
38
|
+
* Composes `Typography`. Used by `SessionDetail` and the initiative reader.
|
|
39
|
+
*/
|
|
40
|
+
const meta: Meta<typeof MarkdownProse> = {
|
|
41
|
+
title: 'Custom/Prose/MarkdownProse',
|
|
42
|
+
component: MarkdownProse,
|
|
43
|
+
tags: ['autodocs', 'status:candidate', '!status:review'],
|
|
44
|
+
args: {
|
|
45
|
+
body: SAMPLE,
|
|
46
|
+
linkers: [TASK_LINKER, WIKI_LINKER, PR_LINKER],
|
|
47
|
+
},
|
|
48
|
+
argTypes: {
|
|
49
|
+
linkers: { table: { disable: true } },
|
|
50
|
+
},
|
|
51
|
+
decorators: [
|
|
52
|
+
(Story) => (
|
|
53
|
+
<View className="w-full max-w-[640px] p-4">
|
|
54
|
+
<Story />
|
|
55
|
+
</View>
|
|
56
|
+
),
|
|
57
|
+
],
|
|
58
|
+
parameters: {
|
|
59
|
+
docs: {
|
|
60
|
+
description: {
|
|
61
|
+
component:
|
|
62
|
+
'Composes **Typography**. Linkers are tried in order; a match renders in its tone and, when the linker carries `onPress`, as a pressable link.',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
export default meta
|
|
68
|
+
|
|
69
|
+
type Story = StoryObj<typeof MarkdownProse>
|
|
70
|
+
|
|
71
|
+
/** Task ids in brand, `[[links]]` as links, PR numbers muted. */
|
|
72
|
+
export const Default: Story = {}
|
|
73
|
+
|
|
74
|
+
/** The same body with no linkers: plain prose. */
|
|
75
|
+
export const NoLinkers: Story = {
|
|
76
|
+
args: { linkers: [] },
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Pressable references: each linker carries an `onPress`. */
|
|
80
|
+
export const Pressable: Story = {
|
|
81
|
+
args: {
|
|
82
|
+
linkers: [
|
|
83
|
+
{ ...TASK_LINKER, onPress: (id) => console.log('task', id) },
|
|
84
|
+
{ ...WIKI_LINKER, onPress: (name) => console.log('link', name) },
|
|
85
|
+
{ ...PR_LINKER, onPress: (ref) => console.log('pr', ref) },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
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 { MarkdownProse, parseProseBlocks, type ProseLinker } from './MarkdownProse'
|
|
5
|
+
|
|
6
|
+
const taskLinker: ProseLinker = { id: 'task', pattern: /\b[A-Z]{2,}-\d+\b/, tone: 'brand' }
|
|
7
|
+
const wikiLinker: ProseLinker = {
|
|
8
|
+
id: 'wiki',
|
|
9
|
+
pattern: /\[\[[^\]]+\]\]/,
|
|
10
|
+
tone: 'link',
|
|
11
|
+
label: (ref) => ref.slice(2, -2),
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('parseProseBlocks', () => {
|
|
15
|
+
it('joins consecutive lines into one paragraph and splits on blank lines', () => {
|
|
16
|
+
const blocks = parseProseBlocks('one\ntwo\n\nthree')
|
|
17
|
+
expect(blocks).toEqual([
|
|
18
|
+
{ type: 'p', text: 'one two' },
|
|
19
|
+
{ type: 'p', text: 'three' },
|
|
20
|
+
])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('reads headings and bullets, flattening deep headings to h3', () => {
|
|
24
|
+
const blocks = parseProseBlocks('# Title\n## Section\n#### Deep\n- item\n* other')
|
|
25
|
+
expect(blocks.map((b) => b.type)).toEqual(['h1', 'h2', 'h3', 'li', 'li'])
|
|
26
|
+
expect(blocks[2]!.text).toBe('Deep')
|
|
27
|
+
expect(blocks[4]!.text).toBe('other')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('folds an indented continuation line into the bullet above it', () => {
|
|
31
|
+
const blocks = parseProseBlocks('- first `code\n span` continues\nnot indented')
|
|
32
|
+
expect(blocks).toEqual([
|
|
33
|
+
{ type: 'li', text: 'first `code span` continues' },
|
|
34
|
+
{ type: 'p', text: 'not indented' },
|
|
35
|
+
])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('renders nothing for an empty body', () => {
|
|
39
|
+
expect(parseProseBlocks('')).toEqual([])
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('MarkdownProse', () => {
|
|
44
|
+
it('renders bold and code spans without their markers', () => {
|
|
45
|
+
render(<MarkdownProse body="a **bold** and `code` span" />)
|
|
46
|
+
expect(screen.getByText('bold')).toBeInTheDocument()
|
|
47
|
+
expect(screen.getByText('code')).toBeInTheDocument()
|
|
48
|
+
expect(screen.queryByText(/\*\*/)).not.toBeInTheDocument()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('auto-links each linker in its tone and applies the label', () => {
|
|
52
|
+
render(
|
|
53
|
+
<MarkdownProse body="See AW-22 and [[memory-note]]." linkers={[taskLinker, wikiLinker]} />
|
|
54
|
+
)
|
|
55
|
+
expect(screen.getByTestId('prose-ref-task')).toHaveTextContent('AW-22')
|
|
56
|
+
expect(screen.getByTestId('prose-ref-wiki')).toHaveTextContent('memory-note')
|
|
57
|
+
expect(screen.queryByText('[[memory-note]]')).not.toBeInTheDocument()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('makes a reference pressable only when its linker has a handler', () => {
|
|
61
|
+
const onPress = vi.fn()
|
|
62
|
+
render(
|
|
63
|
+
<MarkdownProse body="AW-1 then [[note]]" linkers={[{ ...taskLinker, onPress }, wikiLinker]} />
|
|
64
|
+
)
|
|
65
|
+
fireEvent.click(screen.getByTestId('prose-ref-task'))
|
|
66
|
+
expect(onPress).toHaveBeenCalledWith('AW-1')
|
|
67
|
+
expect(screen.getByTestId('prose-ref-task')).toHaveAttribute('role', 'link')
|
|
68
|
+
expect(screen.getByTestId('prose-ref-wiki')).not.toHaveAttribute('role')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('lets the first matching linker win when two patterns overlap', () => {
|
|
72
|
+
const broad: ProseLinker = { id: 'broad', pattern: /\b[A-Z]+-\d+\b/ }
|
|
73
|
+
render(<MarkdownProse body="AW-9" linkers={[taskLinker, broad]} />)
|
|
74
|
+
expect(screen.getByTestId('prose-ref-task')).toBeInTheDocument()
|
|
75
|
+
expect(screen.queryByTestId('prose-ref-broad')).not.toBeInTheDocument()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('has no a11y violations', async () => {
|
|
79
|
+
const { container } = render(
|
|
80
|
+
<MarkdownProse
|
|
81
|
+
body="# Head\n\nBody with AW-3.\n\n- a bullet"
|
|
82
|
+
linkers={[{ ...taskLinker, onPress: () => {} }]}
|
|
83
|
+
/>
|
|
84
|
+
)
|
|
85
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { useMemo, type ReactNode } from 'react'
|
|
3
|
+
import { Text, View } from 'react-native'
|
|
4
|
+
import { cn } from '../../../utils/cn'
|
|
5
|
+
import { Typography } from '../Typography'
|
|
6
|
+
|
|
7
|
+
/** How a linked reference reads: brand for the domain's own ids, link for cross-references, muted for asides. */
|
|
8
|
+
export type ProseLinkTone = 'brand' | 'link' | 'muted'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A pattern the prose auto-links. Patterns must not carry the `g` flag or
|
|
12
|
+
* capture groups: the renderer combines them into one tokenizer.
|
|
13
|
+
*/
|
|
14
|
+
export interface ProseLinker {
|
|
15
|
+
/** Stable id, used in keys and test ids. */
|
|
16
|
+
id: string
|
|
17
|
+
pattern: RegExp
|
|
18
|
+
tone?: ProseLinkTone
|
|
19
|
+
/** Text to show for a match. Defaults to the match itself. */
|
|
20
|
+
label?: (ref: string) => string
|
|
21
|
+
/** When set the reference is pressable and exposes a link role. */
|
|
22
|
+
onPress?: (ref: string) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ProseBlockType = 'h1' | 'h2' | 'h3' | 'li' | 'p'
|
|
26
|
+
|
|
27
|
+
export interface ProseBlock {
|
|
28
|
+
type: ProseBlockType
|
|
29
|
+
text: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface MarkdownProseProps {
|
|
33
|
+
/** Markdown source. Headings, bullet lists, paragraphs, bold and code are understood. */
|
|
34
|
+
body: string
|
|
35
|
+
/** Reference patterns to auto-link, tried in order. */
|
|
36
|
+
linkers?: ProseLinker[]
|
|
37
|
+
className?: string
|
|
38
|
+
testID?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const BOLD = /\*\*[^*]+\*\*/
|
|
42
|
+
const CODE = /`[^`]+`/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Splits markdown into the block kinds this renderer understands. Consecutive
|
|
46
|
+
* text lines join into one paragraph; a blank line ends it. An indented line
|
|
47
|
+
* straight after a bullet continues that bullet. Deeper headings flatten to
|
|
48
|
+
* h3, since session prose never needs more than three levels.
|
|
49
|
+
*/
|
|
50
|
+
export function parseProseBlocks(body: string): ProseBlock[] {
|
|
51
|
+
const blocks: ProseBlock[] = []
|
|
52
|
+
let paragraph: string[] = []
|
|
53
|
+
const flush = () => {
|
|
54
|
+
if (paragraph.length) blocks.push({ type: 'p', text: paragraph.join(' ') })
|
|
55
|
+
paragraph = []
|
|
56
|
+
}
|
|
57
|
+
for (const raw of body.split('\n')) {
|
|
58
|
+
const line = raw.trim()
|
|
59
|
+
if (!line) {
|
|
60
|
+
flush()
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
const heading = line.match(/^(#{1,6})\s+(.*)/)
|
|
64
|
+
if (heading) {
|
|
65
|
+
flush()
|
|
66
|
+
const level = Math.min(3, heading[1]!.length)
|
|
67
|
+
blocks.push({ type: `h${level}` as ProseBlockType, text: heading[2]! })
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
const item = line.match(/^[-*]\s+(.*)/)
|
|
71
|
+
if (item) {
|
|
72
|
+
flush()
|
|
73
|
+
blocks.push({ type: 'li', text: item[1]! })
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
// Lazy continuation: an indented line straight after a bullet belongs to it.
|
|
77
|
+
const last = blocks[blocks.length - 1]
|
|
78
|
+
if (last?.type === 'li' && paragraph.length === 0 && /^\s/.test(raw)) {
|
|
79
|
+
last.text = `${last.text} ${line}`
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
paragraph.push(line)
|
|
83
|
+
}
|
|
84
|
+
flush()
|
|
85
|
+
return blocks
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// One step denser than body2, with a 20px line so long logs stay readable at that size.
|
|
89
|
+
const BODY_TEXT = 'text-xs leading-5 text-text-secondary'
|
|
90
|
+
|
|
91
|
+
const TONE_CLASS: Record<ProseLinkTone, string> = {
|
|
92
|
+
brand: 'font-semibold text-brand-primary',
|
|
93
|
+
link: 'font-medium text-text-link',
|
|
94
|
+
muted: 'text-text-tertiary',
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function anchored(pattern: RegExp): RegExp {
|
|
98
|
+
return new RegExp(`^(?:${pattern.source})$`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** One tokenizer for bold, code and every linker, so a span is classified exactly once. */
|
|
102
|
+
function buildTokenizer(linkers: ProseLinker[]): RegExp {
|
|
103
|
+
const parts = [BOLD.source, CODE.source, ...linkers.map((l) => l.pattern.source)]
|
|
104
|
+
return new RegExp(`(${parts.join('|')})`, 'g')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function LinkedRef({ linker, value }: { linker: ProseLinker; value: string }) {
|
|
108
|
+
const label = linker.label ? linker.label(value) : value
|
|
109
|
+
const pressable = !!linker.onPress
|
|
110
|
+
return (
|
|
111
|
+
<Text
|
|
112
|
+
className={TONE_CLASS[linker.tone ?? 'link']}
|
|
113
|
+
onPress={pressable ? () => linker.onPress?.(value) : undefined}
|
|
114
|
+
accessibilityRole={pressable ? 'link' : undefined}
|
|
115
|
+
testID={`prose-ref-${linker.id}`}
|
|
116
|
+
>
|
|
117
|
+
{label}
|
|
118
|
+
</Text>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderInline(text: string, linkers: ProseLinker[], tokenizer: RegExp): ReactNode[] {
|
|
123
|
+
const anchoredLinkers = linkers.map((l) => ({ linker: l, test: anchored(l.pattern) }))
|
|
124
|
+
return text
|
|
125
|
+
.split(tokenizer)
|
|
126
|
+
.filter((piece) => piece !== '' && piece !== undefined)
|
|
127
|
+
.map((piece, i) => {
|
|
128
|
+
if (anchored(BOLD).test(piece)) {
|
|
129
|
+
return (
|
|
130
|
+
<Text key={i} className="font-semibold text-text-primary">
|
|
131
|
+
{piece.slice(2, -2)}
|
|
132
|
+
</Text>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
if (anchored(CODE).test(piece)) {
|
|
136
|
+
return (
|
|
137
|
+
<Text key={i} className="font-mono text-text-primary">
|
|
138
|
+
{piece.slice(1, -1)}
|
|
139
|
+
</Text>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
const hit = anchoredLinkers.find((l) => l.test.test(piece))
|
|
143
|
+
if (hit) return <LinkedRef key={i} linker={hit.linker} value={piece} />
|
|
144
|
+
return <Text key={i}>{piece}</Text>
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function Block({ block, inline }: { block: ProseBlock; inline: (text: string) => ReactNode[] }) {
|
|
149
|
+
if (block.type === 'h1' || block.type === 'h2') {
|
|
150
|
+
return (
|
|
151
|
+
<Typography variant={block.type === 'h1' ? 'h5' : 'h6'} className="mt-1.5 text-text-primary">
|
|
152
|
+
{block.text}
|
|
153
|
+
</Typography>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
if (block.type === 'h3') {
|
|
157
|
+
return (
|
|
158
|
+
<Typography variant="subtitle2" className="mt-1 font-bold text-text-primary">
|
|
159
|
+
{inline(block.text)}
|
|
160
|
+
</Typography>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
if (block.type === 'li') {
|
|
164
|
+
return (
|
|
165
|
+
<View className="flex-row gap-2 pl-1">
|
|
166
|
+
<Text className="text-xs leading-5 text-brand-primary">•</Text>
|
|
167
|
+
<Typography variant="body2" className={cn('flex-1', BODY_TEXT)}>
|
|
168
|
+
{inline(block.text)}
|
|
169
|
+
</Typography>
|
|
170
|
+
</View>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
return (
|
|
174
|
+
<Typography variant="body2" className={BODY_TEXT}>
|
|
175
|
+
{inline(block.text)}
|
|
176
|
+
</Typography>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* MarkdownProse — renders a small, predictable markdown subset as themed prose
|
|
182
|
+
* and auto-links references the caller describes.
|
|
183
|
+
*
|
|
184
|
+
* It is deliberately not a full markdown engine: headings, bullet lists,
|
|
185
|
+
* paragraphs, bold and code cover the notes, briefs and session logs this
|
|
186
|
+
* system reads, and anything richer would need a design pass of its own.
|
|
187
|
+
* Composes {@link Typography}. Used by `SessionDetail` (session logs) and the
|
|
188
|
+
* initiative reader (brief and handoff prose).
|
|
189
|
+
*/
|
|
190
|
+
export function MarkdownProse({ body, linkers = [], className, testID }: MarkdownProseProps) {
|
|
191
|
+
const blocks = useMemo(() => parseProseBlocks(body), [body])
|
|
192
|
+
const tokenizer = useMemo(() => buildTokenizer(linkers), [linkers])
|
|
193
|
+
const inline = (text: string) => renderInline(text, linkers, tokenizer)
|
|
194
|
+
return (
|
|
195
|
+
<View className={cn('gap-2', className)} testID={testID}>
|
|
196
|
+
{blocks.map((block, i) => (
|
|
197
|
+
<Block key={i} block={block} inline={inline} />
|
|
198
|
+
))}
|
|
199
|
+
</View>
|
|
200
|
+
)
|
|
201
|
+
}
|