@milktop/react-ui 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -2
- package/dist/accordion/Accordion.d.ts +65 -0
- package/dist/accordion/Accordion.js +61 -0
- package/dist/accordion/accordion.css +17 -0
- package/dist/action-bar/ActionBar.d.ts +33 -0
- package/dist/action-bar/ActionBar.js +48 -0
- package/dist/action-bar/action-bar.css +22 -0
- package/dist/alert/Alert.d.ts +28 -0
- package/dist/alert/Alert.js +12 -0
- package/dist/alert/alert.css +26 -0
- package/dist/app-shell/AppShell.d.ts +38 -0
- package/dist/app-shell/AppShell.js +92 -0
- package/dist/app-shell/Topbar.d.ts +20 -0
- package/dist/app-shell/Topbar.js +13 -0
- package/dist/app-shell/app-shell.css +41 -0
- package/dist/app-shell/shell.d.ts +9 -0
- package/dist/app-shell/shell.js +3 -0
- package/dist/attachments/Attachments.d.ts +84 -0
- package/dist/attachments/Attachments.js +235 -0
- package/dist/attachments/attachments.css +86 -0
- package/dist/avatar/Avatar.d.ts +31 -0
- package/dist/avatar/Avatar.js +41 -0
- package/dist/avatar/AvatarGroup.d.ts +41 -0
- package/dist/avatar/AvatarGroup.js +17 -0
- package/dist/avatar/avatar.css +73 -0
- package/dist/badge/Badge.d.ts +26 -0
- package/dist/badge/Badge.js +10 -0
- package/dist/badge/badge.css +24 -0
- package/dist/banner/Banner.d.ts +33 -0
- package/dist/banner/Banner.js +14 -0
- package/dist/banner/banner.css +25 -0
- package/dist/breadcrumbs/Breadcrumbs.d.ts +39 -0
- package/dist/breadcrumbs/Breadcrumbs.js +28 -0
- package/dist/breadcrumbs/breadcrumbs.css +18 -0
- package/dist/button/Button.d.ts +40 -0
- package/dist/button/Button.js +19 -0
- package/dist/button/button.css +63 -0
- package/dist/card/Card.d.ts +355 -0
- package/dist/card/Card.js +56 -0
- package/dist/card/card.css +43 -0
- package/dist/chart/AreaChart.d.ts +19 -0
- package/dist/chart/AreaChart.js +61 -0
- package/dist/chart/BarChart.d.ts +3 -0
- package/dist/chart/BarChart.js +60 -0
- package/dist/chart/ChartFrame.d.ts +25 -0
- package/dist/chart/ChartFrame.js +22 -0
- package/dist/chart/DonutChart.d.ts +20 -0
- package/dist/chart/DonutChart.js +41 -0
- package/dist/chart/Sparkline.d.ts +26 -0
- package/dist/chart/Sparkline.js +36 -0
- package/dist/chart/chart.css +51 -0
- package/dist/chart/chart.d.ts +23 -0
- package/dist/chart/chart.js +19 -0
- package/dist/checkbox/Checkbox.d.ts +50 -0
- package/dist/checkbox/Checkbox.js +19 -0
- package/dist/checkbox/CheckboxGroup.d.ts +44 -0
- package/dist/checkbox/CheckboxGroup.js +80 -0
- package/dist/checkbox/checkbox.css +49 -0
- package/dist/collapsible/Collapsible.d.ts +41 -0
- package/dist/collapsible/Collapsible.js +15 -0
- package/dist/collapsible/collapsible.css +20 -0
- package/dist/color-scheme.d.ts +18 -0
- package/dist/color-scheme.js +84 -0
- package/dist/command/Command.d.ts +64 -0
- package/dist/command/Command.js +162 -0
- package/dist/command/command.css +47 -0
- package/dist/confirm/confirm.css +5 -0
- package/dist/confirm/confirm.d.ts +42 -0
- package/dist/confirm/confirm.js +115 -0
- package/dist/copy-button/CopyButton.d.ts +30 -0
- package/dist/copy-button/CopyButton.js +23 -0
- package/dist/copy-button/copy-button.css +12 -0
- package/dist/css.d.ts +2 -0
- package/dist/data-list/DataList.d.ts +55 -0
- package/dist/data-list/DataList.js +43 -0
- package/dist/data-list/data-list.css +19 -0
- package/dist/date-picker/DatePicker.d.ts +95 -0
- package/dist/date-picker/DatePicker.js +237 -0
- package/dist/date-picker/date-picker.css +53 -0
- package/dist/dialog/Dialog.d.ts +98 -0
- package/dist/dialog/Dialog.js +92 -0
- package/dist/dialog/dialog.css +31 -0
- package/dist/divider/Divider.d.ts +27 -0
- package/dist/divider/Divider.js +8 -0
- package/dist/divider/divider.css +12 -0
- package/dist/editable/Editable.d.ts +47 -0
- package/dist/editable/Editable.js +51 -0
- package/dist/editable/editable.css +25 -0
- package/dist/editor/Editor.d.ts +59 -0
- package/dist/editor/Editor.js +220 -0
- package/dist/editor/editor.css +42 -0
- package/dist/editor/image.d.ts +1 -0
- package/dist/editor/image.js +23 -0
- package/dist/empty-state/EmptyState.d.ts +22 -0
- package/dist/empty-state/EmptyState.js +9 -0
- package/dist/empty-state/empty-state.css +14 -0
- package/dist/event-calendar/EventCalendar.d.ts +213 -0
- package/dist/event-calendar/EventCalendar.js +686 -0
- package/dist/event-calendar/dates.d.ts +8 -0
- package/dist/event-calendar/dates.js +21 -0
- package/dist/event-calendar/event-calendar.css +157 -0
- package/dist/field/Field.d.ts +66 -0
- package/dist/field/Field.js +23 -0
- package/dist/field/field.css +26 -0
- package/dist/file-upload/FileUpload.d.ts +60 -0
- package/dist/file-upload/FileUpload.js +102 -0
- package/dist/file-upload/file-upload.css +32 -0
- package/dist/format/FormatNumber.d.ts +42 -0
- package/dist/format/FormatNumber.js +59 -0
- package/dist/format/LocaleProvider.d.ts +33 -0
- package/dist/format/LocaleProvider.js +48 -0
- package/dist/format/format.d.ts +37 -0
- package/dist/format/format.js +74 -0
- package/dist/format/messages.d.ts +211 -0
- package/dist/format/messages.js +202 -0
- package/dist/highlight/Highlight.d.ts +11 -0
- package/dist/highlight/Highlight.js +14 -0
- package/dist/highlight/highlight.css +17 -0
- package/dist/hover-card/HoverCard.d.ts +47 -0
- package/dist/hover-card/HoverCard.js +18 -0
- package/dist/hover-card/hover-card.css +19 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +85 -0
- package/dist/input/Input.d.ts +41 -0
- package/dist/input/Input.js +48 -0
- package/dist/input/input.css +25 -0
- package/dist/kbd/Kbd.d.ts +19 -0
- package/dist/kbd/Kbd.js +20 -0
- package/dist/kbd/kbd.css +8 -0
- package/dist/menu/Menu.d.ts +223 -0
- package/dist/menu/Menu.js +275 -0
- package/dist/menu/menu.css +38 -0
- package/dist/native-select/NativeSelect.d.ts +36 -0
- package/dist/native-select/NativeSelect.js +51 -0
- package/dist/native-select/native-select.css +16 -0
- package/dist/number-input/NumberInput.d.ts +56 -0
- package/dist/number-input/NumberInput.js +67 -0
- package/dist/number-input/number-input.css +21 -0
- package/dist/page/Page.d.ts +30 -0
- package/dist/page/Page.js +9 -0
- package/dist/page/page.css +17 -0
- package/dist/pagination/Pagination.d.ts +31 -0
- package/dist/pagination/Pagination.js +58 -0
- package/dist/pagination/pagination.css +15 -0
- package/dist/password-input/PasswordInput.d.ts +47 -0
- package/dist/password-input/PasswordInput.js +35 -0
- package/dist/password-input/password-input.css +18 -0
- package/dist/pin-input/PinInput.d.ts +45 -0
- package/dist/pin-input/PinInput.js +22 -0
- package/dist/pin-input/pin-input.css +13 -0
- package/dist/popover/Popover.d.ts +65 -0
- package/dist/popover/Popover.js +26 -0
- package/dist/popover/popover.css +16 -0
- package/dist/progress/Progress.d.ts +56 -0
- package/dist/progress/Progress.js +28 -0
- package/dist/progress/progress.css +24 -0
- package/dist/prose/Prose.d.ts +23 -0
- package/dist/prose/Prose.js +10 -0
- package/dist/prose/prose.css +29 -0
- package/dist/radio-group/RadioGroup.d.ts +59 -0
- package/dist/radio-group/RadioGroup.js +37 -0
- package/dist/radio-group/radio-group.css +30 -0
- package/dist/rolling-number/RollingNumber.d.ts +27 -0
- package/dist/rolling-number/RollingNumber.js +18 -0
- package/dist/rolling-number/rolling-number.css +10 -0
- package/dist/segment-group/Segmented.d.ts +68 -0
- package/dist/segment-group/Segmented.js +79 -0
- package/dist/segment-group/segment-group.css +25 -0
- package/dist/select/ListSelect.d.ts +5 -0
- package/dist/select/ListSelect.js +82 -0
- package/dist/select/SearchSelect.d.ts +43 -0
- package/dist/select/SearchSelect.js +219 -0
- package/dist/select/Select.d.ts +147 -0
- package/dist/select/Select.js +32 -0
- package/dist/select/items.d.ts +17 -0
- package/dist/select/items.js +14 -0
- package/dist/select/search-select.css +45 -0
- package/dist/select/select.css +52 -0
- package/dist/sheet/Sheet.d.ts +88 -0
- package/dist/sheet/Sheet.js +66 -0
- package/dist/sheet/sheet.css +44 -0
- package/dist/sidebar/NavGroup.d.ts +29 -0
- package/dist/sidebar/NavGroup.js +78 -0
- package/dist/sidebar/NavItem.d.ts +33 -0
- package/dist/sidebar/NavItem.js +50 -0
- package/dist/sidebar/NavSection.d.ts +20 -0
- package/dist/sidebar/NavSection.js +44 -0
- package/dist/sidebar/Sidebar.d.ts +65 -0
- package/dist/sidebar/Sidebar.js +59 -0
- package/dist/sidebar/SidebarUser.d.ts +31 -0
- package/dist/sidebar/SidebarUser.js +14 -0
- package/dist/sidebar/context.d.ts +13 -0
- package/dist/sidebar/context.js +6 -0
- package/dist/sidebar/sidebar.css +109 -0
- package/dist/skeleton/Skeleton.d.ts +25 -0
- package/dist/skeleton/Skeleton.js +8 -0
- package/dist/skeleton/skeleton.css +13 -0
- package/dist/slider/Slider.d.ts +70 -0
- package/dist/slider/Slider.js +65 -0
- package/dist/slider/slider.css +17 -0
- package/dist/sortable-list/SortableList.d.ts +46 -0
- package/dist/sortable-list/SortableList.js +85 -0
- package/dist/sortable-list/sortable-list.css +25 -0
- package/dist/spinner/Spinner.d.ts +18 -0
- package/dist/spinner/Spinner.js +11 -0
- package/dist/spinner/spinner.css +9 -0
- package/dist/stat/Stat.d.ts +42 -0
- package/dist/stat/Stat.js +21 -0
- package/dist/stat/Stats.d.ts +23 -0
- package/dist/stat/Stats.js +8 -0
- package/dist/stat/stat.css +34 -0
- package/dist/swatch-picker/SwatchPicker.d.ts +45 -0
- package/dist/swatch-picker/SwatchPicker.js +54 -0
- package/dist/swatch-picker/swatch-picker.css +38 -0
- package/dist/switch/Switch.d.ts +39 -0
- package/dist/switch/Switch.js +17 -0
- package/dist/switch/switch.css +13 -0
- package/dist/table/Table.d.ts +90 -0
- package/dist/table/Table.js +145 -0
- package/dist/table/table.css +44 -0
- package/dist/tabs/Tabs.d.ts +87 -0
- package/dist/tabs/Tabs.js +90 -0
- package/dist/tabs/tabs.css +53 -0
- package/dist/tags-input/TagsInput.d.ts +69 -0
- package/dist/tags-input/TagsInput.js +95 -0
- package/dist/tags-input/tags-input.css +37 -0
- package/dist/textarea/Textarea.d.ts +24 -0
- package/dist/textarea/Textarea.js +51 -0
- package/dist/textarea/textarea.css +14 -0
- package/dist/theme-toggle/ThemeToggle.d.ts +49 -0
- package/dist/theme-toggle/ThemeToggle.js +71 -0
- package/dist/theme-toggle/theme-toggle.css +23 -0
- package/dist/theme.css +119 -0
- package/dist/time-input/TimeInput.d.ts +25 -0
- package/dist/time-input/TimeInput.js +20 -0
- package/dist/timeline/Timeline.d.ts +50 -0
- package/dist/timeline/Timeline.js +15 -0
- package/dist/timeline/timeline.css +29 -0
- package/dist/toast/Toaster.d.ts +36 -0
- package/dist/toast/Toaster.js +25 -0
- package/dist/toast/toast.css +33 -0
- package/dist/tooltip/Tooltip.d.ts +55 -0
- package/dist/tooltip/Tooltip.js +48 -0
- package/dist/tooltip/tooltip.css +18 -0
- package/dist/tree-view/TreeView.d.ts +86 -0
- package/dist/tree-view/TreeView.js +74 -0
- package/dist/tree-view/tree-view.css +29 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +59 -0
- package/package.json +99 -4
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const toISODate: (d: Date) => string;
|
|
2
|
+
export declare const toISODateTime: (d: Date) => string;
|
|
3
|
+
export declare function parse(value: string): Date;
|
|
4
|
+
export declare const addDays: (d: Date, days: number) => Date;
|
|
5
|
+
export declare const addMinutes: (d: Date, minutes: number) => Date;
|
|
6
|
+
export declare const minutesOfDay: (d: Date) => number;
|
|
7
|
+
export declare const sameDay: (a: Date, b: Date) => boolean;
|
|
8
|
+
export declare function startOfWeek(d: Date, weekStartsOn?: number): Date;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Local date-times as ISO strings without a zone ('2025-10-09T16:00'), as
|
|
2
|
+
// DatePicker gives dates: what a lesson's time means where it's taught.
|
|
3
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
4
|
+
export const toISODate = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
5
|
+
export const toISODateTime = (d) => `${toISODate(d)}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
6
|
+
// '2025-10-09' or '2025-10-09T16:00' as a local Date.
|
|
7
|
+
export function parse(value) {
|
|
8
|
+
const [date, time = '00:00'] = value.split('T');
|
|
9
|
+
const [y, m, d] = date.split('-').map(Number);
|
|
10
|
+
const [h, min] = time.split(':').map(Number);
|
|
11
|
+
return new Date(y, m - 1, d, h || 0, min || 0);
|
|
12
|
+
}
|
|
13
|
+
export const addDays = (d, days) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + days, d.getHours(), d.getMinutes());
|
|
14
|
+
export const addMinutes = (d, minutes) => new Date(d.getTime() + minutes * 60000);
|
|
15
|
+
export const minutesOfDay = (d) => d.getHours() * 60 + d.getMinutes();
|
|
16
|
+
export const sameDay = (a, b) => toISODate(a) === toISODate(b);
|
|
17
|
+
// The first day of the week holding `d` (Monday by default).
|
|
18
|
+
export function startOfWeek(d, weekStartsOn = 1) {
|
|
19
|
+
const back = (d.getDay() - weekStartsOn + 7) % 7;
|
|
20
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate() - back);
|
|
21
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
@layer ui {
|
|
2
|
+
[data-scope=event-calendar][data-part=root] { position: relative; display: flex; flex-direction: column; gap: 0.75rem; width: 100%; min-width: 0;
|
|
3
|
+
color: var(--ui-text); font-family: var(--ui-font); font-size: 14px; }
|
|
4
|
+
[data-scope=event-calendar][data-part=toolbar] { display: flex; align-items: center; gap: 0.5rem 0.75rem; flex-wrap: wrap; }
|
|
5
|
+
[data-scope=event-calendar][data-part=nav] { display: flex; align-items: center; gap: 0.25rem; }
|
|
6
|
+
[data-scope=event-calendar][data-part=title] { display: inline-flex; align-items: center; gap: 0.375rem; margin: 0; padding: 0.25rem 0.5rem;
|
|
7
|
+
border: none; border-radius: var(--ui-radius); background: transparent; color: inherit; font: inherit; font-size: 16px; font-weight: 600; cursor: pointer;
|
|
8
|
+
& iconify-icon { color: var(--ui-muted); font-size: 14px; }
|
|
9
|
+
&:hover { background: var(--ui-hover); }
|
|
10
|
+
&:focus-visible { outline: 2px solid var(--ui-ring-soft); } }
|
|
11
|
+
[data-scope=event-calendar][data-part=actions] { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }
|
|
12
|
+
|
|
13
|
+
/* A time gutter, then a column per day (or per resource within each day); it scrolls sideways when narrow. */
|
|
14
|
+
[data-scope=event-calendar][data-part=grid] { display: grid; grid-template-columns: minmax(3.5rem, max-content) repeat(var(--columns), minmax(6.5rem, 1fr));
|
|
15
|
+
overflow-x: auto; background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: calc(var(--ui-radius) + 2px);
|
|
16
|
+
box-shadow: var(--ui-card-shadow); }
|
|
17
|
+
[data-scope=event-calendar]:is([data-part=corner], [data-part=day-header], [data-part=resource-header]) {
|
|
18
|
+
background: var(--ui-surface); border-bottom: 1px solid var(--ui-border); }
|
|
19
|
+
[data-scope=event-calendar][data-part=day-header] { display: flex; align-items: baseline; justify-content: center; gap: 0.375rem; padding: 0.5rem;
|
|
20
|
+
border-left: 1px solid var(--ui-border); color: var(--ui-muted); font-size: 12px;
|
|
21
|
+
& strong { color: var(--ui-text); font-size: 15px; font-weight: 600; }
|
|
22
|
+
&[data-today] strong { display: inline-grid; place-items: center; min-width: 1.625rem; height: 1.625rem; border-radius: 9999px;
|
|
23
|
+
background: var(--ui-accent); color: var(--ui-accent-text); } }
|
|
24
|
+
[data-scope=event-calendar][data-part=resource-header] { display: flex; align-items: center; justify-content: center; gap: 0.5rem; min-width: 0;
|
|
25
|
+
padding: 0.5rem; border-left: 1px solid var(--ui-border); font-size: 13px; font-weight: 500; text-align: center; overflow: hidden; }
|
|
26
|
+
|
|
27
|
+
/* A clickable header (onDayClick, onResourceClick): fills its cell, looking the same. */
|
|
28
|
+
[data-scope=event-calendar][data-part=header-button] { display: inherit; flex: 1; align-items: inherit; justify-content: inherit; gap: inherit;
|
|
29
|
+
min-width: 0; margin: -0.375rem; padding: 0.375rem; border: none; border-radius: var(--ui-radius); background: transparent;
|
|
30
|
+
color: inherit; font: inherit; cursor: pointer;
|
|
31
|
+
&:hover { background: var(--ui-hover); }
|
|
32
|
+
&:focus-visible { outline: 2px solid var(--ui-ring-soft); } }
|
|
33
|
+
|
|
34
|
+
/* Extra rows (`rows`, e.g. the day's messages): a label, then a cell per day or column. */
|
|
35
|
+
[data-scope=event-calendar][data-part=row-label] { display: flex; align-items: center; justify-content: flex-end; padding-right: 0.5rem;
|
|
36
|
+
border-bottom: 1px solid var(--ui-border); color: var(--ui-muted); font-size: 12px; }
|
|
37
|
+
[data-scope=event-calendar][data-part=row-cell] { display: flex; align-items: center; min-width: 0; min-height: 2rem; padding: 0.25rem;
|
|
38
|
+
box-sizing: border-box; border-left: 1px solid var(--ui-border); border-bottom: 1px solid var(--ui-border); font-size: 12px;
|
|
39
|
+
&[data-clickable] { cursor: pointer; outline: none;
|
|
40
|
+
&:hover { background: color-mix(in srgb, var(--ui-accent) 6%, transparent); }
|
|
41
|
+
&:focus-visible { box-shadow: inset 0 0 0 2px var(--ui-ring-soft); } } }
|
|
42
|
+
|
|
43
|
+
/* All-day and multi-day events, in lanes along the top. */
|
|
44
|
+
/* The gutter column grows to fit the longest label ("Journée", in French). */
|
|
45
|
+
[data-scope=event-calendar][data-part=all-day-label] { display: flex; align-items: center; justify-content: flex-end; padding: 0 0.5rem; white-space: nowrap;
|
|
46
|
+
border-bottom: 1px solid var(--ui-border); color: var(--ui-muted); font-size: 11px; }
|
|
47
|
+
[data-scope=event-calendar][data-part=all-day] { position: relative; grid-column: 2 / -1; height: calc(var(--lanes) * 1.625rem + 0.5rem);
|
|
48
|
+
border-bottom: 1px solid var(--ui-border); border-left: 1px solid var(--ui-border);
|
|
49
|
+
/* A line between the days (columns) it spans. */
|
|
50
|
+
background-image: linear-gradient(to right, transparent calc(100% - 1px), var(--ui-border) calc(100% - 1px));
|
|
51
|
+
background-size: calc(100% / var(--columns)) 100%;
|
|
52
|
+
&[data-selectable] { cursor: cell; touch-action: none; } }
|
|
53
|
+
[data-scope=event-calendar][data-part=all-day-event] { --event-color: var(--ui-accent);
|
|
54
|
+
position: absolute; height: 1.375rem; box-sizing: border-box; padding: 0 0.5rem; border: 2px solid var(--ui-surface);
|
|
55
|
+
border-radius: calc(var(--ui-radius) + 2px); background: color-mix(in srgb, var(--event-color) 22%, var(--ui-surface));
|
|
56
|
+
color: var(--ui-text); font: inherit; font-size: 12px; font-weight: 500; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;
|
|
57
|
+
&:focus-visible { outline: 2px solid var(--ui-ring); outline-offset: 1px; }
|
|
58
|
+
&[data-continued] { border-top-left-radius: 0; border-bottom-left-radius: 0; border-left: none; } }
|
|
59
|
+
|
|
60
|
+
[data-scope=event-calendar][data-part=gutter] { position: relative; height: calc(var(--hour-height) * var(--hours)); }
|
|
61
|
+
[data-scope=event-calendar][data-part=hour] { position: absolute; right: 0.5rem; translate: 0 -50%; color: var(--ui-muted); font-size: 11px;
|
|
62
|
+
&:first-child { translate: 0 2px; } }
|
|
63
|
+
/* Hour lines, with a fainter one at the half hour. */
|
|
64
|
+
[data-scope=event-calendar][data-part=day] { position: relative; height: calc(var(--hour-height) * var(--hours)); border-left: 1px solid var(--ui-border);
|
|
65
|
+
background-image: linear-gradient(var(--ui-border) 1px, transparent 1px),
|
|
66
|
+
linear-gradient(color-mix(in srgb, var(--ui-border) 45%, transparent) 1px, transparent 1px);
|
|
67
|
+
background-size: 100% var(--hour-height), 100% var(--hour-height); background-position: 0 0, 0 calc(var(--hour-height) / 2);
|
|
68
|
+
&[data-today] { background-color: color-mix(in srgb, var(--ui-accent) 5%, transparent); }
|
|
69
|
+
&[data-selectable] { cursor: cell; touch-action: none; }
|
|
70
|
+
/* With resources over several days, a stronger line where each day starts. */
|
|
71
|
+
[data-two-rows] > &[data-first] { border-left-color: var(--ui-muted); } }
|
|
72
|
+
[data-two-rows] > [data-scope=event-calendar][data-part=day-header] { border-left-color: var(--ui-muted); }
|
|
73
|
+
|
|
74
|
+
[data-scope=event-calendar][data-part=event], [data-scope=event-calendar][data-part=all-day-event] {
|
|
75
|
+
&[data-color=success] { --event-color: var(--ui-success); }
|
|
76
|
+
&[data-color=warning] { --event-color: #d97706; }
|
|
77
|
+
&[data-color=danger] { --event-color: var(--ui-danger); }
|
|
78
|
+
&[data-color=neutral] { --event-color: var(--ui-muted); } }
|
|
79
|
+
[data-scope=event-calendar][data-part=event] { --event-color: var(--ui-accent);
|
|
80
|
+
position: absolute; z-index: 1; display: flex; flex-direction: column; gap: 0.125rem; box-sizing: border-box; overflow: hidden;
|
|
81
|
+
padding: 0.25rem 0.375rem; border: 2px solid var(--ui-surface); border-radius: calc(var(--ui-radius) + 2px);
|
|
82
|
+
border-left: 3px solid var(--event-color); background: color-mix(in srgb, var(--event-color) 16%, var(--ui-surface));
|
|
83
|
+
color: var(--ui-text); font: inherit; font-size: 12px; text-align: left; cursor: pointer;
|
|
84
|
+
&:hover { background: color-mix(in srgb, var(--event-color) 24%, var(--ui-surface)); }
|
|
85
|
+
&:focus-visible { outline: 2px solid var(--ui-ring); outline-offset: 1px; z-index: 3; }
|
|
86
|
+
&[data-editable] { touch-action: none; }
|
|
87
|
+
/* Short events: title and time on one line. */
|
|
88
|
+
&[data-compact] { flex-direction: row; align-items: center; gap: 0.375rem; padding-block: 0; border-top-width: 1px; border-bottom-width: 1px; line-height: 1.2; }
|
|
89
|
+
&[data-dragging] { z-index: 4; cursor: grabbing; box-shadow: var(--ui-shadow); opacity: 0.92; } }
|
|
90
|
+
[data-scope=event-calendar][data-part=event-title] { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
91
|
+
[data-scope=event-calendar][data-part=event-time] { color: var(--ui-muted); white-space: nowrap; }
|
|
92
|
+
/* The bottom edge drags to resize. */
|
|
93
|
+
[data-scope=event-calendar][data-part=resize] { position: absolute; left: 0; right: 0; bottom: 0; height: 0.5rem; cursor: ns-resize;
|
|
94
|
+
&::after { content: ''; position: absolute; left: 50%; bottom: 2px; width: 1.25rem; height: 2px; translate: -50% 0; border-radius: 2px;
|
|
95
|
+
background: var(--event-color); opacity: 0; transition: opacity 120ms; }
|
|
96
|
+
[data-part=event]:hover > &::after { opacity: 0.6; } }
|
|
97
|
+
[data-scope=event-calendar][data-part=selection] { position: absolute; left: 2px; right: 2px; z-index: 2; box-sizing: border-box; padding: 0.25rem 0.375rem;
|
|
98
|
+
border-radius: calc(var(--ui-radius) + 2px); background: color-mix(in srgb, var(--resource-color, var(--ui-accent)) 18%, transparent);
|
|
99
|
+
border: 1px dashed var(--resource-color, var(--ui-accent)); color: var(--ui-text); font-size: 12px; font-weight: 500; pointer-events: none;
|
|
100
|
+
&[data-all-day] { top: 2px; bottom: 2px; left: auto; right: auto; } }
|
|
101
|
+
[data-scope=event-calendar][data-part=now] { position: absolute; left: 0; right: 0; z-index: 2; height: 2px; background: var(--ui-danger); pointer-events: none;
|
|
102
|
+
&::before { content: ''; position: absolute; left: -4px; top: -3px; width: 8px; height: 8px; border-radius: 9999px; background: var(--ui-danger); } }
|
|
103
|
+
[data-scope=event-calendar][data-part=status] { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
104
|
+
/* Placing a pinned event: a bar saying so, above the grid. */
|
|
105
|
+
[data-scope=event-calendar][data-part=pinned] { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.375rem 0.375rem 0.75rem;
|
|
106
|
+
border: 1px dashed var(--ui-accent); border-radius: calc(var(--ui-radius) + 2px); background: color-mix(in srgb, var(--ui-accent) 8%, var(--ui-surface));
|
|
107
|
+
color: var(--ui-accent-soft-text); font-size: 13px;
|
|
108
|
+
& > span { flex-grow: 1; } }
|
|
109
|
+
|
|
110
|
+
/* Scrolling inside `height`: header rows stick (each below the last; tops set by the component), the gutter sticks left. */
|
|
111
|
+
[data-scope=event-calendar][data-part=grid][data-scroll] { position: relative; overflow: auto;
|
|
112
|
+
& > [data-sticky-row] { position: sticky; z-index: 5; background-color: var(--ui-surface); }
|
|
113
|
+
& > :is([data-part=corner], [data-part=all-day-label], [data-part=row-label]) { left: 0; z-index: 6; }
|
|
114
|
+
& > [data-part=gutter] { position: sticky; left: 0; z-index: 4; background: var(--ui-surface); } }
|
|
115
|
+
|
|
116
|
+
/* Time that can't be used (isAvailable): hatched, with its reason on hover. */
|
|
117
|
+
[data-scope=event-calendar][data-part=unavailable] { position: absolute; left: 0; right: 0; cursor: not-allowed;
|
|
118
|
+
background: repeating-linear-gradient(-45deg, color-mix(in srgb, var(--ui-muted) 14%, transparent) 0 1px, transparent 1px 7px),
|
|
119
|
+
color-mix(in srgb, var(--ui-muted) 7%, transparent); }
|
|
120
|
+
|
|
121
|
+
/* Where a dragged event came from. */
|
|
122
|
+
[data-scope=event-calendar][data-part=ghost] { position: absolute; box-sizing: border-box; border: 1px dashed var(--ui-border);
|
|
123
|
+
border-radius: calc(var(--ui-radius) + 2px); background: color-mix(in srgb, var(--ui-muted) 6%, transparent); pointer-events: none; }
|
|
124
|
+
|
|
125
|
+
[data-scope=event-calendar][data-part=event] {
|
|
126
|
+
/* Blocked time: hatched and grey. */
|
|
127
|
+
&[data-blocked] { --event-color: var(--ui-muted);
|
|
128
|
+
background: repeating-linear-gradient(-45deg, color-mix(in srgb, var(--ui-muted) 18%, transparent) 0 2px, transparent 2px 8px),
|
|
129
|
+
color-mix(in srgb, var(--ui-muted) 10%, var(--ui-surface)); }
|
|
130
|
+
/* Saving: faded, and not draggable. */
|
|
131
|
+
&[data-pending] { opacity: 0.55; border-left-style: dashed; cursor: progress; }
|
|
132
|
+
/* Being placed elsewhere. */
|
|
133
|
+
&[data-pinned] { outline: 2px dashed var(--ui-accent); outline-offset: 1px; }
|
|
134
|
+
/* Dropped here, it'd be refused. */
|
|
135
|
+
&[data-invalid] { --event-color: var(--ui-danger); background: color-mix(in srgb, var(--ui-danger) 14%, var(--ui-surface)); cursor: not-allowed; } }
|
|
136
|
+
[data-scope=event-calendar][data-part=all-day-event][data-blocked] { --event-color: var(--ui-muted);
|
|
137
|
+
background: repeating-linear-gradient(-45deg, color-mix(in srgb, var(--ui-muted) 20%, transparent) 0 2px, transparent 2px 8px),
|
|
138
|
+
color-mix(in srgb, var(--ui-muted) 12%, var(--ui-surface)); }
|
|
139
|
+
[data-scope=event-calendar][data-part=selection] {
|
|
140
|
+
&[data-placing] { background: color-mix(in srgb, var(--ui-accent) 12%, var(--ui-surface)); border-style: solid; }
|
|
141
|
+
&[data-invalid] { border-color: var(--ui-danger); background: color-mix(in srgb, var(--ui-danger) 12%, transparent); color: var(--ui-danger); } }
|
|
142
|
+
[data-placing] [data-scope=event-calendar][data-part=day] { cursor: copy; }
|
|
143
|
+
|
|
144
|
+
/* "+2 more" where all-day lanes are folded; the label opens and closes them. */
|
|
145
|
+
[data-scope=event-calendar][data-part=more] { position: absolute; height: 1.375rem; padding: 0 0.5rem; box-sizing: border-box; border: none;
|
|
146
|
+
background: transparent; color: var(--ui-muted); font: inherit; font-size: 11px; font-weight: 500; text-align: left; cursor: pointer;
|
|
147
|
+
&:hover { color: var(--ui-text); text-decoration: underline; } }
|
|
148
|
+
[data-scope=event-calendar][data-part=all-day-label] > button { display: inline-flex; align-items: center; gap: 0.125rem; padding: 0;
|
|
149
|
+
border: none; background: transparent; color: inherit; font: inherit; cursor: pointer;
|
|
150
|
+
&:hover { color: var(--ui-text); } }
|
|
151
|
+
/* The slot under the pointer, in its resource's colour (grey where it can't be used). */
|
|
152
|
+
[data-scope=event-calendar][data-part=hover] { position: absolute; left: 0; right: 0; z-index: 0; pointer-events: none;
|
|
153
|
+
background: color-mix(in srgb, var(--resource-color, var(--ui-accent)) 12%, transparent);
|
|
154
|
+
&[data-invalid] { background: transparent; } }
|
|
155
|
+
/* A resource with a colour: a stripe along its header. */
|
|
156
|
+
[data-scope=event-calendar][data-part=resource-header][data-colored] { box-shadow: inset 0 3px 0 var(--resource-color); }
|
|
157
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A form field: a label, hint and error around one control, wired up for
|
|
3
|
+
* assistive tech. Controls inside it (Input, Select, Checkbox…) skip their
|
|
4
|
+
* own label and point at its.
|
|
5
|
+
*
|
|
6
|
+
* <Field label="Student" error={errors.student}>
|
|
7
|
+
* <Select items={students} value={student} onValueChange={setStudent} />
|
|
8
|
+
* </Field>
|
|
9
|
+
*
|
|
10
|
+
* - `label`, `hint`, `error` (shown instead of the hint), `required`
|
|
11
|
+
* - `span`: columns (of 12) to take inside Fields
|
|
12
|
+
* - without children it renders an Input, and the other props (value,
|
|
13
|
+
* onValueChange, type, placeholder, icon, suffix…) pass on to it
|
|
14
|
+
*
|
|
15
|
+
* By hand, with Ark's Field parts:
|
|
16
|
+
*
|
|
17
|
+
* <Field.Root invalid={!!error}>
|
|
18
|
+
* <Field.Label>Email <Field.RequiredIndicator /></Field.Label>
|
|
19
|
+
* <Input />
|
|
20
|
+
* <Field.ErrorText>{error}</Field.ErrorText>
|
|
21
|
+
* </Field.Root>
|
|
22
|
+
*/
|
|
23
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
24
|
+
import { Field as Ark } from '@ark-ui/react/field';
|
|
25
|
+
import { type InputProps } from '../input/Input.js';
|
|
26
|
+
import { type Classes } from '../utils.js';
|
|
27
|
+
import '../theme.css';
|
|
28
|
+
import './field.css';
|
|
29
|
+
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
30
|
+
export type FieldProps = DistributiveOmit<InputProps, 'label' | 'classes' | 'children'> & {
|
|
31
|
+
label?: ReactNode;
|
|
32
|
+
hint?: ReactNode;
|
|
33
|
+
error?: ReactNode;
|
|
34
|
+
required?: boolean;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
span?: number;
|
|
37
|
+
classes?: Classes<'root' | 'label' | 'requiredIndicator' | 'helperText' | 'errorText'>;
|
|
38
|
+
children?: ReactNode;
|
|
39
|
+
};
|
|
40
|
+
declare function FieldShortcut({ label, hint, error, required, disabled, span, className, classes, style, children, ...inputProps }: FieldProps): import("react").JSX.Element;
|
|
41
|
+
export declare const Field: typeof FieldShortcut & {
|
|
42
|
+
Root: import("react").ForwardRefExoticComponent<Ark.RootProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
43
|
+
Label: import("react").ForwardRefExoticComponent<Ark.LabelProps & import("react").RefAttributes<HTMLLabelElement>>;
|
|
44
|
+
RequiredIndicator: import("react").ForwardRefExoticComponent<Ark.RequiredIndicatorProps & import("react").RefAttributes<HTMLSpanElement>>;
|
|
45
|
+
HelperText: import("react").ForwardRefExoticComponent<Ark.HelperTextProps & import("react").RefAttributes<HTMLSpanElement>>;
|
|
46
|
+
ErrorText: import("react").ForwardRefExoticComponent<Ark.ErrorTextProps & import("react").RefAttributes<HTMLSpanElement>>;
|
|
47
|
+
Context: (props: Ark.ContextProps) => ReactNode;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* A group of fields on a 12-column grid (one column on phones). Each Field
|
|
51
|
+
* takes `span` columns.
|
|
52
|
+
*
|
|
53
|
+
* <Fields legend="Lesson">
|
|
54
|
+
* <Field label="Student" span={6}>…</Field>
|
|
55
|
+
* <Field label="Date" span={6}>…</Field>
|
|
56
|
+
* </Fields>
|
|
57
|
+
*
|
|
58
|
+
* - `legend`, `description`: a heading for the group
|
|
59
|
+
*/
|
|
60
|
+
export type FieldsProps = ComponentProps<'fieldset'> & {
|
|
61
|
+
legend?: ReactNode;
|
|
62
|
+
description?: ReactNode;
|
|
63
|
+
classes?: Classes<'root' | 'legend' | 'description' | 'grid'>;
|
|
64
|
+
};
|
|
65
|
+
export declare function Fields({ legend, description, className, classes, children, ...rest }: FieldsProps): import("react").JSX.Element;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Field as Ark } from '@ark-ui/react/field';
|
|
3
|
+
import { Input } from '../input/Input.js';
|
|
4
|
+
import { cx, partsOf, withParts } from '../utils.js';
|
|
5
|
+
import '../theme.css';
|
|
6
|
+
import './field.css';
|
|
7
|
+
function FieldShortcut({ label = null, hint = null, error = null, required = false, disabled = false, span = 12, className, classes, style, children, ...inputProps }) {
|
|
8
|
+
const part = partsOf('field', classes);
|
|
9
|
+
return (_jsxs(Ark.Root, { ...part('root'), className: cx(className, classes?.root), style: { '--span': span, ...style }, invalid: !!error, required: required, disabled: disabled, children: [label && _jsxs(Ark.Label, { ...part('label'), children: [label, _jsx(Ark.RequiredIndicator, { ...part('required-indicator'), "aria-hidden": "true", children: " *" })] }), children ?? _jsx(Input, { ...inputProps }), error ? _jsx(Ark.ErrorText, { ...part('error-text'), children: error })
|
|
10
|
+
: hint && _jsx(Ark.HelperText, { ...part('helper-text'), children: hint })] }));
|
|
11
|
+
}
|
|
12
|
+
export const Field = withParts(FieldShortcut, {
|
|
13
|
+
Root: Ark.Root,
|
|
14
|
+
Label: Ark.Label,
|
|
15
|
+
RequiredIndicator: Ark.RequiredIndicator,
|
|
16
|
+
HelperText: Ark.HelperText,
|
|
17
|
+
ErrorText: Ark.ErrorText,
|
|
18
|
+
Context: Ark.Context,
|
|
19
|
+
});
|
|
20
|
+
export function Fields({ legend = null, description = null, className, classes, children, ...rest }) {
|
|
21
|
+
const part = partsOf('fields', classes);
|
|
22
|
+
return (_jsxs("fieldset", { ...rest, ...part('root'), className: cx(className, classes?.root), children: [legend && _jsx("legend", { ...part('legend'), children: legend }), description && _jsx("p", { ...part('description'), children: description }), _jsx("div", { ...part('grid'), children: children })] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@layer ui {
|
|
2
|
+
[data-scope=field][data-part=root] {
|
|
3
|
+
display: flex; flex-direction: column; gap: 0.375rem; min-width: 0;
|
|
4
|
+
color: var(--ui-text); font-family: var(--ui-font);
|
|
5
|
+
grid-column: span var(--span, 12);
|
|
6
|
+
|
|
7
|
+
/* The control fills the field. */
|
|
8
|
+
> [data-part=root] { width: 100%; min-width: 0; }
|
|
9
|
+
}
|
|
10
|
+
[data-scope=field][data-part=label] { font-size: 14px; font-weight: 500; width: max-content; }
|
|
11
|
+
[data-scope=field][data-part=required-indicator] { color: var(--ui-danger); }
|
|
12
|
+
[data-scope=field]:is([data-part=helper-text], [data-part=error-text]) { margin: 0; font-size: 12px; }
|
|
13
|
+
[data-scope=field][data-part=helper-text] { color: var(--ui-muted); }
|
|
14
|
+
[data-scope=field][data-part=error-text] { color: var(--ui-danger); }
|
|
15
|
+
|
|
16
|
+
[data-scope=fields][data-part=root] { margin: 0; padding: 0; border: none; min-width: 0; }
|
|
17
|
+
[data-scope=fields][data-part=legend] { padding: 0; font-size: 16px; font-weight: 600; }
|
|
18
|
+
[data-scope=fields][data-part=description] { margin: 0.25rem 0 0; font-size: 14px; color: var(--ui-muted); }
|
|
19
|
+
[data-scope=fields][data-part=grid] { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 1rem;
|
|
20
|
+
:is([data-part=legend], [data-part=description]) + & { margin-top: 1rem; } }
|
|
21
|
+
|
|
22
|
+
/* One column on phones. */
|
|
23
|
+
@media (max-width: 480px) {
|
|
24
|
+
[data-scope=fields][data-part=grid] > [data-scope=field] { grid-column: 1 / -1; }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A file upload: drop files on it or browse, then see and remove them.
|
|
3
|
+
*
|
|
4
|
+
* <FileUpload label="Worksheets" files={files} onFilesChange={setFiles} accept=".pdf" maxFiles={5} hint="PDFs, up to 5" />
|
|
5
|
+
*
|
|
6
|
+
* - `files`, `defaultFiles`: the accepted Files; `onFilesChange`: called with them
|
|
7
|
+
* - `accept`: types to allow ('image/*', '.pdf', or an array of them)
|
|
8
|
+
* - `maxFiles`: how many (1); past it, the first that fit are taken and the
|
|
9
|
+
* rest listed as "Too many files"; `minFileSize`, `maxFileSize`: in bytes
|
|
10
|
+
* - `hint`: text under the prompt
|
|
11
|
+
* - `validate(file, details)`: your own check, returning error codes for a
|
|
12
|
+
* file it refuses (or null), as Ark's
|
|
13
|
+
* - `name`: for plain form posts
|
|
14
|
+
*
|
|
15
|
+
* By hand, with Ark's parts:
|
|
16
|
+
*
|
|
17
|
+
* <FileUpload.Root maxFiles={3}>
|
|
18
|
+
* <FileUpload.Dropzone>Drop here <FileUpload.Trigger>or browse</FileUpload.Trigger></FileUpload.Dropzone>
|
|
19
|
+
* <FileUpload.ItemGroup>
|
|
20
|
+
* <FileUpload.Context>{(api) => api.acceptedFiles.map((file) => (
|
|
21
|
+
* <FileUpload.Item key={file.name} file={file}>
|
|
22
|
+
* <FileUpload.ItemName /> <FileUpload.ItemSizeText /> <FileUpload.ItemDeleteTrigger>×</FileUpload.ItemDeleteTrigger>
|
|
23
|
+
* </FileUpload.Item>
|
|
24
|
+
* ))}</FileUpload.Context>
|
|
25
|
+
* </FileUpload.ItemGroup>
|
|
26
|
+
* <FileUpload.HiddenInput />
|
|
27
|
+
* </FileUpload.Root>
|
|
28
|
+
*/
|
|
29
|
+
import { type ReactNode } from 'react';
|
|
30
|
+
import { FileUpload as Ark } from '@ark-ui/react/file-upload';
|
|
31
|
+
import 'iconify-icon';
|
|
32
|
+
import { type Classes } from '../utils.js';
|
|
33
|
+
import '../theme.css';
|
|
34
|
+
import './file-upload.css';
|
|
35
|
+
export type FileUploadProps = Omit<Ark.RootProps, 'acceptedFiles' | 'defaultAcceptedFiles' | 'onFileAccept' | 'label'> & {
|
|
36
|
+
label?: ReactNode;
|
|
37
|
+
files?: File[];
|
|
38
|
+
defaultFiles?: File[];
|
|
39
|
+
onFilesChange?: (files: File[]) => void;
|
|
40
|
+
hint?: ReactNode;
|
|
41
|
+
classes?: Classes<'root' | 'label' | 'dropzone' | 'icon' | 'prompt' | 'trigger' | 'hint' | 'itemGroup' | 'item' | 'itemPreview' | 'itemName' | 'size' | 'reason' | 'itemDeleteTrigger'>;
|
|
42
|
+
};
|
|
43
|
+
declare function FileUploadShortcut({ label, files, defaultFiles, onFilesChange, maxFiles, accept, minFileSize, maxFileSize, validate, hint, className, classes, ...rest }: FileUploadProps): import("react").JSX.Element;
|
|
44
|
+
export declare const FileUpload: typeof FileUploadShortcut & {
|
|
45
|
+
Root: import("react").ForwardRefExoticComponent<Ark.RootProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
46
|
+
Label: import("react").ForwardRefExoticComponent<Ark.LabelProps & import("react").RefAttributes<HTMLLabelElement>>;
|
|
47
|
+
Dropzone: import("react").ForwardRefExoticComponent<Ark.DropzoneProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
48
|
+
Trigger: import("react").ForwardRefExoticComponent<Ark.TriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
49
|
+
ItemGroup: import("react").ForwardRefExoticComponent<Ark.ItemGroupProps & import("react").RefAttributes<HTMLUListElement>>;
|
|
50
|
+
Item: import("react").ForwardRefExoticComponent<Ark.ItemProps & import("react").RefAttributes<HTMLLIElement>>;
|
|
51
|
+
ItemPreview: import("react").ForwardRefExoticComponent<Ark.ItemPreviewProps & import("react").RefAttributes<HTMLImageElement>>;
|
|
52
|
+
ItemPreviewImage: import("react").ForwardRefExoticComponent<Ark.ItemPreviewImageProps & import("react").RefAttributes<HTMLImageElement>>;
|
|
53
|
+
ItemName: import("react").ForwardRefExoticComponent<Ark.ItemNameProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
54
|
+
ItemSizeText: import("react").ForwardRefExoticComponent<Ark.ItemSizeTextProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
55
|
+
ItemDeleteTrigger: import("react").ForwardRefExoticComponent<Ark.ItemDeleteTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
56
|
+
ClearTrigger: import("react").ForwardRefExoticComponent<Ark.ClearTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
57
|
+
HiddenInput: import("react").ForwardRefExoticComponent<Ark.HiddenInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
58
|
+
Context: (props: Ark.ContextProps) => ReactNode;
|
|
59
|
+
};
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A file upload: drop files on it or browse, then see and remove them.
|
|
4
|
+
*
|
|
5
|
+
* <FileUpload label="Worksheets" files={files} onFilesChange={setFiles} accept=".pdf" maxFiles={5} hint="PDFs, up to 5" />
|
|
6
|
+
*
|
|
7
|
+
* - `files`, `defaultFiles`: the accepted Files; `onFilesChange`: called with them
|
|
8
|
+
* - `accept`: types to allow ('image/*', '.pdf', or an array of them)
|
|
9
|
+
* - `maxFiles`: how many (1); past it, the first that fit are taken and the
|
|
10
|
+
* rest listed as "Too many files"; `minFileSize`, `maxFileSize`: in bytes
|
|
11
|
+
* - `hint`: text under the prompt
|
|
12
|
+
* - `validate(file, details)`: your own check, returning error codes for a
|
|
13
|
+
* file it refuses (or null), as Ark's
|
|
14
|
+
* - `name`: for plain form posts
|
|
15
|
+
*
|
|
16
|
+
* By hand, with Ark's parts:
|
|
17
|
+
*
|
|
18
|
+
* <FileUpload.Root maxFiles={3}>
|
|
19
|
+
* <FileUpload.Dropzone>Drop here <FileUpload.Trigger>or browse</FileUpload.Trigger></FileUpload.Dropzone>
|
|
20
|
+
* <FileUpload.ItemGroup>
|
|
21
|
+
* <FileUpload.Context>{(api) => api.acceptedFiles.map((file) => (
|
|
22
|
+
* <FileUpload.Item key={file.name} file={file}>
|
|
23
|
+
* <FileUpload.ItemName /> <FileUpload.ItemSizeText /> <FileUpload.ItemDeleteTrigger>×</FileUpload.ItemDeleteTrigger>
|
|
24
|
+
* </FileUpload.Item>
|
|
25
|
+
* ))}</FileUpload.Context>
|
|
26
|
+
* </FileUpload.ItemGroup>
|
|
27
|
+
* <FileUpload.HiddenInput />
|
|
28
|
+
* </FileUpload.Root>
|
|
29
|
+
*/
|
|
30
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
31
|
+
import { FileUpload as Ark } from '@ark-ui/react/file-upload';
|
|
32
|
+
import { createFileTypeValidator, getAcceptAttrString, getFileKey, isValidFileSize } from '@zag-js/file-utils';
|
|
33
|
+
import { useFieldContext } from '@ark-ui/react/field';
|
|
34
|
+
import 'iconify-icon';
|
|
35
|
+
import { cx, partsOf, useControllable, withParts } from '../utils.js';
|
|
36
|
+
import { useLocale, useMessages } from '../format/LocaleProvider.js';
|
|
37
|
+
import { formatBytes } from '../format/format.js';
|
|
38
|
+
import '../theme.css';
|
|
39
|
+
import './file-upload.css';
|
|
40
|
+
// Object URLs for image previews, revoked when the files go.
|
|
41
|
+
function usePreviews(files) {
|
|
42
|
+
const urls = useMemo(() => new Map(files.filter((f) => f.type.startsWith('image/')).map((f) => [f, URL.createObjectURL(f)])), [files]);
|
|
43
|
+
useEffect(() => () => urls.forEach((url) => URL.revokeObjectURL(url)), [urls]);
|
|
44
|
+
return urls;
|
|
45
|
+
}
|
|
46
|
+
function FileUploadShortcut({ label = null, files, defaultFiles, onFilesChange, maxFiles = 1, accept, minFileSize, maxFileSize, validate, hint = null, className, classes, ...rest }) {
|
|
47
|
+
const t = useMessages();
|
|
48
|
+
const locale = useLocale();
|
|
49
|
+
const field = useFieldContext();
|
|
50
|
+
const part = partsOf('file-upload', classes);
|
|
51
|
+
const [accepted, setAccepted] = useControllable(files, defaultFiles ?? [], undefined);
|
|
52
|
+
const previews = usePreviews(accepted);
|
|
53
|
+
// From onFileAccept, which Zag calls whenever the accepted files change.
|
|
54
|
+
// (onFileChange also fires for rejections, carrying the accepted files as
|
|
55
|
+
// they were before this render: it would undo a drop of good and bad files.)
|
|
56
|
+
function acceptFiles(details) {
|
|
57
|
+
setAccepted(details.files);
|
|
58
|
+
onFilesChange?.(details.files);
|
|
59
|
+
}
|
|
60
|
+
// Zag rejects a whole drop that would go past maxFiles. Instead it gets no
|
|
61
|
+
// limit, and this takes files in order until there's no room: Zag checks
|
|
62
|
+
// each dropped file in turn, so it counts the ones Zag would take (the right
|
|
63
|
+
// type and size, not already there) through one drop.
|
|
64
|
+
const fitsType = useMemo(() => createFileTypeValidator(getAcceptAttrString(accept)), [accept]);
|
|
65
|
+
const drop = useRef({ taken: 0, open: false });
|
|
66
|
+
const limit = (file, details) => {
|
|
67
|
+
const own = validate?.(file, details);
|
|
68
|
+
if (own?.length)
|
|
69
|
+
return own;
|
|
70
|
+
if (maxFiles <= 1)
|
|
71
|
+
return null;
|
|
72
|
+
if (!drop.current.open) {
|
|
73
|
+
drop.current = { taken: 0, open: true };
|
|
74
|
+
queueMicrotask(() => { drop.current.open = false; });
|
|
75
|
+
}
|
|
76
|
+
const takes = fitsType(file)[0] && isValidFileSize(file, minFileSize, maxFileSize)[0]
|
|
77
|
+
&& !details.acceptedFiles.some((f) => getFileKey(f) === getFileKey(file));
|
|
78
|
+
if (!takes)
|
|
79
|
+
return null;
|
|
80
|
+
if (details.acceptedFiles.length + drop.current.taken >= maxFiles)
|
|
81
|
+
return ['TOO_MANY_FILES'];
|
|
82
|
+
drop.current.taken++;
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
return (_jsxs(Ark.Root, { ...(t.zag.fileUpload && { translations: t.zag.fileUpload }), ...rest, ...part('root'), className: cx(className, classes?.root), maxFiles: maxFiles > 1 ? Number.MAX_SAFE_INTEGER : maxFiles, validate: limit, ...(accept !== undefined && { accept }), ...(minFileSize !== undefined && { minFileSize }), ...(maxFileSize !== undefined && { maxFileSize }), acceptedFiles: accepted, onFileAccept: acceptFiles, children: [label && !field && _jsx(Ark.Label, { ...part('label'), children: label }), _jsxs(Ark.Dropzone, { ...part('dropzone'), children: [_jsx("iconify-icon", { ...part('icon'), icon: "lucide:upload", "aria-hidden": "true" }), _jsxs("span", { ...part('prompt'), children: [t.fileUpload.prompt(maxFiles > 1), " ", _jsx(Ark.Trigger, { ...part('trigger'), children: t.fileUpload.browse })] }), hint && _jsx("span", { ...part('hint'), children: hint })] }), _jsx(Ark.HiddenInput, {}), _jsx(Ark.Context, { children: (api) => _jsxs(_Fragment, { children: [api.acceptedFiles.length > 0 && _jsx(Ark.ItemGroup, { type: "accepted", ...part('item-group'), children: api.acceptedFiles.map((file) => (_jsxs(Ark.Item, { file: file, ...part('item'), children: [_jsx(Ark.ItemPreview, { type: ".*", ...part('item-preview'), children: previews.has(file) ? _jsx("img", { src: previews.get(file), alt: "" }) : _jsx("iconify-icon", { icon: "lucide:file", "aria-hidden": "true" }) }), _jsx(Ark.ItemName, { ...part('item-name') }), _jsx("span", { ...part('size'), children: formatBytes(file.size, { locale }) }), _jsx(Ark.ItemDeleteTrigger, { ...part('item-delete-trigger'), children: _jsx("iconify-icon", { icon: "lucide:x" }) })] }, `${file.name}:${file.lastModified}`))) }), api.rejectedFiles.length > 0 && _jsx(Ark.ItemGroup, { type: "rejected", ...part('item-group'), children: api.rejectedFiles.map(({ file, errors }) => (_jsxs(Ark.Item, { file: file, ...part('item'), "data-rejected": "", children: [_jsx(Ark.ItemPreview, { type: ".*", ...part('item-preview'), children: _jsx("iconify-icon", { icon: "lucide:file-x", "aria-hidden": "true" }) }), _jsx(Ark.ItemName, { ...part('item-name') }), _jsx("span", { ...part('reason'), children: errors.map((e) => t.fileUpload.reasons[e] ?? e).join(', ') }), _jsx(Ark.ItemDeleteTrigger, { ...part('item-delete-trigger'), children: _jsx("iconify-icon", { icon: "lucide:x" }) })] }, `${file.name}:${file.lastModified}`))) })] }) })] }));
|
|
86
|
+
}
|
|
87
|
+
export const FileUpload = withParts(FileUploadShortcut, {
|
|
88
|
+
Root: Ark.Root,
|
|
89
|
+
Label: Ark.Label,
|
|
90
|
+
Dropzone: Ark.Dropzone,
|
|
91
|
+
Trigger: Ark.Trigger,
|
|
92
|
+
ItemGroup: Ark.ItemGroup,
|
|
93
|
+
Item: Ark.Item,
|
|
94
|
+
ItemPreview: Ark.ItemPreview,
|
|
95
|
+
ItemPreviewImage: Ark.ItemPreviewImage,
|
|
96
|
+
ItemName: Ark.ItemName,
|
|
97
|
+
ItemSizeText: Ark.ItemSizeText,
|
|
98
|
+
ItemDeleteTrigger: Ark.ItemDeleteTrigger,
|
|
99
|
+
ClearTrigger: Ark.ClearTrigger,
|
|
100
|
+
HiddenInput: Ark.HiddenInput,
|
|
101
|
+
Context: Ark.Context,
|
|
102
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
@layer ui {
|
|
2
|
+
[data-scope=file-upload][data-part=root] { display: block; min-width: 0; color: var(--ui-text); font-family: var(--ui-font); }
|
|
3
|
+
[data-scope=file-upload][data-part=label] { display: block; font-size: 14px; font-weight: 500; margin-bottom: 0.375rem; }
|
|
4
|
+
[data-scope=file-upload][data-part=dropzone] { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.375rem;
|
|
5
|
+
padding: 1.5rem; box-sizing: border-box; text-align: center; background: var(--ui-surface);
|
|
6
|
+
border: 1px dashed var(--ui-border); border-radius: var(--ui-radius); font-size: 14px; cursor: pointer; outline: none;
|
|
7
|
+
&:hover { border-color: var(--ui-muted); }
|
|
8
|
+
&:focus-visible { border-color: var(--ui-ring); outline: 2px solid var(--ui-ring-soft); }
|
|
9
|
+
&[data-dragging] { background: var(--ui-accent-soft); border-color: var(--ui-accent); }
|
|
10
|
+
&:is([data-invalid], [aria-invalid=true]) { border-color: var(--ui-danger); }
|
|
11
|
+
&[data-disabled] { opacity: 0.5; cursor: not-allowed; } }
|
|
12
|
+
[data-scope=file-upload][data-part=icon] { font-size: 20px; color: var(--ui-muted); }
|
|
13
|
+
[data-scope=file-upload][data-part=trigger] { padding: 0; border: none; background: transparent; color: var(--ui-accent);
|
|
14
|
+
font: inherit; font-weight: 500; cursor: pointer;
|
|
15
|
+
&:hover { text-decoration: underline; } }
|
|
16
|
+
[data-scope=file-upload][data-part=hint] { color: var(--ui-muted); font-size: 12px; }
|
|
17
|
+
[data-scope=file-upload][data-part=item-group] { display: flex; flex-direction: column; gap: 0.5rem; margin: 0.75rem 0 0; padding: 0; list-style: none;
|
|
18
|
+
/* Rejected files follow the accepted ones in a second list. */
|
|
19
|
+
& + & { margin-top: 0.5rem; } }
|
|
20
|
+
[data-scope=file-upload][data-part=item] { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0.25rem 0.5rem 0.5rem;
|
|
21
|
+
border: 1px solid var(--ui-border); border-radius: var(--ui-radius); font-size: 14px; }
|
|
22
|
+
[data-scope=file-upload][data-part=item-preview] { display: grid; place-items: center; flex-shrink: 0; width: 2rem; height: 2rem;
|
|
23
|
+
border-radius: 4px; overflow: hidden; background: var(--ui-hover); color: var(--ui-muted);
|
|
24
|
+
[data-rejected] > & { color: var(--ui-danger); }
|
|
25
|
+
img { width: 100%; height: 100%; object-fit: cover; } }
|
|
26
|
+
[data-scope=file-upload][data-part=item-name] { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
27
|
+
[data-scope=file-upload]:is([data-part=size], [data-part=reason]) { font-size: 12px; white-space: nowrap; color: var(--ui-muted); }
|
|
28
|
+
[data-scope=file-upload][data-part=reason] { color: var(--ui-danger); }
|
|
29
|
+
[data-scope=file-upload][data-part=item-delete-trigger] { display: grid; place-items: center; flex-shrink: 0; width: 1.75rem; height: 1.75rem;
|
|
30
|
+
border: none; background: transparent; border-radius: 4px; color: var(--ui-muted); cursor: pointer;
|
|
31
|
+
&:hover { background: var(--ui-hover); color: var(--ui-text); } }
|
|
32
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a number for people (in a <data> element, which keeps the raw value):
|
|
3
|
+
* currency, percent, units, compact.
|
|
4
|
+
*
|
|
5
|
+
* <FormatNumber value={1820} style="currency" currency="GBP" />
|
|
6
|
+
* <FormatNumber value={0.125} style="percent" />
|
|
7
|
+
* <FormatNumber value={12400} notation="compact" />
|
|
8
|
+
*
|
|
9
|
+
* - `value`; and Intl.NumberFormat's options as props: `style` ('decimal',
|
|
10
|
+
* 'currency', 'percent', 'unit'), `currency`, `unit`, `notation`,
|
|
11
|
+
* `minimumFractionDigits`, `maximumFractionDigits`, `signDisplay`…
|
|
12
|
+
* (`style` is Intl's, as in Ark's Format.Number: style it with `className`)
|
|
13
|
+
* - `locale`: the LocaleProvider's (or the browser's) by default
|
|
14
|
+
*
|
|
15
|
+
* Also FormatBytes (`value` in bytes: '2.4 MB'; `binary` for MiB) and
|
|
16
|
+
* FormatRelative (`value` a date: '2 hours ago', kept up to date; in a
|
|
17
|
+
* <time>). As functions, for strings: formatNumber, formatBytes, formatRelative.
|
|
18
|
+
*/
|
|
19
|
+
import { type HTMLAttributes } from 'react';
|
|
20
|
+
type Base = Omit<HTMLAttributes<HTMLElement>, 'children' | 'style'> & {
|
|
21
|
+
locale?: string;
|
|
22
|
+
className?: string;
|
|
23
|
+
};
|
|
24
|
+
export type FormatNumberProps = Base & Omit<Intl.NumberFormatOptions, 'style'> & {
|
|
25
|
+
value: number | bigint | null | undefined;
|
|
26
|
+
style?: Intl.NumberFormatOptions['style'];
|
|
27
|
+
};
|
|
28
|
+
export declare function FormatNumber({ value, locale, style, className, ...props }: FormatNumberProps): import("react").JSX.Element;
|
|
29
|
+
export type FormatBytesProps = Base & {
|
|
30
|
+
value: number | null | undefined;
|
|
31
|
+
binary?: boolean;
|
|
32
|
+
maximumFractionDigits?: number;
|
|
33
|
+
};
|
|
34
|
+
export declare function FormatBytes({ value, locale, binary, maximumFractionDigits, className, ...rest }: FormatBytesProps): import("react").JSX.Element;
|
|
35
|
+
export type FormatRelativeProps = Base & {
|
|
36
|
+
value: Date | string | number | null | undefined;
|
|
37
|
+
numeric?: 'auto' | 'always';
|
|
38
|
+
live?: boolean;
|
|
39
|
+
};
|
|
40
|
+
/** A time relative to now, kept up to date (`live`, default true: each 30 seconds). */
|
|
41
|
+
export declare function FormatRelative({ value, locale, numeric, live, className, ...rest }: FormatRelativeProps): import("react").JSX.Element;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Formats a number for people (in a <data> element, which keeps the raw value):
|
|
4
|
+
* currency, percent, units, compact.
|
|
5
|
+
*
|
|
6
|
+
* <FormatNumber value={1820} style="currency" currency="GBP" />
|
|
7
|
+
* <FormatNumber value={0.125} style="percent" />
|
|
8
|
+
* <FormatNumber value={12400} notation="compact" />
|
|
9
|
+
*
|
|
10
|
+
* - `value`; and Intl.NumberFormat's options as props: `style` ('decimal',
|
|
11
|
+
* 'currency', 'percent', 'unit'), `currency`, `unit`, `notation`,
|
|
12
|
+
* `minimumFractionDigits`, `maximumFractionDigits`, `signDisplay`…
|
|
13
|
+
* (`style` is Intl's, as in Ark's Format.Number: style it with `className`)
|
|
14
|
+
* - `locale`: the LocaleProvider's (or the browser's) by default
|
|
15
|
+
*
|
|
16
|
+
* Also FormatBytes (`value` in bytes: '2.4 MB'; `binary` for MiB) and
|
|
17
|
+
* FormatRelative (`value` a date: '2 hours ago', kept up to date; in a
|
|
18
|
+
* <time>). As functions, for strings: formatNumber, formatBytes, formatRelative.
|
|
19
|
+
*/
|
|
20
|
+
import { useEffect, useState } from 'react';
|
|
21
|
+
import { formatBytes, formatNumber, formatRelative } from './format.js';
|
|
22
|
+
import { useLocale } from './LocaleProvider.js';
|
|
23
|
+
import { cx, partsOf } from '../utils.js';
|
|
24
|
+
// Intl's options, taken out of the props (the rest go on the element).
|
|
25
|
+
const intlKeys = ['currency', 'currencyDisplay', 'currencySign', 'unit', 'unitDisplay', 'notation', 'compactDisplay', 'signDisplay',
|
|
26
|
+
'useGrouping', 'minimumIntegerDigits', 'minimumFractionDigits', 'maximumFractionDigits', 'minimumSignificantDigits',
|
|
27
|
+
'maximumSignificantDigits', 'roundingMode', 'roundingIncrement', 'trailingZeroDisplay', 'numberingSystem'];
|
|
28
|
+
export function FormatNumber({ value, locale, style, className, ...props }) {
|
|
29
|
+
const app = useLocale();
|
|
30
|
+
const part = partsOf('format');
|
|
31
|
+
const options = { ...(style && { style }) };
|
|
32
|
+
const rest = {};
|
|
33
|
+
for (const [key, v] of Object.entries(props)) {
|
|
34
|
+
if (intlKeys.includes(key))
|
|
35
|
+
options[key] = v;
|
|
36
|
+
else
|
|
37
|
+
rest[key] = v;
|
|
38
|
+
}
|
|
39
|
+
return _jsx("data", { ...rest, ...part('number'), className: cx(className), value: value == null ? '' : String(value), children: formatNumber(value, { ...options, locale: locale ?? app }) });
|
|
40
|
+
}
|
|
41
|
+
export function FormatBytes({ value, locale, binary, maximumFractionDigits, className, ...rest }) {
|
|
42
|
+
const app = useLocale();
|
|
43
|
+
const part = partsOf('format');
|
|
44
|
+
return _jsx("data", { ...rest, ...part('bytes'), className: cx(className), value: value == null ? '' : String(value), children: formatBytes(value, { locale: locale ?? app, binary, maximumFractionDigits }) });
|
|
45
|
+
}
|
|
46
|
+
/** A time relative to now, kept up to date (`live`, default true: each 30 seconds). */
|
|
47
|
+
export function FormatRelative({ value, locale, numeric, live = true, className, ...rest }) {
|
|
48
|
+
const app = useLocale();
|
|
49
|
+
const part = partsOf('format');
|
|
50
|
+
const [now, setNow] = useState(() => new Date());
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!live)
|
|
53
|
+
return;
|
|
54
|
+
const timer = setInterval(() => setNow(new Date()), 30000);
|
|
55
|
+
return () => clearInterval(timer);
|
|
56
|
+
}, [live]);
|
|
57
|
+
const when = value == null ? null : value instanceof Date ? value : new Date(value);
|
|
58
|
+
return (_jsx("time", { ...rest, ...part('relative'), className: cx(className), dateTime: when && !Number.isNaN(when.getTime()) ? when.toISOString() : undefined, title: when ? when.toLocaleString(locale ?? app) : undefined, children: formatRelative(when, { locale: locale ?? app, now, numeric }) }));
|
|
59
|
+
}
|