@embedpdf/core-ui 3.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +192 -0
- package/dist/index.cjs +650 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +364 -0
- package/dist/index.d.ts +364 -0
- package/dist/index.js +630 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
//#region src/schema.ts
|
|
2
|
+
const PINNED = 5;
|
|
3
|
+
function item(command, opts) {
|
|
4
|
+
return {
|
|
5
|
+
command,
|
|
6
|
+
...opts
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function custom(slot, opts) {
|
|
10
|
+
return {
|
|
11
|
+
slot,
|
|
12
|
+
...opts
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function group(id, optsOrItems, items) {
|
|
16
|
+
if (Array.isArray(optsOrItems)) return {
|
|
17
|
+
id,
|
|
18
|
+
items: optsOrItems
|
|
19
|
+
};
|
|
20
|
+
const { items: optItems, ...opts } = optsOrItems;
|
|
21
|
+
return {
|
|
22
|
+
id,
|
|
23
|
+
...opts,
|
|
24
|
+
items: items ?? optItems ?? []
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const DEFAULT_IMPORTANCE = 3;
|
|
28
|
+
const DEFAULT_COMMAND_VARIANTS = ["icon"];
|
|
29
|
+
const DEFAULT_CUSTOM_VARIANTS = ["default"];
|
|
30
|
+
function normalizeChild(groupId, child, seen) {
|
|
31
|
+
const asItem = typeof child === "string" ? { command: child } : child;
|
|
32
|
+
const unit = "slot" in asItem ? {
|
|
33
|
+
kind: "custom",
|
|
34
|
+
key: `${groupId}:${asItem.slot}`,
|
|
35
|
+
slot: asItem.slot,
|
|
36
|
+
variants: asItem.variants ?? DEFAULT_CUSTOM_VARIANTS,
|
|
37
|
+
terminal: asItem.terminal,
|
|
38
|
+
importance: asItem.importance ?? 3
|
|
39
|
+
} : {
|
|
40
|
+
kind: "command",
|
|
41
|
+
key: `${groupId}:${asItem.command}`,
|
|
42
|
+
command: asItem.command,
|
|
43
|
+
variants: asItem.variants ?? DEFAULT_COMMAND_VARIANTS,
|
|
44
|
+
importance: asItem.importance ?? 3
|
|
45
|
+
};
|
|
46
|
+
if (unit.variants.length === 0) throw new Error(`[ui-core] empty variant ladder: ${unit.key}`);
|
|
47
|
+
if (seen.has(unit.key)) throw new Error(`[ui-core] duplicate unit in bar: ${unit.key}`);
|
|
48
|
+
seen.add(unit.key);
|
|
49
|
+
return unit;
|
|
50
|
+
}
|
|
51
|
+
/** Resolve shorthands + defaults; validate uniqueness. Solver/projection input. */
|
|
52
|
+
function normalizeBar(bar) {
|
|
53
|
+
const seen = /* @__PURE__ */ new Set();
|
|
54
|
+
const groupIds = /* @__PURE__ */ new Set();
|
|
55
|
+
const sections = [];
|
|
56
|
+
for (const name of [
|
|
57
|
+
"start",
|
|
58
|
+
"center",
|
|
59
|
+
"end"
|
|
60
|
+
]) {
|
|
61
|
+
const groups = bar.sections[name];
|
|
62
|
+
if (!groups || groups.length === 0) continue;
|
|
63
|
+
sections.push({
|
|
64
|
+
name,
|
|
65
|
+
groups: groups.map((g) => {
|
|
66
|
+
if (groupIds.has(g.id)) throw new Error(`[ui-core] duplicate group in bar: ${g.id}`);
|
|
67
|
+
groupIds.add(g.id);
|
|
68
|
+
return {
|
|
69
|
+
id: g.id,
|
|
70
|
+
labelKey: g.labelKey,
|
|
71
|
+
role: g.role ?? "buttons",
|
|
72
|
+
shed: g.shed ?? false,
|
|
73
|
+
collapse: g.collapse,
|
|
74
|
+
importance: g.importance ?? 3,
|
|
75
|
+
units: g.items.map((c) => normalizeChild(g.id, c, seen))
|
|
76
|
+
};
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
id: bar.id,
|
|
82
|
+
sections
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Drop units whose command is currently invisible (permissions, disabled
|
|
87
|
+
* categories, contextual visibility) BEFORE solving — an invisible unit must
|
|
88
|
+
* 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.
|
|
90
|
+
*/
|
|
91
|
+
function filterBar(bar, keep) {
|
|
92
|
+
return {
|
|
93
|
+
id: bar.id,
|
|
94
|
+
sections: bar.sections.map((s) => ({
|
|
95
|
+
name: s.name,
|
|
96
|
+
groups: s.groups.map((g) => ({
|
|
97
|
+
...g,
|
|
98
|
+
units: g.units.filter(keep)
|
|
99
|
+
}))
|
|
100
|
+
}))
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Identity + validation. Checks what is checkable without a command registry:
|
|
105
|
+
* bars normalize cleanly, and every `menu` a strip/bar could reference exists
|
|
106
|
+
* is left to `validateChrome` (which also takes the known command ids).
|
|
107
|
+
*/
|
|
108
|
+
function defineChrome(chrome) {
|
|
109
|
+
for (const bar of Object.values(chrome.bars)) normalizeBar(bar);
|
|
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
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/transforms.ts
|
|
131
|
+
/**
|
|
132
|
+
* Pure transforms over ChromeSchema — the "surgical edit" tier of the
|
|
133
|
+
* customization ladder, between "pass nothing" and "own the whole value".
|
|
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`.
|
|
141
|
+
*/
|
|
142
|
+
const SECTION_NAMES = [
|
|
143
|
+
"start",
|
|
144
|
+
"center",
|
|
145
|
+
"end"
|
|
146
|
+
];
|
|
147
|
+
const isCustom = (child) => typeof child !== "string" && "slot" in child;
|
|
148
|
+
const commandOf = (child) => typeof child === "string" ? child : isCustom(child) ? null : child.command;
|
|
149
|
+
/** Every (collection, key) location whose bar matches `id`. */
|
|
150
|
+
function locate(schema, id) {
|
|
151
|
+
const hits = [];
|
|
152
|
+
for (const coll of [
|
|
153
|
+
"bars",
|
|
154
|
+
"modeBars",
|
|
155
|
+
"strips"
|
|
156
|
+
]) for (const [key, bar] of Object.entries(schema[coll] ?? {})) if (bar.id === id || key === id) hits.push([coll, key]);
|
|
157
|
+
return hits;
|
|
158
|
+
}
|
|
159
|
+
function withBar(schema, coll, key, bar) {
|
|
160
|
+
return {
|
|
161
|
+
...schema,
|
|
162
|
+
[coll]: {
|
|
163
|
+
...schema[coll],
|
|
164
|
+
[key]: bar
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** Insert `spec.item` into a group of one bar. See AddItemSpec for semantics. */
|
|
169
|
+
function addItem(schema, spec) {
|
|
170
|
+
const hits = locate(schema, spec.bar);
|
|
171
|
+
if (hits.length === 0) throw new Error(`[ui-core] addItem: no bar "${spec.bar}" in bars/modeBars/strips`);
|
|
172
|
+
if (hits.length > 1) throw new Error(`[ui-core] addItem: bar id "${spec.bar}" is ambiguous (${hits.map(([c]) => c).join(", ")})`);
|
|
173
|
+
const [coll, key] = hits[0];
|
|
174
|
+
const bar = schema[coll][key];
|
|
175
|
+
for (const name of SECTION_NAMES) {
|
|
176
|
+
if (name === spec.section) continue;
|
|
177
|
+
if (bar.sections[name]?.some((g) => g.id === spec.group)) throw new Error(`[ui-core] addItem: group "${spec.group}" lives in section "${name}", not "${spec.section}"`);
|
|
178
|
+
}
|
|
179
|
+
const groups = bar.sections[spec.section] ?? [];
|
|
180
|
+
const idx = groups.findIndex((g) => g.id === spec.group);
|
|
181
|
+
const nextGroups = idx === -1 ? [...groups, group(spec.group, [spec.item])] : groups.map((g, i) => {
|
|
182
|
+
if (i !== idx) return g;
|
|
183
|
+
const items = [...g.items];
|
|
184
|
+
items.splice(spec.at ?? items.length, 0, spec.item);
|
|
185
|
+
return {
|
|
186
|
+
...g,
|
|
187
|
+
items
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
return withBar(schema, coll, key, {
|
|
191
|
+
...bar,
|
|
192
|
+
sections: {
|
|
193
|
+
...bar.sections,
|
|
194
|
+
[spec.section]: nextGroups
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Purge commands (or custom slots, by slot name) from every bar, mode bar,
|
|
200
|
+
* strip, and menu. Groups and menu sections that empty out are dropped;
|
|
201
|
+
* emptied menus keep their key (commands may still target them) with no rows.
|
|
202
|
+
*/
|
|
203
|
+
function removeItems(schema, ids) {
|
|
204
|
+
const gone = new Set(ids);
|
|
205
|
+
const keep = (child) => {
|
|
206
|
+
const cmd = commandOf(child);
|
|
207
|
+
return cmd !== null ? !gone.has(cmd) : !gone.has(child.slot);
|
|
208
|
+
};
|
|
209
|
+
const filterGroups = (groups) => groups?.map((g) => ({
|
|
210
|
+
...g,
|
|
211
|
+
items: g.items.filter(keep)
|
|
212
|
+
})).filter((g) => g.items.length > 0);
|
|
213
|
+
const filterBarSchema = (bar) => ({
|
|
214
|
+
...bar,
|
|
215
|
+
sections: Object.fromEntries(SECTION_NAMES.flatMap((name) => {
|
|
216
|
+
const groups = filterGroups(bar.sections[name]);
|
|
217
|
+
return groups && groups.length > 0 ? [[name, groups]] : [];
|
|
218
|
+
}))
|
|
219
|
+
});
|
|
220
|
+
const filterColl = (coll) => coll && Object.fromEntries(Object.entries(coll).map(([k, b]) => [k, filterBarSchema(b)]));
|
|
221
|
+
return {
|
|
222
|
+
...schema,
|
|
223
|
+
bars: filterColl(schema.bars),
|
|
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
|
+
}]))
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Swap a command in place everywhere it appears — bars, mode bars, strips,
|
|
237
|
+
* and (when the replacement is itself a plain command) menu rows.
|
|
238
|
+
*/
|
|
239
|
+
function replaceItem(schema, id, item) {
|
|
240
|
+
const replacementCommand = commandOf(item);
|
|
241
|
+
const swap = (child) => commandOf(child) === id ? item : child;
|
|
242
|
+
const mapBar = (bar) => ({
|
|
243
|
+
...bar,
|
|
244
|
+
sections: Object.fromEntries(SECTION_NAMES.flatMap((name) => {
|
|
245
|
+
const groups = bar.sections[name];
|
|
246
|
+
return groups ? [[name, groups.map((g) => ({
|
|
247
|
+
...g,
|
|
248
|
+
items: g.items.map(swap)
|
|
249
|
+
}))]] : [];
|
|
250
|
+
}))
|
|
251
|
+
});
|
|
252
|
+
const mapColl = (coll) => coll && Object.fromEntries(Object.entries(coll).map(([k, b]) => [k, mapBar(b)]));
|
|
253
|
+
return {
|
|
254
|
+
...schema,
|
|
255
|
+
bars: mapColl(schema.bars),
|
|
256
|
+
modeBars: mapColl(schema.modeBars),
|
|
257
|
+
strips: mapColl(schema.strips),
|
|
258
|
+
menus: replacementCommand === null ? schema.menus : schema.menus && Object.fromEntries(Object.entries(schema.menus).map(([k, menu]) => [k, {
|
|
259
|
+
...menu,
|
|
260
|
+
sections: menu.sections.map((s) => ({
|
|
261
|
+
...s,
|
|
262
|
+
items: s.items.map((c) => c === id ? replacementCommand : c)
|
|
263
|
+
}))
|
|
264
|
+
}]))
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/helpers.ts
|
|
269
|
+
/**
|
|
270
|
+
* The helpers bundle handed to `chrome: (base, h) => …` transforms — the
|
|
271
|
+
* surgical edits plus the authoring sugar, so simple customizations need no
|
|
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.
|
|
275
|
+
*/
|
|
276
|
+
const chromeHelpers = {
|
|
277
|
+
addItem,
|
|
278
|
+
removeItems,
|
|
279
|
+
replaceItem,
|
|
280
|
+
item,
|
|
281
|
+
group,
|
|
282
|
+
custom,
|
|
283
|
+
defineChrome
|
|
284
|
+
};
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/solver.ts
|
|
287
|
+
/** variant < shed < collapse < overflow when importance ties. */
|
|
288
|
+
const STEP_RANK = {
|
|
289
|
+
variant: 0,
|
|
290
|
+
shed: 1,
|
|
291
|
+
collapse: 2,
|
|
292
|
+
"overflow-unit": 3,
|
|
293
|
+
"overflow-group": 3
|
|
294
|
+
};
|
|
295
|
+
function buildState(bar) {
|
|
296
|
+
const groups = [];
|
|
297
|
+
let position = 0;
|
|
298
|
+
for (const section of bar.sections) for (const g of section.groups) {
|
|
299
|
+
const gs = {
|
|
300
|
+
group: g,
|
|
301
|
+
section: section.name,
|
|
302
|
+
collapsed: false,
|
|
303
|
+
overflowed: false,
|
|
304
|
+
units: []
|
|
305
|
+
};
|
|
306
|
+
for (const u of g.units) {
|
|
307
|
+
gs.units.push({
|
|
308
|
+
unit: u,
|
|
309
|
+
group: gs,
|
|
310
|
+
variantIndex: 0,
|
|
311
|
+
shed: false,
|
|
312
|
+
overflowed: false,
|
|
313
|
+
position
|
|
314
|
+
});
|
|
315
|
+
position += 1;
|
|
316
|
+
}
|
|
317
|
+
groups.push(gs);
|
|
318
|
+
}
|
|
319
|
+
return groups;
|
|
320
|
+
}
|
|
321
|
+
function nextStep(groups) {
|
|
322
|
+
let best = null;
|
|
323
|
+
const consider = (step) => {
|
|
324
|
+
if (!best || step.importance < best.importance || step.importance === best.importance && (STEP_RANK[step.type] < STEP_RANK[best.type] || STEP_RANK[step.type] === STEP_RANK[best.type] && step.position > best.position)) best = step;
|
|
325
|
+
};
|
|
326
|
+
for (const gs of groups) {
|
|
327
|
+
if (gs.overflowed) continue;
|
|
328
|
+
const groupPosition = gs.units.length ? gs.units[gs.units.length - 1].position : 0;
|
|
329
|
+
if (gs.collapsed) {
|
|
330
|
+
if (gs.group.importance < 5) consider({
|
|
331
|
+
type: "overflow-group",
|
|
332
|
+
importance: gs.group.importance,
|
|
333
|
+
position: groupPosition,
|
|
334
|
+
group: gs
|
|
335
|
+
});
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
let childrenExhausted = true;
|
|
339
|
+
const visibleCount = gs.units.filter((u) => !u.overflowed && !u.shed).length;
|
|
340
|
+
for (const us of gs.units) {
|
|
341
|
+
if (us.overflowed || us.shed) continue;
|
|
342
|
+
if (us.variantIndex < us.unit.variants.length - 1) {
|
|
343
|
+
childrenExhausted = false;
|
|
344
|
+
consider({
|
|
345
|
+
type: "variant",
|
|
346
|
+
importance: us.unit.importance,
|
|
347
|
+
position: us.position,
|
|
348
|
+
unit: us
|
|
349
|
+
});
|
|
350
|
+
} else if (us.unit.importance < 5) {
|
|
351
|
+
if (gs.group.shed && visibleCount > 1) consider({
|
|
352
|
+
type: "shed",
|
|
353
|
+
importance: us.unit.importance,
|
|
354
|
+
position: us.position,
|
|
355
|
+
unit: us
|
|
356
|
+
});
|
|
357
|
+
else if (!gs.group.shed && !gs.group.collapse) consider({
|
|
358
|
+
type: "overflow-unit",
|
|
359
|
+
importance: us.unit.importance,
|
|
360
|
+
position: us.position,
|
|
361
|
+
unit: us
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const shedExhausted = !gs.group.shed || visibleCount <= 1;
|
|
366
|
+
if (childrenExhausted && shedExhausted && gs.group.importance < 5) {
|
|
367
|
+
if (gs.group.collapse) consider({
|
|
368
|
+
type: "collapse",
|
|
369
|
+
importance: gs.group.importance,
|
|
370
|
+
position: groupPosition,
|
|
371
|
+
group: gs
|
|
372
|
+
});
|
|
373
|
+
else if (gs.group.shed) consider({
|
|
374
|
+
type: "overflow-group",
|
|
375
|
+
importance: gs.group.importance,
|
|
376
|
+
position: groupPosition,
|
|
377
|
+
group: gs
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return best;
|
|
382
|
+
}
|
|
383
|
+
function applyStep(step) {
|
|
384
|
+
switch (step.type) {
|
|
385
|
+
case "variant":
|
|
386
|
+
step.unit.variantIndex += 1;
|
|
387
|
+
return;
|
|
388
|
+
case "shed":
|
|
389
|
+
step.unit.shed = true;
|
|
390
|
+
return;
|
|
391
|
+
case "collapse":
|
|
392
|
+
step.group.collapsed = true;
|
|
393
|
+
return;
|
|
394
|
+
case "overflow-unit":
|
|
395
|
+
step.unit.overflowed = true;
|
|
396
|
+
if (step.unit.group.units.every((u) => u.overflowed)) step.unit.group.overflowed = true;
|
|
397
|
+
return;
|
|
398
|
+
case "overflow-group":
|
|
399
|
+
step.group.overflowed = true;
|
|
400
|
+
for (const u of step.group.units) u.overflowed = true;
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Visible width of the current assignment: unit widths + gaps + derived
|
|
406
|
+
* separators. Gaps are counted between ALL adjacent visible units, including
|
|
407
|
+
* across section boundaries — deliberately conservative: the adapter's layout
|
|
408
|
+
* spaces sections with collapsing auto margins, so the cross-section gap
|
|
409
|
+
* allowance (≤ 2×gap) is slack those margins absorb, never an overlap risk.
|
|
410
|
+
*/
|
|
411
|
+
function measure(groups, metrics, hasTrigger) {
|
|
412
|
+
let width = 0;
|
|
413
|
+
let complete = true;
|
|
414
|
+
let visibleUnits = 0;
|
|
415
|
+
const visibleBySection = /* @__PURE__ */ new Map();
|
|
416
|
+
for (const gs of groups) {
|
|
417
|
+
if (gs.overflowed) continue;
|
|
418
|
+
if (gs.collapsed) {
|
|
419
|
+
const w = metrics.groupCollapsed(gs.group.id);
|
|
420
|
+
if (w === void 0) complete = false;
|
|
421
|
+
width += w ?? 0;
|
|
422
|
+
visibleUnits += 1;
|
|
423
|
+
visibleBySection.set(gs.section, (visibleBySection.get(gs.section) ?? 0) + 1);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
let groupVisible = false;
|
|
427
|
+
let shedCount = 0;
|
|
428
|
+
for (const us of gs.units) {
|
|
429
|
+
if (us.overflowed) continue;
|
|
430
|
+
if (us.shed) {
|
|
431
|
+
shedCount += 1;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const w = metrics.unit(us.unit.key, us.unit.variants[us.variantIndex]);
|
|
435
|
+
if (w === void 0) complete = false;
|
|
436
|
+
width += w ?? 0;
|
|
437
|
+
visibleUnits += 1;
|
|
438
|
+
groupVisible = true;
|
|
439
|
+
}
|
|
440
|
+
if (shedCount > 0) {
|
|
441
|
+
const w = metrics.groupTrigger(gs.group.id);
|
|
442
|
+
if (w === void 0) complete = false;
|
|
443
|
+
width += w ?? 0;
|
|
444
|
+
visibleUnits += 1;
|
|
445
|
+
groupVisible = true;
|
|
446
|
+
}
|
|
447
|
+
if (groupVisible) visibleBySection.set(gs.section, (visibleBySection.get(gs.section) ?? 0) + 1);
|
|
448
|
+
}
|
|
449
|
+
if (hasTrigger) {
|
|
450
|
+
width += metrics.overflowTrigger;
|
|
451
|
+
visibleUnits += 1;
|
|
452
|
+
}
|
|
453
|
+
if (visibleUnits > 1) width += metrics.gap * (visibleUnits - 1);
|
|
454
|
+
for (const count of visibleBySection.values()) if (count > 1) width += metrics.separator * (count - 1);
|
|
455
|
+
return {
|
|
456
|
+
width,
|
|
457
|
+
complete
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function anyOverflow(groups) {
|
|
461
|
+
return groups.some((gs) => gs.units.some((u) => u.overflowed));
|
|
462
|
+
}
|
|
463
|
+
function solvePass(bar, metrics, budget, hasTrigger) {
|
|
464
|
+
const groups = buildState(bar);
|
|
465
|
+
let measured = measure(groups, metrics, hasTrigger);
|
|
466
|
+
while (measured.width > budget) {
|
|
467
|
+
const step = nextStep(groups);
|
|
468
|
+
if (!step) break;
|
|
469
|
+
applyStep(step);
|
|
470
|
+
measured = measure(groups, metrics, hasTrigger);
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
groups,
|
|
474
|
+
measured
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function solve(bar, metrics, containerWidth) {
|
|
478
|
+
let pass = solvePass(bar, metrics, containerWidth, false);
|
|
479
|
+
if (anyOverflow(pass.groups)) pass = solvePass(bar, metrics, containerWidth, true);
|
|
480
|
+
const units = /* @__PURE__ */ new Map();
|
|
481
|
+
const groupAssignments = /* @__PURE__ */ new Map();
|
|
482
|
+
for (const gs of pass.groups) {
|
|
483
|
+
const shedCount = gs.units.filter((u) => u.shed && !u.overflowed && !gs.collapsed).length;
|
|
484
|
+
groupAssignments.set(gs.group.id, {
|
|
485
|
+
shedCount,
|
|
486
|
+
collapsed: gs.collapsed,
|
|
487
|
+
overflowed: gs.overflowed
|
|
488
|
+
});
|
|
489
|
+
for (const us of gs.units) units.set(us.unit.key, us.overflowed ? { kind: "overflow" } : gs.collapsed ? { kind: "collapsed" } : us.shed ? { kind: "shed" } : {
|
|
490
|
+
kind: "variant",
|
|
491
|
+
variant: us.unit.variants[us.variantIndex]
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
units,
|
|
496
|
+
groups: groupAssignments,
|
|
497
|
+
hasOverflow: anyOverflow(pass.groups),
|
|
498
|
+
width: pass.measured.width,
|
|
499
|
+
complete: pass.measured.complete
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region src/projection.ts
|
|
504
|
+
function projectUnit(unit, resolve) {
|
|
505
|
+
const command = unit.kind === "custom" ? unit.terminal : unit.command;
|
|
506
|
+
const menu = resolve(command)?.menu;
|
|
507
|
+
return menu ? {
|
|
508
|
+
type: "submenu",
|
|
509
|
+
command,
|
|
510
|
+
menu
|
|
511
|
+
} : {
|
|
512
|
+
type: "command",
|
|
513
|
+
command
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function projectOverflow(bar, fit, resolve) {
|
|
517
|
+
const sections = [];
|
|
518
|
+
for (const section of bar.sections) for (const group of section.groups) {
|
|
519
|
+
const rows = group.units.filter((u) => fit.units.get(u.key)?.kind === "overflow").map((u) => projectUnit(u, resolve));
|
|
520
|
+
if (rows.length === 0) continue;
|
|
521
|
+
sections.push({
|
|
522
|
+
labelKey: group.labelKey,
|
|
523
|
+
role: group.role === "tabs" ? "radio" : void 0,
|
|
524
|
+
rows
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return sections;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* 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. This is v2's
|
|
532
|
+
* hand-written `mode-tabs-overflow-menu`, derived.
|
|
533
|
+
*/
|
|
534
|
+
function projectShed(group, fit, resolve) {
|
|
535
|
+
return group.units.filter((u) => fit.units.get(u.key)?.kind === "shed").map((u) => projectUnit(u, resolve));
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Strip projection — a bar projected for UN-measured rendering (a contextual
|
|
539
|
+
* strip anchored to a selection has no fit pressure to solve). The schema
|
|
540
|
+
* declares what COULD appear; `visible` (the command registry's runtime truth)
|
|
541
|
+
* decides what DOES: hidden commands drop out, groups that empty out vanish,
|
|
542
|
+
* and an all-hidden bar projects to [] — "render nothing" falls out. Custom
|
|
543
|
+
* units project through their `terminal` command, same totality rule as the
|
|
544
|
+
* overflow. Upgrading a strip to measured/overflowing rendering later is a
|
|
545
|
+
* renderer swap (<Toolbar>), never a schema change.
|
|
546
|
+
*/
|
|
547
|
+
function projectStrip(bar, visible) {
|
|
548
|
+
const groups = [];
|
|
549
|
+
for (const section of bar.sections) for (const group of section.groups) {
|
|
550
|
+
const commands = group.units.map((u) => u.kind === "custom" ? u.terminal : u.command).filter(visible);
|
|
551
|
+
if (commands.length === 0) continue;
|
|
552
|
+
groups.push({
|
|
553
|
+
id: group.id,
|
|
554
|
+
labelKey: group.labelKey,
|
|
555
|
+
commands
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
return groups;
|
|
559
|
+
}
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/shortcuts.ts
|
|
562
|
+
const MODIFIERS = /* @__PURE__ */ new Set([
|
|
563
|
+
"mod",
|
|
564
|
+
"ctrl",
|
|
565
|
+
"control",
|
|
566
|
+
"meta",
|
|
567
|
+
"cmd",
|
|
568
|
+
"command",
|
|
569
|
+
"alt",
|
|
570
|
+
"option",
|
|
571
|
+
"shift"
|
|
572
|
+
]);
|
|
573
|
+
function parseShortcut(shortcut) {
|
|
574
|
+
const tokens = shortcut.split("+").map((t) => t.trim());
|
|
575
|
+
const parsed = {
|
|
576
|
+
key: (tokens[tokens.length - 1] === "" ? "+" : tokens[tokens.length - 1]).toLowerCase(),
|
|
577
|
+
mod: false,
|
|
578
|
+
ctrl: false,
|
|
579
|
+
meta: false,
|
|
580
|
+
alt: false,
|
|
581
|
+
shift: false
|
|
582
|
+
};
|
|
583
|
+
for (const token of tokens.slice(0, -1)) {
|
|
584
|
+
const t = token.toLowerCase();
|
|
585
|
+
if (t === "") continue;
|
|
586
|
+
if (!MODIFIERS.has(t)) throw new Error(`[ui-core] unknown modifier "${token}" in "${shortcut}"`);
|
|
587
|
+
if (t === "mod") parsed.mod = true;
|
|
588
|
+
else if (t === "ctrl" || t === "control") parsed.ctrl = true;
|
|
589
|
+
else if (t === "meta" || t === "cmd" || t === "command") parsed.meta = true;
|
|
590
|
+
else if (t === "alt" || t === "option") parsed.alt = true;
|
|
591
|
+
else parsed.shift = true;
|
|
592
|
+
}
|
|
593
|
+
if (parsed.key === "") throw new Error(`[ui-core] empty key in shortcut "${shortcut}"`);
|
|
594
|
+
return parsed;
|
|
595
|
+
}
|
|
596
|
+
function matchShortcut(parsed, stroke, opts) {
|
|
597
|
+
const wantCtrl = parsed.ctrl || parsed.mod && !opts.isMac;
|
|
598
|
+
const wantMeta = parsed.meta || parsed.mod && opts.isMac;
|
|
599
|
+
if (stroke.ctrlKey !== wantCtrl) return false;
|
|
600
|
+
if (stroke.metaKey !== wantMeta) return false;
|
|
601
|
+
if (stroke.altKey !== parsed.alt) return false;
|
|
602
|
+
if (parsed.shift && !stroke.shiftKey) return false;
|
|
603
|
+
if (!parsed.shift && stroke.shiftKey && parsed.key.length > 1) return false;
|
|
604
|
+
if (stroke.key.toLowerCase() === parsed.key) return true;
|
|
605
|
+
return parsed.key.length > 1 && stroke.code !== void 0 && stroke.code.toLowerCase() === parsed.key;
|
|
606
|
+
}
|
|
607
|
+
/** Display form for menu rows: '⌘K' on mac, 'Ctrl+K' elsewhere. */
|
|
608
|
+
function formatShortcut(shortcut, opts) {
|
|
609
|
+
const parsed = parseShortcut(shortcut);
|
|
610
|
+
const key = parsed.key.length === 1 ? parsed.key.toUpperCase() : capitalize(parsed.key);
|
|
611
|
+
if (opts.isMac) return `${[
|
|
612
|
+
parsed.ctrl ? "⌃" : "",
|
|
613
|
+
parsed.alt ? "⌥" : "",
|
|
614
|
+
parsed.shift ? "⇧" : "",
|
|
615
|
+
parsed.meta || parsed.mod ? "⌘" : ""
|
|
616
|
+
].join("")}${key}`;
|
|
617
|
+
return [...[
|
|
618
|
+
parsed.ctrl || parsed.mod ? "Ctrl" : "",
|
|
619
|
+
parsed.meta ? "Meta" : "",
|
|
620
|
+
parsed.alt ? "Alt" : "",
|
|
621
|
+
parsed.shift ? "Shift" : ""
|
|
622
|
+
].filter(Boolean), key].join("+");
|
|
623
|
+
}
|
|
624
|
+
function capitalize(s) {
|
|
625
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
626
|
+
}
|
|
627
|
+
//#endregion
|
|
628
|
+
export { DEFAULT_IMPORTANCE, PINNED, addItem, chromeHelpers, custom, defineChrome, filterBar, formatShortcut, group, item, matchShortcut, normalizeBar, parseShortcut, projectOverflow, projectShed, projectStrip, removeItems, replaceItem, solve, validateChrome };
|
|
629
|
+
|
|
630
|
+
//# sourceMappingURL=index.js.map
|