@titan-design/react-ui 0.0.2 → 0.1.1
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/{chunk-NA3EES23.mjs → chunk-MGW37MPO.mjs} +26 -4
- package/dist/chunk-MGW37MPO.mjs.map +1 -0
- package/dist/index.d.mts +115 -2
- package/dist/index.d.ts +115 -2
- package/dist/index.js +262 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +225 -3
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +124 -109
- package/dist/theme/index.d.ts +124 -109
- package/dist/theme/index.js +24 -1
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +1 -1
- package/package.json +12 -5
- package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
- package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
- package/src/components/custom/DateTime/DateTime.tsx +235 -0
- package/src/components/custom/DateTime/index.ts +2 -0
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
- package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
- package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
- package/src/components/custom/EmptyState/index.ts +2 -0
- package/src/components/custom/Metric/Metric.stories.tsx +133 -0
- package/src/components/custom/Metric/Metric.test.tsx +120 -0
- package/src/components/custom/Metric/Metric.tsx +92 -0
- package/src/components/custom/Metric/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
- package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
- package/src/components/custom/Sidebar/index.ts +18 -0
- package/src/components/custom/Table/Table.stories.tsx +481 -0
- package/src/components/custom/Table/Table.test.tsx +531 -0
- package/src/components/custom/Table/Table.tsx +696 -0
- package/src/components/custom/Table/index.ts +22 -0
- package/src/components/custom/Typography/Typography.stories.tsx +153 -0
- package/src/components/custom/Typography/Typography.test.tsx +107 -0
- package/src/components/custom/Typography/Typography.tsx +218 -0
- package/src/components/custom/Typography/index.ts +19 -0
- package/src/components/custom/index.ts +8 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
- package/src/components/custom/stepper/Stepper.test.tsx +230 -0
- package/src/components/custom/stepper/Stepper.tsx +250 -0
- package/src/components/custom/stepper/index.ts +10 -0
- package/src/components/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +154 -0
- package/src/components/ui/alert/Alert.test.tsx +202 -0
- package/src/components/ui/alert/Alert.tsx +170 -0
- package/src/components/ui/alert/index.ts +8 -0
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
- package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
- package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
- package/src/components/ui/autocomplete/index.ts +2 -0
- package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
- package/src/components/ui/avatar/Avatar.test.tsx +135 -0
- package/src/components/ui/avatar/Avatar.tsx +168 -0
- package/src/components/ui/avatar/index.ts +2 -0
- package/src/components/ui/badge/Badge.stories.tsx +100 -0
- package/src/components/ui/badge/Badge.test.tsx +86 -0
- package/src/components/ui/badge/Badge.tsx +105 -0
- package/src/components/ui/badge/index.ts +2 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
- package/src/components/ui/breadcrumbs/index.ts +2 -0
- package/src/components/ui/button/Button.stories.tsx +216 -0
- package/src/components/ui/button/Button.test.tsx +132 -0
- package/src/components/ui/button/Button.tsx +242 -0
- package/src/components/ui/button/index.ts +10 -0
- package/src/components/ui/card/Card.stories.tsx +443 -0
- package/src/components/ui/card/Card.test.tsx +265 -0
- package/src/components/ui/card/Card.tsx +380 -0
- package/src/components/ui/card/index.ts +20 -0
- package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
- package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
- package/src/components/ui/checkbox/Checkbox.tsx +163 -0
- package/src/components/ui/checkbox/index.ts +2 -0
- package/src/components/ui/chip/Chip.stories.tsx +112 -0
- package/src/components/ui/chip/Chip.test.tsx +119 -0
- package/src/components/ui/chip/Chip.tsx +151 -0
- package/src/components/ui/chip/index.ts +2 -0
- package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
- package/src/components/ui/collapse/Collapse.test.tsx +298 -0
- package/src/components/ui/collapse/Collapse.tsx +275 -0
- package/src/components/ui/collapse/index.ts +18 -0
- package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
- package/src/components/ui/data-row/DataRow.test.tsx +78 -0
- package/src/components/ui/data-row/DataRow.tsx +29 -0
- package/src/components/ui/data-row/index.ts +2 -0
- package/src/components/ui/divider/Divider.stories.tsx +71 -0
- package/src/components/ui/divider/Divider.test.tsx +55 -0
- package/src/components/ui/divider/Divider.tsx +37 -0
- package/src/components/ui/divider/index.ts +2 -0
- package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
- package/src/components/ui/drawer/Drawer.test.tsx +142 -0
- package/src/components/ui/drawer/Drawer.tsx +228 -0
- package/src/components/ui/drawer/index.ts +9 -0
- package/src/components/ui/form-field/FormField.stories.tsx +260 -0
- package/src/components/ui/form-field/FormField.test.tsx +280 -0
- package/src/components/ui/form-field/FormField.tsx +308 -0
- package/src/components/ui/form-field/index.ts +2 -0
- package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
- package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
- package/src/components/ui/help-tip/HelpTip.tsx +229 -0
- package/src/components/ui/help-tip/index.ts +2 -0
- package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
- package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
- package/src/components/ui/icon-box/IconBox.tsx +57 -0
- package/src/components/ui/icon-box/index.ts +1 -0
- package/src/components/ui/index.ts +36 -0
- package/src/components/ui/input/Input.stories.tsx +192 -0
- package/src/components/ui/input/Input.test.tsx +86 -0
- package/src/components/ui/input/Input.tsx +273 -0
- package/src/components/ui/input/index.ts +2 -0
- package/src/components/ui/link/Link.stories.tsx +78 -0
- package/src/components/ui/link/Link.test.tsx +94 -0
- package/src/components/ui/link/Link.tsx +86 -0
- package/src/components/ui/link/index.ts +2 -0
- package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
- package/src/components/ui/list-item/ListItem.test.tsx +128 -0
- package/src/components/ui/list-item/ListItem.tsx +89 -0
- package/src/components/ui/list-item/index.ts +14 -0
- package/src/components/ui/menu/Menu.stories.tsx +180 -0
- package/src/components/ui/menu/Menu.test.tsx +353 -0
- package/src/components/ui/menu/Menu.tsx +222 -0
- package/src/components/ui/menu/index.ts +2 -0
- package/src/components/ui/modal/Modal.stories.tsx +350 -0
- package/src/components/ui/modal/Modal.test.tsx +159 -0
- package/src/components/ui/modal/Modal.tsx +254 -0
- package/src/components/ui/modal/index.ts +19 -0
- package/src/components/ui/popover/Popover.stories.tsx +190 -0
- package/src/components/ui/popover/Popover.test.tsx +277 -0
- package/src/components/ui/popover/Popover.tsx +162 -0
- package/src/components/ui/popover/index.ts +2 -0
- package/src/components/ui/progress/Progress.stories.tsx +190 -0
- package/src/components/ui/progress/Progress.test.tsx +181 -0
- package/src/components/ui/progress/Progress.tsx +320 -0
- package/src/components/ui/progress/index.ts +9 -0
- package/src/components/ui/radio/Radio.stories.tsx +199 -0
- package/src/components/ui/radio/Radio.test.tsx +166 -0
- package/src/components/ui/radio/Radio.tsx +200 -0
- package/src/components/ui/radio/index.ts +2 -0
- package/src/components/ui/section/Section.stories.tsx +111 -0
- package/src/components/ui/section/Section.test.tsx +121 -0
- package/src/components/ui/section/Section.tsx +52 -0
- package/src/components/ui/section/index.ts +6 -0
- package/src/components/ui/select/Select.stories.tsx +286 -0
- package/src/components/ui/select/Select.test.tsx +180 -0
- package/src/components/ui/select/Select.tsx +236 -0
- package/src/components/ui/select/index.ts +2 -0
- package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
- package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
- package/src/components/ui/skeleton/Skeleton.tsx +229 -0
- package/src/components/ui/skeleton/index.ts +16 -0
- package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
- package/src/components/ui/spinner/Spinner.test.tsx +67 -0
- package/src/components/ui/spinner/Spinner.tsx +72 -0
- package/src/components/ui/spinner/index.ts +2 -0
- package/src/components/ui/stack/Stack.stories.tsx +127 -0
- package/src/components/ui/stack/Stack.test.tsx +184 -0
- package/src/components/ui/stack/Stack.tsx +76 -0
- package/src/components/ui/stack/index.ts +2 -0
- package/src/components/ui/surface/Surface.stories.tsx +91 -0
- package/src/components/ui/surface/Surface.test.tsx +63 -0
- package/src/components/ui/surface/Surface.tsx +51 -0
- package/src/components/ui/surface/index.ts +1 -0
- package/src/components/ui/switch/Switch.stories.tsx +111 -0
- package/src/components/ui/switch/Switch.test.tsx +112 -0
- package/src/components/ui/switch/Switch.tsx +103 -0
- package/src/components/ui/switch/index.ts +2 -0
- package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
- package/src/components/ui/tabs/Tabs.test.tsx +192 -0
- package/src/components/ui/tabs/Tabs.tsx +253 -0
- package/src/components/ui/tabs/index.ts +16 -0
- package/src/components/ui/toast/Toast.stories.tsx +255 -0
- package/src/components/ui/toast/Toast.test.tsx +207 -0
- package/src/components/ui/toast/Toast.tsx +327 -0
- package/src/components/ui/toast/index.ts +8 -0
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
- package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
- package/src/components/ui/toolbar-button/index.ts +7 -0
- package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
- package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
- package/src/components/ui/tooltip/Tooltip.tsx +143 -0
- package/src/components/ui/tooltip/index.ts +2 -0
- package/src/index.ts +10 -0
- package/src/test/setup.ts +19 -0
- package/src/theme/Colors.stories.tsx +258 -0
- package/src/theme/config.ts +193 -0
- package/src/theme/elevation.stories.tsx +346 -0
- package/src/theme/elevation.test.ts +27 -0
- package/src/theme/elevation.ts +567 -0
- package/src/theme/index.ts +39 -0
- package/src/theme/shadows.stories.tsx +523 -0
- package/src/theme/shadows.ts +439 -0
- package/src/theme/tokens/index.ts +2 -0
- package/src/theme/tokens/primitives.ts +354 -0
- package/src/theme/tokens/semantic.ts +381 -0
- package/src/types/jest-axe.d.ts +16 -0
- package/src/types/nativewind.d.ts +54 -0
- package/src/utils/cn.ts +14 -0
- package/src/utils/colors.ts +140 -0
- package/src/utils/form.ts +188 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/useTheme.ts +72 -0
- package/dist/chunk-NA3EES23.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@titan-design/react-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Cross-platform design system built on Gluestack UI",
|
|
5
5
|
"author": "Henry Jewkes",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,21 +13,26 @@
|
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/HJewkes/titan-design/issues"
|
|
15
15
|
},
|
|
16
|
+
"source": "./src/index.ts",
|
|
17
|
+
"react-native": "./src/index.ts",
|
|
16
18
|
"main": "./dist/index.js",
|
|
17
19
|
"module": "./dist/index.mjs",
|
|
18
20
|
"types": "./dist/index.d.ts",
|
|
19
21
|
"exports": {
|
|
20
22
|
".": {
|
|
23
|
+
"react-native": "./src/index.ts",
|
|
21
24
|
"types": "./dist/index.d.ts",
|
|
22
25
|
"import": "./dist/index.mjs",
|
|
23
26
|
"require": "./dist/index.js"
|
|
24
27
|
},
|
|
25
28
|
"./theme": {
|
|
29
|
+
"react-native": "./src/theme/index.ts",
|
|
26
30
|
"types": "./dist/theme/index.d.ts",
|
|
27
31
|
"import": "./dist/theme/index.mjs",
|
|
28
32
|
"require": "./dist/theme/index.js"
|
|
29
33
|
},
|
|
30
|
-
"./theme/global.css": "./src/theme/global.css"
|
|
34
|
+
"./theme/global.css": "./src/theme/global.css",
|
|
35
|
+
"./tailwind.config.js": "./tailwind.config.js"
|
|
31
36
|
},
|
|
32
37
|
"engines": {
|
|
33
38
|
"node": ">=20.0.0"
|
|
@@ -36,6 +41,8 @@
|
|
|
36
41
|
"build": "tsup",
|
|
37
42
|
"dev": "tsup --watch",
|
|
38
43
|
"lint": "eslint src/",
|
|
44
|
+
"type-check": "tsc --noEmit",
|
|
45
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx,css}\"",
|
|
39
46
|
"test": "vitest",
|
|
40
47
|
"test:coverage": "vitest --coverage",
|
|
41
48
|
"storybook": "storybook dev -p 6006",
|
|
@@ -45,8 +52,8 @@
|
|
|
45
52
|
"peerDependencies": {
|
|
46
53
|
"lucide-react": ">=0.400.0",
|
|
47
54
|
"lucide-react-native": ">=0.400.0",
|
|
48
|
-
"react": "^18.0.0",
|
|
49
|
-
"react-dom": "^18.0.0",
|
|
55
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
56
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
50
57
|
"react-native": ">=0.72.0",
|
|
51
58
|
"react-native-web": ">=0.19.0"
|
|
52
59
|
},
|
|
@@ -104,7 +111,7 @@
|
|
|
104
111
|
},
|
|
105
112
|
"files": [
|
|
106
113
|
"dist",
|
|
107
|
-
"src
|
|
114
|
+
"src",
|
|
108
115
|
"tailwind.config.js"
|
|
109
116
|
],
|
|
110
117
|
"sideEffects": false,
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { DateTime } from './DateTime'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof DateTime> = {
|
|
6
|
+
title: 'Custom/DateTime',
|
|
7
|
+
component: DateTime,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
format: {
|
|
11
|
+
control: 'select',
|
|
12
|
+
options: ['date', 'time', 'datetime', 'relative', 'short', 'medium', 'long', 'full'],
|
|
13
|
+
},
|
|
14
|
+
color: {
|
|
15
|
+
control: 'select',
|
|
16
|
+
options: ['primary', 'secondary', 'tertiary', 'inherit'],
|
|
17
|
+
},
|
|
18
|
+
isUTC: {
|
|
19
|
+
control: 'boolean',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default meta
|
|
25
|
+
type Story = StoryObj<typeof DateTime>
|
|
26
|
+
|
|
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
|
+
|
|
32
|
+
export const Default: Story = {
|
|
33
|
+
args: {
|
|
34
|
+
value: now,
|
|
35
|
+
format: 'datetime',
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const AllFormats: Story = {
|
|
40
|
+
render: () => {
|
|
41
|
+
const date = new Date('2024-06-15T14:30:00')
|
|
42
|
+
return (
|
|
43
|
+
<View className="gap-3">
|
|
44
|
+
<Row label="date" value={date} format="date" />
|
|
45
|
+
<Row label="time" value={date} format="time" />
|
|
46
|
+
<Row label="datetime" value={date} format="datetime" />
|
|
47
|
+
<Row label="short" value={date} format="short" />
|
|
48
|
+
<Row label="medium" value={date} format="medium" />
|
|
49
|
+
<Row label="long" value={date} format="long" />
|
|
50
|
+
<Row label="full" value={date} format="full" />
|
|
51
|
+
</View>
|
|
52
|
+
)
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Row({ label, value, format }: { label: string; value: any; format: any }) {
|
|
57
|
+
return (
|
|
58
|
+
<View className="flex-row items-center">
|
|
59
|
+
<Text className="w-24 text-text-secondary text-sm">{label}:</Text>
|
|
60
|
+
<DateTime value={value} format={format} className="text-text-primary" />
|
|
61
|
+
</View>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const RelativeTime: Story = {
|
|
66
|
+
render: () => {
|
|
67
|
+
const now = Date.now()
|
|
68
|
+
const dates = [
|
|
69
|
+
{ label: '30 seconds ago', value: now - 30 * 1000 },
|
|
70
|
+
{ label: '5 minutes ago', value: now - 5 * 60 * 1000 },
|
|
71
|
+
{ label: '2 hours ago', value: now - 2 * 60 * 60 * 1000 },
|
|
72
|
+
{ label: 'Yesterday', value: now - 24 * 60 * 60 * 1000 },
|
|
73
|
+
{ label: 'Last week', value: now - 7 * 24 * 60 * 60 * 1000 },
|
|
74
|
+
{ label: 'Last month', value: now - 30 * 24 * 60 * 60 * 1000 },
|
|
75
|
+
{ label: 'Last year', value: now - 365 * 24 * 60 * 60 * 1000 },
|
|
76
|
+
{ label: 'In 2 hours', value: now + 2 * 60 * 60 * 1000 },
|
|
77
|
+
{ label: 'Tomorrow', value: now + 24 * 60 * 60 * 1000 },
|
|
78
|
+
{ label: 'Next week', value: now + 7 * 24 * 60 * 60 * 1000 },
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<View className="gap-2">
|
|
83
|
+
{dates.map(({ label, value }) => (
|
|
84
|
+
<View key={label} className="flex-row items-center">
|
|
85
|
+
<Text className="w-32 text-text-secondary text-sm">{label}:</Text>
|
|
86
|
+
<DateTime value={value} format="relative" className="text-text-primary" />
|
|
87
|
+
</View>
|
|
88
|
+
))}
|
|
89
|
+
</View>
|
|
90
|
+
)
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const Colors: Story = {
|
|
95
|
+
render: () => (
|
|
96
|
+
<View className="gap-2">
|
|
97
|
+
<DateTime value={now} color="primary" />
|
|
98
|
+
<DateTime value={now} color="secondary" />
|
|
99
|
+
<DateTime value={now} color="tertiary" />
|
|
100
|
+
</View>
|
|
101
|
+
),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const UTCTime: Story = {
|
|
105
|
+
render: () => (
|
|
106
|
+
<View className="gap-3">
|
|
107
|
+
<View>
|
|
108
|
+
<Text className="text-text-secondary text-sm mb-1">Local Time:</Text>
|
|
109
|
+
<DateTime value={now} format="datetime" className="text-text-primary" />
|
|
110
|
+
</View>
|
|
111
|
+
<View>
|
|
112
|
+
<Text className="text-text-secondary text-sm mb-1">UTC Time:</Text>
|
|
113
|
+
<DateTime value={now} format="datetime" isUTC className="text-text-primary" />
|
|
114
|
+
</View>
|
|
115
|
+
</View>
|
|
116
|
+
),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const NullValue: Story = {
|
|
120
|
+
render: () => (
|
|
121
|
+
<View className="gap-2">
|
|
122
|
+
<View className="flex-row items-center">
|
|
123
|
+
<Text className="w-32 text-text-secondary text-sm">Default fallback:</Text>
|
|
124
|
+
<DateTime value={null} className="text-text-primary" />
|
|
125
|
+
</View>
|
|
126
|
+
<View className="flex-row items-center">
|
|
127
|
+
<Text className="w-32 text-text-secondary text-sm">Custom fallback:</Text>
|
|
128
|
+
<DateTime value={undefined} fallback="Not set" className="text-text-primary" />
|
|
129
|
+
</View>
|
|
130
|
+
</View>
|
|
131
|
+
),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const DifferentInputTypes: Story = {
|
|
135
|
+
render: () => {
|
|
136
|
+
const timestamp = 1718450400000 // June 15, 2024
|
|
137
|
+
const dateObject = new Date(timestamp)
|
|
138
|
+
const isoString = dateObject.toISOString()
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<View className="gap-3">
|
|
142
|
+
<View>
|
|
143
|
+
<Text className="text-text-secondary text-sm mb-1">Timestamp (number):</Text>
|
|
144
|
+
<DateTime value={timestamp} format="medium" className="text-text-primary" />
|
|
145
|
+
</View>
|
|
146
|
+
<View>
|
|
147
|
+
<Text className="text-text-secondary text-sm mb-1">Date object:</Text>
|
|
148
|
+
<DateTime value={dateObject} format="medium" className="text-text-primary" />
|
|
149
|
+
</View>
|
|
150
|
+
<View>
|
|
151
|
+
<Text className="text-text-secondary text-sm mb-1">ISO string:</Text>
|
|
152
|
+
<DateTime value={isoString} format="medium" className="text-text-primary" />
|
|
153
|
+
</View>
|
|
154
|
+
</View>
|
|
155
|
+
)
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const UsageExample: Story = {
|
|
160
|
+
render: () => {
|
|
161
|
+
const items = [
|
|
162
|
+
{ title: 'Project kickoff meeting', date: now - 3 * 24 * 60 * 60 * 1000 },
|
|
163
|
+
{ title: 'Design review', date: now - 1 * 24 * 60 * 60 * 1000 },
|
|
164
|
+
{ title: 'Sprint planning', date: now + 2 * 24 * 60 * 60 * 1000 },
|
|
165
|
+
{ title: 'Release deadline', date: now + 7 * 24 * 60 * 60 * 1000 },
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<View className="gap-2 p-4 bg-surface-elevated rounded-lg max-w-md">
|
|
170
|
+
<Text className="text-lg font-semibold text-text-primary mb-2">Upcoming Events</Text>
|
|
171
|
+
{items.map((item, index) => (
|
|
172
|
+
<View key={index} className="flex-row justify-between items-center py-2 border-b border-border-default">
|
|
173
|
+
<Text className="text-text-primary">{item.title}</Text>
|
|
174
|
+
<DateTime value={item.date} format="relative" color="secondary" className="text-sm" />
|
|
175
|
+
</View>
|
|
176
|
+
))}
|
|
177
|
+
</View>
|
|
178
|
+
)
|
|
179
|
+
},
|
|
180
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { DateTime, formatDateTime } from './DateTime'
|
|
5
|
+
|
|
6
|
+
describe('DateTime', () => {
|
|
7
|
+
const testDate = new Date('2024-06-15T14:30:00Z')
|
|
8
|
+
const testTimestamp = testDate.getTime()
|
|
9
|
+
const testISOString = '2024-06-15T14:30:00Z'
|
|
10
|
+
|
|
11
|
+
it('renders with Date object', () => {
|
|
12
|
+
render(<DateTime value={testDate} format="date" isUTC />)
|
|
13
|
+
// Should render a formatted date string
|
|
14
|
+
expect(screen.getByText(/2024/)).toBeInTheDocument()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('renders with timestamp', () => {
|
|
18
|
+
render(<DateTime value={testTimestamp} format="date" isUTC />)
|
|
19
|
+
expect(screen.getByText(/2024/)).toBeInTheDocument()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('renders with ISO string', () => {
|
|
23
|
+
render(<DateTime value={testISOString} format="date" isUTC />)
|
|
24
|
+
expect(screen.getByText(/2024/)).toBeInTheDocument()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('renders fallback when value is null', () => {
|
|
28
|
+
render(<DateTime value={null} />)
|
|
29
|
+
expect(screen.getByText('-')).toBeInTheDocument()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('renders fallback when value is undefined', () => {
|
|
33
|
+
render(<DateTime value={undefined} />)
|
|
34
|
+
expect(screen.getByText('-')).toBeInTheDocument()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('renders custom fallback', () => {
|
|
38
|
+
render(<DateTime value={null} fallback="N/A" />)
|
|
39
|
+
expect(screen.getByText('N/A')).toBeInTheDocument()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('renders Invalid Date for bad input', () => {
|
|
43
|
+
render(<DateTime value="not-a-date" />)
|
|
44
|
+
expect(screen.getByText('Invalid Date')).toBeInTheDocument()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('formats', () => {
|
|
48
|
+
it('renders date format', () => {
|
|
49
|
+
render(<DateTime value={testDate} format="date" isUTC />)
|
|
50
|
+
expect(screen.getByText(/06/)).toBeInTheDocument()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('renders time format', () => {
|
|
54
|
+
render(<DateTime value={testDate} format="time" isUTC />)
|
|
55
|
+
// Should contain time portion
|
|
56
|
+
const textContent = screen.getByText(/\d/).textContent
|
|
57
|
+
expect(textContent).toBeDefined()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('renders datetime format', () => {
|
|
61
|
+
render(<DateTime value={testDate} format="datetime" isUTC />)
|
|
62
|
+
expect(screen.getByText(/2024/)).toBeInTheDocument()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('renders short format', () => {
|
|
66
|
+
render(<DateTime value={testDate} format="short" isUTC />)
|
|
67
|
+
expect(screen.getByText(/Jun/)).toBeInTheDocument()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('renders medium format', () => {
|
|
71
|
+
render(<DateTime value={testDate} format="medium" isUTC />)
|
|
72
|
+
expect(screen.getByText(/Jun/)).toBeInTheDocument()
|
|
73
|
+
expect(screen.getByText(/2024/)).toBeInTheDocument()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('renders long format', () => {
|
|
77
|
+
render(<DateTime value={testDate} format="long" isUTC />)
|
|
78
|
+
expect(screen.getByText(/June/)).toBeInTheDocument()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('renders full format', () => {
|
|
82
|
+
render(<DateTime value={testDate} format="full" isUTC />)
|
|
83
|
+
expect(screen.getByText(/June/)).toBeInTheDocument()
|
|
84
|
+
expect(screen.getByText(/2024/)).toBeInTheDocument()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('renders relative format', () => {
|
|
88
|
+
// Use a date far in the past to get a stable relative output
|
|
89
|
+
const pastDate = new Date('2020-01-01T00:00:00Z')
|
|
90
|
+
render(<DateTime value={pastDate} format="relative" />)
|
|
91
|
+
// Should contain "ago" or "year" type relative text
|
|
92
|
+
const el = screen.getByText(/year|ago/)
|
|
93
|
+
expect(el).toBeInTheDocument()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('color variants', () => {
|
|
98
|
+
const colors = ['primary', 'secondary', 'tertiary', 'inherit'] as const
|
|
99
|
+
colors.forEach((color) => {
|
|
100
|
+
it(`renders with color ${color}`, () => {
|
|
101
|
+
render(<DateTime value={testDate} format="short" color={color} isUTC />)
|
|
102
|
+
expect(screen.getByText(/Jun/)).toBeInTheDocument()
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('UTC mode', () => {
|
|
108
|
+
it('renders in UTC when isUTC is true', () => {
|
|
109
|
+
render(<DateTime value={testDate} format="date" isUTC />)
|
|
110
|
+
expect(screen.getByText(/2024/)).toBeInTheDocument()
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('formatDateTime utility', () => {
|
|
115
|
+
it('formats a date value', () => {
|
|
116
|
+
const result = formatDateTime(testDate, 'short', true)
|
|
117
|
+
expect(result).toContain('Jun')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('returns fallback for null', () => {
|
|
121
|
+
expect(formatDateTime(null)).toBe('-')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('returns fallback for undefined', () => {
|
|
125
|
+
expect(formatDateTime(undefined)).toBe('-')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('returns custom fallback', () => {
|
|
129
|
+
expect(formatDateTime(null, 'date', false, 'N/A')).toBe('N/A')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('returns Invalid Date for bad input', () => {
|
|
133
|
+
expect(formatDateTime('not-a-date')).toBe('Invalid Date')
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('accessibility', () => {
|
|
138
|
+
it('has no accessibility violations', async () => {
|
|
139
|
+
const { container } = render(
|
|
140
|
+
<DateTime value={testDate} format="medium" isUTC />
|
|
141
|
+
)
|
|
142
|
+
const results = await axe(container)
|
|
143
|
+
expect(results).toHaveNoViolations()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('has no accessibility violations with fallback', async () => {
|
|
147
|
+
const { container } = render(<DateTime value={null} fallback="No date" />)
|
|
148
|
+
const results = await axe(container)
|
|
149
|
+
expect(results).toHaveNoViolations()
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Text, type TextProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export type DateTimeFormat =
|
|
6
|
+
| 'date' // 2024-01-15
|
|
7
|
+
| 'time' // 14:30
|
|
8
|
+
| 'datetime' // 2024-01-15 14:30
|
|
9
|
+
| 'relative' // 2 hours ago
|
|
10
|
+
| 'short' // Jan 15
|
|
11
|
+
| 'medium' // Jan 15, 2024
|
|
12
|
+
| 'long' // January 15, 2024
|
|
13
|
+
| 'full' // Monday, January 15, 2024
|
|
14
|
+
|
|
15
|
+
export interface DateTimeProps extends TextProps {
|
|
16
|
+
/** Date value (timestamp in ms, Date object, or ISO string) */
|
|
17
|
+
value: number | Date | string | null | undefined
|
|
18
|
+
/** Display format */
|
|
19
|
+
format?: DateTimeFormat
|
|
20
|
+
/** Custom format string (overrides format) */
|
|
21
|
+
customFormat?: string
|
|
22
|
+
/** Whether to show in UTC */
|
|
23
|
+
isUTC?: boolean
|
|
24
|
+
/** Fallback text when value is null/undefined */
|
|
25
|
+
fallback?: string
|
|
26
|
+
/** Text color */
|
|
27
|
+
color?: 'primary' | 'secondary' | 'tertiary' | 'inherit'
|
|
28
|
+
/** Additional className */
|
|
29
|
+
className?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const colorStyles = {
|
|
33
|
+
primary: 'text-text-primary',
|
|
34
|
+
secondary: 'text-text-secondary',
|
|
35
|
+
tertiary: 'text-text-tertiary',
|
|
36
|
+
inherit: '',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Formats a date value to a readable string.
|
|
41
|
+
*/
|
|
42
|
+
function formatDate(
|
|
43
|
+
value: number | Date | string,
|
|
44
|
+
format: DateTimeFormat,
|
|
45
|
+
isUTC: boolean
|
|
46
|
+
): string {
|
|
47
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
48
|
+
|
|
49
|
+
if (isNaN(date.getTime())) {
|
|
50
|
+
return 'Invalid Date'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// For relative time
|
|
54
|
+
if (format === 'relative') {
|
|
55
|
+
return getRelativeTime(date)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Use Intl.DateTimeFormat for locale-aware formatting
|
|
59
|
+
const options: Intl.DateTimeFormatOptions = {}
|
|
60
|
+
|
|
61
|
+
switch (format) {
|
|
62
|
+
case 'date':
|
|
63
|
+
options.year = 'numeric'
|
|
64
|
+
options.month = '2-digit'
|
|
65
|
+
options.day = '2-digit'
|
|
66
|
+
break
|
|
67
|
+
case 'time':
|
|
68
|
+
options.hour = '2-digit'
|
|
69
|
+
options.minute = '2-digit'
|
|
70
|
+
break
|
|
71
|
+
case 'datetime':
|
|
72
|
+
options.year = 'numeric'
|
|
73
|
+
options.month = '2-digit'
|
|
74
|
+
options.day = '2-digit'
|
|
75
|
+
options.hour = '2-digit'
|
|
76
|
+
options.minute = '2-digit'
|
|
77
|
+
break
|
|
78
|
+
case 'short':
|
|
79
|
+
options.month = 'short'
|
|
80
|
+
options.day = 'numeric'
|
|
81
|
+
break
|
|
82
|
+
case 'medium':
|
|
83
|
+
options.month = 'short'
|
|
84
|
+
options.day = 'numeric'
|
|
85
|
+
options.year = 'numeric'
|
|
86
|
+
break
|
|
87
|
+
case 'long':
|
|
88
|
+
options.month = 'long'
|
|
89
|
+
options.day = 'numeric'
|
|
90
|
+
options.year = 'numeric'
|
|
91
|
+
break
|
|
92
|
+
case 'full':
|
|
93
|
+
options.weekday = 'long'
|
|
94
|
+
options.month = 'long'
|
|
95
|
+
options.day = 'numeric'
|
|
96
|
+
options.year = 'numeric'
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (isUTC) {
|
|
101
|
+
options.timeZone = 'UTC'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return new Intl.DateTimeFormat(undefined, options).format(date)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Gets relative time string (e.g., "2 hours ago", "in 3 days")
|
|
109
|
+
*/
|
|
110
|
+
function getRelativeTime(date: Date): string {
|
|
111
|
+
const now = new Date()
|
|
112
|
+
const diffMs = date.getTime() - now.getTime()
|
|
113
|
+
const diffSecs = Math.round(diffMs / 1000)
|
|
114
|
+
const diffMins = Math.round(diffSecs / 60)
|
|
115
|
+
const diffHours = Math.round(diffMins / 60)
|
|
116
|
+
const diffDays = Math.round(diffHours / 24)
|
|
117
|
+
const diffWeeks = Math.round(diffDays / 7)
|
|
118
|
+
const diffMonths = Math.round(diffDays / 30)
|
|
119
|
+
const diffYears = Math.round(diffDays / 365)
|
|
120
|
+
|
|
121
|
+
// Use Intl.RelativeTimeFormat if available
|
|
122
|
+
if (typeof Intl !== 'undefined' && Intl.RelativeTimeFormat) {
|
|
123
|
+
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })
|
|
124
|
+
|
|
125
|
+
if (Math.abs(diffSecs) < 60) {
|
|
126
|
+
return rtf.format(diffSecs, 'second')
|
|
127
|
+
}
|
|
128
|
+
if (Math.abs(diffMins) < 60) {
|
|
129
|
+
return rtf.format(diffMins, 'minute')
|
|
130
|
+
}
|
|
131
|
+
if (Math.abs(diffHours) < 24) {
|
|
132
|
+
return rtf.format(diffHours, 'hour')
|
|
133
|
+
}
|
|
134
|
+
if (Math.abs(diffDays) < 7) {
|
|
135
|
+
return rtf.format(diffDays, 'day')
|
|
136
|
+
}
|
|
137
|
+
if (Math.abs(diffWeeks) < 4) {
|
|
138
|
+
return rtf.format(diffWeeks, 'week')
|
|
139
|
+
}
|
|
140
|
+
if (Math.abs(diffMonths) < 12) {
|
|
141
|
+
return rtf.format(diffMonths, 'month')
|
|
142
|
+
}
|
|
143
|
+
return rtf.format(diffYears, 'year')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Fallback for environments without Intl.RelativeTimeFormat
|
|
147
|
+
const absSeconds = Math.abs(diffSecs)
|
|
148
|
+
const isFuture = diffMs > 0
|
|
149
|
+
const prefix = isFuture ? 'in ' : ''
|
|
150
|
+
const suffix = isFuture ? '' : ' ago'
|
|
151
|
+
|
|
152
|
+
if (absSeconds < 60) {
|
|
153
|
+
return 'just now'
|
|
154
|
+
}
|
|
155
|
+
if (Math.abs(diffMins) < 60) {
|
|
156
|
+
const mins = Math.abs(diffMins)
|
|
157
|
+
return `${prefix}${mins} minute${mins === 1 ? '' : 's'}${suffix}`
|
|
158
|
+
}
|
|
159
|
+
if (Math.abs(diffHours) < 24) {
|
|
160
|
+
const hours = Math.abs(diffHours)
|
|
161
|
+
return `${prefix}${hours} hour${hours === 1 ? '' : 's'}${suffix}`
|
|
162
|
+
}
|
|
163
|
+
if (Math.abs(diffDays) < 7) {
|
|
164
|
+
const days = Math.abs(diffDays)
|
|
165
|
+
return `${prefix}${days} day${days === 1 ? '' : 's'}${suffix}`
|
|
166
|
+
}
|
|
167
|
+
if (Math.abs(diffWeeks) < 4) {
|
|
168
|
+
const weeks = Math.abs(diffWeeks)
|
|
169
|
+
return `${prefix}${weeks} week${weeks === 1 ? '' : 's'}${suffix}`
|
|
170
|
+
}
|
|
171
|
+
if (Math.abs(diffMonths) < 12) {
|
|
172
|
+
const months = Math.abs(diffMonths)
|
|
173
|
+
return `${prefix}${months} month${months === 1 ? '' : 's'}${suffix}`
|
|
174
|
+
}
|
|
175
|
+
const years = Math.abs(diffYears)
|
|
176
|
+
return `${prefix}${years} year${years === 1 ? '' : 's'}${suffix}`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* DateTime component for displaying formatted dates and times.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* // Basic usage
|
|
184
|
+
* <DateTime value={Date.now()} />
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* // Different formats
|
|
188
|
+
* <DateTime value={date} format="relative" />
|
|
189
|
+
* <DateTime value={date} format="long" />
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* // UTC time
|
|
193
|
+
* <DateTime value={timestamp} format="datetime" isUTC />
|
|
194
|
+
*/
|
|
195
|
+
export function DateTime({
|
|
196
|
+
value,
|
|
197
|
+
format = 'datetime',
|
|
198
|
+
customFormat,
|
|
199
|
+
isUTC = false,
|
|
200
|
+
fallback = '-',
|
|
201
|
+
color = 'inherit',
|
|
202
|
+
className,
|
|
203
|
+
...props
|
|
204
|
+
}: DateTimeProps) {
|
|
205
|
+
if (value === null || value === undefined) {
|
|
206
|
+
return (
|
|
207
|
+
<Text className={cn(colorStyles[color], className)} {...props}>
|
|
208
|
+
{fallback}
|
|
209
|
+
</Text>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const formattedDate = formatDate(value, format, isUTC)
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<Text className={cn(colorStyles[color], className)} {...props}>
|
|
217
|
+
{formattedDate}
|
|
218
|
+
</Text>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Utility function to format dates outside of React components.
|
|
224
|
+
*/
|
|
225
|
+
export function formatDateTime(
|
|
226
|
+
value: number | Date | string | null | undefined,
|
|
227
|
+
format: DateTimeFormat = 'datetime',
|
|
228
|
+
isUTC: boolean = false,
|
|
229
|
+
fallback: string = '-'
|
|
230
|
+
): string {
|
|
231
|
+
if (value === null || value === undefined) {
|
|
232
|
+
return fallback
|
|
233
|
+
}
|
|
234
|
+
return formatDate(value, format, isUTC)
|
|
235
|
+
}
|