@embedpdf/core-ui 3.0.0-next.14 → 3.0.0-next.15
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/dist/index.cjs +1238 -250
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +598 -127
- package/dist/index.d.ts +598 -127
- package/dist/index.js +1195 -245
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,32 +1,42 @@
|
|
|
1
1
|
//#region src/schema.ts
|
|
2
2
|
const PINNED = 5;
|
|
3
|
-
|
|
3
|
+
/** A command button: `item('zoom:in', { variants: ['icon+label', 'icon'] })`. */
|
|
4
|
+
function item(command, options) {
|
|
4
5
|
return {
|
|
5
6
|
command,
|
|
6
|
-
...
|
|
7
|
+
...options
|
|
7
8
|
};
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* An item you draw yourself, by its slot name, and the command it becomes in the "More" menu:
|
|
12
|
+
* `custom('page-number', 'page:go-to', { variants: ['full', 'compact'] })`. Without a command,
|
|
13
|
+
* the item never leaves the bar.
|
|
14
|
+
*/
|
|
15
|
+
function custom(slot, terminal, options) {
|
|
16
|
+
return terminal === void 0 ? {
|
|
17
|
+
slot,
|
|
18
|
+
...options
|
|
19
|
+
} : {
|
|
11
20
|
slot,
|
|
12
|
-
|
|
21
|
+
terminal,
|
|
22
|
+
...options
|
|
13
23
|
};
|
|
14
24
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
id,
|
|
18
|
-
items: optsOrItems
|
|
19
|
-
};
|
|
20
|
-
const { items: optItems, ...opts } = optsOrItems;
|
|
25
|
+
/** A group of items, with a separator between groups: `group('zoom', ['zoom:out', 'zoom:in'])`. */
|
|
26
|
+
function group(id, items, options) {
|
|
21
27
|
return {
|
|
22
28
|
id,
|
|
23
|
-
...
|
|
24
|
-
items
|
|
29
|
+
...options,
|
|
30
|
+
items
|
|
25
31
|
};
|
|
26
32
|
}
|
|
27
33
|
const DEFAULT_IMPORTANCE = 3;
|
|
28
34
|
const DEFAULT_COMMAND_VARIANTS = ["icon"];
|
|
29
35
|
const DEFAULT_CUSTOM_VARIANTS = ["default"];
|
|
36
|
+
/** Whether a unit is a custom item with no command to become in menus: it never leaves the bar. */
|
|
37
|
+
const isStandaloneUnit = (unit) => unit.kind === "custom" && unit.terminal === null;
|
|
38
|
+
/** The command a unit runs: its own, or a custom item's `terminal`; null for a standalone item. */
|
|
39
|
+
const commandOfUnit = (unit) => unit.kind === "command" ? unit.command : unit.terminal;
|
|
30
40
|
function normalizeChild(groupId, child, seen) {
|
|
31
41
|
const asItem = typeof child === "string" ? { command: child } : child;
|
|
32
42
|
const unit = "slot" in asItem ? {
|
|
@@ -34,8 +44,8 @@ function normalizeChild(groupId, child, seen) {
|
|
|
34
44
|
key: `${groupId}:${asItem.slot}`,
|
|
35
45
|
slot: asItem.slot,
|
|
36
46
|
variants: asItem.variants ?? DEFAULT_CUSTOM_VARIANTS,
|
|
37
|
-
terminal: asItem.terminal,
|
|
38
|
-
importance: asItem.importance ?? 3
|
|
47
|
+
terminal: asItem.terminal ?? null,
|
|
48
|
+
importance: asItem.terminal === void 0 ? 5 : asItem.importance ?? 3
|
|
39
49
|
} : {
|
|
40
50
|
kind: "command",
|
|
41
51
|
key: `${groupId}:${asItem.command}`,
|
|
@@ -48,7 +58,11 @@ function normalizeChild(groupId, child, seen) {
|
|
|
48
58
|
seen.add(unit.key);
|
|
49
59
|
return unit;
|
|
50
60
|
}
|
|
51
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Resolve shorthands + defaults; validate uniqueness. Solver/projection input.
|
|
63
|
+
* A group holding a custom item without a command is pinned with it: it never
|
|
64
|
+
* collapses or overflows whole, so the item stays in the bar.
|
|
65
|
+
*/
|
|
52
66
|
function normalizeBar(bar) {
|
|
53
67
|
const seen = /* @__PURE__ */ new Set();
|
|
54
68
|
const groupIds = /* @__PURE__ */ new Set();
|
|
@@ -62,17 +76,19 @@ function normalizeBar(bar) {
|
|
|
62
76
|
if (!groups || groups.length === 0) continue;
|
|
63
77
|
sections.push({
|
|
64
78
|
name,
|
|
65
|
-
groups: groups.map((
|
|
66
|
-
if (groupIds.has(
|
|
67
|
-
groupIds.add(
|
|
79
|
+
groups: groups.map((group) => {
|
|
80
|
+
if (groupIds.has(group.id)) throw new Error(`[ui-core] duplicate group in bar: ${group.id}`);
|
|
81
|
+
groupIds.add(group.id);
|
|
82
|
+
const units = group.items.map((child) => normalizeChild(group.id, child, seen));
|
|
68
83
|
return {
|
|
69
|
-
id:
|
|
70
|
-
labelKey:
|
|
71
|
-
role:
|
|
72
|
-
shed:
|
|
73
|
-
collapse:
|
|
74
|
-
importance:
|
|
75
|
-
|
|
84
|
+
id: group.id,
|
|
85
|
+
labelKey: group.labelKey,
|
|
86
|
+
role: group.role ?? "buttons",
|
|
87
|
+
shed: group.shed ?? false,
|
|
88
|
+
collapse: group.collapse,
|
|
89
|
+
importance: units.some(isStandaloneUnit) ? 5 : group.importance ?? 3,
|
|
90
|
+
dependent: group.dependent === true,
|
|
91
|
+
units
|
|
76
92
|
};
|
|
77
93
|
})
|
|
78
94
|
});
|
|
@@ -84,204 +100,915 @@ function normalizeBar(bar) {
|
|
|
84
100
|
}
|
|
85
101
|
/**
|
|
86
102
|
* Drop units whose command is currently invisible (permissions, disabled
|
|
87
|
-
* categories, contextual visibility)
|
|
103
|
+
* categories, contextual visibility) before solving — an invisible unit must
|
|
88
104
|
* not consume budget or appear in the overflow. Groups that empty out vanish
|
|
89
|
-
* naturally in the solver's separator math and in the projection.
|
|
105
|
+
* naturally in the solver's separator math and in the projection. A custom
|
|
106
|
+
* item without a command has nothing to ask `keep` about, and stays. A bar
|
|
107
|
+
* that shows nothing ({@link hasVisibleUnits}) keeps no unit at all, so its
|
|
108
|
+
* dependent groups go too.
|
|
90
109
|
*/
|
|
91
110
|
function filterBar(bar, keep) {
|
|
111
|
+
const shows = hasVisibleUnits(bar, keep);
|
|
92
112
|
return {
|
|
93
113
|
id: bar.id,
|
|
94
|
-
sections: bar.sections.map((
|
|
95
|
-
name:
|
|
96
|
-
groups:
|
|
97
|
-
...
|
|
98
|
-
units:
|
|
114
|
+
sections: bar.sections.map((section) => ({
|
|
115
|
+
name: section.name,
|
|
116
|
+
groups: section.groups.map((group) => ({
|
|
117
|
+
...group,
|
|
118
|
+
units: shows ? group.units.filter((unit) => isStandaloneUnit(unit) || keep(unit)) : []
|
|
99
119
|
}))
|
|
100
120
|
}))
|
|
101
121
|
};
|
|
102
122
|
}
|
|
103
123
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
124
|
+
* Whether the bar shows anything once `keep` (as for {@link filterBar}) has had its say: a unit
|
|
125
|
+
* left in a group that isn't dependent. Dependent groups show only beside one, so a bar with
|
|
126
|
+
* only dependent groups left shows nothing.
|
|
107
127
|
*/
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
for (const bar of Object.values(chrome.modeBars ?? {})) normalizeBar(bar);
|
|
111
|
-
for (const strip of Object.values(chrome.strips ?? {})) normalizeBar(strip);
|
|
112
|
-
return chrome;
|
|
113
|
-
}
|
|
114
|
-
/** Dev-time cross-check: every command referenced by the chrome exists. */
|
|
115
|
-
function validateChrome(chrome, knownCommands) {
|
|
116
|
-
const problems = [];
|
|
117
|
-
const checkBar = (bar, where) => {
|
|
118
|
-
for (const section of normalizeBar(bar).sections) for (const g of section.groups) for (const u of g.units) {
|
|
119
|
-
const refs = u.kind === "command" ? [u.command] : [u.terminal];
|
|
120
|
-
for (const c of refs) if (!knownCommands.has(c)) problems.push(`${where}: unknown command "${c}"`);
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
for (const [id, bar] of Object.entries(chrome.bars)) checkBar(bar, `bars.${id}`);
|
|
124
|
-
for (const [id, bar] of Object.entries(chrome.modeBars ?? {})) checkBar(bar, `modeBars.${id}`);
|
|
125
|
-
for (const [id, strip] of Object.entries(chrome.strips ?? {})) checkBar(strip, `strips.${id}`);
|
|
126
|
-
for (const [id, menu] of Object.entries(chrome.menus ?? {})) for (const section of menu.sections) for (const c of section.items) if (!knownCommands.has(c)) problems.push(`menus.${id}: unknown command "${c}"`);
|
|
127
|
-
return problems;
|
|
128
|
+
function hasVisibleUnits(bar, keep) {
|
|
129
|
+
return bar.sections.some((section) => section.groups.some((group) => !group.dependent && group.units.some((unit) => isStandaloneUnit(unit) || keep(unit))));
|
|
128
130
|
}
|
|
129
131
|
//#endregion
|
|
130
|
-
//#region src/
|
|
132
|
+
//#region src/suggestions.ts
|
|
133
|
+
/**
|
|
134
|
+
* "Did you mean …?" for ids: the closest known id to one that doesn't exist,
|
|
135
|
+
* by edit distance, when it is close enough to be a typo.
|
|
136
|
+
*/
|
|
131
137
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* They are ordinary functions over data: no registry, no merge grammar, no
|
|
135
|
-
* order sensitivity beyond function composition. Anything they can't express
|
|
136
|
-
* is a sign the caller should write the schema value instead.
|
|
137
|
-
*
|
|
138
|
-
* Addressing is by BAR ID across all three bar collections (bars, modeBars,
|
|
139
|
-
* strips). Mode bars may also be addressed by their surface key
|
|
140
|
-
* ('mode:annotate') since that is what appears in `chrome.modeBars`.
|
|
138
|
+
* The number of single-character inserts, deletes, substitutions and swaps of two neighbours
|
|
139
|
+
* that turn `left` into `right`.
|
|
141
140
|
*/
|
|
141
|
+
function editDistance(left, right) {
|
|
142
|
+
if (left === right) return 0;
|
|
143
|
+
let beforePrevious = [];
|
|
144
|
+
let previous = Array.from({ length: right.length + 1 }, (_, j) => j);
|
|
145
|
+
for (let i = 1; i <= left.length; i++) {
|
|
146
|
+
const current = [i];
|
|
147
|
+
for (let j = 1; j <= right.length; j++) {
|
|
148
|
+
const cost = left[i - 1] === right[j - 1] ? 0 : 1;
|
|
149
|
+
let distance = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + cost);
|
|
150
|
+
if (i > 1 && j > 1 && left[i - 1] === right[j - 2] && left[i - 2] === right[j - 1]) distance = Math.min(distance, beforePrevious[j - 2] + 1);
|
|
151
|
+
current.push(distance);
|
|
152
|
+
}
|
|
153
|
+
beforePrevious = previous;
|
|
154
|
+
previous = current;
|
|
155
|
+
}
|
|
156
|
+
return previous[right.length];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The candidate closest to `id`, or `null` when none is within a typo's reach: at most a third
|
|
160
|
+
* of the id's length, and never more than 3 edits apart.
|
|
161
|
+
*/
|
|
162
|
+
function closestMatch(id, candidates) {
|
|
163
|
+
const reach = Math.min(3, Math.max(1, Math.floor(id.length / 3)));
|
|
164
|
+
let best = null;
|
|
165
|
+
let bestDistance = reach + 1;
|
|
166
|
+
for (const candidate of candidates) {
|
|
167
|
+
if (candidate === id) continue;
|
|
168
|
+
const distance = editDistance(id, candidate);
|
|
169
|
+
if (distance < bestDistance) {
|
|
170
|
+
best = candidate;
|
|
171
|
+
bestDistance = distance;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return best;
|
|
175
|
+
}
|
|
176
|
+
/** `' (did you mean "x"?)'` for the closest candidate, or `''` when there is none. */
|
|
177
|
+
function didYouMean(id, candidates) {
|
|
178
|
+
const match = closestMatch(id, candidates);
|
|
179
|
+
return match === null ? "" : ` (did you mean "${match}"?)`;
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/layout.ts
|
|
183
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
184
|
+
/** A layout of `data`, which it never changes. */
|
|
185
|
+
function createLayout(data = {}) {
|
|
186
|
+
return layoutOf(data, []);
|
|
187
|
+
}
|
|
188
|
+
/** Whether `value` is a layout made by {@link createLayout} or one of its edits. */
|
|
189
|
+
function isLayout(value) {
|
|
190
|
+
return typeof value === "object" && value !== null && states.has(value);
|
|
191
|
+
}
|
|
192
|
+
/** What the edits that made `layout` couldn't do. */
|
|
193
|
+
function layoutWarningsOf(layout) {
|
|
194
|
+
return states.get(layout)?.warnings ?? [];
|
|
195
|
+
}
|
|
196
|
+
/** `layout` with one more warning. */
|
|
197
|
+
function withLayoutWarning(layout, warning) {
|
|
198
|
+
const state = states.get(layout);
|
|
199
|
+
return state ? layoutOf(state.data, [...state.warnings, warning]) : layout;
|
|
200
|
+
}
|
|
201
|
+
function layoutOf(data, warnings) {
|
|
202
|
+
const next = ([nextData, added]) => layoutOf(nextData, added.length > 0 ? [...warnings, ...added] : warnings);
|
|
203
|
+
const layout = Object.freeze({
|
|
204
|
+
place: (bar, place) => next(placeBar(data, bar, place)),
|
|
205
|
+
addBar: (id, bar) => next([withRecord(data, "bars", id, bar), []]),
|
|
206
|
+
removeBar: (id) => next(removeBar(data, id)),
|
|
207
|
+
add: (item, options) => next(addItem(data, item, options)),
|
|
208
|
+
remove: (ids, options) => next(removeItems(data, typeof ids === "string" ? [ids] : ids, options)),
|
|
209
|
+
replace: (id, item) => next(replaceItem(data, id, item)),
|
|
210
|
+
addMenu: (id, sections) => next([withRecord(data, "menus", id, sections), []]),
|
|
211
|
+
addPanel: (id, panel) => next([withRecord(data, "panels", id, panel), []]),
|
|
212
|
+
panel: (id, changes) => next(updatePanel(data, id, changes)),
|
|
213
|
+
panels: (changes) => next([updatePanels(data, changes), []]),
|
|
214
|
+
removePanel: (id) => next(removePanel(data, id)),
|
|
215
|
+
tabs: (visibility) => next([{
|
|
216
|
+
...data,
|
|
217
|
+
tabs: visibility
|
|
218
|
+
}, []]),
|
|
219
|
+
toJSON: () => data
|
|
220
|
+
});
|
|
221
|
+
states.set(layout, {
|
|
222
|
+
data,
|
|
223
|
+
warnings
|
|
224
|
+
});
|
|
225
|
+
return layout;
|
|
226
|
+
}
|
|
142
227
|
const SECTION_NAMES = [
|
|
143
228
|
"start",
|
|
144
229
|
"center",
|
|
145
230
|
"end"
|
|
146
231
|
];
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
232
|
+
/** Whether a section entry is a group written as an object. */
|
|
233
|
+
const isGroupObject = (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) && "items" in entry;
|
|
234
|
+
/**
|
|
235
|
+
* A bar section's entries; none for a section that isn't a list. Layout data comes from
|
|
236
|
+
* people: what isn't the right shape is skipped here and named by `validateLayout`.
|
|
237
|
+
*/
|
|
238
|
+
function entriesOf(bar, name) {
|
|
239
|
+
const entries = bar[name];
|
|
240
|
+
return Array.isArray(entries) ? entries : [];
|
|
241
|
+
}
|
|
242
|
+
/** A section entry's items: a group's, or the lone item. */
|
|
243
|
+
function itemsOfEntry(entry) {
|
|
244
|
+
if (Array.isArray(entry)) return entry;
|
|
245
|
+
if (isGroupObject(entry)) return Array.isArray(entry.items) ? entry.items : [];
|
|
246
|
+
return [entry];
|
|
247
|
+
}
|
|
248
|
+
/** What an item is called in edits: its command id, your component's id, or `''` for a non-item. */
|
|
249
|
+
function itemIdOf(item) {
|
|
250
|
+
if (typeof item === "string") return item;
|
|
251
|
+
if (typeof item !== "object" || item === null) return "";
|
|
252
|
+
return "custom" in item ? item.custom : item.command;
|
|
253
|
+
}
|
|
254
|
+
/** The command an item runs or stands for in menus, or `null` for your component without one. */
|
|
255
|
+
function itemCommandOf(item) {
|
|
256
|
+
if (typeof item === "string") return item;
|
|
257
|
+
if (typeof item !== "object" || item === null) return null;
|
|
258
|
+
return "custom" in item ? item.command ?? null : item.command;
|
|
259
|
+
}
|
|
260
|
+
/** Whether `value` has an item's shape: a command id, `{ command }` or `{ custom }`. */
|
|
261
|
+
function isLayoutItem(value) {
|
|
262
|
+
if (typeof value === "string") return value.length > 0;
|
|
263
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
264
|
+
const item = value;
|
|
265
|
+
return typeof item.custom === "string" || typeof item.command === "string";
|
|
266
|
+
}
|
|
267
|
+
/** Every item id in a bar, in order, once each. */
|
|
268
|
+
function itemIdsOfBar(bar) {
|
|
269
|
+
const ids = /* @__PURE__ */ new Set();
|
|
270
|
+
for (const name of SECTION_NAMES) for (const entry of entriesOf(bar, name)) for (const item of itemsOfEntry(entry)) if (isLayoutItem(item)) ids.add(itemIdOf(item));
|
|
271
|
+
return [...ids];
|
|
272
|
+
}
|
|
273
|
+
/** A menu section's items; none for a section that isn't one. */
|
|
274
|
+
function itemsOfMenuSection(section) {
|
|
275
|
+
if (Array.isArray(section)) return section;
|
|
276
|
+
const items = section?.items;
|
|
277
|
+
return Array.isArray(items) ? items : [];
|
|
278
|
+
}
|
|
279
|
+
/** A menu's sections; none for a menu that isn't a list. */
|
|
280
|
+
const sectionsOfMenu = (menu) => Array.isArray(menu) ? menu : [];
|
|
281
|
+
/** Every item in a menu, in order, once each. */
|
|
282
|
+
const itemIdsOfMenu = (menu) => [...new Set(sectionsOfMenu(menu).flatMap((section) => itemsOfMenuSection(section)))];
|
|
283
|
+
const barsOf$1 = (data) => data.bars ?? {};
|
|
284
|
+
const menusOf = (data) => data.menus ?? {};
|
|
285
|
+
const panelsOf = (data) => data.panels ?? {};
|
|
286
|
+
function withRecord(data, key, id, value) {
|
|
160
287
|
return {
|
|
161
|
-
...
|
|
162
|
-
[
|
|
163
|
-
...
|
|
164
|
-
[
|
|
288
|
+
...data,
|
|
289
|
+
[key]: {
|
|
290
|
+
...data[key],
|
|
291
|
+
[id]: value
|
|
165
292
|
}
|
|
166
293
|
};
|
|
167
294
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
295
|
+
function withoutRecord(data, key, id) {
|
|
296
|
+
const { [id]: _removed, ...rest } = data[key] ?? {};
|
|
297
|
+
return {
|
|
298
|
+
...data,
|
|
299
|
+
[key]: rest
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* The entry with its items replaced: `null` when the edit took its last one, so the group goes.
|
|
304
|
+
* A group that held none to begin with stays: the `modes` group, whose tabs are derived.
|
|
305
|
+
*/
|
|
306
|
+
function withEntryItems(entry, items) {
|
|
307
|
+
if (items.length === 0) return itemsOfEntry(entry).length === 0 ? entry : null;
|
|
308
|
+
if (isGroupObject(entry)) return {
|
|
309
|
+
...entry,
|
|
310
|
+
items
|
|
311
|
+
};
|
|
312
|
+
if (Array.isArray(entry)) return items;
|
|
313
|
+
return items.length === 1 ? items[0] : items;
|
|
314
|
+
}
|
|
315
|
+
/** The bar with each entry mapped; entries mapped to `null` and sections left empty go. */
|
|
316
|
+
function mapEntries(bar, map) {
|
|
317
|
+
const next = {};
|
|
175
318
|
for (const name of SECTION_NAMES) {
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
319
|
+
const entries = entriesOf(bar, name);
|
|
320
|
+
if (entries.length === 0) continue;
|
|
321
|
+
const mapped = entries.map((entry, index) => map(entry, name, index)).filter((entry) => entry !== null);
|
|
322
|
+
if (mapped.length > 0) next[name] = mapped;
|
|
323
|
+
}
|
|
324
|
+
const { start: _start, center: _center, end: _end, ...rest } = bar;
|
|
325
|
+
return {
|
|
326
|
+
...rest,
|
|
327
|
+
...next
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function withMenuSectionItems(section, items) {
|
|
331
|
+
if (items.length === 0) return null;
|
|
332
|
+
return Array.isArray(section) ? items : {
|
|
333
|
+
...section,
|
|
334
|
+
items
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const notFound = (verb, id, where, candidates) => `${verb}: "${id}" is not in "${where}"${didYouMean(id, candidates)}`;
|
|
338
|
+
function placeBar(data, id, place) {
|
|
339
|
+
const bar = barsOf$1(data)[id];
|
|
340
|
+
if (!bar) return [data, [`place: no bar "${id}"${didYouMean(id, Object.keys(barsOf$1(data)))}`]];
|
|
341
|
+
return [withRecord(data, "bars", id, {
|
|
191
342
|
...bar,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
343
|
+
place
|
|
344
|
+
}), []];
|
|
345
|
+
}
|
|
346
|
+
function removeBar(data, id) {
|
|
347
|
+
if (!barsOf$1(data)[id]) return [data, [`removeBar: no bar "${id}"${didYouMean(id, Object.keys(barsOf$1(data)))}`]];
|
|
348
|
+
return [withoutRecord(data, "bars", id), []];
|
|
349
|
+
}
|
|
350
|
+
function addItem(data, item, options) {
|
|
351
|
+
if (!isLayoutItem(item)) return [data, [`add: ${JSON.stringify(item)} is not an item`]];
|
|
352
|
+
const to = options?.to;
|
|
353
|
+
if (!to) return [data, [`add: "${itemIdOf(item)}" needs a bar or menu to go to ({ to })`]];
|
|
354
|
+
const bar = barsOf$1(data)[to];
|
|
355
|
+
if (bar) return addToBar(data, to, bar, item, options);
|
|
356
|
+
const menu = menusOf(data)[to];
|
|
357
|
+
if (menu) return addToMenu(data, to, menu, item, options);
|
|
358
|
+
return [data, [`add: no bar or menu "${to}"${didYouMean(to, [...Object.keys(barsOf$1(data)), ...Object.keys(menusOf(data))])}`]];
|
|
359
|
+
}
|
|
360
|
+
function addToBar(data, barId, bar, item, options) {
|
|
361
|
+
const id = itemIdOf(item);
|
|
362
|
+
const warnings = [];
|
|
363
|
+
const twice = (items) => items.some((each) => itemIdOf(each) === id);
|
|
364
|
+
const already = `add: "${id}" is already in that group of "${barId}"`;
|
|
365
|
+
const anchor = options.before ?? options.after;
|
|
366
|
+
if (anchor !== void 0) {
|
|
367
|
+
for (const name of SECTION_NAMES) {
|
|
368
|
+
const entries = entriesOf(bar, name);
|
|
369
|
+
for (let index = 0; index < entries.length; index++) {
|
|
370
|
+
const items = itemsOfEntry(entries[index]);
|
|
371
|
+
const at = items.findIndex((each) => itemIdOf(each) === anchor);
|
|
372
|
+
if (at === -1) continue;
|
|
373
|
+
if (twice(items)) return [data, [already]];
|
|
374
|
+
const cut = options.before !== void 0 ? at : at + 1;
|
|
375
|
+
const nextItems = [
|
|
376
|
+
...items.slice(0, cut),
|
|
377
|
+
item,
|
|
378
|
+
...items.slice(cut)
|
|
379
|
+
];
|
|
380
|
+
return [withRecord(data, "bars", barId, mapEntries(bar, (entry, section, i) => section === name && i === index ? withEntryItems(entry, nextItems) : entry)), []];
|
|
381
|
+
}
|
|
195
382
|
}
|
|
196
|
-
|
|
383
|
+
warnings.push(notFound("add", anchor, barId, itemIdsOfBar(bar)));
|
|
384
|
+
}
|
|
385
|
+
if (options.group !== void 0) for (const name of SECTION_NAMES) {
|
|
386
|
+
const entries = entriesOf(bar, name);
|
|
387
|
+
const index = entries.findIndex((entry) => isGroupObject(entry) && entry.id === options.group);
|
|
388
|
+
if (index === -1) continue;
|
|
389
|
+
const items = itemsOfEntry(entries[index]);
|
|
390
|
+
if (twice(items)) return [data, [already]];
|
|
391
|
+
return [withRecord(data, "bars", barId, mapEntries(bar, (entry, section, i) => section === name && i === index ? withEntryItems(entry, [...items, item]) : entry)), warnings];
|
|
392
|
+
}
|
|
393
|
+
let section = options.section ?? "end";
|
|
394
|
+
if (!SECTION_NAMES.includes(section)) {
|
|
395
|
+
warnings.push(`add: "${section}" is not a section (start, center or end); using "end"`);
|
|
396
|
+
section = "end";
|
|
397
|
+
}
|
|
398
|
+
const entry = options.group !== void 0 ? {
|
|
399
|
+
id: options.group,
|
|
400
|
+
items: [item]
|
|
401
|
+
} : [item];
|
|
402
|
+
return [withRecord(data, "bars", barId, {
|
|
403
|
+
...bar,
|
|
404
|
+
[section]: [...entriesOf(bar, section), entry]
|
|
405
|
+
}), warnings];
|
|
406
|
+
}
|
|
407
|
+
function addToMenu(data, menuId, menu, item, options) {
|
|
408
|
+
const command = typeof item === "string" ? item : "custom" in item ? null : item.command;
|
|
409
|
+
if (command === null) return [data, [`add: "${menuId}" is a menu, which holds commands, not your own components`]];
|
|
410
|
+
if (itemIdsOfMenu(menu).includes(command)) return [data, [`add: "${command}" is already in "${menuId}"`]];
|
|
411
|
+
const warnings = [];
|
|
412
|
+
const anchor = options.before ?? options.after;
|
|
413
|
+
if (anchor !== void 0) {
|
|
414
|
+
const sections = sectionsOfMenu(menu);
|
|
415
|
+
const index = sections.findIndex((section) => itemsOfMenuSection(section).includes(anchor));
|
|
416
|
+
if (index !== -1) {
|
|
417
|
+
const items = itemsOfMenuSection(sections[index]);
|
|
418
|
+
const at = items.indexOf(anchor);
|
|
419
|
+
const cut = options.before !== void 0 ? at : at + 1;
|
|
420
|
+
const nextItems = [
|
|
421
|
+
...items.slice(0, cut),
|
|
422
|
+
command,
|
|
423
|
+
...items.slice(cut)
|
|
424
|
+
];
|
|
425
|
+
return [withRecord(data, "menus", menuId, sections.map((section, i) => i === index ? withMenuSectionItems(section, nextItems) : section)), []];
|
|
426
|
+
}
|
|
427
|
+
warnings.push(notFound("add", anchor, menuId, itemIdsOfMenu(menu)));
|
|
428
|
+
}
|
|
429
|
+
return [withRecord(data, "menus", menuId, [...sectionsOfMenu(menu), [command]]), warnings];
|
|
430
|
+
}
|
|
431
|
+
function removeItems(data, ids, options) {
|
|
432
|
+
const from = options?.from;
|
|
433
|
+
if (!from) return [data, [`remove: ${ids.map((id) => `"${id}"`).join(", ")} needs a bar or menu to come out of ({ from }); to leave something out everywhere, turn it off with \`disable\``]];
|
|
434
|
+
const gone = new Set(ids);
|
|
435
|
+
const found = /* @__PURE__ */ new Set();
|
|
436
|
+
const bar = barsOf$1(data)[from];
|
|
437
|
+
if (bar) {
|
|
438
|
+
const nextBar = mapEntries(bar, (entry) => withEntryItems(entry, itemsOfEntry(entry).filter((each) => {
|
|
439
|
+
const id = itemIdOf(each);
|
|
440
|
+
if (!gone.has(id)) return true;
|
|
441
|
+
found.add(id);
|
|
442
|
+
return false;
|
|
443
|
+
})));
|
|
444
|
+
const missing = ids.filter((id) => !found.has(id));
|
|
445
|
+
return [found.size > 0 ? withRecord(data, "bars", from, nextBar) : data, missing.map((id) => notFound("remove", id, from, itemIdsOfBar(bar)))];
|
|
446
|
+
}
|
|
447
|
+
const menu = menusOf(data)[from];
|
|
448
|
+
if (menu) {
|
|
449
|
+
const nextMenu = sectionsOfMenu(menu).map((section) => withMenuSectionItems(section, itemsOfMenuSection(section).filter((id) => {
|
|
450
|
+
if (!gone.has(id)) return true;
|
|
451
|
+
found.add(id);
|
|
452
|
+
return false;
|
|
453
|
+
}))).filter((section) => section !== null);
|
|
454
|
+
const missing = ids.filter((id) => !found.has(id));
|
|
455
|
+
return [found.size > 0 ? withRecord(data, "menus", from, nextMenu) : data, missing.map((id) => notFound("remove", id, from, itemIdsOfMenu(menu)))];
|
|
456
|
+
}
|
|
457
|
+
return [data, [`remove: no bar or menu "${from}"${didYouMean(from, [...Object.keys(barsOf$1(data)), ...Object.keys(menusOf(data))])}`]];
|
|
458
|
+
}
|
|
459
|
+
function replaceItem(data, id, item) {
|
|
460
|
+
if (!isLayoutItem(item)) return [data, [`replace: ${JSON.stringify(item)} is not an item`]];
|
|
461
|
+
let found = false;
|
|
462
|
+
const bars = {};
|
|
463
|
+
for (const [barId, bar] of Object.entries(barsOf$1(data))) {
|
|
464
|
+
if (!itemIdsOfBar(bar).includes(id)) {
|
|
465
|
+
bars[barId] = bar;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
found = true;
|
|
469
|
+
bars[barId] = mapEntries(bar, (entry) => withEntryItems(entry, itemsOfEntry(entry).map((each) => itemIdOf(each) === id ? item : each)));
|
|
470
|
+
}
|
|
471
|
+
const command = typeof item === "string" ? item : "custom" in item ? null : item.command;
|
|
472
|
+
const menus = {};
|
|
473
|
+
for (const [menuId, menu] of Object.entries(menusOf(data))) {
|
|
474
|
+
if (command === null || !itemIdsOfMenu(menu).includes(id)) {
|
|
475
|
+
menus[menuId] = menu;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
found = true;
|
|
479
|
+
menus[menuId] = sectionsOfMenu(menu).map((section) => withMenuSectionItems(section, itemsOfMenuSection(section).map((each) => each === id ? command : each)));
|
|
480
|
+
}
|
|
481
|
+
if (!found) return [data, [`replace: "${id}" is in no bar or menu${didYouMean(id, [...Object.values(barsOf$1(data)).flatMap(itemIdsOfBar), ...Object.values(menusOf(data)).flatMap(itemIdsOfMenu)])}`]];
|
|
482
|
+
return [{
|
|
483
|
+
...data,
|
|
484
|
+
bars,
|
|
485
|
+
menus
|
|
486
|
+
}, []];
|
|
197
487
|
}
|
|
488
|
+
/** A panel's `present` as a map of sizes: one way at every size, or the sizes it names. */
|
|
489
|
+
const panelPresentationsOf = (present) => typeof present === "string" ? {
|
|
490
|
+
compact: present,
|
|
491
|
+
medium: present,
|
|
492
|
+
expanded: present
|
|
493
|
+
} : present;
|
|
494
|
+
/** A panel's `width` as its fields: a number is where it starts. */
|
|
495
|
+
const panelWidthFieldsOf = (width) => typeof width === "number" ? { initial: width } : width;
|
|
198
496
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* emptied menus keep their key (commands may still target them) with no rows.
|
|
497
|
+
* `panel` with `changes`: each field replaced, except `present` and `width`, which change only
|
|
498
|
+
* what the change names (a map of sizes keeps the other sizes, `{ initial }` keeps the limits).
|
|
202
499
|
*/
|
|
203
|
-
function
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
});
|
|
220
|
-
|
|
500
|
+
function changedPanel(panel, changes) {
|
|
501
|
+
const next = { ...panel };
|
|
502
|
+
for (const [field, value] of Object.entries(changes)) if (value !== void 0) next[field] = value;
|
|
503
|
+
const { present, width } = changes;
|
|
504
|
+
if (typeof present === "object" && panel.present !== void 0) next.present = {
|
|
505
|
+
...panelPresentationsOf(panel.present),
|
|
506
|
+
...present
|
|
507
|
+
};
|
|
508
|
+
if (width !== void 0 && panel.width !== void 0 && (typeof width === "object" || typeof panel.width === "object")) next.width = {
|
|
509
|
+
...panelWidthFieldsOf(panel.width),
|
|
510
|
+
...panelWidthFieldsOf(width)
|
|
511
|
+
};
|
|
512
|
+
return next;
|
|
513
|
+
}
|
|
514
|
+
function updatePanel(data, id, changes) {
|
|
515
|
+
const panel = panelsOf(data)[id];
|
|
516
|
+
if (!panel) return [data, [`panel: no panel "${id}"${didYouMean(id, Object.keys(panelsOf(data)))}`]];
|
|
517
|
+
return [withRecord(data, "panels", id, changedPanel(panel, changes)), []];
|
|
518
|
+
}
|
|
519
|
+
function updatePanels(data, changes) {
|
|
520
|
+
const panels = Object.fromEntries(Object.entries(panelsOf(data)).map(([id, panel]) => [id, changedPanel(panel, changes)]));
|
|
221
521
|
return {
|
|
222
|
-
...
|
|
223
|
-
|
|
224
|
-
modeBars: filterColl(schema.modeBars),
|
|
225
|
-
strips: filterColl(schema.strips),
|
|
226
|
-
menus: schema.menus && Object.fromEntries(Object.entries(schema.menus).map(([k, menu]) => [k, {
|
|
227
|
-
...menu,
|
|
228
|
-
sections: menu.sections.map((s) => ({
|
|
229
|
-
...s,
|
|
230
|
-
items: s.items.filter((c) => !gone.has(c))
|
|
231
|
-
})).filter((s) => s.items.length > 0)
|
|
232
|
-
}]))
|
|
522
|
+
...data,
|
|
523
|
+
panels
|
|
233
524
|
};
|
|
234
525
|
}
|
|
526
|
+
function removePanel(data, id) {
|
|
527
|
+
if (!panelsOf(data)[id]) return [data, [`removePanel: no panel "${id}"${didYouMean(id, Object.keys(panelsOf(data)))}`]];
|
|
528
|
+
const button = `panel:${id}`;
|
|
529
|
+
const bars = Object.fromEntries(Object.entries(barsOf$1(data)).map(([barId, bar]) => [barId, itemIdsOfBar(bar).includes(button) ? mapEntries(bar, (entry) => withEntryItems(entry, itemsOfEntry(entry).filter((each) => itemIdOf(each) !== button))) : bar]));
|
|
530
|
+
const menus = Object.fromEntries(Object.entries(menusOf(data)).map(([menuId, menu]) => [menuId, itemIdsOfMenu(menu).includes(button) ? sectionsOfMenu(menu).map((section) => withMenuSectionItems(section, itemsOfMenuSection(section).filter((each) => each !== button))).filter((section) => section !== null) : menu]));
|
|
531
|
+
return [{
|
|
532
|
+
...withoutRecord(data, "panels", id),
|
|
533
|
+
bars,
|
|
534
|
+
menus
|
|
535
|
+
}, []];
|
|
536
|
+
}
|
|
537
|
+
/** The narrowest a medium viewer is, in px. */
|
|
538
|
+
const MEDIUM_MIN_WIDTH_PX = 600;
|
|
539
|
+
/** The narrowest an expanded viewer is, in px. */
|
|
540
|
+
const EXPANDED_MIN_WIDTH_PX = 1024;
|
|
541
|
+
/** The size class of a viewer this wide. */
|
|
542
|
+
function sizeOf(widthPx) {
|
|
543
|
+
if (widthPx < 600) return "compact";
|
|
544
|
+
return widthPx < 1024 ? "medium" : "expanded";
|
|
545
|
+
}
|
|
546
|
+
/** Whether two envs are the same: the layout function runs again only when one of the fields changes. */
|
|
547
|
+
const sameLayoutEnv = (left, right) => left.size === right.size && left.width === right.width && left.touch === right.touch && left.dir === right.dir;
|
|
235
548
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
549
|
+
* The layout an option gives at this env: the default without one, the function's result, or
|
|
550
|
+
* the option itself. A function that throws or returns no layout gives the default, with a
|
|
551
|
+
* warning saying so.
|
|
238
552
|
*/
|
|
239
|
-
function
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
553
|
+
function resolveLayout(option, defaults, env) {
|
|
554
|
+
if (option === void 0 || option === null) return defaults;
|
|
555
|
+
if (typeof option === "function") {
|
|
556
|
+
let result;
|
|
557
|
+
try {
|
|
558
|
+
result = option(defaults, env);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
return withLayoutWarning(defaults, `the layout function threw (${error instanceof Error ? error.message : String(error)}); showing the default layout`);
|
|
561
|
+
}
|
|
562
|
+
return toLayout(result, defaults, "the layout function returned");
|
|
563
|
+
}
|
|
564
|
+
return toLayout(option, defaults, "layout is");
|
|
565
|
+
}
|
|
566
|
+
function toLayout(value, defaults, what) {
|
|
567
|
+
if (isLayout(value)) return value;
|
|
568
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) return createLayout(value);
|
|
569
|
+
return withLayoutWarning(defaults, `${what} ${value === void 0 ? "nothing" : typeof value}, not a layout; showing the default layout`);
|
|
570
|
+
}
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region src/layout-compile.ts
|
|
573
|
+
/** The id of the group that holds the mode tabs. */
|
|
574
|
+
const MODES_GROUP = "modes";
|
|
575
|
+
/** The command of the mode with no bar of its own: viewing. */
|
|
576
|
+
const VIEW_MODE_COMMAND = "mode:view";
|
|
577
|
+
/** The command that opens a mode, and its tab. */
|
|
578
|
+
const modeCommandOf = (mode) => `mode:${mode}`;
|
|
579
|
+
/** The command that opens and closes a panel, and its button. */
|
|
580
|
+
const panelCommandOf = (panel) => `panel:${panel}`;
|
|
581
|
+
const EDGES$1 = [
|
|
582
|
+
"top",
|
|
583
|
+
"bottom",
|
|
584
|
+
"start",
|
|
585
|
+
"end"
|
|
586
|
+
];
|
|
587
|
+
/** Every place a bar can float over the pages: each corner, and the middle of each side. */
|
|
588
|
+
const FLOAT_POSITIONS = [
|
|
589
|
+
"top-start",
|
|
590
|
+
"top",
|
|
591
|
+
"top-end",
|
|
592
|
+
"start",
|
|
593
|
+
"end",
|
|
594
|
+
"bottom-start",
|
|
595
|
+
"bottom",
|
|
596
|
+
"bottom-end"
|
|
597
|
+
];
|
|
598
|
+
/** The layout's bars, by id, in order: the ones that are objects (`validateLayout` names the rest). */
|
|
599
|
+
const barsOf = (data) => Object.entries(data.bars ?? {}).filter((entry) => typeof entry[1] === "object" && entry[1] !== null);
|
|
600
|
+
/** The edge a place is on, or `null` for a place that isn't an edge. */
|
|
601
|
+
function edgeOf(place) {
|
|
602
|
+
if (typeof place === "string") return EDGES$1.includes(place) ? place : null;
|
|
603
|
+
if (typeof place === "object" && place !== null && "edge" in place) return EDGES$1.includes(place.edge) ? place.edge : null;
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
/** The bar a place rides along, or `null`. */
|
|
607
|
+
function hostOf(place) {
|
|
608
|
+
return typeof place === "object" && place !== null && "with" in place ? place.with : null;
|
|
609
|
+
}
|
|
610
|
+
/** What a place follows when it is over a selection, or `null`. */
|
|
611
|
+
function selectionOf(place) {
|
|
612
|
+
return typeof place === "object" && place !== null && "at" in place ? place.at : null;
|
|
613
|
+
}
|
|
614
|
+
/** Where a place floats over the pages, or `null` for a place that doesn't float. */
|
|
615
|
+
function floatOf(place) {
|
|
616
|
+
return typeof place === "object" && place !== null && "float" in place && FLOAT_POSITIONS.includes(place.float) ? place.float : null;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* The bars along an edge, from the outside in: in the order they were added, then each bar
|
|
620
|
+
* with `before` or `after` put next to the bar it names. A bar that names a bar not on this
|
|
621
|
+
* edge goes innermost.
|
|
622
|
+
*/
|
|
623
|
+
function barsOnEdge(data, edge) {
|
|
624
|
+
const placed = barsOf(data).filter(([, bar]) => edgeOf(bar.place) === edge);
|
|
625
|
+
const anchorOf = (place) => typeof place === "object" && place !== null && "edge" in place ? place.before !== void 0 ? {
|
|
626
|
+
id: place.before,
|
|
627
|
+
after: false
|
|
628
|
+
} : place.after !== void 0 ? {
|
|
629
|
+
id: place.after,
|
|
630
|
+
after: true
|
|
631
|
+
} : null : null;
|
|
632
|
+
const stack = placed.filter(([, bar]) => anchorOf(bar.place) === null).map(([id]) => id);
|
|
633
|
+
let pending = placed.filter(([, bar]) => anchorOf(bar.place) !== null);
|
|
634
|
+
for (let placedOne = true; placedOne && pending.length > 0;) {
|
|
635
|
+
placedOne = false;
|
|
636
|
+
pending = pending.filter(([id, bar]) => {
|
|
637
|
+
const anchor = anchorOf(bar.place);
|
|
638
|
+
const index = stack.indexOf(anchor.id);
|
|
639
|
+
if (index === -1) return true;
|
|
640
|
+
stack.splice(anchor.after ? index + 1 : index, 0, id);
|
|
641
|
+
placedOne = true;
|
|
642
|
+
return false;
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
return [...stack, ...pending.map(([id]) => id)];
|
|
646
|
+
}
|
|
647
|
+
/** The bars that ride along `host`, in the order they were added, nearest first. */
|
|
648
|
+
function barsWith(data, host) {
|
|
649
|
+
return barsOf(data).filter(([, bar]) => hostOf(bar.place) === host).map(([id]) => id);
|
|
650
|
+
}
|
|
651
|
+
/** `bars` in order, each followed by the bars that ride along it (nearest first), and theirs. */
|
|
652
|
+
function withRiders(data, bars) {
|
|
653
|
+
const stack = [];
|
|
654
|
+
const visit = (id) => {
|
|
655
|
+
if (stack.includes(id)) return;
|
|
656
|
+
stack.push(id);
|
|
657
|
+
for (const rider of barsWith(data, id)) visit(rider);
|
|
658
|
+
};
|
|
659
|
+
for (const id of bars) visit(id);
|
|
660
|
+
return stack;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Every bar drawn along an edge, from the outside in: each bar on the edge, followed by the bars
|
|
664
|
+
* that ride along it (nearest first) and the bars that ride along those.
|
|
665
|
+
*/
|
|
666
|
+
function edgeStackOf(data, edge) {
|
|
667
|
+
return withRiders(data, barsOnEdge(data, edge));
|
|
668
|
+
}
|
|
669
|
+
/** The bars floating at `position`, in the order they were added. */
|
|
670
|
+
function barsFloating(data, position) {
|
|
671
|
+
return barsOf(data).filter(([, bar]) => floatOf(bar.place) === position).map(([id]) => id);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Every bar drawn floating at `position`, from the viewer's edge in: each bar floating there,
|
|
675
|
+
* followed by the bars that ride along it, as {@link edgeStackOf} orders an edge.
|
|
676
|
+
*/
|
|
677
|
+
function floatStackOf(data, position) {
|
|
678
|
+
return withRiders(data, barsFloating(data, position));
|
|
679
|
+
}
|
|
680
|
+
/** The bars over a selection of this kind, in the order they were added. */
|
|
681
|
+
function barsAt(data, target) {
|
|
682
|
+
return barsOf(data).filter(([, bar]) => selectionOf(bar.place) === target).map(([id]) => id);
|
|
683
|
+
}
|
|
684
|
+
/** Every mode of the layout's bars, once each, in bar order. */
|
|
685
|
+
function modesOf(data) {
|
|
686
|
+
const modes = /* @__PURE__ */ new Set();
|
|
687
|
+
for (const [, bar] of barsOf(data)) if (typeof bar.mode === "string" && bar.mode.length > 0) modes.add(bar.mode);
|
|
688
|
+
return [...modes];
|
|
689
|
+
}
|
|
690
|
+
/** The bars shown in a mode, in bar order. */
|
|
691
|
+
function barsInMode(data, mode) {
|
|
692
|
+
return barsOf(data).filter(([, bar]) => bar.mode === mode).map(([id]) => id);
|
|
693
|
+
}
|
|
694
|
+
const NOTHING = /* @__PURE__ */ new Set();
|
|
695
|
+
const toBarChild = (item) => {
|
|
696
|
+
if (typeof item === "string") return item;
|
|
697
|
+
if ("custom" in item) {
|
|
698
|
+
const custom = { slot: item.custom };
|
|
699
|
+
if (item.command !== void 0) custom.terminal = item.command;
|
|
700
|
+
if (item.variants !== void 0) custom.variants = item.variants;
|
|
701
|
+
if (item.importance !== void 0) custom.importance = item.importance;
|
|
702
|
+
return custom;
|
|
703
|
+
}
|
|
704
|
+
const command = { command: item.command };
|
|
705
|
+
if (item.variants !== void 0) command.variants = item.variants;
|
|
706
|
+
if (item.importance !== void 0) command.importance = item.importance;
|
|
707
|
+
return command;
|
|
708
|
+
};
|
|
709
|
+
/** The mode tabs, each a label at importance 1, so the tab strip is the first to compact. */
|
|
710
|
+
function modeTabsOf(data, omit) {
|
|
711
|
+
const modes = modesOf(data).map(modeCommandOf).filter((command) => !omit.has(command));
|
|
712
|
+
if (modes.length === 0) return [];
|
|
713
|
+
return (omit.has("mode:view") ? modes : [VIEW_MODE_COMMAND, ...modes]).map((command) => ({
|
|
714
|
+
command,
|
|
715
|
+
variants: ["label"],
|
|
716
|
+
importance: 1
|
|
717
|
+
}));
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* A bar as the toolbar fits it, or `null` for a bar the layout doesn't have. Groups written as
|
|
721
|
+
* arrays and lone items get ids of their own (`start-0`, …); an item twice in one group shows
|
|
722
|
+
* once; a group with nothing left goes.
|
|
723
|
+
*/
|
|
724
|
+
function compileBar(data, id, options = {}) {
|
|
725
|
+
const bar = data.bars?.[id];
|
|
726
|
+
if (typeof bar !== "object" || bar === null) return null;
|
|
727
|
+
const omit = options.omit ?? NOTHING;
|
|
728
|
+
const used = /* @__PURE__ */ new Set();
|
|
729
|
+
const uniqueId = (wanted) => {
|
|
730
|
+
let groupId = wanted;
|
|
731
|
+
for (let count = 2; used.has(groupId); count++) groupId = `${wanted}-${count}`;
|
|
732
|
+
used.add(groupId);
|
|
733
|
+
return groupId;
|
|
734
|
+
};
|
|
735
|
+
const sections = {};
|
|
736
|
+
for (const name of SECTION_NAMES) {
|
|
737
|
+
const groups = [];
|
|
738
|
+
entriesOf(bar, name).forEach((entry, index) => {
|
|
739
|
+
const object = isGroupObject(entry) ? entry : null;
|
|
740
|
+
const seen = /* @__PURE__ */ new Set();
|
|
741
|
+
let items = itemsOfEntry(entry).filter((item) => {
|
|
742
|
+
if (!isLayoutItem(item)) return false;
|
|
743
|
+
const itemId = itemIdOf(item);
|
|
744
|
+
if (seen.has(itemId)) return false;
|
|
745
|
+
seen.add(itemId);
|
|
746
|
+
return typeof item === "object" && "custom" in item ? true : !omit.has(itemId);
|
|
747
|
+
});
|
|
748
|
+
const isModes = object?.id === MODES_GROUP;
|
|
749
|
+
if (isModes) items = [...modeTabsOf(data, omit).filter((tab) => !seen.has(tab.command)), ...items];
|
|
750
|
+
if (items.length === 0) return;
|
|
751
|
+
const group = {
|
|
752
|
+
id: uniqueId(object?.id ?? `${name}-${index}`),
|
|
753
|
+
items: items.map(toBarChild)
|
|
754
|
+
};
|
|
755
|
+
if (isModes) group.role = "tabs";
|
|
756
|
+
if (object?.titleKey !== void 0) group.labelKey = object.titleKey;
|
|
757
|
+
if (object?.shed !== void 0) group.shed = object.shed;
|
|
758
|
+
if (object?.collapse !== void 0) group.collapse = object.collapse;
|
|
759
|
+
if (object?.importance !== void 0) group.importance = object.importance;
|
|
760
|
+
if (object?.dependent === true) group.dependent = true;
|
|
761
|
+
groups.push(group);
|
|
762
|
+
});
|
|
763
|
+
if (groups.length > 0) sections[name] = groups;
|
|
764
|
+
}
|
|
253
765
|
return {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
766
|
+
id,
|
|
767
|
+
sections
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
/** When the document tabs show: as the layout says, else while more than one document is open. */
|
|
771
|
+
function tabsOf(data) {
|
|
772
|
+
return data.tabs ?? "multiple";
|
|
773
|
+
}
|
|
774
|
+
/** A menu's sections, or `null` for a menu the layout doesn't have. An empty section goes. */
|
|
775
|
+
function menuOf(data, id) {
|
|
776
|
+
const menu = data.menus?.[id];
|
|
777
|
+
if (!menu) return null;
|
|
778
|
+
const commandsOf = (section) => [...new Set(itemsOfMenuSection(section).filter((item) => typeof item === "string"))];
|
|
779
|
+
return sectionsOfMenu(menu).map((section) => {
|
|
780
|
+
if (Array.isArray(section) || typeof section !== "object" || section === null) return { items: commandsOf(section) };
|
|
781
|
+
const titled = section;
|
|
782
|
+
const read = { items: commandsOf(section) };
|
|
783
|
+
if (titled.title !== void 0) read.title = titled.title;
|
|
784
|
+
if (titled.titleKey !== void 0) read.titleKey = titled.titleKey;
|
|
785
|
+
return read;
|
|
786
|
+
}).filter((section) => section.items.length > 0);
|
|
787
|
+
}
|
|
788
|
+
const reachByData = /* @__PURE__ */ new WeakMap();
|
|
789
|
+
function reachOf(data) {
|
|
790
|
+
const known = reachByData.get(data);
|
|
791
|
+
if (known) return known;
|
|
792
|
+
const modes = modesOf(data).map((mode) => [mode, barsInMode(data, mode).map((id) => normalizeBar(compileBar(data, id)))]);
|
|
793
|
+
const menus = new Map(Object.keys(data.menus ?? {}).map((id) => [id, menuOf(data, id).flatMap((section) => section.items)]));
|
|
794
|
+
const buttons = /* @__PURE__ */ new Set();
|
|
795
|
+
for (const [, bar] of barsOf(data)) for (const name of SECTION_NAMES) for (const entry of entriesOf(bar, name)) for (const item of itemsOfEntry(entry)) if (typeof item === "string") buttons.add(item);
|
|
796
|
+
else if (isLayoutItem(item) && !("custom" in item)) buttons.add(item.command);
|
|
797
|
+
const reach = {
|
|
798
|
+
modes,
|
|
799
|
+
menus,
|
|
800
|
+
buttons: [...buttons]
|
|
265
801
|
};
|
|
802
|
+
reachByData.set(data, reach);
|
|
803
|
+
return reach;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* The commands the bars leave out because what they lead to shows nothing: the tab of a mode
|
|
807
|
+
* whose bars have nothing visible ({@link hasVisibleUnits}: dependent groups alone don't count),
|
|
808
|
+
* and a button whose menu has nothing visible (or that the layout has no menu for). Sorted, so
|
|
809
|
+
* two equal results compare equal; pass it to {@link compileBar} as `omit`. Cheap to call on every change: what it reads of the layout is
|
|
810
|
+
* read once per layout data.
|
|
811
|
+
*/
|
|
812
|
+
function omittedCommandsOf(data, resolve) {
|
|
813
|
+
const reach = reachOf(data);
|
|
814
|
+
const isVisible = (id) => resolve(id)?.visible === true;
|
|
815
|
+
const omitted = [];
|
|
816
|
+
for (const [mode, bars] of reach.modes) if (!bars.some((bar) => hasVisibleUnits(bar, (unit) => {
|
|
817
|
+
const command = commandOfUnit(unit);
|
|
818
|
+
return command !== null && isVisible(command);
|
|
819
|
+
}))) omitted.push(modeCommandOf(mode));
|
|
820
|
+
for (const button of reach.buttons) {
|
|
821
|
+
const menu = resolve(button)?.menu;
|
|
822
|
+
if (menu !== void 0 && !(reach.menus.get(menu) ?? []).some(isVisible)) omitted.push(button);
|
|
823
|
+
}
|
|
824
|
+
return omitted.sort();
|
|
266
825
|
}
|
|
267
826
|
//#endregion
|
|
268
|
-
//#region src/
|
|
827
|
+
//#region src/layout-validate.ts
|
|
828
|
+
const EDGES = [
|
|
829
|
+
"top",
|
|
830
|
+
"bottom",
|
|
831
|
+
"start",
|
|
832
|
+
"end"
|
|
833
|
+
];
|
|
834
|
+
const FLOAT_POSITIONS$1 = [
|
|
835
|
+
"top-start",
|
|
836
|
+
"top",
|
|
837
|
+
"top-end",
|
|
838
|
+
"start",
|
|
839
|
+
"end",
|
|
840
|
+
"bottom-start",
|
|
841
|
+
"bottom",
|
|
842
|
+
"bottom-end"
|
|
843
|
+
];
|
|
844
|
+
const SELECTION_TARGETS = ["selection", "annotation"];
|
|
845
|
+
const PRESENTATIONS = [
|
|
846
|
+
"dock",
|
|
847
|
+
"overlay",
|
|
848
|
+
"sheet"
|
|
849
|
+
];
|
|
850
|
+
const SIZES = [
|
|
851
|
+
"compact",
|
|
852
|
+
"medium",
|
|
853
|
+
"expanded"
|
|
854
|
+
];
|
|
855
|
+
const WIDTH_FIELDS = [
|
|
856
|
+
"initial",
|
|
857
|
+
"min",
|
|
858
|
+
"max"
|
|
859
|
+
];
|
|
860
|
+
const TABS = [
|
|
861
|
+
"always",
|
|
862
|
+
"multiple",
|
|
863
|
+
"never"
|
|
864
|
+
];
|
|
269
865
|
/**
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* imports at all. One object, frozen shape: it IS public API (the vanilla
|
|
273
|
-
* snippet passes it across the config boundary), so additions are features
|
|
274
|
-
* and removals are breaking.
|
|
866
|
+
* Every problem with a layout, as sentences: what its edits couldn't do first, then what its
|
|
867
|
+
* data names that the viewer doesn't have. Empty when there is none.
|
|
275
868
|
*/
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
869
|
+
function validateLayout(layout, known) {
|
|
870
|
+
const problems = isLayout(layout) ? [...layoutWarningsOf(layout)] : [];
|
|
871
|
+
const data = isLayout(layout) ? layout.toJSON() : layout;
|
|
872
|
+
if (typeof data !== "object" || data === null) return [...problems, "the layout is not an object"];
|
|
873
|
+
const bars = data.bars ?? {};
|
|
874
|
+
const knownCommands = new Set(known.commands);
|
|
875
|
+
const commands = new Set(knownCommands);
|
|
876
|
+
for (const mode of modesOf(data)) commands.add(modeCommandOf(mode));
|
|
877
|
+
if (modesOf(data).length > 0) commands.add(VIEW_MODE_COMMAND);
|
|
878
|
+
for (const panel of Object.keys(data.panels ?? {})) commands.add(panelCommandOf(panel));
|
|
879
|
+
const checkCommand = (where, command) => {
|
|
880
|
+
if (!commands.has(command) && known.hasCommand?.(command) !== true) problems.push(`${where}: no command "${command}"${didYouMean(command, commands)}`);
|
|
881
|
+
};
|
|
882
|
+
for (const [id, bar] of Object.entries(bars)) {
|
|
883
|
+
const where = `bar "${id}"`;
|
|
884
|
+
if (typeof bar !== "object" || bar === null) {
|
|
885
|
+
problems.push(`${where} is not a bar`);
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
problems.push(...placeProblems(where, id, bar.place, data));
|
|
889
|
+
if (bar.mode !== void 0 && bar.title === void 0 && bar.titleKey === void 0) {
|
|
890
|
+
if (!knownCommands.has(modeCommandOf(bar.mode))) problems.push(`${where}: its mode "${bar.mode}" has no title, so its tab reads "${bar.mode}"`);
|
|
891
|
+
}
|
|
892
|
+
problems.push(...itemProblems(where, bar, checkCommand));
|
|
893
|
+
}
|
|
894
|
+
for (const [id, menu] of Object.entries(data.menus ?? {})) problems.push(...menuProblems(`menu "${id}"`, menu, checkCommand));
|
|
895
|
+
for (const [id, panel] of Object.entries(data.panels ?? {})) problems.push(...panelProblems(`panel "${id}"`, panel));
|
|
896
|
+
if (data.tabs !== void 0 && !TABS.includes(data.tabs)) problems.push(`tabs: "${data.tabs}" is not one of always, multiple or never`);
|
|
897
|
+
return problems;
|
|
898
|
+
}
|
|
899
|
+
function placeProblems(where, id, place, data) {
|
|
900
|
+
const bars = data.bars ?? {};
|
|
901
|
+
const notAPlace = (shown) => `${where}: ${shown} is not a place (an edge, { edge, before | after }, { float }, { with } or { at })`;
|
|
902
|
+
if (place === void 0) return [`${where} has no place`];
|
|
903
|
+
if (typeof place === "string") return EDGES.includes(place) ? [] : [notAPlace(`"${place}"`)];
|
|
904
|
+
if (typeof place !== "object" || place === null) return [notAPlace(String(place))];
|
|
905
|
+
if ("edge" in place) {
|
|
906
|
+
if (!EDGES.includes(place.edge)) return [`${where}: "${place.edge}" is not an edge`];
|
|
907
|
+
const anchor = place.before ?? place.after;
|
|
908
|
+
if (anchor === void 0) return [];
|
|
909
|
+
const side = place.before !== void 0 ? "before" : "after";
|
|
910
|
+
const onEdge = Object.keys(bars).filter((other) => other !== id && edgeOf(bars[other].place) === place.edge);
|
|
911
|
+
if (onEdge.includes(anchor)) return [];
|
|
912
|
+
if (bars[anchor]) return [`${where} goes ${side} "${anchor}", which is not on the ${place.edge} edge`];
|
|
913
|
+
return [`${where} goes ${side} "${anchor}": no such bar${didYouMean(anchor, onEdge)}`];
|
|
914
|
+
}
|
|
915
|
+
if ("float" in place) return FLOAT_POSITIONS$1.includes(place.float) ? [] : [`${where}: "${place.float}" is not a floating position${didYouMean(place.float, FLOAT_POSITIONS$1)}`];
|
|
916
|
+
if ("with" in place) {
|
|
917
|
+
if (place.with === id) return [`${where} rides along itself`];
|
|
918
|
+
if (!bars[place.with]) return [`${where} rides along "${place.with}": no such bar${didYouMean(place.with, Object.keys(bars))}`];
|
|
919
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
920
|
+
for (let host = place.with; host !== null; host = hostOf(bars[host]?.place)) {
|
|
921
|
+
if (seen.has(host)) return [`${where} rides along a bar that rides along it`];
|
|
922
|
+
seen.add(host);
|
|
923
|
+
}
|
|
924
|
+
return [];
|
|
925
|
+
}
|
|
926
|
+
if ("at" in place) return SELECTION_TARGETS.includes(place.at) ? [] : [`${where}: "${place.at}" is not selection or annotation`];
|
|
927
|
+
return [notAPlace(JSON.stringify(place))];
|
|
928
|
+
}
|
|
929
|
+
function itemProblems(where, bar, checkCommand) {
|
|
930
|
+
const problems = [];
|
|
931
|
+
const groupIds = /* @__PURE__ */ new Set();
|
|
932
|
+
let groups = 0;
|
|
933
|
+
let dependentGroups = 0;
|
|
934
|
+
for (const name of SECTION_NAMES) {
|
|
935
|
+
const entries = bar[name];
|
|
936
|
+
if (entries === void 0) continue;
|
|
937
|
+
if (!Array.isArray(entries)) {
|
|
938
|
+
problems.push(`${where}: its ${name} section is not a list of groups`);
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
for (const entry of entries) {
|
|
942
|
+
if (isGroupObject(entry) && entry.id !== void 0) {
|
|
943
|
+
if (groupIds.has(entry.id)) problems.push(`${where}: two groups are called "${entry.id}"`);
|
|
944
|
+
groupIds.add(entry.id);
|
|
945
|
+
}
|
|
946
|
+
groups++;
|
|
947
|
+
if (isGroupObject(entry) && entry.dependent !== void 0) {
|
|
948
|
+
const dependent = entry.dependent;
|
|
949
|
+
if (dependent === true) dependentGroups++;
|
|
950
|
+
else if (dependent !== false) problems.push(`${where}: ${entry.id === void 0 ? "a group" : `group "${entry.id}"`} has dependent ${JSON.stringify(dependent)}, which is not true or false`);
|
|
951
|
+
}
|
|
952
|
+
const seen = /* @__PURE__ */ new Set();
|
|
953
|
+
for (const item of itemsOfEntry(entry)) {
|
|
954
|
+
if (!isLayoutItem(item)) {
|
|
955
|
+
problems.push(`${where}: ${JSON.stringify(item)} is not an item`);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
const id = itemIdOf(item);
|
|
959
|
+
if (seen.has(id)) problems.push(`${where}: "${id}" is twice in one group`);
|
|
960
|
+
seen.add(id);
|
|
961
|
+
const command = itemCommandOf(item);
|
|
962
|
+
if (command !== null) checkCommand(where, command);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (groups > 0 && dependentGroups === groups) problems.push(`${where}: every group in it is dependent, so it never shows`);
|
|
967
|
+
return problems;
|
|
968
|
+
}
|
|
969
|
+
function menuProblems(where, menu, checkCommand) {
|
|
970
|
+
if (!Array.isArray(menu)) return [`${where} is not a list of sections`];
|
|
971
|
+
const problems = [];
|
|
972
|
+
const seen = /* @__PURE__ */ new Set();
|
|
973
|
+
for (const section of menu) for (const command of itemsOfMenuSection(section)) {
|
|
974
|
+
if (typeof command !== "string") {
|
|
975
|
+
problems.push(`${where}: ${JSON.stringify(command)} is not a command id`);
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
if (seen.has(command)) problems.push(`${where}: "${command}" is in it twice`);
|
|
979
|
+
seen.add(command);
|
|
980
|
+
checkCommand(where, command);
|
|
981
|
+
}
|
|
982
|
+
return problems;
|
|
983
|
+
}
|
|
984
|
+
const isWidthPx = (value) => typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
985
|
+
function presentProblems(where, present) {
|
|
986
|
+
if (typeof present === "string") return PRESENTATIONS.includes(present) ? [] : [`${where}: present "${present}" is not dock, overlay or sheet`];
|
|
987
|
+
if (typeof present !== "object" || present === null || Array.isArray(present)) return [`${where}: present ${JSON.stringify(present)} is not a way to open, or one per size`];
|
|
988
|
+
const problems = [];
|
|
989
|
+
for (const [size, way] of Object.entries(present)) if (!SIZES.includes(size)) problems.push(`${where}: present has "${size}", not a size${didYouMean(size, SIZES)}`);
|
|
990
|
+
else if (way !== void 0 && !PRESENTATIONS.includes(way)) problems.push(`${where}: present.${size} "${way}" is not dock, overlay or sheet`);
|
|
991
|
+
return problems;
|
|
992
|
+
}
|
|
993
|
+
function widthProblems(where, width) {
|
|
994
|
+
if (typeof width === "number") return isWidthPx(width) ? [] : [`${where}: width ${width} is not a width in px`];
|
|
995
|
+
if (typeof width !== "object" || width === null || Array.isArray(width)) return [`${where}: width ${JSON.stringify(width)} is not a width in px, or { initial, min, max }`];
|
|
996
|
+
const problems = [];
|
|
997
|
+
const fields = width;
|
|
998
|
+
for (const [field, value] of Object.entries(fields)) if (!WIDTH_FIELDS.includes(field)) problems.push(`${where}: width has "${field}", not initial, min or max`);
|
|
999
|
+
else if (value !== void 0 && !isWidthPx(value)) problems.push(`${where}: width.${field} ${JSON.stringify(value)} is not a width in px`);
|
|
1000
|
+
if (isWidthPx(fields.min) && isWidthPx(fields.max) && fields.min > fields.max) problems.push(`${where}: width.min ${fields.min} is wider than width.max ${fields.max}`);
|
|
1001
|
+
return problems;
|
|
1002
|
+
}
|
|
1003
|
+
function panelProblems(where, panel) {
|
|
1004
|
+
if (typeof panel !== "object" || panel === null) return [`${where} is not a panel`];
|
|
1005
|
+
const problems = [];
|
|
1006
|
+
if (panel.side !== void 0 && panel.side !== "start" && panel.side !== "end") problems.push(`${where}: side "${panel.side}" is not start or end`);
|
|
1007
|
+
if (panel.present !== void 0) problems.push(...presentProblems(where, panel.present));
|
|
1008
|
+
if (panel.width !== void 0) problems.push(...widthProblems(where, panel.width));
|
|
1009
|
+
if (panel.resizable !== void 0 && typeof panel.resizable !== "boolean") problems.push(`${where}: resizable ${JSON.stringify(panel.resizable)} is not true or false`);
|
|
1010
|
+
return problems;
|
|
1011
|
+
}
|
|
285
1012
|
//#endregion
|
|
286
1013
|
//#region src/solver.ts
|
|
287
1014
|
/** variant < shed < collapse < overflow when importance ties. */
|
|
@@ -295,17 +1022,17 @@ const STEP_RANK = {
|
|
|
295
1022
|
function buildState(bar) {
|
|
296
1023
|
const groups = [];
|
|
297
1024
|
let position = 0;
|
|
298
|
-
for (const section of bar.sections) for (const
|
|
1025
|
+
for (const section of bar.sections) for (const group of section.groups) {
|
|
299
1026
|
const gs = {
|
|
300
|
-
group
|
|
1027
|
+
group,
|
|
301
1028
|
section: section.name,
|
|
302
1029
|
collapsed: false,
|
|
303
1030
|
overflowed: false,
|
|
304
1031
|
units: []
|
|
305
1032
|
};
|
|
306
|
-
for (const
|
|
1033
|
+
for (const unit of group.units) {
|
|
307
1034
|
gs.units.push({
|
|
308
|
-
unit
|
|
1035
|
+
unit,
|
|
309
1036
|
group: gs,
|
|
310
1037
|
variantIndex: 0,
|
|
311
1038
|
shed: false,
|
|
@@ -336,7 +1063,7 @@ function nextStep(groups) {
|
|
|
336
1063
|
continue;
|
|
337
1064
|
}
|
|
338
1065
|
let childrenExhausted = true;
|
|
339
|
-
const visibleCount = gs.units.filter((
|
|
1066
|
+
const visibleCount = gs.units.filter((unit) => !unit.overflowed && !unit.shed).length;
|
|
340
1067
|
for (const us of gs.units) {
|
|
341
1068
|
if (us.overflowed || us.shed) continue;
|
|
342
1069
|
if (us.variantIndex < us.unit.variants.length - 1) {
|
|
@@ -393,32 +1120,32 @@ function applyStep(step) {
|
|
|
393
1120
|
return;
|
|
394
1121
|
case "overflow-unit":
|
|
395
1122
|
step.unit.overflowed = true;
|
|
396
|
-
if (step.unit.group.units.every((
|
|
1123
|
+
if (step.unit.group.units.every((unit) => unit.overflowed)) step.unit.group.overflowed = true;
|
|
397
1124
|
return;
|
|
398
1125
|
case "overflow-group":
|
|
399
1126
|
step.group.overflowed = true;
|
|
400
|
-
for (const
|
|
1127
|
+
for (const unit of step.group.units) unit.overflowed = true;
|
|
401
1128
|
return;
|
|
402
1129
|
}
|
|
403
1130
|
}
|
|
404
1131
|
/**
|
|
405
|
-
* Visible
|
|
406
|
-
* separators. Gaps are counted between
|
|
1132
|
+
* Visible length of the current assignment: unit lengths + gaps + derived
|
|
1133
|
+
* separators. Gaps are counted between all adjacent visible units, including
|
|
407
1134
|
* across section boundaries — deliberately conservative: the adapter's layout
|
|
408
1135
|
* spaces sections with collapsing auto margins, so the cross-section gap
|
|
409
1136
|
* allowance (≤ 2×gap) is slack those margins absorb, never an overlap risk.
|
|
410
1137
|
*/
|
|
411
1138
|
function measure(groups, metrics, hasTrigger) {
|
|
412
|
-
let
|
|
1139
|
+
let length = 0;
|
|
413
1140
|
let complete = true;
|
|
414
1141
|
let visibleUnits = 0;
|
|
415
1142
|
const visibleBySection = /* @__PURE__ */ new Map();
|
|
416
1143
|
for (const gs of groups) {
|
|
417
1144
|
if (gs.overflowed) continue;
|
|
418
1145
|
if (gs.collapsed) {
|
|
419
|
-
const
|
|
420
|
-
if (
|
|
421
|
-
|
|
1146
|
+
const collapsedLength = metrics.groupCollapsed(gs.group.id);
|
|
1147
|
+
if (collapsedLength === void 0) complete = false;
|
|
1148
|
+
length += collapsedLength ?? 0;
|
|
422
1149
|
visibleUnits += 1;
|
|
423
1150
|
visibleBySection.set(gs.section, (visibleBySection.get(gs.section) ?? 0) + 1);
|
|
424
1151
|
continue;
|
|
@@ -431,39 +1158,39 @@ function measure(groups, metrics, hasTrigger) {
|
|
|
431
1158
|
shedCount += 1;
|
|
432
1159
|
continue;
|
|
433
1160
|
}
|
|
434
|
-
const
|
|
435
|
-
if (
|
|
436
|
-
|
|
1161
|
+
const unitLength = metrics.unit(us.unit.key, us.unit.variants[us.variantIndex]);
|
|
1162
|
+
if (unitLength === void 0) complete = false;
|
|
1163
|
+
length += unitLength ?? 0;
|
|
437
1164
|
visibleUnits += 1;
|
|
438
1165
|
groupVisible = true;
|
|
439
1166
|
}
|
|
440
1167
|
if (shedCount > 0) {
|
|
441
|
-
const
|
|
442
|
-
if (
|
|
443
|
-
|
|
1168
|
+
const triggerLength = metrics.groupTrigger(gs.group.id);
|
|
1169
|
+
if (triggerLength === void 0) complete = false;
|
|
1170
|
+
length += triggerLength ?? 0;
|
|
444
1171
|
visibleUnits += 1;
|
|
445
1172
|
groupVisible = true;
|
|
446
1173
|
}
|
|
447
1174
|
if (groupVisible) visibleBySection.set(gs.section, (visibleBySection.get(gs.section) ?? 0) + 1);
|
|
448
1175
|
}
|
|
449
1176
|
if (hasTrigger) {
|
|
450
|
-
|
|
1177
|
+
length += metrics.overflowTrigger;
|
|
451
1178
|
visibleUnits += 1;
|
|
452
1179
|
}
|
|
453
|
-
if (visibleUnits > 1)
|
|
454
|
-
for (const count of visibleBySection.values()) if (count > 1)
|
|
1180
|
+
if (visibleUnits > 1) length += metrics.gap * (visibleUnits - 1);
|
|
1181
|
+
for (const count of visibleBySection.values()) if (count > 1) length += metrics.separator * (count - 1);
|
|
455
1182
|
return {
|
|
456
|
-
|
|
1183
|
+
length,
|
|
457
1184
|
complete
|
|
458
1185
|
};
|
|
459
1186
|
}
|
|
460
1187
|
function anyOverflow(groups) {
|
|
461
|
-
return groups.some((gs) => gs.units.some((
|
|
1188
|
+
return groups.some((gs) => gs.units.some((unit) => unit.overflowed));
|
|
462
1189
|
}
|
|
463
1190
|
function solvePass(bar, metrics, budget, hasTrigger) {
|
|
464
1191
|
const groups = buildState(bar);
|
|
465
1192
|
let measured = measure(groups, metrics, hasTrigger);
|
|
466
|
-
while (measured.
|
|
1193
|
+
while (measured.length > budget) {
|
|
467
1194
|
const step = nextStep(groups);
|
|
468
1195
|
if (!step) break;
|
|
469
1196
|
applyStep(step);
|
|
@@ -474,13 +1201,14 @@ function solvePass(bar, metrics, budget, hasTrigger) {
|
|
|
474
1201
|
measured
|
|
475
1202
|
};
|
|
476
1203
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
1204
|
+
/** Fit `bar` into `containerLength`, the room along its axis. */
|
|
1205
|
+
function solve(bar, metrics, containerLength) {
|
|
1206
|
+
let pass = solvePass(bar, metrics, containerLength, false);
|
|
1207
|
+
if (anyOverflow(pass.groups)) pass = solvePass(bar, metrics, containerLength, true);
|
|
480
1208
|
const units = /* @__PURE__ */ new Map();
|
|
481
1209
|
const groupAssignments = /* @__PURE__ */ new Map();
|
|
482
1210
|
for (const gs of pass.groups) {
|
|
483
|
-
const shedCount = gs.units.filter((
|
|
1211
|
+
const shedCount = gs.units.filter((unit) => unit.shed && !unit.overflowed && !gs.collapsed).length;
|
|
484
1212
|
groupAssignments.set(gs.group.id, {
|
|
485
1213
|
shedCount,
|
|
486
1214
|
collapsed: gs.collapsed,
|
|
@@ -495,28 +1223,30 @@ function solve(bar, metrics, containerWidth) {
|
|
|
495
1223
|
units,
|
|
496
1224
|
groups: groupAssignments,
|
|
497
1225
|
hasOverflow: anyOverflow(pass.groups),
|
|
498
|
-
|
|
1226
|
+
length: pass.measured.length,
|
|
499
1227
|
complete: pass.measured.complete
|
|
500
1228
|
};
|
|
501
1229
|
}
|
|
502
1230
|
//#endregion
|
|
503
1231
|
//#region src/projection.ts
|
|
1232
|
+
/** A unit's menu form; a custom item's is its terminal command's. A standalone item has none. */
|
|
504
1233
|
function projectUnit(unit, resolve) {
|
|
505
|
-
const command = unit
|
|
1234
|
+
const command = commandOfUnit(unit);
|
|
1235
|
+
if (command === null) return [];
|
|
506
1236
|
const menu = resolve(command)?.menu;
|
|
507
|
-
return menu ? {
|
|
1237
|
+
return [menu ? {
|
|
508
1238
|
type: "submenu",
|
|
509
1239
|
command,
|
|
510
1240
|
menu
|
|
511
1241
|
} : {
|
|
512
1242
|
type: "command",
|
|
513
1243
|
command
|
|
514
|
-
};
|
|
1244
|
+
}];
|
|
515
1245
|
}
|
|
516
1246
|
function projectOverflow(bar, fit, resolve) {
|
|
517
1247
|
const sections = [];
|
|
518
1248
|
for (const section of bar.sections) for (const group of section.groups) {
|
|
519
|
-
const rows = group.units.filter((
|
|
1249
|
+
const rows = group.units.filter((unit) => fit.units.get(unit.key)?.kind === "overflow").flatMap((unit) => projectUnit(unit, resolve));
|
|
520
1250
|
if (rows.length === 0) continue;
|
|
521
1251
|
sections.push({
|
|
522
1252
|
labelKey: group.labelKey,
|
|
@@ -528,26 +1258,30 @@ function projectOverflow(bar, fit, resolve) {
|
|
|
528
1258
|
}
|
|
529
1259
|
/**
|
|
530
1260
|
* The content of a group's disclosure trigger: its shed units, in bar order,
|
|
531
|
-
* in menu form — same totality rules as the global overflow.
|
|
532
|
-
* hand-written `mode-tabs-overflow-menu`, derived.
|
|
1261
|
+
* in menu form — same totality rules as the global overflow.
|
|
533
1262
|
*/
|
|
534
1263
|
function projectShed(group, fit, resolve) {
|
|
535
|
-
return group.units.filter((
|
|
1264
|
+
return group.units.filter((unit) => fit.units.get(unit.key)?.kind === "shed").flatMap((unit) => projectUnit(unit, resolve));
|
|
536
1265
|
}
|
|
537
1266
|
/**
|
|
538
|
-
* Strip projection — a bar projected for
|
|
1267
|
+
* Strip projection — a bar projected for un-measured rendering (a contextual
|
|
539
1268
|
* strip anchored to a selection has no fit pressure to solve). The schema
|
|
540
|
-
* declares what
|
|
541
|
-
* decides what
|
|
542
|
-
* and an all-hidden bar projects to [] — "render nothing" falls out
|
|
1269
|
+
* declares what could appear; `visible` (the command registry's runtime truth)
|
|
1270
|
+
* decides what does: hidden commands drop out, groups that empty out vanish,
|
|
1271
|
+
* and an all-hidden bar projects to [] — "render nothing" falls out, as it
|
|
1272
|
+
* does for a bar with only dependent groups left ({@link filterBar}). Custom
|
|
543
1273
|
* units project through their `terminal` command, same totality rule as the
|
|
544
1274
|
* overflow. Upgrading a strip to measured/overflowing rendering later is a
|
|
545
1275
|
* renderer swap (<Toolbar>), never a schema change.
|
|
546
1276
|
*/
|
|
547
1277
|
function projectStrip(bar, visible) {
|
|
1278
|
+
const shown = filterBar(bar, (unit) => {
|
|
1279
|
+
const command = commandOfUnit(unit);
|
|
1280
|
+
return command !== null && visible(command);
|
|
1281
|
+
});
|
|
548
1282
|
const groups = [];
|
|
549
|
-
for (const section of
|
|
550
|
-
const commands = group.units.map((
|
|
1283
|
+
for (const section of shown.sections) for (const group of section.groups) {
|
|
1284
|
+
const commands = group.units.map(commandOfUnit).filter((command) => command !== null);
|
|
551
1285
|
if (commands.length === 0) continue;
|
|
552
1286
|
groups.push({
|
|
553
1287
|
id: group.id,
|
|
@@ -558,6 +1292,182 @@ function projectStrip(bar, visible) {
|
|
|
558
1292
|
return groups;
|
|
559
1293
|
}
|
|
560
1294
|
//#endregion
|
|
1295
|
+
//#region src/toolbar-layout.ts
|
|
1296
|
+
const SECTIONS = [
|
|
1297
|
+
"start",
|
|
1298
|
+
"center",
|
|
1299
|
+
"end"
|
|
1300
|
+
];
|
|
1301
|
+
const ICON_ONLY = ["icon"];
|
|
1302
|
+
/** The bar as a vertical toolbar draws it: every command an icon; custom items keep their variants. */
|
|
1303
|
+
function iconsOnly(bar) {
|
|
1304
|
+
return {
|
|
1305
|
+
...bar,
|
|
1306
|
+
sections: bar.sections.map((section) => ({
|
|
1307
|
+
...section,
|
|
1308
|
+
groups: section.groups.map((group) => ({
|
|
1309
|
+
...group,
|
|
1310
|
+
units: group.units.map((unit) => unit.kind === "command" ? {
|
|
1311
|
+
...unit,
|
|
1312
|
+
variants: ICON_ONLY
|
|
1313
|
+
} : unit)
|
|
1314
|
+
}))
|
|
1315
|
+
}))
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
/** Fit the bar into `containerLength` and describe what to draw. */
|
|
1319
|
+
function layoutToolbar(input) {
|
|
1320
|
+
const { resolve, execute, measureKey } = input;
|
|
1321
|
+
/** A unit's command, resolved; null for a standalone item or an unregistered command. */
|
|
1322
|
+
const resolveUnit = (unit) => {
|
|
1323
|
+
const command = commandOfUnit(unit);
|
|
1324
|
+
return command === null ? null : resolve(command);
|
|
1325
|
+
};
|
|
1326
|
+
const visible = filterBar(input.bar, (unit) => resolveUnit(unit)?.visible === true);
|
|
1327
|
+
const bar = input.orientation === "vertical" ? iconsOnly(visible) : visible;
|
|
1328
|
+
const fit = solve(bar, input.metrics, input.containerLength);
|
|
1329
|
+
const visibleCommands = (units) => units.map(resolveUnit).filter((command) => command !== null && command.visible);
|
|
1330
|
+
const parts = {
|
|
1331
|
+
unit: (unit, variant) => {
|
|
1332
|
+
if (unit.kind === "custom") return {
|
|
1333
|
+
kind: "custom",
|
|
1334
|
+
key: unit.key,
|
|
1335
|
+
measureKey: measureKey.unit(unit.key, variant),
|
|
1336
|
+
name: unit.slot,
|
|
1337
|
+
variant,
|
|
1338
|
+
terminal: resolveUnit(unit),
|
|
1339
|
+
runTerminal: () => {
|
|
1340
|
+
if (unit.terminal !== null) execute(unit.terminal);
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
const command = resolve(unit.command);
|
|
1344
|
+
return {
|
|
1345
|
+
kind: "command",
|
|
1346
|
+
key: unit.key,
|
|
1347
|
+
measureKey: measureKey.unit(unit.key, variant),
|
|
1348
|
+
command: command ?? input.unregistered(unit.command),
|
|
1349
|
+
variant,
|
|
1350
|
+
run: command ? () => execute(unit.command) : () => {}
|
|
1351
|
+
};
|
|
1352
|
+
},
|
|
1353
|
+
collapsed: (group) => ({
|
|
1354
|
+
kind: "collapsed",
|
|
1355
|
+
key: group.id,
|
|
1356
|
+
measureKey: measureKey.group(group.id),
|
|
1357
|
+
view: {
|
|
1358
|
+
id: group.id,
|
|
1359
|
+
labelKey: group.labelKey,
|
|
1360
|
+
collapse: group.collapse ?? "menu",
|
|
1361
|
+
role: group.role,
|
|
1362
|
+
commands: visibleCommands(group.units),
|
|
1363
|
+
execute
|
|
1364
|
+
}
|
|
1365
|
+
}),
|
|
1366
|
+
disclosure: (group, allChildren) => ({
|
|
1367
|
+
kind: "disclosure",
|
|
1368
|
+
key: `${group.id}::trigger`,
|
|
1369
|
+
measureKey: measureKey.groupTrigger(group.id),
|
|
1370
|
+
view: {
|
|
1371
|
+
id: group.id,
|
|
1372
|
+
labelKey: group.labelKey,
|
|
1373
|
+
role: group.role,
|
|
1374
|
+
commands: visibleCommands(group.units.filter((unit) => allChildren || fit.units.get(unit.key)?.kind === "shed")),
|
|
1375
|
+
execute
|
|
1376
|
+
}
|
|
1377
|
+
})
|
|
1378
|
+
};
|
|
1379
|
+
return {
|
|
1380
|
+
sections: SECTIONS.map((name) => liveSection(name, bar, fit, parts)),
|
|
1381
|
+
measured: measuredParts(bar, parts),
|
|
1382
|
+
hasOverflow: fit.hasOverflow,
|
|
1383
|
+
overflow: projectOverflow(bar, fit, input.menuTarget)
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
function liveSection(name, bar, fit, parts) {
|
|
1387
|
+
const section = bar.sections.find((each) => each.name === name);
|
|
1388
|
+
const groups = [];
|
|
1389
|
+
for (const group of section?.groups ?? []) {
|
|
1390
|
+
const assignment = fit.groups.get(group.id);
|
|
1391
|
+
if (!assignment || assignment.overflowed) continue;
|
|
1392
|
+
if (assignment.collapsed) {
|
|
1393
|
+
groups.push({
|
|
1394
|
+
id: group.id,
|
|
1395
|
+
parts: [parts.collapsed(group)]
|
|
1396
|
+
});
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
const drawn = [];
|
|
1400
|
+
for (const unit of group.units) {
|
|
1401
|
+
const placed = fit.units.get(unit.key);
|
|
1402
|
+
if (placed?.kind === "variant") drawn.push(parts.unit(unit, placed.variant));
|
|
1403
|
+
}
|
|
1404
|
+
if (assignment.shedCount > 0) drawn.push(parts.disclosure(group, false));
|
|
1405
|
+
if (drawn.length > 0) groups.push({
|
|
1406
|
+
id: group.id,
|
|
1407
|
+
parts: drawn
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
return {
|
|
1411
|
+
name,
|
|
1412
|
+
groups
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
function measuredParts(bar, parts) {
|
|
1416
|
+
const measured = [];
|
|
1417
|
+
for (const section of bar.sections) for (const group of section.groups) {
|
|
1418
|
+
for (const unit of group.units) for (const variant of unit.variants) measured.push(parts.unit(unit, variant));
|
|
1419
|
+
if (group.collapse) measured.push(parts.collapsed(group));
|
|
1420
|
+
if (group.shed) measured.push(parts.disclosure(group, true));
|
|
1421
|
+
}
|
|
1422
|
+
return measured;
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* The menu a default folded group or group trigger opens: one section of the
|
|
1426
|
+
* group's commands, a radio group when the group is tabs.
|
|
1427
|
+
*/
|
|
1428
|
+
function groupMenuView(view, isOpen, close) {
|
|
1429
|
+
return {
|
|
1430
|
+
sections: [{
|
|
1431
|
+
labelKey: view.labelKey,
|
|
1432
|
+
role: view.role === "tabs" ? "radio" : void 0,
|
|
1433
|
+
rows: view.commands.map((command) => ({
|
|
1434
|
+
type: "command",
|
|
1435
|
+
command: command.id
|
|
1436
|
+
}))
|
|
1437
|
+
}],
|
|
1438
|
+
isOpen,
|
|
1439
|
+
close,
|
|
1440
|
+
resolve: (id) => view.commands.find((command) => command.id === id) ?? null,
|
|
1441
|
+
execute: (id) => view.execute(id)
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* A bar as a strip shows it now (`projectStrip`), each command resolved: the
|
|
1446
|
+
* schema says what could appear, each command's `visible` decides what does.
|
|
1447
|
+
* Empty while nothing applies.
|
|
1448
|
+
*/
|
|
1449
|
+
function stripGroupsOf(bar, resolve) {
|
|
1450
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1451
|
+
const visible = (id) => {
|
|
1452
|
+
const command = resolve(id);
|
|
1453
|
+
if (command) resolved.set(id, command);
|
|
1454
|
+
return command?.visible === true;
|
|
1455
|
+
};
|
|
1456
|
+
return projectStrip(bar, visible).map((group) => ({
|
|
1457
|
+
id: group.id,
|
|
1458
|
+
labelKey: group.labelKey,
|
|
1459
|
+
commands: group.commands.map((id) => resolved.get(id))
|
|
1460
|
+
}));
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Whether two resolved strips show the same, group by group and command by
|
|
1464
|
+
* command (`sameCommand` is plugin-commands' `resolvedCommandsEqual`): a
|
|
1465
|
+
* strip's readers pass it on only when this is false.
|
|
1466
|
+
*/
|
|
1467
|
+
function sameStripGroups(left, right, sameCommand) {
|
|
1468
|
+
return left.length === right.length && left.every((group, i) => group.id === right[i].id && group.commands.length === right[i].commands.length && group.commands.every((command, j) => sameCommand(command, right[i].commands[j])));
|
|
1469
|
+
}
|
|
1470
|
+
//#endregion
|
|
561
1471
|
//#region src/shortcuts.ts
|
|
562
1472
|
const MODIFIERS = /* @__PURE__ */ new Set([
|
|
563
1473
|
"mod",
|
|
@@ -571,7 +1481,7 @@ const MODIFIERS = /* @__PURE__ */ new Set([
|
|
|
571
1481
|
"shift"
|
|
572
1482
|
]);
|
|
573
1483
|
function parseShortcut(shortcut) {
|
|
574
|
-
const tokens = shortcut.split("+").map((
|
|
1484
|
+
const tokens = shortcut.split("+").map((token) => token.trim());
|
|
575
1485
|
const parsed = {
|
|
576
1486
|
key: (tokens[tokens.length - 1] === "" ? "+" : tokens[tokens.length - 1]).toLowerCase(),
|
|
577
1487
|
mod: false,
|
|
@@ -581,34 +1491,74 @@ function parseShortcut(shortcut) {
|
|
|
581
1491
|
shift: false
|
|
582
1492
|
};
|
|
583
1493
|
for (const token of tokens.slice(0, -1)) {
|
|
584
|
-
const
|
|
585
|
-
if (
|
|
586
|
-
if (!MODIFIERS.has(
|
|
587
|
-
if (
|
|
588
|
-
else if (
|
|
589
|
-
else if (
|
|
590
|
-
else if (
|
|
1494
|
+
const modifier = token.toLowerCase();
|
|
1495
|
+
if (modifier === "") continue;
|
|
1496
|
+
if (!MODIFIERS.has(modifier)) throw new Error(`[ui-core] unknown modifier "${token}" in "${shortcut}"`);
|
|
1497
|
+
if (modifier === "mod") parsed.mod = true;
|
|
1498
|
+
else if (modifier === "ctrl" || modifier === "control") parsed.ctrl = true;
|
|
1499
|
+
else if (modifier === "meta" || modifier === "cmd" || modifier === "command") parsed.meta = true;
|
|
1500
|
+
else if (modifier === "alt" || modifier === "option") parsed.alt = true;
|
|
591
1501
|
else parsed.shift = true;
|
|
592
1502
|
}
|
|
593
1503
|
if (parsed.key === "") throw new Error(`[ui-core] empty key in shortcut "${shortcut}"`);
|
|
594
1504
|
return parsed;
|
|
595
1505
|
}
|
|
596
|
-
function matchShortcut(parsed, stroke,
|
|
597
|
-
const wantCtrl = parsed.ctrl || parsed.mod && !
|
|
598
|
-
const wantMeta = parsed.meta || parsed.mod &&
|
|
1506
|
+
function matchShortcut(parsed, stroke, options) {
|
|
1507
|
+
const wantCtrl = parsed.ctrl || parsed.mod && !options.isMac;
|
|
1508
|
+
const wantMeta = parsed.meta || parsed.mod && options.isMac;
|
|
599
1509
|
if (stroke.ctrlKey !== wantCtrl) return false;
|
|
600
1510
|
if (stroke.metaKey !== wantMeta) return false;
|
|
601
1511
|
if (stroke.altKey !== parsed.alt) return false;
|
|
602
1512
|
if (parsed.shift && !stroke.shiftKey) return false;
|
|
603
|
-
|
|
1513
|
+
const hasCase = parsed.key !== parsed.key.toUpperCase();
|
|
1514
|
+
if (!parsed.shift && stroke.shiftKey && (parsed.key.length > 1 || hasCase)) return false;
|
|
604
1515
|
if (stroke.key.toLowerCase() === parsed.key) return true;
|
|
605
1516
|
return parsed.key.length > 1 && stroke.code !== void 0 && stroke.code.toLowerCase() === parsed.key;
|
|
606
1517
|
}
|
|
607
|
-
/**
|
|
608
|
-
|
|
1518
|
+
/**
|
|
1519
|
+
* Whether two shortcuts are pressed with the same keys on this platform: `'Mod+S'` and
|
|
1520
|
+
* `'Ctrl+s'` are where `Mod` is Ctrl, and not on a Mac.
|
|
1521
|
+
*/
|
|
1522
|
+
function sameShortcut(left, right, options) {
|
|
1523
|
+
const keysOf = (shortcut) => {
|
|
1524
|
+
const parsed = parseShortcut(shortcut);
|
|
1525
|
+
return [
|
|
1526
|
+
parsed.key,
|
|
1527
|
+
parsed.ctrl || parsed.mod && !options.isMac,
|
|
1528
|
+
parsed.meta || parsed.mod && options.isMac,
|
|
1529
|
+
parsed.alt,
|
|
1530
|
+
parsed.shift
|
|
1531
|
+
].join(" ");
|
|
1532
|
+
};
|
|
1533
|
+
return keysOf(left) === keysOf(right);
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* How a key shows, by its lowercase name, where that isn't the name itself:
|
|
1537
|
+
* on a Mac, then elsewhere. Matches what each platform's own menus show.
|
|
1538
|
+
*/
|
|
1539
|
+
const KEY_NAMES = {
|
|
1540
|
+
arrowleft: ["←", "←"],
|
|
1541
|
+
arrowright: ["→", "→"],
|
|
1542
|
+
arrowup: ["↑", "↑"],
|
|
1543
|
+
arrowdown: ["↓", "↓"],
|
|
1544
|
+
backspace: ["⌫", "Backspace"],
|
|
1545
|
+
delete: ["⌦", "Delete"],
|
|
1546
|
+
enter: ["↩", "Enter"],
|
|
1547
|
+
escape: ["Esc", "Esc"],
|
|
1548
|
+
tab: ["⇥", "Tab"],
|
|
1549
|
+
" ": ["Space", "Space"],
|
|
1550
|
+
space: ["Space", "Space"],
|
|
1551
|
+
pageup: ["Page Up", "Page Up"],
|
|
1552
|
+
pagedown: ["Page Down", "Page Down"],
|
|
1553
|
+
numpadadd: ["Num +", "Num +"],
|
|
1554
|
+
numpadsubtract: ["Num -", "Num -"]
|
|
1555
|
+
};
|
|
1556
|
+
/** A shortcut as a menu or a tooltip shows it: `'⌘K'` on a Mac, `'Ctrl+K'` elsewhere. */
|
|
1557
|
+
function formatShortcut(shortcut, options) {
|
|
609
1558
|
const parsed = parseShortcut(shortcut);
|
|
610
|
-
const
|
|
611
|
-
|
|
1559
|
+
const named = KEY_NAMES[parsed.key];
|
|
1560
|
+
const key = named ? named[options.isMac ? 0 : 1] : parsed.key.length === 1 ? parsed.key.toUpperCase() : capitalize(parsed.key);
|
|
1561
|
+
if (options.isMac) return `${[
|
|
612
1562
|
parsed.ctrl ? "⌃" : "",
|
|
613
1563
|
parsed.alt ? "⌥" : "",
|
|
614
1564
|
parsed.shift ? "⇧" : "",
|
|
@@ -621,10 +1571,10 @@ function formatShortcut(shortcut, opts) {
|
|
|
621
1571
|
parsed.shift ? "Shift" : ""
|
|
622
1572
|
].filter(Boolean), key].join("+");
|
|
623
1573
|
}
|
|
624
|
-
function capitalize(
|
|
625
|
-
return
|
|
1574
|
+
function capitalize(text) {
|
|
1575
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
626
1576
|
}
|
|
627
1577
|
//#endregion
|
|
628
|
-
export { DEFAULT_IMPORTANCE, PINNED,
|
|
1578
|
+
export { DEFAULT_IMPORTANCE, EXPANDED_MIN_WIDTH_PX, FLOAT_POSITIONS, MEDIUM_MIN_WIDTH_PX, MODES_GROUP, PINNED, VIEW_MODE_COMMAND, barsAt, barsFloating, barsInMode, barsOnEdge, barsWith, closestMatch, commandOfUnit, compileBar, createLayout, custom, didYouMean, edgeOf, edgeStackOf, editDistance, filterBar, floatOf, floatStackOf, formatShortcut, group, groupMenuView, hasVisibleUnits, hostOf, isLayout, isStandaloneUnit, item, itemCommandOf, itemIdOf, layoutToolbar, matchShortcut, menuOf, modeCommandOf, modesOf, normalizeBar, omittedCommandsOf, panelCommandOf, panelPresentationsOf, panelWidthFieldsOf, parseShortcut, projectOverflow, projectShed, projectStrip, resolveLayout, sameLayoutEnv, sameShortcut, sameStripGroups, selectionOf, sizeOf, solve, stripGroupsOf, tabsOf, validateLayout };
|
|
629
1579
|
|
|
630
1580
|
//# sourceMappingURL=index.js.map
|