ooxml-ui 0.9.0 → 0.10.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/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,716 @@ 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 at = tabs.findIndex((tab) => tab.dataset.tab === this.selected);
598
+ const step = { ArrowRight: 1, ArrowLeft: -1 }[event.key];
599
+ const next = event.key === "Home" ? tabs[0] : event.key === "End" ? tabs.at(-1) : step ? tabs[(at + step + tabs.length) % tabs.length] : void 0;
600
+ if (!next) return;
601
+ event.preventDefault();
602
+ this.#choose(next.dataset.tab);
603
+ this.focusTab();
604
+ }
605
+ }
606
+ return OfficeUiRibbon;
607
+ });
608
+
609
+ // src/ruler.ts
610
+ var CSS5 = `
611
+ :host { display: block; box-sizing: border-box; background: var(--office-background, #fff);
612
+ color: var(--office-muted-foreground, #605e5c); overflow: hidden; }
613
+ :host(:not([orientation="vertical"])) { height: 18px; border-bottom: 1px solid var(--office-border, #d1d5db); }
614
+ :host([orientation="vertical"]) { width: 18px; border-inline-end: 1px solid var(--office-border, #d1d5db); }
615
+ canvas { display: block; width: 100%; height: 100%; }
616
+ `;
617
+ function rulerDivisions(unitPixels) {
618
+ return unitPixels >= 192 ? 16 : unitPixels >= 96 ? 8 : unitPixels >= 48 ? 4 : 2;
619
+ }
620
+ var defineRuler = definer("office-ui-ruler", () => {
621
+ class OfficeUiRuler extends HTMLElement {
622
+ static observedAttributes = ["orientation", "origin", "scale", "direction"];
623
+ #canvas;
624
+ #frame = 0;
625
+ #observer;
626
+ constructor() {
627
+ super();
628
+ const root = this.attachShadow({ mode: "open" });
629
+ attachStyles(root, controlCss(CSS5));
630
+ this.#canvas = this.ownerDocument.createElement("canvas");
631
+ root.append(this.#canvas);
632
+ }
633
+ connectedCallback() {
634
+ this.setAttribute("aria-hidden", "true");
635
+ const Observer = this.ownerDocument.defaultView?.ResizeObserver;
636
+ this.#observer = Observer ? new Observer(() => this.#schedule()) : void 0;
637
+ this.#observer?.observe(this);
638
+ this.#schedule();
639
+ }
640
+ disconnectedCallback() {
641
+ this.#observer?.disconnect();
642
+ if (this.#frame) this.ownerDocument.defaultView?.cancelAnimationFrame?.(this.#frame);
643
+ this.#frame = 0;
644
+ }
645
+ attributeChangedCallback() {
646
+ this.#schedule();
647
+ }
648
+ #number(name, fallback) {
649
+ const value = Number(this.getAttribute(name));
650
+ return this.hasAttribute(name) && Number.isFinite(value) ? value : fallback;
651
+ }
652
+ #schedule() {
653
+ if (this.#frame || !this.isConnected) return;
654
+ const view = this.ownerDocument.defaultView;
655
+ this.#frame = view?.requestAnimationFrame?.(() => this.#draw()) ?? 0;
656
+ if (!this.#frame) this.#draw();
657
+ }
658
+ #draw() {
659
+ this.#frame = 0;
660
+ const view = this.ownerDocument.defaultView;
661
+ const vertical = this.getAttribute("orientation") === "vertical";
662
+ const reverse = this.getAttribute("direction") === "reverse";
663
+ const box = this.getBoundingClientRect();
664
+ const length = vertical ? box.height : box.width;
665
+ const thickness = (vertical ? box.width : box.height) || 18;
666
+ const ratio = view?.devicePixelRatio ?? 1;
667
+ const canvas = this.#canvas;
668
+ canvas.width = Math.max(1, Math.round((vertical ? thickness : length) * ratio));
669
+ canvas.height = Math.max(1, Math.round((vertical ? length : thickness) * ratio));
670
+ const context = canvas.getContext?.("2d");
671
+ if (!context || !length) return;
672
+ context.scale(ratio, ratio);
673
+ const ink = view?.getComputedStyle(this).color || "#605e5c";
674
+ context.strokeStyle = ink;
675
+ context.fillStyle = ink;
676
+ context.lineWidth = 1;
677
+ context.font = `9px ${view?.getComputedStyle(this).fontFamily || "system-ui, sans-serif"}`;
678
+ const origin = this.#number("origin", 0);
679
+ const unit = Math.max(1, this.#number("scale", 96));
680
+ const divisions = rulerDivisions(unit);
681
+ const step = unit / divisions;
682
+ const sign = reverse ? -1 : 1;
683
+ const a = (0 - origin) / (sign * step);
684
+ const b = (length - origin) / (sign * step);
685
+ const first = Math.floor(Math.min(a, b)) - 1;
686
+ const last = Math.ceil(Math.max(a, b)) + 1;
687
+ context.beginPath();
688
+ for (let index = first; index <= last; index++) {
689
+ const at = Math.round(origin + sign * index * step) + 0.5;
690
+ const tick = index % divisions === 0 ? thickness : index % (divisions / 2) === 0 ? thickness * 0.5 : thickness * 0.3;
691
+ if (vertical) {
692
+ context.moveTo(thickness, at);
693
+ context.lineTo(thickness - tick, at);
694
+ } else {
695
+ context.moveTo(at, thickness);
696
+ context.lineTo(at, thickness - tick);
697
+ }
698
+ if (index % divisions !== 0) continue;
699
+ const label = String(index / divisions);
700
+ if (!vertical) context.fillText(label, at + 2, 9);
701
+ else {
702
+ context.save();
703
+ context.translate(9, at - 2);
704
+ context.rotate(-Math.PI / 2);
705
+ context.fillText(label, 0, 0);
706
+ context.restore();
707
+ }
708
+ }
709
+ context.stroke();
710
+ }
711
+ }
712
+ return OfficeUiRuler;
713
+ });
714
+
5
715
  // src/account.ts
6
716
  var DEFAULT_OFFICE_PROFILE = { displayName: "", avatarColor: "#6366f1" };
7
717
  var OFFICE_AVATAR_SWATCHES = [
@@ -52,7 +762,7 @@ function clearOfficeProfile(storage = defaultStorage(), key = OFFICE_PROFILE_STO
52
762
  } catch {
53
763
  }
54
764
  }
55
- var CSS = `
765
+ var CSS6 = `
56
766
  :host { display: block; font-family: var(--office-font, system-ui, sans-serif); font-size: 13px; }
57
767
  h2 { margin: 0 0 12px; font-size: 15px; font-weight: 600; }
58
768
  .who { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
@@ -82,7 +792,7 @@ var defineAccount = definer("office-ui-account", () => {
82
792
  super();
83
793
  const doc = this.ownerDocument;
84
794
  const root = this.attachShadow({ mode: "open" });
85
- attachStyles(root, controlCss(CSS));
795
+ attachStyles(root, controlCss(CSS6));
86
796
  const heading = doc.createElement("h2");
87
797
  heading.textContent = "User Information";
88
798
  const who = doc.createElement("div");
@@ -183,7 +893,7 @@ var defineAccount = definer("office-ui-account", () => {
183
893
  });
184
894
 
185
895
  // src/button.ts
186
- var CSS2 = `
896
+ var CSS7 = `
187
897
  :host { display: inline-flex; vertical-align: middle; }
188
898
  button { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center;
189
899
  gap: 6px; min-width: var(--office-target-size, 28px); min-height: var(--office-target-size, 28px);
@@ -224,7 +934,7 @@ var defineButton = definer("office-ui-button", () => {
224
934
  constructor() {
225
935
  super();
226
936
  const root = this.attachShadow({ mode: "open", delegatesFocus: true });
227
- attachStyles(root, controlCss(CSS2));
937
+ attachStyles(root, controlCss(CSS7));
228
938
  this.button = this.ownerDocument.createElement("button");
229
939
  this.button.type = "button";
230
940
  this.button.setAttribute("part", "button");
@@ -399,7 +1109,7 @@ var defineCheckbox = definer(
399
1109
  var defineSwitch = definer("office-ui-switch", makeCheckable("switch", SWITCH_CSS));
400
1110
 
401
1111
  // src/dialog.ts
402
- var CSS3 = `
1112
+ var CSS8 = `
403
1113
  :host { display: none; position: fixed; inset: 0; z-index: 1000; align-items: center; justify-content: center; }
404
1114
  :host([open]) { display: flex; }
405
1115
  .backdrop { position: absolute; inset: 0; background: rgb(0 0 0 / .4); }
@@ -432,7 +1142,7 @@ var defineDialog = definer("office-ui-dialog", () => {
432
1142
  super();
433
1143
  const doc = this.ownerDocument;
434
1144
  const root = this.attachShadow({ mode: "open" });
435
- attachStyles(root, controlCss(CSS3));
1145
+ attachStyles(root, controlCss(CSS8));
436
1146
  const backdrop = doc.createElement("div");
437
1147
  backdrop.className = "backdrop";
438
1148
  this.box = doc.createElement("div");
@@ -535,7 +1245,7 @@ var defineDialog = definer("office-ui-dialog", () => {
535
1245
  });
536
1246
 
537
1247
  // src/command-search.ts
538
- var CSS4 = `
1248
+ var CSS9 = `
539
1249
  :host { display: inline-flex; align-items: center; min-width: 0; font-family: var(--office-font, system-ui, sans-serif); }
540
1250
  .field { display: flex; align-items: center; gap: 6px; width: 100%; min-width: 0; height: var(--office-target-size, 28px);
541
1251
  padding: 0 8px; box-sizing: border-box; border: 1px solid transparent; border-radius: var(--office-radius, 4px);
@@ -575,7 +1285,7 @@ var defineCommandSearch = definer("office-ui-command-search", () => {
575
1285
  super();
576
1286
  const doc = this.ownerDocument;
577
1287
  const root = this.attachShadow({ mode: "open", delegatesFocus: true });
578
- attachStyles(root, controlCss(CSS4));
1288
+ attachStyles(root, controlCss(CSS9));
579
1289
  const field = doc.createElement("div");
580
1290
  field.className = "field";
581
1291
  const glass = createIconSvg(doc);
@@ -732,7 +1442,7 @@ var defineCommandSearch = definer("office-ui-command-search", () => {
732
1442
  });
733
1443
 
734
1444
  // src/context-menu.ts
735
- var CSS5 = `
1445
+ var CSS10 = `
736
1446
  :host { display: contents; }
737
1447
  .panel { position: fixed; inset: auto; margin: 0; min-width: 200px; padding: 4px; box-sizing: border-box;
738
1448
  background: var(--office-background, #fff); color: var(--office-foreground, #1f2937);
@@ -768,7 +1478,7 @@ var defineContextMenu = definer("office-ui-context-menu", () => {
768
1478
  super();
769
1479
  const doc = this.ownerDocument;
770
1480
  const root = this.attachShadow({ mode: "open" });
771
- attachStyles(root, controlCss(CSS5));
1481
+ attachStyles(root, controlCss(CSS10));
772
1482
  this.panel = doc.createElement("div");
773
1483
  this.panel.className = "panel";
774
1484
  this.panel.setAttribute("role", "menu");
@@ -1178,7 +1888,7 @@ function clampOptionNumber(raw, min, max) {
1178
1888
  const parsed = Number(raw);
1179
1889
  return Number.isFinite(parsed) && raw.trim() !== "" ? Math.min(max, Math.max(min, parsed)) : void 0;
1180
1890
  }
1181
- var CSS6 = `
1891
+ var CSS11 = `
1182
1892
  :host { display: contents; }
1183
1893
  office-ui-dialog { --office-dialog-width: min(820px, 100vw); --office-dialog-max-width: min(820px, 100vw); }
1184
1894
  .layout { display: grid; grid-template-columns: 180px minmax(0, 1fr); gap: 0; min-height: min(440px, 70vh); margin: -16px; }
@@ -1227,7 +1937,7 @@ var defineOptionsDialog = definer("office-ui-options-dialog", () => {
1227
1937
  super();
1228
1938
  const doc = this.ownerDocument;
1229
1939
  const root = this.attachShadow({ mode: "open" });
1230
- attachStyles(root, controlCss(CSS6));
1940
+ attachStyles(root, controlCss(CSS11));
1231
1941
  this.#dialog = doc.createElement("office-ui-dialog");
1232
1942
  const layout = doc.createElement("div");
1233
1943
  layout.className = "layout";
@@ -1572,7 +2282,7 @@ var defineToolbar = definer("office-ui-toolbar", () => {
1572
2282
  });
1573
2283
 
1574
2284
  // src/select.ts
1575
- var CSS7 = `
2285
+ var CSS12 = `
1576
2286
  :host { display: inline-block; position: relative; vertical-align: middle; }
1577
2287
  button { box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: 8px;
1578
2288
  width: 100%; min-height: var(--office-target-size, 28px); padding: 0 8px; font: inherit; font-size: 12px;
@@ -1611,7 +2321,7 @@ var defineSelect = definer("office-ui-select", () => {
1611
2321
  super();
1612
2322
  const doc = this.ownerDocument;
1613
2323
  const root = this.attachShadow({ mode: "open", delegatesFocus: true });
1614
- attachStyles(root, controlCss(CSS7));
2324
+ attachStyles(root, controlCss(CSS12));
1615
2325
  this.trigger = doc.createElement("button");
1616
2326
  this.trigger.type = "button";
1617
2327
  this.trigger.setAttribute("role", "combobox");
@@ -1862,7 +2572,7 @@ var defineStatusItem = definer("office-ui-status-item", () => {
1862
2572
  });
1863
2573
 
1864
2574
  // src/tab-strip.ts
1865
- var CSS8 = `
2575
+ var CSS13 = `
1866
2576
  :host { display: flex; align-items: stretch; min-width: 0; min-height: 28px; gap: 2px; padding: 0 6px;
1867
2577
  background: var(--office-surface, #f3f4f6); color: var(--office-muted-foreground, #6b7280);
1868
2578
  border-top: 1px solid var(--office-border, #d1d5db); font-size: 12px;
@@ -1909,7 +2619,7 @@ var defineTabStrip = definer("office-ui-tab-strip", () => {
1909
2619
  super();
1910
2620
  const doc = this.ownerDocument;
1911
2621
  const root = this.attachShadow({ mode: "open" });
1912
- attachStyles(root, controlCss(CSS8));
2622
+ attachStyles(root, controlCss(CSS13));
1913
2623
  const step = (name, icon) => {
1914
2624
  const button = doc.createElement("button");
1915
2625
  button.type = "button";
@@ -2048,7 +2758,7 @@ var defineTabStrip = definer("office-ui-tab-strip", () => {
2048
2758
  });
2049
2759
 
2050
2760
  // src/zoom-slider.ts
2051
- var CSS9 = `
2761
+ var CSS14 = `
2052
2762
  :host { display: inline-flex; align-items: center; gap: 2px; font-size: 11px;
2053
2763
  color: var(--office-foreground, #1f2937); font-family: var(--office-font, system-ui, sans-serif); }
2054
2764
  button { box-sizing: border-box; display: inline-grid; place-items: center; min-width: var(--office-target-size, 24px);
@@ -2084,7 +2794,7 @@ var defineZoomSlider = definer("office-ui-zoom-slider", () => {
2084
2794
  super();
2085
2795
  const doc = this.ownerDocument;
2086
2796
  const root = this.attachShadow({ mode: "open" });
2087
- attachStyles(root, controlCss(CSS9));
2797
+ attachStyles(root, controlCss(CSS14));
2088
2798
  const button = (name, text2) => {
2089
2799
  const element = doc.createElement("button");
2090
2800
  element.type = "button";
@@ -2186,6 +2896,19 @@ var visible = (el) => {
2186
2896
  const box = el.getBoundingClientRect();
2187
2897
  return box.width > 0 && box.height > 0;
2188
2898
  };
2899
+ function deepTips(root) {
2900
+ const found = [...root.querySelectorAll("[data-keytip]")];
2901
+ for (const host of root.querySelectorAll("*")) {
2902
+ const shadow = host.shadowRoot;
2903
+ if (!shadow) continue;
2904
+ const inner = deepTips(shadow);
2905
+ if (!inner.length) continue;
2906
+ for (const el of inner) hostOf.set(el, host);
2907
+ found.push(...inner);
2908
+ }
2909
+ return found;
2910
+ }
2911
+ var hostOf = /* @__PURE__ */ new WeakMap();
2189
2912
  function activate(el) {
2190
2913
  const inner = el.shadowRoot?.querySelector(".main, button");
2191
2914
  (inner ?? el).click();
@@ -2203,8 +2926,8 @@ function attachKeyTips(scope) {
2203
2926
  let levels = [];
2204
2927
  let typed = "";
2205
2928
  let altAlone = false;
2206
- const tipsAt = (container) => [...(container ?? scope).querySelectorAll("[data-keytip]")].filter((el) => {
2207
- const owner = el.parentElement?.closest("[data-keytip-level]") ?? null;
2929
+ const tipsAt = (container) => deepTips(container ?? scope).filter((el) => {
2930
+ const owner = (hostOf.get(el) ?? el.parentElement)?.closest("[data-keytip-level]") ?? null;
2208
2931
  return owner === container && visible(el);
2209
2932
  });
2210
2933
  const current = () => tipsAt(levels.at(-1) ?? null);
@@ -2333,10 +3056,15 @@ var CONTROL_DEFINERS = [
2333
3056
  defineZoomSlider,
2334
3057
  defineTabStrip,
2335
3058
  defineOptionsDialog,
2336
- defineAccount
3059
+ defineAccount,
3060
+ defineRibbon,
3061
+ defineFindBar,
3062
+ defineRuler,
3063
+ defineBackstage,
3064
+ definePrintPreview
2337
3065
  ];
2338
3066
  function registerControls(registry) {
2339
3067
  for (const define of CONTROL_DEFINERS) define(registry);
2340
3068
  }
2341
3069
 
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 };
3070
+ 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-5YR7YA57.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-5YR7YA57.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-5YR7YA57.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.0",
3
+ "version": "0.10.0",
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.13.0"
73
+ "ooxml-core": "^0.14.1"
74
74
  },
75
75
  "devDependencies": {
76
76
  "@types/jsdom": "^21.1.7",