@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
|
@@ -2,9 +2,12 @@ import React, { useState, useCallback, createContext, useContext } from 'react'
|
|
|
2
2
|
import { View, Text, Pressable, type ViewProps, StyleSheet, Platform } from 'react-native'
|
|
3
3
|
import { cn } from '../../../utils/cn'
|
|
4
4
|
import { getHoverColors } from '../../../theme'
|
|
5
|
-
import { greyRamp } from '../../../theme/tokens/primitives'
|
|
6
|
-
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
5
|
+
import { greyRamp, primitiveColors } from '../../../theme/tokens/primitives'
|
|
7
6
|
import { resolveColor } from '../../../theme/resolve-color'
|
|
7
|
+
import { getPressedRecessShadow } from '../../../theme/elevation'
|
|
8
|
+
import { liftStyle } from '../../../theme/lift'
|
|
9
|
+
import { alpha } from '../../../utils/colors'
|
|
10
|
+
import { Surface, useSurfaceMode } from '../surface'
|
|
8
11
|
|
|
9
12
|
export type ToolbarButtonVariant = 'default' | 'raised'
|
|
10
13
|
export type ToolbarButtonSize = 'sm' | 'md' | 'lg'
|
|
@@ -14,6 +17,14 @@ interface ToolbarButtonContextType {
|
|
|
14
17
|
setIsOpen: (open: boolean) => void
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Idle icon/label tone. VW-82: no token resolves to this — it sits between
|
|
22
|
+
* `text-secondary` (#A29F9D) and `text-primary` (#F9F6F3) and is cooler than
|
|
23
|
+
* either, so snapping it to the warm grey ramp is a visual change awaiting the
|
|
24
|
+
* proposed `on-control-idle` role rather than a silent swap.
|
|
25
|
+
*/
|
|
26
|
+
const ICON_IDLE = '#D1D1D1'
|
|
27
|
+
|
|
17
28
|
const ToolbarButtonContext = createContext<ToolbarButtonContextType>({
|
|
18
29
|
isOpen: false,
|
|
19
30
|
setIsOpen: () => {},
|
|
@@ -24,7 +35,7 @@ export interface ToolbarButtonProps extends ViewProps {
|
|
|
24
35
|
label: string
|
|
25
36
|
/** Icon component */
|
|
26
37
|
icon?: React.ReactNode
|
|
27
|
-
/**
|
|
38
|
+
/**
|
|
28
39
|
* Whether the button is in active/selected state.
|
|
29
40
|
* - `true`: pressed/active appearance (darker, white text, orange icon)
|
|
30
41
|
* - `undefined`: pressed appearance (darker, white text/icon)
|
|
@@ -49,19 +60,8 @@ export interface ToolbarButtonProps extends ViewProps {
|
|
|
49
60
|
className?: string
|
|
50
61
|
}
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Base button colours. These were raw `#3C3C3C`/`#2C2C2C` literals — old cold
|
|
55
|
-
// charcoal steps that survived the grey migration only because they were plain
|
|
56
|
-
// strings rather than scale references.
|
|
63
|
+
// Base button colours.
|
|
57
64
|
const BUTTON_BG = greyRamp[800]
|
|
58
|
-
const MENU_BG = greyRamp[900]
|
|
59
|
-
|
|
60
|
-
// Inline depth is a HAIRLINE, not a shadow (TD-07.16). On a ~10% lightness
|
|
61
|
-
// surface a dual-opposing neumorphic shadow has no room below to read; alpha
|
|
62
|
-
// composites by the same amount over any plane, so the edge holds everywhere.
|
|
63
|
-
const EDGE = { borderWidth: 1, borderColor: SEMANTIC['hairline-default'] } as const
|
|
64
|
-
const EDGE_PRESSED = { borderWidth: 1, borderColor: SEMANTIC['hairline-strong'] } as const
|
|
65
65
|
|
|
66
66
|
// Calculate hover colors using color math
|
|
67
67
|
const hoverColors = getHoverColors(BUTTON_BG, 'medium')
|
|
@@ -83,11 +83,11 @@ const textSizeStyles: Record<ToolbarButtonSize, string> = {
|
|
|
83
83
|
* ToolbarButton component for toolbar actions with toggle state support.
|
|
84
84
|
*
|
|
85
85
|
* Features:
|
|
86
|
-
* -
|
|
86
|
+
* - Depth from the fill plus the lift (raised) or the inset recess (active)
|
|
87
87
|
* - Active (pressed) state is the default visual appearance
|
|
88
88
|
* - Set isActive={false} explicitly for the raised/inactive appearance
|
|
89
89
|
* - Orange accent color on icon when isActive={true}
|
|
90
|
-
* - Hover states that lighten background
|
|
90
|
+
* - Hover states that lighten the background
|
|
91
91
|
*
|
|
92
92
|
* @example
|
|
93
93
|
* // Toggle button - orange icon when active
|
|
@@ -123,6 +123,7 @@ export function ToolbarButton({
|
|
|
123
123
|
}: ToolbarButtonProps) {
|
|
124
124
|
const [isOpen, setIsOpen] = useState(false)
|
|
125
125
|
const [isHovered, setIsHovered] = useState(false)
|
|
126
|
+
const mode = useSurfaceMode()
|
|
126
127
|
|
|
127
128
|
const handlePress = useCallback(() => {
|
|
128
129
|
if (isDisabled) return
|
|
@@ -143,31 +144,24 @@ export function ToolbarButton({
|
|
|
143
144
|
// Determine icon color based on state
|
|
144
145
|
const getIconColor = () => {
|
|
145
146
|
if (isActive === true) return resolveColor('brand-primary')
|
|
146
|
-
return showActive ?
|
|
147
|
+
return showActive ? primitiveColors.white : ICON_IDLE
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
// Get shadow and background styles for neumorphic variant
|
|
150
150
|
/**
|
|
151
|
-
* Raised/pressed treatment
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* for the dark half to have somewhere to fall, and these buttons sit at ~10%
|
|
156
|
-
* lightness.
|
|
151
|
+
* Raised/pressed treatment on the depth model: the raised face is a plane one
|
|
152
|
+
* step above the toolbar and wears the lift, the active face is pressed into
|
|
153
|
+
* it and wears the recess. The hairline ring both used to carry is an edge,
|
|
154
|
+
* not depth, and is gone with the rest of the rings on this pass.
|
|
157
155
|
*/
|
|
158
156
|
const getRaisedStyle = () => {
|
|
159
|
-
if (isDisabled)
|
|
160
|
-
return styles.disabledBg
|
|
161
|
-
}
|
|
157
|
+
if (isDisabled) return styles.disabledBg
|
|
162
158
|
if (showActive) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
...EDGE_PRESSED,
|
|
166
|
-
}
|
|
159
|
+
const fill = isHovered ? hoverColors.pressed : greyRamp[900]
|
|
160
|
+
return { backgroundColor: fill, ...getPressedRecessShadow(fill, mode) }
|
|
167
161
|
}
|
|
168
162
|
return {
|
|
169
163
|
backgroundColor: isHovered ? hoverColors.raised : BUTTON_BG,
|
|
170
|
-
...
|
|
164
|
+
...liftStyle(1, mode),
|
|
171
165
|
}
|
|
172
166
|
}
|
|
173
167
|
|
|
@@ -202,20 +196,19 @@ export function ToolbarButton({
|
|
|
202
196
|
>
|
|
203
197
|
{icon && (
|
|
204
198
|
<View className="w-5 h-5 items-center justify-center">
|
|
205
|
-
{React.isValidElement(icon)
|
|
199
|
+
{React.isValidElement(icon)
|
|
206
200
|
? React.cloneElement(icon as React.ReactElement<any>, {
|
|
207
201
|
size: 20,
|
|
208
202
|
width: 20,
|
|
209
203
|
height: 20,
|
|
210
204
|
color: getIconColor(),
|
|
211
|
-
style: {
|
|
205
|
+
style: {
|
|
212
206
|
color: getIconColor(),
|
|
213
207
|
width: 20,
|
|
214
208
|
height: 20,
|
|
215
209
|
},
|
|
216
210
|
})
|
|
217
|
-
: icon
|
|
218
|
-
}
|
|
211
|
+
: icon}
|
|
219
212
|
</View>
|
|
220
213
|
)}
|
|
221
214
|
{showLabel && (
|
|
@@ -237,22 +230,18 @@ export function ToolbarButton({
|
|
|
237
230
|
{menuContent && isOpen && (
|
|
238
231
|
<>
|
|
239
232
|
{/* Backdrop */}
|
|
240
|
-
<Pressable
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
{/* Menu Content */}
|
|
246
|
-
<View
|
|
233
|
+
<Pressable onPress={handleClose} style={StyleSheet.absoluteFill} className="z-40" />
|
|
234
|
+
{/* Menu Content — floating: overlay plane + lift, no ring. */}
|
|
235
|
+
<Surface
|
|
236
|
+
elevation={4}
|
|
237
|
+
rounded={false}
|
|
247
238
|
className={cn(
|
|
248
239
|
'absolute z-50 top-full left-0 mt-1',
|
|
249
|
-
'rounded-lg
|
|
250
|
-
'min-w-[150px] overflow-hidden',
|
|
251
|
-
`bg-[${MENU_BG}]`
|
|
240
|
+
'rounded-lg min-w-[150px] overflow-hidden'
|
|
252
241
|
)}
|
|
253
242
|
>
|
|
254
243
|
{menuContent}
|
|
255
|
-
</
|
|
244
|
+
</Surface>
|
|
256
245
|
</>
|
|
257
246
|
)}
|
|
258
247
|
</View>
|
|
@@ -264,7 +253,7 @@ export function ToolbarButton({
|
|
|
264
253
|
const styles = StyleSheet.create({
|
|
265
254
|
// Disabled - flat gray background, no shadows
|
|
266
255
|
disabledBg: {
|
|
267
|
-
backgroundColor:
|
|
256
|
+
backgroundColor: alpha(primitiveColors.white, 0.12),
|
|
268
257
|
...Platform.select({
|
|
269
258
|
web: { boxShadow: 'none' } as any,
|
|
270
259
|
default: { shadowOpacity: 0, elevation: 0 },
|
|
@@ -10,7 +10,16 @@ const meta: Meta<typeof Tooltip> = {
|
|
|
10
10
|
argTypes: {
|
|
11
11
|
placement: {
|
|
12
12
|
control: 'select',
|
|
13
|
-
options: [
|
|
13
|
+
options: [
|
|
14
|
+
'top',
|
|
15
|
+
'top-start',
|
|
16
|
+
'top-end',
|
|
17
|
+
'bottom',
|
|
18
|
+
'bottom-start',
|
|
19
|
+
'bottom-end',
|
|
20
|
+
'left',
|
|
21
|
+
'right',
|
|
22
|
+
],
|
|
14
23
|
},
|
|
15
24
|
hasArrow: { control: 'boolean' },
|
|
16
25
|
isDisabled: { control: 'boolean' },
|
|
@@ -118,8 +127,8 @@ export const OnText: Story = {
|
|
|
118
127
|
Hover over the{' '}
|
|
119
128
|
<Tooltip label="This is helpful information">
|
|
120
129
|
<Text className="text-brand-primary underline">underlined text</Text>
|
|
121
|
-
</Tooltip>
|
|
122
|
-
|
|
130
|
+
</Tooltip>{' '}
|
|
131
|
+
to see a tooltip.
|
|
123
132
|
</Text>
|
|
124
133
|
</View>
|
|
125
134
|
),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { Text } from 'react-native'
|
|
3
4
|
import { axe } from 'jest-axe'
|
|
4
5
|
import { Tooltip } from './Tooltip'
|
|
5
6
|
|
|
@@ -202,9 +203,7 @@ describe('Tooltip', () => {
|
|
|
202
203
|
hoverTrigger('Hover me')
|
|
203
204
|
expect(screen.getByText('Portal tooltip')).toBeInTheDocument()
|
|
204
205
|
expect(screen.getByTestId('tooltip-portal')).toBeInTheDocument()
|
|
205
|
-
expect(screen.getByTestId('tooltip-portal').parentElement).toBe(
|
|
206
|
-
document.body
|
|
207
|
-
)
|
|
206
|
+
expect(screen.getByTestId('tooltip-portal').parentElement).toBe(document.body)
|
|
208
207
|
})
|
|
209
208
|
|
|
210
209
|
it('applies fixed positioning and z-index to portal tooltip', () => {
|
|
@@ -260,3 +259,22 @@ describe('Tooltip', () => {
|
|
|
260
259
|
})
|
|
261
260
|
})
|
|
262
261
|
})
|
|
262
|
+
|
|
263
|
+
describe('controlled visibility', () => {
|
|
264
|
+
it('follows isOpen instead of its own hover, and renders no pressable wrapper', () => {
|
|
265
|
+
const { rerender } = render(
|
|
266
|
+
<Tooltip label="Controlled" isOpen={false}>
|
|
267
|
+
<Text>Trigger</Text>
|
|
268
|
+
</Tooltip>
|
|
269
|
+
)
|
|
270
|
+
hoverTrigger('Trigger')
|
|
271
|
+
expect(screen.queryByText('Controlled')).not.toBeInTheDocument()
|
|
272
|
+
rerender(
|
|
273
|
+
<Tooltip label="Controlled" isOpen>
|
|
274
|
+
<Text>Trigger</Text>
|
|
275
|
+
</Tooltip>
|
|
276
|
+
)
|
|
277
|
+
expect(screen.getByText('Controlled')).toBeInTheDocument()
|
|
278
|
+
expect(screen.getByText('Trigger').parentElement).not.toHaveAttribute('tabindex')
|
|
279
|
+
})
|
|
280
|
+
})
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect } from 'react'
|
|
2
|
-
import { View, Text,
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react'
|
|
2
|
+
import { View, Text, Platform, type ViewProps } from 'react-native'
|
|
3
3
|
import { cn } from '../../../utils/cn'
|
|
4
|
+
import { Surface } from '../surface'
|
|
5
|
+
import { TriggerSurface } from '../trigger'
|
|
4
6
|
|
|
5
7
|
let createPortal: typeof import('react-dom').createPortal | undefined
|
|
6
8
|
if (Platform.OS === 'web') {
|
|
@@ -45,6 +47,21 @@ export interface TooltipProps extends ViewProps {
|
|
|
45
47
|
className?: string
|
|
46
48
|
/** Render tooltip via portal to escape overflow:hidden ancestors (web only) */
|
|
47
49
|
usePortal?: boolean
|
|
50
|
+
/**
|
|
51
|
+
* Controlled visibility. In this mode the tooltip renders no Pressable of its
|
|
52
|
+
* own, so it can sit inside a pressable row without taking its press; the
|
|
53
|
+
* caller owns hover (see {@link useHoverState}). Needed because RNW ends a
|
|
54
|
+
* wrapper's hover the moment a nested Pressable claims the pointer.
|
|
55
|
+
*/
|
|
56
|
+
isOpen?: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Hover state from pointer enter/leave on any View, for driving a controlled Tooltip. */
|
|
60
|
+
export function useHoverState() {
|
|
61
|
+
const [hovered, setHovered] = useState(false)
|
|
62
|
+
const onPointerEnter = useCallback(() => setHovered(true), [])
|
|
63
|
+
const onPointerLeave = useCallback(() => setHovered(false), [])
|
|
64
|
+
return { hovered, hoverProps: { onPointerEnter, onPointerLeave } }
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
/**
|
|
@@ -74,9 +91,11 @@ export function Tooltip({
|
|
|
74
91
|
isDisabled = false,
|
|
75
92
|
className,
|
|
76
93
|
usePortal: usePortalProp = false,
|
|
94
|
+
isOpen,
|
|
77
95
|
...props
|
|
78
96
|
}: TooltipProps) {
|
|
79
|
-
const [
|
|
97
|
+
const [hovered, setHovered] = useState(false)
|
|
98
|
+
const isVisible = isOpen ?? hovered
|
|
80
99
|
const [portalPos, setPortalPos] = useState<PortalPosition | null>(null)
|
|
81
100
|
const openTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
82
101
|
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
@@ -129,31 +148,37 @@ export function Tooltip({
|
|
|
129
148
|
if (isDisabled) return
|
|
130
149
|
clearTimeouts()
|
|
131
150
|
if (openDelay > 0) {
|
|
132
|
-
openTimeoutRef.current = setTimeout(() =>
|
|
151
|
+
openTimeoutRef.current = setTimeout(() => setHovered(true), openDelay)
|
|
133
152
|
} else {
|
|
134
|
-
|
|
153
|
+
setHovered(true)
|
|
135
154
|
}
|
|
136
155
|
}
|
|
137
156
|
|
|
138
157
|
const hide = () => {
|
|
139
158
|
clearTimeouts()
|
|
140
159
|
if (closeDelay > 0) {
|
|
141
|
-
closeTimeoutRef.current = setTimeout(() =>
|
|
160
|
+
closeTimeoutRef.current = setTimeout(() => setHovered(false), closeDelay)
|
|
142
161
|
} else {
|
|
143
|
-
|
|
162
|
+
setHovered(false)
|
|
144
163
|
}
|
|
145
164
|
}
|
|
146
165
|
|
|
147
|
-
// Arrow positioning
|
|
166
|
+
// Arrow positioning. The arrow is a border triangle, so it takes the same surface token as the box.
|
|
148
167
|
const arrowStyles = {
|
|
149
|
-
top: 'bottom-0 left-1/2 -translate-x-1/2 translate-y-full border-l-transparent border-r-transparent border-b-transparent border-t-
|
|
150
|
-
'top-start':
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
168
|
+
top: 'bottom-0 left-1/2 -translate-x-1/2 translate-y-full border-l-transparent border-r-transparent border-b-transparent border-t-surface-overlay',
|
|
169
|
+
'top-start':
|
|
170
|
+
'bottom-0 left-4 translate-y-full border-l-transparent border-r-transparent border-b-transparent border-t-surface-overlay',
|
|
171
|
+
'top-end':
|
|
172
|
+
'bottom-0 right-4 translate-y-full border-l-transparent border-r-transparent border-b-transparent border-t-surface-overlay',
|
|
173
|
+
bottom:
|
|
174
|
+
'top-0 left-1/2 -translate-x-1/2 -translate-y-full border-l-transparent border-r-transparent border-t-transparent border-b-surface-overlay',
|
|
175
|
+
'bottom-start':
|
|
176
|
+
'top-0 left-4 -translate-y-full border-l-transparent border-r-transparent border-t-transparent border-b-surface-overlay',
|
|
177
|
+
'bottom-end':
|
|
178
|
+
'top-0 right-4 -translate-y-full border-l-transparent border-r-transparent border-t-transparent border-b-surface-overlay',
|
|
179
|
+
left: 'right-0 top-1/2 translate-x-full -translate-y-1/2 border-t-transparent border-b-transparent border-r-transparent border-l-surface-overlay',
|
|
180
|
+
right:
|
|
181
|
+
'left-0 top-1/2 -translate-x-full -translate-y-1/2 border-t-transparent border-b-transparent border-l-transparent border-r-surface-overlay',
|
|
157
182
|
}
|
|
158
183
|
|
|
159
184
|
// Tooltip positioning
|
|
@@ -168,18 +193,12 @@ export function Tooltip({
|
|
|
168
193
|
right: 'left-full top-1/2 -translate-y-1/2 ml-2',
|
|
169
194
|
}
|
|
170
195
|
|
|
196
|
+
// Floating: overlay plane + lift, no ring.
|
|
171
197
|
const tooltipContent = (
|
|
172
|
-
<
|
|
173
|
-
{content ?? <Text className="text-
|
|
174
|
-
{hasArrow && (
|
|
175
|
-
|
|
176
|
-
className={cn(
|
|
177
|
-
'absolute w-0 h-0 border-4',
|
|
178
|
-
arrowStyles[placement]
|
|
179
|
-
)}
|
|
180
|
-
/>
|
|
181
|
-
)}
|
|
182
|
-
</View>
|
|
198
|
+
<Surface elevation={4} rounded={false} className="px-3 py-2 rounded-md max-w-xs">
|
|
199
|
+
{content ?? <Text className="text-text-primary text-sm">{label}</Text>}
|
|
200
|
+
{hasArrow && <View className={cn('absolute w-0 h-0 border-4', arrowStyles[placement])} />}
|
|
201
|
+
</Surface>
|
|
183
202
|
)
|
|
184
203
|
|
|
185
204
|
const renderPortalTooltip = () => {
|
|
@@ -205,15 +224,15 @@ export function Tooltip({
|
|
|
205
224
|
|
|
206
225
|
return (
|
|
207
226
|
<View className="relative" ref={triggerRef} {...props}>
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
227
|
+
{isOpen === undefined ? (
|
|
228
|
+
<TriggerSurface
|
|
229
|
+
handlers={{ onHoverIn: show, onHoverOut: hide, onLongPress: show, onPressOut: hide }}
|
|
230
|
+
>
|
|
231
|
+
{children}
|
|
232
|
+
</TriggerSurface>
|
|
233
|
+
) : (
|
|
234
|
+
children
|
|
235
|
+
)}
|
|
217
236
|
|
|
218
237
|
{isPortalMode
|
|
219
238
|
? renderPortalTooltip()
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { Tooltip } from './Tooltip'
|
|
1
|
+
export { Tooltip, useHoverState } from './Tooltip'
|
|
2
2
|
export type { TooltipProps, TooltipPlacement } from './Tooltip'
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { View, Text } from 'react-native'
|
|
5
|
+
import { TriggerSurface } from './TriggerSurface'
|
|
6
|
+
import { Button, ButtonText } from '../button'
|
|
7
|
+
import { Menu, MenuTrigger, MenuList, MenuItem } from '../menu'
|
|
8
|
+
import { Popover, PopoverTrigger, PopoverContent } from '../popover'
|
|
9
|
+
import { Tooltip } from '../tooltip'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A real pointer press, not a bare synthetic click.
|
|
13
|
+
*
|
|
14
|
+
* The bug (AW-130) only shows when the whole gesture reaches the innermost
|
|
15
|
+
* element: React Native Web's PressResponder calls `stopPropagation()` on the
|
|
16
|
+
* click it handles, so an outer Pressable wrapping a Pressable child never runs
|
|
17
|
+
* its handler. `fireEvent.click` on the outer element sidesteps that entirely.
|
|
18
|
+
*/
|
|
19
|
+
function pressWithPointer(element: Element) {
|
|
20
|
+
fireEvent.mouseDown(element, { button: 0, detail: 1 })
|
|
21
|
+
fireEvent.mouseUp(element, { button: 0, detail: 1 })
|
|
22
|
+
fireEvent.click(element, { button: 0, detail: 1 })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Hover the element itself. Enter events do not bubble, and RNW's `useHover`
|
|
27
|
+
* contains hover to the innermost node, so a wrapper never learns about a hover
|
|
28
|
+
* that lands on a Pressable child.
|
|
29
|
+
*/
|
|
30
|
+
function hoverWithPointer(element: Element) {
|
|
31
|
+
fireEvent.pointerEnter(element)
|
|
32
|
+
fireEvent.mouseEnter(element)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('TriggerSurface', () => {
|
|
36
|
+
it('fires when the child is a Pressable component that claims the gesture', () => {
|
|
37
|
+
const onPress = vi.fn()
|
|
38
|
+
render(
|
|
39
|
+
<TriggerSurface handlers={{ onPress }} accessibilityRole="button">
|
|
40
|
+
<Button>
|
|
41
|
+
<ButtonText>Options</ButtonText>
|
|
42
|
+
</Button>
|
|
43
|
+
</TriggerSurface>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
pressWithPointer(screen.getByText('Options'))
|
|
47
|
+
expect(onPress).toHaveBeenCalledTimes(1)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('fires exactly once, not once per nested pressable', () => {
|
|
51
|
+
const onPress = vi.fn()
|
|
52
|
+
render(
|
|
53
|
+
<TriggerSurface handlers={{ onPress }} accessibilityRole="button">
|
|
54
|
+
<Button>
|
|
55
|
+
<ButtonText>Options</ButtonText>
|
|
56
|
+
</Button>
|
|
57
|
+
</TriggerSurface>
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
pressWithPointer(screen.getByText('Options'))
|
|
61
|
+
pressWithPointer(screen.getByText('Options'))
|
|
62
|
+
expect(onPress).toHaveBeenCalledTimes(2)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('keeps the child own onPress alongside the trigger handler', () => {
|
|
66
|
+
const onPress = vi.fn()
|
|
67
|
+
const childPress = vi.fn()
|
|
68
|
+
render(
|
|
69
|
+
<TriggerSurface handlers={{ onPress }} accessibilityRole="button">
|
|
70
|
+
<Button onPress={childPress}>
|
|
71
|
+
<ButtonText>Options</ButtonText>
|
|
72
|
+
</Button>
|
|
73
|
+
</TriggerSurface>
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
pressWithPointer(screen.getByText('Options'))
|
|
77
|
+
expect(childPress).toHaveBeenCalledTimes(1)
|
|
78
|
+
expect(onPress).toHaveBeenCalledTimes(1)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('fires when the child is a host element that cannot take onPress', () => {
|
|
82
|
+
const onPress = vi.fn()
|
|
83
|
+
render(
|
|
84
|
+
<TriggerSurface handlers={{ onPress }} accessibilityRole="button">
|
|
85
|
+
<span>Options</span>
|
|
86
|
+
</TriggerSurface>
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
pressWithPointer(screen.getByText('Options'))
|
|
90
|
+
expect(onPress).toHaveBeenCalledTimes(1)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('fires when the child is a component with no press handling of its own', () => {
|
|
94
|
+
const onPress = vi.fn()
|
|
95
|
+
render(
|
|
96
|
+
<TriggerSurface handlers={{ onPress }} accessibilityRole="button">
|
|
97
|
+
<View>
|
|
98
|
+
<Text>Options</Text>
|
|
99
|
+
</View>
|
|
100
|
+
</TriggerSurface>
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
pressWithPointer(screen.getByText('Options'))
|
|
104
|
+
expect(onPress).toHaveBeenCalledTimes(1)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('leaves a single button role rather than nesting two', () => {
|
|
108
|
+
const { container } = render(
|
|
109
|
+
<TriggerSurface
|
|
110
|
+
handlers={{ onPress: () => {} }}
|
|
111
|
+
accessibilityRole="button"
|
|
112
|
+
accessibilityState={{ expanded: false }}
|
|
113
|
+
>
|
|
114
|
+
<Button>
|
|
115
|
+
<ButtonText>Options</ButtonText>
|
|
116
|
+
</Button>
|
|
117
|
+
</TriggerSurface>
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
expect(container.querySelectorAll('[role="button"]')).toHaveLength(1)
|
|
121
|
+
expect(container.querySelector('button button')).toBeNull()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('keeps the wrapper out of the tab order when the child owns the press', () => {
|
|
125
|
+
const { container } = render(
|
|
126
|
+
<TriggerSurface handlers={{ onPress: () => {} }} accessibilityRole="button">
|
|
127
|
+
<Button>
|
|
128
|
+
<ButtonText>Options</ButtonText>
|
|
129
|
+
</Button>
|
|
130
|
+
</TriggerSurface>
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(1)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('has no accessibility violations', async () => {
|
|
137
|
+
const { container } = render(
|
|
138
|
+
<TriggerSurface
|
|
139
|
+
handlers={{ onPress: () => {} }}
|
|
140
|
+
accessibilityRole="button"
|
|
141
|
+
accessibilityState={{ expanded: false }}
|
|
142
|
+
>
|
|
143
|
+
<Button>
|
|
144
|
+
<ButtonText>Options</ButtonText>
|
|
145
|
+
</Button>
|
|
146
|
+
</TriggerSurface>
|
|
147
|
+
)
|
|
148
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('trigger families under a real pointer press', () => {
|
|
153
|
+
it('Menu opens when its Button trigger is pressed', () => {
|
|
154
|
+
render(
|
|
155
|
+
<Menu>
|
|
156
|
+
<MenuTrigger>
|
|
157
|
+
<Button>
|
|
158
|
+
<ButtonText>Options</ButtonText>
|
|
159
|
+
</Button>
|
|
160
|
+
</MenuTrigger>
|
|
161
|
+
<MenuList>
|
|
162
|
+
<MenuItem>Edit</MenuItem>
|
|
163
|
+
</MenuList>
|
|
164
|
+
</Menu>
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
pressWithPointer(screen.getByText('Options'))
|
|
168
|
+
expect(screen.getByText('Edit')).toBeInTheDocument()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('Popover opens when its Button trigger is pressed', () => {
|
|
172
|
+
render(
|
|
173
|
+
<Popover>
|
|
174
|
+
<PopoverTrigger>
|
|
175
|
+
<Button>
|
|
176
|
+
<ButtonText>Open</ButtonText>
|
|
177
|
+
</Button>
|
|
178
|
+
</PopoverTrigger>
|
|
179
|
+
<PopoverContent>
|
|
180
|
+
<Text>Details</Text>
|
|
181
|
+
</PopoverContent>
|
|
182
|
+
</Popover>
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
pressWithPointer(screen.getByText('Open'))
|
|
186
|
+
expect(screen.getByText('Details')).toBeInTheDocument()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('Tooltip shows when its Button child is hovered', () => {
|
|
190
|
+
render(
|
|
191
|
+
<Tooltip label="Helpful hint">
|
|
192
|
+
<Button>
|
|
193
|
+
<ButtonText>Hover me</ButtonText>
|
|
194
|
+
</Button>
|
|
195
|
+
</Tooltip>
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
hoverWithPointer(screen.getByRole('button'))
|
|
199
|
+
expect(screen.getByText('Helpful hint')).toBeInTheDocument()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('Tooltip leaves its Button child clickable', () => {
|
|
203
|
+
const onPress = vi.fn()
|
|
204
|
+
render(
|
|
205
|
+
<Tooltip label="Helpful hint">
|
|
206
|
+
<Button onPress={onPress}>
|
|
207
|
+
<ButtonText>Hover me</ButtonText>
|
|
208
|
+
</Button>
|
|
209
|
+
</Tooltip>
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
pressWithPointer(screen.getByText('Hover me'))
|
|
213
|
+
expect(onPress).toHaveBeenCalledTimes(1)
|
|
214
|
+
})
|
|
215
|
+
})
|