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.
- 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/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/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 +362 -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,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
|
|
183
|
-
|
|
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
|
-
|
|
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.
|
|
472
|
-
|
|
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;
|