@titan-design/react-ui 0.6.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 +12 -7
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +12 -7
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +855 -124
- package/dist/index.d.ts +855 -124
- package/dist/index.js +2276 -801
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2230 -802
- 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 +1255 -798
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1255 -798
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +16 -82
- package/dist/theme/index.d.ts +16 -82
- package/dist/theme/index.js +13 -115
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +13 -115
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +14 -6
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +14 -6
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +1 -1
- package/src/arch/Architecture.stories.tsx +870 -0
- package/src/arch/annotations.json +48 -0
- package/src/arch/arch-graph.json +4301 -0
- package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
- package/src/components/custom/Metric/Metric.stories.tsx +1 -1
- package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
- package/src/components/custom/Table/Table.stories.tsx +1 -1
- package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
- package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
- package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
- package/src/components/custom/TimerReadout/index.ts +1 -0
- package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
- package/src/components/custom/Typography/Typography.stories.tsx +1 -1
- package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
- package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +9 -15
- package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
- package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
- package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
- package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
- package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
- package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
- package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
- package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
- package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
- package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -5
- package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
- package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
- package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
- package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
- package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
- package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
- package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
- package/src/components/custom/Workout/FatigueMeter.test.tsx +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/IntensityBar.stories.tsx +1 -1
- package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
- package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
- package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
- package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
- package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
- package/src/components/custom/Workout/MetricTiles.tsx +48 -0
- package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
- package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
- package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -6
- package/src/components/custom/Workout/README.md +146 -4
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.tsx +15 -10
- package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
- package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
- package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
- package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
- package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
- package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
- package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
- package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
- package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
- package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
- package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
- package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
- package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
- package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
- package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
- package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
- package/src/components/custom/Workout/SessionHeader.tsx +152 -0
- package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
- package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
- package/src/components/custom/Workout/SessionRail.tsx +126 -0
- package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
- package/src/components/custom/Workout/SetBar.test.tsx +123 -0
- package/src/components/custom/Workout/SetBar.tsx +171 -0
- package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
- package/src/components/custom/Workout/SetRow.test.tsx +94 -166
- package/src/components/custom/Workout/SetRow.tsx +169 -232
- package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
- package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
- package/src/components/custom/Workout/SetStrip.tsx +71 -0
- package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
- package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
- package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
- package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
- package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
- package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
- package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
- package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
- package/src/components/custom/Workout/StatusPill.tsx +88 -0
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
- package/src/components/custom/Workout/TempoBar.stories.tsx +1 -1
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
- package/src/components/custom/Workout/TempoDisplay.tsx +23 -33
- package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
- package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
- package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +129 -66
- package/src/components/custom/Workout/VelocityStrip.test.tsx +376 -141
- 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/WeightBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
- package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
- package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
- package/src/components/custom/Workout/ZoneTrack.stories.tsx +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/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/icons.stories.tsx +67 -2
- package/src/components/icons/icons.test.tsx +32 -2
- package/src/components/icons/icons.tsx +108 -0
- package/src/components/shell/BrandLockup.stories.tsx +3 -3
- package/src/components/shell/DashboardShell.stories.tsx +65 -0
- package/src/components/shell/DashboardShell.test.tsx +42 -0
- package/src/components/shell/DashboardShell.tsx +74 -0
- package/src/components/shell/DeviceIndicator.stories.tsx +2 -2
- package/src/components/shell/DeviceMenu.stories.tsx +5 -5
- package/src/components/shell/DeviceRow.stories.tsx +3 -3
- package/src/components/shell/NavItem.stories.tsx +52 -0
- package/src/components/shell/NavItem.test.tsx +35 -0
- package/src/components/shell/NavItem.tsx +75 -0
- package/src/components/shell/README.md +30 -0
- package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
- package/src/components/shell/SessionStatePill.stories.tsx +3 -3
- package/src/components/shell/SessionStatePill.tsx +1 -1
- package/src/components/shell/SideNav.stories.tsx +81 -0
- package/src/components/shell/SideNav.test.tsx +51 -0
- package/src/components/shell/SideNav.tsx +74 -0
- package/src/components/shell/TopBar.stories.tsx +5 -5
- package/src/components/shell/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +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 +2 -2
- package/src/components/ui/indicator/Indicator.test.tsx +1 -0
- package/src/components/ui/indicator/Indicator.tsx +6 -1
- package/src/components/ui/input/Input.stories.tsx +1 -1
- package/src/components/ui/link/Link.stories.tsx +1 -1
- package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
- package/src/components/ui/menu/Menu.stories.tsx +1 -1
- package/src/components/ui/modal/Modal.stories.tsx +1 -1
- package/src/components/ui/pill/Pill.stories.tsx +1 -1
- package/src/components/ui/popover/Popover.stories.tsx +1 -1
- package/src/components/ui/progress/Progress.stories.tsx +1 -1
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/section/Section.stories.tsx +1 -1
- package/src/components/ui/select/Select.stories.tsx +1 -1
- package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
- package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
- package/src/components/ui/stack/Stack.stories.tsx +1 -1
- package/src/components/ui/surface/Surface.stories.tsx +1 -1
- package/src/components/ui/switch/Switch.stories.tsx +1 -1
- package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
- package/src/components/ui/tile/Tile.stories.tsx +79 -0
- package/src/components/ui/tile/Tile.test.tsx +48 -0
- package/src/components/ui/tile/Tile.tsx +64 -0
- package/src/components/ui/tile/index.ts +2 -0
- package/src/components/ui/toast/Toast.stories.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
- package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
- package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useTimer.test.ts +150 -0
- package/src/hooks/useTimer.ts +77 -0
- package/src/index.ts +3 -0
- package/src/pages.ts +1 -1
- package/src/stories/PresetShowcase.stories.tsx +1 -1
- package/src/stories/ThemePresets.stories.tsx +1 -1
- package/src/theme/ColorSystem.stories.tsx +1 -1
- package/src/theme/Colors.stories.tsx +5 -8
- package/src/theme/config.ts +4 -0
- package/src/theme/elevation.stories.tsx +1 -1
- package/src/theme/global.css +5 -1
- package/src/theme/shadows.stories.tsx +1 -1
- package/src/theme/tokens/primitives.ts +11 -107
- package/src/theme/tokens/semantic.ts +8 -0
- package/src/theme/workout-tokens.ts +11 -7
- package/tailwind.config.js +5 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { Tile } from './Tile'
|
|
4
|
+
import { getSemanticColors } from '../../../theme'
|
|
5
|
+
|
|
6
|
+
const ACCENT = getSemanticColors('dark')['status-warning']
|
|
7
|
+
const SUCCESS = getSemanticColors('dark')['status-success']
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof Tile> = {
|
|
10
|
+
title: 'Components/Atoms/Tile',
|
|
11
|
+
component: Tile,
|
|
12
|
+
tags: ['autodocs'],
|
|
13
|
+
parameters: {
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component: '**Atom.** A compact card preset; grouped by SessionMetrics / ScheduleTiles.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
argTypes: {
|
|
21
|
+
label: { control: 'text', description: 'Uppercase micro-label' },
|
|
22
|
+
value: { control: 'text', description: 'Primary mono value' },
|
|
23
|
+
valueColor: { control: 'color', description: 'Tints the value' },
|
|
24
|
+
align: {
|
|
25
|
+
control: 'radio',
|
|
26
|
+
options: ['center', 'start'],
|
|
27
|
+
description: 'Content alignment',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default meta
|
|
33
|
+
type Story = StoryObj<typeof Tile>
|
|
34
|
+
|
|
35
|
+
export const Default: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
label: 'Volume',
|
|
38
|
+
value: '76%',
|
|
39
|
+
align: 'center',
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const Row: Story = {
|
|
44
|
+
render: () => (
|
|
45
|
+
<View style={{ flexDirection: 'row', gap: 4, width: 246 }}>
|
|
46
|
+
<Tile label="Volume" value="76%" />
|
|
47
|
+
<Tile label="Load" value="7.3k" />
|
|
48
|
+
<Tile label="Fatigue" value="MOD" valueColor={ACCENT} />
|
|
49
|
+
</View>
|
|
50
|
+
),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const ScheduleTiles: Story = {
|
|
54
|
+
render: () => (
|
|
55
|
+
<View style={{ flexDirection: 'row', gap: 4, width: 246 }}>
|
|
56
|
+
<Tile label="Date" value="Jul 10" />
|
|
57
|
+
<Tile label="Time" value="6:30 PM" />
|
|
58
|
+
<Tile label="Until" value="5h" valueColor={ACCENT} />
|
|
59
|
+
</View>
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const ValueColors: Story = {
|
|
64
|
+
render: () => (
|
|
65
|
+
<View style={{ flexDirection: 'row', gap: 4, width: 246 }}>
|
|
66
|
+
<Tile label="Default" value="12" />
|
|
67
|
+
<Tile label="Success" value="OK" valueColor={SUCCESS} />
|
|
68
|
+
<Tile label="Warning" value="MOD" valueColor={ACCENT} />
|
|
69
|
+
</View>
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const StartAligned: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
label: 'Program',
|
|
76
|
+
value: 'Pull A',
|
|
77
|
+
align: 'start',
|
|
78
|
+
},
|
|
79
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { Tile } from './Tile'
|
|
5
|
+
|
|
6
|
+
describe('Tile', () => {
|
|
7
|
+
it('renders label and value', () => {
|
|
8
|
+
render(<Tile label="Volume" value="76%" />)
|
|
9
|
+
expect(screen.getByText('Volume')).toBeInTheDocument()
|
|
10
|
+
expect(screen.getByText('76%')).toBeInTheDocument()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('applies valueColor to the value text', () => {
|
|
14
|
+
render(<Tile label="Fatigue" value="MOD" valueColor="#F5A623" />)
|
|
15
|
+
const value = screen.getByText('MOD')
|
|
16
|
+
expect(value).toHaveStyle({ color: '#F5A623' })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('does not set an inline color when valueColor is omitted', () => {
|
|
20
|
+
render(<Tile label="Volume" value="76%" />)
|
|
21
|
+
const value = screen.getByText('76%')
|
|
22
|
+
expect(value.style.color).toBe('')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('renders with start alignment', () => {
|
|
26
|
+
render(<Tile label="Program" value="Pull A" align="start" />)
|
|
27
|
+
expect(screen.getByText('Program')).toBeInTheDocument()
|
|
28
|
+
expect(screen.getByText('Pull A')).toBeInTheDocument()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('applies custom className', () => {
|
|
32
|
+
const { container } = render(<Tile label="Load" value="7.3k" className="custom-class" />)
|
|
33
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('passes additional props through', () => {
|
|
37
|
+
render(<Tile label="Load" value="7.3k" testID="tile-test" />)
|
|
38
|
+
expect(screen.getByText('7.3k')).toBeInTheDocument()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('accessibility', () => {
|
|
42
|
+
it('has no accessibility violations', async () => {
|
|
43
|
+
const { container } = render(<Tile label="Volume" value="76%" />)
|
|
44
|
+
const results = await axe(container)
|
|
45
|
+
expect(results).toHaveNoViolations()
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { View, Text, type ViewProps } from 'react-native'
|
|
2
|
+
import { cn } from '../../../utils/cn'
|
|
3
|
+
|
|
4
|
+
export interface TileProps extends ViewProps {
|
|
5
|
+
/** Uppercase micro-label shown above the value */
|
|
6
|
+
label: string
|
|
7
|
+
/** Primary value, rendered in a bold mono face */
|
|
8
|
+
value: string
|
|
9
|
+
/** Tints the value (status/accent hex or CSS color); defaults to primary text */
|
|
10
|
+
valueColor?: string
|
|
11
|
+
/** Content alignment within the tile (default: 'center') */
|
|
12
|
+
align?: 'center' | 'start'
|
|
13
|
+
/** Additional className */
|
|
14
|
+
className?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Tile — a compact label-over-value stat card.
|
|
19
|
+
*
|
|
20
|
+
* A rounded raised box with an uppercase micro-label stacked over a bold mono
|
|
21
|
+
* value. Fills its flex slot so a row of Tiles reads as an even HStack.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <Tile label="Volume" value="76%" />
|
|
25
|
+
* <Tile label="Fatigue" value="MOD" valueColor="#F5A623" />
|
|
26
|
+
*/
|
|
27
|
+
export function Tile({
|
|
28
|
+
label,
|
|
29
|
+
value,
|
|
30
|
+
valueColor,
|
|
31
|
+
align = 'center',
|
|
32
|
+
className,
|
|
33
|
+
...props
|
|
34
|
+
}: TileProps) {
|
|
35
|
+
const isCenter = align === 'center'
|
|
36
|
+
return (
|
|
37
|
+
<View
|
|
38
|
+
className={cn(
|
|
39
|
+
'flex-1 rounded-md bg-surface-raised px-1.5 py-2',
|
|
40
|
+
isCenter ? 'items-center' : 'items-start',
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
<Text
|
|
46
|
+
className={cn(
|
|
47
|
+
'text-[10px] font-bold uppercase tracking-wider text-text-tertiary',
|
|
48
|
+
isCenter ? 'text-center' : 'text-left'
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
{label}
|
|
52
|
+
</Text>
|
|
53
|
+
<Text
|
|
54
|
+
className={cn(
|
|
55
|
+
'mt-0.5 font-mono text-sm font-bold text-text-primary',
|
|
56
|
+
isCenter ? 'text-center' : 'text-left'
|
|
57
|
+
)}
|
|
58
|
+
style={valueColor ? { color: valueColor } : undefined}
|
|
59
|
+
>
|
|
60
|
+
{value}
|
|
61
|
+
</Text>
|
|
62
|
+
</View>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -4,7 +4,7 @@ import { Toast, ToastProvider, useToast } from './Toast'
|
|
|
4
4
|
import { Button, ButtonText } from '../button'
|
|
5
5
|
|
|
6
6
|
const meta: Meta<typeof Toast> = {
|
|
7
|
-
title: 'Components/Toast',
|
|
7
|
+
title: 'Components/Molecules/Toast',
|
|
8
8
|
component: Toast,
|
|
9
9
|
tags: ['autodocs'],
|
|
10
10
|
argTypes: {
|
|
@@ -4,7 +4,7 @@ import { View, Text } from 'react-native'
|
|
|
4
4
|
import { ToolbarButton, ToolbarButtonGroup } from './ToolbarButton'
|
|
5
5
|
|
|
6
6
|
const meta: Meta<typeof ToolbarButton> = {
|
|
7
|
-
title: 'Components/ToolbarButton',
|
|
7
|
+
title: 'Components/Molecules/ToolbarButton',
|
|
8
8
|
component: ToolbarButton,
|
|
9
9
|
tags: ['autodocs'],
|
|
10
10
|
argTypes: {
|
|
@@ -10,7 +10,7 @@ import { RhfContactForm, type ContactFormValues } from './RhfContactForm'
|
|
|
10
10
|
* `Controller` render-prop idiom. See `ControlledFields.tsx` for the adapters.
|
|
11
11
|
*/
|
|
12
12
|
const meta: Meta<typeof RhfContactForm> = {
|
|
13
|
-
title: '
|
|
13
|
+
title: 'Lab/Recipes/React Hook Form',
|
|
14
14
|
component: RhfContactForm,
|
|
15
15
|
tags: ['autodocs'],
|
|
16
16
|
parameters: {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
|
+
import { renderHook, act } from '@testing-library/react'
|
|
3
|
+
import { useTimer, formatDuration } from './useTimer'
|
|
4
|
+
|
|
5
|
+
describe('formatDuration', () => {
|
|
6
|
+
it('formats zero as 0:00', () => {
|
|
7
|
+
expect(formatDuration(0)).toBe('0:00')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('pads seconds with a leading zero', () => {
|
|
11
|
+
expect(formatDuration(65000)).toBe('1:05')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('rounds partial seconds up', () => {
|
|
15
|
+
expect(formatDuration(4500)).toBe('0:05')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('clamps negative input to 0:00', () => {
|
|
19
|
+
expect(formatDuration(-1000)).toBe('0:00')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('formats multi-minute durations', () => {
|
|
23
|
+
expect(formatDuration(150000)).toBe('2:30')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('useTimer (controlled, down)', () => {
|
|
28
|
+
it('derives remaining, progress, and label from elapsedMs', () => {
|
|
29
|
+
const { result } = renderHook(() =>
|
|
30
|
+
useTimer({ mode: 'down', durationMs: 100000, elapsedMs: 30000 })
|
|
31
|
+
)
|
|
32
|
+
expect(result.current.remainingMs).toBe(70000)
|
|
33
|
+
expect(result.current.progress).toBeCloseTo(0.3)
|
|
34
|
+
expect(result.current.label).toBe('1:10')
|
|
35
|
+
expect(result.current.done).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('clamps remaining to 0 and progress to 1 when elapsed exceeds duration', () => {
|
|
39
|
+
const { result } = renderHook(() =>
|
|
40
|
+
useTimer({ mode: 'down', durationMs: 100000, elapsedMs: 200000 })
|
|
41
|
+
)
|
|
42
|
+
expect(result.current.remainingMs).toBe(0)
|
|
43
|
+
expect(result.current.progress).toBe(1)
|
|
44
|
+
expect(result.current.label).toBe('0:00')
|
|
45
|
+
expect(result.current.done).toBe(true)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('reports progress 1 and done for a zero-duration countdown', () => {
|
|
49
|
+
const { result } = renderHook(() => useTimer({ mode: 'down', durationMs: 0, elapsedMs: 0 }))
|
|
50
|
+
expect(result.current.progress).toBe(1)
|
|
51
|
+
expect(result.current.done).toBe(true)
|
|
52
|
+
expect(result.current.label).toBe('0:00')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('useTimer (controlled, up)', () => {
|
|
57
|
+
it('counts elapsed upward and computes progress against duration', () => {
|
|
58
|
+
const { result } = renderHook(() =>
|
|
59
|
+
useTimer({ mode: 'up', durationMs: 120000, elapsedMs: 30000 })
|
|
60
|
+
)
|
|
61
|
+
expect(result.current.elapsedMs).toBe(30000)
|
|
62
|
+
expect(result.current.progress).toBeCloseTo(0.25)
|
|
63
|
+
expect(result.current.label).toBe('0:30')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('reports progress 0 and never done without a duration', () => {
|
|
67
|
+
const { result } = renderHook(() => useTimer({ mode: 'up', elapsedMs: 45000 }))
|
|
68
|
+
expect(result.current.progress).toBe(0)
|
|
69
|
+
expect(result.current.done).toBe(false)
|
|
70
|
+
expect(result.current.label).toBe('0:45')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('useTimer (self-ticking)', () => {
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
vi.useRealTimers()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('advances elapsed on each interval while running', () => {
|
|
80
|
+
vi.useFakeTimers()
|
|
81
|
+
const { result } = renderHook(() =>
|
|
82
|
+
useTimer({ mode: 'up', autoTick: true, running: true, tickMs: 1000 })
|
|
83
|
+
)
|
|
84
|
+
expect(result.current.elapsedMs).toBe(0)
|
|
85
|
+
act(() => {
|
|
86
|
+
vi.advanceTimersByTime(3000)
|
|
87
|
+
})
|
|
88
|
+
expect(result.current.elapsedMs).toBe(3000)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('counts a down-timer toward 0 and reports done', () => {
|
|
92
|
+
vi.useFakeTimers()
|
|
93
|
+
const { result } = renderHook(() =>
|
|
94
|
+
useTimer({ mode: 'down', durationMs: 3000, autoTick: true, running: true })
|
|
95
|
+
)
|
|
96
|
+
expect(result.current.label).toBe('0:03')
|
|
97
|
+
act(() => {
|
|
98
|
+
vi.advanceTimersByTime(3000)
|
|
99
|
+
})
|
|
100
|
+
expect(result.current.remainingMs).toBe(0)
|
|
101
|
+
expect(result.current.label).toBe('0:00')
|
|
102
|
+
expect(result.current.done).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('does not tick when not running', () => {
|
|
106
|
+
vi.useFakeTimers()
|
|
107
|
+
const { result } = renderHook(() => useTimer({ mode: 'up', autoTick: true, running: false }))
|
|
108
|
+
act(() => {
|
|
109
|
+
vi.advanceTimersByTime(5000)
|
|
110
|
+
})
|
|
111
|
+
expect(result.current.elapsedMs).toBe(0)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('does not tick when a controlled elapsedMs is supplied', () => {
|
|
115
|
+
vi.useFakeTimers()
|
|
116
|
+
const { result } = renderHook(() =>
|
|
117
|
+
useTimer({ mode: 'up', autoTick: true, running: true, elapsedMs: 10000 })
|
|
118
|
+
)
|
|
119
|
+
act(() => {
|
|
120
|
+
vi.advanceTimersByTime(5000)
|
|
121
|
+
})
|
|
122
|
+
expect(result.current.elapsedMs).toBe(10000)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('clears the interval on unmount', () => {
|
|
126
|
+
vi.useFakeTimers()
|
|
127
|
+
const clearSpy = vi.spyOn(global, 'clearInterval')
|
|
128
|
+
const { unmount } = renderHook(() => useTimer({ mode: 'up', autoTick: true, running: true }))
|
|
129
|
+
unmount()
|
|
130
|
+
expect(clearSpy).toHaveBeenCalled()
|
|
131
|
+
clearSpy.mockRestore()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('stops ticking after running flips to false', () => {
|
|
135
|
+
vi.useFakeTimers()
|
|
136
|
+
const { result, rerender } = renderHook(
|
|
137
|
+
({ running }) => useTimer({ mode: 'up', autoTick: true, running }),
|
|
138
|
+
{ initialProps: { running: true } }
|
|
139
|
+
)
|
|
140
|
+
act(() => {
|
|
141
|
+
vi.advanceTimersByTime(2000)
|
|
142
|
+
})
|
|
143
|
+
expect(result.current.elapsedMs).toBe(2000)
|
|
144
|
+
rerender({ running: false })
|
|
145
|
+
act(() => {
|
|
146
|
+
vi.advanceTimersByTime(5000)
|
|
147
|
+
})
|
|
148
|
+
expect(result.current.elapsedMs).toBe(2000)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export type TimerMode = 'up' | 'down'
|
|
4
|
+
|
|
5
|
+
export interface UseTimerOptions {
|
|
6
|
+
/** 'up' counts elapsed upward; 'down' counts remaining toward 0. Default 'up'. */
|
|
7
|
+
mode?: TimerMode
|
|
8
|
+
/** Total duration in ms. Required for 'down' mode and for progress in 'up' mode. */
|
|
9
|
+
durationMs?: number
|
|
10
|
+
/** Controlled elapsed time in ms. When provided, the hook derives everything from it and never ticks. */
|
|
11
|
+
elapsedMs?: number
|
|
12
|
+
/** Self-tick only advances while running. */
|
|
13
|
+
running?: boolean
|
|
14
|
+
/** Enable the internal interval. Ignored when `elapsedMs` is controlled. */
|
|
15
|
+
autoTick?: boolean
|
|
16
|
+
/** Interval step in ms. Default 1000. */
|
|
17
|
+
tickMs?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TimerState {
|
|
21
|
+
elapsedMs: number
|
|
22
|
+
remainingMs: number
|
|
23
|
+
/** 0..1, clamped. */
|
|
24
|
+
progress: number
|
|
25
|
+
/** mm:ss for the active direction (remaining when 'down', elapsed when 'up'). */
|
|
26
|
+
label: string
|
|
27
|
+
/** True once a down-timer reaches 0 (or an up-timer passes its duration). */
|
|
28
|
+
done: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Format a millisecond duration as mm:ss, rounding partial seconds up. */
|
|
32
|
+
export function formatDuration(ms: number): string {
|
|
33
|
+
const totalSeconds = Math.max(0, Math.ceil(ms / 1000))
|
|
34
|
+
const minutes = Math.floor(totalSeconds / 60)
|
|
35
|
+
const seconds = totalSeconds % 60
|
|
36
|
+
return `${minutes}:${String(seconds).padStart(2, '0')}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useTimer(options: UseTimerOptions = {}): TimerState {
|
|
40
|
+
const {
|
|
41
|
+
mode = 'up',
|
|
42
|
+
durationMs,
|
|
43
|
+
elapsedMs: controlledElapsedMs,
|
|
44
|
+
running = false,
|
|
45
|
+
autoTick = false,
|
|
46
|
+
tickMs = 1000,
|
|
47
|
+
} = options
|
|
48
|
+
|
|
49
|
+
const isControlled = controlledElapsedMs != null
|
|
50
|
+
const [internalElapsedMs, setInternalElapsedMs] = useState(0)
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (isControlled || !autoTick || !running) return
|
|
54
|
+
const id = setInterval(() => {
|
|
55
|
+
setInternalElapsedMs((prev) => prev + tickMs)
|
|
56
|
+
}, tickMs)
|
|
57
|
+
return () => clearInterval(id)
|
|
58
|
+
}, [isControlled, autoTick, running, tickMs])
|
|
59
|
+
|
|
60
|
+
const elapsedMs = isControlled ? controlledElapsedMs : internalElapsedMs
|
|
61
|
+
|
|
62
|
+
const hasDuration = durationMs != null && durationMs > 0
|
|
63
|
+
const remainingMs = durationMs != null ? Math.max(0, durationMs - elapsedMs) : 0
|
|
64
|
+
|
|
65
|
+
let progress: number
|
|
66
|
+
if (hasDuration) {
|
|
67
|
+
progress = Math.min(1, Math.max(0, elapsedMs / durationMs))
|
|
68
|
+
} else {
|
|
69
|
+
// A zero/undefined duration is trivially complete for a countdown, empty for a stopwatch.
|
|
70
|
+
progress = mode === 'down' ? 1 : 0
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const label = formatDuration(mode === 'down' ? remainingMs : elapsedMs)
|
|
74
|
+
const done = durationMs != null && elapsedMs >= durationMs
|
|
75
|
+
|
|
76
|
+
return { elapsedMs, remainingMs, progress, label, done }
|
|
77
|
+
}
|
package/src/index.ts
CHANGED
package/src/pages.ts
CHANGED
|
@@ -438,7 +438,7 @@ function Section({ title, children }: { title: string; children: React.ReactNode
|
|
|
438
438
|
// ---------------------------------------------------------------------------
|
|
439
439
|
|
|
440
440
|
const meta: Meta = {
|
|
441
|
-
title: 'Theme
|
|
441
|
+
title: 'Lab/Recipes/Theme Preset Showcase',
|
|
442
442
|
parameters: {
|
|
443
443
|
layout: 'fullscreen',
|
|
444
444
|
},
|
|
@@ -4,7 +4,7 @@ import { primitiveColors, discreteRainbow, primitiveRamps, categoricalPalette }
|
|
|
4
4
|
import { semanticColorsLight, semanticColorsDark } from './tokens/semantic'
|
|
5
5
|
|
|
6
6
|
const meta: Meta = {
|
|
7
|
-
title: '
|
|
7
|
+
title: 'Foundations/Color/Palette',
|
|
8
8
|
tags: ['autodocs'],
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -79,16 +79,13 @@ export const PrimitiveColors: StoryObj = {
|
|
|
79
79
|
Primitive Colors
|
|
80
80
|
</Text>
|
|
81
81
|
<Text className="text-text-secondary mb-6">
|
|
82
|
-
Raw color values.
|
|
82
|
+
Raw color values. Chromatic hues live as OKLCH tonal ramps (see the Tonal Ramps story);
|
|
83
|
+
these are the achromatic and support scales. Use semantic tokens when building components.
|
|
83
84
|
</Text>
|
|
84
85
|
|
|
85
|
-
<ColorScale name="Orange (Primary Brand)" colors={primitiveColors.accent} />
|
|
86
|
-
<ColorScale name="Steel (Secondary Brand)" colors={primitiveColors.steel} />
|
|
87
86
|
<ColorScale name="Blue" colors={primitiveColors.blue} />
|
|
88
|
-
<ColorScale name="
|
|
89
|
-
<ColorScale name="
|
|
90
|
-
<ColorScale name="Red (Error)" colors={primitiveColors.red} />
|
|
91
|
-
<ColorScale name="Sky (Info)" colors={primitiveColors.sky} />
|
|
87
|
+
<ColorScale name="Red" colors={primitiveColors.red} />
|
|
88
|
+
<ColorScale name="Red Vivid" colors={primitiveColors.redVivid} />
|
|
92
89
|
<ColorScale name="Neutral" colors={primitiveColors.neutral} />
|
|
93
90
|
<ColorScale name="Charcoal (Dark Backgrounds)" colors={primitiveColors.charcoal} />
|
|
94
91
|
</View>
|
package/src/theme/config.ts
CHANGED
|
@@ -53,6 +53,8 @@ export const lightThemeCSSVars = {
|
|
|
53
53
|
|
|
54
54
|
'--color-status-success': semanticColorsLight['status-success'],
|
|
55
55
|
'--color-status-success-subtle': semanticColorsLight['status-success-subtle'],
|
|
56
|
+
'--color-status-live': semanticColorsLight['status-live'],
|
|
57
|
+
'--color-status-live-muted': semanticColorsLight['status-live-muted'],
|
|
56
58
|
'--color-status-error': semanticColorsLight['status-error'],
|
|
57
59
|
'--color-status-error-subtle': semanticColorsLight['status-error-subtle'],
|
|
58
60
|
'--color-status-warning': semanticColorsLight['status-warning'],
|
|
@@ -172,6 +174,8 @@ export const darkThemeCSSVars = {
|
|
|
172
174
|
|
|
173
175
|
'--color-status-success': semanticColorsDark['status-success'],
|
|
174
176
|
'--color-status-success-subtle': semanticColorsDark['status-success-subtle'],
|
|
177
|
+
'--color-status-live': semanticColorsDark['status-live'],
|
|
178
|
+
'--color-status-live-muted': semanticColorsDark['status-live-muted'],
|
|
175
179
|
'--color-status-error': semanticColorsDark['status-error'],
|
|
176
180
|
'--color-status-error-subtle': semanticColorsDark['status-error-subtle'],
|
|
177
181
|
'--color-status-warning': semanticColorsDark['status-warning'],
|
package/src/theme/global.css
CHANGED
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
--color-status-success-light: #58F69E;
|
|
36
36
|
--color-status-success-dark: #298732;
|
|
37
37
|
--color-status-success-subtle: rgba(46, 213, 115, 0.12);
|
|
38
|
+
--color-status-live: #2ED573;
|
|
39
|
+
--color-status-live-muted: #22A444;
|
|
38
40
|
--color-status-error: #D14343;
|
|
39
41
|
--color-status-error-light: #E05254;
|
|
40
42
|
--color-status-error-dark: #A4221C;
|
|
@@ -170,6 +172,8 @@
|
|
|
170
172
|
--color-status-success-light: #58F69E;
|
|
171
173
|
--color-status-success-dark: #298732;
|
|
172
174
|
--color-status-success-subtle: #E3FFEE;
|
|
175
|
+
--color-status-live: #2ED573;
|
|
176
|
+
--color-status-live-muted: #22A444;
|
|
173
177
|
--color-status-error: #D14343;
|
|
174
178
|
--color-status-error-light: #E05254;
|
|
175
179
|
--color-status-error-dark: #A4221C;
|
|
@@ -369,7 +373,7 @@
|
|
|
369
373
|
z-index: 0;
|
|
370
374
|
background:
|
|
371
375
|
radial-gradient(ellipse 140% 100% at 15% 5%, rgba(var(--color-brand-primary-rgb, 255, 121, 0), 0.06) 0%, transparent 45%),
|
|
372
|
-
radial-gradient(ellipse 120% 80% at 85% 95%, rgba(var(--color-brand-secondary-rgb,
|
|
376
|
+
radial-gradient(ellipse 120% 80% at 85% 95%, rgba(var(--color-brand-secondary-rgb, 48, 123, 155), 0.04) 0%, transparent 45%);
|
|
373
377
|
}
|
|
374
378
|
|
|
375
379
|
/* Film grain overlay for vintage texture */
|