ooxml-ui 0.1.12 → 0.3.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
@@ -43,14 +43,14 @@ document.body.append(button);
43
43
 
44
44
  ## API: entries (ESM only)
45
45
 
46
- | Entry | Contents |
47
- | ------------------- | --------------------------------------------------------------------------------------- |
48
- | `ooxml-ui` | everything below plus `registerOfficeUi()` and `OFFICE_UI_TAGS` |
49
- | `ooxml-ui/theme` | `THEME_CSS`, `installOfficeUiTheme()` (tokens, dark mode, forced-colors, touch targets) |
50
- | `ooxml-ui/icons` | icon registry (`registerIcon`, `getIcon`, `listIcons`) and `<office-ui-icon>` |
51
- | `ooxml-ui/controls` | button, checkbox, switch, select, ribbon group, toolbar, dialog, status bar and item |
52
- | `ooxml-ui/presence` | `<office-ui-presence>` avatar stack for collaboration awareness |
53
- | `ooxml-ui/smartart` | `<office-ui-smartart>`: draws a core `DiagramDrawing` as SVG |
46
+ | Entry | Contents |
47
+ | ------------------- | ------------------------------------------------------------------------------------------------------------ |
48
+ | `ooxml-ui` | everything below plus `registerOfficeUi()` and `OFFICE_UI_TAGS` |
49
+ | `ooxml-ui/theme` | `THEME_CSS`, `installOfficeUiTheme()` (tokens, dark mode, forced-colors, touch targets) |
50
+ | `ooxml-ui/icons` | icon registry (`registerIcon`, `getIcon`, `listIcons`) and `<office-ui-icon>` |
51
+ | `ooxml-ui/controls` | button, checkbox, switch, select, ribbon group, toolbar, dialog, status bar and item, zoom slider, tab strip |
52
+ | `ooxml-ui/presence` | `<office-ui-presence>` avatar stack for collaboration awareness |
53
+ | `ooxml-ui/smartart` | `<office-ui-smartart>`: draws a core `DiagramDrawing` as SVG |
54
54
 
55
55
  Importing an entry never touches the DOM (SSR-safe). Elements are defined by the `define*` /
56
56
  `register*` functions, which are idempotent, no-ops without a `window`, and refuse to coexist
@@ -58,18 +58,22 @@ with an incompatible build of this package.
58
58
 
59
59
  ## Features: elements
60
60
 
61
- | Tag | Attributes / properties | Events (all bubble, composed) |
62
- | ---------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------- |
63
- | `office-ui-button` | `label`, `icon`, `command`, `disabled`, `pressed`, `expanded`, `icon-only`, `variant="stacked"` | `office-command` `{ command }` |
64
- | `office-ui-checkbox`, `office-ui-switch` | `checked`, `disabled`, `value`, `aria-label`; form-associated | `input`, `change` |
65
- | `office-ui-select` | `options` `[{ value, label, disabled? }]`, `value`, `selectedIndex`, `disabled` | `input`, `change` |
66
- | `office-ui-ribbon-group` | `label`; default slot | none |
67
- | `office-ui-toolbar` | `aria-label`, `aria-orientation`; arrow-key focus movement | none |
68
- | `office-ui-dialog` | `open`, `heading`, `dismissible="false"`, `show()`, `close()`; slots default, `footer` | `office-dialog-close` `{ reason }` (cancelable) |
69
- | `office-ui-status-bar` / `-status-item` | `label`, `value`, `interactive`, `id` | `office-status-activate` `{ id }` |
70
- | `office-ui-presence` | `participants`, `max`, `label` | `office-presence-select` `{ id }` |
71
- | `office-ui-smartart` | `drawing` (core `DiagramDrawing`), `schemeColors`, `label` | `office-smartart-render` (render report) |
72
- | `office-ui-icon` | `name`, `label` | none |
61
+ | Tag | Attributes / properties | Events (all bubble, composed) |
62
+ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
63
+ | `office-ui-button` | `label`, `icon`, `command`, `disabled`, `pressed`, `expanded`, `icon-only`, `variant="stacked"`, `keyshortcuts` | `office-command` `{ command }` |
64
+ | `office-ui-checkbox`, `office-ui-switch` | `checked`, `disabled`, `value`, `aria-label`; form-associated | `input`, `change` |
65
+ | `office-ui-select` | `options` `[{ value, label, disabled? }]`, `value`, `selectedIndex`, `disabled` | `input`, `change` |
66
+ | `office-ui-ribbon-group` | `label`, `launcher` (dialog launcher command), `launcher-disabled`; default slot | launcher: `office-command` `{ command }` |
67
+ | `office-ui-ribbon-stack` | `orientation="horizontal"`; default slot (three-row column of small commands by default) | none |
68
+ | `office-ui-menu-button` / `-menu-item` | button: `label`, `icon`, `command` (split button), `variant`, `disabled`, `keyshortcuts`; item: `command`, `label`, `icon`, `disabled`, `checked` | item choice: `office-command` `{ command }` |
69
+ | `office-ui-toolbar` | `aria-label`, `aria-orientation`; arrow-key focus movement | none |
70
+ | `office-ui-dialog` | `open`, `heading`, `dismissible="false"`, `show()`, `close()`; slots default, `footer` | `office-dialog-close` `{ reason }` (cancelable) |
71
+ | `office-ui-status-bar` / `-status-item` | `label`, `value`, `interactive`, `id` | `office-status-activate` `{ id }` |
72
+ | `office-ui-zoom-slider` | `value` (percent), `min`, `max`, `step`, `disabled`, `fit` (fit button name), `label` | `input`, `change`; fit: `office-command` `{ command: 'zoom-fit' }` |
73
+ | `office-ui-tab-strip` | `tabs` `[{ id, label, title? }]`, `selected`, `label`, `disabled` | `office-tab-select` `{ id }` (cancelable) |
74
+ | `office-ui-presence` | `participants`, `max`, `label` | `office-presence-select` `{ id }` |
75
+ | `office-ui-smartart` | `drawing` (core `DiagramDrawing`), `schemeColors`, `label` | `office-smartart-render` (render report) |
76
+ | `office-ui-icon` | `name`, `label` | none |
73
77
 
74
78
  Setting a property never emits an event; only user activation does. Controls honour
75
79
  `prefers-color-scheme`, `forced-colors: active` (system colours and a visible focus ring) and
package/dist/button.d.ts CHANGED
@@ -3,7 +3,8 @@ export type OfficeCommandEvent = CustomEvent<{
3
3
  }>;
4
4
  /**
5
5
  * Command button. Attributes: `label`, `icon`, `command`, `disabled`, `pressed` (aria-pressed),
6
- * `expanded` (aria-expanded), `icon-only`, `variant="stacked"`, `title`.
6
+ * `expanded` (aria-expanded), `icon-only`, `variant="stacked"`, `title`, `keyshortcuts`
7
+ * (forwarded as aria-keyshortcuts, e.g. `Control+Z`).
7
8
  * Activation (pointer, Enter, Space) emits one bubbling, composed `office-command`
8
9
  * event with `{ command }`; the host decides what it does. Nothing happens when disabled.
9
10
  */
@@ -18,7 +18,51 @@ var BUILT_IN = {
18
18
  settings: "M3 5h14M6 10h8M8 15h4M6 3v4M12 8v4M10 13v4",
19
19
  clock: "M3 10a7 7 0 1 0 14 0 7 7 0 1 0-14 0M10 6v4l3 2",
20
20
  reset: "M4 10a6 6 0 1 1 1.8 4.2M4 6v4h4",
21
- search: "M3 8.5a5.5 5.5 0 1 0 11 0 5.5 5.5 0 1 0-11 0M13 13l4 4"
21
+ search: "M3 8.5a5.5 5.5 0 1 0 11 0 5.5 5.5 0 1 0-11 0M13 13l4 4",
22
+ undo: "M7.5 11.5 3.5 7.5l4-4M3.5 7.5h9a4 4 0 0 1 0 8H10",
23
+ redo: "m12.5 11.5 4-4-4-4M16.5 7.5h-9a4 4 0 0 0 0 8H10",
24
+ pointer: "m5 2.5v13.5l3.6-3.6 2.4 5.1 2.1-1-2.4-5H15.5Z",
25
+ rectangle: "M3 5.5h14v9H3Z",
26
+ fullscreen: "M3 7.5V3h4.5M12.5 3H17v4.5M17 12.5V17h-4.5M7.5 17H3v-4.5",
27
+ grid: "M3 3h14v14H3ZM7.7 3v14M12.3 3v14M3 7.7h14M3 12.3h14",
28
+ fitPage: "M6.5 6.5h7v7h-7ZM2.5 6V2.5H6M14 2.5h3.5V6M17.5 14v3.5H14M6 17.5H2.5V14",
29
+ pageWidth: "M5.5 3.5h9v13h-9ZM1.5 10h17M4 8l-2.5 2L4 12M16 8l2.5 2-2.5 2",
30
+ zoomIn: "M3 8.5a5.5 5.5 0 1 0 11 0 5.5 5.5 0 1 0-11 0M13 13l4 4M6 8.5h5M8.5 6v5",
31
+ zoomOut: "M3 8.5a5.5 5.5 0 1 0 11 0 5.5 5.5 0 1 0-11 0M13 13l4 4M6 8.5h5",
32
+ cut: "M7 3l7 10M13 3 6 13M3 15.5a2 2 0 1 0 4 0 2 2 0 1 0-4 0M13 15.5a2 2 0 1 0 4 0 2 2 0 1 0-4 0",
33
+ paste: "M7 3.5h6v2.5H7ZM7 4.5H4.5v13h11v-13H13",
34
+ formatPainter: "M4 3h10v4H4ZM14 5h2.5v4.5H9.5V12M8.5 12h2v5h-2Z",
35
+ bold: "M6 3.5h4.5a3 3 0 0 1 0 6H6ZM6 9.5h5.5a3.5 3.5 0 0 1 0 7H6Z",
36
+ italic: "M9 3.5h6M5 16.5h6M12 3.5 8 16.5",
37
+ underline: "M6 3v6a4 4 0 0 0 8 0V3M5 17h10",
38
+ strikethrough: "M3.5 10h13M13.5 5.5C13 4.3 11.7 3.5 10 3.5c-2 0-3.5 1-3.5 2.6 0 1.4 1.2 2.2 3.5 2.8M6.5 13.8c.5 1.6 1.9 2.7 3.8 2.7 2 0 3.5-1.1 3.5-2.8",
39
+ fontColor: "M5.5 13 10 3l4.5 10M7.2 9.3h5.6M3 17h14",
40
+ growFont: "M2.5 16 6.5 6l4 10M4 12.3h5M13 8.5l2-3 2 3M15 5.5V12",
41
+ shrinkFont: "M2.5 16 6.5 6l4 10M4 12.3h5M13 9l2 3 2-3M15 12V5.5",
42
+ changeCase: "M2 15 5.5 5 9 15M3.2 11.7h4.6M15.5 9.5v5.5M15.5 12.2a2.3 2.3 0 1 1-4.6 0 2.3 2.3 0 1 1 4.6 0",
43
+ alignLeft: "M3 4.5h14M3 8.5h9M3 12.5h14M3 16.5h9",
44
+ alignCenter: "M3 4.5h14M5.5 8.5h9M3 12.5h14M5.5 16.5h9",
45
+ alignRight: "M3 4.5h14M8 8.5h9M3 12.5h14M8 16.5h9",
46
+ justify: "M3 4.5h14M3 8.5h14M3 12.5h14M3 16.5h14",
47
+ alignTop: "M3 3.5h14M6 7h8M6 10.5h8",
48
+ alignMiddle: "M3 10h2M15 10h2M6 6.5h8M6 13.5h8M6 10h8",
49
+ alignBottom: "M3 16.5h14M6 9.5h8M6 13h8",
50
+ bullets: "M8 5h9M8 10h9M8 15h9M3.5 5h1M3.5 10h1M3.5 15h1",
51
+ indentIncrease: "M3 4h14M9 8h8M9 12h8M3 16h14M3 7.5 6 10l-3 2.5",
52
+ indentDecrease: "M3 4h14M9 8h8M9 12h8M3 16h14M6 7.5 3 10l3 2.5",
53
+ fill: "M3.5 10 9 4.5l6 6L9.5 16ZM9 4.5 7 2.5M16.5 12.5s1.5 2 1.5 3a1.5 1.5 0 0 1-3 0c0-1 1.5-3 1.5-3",
54
+ line: "M3.5 16.5 16.5 3.5",
55
+ effects: "M10 2.5l1.9 4.6 4.6 1.9-4.6 1.9L10 15.5l-1.9-4.6L3.5 9l4.6-1.9Z",
56
+ quickStyles: "M10 3a7 7 0 1 0 0 14c1 0 1.5-.7 1.5-1.5S11 14 11 13s.8-1.5 2-1.5h2a2 2 0 0 0 2-2A7 7 0 0 0 10 3ZM6.5 9.5h.1M8.5 6.2h.1M12.5 6.5h.1",
57
+ bringToFront: "M7.5 7.5h9v9h-9ZM12.5 7.5v-4h-9v9h4",
58
+ sendToBack: "M3.5 3.5h9v9h-9ZM12.5 7.5h4v9h-9v-4",
59
+ group: "M3 3h5v5H3ZM12 12h5v5h-5ZM2 2h1M17 2h1M2 18h1M17 18h1",
60
+ alignObjects: "M3 2.5v15M6 5h8v4H6ZM6 11h11v4H6Z",
61
+ position: "M10 2.5v15M2.5 10h15M7.5 5 10 2.5 12.5 5M7.5 15l2.5 2.5 2.5-2.5M5 7.5 2.5 10 5 12.5M15 7.5l2.5 2.5-2.5 2.5",
62
+ connector: "M3.5 15.5h5v-11h8M14.5 2.5l2 2-2 2M2.5 13.5v4",
63
+ textBox: "M3 3h14v14H3ZM6.5 6.5h7M10 6.5v7",
64
+ ruler: "M2 7h16v6H2ZM5 7v3M8 7v2M11 7v3M14 7v2",
65
+ launcher: "M5 15 15 5M9 5h6v6"
22
66
  };
23
67
  for (const [name, d] of Object.entries(BUILT_IN)) registry.set(name, { d });
24
68
  function registerIcon(name, icon) {
@@ -1,4 +1,4 @@
1
- import { createIconSvg, paintIcon, defineIcon } from './chunk-7MCSBDSU.js';
1
+ import { createIconSvg, paintIcon, defineIcon } from './chunk-PCSUQPMC.js';
2
2
  import { definer, attachStyles, controlCss, emit } from './chunk-EY266IK3.js';
3
3
 
4
4
  // src/button.ts
@@ -34,7 +34,8 @@ var defineButton = definer("office-ui-button", () => {
34
34
  "pressed",
35
35
  "expanded",
36
36
  "icon-only",
37
- "title"
37
+ "title",
38
+ "keyshortcuts"
38
39
  ];
39
40
  button;
40
41
  svg;
@@ -84,6 +85,9 @@ var defineButton = definer("office-ui-button", () => {
84
85
  else this.button.removeAttribute("aria-label");
85
86
  this.button.title = this.getAttribute("title") ?? (iconOnly ? label : "");
86
87
  this.button.disabled = this.disabled;
88
+ const shortcuts = this.getAttribute("keyshortcuts");
89
+ if (shortcuts) this.button.setAttribute("aria-keyshortcuts", shortcuts);
90
+ else this.button.removeAttribute("aria-keyshortcuts");
87
91
  if (paintIcon(this.svg, this.getAttribute("icon"))) this.svg.setAttribute("data-painted", "");
88
92
  else this.svg.removeAttribute("data-painted");
89
93
  for (const attr of ["pressed", "expanded"]) {
@@ -349,6 +353,286 @@ var defineDialog = definer("office-ui-dialog", () => {
349
353
  return OfficeUiDialog;
350
354
  });
351
355
 
356
+ // src/menu.ts
357
+ var BUTTON_CSS = `
358
+ :host { display: inline-flex; vertical-align: middle; }
359
+ .wrap { display: inline-flex; align-items: stretch; border: 1px solid transparent; border-radius: var(--office-radius, 4px); }
360
+ :host([variant="stacked"]) .wrap { flex-direction: column; }
361
+ button { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 4px;
362
+ min-width: var(--office-target-size, 28px); min-height: var(--office-target-size, 28px); padding: 0 6px;
363
+ border: 0; border-radius: var(--office-radius, 4px); background: transparent; color: var(--office-foreground, #1f2937);
364
+ font: inherit; font-size: 12px; cursor: pointer; white-space: nowrap; }
365
+ :host([variant="stacked"]) .main { flex-direction: column; padding: 4px 6px 0; }
366
+ :host([variant="stacked"]) .caret { min-height: 16px; padding: 0 6px 2px; }
367
+ :host([variant="stacked"]:not([command])) .main { padding-bottom: 0; }
368
+ .wrap:hover:not(.disabled) { border-color: var(--office-border, #d1d5db); }
369
+ button:hover:not(:disabled) { background: var(--office-surface, #f3f4f6); }
370
+ button:focus-visible { outline: 2px solid var(--office-ring, #2563eb); outline-offset: -2px; }
371
+ button:disabled { opacity: .5; cursor: not-allowed; }
372
+ button[aria-expanded="true"] { background: var(--office-surface, #f3f4f6); }
373
+ .caret svg { width: 12px; height: 12px; }
374
+ svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.5;
375
+ stroke-linecap: round; stroke-linejoin: round; }
376
+ svg:not([data-painted]) { display: none; }
377
+ .panel { position: fixed; inset: auto; margin: 0; min-width: 180px; padding: 4px; box-sizing: border-box;
378
+ background: var(--office-background, #fff); color: var(--office-foreground, #1f2937);
379
+ border: 1px solid var(--office-border, #d1d5db); border-radius: var(--office-radius, 4px);
380
+ box-shadow: 0 8px 24px rgb(0 0 0 / 18%); }
381
+ .panel[hidden] { display: none; }
382
+ @media (forced-colors: active) {
383
+ button { color: ButtonText; } button:disabled { color: GrayText; }
384
+ .panel { border-color: CanvasText; background: Canvas; color: CanvasText; }
385
+ }
386
+ `;
387
+ var ITEM_CSS = `
388
+ :host { display: block; }
389
+ :host([hidden]) { display: none; }
390
+ button { box-sizing: border-box; display: flex; align-items: center; gap: 8px; width: 100%;
391
+ min-height: var(--office-target-size, 28px); padding: 0 10px 0 8px; border: 0; border-radius: 3px;
392
+ background: transparent; color: inherit; font: inherit; font-size: 12px; text-align: start; cursor: pointer; }
393
+ button:hover:not(:disabled), button:focus-visible { background: var(--office-surface, #f3f4f6); outline: none; }
394
+ button:disabled { opacity: .5; cursor: not-allowed; }
395
+ .check { width: 14px; text-align: center; }
396
+ svg { width: 16px; height: 16px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.5;
397
+ stroke-linecap: round; stroke-linejoin: round; }
398
+ svg:not([data-painted]) { visibility: hidden; }
399
+ @media (forced-colors: active) { button:focus-visible { background: Highlight; color: HighlightText; } }
400
+ `;
401
+ var ENABLED_ITEMS = "office-ui-menu-item:not([disabled]):not([hidden])";
402
+ var defineMenuItem = definer("office-ui-menu-item", () => {
403
+ class OfficeUiMenuItem extends HTMLElement {
404
+ static observedAttributes = ["label", "icon", "command", "disabled", "checked", "title"];
405
+ button;
406
+ svg;
407
+ text;
408
+ mark;
409
+ constructor() {
410
+ super();
411
+ const doc = this.ownerDocument;
412
+ const root = this.attachShadow({ mode: "open", delegatesFocus: true });
413
+ attachStyles(root, controlCss(ITEM_CSS));
414
+ this.button = doc.createElement("button");
415
+ this.button.type = "button";
416
+ this.button.tabIndex = -1;
417
+ this.mark = doc.createElement("span");
418
+ this.mark.className = "check";
419
+ this.mark.setAttribute("aria-hidden", "true");
420
+ this.svg = createIconSvg(doc);
421
+ this.text = doc.createElement("span");
422
+ this.button.append(this.mark, this.svg, this.text);
423
+ root.append(this.button);
424
+ this.button.addEventListener("click", () => {
425
+ const command = this.getAttribute("command");
426
+ if (!this.button.disabled && command) emit(this, "office-command", { command });
427
+ });
428
+ }
429
+ connectedCallback() {
430
+ this.sync();
431
+ }
432
+ attributeChangedCallback() {
433
+ this.sync();
434
+ }
435
+ get disabled() {
436
+ return this.hasAttribute("disabled");
437
+ }
438
+ set disabled(value) {
439
+ this.toggleAttribute("disabled", Boolean(value));
440
+ }
441
+ /** Focus the menuitem itself, so arrow navigation never depends on delegatesFocus. */
442
+ focus(options) {
443
+ this.button.focus(options);
444
+ }
445
+ sync() {
446
+ const checked = this.getAttribute("checked");
447
+ this.button.setAttribute("role", checked === null ? "menuitem" : "menuitemcheckbox");
448
+ if (checked === null) this.button.removeAttribute("aria-checked");
449
+ else this.button.setAttribute("aria-checked", String(checked !== "false"));
450
+ this.mark.textContent = checked !== null && checked !== "false" ? "\u2713" : "";
451
+ this.text.textContent = this.getAttribute("label") ?? "";
452
+ this.button.title = this.getAttribute("title") ?? "";
453
+ this.button.disabled = this.disabled;
454
+ if (paintIcon(this.svg, this.getAttribute("icon"))) this.svg.setAttribute("data-painted", "");
455
+ else this.svg.removeAttribute("data-painted");
456
+ }
457
+ }
458
+ return OfficeUiMenuItem;
459
+ });
460
+ var defineMenuButton = definer("office-ui-menu-button", () => {
461
+ class OfficeUiMenuButton extends HTMLElement {
462
+ static observedAttributes = ["label", "icon", "command", "disabled", "title", "keyshortcuts"];
463
+ wrap;
464
+ main;
465
+ caret;
466
+ svg;
467
+ text;
468
+ panel;
469
+ outside;
470
+ constructor() {
471
+ super();
472
+ const doc = this.ownerDocument;
473
+ const root = this.attachShadow({ mode: "open" });
474
+ attachStyles(root, controlCss(BUTTON_CSS));
475
+ this.wrap = doc.createElement("div");
476
+ this.wrap.className = "wrap";
477
+ this.main = doc.createElement("button");
478
+ this.main.type = "button";
479
+ this.main.className = "main";
480
+ this.main.setAttribute("part", "button");
481
+ this.svg = createIconSvg(doc);
482
+ this.text = doc.createElement("span");
483
+ this.main.append(this.svg, this.text);
484
+ this.caret = doc.createElement("button");
485
+ this.caret.type = "button";
486
+ this.caret.className = "caret";
487
+ this.caret.setAttribute("aria-haspopup", "menu");
488
+ this.caret.setAttribute("aria-expanded", "false");
489
+ const chevron = createIconSvg(doc);
490
+ paintIcon(chevron, "chevronDown");
491
+ chevron.setAttribute("data-painted", "");
492
+ this.caret.append(chevron);
493
+ this.panel = doc.createElement("div");
494
+ this.panel.className = "panel";
495
+ this.panel.setAttribute("role", "menu");
496
+ if (typeof this.panel.showPopover === "function")
497
+ this.panel.setAttribute("popover", "auto");
498
+ else this.panel.hidden = true;
499
+ this.panel.append(doc.createElement("slot"));
500
+ this.wrap.append(this.main, this.caret);
501
+ root.append(this.wrap, this.panel);
502
+ this.main.addEventListener("click", () => {
503
+ const command = this.getAttribute("command");
504
+ if (this.disabled) return;
505
+ if (command) emit(this, "office-command", { command });
506
+ else this.toggle();
507
+ });
508
+ this.caret.addEventListener("click", () => {
509
+ if (!this.disabled) this.toggle();
510
+ });
511
+ for (const trigger of [this.main, this.caret])
512
+ trigger.addEventListener("keydown", (event) => {
513
+ if (event.key === "ArrowDown" && !this.disabled) {
514
+ event.preventDefault();
515
+ this.show(0);
516
+ }
517
+ });
518
+ this.panel.addEventListener("toggle", (event) => {
519
+ if (event.newState === "closed") this.closed();
520
+ });
521
+ this.panel.addEventListener("keydown", (event) => this.onMenuKey(event));
522
+ this.addEventListener("office-command", (event) => {
523
+ if (event.target !== this) this.hide(true);
524
+ });
525
+ }
526
+ connectedCallback() {
527
+ this.sync();
528
+ }
529
+ disconnectedCallback() {
530
+ this.hide(false);
531
+ }
532
+ attributeChangedCallback() {
533
+ this.sync();
534
+ }
535
+ get disabled() {
536
+ return this.hasAttribute("disabled");
537
+ }
538
+ set disabled(value) {
539
+ this.toggleAttribute("disabled", Boolean(value));
540
+ }
541
+ get open() {
542
+ return this.caret.getAttribute("aria-expanded") === "true";
543
+ }
544
+ items() {
545
+ return [...this.querySelectorAll(ENABLED_ITEMS)];
546
+ }
547
+ toggle() {
548
+ if (this.open) this.hide(true);
549
+ else this.show(0);
550
+ }
551
+ show(focusIndex) {
552
+ const anchor = this.wrap.getBoundingClientRect();
553
+ this.panel.style.left = `${Math.round(anchor.left)}px`;
554
+ this.panel.style.top = `${Math.round(anchor.bottom + 2)}px`;
555
+ if (this.panel.hasAttribute("popover")) this.panel.showPopover?.();
556
+ else {
557
+ this.panel.hidden = false;
558
+ this.outside = (event) => {
559
+ if (!event.composedPath().includes(this)) this.hide(false);
560
+ };
561
+ this.ownerDocument.addEventListener("pointerdown", this.outside, true);
562
+ }
563
+ this.setExpanded(true);
564
+ this.items()[focusIndex]?.focus();
565
+ }
566
+ hide(restoreFocus) {
567
+ if (!this.open) return;
568
+ if (this.panel.hasAttribute("popover")) {
569
+ try {
570
+ this.panel.hidePopover?.();
571
+ } catch {
572
+ }
573
+ }
574
+ this.closed();
575
+ if (restoreFocus) (this.getAttribute("command") ? this.caret : this.main).focus();
576
+ }
577
+ closed() {
578
+ if (!this.panel.hasAttribute("popover")) this.panel.hidden = true;
579
+ if (this.outside) this.ownerDocument.removeEventListener("pointerdown", this.outside, true);
580
+ this.outside = void 0;
581
+ this.setExpanded(false);
582
+ }
583
+ setExpanded(open) {
584
+ this.caret.setAttribute("aria-expanded", String(open));
585
+ if (this.hasAttribute("command")) this.main.removeAttribute("aria-expanded");
586
+ else this.main.setAttribute("aria-expanded", String(open));
587
+ }
588
+ onMenuKey(event) {
589
+ const items = this.items();
590
+ const from = items.findIndex(
591
+ (item) => item === event.target || item.contains(event.target)
592
+ );
593
+ const move = (index) => {
594
+ event.preventDefault();
595
+ items[(index + items.length) % items.length]?.focus();
596
+ };
597
+ if (event.key === "ArrowDown") move(from + 1);
598
+ else if (event.key === "ArrowUp") move(from - 1);
599
+ else if (event.key === "Home") move(0);
600
+ else if (event.key === "End") move(items.length - 1);
601
+ else if (event.key === "Escape") {
602
+ event.preventDefault();
603
+ this.hide(true);
604
+ } else if (event.key === "Tab") this.hide(false);
605
+ }
606
+ sync() {
607
+ const label = this.getAttribute("label") ?? "";
608
+ const split = this.hasAttribute("command");
609
+ this.text.textContent = label;
610
+ const title = this.getAttribute("title") ?? label;
611
+ this.main.title = title;
612
+ this.caret.title = split ? `${label} options` : title;
613
+ this.caret.setAttribute("aria-label", split ? `${label} options` : label);
614
+ if (split) {
615
+ this.main.removeAttribute("aria-haspopup");
616
+ this.main.removeAttribute("aria-expanded");
617
+ this.caret.tabIndex = 0;
618
+ } else {
619
+ this.main.setAttribute("aria-haspopup", "menu");
620
+ this.main.setAttribute("aria-expanded", String(this.open));
621
+ this.caret.tabIndex = -1;
622
+ }
623
+ const shortcuts = this.getAttribute("keyshortcuts");
624
+ if (shortcuts) this.main.setAttribute("aria-keyshortcuts", shortcuts);
625
+ else this.main.removeAttribute("aria-keyshortcuts");
626
+ this.main.disabled = this.caret.disabled = this.disabled;
627
+ this.wrap.classList.toggle("disabled", this.disabled);
628
+ if (this.disabled) this.hide(false);
629
+ if (paintIcon(this.svg, this.getAttribute("icon"))) this.svg.setAttribute("data-painted", "");
630
+ else this.svg.removeAttribute("data-painted");
631
+ }
632
+ }
633
+ return OfficeUiMenuButton;
634
+ });
635
+
352
636
  // src/ribbon.ts
353
637
  var GROUP_CSS = `
354
638
  :host { display: inline-flex; flex: none; align-self: stretch; }
@@ -358,14 +642,25 @@ var GROUP_CSS = `
358
642
  :host(:last-child) .group { border-inline-end: 0; }
359
643
  .row { display: flex; align-items: flex-start; gap: 4px; }
360
644
  ::slotted(*) { flex-shrink: 0; }
361
- .caption { padding-top: 2px; color: var(--office-muted-foreground, #6b7280); font-size: 10px;
645
+ .foot { position: relative; display: flex; justify-content: center; padding-top: 2px; }
646
+ .caption { color: var(--office-muted-foreground, #6b7280); font-size: 10px;
362
647
  line-height: 12px; text-align: center; white-space: nowrap; }
648
+ .launcher { position: absolute; inset-inline-end: -6px; bottom: -1px; display: inline-grid; place-items: center;
649
+ width: 16px; height: 14px; padding: 0; border: 0; border-radius: 2px; background: transparent;
650
+ color: var(--office-muted-foreground, #6b7280); cursor: pointer; }
651
+ .launcher:hover:not(:disabled) { background: var(--office-surface, #f3f4f6); }
652
+ .launcher:focus-visible { outline: 2px solid var(--office-ring, #2563eb); }
653
+ .launcher:disabled { opacity: .45; cursor: not-allowed; }
654
+ .launcher[hidden] { display: none; }
655
+ .launcher svg { width: 10px; height: 10px; fill: none; stroke: currentColor; stroke-width: 1.6;
656
+ stroke-linecap: round; stroke-linejoin: round; }
363
657
  @media (forced-colors: active) { .group { border-inline-end-color: CanvasText; } .caption { color: CanvasText; } }
364
658
  `;
365
659
  var defineRibbonGroup = definer("office-ui-ribbon-group", () => {
366
660
  class OfficeUiRibbonGroup extends HTMLElement {
367
- static observedAttributes = ["label"];
661
+ static observedAttributes = ["label", "launcher", "launcher-disabled"];
368
662
  caption;
663
+ launcher;
369
664
  constructor() {
370
665
  super();
371
666
  const doc = this.ownerDocument;
@@ -378,7 +673,20 @@ var defineRibbonGroup = definer("office-ui-ribbon-group", () => {
378
673
  row.append(doc.createElement("slot"));
379
674
  this.caption = doc.createElement("span");
380
675
  this.caption.className = "caption";
381
- group.append(row, this.caption);
676
+ this.launcher = doc.createElement("button");
677
+ this.launcher.type = "button";
678
+ this.launcher.className = "launcher";
679
+ const glyph = createIconSvg(doc);
680
+ paintIcon(glyph, "launcher");
681
+ this.launcher.append(glyph);
682
+ this.launcher.addEventListener("click", () => {
683
+ const command = this.getAttribute("launcher");
684
+ if (command && !this.launcher.disabled) emit(this, "office-command", { command });
685
+ });
686
+ const foot = doc.createElement("div");
687
+ foot.className = "foot";
688
+ foot.append(this.caption, this.launcher);
689
+ group.append(row, foot);
382
690
  root.append(group);
383
691
  }
384
692
  connectedCallback() {
@@ -392,6 +700,10 @@ var defineRibbonGroup = definer("office-ui-ribbon-group", () => {
392
700
  const label = this.getAttribute("label") ?? "";
393
701
  this.caption.textContent = label;
394
702
  this.setAttribute("aria-label", label);
703
+ this.launcher.hidden = !this.getAttribute("launcher");
704
+ this.launcher.disabled = this.hasAttribute("launcher-disabled");
705
+ this.launcher.setAttribute("aria-label", `${label} options`);
706
+ this.launcher.title = `${label} options`;
395
707
  }
396
708
  }
397
709
  return OfficeUiRibbonGroup;
@@ -401,7 +713,24 @@ var TOOLBAR_CSS = `
401
713
  background: var(--office-surface, #f3f4f6); border-bottom: 1px solid var(--office-border, #d1d5db); }
402
714
  @media (forced-colors: active) { :host { border-bottom-color: CanvasText; background: Canvas; } }
403
715
  `;
404
- var ITEM = "office-ui-button, office-ui-select, office-ui-checkbox, office-ui-switch";
716
+ var STACK_CSS = `
717
+ :host { display: inline-flex; flex-direction: column; align-items: flex-start; justify-content: flex-start;
718
+ gap: 1px; align-self: stretch; }
719
+ :host([orientation="horizontal"]) { flex-direction: row; align-items: center; gap: 2px; align-self: auto; }
720
+ ::slotted(*) { flex-shrink: 0; }
721
+ `;
722
+ var defineRibbonStack = definer("office-ui-ribbon-stack", () => {
723
+ class OfficeUiRibbonStack extends HTMLElement {
724
+ constructor() {
725
+ super();
726
+ const root = this.attachShadow({ mode: "open" });
727
+ attachStyles(root, controlCss(STACK_CSS));
728
+ root.append(this.ownerDocument.createElement("slot"));
729
+ }
730
+ }
731
+ return OfficeUiRibbonStack;
732
+ });
733
+ var ITEM = "office-ui-button, office-ui-menu-button, office-ui-select, office-ui-checkbox, office-ui-switch";
405
734
  var defineToolbar = definer("office-ui-toolbar", () => {
406
735
  class OfficeUiToolbar extends HTMLElement {
407
736
  constructor() {
@@ -648,7 +977,9 @@ var defineStatusBar = definer("office-ui-status-bar", () => {
648
977
  super();
649
978
  const root = this.attachShadow({ mode: "open" });
650
979
  attachStyles(root, controlCss(BAR_CSS));
651
- root.append(this.ownerDocument.createElement("slot"));
980
+ const end = this.ownerDocument.createElement("slot");
981
+ end.name = "end";
982
+ root.append(this.ownerDocument.createElement("slot"), end);
652
983
  }
653
984
  connectedCallback() {
654
985
  this.setAttribute("role", "group");
@@ -658,7 +989,7 @@ var defineStatusBar = definer("office-ui-status-bar", () => {
658
989
  }
659
990
  return OfficeUiStatusBar;
660
991
  });
661
- var ITEM_CSS = `
992
+ var ITEM_CSS2 = `
662
993
  :host { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
663
994
  .label { color: var(--office-muted-foreground, #6b7280); }
664
995
  button { all: unset; display: inline-flex; gap: 4px; align-items: center; cursor: pointer; padding: 0 4px;
@@ -674,7 +1005,7 @@ var defineStatusItem = definer("office-ui-status-item", () => {
674
1005
  constructor() {
675
1006
  super();
676
1007
  this.root = this.attachShadow({ mode: "open", delegatesFocus: true });
677
- attachStyles(this.root, controlCss(ITEM_CSS));
1008
+ attachStyles(this.root, controlCss(ITEM_CSS2));
678
1009
  }
679
1010
  connectedCallback() {
680
1011
  this.render();
@@ -725,6 +1056,300 @@ var defineStatusItem = definer("office-ui-status-item", () => {
725
1056
  return OfficeUiStatusItem;
726
1057
  });
727
1058
 
1059
+ // src/tab-strip.ts
1060
+ var CSS4 = `
1061
+ :host { display: flex; align-items: stretch; min-width: 0; min-height: 28px; gap: 2px; padding: 0 6px;
1062
+ background: var(--office-surface, #f3f4f6); color: var(--office-muted-foreground, #6b7280);
1063
+ border-top: 1px solid var(--office-border, #d1d5db); font-size: 12px;
1064
+ font-family: var(--office-font, system-ui, sans-serif); }
1065
+ .step { flex: none; display: inline-grid; place-items: center; width: var(--office-target-size, 26px);
1066
+ min-height: var(--office-target-size, 26px);
1067
+ padding: 0; border: 0; background: transparent; color: inherit; cursor: pointer; }
1068
+ .step:disabled { opacity: .4; cursor: default; }
1069
+ .list { display: flex; align-items: stretch; min-width: 0; overflow-x: auto; scrollbar-width: thin; }
1070
+ .tab { flex: none; max-width: 220px; min-height: var(--office-target-size, 26px); margin-top: -1px;
1071
+ padding: 2px 14px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
1072
+ border: 1px solid transparent; border-top: 0; border-radius: 0 0 var(--office-radius, 4px) var(--office-radius, 4px);
1073
+ background: transparent; color: inherit; font: inherit; cursor: pointer; }
1074
+ .tab:hover { color: var(--office-foreground, #1f2937); }
1075
+ .tab[aria-selected="true"] { border-color: var(--office-border, #d1d5db); background: var(--office-background, #fff);
1076
+ color: var(--office-accent, #2563eb); font-weight: 600; box-shadow: inset 0 2px 0 var(--office-accent, #2563eb); }
1077
+ button:focus-visible { outline: 2px solid var(--office-ring, #2563eb); outline-offset: -2px; }
1078
+ svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.5;
1079
+ stroke-linecap: round; stroke-linejoin: round; }
1080
+ @media (forced-colors: active) {
1081
+ :host { border-top-color: CanvasText; }
1082
+ .tab[aria-selected="true"] { background: Highlight; color: HighlightText; forced-color-adjust: none; }
1083
+ }
1084
+ `;
1085
+ var defineTabStrip = definer("office-ui-tab-strip", () => {
1086
+ class OfficeUiTabStrip extends HTMLElement {
1087
+ static observedAttributes = ["label", "disabled", "selected", "previous-label", "next-label"];
1088
+ items = [];
1089
+ list;
1090
+ previous;
1091
+ next;
1092
+ constructor() {
1093
+ super();
1094
+ const doc = this.ownerDocument;
1095
+ const root = this.attachShadow({ mode: "open" });
1096
+ attachStyles(root, controlCss(CSS4));
1097
+ const step = (name, icon) => {
1098
+ const button = doc.createElement("button");
1099
+ button.type = "button";
1100
+ button.className = "step";
1101
+ button.setAttribute("aria-label", name);
1102
+ button.title = name;
1103
+ const svg = createIconSvg(doc);
1104
+ paintIcon(svg, icon);
1105
+ button.append(svg);
1106
+ return button;
1107
+ };
1108
+ this.previous = step("Previous", "chevronLeft");
1109
+ this.next = step("Next", "chevronRight");
1110
+ this.list = doc.createElement("div");
1111
+ this.list.className = "list";
1112
+ this.list.setAttribute("role", "tablist");
1113
+ root.append(this.previous, this.next, this.list);
1114
+ this.previous.addEventListener("click", () => this.move(-1, false));
1115
+ this.next.addEventListener("click", () => this.move(1, false));
1116
+ this.list.addEventListener("click", (event) => {
1117
+ const tab = event.target.closest(".tab");
1118
+ if (tab?.dataset.id !== void 0) this.choose(tab.dataset.id, false);
1119
+ });
1120
+ this.list.addEventListener("keydown", (event) => this.onKey(event));
1121
+ }
1122
+ connectedCallback() {
1123
+ this.render();
1124
+ }
1125
+ attributeChangedCallback(name) {
1126
+ if (name === "selected") this.sync();
1127
+ else this.render();
1128
+ }
1129
+ get tabs() {
1130
+ return this.items.map((tab) => ({ ...tab }));
1131
+ }
1132
+ set tabs(value) {
1133
+ this.items = (Array.isArray(value) ? value : []).filter((tab) => tab && typeof tab.id === "string").map((tab) => ({
1134
+ id: tab.id,
1135
+ label: String(tab.label ?? ""),
1136
+ ...tab.title === void 0 ? {} : { title: String(tab.title) }
1137
+ }));
1138
+ this.render();
1139
+ }
1140
+ get selected() {
1141
+ return this.getAttribute("selected") ?? "";
1142
+ }
1143
+ set selected(id) {
1144
+ this.setAttribute("selected", String(id));
1145
+ }
1146
+ index() {
1147
+ return this.items.findIndex((tab) => tab.id === this.selected);
1148
+ }
1149
+ move(delta, focus) {
1150
+ const index = this.index();
1151
+ const target = this.items[Math.max(0, Math.min(this.items.length - 1, index + delta))];
1152
+ if (target) this.choose(target.id, focus);
1153
+ }
1154
+ choose(id, focus) {
1155
+ if (this.hasAttribute("disabled")) return;
1156
+ if (id !== this.selected && emit(this, "office-tab-select", { id }, true)) this.selected = id;
1157
+ if (focus) this.tabButton(this.selected)?.focus();
1158
+ }
1159
+ tabButton(id) {
1160
+ return [...this.list.querySelectorAll(".tab")].find(
1161
+ (tab) => tab.dataset.id === id
1162
+ );
1163
+ }
1164
+ onKey(event) {
1165
+ const keys = {
1166
+ ArrowLeft: -1,
1167
+ ArrowRight: 1,
1168
+ ArrowUp: -1,
1169
+ ArrowDown: 1
1170
+ };
1171
+ if (event.key === "Home" || event.key === "End") {
1172
+ event.preventDefault();
1173
+ const target = event.key === "Home" ? this.items[0] : this.items[this.items.length - 1];
1174
+ if (target) this.choose(target.id, true);
1175
+ } else if (keys[event.key] !== void 0) {
1176
+ event.preventDefault();
1177
+ this.move(keys[event.key], true);
1178
+ }
1179
+ }
1180
+ render() {
1181
+ const doc = this.ownerDocument;
1182
+ this.list.setAttribute("aria-label", this.getAttribute("label") ?? "Tabs");
1183
+ for (const [button, name, fallback] of [
1184
+ [this.previous, "previous-label", "Previous"],
1185
+ [this.next, "next-label", "Next"]
1186
+ ]) {
1187
+ const label = this.getAttribute(name) || fallback;
1188
+ button.setAttribute("aria-label", label);
1189
+ button.title = label;
1190
+ }
1191
+ this.list.replaceChildren(
1192
+ ...this.items.map((item) => {
1193
+ const tab = doc.createElement("button");
1194
+ tab.type = "button";
1195
+ tab.className = "tab";
1196
+ tab.setAttribute("role", "tab");
1197
+ tab.dataset.id = item.id;
1198
+ tab.textContent = item.label;
1199
+ tab.title = item.title ?? item.label;
1200
+ return tab;
1201
+ })
1202
+ );
1203
+ this.sync();
1204
+ }
1205
+ sync() {
1206
+ const disabled = this.hasAttribute("disabled");
1207
+ const index = this.index();
1208
+ for (const tab of this.list.querySelectorAll(".tab")) {
1209
+ const selected = tab.dataset.id === this.selected;
1210
+ tab.setAttribute("aria-selected", String(selected));
1211
+ tab.tabIndex = selected || index < 0 && tab === this.list.firstElementChild ? 0 : -1;
1212
+ tab.disabled = disabled;
1213
+ if (selected) tab.scrollIntoView?.({ block: "nearest", inline: "nearest" });
1214
+ }
1215
+ this.previous.disabled = disabled || index <= 0;
1216
+ this.next.disabled = disabled || index < 0 || index >= this.items.length - 1;
1217
+ }
1218
+ }
1219
+ return OfficeUiTabStrip;
1220
+ });
1221
+
1222
+ // src/zoom-slider.ts
1223
+ var CSS5 = `
1224
+ :host { display: inline-flex; align-items: center; gap: 2px; font-size: 11px;
1225
+ color: var(--office-foreground, #1f2937); font-family: var(--office-font, system-ui, sans-serif); }
1226
+ button { box-sizing: border-box; display: inline-grid; place-items: center; min-width: var(--office-target-size, 24px);
1227
+ min-height: var(--office-target-size, 24px); padding: 0; border: 1px solid transparent;
1228
+ border-radius: var(--office-radius, 4px); background: transparent; color: inherit; font: inherit;
1229
+ font-size: 15px; line-height: 1; cursor: pointer; }
1230
+ button:hover:not(:disabled) { background: var(--office-background, #fff); }
1231
+ button:focus-visible, input:focus-visible { outline: 2px solid var(--office-ring, #2563eb); outline-offset: 1px; }
1232
+ button:disabled, input:disabled { opacity: .5; cursor: not-allowed; }
1233
+ input { width: var(--office-zoom-slider-width, 110px); min-width: 48px; margin: 0 2px;
1234
+ accent-color: var(--office-accent, #2563eb); }
1235
+ output { min-width: 40px; text-align: center; font-variant-numeric: tabular-nums; }
1236
+ svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.5;
1237
+ stroke-linecap: round; stroke-linejoin: round; }
1238
+ :host(:not([fit])) .fit { display: none; }
1239
+ @media (pointer: coarse), (max-width: 767px) { input { flex: 1; } }
1240
+ @media (forced-colors: active) { button { color: ButtonText; } button:disabled { color: GrayText; } }
1241
+ `;
1242
+ var clampTo = (value, min, max) => Math.min(max, Math.max(min, Math.round(value)));
1243
+ var number = (value, fallback) => {
1244
+ const parsed = Number(value);
1245
+ return value !== null && Number.isFinite(parsed) ? parsed : fallback;
1246
+ };
1247
+ var defineZoomSlider = definer("office-ui-zoom-slider", () => {
1248
+ class OfficeUiZoomSlider extends HTMLElement {
1249
+ static observedAttributes = ["value", "min", "max", "disabled", "fit", "label"];
1250
+ out;
1251
+ in;
1252
+ range;
1253
+ output;
1254
+ fit;
1255
+ constructor() {
1256
+ super();
1257
+ const doc = this.ownerDocument;
1258
+ const root = this.attachShadow({ mode: "open" });
1259
+ attachStyles(root, controlCss(CSS5));
1260
+ const button = (name, text) => {
1261
+ const element = doc.createElement("button");
1262
+ element.type = "button";
1263
+ element.setAttribute("aria-label", name);
1264
+ element.title = name;
1265
+ element.textContent = text;
1266
+ return element;
1267
+ };
1268
+ this.out = button("Zoom out", "\u2212");
1269
+ this.in = button("Zoom in", "+");
1270
+ this.range = doc.createElement("input");
1271
+ this.range.type = "range";
1272
+ this.range.setAttribute("aria-label", "Zoom");
1273
+ this.output = doc.createElement("output");
1274
+ this.fit = button("Fit", "");
1275
+ this.fit.className = "fit";
1276
+ const svg = createIconSvg(doc);
1277
+ paintIcon(svg, "fitPage");
1278
+ this.fit.append(svg);
1279
+ root.append(this.out, this.range, this.in, this.output, this.fit);
1280
+ this.out.addEventListener("click", () => this.stepBy(-1));
1281
+ this.in.addEventListener("click", () => this.stepBy(1));
1282
+ this.range.addEventListener("input", () => this.commit(this.range.valueAsNumber, "input"));
1283
+ this.range.addEventListener(
1284
+ "change",
1285
+ () => this.dispatchEvent(new Event("change", { bubbles: true, composed: true }))
1286
+ );
1287
+ this.fit.addEventListener(
1288
+ "click",
1289
+ () => emit(this, "office-command", { command: "zoom-fit" })
1290
+ );
1291
+ }
1292
+ connectedCallback() {
1293
+ this.setAttribute("role", "group");
1294
+ this.sync();
1295
+ }
1296
+ attributeChangedCallback() {
1297
+ this.sync();
1298
+ }
1299
+ get min() {
1300
+ return number(this.getAttribute("min"), 10);
1301
+ }
1302
+ get max() {
1303
+ return Math.max(this.min, number(this.getAttribute("max"), 400));
1304
+ }
1305
+ get value() {
1306
+ return clampTo(number(this.getAttribute("value"), 100), this.min, this.max);
1307
+ }
1308
+ set value(next) {
1309
+ this.setAttribute("value", String(clampTo(Number(next) || 0, this.min, this.max)));
1310
+ }
1311
+ get disabled() {
1312
+ return this.hasAttribute("disabled");
1313
+ }
1314
+ set disabled(value) {
1315
+ this.toggleAttribute("disabled", Boolean(value));
1316
+ }
1317
+ /** Office buttons snap to the next multiple of `step`, so 67% steps to 70%, not 77%. */
1318
+ stepBy(direction) {
1319
+ const step = Math.max(1, number(this.getAttribute("step"), 10));
1320
+ const current = this.value;
1321
+ const next = direction > 0 ? Math.floor(current / step) * step + step : Math.ceil(current / step) * step - step;
1322
+ this.commit(next, "change");
1323
+ }
1324
+ commit(next, type) {
1325
+ if (this.disabled) return;
1326
+ const value = clampTo(next, this.min, this.max);
1327
+ if (value === this.value && type === "change") return;
1328
+ this.value = value;
1329
+ this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
1330
+ if (type === "change")
1331
+ this.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
1332
+ }
1333
+ sync() {
1334
+ const { min, max, value, disabled } = this;
1335
+ this.setAttribute("aria-label", this.getAttribute("label") ?? "Zoom");
1336
+ this.range.min = String(min);
1337
+ this.range.max = String(max);
1338
+ this.range.value = String(value);
1339
+ this.range.setAttribute("aria-valuetext", `${value}%`);
1340
+ this.output.value = `${value}%`;
1341
+ this.out.disabled = disabled || value <= min;
1342
+ this.in.disabled = disabled || value >= max;
1343
+ this.range.disabled = disabled;
1344
+ const fit = this.getAttribute("fit");
1345
+ this.fit.disabled = disabled;
1346
+ this.fit.setAttribute("aria-label", fit || "Fit");
1347
+ this.fit.title = fit || "Fit";
1348
+ }
1349
+ }
1350
+ return OfficeUiZoomSlider;
1351
+ });
1352
+
728
1353
  // src/controls.ts
729
1354
  var CONTROL_DEFINERS = [
730
1355
  defineIcon,
@@ -733,13 +1358,18 @@ var CONTROL_DEFINERS = [
733
1358
  defineSwitch,
734
1359
  defineSelect,
735
1360
  defineRibbonGroup,
1361
+ defineRibbonStack,
1362
+ defineMenuItem,
1363
+ defineMenuButton,
736
1364
  defineToolbar,
737
1365
  defineDialog,
738
1366
  defineStatusBar,
739
- defineStatusItem
1367
+ defineStatusItem,
1368
+ defineZoomSlider,
1369
+ defineTabStrip
740
1370
  ];
741
1371
  function registerControls(registry) {
742
1372
  for (const define of CONTROL_DEFINERS) define(registry);
743
1373
  }
744
1374
 
745
- export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineDialog, defineRibbonGroup, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineToolbar, registerControls };
1375
+ export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineDialog, defineMenuButton, defineMenuItem, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls };
@@ -2,15 +2,19 @@ import { defineButton } from './button.js';
2
2
  import { defineCheckbox, defineSwitch } from './checkable.js';
3
3
  import { defineDialog } from './dialog.js';
4
4
  import { defineIcon } from './icons.js';
5
- import { defineRibbonGroup, defineToolbar } from './ribbon.js';
5
+ import { defineMenuButton, defineMenuItem } from './menu.js';
6
+ import { defineRibbonGroup, defineRibbonStack, defineToolbar } from './ribbon.js';
6
7
  import { defineSelect } from './select.js';
7
8
  import { defineStatusBar, defineStatusItem } from './status-bar.js';
8
- export { defineButton, defineCheckbox, defineDialog, defineIcon, defineRibbonGroup, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineToolbar, };
9
+ import { defineTabStrip } from './tab-strip.js';
10
+ import { defineZoomSlider } from './zoom-slider.js';
11
+ export { defineButton, defineCheckbox, defineDialog, defineIcon, defineMenuButton, defineMenuItem, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, };
9
12
  export type { OfficeCommandEvent } from './button.js';
10
13
  export type { OfficeDialogCloseEvent, OfficeDialogCloseReason } from './dialog.js';
11
14
  export type { OfficeSelectOption } from './select.js';
12
15
  export type { OfficeStatusActivateEvent } from './status-bar.js';
16
+ export type { OfficeTab, OfficeTabSelectEvent } from './tab-strip.js';
13
17
  /** Every control of this entry. */
14
- 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];
18
+ 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];
15
19
  /** Idempotent, SSR-safe (no-op without a DOM). */
16
20
  export declare function registerControls(registry?: CustomElementRegistry): void;
package/dist/controls.js CHANGED
@@ -1,3 +1,3 @@
1
- export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineDialog, defineRibbonGroup, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineToolbar, registerControls } from './chunk-5UKGBJXE.js';
2
- export { defineIcon } from './chunk-7MCSBDSU.js';
1
+ export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineDialog, defineMenuButton, defineMenuItem, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls } from './chunk-TVZ4V4CX.js';
2
+ export { defineIcon } from './chunk-PCSUQPMC.js';
3
3
  import './chunk-EY266IK3.js';
package/dist/icons.js CHANGED
@@ -1,2 +1,2 @@
1
- export { ICON_CSS, createIconSvg, defineIcon, getIcon, listIcons, paintIcon, registerIcon } from './chunk-7MCSBDSU.js';
1
+ export { ICON_CSS, createIconSvg, defineIcon, getIcon, listIcons, paintIcon, registerIcon } from './chunk-PCSUQPMC.js';
2
2
  import './chunk-EY266IK3.js';
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-toolbar", "office-ui-dialog", "office-ui-status-bar", "office-ui-status-item", "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-toolbar", "office-ui-dialog", "office-ui-status-bar", "office-ui-status-item", "office-ui-zoom-slider", "office-ui-tab-strip", "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,8 +1,8 @@
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-5UKGBJXE.js';
4
- export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineDialog, defineRibbonGroup, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineToolbar, registerControls } from './chunk-5UKGBJXE.js';
5
- export { ICON_CSS, createIconSvg, defineIcon, getIcon, listIcons, paintIcon, registerIcon } from './chunk-7MCSBDSU.js';
3
+ import { registerControls } from './chunk-TVZ4V4CX.js';
4
+ export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineDialog, defineMenuButton, defineMenuItem, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls } from './chunk-TVZ4V4CX.js';
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';
8
8
  import { defineSmartArt } from './chunk-KJV76AB7.js';
@@ -17,10 +17,15 @@ var OFFICE_UI_TAGS = [
17
17
  "office-ui-switch",
18
18
  "office-ui-select",
19
19
  "office-ui-ribbon-group",
20
+ "office-ui-ribbon-stack",
21
+ "office-ui-menu-item",
22
+ "office-ui-menu-button",
20
23
  "office-ui-toolbar",
21
24
  "office-ui-dialog",
22
25
  "office-ui-status-bar",
23
26
  "office-ui-status-item",
27
+ "office-ui-zoom-slider",
28
+ "office-ui-tab-strip",
24
29
  "office-ui-presence",
25
30
  "office-ui-smartart"
26
31
  ];
package/dist/menu.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * One menu entry: `command`, `label`, `icon`, `disabled`, `checked` (renders a
3
+ * `menuitemcheckbox`), `title`. Activation emits `office-command` `{ command }`.
4
+ */
5
+ export declare const defineMenuItem: import("./registry.js").Definer;
6
+ /**
7
+ * Dropdown command with a menu of `office-ui-menu-item` children (Office's Find, Layers,
8
+ * Align). With a `command` attribute it is a split button: the main part emits that command
9
+ * and the caret opens the menu. Attributes: `label`, `icon`, `command`, `variant="stacked"`,
10
+ * `disabled`, `title`, `keyshortcuts`. The menu uses the top layer (`popover`) so ribbon
11
+ * overflow never clips it. Keyboard: Enter, Space or ArrowDown open; arrows, Home and End move;
12
+ * Escape and Tab close; focus returns to the trigger. Choosing an item closes the menu; its
13
+ * `office-command` bubbles to the host.
14
+ */
15
+ export declare const defineMenuButton: import("./registry.js").Definer;
package/dist/ribbon.d.ts CHANGED
@@ -1,5 +1,14 @@
1
- /** Labelled group of ribbon commands: `role="group"`, `label` attribute is caption and name. */
1
+ /**
2
+ * Labelled group of ribbon commands: `role="group"`, `label` attribute is caption and name.
3
+ * `launcher="<command>"` adds Office's corner dialog launcher, which emits `office-command`
4
+ * `{ command }`; `launcher-disabled` disables it.
5
+ */
2
6
  export declare const defineRibbonGroup: import("./registry.js").Definer;
7
+ /**
8
+ * Layout for small ribbon commands: a column (Office's three-row stack of Cut, Copy and Format
9
+ * Painter) or, with `orientation="horizontal"`, a row (Bold, Italic, Underline). Not a group.
10
+ */
11
+ export declare const defineRibbonStack: import("./registry.js").Definer;
3
12
  /**
4
13
  * `role="toolbar"` container. Left/Right/Home/End move focus between enabled direct or
5
14
  * group-nested controls (Up/Down instead when `aria-orientation="vertical"`).
@@ -1,7 +1,10 @@
1
1
  export type OfficeStatusActivateEvent = CustomEvent<{
2
2
  id: string;
3
3
  }>;
4
- /** Status bar container: `role="group"` named by its `label` attribute (default "Status"). */
4
+ /**
5
+ * Status bar container: `role="group"` named by its `label` attribute (default "Status").
6
+ * Children with `slot="end"` are pushed to the trailing edge (zoom, view switches).
7
+ */
5
8
  export declare const defineStatusBar: import("./registry.js").Definer;
6
9
  /**
7
10
  * One status entry: `label` and `value` attributes (textual, so assistive tech reads both).
@@ -0,0 +1,18 @@
1
+ export interface OfficeTab {
2
+ id: string;
3
+ label: string;
4
+ /** Tooltip; defaults to the label. */
5
+ title?: string;
6
+ }
7
+ export type OfficeTabSelectEvent = CustomEvent<{
8
+ id: string;
9
+ }>;
10
+ /**
11
+ * Bottom document tabs (Visio pages, Excel sheets). Set `tabs` (`OfficeTab[]`) and `selected`
12
+ * (a tab id). Labels are inserted as text. The previous/next buttons and arrow, Home and End
13
+ * keys move the selection; the tab list uses a roving tabindex. User selection emits
14
+ * `office-tab-select` `{ id }` (cancelable); setting properties never emits.
15
+ * Attributes: `label` (tab list name, default "Tabs"), `previous-label` and `next-label`
16
+ * (step button names, default "Previous" and "Next"), `disabled`.
17
+ */
18
+ export declare const defineTabStrip: import("./registry.js").Definer;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Office status-bar zoom: zoom out, slider, zoom in, percentage and an optional fit button.
3
+ * Attributes: `value` (percent, default 100), `min` (10), `max` (400), `step` (10, for the
4
+ * buttons), `disabled`, `fit` (shows the fit button; its value is the button's name, e.g.
5
+ * "Fit page to current window"), `label` (group name, default "Zoom").
6
+ * User changes emit native `input` (slider drag) and `change` events with `value` updated;
7
+ * the fit button emits `office-command` `{ command: 'zoom-fit' }`. Setting `value` never emits.
8
+ */
9
+ export declare const defineZoomSlider: import("./registry.js").Definer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ooxml-ui",
3
- "version": "0.1.12",
3
+ "version": "0.3.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",