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