@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.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/styles.css +2 -0
- package/dist/theme.generated.css +438 -0
- package/package.json +238 -0
- package/src/components/Avatar/Avatar.tsx +19 -0
- package/src/components/Avatar/index.ts +1 -0
- package/src/components/Badge/Badge.tsx +59 -0
- package/src/components/Badge/index.ts +1 -0
- package/src/components/BarChart/BarChart.tsx +51 -0
- package/src/components/BarChart/index.ts +1 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +47 -0
- package/src/components/Breadcrumb/index.ts +5 -0
- package/src/components/Button/Button.tsx +157 -0
- package/src/components/Button/index.ts +1 -0
- package/src/components/Calendar/Calendar.tsx +433 -0
- package/src/components/Calendar/calendar.hooks.ts +329 -0
- package/src/components/Calendar/calendar.types.ts +117 -0
- package/src/components/Calendar/index.ts +28 -0
- package/src/components/Card/Card.tsx +47 -0
- package/src/components/Card/index.ts +1 -0
- package/src/components/Checkbox/Checkbox.tsx +144 -0
- package/src/components/Checkbox/index.ts +1 -0
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +173 -0
- package/src/components/CheckboxGroup/index.ts +5 -0
- package/src/components/Code/Editor/Editor.tsx +161 -0
- package/src/components/Code/Editor/index.ts +2 -0
- package/src/components/Code/Editor/semanticTokenProvider.ts +69 -0
- package/src/components/Code/Editor/themes.ts +112 -0
- package/src/components/Code/ValidationStatus/ValidationStatus.tsx +81 -0
- package/src/components/Code/ValidationStatus/index.ts +1 -0
- package/src/components/Code/Workspace/StatusBar.tsx +47 -0
- package/src/components/Code/Workspace/Toolbar.tsx +47 -0
- package/src/components/Code/Workspace/Workspace.tsx +109 -0
- package/src/components/Code/Workspace/index.ts +3 -0
- package/src/components/ColorDot/ColorDot.tsx +50 -0
- package/src/components/ColorDot/index.ts +1 -0
- package/src/components/DatePicker/DatePicker.tsx +236 -0
- package/src/components/DatePicker/datepicker.hooks.ts +381 -0
- package/src/components/DatePicker/datepicker.types.ts +140 -0
- package/src/components/DatePicker/index.ts +12 -0
- package/src/components/DateRangePicker/DateRangePicker.tsx +127 -0
- package/src/components/DateRangePicker/daterangepicker.types.ts +53 -0
- package/src/components/DateRangePicker/index.ts +2 -0
- package/src/components/DateTimePicker/DateTimePicker.tsx +399 -0
- package/src/components/DateTimePicker/datetimepicker.types.ts +48 -0
- package/src/components/DateTimePicker/index.ts +2 -0
- package/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +679 -0
- package/src/components/DateTimeRangePicker/datetimerangepicker.types.ts +62 -0
- package/src/components/DateTimeRangePicker/index.ts +2 -0
- package/src/components/EmptyState/EmptyState.tsx +29 -0
- package/src/components/EmptyState/index.ts +1 -0
- package/src/components/Field/Field.tsx +60 -0
- package/src/components/Field/index.ts +1 -0
- package/src/components/FormField/FormField.tsx +55 -0
- package/src/components/FormField/index.ts +2 -0
- package/src/components/HeatMap/HeatMapCalendar.tsx +127 -0
- package/src/components/HeatMap/HeatMapMatrix.tsx +75 -0
- package/src/components/HeatMap/heat-map.utils.ts +15 -0
- package/src/components/HeatMap/index.ts +3 -0
- package/src/components/HoverCard/HoverCard.tsx +83 -0
- package/src/components/HoverCard/components/HoverCardContent.tsx +30 -0
- package/src/components/HoverCard/components/HoverCardContext.tsx +22 -0
- package/src/components/HoverCard/components/HoverCardTrigger.tsx +32 -0
- package/src/components/HoverCard/index.ts +3 -0
- package/src/components/Input/Input.tsx +48 -0
- package/src/components/Input/index.ts +1 -0
- package/src/components/LineChart/LineChart.tsx +251 -0
- package/src/components/LineChart/index.ts +1 -0
- package/src/components/Markdown/Markdown.tsx +40 -0
- package/src/components/Markdown/index.ts +1 -0
- package/src/components/Message/Message.tsx +59 -0
- package/src/components/Message/components/MessageBody.tsx +9 -0
- package/src/components/Message/components/MessageDescription.tsx +9 -0
- package/src/components/Message/components/MessageIcon.tsx +13 -0
- package/src/components/Message/components/MessageTitle.tsx +9 -0
- package/src/components/Message/index.ts +10 -0
- package/src/components/MetricList/MetricList.tsx +13 -0
- package/src/components/MetricList/MetricRow.tsx +30 -0
- package/src/components/MetricList/index.ts +2 -0
- package/src/components/Modal/Modal.tsx +146 -0
- package/src/components/Modal/components/ModalBody.tsx +9 -0
- package/src/components/Modal/components/ModalContext.tsx +27 -0
- package/src/components/Modal/components/ModalFooter.tsx +9 -0
- package/src/components/Modal/components/ModalHeader.tsx +29 -0
- package/src/components/Modal/components/index.ts +9 -0
- package/src/components/Modal/index.ts +13 -0
- package/src/components/NavBottom/NavBottom.tsx +53 -0
- package/src/components/NavBottom/index.ts +3 -0
- package/src/components/NavSide/NavItem.tsx +39 -0
- package/src/components/NavSide/NavSide.tsx +40 -0
- package/src/components/NavSide/index.ts +2 -0
- package/src/components/NavSide/types.ts +36 -0
- package/src/components/Pagination/Pagination.tsx +86 -0
- package/src/components/Pagination/index.ts +2 -0
- package/src/components/Popover/Popover.tsx +68 -0
- package/src/components/Popover/components/PopoverBody.tsx +32 -0
- package/src/components/Popover/components/PopoverClose.tsx +26 -0
- package/src/components/Popover/components/PopoverContext.tsx +24 -0
- package/src/components/Popover/components/PopoverTrigger.tsx +31 -0
- package/src/components/Popover/index.ts +10 -0
- package/src/components/ProgressBar/ProgressBar.tsx +42 -0
- package/src/components/ProgressBar/index.ts +1 -0
- package/src/components/RadioButton/RadioButton.tsx +89 -0
- package/src/components/RadioButton/index.ts +1 -0
- package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +193 -0
- package/src/components/RadioButtonGroup/index.ts +5 -0
- package/src/components/SearchInput/SearchInput.tsx +45 -0
- package/src/components/SearchInput/index.ts +2 -0
- package/src/components/Select/Select.tsx +289 -0
- package/src/components/Select/SelectDropdown.tsx +90 -0
- package/src/components/Select/SelectTrigger.tsx +130 -0
- package/src/components/Select/index.ts +3 -0
- package/src/components/Select/useSelectKeyboard.ts +111 -0
- package/src/components/Separator/Separator.tsx +20 -0
- package/src/components/Separator/index.ts +1 -0
- package/src/components/Sheet/Sheet.tsx +136 -0
- package/src/components/Sheet/components/SheetBody.tsx +9 -0
- package/src/components/Sheet/components/SheetContext.tsx +23 -0
- package/src/components/Sheet/components/SheetFooter.tsx +9 -0
- package/src/components/Sheet/components/SheetHeader.tsx +29 -0
- package/src/components/Sheet/components/index.ts +9 -0
- package/src/components/Sheet/index.ts +13 -0
- package/src/components/Skeleton/Skeleton.tsx +7 -0
- package/src/components/Skeleton/index.ts +1 -0
- package/src/components/Sparkline/Sparkline.tsx +93 -0
- package/src/components/Sparkline/index.ts +1 -0
- package/src/components/Spinner/Spinner.tsx +41 -0
- package/src/components/Spinner/index.ts +1 -0
- package/src/components/StatsCard/StatsCard.tsx +40 -0
- package/src/components/StatsCard/index.ts +1 -0
- package/src/components/Switch/Switch.tsx +128 -0
- package/src/components/Switch/index.ts +1 -0
- package/src/components/Tabs/Tabs.tsx +49 -0
- package/src/components/Tabs/components/TabsContext.tsx +35 -0
- package/src/components/Tabs/components/TabsList.tsx +98 -0
- package/src/components/Tabs/components/TabsPanels.tsx +26 -0
- package/src/components/Tabs/components/TabsTrigger.tsx +69 -0
- package/src/components/Tabs/index.ts +3 -0
- package/src/components/TagChip/TagChip.tsx +46 -0
- package/src/components/TagChip/index.ts +1 -0
- package/src/components/TagInput/TagInput.tsx +60 -0
- package/src/components/TagInput/index.ts +1 -0
- package/src/components/Text/Text.tsx +35 -0
- package/src/components/Text/index.ts +1 -0
- package/src/components/TextInline/TextInline.tsx +35 -0
- package/src/components/TextInline/index.ts +1 -0
- package/src/components/Textarea/Textarea.tsx +20 -0
- package/src/components/Textarea/index.ts +1 -0
- package/src/components/TimePicker/PeriodSelector.tsx +28 -0
- package/src/components/TimePicker/TimeColumn.tsx +62 -0
- package/src/components/TimePicker/TimePicker.tsx +323 -0
- package/src/components/TimePicker/index.ts +7 -0
- package/src/components/TimePicker/timepicker.hooks.ts +173 -0
- package/src/components/TimePicker/timepicker.types.ts +101 -0
- package/src/components/Toast/Toast.tsx +111 -0
- package/src/components/Toast/index.ts +1 -0
- package/src/components/Toggle/Toggle.tsx +72 -0
- package/src/components/Toggle/index.ts +1 -0
- package/src/components/ToggleGroup/ToggleGroup.tsx +14 -0
- package/src/components/ToggleGroup/ToggleGroupContainer.tsx +19 -0
- package/src/components/ToggleGroup/ToggleGroupItem.tsx +51 -0
- package/src/components/ToggleGroup/ToggleGroupMultiple.tsx +61 -0
- package/src/components/ToggleGroup/ToggleGroupSingle.tsx +105 -0
- package/src/components/ToggleGroup/index.ts +2 -0
- package/src/components/ToggleGroup/toggleGroup.types.ts +55 -0
- package/src/components/Tooltip/Tooltip.tsx +66 -0
- package/src/components/Tooltip/components/TooltipBody.tsx +30 -0
- package/src/components/Tooltip/components/TooltipContext.tsx +22 -0
- package/src/components/Tooltip/components/TooltipTrigger.tsx +27 -0
- package/src/components/Tooltip/index.ts +9 -0
- package/src/date-time/arithmetic.ts +101 -0
- package/src/date-time/boundaries.ts +91 -0
- package/src/date-time/compare.ts +85 -0
- package/src/date-time/duration.ts +63 -0
- package/src/date-time/elapsed.ts +18 -0
- package/src/date-time/format.ts +722 -0
- package/src/date-time/index.ts +279 -0
- package/src/date-time/now.ts +70 -0
- package/src/date-time/parse.ts +170 -0
- package/src/date-time/serialize.ts +90 -0
- package/src/date-time/timezone.ts +47 -0
- package/src/date-time/validate.ts +278 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useCollapsible.ts +16 -0
- package/src/hooks/useControllableState.ts +54 -0
- package/src/hooks/useDebouncedCallback.ts +21 -0
- package/src/hooks/useDebouncedState.ts +20 -0
- package/src/hooks/useDeviceFormFactor.ts +49 -0
- package/src/hooks/useLocalStorage.ts +67 -0
- package/src/hooks/useMergedFloatingRef.ts +29 -0
- package/src/hooks/usePresence.ts +127 -0
- package/src/hooks/useStack.ts +99 -0
- package/src/hooks/useTheme.ts +105 -0
- package/src/internal/Slot.tsx +79 -0
- package/src/internal/index.ts +2 -0
- package/src/internal/rovingFocus.ts +39 -0
- package/src/internal/types.ts +10 -0
- package/src/lib/EmptyBoundary.tsx +12 -0
- package/src/lib/ErrorBoundary.tsx +48 -0
- package/src/lib/contexts/ComponentsContext/ComponentsContext.tsx +34 -0
- package/src/lib/contexts/ComponentsContext/index.ts +1 -0
- package/src/lib/contexts/OverlayContext/OverlayContext.tsx +93 -0
- package/src/lib/contexts/OverlayContext/hooks/useModal.tsx +125 -0
- package/src/lib/contexts/OverlayContext/hooks/useToast.ts +80 -0
- package/src/lib/contexts/OverlayContext/index.ts +6 -0
- package/src/lib/format.ts +51 -0
- package/src/lib/utils.ts +10 -0
- package/src/lib/withEmpty.tsx +14 -0
- package/src/lib/withErrorBoundary.tsx +16 -0
- package/src/lib/withSuspense.tsx +16 -0
- package/src/lib/withSuspenseAndErrorBoundary.tsx +26 -0
- package/src/styles/base.css +41 -0
- package/src/theme/index.ts +37 -0
- package/src/theme/palette.json +100 -0
- package/src/theme/palette.schema.json +40 -0
- package/src/theme/schema.ts +122 -0
- package/src/theme/theme.json +349 -0
- package/src/theme/theme.schema.json +876 -0
- package/src/theme/typography.json +7 -0
- package/src/theme/typography.schema.json +36 -0
- package/src/typography/Body.tsx +8 -0
- package/src/typography/Caption.tsx +8 -0
- package/src/typography/Code.tsx +8 -0
- package/src/typography/Display.tsx +8 -0
- package/src/typography/Emphasis.tsx +8 -0
- package/src/typography/Heading.tsx +23 -0
- package/src/typography/Label.tsx +10 -0
- package/src/typography/Lead.tsx +8 -0
- package/src/typography/Mono.tsx +8 -0
- package/src/typography/Muted.tsx +8 -0
- package/src/typography/SectionTitle.tsx +8 -0
- package/src/typography/Subtitle.tsx +8 -0
- package/src/typography/Timestamp.tsx +8 -0
- package/src/typography/index.ts +10 -0
- 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
|
+
}
|