sherick-ui 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +55 -199
  2. package/dist/cjs/components/Breadcrumb.cjs +47 -0
  3. package/dist/cjs/components/Breadcrumb.cjs.map +1 -0
  4. package/dist/cjs/components/Calendar.cjs +298 -0
  5. package/dist/cjs/components/Calendar.cjs.map +1 -0
  6. package/dist/cjs/components/Combobox.cjs +1 -1
  7. package/dist/cjs/components/Combobox.cjs.map +1 -1
  8. package/dist/cjs/components/Command.cjs +81 -0
  9. package/dist/cjs/components/Command.cjs.map +1 -0
  10. package/dist/cjs/components/CommandPalette.cjs +52 -0
  11. package/dist/cjs/components/CommandPalette.cjs.map +1 -0
  12. package/dist/cjs/components/DatePicker.cjs +107 -0
  13. package/dist/cjs/components/DatePicker.cjs.map +1 -0
  14. package/dist/cjs/components/DateRangePicker.cjs +165 -0
  15. package/dist/cjs/components/DateRangePicker.cjs.map +1 -0
  16. package/dist/cjs/components/FileUpload.cjs +203 -0
  17. package/dist/cjs/components/FileUpload.cjs.map +1 -0
  18. package/dist/cjs/components/Media.cjs +27 -0
  19. package/dist/cjs/components/Media.cjs.map +1 -0
  20. package/dist/cjs/components/Pagination.cjs +115 -0
  21. package/dist/cjs/components/Pagination.cjs.map +1 -0
  22. package/dist/cjs/components/Select.cjs +1 -1
  23. package/dist/cjs/components/Select.cjs.map +1 -1
  24. package/dist/cjs/components/Stepper.cjs +40 -0
  25. package/dist/cjs/components/Stepper.cjs.map +1 -0
  26. package/dist/cjs/components/Tabs.cjs +1 -1
  27. package/dist/cjs/components/Tabs.cjs.map +1 -1
  28. package/dist/cjs/components/TreeView.cjs +247 -0
  29. package/dist/cjs/components/TreeView.cjs.map +1 -0
  30. package/dist/cjs/components/date-family.cjs +191 -0
  31. package/dist/cjs/components/date-family.cjs.map +1 -0
  32. package/dist/cjs/components/date-field.cjs +90 -0
  33. package/dist/cjs/components/date-field.cjs.map +1 -0
  34. package/dist/cjs/components/fileUpload.rules.cjs +121 -0
  35. package/dist/cjs/components/fileUpload.rules.cjs.map +1 -0
  36. package/dist/cjs/components/tree.cjs +95 -0
  37. package/dist/cjs/components/tree.cjs.map +1 -0
  38. package/dist/cjs/components/ui.common.cjs +27 -7
  39. package/dist/cjs/components/ui.common.cjs.map +1 -1
  40. package/dist/cjs/components/ui.motion.cjs +3 -3
  41. package/dist/cjs/components/ui.motion.cjs.map +1 -1
  42. package/dist/cjs/dev.cjs +5 -0
  43. package/dist/cjs/dev.cjs.map +1 -1
  44. package/dist/cjs/index.cjs +22 -0
  45. package/dist/cjs/index.cjs.map +1 -1
  46. package/dist/esm/components/Breadcrumb.js +43 -0
  47. package/dist/esm/components/Breadcrumb.js.map +1 -0
  48. package/dist/esm/components/Calendar.js +294 -0
  49. package/dist/esm/components/Calendar.js.map +1 -0
  50. package/dist/esm/components/Combobox.js +2 -2
  51. package/dist/esm/components/Combobox.js.map +1 -1
  52. package/dist/esm/components/Command.js +77 -0
  53. package/dist/esm/components/Command.js.map +1 -0
  54. package/dist/esm/components/CommandPalette.js +48 -0
  55. package/dist/esm/components/CommandPalette.js.map +1 -0
  56. package/dist/esm/components/DatePicker.js +103 -0
  57. package/dist/esm/components/DatePicker.js.map +1 -0
  58. package/dist/esm/components/DateRangePicker.js +161 -0
  59. package/dist/esm/components/DateRangePicker.js.map +1 -0
  60. package/dist/esm/components/FileUpload.js +199 -0
  61. package/dist/esm/components/FileUpload.js.map +1 -0
  62. package/dist/esm/components/Media.js +25 -0
  63. package/dist/esm/components/Media.js.map +1 -0
  64. package/dist/esm/components/Pagination.js +111 -0
  65. package/dist/esm/components/Pagination.js.map +1 -0
  66. package/dist/esm/components/Select.js +2 -2
  67. package/dist/esm/components/Select.js.map +1 -1
  68. package/dist/esm/components/Stepper.js +36 -0
  69. package/dist/esm/components/Stepper.js.map +1 -0
  70. package/dist/esm/components/Tabs.js +2 -2
  71. package/dist/esm/components/Tabs.js.map +1 -1
  72. package/dist/esm/components/TreeView.js +243 -0
  73. package/dist/esm/components/TreeView.js.map +1 -0
  74. package/dist/esm/components/date-family.js +173 -0
  75. package/dist/esm/components/date-family.js.map +1 -0
  76. package/dist/esm/components/date-field.js +84 -0
  77. package/dist/esm/components/date-field.js.map +1 -0
  78. package/dist/esm/components/fileUpload.rules.js +114 -0
  79. package/dist/esm/components/fileUpload.rules.js.map +1 -0
  80. package/dist/esm/components/tree.js +88 -0
  81. package/dist/esm/components/tree.js.map +1 -0
  82. package/dist/esm/components/ui.common.js +23 -8
  83. package/dist/esm/components/ui.common.js.map +1 -1
  84. package/dist/esm/components/ui.motion.js +3 -3
  85. package/dist/esm/components/ui.motion.js.map +1 -1
  86. package/dist/esm/dev.js +1 -1
  87. package/dist/esm/index.js +11 -0
  88. package/dist/esm/index.js.map +1 -1
  89. package/dist/styles.css +362 -20
  90. package/dist/theme.css +15 -9
  91. package/dist/types/dev.d.cts +8 -3
  92. package/dist/types/dev.d.ts +8 -3
  93. package/dist/types/index.d.cts +600 -66
  94. package/dist/types/index.d.ts +600 -66
  95. package/package.json +1 -1
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ /* File selection rules for `FileUpload`.
4
+
5
+ The native chooser and the drop zone are two doors into one policy: the same accept, the same
6
+ per-file size and the same total count decide both, so a file the picker would refuse cannot
7
+ slip in through a drop. Nothing here reads a browser global — every rule works from the
8
+ metadata a `File` already carries — so the module is safe to evaluate during SSR and can be
9
+ probed without a document.
10
+
11
+ `FileUpload.tsx` composes the field around these rules; it owns no rule of its own. */
12
+ /** The identity of one selection entry: the fields that make two `File`s the same choice. */
13
+ const fileKey = (file) => `${file.name}\u0000${file.size}\u0000${file.type}\u0000${file.lastModified}`;
14
+ /** The per-file byte limit as a whole number, or `undefined` when the caller set none. */
15
+ const resolveMaxSize = (maxSize) => maxSize !== undefined && Number.isFinite(maxSize) && maxSize >= 0 ? Math.floor(maxSize) : undefined;
16
+ /**
17
+ * The total file limit, or `undefined` when the selection is unbounded. A single-file field
18
+ * holds exactly one: the picker is told nothing about it, because that limit is what makes a
19
+ * second file a replacement rather than an addition.
20
+ */
21
+ const resolveMaxFiles = (maxFiles, multiple) => {
22
+ if (!multiple)
23
+ return 1;
24
+ return maxFiles !== undefined && Number.isFinite(maxFiles) && maxFiles >= 0
25
+ ? Math.floor(maxFiles)
26
+ : undefined;
27
+ };
28
+ /** A byte limit as the copy spells it: `2 MB`, never `2097152`. */
29
+ const formatBytes = (bytes) => {
30
+ const units = ["B", "KB", "MB", "GB"];
31
+ let value = bytes;
32
+ let unit = 0;
33
+ while (value >= 1024 && unit < units.length - 1) {
34
+ value /= 1024;
35
+ unit += 1;
36
+ }
37
+ return `${Number.isInteger(value) ? value : value.toFixed(1)} ${units[unit]}`;
38
+ };
39
+ const acceptTokens = (accept) => (accept !== null && accept !== void 0 ? accept : "")
40
+ .split(",")
41
+ .map((token) => token.trim().toLowerCase())
42
+ .filter(Boolean);
43
+ /** An extension token starts with a dot; anything else has to name a media type. */
44
+ const isAcceptToken = (token) => (token.startsWith(".") ? token.length > 1 : token.includes("/"));
45
+ /**
46
+ * Whether a file satisfies the native `accept` restriction. An extension token matches the file
47
+ * name, a media type matches the reported MIME type, and a `type/*` wildcard matches a whole
48
+ * family of them. A file that reports no MIME type at all can satisfy an extension but can never
49
+ * establish a MIME-only match, which is exactly what the platform does. A malformed `accept`
50
+ * value is ignored entirely rather than taken as a restriction that refuses everything, again
51
+ * mirroring the platform: a typo must not close the field.
52
+ */
53
+ const matchesAccept = (file, accept) => {
54
+ const tokens = acceptTokens(accept);
55
+ if (tokens.length === 0 || !tokens.every(isAcceptToken))
56
+ return true;
57
+ const name = file.name.toLowerCase();
58
+ const type = file.type.toLowerCase();
59
+ return tokens.some((token) => {
60
+ if (token.startsWith("."))
61
+ return name.endsWith(token);
62
+ if (token.endsWith("/*"))
63
+ return type !== "" && type.startsWith(token.slice(0, -1));
64
+ return type !== "" && type === token;
65
+ });
66
+ };
67
+ const countMessage = (file, limit, multiple) => multiple
68
+ ? `${file.name} was not added: at most ${limit} file${limit === 1 ? "" : "s"} can be selected.`
69
+ : `${file.name} was not added: this field holds one file.`;
70
+ /**
71
+ * Applies the whole policy to one batch, from either door.
72
+ *
73
+ * Each file is judged type first, then size, then count — the caller's order of severity — and a
74
+ * refusal never removes a file that was already accepted, so the valid part of a mixed batch
75
+ * still enters. A file the selection already holds is not a failure, it is the same choice
76
+ * arriving twice, so it is neither reported nor counted against the limit.
77
+ *
78
+ * A single-file field starts from an empty selection: the first accepted file replaces what was
79
+ * there and everything after it is a count failure.
80
+ */
81
+ const selectFiles = ({ incoming, current, limits, }) => {
82
+ const maxSize = resolveMaxSize(limits.maxSize);
83
+ const maxFiles = resolveMaxFiles(limits.maxFiles, limits.multiple);
84
+ const files = limits.multiple ? [...current] : [];
85
+ const keys = new Set(current.map(fileKey));
86
+ const rejections = [];
87
+ let accepted = 0;
88
+ for (const file of incoming) {
89
+ if (!matchesAccept(file, limits.accept)) {
90
+ rejections.push({ file, code: "type", message: `${file.name} is not an accepted file type.` });
91
+ continue;
92
+ }
93
+ if (maxSize !== undefined && file.size > maxSize) {
94
+ rejections.push({
95
+ file,
96
+ code: "size",
97
+ message: `${file.name} is larger than the ${formatBytes(maxSize)} limit.`,
98
+ });
99
+ continue;
100
+ }
101
+ const key = fileKey(file);
102
+ if (keys.has(key))
103
+ continue;
104
+ if (maxFiles !== undefined && files.length >= maxFiles) {
105
+ rejections.push({ file, code: "count", message: countMessage(file, maxFiles, limits.multiple) });
106
+ continue;
107
+ }
108
+ keys.add(key);
109
+ files.push(file);
110
+ accepted += 1;
111
+ }
112
+ return { files, rejections, accepted };
113
+ };
114
+
115
+ exports.fileKey = fileKey;
116
+ exports.formatBytes = formatBytes;
117
+ exports.matchesAccept = matchesAccept;
118
+ exports.resolveMaxFiles = resolveMaxFiles;
119
+ exports.resolveMaxSize = resolveMaxSize;
120
+ exports.selectFiles = selectFiles;
121
+ //# sourceMappingURL=fileUpload.rules.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileUpload.rules.cjs","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,95 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * How long a typed sequence stays open for extension.
5
+ *
6
+ * A node label is a word rather than a document, so a search only spans a few characters: long
7
+ * enough that typing quickly reads as one search, short enough that the next deliberate
8
+ * character starts a new one instead of extending a stale one.
9
+ */
10
+ const typeaheadWindow = 500;
11
+ /** Whether a node holds children the tree can show. */
12
+ 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; };
13
+ /**
14
+ * The nodes in the order the tree presents them: a document-order walk that descends into an
15
+ * expanded branch and leaves a collapsed one alone.
16
+ *
17
+ * This is the one place that decides what "visible" means, and the keyboard, the typeahead and
18
+ * the roving tab stop all read it rather than walking the tree again — so they can never
19
+ * disagree about which node is where.
20
+ */
21
+ const flattenTree = (items, expanded) => {
22
+ const rows = [];
23
+ const collect = (nodes, depth, ancestors, ancestorDisabled) => {
24
+ var _a, _b;
25
+ for (const item of nodes) {
26
+ const expandedHere = isBranch(item) && expanded.has(item.value);
27
+ rows.push({
28
+ item,
29
+ depth,
30
+ parent: (_a = ancestors[ancestors.length - 1]) !== null && _a !== void 0 ? _a : null,
31
+ ancestors,
32
+ expanded: expandedHere,
33
+ ancestorDisabled,
34
+ });
35
+ if (expandedHere) {
36
+ collect((_b = item.children) !== null && _b !== void 0 ? _b : [], depth + 1, [...ancestors, item.value], ancestorDisabled || item.disabled === true);
37
+ }
38
+ }
39
+ };
40
+ collect(items, 0, [], false);
41
+ return rows;
42
+ };
43
+ /** The nearest of `ancestors`, deepest first, that the tree still shows. */
44
+ const nearestVisible = (ancestors, visible) => {
45
+ for (let index = ancestors.length - 1; index >= 0; index -= 1) {
46
+ if (visible.has(ancestors[index]))
47
+ return ancestors[index];
48
+ }
49
+ return null;
50
+ };
51
+ /**
52
+ * The APG typeahead search: the next *visible* node whose label starts with the characters typed
53
+ * so far, starting after `from` and wrapping at the end of the tree.
54
+ *
55
+ * A search moves focus and nothing else. Selection is the user's decision, so typing a letter can
56
+ * never change what is chosen, and a node that a typeahead lands on is not selected by arriving.
57
+ */
58
+ const matchTypeahead = (rows, pattern, from) => {
59
+ const query = pattern.toLowerCase();
60
+ if (query.length === 0 || rows.length === 0)
61
+ return null;
62
+ const start = rows.findIndex((row) => row.item.value === from);
63
+ for (let step = 1; step <= rows.length; step += 1) {
64
+ const row = rows[(start + step + rows.length) % rows.length];
65
+ if (row.item.label.toLowerCase().startsWith(query))
66
+ return row;
67
+ }
68
+ return null;
69
+ };
70
+ /**
71
+ * Advances the typeahead buffer by one keystroke.
72
+ *
73
+ * Characters typed in quick succession are one search — `d`, `o`, `c` looks for a label starting
74
+ * with "doc" — and the *same* character typed again cycles through the nodes that start with it
75
+ * rather than searching for a two-letter word that almost certainly does not exist. A repeat
76
+ * neither clears nor extends the buffer, so the next different character still widens the
77
+ * single-character search it was part of.
78
+ */
79
+ const typeaheadStep = (buffer, key, now) => {
80
+ if (buffer.text.length === 0 || now - buffer.time >= typeaheadWindow) {
81
+ return { buffer: { text: key, time: now }, pattern: key };
82
+ }
83
+ if (buffer.text === key) {
84
+ return { buffer: { text: buffer.text, time: now }, pattern: key };
85
+ }
86
+ return { buffer: { text: buffer.text + key, time: now }, pattern: buffer.text + key };
87
+ };
88
+
89
+ exports.flattenTree = flattenTree;
90
+ exports.isBranch = isBranch;
91
+ exports.matchTypeahead = matchTypeahead;
92
+ exports.nearestVisible = nearestVisible;
93
+ exports.typeaheadStep = typeaheadStep;
94
+ exports.typeaheadWindow = typeaheadWindow;
95
+ //# sourceMappingURL=tree.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.cjs","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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"}
@@ -68,6 +68,8 @@ var ui_motion = require('./ui.motion.cjs');
68
68
  draws it from the wrapping control instead of the track it contains. */
69
69
  const focusRing = "focus:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-sherick-focus focus-visible:outline-offset-[3px]";
70
70
  const focusRingInset = "focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-sherick-focus";
71
+ /* A focus owner that also contains a subtree paints the inset ring on its direct row only. */
72
+ const parentFocusRingInset = "[:focus-visible>&]:ring-2 [:focus-visible>&]:ring-inset [:focus-visible>&]:ring-sherick-focus";
71
73
  /* The ring of a value control that is itself the focusable element: it wears it whenever it
72
74
  holds focus, however it was focused, and whenever the surface it opened is on screen. A select
73
75
  hands the DOM focus to its list while that list is open, so the ring follows the control's
@@ -81,6 +83,8 @@ const focusRingHeld = "focus:outline-none focus:outline focus:outline-2 focus:ou
81
83
  input is `:focus-visible` only for the keyboard, so a slider's handle saves its ring for the
82
84
  keyboard and lets its own engagement answer the pointer. */
83
85
  const focusRingWithin = "has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-sherick-focus has-[:focus-visible]:outline-offset-[3px]";
86
+ /* A file drag marks the same target with the same outline as visible keyboard focus. */
87
+ const focusRingDrag = "data-[dragging]:outline data-[dragging]:outline-2 data-[dragging]:outline-sherick-focus data-[dragging]:outline-offset-[3px]";
84
88
  const groupFocusRing = "group-focus-visible:outline group-focus-visible:outline-2 group-focus-visible:outline-sherick-focus group-focus-visible:outline-offset-[3px]";
85
89
  /* Motion is not owned here. Every physical event — feedback, tactile, arrive, orient,
86
90
  relocate, direct, presence and activity — is a named recipe in `ui.motion.ts`, which
@@ -94,7 +98,7 @@ const groupFocusRing = "group-focus-visible:outline group-focus-visible:outline-
94
98
  touchable; pressing it returns it to the track.
95
99
  - floating: a surface that genuinely sits above the application.
96
100
  - control: the resting half of the tactile pair, for a part the user moves —
97
- a switch thumb, a selected segment.
101
+ a switch thumb, a selected segment or a held option/date selection.
98
102
  - recessed: the other half of that pair: a groove, a track or a well, which is
99
103
  recessed by definition, and a raised control while it is held, which
100
104
  lands at the same depth.
@@ -112,6 +116,9 @@ const elevation = {
112
116
  of the well is deepened and that boundary is what says what it is. See `selectable.markSurface`. */
113
117
  well: "shadow-sherick-well",
114
118
  };
119
+ /* An inset sheet's shaded upper wall, without a lower rim. Kept independent so
120
+ unrelated components do not bundle this recipe. */
121
+ const recessedTop = "shadow-sherick-recessed-top";
115
122
  /* Shape — semantic corner roles, never an arbitrary radius. Softness grows with
116
123
  the size of the object and the emphasis it carries:
117
124
  - control: ordinary controls and dense data regions — fields, rows, options,
@@ -179,8 +186,8 @@ const effectiveDisabled = "data-[disabled]:cursor-not-allowed data-[disabled]:op
179
186
  pressed a raised control returning to the recessed depth of its own track, plus
180
187
  a slight compression; a flat control stays flat and a floating one keeps
181
188
  its elevation
182
- selected a selected tone; depth comes from the component's anatomy — a
183
- segment inside a groove is raised, a row in a list is not
189
+ selected a held choice: selected tone plus control elevation on a segment,
190
+ option/tree row or date surface; never on mere navigation highlight
184
191
  disabled 45% opacity, no pointer affordance, no interactive state at all
185
192
  focus the shared outer focus ring, visible on keyboard focus */
186
193
  const state = {
@@ -220,7 +227,8 @@ const state = {
220
227
  field: {
221
228
  hover: "hover:bg-sherick-surface-high/[0.82]",
222
229
  focus: "focus:bg-sherick-surface-high/[0.9]",
223
- focusWithin: "focus-within:bg-sherick-surface-high/[0.9]",
230
+ /* A composite's engaged tone outranks hover, independent of variant emission order. */
231
+ focusWithin: "[&&]:focus-within:bg-sherick-surface-high/[0.9]",
224
232
  engaged: "bg-sherick-surface-high/[0.9]",
225
233
  errorHover: "hover:bg-sherick-danger/[0.10]",
226
234
  errorFocus: "focus:bg-sherick-danger/[0.13]",
@@ -231,7 +239,7 @@ const state = {
231
239
  invalid: "data-[invalid]:bg-sherick-danger/[0.075]",
232
240
  invalidHover: "data-[invalid]:hover:bg-sherick-danger/[0.10]",
233
241
  invalidEngaged: "data-[invalid]:bg-sherick-danger/[0.13]",
234
- invalidFocusWithin: "data-[invalid]:focus-within:bg-sherick-danger/[0.13]",
242
+ invalidFocusWithin: "[&&]:data-[invalid]:focus-within:bg-sherick-danger/[0.13]",
235
243
  },
236
244
  };
237
245
  /* State layers.
@@ -310,6 +318,8 @@ const hitArea = "relative after:pointer-events-auto after:absolute after:inset-[
310
318
  should sit back from the surface around them.
311
319
  - matte: a matte surface that separates from the canvas by tone alone.
312
320
  - matteHigh: the second matte step, for nesting inside another matte surface.
321
+ - matteInset: a passive sheet tucked under a stronger control; subdued without
322
+ blending into the canvas or needing a drawn rim.
313
323
  - control: the fill every text control shares, plus its placeholder tone.
314
324
  - handle: the fill of a small part the user has to find — a value control's handle.
315
325
  Surface steps sit within a few percent of their neighbours, so this role
@@ -357,6 +367,10 @@ const material = {
357
367
  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)]",
358
368
  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)]",
359
369
  };
370
+ /* Dark neutral behind native video, including poster loading and letterboxing. */
371
+ const mediaCanvas = "bg-sherick-media";
372
+ /* A passive sheet tucked below a stronger control; independently tree-shakeable. */
373
+ const matteInset = "bg-sherick-surface-high/[0.35] text-sherick-ink";
360
374
  /* Density — three control steps plus the accessible hit target.
361
375
  Density owns height and the type step, so controls of one density share a
362
376
  rhythm. A component's anatomy owns its padding: a button's inline padding is
@@ -468,8 +482,9 @@ const detail = {
468
482
  The sheet never exceeds what the viewport leaves it, and scrolls inside itself rather than
469
483
  growing. Its width is the list's own decision: a control's list is never narrower than the
470
484
  control it came from and grows to fit its own content until the viewport clamp, and a command
471
- list only ever grows to its content. A row is flat — depth never announces a state —
472
- and one row step of `stateLayer.quiet` plus `stateLayer.activeRow` carries both its hover and
485
+ list only ever grows to its content. Unselected rows and commands stay flat; held options
486
+ compose `tone.selected` with `elevation.control`. One row step of `stateLayer.quiet` plus
487
+ `stateLayer.activeRow` carries both its hover and
473
488
  its keyboard highlight, so an option and a command are highlighted by the same tone at the
474
489
  same strength. Keyboard focus adds the shared inset ring independently of that highlight.
475
490
  Selection is a tint of the sheet through `tone.selected` and never the opaque
@@ -659,6 +674,7 @@ exports.edgeTone = edgeTone;
659
674
  exports.elevation = elevation;
660
675
  exports.fieldLayout = fieldLayout;
661
676
  exports.focusRing = focusRing;
677
+ exports.focusRingDrag = focusRingDrag;
662
678
  exports.focusRingHeld = focusRingHeld;
663
679
  exports.focusRingInset = focusRingInset;
664
680
  exports.focusRingWithin = focusRingWithin;
@@ -666,7 +682,11 @@ exports.groupFocusRing = groupFocusRing;
666
682
  exports.hitArea = hitArea;
667
683
  exports.list = list;
668
684
  exports.material = material;
685
+ exports.matteInset = matteInset;
686
+ exports.mediaCanvas = mediaCanvas;
669
687
  exports.overlay = overlay;
688
+ exports.parentFocusRingInset = parentFocusRingInset;
689
+ exports.recessedTop = recessedTop;
670
690
  exports.recipeAlphas = recipeAlphas;
671
691
  exports.selectable = selectable;
672
692
  exports.shape = shape;