@sixthshift/design-system 0.1.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.
Files changed (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/dist/styles.css +2 -0
  4. package/dist/theme.generated.css +438 -0
  5. package/package.json +238 -0
  6. package/src/components/Avatar/Avatar.tsx +19 -0
  7. package/src/components/Avatar/index.ts +1 -0
  8. package/src/components/Badge/Badge.tsx +59 -0
  9. package/src/components/Badge/index.ts +1 -0
  10. package/src/components/BarChart/BarChart.tsx +51 -0
  11. package/src/components/BarChart/index.ts +1 -0
  12. package/src/components/Breadcrumb/Breadcrumb.tsx +47 -0
  13. package/src/components/Breadcrumb/index.ts +5 -0
  14. package/src/components/Button/Button.tsx +157 -0
  15. package/src/components/Button/index.ts +1 -0
  16. package/src/components/Calendar/Calendar.tsx +433 -0
  17. package/src/components/Calendar/calendar.hooks.ts +329 -0
  18. package/src/components/Calendar/calendar.types.ts +117 -0
  19. package/src/components/Calendar/index.ts +28 -0
  20. package/src/components/Card/Card.tsx +47 -0
  21. package/src/components/Card/index.ts +1 -0
  22. package/src/components/Checkbox/Checkbox.tsx +144 -0
  23. package/src/components/Checkbox/index.ts +1 -0
  24. package/src/components/CheckboxGroup/CheckboxGroup.tsx +173 -0
  25. package/src/components/CheckboxGroup/index.ts +5 -0
  26. package/src/components/Code/Editor/Editor.tsx +161 -0
  27. package/src/components/Code/Editor/index.ts +2 -0
  28. package/src/components/Code/Editor/semanticTokenProvider.ts +69 -0
  29. package/src/components/Code/Editor/themes.ts +112 -0
  30. package/src/components/Code/ValidationStatus/ValidationStatus.tsx +81 -0
  31. package/src/components/Code/ValidationStatus/index.ts +1 -0
  32. package/src/components/Code/Workspace/StatusBar.tsx +47 -0
  33. package/src/components/Code/Workspace/Toolbar.tsx +47 -0
  34. package/src/components/Code/Workspace/Workspace.tsx +109 -0
  35. package/src/components/Code/Workspace/index.ts +3 -0
  36. package/src/components/ColorDot/ColorDot.tsx +50 -0
  37. package/src/components/ColorDot/index.ts +1 -0
  38. package/src/components/DatePicker/DatePicker.tsx +236 -0
  39. package/src/components/DatePicker/datepicker.hooks.ts +381 -0
  40. package/src/components/DatePicker/datepicker.types.ts +140 -0
  41. package/src/components/DatePicker/index.ts +12 -0
  42. package/src/components/DateRangePicker/DateRangePicker.tsx +127 -0
  43. package/src/components/DateRangePicker/daterangepicker.types.ts +53 -0
  44. package/src/components/DateRangePicker/index.ts +2 -0
  45. package/src/components/DateTimePicker/DateTimePicker.tsx +399 -0
  46. package/src/components/DateTimePicker/datetimepicker.types.ts +48 -0
  47. package/src/components/DateTimePicker/index.ts +2 -0
  48. package/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +679 -0
  49. package/src/components/DateTimeRangePicker/datetimerangepicker.types.ts +62 -0
  50. package/src/components/DateTimeRangePicker/index.ts +2 -0
  51. package/src/components/EmptyState/EmptyState.tsx +29 -0
  52. package/src/components/EmptyState/index.ts +1 -0
  53. package/src/components/Field/Field.tsx +60 -0
  54. package/src/components/Field/index.ts +1 -0
  55. package/src/components/FormField/FormField.tsx +55 -0
  56. package/src/components/FormField/index.ts +2 -0
  57. package/src/components/HeatMap/HeatMapCalendar.tsx +127 -0
  58. package/src/components/HeatMap/HeatMapMatrix.tsx +75 -0
  59. package/src/components/HeatMap/heat-map.utils.ts +15 -0
  60. package/src/components/HeatMap/index.ts +3 -0
  61. package/src/components/HoverCard/HoverCard.tsx +83 -0
  62. package/src/components/HoverCard/components/HoverCardContent.tsx +30 -0
  63. package/src/components/HoverCard/components/HoverCardContext.tsx +22 -0
  64. package/src/components/HoverCard/components/HoverCardTrigger.tsx +32 -0
  65. package/src/components/HoverCard/index.ts +3 -0
  66. package/src/components/Input/Input.tsx +48 -0
  67. package/src/components/Input/index.ts +1 -0
  68. package/src/components/LineChart/LineChart.tsx +251 -0
  69. package/src/components/LineChart/index.ts +1 -0
  70. package/src/components/Markdown/Markdown.tsx +40 -0
  71. package/src/components/Markdown/index.ts +1 -0
  72. package/src/components/Message/Message.tsx +59 -0
  73. package/src/components/Message/components/MessageBody.tsx +9 -0
  74. package/src/components/Message/components/MessageDescription.tsx +9 -0
  75. package/src/components/Message/components/MessageIcon.tsx +13 -0
  76. package/src/components/Message/components/MessageTitle.tsx +9 -0
  77. package/src/components/Message/index.ts +10 -0
  78. package/src/components/MetricList/MetricList.tsx +13 -0
  79. package/src/components/MetricList/MetricRow.tsx +30 -0
  80. package/src/components/MetricList/index.ts +2 -0
  81. package/src/components/Modal/Modal.tsx +146 -0
  82. package/src/components/Modal/components/ModalBody.tsx +9 -0
  83. package/src/components/Modal/components/ModalContext.tsx +27 -0
  84. package/src/components/Modal/components/ModalFooter.tsx +9 -0
  85. package/src/components/Modal/components/ModalHeader.tsx +29 -0
  86. package/src/components/Modal/components/index.ts +9 -0
  87. package/src/components/Modal/index.ts +13 -0
  88. package/src/components/NavBottom/NavBottom.tsx +53 -0
  89. package/src/components/NavBottom/index.ts +3 -0
  90. package/src/components/NavSide/NavItem.tsx +39 -0
  91. package/src/components/NavSide/NavSide.tsx +40 -0
  92. package/src/components/NavSide/index.ts +2 -0
  93. package/src/components/NavSide/types.ts +36 -0
  94. package/src/components/Pagination/Pagination.tsx +86 -0
  95. package/src/components/Pagination/index.ts +2 -0
  96. package/src/components/Popover/Popover.tsx +68 -0
  97. package/src/components/Popover/components/PopoverBody.tsx +32 -0
  98. package/src/components/Popover/components/PopoverClose.tsx +26 -0
  99. package/src/components/Popover/components/PopoverContext.tsx +24 -0
  100. package/src/components/Popover/components/PopoverTrigger.tsx +31 -0
  101. package/src/components/Popover/index.ts +10 -0
  102. package/src/components/ProgressBar/ProgressBar.tsx +42 -0
  103. package/src/components/ProgressBar/index.ts +1 -0
  104. package/src/components/RadioButton/RadioButton.tsx +89 -0
  105. package/src/components/RadioButton/index.ts +1 -0
  106. package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +193 -0
  107. package/src/components/RadioButtonGroup/index.ts +5 -0
  108. package/src/components/SearchInput/SearchInput.tsx +45 -0
  109. package/src/components/SearchInput/index.ts +2 -0
  110. package/src/components/Select/Select.tsx +289 -0
  111. package/src/components/Select/SelectDropdown.tsx +90 -0
  112. package/src/components/Select/SelectTrigger.tsx +130 -0
  113. package/src/components/Select/index.ts +3 -0
  114. package/src/components/Select/useSelectKeyboard.ts +111 -0
  115. package/src/components/Separator/Separator.tsx +20 -0
  116. package/src/components/Separator/index.ts +1 -0
  117. package/src/components/Sheet/Sheet.tsx +136 -0
  118. package/src/components/Sheet/components/SheetBody.tsx +9 -0
  119. package/src/components/Sheet/components/SheetContext.tsx +23 -0
  120. package/src/components/Sheet/components/SheetFooter.tsx +9 -0
  121. package/src/components/Sheet/components/SheetHeader.tsx +29 -0
  122. package/src/components/Sheet/components/index.ts +9 -0
  123. package/src/components/Sheet/index.ts +13 -0
  124. package/src/components/Skeleton/Skeleton.tsx +7 -0
  125. package/src/components/Skeleton/index.ts +1 -0
  126. package/src/components/Sparkline/Sparkline.tsx +93 -0
  127. package/src/components/Sparkline/index.ts +1 -0
  128. package/src/components/Spinner/Spinner.tsx +41 -0
  129. package/src/components/Spinner/index.ts +1 -0
  130. package/src/components/StatsCard/StatsCard.tsx +40 -0
  131. package/src/components/StatsCard/index.ts +1 -0
  132. package/src/components/Switch/Switch.tsx +128 -0
  133. package/src/components/Switch/index.ts +1 -0
  134. package/src/components/Tabs/Tabs.tsx +49 -0
  135. package/src/components/Tabs/components/TabsContext.tsx +35 -0
  136. package/src/components/Tabs/components/TabsList.tsx +98 -0
  137. package/src/components/Tabs/components/TabsPanels.tsx +26 -0
  138. package/src/components/Tabs/components/TabsTrigger.tsx +69 -0
  139. package/src/components/Tabs/index.ts +3 -0
  140. package/src/components/TagChip/TagChip.tsx +46 -0
  141. package/src/components/TagChip/index.ts +1 -0
  142. package/src/components/TagInput/TagInput.tsx +60 -0
  143. package/src/components/TagInput/index.ts +1 -0
  144. package/src/components/Text/Text.tsx +35 -0
  145. package/src/components/Text/index.ts +1 -0
  146. package/src/components/TextInline/TextInline.tsx +35 -0
  147. package/src/components/TextInline/index.ts +1 -0
  148. package/src/components/Textarea/Textarea.tsx +20 -0
  149. package/src/components/Textarea/index.ts +1 -0
  150. package/src/components/TimePicker/PeriodSelector.tsx +28 -0
  151. package/src/components/TimePicker/TimeColumn.tsx +62 -0
  152. package/src/components/TimePicker/TimePicker.tsx +323 -0
  153. package/src/components/TimePicker/index.ts +7 -0
  154. package/src/components/TimePicker/timepicker.hooks.ts +173 -0
  155. package/src/components/TimePicker/timepicker.types.ts +101 -0
  156. package/src/components/Toast/Toast.tsx +111 -0
  157. package/src/components/Toast/index.ts +1 -0
  158. package/src/components/Toggle/Toggle.tsx +72 -0
  159. package/src/components/Toggle/index.ts +1 -0
  160. package/src/components/ToggleGroup/ToggleGroup.tsx +14 -0
  161. package/src/components/ToggleGroup/ToggleGroupContainer.tsx +19 -0
  162. package/src/components/ToggleGroup/ToggleGroupItem.tsx +51 -0
  163. package/src/components/ToggleGroup/ToggleGroupMultiple.tsx +61 -0
  164. package/src/components/ToggleGroup/ToggleGroupSingle.tsx +105 -0
  165. package/src/components/ToggleGroup/index.ts +2 -0
  166. package/src/components/ToggleGroup/toggleGroup.types.ts +55 -0
  167. package/src/components/Tooltip/Tooltip.tsx +66 -0
  168. package/src/components/Tooltip/components/TooltipBody.tsx +30 -0
  169. package/src/components/Tooltip/components/TooltipContext.tsx +22 -0
  170. package/src/components/Tooltip/components/TooltipTrigger.tsx +27 -0
  171. package/src/components/Tooltip/index.ts +9 -0
  172. package/src/date-time/arithmetic.ts +101 -0
  173. package/src/date-time/boundaries.ts +91 -0
  174. package/src/date-time/compare.ts +85 -0
  175. package/src/date-time/duration.ts +63 -0
  176. package/src/date-time/elapsed.ts +18 -0
  177. package/src/date-time/format.ts +722 -0
  178. package/src/date-time/index.ts +279 -0
  179. package/src/date-time/now.ts +70 -0
  180. package/src/date-time/parse.ts +170 -0
  181. package/src/date-time/serialize.ts +90 -0
  182. package/src/date-time/timezone.ts +47 -0
  183. package/src/date-time/validate.ts +278 -0
  184. package/src/hooks/index.ts +10 -0
  185. package/src/hooks/useCollapsible.ts +16 -0
  186. package/src/hooks/useControllableState.ts +54 -0
  187. package/src/hooks/useDebouncedCallback.ts +21 -0
  188. package/src/hooks/useDebouncedState.ts +20 -0
  189. package/src/hooks/useDeviceFormFactor.ts +49 -0
  190. package/src/hooks/useLocalStorage.ts +67 -0
  191. package/src/hooks/useMergedFloatingRef.ts +29 -0
  192. package/src/hooks/usePresence.ts +127 -0
  193. package/src/hooks/useStack.ts +99 -0
  194. package/src/hooks/useTheme.ts +105 -0
  195. package/src/internal/Slot.tsx +79 -0
  196. package/src/internal/index.ts +2 -0
  197. package/src/internal/rovingFocus.ts +39 -0
  198. package/src/internal/types.ts +10 -0
  199. package/src/lib/EmptyBoundary.tsx +12 -0
  200. package/src/lib/ErrorBoundary.tsx +48 -0
  201. package/src/lib/contexts/ComponentsContext/ComponentsContext.tsx +34 -0
  202. package/src/lib/contexts/ComponentsContext/index.ts +1 -0
  203. package/src/lib/contexts/OverlayContext/OverlayContext.tsx +93 -0
  204. package/src/lib/contexts/OverlayContext/hooks/useModal.tsx +125 -0
  205. package/src/lib/contexts/OverlayContext/hooks/useToast.ts +80 -0
  206. package/src/lib/contexts/OverlayContext/index.ts +6 -0
  207. package/src/lib/format.ts +51 -0
  208. package/src/lib/utils.ts +10 -0
  209. package/src/lib/withEmpty.tsx +14 -0
  210. package/src/lib/withErrorBoundary.tsx +16 -0
  211. package/src/lib/withSuspense.tsx +16 -0
  212. package/src/lib/withSuspenseAndErrorBoundary.tsx +26 -0
  213. package/src/styles/base.css +41 -0
  214. package/src/theme/index.ts +37 -0
  215. package/src/theme/palette.json +100 -0
  216. package/src/theme/palette.schema.json +40 -0
  217. package/src/theme/schema.ts +122 -0
  218. package/src/theme/theme.json +349 -0
  219. package/src/theme/theme.schema.json +876 -0
  220. package/src/theme/typography.json +7 -0
  221. package/src/theme/typography.schema.json +36 -0
  222. package/src/typography/Body.tsx +8 -0
  223. package/src/typography/Caption.tsx +8 -0
  224. package/src/typography/Code.tsx +8 -0
  225. package/src/typography/Display.tsx +8 -0
  226. package/src/typography/Emphasis.tsx +8 -0
  227. package/src/typography/Heading.tsx +23 -0
  228. package/src/typography/Label.tsx +10 -0
  229. package/src/typography/Lead.tsx +8 -0
  230. package/src/typography/Mono.tsx +8 -0
  231. package/src/typography/Muted.tsx +8 -0
  232. package/src/typography/SectionTitle.tsx +8 -0
  233. package/src/typography/Subtitle.tsx +8 -0
  234. package/src/typography/Timestamp.tsx +8 -0
  235. package/src/typography/index.ts +10 -0
  236. package/tailwind.config.ts +261 -0
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Validation utilities: string validation and type guards
3
+ *
4
+ * Use these to validate input before parsing, or to narrow types at runtime.
5
+ */
6
+
7
+ import { Temporal } from "@js-temporal/polyfill";
8
+
9
+ // =============================================================================
10
+ // String validation (is this string valid ISO 8601?)
11
+ // =============================================================================
12
+
13
+ /**
14
+ * Check if a string is a valid ISO 8601 date (YYYY-MM-DD)
15
+ *
16
+ * @example
17
+ * isISODate('2025-01-15') // true
18
+ * isISODate('2025-13-45') // false
19
+ * isISODate('not a date') // false
20
+ */
21
+ export function isISODate(value: unknown): value is string {
22
+ if (typeof value !== "string") return false;
23
+ try {
24
+ Temporal.PlainDate.from(value);
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Check if a string is a valid ISO 8601 time (HH:mm:ss or HH:mm)
33
+ *
34
+ * @example
35
+ * isISOTime('14:30:00') // true
36
+ * isISOTime('14:30') // true
37
+ * isISOTime('25:00:00') // false
38
+ */
39
+ export function isISOTime(value: unknown): value is string {
40
+ if (typeof value !== "string") return false;
41
+ try {
42
+ Temporal.PlainTime.from(value);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Check if a string is a valid ISO 8601 datetime (YYYY-MM-DDTHH:mm:ss)
51
+ *
52
+ * @example
53
+ * isISODateTime('2025-01-15T14:30:00') // true
54
+ * isISODateTime('2025-01-15') // false (date only)
55
+ * isISODateTime('invalid') // false
56
+ */
57
+ export function isISODateTime(value: unknown): value is string {
58
+ if (typeof value !== "string") return false;
59
+ try {
60
+ Temporal.PlainDateTime.from(value);
61
+ return true;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Check if a string is a valid ISO 8601 instant (with Z or offset)
69
+ *
70
+ * @example
71
+ * isISOInstant('2025-01-15T14:30:00.000Z') // true
72
+ * isISOInstant('2025-01-15T14:30:00+05:30') // true
73
+ * isISOInstant('2025-01-15T14:30:00') // false (no timezone)
74
+ * isISOInstant('2025-01-15') // false (date only)
75
+ */
76
+ export function isISOInstant(value: unknown): value is string {
77
+ if (typeof value !== "string") return false;
78
+ try {
79
+ Temporal.Instant.from(value);
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Check if a string is a valid ISO 8601 zoned datetime
88
+ *
89
+ * @example
90
+ * isISOZoned('2025-01-15T14:30:00[America/New_York]') // true
91
+ * isISOZoned('2025-01-15T14:30:00-05:00[America/New_York]') // true
92
+ * isISOZoned('2025-01-15T14:30:00Z') // false (instant, not zoned)
93
+ */
94
+ export function isISOZoned(value: unknown): value is string {
95
+ if (typeof value !== "string") return false;
96
+ // Must contain a bracketed timezone identifier
97
+ if (!value.includes("[") || !value.includes("]")) return false;
98
+ try {
99
+ Temporal.ZonedDateTime.from(value);
100
+ return true;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Check if a string is a valid ISO 8601 duration
108
+ *
109
+ * @example
110
+ * isISODuration('PT1H30M') // true (1 hour 30 minutes)
111
+ * isISODuration('P1D') // true (1 day)
112
+ * isISODuration('P1Y2M3D') // true (1 year 2 months 3 days)
113
+ * isISODuration('1 hour') // false
114
+ */
115
+ export function isISODuration(value: unknown): value is string {
116
+ if (typeof value !== "string") return false;
117
+ try {
118
+ Temporal.Duration.from(value);
119
+ return true;
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Check if a string is any valid ISO 8601 date/time format
127
+ * (date, time, datetime, instant, or zoned)
128
+ *
129
+ * @example
130
+ * isISOString('2025-01-15') // true
131
+ * isISOString('2025-01-15T14:30:00.000Z') // true
132
+ * isISOString('not a date') // false
133
+ */
134
+ export function isISOString(value: unknown): value is string {
135
+ return isISODate(value) || isISOTime(value) || isISODateTime(value) || isISOInstant(value) || isISOZoned(value);
136
+ }
137
+
138
+ // =============================================================================
139
+ // Type guards (is this value a Temporal type?)
140
+ // =============================================================================
141
+
142
+ /**
143
+ * Check if a value is a Temporal.PlainDate
144
+ */
145
+ export function isPlainDate(value: unknown): value is Temporal.PlainDate {
146
+ return value instanceof Temporal.PlainDate;
147
+ }
148
+
149
+ /**
150
+ * Check if a value is a Temporal.PlainTime
151
+ */
152
+ export function isPlainTime(value: unknown): value is Temporal.PlainTime {
153
+ return value instanceof Temporal.PlainTime;
154
+ }
155
+
156
+ /**
157
+ * Check if a value is a Temporal.PlainDateTime
158
+ */
159
+ export function isPlainDateTime(value: unknown): value is Temporal.PlainDateTime {
160
+ return value instanceof Temporal.PlainDateTime;
161
+ }
162
+
163
+ /**
164
+ * Check if a value is a Temporal.Instant
165
+ */
166
+ export function isInstant(value: unknown): value is Temporal.Instant {
167
+ return value instanceof Temporal.Instant;
168
+ }
169
+
170
+ /**
171
+ * Check if a value is a Temporal.ZonedDateTime
172
+ */
173
+ export function isZonedDateTime(value: unknown): value is Temporal.ZonedDateTime {
174
+ return value instanceof Temporal.ZonedDateTime;
175
+ }
176
+
177
+ /**
178
+ * Check if a value is a Temporal.Duration
179
+ */
180
+ export function isDuration(value: unknown): value is Temporal.Duration {
181
+ return value instanceof Temporal.Duration;
182
+ }
183
+
184
+ /**
185
+ * Check if a value is any Temporal date/time type
186
+ */
187
+ export function isTemporal(
188
+ value: unknown
189
+ ): value is Temporal.PlainDate | Temporal.PlainTime | Temporal.PlainDateTime | Temporal.Instant | Temporal.ZonedDateTime | Temporal.Duration {
190
+ return isPlainDate(value) || isPlainTime(value) || isPlainDateTime(value) || isInstant(value) || isZonedDateTime(value) || isDuration(value);
191
+ }
192
+
193
+ // =============================================================================
194
+ // Validation with error messages
195
+ // =============================================================================
196
+
197
+ export interface ValidationResult {
198
+ valid: boolean;
199
+ error?: string;
200
+ }
201
+
202
+ /**
203
+ * Validate an ISO date string and return detailed error if invalid
204
+ */
205
+ export function validateISODate(value: unknown): ValidationResult {
206
+ if (typeof value !== "string") {
207
+ return { valid: false, error: `Expected string, got ${typeof value}` };
208
+ }
209
+ try {
210
+ Temporal.PlainDate.from(value);
211
+ return { valid: true };
212
+ } catch (e) {
213
+ return {
214
+ valid: false,
215
+ error: e instanceof Error ? e.message : "Invalid ISO date",
216
+ };
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Validate an ISO instant string and return detailed error if invalid
222
+ */
223
+ export function validateISOInstant(value: unknown): ValidationResult {
224
+ if (typeof value !== "string") {
225
+ return { valid: false, error: `Expected string, got ${typeof value}` };
226
+ }
227
+ try {
228
+ Temporal.Instant.from(value);
229
+ return { valid: true };
230
+ } catch (e) {
231
+ return {
232
+ valid: false,
233
+ error: e instanceof Error ? e.message : "Invalid ISO instant",
234
+ };
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Validate an ISO zoned datetime string and return detailed error if invalid
240
+ */
241
+ export function validateISOZoned(value: unknown): ValidationResult {
242
+ if (typeof value !== "string") {
243
+ return { valid: false, error: `Expected string, got ${typeof value}` };
244
+ }
245
+ if (!value.includes("[") || !value.includes("]")) {
246
+ return {
247
+ valid: false,
248
+ error: "Zoned datetime must include timezone in brackets, e.g. [America/New_York]",
249
+ };
250
+ }
251
+ try {
252
+ Temporal.ZonedDateTime.from(value);
253
+ return { valid: true };
254
+ } catch (e) {
255
+ return {
256
+ valid: false,
257
+ error: e instanceof Error ? e.message : "Invalid ISO zoned datetime",
258
+ };
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Validate an ISO duration string and return detailed error if invalid
264
+ */
265
+ export function validateISODuration(value: unknown): ValidationResult {
266
+ if (typeof value !== "string") {
267
+ return { valid: false, error: `Expected string, got ${typeof value}` };
268
+ }
269
+ try {
270
+ Temporal.Duration.from(value);
271
+ return { valid: true };
272
+ } catch (e) {
273
+ return {
274
+ valid: false,
275
+ error: e instanceof Error ? e.message : "Invalid ISO duration",
276
+ };
277
+ }
278
+ }
@@ -0,0 +1,10 @@
1
+ export { useCollapsible } from "./useCollapsible";
2
+ export { useControllableState } from "./useControllableState";
3
+ export { useDebouncedCallback } from "./useDebouncedCallback";
4
+ export { useDebouncedState } from "./useDebouncedState";
5
+ export { type DeviceFormFactor, useDeviceFormFactor } from "./useDeviceFormFactor";
6
+ export { useLocalStorage } from "./useLocalStorage";
7
+ export { useMergedFloatingRef } from "./useMergedFloatingRef";
8
+ export { usePresence } from "./usePresence";
9
+ export { type StackItem, useStack } from "./useStack";
10
+ export { bootstrapTheme, type ResolvedTheme, type Theme, useTheme } from "./useTheme";
@@ -0,0 +1,16 @@
1
+ import { useCallback, useState } from "react";
2
+
3
+ export function useCollapsible(defaultExpanded = true) {
4
+ const [expanded, setExpanded] = useState(defaultExpanded);
5
+
6
+ const toggle = useCallback(() => setExpanded((prev) => !prev), []);
7
+ const collapse = useCallback(() => setExpanded(false), []);
8
+ const expand = useCallback(() => setExpanded(true), []);
9
+
10
+ return {
11
+ expanded,
12
+ toggle,
13
+ collapse,
14
+ expand,
15
+ };
16
+ }
@@ -0,0 +1,54 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+
3
+ declare const process: undefined | { env?: { NODE_ENV?: string } };
4
+
5
+ interface UseControllableStateOptions<T> {
6
+ /** Controlled value */
7
+ value?: T | undefined;
8
+ /** Default value for uncontrolled mode */
9
+ defaultValue: T;
10
+ /** Callback when value changes */
11
+ onChange?: ((value: T) => void) | undefined;
12
+ }
13
+
14
+ /**
15
+ * Hook for managing controlled/uncontrolled state pattern.
16
+ * If `value` is provided, the component is controlled.
17
+ * Otherwise, it manages its own internal state starting with `defaultValue`.
18
+ */
19
+ export function useControllableState<T>({ value: controlledValue, defaultValue, onChange }: UseControllableStateOptions<T>): [T, (value: T) => void] {
20
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
21
+
22
+ const isControlled = controlledValue !== undefined;
23
+ const value = isControlled ? controlledValue : uncontrolledValue;
24
+
25
+ const wasControlledRef = useRef(isControlled);
26
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
27
+ if (wasControlledRef.current && !isControlled) {
28
+ console.warn(
29
+ "useControllableState: switched from controlled to uncontrolled. This usually means `value` changed from a defined value to `undefined`. Decide between controlled and uncontrolled for the lifetime of the component."
30
+ );
31
+ }
32
+ if (!wasControlledRef.current && isControlled) {
33
+ console.warn(
34
+ "useControllableState: switched from uncontrolled to controlled. This usually means `value` changed from `undefined` to a defined value. Use `defaultValue` for uncontrolled components, or provide `value` from the first render."
35
+ );
36
+ }
37
+ if (isControlled && !onChange) {
38
+ console.warn("useControllableState: controlled component has `value` but no `onChange`. The component will be read-only.");
39
+ }
40
+ }
41
+ wasControlledRef.current = isControlled;
42
+
43
+ const setValue = useCallback(
44
+ (newValue: T) => {
45
+ if (!isControlled) {
46
+ setUncontrolledValue(newValue);
47
+ }
48
+ onChange?.(newValue);
49
+ },
50
+ [isControlled, onChange]
51
+ );
52
+
53
+ return [value, setValue];
54
+ }
@@ -0,0 +1,21 @@
1
+ import { useCallback, useRef } from "react";
2
+
3
+ /**
4
+ * Returns a debounced version of the callback.
5
+ * The callback is invoked after `delay` ms of inactivity.
6
+ */
7
+ export const useDebouncedCallback = <T extends (...args: never[]) => void>(callback: T, delay: number): T => {
8
+ // Passed explicitly: React 19's types dropped the zero-argument `useRef`
9
+ // overload, so the initial value has to be spelled out to build on both 18 and 19.
10
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
11
+ const callbackRef = useRef(callback);
12
+ callbackRef.current = callback;
13
+
14
+ return useCallback(
15
+ ((...args: Parameters<T>) => {
16
+ clearTimeout(timeoutRef.current);
17
+ timeoutRef.current = setTimeout(() => callbackRef.current(...args), delay);
18
+ }) as T,
19
+ [delay]
20
+ );
21
+ };
@@ -0,0 +1,20 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ /**
4
+ * Debounces a value by delaying updates until after the specified delay.
5
+ * Useful for delaying API calls until user stops typing.
6
+ *
7
+ * @param value - The value to debounce
8
+ * @param delay - The delay in milliseconds
9
+ * @returns The debounced value
10
+ */
11
+ export const useDebouncedState = <T>(value: T, delay: number): T => {
12
+ const [debouncedValue, setDebouncedValue] = useState(value);
13
+
14
+ useEffect(() => {
15
+ const timer = setTimeout(() => setDebouncedValue(value), delay);
16
+ return () => clearTimeout(timer);
17
+ }, [value, delay]);
18
+
19
+ return debouncedValue;
20
+ };
@@ -0,0 +1,49 @@
1
+ import { useSyncExternalStore } from "react";
2
+
3
+ /**
4
+ * Breakpoint thresholds for adaptive rendering.
5
+ *
6
+ * These define the minimum width for each form factor:
7
+ * - mobile: 0 – 639px
8
+ * - tablet: 640 – 1023px
9
+ * - desktop: 1024px+
10
+ */
11
+ const breakpoints = {
12
+ mobile: 0,
13
+ tablet: 640,
14
+ desktop: 1024,
15
+ } as const;
16
+
17
+ export type DeviceFormFactor = keyof typeof breakpoints;
18
+
19
+ function getFormFactor(width: number): DeviceFormFactor {
20
+ if (width >= breakpoints.desktop) return "desktop";
21
+ if (width >= breakpoints.tablet) return "tablet";
22
+ return "mobile";
23
+ }
24
+
25
+ function getServerSnapshot(): DeviceFormFactor {
26
+ return "desktop";
27
+ }
28
+
29
+ function getSnapshot(): DeviceFormFactor {
30
+ return getFormFactor(window.innerWidth);
31
+ }
32
+
33
+ function subscribe(callback: () => void): () => void {
34
+ window.addEventListener("resize", callback);
35
+ return () => window.removeEventListener("resize", callback);
36
+ }
37
+
38
+ /**
39
+ * Returns the current device form factor based on viewport width.
40
+ *
41
+ * - mobile: 0 – 639px
42
+ * - tablet: 640 – 1023px
43
+ * - desktop: 1024px+
44
+ *
45
+ * Uses useSyncExternalStore for efficient subscription to resize events.
46
+ */
47
+ export function useDeviceFormFactor(): DeviceFormFactor {
48
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
49
+ }
@@ -0,0 +1,67 @@
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+
3
+ type SetValue<T> = T | ((prev: T) => T);
4
+
5
+ function getStorageValue<T>(key: string, defaultValue: T): T {
6
+ if (typeof window === "undefined") {
7
+ return defaultValue;
8
+ }
9
+
10
+ try {
11
+ const item = window.localStorage.getItem(key);
12
+ return item ? (JSON.parse(item) as T) : defaultValue;
13
+ } catch {
14
+ return defaultValue;
15
+ }
16
+ }
17
+
18
+ function setStorageValue<T>(key: string, value: T): void {
19
+ if (typeof window === "undefined") {
20
+ return;
21
+ }
22
+
23
+ try {
24
+ const serialized = JSON.stringify(value);
25
+ window.localStorage.setItem(key, serialized);
26
+ window.dispatchEvent(new StorageEvent("storage", { key, newValue: serialized }));
27
+ } catch {
28
+ // Storage full or unavailable
29
+ }
30
+ }
31
+
32
+ export function useLocalStorage<T>(key: string, defaultValue: T): [T, (value: SetValue<T>) => void] {
33
+ const subscribe = useCallback(
34
+ (callback: () => void) => {
35
+ const handleStorage = (e: StorageEvent) => {
36
+ if (e.key === key || e.key === null) {
37
+ callback();
38
+ }
39
+ };
40
+
41
+ window.addEventListener("storage", handleStorage);
42
+ return () => window.removeEventListener("storage", handleStorage);
43
+ },
44
+ [key]
45
+ );
46
+
47
+ const getSnapshot = useCallback(() => {
48
+ return JSON.stringify(getStorageValue(key, defaultValue));
49
+ }, [key, defaultValue]);
50
+
51
+ const getServerSnapshot = useCallback(() => {
52
+ return JSON.stringify(defaultValue);
53
+ }, [defaultValue]);
54
+
55
+ const storedValue = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
56
+
57
+ const setValue = useCallback(
58
+ (value: SetValue<T>) => {
59
+ const currentValue = getStorageValue(key, defaultValue);
60
+ const newValue = value instanceof Function ? value(currentValue) : value;
61
+ setStorageValue(key, newValue);
62
+ },
63
+ [key, defaultValue]
64
+ );
65
+
66
+ return [JSON.parse(storedValue) as T, setValue];
67
+ }
@@ -0,0 +1,29 @@
1
+ import { useCallback } from "react";
2
+ import type { WritableRefObject } from "../internal/types";
3
+
4
+ type FloatingRefs = {
5
+ setFloating: (node: HTMLElement | null) => void;
6
+ };
7
+
8
+ type PresenceRef = (node: HTMLElement | null) => void;
9
+
10
+ type ConsumerRef<T extends HTMLElement> = React.Ref<T> | undefined;
11
+
12
+ /**
13
+ * Merges a floating-ui ref, a usePresence ref, and an optional consumer ref
14
+ * into a single callback ref. Used by Modal, Sheet, and Toast.
15
+ */
16
+ export function useMergedFloatingRef<T extends HTMLElement = HTMLElement>(floatingRefs: FloatingRefs, presenceRef: PresenceRef, consumerRef?: ConsumerRef<T>) {
17
+ return useCallback(
18
+ (node: T | null) => {
19
+ floatingRefs.setFloating(node);
20
+ presenceRef(node);
21
+ if (typeof consumerRef === "function") {
22
+ consumerRef(node);
23
+ } else if (consumerRef && "current" in consumerRef) {
24
+ (consumerRef as WritableRefObject<T | null>).current = node;
25
+ }
26
+ },
27
+ [floatingRefs, presenceRef, consumerRef]
28
+ );
29
+ }
@@ -0,0 +1,127 @@
1
+ import { type RefCallback, useCallback, useEffect, useRef, useState } from "react";
2
+
3
+ // =============================================================================
4
+ // Types
5
+ // =============================================================================
6
+
7
+ type PresenceState = "hidden" | "entering" | "visible" | "exiting";
8
+
9
+ type UsePresenceReturn = {
10
+ /** Ref callback to attach to the animated element */
11
+ ref: RefCallback<HTMLElement>;
12
+ /** Current presence state */
13
+ state: PresenceState;
14
+ /** Whether the component should be mounted in the DOM */
15
+ isMounted: boolean;
16
+ /** Whether the component is visible (entering or fully visible) */
17
+ isVisible: boolean;
18
+ /** Call to start showing the component */
19
+ show: () => void;
20
+ /** Call to start hiding the component, with optional callback after animation */
21
+ hide: (onComplete?: () => void) => void;
22
+ };
23
+
24
+ // =============================================================================
25
+ // Hook
26
+ // =============================================================================
27
+
28
+ /**
29
+ * Manages mount/unmount presence with CSS animation support.
30
+ *
31
+ * Coordinates component lifecycle with CSS @keyframes animations by listening
32
+ * to `animationend` events. This is more robust than setTimeout because:
33
+ * 1. The browser tells us when the animation actually ends
34
+ * 2. No need to keep JS duration in sync with CSS duration
35
+ * 3. Works correctly even if animations are interrupted
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * function Modal({ open, onClose }) {
40
+ * const { ref, state, isMounted, show, hide } = usePresence();
41
+ *
42
+ * useEffect(() => {
43
+ * if (open) show();
44
+ * }, [open, show]);
45
+ *
46
+ * if (!isMounted) return null;
47
+ *
48
+ * return (
49
+ * <div
50
+ * ref={ref}
51
+ * data-state={state}
52
+ * className={cn(
53
+ * state === "entering" && "animate-fade-in",
54
+ * state === "exiting" && "animate-fade-out",
55
+ * )}
56
+ * >
57
+ * ...
58
+ * </div>
59
+ * );
60
+ * }
61
+ * ```
62
+ */
63
+ export function usePresence(): UsePresenceReturn {
64
+ const [state, setState] = useState<PresenceState>("hidden");
65
+ const nodeRef = useRef<HTMLElement | null>(null);
66
+ const onExitCompleteRef = useRef<(() => void) | null>(null);
67
+
68
+ // Track if we were exiting to know when to call the callback
69
+ const wasExitingRef = useRef(false);
70
+
71
+ // Handle animation end events
72
+ const handleAnimationEnd = useCallback(() => {
73
+ setState((current) => {
74
+ if (current === "entering") return "visible";
75
+ if (current === "exiting") return "hidden";
76
+ return current;
77
+ });
78
+ }, []);
79
+
80
+ // Call exit callback after state transitions to hidden (after render completes)
81
+ // This avoids the "Cannot update a component while rendering" warning
82
+ useEffect(() => {
83
+ if (state === "exiting") {
84
+ wasExitingRef.current = true;
85
+ } else if (state === "hidden" && wasExitingRef.current) {
86
+ wasExitingRef.current = false;
87
+ const callback = onExitCompleteRef.current;
88
+ onExitCompleteRef.current = null;
89
+ callback?.();
90
+ }
91
+ }, [state]);
92
+
93
+ // Ref callback to manage event listeners
94
+ const ref: RefCallback<HTMLElement> = useCallback(
95
+ (node) => {
96
+ // Cleanup old node
97
+ if (nodeRef.current) {
98
+ nodeRef.current.removeEventListener("animationend", handleAnimationEnd);
99
+ }
100
+
101
+ // Setup new node
102
+ nodeRef.current = node;
103
+ if (node) {
104
+ node.addEventListener("animationend", handleAnimationEnd);
105
+ }
106
+ },
107
+ [handleAnimationEnd]
108
+ );
109
+
110
+ const show = useCallback(() => {
111
+ setState("entering");
112
+ }, []);
113
+
114
+ const hide = useCallback((onComplete?: () => void) => {
115
+ onExitCompleteRef.current = onComplete ?? null;
116
+ setState("exiting");
117
+ }, []);
118
+
119
+ return {
120
+ ref,
121
+ state,
122
+ isMounted: state !== "hidden",
123
+ isVisible: state === "visible" || state === "entering",
124
+ show,
125
+ hide,
126
+ };
127
+ }