@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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
2
|
import { View, Text } from 'react-native'
|
|
3
|
-
import { DateTime } from './DateTime'
|
|
3
|
+
import { DateTime, type DateTimeFormat } from './DateTime'
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof DateTime> = {
|
|
6
|
-
title: '
|
|
6
|
+
title: 'Components/Molecules/DateTime',
|
|
7
7
|
component: DateTime,
|
|
8
8
|
tags: ['autodocs'],
|
|
9
9
|
argTypes: {
|
|
@@ -25,9 +25,6 @@ export default meta
|
|
|
25
25
|
type Story = StoryObj<typeof DateTime>
|
|
26
26
|
|
|
27
27
|
const now = Date.now()
|
|
28
|
-
const yesterday = now - 24 * 60 * 60 * 1000
|
|
29
|
-
const lastWeek = now - 7 * 24 * 60 * 60 * 1000
|
|
30
|
-
const nextWeek = now + 7 * 24 * 60 * 60 * 1000
|
|
31
28
|
|
|
32
29
|
export const Default: Story = {
|
|
33
30
|
args: {
|
|
@@ -53,7 +50,15 @@ export const AllFormats: Story = {
|
|
|
53
50
|
},
|
|
54
51
|
}
|
|
55
52
|
|
|
56
|
-
function Row({
|
|
53
|
+
function Row({
|
|
54
|
+
label,
|
|
55
|
+
value,
|
|
56
|
+
format,
|
|
57
|
+
}: {
|
|
58
|
+
label: string
|
|
59
|
+
value: number | Date | string
|
|
60
|
+
format: DateTimeFormat
|
|
61
|
+
}) {
|
|
57
62
|
return (
|
|
58
63
|
<View className="flex-row items-center">
|
|
59
64
|
<Text className="w-24 text-text-secondary text-sm">{label}:</Text>
|
|
@@ -62,6 +67,74 @@ function Row({ label, value, format }: { label: string; value: any; format: any
|
|
|
62
67
|
)
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
/** Self-ticking clock (`live`), 24h, via the `mono` Typography variant — the substrate the shell TopBar uses. */
|
|
71
|
+
export const LiveClock: Story = {
|
|
72
|
+
render: () => (
|
|
73
|
+
<View className="gap-4 p-6 bg-surface-elevated rounded-xl items-start">
|
|
74
|
+
<DateTime
|
|
75
|
+
live
|
|
76
|
+
format="time"
|
|
77
|
+
hour12={false}
|
|
78
|
+
variant="mono"
|
|
79
|
+
className="text-4xl text-text-primary"
|
|
80
|
+
/>
|
|
81
|
+
<View className="flex-row items-center gap-3">
|
|
82
|
+
<DateTime live format="time" hour12={false} variant="mono" color="secondary" />
|
|
83
|
+
<Text className="text-text-tertiary text-xs">← ticks every second, 24h, mono</Text>
|
|
84
|
+
</View>
|
|
85
|
+
</View>
|
|
86
|
+
),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** With and without seconds (`seconds`). */
|
|
90
|
+
export const Seconds: Story = {
|
|
91
|
+
render: () => {
|
|
92
|
+
const at = new Date(2024, 0, 1, 16, 12, 7)
|
|
93
|
+
return (
|
|
94
|
+
<View className="gap-3">
|
|
95
|
+
<View className="flex-row items-center">
|
|
96
|
+
<Text className="w-32 text-text-secondary text-sm">no seconds:</Text>
|
|
97
|
+
<DateTime value={at} format="time" hour12={false} variant="mono" color="primary" />
|
|
98
|
+
</View>
|
|
99
|
+
<View className="flex-row items-center">
|
|
100
|
+
<Text className="w-32 text-text-secondary text-sm">with seconds:</Text>
|
|
101
|
+
<DateTime
|
|
102
|
+
value={at}
|
|
103
|
+
format="time"
|
|
104
|
+
hour12={false}
|
|
105
|
+
seconds
|
|
106
|
+
variant="mono"
|
|
107
|
+
color="primary"
|
|
108
|
+
/>
|
|
109
|
+
</View>
|
|
110
|
+
<View className="flex-row items-center">
|
|
111
|
+
<Text className="w-32 text-text-secondary text-sm">live w/ seconds:</Text>
|
|
112
|
+
<DateTime live format="time" hour12={false} seconds variant="mono" color="primary" />
|
|
113
|
+
</View>
|
|
114
|
+
</View>
|
|
115
|
+
)
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 24h vs 12h for the same moment (`hour12`). */
|
|
120
|
+
export const HourCycle: Story = {
|
|
121
|
+
render: () => {
|
|
122
|
+
const at = new Date(2024, 0, 1, 16, 12)
|
|
123
|
+
return (
|
|
124
|
+
<View className="gap-3">
|
|
125
|
+
<View className="flex-row items-center">
|
|
126
|
+
<Text className="w-24 text-text-secondary text-sm">24h:</Text>
|
|
127
|
+
<DateTime value={at} format="time" hour12={false} className="text-text-primary" />
|
|
128
|
+
</View>
|
|
129
|
+
<View className="flex-row items-center">
|
|
130
|
+
<Text className="w-24 text-text-secondary text-sm">12h:</Text>
|
|
131
|
+
<DateTime value={at} format="time" hour12 className="text-text-primary" />
|
|
132
|
+
</View>
|
|
133
|
+
</View>
|
|
134
|
+
)
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
65
138
|
export const RelativeTime: Story = {
|
|
66
139
|
render: () => {
|
|
67
140
|
const now = Date.now()
|
|
@@ -169,7 +242,10 @@ export const UsageExample: Story = {
|
|
|
169
242
|
<View className="gap-2 p-4 bg-surface-elevated rounded-lg max-w-md">
|
|
170
243
|
<Text className="text-lg font-semibold text-text-primary mb-2">Upcoming Events</Text>
|
|
171
244
|
{items.map((item, index) => (
|
|
172
|
-
<View
|
|
245
|
+
<View
|
|
246
|
+
key={index}
|
|
247
|
+
className="flex-row justify-between items-center py-2 border-b border-border"
|
|
248
|
+
>
|
|
173
249
|
<Text className="text-text-primary">{item.title}</Text>
|
|
174
250
|
<DateTime value={item.date} format="relative" color="secondary" className="text-sm" />
|
|
175
251
|
</View>
|
|
@@ -39,6 +39,34 @@ describe('DateTime', () => {
|
|
|
39
39
|
expect(screen.getByText('N/A')).toBeInTheDocument()
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
+
it('forces 24h time with hour12={false}', () => {
|
|
43
|
+
render(<DateTime value={new Date(2024, 0, 1, 16, 12)} format="time" hour12={false} />)
|
|
44
|
+
expect(screen.getByText('16:12')).toBeInTheDocument()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('forces 12h time with hour12={true}', () => {
|
|
48
|
+
render(<DateTime value={new Date(2024, 0, 1, 16, 12)} format="time" hour12 />)
|
|
49
|
+
expect(screen.getByText(/4:12/)).toBeInTheDocument()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('renders a live clock (ignores value, shows current time)', () => {
|
|
53
|
+
const { container } = render(<DateTime live format="time" hour12={false} />)
|
|
54
|
+
// renders a HH:MM string without crashing
|
|
55
|
+
expect(container.textContent).toMatch(/^\d{1,2}:\d{2}/)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('includes seconds when seconds is set', () => {
|
|
59
|
+
render(<DateTime value={new Date(2024, 0, 1, 16, 12, 7)} format="time" hour12={false} seconds />)
|
|
60
|
+
expect(screen.getByText('16:12:07')).toBeInTheDocument()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('renders through Typography when a variant is given', () => {
|
|
64
|
+
render(
|
|
65
|
+
<DateTime value={new Date(2024, 0, 1, 16, 12)} format="time" hour12={false} variant="mono" />
|
|
66
|
+
)
|
|
67
|
+
expect(screen.getByText('16:12')).toBeInTheDocument()
|
|
68
|
+
})
|
|
69
|
+
|
|
42
70
|
it('renders Invalid Date for bad input', () => {
|
|
43
71
|
render(<DateTime value="not-a-date" />)
|
|
44
72
|
expect(screen.getByText('Invalid Date')).toBeInTheDocument()
|
|
@@ -136,9 +164,7 @@ describe('DateTime', () => {
|
|
|
136
164
|
|
|
137
165
|
describe('accessibility', () => {
|
|
138
166
|
it('has no accessibility violations', async () => {
|
|
139
|
-
const { container } = render(
|
|
140
|
-
<DateTime value={testDate} format="medium" isUTC />
|
|
141
|
-
)
|
|
167
|
+
const { container } = render(<DateTime value={testDate} format="medium" isUTC />)
|
|
142
168
|
const results = await axe(container)
|
|
143
169
|
expect(results).toHaveNoViolations()
|
|
144
170
|
})
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
2
|
import { Text, type TextProps } from 'react-native'
|
|
3
3
|
import { cn } from '../../../utils/cn'
|
|
4
|
+
import { Typography, type TypographyVariant } from '../Typography'
|
|
4
5
|
|
|
5
|
-
export type DateTimeFormat =
|
|
6
|
+
export type DateTimeFormat =
|
|
6
7
|
| 'date' // 2024-01-15
|
|
7
8
|
| 'time' // 14:30
|
|
8
9
|
| 'datetime' // 2024-01-15 14:30
|
|
@@ -13,14 +14,24 @@ export type DateTimeFormat =
|
|
|
13
14
|
| 'full' // Monday, January 15, 2024
|
|
14
15
|
|
|
15
16
|
export interface DateTimeProps extends TextProps {
|
|
16
|
-
/** Date value (timestamp in ms, Date object, or ISO string) */
|
|
17
|
-
value
|
|
17
|
+
/** Date value (timestamp in ms, Date object, or ISO string). Optional when `live`. */
|
|
18
|
+
value?: number | Date | string | null | undefined
|
|
18
19
|
/** Display format */
|
|
19
20
|
format?: DateTimeFormat
|
|
20
21
|
/** Custom format string (overrides format) */
|
|
21
22
|
customFormat?: string
|
|
22
23
|
/** Whether to show in UTC */
|
|
23
24
|
isUTC?: boolean
|
|
25
|
+
/** Force 12h (true) or 24h (false) for time/datetime formats; locale default when omitted. */
|
|
26
|
+
hour12?: boolean
|
|
27
|
+
/** Include seconds in time/datetime formats. */
|
|
28
|
+
seconds?: boolean
|
|
29
|
+
/** Render through Typography with this variant (e.g. 'mono'); plain inheriting Text when omitted. */
|
|
30
|
+
variant?: TypographyVariant
|
|
31
|
+
/** Track the current time and re-render on an interval (ignores `value`). For clocks / relative time. */
|
|
32
|
+
live?: boolean
|
|
33
|
+
/** Refresh interval in ms when `live` (default 1000). */
|
|
34
|
+
refreshMs?: number
|
|
24
35
|
/** Fallback text when value is null/undefined */
|
|
25
36
|
fallback?: string
|
|
26
37
|
/** Text color */
|
|
@@ -42,10 +53,12 @@ const colorStyles = {
|
|
|
42
53
|
function formatDate(
|
|
43
54
|
value: number | Date | string,
|
|
44
55
|
format: DateTimeFormat,
|
|
45
|
-
isUTC: boolean
|
|
56
|
+
isUTC: boolean,
|
|
57
|
+
hour12?: boolean,
|
|
58
|
+
seconds?: boolean
|
|
46
59
|
): string {
|
|
47
60
|
const date = value instanceof Date ? value : new Date(value)
|
|
48
|
-
|
|
61
|
+
|
|
49
62
|
if (isNaN(date.getTime())) {
|
|
50
63
|
return 'Invalid Date'
|
|
51
64
|
}
|
|
@@ -57,7 +70,7 @@ function formatDate(
|
|
|
57
70
|
|
|
58
71
|
// Use Intl.DateTimeFormat for locale-aware formatting
|
|
59
72
|
const options: Intl.DateTimeFormatOptions = {}
|
|
60
|
-
|
|
73
|
+
|
|
61
74
|
switch (format) {
|
|
62
75
|
case 'date':
|
|
63
76
|
options.year = 'numeric'
|
|
@@ -101,6 +114,14 @@ function formatDate(
|
|
|
101
114
|
options.timeZone = 'UTC'
|
|
102
115
|
}
|
|
103
116
|
|
|
117
|
+
if (hour12 !== undefined && (options.hour !== undefined || options.minute !== undefined)) {
|
|
118
|
+
options.hour12 = hour12
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (seconds && options.hour !== undefined) {
|
|
122
|
+
options.second = '2-digit'
|
|
123
|
+
}
|
|
124
|
+
|
|
104
125
|
return new Intl.DateTimeFormat(undefined, options).format(date)
|
|
105
126
|
}
|
|
106
127
|
|
|
@@ -121,7 +142,7 @@ function getRelativeTime(date: Date): string {
|
|
|
121
142
|
// Use Intl.RelativeTimeFormat if available
|
|
122
143
|
if (typeof Intl !== 'undefined' && Intl.RelativeTimeFormat) {
|
|
123
144
|
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })
|
|
124
|
-
|
|
145
|
+
|
|
125
146
|
if (Math.abs(diffSecs) < 60) {
|
|
126
147
|
return rtf.format(diffSecs, 'second')
|
|
127
148
|
}
|
|
@@ -197,24 +218,43 @@ export function DateTime({
|
|
|
197
218
|
format = 'datetime',
|
|
198
219
|
customFormat,
|
|
199
220
|
isUTC = false,
|
|
221
|
+
hour12,
|
|
222
|
+
seconds,
|
|
223
|
+
variant,
|
|
224
|
+
live = false,
|
|
225
|
+
refreshMs = 1000,
|
|
200
226
|
fallback = '-',
|
|
201
227
|
color = 'inherit',
|
|
202
228
|
className,
|
|
203
229
|
...props
|
|
204
230
|
}: DateTimeProps) {
|
|
205
|
-
|
|
231
|
+
// When live, track "now" and re-render on an interval (the value prop is ignored).
|
|
232
|
+
const [now, setNow] = useState(() => Date.now())
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
if (!live) return
|
|
235
|
+
const id = setInterval(() => setNow(Date.now()), refreshMs)
|
|
236
|
+
return () => clearInterval(id)
|
|
237
|
+
}, [live, refreshMs])
|
|
238
|
+
|
|
239
|
+
const effectiveValue = live ? now : value
|
|
240
|
+
const text =
|
|
241
|
+
effectiveValue === null || effectiveValue === undefined
|
|
242
|
+
? fallback
|
|
243
|
+
: formatDate(effectiveValue, format, isUTC, hour12, seconds)
|
|
244
|
+
|
|
245
|
+
// Route through Typography (shared text substrate) when a variant is given;
|
|
246
|
+
// otherwise render a plain inheriting Text (backward-compatible default).
|
|
247
|
+
if (variant) {
|
|
206
248
|
return (
|
|
207
|
-
<
|
|
208
|
-
{
|
|
209
|
-
</
|
|
249
|
+
<Typography variant={variant} color={color} className={className} {...props}>
|
|
250
|
+
{text}
|
|
251
|
+
</Typography>
|
|
210
252
|
)
|
|
211
253
|
}
|
|
212
254
|
|
|
213
|
-
const formattedDate = formatDate(value, format, isUTC)
|
|
214
|
-
|
|
215
255
|
return (
|
|
216
256
|
<Text className={cn(colorStyles[color], className)} {...props}>
|
|
217
|
-
{
|
|
257
|
+
{text}
|
|
218
258
|
</Text>
|
|
219
259
|
)
|
|
220
260
|
}
|
|
@@ -20,9 +20,9 @@ describe('Gauge', () => {
|
|
|
20
20
|
const { rerender } = render(<Gauge value={30} />)
|
|
21
21
|
expect(screen.getByTestId('gauge-value')).toHaveStyle({ color: '#D14343' })
|
|
22
22
|
rerender(<Gauge value={70} />)
|
|
23
|
-
expect(screen.getByTestId('gauge-value')).toHaveStyle({ color: '#
|
|
23
|
+
expect(screen.getByTestId('gauge-value')).toHaveStyle({ color: '#F9B415' })
|
|
24
24
|
rerender(<Gauge value={90} />)
|
|
25
|
-
expect(screen.getByTestId('gauge-value')).toHaveStyle({ color: '#
|
|
25
|
+
expect(screen.getByTestId('gauge-value')).toHaveStyle({ color: '#2ED573' })
|
|
26
26
|
})
|
|
27
27
|
|
|
28
28
|
it('honors a single-color override over thresholds', () => {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { View, Text, type ViewProps } from 'react-native'
|
|
2
2
|
import { cn } from '../../../utils/cn'
|
|
3
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
4
|
+
|
|
5
|
+
const sem = getSemanticColors('dark')
|
|
3
6
|
|
|
4
7
|
export interface GaugeThreshold {
|
|
5
8
|
/** Band start, in the gauge's value units. */
|
|
@@ -30,9 +33,9 @@ export interface GaugeProps extends Omit<ViewProps, 'children'> {
|
|
|
30
33
|
className?: string
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
const STATUS_SUCCESS = '
|
|
34
|
-
const STATUS_WARNING = '
|
|
35
|
-
const STATUS_ERROR = '
|
|
36
|
+
const STATUS_SUCCESS = sem['status-success']
|
|
37
|
+
const STATUS_WARNING = sem['status-warning']
|
|
38
|
+
const STATUS_ERROR = sem['status-error']
|
|
36
39
|
const TRACK = 'rgba(255,255,255,0.08)'
|
|
37
40
|
|
|
38
41
|
/** Titan status-token bands for a 0–100 score. */
|
|
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
|
2
2
|
import { Scatter, type ScatterDatum } from './Scatter'
|
|
3
3
|
|
|
4
4
|
const meta: Meta<typeof Scatter> = {
|
|
5
|
-
title: '
|
|
5
|
+
title: 'Components/DataViz/Scatter',
|
|
6
6
|
component: Scatter,
|
|
7
7
|
tags: ['autodocs'],
|
|
8
8
|
argTypes: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { View, Text, Pressable, type ViewProps } from 'react-native'
|
|
2
2
|
import { cn } from '../../../utils/cn'
|
|
3
|
+
import { DATAVIZ_CATEGORICAL_PALETTE } from '../../../theme/extracted-colors-dataviz'
|
|
3
4
|
|
|
4
5
|
export interface ScatterDatum {
|
|
5
6
|
/** Stable identity — returned by onPress and used as the React key. */
|
|
@@ -45,11 +46,8 @@ export interface ScatterProps extends Omit<ViewProps, 'children'> {
|
|
|
45
46
|
className?: string
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
/** Titan categorical fallback palette (
|
|
49
|
-
const PALETTE =
|
|
50
|
-
'#5B9BD5', '#14B8A6', '#F4A736', '#F83030',
|
|
51
|
-
'#823CA0', '#D4A520', '#406D87', '#43C6B7',
|
|
52
|
-
]
|
|
49
|
+
/** Titan categorical fallback palette (see extracted-colors-dataviz). */
|
|
50
|
+
const PALETTE = DATAVIZ_CATEGORICAL_PALETTE
|
|
53
51
|
|
|
54
52
|
const GRID_LINE = 'rgba(255,255,255,0.07)'
|
|
55
53
|
const AXIS_LINE = 'rgba(255,255,255,0.18)'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { TimerReadout } from './TimerReadout'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `TimerReadout` — a small textual timer (⏱ + mono, right-justified) built on the
|
|
7
|
+
* `useTimer` hook. Only the live/current value goes green (status-live-muted) while
|
|
8
|
+
* `running`; the ` / total` suffix stays dim tertiary.
|
|
9
|
+
*/
|
|
10
|
+
const meta: Meta<typeof TimerReadout> = {
|
|
11
|
+
title: 'Custom/TimerReadout',
|
|
12
|
+
component: TimerReadout,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
parameters: {
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component: '**Atom.** A small textual timer on [useTimer]. Used-by ↑ SessionHeader.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
decorators: [
|
|
22
|
+
(Story) => (
|
|
23
|
+
<View style={{ padding: 16, backgroundColor: '#131313', alignItems: 'flex-end', width: 200 }}>
|
|
24
|
+
<Story />
|
|
25
|
+
</View>
|
|
26
|
+
),
|
|
27
|
+
],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default meta
|
|
31
|
+
type Story = StoryObj<typeof TimerReadout>
|
|
32
|
+
|
|
33
|
+
/** Live stopwatch (controlled), ticking → the elapsed value is green. */
|
|
34
|
+
export const Running: Story = {
|
|
35
|
+
args: { mode: 'up', elapsedMs: 42 * 60000 + 18 * 1000, running: true },
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Paused → the value drops to secondary (no live cue). */
|
|
39
|
+
export const Paused: Story = {
|
|
40
|
+
args: { mode: 'up', elapsedMs: 42 * 60000 + 18 * 1000, running: false },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Countdown, running → shows remaining time, green while live. */
|
|
44
|
+
export const CountdownRunning: Story = {
|
|
45
|
+
args: { mode: 'down', durationMs: 90 * 1000, elapsedMs: 27 * 1000, running: true },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** With a ` / total` budget suffix (dim tertiary). */
|
|
49
|
+
export const WithTotal: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
mode: 'up',
|
|
52
|
+
elapsedMs: 42 * 60000 + 18 * 1000,
|
|
53
|
+
durationMs: 60 * 60000,
|
|
54
|
+
showTotal: true,
|
|
55
|
+
running: true,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Self-ticking stopwatch — the internal interval advances the value once per second. */
|
|
60
|
+
export const LiveAutoTick: Story = {
|
|
61
|
+
args: { mode: 'up', autoTick: true, running: true },
|
|
62
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
|
+
import { render, screen, act } from '@testing-library/react'
|
|
3
|
+
import { TimerReadout } from './TimerReadout'
|
|
4
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
5
|
+
import { primitiveRamps } from '../../../theme/tokens/primitives'
|
|
6
|
+
|
|
7
|
+
const semantic = getSemanticColors('dark')
|
|
8
|
+
const LIVE_GREEN = primitiveRamps.green[500]
|
|
9
|
+
const SECONDARY = semantic['text-secondary']
|
|
10
|
+
const TERTIARY = semantic['text-tertiary']
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.useRealTimers()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('TimerReadout', () => {
|
|
17
|
+
it('renders elapsed mm:ss from a controlled up-timer', () => {
|
|
18
|
+
render(<TimerReadout mode="up" elapsedMs={65 * 1000} />)
|
|
19
|
+
expect(screen.getByTestId('timer-readout-current')).toHaveTextContent('1:05')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('renders the default ⏱ glyph', () => {
|
|
23
|
+
render(<TimerReadout elapsedMs={0} />)
|
|
24
|
+
expect(screen.getByTestId('timer-readout-glyph')).toHaveTextContent('⏱')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('accepts a custom glyph', () => {
|
|
28
|
+
render(<TimerReadout elapsedMs={0} glyph="◷" />)
|
|
29
|
+
expect(screen.getByTestId('timer-readout-glyph')).toHaveTextContent('◷')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('shows the current value in live green while running', () => {
|
|
33
|
+
render(<TimerReadout mode="up" elapsedMs={0} running />)
|
|
34
|
+
expect(screen.getByTestId('timer-readout-current')).toHaveStyle({ color: LIVE_GREEN })
|
|
35
|
+
expect(screen.getByTestId('timer-readout-glyph')).toHaveStyle({ color: LIVE_GREEN })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('shows the current value in secondary when paused', () => {
|
|
39
|
+
render(<TimerReadout mode="up" elapsedMs={0} running={false} />)
|
|
40
|
+
expect(screen.getByTestId('timer-readout-current')).toHaveStyle({ color: SECONDARY })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('shows remaining time in down mode', () => {
|
|
44
|
+
render(<TimerReadout mode="down" durationMs={90 * 1000} elapsedMs={27 * 1000} running />)
|
|
45
|
+
expect(screen.getByTestId('timer-readout-current')).toHaveTextContent('1:03')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('appends the total in dim tertiary when showTotal is set', () => {
|
|
49
|
+
render(
|
|
50
|
+
<TimerReadout mode="up" elapsedMs={42 * 1000} durationMs={60 * 60000} showTotal running />
|
|
51
|
+
)
|
|
52
|
+
const total = screen.getByTestId('timer-readout-total')
|
|
53
|
+
expect(total).toHaveTextContent('/ 60:00')
|
|
54
|
+
expect(total).toHaveStyle({ color: TERTIARY })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('omits the total when showTotal is false', () => {
|
|
58
|
+
render(<TimerReadout mode="up" elapsedMs={0} durationMs={60000} />)
|
|
59
|
+
expect(screen.queryByTestId('timer-readout-total')).toBeNull()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('self-ticks once per second while running with autoTick', () => {
|
|
63
|
+
vi.useFakeTimers()
|
|
64
|
+
render(<TimerReadout mode="up" autoTick running />)
|
|
65
|
+
expect(screen.getByTestId('timer-readout-current')).toHaveTextContent('0:00')
|
|
66
|
+
|
|
67
|
+
act(() => {
|
|
68
|
+
vi.advanceTimersByTime(3000)
|
|
69
|
+
})
|
|
70
|
+
expect(screen.getByTestId('timer-readout-current')).toHaveTextContent('0:03')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type ReactNode } from 'react'
|
|
2
|
+
import { View, Text, type ViewProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
import { useTimer, formatDuration, type TimerMode } from '../../../hooks/useTimer'
|
|
5
|
+
import { Typography } from '../Typography'
|
|
6
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
7
|
+
import { primitiveRamps } from '../../../theme/tokens/primitives'
|
|
8
|
+
|
|
9
|
+
const semantic = getSemanticColors('dark')
|
|
10
|
+
// status-live-muted — the SideNav "Live" cue; the elapsed value goes green while ticking.
|
|
11
|
+
const LIVE_GREEN = primitiveRamps.green[500]
|
|
12
|
+
const SECONDARY = semantic['text-secondary']
|
|
13
|
+
const TERTIARY = semantic['text-tertiary']
|
|
14
|
+
|
|
15
|
+
const DEFAULT_GLYPH = '⏱'
|
|
16
|
+
|
|
17
|
+
export interface TimerReadoutProps extends ViewProps {
|
|
18
|
+
/** 'up' counts elapsed upward; 'down' counts remaining toward 0. Default 'up'. */
|
|
19
|
+
mode?: TimerMode
|
|
20
|
+
/** Controlled elapsed time in ms. When set, the value is derived from it and never self-ticks. */
|
|
21
|
+
elapsedMs?: number
|
|
22
|
+
/** Total duration in ms — required for 'down' mode and for the ` / total` suffix. */
|
|
23
|
+
durationMs?: number
|
|
24
|
+
/** Live cue: the current value goes green while true. */
|
|
25
|
+
running?: boolean
|
|
26
|
+
/** Enable the internal 1s interval (ignored when `elapsedMs` is controlled). */
|
|
27
|
+
autoTick?: boolean
|
|
28
|
+
/** When `durationMs` is set, append ` / mm:ss`. */
|
|
29
|
+
showTotal?: boolean
|
|
30
|
+
/** Leading glyph; defaults to the ⏱ unicode. */
|
|
31
|
+
glyph?: ReactNode
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Atom · TimerReadout — a small textual timer (⏱ + mono, right-justified) built on
|
|
36
|
+
* [useTimer]. Matches the TopBar clock treatment: only the live/current value goes
|
|
37
|
+
* green (status-live-muted) while `running`; the ` / total` suffix stays dim tertiary.
|
|
38
|
+
*/
|
|
39
|
+
export function TimerReadout({
|
|
40
|
+
mode = 'up',
|
|
41
|
+
elapsedMs,
|
|
42
|
+
durationMs,
|
|
43
|
+
running = false,
|
|
44
|
+
autoTick = false,
|
|
45
|
+
showTotal = false,
|
|
46
|
+
glyph = DEFAULT_GLYPH,
|
|
47
|
+
className,
|
|
48
|
+
...rest
|
|
49
|
+
}: TimerReadoutProps) {
|
|
50
|
+
const { label } = useTimer({ mode, durationMs, elapsedMs, running, autoTick })
|
|
51
|
+
|
|
52
|
+
const currentColor = running ? LIVE_GREEN : SECONDARY
|
|
53
|
+
const total = showTotal && durationMs != null ? formatDuration(durationMs) : null
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<View
|
|
57
|
+
accessibilityRole="timer"
|
|
58
|
+
accessibilityLabel={total != null ? `${label} of ${total}` : label}
|
|
59
|
+
className={cn('flex-row items-baseline justify-end', className)}
|
|
60
|
+
{...rest}
|
|
61
|
+
>
|
|
62
|
+
<Typography variant="mono" style={{ fontSize: 11, textAlign: 'right' }}>
|
|
63
|
+
<Text testID="timer-readout-glyph" style={{ color: currentColor, marginRight: 3 }}>
|
|
64
|
+
{glyph}
|
|
65
|
+
</Text>
|
|
66
|
+
<Text testID="timer-readout-current" style={{ color: currentColor }}>
|
|
67
|
+
{label}
|
|
68
|
+
</Text>
|
|
69
|
+
{total != null ? (
|
|
70
|
+
<Text testID="timer-readout-total" style={{ color: TERTIARY }}>{` / ${total}`}</Text>
|
|
71
|
+
) : null}
|
|
72
|
+
</Typography>
|
|
73
|
+
</View>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TimerReadout, type TimerReadoutProps } from './TimerReadout'
|
|
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
|
2
2
|
import { Treemap, type TreemapDatum } from './Treemap'
|
|
3
3
|
|
|
4
4
|
const meta: Meta<typeof Treemap> = {
|
|
5
|
-
title: '
|
|
5
|
+
title: 'Components/DataViz/Treemap',
|
|
6
6
|
component: Treemap,
|
|
7
7
|
tags: ['autodocs'],
|
|
8
8
|
argTypes: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { View, Text, Pressable, type ViewProps } from 'react-native'
|
|
3
3
|
import { cn } from '../../../utils/cn'
|
|
4
|
+
import { DATAVIZ_CATEGORICAL_PALETTE } from '../../../theme/extracted-colors-dataviz'
|
|
4
5
|
|
|
5
6
|
export interface TreemapDatum {
|
|
6
7
|
/** Stable identity — returned by onPress and used as the React key. */
|
|
@@ -35,11 +36,8 @@ export interface TreemapProps extends Omit<ViewProps, 'children'> {
|
|
|
35
36
|
className?: string
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
/** Titan categorical fallback palette (
|
|
39
|
-
const PALETTE =
|
|
40
|
-
'#5B9BD5', '#14B8A6', '#F4A736', '#F83030',
|
|
41
|
-
'#823CA0', '#D4A520', '#406D87', '#43C6B7',
|
|
42
|
-
]
|
|
39
|
+
/** Titan categorical fallback palette (see extracted-colors-dataviz). */
|
|
40
|
+
const PALETTE = DATAVIZ_CATEGORICAL_PALETTE
|
|
43
41
|
|
|
44
42
|
interface Rect {
|
|
45
43
|
x: number
|