@titan-design/react-ui 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bodymap.js +241 -189
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +241 -189
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +1017 -130
- package/dist/index.d.ts +1017 -130
- package/dist/index.js +3058 -1090
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2992 -1091
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-MO0JCySL.d.mts → pages-8u_4WbL-.d.mts} +280 -57
- package/dist/{pages-D7Jlssvp.d.ts → pages-DaWiBOGa.d.ts} +280 -57
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1797 -1292
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1797 -1292
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +464 -268
- package/dist/theme/index.d.ts +464 -268
- package/dist/theme/index.js +303 -191
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +295 -192
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +160 -143
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +160 -143
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +3 -1
- package/src/arch/Architecture.stories.tsx +870 -0
- package/src/arch/annotations.json +48 -0
- package/src/arch/arch-graph.json +4301 -0
- package/src/components/custom/DateTime/DateTime.stories.tsx +83 -7
- package/src/components/custom/DateTime/DateTime.test.tsx +29 -3
- package/src/components/custom/DateTime/DateTime.tsx +55 -15
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.test.tsx +2 -2
- package/src/components/custom/Gauge/Gauge.tsx +6 -3
- package/src/components/custom/Metric/Metric.stories.tsx +1 -1
- package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
- package/src/components/custom/Scatter/Scatter.tsx +3 -5
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
- package/src/components/custom/Table/Table.stories.tsx +1 -1
- package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
- package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
- package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
- package/src/components/custom/TimerReadout/index.ts +1 -0
- package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
- package/src/components/custom/Treemap/Treemap.tsx +3 -5
- package/src/components/custom/Typography/Typography.stories.tsx +46 -28
- package/src/components/custom/Typography/Typography.test.tsx +13 -3
- package/src/components/custom/Typography/Typography.tsx +5 -0
- package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
- package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +11 -16
- package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMap.tsx +6 -3
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +6 -3
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
- package/src/components/custom/Workout/CapacityBandChart.test.tsx +2 -2
- package/src/components/custom/Workout/CapacityBandChart.tsx +9 -6
- package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
- package/src/components/custom/Workout/DeviationBar.test.tsx +1 -1
- package/src/components/custom/Workout/DeviationBar.tsx +7 -4
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
- package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
- package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
- package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
- package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
- package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
- package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
- package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
- package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +6 -6
- package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
- package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
- package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
- package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
- package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
- package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
- package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
- package/src/components/custom/Workout/FatigueMeter.test.tsx +73 -0
- package/src/components/custom/Workout/FatigueMeter.tsx +82 -0
- package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
- package/src/components/custom/Workout/InputBar.tsx +2 -1
- package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
- package/src/components/custom/Workout/IntensityBar.test.tsx +8 -8
- package/src/components/custom/Workout/IntensityBar.tsx +10 -6
- package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
- package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
- package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
- package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoCard.tsx +8 -3
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoProgressBar.test.tsx +1 -1
- package/src/components/custom/Workout/MesoProgressBar.tsx +3 -2
- package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoStatusCard.tsx +20 -13
- package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
- package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
- package/src/components/custom/Workout/MetricTiles.tsx +48 -0
- package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
- package/src/components/custom/Workout/MuscleGroupChip.tsx +7 -4
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
- package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/PrBadge.tsx +5 -2
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
- package/src/components/custom/Workout/PrHistoryModal.tsx +4 -3
- package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +10 -7
- package/src/components/custom/Workout/README.md +146 -4
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
- package/src/components/custom/Workout/ReadinessCheck.tsx +7 -4
- package/src/components/custom/Workout/RestTimer.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.tsx +17 -11
- package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
- package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
- package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
- package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
- package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
- package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
- package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
- package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
- package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
- package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
- package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
- package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
- package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
- package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
- package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
- package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
- package/src/components/custom/Workout/SessionHeader.tsx +152 -0
- package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
- package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
- package/src/components/custom/Workout/SessionRail.tsx +126 -0
- package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
- package/src/components/custom/Workout/SetBar.test.tsx +123 -0
- package/src/components/custom/Workout/SetBar.tsx +171 -0
- package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
- package/src/components/custom/Workout/SetRow.test.tsx +94 -166
- package/src/components/custom/Workout/SetRow.tsx +170 -232
- package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
- package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
- package/src/components/custom/Workout/SetStrip.tsx +71 -0
- package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
- package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
- package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
- package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
- package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
- package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
- package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
- package/src/components/custom/Workout/Sparkline.tsx +2 -1
- package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusDot.test.tsx +2 -2
- package/src/components/custom/Workout/StatusDot.tsx +12 -9
- package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
- package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
- package/src/components/custom/Workout/StatusPill.tsx +88 -0
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
- package/src/components/custom/Workout/StrengthTrendChart.tsx +9 -6
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
- package/src/components/custom/Workout/SupersetWrapper.tsx +2 -1
- package/src/components/custom/Workout/TempoBar.stories.tsx +1 -1
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
- package/src/components/custom/Workout/TempoDisplay.tsx +29 -36
- package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
- package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
- package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +129 -66
- package/src/components/custom/Workout/VelocityStrip.test.tsx +379 -144
- package/src/components/custom/Workout/VelocityStrip.tsx +508 -120
- package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
- package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
- package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
- package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
- package/src/components/custom/Workout/WeekRow.tsx +3 -2
- package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/WeightBadge.tsx +6 -2
- package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
- package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
- package/src/components/custom/Workout/WorkoutCard.tsx +5 -2
- package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
- package/src/components/custom/Workout/WorkoutPill.tsx +12 -8
- package/src/components/custom/Workout/ZoneTrack.stories.tsx +140 -0
- package/src/components/custom/Workout/ZoneTrack.test.tsx +200 -0
- package/src/components/custom/Workout/ZoneTrack.tsx +315 -0
- package/src/components/custom/Workout/icons.tsx +6 -67
- package/src/components/custom/Workout/index.ts +55 -2
- package/src/components/custom/Workout/metricText.stories.tsx +69 -0
- package/src/components/custom/Workout/metricText.tsx +34 -0
- package/src/components/custom/Workout/paceTone.test.ts +29 -0
- package/src/components/custom/Workout/paceTone.ts +27 -0
- package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
- package/src/components/custom/index.ts +1 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
- package/src/components/icons/SvgIcon.tsx +50 -0
- package/src/components/icons/icons.stories.tsx +122 -0
- package/src/components/icons/icons.test.tsx +73 -0
- package/src/components/icons/icons.tsx +153 -0
- package/src/components/icons/index.ts +4 -0
- package/src/components/index.ts +6 -0
- package/src/components/shell/BrandLockup.stories.tsx +27 -0
- package/src/components/shell/BrandLockup.test.tsx +23 -0
- package/src/components/shell/BrandLockup.tsx +47 -0
- package/src/components/shell/DashboardShell.stories.tsx +65 -0
- package/src/components/shell/DashboardShell.test.tsx +42 -0
- package/src/components/shell/DashboardShell.tsx +74 -0
- package/src/components/shell/DeviceIndicator.stories.tsx +28 -0
- package/src/components/shell/DeviceIndicator.test.tsx +26 -0
- package/src/components/shell/DeviceIndicator.tsx +52 -0
- package/src/components/shell/DeviceMenu.stories.tsx +47 -0
- package/src/components/shell/DeviceMenu.test.tsx +40 -0
- package/src/components/shell/DeviceMenu.tsx +65 -0
- package/src/components/shell/DeviceRow.stories.tsx +38 -0
- package/src/components/shell/DeviceRow.test.tsx +37 -0
- package/src/components/shell/DeviceRow.tsx +58 -0
- package/src/components/shell/NavItem.stories.tsx +52 -0
- package/src/components/shell/NavItem.test.tsx +35 -0
- package/src/components/shell/NavItem.tsx +75 -0
- package/src/components/shell/README.md +116 -0
- package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
- package/src/components/shell/SessionStatePill.stories.tsx +27 -0
- package/src/components/shell/SessionStatePill.test.tsx +25 -0
- package/src/components/shell/SessionStatePill.tsx +46 -0
- package/src/components/shell/SideNav.stories.tsx +81 -0
- package/src/components/shell/SideNav.test.tsx +51 -0
- package/src/components/shell/SideNav.tsx +74 -0
- package/src/components/shell/TopBar.stories.tsx +49 -0
- package/src/components/shell/TopBar.test.tsx +36 -0
- package/src/components/shell/TopBar.tsx +89 -0
- package/src/components/shell/index.ts +13 -0
- package/src/components/ui/alert/Alert.stories.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
- package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
- package/src/components/ui/badge/Badge.stories.tsx +1 -1
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
- package/src/components/ui/button/Button.stories.tsx +1 -1
- package/src/components/ui/card/Card.stories.tsx +1 -1
- package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
- package/src/components/ui/chip/Chip.stories.tsx +1 -1
- package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
- package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
- package/src/components/ui/divider/Divider.stories.tsx +1 -1
- package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
- package/src/components/ui/form-field/FormField.stories.tsx +1 -1
- package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
- package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/indicator/Indicator.stories.tsx +38 -2
- package/src/components/ui/indicator/Indicator.test.tsx +20 -1
- package/src/components/ui/indicator/Indicator.tsx +50 -6
- package/src/components/ui/indicator/index.ts +1 -1
- package/src/components/ui/input/Input.stories.tsx +1 -1
- package/src/components/ui/link/Link.stories.tsx +1 -1
- package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
- package/src/components/ui/menu/Menu.stories.tsx +1 -1
- package/src/components/ui/modal/Modal.stories.tsx +1 -1
- package/src/components/ui/pill/Pill.stories.tsx +1 -1
- package/src/components/ui/popover/Popover.stories.tsx +1 -1
- package/src/components/ui/progress/Progress.stories.tsx +1 -1
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/section/Section.stories.tsx +1 -1
- package/src/components/ui/select/Select.stories.tsx +1 -1
- package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
- package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
- package/src/components/ui/spinner/Spinner.tsx +3 -2
- package/src/components/ui/stack/Stack.stories.tsx +1 -1
- package/src/components/ui/surface/Surface.stories.tsx +1 -1
- package/src/components/ui/switch/Switch.stories.tsx +1 -1
- package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
- package/src/components/ui/tile/Tile.stories.tsx +79 -0
- package/src/components/ui/tile/Tile.test.tsx +48 -0
- package/src/components/ui/tile/Tile.tsx +64 -0
- package/src/components/ui/tile/index.ts +2 -0
- package/src/components/ui/toast/Toast.stories.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +2 -1
- package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
- package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useTimer.test.ts +150 -0
- package/src/hooks/useTimer.ts +77 -0
- package/src/index.ts +3 -0
- package/src/pages.ts +1 -1
- package/src/stories/PresetShowcase.stories.tsx +1 -1
- package/src/stories/ThemePresets.stories.tsx +1 -1
- package/src/theme/ColorSystem.stories.tsx +323 -0
- package/src/theme/Colors.stories.tsx +74 -9
- package/src/theme/barrel.ts +1 -0
- package/src/theme/config.ts +20 -3
- package/src/theme/elevation.stories.tsx +1 -1
- package/src/theme/extracted-colors-dataviz.ts +21 -0
- package/src/theme/extracted-colors-ui.ts +20 -0
- package/src/theme/global.css +69 -53
- package/src/theme/gradients.test.ts +25 -0
- package/src/theme/gradients.ts +36 -0
- package/src/theme/shadows.stories.tsx +1 -1
- package/src/theme/tokens/primitives.test.ts +232 -0
- package/src/theme/tokens/primitives.ts +238 -87
- package/src/theme/tokens/semantic.ts +101 -81
- package/src/theme/workout-tokens.ts +26 -21
- package/src/utils/avatar-color.ts +3 -3
- package/tailwind.config.js +15 -3
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { View, Text } from 'react-native'
|
|
4
|
+
import {
|
|
5
|
+
VoltrasMark,
|
|
6
|
+
BluetoothIcon,
|
|
7
|
+
DumbbellIcon,
|
|
8
|
+
StarIcon,
|
|
9
|
+
ActivityIcon,
|
|
10
|
+
HistoryIcon,
|
|
11
|
+
LayersIcon,
|
|
12
|
+
PersonStandingIcon,
|
|
13
|
+
ScaleIcon,
|
|
14
|
+
AlertTriangleIcon,
|
|
15
|
+
TrendingDownIcon,
|
|
16
|
+
CircleSlashIcon,
|
|
17
|
+
AwardIcon,
|
|
18
|
+
InfoIcon,
|
|
19
|
+
} from './icons'
|
|
20
|
+
|
|
21
|
+
const meta: Meta = {
|
|
22
|
+
title: 'Foundations/Icons',
|
|
23
|
+
tags: ['autodocs'],
|
|
24
|
+
parameters: {
|
|
25
|
+
docs: {
|
|
26
|
+
description: {
|
|
27
|
+
component:
|
|
28
|
+
'The shared icon primitive — 24×24 web-`<svg>` glyphs on a common `SvgIcon` base ' +
|
|
29
|
+
'(a11y contract: titled → `role="img"`, untitled → `aria-hidden`; `currentColor` inheritance). ' +
|
|
30
|
+
'Reusable design-system-wide (the shell, Workout badges, etc. compose these).',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
export default meta
|
|
36
|
+
type Story = StoryObj
|
|
37
|
+
|
|
38
|
+
function Swatch({ label, children }: { label: string; children: ReactNode }) {
|
|
39
|
+
return (
|
|
40
|
+
<View className="items-center gap-2">
|
|
41
|
+
{children}
|
|
42
|
+
<Text className="font-mono text-[10px] text-text-tertiary">{label}</Text>
|
|
43
|
+
</View>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const All: Story = {
|
|
48
|
+
render: () => (
|
|
49
|
+
<View className="flex-row gap-8 p-4 items-start">
|
|
50
|
+
<Swatch label="VoltrasMark">
|
|
51
|
+
<View className="text-brand-primary">
|
|
52
|
+
<VoltrasMark size={28} />
|
|
53
|
+
</View>
|
|
54
|
+
</Swatch>
|
|
55
|
+
<Swatch label="BluetoothIcon">
|
|
56
|
+
<View className="text-status-success">
|
|
57
|
+
<BluetoothIcon size={28} />
|
|
58
|
+
</View>
|
|
59
|
+
</Swatch>
|
|
60
|
+
<Swatch label="DumbbellIcon">
|
|
61
|
+
<View className="text-text-primary">
|
|
62
|
+
<DumbbellIcon size={28} />
|
|
63
|
+
</View>
|
|
64
|
+
</Swatch>
|
|
65
|
+
<Swatch label="StarIcon (fill)">
|
|
66
|
+
<View className="text-brand-primary">
|
|
67
|
+
<StarIcon size={28} fill="currentColor" />
|
|
68
|
+
</View>
|
|
69
|
+
</Swatch>
|
|
70
|
+
<Swatch label="ActivityIcon">
|
|
71
|
+
<View className="text-text-primary">
|
|
72
|
+
<ActivityIcon size={28} />
|
|
73
|
+
</View>
|
|
74
|
+
</Swatch>
|
|
75
|
+
<Swatch label="HistoryIcon">
|
|
76
|
+
<View className="text-text-primary">
|
|
77
|
+
<HistoryIcon size={28} />
|
|
78
|
+
</View>
|
|
79
|
+
</Swatch>
|
|
80
|
+
<Swatch label="LayersIcon">
|
|
81
|
+
<View className="text-text-primary">
|
|
82
|
+
<LayersIcon size={28} />
|
|
83
|
+
</View>
|
|
84
|
+
</Swatch>
|
|
85
|
+
<Swatch label="PersonStandingIcon">
|
|
86
|
+
<View className="text-text-primary">
|
|
87
|
+
<PersonStandingIcon size={28} />
|
|
88
|
+
</View>
|
|
89
|
+
</Swatch>
|
|
90
|
+
<Swatch label="ScaleIcon">
|
|
91
|
+
<View className="text-status-error">
|
|
92
|
+
<ScaleIcon size={28} />
|
|
93
|
+
</View>
|
|
94
|
+
</Swatch>
|
|
95
|
+
<Swatch label="AlertTriangleIcon">
|
|
96
|
+
<View className="text-status-error">
|
|
97
|
+
<AlertTriangleIcon size={28} />
|
|
98
|
+
</View>
|
|
99
|
+
</Swatch>
|
|
100
|
+
<Swatch label="TrendingDownIcon">
|
|
101
|
+
<View className="text-status-warning">
|
|
102
|
+
<TrendingDownIcon size={28} />
|
|
103
|
+
</View>
|
|
104
|
+
</Swatch>
|
|
105
|
+
<Swatch label="CircleSlashIcon">
|
|
106
|
+
<View className="text-status-warning">
|
|
107
|
+
<CircleSlashIcon size={28} />
|
|
108
|
+
</View>
|
|
109
|
+
</Swatch>
|
|
110
|
+
<Swatch label="AwardIcon">
|
|
111
|
+
<View className="text-status-success">
|
|
112
|
+
<AwardIcon size={28} />
|
|
113
|
+
</View>
|
|
114
|
+
</Swatch>
|
|
115
|
+
<Swatch label="InfoIcon">
|
|
116
|
+
<View className="text-status-info">
|
|
117
|
+
<InfoIcon size={28} />
|
|
118
|
+
</View>
|
|
119
|
+
</Swatch>
|
|
120
|
+
</View>
|
|
121
|
+
),
|
|
122
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import {
|
|
5
|
+
VoltrasMark,
|
|
6
|
+
BluetoothIcon,
|
|
7
|
+
DumbbellIcon,
|
|
8
|
+
StarIcon,
|
|
9
|
+
ActivityIcon,
|
|
10
|
+
HistoryIcon,
|
|
11
|
+
LayersIcon,
|
|
12
|
+
PersonStandingIcon,
|
|
13
|
+
ScaleIcon,
|
|
14
|
+
AlertTriangleIcon,
|
|
15
|
+
TrendingDownIcon,
|
|
16
|
+
CircleSlashIcon,
|
|
17
|
+
AwardIcon,
|
|
18
|
+
InfoIcon,
|
|
19
|
+
} from './icons'
|
|
20
|
+
import { SvgIcon } from './SvgIcon'
|
|
21
|
+
|
|
22
|
+
describe('icon primitives', () => {
|
|
23
|
+
it('every icon renders an svg', () => {
|
|
24
|
+
;[
|
|
25
|
+
VoltrasMark,
|
|
26
|
+
BluetoothIcon,
|
|
27
|
+
DumbbellIcon,
|
|
28
|
+
StarIcon,
|
|
29
|
+
ActivityIcon,
|
|
30
|
+
HistoryIcon,
|
|
31
|
+
LayersIcon,
|
|
32
|
+
PersonStandingIcon,
|
|
33
|
+
ScaleIcon,
|
|
34
|
+
AlertTriangleIcon,
|
|
35
|
+
TrendingDownIcon,
|
|
36
|
+
CircleSlashIcon,
|
|
37
|
+
AwardIcon,
|
|
38
|
+
InfoIcon,
|
|
39
|
+
].forEach((Icon) => {
|
|
40
|
+
const { container, unmount } = render(<Icon />)
|
|
41
|
+
expect(container.querySelector('svg')).toBeInTheDocument()
|
|
42
|
+
unmount()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('a titled icon is exposed as an accessible image', () => {
|
|
47
|
+
render(<BluetoothIcon title="Devices" />)
|
|
48
|
+
expect(screen.getByRole('img', { name: 'Devices' })).toBeInTheDocument()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('an untitled icon is decorative (aria-hidden, no role)', () => {
|
|
52
|
+
const { container } = render(<VoltrasMark />)
|
|
53
|
+
const svg = container.querySelector('svg')!
|
|
54
|
+
expect(svg.getAttribute('aria-hidden')).toBe('true')
|
|
55
|
+
expect(svg.getAttribute('role')).toBeNull()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('honors size + strokeWidth via the SvgIcon base', () => {
|
|
59
|
+
const { container } = render(
|
|
60
|
+
<SvgIcon size={32} strokeWidth={3}>
|
|
61
|
+
<path d="M0 0" />
|
|
62
|
+
</SvgIcon>
|
|
63
|
+
)
|
|
64
|
+
const svg = container.querySelector('svg')!
|
|
65
|
+
expect(svg.getAttribute('width')).toBe('32')
|
|
66
|
+
expect(svg.getAttribute('stroke-width')).toBe('3')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('a decorative icon has no accessibility violations', async () => {
|
|
70
|
+
const { container } = render(<StarIcon />)
|
|
71
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { SvgIcon, type IconProps } from './SvgIcon'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Voltras brand mark — the ◇ diamond (outline). Pass `fill` for a solid mark.
|
|
5
|
+
*/
|
|
6
|
+
export function VoltrasMark({ size = 14, ...props }: IconProps) {
|
|
7
|
+
return (
|
|
8
|
+
<SvgIcon size={size} {...props}>
|
|
9
|
+
<path d="M12 2 22 12 12 22 2 12Z" />
|
|
10
|
+
</SvgIcon>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Bluetooth glyph (stroked) — e.g. device-connection state (color via `currentColor`).
|
|
16
|
+
*/
|
|
17
|
+
export function BluetoothIcon({ size = 18, ...props }: IconProps) {
|
|
18
|
+
return (
|
|
19
|
+
<SvgIcon size={size} {...props}>
|
|
20
|
+
<path d="M7 7 17 17 12 22 12 2 17 7 7 17" />
|
|
21
|
+
</SvgIcon>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Dumbbell glyph (mirrors lucide-react `Dumbbell`). Outline-only by default. */
|
|
26
|
+
export function DumbbellIcon(props: IconProps) {
|
|
27
|
+
return (
|
|
28
|
+
<SvgIcon {...props}>
|
|
29
|
+
<path d="M14.4 14.4 9.6 9.6" />
|
|
30
|
+
<path d="M18.657 21.485a2 2 0 1 1-2.829-2.828l-1.767 1.768a2 2 0 1 1-2.829-2.829l6.364-6.364a2 2 0 1 1 2.829 2.829l-1.768 1.767a2 2 0 1 1 2.828 2.829z" />
|
|
31
|
+
<path d="m21.5 21.5-1.4-1.4" />
|
|
32
|
+
<path d="M3.9 3.9 2.5 2.5" />
|
|
33
|
+
<path d="M6.404 12.768a2 2 0 1 1-2.829-2.829l1.768-1.767a2 2 0 1 1-2.828-2.829l2.828-2.828a2 2 0 1 1 2.829 2.828l1.767-1.768a2 2 0 1 1 2.829 2.829z" />
|
|
34
|
+
</SvgIcon>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Star glyph (mirrors lucide-react `Star`). Pass `fill` for a solid star. */
|
|
39
|
+
export function StarIcon(props: IconProps) {
|
|
40
|
+
return (
|
|
41
|
+
<SvgIcon {...props}>
|
|
42
|
+
<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" />
|
|
43
|
+
</SvgIcon>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Activity / pulse-line glyph (mirrors lucide-react `Activity`). Shell S2 nav → Live. */
|
|
48
|
+
export function ActivityIcon(props: IconProps) {
|
|
49
|
+
return (
|
|
50
|
+
<SvgIcon {...props}>
|
|
51
|
+
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
|
52
|
+
</SvgIcon>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** History / clock-arrow glyph (mirrors lucide-react `History`). Shell S2 nav → Review. */
|
|
57
|
+
export function HistoryIcon(props: IconProps) {
|
|
58
|
+
return (
|
|
59
|
+
<SvgIcon {...props}>
|
|
60
|
+
<path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
|
|
61
|
+
<path d="M3 3v5h5" />
|
|
62
|
+
<path d="M12 7v5l4 2" />
|
|
63
|
+
</SvgIcon>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Layers / stack glyph (mirrors lucide-react `Layers`). Shell S2 nav → Program (mesocycle stack). */
|
|
68
|
+
export function LayersIcon(props: IconProps) {
|
|
69
|
+
return (
|
|
70
|
+
<SvgIcon {...props}>
|
|
71
|
+
<path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z" />
|
|
72
|
+
<path d="m6.08 9.5-3.49 1.59a1 1 0 0 0 0 1.81l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9a1 1 0 0 0 0-1.83l-3.5-1.59" />
|
|
73
|
+
<path d="m6.08 14.5-3.49 1.59a1 1 0 0 0 0 1.81l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9a1 1 0 0 0 0-1.83l-3.5-1.59" />
|
|
74
|
+
</SvgIcon>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Standing-figure glyph (mirrors lucide-react `PersonStanding`). Shell S2 nav → Body (muscle map). */
|
|
79
|
+
export function PersonStandingIcon(props: IconProps) {
|
|
80
|
+
return (
|
|
81
|
+
<SvgIcon {...props}>
|
|
82
|
+
<circle cx="12" cy="5" r="1" />
|
|
83
|
+
<path d="m9 20 3-6 3 6" />
|
|
84
|
+
<path d="m6 8 6 2 6-2" />
|
|
85
|
+
<path d="M12 10v4" />
|
|
86
|
+
</SvgIcon>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Balance-scale glyph (mirrors lucide-react `Scale`). ExerciseIndicator → `imbalance`. */
|
|
91
|
+
export function ScaleIcon(props: IconProps) {
|
|
92
|
+
return (
|
|
93
|
+
<SvgIcon {...props}>
|
|
94
|
+
<path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" />
|
|
95
|
+
<path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" />
|
|
96
|
+
<path d="M7 21h10" />
|
|
97
|
+
<path d="M12 3v18" />
|
|
98
|
+
<path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2" />
|
|
99
|
+
</SvgIcon>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Warning-triangle glyph (mirrors lucide-react `AlertTriangle`). ExerciseIndicator → `overshoot`. */
|
|
104
|
+
export function AlertTriangleIcon(props: IconProps) {
|
|
105
|
+
return (
|
|
106
|
+
<SvgIcon {...props}>
|
|
107
|
+
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
|
108
|
+
<path d="M12 9v4" />
|
|
109
|
+
<path d="M12 17h.01" />
|
|
110
|
+
</SvgIcon>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Down-trend glyph (mirrors lucide-react `TrendingDown`). ExerciseIndicator → `velocity-loss`. */
|
|
115
|
+
export function TrendingDownIcon(props: IconProps) {
|
|
116
|
+
return (
|
|
117
|
+
<SvgIcon {...props}>
|
|
118
|
+
<path d="M16 17h6v-6" />
|
|
119
|
+
<path d="m22 17-8.5-8.5-5 5L2 7" />
|
|
120
|
+
</SvgIcon>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Slashed-circle glyph (mirrors lucide-react `CircleSlash`). ExerciseIndicator → `missed-reps`. */
|
|
125
|
+
export function CircleSlashIcon(props: IconProps) {
|
|
126
|
+
return (
|
|
127
|
+
<SvgIcon {...props}>
|
|
128
|
+
<circle cx="12" cy="12" r="10" />
|
|
129
|
+
<path d="m9 15 6-6" />
|
|
130
|
+
</SvgIcon>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Medal glyph (mirrors lucide-react `Award`). ExerciseIndicator → `pr`. */
|
|
135
|
+
export function AwardIcon(props: IconProps) {
|
|
136
|
+
return (
|
|
137
|
+
<SvgIcon {...props}>
|
|
138
|
+
<path d="m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526" />
|
|
139
|
+
<circle cx="12" cy="8" r="6" />
|
|
140
|
+
</SvgIcon>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Info-circle glyph (mirrors lucide-react `Info`). ExerciseIndicator → `info`. */
|
|
145
|
+
export function InfoIcon(props: IconProps) {
|
|
146
|
+
return (
|
|
147
|
+
<SvgIcon {...props}>
|
|
148
|
+
<circle cx="12" cy="12" r="10" />
|
|
149
|
+
<path d="M12 16v-4" />
|
|
150
|
+
<path d="M12 8h.01" />
|
|
151
|
+
</SvgIcon>
|
|
152
|
+
)
|
|
153
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
// Icon primitives (shared, design-system-wide)
|
|
2
|
+
export * from './icons'
|
|
3
|
+
|
|
1
4
|
// Re-export all UI components
|
|
2
5
|
export * from './ui'
|
|
3
6
|
|
|
4
7
|
// Re-export custom components
|
|
5
8
|
export * from './custom'
|
|
9
|
+
|
|
10
|
+
// Re-export shell chrome components
|
|
11
|
+
export * from './shell'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { BrandLockup } from './BrandLockup'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof BrandLockup> = {
|
|
5
|
+
title: 'Shell/BrandLockup',
|
|
6
|
+
component: BrandLockup,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { subtitle: 'wall dashboard', showSubtitle: true },
|
|
9
|
+
argTypes: {
|
|
10
|
+
subtitle: { control: 'text' },
|
|
11
|
+
showSubtitle: { control: 'boolean' },
|
|
12
|
+
},
|
|
13
|
+
parameters: {
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component:
|
|
17
|
+
'**Molecule.** Composes [VoltrasMark](?path=/docs/foundations-icons--docs) (icon) + ' +
|
|
18
|
+
'[Typography](?path=/docs/foundations-typography--docs) (wordmark + subtitle). ' +
|
|
19
|
+
'Toggle `showSubtitle` to see the responsive collapse.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
export default meta
|
|
25
|
+
type Story = StoryObj<typeof BrandLockup>
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { BrandLockup } from './BrandLockup'
|
|
5
|
+
|
|
6
|
+
describe('BrandLockup', () => {
|
|
7
|
+
it('renders the wordmark', () => {
|
|
8
|
+
render(<BrandLockup />)
|
|
9
|
+
expect(screen.getByText('VOLTRAS')).toBeInTheDocument()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('shows the subtitle by default and hides it when showSubtitle is false', () => {
|
|
13
|
+
const { rerender } = render(<BrandLockup subtitle="wall dashboard" />)
|
|
14
|
+
expect(screen.getByText('/ wall dashboard')).toBeInTheDocument()
|
|
15
|
+
rerender(<BrandLockup subtitle="wall dashboard" showSubtitle={false} />)
|
|
16
|
+
expect(screen.queryByText('/ wall dashboard')).not.toBeInTheDocument()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('has no accessibility violations', async () => {
|
|
20
|
+
const { container } = render(<BrandLockup />)
|
|
21
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { View, type ViewProps } from 'react-native'
|
|
2
|
+
import { cn } from '../../utils/cn'
|
|
3
|
+
import { Typography } from '../custom/Typography'
|
|
4
|
+
import { VoltrasMark } from '../icons'
|
|
5
|
+
|
|
6
|
+
export interface BrandLockupProps extends ViewProps {
|
|
7
|
+
/** The "/ subtitle" that follows the wordmark. Default "wall dashboard". */
|
|
8
|
+
subtitle?: string
|
|
9
|
+
/** Show the subtitle. Hidden at tablet width and below (S1-Q4 responsive). */
|
|
10
|
+
showSubtitle?: boolean
|
|
11
|
+
className?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Product identity lockup for the top bar: ◇ mark + VOLTRAS wordmark + optional
|
|
16
|
+
* subtitle. Composes the VoltrasMark icon + Typography. S1 · BrandLockup.
|
|
17
|
+
*/
|
|
18
|
+
export function BrandLockup({
|
|
19
|
+
subtitle = 'wall dashboard',
|
|
20
|
+
showSubtitle = true,
|
|
21
|
+
className,
|
|
22
|
+
...props
|
|
23
|
+
}: BrandLockupProps) {
|
|
24
|
+
return (
|
|
25
|
+
<View className={cn('flex-row items-center gap-[7px]', className)} {...props}>
|
|
26
|
+
<View className="text-brand-primary">
|
|
27
|
+
<VoltrasMark size={14} color="currentColor" />
|
|
28
|
+
</View>
|
|
29
|
+
<Typography
|
|
30
|
+
variant="button"
|
|
31
|
+
color="primary"
|
|
32
|
+
className="text-[12px] font-extrabold tracking-[1.5px]"
|
|
33
|
+
>
|
|
34
|
+
VOLTRAS
|
|
35
|
+
</Typography>
|
|
36
|
+
{showSubtitle && subtitle ? (
|
|
37
|
+
<Typography
|
|
38
|
+
variant="button"
|
|
39
|
+
color="tertiary"
|
|
40
|
+
className="text-[10px] font-semibold tracking-[0.4px]"
|
|
41
|
+
>
|
|
42
|
+
/ {subtitle}
|
|
43
|
+
</Typography>
|
|
44
|
+
) : null}
|
|
45
|
+
</View>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { DashboardShell } from './DashboardShell'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `Pages/DashboardShell` — the wall-dashboard chrome as one registered surface.
|
|
7
|
+
* Composes the shell-nav island (SideNav + TopBar and everything they pull in) over a
|
|
8
|
+
* `children` content slot, so the dashboard app mounts a single component.
|
|
9
|
+
*/
|
|
10
|
+
const meta: Meta<typeof DashboardShell> = {
|
|
11
|
+
title: 'Pages/DashboardShell',
|
|
12
|
+
component: DashboardShell,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
parameters: {
|
|
15
|
+
layout: 'fullscreen',
|
|
16
|
+
docs: {
|
|
17
|
+
description: {
|
|
18
|
+
component:
|
|
19
|
+
'**Page shell.** The wall-dashboard chrome. Composes ' +
|
|
20
|
+
'[SideNav](?path=/docs/shell-sidenav--docs) + ' +
|
|
21
|
+
'[TopBar](?path=/docs/shell-topbar--docs) — which pulls in DeviceMenu, DeviceRow, ' +
|
|
22
|
+
'DeviceIndicator, SessionStatePill, BrandLockup, DateTime and Divider — over a ' +
|
|
23
|
+
'`children` content slot. Mount this instead of re-assembling the chrome per view.',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
argTypes: {
|
|
28
|
+
state: { control: 'select', options: ['live', 'rest', 'idle'] },
|
|
29
|
+
activeKey: { control: 'select', options: ['live', 'review', 'program', 'body'] },
|
|
30
|
+
liveKey: { control: 'select', options: [null, 'live', 'review', 'program', 'body'] },
|
|
31
|
+
onNavigate: { control: false },
|
|
32
|
+
onSelectDevice: { control: false },
|
|
33
|
+
},
|
|
34
|
+
decorators: [
|
|
35
|
+
(Story) => (
|
|
36
|
+
<View style={{ height: '100vh' as unknown as number, backgroundColor: '#0E0E0E' }}>
|
|
37
|
+
<Story />
|
|
38
|
+
</View>
|
|
39
|
+
),
|
|
40
|
+
],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default meta
|
|
44
|
+
type Story = StoryObj<typeof DashboardShell>
|
|
45
|
+
|
|
46
|
+
export const Default: Story = {
|
|
47
|
+
args: { activeKey: 'live', state: 'live' },
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const WithContent: Story = {
|
|
51
|
+
args: { activeKey: 'review', state: 'rest', liveKey: 'live' },
|
|
52
|
+
render: (args) => (
|
|
53
|
+
<DashboardShell {...args}>
|
|
54
|
+
<View style={{ flex: 1, padding: 28 }}>
|
|
55
|
+
<Text style={{ color: '#E5E5E5', fontSize: 22, fontWeight: '700' }}>Review</Text>
|
|
56
|
+
<Text style={{ color: '#8A8A8A', fontSize: 13, marginTop: 6 }}>
|
|
57
|
+
A page mounts its content here; the shell keeps the nav + top-bar chrome persistent.
|
|
58
|
+
</Text>
|
|
59
|
+
</View>
|
|
60
|
+
</DashboardShell>
|
|
61
|
+
),
|
|
62
|
+
parameters: {
|
|
63
|
+
docs: { description: { story: 'The shell with a page mounted in its content slot.' } },
|
|
64
|
+
},
|
|
65
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { Text } from 'react-native'
|
|
4
|
+
import { axe } from 'jest-axe'
|
|
5
|
+
import { DashboardShell } from './DashboardShell'
|
|
6
|
+
|
|
7
|
+
describe('DashboardShell', () => {
|
|
8
|
+
it('composes the SideNav rail and its categories', () => {
|
|
9
|
+
render(<DashboardShell activeKey="live" />)
|
|
10
|
+
expect(screen.getByRole('tablist')).toBeInTheDocument()
|
|
11
|
+
;['Live', 'Review', 'Plan', 'Body'].forEach((name) => {
|
|
12
|
+
expect(screen.getByRole('tab', { name })).toBeInTheDocument()
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('renders the content-slot placeholder when no children are given', () => {
|
|
17
|
+
render(<DashboardShell activeKey="live" />)
|
|
18
|
+
expect(screen.getByText('main content region')).toBeInTheDocument()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('mounts children in the content slot and drops the placeholder', () => {
|
|
22
|
+
render(
|
|
23
|
+
<DashboardShell activeKey="review">
|
|
24
|
+
<Text>my page body</Text>
|
|
25
|
+
</DashboardShell>
|
|
26
|
+
)
|
|
27
|
+
expect(screen.getByText('my page body')).toBeInTheDocument()
|
|
28
|
+
expect(screen.queryByText('main content region')).not.toBeInTheDocument()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('forwards nav taps through onNavigate', () => {
|
|
32
|
+
const onNavigate = vi.fn()
|
|
33
|
+
render(<DashboardShell activeKey="live" onNavigate={onNavigate} />)
|
|
34
|
+
fireEvent.click(screen.getByRole('tab', { name: 'Body' }))
|
|
35
|
+
expect(onNavigate).toHaveBeenCalledWith('body')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('has no a11y violations', async () => {
|
|
39
|
+
const { container } = render(<DashboardShell activeKey="live" />)
|
|
40
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { cn } from '../../utils/cn'
|
|
4
|
+
import { SideNav, type SideNavItem } from './SideNav'
|
|
5
|
+
import { TopBar } from './TopBar'
|
|
6
|
+
import { type Device } from './DeviceRow'
|
|
7
|
+
import { type SessionState } from './SessionStatePill'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `DashboardShell` — the wall-dashboard chrome: a persistent {@link SideNav} rail +
|
|
11
|
+
* {@link TopBar} over a main content region. A thin frame that wires the shell-nav
|
|
12
|
+
* organisms together and exposes a `children` content slot, so the dashboard app
|
|
13
|
+
* mounts one component instead of re-assembling the chrome. Driven entirely by props.
|
|
14
|
+
*/
|
|
15
|
+
export interface DashboardShellProps {
|
|
16
|
+
/** Active nav category key. */
|
|
17
|
+
activeKey?: string
|
|
18
|
+
/** Nav categories, top → bottom. Defaults to SideNav's four dashboard categories. */
|
|
19
|
+
navItems?: SideNavItem[]
|
|
20
|
+
/** A category with off-view live activity → a quiet cue on that item. */
|
|
21
|
+
liveKey?: string | null
|
|
22
|
+
/** Global session state → the TopBar status pill. */
|
|
23
|
+
state?: SessionState
|
|
24
|
+
/** Connected devices → the TopBar connection glyph + dropdown. */
|
|
25
|
+
devices?: Device[]
|
|
26
|
+
/** TopBar brand subtitle. */
|
|
27
|
+
subtitle?: string
|
|
28
|
+
onNavigate?: (key: string) => void
|
|
29
|
+
onSelectDevice?: (device: Device) => void
|
|
30
|
+
/** Main content region. A placeholder renders when omitted. */
|
|
31
|
+
children?: ReactNode
|
|
32
|
+
className?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEMO_DEVICES: Device[] = [
|
|
36
|
+
{ id: 'Voltra-A3F2', nickname: 'Left Cable', slot: 'L', state: 'connected' },
|
|
37
|
+
{ id: 'Voltra-9B1C', nickname: 'Right Cable', slot: 'R', state: 'connected' },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
function ContentPlaceholder() {
|
|
41
|
+
return (
|
|
42
|
+
<View className="flex-1 items-center justify-center">
|
|
43
|
+
<Text className="font-heading text-sm text-text-tertiary">main content region</Text>
|
|
44
|
+
</View>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function DashboardShell({
|
|
49
|
+
activeKey = 'live',
|
|
50
|
+
navItems,
|
|
51
|
+
liveKey = null,
|
|
52
|
+
state = 'live',
|
|
53
|
+
devices = DEMO_DEVICES,
|
|
54
|
+
subtitle = 'wall dashboard',
|
|
55
|
+
onNavigate,
|
|
56
|
+
onSelectDevice,
|
|
57
|
+
children,
|
|
58
|
+
className,
|
|
59
|
+
}: DashboardShellProps) {
|
|
60
|
+
return (
|
|
61
|
+
<View className={cn('flex-1 flex-row bg-surface-base', className)}>
|
|
62
|
+
<SideNav activeKey={activeKey} items={navItems} liveKey={liveKey} onNavigate={onNavigate} />
|
|
63
|
+
<View className="flex-1">
|
|
64
|
+
<TopBar
|
|
65
|
+
state={state}
|
|
66
|
+
devices={devices}
|
|
67
|
+
subtitle={subtitle}
|
|
68
|
+
onSelectDevice={onSelectDevice}
|
|
69
|
+
/>
|
|
70
|
+
<View className="flex-1">{children ?? <ContentPlaceholder />}</View>
|
|
71
|
+
</View>
|
|
72
|
+
</View>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { DeviceIndicator } from './DeviceIndicator'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof DeviceIndicator> = {
|
|
5
|
+
title: 'Shell/DeviceIndicator',
|
|
6
|
+
component: DeviceIndicator,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { status: 'connected' },
|
|
9
|
+
argTypes: {
|
|
10
|
+
status: { control: 'select', options: ['connected', 'degraded', 'lost'] },
|
|
11
|
+
label: { control: 'text' },
|
|
12
|
+
onPress: { control: false },
|
|
13
|
+
},
|
|
14
|
+
parameters: {
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component:
|
|
18
|
+
'**Molecule.** Composes [BluetoothIcon](?path=/docs/foundations-icons--docs), color-coded by ' +
|
|
19
|
+
'connection state via a `text-status-*-vivid` token — the glyph color alone carries the fault ' +
|
|
20
|
+
'(no separate badge). Switch the `status` control to see connected / degraded / lost.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
export default meta
|
|
26
|
+
type Story = StoryObj<typeof DeviceIndicator>
|
|
27
|
+
|
|
28
|
+
export const Default: Story = {}
|