@suphark/ui 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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/components.json +21 -0
- package/package.json +108 -0
- package/scripts/audit-base-ui-props.mjs +179 -0
- package/scripts/audit-tailwind-typography.mjs +100 -0
- package/src/components/design-system/design-system-showcase.tsx +138 -0
- package/src/components/design-system/sections/showcase-data-display.tsx +454 -0
- package/src/components/design-system/sections/showcase-feedback.tsx +273 -0
- package/src/components/design-system/sections/showcase-forms.tsx +373 -0
- package/src/components/design-system/sections/showcase-foundations.tsx +384 -0
- package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +397 -0
- package/src/components/design-system/sections/showcase-overlays-navigation.tsx +479 -0
- package/src/components/design-system/sections/showcase-overview.tsx +368 -0
- package/src/components/design-system/sections/showcase-primitives.tsx +463 -0
- package/src/components/shared/__tests__/multi-select-combobox.test.tsx +180 -0
- package/src/components/shared/__tests__/name-id-fields.test.ts +24 -0
- package/src/components/shared/button/add-new-button.tsx +19 -0
- package/src/components/shared/button/base/action-button.tsx +134 -0
- package/src/components/shared/button/base/link-button.tsx +130 -0
- package/src/components/shared/button/cancel-button.tsx +27 -0
- package/src/components/shared/button/confirm-button.tsx +26 -0
- package/src/components/shared/button/delete-button.tsx +36 -0
- package/src/components/shared/button/floating-save-button.tsx +89 -0
- package/src/components/shared/button/index.ts +13 -0
- package/src/components/shared/button/print-button.tsx +67 -0
- package/src/components/shared/button/refresh-button.tsx +63 -0
- package/src/components/shared/button/save-button.tsx +29 -0
- package/src/components/shared/button/submit-button.tsx +58 -0
- package/src/components/shared/client-redirect.tsx +11 -0
- package/src/components/shared/combobox.tsx +116 -0
- package/src/components/shared/data-list.tsx +65 -0
- package/src/components/shared/date/date-range-picker.tsx +106 -0
- package/src/components/shared/description-list.tsx +27 -0
- package/src/components/shared/empty-row.tsx +13 -0
- package/src/components/shared/empty-state.tsx +31 -0
- package/src/components/shared/env-snippet.tsx +62 -0
- package/src/components/shared/form/__tests__/simple-select.test.tsx +72 -0
- package/src/components/shared/form/form-field-boolean.tsx +93 -0
- package/src/components/shared/form/form-field-color-select.tsx +86 -0
- package/src/components/shared/form/form-field-combobox.tsx +167 -0
- package/src/components/shared/form/form-field-date.tsx +55 -0
- package/src/components/shared/form/form-field-formatted-number.tsx +103 -0
- package/src/components/shared/form/form-field-input.tsx +372 -0
- package/src/components/shared/form/form-field-radio-cards.tsx +127 -0
- package/src/components/shared/form/form-field-radio-group.tsx +56 -0
- package/src/components/shared/form/form-field-select.tsx +312 -0
- package/src/components/shared/form/simple-select.tsx +152 -0
- package/src/components/shared/form-dialog.tsx +109 -0
- package/src/components/shared/form-message.tsx +30 -0
- package/src/components/shared/initials.tsx +33 -0
- package/src/components/shared/layout/error-state.tsx +160 -0
- package/src/components/shared/layout/page-shell.tsx +57 -0
- package/src/components/shared/link/transition-link.tsx +72 -0
- package/src/components/shared/multi-select-combobox.tsx +392 -0
- package/src/components/shared/name-id-fields.tsx +133 -0
- package/src/components/shared/navigation/page-breadcrumb.tsx +100 -0
- package/src/components/shared/notice.tsx +35 -0
- package/src/components/shared/number/formatted-number.tsx +133 -0
- package/src/components/shared/page-header.tsx +25 -0
- package/src/components/shared/page-skeleton.tsx +18 -0
- package/src/components/shared/row-actions.tsx +93 -0
- package/src/components/shared/row-menu.tsx +55 -0
- package/src/components/shared/secret-reveal.tsx +53 -0
- package/src/components/shared/section-card.tsx +44 -0
- package/src/components/shared/stat.tsx +26 -0
- package/src/components/shared/status-badge.tsx +47 -0
- package/src/components/shared/text/primary-secondary-text.tsx +186 -0
- package/src/components/shared/theme/index.ts +4 -0
- package/src/components/shared/theme/theme-menu-items.tsx +43 -0
- package/src/components/shared/theme/theme-provider.tsx +39 -0
- package/src/components/shared/theme/theme-switch.tsx +36 -0
- package/src/components/shared/theme/theme-toggle.tsx +76 -0
- package/src/components/shared/time/client-date-formatter.tsx +188 -0
- package/src/components/shared/ui-text.tsx +47 -0
- package/src/components/ui/__tests__/combobox.test.tsx +40 -0
- package/src/components/ui/__tests__/date-picker.test.tsx +53 -0
- package/src/components/ui/__tests__/slider.test.tsx +18 -0
- package/src/components/ui/__tests__/tabs.test.tsx +33 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +161 -0
- package/src/components/ui/alert.tsx +68 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +194 -0
- package/src/components/ui/avatar.tsx +92 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/breadcrumb.tsx +102 -0
- package/src/components/ui/button-group.tsx +77 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/calendar.tsx +184 -0
- package/src/components/ui/card.tsx +87 -0
- package/src/components/ui/carousel.tsx +230 -0
- package/src/components/ui/chart.tsx +338 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +17 -0
- package/src/components/ui/combobox.tsx +272 -0
- package/src/components/ui/command.tsx +180 -0
- package/src/components/ui/context-menu.tsx +244 -0
- package/src/components/ui/date-picker.tsx +308 -0
- package/src/components/ui/dialog.tsx +137 -0
- package/src/components/ui/drawer.tsx +208 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/empty.tsx +93 -0
- package/src/components/ui/field.tsx +223 -0
- package/src/components/ui/form.tsx +161 -0
- package/src/components/ui/hover-card.tsx +46 -0
- package/src/components/ui/input-group.tsx +145 -0
- package/src/components/ui/input-otp.tsx +85 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/item.tsx +187 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/menubar.tsx +266 -0
- package/src/components/ui/navigation-menu.tsx +160 -0
- package/src/components/ui/pagination.tsx +117 -0
- package/src/components/ui/popover.tsx +76 -0
- package/src/components/ui/progress.tsx +66 -0
- package/src/components/ui/radio-group.tsx +38 -0
- package/src/components/ui/relative-date.tsx +40 -0
- package/src/components/ui/resizable.tsx +41 -0
- package/src/components/ui/scroll-area.tsx +67 -0
- package/src/components/ui/searchable-select.tsx +124 -0
- package/src/components/ui/select.tsx +189 -0
- package/src/components/ui/separator.tsx +21 -0
- package/src/components/ui/sheet.tsx +124 -0
- package/src/components/ui/sidebar.tsx +691 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +55 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +95 -0
- package/src/components/ui/tabs.tsx +79 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toggle-group.tsx +86 -0
- package/src/components/ui/toggle.tsx +44 -0
- package/src/components/ui/tooltip.tsx +54 -0
- package/src/hooks/use-copy-to-clipboard.ts +27 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/index.ts +156 -0
- package/src/lib/formatters.ts +44 -0
- package/src/lib/permission.ts +75 -0
- package/src/lib/status-colors.ts +52 -0
- package/src/lib/utils.ts +6 -0
- package/src/next.ts +15 -0
- package/src/styles.css +214 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@suphark/ui/components/ui/button";
|
|
4
|
+
import { Calendar } from "@suphark/ui/components/ui/calendar";
|
|
5
|
+
import {
|
|
6
|
+
InputGroup,
|
|
7
|
+
InputGroupAddon,
|
|
8
|
+
InputGroupButton,
|
|
9
|
+
InputGroupInput,
|
|
10
|
+
} from "@suphark/ui/components/ui/input-group";
|
|
11
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@suphark/ui/components/ui/popover";
|
|
12
|
+
import { formatRelativeThaiDate, formatThaiFullDate } from "@suphark/ui/lib/formatters";
|
|
13
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
14
|
+
import { addDays, addMonths, addWeeks, subDays, subMonths, subWeeks } from "date-fns";
|
|
15
|
+
import { CalendarIcon, Check, Clock, X } from "lucide-react";
|
|
16
|
+
import { useMemo, useState } from "react";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Smart date parser supporting intuitive Thai & international date inputs:
|
|
20
|
+
* - "1/7" -> 1 July of current year
|
|
21
|
+
* - "1/7/25" -> 1 July 2025 CE
|
|
22
|
+
* - "1/7/68" -> 1 July 2025 CE (BE 2568)
|
|
23
|
+
* - "1/7/2568" -> 1 July 2025 CE (BE 2568)
|
|
24
|
+
*/
|
|
25
|
+
export function parseSmartDateInput(input: string): Date | null {
|
|
26
|
+
if (!input?.trim()) return null;
|
|
27
|
+
const clean = input.trim().replace(/\s+/g, "");
|
|
28
|
+
|
|
29
|
+
const parts = clean.split(/[/\-.]/);
|
|
30
|
+
if (parts.length < 2 || parts.length > 3) return null;
|
|
31
|
+
|
|
32
|
+
const day = parseInt(parts[0] ?? "", 10);
|
|
33
|
+
const month = parseInt(parts[1] ?? "", 10);
|
|
34
|
+
if (Number.isNaN(day) || Number.isNaN(month) || day < 1 || day > 31 || month < 1 || month > 12) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const currentYear = new Date().getFullYear();
|
|
39
|
+
let year = currentYear;
|
|
40
|
+
|
|
41
|
+
if (parts.length === 3 && parts[2]) {
|
|
42
|
+
const rawYear = parseInt(parts[2], 10);
|
|
43
|
+
if (Number.isNaN(rawYear)) return null;
|
|
44
|
+
|
|
45
|
+
if (rawYear > 2400) {
|
|
46
|
+
// Full Buddhist Era year (e.g. 2568 -> 2025 CE)
|
|
47
|
+
year = rawYear - 543;
|
|
48
|
+
} else if (rawYear >= 50 && rawYear < 100) {
|
|
49
|
+
// Short BE year (e.g. 68 -> 2568 -> 2025 CE)
|
|
50
|
+
year = 2500 + rawYear - 543;
|
|
51
|
+
} else if (rawYear < 50) {
|
|
52
|
+
// Short AD year (e.g. 25 -> 2025 CE)
|
|
53
|
+
year = 2000 + rawYear;
|
|
54
|
+
} else {
|
|
55
|
+
year = rawYear;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const d = new Date(year, month - 1, day);
|
|
60
|
+
if (d.getFullYear() === year && d.getMonth() === month - 1 && d.getDate() === day) {
|
|
61
|
+
return d;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface DatePickerProps {
|
|
68
|
+
id?: string;
|
|
69
|
+
value: Date | string | number | null | undefined;
|
|
70
|
+
onChange: (date: Date | null) => void;
|
|
71
|
+
placeholder?: string;
|
|
72
|
+
disabled?: boolean;
|
|
73
|
+
className?: string;
|
|
74
|
+
allowClear?: boolean;
|
|
75
|
+
showPresets?: boolean;
|
|
76
|
+
showSmartInput?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function DatePicker({
|
|
80
|
+
id,
|
|
81
|
+
value,
|
|
82
|
+
onChange,
|
|
83
|
+
placeholder = "เลือกวันที่อ้างอิงราคา...",
|
|
84
|
+
disabled = false,
|
|
85
|
+
className,
|
|
86
|
+
allowClear = true,
|
|
87
|
+
showPresets = true,
|
|
88
|
+
showSmartInput = true,
|
|
89
|
+
}: DatePickerProps) {
|
|
90
|
+
const [open, setOpen] = useState(false);
|
|
91
|
+
const [smartText, setSmartText] = useState("");
|
|
92
|
+
|
|
93
|
+
const selectedDate = value ? new Date(value) : undefined;
|
|
94
|
+
const isValidDate = selectedDate && !Number.isNaN(selectedDate.getTime());
|
|
95
|
+
|
|
96
|
+
const parsedSmartDate = useMemo(() => parseSmartDateInput(smartText), [smartText]);
|
|
97
|
+
|
|
98
|
+
const handleSelectDate = (date: Date | undefined) => {
|
|
99
|
+
onChange(date || null);
|
|
100
|
+
setOpen(false);
|
|
101
|
+
setSmartText("");
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const applyPreset = (date: Date | null) => {
|
|
105
|
+
onChange(date);
|
|
106
|
+
setOpen(false);
|
|
107
|
+
setSmartText("");
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleSmartSubmit = () => {
|
|
111
|
+
if (parsedSmartDate) {
|
|
112
|
+
onChange(parsedSmartDate);
|
|
113
|
+
setOpen(false);
|
|
114
|
+
setSmartText("");
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const today = new Date();
|
|
119
|
+
const displayMonth = parsedSmartDate || (isValidDate ? selectedDate : undefined);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Popover
|
|
123
|
+
open={open}
|
|
124
|
+
onOpenChange={(o) => {
|
|
125
|
+
setOpen(o);
|
|
126
|
+
if (!o) setSmartText("");
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
<PopoverTrigger
|
|
130
|
+
render={
|
|
131
|
+
<Button
|
|
132
|
+
id={id}
|
|
133
|
+
variant="outline"
|
|
134
|
+
disabled={disabled}
|
|
135
|
+
className={cn(
|
|
136
|
+
"h-8 w-full justify-between bg-background pr-2 pl-3 text-left font-normal text-xs",
|
|
137
|
+
!isValidDate && "text-muted-foreground",
|
|
138
|
+
className,
|
|
139
|
+
)}
|
|
140
|
+
/>
|
|
141
|
+
}
|
|
142
|
+
>
|
|
143
|
+
<div className="flex items-center gap-2 truncate">
|
|
144
|
+
<CalendarIcon className="h-3.5 w-3.5 shrink-0 text-slate-500" />
|
|
145
|
+
<span className="truncate">
|
|
146
|
+
{isValidDate ? (
|
|
147
|
+
<>
|
|
148
|
+
<span className="font-medium text-foreground">
|
|
149
|
+
{formatThaiFullDate(selectedDate)}
|
|
150
|
+
</span>
|
|
151
|
+
<span className="ml-1 text-muted-foreground text-xs">
|
|
152
|
+
({formatRelativeThaiDate(selectedDate)})
|
|
153
|
+
</span>
|
|
154
|
+
</>
|
|
155
|
+
) : (
|
|
156
|
+
<span>{placeholder}</span>
|
|
157
|
+
)}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
{allowClear && isValidDate && !disabled && (
|
|
161
|
+
<span
|
|
162
|
+
role="button"
|
|
163
|
+
tabIndex={0}
|
|
164
|
+
className="ml-1 cursor-pointer rounded p-0.5 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-600"
|
|
165
|
+
onClick={(e) => {
|
|
166
|
+
e.stopPropagation();
|
|
167
|
+
onChange(null);
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
<X className="h-3.5 w-3.5" />
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
</PopoverTrigger>
|
|
174
|
+
<PopoverContent
|
|
175
|
+
className="w-[320px] border bg-background p-3 shadow-xl sm:w-[340px]"
|
|
176
|
+
align="start"
|
|
177
|
+
>
|
|
178
|
+
{/* Smart Date Text Input */}
|
|
179
|
+
{showSmartInput && (
|
|
180
|
+
<div className="mb-3 flex flex-col gap-1.5 border-b pb-2.5">
|
|
181
|
+
<InputGroup className="h-8">
|
|
182
|
+
<InputGroupInput
|
|
183
|
+
value={smartText}
|
|
184
|
+
onChange={(e) => setSmartText(e.target.value)}
|
|
185
|
+
onKeyDown={(event) => {
|
|
186
|
+
if (event.key === "Enter") {
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
event.stopPropagation();
|
|
189
|
+
handleSmartSubmit();
|
|
190
|
+
}
|
|
191
|
+
}}
|
|
192
|
+
placeholder="เช่น 1/7, 1/7/25, 1-7-2568..."
|
|
193
|
+
className="h-8 text-xs"
|
|
194
|
+
/>
|
|
195
|
+
<InputGroupAddon align="inline-end">
|
|
196
|
+
<InputGroupButton
|
|
197
|
+
type="submit"
|
|
198
|
+
variant="default"
|
|
199
|
+
size="xs"
|
|
200
|
+
onClick={handleSmartSubmit}
|
|
201
|
+
disabled={!parsedSmartDate}
|
|
202
|
+
>
|
|
203
|
+
ตกลง
|
|
204
|
+
</InputGroupButton>
|
|
205
|
+
</InputGroupAddon>
|
|
206
|
+
</InputGroup>
|
|
207
|
+
{parsedSmartDate && (
|
|
208
|
+
<span className="flex items-center gap-1 font-medium text-emerald-600 text-xs">
|
|
209
|
+
<Check className="size-3" />
|
|
210
|
+
{formatThaiFullDate(parsedSmartDate)}
|
|
211
|
+
</span>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{/* Quick Presets Section (Flex Wrap & Clean Spacing) */}
|
|
217
|
+
{showPresets && (
|
|
218
|
+
<div className="mb-3 border-b pb-2.5">
|
|
219
|
+
<div className="mb-1.5 flex items-center gap-1 font-semibold text-muted-foreground text-xs">
|
|
220
|
+
<Clock className="h-3 w-3" />
|
|
221
|
+
Quick Preset
|
|
222
|
+
</div>
|
|
223
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
224
|
+
<Button
|
|
225
|
+
type="button"
|
|
226
|
+
variant="outline"
|
|
227
|
+
size="sm"
|
|
228
|
+
className="h-6 px-2 font-normal text-xs hover:bg-slate-100"
|
|
229
|
+
onClick={() => applyPreset(today)}
|
|
230
|
+
>
|
|
231
|
+
Today
|
|
232
|
+
</Button>
|
|
233
|
+
<Button
|
|
234
|
+
type="button"
|
|
235
|
+
variant="outline"
|
|
236
|
+
size="sm"
|
|
237
|
+
className="h-6 px-2 font-normal text-xs hover:bg-slate-100"
|
|
238
|
+
onClick={() => applyPreset(subDays(today, 1))}
|
|
239
|
+
>
|
|
240
|
+
Yesterday
|
|
241
|
+
</Button>
|
|
242
|
+
<Button
|
|
243
|
+
type="button"
|
|
244
|
+
variant="outline"
|
|
245
|
+
size="sm"
|
|
246
|
+
className="h-6 px-2 font-normal text-xs hover:bg-slate-100"
|
|
247
|
+
onClick={() => applyPreset(addDays(today, 1))}
|
|
248
|
+
>
|
|
249
|
+
Tomorrow
|
|
250
|
+
</Button>
|
|
251
|
+
<Button
|
|
252
|
+
type="button"
|
|
253
|
+
variant="outline"
|
|
254
|
+
size="sm"
|
|
255
|
+
className="h-6 px-2 font-normal text-xs hover:bg-slate-100"
|
|
256
|
+
onClick={() => applyPreset(addWeeks(today, 1))}
|
|
257
|
+
>
|
|
258
|
+
+1 Week
|
|
259
|
+
</Button>
|
|
260
|
+
<Button
|
|
261
|
+
type="button"
|
|
262
|
+
variant="outline"
|
|
263
|
+
size="sm"
|
|
264
|
+
className="h-6 px-2 font-normal text-xs hover:bg-slate-100"
|
|
265
|
+
onClick={() => applyPreset(subWeeks(today, 1))}
|
|
266
|
+
>
|
|
267
|
+
-1 Week
|
|
268
|
+
</Button>
|
|
269
|
+
<Button
|
|
270
|
+
type="button"
|
|
271
|
+
variant="outline"
|
|
272
|
+
size="sm"
|
|
273
|
+
className="h-6 px-2 font-normal text-xs hover:bg-slate-100"
|
|
274
|
+
onClick={() => applyPreset(addMonths(today, 1))}
|
|
275
|
+
>
|
|
276
|
+
+1 Month
|
|
277
|
+
</Button>
|
|
278
|
+
<Button
|
|
279
|
+
type="button"
|
|
280
|
+
variant="outline"
|
|
281
|
+
size="sm"
|
|
282
|
+
className="h-6 px-2 font-normal text-xs hover:bg-slate-100"
|
|
283
|
+
onClick={() => applyPreset(subMonths(today, 1))}
|
|
284
|
+
>
|
|
285
|
+
-1 Month
|
|
286
|
+
</Button>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
|
|
291
|
+
{/* Visual DayPicker Calendar */}
|
|
292
|
+
<div className="flex w-full justify-center">
|
|
293
|
+
<Calendar
|
|
294
|
+
mode="single"
|
|
295
|
+
selected={isValidDate ? selectedDate : undefined}
|
|
296
|
+
onSelect={handleSelectDate}
|
|
297
|
+
disabled={disabled}
|
|
298
|
+
captionLayout="dropdown"
|
|
299
|
+
startMonth={new Date(1990, 0)}
|
|
300
|
+
endMonth={new Date(2040, 11)}
|
|
301
|
+
defaultMonth={displayMonth}
|
|
302
|
+
className="p-0"
|
|
303
|
+
/>
|
|
304
|
+
</div>
|
|
305
|
+
</PopoverContent>
|
|
306
|
+
</Popover>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
|
|
4
|
+
import { Button } from "@suphark/ui/components/ui/button";
|
|
5
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
6
|
+
import { XIcon } from "lucide-react";
|
|
7
|
+
import type * as React from "react";
|
|
8
|
+
|
|
9
|
+
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
|
10
|
+
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
|
|
14
|
+
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
|
|
18
|
+
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
|
|
22
|
+
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) {
|
|
26
|
+
return (
|
|
27
|
+
<DialogPrimitive.Backdrop
|
|
28
|
+
data-slot="dialog-overlay"
|
|
29
|
+
className={cn(
|
|
30
|
+
"data-open:fade-in-0 data-closed:fade-out-0 fixed inset-0 isolate z-50 bg-black/10 duration-100 data-closed:animate-out data-open:animate-in supports-backdrop-filter:backdrop-blur-xs",
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function DialogContent({
|
|
39
|
+
className,
|
|
40
|
+
children,
|
|
41
|
+
showCloseButton = true,
|
|
42
|
+
...props
|
|
43
|
+
}: DialogPrimitive.Popup.Props & {
|
|
44
|
+
showCloseButton?: boolean;
|
|
45
|
+
}) {
|
|
46
|
+
return (
|
|
47
|
+
<DialogPortal>
|
|
48
|
+
<DialogOverlay />
|
|
49
|
+
<DialogPrimitive.Popup
|
|
50
|
+
data-slot="dialog-content"
|
|
51
|
+
className={cn(
|
|
52
|
+
"data-open:fade-in-0 data-open:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground text-sm outline-none ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in sm:max-w-sm",
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
{showCloseButton && (
|
|
59
|
+
<DialogPrimitive.Close
|
|
60
|
+
data-slot="dialog-close"
|
|
61
|
+
render={<Button variant="ghost" className="absolute top-2 right-2" size="icon-sm" />}
|
|
62
|
+
>
|
|
63
|
+
<XIcon />
|
|
64
|
+
<span className="sr-only">Close</span>
|
|
65
|
+
</DialogPrimitive.Close>
|
|
66
|
+
)}
|
|
67
|
+
</DialogPrimitive.Popup>
|
|
68
|
+
</DialogPortal>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
73
|
+
return (
|
|
74
|
+
<div data-slot="dialog-header" className={cn("flex flex-col gap-2", className)} {...props} />
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function DialogFooter({
|
|
79
|
+
className,
|
|
80
|
+
showCloseButton = false,
|
|
81
|
+
children,
|
|
82
|
+
...props
|
|
83
|
+
}: React.ComponentProps<"div"> & {
|
|
84
|
+
showCloseButton?: boolean;
|
|
85
|
+
}) {
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
data-slot="dialog-footer"
|
|
89
|
+
className={cn(
|
|
90
|
+
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
|
|
91
|
+
className,
|
|
92
|
+
)}
|
|
93
|
+
{...props}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
{showCloseButton && (
|
|
97
|
+
<DialogPrimitive.Close render={<Button variant="outline" />}>Close</DialogPrimitive.Close>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
|
104
|
+
return (
|
|
105
|
+
<DialogPrimitive.Title
|
|
106
|
+
data-slot="dialog-title"
|
|
107
|
+
className={cn("font-medium text-base leading-none", className)}
|
|
108
|
+
{...props}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
|
|
114
|
+
return (
|
|
115
|
+
<DialogPrimitive.Description
|
|
116
|
+
data-slot="dialog-description"
|
|
117
|
+
className={cn(
|
|
118
|
+
"text-muted-foreground text-sm *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
|
119
|
+
className,
|
|
120
|
+
)}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
Dialog,
|
|
128
|
+
DialogClose,
|
|
129
|
+
DialogContent,
|
|
130
|
+
DialogDescription,
|
|
131
|
+
DialogFooter,
|
|
132
|
+
DialogHeader,
|
|
133
|
+
DialogOverlay,
|
|
134
|
+
DialogPortal,
|
|
135
|
+
DialogTitle,
|
|
136
|
+
DialogTrigger,
|
|
137
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Drawer as DrawerPrimitive } from "@base-ui/react/drawer";
|
|
4
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
type DrawerContextProps = {
|
|
8
|
+
hasSnapPoints: boolean;
|
|
9
|
+
modal: DrawerPrimitive.Root.Props["modal"];
|
|
10
|
+
showSwipeHandle: boolean;
|
|
11
|
+
swipeDirection: NonNullable<DrawerPrimitive.Root.Props["swipeDirection"]>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const DrawerContext = React.createContext<DrawerContextProps | null>(null);
|
|
15
|
+
|
|
16
|
+
function useDrawer() {
|
|
17
|
+
const context = React.useContext(DrawerContext);
|
|
18
|
+
|
|
19
|
+
if (!context) {
|
|
20
|
+
throw new Error("useDrawer must be used within a Drawer.");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return context;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function Drawer({
|
|
27
|
+
modal = true,
|
|
28
|
+
showSwipeHandle = false,
|
|
29
|
+
snapPoints,
|
|
30
|
+
swipeDirection = "down",
|
|
31
|
+
...props
|
|
32
|
+
}: DrawerPrimitive.Root.Props & {
|
|
33
|
+
showSwipeHandle?: boolean;
|
|
34
|
+
}) {
|
|
35
|
+
const hasSnapPoints = snapPoints != null && snapPoints.length > 0;
|
|
36
|
+
const contextValue = React.useMemo(
|
|
37
|
+
() => ({ hasSnapPoints, modal, showSwipeHandle, swipeDirection }),
|
|
38
|
+
[hasSnapPoints, modal, showSwipeHandle, swipeDirection],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<DrawerContext.Provider value={contextValue}>
|
|
43
|
+
<DrawerPrimitive.Root
|
|
44
|
+
data-slot="drawer"
|
|
45
|
+
modal={modal}
|
|
46
|
+
snapPoints={snapPoints}
|
|
47
|
+
swipeDirection={swipeDirection}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
</DrawerContext.Provider>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function DrawerTrigger({ ...props }: DrawerPrimitive.Trigger.Props) {
|
|
55
|
+
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function DrawerPortal({ ...props }: DrawerPrimitive.Portal.Props) {
|
|
59
|
+
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function DrawerClose({ ...props }: DrawerPrimitive.Close.Props) {
|
|
63
|
+
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function DrawerOverlay({ className, ...props }: DrawerPrimitive.Backdrop.Props) {
|
|
67
|
+
return (
|
|
68
|
+
<DrawerPrimitive.Backdrop
|
|
69
|
+
data-slot="drawer-overlay"
|
|
70
|
+
className={cn(
|
|
71
|
+
"fixed inset-0 z-50 min-h-dvh select-none bg-black/10 opacity-[max(var(--drawer-overlay-min-opacity,0),calc(1-var(--drawer-swipe-progress)))] transition-opacity duration-450 ease-[cubic-bezier(0.32,0.72,0,1)] data-ending-style:pointer-events-none data-ending-style:opacity-0 data-starting-style:opacity-0 data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-swiping:duration-0 supports-[-webkit-touch-callout:none]:absolute supports-backdrop-filter:backdrop-blur-xs data-snap-points:[--drawer-overlay-min-opacity:0.5]",
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function DrawerSwipeHandle({ className, ...props }: React.ComponentProps<"div">) {
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
data-slot="drawer-swipe-handle"
|
|
83
|
+
aria-hidden="true"
|
|
84
|
+
className={cn(
|
|
85
|
+
"relative z-10 flex shrink-0 cursor-grab transition-opacity duration-200 after:block after:shrink-0 after:rounded-full after:bg-muted active:cursor-grabbing group-data-[swipe-direction=left]/drawer-popup:order-last group-data-[swipe-direction=up]/drawer-popup:order-last group-data-[swipe-axis=x]/drawer-popup:h-full group-data-[swipe-axis=y]/drawer-popup:h-3 group-data-[swipe-axis=x]/drawer-popup:w-3 group-data-[swipe-axis=y]/drawer-popup:w-full group-data-[swipe-direction=up]/drawer-popup:items-start group-data-[swipe-direction=down]/drawer-popup:items-end group-data-[swipe-axis=x]/drawer-popup:items-center group-data-[swipe-direction=left]/drawer-popup:justify-start group-data-[swipe-direction=right]/drawer-popup:justify-end group-data-[swipe-axis=y]/drawer-popup:justify-center group-data-nested-drawer-open/drawer-popup:opacity-0 group-data-nested-drawer-swiping/drawer-popup:opacity-100 group-data-[swipe-axis=x]/drawer-popup:after:h-24 group-data-[swipe-axis=y]/drawer-popup:after:h-1 group-data-[swipe-axis=x]/drawer-popup:after:w-1 group-data-[swipe-axis=y]/drawer-popup:after:w-24",
|
|
86
|
+
className,
|
|
87
|
+
)}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function DrawerContent({ className, children, ...props }: DrawerPrimitive.Popup.Props) {
|
|
94
|
+
const { hasSnapPoints, modal, showSwipeHandle, swipeDirection } = useDrawer();
|
|
95
|
+
const swipeAxis = swipeDirection === "down" || swipeDirection === "up" ? "y" : "x";
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<DrawerPortal data-slot="drawer-portal">
|
|
99
|
+
{modal === true && <DrawerOverlay data-snap-points={hasSnapPoints ? "" : undefined} />}
|
|
100
|
+
<DrawerPrimitive.Viewport
|
|
101
|
+
data-slot="drawer-viewport"
|
|
102
|
+
data-modal={modal}
|
|
103
|
+
className="pointer-events-none fixed inset-0 z-50 select-none data-[modal=true]:pointer-events-auto"
|
|
104
|
+
>
|
|
105
|
+
<DrawerPrimitive.Popup
|
|
106
|
+
data-slot="drawer-popup"
|
|
107
|
+
data-swipe-axis={swipeAxis}
|
|
108
|
+
data-snap-points={hasSnapPoints ? "" : undefined}
|
|
109
|
+
className={cn(
|
|
110
|
+
// Base.
|
|
111
|
+
"group/drawer-popup transform-[translate3d(var(--translate-x,0px),var(--translate-y,0px),0)_scale(var(--stack-scale))] pointer-events-auto fixed z-50 m-(--drawer-inset,0px) flex h-(--drawer-content-height) max-h-(--drawer-content-max-height,none) min-h-0 w-(--drawer-content-width,auto) select-none flex-col bg-popover text-popover-foreground text-sm outline-none transition-[transform,height,opacity,filter] duration-450 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform [interpolate-size:allow-keywords] data-[swipe-direction=down]:rounded-t-xl data-[swipe-direction=left]:rounded-r-xl data-[swipe-direction=up]:rounded-b-xl data-[swipe-direction=right]:rounded-l-xl data-[swipe-direction=down]:border-t data-[swipe-direction=left]:border-r data-[swipe-direction=up]:border-b data-[swipe-direction=right]:border-l",
|
|
112
|
+
// Nested.
|
|
113
|
+
"data-nested-drawer-open:overflow-hidden data-nested-drawer-open:brightness-95",
|
|
114
|
+
// Bleed.
|
|
115
|
+
"after:pointer-events-none after:absolute after:bg-(--drawer-bleed-background,var(--color-popover)) data-[swipe-axis=y]:after:inset-x-0 data-[swipe-axis=x]:after:inset-y-0 data-[swipe-direction=down]:after:top-full data-[swipe-direction=left]:after:right-full data-[swipe-direction=up]:after:bottom-full data-[swipe-direction=right]:after:left-full data-[swipe-axis=y]:after:h-(--bleed) data-[swipe-axis=x]:after:w-(--bleed)",
|
|
116
|
+
// Sizing.
|
|
117
|
+
"[--drawer-content-height:var(--drawer-height,auto)] data-[swipe-axis=y]:data-snap-points:[--drawer-content-height:100dvh] data-[swipe-axis=x]:[--drawer-content-width:75%] data-[swipe-axis=y]:[--drawer-content-max-height:calc(100dvh-6rem)] data-[swipe-axis=x]:sm:[--drawer-content-width:24rem]",
|
|
118
|
+
// Stack.
|
|
119
|
+
"[--bleed:3rem] [--peek:1rem] [--stack-height:var(--drawer-frontmost-height,var(--drawer-height,0px))] [--stack-peek-offset:max(0px,calc((var(--nested-drawers)-var(--stack-progress))*var(--peek)))] [--stack-progress:clamp(0,var(--drawer-swipe-progress),1)] [--stack-scale-base:max(0,calc(1-(var(--nested-drawers)*var(--stack-step))))] [--stack-scale:clamp(0,calc(var(--stack-scale-base)+(var(--stack-step)*var(--stack-progress))),1)] [--stack-shrink:calc(1-var(--stack-scale))] [--stack-step:0.05]",
|
|
120
|
+
// Transitions.
|
|
121
|
+
"data-ending-style:transform-(--closed-transform) data-starting-style:transform-(--closed-transform) data-ending-style:data-nested-drawer-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-ending-style:data-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-ending-style:opacity-[0.9999] data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-nested-drawer-swiping:duration-0 data-swiping:duration-0",
|
|
122
|
+
// Axis: y.
|
|
123
|
+
"data-[swipe-axis=y]:data-nested-drawer-open:h-(--stack-height) data-[swipe-axis=y]:inset-x-0",
|
|
124
|
+
// Axis: x.
|
|
125
|
+
"data-[swipe-axis=x]:inset-y-0 data-[swipe-axis=x]:flex-row",
|
|
126
|
+
// Direction: down.
|
|
127
|
+
"data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:origin-bottom data-[swipe-direction=down]:[--closed-transform:translate3d(0,calc(100%+var(--drawer-inset,0px)+2px),0)] data-[swipe-direction=down]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)-var(--stack-peek-offset)-(var(--stack-shrink)*var(--stack-height)))]",
|
|
128
|
+
// Direction: up.
|
|
129
|
+
"data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:origin-top data-[swipe-direction=up]:[--closed-transform:translate3d(0,calc(-100%-var(--drawer-inset,0px)-2px),0)] data-[swipe-direction=up]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)+var(--stack-peek-offset)+(var(--stack-shrink)*var(--stack-height)))]",
|
|
130
|
+
// Direction: left.
|
|
131
|
+
"data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:origin-left data-[swipe-direction=left]:[--closed-transform:translate3d(calc(-100%-var(--drawer-inset,0px)-2px),0,0)] data-[swipe-direction=left]:[--translate-x:calc(var(--drawer-swipe-movement-x)+var(--stack-peek-offset)+(var(--stack-shrink)*100%))]",
|
|
132
|
+
// Direction: right.
|
|
133
|
+
"data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:origin-right data-[swipe-direction=right]:[--closed-transform:translate3d(calc(100%+var(--drawer-inset,0px)+2px),0,0)] data-[swipe-direction=right]:[--translate-x:calc(var(--drawer-swipe-movement-x)-var(--stack-peek-offset)-(var(--stack-shrink)*100%))]",
|
|
134
|
+
className,
|
|
135
|
+
)}
|
|
136
|
+
{...props}
|
|
137
|
+
>
|
|
138
|
+
{showSwipeHandle && <DrawerSwipeHandle />}
|
|
139
|
+
<DrawerPrimitive.Content
|
|
140
|
+
data-slot="drawer-content"
|
|
141
|
+
className={cn(
|
|
142
|
+
"flex min-h-0 flex-1 select-text flex-col overflow-hidden overscroll-contain rounded-[inherit] transition-opacity duration-300 ease-[cubic-bezier(0.45,1.005,0,1.005)] group-data-swiping/drawer-popup:select-none group-data-nested-drawer-open/drawer-popup:opacity-0 group-data-nested-drawer-swiping/drawer-popup:opacity-100",
|
|
143
|
+
)}
|
|
144
|
+
>
|
|
145
|
+
{children}
|
|
146
|
+
</DrawerPrimitive.Content>
|
|
147
|
+
</DrawerPrimitive.Popup>
|
|
148
|
+
</DrawerPrimitive.Viewport>
|
|
149
|
+
</DrawerPortal>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
154
|
+
return (
|
|
155
|
+
<div
|
|
156
|
+
data-slot="drawer-header"
|
|
157
|
+
className={cn(
|
|
158
|
+
"flex shrink-0 flex-col gap-0.5 p-4 pb-0 group-data-[swipe-axis=y]/drawer-popup:text-center md:gap-0.5 md:text-left",
|
|
159
|
+
className,
|
|
160
|
+
)}
|
|
161
|
+
{...props}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
167
|
+
return (
|
|
168
|
+
<div
|
|
169
|
+
data-slot="drawer-footer"
|
|
170
|
+
className={cn("mt-auto flex shrink-0 flex-col gap-2 p-4 pt-0", className)}
|
|
171
|
+
{...props}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function DrawerTitle({ className, ...props }: DrawerPrimitive.Title.Props) {
|
|
177
|
+
return (
|
|
178
|
+
<DrawerPrimitive.Title
|
|
179
|
+
data-slot="drawer-title"
|
|
180
|
+
className={cn("font-medium text-base text-foreground", className)}
|
|
181
|
+
{...props}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function DrawerDescription({ className, ...props }: DrawerPrimitive.Description.Props) {
|
|
187
|
+
return (
|
|
188
|
+
<DrawerPrimitive.Description
|
|
189
|
+
data-slot="drawer-description"
|
|
190
|
+
className={cn("text-balance text-muted-foreground text-sm", className)}
|
|
191
|
+
{...props}
|
|
192
|
+
/>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export {
|
|
197
|
+
Drawer,
|
|
198
|
+
DrawerClose,
|
|
199
|
+
DrawerContent,
|
|
200
|
+
DrawerDescription,
|
|
201
|
+
DrawerFooter,
|
|
202
|
+
DrawerHeader,
|
|
203
|
+
DrawerOverlay,
|
|
204
|
+
DrawerPortal,
|
|
205
|
+
DrawerSwipeHandle,
|
|
206
|
+
DrawerTitle,
|
|
207
|
+
DrawerTrigger,
|
|
208
|
+
};
|