ooxml-ui 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -75,6 +75,11 @@ with an incompatible build of this package.
75
75
  | `office-ui-command-search` | `commands` `[{ id, label, keywords?, description?, disabled?, title? }]`, `placeholder`, `label`, `limit` (Office's "Tell me what you want to do") | choice: `office-command` `{ command }` |
76
76
  | `office-ui-options-dialog` | `categories` `[{ id, label, description?, disabled?, sections: [{ id, title, controls }] }]` (controls: `toggle`, `select`, `number`, `text`, each optionally `disabled` with a reason), `values`, `category`, `open`, `heading`, `show()`, `close()` | OK: `office-options-change` `{ values, changed }` |
77
77
  | `office-ui-account` | `profile` `{ displayName, avatarColor, initial? }`; slots `sign-in` and default (product sections) | `office-profile-change` `{ profile }` |
78
+ | `office-ui-ribbon` | Tab row over tab panels. Panels are children with `data-ribbon-tab`, `data-label`, optional `data-tab-keytip`; slots `quick-access`, `search`, `end`; `selected`, `label`, `file-label`, `no-file`, `file-expanded`, `file-keytip`; `focusFile()`, `focusTab()` | `office-ribbon-select` `{ tab }` (cancelable), `office-ribbon-file` |
79
+ | `office-ui-backstage` | `items` `[{ id, label, group?: 'footer', disabled?, title? }]`, `open`, `selected`, `label`, `back-label`, `show(page?)`, `close()`; pages are children with `data-backstage-page` | `office-backstage-select` `{ id }` (cancelable), `office-backstage-close` `{ reason }` (cancelable) |
80
+ | `office-ui-find-bar` | `open`, `label`, `input-label`, `placeholder`, `maxlength`, `disabled`, `navigation-disabled`, `previous-label`, `next-label`, `close-label`; `value`, `status`, `statusTitle`, `show()`, `close()` | `office-find-input` `{ query }`, `office-find-step` `{ direction }`, `office-find-close` |
81
+ | `office-ui-ruler` | `orientation="vertical"`, `origin` (px offset of zero), `scale` (px per unit, default 96), `direction="reverse"`; decorative | none |
82
+ | `office-ui-print-preview` | `pages` (rendered nodes; an inert clone of the current one is shown), `index`, `label`, `empty-label` | `office-print-preview-page` `{ index }` |
78
83
  | `office-ui-presence` | `participants`, `max`, `label` | `office-presence-select` `{ id }` |
79
84
  | `office-ui-smartart` | `drawing` (core `DiagramDrawing`), `schemeColors`, `label` | `office-smartart-render` (render report) |
80
85
  | `office-ui-icon` | `name`, `label` | none |
@@ -85,7 +90,7 @@ an origin shows the same name and avatar colour.
85
90
 
86
91
  `attachKeyTips(scope)` (from `ooxml-ui/controls`) adds Office KeyTips to any element or shadow
87
92
  root: press and release Alt to show badges on controls marked `data-keytip="H"`, type a badge to
88
- activate it, and use `data-keytip-panel` / `data-keytip-level` for a tab's second level. Escape
93
+ activate it (controls inside open shadow roots of shared elements, such as ribbon tabs, are included), and use `data-keytip-panel` / `data-keytip-level` for a tab's second level. Escape
89
94
  steps back; disabled controls are dimmed and never run.
90
95
 
91
96
  Setting a property never emits an event; only user activation does. Controls honour
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Office's File view (backstage): a navigation column and one page per item. Generalised from
3
+ * pptx-viewer's `BACKSTAGE_NAV` model (render/backstage.ts) and the Visio viewer's backstage.
4
+ */
5
+ export interface OfficeBackstageItem {
6
+ id: string;
7
+ label: string;
8
+ /** Items in the footer group sit at the bottom of the column (Account, Options). */
9
+ group?: 'footer';
10
+ disabled?: boolean;
11
+ /** Tooltip, for example why an item is disabled. */
12
+ title?: string;
13
+ }
14
+ /** An item was chosen; cancelable (`preventDefault()` keeps the current page). */
15
+ export type OfficeBackstageSelectEvent = CustomEvent<{
16
+ id: string;
17
+ }>;
18
+ export type OfficeBackstageCloseEvent = CustomEvent<{
19
+ reason: 'back' | 'escape' | 'api';
20
+ }>;
21
+ /**
22
+ * `<office-ui-backstage>`: set `items`, put one light-DOM child per page with
23
+ * `data-backstage-page="<id>"`, then `show(id)`. Choosing an item emits `office-backstage-select`
24
+ * and, unless prevented, shows its page; items without a page (Save, Close) only emit. Back and
25
+ * Escape emit a cancelable `office-backstage-close` and close. Attributes: `open`, `selected`,
26
+ * `label` (default "File"), `back-label`. Nav buttons carry `data-backstage-item`.
27
+ */
28
+ export declare const defineBackstage: import("./registry.js").Definer;
@@ -2,6 +2,717 @@ import { createIconSvg, paintIcon, defineIcon } from './chunk-PCSUQPMC.js';
2
2
  import { initialsOf } from './chunk-RLPL7Q6J.js';
3
3
  import { definer, attachStyles, controlCss, emit } from './chunk-EY266IK3.js';
4
4
 
5
+ // src/backstage.ts
6
+ var CSS = `
7
+ :host { position: absolute; inset: 0; z-index: 30; display: none; min-width: 0;
8
+ background: var(--office-background, #fff); color: var(--office-foreground, #1f2937);
9
+ font-family: var(--office-font, system-ui, sans-serif); }
10
+ :host([open]) { display: block; }
11
+ .frame { display: flex; height: 100%; min-width: 0; }
12
+ nav { display: flex; flex: 0 0 200px; flex-direction: column; padding: 12px 0; overflow: auto;
13
+ background: var(--office-accent, #2563eb); color: var(--office-accent-foreground, #fff); }
14
+ nav button { border: 0; border-radius: 0; background: transparent; color: inherit; font: inherit; text-align: start; cursor: pointer; }
15
+ .back { width: 36px; min-height: 36px; margin: 0 0 12px 16px; padding: 0; border: 1.5px solid currentColor;
16
+ border-radius: 50%; font-size: 18px; text-align: center; }
17
+ .item { min-height: 38px; padding: 6px 24px; font-size: 14px; }
18
+ .item:hover:enabled, .item[aria-current="page"] { background: color-mix(in srgb, #000 22%, transparent); }
19
+ .item:disabled { opacity: .55; cursor: default; }
20
+ nav button:focus-visible { outline: 2px solid currentColor; outline-offset: -3px; }
21
+ .footer { display: flex; flex-direction: column; margin-top: auto; padding-top: 16px; }
22
+ .body { flex: 1; min-width: 0; overflow: auto; padding: 28px 40px; }
23
+ @media (max-width: 760px) {
24
+ .frame { flex-direction: column; }
25
+ nav { flex: none; flex-direction: row; flex-wrap: wrap; padding: 6px; }
26
+ .back { margin: 4px; }
27
+ .item { min-height: 44px; padding: 6px 12px; }
28
+ .footer { flex-direction: row; margin-top: 0; padding-top: 0; }
29
+ .body { padding: 16px; }
30
+ }
31
+ @media (forced-colors: active) { nav { border-inline-end: 1px solid CanvasText; } .item[aria-current="page"] { outline: 2px solid Highlight; } }
32
+ `;
33
+ var defineBackstage = definer("office-ui-backstage", () => {
34
+ class OfficeUiBackstage extends HTMLElement {
35
+ static observedAttributes = ["selected", "label", "back-label"];
36
+ #items = [];
37
+ #frame;
38
+ #nav;
39
+ #main;
40
+ #footer;
41
+ #back;
42
+ constructor() {
43
+ super();
44
+ const doc = this.ownerDocument;
45
+ const root = this.attachShadow({ mode: "open" });
46
+ attachStyles(root, controlCss(CSS));
47
+ this.#frame = doc.createElement("div");
48
+ this.#frame.className = "frame";
49
+ this.#frame.setAttribute("role", "dialog");
50
+ this.#frame.setAttribute("aria-modal", "true");
51
+ this.#nav = doc.createElement("nav");
52
+ this.#nav.setAttribute("part", "nav");
53
+ this.#back = doc.createElement("button");
54
+ this.#back.type = "button";
55
+ this.#back.className = "back";
56
+ this.#back.textContent = "\u2190";
57
+ this.#back.dataset.backstage = "back";
58
+ this.#main = doc.createElement("div");
59
+ this.#footer = doc.createElement("div");
60
+ this.#footer.className = "footer";
61
+ this.#nav.append(this.#back, this.#main, this.#footer);
62
+ const body = doc.createElement("div");
63
+ body.className = "body";
64
+ body.setAttribute("part", "body");
65
+ body.append(doc.createElement("slot"));
66
+ this.#frame.append(this.#nav, body);
67
+ root.append(this.#frame);
68
+ this.#back.addEventListener("click", () => this.#requestClose("back"));
69
+ this.#nav.addEventListener("click", (event) => {
70
+ const item = event.target.closest?.(
71
+ "[data-backstage-item]"
72
+ );
73
+ if (item && !item.disabled) this.#choose(item.dataset.backstageItem);
74
+ });
75
+ this.addEventListener("keydown", (event) => {
76
+ if (event.key !== "Escape" || !this.open) return;
77
+ event.preventDefault();
78
+ event.stopPropagation();
79
+ this.#requestClose("escape");
80
+ });
81
+ this.#labels();
82
+ }
83
+ attributeChangedCallback(name) {
84
+ if (name === "selected") this.#sync();
85
+ else this.#labels();
86
+ }
87
+ get items() {
88
+ return this.#items;
89
+ }
90
+ set items(value) {
91
+ this.#items = value;
92
+ this.#render();
93
+ }
94
+ get open() {
95
+ return this.hasAttribute("open");
96
+ }
97
+ set open(value) {
98
+ this.toggleAttribute("open", Boolean(value));
99
+ }
100
+ get selected() {
101
+ return this.getAttribute("selected") ?? "";
102
+ }
103
+ set selected(id) {
104
+ this.setAttribute("selected", id);
105
+ }
106
+ /** Open on a page (default: the selected one) and focus its item. */
107
+ show(page = this.selected || this.#items.find((item) => this.#page(item.id))?.id || "") {
108
+ this.open = true;
109
+ if (page) this.selected = page;
110
+ this.#button(this.selected)?.focus();
111
+ }
112
+ /** Close without asking (emits `office-backstage-close` with reason `api`). */
113
+ close() {
114
+ if (!this.open) return;
115
+ this.open = false;
116
+ emit(this, "office-backstage-close", { reason: "api" });
117
+ }
118
+ #labels() {
119
+ const label = this.getAttribute("label") ?? "File";
120
+ this.#frame.setAttribute("aria-label", label);
121
+ this.#nav.setAttribute("aria-label", label);
122
+ const back = this.getAttribute("back-label") ?? "Back";
123
+ this.#back.setAttribute("aria-label", back);
124
+ this.#back.title = `${back} (Esc)`;
125
+ }
126
+ #page(id) {
127
+ return [...this.children].find(
128
+ (child) => child instanceof HTMLElement && child.dataset.backstagePage === id
129
+ );
130
+ }
131
+ #button(id) {
132
+ return [...this.#nav.querySelectorAll("[data-backstage-item]")].find(
133
+ (button) => button.dataset.backstageItem === id
134
+ ) ?? null;
135
+ }
136
+ #render() {
137
+ const doc = this.ownerDocument;
138
+ const button = (item) => {
139
+ const element = doc.createElement("button");
140
+ element.type = "button";
141
+ element.className = "item";
142
+ element.dataset.backstageItem = item.id;
143
+ element.textContent = item.label;
144
+ element.disabled = !!item.disabled;
145
+ if (item.title) element.title = item.title;
146
+ return element;
147
+ };
148
+ this.#main.replaceChildren(
149
+ ...this.#items.filter((item) => item.group !== "footer").map(button)
150
+ );
151
+ this.#footer.replaceChildren(
152
+ ...this.#items.filter((item) => item.group === "footer").map(button)
153
+ );
154
+ this.#sync();
155
+ }
156
+ #sync() {
157
+ const selected = this.selected;
158
+ for (const item of this.#nav.querySelectorAll("[data-backstage-item]")) {
159
+ if (item.dataset.backstageItem === selected) item.setAttribute("aria-current", "page");
160
+ else item.removeAttribute("aria-current");
161
+ }
162
+ for (const child of this.children)
163
+ if (child instanceof HTMLElement && child.dataset.backstagePage !== void 0)
164
+ child.hidden = child.dataset.backstagePage !== selected;
165
+ }
166
+ #choose(id) {
167
+ if (emit(this, "office-backstage-select", { id }, true) && this.#page(id)) this.selected = id;
168
+ }
169
+ #requestClose(reason) {
170
+ if (emit(this, "office-backstage-close", { reason }, true)) this.open = false;
171
+ }
172
+ }
173
+ return OfficeUiBackstage;
174
+ });
175
+
176
+ // src/find-bar.ts
177
+ var CSS2 = `
178
+ :host { display: flex; align-items: center; gap: 8px; min-height: 40px; padding: 4px 8px 4px 12px;
179
+ border-bottom: 1px solid var(--office-border, #d1d5db); background: var(--office-surface, #f3f4f6);
180
+ color: var(--office-foreground, #1f2937); font: 12px var(--office-font, system-ui, sans-serif); }
181
+ :host(:not([open])) { display: none; }
182
+ [role="search"] { display: flex; flex: 1; align-items: center; gap: 4px; min-width: 0; max-width: 640px; }
183
+ label { display: flex; flex: 1; align-items: center; gap: 6px; min-width: 0; }
184
+ input { flex: 1; min-width: 0; min-height: 26px; padding: 2px 6px; font: inherit; color: inherit;
185
+ border: 1px solid var(--office-border, #d1d5db); border-radius: 3px; background: var(--office-background, #fff); }
186
+ button { min-height: 26px; padding: 2px 8px; font: inherit; color: inherit; cursor: pointer;
187
+ border: 1px solid var(--office-border, #d1d5db); border-radius: 3px; background: var(--office-background, #fff); }
188
+ button:disabled { opacity: .5; cursor: default; }
189
+ .status { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
190
+ color: var(--office-muted-foreground, #6b7280); }
191
+ .close { margin-inline-start: auto; width: var(--office-target-size, 28px); padding: 0; border-color: transparent;
192
+ background: transparent; font-size: 18px; line-height: 1; }
193
+ input:focus-visible, button:focus-visible { outline: 2px solid var(--office-ring, #2563eb); outline-offset: 1px; }
194
+ @media (max-width: 760px) { :host { flex-wrap: wrap; } [role="search"] { flex-wrap: wrap; } }
195
+ `;
196
+ var defineFindBar = definer("office-ui-find-bar", () => {
197
+ class OfficeUiFindBar extends HTMLElement {
198
+ static observedAttributes = [
199
+ "label",
200
+ "input-label",
201
+ "placeholder",
202
+ "maxlength",
203
+ "disabled",
204
+ "navigation-disabled",
205
+ "previous-label",
206
+ "next-label",
207
+ "close-label"
208
+ ];
209
+ #region;
210
+ #input;
211
+ #previous;
212
+ #next;
213
+ #status;
214
+ #close;
215
+ constructor() {
216
+ super();
217
+ const doc = this.ownerDocument;
218
+ const root = this.attachShadow({ mode: "open", delegatesFocus: true });
219
+ attachStyles(root, controlCss(CSS2));
220
+ this.#region = doc.createElement("div");
221
+ this.#region.setAttribute("role", "search");
222
+ const field = doc.createElement("label");
223
+ field.append("Find ");
224
+ this.#input = doc.createElement("input");
225
+ this.#input.type = "search";
226
+ this.#input.autocomplete = "off";
227
+ this.#input.spellcheck = false;
228
+ this.#input.setAttribute("aria-describedby", "status");
229
+ field.append(this.#input);
230
+ const button = (action, text2) => {
231
+ const element = doc.createElement("button");
232
+ element.type = "button";
233
+ element.dataset.action = action;
234
+ element.textContent = text2;
235
+ return element;
236
+ };
237
+ this.#previous = button("previous", "Previous");
238
+ this.#next = button("next", "Next");
239
+ this.#status = doc.createElement("span");
240
+ this.#status.id = "status";
241
+ this.#status.className = "status";
242
+ this.#status.setAttribute("role", "status");
243
+ this.#status.setAttribute("aria-live", "polite");
244
+ this.#status.setAttribute("aria-atomic", "true");
245
+ this.#region.append(field, this.#previous, this.#next, this.#status);
246
+ this.#close = button("close", "\xD7");
247
+ this.#close.className = "close";
248
+ root.append(this.#region, this.#close);
249
+ this.#input.addEventListener(
250
+ "input",
251
+ () => emit(this, "office-find-input", { query: this.#input.value })
252
+ );
253
+ this.#input.addEventListener("keydown", (event) => this.#key(event));
254
+ this.#previous.addEventListener("click", () => this.#step("previous"));
255
+ this.#next.addEventListener("click", () => this.#step("next"));
256
+ this.#close.addEventListener("click", () => this.close());
257
+ this.#sync();
258
+ }
259
+ attributeChangedCallback() {
260
+ this.#sync();
261
+ }
262
+ get open() {
263
+ return this.hasAttribute("open");
264
+ }
265
+ set open(value) {
266
+ this.toggleAttribute("open", Boolean(value));
267
+ }
268
+ get value() {
269
+ return this.#input.value;
270
+ }
271
+ set value(query) {
272
+ if (this.#input.value !== query) this.#input.value = query;
273
+ }
274
+ get status() {
275
+ return this.#status.textContent ?? "";
276
+ }
277
+ set status(text2) {
278
+ this.#status.textContent = text2;
279
+ }
280
+ get statusTitle() {
281
+ return this.#status.title;
282
+ }
283
+ set statusTitle(text2) {
284
+ this.#status.title = text2;
285
+ }
286
+ /** Open, focus the field and select its query. */
287
+ show() {
288
+ this.open = true;
289
+ this.#input.focus();
290
+ this.#input.select();
291
+ }
292
+ /** Close and emit `office-find-close`. */
293
+ close() {
294
+ if (!this.open) return;
295
+ this.open = false;
296
+ emit(this, "office-find-close", {});
297
+ }
298
+ #step(direction) {
299
+ if (!this.hasAttribute("navigation-disabled")) emit(this, "office-find-step", { direction });
300
+ }
301
+ #key(event) {
302
+ if (event.isComposing) return;
303
+ if (event.key === "Enter") {
304
+ event.preventDefault();
305
+ this.#step(event.shiftKey ? "previous" : "next");
306
+ } else if (event.key === "Escape") {
307
+ event.preventDefault();
308
+ event.stopPropagation();
309
+ if (this.#input.value) {
310
+ this.#input.value = "";
311
+ emit(this, "office-find-input", { query: "" });
312
+ } else this.close();
313
+ }
314
+ }
315
+ #sync() {
316
+ const text2 = (name, fallback) => this.getAttribute(name) ?? fallback;
317
+ this.#region.setAttribute("aria-label", text2("label", "Find"));
318
+ this.#input.setAttribute("aria-label", text2("input-label", "Find"));
319
+ this.#input.placeholder = text2("placeholder", "");
320
+ const max = Number(this.getAttribute("maxlength"));
321
+ if (Number.isInteger(max) && max > 0) this.#input.maxLength = max;
322
+ else this.#input.removeAttribute("maxlength");
323
+ this.#input.disabled = this.hasAttribute("disabled");
324
+ const stuck = this.hasAttribute("navigation-disabled") || this.#input.disabled;
325
+ this.#previous.disabled = this.#next.disabled = stuck;
326
+ this.#previous.setAttribute("aria-label", text2("previous-label", "Previous result"));
327
+ this.#next.setAttribute("aria-label", text2("next-label", "Next result"));
328
+ const close = text2("close-label", "Close Find");
329
+ this.#close.setAttribute("aria-label", close);
330
+ this.#close.title = `${close} (Esc)`;
331
+ }
332
+ }
333
+ return OfficeUiFindBar;
334
+ });
335
+
336
+ // src/print-preview.ts
337
+ var CSS3 = `
338
+ :host { display: block; max-width: 420px; font: 12px var(--office-font, system-ui, sans-serif);
339
+ color: var(--office-foreground, #1f2937); }
340
+ .sheet { padding: 12px; border: 1px solid var(--office-border, #d1d5db); background: #fff;
341
+ box-shadow: 0 1px 4px rgb(0 0 0 / 15%); }
342
+ .sheet > * { display: block; width: 100%; height: auto; }
343
+ .empty { color: var(--office-muted-foreground, #6b7280); }
344
+ .nav { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 8px; }
345
+ .nav[hidden] { display: none; }
346
+ button { min-width: var(--office-target-size, 28px); min-height: var(--office-target-size, 28px); font: inherit;
347
+ color: inherit; border: 1px solid var(--office-border, #d1d5db); border-radius: 3px;
348
+ background: var(--office-background, #fff); cursor: pointer; }
349
+ button:disabled { opacity: .5; cursor: default; }
350
+ button:focus-visible { outline: 2px solid var(--office-ring, #2563eb); outline-offset: 1px; }
351
+ `;
352
+ var definePrintPreview = definer("office-ui-print-preview", () => {
353
+ class OfficeUiPrintPreview extends HTMLElement {
354
+ static observedAttributes = ["label", "empty-label"];
355
+ #pages = [];
356
+ #index = 0;
357
+ #sheet;
358
+ #nav;
359
+ #previous;
360
+ #next;
361
+ #count;
362
+ constructor() {
363
+ super();
364
+ const doc = this.ownerDocument;
365
+ const root = this.attachShadow({ mode: "open" });
366
+ attachStyles(root, controlCss(CSS3));
367
+ this.#sheet = doc.createElement("div");
368
+ this.#sheet.className = "sheet";
369
+ this.#sheet.setAttribute("role", "img");
370
+ this.#nav = doc.createElement("div");
371
+ this.#nav.className = "nav";
372
+ const button = (label, text2, step) => {
373
+ const element = doc.createElement("button");
374
+ element.type = "button";
375
+ element.setAttribute("aria-label", label);
376
+ element.textContent = text2;
377
+ element.addEventListener("click", () => this.#go(this.#index + step));
378
+ return element;
379
+ };
380
+ this.#previous = button("Previous page", "\u2039", -1);
381
+ this.#next = button("Next page", "\u203A", 1);
382
+ this.#count = doc.createElement("span");
383
+ this.#nav.append(this.#previous, this.#count, this.#next);
384
+ root.append(this.#sheet, this.#nav);
385
+ this.#render();
386
+ }
387
+ attributeChangedCallback() {
388
+ this.#render();
389
+ }
390
+ get pages() {
391
+ return this.#pages;
392
+ }
393
+ set pages(value) {
394
+ this.#pages = value;
395
+ this.#index = Math.min(this.#index, Math.max(0, value.length - 1));
396
+ this.#render();
397
+ }
398
+ get index() {
399
+ return this.#index;
400
+ }
401
+ set index(value) {
402
+ const next = Math.max(0, Math.min(this.#pages.length - 1, Math.trunc(value) || 0));
403
+ if (next === this.#index) return;
404
+ this.#index = next;
405
+ this.#render();
406
+ }
407
+ #go(index) {
408
+ const before = this.#index;
409
+ this.index = index;
410
+ if (this.#index !== before) emit(this, "office-print-preview-page", { index: this.#index });
411
+ }
412
+ #render() {
413
+ const doc = this.ownerDocument;
414
+ const total = this.#pages.length;
415
+ const page = this.#pages[this.#index];
416
+ const label = this.getAttribute("label") ?? "Print preview";
417
+ this.#sheet.setAttribute(
418
+ "aria-label",
419
+ total > 1 ? `${label}, page ${this.#index + 1} of ${total}` : label
420
+ );
421
+ if (!page) {
422
+ const empty = doc.createElement("p");
423
+ empty.className = "empty";
424
+ empty.textContent = this.getAttribute("empty-label") ?? "Nothing to print.";
425
+ this.#sheet.replaceChildren(empty);
426
+ } else {
427
+ const clone = doc.importNode(page, true);
428
+ if (clone instanceof Element) {
429
+ clone.removeAttribute("style");
430
+ clone.removeAttribute("tabindex");
431
+ clone.querySelectorAll("[tabindex]").forEach((node) => node.removeAttribute("tabindex"));
432
+ }
433
+ this.#sheet.replaceChildren(clone);
434
+ }
435
+ this.#nav.hidden = total < 2;
436
+ this.#count.textContent = `${this.#index + 1} of ${total}`;
437
+ this.#previous.disabled = this.#index === 0;
438
+ this.#next.disabled = this.#index >= total - 1;
439
+ }
440
+ }
441
+ return OfficeUiPrintPreview;
442
+ });
443
+
444
+ // src/ribbon-tabs.ts
445
+ var CSS4 = `
446
+ :host { display: block; min-width: 0; }
447
+ .head { display: flex; align-items: stretch; min-width: 0; border-bottom: 1px solid var(--office-border, #d1d5db); }
448
+ .file { flex: none; min-height: var(--office-ribbon-tab-height, 35px); margin: 0 4px 0 0; padding: 6px 16px; border: 0;
449
+ border-radius: 0; background: var(--office-accent, #2563eb); color: var(--office-accent-foreground, #fff);
450
+ font: inherit; font-size: 13px; cursor: pointer; }
451
+ .file:hover { background: color-mix(in srgb, var(--office-accent, #2563eb) 85%, #000); }
452
+ .file[hidden] { display: none; }
453
+ [role="tablist"] { display: flex; flex: 0 1 auto; align-items: stretch; min-width: 0; overflow-x: auto; scrollbar-width: none; }
454
+ [role="tab"] { position: relative; flex: none; min-height: var(--office-ribbon-tab-height, 35px); padding: 6px 12px; border: 0;
455
+ border-radius: 0; background: transparent; color: var(--office-muted-foreground, #6b7280); font: inherit; font-size: 13px;
456
+ cursor: pointer; }
457
+ [role="tab"]:hover { color: var(--office-foreground, #1f2937); background: var(--office-surface, #f3f4f6); }
458
+ [role="tab"][aria-selected="true"] { color: var(--office-accent, #2563eb); }
459
+ [role="tab"][aria-selected="true"]::after { content: ""; position: absolute; inset: auto 0 0; height: 2.5px;
460
+ background: var(--office-accent, #2563eb); }
461
+ .file:focus-visible, [role="tab"]:focus-visible { outline: 2px solid var(--office-ring, #2563eb); outline-offset: -2px; }
462
+ ::slotted([slot="search"]) { align-self: center; }
463
+ @media (max-width: 760px), (pointer: coarse) {
464
+ .file, [role="tab"] { min-height: 44px; }
465
+ .file { min-width: 52px; }
466
+ }
467
+ @media (forced-colors: active) {
468
+ .file { border: 1px solid ButtonText; }
469
+ [role="tab"][aria-selected="true"]::after { background: Highlight; }
470
+ }
471
+ `;
472
+ var defineRibbon = definer("office-ui-ribbon", () => {
473
+ class OfficeUiRibbon extends HTMLElement {
474
+ static observedAttributes = [
475
+ "selected",
476
+ "label",
477
+ "file-label",
478
+ "no-file",
479
+ "file-expanded",
480
+ "file-keytip"
481
+ ];
482
+ #file;
483
+ #tabs;
484
+ #panelSlot;
485
+ constructor() {
486
+ super();
487
+ const doc = this.ownerDocument;
488
+ const root = this.attachShadow({ mode: "open" });
489
+ attachStyles(root, controlCss(CSS4));
490
+ const head = doc.createElement("div");
491
+ head.className = "head";
492
+ head.setAttribute("part", "head");
493
+ const slot = (name) => {
494
+ const element = doc.createElement("slot");
495
+ element.name = name;
496
+ return element;
497
+ };
498
+ this.#file = doc.createElement("button");
499
+ this.#file.type = "button";
500
+ this.#file.className = "file";
501
+ this.#file.setAttribute("part", "file");
502
+ this.#file.setAttribute("aria-haspopup", "dialog");
503
+ this.#file.setAttribute("aria-expanded", "false");
504
+ this.#file.addEventListener("click", () => emit(this, "office-ribbon-file", {}));
505
+ this.#tabs = doc.createElement("div");
506
+ this.#tabs.setAttribute("role", "tablist");
507
+ this.#tabs.setAttribute("part", "tabs");
508
+ this.#tabs.addEventListener("click", (event) => {
509
+ const tab = event.target.closest?.('[role="tab"]');
510
+ if (tab) this.#choose(tab.dataset.tab);
511
+ });
512
+ this.#tabs.addEventListener("keydown", (event) => this.#key(event));
513
+ head.append(slot("quick-access"), this.#file, this.#tabs, slot("search"), slot("end"));
514
+ this.#panelSlot = doc.createElement("slot");
515
+ this.#panelSlot.addEventListener("slotchange", () => this.#build());
516
+ root.append(head, this.#panelSlot);
517
+ this.#syncFile();
518
+ }
519
+ connectedCallback() {
520
+ this.#build();
521
+ }
522
+ attributeChangedCallback(name) {
523
+ if (name === "selected") this.#sync();
524
+ else if (name === "label")
525
+ this.#tabs.setAttribute("aria-label", this.getAttribute("label") ?? "Ribbon");
526
+ else this.#syncFile();
527
+ }
528
+ get selected() {
529
+ return this.getAttribute("selected") ?? this.#panels()[0]?.dataset.ribbonTab ?? "";
530
+ }
531
+ set selected(tab) {
532
+ this.setAttribute("selected", tab);
533
+ }
534
+ /** Focus the File button (for example when the backstage it opened closes). */
535
+ focusFile() {
536
+ this.#file.focus();
537
+ }
538
+ /** Focus the selected tab. */
539
+ focusTab() {
540
+ this.#tabs.querySelector('[aria-selected="true"]')?.focus();
541
+ }
542
+ #panels() {
543
+ return [...this.children].filter(
544
+ (child) => child instanceof HTMLElement && !!child.dataset.ribbonTab
545
+ );
546
+ }
547
+ #build() {
548
+ const doc = this.ownerDocument;
549
+ if (!this.#tabs.hasAttribute("aria-label"))
550
+ this.#tabs.setAttribute("aria-label", this.getAttribute("label") ?? "Ribbon");
551
+ this.#tabs.replaceChildren(
552
+ ...this.#panels().map((panel) => {
553
+ const id = panel.dataset.ribbonTab;
554
+ const tab = doc.createElement("button");
555
+ tab.type = "button";
556
+ tab.setAttribute("role", "tab");
557
+ tab.id = `tab-${id}`;
558
+ tab.dataset.tab = id;
559
+ tab.textContent = panel.dataset.label ?? id;
560
+ if (panel.dataset.tabKeytip) {
561
+ tab.dataset.keytip = panel.dataset.tabKeytip;
562
+ if (panel.id) tab.dataset.keytipPanel = panel.id;
563
+ }
564
+ panel.setAttribute("role", "tabpanel");
565
+ panel.setAttribute("aria-label", panel.dataset.label ?? id);
566
+ return tab;
567
+ })
568
+ );
569
+ this.#sync();
570
+ }
571
+ #sync() {
572
+ const selected = this.selected;
573
+ for (const tab of this.#tabs.querySelectorAll('[role="tab"]')) {
574
+ const on = tab.dataset.tab === selected;
575
+ tab.setAttribute("aria-selected", String(on));
576
+ tab.tabIndex = on ? 0 : -1;
577
+ }
578
+ for (const panel of this.#panels()) panel.hidden = panel.dataset.ribbonTab !== selected;
579
+ }
580
+ #syncFile() {
581
+ this.#file.textContent = this.getAttribute("file-label") ?? "File";
582
+ this.#file.hidden = this.hasAttribute("no-file");
583
+ this.#file.setAttribute(
584
+ "aria-expanded",
585
+ String(this.getAttribute("file-expanded") === "true")
586
+ );
587
+ const keytip = this.getAttribute("file-keytip");
588
+ if (keytip) this.#file.dataset.keytip = keytip;
589
+ else delete this.#file.dataset.keytip;
590
+ }
591
+ #choose(tab) {
592
+ if (tab === this.selected) return;
593
+ if (emit(this, "office-ribbon-select", { tab }, true)) this.selected = tab;
594
+ }
595
+ #key(event) {
596
+ const tabs = [...this.#tabs.querySelectorAll('[role="tab"]')];
597
+ const focused = event.target.closest?.('[role="tab"]');
598
+ const at = focused ? tabs.indexOf(focused) : tabs.findIndex((tab) => tab.dataset.tab === this.selected);
599
+ const step = { ArrowRight: 1, ArrowLeft: -1 }[event.key];
600
+ const next = event.key === "Home" ? tabs[0] : event.key === "End" ? tabs.at(-1) : step ? tabs[(at + step + tabs.length) % tabs.length] : void 0;
601
+ if (!next) return;
602
+ event.preventDefault();
603
+ this.#choose(next.dataset.tab);
604
+ this.focusTab();
605
+ }
606
+ }
607
+ return OfficeUiRibbon;
608
+ });
609
+
610
+ // src/ruler.ts
611
+ var CSS5 = `
612
+ :host { display: block; box-sizing: border-box; background: var(--office-background, #fff);
613
+ color: var(--office-muted-foreground, #605e5c); overflow: hidden; }
614
+ :host(:not([orientation="vertical"])) { height: 18px; border-bottom: 1px solid var(--office-border, #d1d5db); }
615
+ :host([orientation="vertical"]) { width: 18px; border-inline-end: 1px solid var(--office-border, #d1d5db); }
616
+ canvas { display: block; width: 100%; height: 100%; }
617
+ `;
618
+ function rulerDivisions(unitPixels) {
619
+ return unitPixels >= 192 ? 16 : unitPixels >= 96 ? 8 : unitPixels >= 48 ? 4 : 2;
620
+ }
621
+ var defineRuler = definer("office-ui-ruler", () => {
622
+ class OfficeUiRuler extends HTMLElement {
623
+ static observedAttributes = ["orientation", "origin", "scale", "direction"];
624
+ #canvas;
625
+ #frame = 0;
626
+ #observer;
627
+ constructor() {
628
+ super();
629
+ const root = this.attachShadow({ mode: "open" });
630
+ attachStyles(root, controlCss(CSS5));
631
+ this.#canvas = this.ownerDocument.createElement("canvas");
632
+ root.append(this.#canvas);
633
+ }
634
+ connectedCallback() {
635
+ this.setAttribute("aria-hidden", "true");
636
+ const Observer = this.ownerDocument.defaultView?.ResizeObserver;
637
+ this.#observer = Observer ? new Observer(() => this.#schedule()) : void 0;
638
+ this.#observer?.observe(this);
639
+ this.#schedule();
640
+ }
641
+ disconnectedCallback() {
642
+ this.#observer?.disconnect();
643
+ if (this.#frame) this.ownerDocument.defaultView?.cancelAnimationFrame?.(this.#frame);
644
+ this.#frame = 0;
645
+ }
646
+ attributeChangedCallback() {
647
+ this.#schedule();
648
+ }
649
+ #number(name, fallback) {
650
+ const value = Number(this.getAttribute(name));
651
+ return this.hasAttribute(name) && Number.isFinite(value) ? value : fallback;
652
+ }
653
+ #schedule() {
654
+ if (this.#frame || !this.isConnected) return;
655
+ const view = this.ownerDocument.defaultView;
656
+ this.#frame = view?.requestAnimationFrame?.(() => this.#draw()) ?? 0;
657
+ if (!this.#frame) this.#draw();
658
+ }
659
+ #draw() {
660
+ this.#frame = 0;
661
+ const view = this.ownerDocument.defaultView;
662
+ const vertical = this.getAttribute("orientation") === "vertical";
663
+ const reverse = this.getAttribute("direction") === "reverse";
664
+ const box = this.getBoundingClientRect();
665
+ const length = vertical ? box.height : box.width;
666
+ const thickness = (vertical ? box.width : box.height) || 18;
667
+ const ratio = view?.devicePixelRatio ?? 1;
668
+ const canvas = this.#canvas;
669
+ canvas.width = Math.max(1, Math.round((vertical ? thickness : length) * ratio));
670
+ canvas.height = Math.max(1, Math.round((vertical ? length : thickness) * ratio));
671
+ const context = canvas.getContext?.("2d");
672
+ if (!context || !length) return;
673
+ context.scale(ratio, ratio);
674
+ const ink = view?.getComputedStyle(this).color || "#605e5c";
675
+ context.strokeStyle = ink;
676
+ context.fillStyle = ink;
677
+ context.lineWidth = 1;
678
+ context.font = `9px ${view?.getComputedStyle(this).fontFamily || "system-ui, sans-serif"}`;
679
+ const origin = this.#number("origin", 0);
680
+ const unit = Math.max(1, this.#number("scale", 96));
681
+ const divisions = rulerDivisions(unit);
682
+ const step = unit / divisions;
683
+ const sign = reverse ? -1 : 1;
684
+ const a = (0 - origin) / (sign * step);
685
+ const b = (length - origin) / (sign * step);
686
+ const first = Math.floor(Math.min(a, b)) - 1;
687
+ const last = Math.ceil(Math.max(a, b)) + 1;
688
+ context.beginPath();
689
+ for (let index = first; index <= last; index++) {
690
+ const at = Math.round(origin + sign * index * step) + 0.5;
691
+ const tick = index % divisions === 0 ? thickness : index % (divisions / 2) === 0 ? thickness * 0.5 : thickness * 0.3;
692
+ if (vertical) {
693
+ context.moveTo(thickness, at);
694
+ context.lineTo(thickness - tick, at);
695
+ } else {
696
+ context.moveTo(at, thickness);
697
+ context.lineTo(at, thickness - tick);
698
+ }
699
+ if (index % divisions !== 0) continue;
700
+ const label = String(index / divisions);
701
+ if (!vertical) context.fillText(label, at + 2, 9);
702
+ else {
703
+ context.save();
704
+ context.translate(9, at - 2);
705
+ context.rotate(-Math.PI / 2);
706
+ context.fillText(label, 0, 0);
707
+ context.restore();
708
+ }
709
+ }
710
+ context.stroke();
711
+ }
712
+ }
713
+ return OfficeUiRuler;
714
+ });
715
+
5
716
  // src/account.ts
6
717
  var DEFAULT_OFFICE_PROFILE = { displayName: "", avatarColor: "#6366f1" };
7
718
  var OFFICE_AVATAR_SWATCHES = [
@@ -52,7 +763,7 @@ function clearOfficeProfile(storage = defaultStorage(), key = OFFICE_PROFILE_STO
52
763
  } catch {
53
764
  }
54
765
  }
55
- var CSS = `
766
+ var CSS6 = `
56
767
  :host { display: block; font-family: var(--office-font, system-ui, sans-serif); font-size: 13px; }
57
768
  h2 { margin: 0 0 12px; font-size: 15px; font-weight: 600; }
58
769
  .who { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
@@ -82,7 +793,7 @@ var defineAccount = definer("office-ui-account", () => {
82
793
  super();
83
794
  const doc = this.ownerDocument;
84
795
  const root = this.attachShadow({ mode: "open" });
85
- attachStyles(root, controlCss(CSS));
796
+ attachStyles(root, controlCss(CSS6));
86
797
  const heading = doc.createElement("h2");
87
798
  heading.textContent = "User Information";
88
799
  const who = doc.createElement("div");
@@ -183,7 +894,7 @@ var defineAccount = definer("office-ui-account", () => {
183
894
  });
184
895
 
185
896
  // src/button.ts
186
- var CSS2 = `
897
+ var CSS7 = `
187
898
  :host { display: inline-flex; vertical-align: middle; }
188
899
  button { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center;
189
900
  gap: 6px; min-width: var(--office-target-size, 28px); min-height: var(--office-target-size, 28px);
@@ -224,7 +935,7 @@ var defineButton = definer("office-ui-button", () => {
224
935
  constructor() {
225
936
  super();
226
937
  const root = this.attachShadow({ mode: "open", delegatesFocus: true });
227
- attachStyles(root, controlCss(CSS2));
938
+ attachStyles(root, controlCss(CSS7));
228
939
  this.button = this.ownerDocument.createElement("button");
229
940
  this.button.type = "button";
230
941
  this.button.setAttribute("part", "button");
@@ -399,7 +1110,7 @@ var defineCheckbox = definer(
399
1110
  var defineSwitch = definer("office-ui-switch", makeCheckable("switch", SWITCH_CSS));
400
1111
 
401
1112
  // src/dialog.ts
402
- var CSS3 = `
1113
+ var CSS8 = `
403
1114
  :host { display: none; position: fixed; inset: 0; z-index: 1000; align-items: center; justify-content: center; }
404
1115
  :host([open]) { display: flex; }
405
1116
  .backdrop { position: absolute; inset: 0; background: rgb(0 0 0 / .4); }
@@ -432,7 +1143,7 @@ var defineDialog = definer("office-ui-dialog", () => {
432
1143
  super();
433
1144
  const doc = this.ownerDocument;
434
1145
  const root = this.attachShadow({ mode: "open" });
435
- attachStyles(root, controlCss(CSS3));
1146
+ attachStyles(root, controlCss(CSS8));
436
1147
  const backdrop = doc.createElement("div");
437
1148
  backdrop.className = "backdrop";
438
1149
  this.box = doc.createElement("div");
@@ -535,7 +1246,7 @@ var defineDialog = definer("office-ui-dialog", () => {
535
1246
  });
536
1247
 
537
1248
  // src/command-search.ts
538
- var CSS4 = `
1249
+ var CSS9 = `
539
1250
  :host { display: inline-flex; align-items: center; min-width: 0; font-family: var(--office-font, system-ui, sans-serif); }
540
1251
  .field { display: flex; align-items: center; gap: 6px; width: 100%; min-width: 0; height: var(--office-target-size, 28px);
541
1252
  padding: 0 8px; box-sizing: border-box; border: 1px solid transparent; border-radius: var(--office-radius, 4px);
@@ -575,7 +1286,7 @@ var defineCommandSearch = definer("office-ui-command-search", () => {
575
1286
  super();
576
1287
  const doc = this.ownerDocument;
577
1288
  const root = this.attachShadow({ mode: "open", delegatesFocus: true });
578
- attachStyles(root, controlCss(CSS4));
1289
+ attachStyles(root, controlCss(CSS9));
579
1290
  const field = doc.createElement("div");
580
1291
  field.className = "field";
581
1292
  const glass = createIconSvg(doc);
@@ -732,7 +1443,7 @@ var defineCommandSearch = definer("office-ui-command-search", () => {
732
1443
  });
733
1444
 
734
1445
  // src/context-menu.ts
735
- var CSS5 = `
1446
+ var CSS10 = `
736
1447
  :host { display: contents; }
737
1448
  .panel { position: fixed; inset: auto; margin: 0; min-width: 200px; padding: 4px; box-sizing: border-box;
738
1449
  background: var(--office-background, #fff); color: var(--office-foreground, #1f2937);
@@ -768,7 +1479,7 @@ var defineContextMenu = definer("office-ui-context-menu", () => {
768
1479
  super();
769
1480
  const doc = this.ownerDocument;
770
1481
  const root = this.attachShadow({ mode: "open" });
771
- attachStyles(root, controlCss(CSS5));
1482
+ attachStyles(root, controlCss(CSS10));
772
1483
  this.panel = doc.createElement("div");
773
1484
  this.panel.className = "panel";
774
1485
  this.panel.setAttribute("role", "menu");
@@ -1178,7 +1889,7 @@ function clampOptionNumber(raw, min, max) {
1178
1889
  const parsed = Number(raw);
1179
1890
  return Number.isFinite(parsed) && raw.trim() !== "" ? Math.min(max, Math.max(min, parsed)) : void 0;
1180
1891
  }
1181
- var CSS6 = `
1892
+ var CSS11 = `
1182
1893
  :host { display: contents; }
1183
1894
  office-ui-dialog { --office-dialog-width: min(820px, 100vw); --office-dialog-max-width: min(820px, 100vw); }
1184
1895
  .layout { display: grid; grid-template-columns: 180px minmax(0, 1fr); gap: 0; min-height: min(440px, 70vh); margin: -16px; }
@@ -1227,7 +1938,7 @@ var defineOptionsDialog = definer("office-ui-options-dialog", () => {
1227
1938
  super();
1228
1939
  const doc = this.ownerDocument;
1229
1940
  const root = this.attachShadow({ mode: "open" });
1230
- attachStyles(root, controlCss(CSS6));
1941
+ attachStyles(root, controlCss(CSS11));
1231
1942
  this.#dialog = doc.createElement("office-ui-dialog");
1232
1943
  const layout = doc.createElement("div");
1233
1944
  layout.className = "layout";
@@ -1572,7 +2283,7 @@ var defineToolbar = definer("office-ui-toolbar", () => {
1572
2283
  });
1573
2284
 
1574
2285
  // src/select.ts
1575
- var CSS7 = `
2286
+ var CSS12 = `
1576
2287
  :host { display: inline-block; position: relative; vertical-align: middle; }
1577
2288
  button { box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: 8px;
1578
2289
  width: 100%; min-height: var(--office-target-size, 28px); padding: 0 8px; font: inherit; font-size: 12px;
@@ -1611,7 +2322,7 @@ var defineSelect = definer("office-ui-select", () => {
1611
2322
  super();
1612
2323
  const doc = this.ownerDocument;
1613
2324
  const root = this.attachShadow({ mode: "open", delegatesFocus: true });
1614
- attachStyles(root, controlCss(CSS7));
2325
+ attachStyles(root, controlCss(CSS12));
1615
2326
  this.trigger = doc.createElement("button");
1616
2327
  this.trigger.type = "button";
1617
2328
  this.trigger.setAttribute("role", "combobox");
@@ -1862,7 +2573,7 @@ var defineStatusItem = definer("office-ui-status-item", () => {
1862
2573
  });
1863
2574
 
1864
2575
  // src/tab-strip.ts
1865
- var CSS8 = `
2576
+ var CSS13 = `
1866
2577
  :host { display: flex; align-items: stretch; min-width: 0; min-height: 28px; gap: 2px; padding: 0 6px;
1867
2578
  background: var(--office-surface, #f3f4f6); color: var(--office-muted-foreground, #6b7280);
1868
2579
  border-top: 1px solid var(--office-border, #d1d5db); font-size: 12px;
@@ -1909,7 +2620,7 @@ var defineTabStrip = definer("office-ui-tab-strip", () => {
1909
2620
  super();
1910
2621
  const doc = this.ownerDocument;
1911
2622
  const root = this.attachShadow({ mode: "open" });
1912
- attachStyles(root, controlCss(CSS8));
2623
+ attachStyles(root, controlCss(CSS13));
1913
2624
  const step = (name, icon) => {
1914
2625
  const button = doc.createElement("button");
1915
2626
  button.type = "button";
@@ -2048,7 +2759,7 @@ var defineTabStrip = definer("office-ui-tab-strip", () => {
2048
2759
  });
2049
2760
 
2050
2761
  // src/zoom-slider.ts
2051
- var CSS9 = `
2762
+ var CSS14 = `
2052
2763
  :host { display: inline-flex; align-items: center; gap: 2px; font-size: 11px;
2053
2764
  color: var(--office-foreground, #1f2937); font-family: var(--office-font, system-ui, sans-serif); }
2054
2765
  button { box-sizing: border-box; display: inline-grid; place-items: center; min-width: var(--office-target-size, 24px);
@@ -2084,7 +2795,7 @@ var defineZoomSlider = definer("office-ui-zoom-slider", () => {
2084
2795
  super();
2085
2796
  const doc = this.ownerDocument;
2086
2797
  const root = this.attachShadow({ mode: "open" });
2087
- attachStyles(root, controlCss(CSS9));
2798
+ attachStyles(root, controlCss(CSS14));
2088
2799
  const button = (name, text2) => {
2089
2800
  const element = doc.createElement("button");
2090
2801
  element.type = "button";
@@ -2186,6 +2897,19 @@ var visible = (el) => {
2186
2897
  const box = el.getBoundingClientRect();
2187
2898
  return box.width > 0 && box.height > 0;
2188
2899
  };
2900
+ function deepTips(root) {
2901
+ const found = [...root.querySelectorAll("[data-keytip]")];
2902
+ for (const host of root.querySelectorAll("*")) {
2903
+ const shadow = host.shadowRoot;
2904
+ if (!shadow) continue;
2905
+ const inner = deepTips(shadow);
2906
+ if (!inner.length) continue;
2907
+ for (const el of inner) hostOf.set(el, host);
2908
+ found.push(...inner);
2909
+ }
2910
+ return found;
2911
+ }
2912
+ var hostOf = /* @__PURE__ */ new WeakMap();
2189
2913
  function activate(el) {
2190
2914
  const inner = el.shadowRoot?.querySelector(".main, button");
2191
2915
  (inner ?? el).click();
@@ -2203,8 +2927,8 @@ function attachKeyTips(scope) {
2203
2927
  let levels = [];
2204
2928
  let typed = "";
2205
2929
  let altAlone = false;
2206
- const tipsAt = (container) => [...(container ?? scope).querySelectorAll("[data-keytip]")].filter((el) => {
2207
- const owner = el.parentElement?.closest("[data-keytip-level]") ?? null;
2930
+ const tipsAt = (container) => deepTips(container ?? scope).filter((el) => {
2931
+ const owner = (hostOf.get(el) ?? el.parentElement)?.closest("[data-keytip-level]") ?? null;
2208
2932
  return owner === container && visible(el);
2209
2933
  });
2210
2934
  const current = () => tipsAt(levels.at(-1) ?? null);
@@ -2333,10 +3057,15 @@ var CONTROL_DEFINERS = [
2333
3057
  defineZoomSlider,
2334
3058
  defineTabStrip,
2335
3059
  defineOptionsDialog,
2336
- defineAccount
3060
+ defineAccount,
3061
+ defineRibbon,
3062
+ defineFindBar,
3063
+ defineRuler,
3064
+ defineBackstage,
3065
+ definePrintPreview
2337
3066
  ];
2338
3067
  function registerControls(registry) {
2339
3068
  for (const define of CONTROL_DEFINERS) define(registry);
2340
3069
  }
2341
3070
 
2342
- export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, attachKeyTips, clampOptionNumber, clearOfficeProfile, defineAccount, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, profileInitials, readOfficeProfile, registerControls, sanitizeOfficeProfile, writeOfficeProfile };
3071
+ export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, attachKeyTips, clampOptionNumber, clearOfficeProfile, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineFindBar, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePrintPreview, defineRibbon, defineRibbonGroup, defineRibbonStack, defineRuler, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, profileInitials, readOfficeProfile, registerControls, rulerDivisions, sanitizeOfficeProfile, writeOfficeProfile };
@@ -1,3 +1,8 @@
1
+ import { defineBackstage } from './backstage.js';
2
+ import { defineFindBar } from './find-bar.js';
3
+ import { definePrintPreview } from './print-preview.js';
4
+ import { defineRibbon } from './ribbon-tabs.js';
5
+ import { defineRuler } from './ruler.js';
1
6
  import { defineAccount } from './account.js';
2
7
  import { defineButton } from './button.js';
3
8
  import { defineCheckbox, defineSwitch } from './checkable.js';
@@ -12,7 +17,7 @@ import { defineSelect } from './select.js';
12
17
  import { defineStatusBar, defineStatusItem } from './status-bar.js';
13
18
  import { defineTabStrip } from './tab-strip.js';
14
19
  import { defineZoomSlider } from './zoom-slider.js';
15
- export { defineAccount, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineIcon, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, };
20
+ export { defineAccount, defineBackstage, defineFindBar, definePrintPreview, defineRibbon, defineRuler, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineIcon, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, };
16
21
  export type { OfficeCommandEvent } from './button.js';
17
22
  export type { OfficeDialogCloseEvent, OfficeDialogCloseReason } from './dialog.js';
18
23
  export type { OfficeSelectOption } from './select.js';
@@ -20,9 +25,14 @@ export type { OfficeStatusActivateEvent } from './status-bar.js';
20
25
  export type { OfficeTab, OfficeTabSelectEvent } from './tab-strip.js';
21
26
  export type { OfficeSearchCommand } from './command-search.js';
22
27
  export { attachKeyTips, type KeyTipsHandle } from './keytips.js';
28
+ export type { OfficeBackstageCloseEvent, OfficeBackstageItem, OfficeBackstageSelectEvent, } from './backstage.js';
29
+ export type { OfficeFindInputEvent, OfficeFindStepEvent } from './find-bar.js';
30
+ export type { OfficePrintPreviewPageEvent } from './print-preview.js';
31
+ export type { OfficeRibbonSelectEvent } from './ribbon-tabs.js';
32
+ export { rulerDivisions } from './ruler.js';
23
33
  export { clearOfficeProfile, DEFAULT_OFFICE_PROFILE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, profileInitials, readOfficeProfile, sanitizeOfficeProfile, writeOfficeProfile, type OfficeProfile, type OfficeProfileChangeEvent, } from './account.js';
24
34
  export { clampOptionNumber, type OfficeOptionCategory, type OfficeOptionChoice, type OfficeOptionControl, type OfficeOptionSection, type OfficeOptionsChangeEvent, type OfficeOptionValue, type OfficeOptionValues, } from './options.js';
25
35
  /** Every control of this entry. */
26
- export declare const CONTROL_DEFINERS: readonly [import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer];
36
+ export declare const CONTROL_DEFINERS: readonly [import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer];
27
37
  /** Idempotent, SSR-safe (no-op without a DOM). */
28
38
  export declare function registerControls(registry?: CustomElementRegistry): void;
package/dist/controls.js CHANGED
@@ -1,4 +1,4 @@
1
- export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, attachKeyTips, clampOptionNumber, clearOfficeProfile, defineAccount, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, profileInitials, readOfficeProfile, registerControls, sanitizeOfficeProfile, writeOfficeProfile } from './chunk-TMSFX6TC.js';
1
+ export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, attachKeyTips, clampOptionNumber, clearOfficeProfile, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineFindBar, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePrintPreview, defineRibbon, defineRibbonGroup, defineRibbonStack, defineRuler, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, profileInitials, readOfficeProfile, registerControls, rulerDivisions, sanitizeOfficeProfile, writeOfficeProfile } from './chunk-27XQVGRI.js';
2
2
  export { defineIcon } from './chunk-PCSUQPMC.js';
3
3
  import './chunk-RLPL7Q6J.js';
4
4
  import './chunk-EY266IK3.js';
@@ -0,0 +1,15 @@
1
+ export type OfficeFindInputEvent = CustomEvent<{
2
+ query: string;
3
+ }>;
4
+ export type OfficeFindStepEvent = CustomEvent<{
5
+ direction: 'next' | 'previous';
6
+ }>;
7
+ /**
8
+ * `<office-ui-find-bar>`: Office's on-demand Find bar under the ribbon. Shown while `open`;
9
+ * `show()` opens it and selects the query. Enter steps to the next result, Shift+Enter to the
10
+ * previous; Escape clears a query, or closes the bar when it is already empty. Attributes:
11
+ * `open`, `label`, `input-label`, `placeholder`, `maxlength`, `disabled`, `navigation-disabled`,
12
+ * `previous-label`, `next-label`, `close-label`. Properties: `value`, `status`, `statusTitle`.
13
+ * Events: `office-find-input` `{ query }`, `office-find-step` `{ direction }`, `office-find-close`.
14
+ */
15
+ export declare const defineFindBar: import("./registry.js").Definer;
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export * from './smartart.js';
5
5
  export * from './theme.js';
6
6
  export { CONTRACT_REVISION, type Definer } from './registry.js';
7
7
  /** Every ooxml-ui tag this package defines, in registration order. */
8
- export declare const OFFICE_UI_TAGS: readonly ["office-ui-icon", "office-ui-button", "office-ui-checkbox", "office-ui-switch", "office-ui-select", "office-ui-ribbon-group", "office-ui-ribbon-stack", "office-ui-menu-item", "office-ui-menu-button", "office-ui-menu-separator", "office-ui-context-menu", "office-ui-command-search", "office-ui-toolbar", "office-ui-dialog", "office-ui-status-bar", "office-ui-status-item", "office-ui-zoom-slider", "office-ui-tab-strip", "office-ui-options-dialog", "office-ui-account", "office-ui-presence", "office-ui-smartart"];
8
+ export declare const OFFICE_UI_TAGS: readonly ["office-ui-icon", "office-ui-button", "office-ui-checkbox", "office-ui-switch", "office-ui-select", "office-ui-ribbon-group", "office-ui-ribbon-stack", "office-ui-menu-item", "office-ui-menu-button", "office-ui-menu-separator", "office-ui-context-menu", "office-ui-command-search", "office-ui-toolbar", "office-ui-dialog", "office-ui-status-bar", "office-ui-status-item", "office-ui-zoom-slider", "office-ui-tab-strip", "office-ui-options-dialog", "office-ui-account", "office-ui-ribbon", "office-ui-find-bar", "office-ui-ruler", "office-ui-backstage", "office-ui-print-preview", "office-ui-presence", "office-ui-smartart"];
9
9
  /**
10
10
  * Install the theme tokens and define every element. Idempotent and safe to call from every
11
11
  * viewer binding; a no-op where there is no DOM (SSR).
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { installOfficeUiTheme } from './chunk-H4KFVIGT.js';
2
2
  export { THEME_CSS, THEME_STYLE_ID, installOfficeUiTheme } from './chunk-H4KFVIGT.js';
3
- import { registerControls } from './chunk-TMSFX6TC.js';
4
- export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, attachKeyTips, clampOptionNumber, clearOfficeProfile, defineAccount, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, profileInitials, readOfficeProfile, registerControls, sanitizeOfficeProfile, writeOfficeProfile } from './chunk-TMSFX6TC.js';
3
+ import { registerControls } from './chunk-27XQVGRI.js';
4
+ export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, attachKeyTips, clampOptionNumber, clearOfficeProfile, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineFindBar, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePrintPreview, defineRibbon, defineRibbonGroup, defineRibbonStack, defineRuler, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, profileInitials, readOfficeProfile, registerControls, rulerDivisions, sanitizeOfficeProfile, writeOfficeProfile } from './chunk-27XQVGRI.js';
5
5
  export { ICON_CSS, createIconSvg, defineIcon, getIcon, listIcons, paintIcon, registerIcon } from './chunk-PCSUQPMC.js';
6
6
  import { definePresence } from './chunk-RLPL7Q6J.js';
7
7
  export { definePresence, initialsOf, participantsFromAwareness } from './chunk-RLPL7Q6J.js';
@@ -31,6 +31,11 @@ var OFFICE_UI_TAGS = [
31
31
  "office-ui-tab-strip",
32
32
  "office-ui-options-dialog",
33
33
  "office-ui-account",
34
+ "office-ui-ribbon",
35
+ "office-ui-find-bar",
36
+ "office-ui-ruler",
37
+ "office-ui-backstage",
38
+ "office-ui-print-preview",
34
39
  "office-ui-presence",
35
40
  "office-ui-smartart"
36
41
  ];
@@ -0,0 +1,11 @@
1
+ export type OfficePrintPreviewPageEvent = CustomEvent<{
2
+ index: number;
3
+ }>;
4
+ /**
5
+ * `<office-ui-print-preview>`: the Print page's paper preview with page navigation. Set `pages`
6
+ * to rendered nodes (for example the SVG of each page); the element shows a deep clone of the
7
+ * current one with `tabindex` and inline `style` removed, so the preview is inert and never
8
+ * parses markup. `index` selects the page; the arrows emit `office-print-preview-page`.
9
+ * Attributes: `label` (default "Print preview"), `empty-label`.
10
+ */
11
+ export declare const definePrintPreview: import("./registry.js").Definer;
@@ -0,0 +1,14 @@
1
+ /** Emitted when the user picks a tab; cancelable (`preventDefault()` keeps the current tab). */
2
+ export type OfficeRibbonSelectEvent = CustomEvent<{
3
+ tab: string;
4
+ }>;
5
+ /**
6
+ * `<office-ui-ribbon>`: the Office tab row (Quick Access Toolbar, File, tabs, search) over the
7
+ * tab panels. Panels are light-DOM children carrying `data-ribbon-tab` (id), `data-label` and
8
+ * optionally `data-tab-keytip`; the element builds the tabs from them, keeps exactly one panel
9
+ * visible and moves between tabs with the arrow keys, Home and End. Slots: `quick-access`,
10
+ * `search`, `end`. Attributes: `selected`, `label` (tab list name), `file-label` (default "File"),
11
+ * `no-file`, `file-expanded`, `file-keytip`. Events: `office-ribbon-select` `{ tab }`
12
+ * (cancelable) and `office-ribbon-file` when File is activated.
13
+ */
14
+ export declare const defineRibbon: import("./registry.js").Definer;
@@ -0,0 +1,10 @@
1
+ /** Tick subdivisions per unit for a given unit length in CSS pixels, finer as it grows. */
2
+ export declare function rulerDivisions(unitPixels: number): number;
3
+ /**
4
+ * `<office-ui-ruler>`: a decorative Office ruler (inches by default). The product positions it
5
+ * and says where zero is: `origin` is the CSS-pixel offset of zero from the ruler's start edge,
6
+ * `scale` the CSS pixels per unit (96 at 100% for inches). `orientation="vertical"` draws a
7
+ * side ruler; `direction="reverse"` makes values grow toward the start edge, as Visio's
8
+ * vertical ruler grows upward from the page bottom. Hidden from assistive technology.
9
+ */
10
+ export declare const defineRuler: import("./registry.js").Definer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ooxml-ui",
3
- "version": "0.9.1",
3
+ "version": "0.10.1",
4
4
  "description": "Shared, format-neutral web components and styles for the Office viewers (Word, PowerPoint, Excel)",
5
5
  "keywords": [
6
6
  "custom-elements",
@@ -70,7 +70,7 @@
70
70
  "test:package": "node scripts/package-smoke.mjs"
71
71
  },
72
72
  "dependencies": {
73
- "ooxml-core": "^0.14.0"
73
+ "ooxml-core": "^0.14.1"
74
74
  },
75
75
  "devDependencies": {
76
76
  "@types/jsdom": "^21.1.7",