@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.
Files changed (219) hide show
  1. package/README.md +3 -3
  2. package/dist/components/collections/table.d.ts +57 -3
  3. package/dist/components/collections/table.js +339 -20
  4. package/dist/components/feedback/due-badge.d.ts +38 -0
  5. package/dist/components/feedback/due-badge.js +168 -0
  6. package/dist/components/feedback/due-types.d.ts +32 -0
  7. package/dist/components/feedback/due-types.js +91 -0
  8. package/dist/components/feedback/progress-steps.d.ts +51 -5
  9. package/dist/components/feedback/progress-steps.js +200 -22
  10. package/dist/components/feedback/stage-path.d.ts +44 -0
  11. package/dist/components/feedback/stage-path.js +235 -0
  12. package/dist/components/forms/select.d.ts +21 -0
  13. package/dist/components/forms/select.js +100 -3
  14. package/dist/components/forms/text-field.d.ts +21 -0
  15. package/dist/components/forms/text-field.js +100 -1
  16. package/dist/components/layout/nav-sidebar.d.ts +14 -0
  17. package/dist/components/layout/nav-sidebar.js +49 -0
  18. package/dist/components/layout/split-view.d.ts +14 -0
  19. package/dist/components/layout/split-view.js +80 -1
  20. package/dist/components/overlays/command-palette.d.ts +62 -0
  21. package/dist/components/overlays/command-palette.js +537 -0
  22. package/dist/components/overlays/command-types.d.ts +90 -0
  23. package/dist/components/overlays/command-types.js +259 -0
  24. package/dist/components/overlays/drawer.d.ts +18 -0
  25. package/dist/components/overlays/drawer.js +135 -9
  26. package/dist/components/overlays/shortcuts-overlay.d.ts +48 -0
  27. package/dist/components/overlays/shortcuts-overlay.js +385 -0
  28. package/dist/components/overlays/tooltip.js +1 -1
  29. package/dist/element-maps.d.ts +292 -0
  30. package/dist/element-maps.js +1 -0
  31. package/dist/entries/activity-density.d.ts +1 -0
  32. package/dist/entries/activity-density.js +1 -0
  33. package/dist/entries/agent-chat.d.ts +1 -0
  34. package/dist/entries/agent-chat.js +1 -0
  35. package/dist/entries/audit-log.d.ts +1 -0
  36. package/dist/entries/audit-log.js +1 -0
  37. package/dist/entries/command-palette.d.ts +1 -0
  38. package/dist/entries/command-palette.js +1 -0
  39. package/dist/entries/compare-view.d.ts +1 -0
  40. package/dist/entries/compare-view.js +1 -0
  41. package/dist/entries/diff-viewer.d.ts +1 -0
  42. package/dist/entries/diff-viewer.js +1 -0
  43. package/dist/entries/due-badge.d.ts +1 -0
  44. package/dist/entries/due-badge.js +1 -0
  45. package/dist/entries/form-wizard.d.ts +1 -0
  46. package/dist/entries/form-wizard.js +1 -0
  47. package/dist/entries/lineage-graph.d.ts +1 -0
  48. package/dist/entries/lineage-graph.js +1 -0
  49. package/dist/entries/notification-bell.d.ts +1 -0
  50. package/dist/entries/notification-bell.js +1 -0
  51. package/dist/entries/notification-inbox.d.ts +1 -0
  52. package/dist/entries/notification-inbox.js +1 -0
  53. package/dist/entries/provenance-strip.d.ts +1 -0
  54. package/dist/entries/provenance-strip.js +1 -0
  55. package/dist/entries/run-trace.d.ts +1 -0
  56. package/dist/entries/run-trace.js +1 -0
  57. package/dist/entries/shortcuts-overlay.d.ts +1 -0
  58. package/dist/entries/shortcuts-overlay.js +1 -0
  59. package/dist/entries/signature-ceremony.d.ts +1 -0
  60. package/dist/entries/signature-ceremony.js +1 -0
  61. package/dist/entries/stage-path.d.ts +1 -0
  62. package/dist/entries/stage-path.js +1 -0
  63. package/dist/entries/timeline.d.ts +1 -0
  64. package/dist/entries/timeline.js +1 -0
  65. package/dist/entries/version-graph.d.ts +1 -0
  66. package/dist/entries/version-graph.js +1 -0
  67. package/dist/entries/version-list.d.ts +1 -0
  68. package/dist/entries/version-list.js +1 -0
  69. package/dist/entries/wizard-summary.d.ts +1 -0
  70. package/dist/entries/wizard-summary.js +1 -0
  71. package/dist/entries/work-queue.d.ts +1 -0
  72. package/dist/entries/work-queue.js +1 -0
  73. package/dist/entries/workload-board.d.ts +1 -0
  74. package/dist/entries/workload-board.js +1 -0
  75. package/dist/index.d.ts +21 -0
  76. package/dist/index.js +21 -0
  77. package/dist/patterns/agent-chat/agent-chat.d.ts +75 -0
  78. package/dist/patterns/agent-chat/agent-chat.js +820 -0
  79. package/dist/patterns/agent-chat/controller.d.ts +27 -0
  80. package/dist/patterns/agent-chat/controller.js +182 -0
  81. package/dist/patterns/agent-chat/index.d.ts +3 -0
  82. package/dist/patterns/agent-chat/index.js +6 -0
  83. package/dist/patterns/agent-chat/types.d.ts +109 -0
  84. package/dist/patterns/agent-chat/types.js +1 -0
  85. package/dist/patterns/audit/activity-density.d.ts +68 -0
  86. package/dist/patterns/audit/activity-density.js +428 -0
  87. package/dist/patterns/audit/audit-log.d.ts +81 -0
  88. package/dist/patterns/audit/audit-log.js +794 -0
  89. package/dist/patterns/audit/index.d.ts +3 -0
  90. package/dist/patterns/audit/index.js +7 -0
  91. package/dist/patterns/audit/shared.d.ts +10 -0
  92. package/dist/patterns/audit/shared.js +50 -0
  93. package/dist/patterns/audit/types.d.ts +143 -0
  94. package/dist/patterns/audit/types.js +327 -0
  95. package/dist/patterns/content-explorer/actions/controller.js +12 -2
  96. package/dist/patterns/content-explorer/box-transport.js +117 -25
  97. package/dist/patterns/content-explorer/content-explorer.d.ts +13 -1
  98. package/dist/patterns/content-explorer/content-explorer.js +36 -3
  99. package/dist/patterns/content-explorer/contracts.d.ts +24 -1
  100. package/dist/patterns/content-explorer/contracts.js +68 -11
  101. package/dist/patterns/content-explorer/controller.d.ts +16 -1
  102. package/dist/patterns/content-explorer/controller.js +130 -4
  103. package/dist/patterns/content-explorer/navigation/controller.js +11 -9
  104. package/dist/patterns/content-explorer/types.d.ts +58 -1
  105. package/dist/patterns/content-picker/content-picker.d.ts +71 -0
  106. package/dist/patterns/content-picker/content-picker.js +747 -0
  107. package/dist/patterns/content-picker/controller.d.ts +38 -0
  108. package/dist/patterns/content-picker/controller.js +141 -0
  109. package/dist/patterns/content-picker/index.d.ts +3 -0
  110. package/dist/patterns/content-picker/index.js +6 -0
  111. package/dist/patterns/content-picker/types.d.ts +52 -0
  112. package/dist/patterns/content-picker/types.js +26 -0
  113. package/dist/patterns/content-sidebar/content-sidebar.d.ts +43 -0
  114. package/dist/patterns/content-sidebar/content-sidebar.js +293 -0
  115. package/dist/patterns/content-sidebar/index.d.ts +2 -0
  116. package/dist/patterns/content-sidebar/index.js +4 -0
  117. package/dist/patterns/content-sidebar/types.d.ts +18 -0
  118. package/dist/patterns/content-sidebar/types.js +20 -0
  119. package/dist/patterns/content-uploader/box-transport.d.ts +14 -0
  120. package/dist/patterns/content-uploader/box-transport.js +54 -0
  121. package/dist/patterns/content-uploader/content-uploader.d.ts +62 -0
  122. package/dist/patterns/content-uploader/content-uploader.js +559 -0
  123. package/dist/patterns/content-uploader/controller.d.ts +39 -0
  124. package/dist/patterns/content-uploader/controller.js +227 -0
  125. package/dist/patterns/content-uploader/index.d.ts +4 -0
  126. package/dist/patterns/content-uploader/index.js +8 -0
  127. package/dist/patterns/content-uploader/types.d.ts +120 -0
  128. package/dist/patterns/content-uploader/types.js +38 -0
  129. package/dist/patterns/diff/compare-view.d.ts +47 -0
  130. package/dist/patterns/diff/compare-view.js +276 -0
  131. package/dist/patterns/diff/diff-viewer.d.ts +42 -0
  132. package/dist/patterns/diff/diff-viewer.js +434 -0
  133. package/dist/patterns/diff/engine.d.ts +22 -0
  134. package/dist/patterns/diff/engine.js +276 -0
  135. package/dist/patterns/diff/index.d.ts +4 -0
  136. package/dist/patterns/diff/index.js +8 -0
  137. package/dist/patterns/diff/types.d.ts +69 -0
  138. package/dist/patterns/diff/types.js +24 -0
  139. package/dist/patterns/form-wizard/controller.d.ts +39 -0
  140. package/dist/patterns/form-wizard/controller.js +157 -0
  141. package/dist/patterns/form-wizard/form-wizard.d.ts +63 -0
  142. package/dist/patterns/form-wizard/form-wizard.js +468 -0
  143. package/dist/patterns/form-wizard/index.d.ts +4 -0
  144. package/dist/patterns/form-wizard/index.js +8 -0
  145. package/dist/patterns/form-wizard/types.d.ts +109 -0
  146. package/dist/patterns/form-wizard/types.js +83 -0
  147. package/dist/patterns/form-wizard/wizard-summary.d.ts +62 -0
  148. package/dist/patterns/form-wizard/wizard-summary.js +324 -0
  149. package/dist/patterns/internal/safe-href.d.ts +17 -0
  150. package/dist/patterns/internal/safe-href.js +17 -0
  151. package/dist/patterns/lineage/index.d.ts +3 -0
  152. package/dist/patterns/lineage/index.js +6 -0
  153. package/dist/patterns/lineage/lineage-graph.d.ts +30 -0
  154. package/dist/patterns/lineage/lineage-graph.js +434 -0
  155. package/dist/patterns/lineage/provenance-strip.d.ts +22 -0
  156. package/dist/patterns/lineage/provenance-strip.js +194 -0
  157. package/dist/patterns/lineage/types.d.ts +53 -0
  158. package/dist/patterns/lineage/types.js +49 -0
  159. package/dist/patterns/notifications/index.d.ts +3 -0
  160. package/dist/patterns/notifications/index.js +6 -0
  161. package/dist/patterns/notifications/notification-bell.d.ts +38 -0
  162. package/dist/patterns/notifications/notification-bell.js +198 -0
  163. package/dist/patterns/notifications/notification-inbox.d.ts +46 -0
  164. package/dist/patterns/notifications/notification-inbox.js +537 -0
  165. package/dist/patterns/notifications/types.d.ts +61 -0
  166. package/dist/patterns/notifications/types.js +108 -0
  167. package/dist/patterns/preview/annotation-inspector.d.ts +25 -6
  168. package/dist/patterns/preview/annotation-inspector.js +239 -94
  169. package/dist/patterns/preview/annotation-thread.d.ts +25 -5
  170. package/dist/patterns/preview/annotation-thread.js +233 -82
  171. package/dist/patterns/preview/annotation-toolbar.d.ts +20 -6
  172. package/dist/patterns/preview/annotation-toolbar.js +162 -96
  173. package/dist/patterns/preview/content-preview-adapter.d.ts +10 -1
  174. package/dist/patterns/preview/content-preview-adapter.js +26 -1
  175. package/dist/patterns/preview/preview-element.d.ts +15 -2
  176. package/dist/patterns/preview/preview-element.js +223 -52
  177. package/dist/patterns/preview/provider-adapter.d.ts +47 -0
  178. package/dist/patterns/preview/provider-adapter.js +17 -0
  179. package/dist/patterns/run/index.d.ts +2 -0
  180. package/dist/patterns/run/index.js +4 -0
  181. package/dist/patterns/run/run-trace.d.ts +40 -0
  182. package/dist/patterns/run/run-trace.js +441 -0
  183. package/dist/patterns/run/types.d.ts +66 -0
  184. package/dist/patterns/run/types.js +152 -0
  185. package/dist/patterns/signature/index.d.ts +2 -0
  186. package/dist/patterns/signature/index.js +4 -0
  187. package/dist/patterns/signature/signature-ceremony.d.ts +33 -0
  188. package/dist/patterns/signature/signature-ceremony.js +317 -0
  189. package/dist/patterns/signature/types.d.ts +68 -0
  190. package/dist/patterns/signature/types.js +89 -0
  191. package/dist/patterns/timeline/index.d.ts +2 -0
  192. package/dist/patterns/timeline/index.js +4 -0
  193. package/dist/patterns/timeline/timeline.d.ts +27 -0
  194. package/dist/patterns/timeline/timeline.js +430 -0
  195. package/dist/patterns/timeline/types.d.ts +36 -0
  196. package/dist/patterns/timeline/types.js +27 -0
  197. package/dist/patterns/versions/graph-layout.d.ts +43 -0
  198. package/dist/patterns/versions/graph-layout.js +159 -0
  199. package/dist/patterns/versions/index.d.ts +4 -0
  200. package/dist/patterns/versions/index.js +8 -0
  201. package/dist/patterns/versions/shared.d.ts +4 -0
  202. package/dist/patterns/versions/shared.js +13 -0
  203. package/dist/patterns/versions/types.d.ts +37 -0
  204. package/dist/patterns/versions/types.js +39 -0
  205. package/dist/patterns/versions/version-graph.d.ts +36 -0
  206. package/dist/patterns/versions/version-graph.js +438 -0
  207. package/dist/patterns/versions/version-list.d.ts +39 -0
  208. package/dist/patterns/versions/version-list.js +407 -0
  209. package/dist/patterns/work-queue/controller.d.ts +26 -0
  210. package/dist/patterns/work-queue/controller.js +150 -0
  211. package/dist/patterns/work-queue/index.d.ts +4 -0
  212. package/dist/patterns/work-queue/index.js +7 -0
  213. package/dist/patterns/work-queue/types.d.ts +128 -0
  214. package/dist/patterns/work-queue/types.js +45 -0
  215. package/dist/patterns/work-queue/work-queue.d.ts +48 -0
  216. package/dist/patterns/work-queue/work-queue.js +534 -0
  217. package/dist/patterns/work-queue/workload-board.d.ts +53 -0
  218. package/dist/patterns/work-queue/workload-board.js +608 -0
  219. 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.close();
317
+ this.requestDismiss("backdrop");
224
318
  return;
225
319
  }
226
320
  if (target.closest('[part="close"]')) {
227
- this.dispatchEvent(new CustomEvent("dismiss", { bubbles: true, composed: true }));
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.dispatchEvent(new CustomEvent("dismiss", { bubbles: true, composed: true }));
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
+ }