@titan-design/react-ui 0.1.0 → 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/package.json +10 -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
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form utility functions for consistent form field handling
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react'
|
|
6
|
+
|
|
7
|
+
export interface FieldState {
|
|
8
|
+
/** Whether the field has been touched */
|
|
9
|
+
touched: boolean
|
|
10
|
+
/** Whether the field has an error */
|
|
11
|
+
hasError: boolean
|
|
12
|
+
/** Error message if any */
|
|
13
|
+
errorMessage?: string
|
|
14
|
+
/** Whether the field is required */
|
|
15
|
+
isRequired?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FieldWrapperProps {
|
|
19
|
+
/** Field label */
|
|
20
|
+
label?: string
|
|
21
|
+
/** Helper text */
|
|
22
|
+
helperText?: string
|
|
23
|
+
/** Error message */
|
|
24
|
+
errorMessage?: string
|
|
25
|
+
/** Whether the field is required */
|
|
26
|
+
isRequired?: boolean
|
|
27
|
+
/** Whether the field is disabled */
|
|
28
|
+
isDisabled?: boolean
|
|
29
|
+
/** Whether to show error state */
|
|
30
|
+
isInvalid?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get validation state props from field state
|
|
35
|
+
*/
|
|
36
|
+
export function getFieldValidationProps(state: FieldState): {
|
|
37
|
+
isInvalid: boolean
|
|
38
|
+
errorMessage?: string
|
|
39
|
+
} {
|
|
40
|
+
return {
|
|
41
|
+
isInvalid: state.touched && state.hasError,
|
|
42
|
+
errorMessage: state.touched && state.hasError ? state.errorMessage : undefined,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a field ID from label text
|
|
48
|
+
*/
|
|
49
|
+
export function createFieldId(label: string): string {
|
|
50
|
+
return label
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
53
|
+
.replace(/(^-|-$)/g, '')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get aria attributes for form field
|
|
58
|
+
*/
|
|
59
|
+
export function getFieldAriaProps(props: FieldWrapperProps & { id: string }): {
|
|
60
|
+
'aria-invalid'?: boolean
|
|
61
|
+
'aria-required'?: boolean
|
|
62
|
+
'aria-describedby'?: string
|
|
63
|
+
} {
|
|
64
|
+
const describedBy: string[] = []
|
|
65
|
+
|
|
66
|
+
if (props.helperText) {
|
|
67
|
+
describedBy.push(`${props.id}-helper`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (props.isInvalid && props.errorMessage) {
|
|
71
|
+
describedBy.push(`${props.id}-error`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
'aria-invalid': props.isInvalid || undefined,
|
|
76
|
+
'aria-required': props.isRequired || undefined,
|
|
77
|
+
'aria-describedby': describedBy.length > 0 ? describedBy.join(' ') : undefined,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validate email format
|
|
83
|
+
*/
|
|
84
|
+
export function isValidEmail(email: string): boolean {
|
|
85
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
86
|
+
return emailRegex.test(email)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate minimum length
|
|
91
|
+
*/
|
|
92
|
+
export function hasMinLength(value: string, minLength: number): boolean {
|
|
93
|
+
return value.length >= minLength
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate maximum length
|
|
98
|
+
*/
|
|
99
|
+
export function hasMaxLength(value: string, maxLength: number): boolean {
|
|
100
|
+
return value.length <= maxLength
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if value is empty (null, undefined, or empty string)
|
|
105
|
+
*/
|
|
106
|
+
export function isEmpty(value: unknown): boolean {
|
|
107
|
+
if (value === null || value === undefined) return true
|
|
108
|
+
if (typeof value === 'string') return value.trim() === ''
|
|
109
|
+
if (Array.isArray(value)) return value.length === 0
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Common validation rules
|
|
115
|
+
*/
|
|
116
|
+
export const validationRules = {
|
|
117
|
+
required: (value: unknown) => {
|
|
118
|
+
if (isEmpty(value)) {
|
|
119
|
+
return 'This field is required'
|
|
120
|
+
}
|
|
121
|
+
return undefined
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
email: (value: string) => {
|
|
125
|
+
if (value && !isValidEmail(value)) {
|
|
126
|
+
return 'Please enter a valid email address'
|
|
127
|
+
}
|
|
128
|
+
return undefined
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
minLength: (min: number) => (value: string) => {
|
|
132
|
+
if (value && !hasMinLength(value, min)) {
|
|
133
|
+
return `Must be at least ${min} characters`
|
|
134
|
+
}
|
|
135
|
+
return undefined
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
maxLength: (max: number) => (value: string) => {
|
|
139
|
+
if (value && !hasMaxLength(value, max)) {
|
|
140
|
+
return `Must be no more than ${max} characters`
|
|
141
|
+
}
|
|
142
|
+
return undefined
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
pattern: (regex: RegExp, message: string) => (value: string) => {
|
|
146
|
+
if (value && !regex.test(value)) {
|
|
147
|
+
return message
|
|
148
|
+
}
|
|
149
|
+
return undefined
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
match: (otherValue: string, fieldName: string) => (value: string) => {
|
|
153
|
+
if (value !== otherValue) {
|
|
154
|
+
return `Must match ${fieldName}`
|
|
155
|
+
}
|
|
156
|
+
return undefined
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Compose multiple validation functions
|
|
162
|
+
*/
|
|
163
|
+
export function composeValidators(
|
|
164
|
+
...validators: Array<(value: any) => string | undefined>
|
|
165
|
+
) {
|
|
166
|
+
return (value: any): string | undefined => {
|
|
167
|
+
for (const validator of validators) {
|
|
168
|
+
const error = validator(value)
|
|
169
|
+
if (error) return error
|
|
170
|
+
}
|
|
171
|
+
return undefined
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Type for form values
|
|
177
|
+
*/
|
|
178
|
+
export type FormValues = Record<string, unknown>
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Type for form errors
|
|
182
|
+
*/
|
|
183
|
+
export type FormErrors<T extends FormValues> = Partial<Record<keyof T, string>>
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Type for form touched state
|
|
187
|
+
*/
|
|
188
|
+
export type FormTouched<T extends FormValues> = Partial<Record<keyof T, boolean>>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export { cn } from './cn'
|
|
2
|
+
export {
|
|
3
|
+
getStatusColor,
|
|
4
|
+
getResultColor,
|
|
5
|
+
getContrastText,
|
|
6
|
+
getLuminance,
|
|
7
|
+
alpha,
|
|
8
|
+
lighten,
|
|
9
|
+
darken,
|
|
10
|
+
} from './colors'
|
|
11
|
+
export type { StatusType, ResultType } from './colors'
|
|
12
|
+
export {
|
|
13
|
+
getFieldValidationProps,
|
|
14
|
+
createFieldId,
|
|
15
|
+
getFieldAriaProps,
|
|
16
|
+
isValidEmail,
|
|
17
|
+
hasMinLength,
|
|
18
|
+
hasMaxLength,
|
|
19
|
+
isEmpty,
|
|
20
|
+
validationRules,
|
|
21
|
+
composeValidators,
|
|
22
|
+
} from './form'
|
|
23
|
+
export type {
|
|
24
|
+
FieldState,
|
|
25
|
+
FieldWrapperProps,
|
|
26
|
+
FormValues,
|
|
27
|
+
FormErrors,
|
|
28
|
+
FormTouched,
|
|
29
|
+
} from './form'
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utility function to detect the current theme.
|
|
5
|
+
* Checks for the 'light' class on the html element (set by Storybook's theme addon).
|
|
6
|
+
*
|
|
7
|
+
* NOTE: This function reads the theme at call time. For reactive theme detection
|
|
8
|
+
* that re-renders when the theme changes, use the `useTheme` hook instead.
|
|
9
|
+
*
|
|
10
|
+
* @returns 'light' if light theme is active, 'dark' otherwise
|
|
11
|
+
*/
|
|
12
|
+
export function getTheme(): 'light' | 'dark' {
|
|
13
|
+
if (typeof document === 'undefined') return 'dark'
|
|
14
|
+
return document.documentElement.classList.contains('light') ? 'light' : 'dark'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Subscribers for theme changes
|
|
18
|
+
let listeners: Array<() => void> = []
|
|
19
|
+
|
|
20
|
+
function subscribe(listener: () => void) {
|
|
21
|
+
listeners = [...listeners, listener]
|
|
22
|
+
|
|
23
|
+
// Set up MutationObserver to watch for class changes on html element
|
|
24
|
+
if (typeof document !== 'undefined' && listeners.length === 1) {
|
|
25
|
+
const observer = new MutationObserver((mutations) => {
|
|
26
|
+
for (const mutation of mutations) {
|
|
27
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
|
28
|
+
// Notify all listeners when class changes
|
|
29
|
+
listeners.forEach(l => l())
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
observer.observe(document.documentElement, {
|
|
35
|
+
attributes: true,
|
|
36
|
+
attributeFilter: ['class'],
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Store observer for cleanup
|
|
40
|
+
;(subscribe as any)._observer = observer
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return () => {
|
|
44
|
+
listeners = listeners.filter(l => l !== listener)
|
|
45
|
+
|
|
46
|
+
// Clean up observer when no more listeners
|
|
47
|
+
if (listeners.length === 0 && (subscribe as any)._observer) {
|
|
48
|
+
;(subscribe as any)._observer.disconnect()
|
|
49
|
+
;(subscribe as any)._observer = null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getSnapshot(): 'light' | 'dark' {
|
|
55
|
+
return getTheme()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getServerSnapshot(): 'light' | 'dark' {
|
|
59
|
+
return 'dark'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* React hook for reactive theme detection.
|
|
64
|
+
* Automatically re-renders when the theme changes (via class changes on html element).
|
|
65
|
+
*
|
|
66
|
+
* Uses useSyncExternalStore for proper React 18 concurrent mode support.
|
|
67
|
+
*
|
|
68
|
+
* @returns 'light' if light theme is active, 'dark' otherwise
|
|
69
|
+
*/
|
|
70
|
+
export function useTheme(): 'light' | 'dark' {
|
|
71
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
|
72
|
+
}
|