@unofficialbox/box-open-elements 0.5.0 → 0.6.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 +3 -3
- package/dist/components/collections/table.d.ts +57 -3
- package/dist/components/collections/table.js +339 -20
- package/dist/components/feedback/due-badge.d.ts +38 -0
- package/dist/components/feedback/due-badge.js +168 -0
- package/dist/components/feedback/due-types.d.ts +32 -0
- package/dist/components/feedback/due-types.js +91 -0
- package/dist/components/feedback/progress-steps.d.ts +51 -5
- package/dist/components/feedback/progress-steps.js +200 -22
- package/dist/components/feedback/stage-path.d.ts +44 -0
- package/dist/components/feedback/stage-path.js +235 -0
- package/dist/components/forms/select.d.ts +21 -0
- package/dist/components/forms/select.js +100 -3
- package/dist/components/forms/text-field.d.ts +21 -0
- package/dist/components/forms/text-field.js +100 -1
- package/dist/components/layout/nav-sidebar.d.ts +14 -0
- package/dist/components/layout/nav-sidebar.js +49 -0
- package/dist/components/layout/split-view.d.ts +14 -0
- package/dist/components/layout/split-view.js +80 -1
- package/dist/components/overlays/command-palette.d.ts +62 -0
- package/dist/components/overlays/command-palette.js +537 -0
- package/dist/components/overlays/command-types.d.ts +90 -0
- package/dist/components/overlays/command-types.js +259 -0
- package/dist/components/overlays/drawer.d.ts +18 -0
- package/dist/components/overlays/drawer.js +135 -9
- package/dist/components/overlays/shortcuts-overlay.d.ts +48 -0
- package/dist/components/overlays/shortcuts-overlay.js +385 -0
- package/dist/components/overlays/tooltip.js +1 -1
- package/dist/element-maps.d.ts +292 -0
- package/dist/element-maps.js +1 -0
- package/dist/entries/activity-density.d.ts +1 -0
- package/dist/entries/activity-density.js +1 -0
- package/dist/entries/agent-chat.d.ts +1 -0
- package/dist/entries/agent-chat.js +1 -0
- package/dist/entries/audit-log.d.ts +1 -0
- package/dist/entries/audit-log.js +1 -0
- package/dist/entries/command-palette.d.ts +1 -0
- package/dist/entries/command-palette.js +1 -0
- package/dist/entries/compare-view.d.ts +1 -0
- package/dist/entries/compare-view.js +1 -0
- package/dist/entries/diff-viewer.d.ts +1 -0
- package/dist/entries/diff-viewer.js +1 -0
- package/dist/entries/due-badge.d.ts +1 -0
- package/dist/entries/due-badge.js +1 -0
- package/dist/entries/form-wizard.d.ts +1 -0
- package/dist/entries/form-wizard.js +1 -0
- package/dist/entries/lineage-graph.d.ts +1 -0
- package/dist/entries/lineage-graph.js +1 -0
- package/dist/entries/notification-bell.d.ts +1 -0
- package/dist/entries/notification-bell.js +1 -0
- package/dist/entries/notification-inbox.d.ts +1 -0
- package/dist/entries/notification-inbox.js +1 -0
- package/dist/entries/provenance-strip.d.ts +1 -0
- package/dist/entries/provenance-strip.js +1 -0
- package/dist/entries/run-trace.d.ts +1 -0
- package/dist/entries/run-trace.js +1 -0
- package/dist/entries/shortcuts-overlay.d.ts +1 -0
- package/dist/entries/shortcuts-overlay.js +1 -0
- package/dist/entries/signature-ceremony.d.ts +1 -0
- package/dist/entries/signature-ceremony.js +1 -0
- package/dist/entries/stage-path.d.ts +1 -0
- package/dist/entries/stage-path.js +1 -0
- package/dist/entries/timeline.d.ts +1 -0
- package/dist/entries/timeline.js +1 -0
- package/dist/entries/version-graph.d.ts +1 -0
- package/dist/entries/version-graph.js +1 -0
- package/dist/entries/version-list.d.ts +1 -0
- package/dist/entries/version-list.js +1 -0
- package/dist/entries/wizard-summary.d.ts +1 -0
- package/dist/entries/wizard-summary.js +1 -0
- package/dist/entries/work-queue.d.ts +1 -0
- package/dist/entries/work-queue.js +1 -0
- package/dist/entries/workload-board.d.ts +1 -0
- package/dist/entries/workload-board.js +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/patterns/agent-chat/agent-chat.d.ts +75 -0
- package/dist/patterns/agent-chat/agent-chat.js +820 -0
- package/dist/patterns/agent-chat/controller.d.ts +27 -0
- package/dist/patterns/agent-chat/controller.js +182 -0
- package/dist/patterns/agent-chat/index.d.ts +3 -0
- package/dist/patterns/agent-chat/index.js +6 -0
- package/dist/patterns/agent-chat/types.d.ts +109 -0
- package/dist/patterns/agent-chat/types.js +1 -0
- package/dist/patterns/audit/activity-density.d.ts +68 -0
- package/dist/patterns/audit/activity-density.js +428 -0
- package/dist/patterns/audit/audit-log.d.ts +81 -0
- package/dist/patterns/audit/audit-log.js +794 -0
- package/dist/patterns/audit/index.d.ts +3 -0
- package/dist/patterns/audit/index.js +7 -0
- package/dist/patterns/audit/shared.d.ts +10 -0
- package/dist/patterns/audit/shared.js +50 -0
- package/dist/patterns/audit/types.d.ts +143 -0
- package/dist/patterns/audit/types.js +327 -0
- package/dist/patterns/content-explorer/actions/controller.js +12 -2
- package/dist/patterns/content-explorer/box-transport.js +117 -25
- package/dist/patterns/content-explorer/content-explorer.d.ts +13 -1
- package/dist/patterns/content-explorer/content-explorer.js +36 -3
- package/dist/patterns/content-explorer/contracts.d.ts +24 -1
- package/dist/patterns/content-explorer/contracts.js +68 -11
- package/dist/patterns/content-explorer/controller.d.ts +16 -1
- package/dist/patterns/content-explorer/controller.js +130 -4
- package/dist/patterns/content-explorer/navigation/controller.js +11 -9
- package/dist/patterns/content-explorer/types.d.ts +58 -1
- package/dist/patterns/content-picker/content-picker.d.ts +71 -0
- package/dist/patterns/content-picker/content-picker.js +747 -0
- package/dist/patterns/content-picker/controller.d.ts +38 -0
- package/dist/patterns/content-picker/controller.js +141 -0
- package/dist/patterns/content-picker/index.d.ts +3 -0
- package/dist/patterns/content-picker/index.js +6 -0
- package/dist/patterns/content-picker/types.d.ts +52 -0
- package/dist/patterns/content-picker/types.js +26 -0
- package/dist/patterns/content-sidebar/content-sidebar.d.ts +43 -0
- package/dist/patterns/content-sidebar/content-sidebar.js +293 -0
- package/dist/patterns/content-sidebar/index.d.ts +2 -0
- package/dist/patterns/content-sidebar/index.js +4 -0
- package/dist/patterns/content-sidebar/types.d.ts +18 -0
- package/dist/patterns/content-sidebar/types.js +20 -0
- package/dist/patterns/content-uploader/box-transport.d.ts +14 -0
- package/dist/patterns/content-uploader/box-transport.js +54 -0
- package/dist/patterns/content-uploader/content-uploader.d.ts +62 -0
- package/dist/patterns/content-uploader/content-uploader.js +559 -0
- package/dist/patterns/content-uploader/controller.d.ts +39 -0
- package/dist/patterns/content-uploader/controller.js +227 -0
- package/dist/patterns/content-uploader/index.d.ts +4 -0
- package/dist/patterns/content-uploader/index.js +8 -0
- package/dist/patterns/content-uploader/types.d.ts +120 -0
- package/dist/patterns/content-uploader/types.js +38 -0
- package/dist/patterns/diff/compare-view.d.ts +47 -0
- package/dist/patterns/diff/compare-view.js +276 -0
- package/dist/patterns/diff/diff-viewer.d.ts +42 -0
- package/dist/patterns/diff/diff-viewer.js +434 -0
- package/dist/patterns/diff/engine.d.ts +22 -0
- package/dist/patterns/diff/engine.js +276 -0
- package/dist/patterns/diff/index.d.ts +4 -0
- package/dist/patterns/diff/index.js +8 -0
- package/dist/patterns/diff/types.d.ts +69 -0
- package/dist/patterns/diff/types.js +24 -0
- package/dist/patterns/form-wizard/controller.d.ts +39 -0
- package/dist/patterns/form-wizard/controller.js +157 -0
- package/dist/patterns/form-wizard/form-wizard.d.ts +63 -0
- package/dist/patterns/form-wizard/form-wizard.js +468 -0
- package/dist/patterns/form-wizard/index.d.ts +4 -0
- package/dist/patterns/form-wizard/index.js +8 -0
- package/dist/patterns/form-wizard/types.d.ts +109 -0
- package/dist/patterns/form-wizard/types.js +83 -0
- package/dist/patterns/form-wizard/wizard-summary.d.ts +62 -0
- package/dist/patterns/form-wizard/wizard-summary.js +324 -0
- package/dist/patterns/internal/safe-href.d.ts +17 -0
- package/dist/patterns/internal/safe-href.js +17 -0
- package/dist/patterns/lineage/index.d.ts +3 -0
- package/dist/patterns/lineage/index.js +6 -0
- package/dist/patterns/lineage/lineage-graph.d.ts +30 -0
- package/dist/patterns/lineage/lineage-graph.js +434 -0
- package/dist/patterns/lineage/provenance-strip.d.ts +22 -0
- package/dist/patterns/lineage/provenance-strip.js +194 -0
- package/dist/patterns/lineage/types.d.ts +53 -0
- package/dist/patterns/lineage/types.js +49 -0
- package/dist/patterns/notifications/index.d.ts +3 -0
- package/dist/patterns/notifications/index.js +6 -0
- package/dist/patterns/notifications/notification-bell.d.ts +38 -0
- package/dist/patterns/notifications/notification-bell.js +198 -0
- package/dist/patterns/notifications/notification-inbox.d.ts +46 -0
- package/dist/patterns/notifications/notification-inbox.js +537 -0
- package/dist/patterns/notifications/types.d.ts +61 -0
- package/dist/patterns/notifications/types.js +108 -0
- package/dist/patterns/preview/annotation-inspector.d.ts +25 -6
- package/dist/patterns/preview/annotation-inspector.js +239 -94
- package/dist/patterns/preview/annotation-thread.d.ts +25 -5
- package/dist/patterns/preview/annotation-thread.js +233 -82
- package/dist/patterns/preview/annotation-toolbar.d.ts +20 -6
- package/dist/patterns/preview/annotation-toolbar.js +162 -96
- package/dist/patterns/preview/content-preview-adapter.d.ts +10 -1
- package/dist/patterns/preview/content-preview-adapter.js +26 -1
- package/dist/patterns/preview/preview-element.d.ts +15 -2
- package/dist/patterns/preview/preview-element.js +223 -52
- package/dist/patterns/preview/provider-adapter.d.ts +47 -0
- package/dist/patterns/preview/provider-adapter.js +17 -0
- package/dist/patterns/run/index.d.ts +2 -0
- package/dist/patterns/run/index.js +4 -0
- package/dist/patterns/run/run-trace.d.ts +40 -0
- package/dist/patterns/run/run-trace.js +441 -0
- package/dist/patterns/run/types.d.ts +66 -0
- package/dist/patterns/run/types.js +152 -0
- package/dist/patterns/signature/index.d.ts +2 -0
- package/dist/patterns/signature/index.js +4 -0
- package/dist/patterns/signature/signature-ceremony.d.ts +33 -0
- package/dist/patterns/signature/signature-ceremony.js +317 -0
- package/dist/patterns/signature/types.d.ts +68 -0
- package/dist/patterns/signature/types.js +89 -0
- package/dist/patterns/timeline/index.d.ts +2 -0
- package/dist/patterns/timeline/index.js +4 -0
- package/dist/patterns/timeline/timeline.d.ts +27 -0
- package/dist/patterns/timeline/timeline.js +430 -0
- package/dist/patterns/timeline/types.d.ts +36 -0
- package/dist/patterns/timeline/types.js +27 -0
- package/dist/patterns/versions/graph-layout.d.ts +43 -0
- package/dist/patterns/versions/graph-layout.js +159 -0
- package/dist/patterns/versions/index.d.ts +4 -0
- package/dist/patterns/versions/index.js +8 -0
- package/dist/patterns/versions/shared.d.ts +4 -0
- package/dist/patterns/versions/shared.js +13 -0
- package/dist/patterns/versions/types.d.ts +37 -0
- package/dist/patterns/versions/types.js +39 -0
- package/dist/patterns/versions/version-graph.d.ts +36 -0
- package/dist/patterns/versions/version-graph.js +438 -0
- package/dist/patterns/versions/version-list.d.ts +39 -0
- package/dist/patterns/versions/version-list.js +407 -0
- package/dist/patterns/work-queue/controller.d.ts +26 -0
- package/dist/patterns/work-queue/controller.js +150 -0
- package/dist/patterns/work-queue/index.d.ts +4 -0
- package/dist/patterns/work-queue/index.js +7 -0
- package/dist/patterns/work-queue/types.d.ts +128 -0
- package/dist/patterns/work-queue/types.js +45 -0
- package/dist/patterns/work-queue/work-queue.d.ts +48 -0
- package/dist/patterns/work-queue/work-queue.js +534 -0
- package/dist/patterns/work-queue/workload-board.d.ts +53 -0
- package/dist/patterns/work-queue/workload-board.js +608 -0
- package/package.json +89 -5
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command model and the pure match/rank/group engine behind
|
|
3
|
+
* `box-command-palette`. DOM-free and deterministic, so ranking can be
|
|
4
|
+
* tested and reused (a host can drive its own launcher from the same
|
|
5
|
+
* functions).
|
|
6
|
+
*/
|
|
7
|
+
export const COMMAND_UNGROUPED_KEY = "";
|
|
8
|
+
export const COMMAND_UNGROUPED_LABEL = "Other";
|
|
9
|
+
/** Scoring weights. Tuned so an exact prefix always beats a scattered subsequence. */
|
|
10
|
+
const SCORE_EXACT = 1000;
|
|
11
|
+
const SCORE_PREFIX = 500;
|
|
12
|
+
const SCORE_WORD_START = 250;
|
|
13
|
+
const SCORE_SUBSTRING = 120;
|
|
14
|
+
const SCORE_CONSECUTIVE = 8;
|
|
15
|
+
const SCORE_KEYWORD = 40;
|
|
16
|
+
const PENALTY_DISTANCE = 1;
|
|
17
|
+
const isWordBoundary = (text, index) => index === 0 || /[\s\-_/.]/.test(text[index - 1]);
|
|
18
|
+
/**
|
|
19
|
+
* Subsequence match: every query character must appear in order. Returns the
|
|
20
|
+
* matched indices, preferring word starts and consecutive runs — so "cl" hits
|
|
21
|
+
* "**Cl**ause" ahead of "Can**c**e**l**".
|
|
22
|
+
*/
|
|
23
|
+
const subsequence = (haystack, needle) => {
|
|
24
|
+
const ranges = [];
|
|
25
|
+
let score = 0;
|
|
26
|
+
let cursor = 0;
|
|
27
|
+
let previous = -1;
|
|
28
|
+
for (const character of needle) {
|
|
29
|
+
let index = haystack.indexOf(character, cursor);
|
|
30
|
+
if (index === -1) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
// Prefer a later occurrence if it starts a word — matching initials reads
|
|
34
|
+
// far better than matching the middle of the first word.
|
|
35
|
+
let candidate = index;
|
|
36
|
+
while (candidate !== -1 && !isWordBoundary(haystack, candidate)) {
|
|
37
|
+
const next = haystack.indexOf(character, candidate + 1);
|
|
38
|
+
if (next === -1) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
if (isWordBoundary(haystack, next)) {
|
|
42
|
+
candidate = next;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
candidate = next;
|
|
46
|
+
}
|
|
47
|
+
if (candidate !== -1 && isWordBoundary(haystack, candidate)) {
|
|
48
|
+
index = candidate;
|
|
49
|
+
}
|
|
50
|
+
ranges.push(index);
|
|
51
|
+
if (isWordBoundary(haystack, index)) {
|
|
52
|
+
score += SCORE_WORD_START;
|
|
53
|
+
}
|
|
54
|
+
if (previous !== -1 && index === previous + 1) {
|
|
55
|
+
score += SCORE_CONSECUTIVE;
|
|
56
|
+
}
|
|
57
|
+
score -= Math.min(index - cursor, 20) * PENALTY_DISTANCE;
|
|
58
|
+
previous = index;
|
|
59
|
+
cursor = index + 1;
|
|
60
|
+
}
|
|
61
|
+
return { score, ranges };
|
|
62
|
+
};
|
|
63
|
+
const scoreCommand = (command, query) => {
|
|
64
|
+
const label = command.label.toLowerCase();
|
|
65
|
+
if (label === query) {
|
|
66
|
+
return { score: SCORE_EXACT, ranges: [...label].map((_, index) => index) };
|
|
67
|
+
}
|
|
68
|
+
const substringIndex = label.indexOf(query);
|
|
69
|
+
if (substringIndex === 0) {
|
|
70
|
+
return {
|
|
71
|
+
score: SCORE_PREFIX,
|
|
72
|
+
ranges: Array.from({ length: query.length }, (_, index) => index),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (substringIndex > 0) {
|
|
76
|
+
return {
|
|
77
|
+
score: SCORE_SUBSTRING + (isWordBoundary(label, substringIndex) ? SCORE_WORD_START : 0),
|
|
78
|
+
ranges: Array.from({ length: query.length }, (_, index) => substringIndex + index),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const bySubsequence = subsequence(label, query);
|
|
82
|
+
if (bySubsequence) {
|
|
83
|
+
return bySubsequence;
|
|
84
|
+
}
|
|
85
|
+
// Keywords match but never highlight — they are not on screen.
|
|
86
|
+
for (const keyword of command.keywords ?? []) {
|
|
87
|
+
if (keyword.toLowerCase().includes(query)) {
|
|
88
|
+
return { score: SCORE_KEYWORD, ranges: [] };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Rank commands against a query.
|
|
95
|
+
*
|
|
96
|
+
* An empty query returns everything in recent-first then input order, which
|
|
97
|
+
* is what a palette should show when it opens. Ties are broken by input order
|
|
98
|
+
* (the sort is stable), never by object identity, so the same catalogue always
|
|
99
|
+
* renders the same way.
|
|
100
|
+
*/
|
|
101
|
+
export const matchCommands = (commands, query, options = {}) => {
|
|
102
|
+
const recent = options.recentIds ?? [];
|
|
103
|
+
const recentRank = new Map(recent.map((id, index) => [id, recent.length - index]));
|
|
104
|
+
const normalized = query.trim().toLowerCase();
|
|
105
|
+
const pool = options.hideDisabled ? commands.filter(command => !command.disabled) : commands;
|
|
106
|
+
if (!normalized) {
|
|
107
|
+
return [...pool]
|
|
108
|
+
.map(command => ({
|
|
109
|
+
command,
|
|
110
|
+
score: (recentRank.get(command.id) ?? 0) * 10,
|
|
111
|
+
ranges: [],
|
|
112
|
+
}))
|
|
113
|
+
.sort((left, right) => right.score - left.score);
|
|
114
|
+
}
|
|
115
|
+
const matches = [];
|
|
116
|
+
for (const command of pool) {
|
|
117
|
+
const scored = scoreCommand(command, normalized);
|
|
118
|
+
if (!scored) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
matches.push({
|
|
122
|
+
command,
|
|
123
|
+
// A disabled command still matches, but never outranks a usable one.
|
|
124
|
+
score: scored.score + (recentRank.get(command.id) ?? 0) - (command.disabled ? SCORE_EXACT : 0),
|
|
125
|
+
ranges: scored.ranges,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return matches.sort((left, right) => right.score - left.score);
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Bucket ranked matches into sections, preserving rank order both between
|
|
132
|
+
* groups (a group leads where its best match ranks) and within them.
|
|
133
|
+
* Ungrouped commands trail.
|
|
134
|
+
*/
|
|
135
|
+
export const groupCommandMatches = (matches) => {
|
|
136
|
+
const groups = new Map();
|
|
137
|
+
for (const match of matches) {
|
|
138
|
+
const key = match.command.group ?? COMMAND_UNGROUPED_KEY;
|
|
139
|
+
let group = groups.get(key);
|
|
140
|
+
if (!group) {
|
|
141
|
+
group = { key, label: key || COMMAND_UNGROUPED_LABEL, matches: [] };
|
|
142
|
+
groups.set(key, group);
|
|
143
|
+
}
|
|
144
|
+
group.matches.push(match);
|
|
145
|
+
}
|
|
146
|
+
const ordered = [...groups.values()];
|
|
147
|
+
const trailing = groups.get(COMMAND_UNGROUPED_KEY);
|
|
148
|
+
if (!trailing) {
|
|
149
|
+
return ordered;
|
|
150
|
+
}
|
|
151
|
+
return [...ordered.filter(group => group.key !== COMMAND_UNGROUPED_KEY), trailing];
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* The commands that declare a keyboard shortcut, grouped for a shortcuts
|
|
155
|
+
* sheet. Takes the same catalogue the palette does, so one list drives both
|
|
156
|
+
* surfaces and a shortcut can never be documented but unreachable — or
|
|
157
|
+
* reachable but undocumented.
|
|
158
|
+
*
|
|
159
|
+
* Groups keep catalogue order, with the ungrouped section trailing, matching
|
|
160
|
+
* how the palette sections its results.
|
|
161
|
+
*/
|
|
162
|
+
export const groupShortcutCommands = (commands) => {
|
|
163
|
+
const groups = new Map();
|
|
164
|
+
for (const command of commands) {
|
|
165
|
+
if (!command.shortcut) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const key = command.group ?? COMMAND_UNGROUPED_KEY;
|
|
169
|
+
let group = groups.get(key);
|
|
170
|
+
if (!group) {
|
|
171
|
+
group = { key, label: key || COMMAND_UNGROUPED_LABEL, commands: [] };
|
|
172
|
+
groups.set(key, group);
|
|
173
|
+
}
|
|
174
|
+
group.commands.push(command);
|
|
175
|
+
}
|
|
176
|
+
const ordered = [...groups.values()];
|
|
177
|
+
const trailing = groups.get(COMMAND_UNGROUPED_KEY);
|
|
178
|
+
if (!trailing) {
|
|
179
|
+
return ordered;
|
|
180
|
+
}
|
|
181
|
+
return [...ordered.filter(group => group.key !== COMMAND_UNGROUPED_KEY), trailing];
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Split a shortcut string into its keys: `mod+shift+k` → `["mod","shift","k"]`.
|
|
185
|
+
* Rendering each key separately is what lets a sheet show them as `<kbd>`
|
|
186
|
+
* elements rather than one opaque run of text.
|
|
187
|
+
*
|
|
188
|
+
* `+` is both the separator and a real key — `mod++` is how you write the zoom
|
|
189
|
+
* shortcut. A `+` with no key pending in front of it is the key itself, so
|
|
190
|
+
* `mod++` and `ctrl + +` both give `["mod", "+"]` rather than dropping it.
|
|
191
|
+
*/
|
|
192
|
+
export const splitShortcutKeys = (shortcut) => {
|
|
193
|
+
const keys = [];
|
|
194
|
+
let buffer = "";
|
|
195
|
+
const flush = () => {
|
|
196
|
+
const key = buffer.trim();
|
|
197
|
+
if (key) {
|
|
198
|
+
keys.push(key);
|
|
199
|
+
}
|
|
200
|
+
buffer = "";
|
|
201
|
+
};
|
|
202
|
+
for (const character of shortcut) {
|
|
203
|
+
if (character !== "+") {
|
|
204
|
+
buffer += character;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (!buffer.trim()) {
|
|
208
|
+
buffer = "+";
|
|
209
|
+
}
|
|
210
|
+
flush();
|
|
211
|
+
}
|
|
212
|
+
flush();
|
|
213
|
+
return keys;
|
|
214
|
+
};
|
|
215
|
+
/** Split a label into highlighted and plain runs for rendering. */
|
|
216
|
+
export const splitCommandLabel = (label, ranges) => {
|
|
217
|
+
if (ranges.length === 0) {
|
|
218
|
+
return [{ text: label, match: false }];
|
|
219
|
+
}
|
|
220
|
+
const flags = new Set(ranges);
|
|
221
|
+
const parts = [];
|
|
222
|
+
let buffer = "";
|
|
223
|
+
let bufferMatch = flags.has(0);
|
|
224
|
+
for (let index = 0; index < label.length; index += 1) {
|
|
225
|
+
const isMatch = flags.has(index);
|
|
226
|
+
if (isMatch !== bufferMatch && buffer) {
|
|
227
|
+
parts.push({ text: buffer, match: bufferMatch });
|
|
228
|
+
buffer = "";
|
|
229
|
+
}
|
|
230
|
+
bufferMatch = isMatch;
|
|
231
|
+
buffer += label[index];
|
|
232
|
+
}
|
|
233
|
+
if (buffer) {
|
|
234
|
+
parts.push({ text: buffer, match: bufferMatch });
|
|
235
|
+
}
|
|
236
|
+
return parts;
|
|
237
|
+
};
|
|
238
|
+
/** Attribute payloads are author input — validate every record. */
|
|
239
|
+
export const isCommandDescriptorRecord = (value) => {
|
|
240
|
+
if (typeof value !== "object" || value === null) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
const command = value;
|
|
244
|
+
if (typeof command.id !== "string" || command.id.length === 0) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
if (typeof command.label !== "string" || command.label.length === 0) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
if (command.keywords !== undefined) {
|
|
251
|
+
if (!Array.isArray(command.keywords)) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
if (command.keywords.some(keyword => typeof keyword !== "string")) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
};
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
/** Detail of the cancelable `dismiss` event: what asked the drawer to close. */
|
|
3
|
+
export interface DrawerDismissDetail {
|
|
4
|
+
source: "close-button" | "backdrop" | "escape";
|
|
5
|
+
}
|
|
2
6
|
export declare class Drawer extends BaseElement {
|
|
3
7
|
static readonly tagName: string;
|
|
4
8
|
static get observedAttributes(): string[];
|
|
@@ -18,6 +22,12 @@ export declare class Drawer extends BaseElement {
|
|
|
18
22
|
set open(value: boolean);
|
|
19
23
|
get position(): string;
|
|
20
24
|
set position(value: string);
|
|
25
|
+
/** Width preset for side drawers, height for the bottom sheet. */
|
|
26
|
+
get size(): string;
|
|
27
|
+
set size(value: string);
|
|
28
|
+
/** Busy: a saving/loading veil over the body; Close stays reachable. */
|
|
29
|
+
get busy(): boolean;
|
|
30
|
+
set busy(value: boolean);
|
|
21
31
|
get heading(): string;
|
|
22
32
|
set heading(value: string);
|
|
23
33
|
connectedCallback(): void;
|
|
@@ -27,6 +37,14 @@ export declare class Drawer extends BaseElement {
|
|
|
27
37
|
private portalToBody;
|
|
28
38
|
private restoreFromPortal;
|
|
29
39
|
protected renderTemplate(): void;
|
|
40
|
+
/**
|
|
41
|
+
* The unsaved-changes guard, host-owned: `dismiss` is cancelable, and a
|
|
42
|
+
* host that calls `preventDefault()` keeps the drawer open — the drawer
|
|
43
|
+
* learns nothing about forms. Programmatic `close()` is not guarded; the
|
|
44
|
+
* host asked for it.
|
|
45
|
+
*/
|
|
46
|
+
private requestDismiss;
|
|
30
47
|
protected setupListeners(): void;
|
|
31
48
|
protected update(): void;
|
|
49
|
+
private syncFooter;
|
|
32
50
|
}
|
|
@@ -18,16 +18,23 @@ const drawerStyles = `
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
[part="drawer"] {
|
|
21
|
-
width: min(${boePanel.drawerWidth}, calc(100vw - 2rem));
|
|
21
|
+
width: min(var(--drawer-size, ${boePanel.drawerWidth}), calc(100vw - 2rem));
|
|
22
22
|
height: 100%;
|
|
23
23
|
display: grid;
|
|
24
|
-
grid-template-rows: auto minmax(0, 1fr);
|
|
24
|
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
25
25
|
background: ${boePanel.background};
|
|
26
26
|
color: var(--boe-token-text-text, #222222);
|
|
27
27
|
border: ${boePanel.border};
|
|
28
28
|
box-shadow: ${boeOverlay.modalShadow};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/* Size presets scale the travel axis: width for side drawers, height for
|
|
32
|
+
the bottom sheet. */
|
|
33
|
+
[part="drawer"][data-size="small"] { --drawer-size: 320px; --drawer-block-size: 220px; }
|
|
34
|
+
[part="drawer"][data-size="medium"] { --drawer-size: ${boePanel.drawerWidth}; --drawer-block-size: 320px; }
|
|
35
|
+
[part="drawer"][data-size="large"] { --drawer-size: 640px; --drawer-block-size: 480px; }
|
|
36
|
+
[part="drawer"][data-size="full"] { --drawer-size: 100vw; --drawer-block-size: calc(100vh - 3rem); }
|
|
37
|
+
|
|
31
38
|
[part="drawer"][data-position="left"] {
|
|
32
39
|
border-left: 0;
|
|
33
40
|
border-top-right-radius: ${boeOverlay.modalRadius};
|
|
@@ -43,7 +50,7 @@ const drawerStyles = `
|
|
|
43
50
|
[part="drawer"][data-position="bottom"] {
|
|
44
51
|
width: 100%;
|
|
45
52
|
max-width: none;
|
|
46
|
-
height: min(320px, calc(100vh - 3rem));
|
|
53
|
+
height: min(var(--drawer-block-size, 320px), calc(100vh - 3rem));
|
|
47
54
|
border-left: 0;
|
|
48
55
|
border-right: 0;
|
|
49
56
|
border-bottom: 0;
|
|
@@ -51,6 +58,19 @@ const drawerStyles = `
|
|
|
51
58
|
border-top-right-radius: ${boeOverlay.modalRadius};
|
|
52
59
|
}
|
|
53
60
|
|
|
61
|
+
/* On a phone every drawer is the screen: partial overlays waste the one
|
|
62
|
+
dimension the device is short of. */
|
|
63
|
+
@media (max-width: 640px) {
|
|
64
|
+
[part="drawer"],
|
|
65
|
+
[part="drawer"][data-position="bottom"] {
|
|
66
|
+
width: 100vw;
|
|
67
|
+
max-width: none;
|
|
68
|
+
height: 100%;
|
|
69
|
+
border: 0;
|
|
70
|
+
border-radius: 0;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
[part="header"] {
|
|
55
75
|
display: flex;
|
|
56
76
|
align-items: start;
|
|
@@ -104,14 +124,56 @@ const drawerStyles = `
|
|
|
104
124
|
${boeNeutralInteractiveStyles('[part="close"]')}
|
|
105
125
|
|
|
106
126
|
[part="body"] {
|
|
127
|
+
position: relative;
|
|
107
128
|
padding: ${boeSpace[4]};
|
|
108
129
|
overflow: auto;
|
|
109
130
|
}
|
|
131
|
+
|
|
132
|
+
/* Footer is the third grid row: it stays put while the body scrolls. */
|
|
133
|
+
[part="footer"] {
|
|
134
|
+
padding: ${boeSpace[3]} ${boeSpace[4]};
|
|
135
|
+
border-top: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
|
|
136
|
+
background: ${boePanel.background};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
[part="footer"][hidden] {
|
|
140
|
+
display: none;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
[part="busy"] {
|
|
144
|
+
position: absolute;
|
|
145
|
+
inset: 0;
|
|
146
|
+
display: grid;
|
|
147
|
+
place-items: center;
|
|
148
|
+
background: color-mix(in srgb, var(--boe-token-surface-surface, #ffffff) 65%, transparent);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
[part="busy"][hidden] {
|
|
152
|
+
display: none;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
[part="busy-spinner"] {
|
|
156
|
+
inline-size: 1.6rem;
|
|
157
|
+
block-size: 1.6rem;
|
|
158
|
+
border-radius: 999px;
|
|
159
|
+
border: 3px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 20%, transparent);
|
|
160
|
+
border-top-color: var(--boe-token-surface-surface-brand, #0061d5);
|
|
161
|
+
animation: boe-drawer-spin 0.9s linear infinite;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@keyframes boe-drawer-spin {
|
|
165
|
+
to { transform: rotate(360deg); }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@media (prefers-reduced-motion: reduce) {
|
|
169
|
+
[part="busy-spinner"] { animation-duration: 1.6s; }
|
|
170
|
+
}
|
|
110
171
|
`;
|
|
172
|
+
const DRAWER_SIZES = new Set(["small", "medium", "large", "full"]);
|
|
111
173
|
export class Drawer extends BaseElement {
|
|
112
174
|
static tagName = DEFAULT_TAG_NAME;
|
|
113
175
|
static get observedAttributes() {
|
|
114
|
-
return ["description", "heading", "open", "position"];
|
|
176
|
+
return ["busy", "description", "heading", "open", "position", "size"];
|
|
115
177
|
}
|
|
116
178
|
openValue = false;
|
|
117
179
|
wasOpen = false;
|
|
@@ -152,6 +214,21 @@ export class Drawer extends BaseElement {
|
|
|
152
214
|
set position(value) {
|
|
153
215
|
this.setAttribute("position", value);
|
|
154
216
|
}
|
|
217
|
+
/** Width preset for side drawers, height for the bottom sheet. */
|
|
218
|
+
get size() {
|
|
219
|
+
const value = this.getAttribute("size");
|
|
220
|
+
return value && DRAWER_SIZES.has(value) ? value : "medium";
|
|
221
|
+
}
|
|
222
|
+
set size(value) {
|
|
223
|
+
this.setAttribute("size", value);
|
|
224
|
+
}
|
|
225
|
+
/** Busy: a saving/loading veil over the body; Close stays reachable. */
|
|
226
|
+
get busy() {
|
|
227
|
+
return this.hasAttribute("busy");
|
|
228
|
+
}
|
|
229
|
+
set busy(value) {
|
|
230
|
+
this.toggleAttribute("busy", Boolean(value));
|
|
231
|
+
}
|
|
155
232
|
get heading() {
|
|
156
233
|
return this.getAttribute("heading") ?? "Drawer";
|
|
157
234
|
}
|
|
@@ -213,6 +290,23 @@ export class Drawer extends BaseElement {
|
|
|
213
290
|
this.shadowRoot.innerHTML = `<div part="host"></div>`;
|
|
214
291
|
this.hostEl = this.shadowRoot.querySelector('[part="host"]');
|
|
215
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* The unsaved-changes guard, host-owned: `dismiss` is cancelable, and a
|
|
295
|
+
* host that calls `preventDefault()` keeps the drawer open — the drawer
|
|
296
|
+
* learns nothing about forms. Programmatic `close()` is not guarded; the
|
|
297
|
+
* host asked for it.
|
|
298
|
+
*/
|
|
299
|
+
requestDismiss(source) {
|
|
300
|
+
const proceed = this.dispatchEvent(new CustomEvent("dismiss", {
|
|
301
|
+
bubbles: true,
|
|
302
|
+
composed: true,
|
|
303
|
+
cancelable: true,
|
|
304
|
+
detail: { source },
|
|
305
|
+
}));
|
|
306
|
+
if (proceed) {
|
|
307
|
+
this.close();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
216
310
|
setupListeners() {
|
|
217
311
|
this.hostEl.addEventListener("click", event => {
|
|
218
312
|
const target = event.target;
|
|
@@ -220,12 +314,11 @@ export class Drawer extends BaseElement {
|
|
|
220
314
|
return;
|
|
221
315
|
}
|
|
222
316
|
if (target.getAttribute("part") === "backdrop") {
|
|
223
|
-
this.
|
|
317
|
+
this.requestDismiss("backdrop");
|
|
224
318
|
return;
|
|
225
319
|
}
|
|
226
320
|
if (target.closest('[part="close"]')) {
|
|
227
|
-
this.
|
|
228
|
-
this.close();
|
|
321
|
+
this.requestDismiss("close-button");
|
|
229
322
|
}
|
|
230
323
|
});
|
|
231
324
|
this.hostEl.addEventListener("keydown", event => {
|
|
@@ -236,8 +329,7 @@ export class Drawer extends BaseElement {
|
|
|
236
329
|
}
|
|
237
330
|
if (keyboardEvent.key === "Escape") {
|
|
238
331
|
keyboardEvent.preventDefault();
|
|
239
|
-
this.
|
|
240
|
-
this.close();
|
|
332
|
+
this.requestDismiss("escape");
|
|
241
333
|
return;
|
|
242
334
|
}
|
|
243
335
|
if (keyboardEvent.key === "Tab") {
|
|
@@ -276,15 +368,26 @@ export class Drawer extends BaseElement {
|
|
|
276
368
|
<div part="meta">
|
|
277
369
|
<h2 id="drawer-title"></h2>
|
|
278
370
|
<p part="description" hidden></p>
|
|
371
|
+
<slot name="header"></slot>
|
|
279
372
|
</div>
|
|
280
373
|
<button type="button" part="close" aria-label="Close drawer">Close</button>
|
|
281
374
|
</header>
|
|
282
375
|
<div part="body">
|
|
283
376
|
<slot></slot>
|
|
377
|
+
<div part="busy" hidden><span part="busy-spinner" aria-hidden="true"></span></div>
|
|
284
378
|
</div>
|
|
379
|
+
<footer part="footer" hidden>
|
|
380
|
+
<slot name="footer"></slot>
|
|
381
|
+
</footer>
|
|
285
382
|
</aside>
|
|
286
383
|
</div>
|
|
287
384
|
`;
|
|
385
|
+
// The footer row only exists when the host slots one: an empty sticky
|
|
386
|
+
// bar would just eat drawer height.
|
|
387
|
+
const footerSlot = this.hostEl.querySelector('slot[name="footer"]');
|
|
388
|
+
footerSlot?.addEventListener("slotchange", () => {
|
|
389
|
+
this.syncFooter();
|
|
390
|
+
});
|
|
288
391
|
}
|
|
289
392
|
this.backdropEl = this.hostEl.querySelector('[part="backdrop"]');
|
|
290
393
|
this.drawerEl = this.hostEl.querySelector('[part="drawer"]');
|
|
@@ -298,7 +401,23 @@ export class Drawer extends BaseElement {
|
|
|
298
401
|
}
|
|
299
402
|
if (this.drawerEl) {
|
|
300
403
|
this.drawerEl.dataset.position = this.position;
|
|
404
|
+
this.drawerEl.dataset.size = this.size;
|
|
405
|
+
if (this.busy) {
|
|
406
|
+
this.drawerEl.setAttribute("aria-busy", "true");
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
this.drawerEl.removeAttribute("aria-busy");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const busyEl = this.hostEl.querySelector('[part="busy"]');
|
|
413
|
+
if (busyEl) {
|
|
414
|
+
busyEl.hidden = !this.busy;
|
|
301
415
|
}
|
|
416
|
+
// The veil blocks the pointer; inert blocks the keyboard. Both or the
|
|
417
|
+
// busy state is a suggestion. Close (header) and footer actions stay out.
|
|
418
|
+
const bodyEl = this.hostEl.querySelector('[part="body"]');
|
|
419
|
+
bodyEl?.toggleAttribute("inert", this.busy);
|
|
420
|
+
this.syncFooter();
|
|
302
421
|
if (this.titleEl) {
|
|
303
422
|
this.titleEl.textContent = this.heading;
|
|
304
423
|
}
|
|
@@ -313,5 +432,12 @@ export class Drawer extends BaseElement {
|
|
|
313
432
|
});
|
|
314
433
|
}
|
|
315
434
|
}
|
|
435
|
+
syncFooter() {
|
|
436
|
+
const footerEl = this.hostEl.querySelector('[part="footer"]');
|
|
437
|
+
const footerSlot = this.hostEl.querySelector('slot[name="footer"]');
|
|
438
|
+
if (footerEl && footerSlot) {
|
|
439
|
+
footerEl.hidden = footerSlot.assignedNodes({ flatten: true }).length === 0;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
316
442
|
}
|
|
317
443
|
Drawer.register();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { CommandDescriptor } from "./command-types.js";
|
|
2
|
+
import { BaseElement } from "../../core/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* The keyboard shortcuts sheet that makes keyboard-first real — the pair to
|
|
5
|
+
* `box-command-palette`.
|
|
6
|
+
*
|
|
7
|
+
* It reads the *same* `CommandDescriptor[]` the palette does and lists only
|
|
8
|
+
* the commands that declare a `shortcut`. One catalogue driving both surfaces
|
|
9
|
+
* is the point: a shortcut cannot end up documented but unreachable, or
|
|
10
|
+
* reachable but undocumented, because there is only one place to add it.
|
|
11
|
+
*
|
|
12
|
+
* Keys render as `<kbd>` elements rather than a run of text, so the markup
|
|
13
|
+
* says what it means.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ShortcutsOverlay extends BaseElement {
|
|
16
|
+
static readonly tagName: string;
|
|
17
|
+
static get observedAttributes(): string[];
|
|
18
|
+
private hostEl;
|
|
19
|
+
private readonly focusRestore;
|
|
20
|
+
private wasOpen;
|
|
21
|
+
private commandsRaw;
|
|
22
|
+
private commandsCache;
|
|
23
|
+
private readonly onDocumentKeydown;
|
|
24
|
+
get open(): boolean;
|
|
25
|
+
set open(value: boolean);
|
|
26
|
+
get heading(): string;
|
|
27
|
+
set heading(value: string);
|
|
28
|
+
/**
|
|
29
|
+
* Character that opens the sheet. Defaults to `?`, the convention. Matched
|
|
30
|
+
* against the produced character, so the Shift needed to type it is already
|
|
31
|
+
* accounted for. Set empty to let the host own the trigger.
|
|
32
|
+
*/
|
|
33
|
+
get hotkey(): string;
|
|
34
|
+
set hotkey(value: string);
|
|
35
|
+
/** The same catalogue `box-command-palette` takes; validated per record. */
|
|
36
|
+
get commands(): CommandDescriptor[];
|
|
37
|
+
set commands(value: CommandDescriptor[]);
|
|
38
|
+
/** Only the commands that declare a shortcut, in catalogue order. */
|
|
39
|
+
get documentedCommands(): CommandDescriptor[];
|
|
40
|
+
show(): void;
|
|
41
|
+
hide(): void;
|
|
42
|
+
connectedCallback(): void;
|
|
43
|
+
disconnectedCallback(): void;
|
|
44
|
+
private dismiss;
|
|
45
|
+
protected renderTemplate(): void;
|
|
46
|
+
protected setupListeners(): void;
|
|
47
|
+
protected update(): void;
|
|
48
|
+
}
|