sherick-ui 2.0.0 → 2.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 +55 -199
- package/dist/cjs/components/Breadcrumb.cjs +47 -0
- package/dist/cjs/components/Breadcrumb.cjs.map +1 -0
- package/dist/cjs/components/Calendar.cjs +298 -0
- package/dist/cjs/components/Calendar.cjs.map +1 -0
- package/dist/cjs/components/Combobox.cjs +1 -1
- package/dist/cjs/components/Combobox.cjs.map +1 -1
- package/dist/cjs/components/Command.cjs +81 -0
- package/dist/cjs/components/Command.cjs.map +1 -0
- package/dist/cjs/components/CommandPalette.cjs +52 -0
- package/dist/cjs/components/CommandPalette.cjs.map +1 -0
- package/dist/cjs/components/DatePicker.cjs +107 -0
- package/dist/cjs/components/DatePicker.cjs.map +1 -0
- package/dist/cjs/components/DateRangePicker.cjs +165 -0
- package/dist/cjs/components/DateRangePicker.cjs.map +1 -0
- package/dist/cjs/components/DialogSurface.cjs +6 -5
- package/dist/cjs/components/DialogSurface.cjs.map +1 -1
- package/dist/cjs/components/FileUpload.cjs +203 -0
- package/dist/cjs/components/FileUpload.cjs.map +1 -0
- package/dist/cjs/components/Media.cjs +27 -0
- package/dist/cjs/components/Media.cjs.map +1 -0
- package/dist/cjs/components/Pagination.cjs +115 -0
- package/dist/cjs/components/Pagination.cjs.map +1 -0
- package/dist/cjs/components/Select.cjs +1 -1
- package/dist/cjs/components/Select.cjs.map +1 -1
- package/dist/cjs/components/Stepper.cjs +40 -0
- package/dist/cjs/components/Stepper.cjs.map +1 -0
- package/dist/cjs/components/Tabs.cjs +1 -1
- package/dist/cjs/components/Tabs.cjs.map +1 -1
- package/dist/cjs/components/TreeView.cjs +247 -0
- package/dist/cjs/components/TreeView.cjs.map +1 -0
- package/dist/cjs/components/date-family.cjs +191 -0
- package/dist/cjs/components/date-family.cjs.map +1 -0
- package/dist/cjs/components/date-field.cjs +90 -0
- package/dist/cjs/components/date-field.cjs.map +1 -0
- package/dist/cjs/components/fileUpload.rules.cjs +121 -0
- package/dist/cjs/components/fileUpload.rules.cjs.map +1 -0
- package/dist/cjs/components/tree.cjs +95 -0
- package/dist/cjs/components/tree.cjs.map +1 -0
- package/dist/cjs/components/ui.common.cjs +27 -7
- package/dist/cjs/components/ui.common.cjs.map +1 -1
- package/dist/cjs/components/ui.motion.cjs +3 -3
- package/dist/cjs/components/ui.motion.cjs.map +1 -1
- package/dist/cjs/dev.cjs +5 -0
- package/dist/cjs/dev.cjs.map +1 -1
- package/dist/cjs/index.cjs +22 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/components/Breadcrumb.js +43 -0
- package/dist/esm/components/Breadcrumb.js.map +1 -0
- package/dist/esm/components/Calendar.js +294 -0
- package/dist/esm/components/Calendar.js.map +1 -0
- package/dist/esm/components/Combobox.js +2 -2
- package/dist/esm/components/Combobox.js.map +1 -1
- package/dist/esm/components/Command.js +77 -0
- package/dist/esm/components/Command.js.map +1 -0
- package/dist/esm/components/CommandPalette.js +48 -0
- package/dist/esm/components/CommandPalette.js.map +1 -0
- package/dist/esm/components/DatePicker.js +103 -0
- package/dist/esm/components/DatePicker.js.map +1 -0
- package/dist/esm/components/DateRangePicker.js +161 -0
- package/dist/esm/components/DateRangePicker.js.map +1 -0
- package/dist/esm/components/DialogSurface.js +6 -5
- package/dist/esm/components/DialogSurface.js.map +1 -1
- package/dist/esm/components/FileUpload.js +199 -0
- package/dist/esm/components/FileUpload.js.map +1 -0
- package/dist/esm/components/Media.js +25 -0
- package/dist/esm/components/Media.js.map +1 -0
- package/dist/esm/components/Pagination.js +111 -0
- package/dist/esm/components/Pagination.js.map +1 -0
- package/dist/esm/components/Select.js +2 -2
- package/dist/esm/components/Select.js.map +1 -1
- package/dist/esm/components/Stepper.js +36 -0
- package/dist/esm/components/Stepper.js.map +1 -0
- package/dist/esm/components/Tabs.js +2 -2
- package/dist/esm/components/Tabs.js.map +1 -1
- package/dist/esm/components/TreeView.js +243 -0
- package/dist/esm/components/TreeView.js.map +1 -0
- package/dist/esm/components/date-family.js +173 -0
- package/dist/esm/components/date-family.js.map +1 -0
- package/dist/esm/components/date-field.js +84 -0
- package/dist/esm/components/date-field.js.map +1 -0
- package/dist/esm/components/fileUpload.rules.js +114 -0
- package/dist/esm/components/fileUpload.rules.js.map +1 -0
- package/dist/esm/components/tree.js +88 -0
- package/dist/esm/components/tree.js.map +1 -0
- package/dist/esm/components/ui.common.js +23 -8
- package/dist/esm/components/ui.common.js.map +1 -1
- package/dist/esm/components/ui.motion.js +3 -3
- package/dist/esm/components/ui.motion.js.map +1 -1
- package/dist/esm/dev.js +1 -1
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/styles.css +365 -20
- package/dist/theme.css +15 -9
- package/dist/types/dev.d.cts +8 -3
- package/dist/types/dev.d.ts +8 -3
- package/dist/types/index.d.cts +600 -66
- package/dist/types/index.d.ts +600 -66
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
import { Popover } from '@base-ui/react/popover';
|
|
4
|
+
import { CalendarDays } from 'lucide-react';
|
|
5
|
+
import { useRef, useEffect } from 'react';
|
|
6
|
+
import { cn } from '../libs/utils.js';
|
|
7
|
+
import { density, shape, text, stateLayer, state, focusRingWithin, material, overlay, stacking } from './ui.common.js';
|
|
8
|
+
import { motionInkPress, motionFeedback, motionPresenceAnchored } from './ui.motion.js';
|
|
9
|
+
|
|
10
|
+
/* The parts the two date pickers share.
|
|
11
|
+
====================================
|
|
12
|
+
A picker is a native `type=date` field with a named control beside it that opens the calendar,
|
|
13
|
+
so the field's surface, its trailing control and the popup that holds the grid have to be one
|
|
14
|
+
definition rather than two that drift. This module is private to the date family: nothing here
|
|
15
|
+
is published, and neither picker adds a shell of its own.
|
|
16
|
+
|
|
17
|
+
`DatePicker` and `DateRangePicker` compose Base `Field` for the label/description/error
|
|
18
|
+
relationship and Base `Popover` for the popup; what lives here is only the visual and
|
|
19
|
+
structural arrangement those compositions hold in common. */
|
|
20
|
+
/**
|
|
21
|
+
* The one field surface the date family uses: a borderless matte field whose focus treatment
|
|
22
|
+
* wraps the whole row, exactly as the library's other composite fields do.
|
|
23
|
+
*
|
|
24
|
+
* The row carries `group/field` and its own `data-disabled` marker so the trailing control's dim
|
|
25
|
+
* is neutralised inside an already dimmed field — a disabled field dims once, at the field.
|
|
26
|
+
*/
|
|
27
|
+
const dateFieldRowClassName = (disabled) => cn("group/field flex min-w-0 w-full items-center ps-5 pe-3", density.normal, shape.control, material.control, motionFeedback, focusRingWithin, state.field.invalid, !disabled && state.field.hover, !disabled && state.field.focusWithin, !disabled && state.field.invalidHover, !disabled && state.field.invalidFocusWithin, disabled ? state.disabledDescendant : state.text);
|
|
28
|
+
/** Native date entry remains intact. Its trailing browser affordance sits outside the clipped
|
|
29
|
+
* input slot, leaving one visible calendar trigger in every engine (Firefox does not expose
|
|
30
|
+
* a picker-indicator pseudo-element). The composite row owns focus, not the clipped input. */
|
|
31
|
+
const dateInputClassName = "block w-[calc(100%+2.5rem)] min-w-0 bg-transparent py-3 text-inherit outline-none disabled:cursor-not-allowed";
|
|
32
|
+
/* The 36×44 target holds a concentric 32px hover surface, leaving air inside the 48px field.
|
|
33
|
+
Only the icon presses: the adjacent native text entry never activates this button. */
|
|
34
|
+
const datePartClassName = `group inline-flex shrink-0 items-center justify-center ${density.part} ${shape.circle} ${text.medium} ${motionFeedback} ${stateLayer.quiet} before:inset-x-0.5 before:inset-y-1.5 ${state.enabled} ${state.disabledPart} [&:not([data-disabled]):not(:disabled)]:hover:text-sherick-ink`;
|
|
35
|
+
/**
|
|
36
|
+
* The named control that opens the calendar. It is a Base `Popover.Trigger` rendered as a native
|
|
37
|
+
* button, so Base owns the relationship it publishes on the field (`aria-haspopup`,
|
|
38
|
+
* `aria-expanded`, `aria-controls`) and the pointer and keyboard activation that opens the
|
|
39
|
+
* surface; the button itself only carries the type it needs to stay out of form submission.
|
|
40
|
+
*/
|
|
41
|
+
const DateFieldTrigger = ({ label, disabled = false }) => (jsx(Popover.Trigger, { disabled: disabled, render: jsx("button", { type: "button", "aria-label": label, disabled: disabled, className: cn(datePartClassName) }), children: jsx("span", { className: cn("inline-flex size-5 items-center justify-center [&>svg]:size-5", !disabled && motionInkPress), children: jsx(CalendarDays, { "aria-hidden": "true" }) }) }));
|
|
42
|
+
/**
|
|
43
|
+
* The calendar's popup: the same shell `Popover.Content` renders, composed straight from Base's
|
|
44
|
+
* public parts because the grid needs one thing the published shell does not offer — `initialFocus`
|
|
45
|
+
* aimed at the day the reader is on, so the grid is usable from the moment it opens.
|
|
46
|
+
*
|
|
47
|
+
* Base owns the portal, the placement and collision handling, the outside-interaction and Escape
|
|
48
|
+
* dismissal and the focus restoration; the surface is non-modal, so the rest of the page stays
|
|
49
|
+
* reachable. Sherick owns only the sheet and its presence.
|
|
50
|
+
*/
|
|
51
|
+
const DateCalendarPopup = ({ title, children, anchor }) => {
|
|
52
|
+
const popupRef = useRef(null);
|
|
53
|
+
return (jsx(Popover.Portal, { children: jsx(Popover.Positioner, { anchor: anchor, side: "bottom", align: "start", sideOffset: 8, className: cn(stacking.float), children: jsxs(Popover.Popup, { ref: popupRef,
|
|
54
|
+
/* The roving tab stop is the calendar's own active day, so this is exactly the cell a
|
|
55
|
+
keyboard reader would land on. `null` leaves Base's default focus behaviour in place
|
|
56
|
+
if the grid has no tab stop to give. */
|
|
57
|
+
initialFocus: () => { var _a, _b; return (_b = (_a = popupRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('[data-day][tabindex="0"]')) !== null && _b !== void 0 ? _b : null; }, className: cn("w-max max-w-[min(24rem,var(--available-width))] max-h-[var(--available-height)] overflow-y-auto p-3 [overflow-wrap:anywhere]", overlay.popup, motionPresenceAnchored), children: [jsx(Popover.Title, { className: cn("sr-only"), children: title }), children] }) }) }));
|
|
58
|
+
};
|
|
59
|
+
/** Base Field controls entry and validity, but native reset emits no input/change event.
|
|
60
|
+
* Bridge only the date family's domain state, after the owning form's cancelable reset. */
|
|
61
|
+
const useDateFormReset = (inputRef, formId, restore) => {
|
|
62
|
+
const latestRestore = useRef(restore);
|
|
63
|
+
useEffect(() => { latestRestore.current = restore; }, [restore]);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
var _a;
|
|
66
|
+
const form = (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.form;
|
|
67
|
+
if (!form)
|
|
68
|
+
return;
|
|
69
|
+
const reset = (event) => {
|
|
70
|
+
// A microtask can run between native listeners, before React's delegated onReset.
|
|
71
|
+
// The next task observes cancellation after the entire native dispatch/default action.
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
var _a;
|
|
74
|
+
if (!event.defaultPrevented && ((_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.form) === form)
|
|
75
|
+
latestRestore.current();
|
|
76
|
+
}, 0);
|
|
77
|
+
};
|
|
78
|
+
form.addEventListener("reset", reset);
|
|
79
|
+
return () => form.removeEventListener("reset", reset);
|
|
80
|
+
}, [inputRef, formId]);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export { DateCalendarPopup, DateFieldTrigger, dateFieldRowClassName, dateInputClassName, useDateFormReset };
|
|
84
|
+
//# sourceMappingURL=date-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date-field.js","sources":["../../../src/components/date-field.tsx"],"sourcesContent":["\"use client\";\n\nimport { Popover as BasePopover } from \"@base-ui/react/popover\";\nimport { CalendarDays } from \"lucide-react\";\nimport React, { useEffect, useRef, type ReactNode } from \"react\";\nimport { cn } from \"@/libs/utils\";\nimport { density, focusRingWithin, material, overlay, shape, stacking, state, stateLayer, text } from \"./ui.common\";\nimport { motionFeedback, motionInkPress, motionPresenceAnchored } from \"./ui.motion\";\n\n/* The parts the two date pickers share.\n ====================================\n A picker is a native `type=date` field with a named control beside it that opens the calendar,\n so the field's surface, its trailing control and the popup that holds the grid have to be one\n definition rather than two that drift. This module is private to the date family: nothing here\n is published, and neither picker adds a shell of its own.\n\n `DatePicker` and `DateRangePicker` compose Base `Field` for the label/description/error\n relationship and Base `Popover` for the popup; what lives here is only the visual and\n structural arrangement those compositions hold in common. */\n\n/**\n * The one field surface the date family uses: a borderless matte field whose focus treatment\n * wraps the whole row, exactly as the library's other composite fields do.\n *\n * The row carries `group/field` and its own `data-disabled` marker so the trailing control's dim\n * is neutralised inside an already dimmed field — a disabled field dims once, at the field.\n */\nexport const dateFieldRowClassName = (disabled: boolean) =>\n cn(\n \"group/field flex min-w-0 w-full items-center ps-5 pe-3\",\n density.normal,\n shape.control,\n material.control,\n motionFeedback,\n focusRingWithin,\n state.field.invalid,\n !disabled && state.field.hover,\n !disabled && state.field.focusWithin,\n !disabled && state.field.invalidHover,\n !disabled && state.field.invalidFocusWithin,\n disabled ? state.disabledDescendant : state.text\n );\n\n/** Native date entry remains intact. Its trailing browser affordance sits outside the clipped\n * input slot, leaving one visible calendar trigger in every engine (Firefox does not expose\n * a picker-indicator pseudo-element). The composite row owns focus, not the clipped input. */\nexport const dateInputClassName =\n \"block w-[calc(100%+2.5rem)] min-w-0 bg-transparent py-3 text-inherit outline-none disabled:cursor-not-allowed\";\n\n/* The 36×44 target holds a concentric 32px hover surface, leaving air inside the 48px field.\n Only the icon presses: the adjacent native text entry never activates this button. */\nconst datePartClassName = `group inline-flex shrink-0 items-center justify-center ${density.part} ${shape.circle} ${text.medium} ${motionFeedback} ${stateLayer.quiet} before:inset-x-0.5 before:inset-y-1.5 ${state.enabled} ${state.disabledPart} [&:not([data-disabled]):not(:disabled)]:hover:text-sherick-ink`;\n\nexport interface DateFieldTriggerProps {\n /** The control's accessible name. It has no visible label, so it has to be named. */\n label: string;\n disabled?: boolean;\n}\n\n/**\n * The named control that opens the calendar. It is a Base `Popover.Trigger` rendered as a native\n * button, so Base owns the relationship it publishes on the field (`aria-haspopup`,\n * `aria-expanded`, `aria-controls`) and the pointer and keyboard activation that opens the\n * surface; the button itself only carries the type it needs to stay out of form submission.\n */\nexport const DateFieldTrigger = ({ label, disabled = false }: DateFieldTriggerProps) => (\n <BasePopover.Trigger\n disabled={disabled}\n render={<button type=\"button\" aria-label={label} disabled={disabled} className={cn(datePartClassName)} />}\n >\n <span className={cn(\"inline-flex size-5 items-center justify-center [&>svg]:size-5\", !disabled && motionInkPress)}>\n <CalendarDays aria-hidden=\"true\" />\n </span>\n </BasePopover.Trigger>\n);\n\nexport interface DateCalendarPopupProps {\n /** Names the popup, through Base's own Title part. */\n title: string;\n children: ReactNode;\n anchor: React.ComponentProps<typeof BasePopover.Positioner>[\"anchor\"];\n}\n\n/**\n * The calendar's popup: the same shell `Popover.Content` renders, composed straight from Base's\n * public parts because the grid needs one thing the published shell does not offer — `initialFocus`\n * aimed at the day the reader is on, so the grid is usable from the moment it opens.\n *\n * Base owns the portal, the placement and collision handling, the outside-interaction and Escape\n * dismissal and the focus restoration; the surface is non-modal, so the rest of the page stays\n * reachable. Sherick owns only the sheet and its presence.\n */\nexport const DateCalendarPopup = ({ title, children, anchor }: DateCalendarPopupProps) => {\n const popupRef = useRef<HTMLDivElement | null>(null);\n\n return (\n <BasePopover.Portal>\n <BasePopover.Positioner\n anchor={anchor}\n side=\"bottom\"\n align=\"start\"\n sideOffset={8}\n className={cn(stacking.float)}\n >\n <BasePopover.Popup\n ref={popupRef}\n /* The roving tab stop is the calendar's own active day, so this is exactly the cell a\n keyboard reader would land on. `null` leaves Base's default focus behaviour in place\n if the grid has no tab stop to give. */\n initialFocus={() =>\n popupRef.current?.querySelector<HTMLElement>('[data-day][tabindex=\"0\"]') ?? null\n }\n className={cn(\n \"w-max max-w-[min(24rem,var(--available-width))] max-h-[var(--available-height)] overflow-y-auto p-3 [overflow-wrap:anywhere]\",\n overlay.popup,\n motionPresenceAnchored\n )}\n >\n <BasePopover.Title className={cn(\"sr-only\")}>{title}</BasePopover.Title>\n {children}\n </BasePopover.Popup>\n </BasePopover.Positioner>\n </BasePopover.Portal>\n );\n};\n\n/** Base Field controls entry and validity, but native reset emits no input/change event.\n * Bridge only the date family's domain state, after the owning form's cancelable reset. */\nexport const useDateFormReset = (\n inputRef: React.RefObject<HTMLInputElement | null>,\n formId: string | undefined,\n restore: () => void\n) => {\n const latestRestore = useRef(restore);\n useEffect(() => { latestRestore.current = restore; }, [restore]);\n useEffect(() => {\n const form = inputRef.current?.form;\n if (!form) return;\n const reset = (event: Event) => {\n // A microtask can run between native listeners, before React's delegated onReset.\n // The next task observes cancellation after the entire native dispatch/default action.\n setTimeout(() => {\n if (!event.defaultPrevented && inputRef.current?.form === form) latestRestore.current();\n }, 0);\n };\n form.addEventListener(\"reset\", reset);\n return () => form.removeEventListener(\"reset\", reset);\n }, [inputRef, formId]);\n};\n"],"names":["_jsx","BasePopover","_jsxs"],"mappings":"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C,CAAA,CAAA,CAAA,CAAI,CAAA;AAGpD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;;AAE+F,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACxF,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,kBAAkB,CAAA,CAAA,CAC7B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAEF,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACwF,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* File selection rules for `FileUpload`.
|
|
2
|
+
|
|
3
|
+
The native chooser and the drop zone are two doors into one policy: the same accept, the same
|
|
4
|
+
per-file size and the same total count decide both, so a file the picker would refuse cannot
|
|
5
|
+
slip in through a drop. Nothing here reads a browser global — every rule works from the
|
|
6
|
+
metadata a `File` already carries — so the module is safe to evaluate during SSR and can be
|
|
7
|
+
probed without a document.
|
|
8
|
+
|
|
9
|
+
`FileUpload.tsx` composes the field around these rules; it owns no rule of its own. */
|
|
10
|
+
/** The identity of one selection entry: the fields that make two `File`s the same choice. */
|
|
11
|
+
const fileKey = (file) => `${file.name}\u0000${file.size}\u0000${file.type}\u0000${file.lastModified}`;
|
|
12
|
+
/** The per-file byte limit as a whole number, or `undefined` when the caller set none. */
|
|
13
|
+
const resolveMaxSize = (maxSize) => maxSize !== undefined && Number.isFinite(maxSize) && maxSize >= 0 ? Math.floor(maxSize) : undefined;
|
|
14
|
+
/**
|
|
15
|
+
* The total file limit, or `undefined` when the selection is unbounded. A single-file field
|
|
16
|
+
* holds exactly one: the picker is told nothing about it, because that limit is what makes a
|
|
17
|
+
* second file a replacement rather than an addition.
|
|
18
|
+
*/
|
|
19
|
+
const resolveMaxFiles = (maxFiles, multiple) => {
|
|
20
|
+
if (!multiple)
|
|
21
|
+
return 1;
|
|
22
|
+
return maxFiles !== undefined && Number.isFinite(maxFiles) && maxFiles >= 0
|
|
23
|
+
? Math.floor(maxFiles)
|
|
24
|
+
: undefined;
|
|
25
|
+
};
|
|
26
|
+
/** A byte limit as the copy spells it: `2 MB`, never `2097152`. */
|
|
27
|
+
const formatBytes = (bytes) => {
|
|
28
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
29
|
+
let value = bytes;
|
|
30
|
+
let unit = 0;
|
|
31
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
32
|
+
value /= 1024;
|
|
33
|
+
unit += 1;
|
|
34
|
+
}
|
|
35
|
+
return `${Number.isInteger(value) ? value : value.toFixed(1)} ${units[unit]}`;
|
|
36
|
+
};
|
|
37
|
+
const acceptTokens = (accept) => (accept !== null && accept !== void 0 ? accept : "")
|
|
38
|
+
.split(",")
|
|
39
|
+
.map((token) => token.trim().toLowerCase())
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
/** An extension token starts with a dot; anything else has to name a media type. */
|
|
42
|
+
const isAcceptToken = (token) => (token.startsWith(".") ? token.length > 1 : token.includes("/"));
|
|
43
|
+
/**
|
|
44
|
+
* Whether a file satisfies the native `accept` restriction. An extension token matches the file
|
|
45
|
+
* name, a media type matches the reported MIME type, and a `type/*` wildcard matches a whole
|
|
46
|
+
* family of them. A file that reports no MIME type at all can satisfy an extension but can never
|
|
47
|
+
* establish a MIME-only match, which is exactly what the platform does. A malformed `accept`
|
|
48
|
+
* value is ignored entirely rather than taken as a restriction that refuses everything, again
|
|
49
|
+
* mirroring the platform: a typo must not close the field.
|
|
50
|
+
*/
|
|
51
|
+
const matchesAccept = (file, accept) => {
|
|
52
|
+
const tokens = acceptTokens(accept);
|
|
53
|
+
if (tokens.length === 0 || !tokens.every(isAcceptToken))
|
|
54
|
+
return true;
|
|
55
|
+
const name = file.name.toLowerCase();
|
|
56
|
+
const type = file.type.toLowerCase();
|
|
57
|
+
return tokens.some((token) => {
|
|
58
|
+
if (token.startsWith("."))
|
|
59
|
+
return name.endsWith(token);
|
|
60
|
+
if (token.endsWith("/*"))
|
|
61
|
+
return type !== "" && type.startsWith(token.slice(0, -1));
|
|
62
|
+
return type !== "" && type === token;
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
const countMessage = (file, limit, multiple) => multiple
|
|
66
|
+
? `${file.name} was not added: at most ${limit} file${limit === 1 ? "" : "s"} can be selected.`
|
|
67
|
+
: `${file.name} was not added: this field holds one file.`;
|
|
68
|
+
/**
|
|
69
|
+
* Applies the whole policy to one batch, from either door.
|
|
70
|
+
*
|
|
71
|
+
* Each file is judged type first, then size, then count — the caller's order of severity — and a
|
|
72
|
+
* refusal never removes a file that was already accepted, so the valid part of a mixed batch
|
|
73
|
+
* still enters. A file the selection already holds is not a failure, it is the same choice
|
|
74
|
+
* arriving twice, so it is neither reported nor counted against the limit.
|
|
75
|
+
*
|
|
76
|
+
* A single-file field starts from an empty selection: the first accepted file replaces what was
|
|
77
|
+
* there and everything after it is a count failure.
|
|
78
|
+
*/
|
|
79
|
+
const selectFiles = ({ incoming, current, limits, }) => {
|
|
80
|
+
const maxSize = resolveMaxSize(limits.maxSize);
|
|
81
|
+
const maxFiles = resolveMaxFiles(limits.maxFiles, limits.multiple);
|
|
82
|
+
const files = limits.multiple ? [...current] : [];
|
|
83
|
+
const keys = new Set(current.map(fileKey));
|
|
84
|
+
const rejections = [];
|
|
85
|
+
let accepted = 0;
|
|
86
|
+
for (const file of incoming) {
|
|
87
|
+
if (!matchesAccept(file, limits.accept)) {
|
|
88
|
+
rejections.push({ file, code: "type", message: `${file.name} is not an accepted file type.` });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (maxSize !== undefined && file.size > maxSize) {
|
|
92
|
+
rejections.push({
|
|
93
|
+
file,
|
|
94
|
+
code: "size",
|
|
95
|
+
message: `${file.name} is larger than the ${formatBytes(maxSize)} limit.`,
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const key = fileKey(file);
|
|
100
|
+
if (keys.has(key))
|
|
101
|
+
continue;
|
|
102
|
+
if (maxFiles !== undefined && files.length >= maxFiles) {
|
|
103
|
+
rejections.push({ file, code: "count", message: countMessage(file, maxFiles, limits.multiple) });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
keys.add(key);
|
|
107
|
+
files.push(file);
|
|
108
|
+
accepted += 1;
|
|
109
|
+
}
|
|
110
|
+
return { files, rejections, accepted };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export { fileKey, formatBytes, matchesAccept, resolveMaxFiles, resolveMaxSize, selectFiles };
|
|
114
|
+
//# sourceMappingURL=fileUpload.rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileUpload.rules.js","sources":["../../../src/components/fileUpload.rules.ts"],"sourcesContent":["/* File selection rules for `FileUpload`.\n\n The native chooser and the drop zone are two doors into one policy: the same accept, the same\n per-file size and the same total count decide both, so a file the picker would refuse cannot\n slip in through a drop. Nothing here reads a browser global — every rule works from the\n metadata a `File` already carries — so the module is safe to evaluate during SSR and can be\n probed without a document.\n\n `FileUpload.tsx` composes the field around these rules; it owns no rule of its own. */\n\nexport type RejectionCode = \"type\" | \"size\" | \"count\";\n\n/** One file a selection operation refused, with the reason it was refused. */\nexport interface FileRejection {\n file: File;\n code: RejectionCode;\n message: string;\n}\n\nexport interface FileSelectionLimits {\n /** The native `accept` value, comma-separated MIME types and extensions. */\n accept?: string;\n /** Per-file byte limit, already normalized. */\n maxSize?: number;\n /** Total file limit, already normalized. */\n maxFiles?: number;\n /** Whether the field holds several files or replaces its selection with one. */\n multiple: boolean;\n}\n\nexport interface FileSelection {\n /** The selection the operation leaves behind. */\n files: File[];\n /** Everything the batch refused, in the order it arrived. */\n rejections: FileRejection[];\n /** How many of the incoming files entered the selection. */\n accepted: number;\n}\n\n/** The identity of one selection entry: the fields that make two `File`s the same choice. */\nexport const fileKey = (file: File) =>\n `${file.name}\\u0000${file.size}\\u0000${file.type}\\u0000${file.lastModified}`;\n\n/** The per-file byte limit as a whole number, or `undefined` when the caller set none. */\nexport const resolveMaxSize = (maxSize?: number) =>\n maxSize !== undefined && Number.isFinite(maxSize) && maxSize >= 0 ? Math.floor(maxSize) : undefined;\n\n/**\n * The total file limit, or `undefined` when the selection is unbounded. A single-file field\n * holds exactly one: the picker is told nothing about it, because that limit is what makes a\n * second file a replacement rather than an addition.\n */\nexport const resolveMaxFiles = (maxFiles: number | undefined, multiple: boolean) => {\n if (!multiple) return 1;\n return maxFiles !== undefined && Number.isFinite(maxFiles) && maxFiles >= 0\n ? Math.floor(maxFiles)\n : undefined;\n};\n\n/** A byte limit as the copy spells it: `2 MB`, never `2097152`. */\nexport const formatBytes = (bytes: number) => {\n const units = [\"B\", \"KB\", \"MB\", \"GB\"];\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit += 1;\n }\n return `${Number.isInteger(value) ? value : value.toFixed(1)} ${units[unit]}`;\n};\n\nconst acceptTokens = (accept?: string) =>\n (accept ?? \"\")\n .split(\",\")\n .map((token) => token.trim().toLowerCase())\n .filter(Boolean);\n\n/** An extension token starts with a dot; anything else has to name a media type. */\nconst isAcceptToken = (token: string) => (token.startsWith(\".\") ? token.length > 1 : token.includes(\"/\"));\n\n/**\n * Whether a file satisfies the native `accept` restriction. An extension token matches the file\n * name, a media type matches the reported MIME type, and a `type/*` wildcard matches a whole\n * family of them. A file that reports no MIME type at all can satisfy an extension but can never\n * establish a MIME-only match, which is exactly what the platform does. A malformed `accept`\n * value is ignored entirely rather than taken as a restriction that refuses everything, again\n * mirroring the platform: a typo must not close the field.\n */\nexport const matchesAccept = (file: File, accept?: string) => {\n const tokens = acceptTokens(accept);\n if (tokens.length === 0 || !tokens.every(isAcceptToken)) return true;\n\n const name = file.name.toLowerCase();\n const type = file.type.toLowerCase();\n\n return tokens.some((token) => {\n if (token.startsWith(\".\")) return name.endsWith(token);\n if (token.endsWith(\"/*\")) return type !== \"\" && type.startsWith(token.slice(0, -1));\n return type !== \"\" && type === token;\n });\n};\n\nconst countMessage = (file: File, limit: number, multiple: boolean) =>\n multiple\n ? `${file.name} was not added: at most ${limit} file${limit === 1 ? \"\" : \"s\"} can be selected.`\n : `${file.name} was not added: this field holds one file.`;\n\n/**\n * Applies the whole policy to one batch, from either door.\n *\n * Each file is judged type first, then size, then count — the caller's order of severity — and a\n * refusal never removes a file that was already accepted, so the valid part of a mixed batch\n * still enters. A file the selection already holds is not a failure, it is the same choice\n * arriving twice, so it is neither reported nor counted against the limit.\n *\n * A single-file field starts from an empty selection: the first accepted file replaces what was\n * there and everything after it is a count failure.\n */\nexport const selectFiles = ({\n incoming,\n current,\n limits,\n}: {\n incoming: File[];\n current: File[];\n limits: FileSelectionLimits;\n}): FileSelection => {\n const maxSize = resolveMaxSize(limits.maxSize);\n const maxFiles = resolveMaxFiles(limits.maxFiles, limits.multiple);\n\n const files = limits.multiple ? [...current] : [];\n const keys = new Set(current.map(fileKey));\n const rejections: FileRejection[] = [];\n let accepted = 0;\n\n for (const file of incoming) {\n if (!matchesAccept(file, limits.accept)) {\n rejections.push({ file, code: \"type\", message: `${file.name} is not an accepted file type.` });\n continue;\n }\n\n if (maxSize !== undefined && file.size > maxSize) {\n rejections.push({\n file,\n code: \"size\",\n message: `${file.name} is larger than the ${formatBytes(maxSize)} limit.`,\n });\n continue;\n }\n\n const key = fileKey(file);\n if (keys.has(key)) continue;\n\n if (maxFiles !== undefined && files.length >= maxFiles) {\n rejections.push({ file, code: \"count\", message: countMessage(file, maxFiles, limits.multiple) });\n continue;\n }\n\n keys.add(key);\n files.push(file);\n accepted += 1;\n }\n\n return { files, rejections, accepted };\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long a typed sequence stays open for extension.
|
|
3
|
+
*
|
|
4
|
+
* A node label is a word rather than a document, so a search only spans a few characters: long
|
|
5
|
+
* enough that typing quickly reads as one search, short enough that the next deliberate
|
|
6
|
+
* character starts a new one instead of extending a stale one.
|
|
7
|
+
*/
|
|
8
|
+
const typeaheadWindow = 500;
|
|
9
|
+
/** Whether a node holds children the tree can show. */
|
|
10
|
+
const isBranch = (item) => { var _a, _b; return ((_b = (_a = item.children) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0; };
|
|
11
|
+
/**
|
|
12
|
+
* The nodes in the order the tree presents them: a document-order walk that descends into an
|
|
13
|
+
* expanded branch and leaves a collapsed one alone.
|
|
14
|
+
*
|
|
15
|
+
* This is the one place that decides what "visible" means, and the keyboard, the typeahead and
|
|
16
|
+
* the roving tab stop all read it rather than walking the tree again — so they can never
|
|
17
|
+
* disagree about which node is where.
|
|
18
|
+
*/
|
|
19
|
+
const flattenTree = (items, expanded) => {
|
|
20
|
+
const rows = [];
|
|
21
|
+
const collect = (nodes, depth, ancestors, ancestorDisabled) => {
|
|
22
|
+
var _a, _b;
|
|
23
|
+
for (const item of nodes) {
|
|
24
|
+
const expandedHere = isBranch(item) && expanded.has(item.value);
|
|
25
|
+
rows.push({
|
|
26
|
+
item,
|
|
27
|
+
depth,
|
|
28
|
+
parent: (_a = ancestors[ancestors.length - 1]) !== null && _a !== void 0 ? _a : null,
|
|
29
|
+
ancestors,
|
|
30
|
+
expanded: expandedHere,
|
|
31
|
+
ancestorDisabled,
|
|
32
|
+
});
|
|
33
|
+
if (expandedHere) {
|
|
34
|
+
collect((_b = item.children) !== null && _b !== void 0 ? _b : [], depth + 1, [...ancestors, item.value], ancestorDisabled || item.disabled === true);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
collect(items, 0, [], false);
|
|
39
|
+
return rows;
|
|
40
|
+
};
|
|
41
|
+
/** The nearest of `ancestors`, deepest first, that the tree still shows. */
|
|
42
|
+
const nearestVisible = (ancestors, visible) => {
|
|
43
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
44
|
+
if (visible.has(ancestors[index]))
|
|
45
|
+
return ancestors[index];
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The APG typeahead search: the next *visible* node whose label starts with the characters typed
|
|
51
|
+
* so far, starting after `from` and wrapping at the end of the tree.
|
|
52
|
+
*
|
|
53
|
+
* A search moves focus and nothing else. Selection is the user's decision, so typing a letter can
|
|
54
|
+
* never change what is chosen, and a node that a typeahead lands on is not selected by arriving.
|
|
55
|
+
*/
|
|
56
|
+
const matchTypeahead = (rows, pattern, from) => {
|
|
57
|
+
const query = pattern.toLowerCase();
|
|
58
|
+
if (query.length === 0 || rows.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
const start = rows.findIndex((row) => row.item.value === from);
|
|
61
|
+
for (let step = 1; step <= rows.length; step += 1) {
|
|
62
|
+
const row = rows[(start + step + rows.length) % rows.length];
|
|
63
|
+
if (row.item.label.toLowerCase().startsWith(query))
|
|
64
|
+
return row;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Advances the typeahead buffer by one keystroke.
|
|
70
|
+
*
|
|
71
|
+
* Characters typed in quick succession are one search — `d`, `o`, `c` looks for a label starting
|
|
72
|
+
* with "doc" — and the *same* character typed again cycles through the nodes that start with it
|
|
73
|
+
* rather than searching for a two-letter word that almost certainly does not exist. A repeat
|
|
74
|
+
* neither clears nor extends the buffer, so the next different character still widens the
|
|
75
|
+
* single-character search it was part of.
|
|
76
|
+
*/
|
|
77
|
+
const typeaheadStep = (buffer, key, now) => {
|
|
78
|
+
if (buffer.text.length === 0 || now - buffer.time >= typeaheadWindow) {
|
|
79
|
+
return { buffer: { text: key, time: now }, pattern: key };
|
|
80
|
+
}
|
|
81
|
+
if (buffer.text === key) {
|
|
82
|
+
return { buffer: { text: buffer.text, time: now }, pattern: key };
|
|
83
|
+
}
|
|
84
|
+
return { buffer: { text: buffer.text + key, time: now }, pattern: buffer.text + key };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export { flattenTree, isBranch, matchTypeahead, nearestVisible, typeaheadStep, typeaheadWindow };
|
|
88
|
+
//# sourceMappingURL=tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree.js","sources":["../../../src/components/tree.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\n\n/**\n * The finite static tree a `TreeView` presents.\n *\n * A node is data, never markup: the tree renders the roles, the nesting and the indentation\n * itself, so every node in it is reachable the same way whether it is a leaf or a branch.\n * `value` identifies a node throughout the tree and is unique; `label` is what the node is\n * called — its accessible name and what a typeahead search matches.\n */\nexport interface TreeViewItem {\n value: string;\n label: string;\n icon?: ReactNode;\n disabled?: boolean;\n children?: TreeViewItem[];\n}\n\n/** One node, as the tree currently shows it. */\nexport interface TreeRow {\n item: TreeViewItem;\n /** How deep the node sits, counted from the roots. */\n depth: number;\n /** The node that contains it, or `null` at the top level. */\n parent: string | null;\n /** Its ancestors, outermost first. */\n ancestors: string[];\n /** Whether this node's own children are shown. */\n expanded: boolean;\n /**\n * Whether a node above this one is disabled. `aria-disabled` is inherited down a subtree, and\n * this tree deliberately does not mean that: a node below an unavailable branch is its own\n * node, so the row has to be able to say it is not itself unavailable.\n */\n ancestorDisabled: boolean;\n}\n\n/** The characters a typeahead search has accumulated, and when the last one arrived. */\nexport interface TypeaheadBuffer {\n text: string;\n time: number;\n}\n\n/**\n * How long a typed sequence stays open for extension.\n *\n * A node label is a word rather than a document, so a search only spans a few characters: long\n * enough that typing quickly reads as one search, short enough that the next deliberate\n * character starts a new one instead of extending a stale one.\n */\nexport const typeaheadWindow = 500;\n\n/** Whether a node holds children the tree can show. */\nexport const isBranch = (item: TreeViewItem) => (item.children?.length ?? 0) > 0;\n\n/**\n * The nodes in the order the tree presents them: a document-order walk that descends into an\n * expanded branch and leaves a collapsed one alone.\n *\n * This is the one place that decides what \"visible\" means, and the keyboard, the typeahead and\n * the roving tab stop all read it rather than walking the tree again — so they can never\n * disagree about which node is where.\n */\nexport const flattenTree = (items: TreeViewItem[], expanded: ReadonlySet<string>): TreeRow[] => {\n const rows: TreeRow[] = [];\n\n const collect = (\n nodes: TreeViewItem[],\n depth: number,\n ancestors: string[],\n ancestorDisabled: boolean\n ) => {\n for (const item of nodes) {\n const expandedHere = isBranch(item) && expanded.has(item.value);\n rows.push({\n item,\n depth,\n parent: ancestors[ancestors.length - 1] ?? null,\n ancestors,\n expanded: expandedHere,\n ancestorDisabled,\n });\n if (expandedHere) {\n collect(\n item.children ?? [],\n depth + 1,\n [...ancestors, item.value],\n ancestorDisabled || item.disabled === true\n );\n }\n }\n };\n\n collect(items, 0, [], false);\n return rows;\n};\n\n/** The nearest of `ancestors`, deepest first, that the tree still shows. */\nexport const nearestVisible = (\n ancestors: string[],\n visible: ReadonlySet<string>\n): string | null => {\n for (let index = ancestors.length - 1; index >= 0; index -= 1) {\n if (visible.has(ancestors[index])) return ancestors[index];\n }\n return null;\n};\n\n/**\n * The APG typeahead search: the next *visible* node whose label starts with the characters typed\n * so far, starting after `from` and wrapping at the end of the tree.\n *\n * A search moves focus and nothing else. Selection is the user's decision, so typing a letter can\n * never change what is chosen, and a node that a typeahead lands on is not selected by arriving.\n */\nexport const matchTypeahead = (\n rows: TreeRow[],\n pattern: string,\n from: string | null\n): TreeRow | null => {\n const query = pattern.toLowerCase();\n if (query.length === 0 || rows.length === 0) return null;\n\n const start = rows.findIndex((row) => row.item.value === from);\n for (let step = 1; step <= rows.length; step += 1) {\n const row = rows[(start + step + rows.length) % rows.length];\n if (row.item.label.toLowerCase().startsWith(query)) return row;\n }\n return null;\n};\n\n/**\n * Advances the typeahead buffer by one keystroke.\n *\n * Characters typed in quick succession are one search — `d`, `o`, `c` looks for a label starting\n * with \"doc\" — and the *same* character typed again cycles through the nodes that start with it\n * rather than searching for a two-letter word that almost certainly does not exist. A repeat\n * neither clears nor extends the buffer, so the next different character still widens the\n * single-character search it was part of.\n */\nexport const typeaheadStep = (\n buffer: TypeaheadBuffer,\n key: string,\n now: number\n): { buffer: TypeaheadBuffer; pattern: string } => {\n if (buffer.text.length === 0 || now - buffer.time >= typeaheadWindow) {\n return { buffer: { text: key, time: now }, pattern: key };\n }\n\n if (buffer.text === key) {\n return { buffer: { text: buffer.text, time: now }, pattern: key };\n }\n\n return { buffer: { text: buffer.text + key, time: now }, pattern: buffer.text + key };\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -66,6 +66,8 @@ import { motionFeedback, motionDisclose, motionStateLayer } from './ui.motion.js
|
|
|
66
66
|
draws it from the wrapping control instead of the track it contains. */
|
|
67
67
|
const focusRing = "focus:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-sherick-focus focus-visible:outline-offset-[3px]";
|
|
68
68
|
const focusRingInset = "focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-sherick-focus";
|
|
69
|
+
/* A focus owner that also contains a subtree paints the inset ring on its direct row only. */
|
|
70
|
+
const parentFocusRingInset = "[:focus-visible>&]:ring-2 [:focus-visible>&]:ring-inset [:focus-visible>&]:ring-sherick-focus";
|
|
69
71
|
/* The ring of a value control that is itself the focusable element: it wears it whenever it
|
|
70
72
|
holds focus, however it was focused, and whenever the surface it opened is on screen. A select
|
|
71
73
|
hands the DOM focus to its list while that list is open, so the ring follows the control's
|
|
@@ -79,6 +81,8 @@ const focusRingHeld = "focus:outline-none focus:outline focus:outline-2 focus:ou
|
|
|
79
81
|
input is `:focus-visible` only for the keyboard, so a slider's handle saves its ring for the
|
|
80
82
|
keyboard and lets its own engagement answer the pointer. */
|
|
81
83
|
const focusRingWithin = "has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-sherick-focus has-[:focus-visible]:outline-offset-[3px]";
|
|
84
|
+
/* A file drag marks the same target with the same outline as visible keyboard focus. */
|
|
85
|
+
const focusRingDrag = "data-[dragging]:outline data-[dragging]:outline-2 data-[dragging]:outline-sherick-focus data-[dragging]:outline-offset-[3px]";
|
|
82
86
|
const groupFocusRing = "group-focus-visible:outline group-focus-visible:outline-2 group-focus-visible:outline-sherick-focus group-focus-visible:outline-offset-[3px]";
|
|
83
87
|
/* Motion is not owned here. Every physical event — feedback, tactile, arrive, orient,
|
|
84
88
|
relocate, direct, presence and activity — is a named recipe in `ui.motion.ts`, which
|
|
@@ -92,7 +96,7 @@ const groupFocusRing = "group-focus-visible:outline group-focus-visible:outline-
|
|
|
92
96
|
touchable; pressing it returns it to the track.
|
|
93
97
|
- floating: a surface that genuinely sits above the application.
|
|
94
98
|
- control: the resting half of the tactile pair, for a part the user moves —
|
|
95
|
-
a switch thumb, a selected segment.
|
|
99
|
+
a switch thumb, a selected segment or a held option/date selection.
|
|
96
100
|
- recessed: the other half of that pair: a groove, a track or a well, which is
|
|
97
101
|
recessed by definition, and a raised control while it is held, which
|
|
98
102
|
lands at the same depth.
|
|
@@ -110,6 +114,9 @@ const elevation = {
|
|
|
110
114
|
of the well is deepened and that boundary is what says what it is. See `selectable.markSurface`. */
|
|
111
115
|
well: "shadow-sherick-well",
|
|
112
116
|
};
|
|
117
|
+
/* An inset sheet's shaded upper wall, without a lower rim. Kept independent so
|
|
118
|
+
unrelated components do not bundle this recipe. */
|
|
119
|
+
const recessedTop = "shadow-sherick-recessed-top";
|
|
113
120
|
/* Shape — semantic corner roles, never an arbitrary radius. Softness grows with
|
|
114
121
|
the size of the object and the emphasis it carries:
|
|
115
122
|
- control: ordinary controls and dense data regions — fields, rows, options,
|
|
@@ -177,8 +184,8 @@ const effectiveDisabled = "data-[disabled]:cursor-not-allowed data-[disabled]:op
|
|
|
177
184
|
pressed a raised control returning to the recessed depth of its own track, plus
|
|
178
185
|
a slight compression; a flat control stays flat and a floating one keeps
|
|
179
186
|
its elevation
|
|
180
|
-
selected a selected tone
|
|
181
|
-
|
|
187
|
+
selected a held choice: selected tone plus control elevation on a segment,
|
|
188
|
+
option/tree row or date surface; never on mere navigation highlight
|
|
182
189
|
disabled 45% opacity, no pointer affordance, no interactive state at all
|
|
183
190
|
focus the shared outer focus ring, visible on keyboard focus */
|
|
184
191
|
const state = {
|
|
@@ -218,7 +225,8 @@ const state = {
|
|
|
218
225
|
field: {
|
|
219
226
|
hover: "hover:bg-sherick-surface-high/[0.82]",
|
|
220
227
|
focus: "focus:bg-sherick-surface-high/[0.9]",
|
|
221
|
-
|
|
228
|
+
/* A composite's engaged tone outranks hover, independent of variant emission order. */
|
|
229
|
+
focusWithin: "[&&]:focus-within:bg-sherick-surface-high/[0.9]",
|
|
222
230
|
engaged: "bg-sherick-surface-high/[0.9]",
|
|
223
231
|
errorHover: "hover:bg-sherick-danger/[0.10]",
|
|
224
232
|
errorFocus: "focus:bg-sherick-danger/[0.13]",
|
|
@@ -229,7 +237,7 @@ const state = {
|
|
|
229
237
|
invalid: "data-[invalid]:bg-sherick-danger/[0.075]",
|
|
230
238
|
invalidHover: "data-[invalid]:hover:bg-sherick-danger/[0.10]",
|
|
231
239
|
invalidEngaged: "data-[invalid]:bg-sherick-danger/[0.13]",
|
|
232
|
-
invalidFocusWithin: "data-[invalid]:focus-within:bg-sherick-danger/[0.13]",
|
|
240
|
+
invalidFocusWithin: "[&&]:data-[invalid]:focus-within:bg-sherick-danger/[0.13]",
|
|
233
241
|
},
|
|
234
242
|
};
|
|
235
243
|
/* State layers.
|
|
@@ -308,6 +316,8 @@ const hitArea = "relative after:pointer-events-auto after:absolute after:inset-[
|
|
|
308
316
|
should sit back from the surface around them.
|
|
309
317
|
- matte: a matte surface that separates from the canvas by tone alone.
|
|
310
318
|
- matteHigh: the second matte step, for nesting inside another matte surface.
|
|
319
|
+
- matteInset: a passive sheet tucked under a stronger control; subdued without
|
|
320
|
+
blending into the canvas or needing a drawn rim.
|
|
311
321
|
- control: the fill every text control shares, plus its placeholder tone.
|
|
312
322
|
- handle: the fill of a small part the user has to find — a value control's handle.
|
|
313
323
|
Surface steps sit within a few percent of their neighbours, so this role
|
|
@@ -355,6 +365,10 @@ const material = {
|
|
|
355
365
|
acrylicDense: "bg-sherick-surface-float/[var(--sui-glass-dense-fill)] bg-[image:var(--sui-glass-gradient-dense)] text-sherick-ink backdrop-blur-[var(--sui-glass-dense-blur)] backdrop-saturate-[var(--sui-glass-dense-saturation)] backdrop-brightness-[var(--sui-glass-dense-brightness)]",
|
|
356
366
|
acrylicHero: "bg-sherick-surface-overlay/[var(--sui-overlay-fill)] bg-[image:var(--sui-glass-hero-gradient)] text-sherick-ink backdrop-blur-[var(--sui-glass-hero-blur)] backdrop-saturate-[var(--sui-glass-hero-saturation)] backdrop-brightness-[var(--sui-glass-hero-brightness)]",
|
|
357
367
|
};
|
|
368
|
+
/* Dark neutral behind native video, including poster loading and letterboxing. */
|
|
369
|
+
const mediaCanvas = "bg-sherick-media";
|
|
370
|
+
/* A passive sheet tucked below a stronger control; independently tree-shakeable. */
|
|
371
|
+
const matteInset = "bg-sherick-surface-high/[0.35] text-sherick-ink";
|
|
358
372
|
/* Density — three control steps plus the accessible hit target.
|
|
359
373
|
Density owns height and the type step, so controls of one density share a
|
|
360
374
|
rhythm. A component's anatomy owns its padding: a button's inline padding is
|
|
@@ -466,8 +480,9 @@ const detail = {
|
|
|
466
480
|
The sheet never exceeds what the viewport leaves it, and scrolls inside itself rather than
|
|
467
481
|
growing. Its width is the list's own decision: a control's list is never narrower than the
|
|
468
482
|
control it came from and grows to fit its own content until the viewport clamp, and a command
|
|
469
|
-
list only ever grows to its content.
|
|
470
|
-
|
|
483
|
+
list only ever grows to its content. Unselected rows and commands stay flat; held options
|
|
484
|
+
compose `tone.selected` with `elevation.control`. One row step of `stateLayer.quiet` plus
|
|
485
|
+
`stateLayer.activeRow` carries both its hover and
|
|
471
486
|
its keyboard highlight, so an option and a command are highlighted by the same tone at the
|
|
472
487
|
same strength. Keyboard focus adds the shared inset ring independently of that highlight.
|
|
473
488
|
Selection is a tint of the sheet through `tone.selected` and never the opaque
|
|
@@ -649,5 +664,5 @@ const recipeAlphas = {
|
|
|
649
664
|
the input itself keeps its native single-line scrolling behavior. */
|
|
650
665
|
const fieldLayout = "flex min-w-0 w-full flex-col [overflow-wrap:anywhere]";
|
|
651
666
|
|
|
652
|
-
export { density, detail, disclosure, edge, edgeTone, elevation, fieldLayout, focusRing, focusRingHeld, focusRingInset, focusRingWithin, groupFocusRing, hitArea, list, material, overlay, recipeAlphas, selectable, shape, stacking, state, stateLayer, text, tone };
|
|
667
|
+
export { density, detail, disclosure, edge, edgeTone, elevation, fieldLayout, focusRing, focusRingDrag, focusRingHeld, focusRingInset, focusRingWithin, groupFocusRing, hitArea, list, material, matteInset, mediaCanvas, overlay, parentFocusRingInset, recessedTop, recipeAlphas, selectable, shape, stacking, state, stateLayer, text, tone };
|
|
653
668
|
//# sourceMappingURL=ui.common.js.map
|