ooxml-ui 0.4.1 → 0.6.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
@@ -58,22 +58,24 @@ 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"`, `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 |
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`, `previous-label`, `next-label`, `add-label`, `add-disabled`, `add-title`, `disabled` | `office-tab-select` `{ id }` (cancelable); add: `office-command` `{ command: 'tab-add' }` |
74
+ | `office-ui-context-menu` / `office-ui-menu-separator` | `label`; `openAt(x, y)`, `close()`, `open`; `office-ui-menu-item` and separator children | item choice: `office-command` `{ command }` |
75
+ | `office-ui-command-search` | `commands` `[{ id, label, keywords?, disabled?, title? }]`, `placeholder`, `label`, `limit` (Office's "Tell me what you want to do") | choice: `office-command` `{ command }` |
76
+ | `office-ui-presence` | `participants`, `max`, `label` | `office-presence-select` `{ id }` |
77
+ | `office-ui-smartart` | `drawing` (core `DiagramDrawing`), `schemeColors`, `label` | `office-smartart-render` (render report) |
78
+ | `office-ui-icon` | `name`, `label` | none |
77
79
 
78
80
  Setting a property never emits an event; only user activation does. Controls honour
79
81
  `prefers-color-scheme`, `forced-colors: active` (system colours and a visible focus ring) and
@@ -353,6 +353,330 @@ var defineDialog = definer("office-ui-dialog", () => {
353
353
  return OfficeUiDialog;
354
354
  });
355
355
 
356
+ // src/command-search.ts
357
+ var CSS3 = `
358
+ :host { display: inline-flex; align-items: center; min-width: 0; font-family: var(--office-font, system-ui, sans-serif); }
359
+ .field { display: flex; align-items: center; gap: 6px; width: 100%; min-width: 0; height: var(--office-target-size, 28px);
360
+ padding: 0 8px; box-sizing: border-box; border: 1px solid transparent; border-radius: var(--office-radius, 4px);
361
+ color: var(--office-muted-foreground, #6b7280); }
362
+ .field:hover, .field:focus-within { border-color: var(--office-border, #d1d5db); background: var(--office-background, #fff); }
363
+ input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--office-foreground, #1f2937);
364
+ font: inherit; font-size: 13px; }
365
+ svg { width: 16px; height: 16px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.5;
366
+ stroke-linecap: round; stroke-linejoin: round; }
367
+ ul { position: fixed; inset: auto; margin: 0; padding: 4px; min-width: 240px; max-height: 320px; overflow: auto;
368
+ box-sizing: border-box; list-style: none; background: var(--office-background, #fff); color: var(--office-foreground, #1f2937);
369
+ border: 1px solid var(--office-border, #d1d5db); border-radius: var(--office-radius, 4px);
370
+ box-shadow: 0 8px 24px rgb(0 0 0 / 18%); font-size: 12px; }
371
+ ul[hidden] { display: none; }
372
+ li { display: flex; flex-direction: column; justify-content: center; min-height: var(--office-target-size, 28px);
373
+ padding: 2px 10px; border-radius: 3px; cursor: pointer; }
374
+ li[data-active] { background: var(--office-surface, #f3f4f6); }
375
+ li[aria-disabled="true"] { cursor: not-allowed; color: var(--office-muted-foreground, #6b7280); }
376
+ li small { color: var(--office-muted-foreground, #6b7280); font-size: 11px; }
377
+ li.empty { cursor: default; color: var(--office-muted-foreground, #6b7280); }
378
+ @media (forced-colors: active) {
379
+ .field, ul { border-color: CanvasText; background: Canvas; color: CanvasText; }
380
+ li[data-active] { background: Highlight; color: HighlightText; }
381
+ }
382
+ `;
383
+ var uid = 0;
384
+ var defineCommandSearch = definer("office-ui-command-search", () => {
385
+ class OfficeUiCommandSearch extends HTMLElement {
386
+ static observedAttributes = ["placeholder", "label"];
387
+ items = [];
388
+ found = [];
389
+ active = -1;
390
+ input;
391
+ list;
392
+ uid = `office-command-search-${++uid}`;
393
+ constructor() {
394
+ super();
395
+ const doc = this.ownerDocument;
396
+ const root = this.attachShadow({ mode: "open", delegatesFocus: true });
397
+ attachStyles(root, controlCss(CSS3));
398
+ const field = doc.createElement("div");
399
+ field.className = "field";
400
+ const glass = createIconSvg(doc);
401
+ paintIcon(glass, "search");
402
+ glass.setAttribute("data-painted", "");
403
+ this.input = doc.createElement("input");
404
+ this.input.type = "search";
405
+ this.input.autocomplete = "off";
406
+ this.input.spellcheck = false;
407
+ this.input.setAttribute("role", "combobox");
408
+ this.input.setAttribute("aria-autocomplete", "list");
409
+ this.input.setAttribute("aria-expanded", "false");
410
+ this.input.setAttribute("aria-controls", `${this.uid}-list`);
411
+ this.list = doc.createElement("ul");
412
+ this.list.id = `${this.uid}-list`;
413
+ this.list.setAttribute("role", "listbox");
414
+ if (typeof this.list.showPopover === "function")
415
+ this.list.setAttribute("popover", "manual");
416
+ else this.list.hidden = true;
417
+ field.append(glass, this.input);
418
+ root.append(field, this.list);
419
+ this.input.addEventListener("input", () => this.update());
420
+ this.input.addEventListener("keydown", (event) => this.onKey(event));
421
+ this.input.addEventListener("blur", () => this.hide());
422
+ this.list.addEventListener("pointerdown", (event) => event.preventDefault());
423
+ this.list.addEventListener("click", (event) => {
424
+ const item = event.target.closest("li[data-index]");
425
+ if (item) this.choose(Number(item.dataset.index));
426
+ });
427
+ }
428
+ connectedCallback() {
429
+ this.sync();
430
+ }
431
+ disconnectedCallback() {
432
+ this.hide();
433
+ }
434
+ attributeChangedCallback() {
435
+ this.sync();
436
+ }
437
+ get commands() {
438
+ return this.items.map((item) => ({ ...item }));
439
+ }
440
+ set commands(value) {
441
+ this.items = (Array.isArray(value) ? value : []).filter((item) => item && typeof item.id === "string").map((item) => ({ ...item, label: String(item.label ?? "") }));
442
+ if (this.isOpen) this.update();
443
+ }
444
+ get isOpen() {
445
+ return this.input.getAttribute("aria-expanded") === "true";
446
+ }
447
+ sync() {
448
+ this.input.placeholder = this.getAttribute("placeholder") ?? "Tell me what you want to do";
449
+ this.input.setAttribute("aria-label", this.getAttribute("label") ?? this.input.placeholder);
450
+ }
451
+ update() {
452
+ const query = this.input.value.trim().toLowerCase();
453
+ if (!query) return this.hide();
454
+ const limit = Math.max(1, Number(this.getAttribute("limit")) || 8);
455
+ this.found = this.items.filter((item) => `${item.label} ${item.keywords ?? ""}`.toLowerCase().includes(query)).slice(0, limit);
456
+ this.active = this.found.findIndex((item) => !item.disabled);
457
+ this.render();
458
+ this.show();
459
+ }
460
+ render() {
461
+ const doc = this.ownerDocument;
462
+ if (!this.found.length) {
463
+ const empty = doc.createElement("li");
464
+ empty.className = "empty";
465
+ empty.setAttribute("role", "option");
466
+ empty.setAttribute("aria-disabled", "true");
467
+ empty.textContent = "No matching commands";
468
+ this.list.replaceChildren(empty);
469
+ this.input.removeAttribute("aria-activedescendant");
470
+ return;
471
+ }
472
+ this.list.replaceChildren(
473
+ ...this.found.map((item, index) => {
474
+ const option = doc.createElement("li");
475
+ option.id = `${this.uid}-${index}`;
476
+ option.dataset.index = String(index);
477
+ option.setAttribute("role", "option");
478
+ option.setAttribute("aria-selected", String(index === this.active));
479
+ if (index === this.active) option.setAttribute("data-active", "");
480
+ const label = doc.createElement("span");
481
+ label.textContent = item.label;
482
+ option.append(label);
483
+ if (item.disabled) {
484
+ option.setAttribute("aria-disabled", "true");
485
+ const reason = doc.createElement("small");
486
+ reason.textContent = item.title ?? "Not available";
487
+ option.append(reason);
488
+ }
489
+ return option;
490
+ })
491
+ );
492
+ if (this.active >= 0)
493
+ this.input.setAttribute("aria-activedescendant", `${this.uid}-${this.active}`);
494
+ else this.input.removeAttribute("aria-activedescendant");
495
+ }
496
+ show() {
497
+ const box = this.input.getBoundingClientRect();
498
+ this.list.style.left = `${Math.round(box.left)}px`;
499
+ this.list.style.top = `${Math.round(box.bottom + 4)}px`;
500
+ if (this.list.hasAttribute("popover")) {
501
+ if (!this.isOpen) this.list.showPopover?.();
502
+ } else this.list.hidden = false;
503
+ this.input.setAttribute("aria-expanded", "true");
504
+ }
505
+ hide() {
506
+ if (!this.isOpen) return;
507
+ if (this.list.hasAttribute("popover")) {
508
+ try {
509
+ this.list.hidePopover?.();
510
+ } catch {
511
+ }
512
+ } else this.list.hidden = true;
513
+ this.input.setAttribute("aria-expanded", "false");
514
+ this.input.removeAttribute("aria-activedescendant");
515
+ }
516
+ move(step) {
517
+ const enabled = this.found.map((item, index) => item.disabled ? -1 : index).filter((i) => i >= 0);
518
+ if (!enabled.length) return;
519
+ const at = enabled.indexOf(this.active);
520
+ this.active = enabled[(at + step + enabled.length) % enabled.length];
521
+ this.render();
522
+ }
523
+ choose(index) {
524
+ const item = this.found[index];
525
+ if (!item || item.disabled) return;
526
+ this.input.value = "";
527
+ this.hide();
528
+ emit(this, "office-command", { command: item.id });
529
+ }
530
+ onKey(event) {
531
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
532
+ event.preventDefault();
533
+ if (!this.isOpen) this.update();
534
+ else this.move(event.key === "ArrowDown" ? 1 : -1);
535
+ } else if (event.key === "Enter") {
536
+ event.preventDefault();
537
+ this.choose(this.active);
538
+ } else if (event.key === "Escape") {
539
+ event.preventDefault();
540
+ event.stopPropagation();
541
+ if (this.isOpen) this.hide();
542
+ else this.input.value = "";
543
+ }
544
+ }
545
+ }
546
+ return OfficeUiCommandSearch;
547
+ });
548
+
549
+ // src/context-menu.ts
550
+ var CSS4 = `
551
+ :host { display: contents; }
552
+ .panel { position: fixed; inset: auto; margin: 0; min-width: 200px; padding: 4px; box-sizing: border-box;
553
+ background: var(--office-background, #fff); color: var(--office-foreground, #1f2937);
554
+ border: 1px solid var(--office-border, #d1d5db); border-radius: var(--office-radius, 4px);
555
+ box-shadow: 0 8px 24px rgb(0 0 0 / 18%); font-family: var(--office-font, system-ui, sans-serif); }
556
+ .panel[hidden] { display: none; }
557
+ @media (forced-colors: active) { .panel { border-color: CanvasText; background: Canvas; color: CanvasText; } }
558
+ `;
559
+ var SEPARATOR_CSS = `
560
+ :host { display: block; height: 1px; margin: 4px 6px; background: var(--office-border, #d1d5db); }
561
+ @media (forced-colors: active) { :host { background: CanvasText; } }
562
+ `;
563
+ var ITEMS = "office-ui-menu-item:not([disabled]):not([hidden])";
564
+ var defineMenuSeparator = definer("office-ui-menu-separator", () => {
565
+ class OfficeUiMenuSeparator extends HTMLElement {
566
+ constructor() {
567
+ super();
568
+ attachStyles(this.attachShadow({ mode: "open" }), controlCss(SEPARATOR_CSS));
569
+ }
570
+ connectedCallback() {
571
+ this.setAttribute("role", "separator");
572
+ }
573
+ }
574
+ return OfficeUiMenuSeparator;
575
+ });
576
+ var defineContextMenu = definer("office-ui-context-menu", () => {
577
+ class OfficeUiContextMenu extends HTMLElement {
578
+ static observedAttributes = ["label"];
579
+ panel;
580
+ returnFocus = null;
581
+ outside;
582
+ constructor() {
583
+ super();
584
+ const doc = this.ownerDocument;
585
+ const root = this.attachShadow({ mode: "open" });
586
+ attachStyles(root, controlCss(CSS4));
587
+ this.panel = doc.createElement("div");
588
+ this.panel.className = "panel";
589
+ this.panel.setAttribute("role", "menu");
590
+ if (typeof this.panel.showPopover === "function")
591
+ this.panel.setAttribute("popover", "auto");
592
+ else this.panel.hidden = true;
593
+ this.panel.append(doc.createElement("slot"));
594
+ root.append(this.panel);
595
+ this.panel.addEventListener("toggle", (event) => {
596
+ if (event.newState === "closed") this.closed(false);
597
+ });
598
+ this.panel.addEventListener("keydown", (event) => this.onKey(event));
599
+ this.addEventListener("office-command", () => this.close());
600
+ }
601
+ connectedCallback() {
602
+ this.panel.setAttribute("aria-label", this.getAttribute("label") ?? "Context menu");
603
+ }
604
+ attributeChangedCallback() {
605
+ this.panel.setAttribute("aria-label", this.getAttribute("label") ?? "Context menu");
606
+ }
607
+ disconnectedCallback() {
608
+ this.close();
609
+ }
610
+ get open() {
611
+ return this.hasAttribute("data-open");
612
+ }
613
+ items() {
614
+ return [...this.querySelectorAll(ITEMS)];
615
+ }
616
+ /** Show at client coordinates; focus the first enabled item. */
617
+ openAt(x, y) {
618
+ const doc = this.ownerDocument;
619
+ const active = doc.activeElement;
620
+ this.returnFocus = active instanceof HTMLElement ? active : null;
621
+ if (this.panel.hasAttribute("popover")) {
622
+ if (!this.open) this.panel.showPopover?.();
623
+ } else {
624
+ this.panel.hidden = false;
625
+ this.outside ??= (event) => {
626
+ if (!event.composedPath().includes(this)) this.close();
627
+ };
628
+ doc.addEventListener("pointerdown", this.outside, true);
629
+ }
630
+ this.setAttribute("data-open", "");
631
+ const view = doc.defaultView;
632
+ const width = this.panel.offsetWidth;
633
+ const height = this.panel.offsetHeight;
634
+ const left = view && width ? Math.min(x, view.innerWidth - width - 4) : x;
635
+ const top = view && height ? Math.min(y, view.innerHeight - height - 4) : y;
636
+ this.panel.style.left = `${Math.max(4, Math.round(left))}px`;
637
+ this.panel.style.top = `${Math.max(4, Math.round(top))}px`;
638
+ this.items()[0]?.focus();
639
+ }
640
+ close() {
641
+ if (!this.open) return;
642
+ if (this.panel.hasAttribute("popover")) {
643
+ try {
644
+ this.panel.hidePopover?.();
645
+ } catch {
646
+ }
647
+ }
648
+ this.closed(true);
649
+ }
650
+ closed(restoreFocus) {
651
+ if (!this.open) return;
652
+ this.removeAttribute("data-open");
653
+ if (!this.panel.hasAttribute("popover")) this.panel.hidden = true;
654
+ if (this.outside) this.ownerDocument.removeEventListener("pointerdown", this.outside, true);
655
+ if (restoreFocus) this.returnFocus?.focus?.();
656
+ this.returnFocus = null;
657
+ }
658
+ onKey(event) {
659
+ const items = this.items();
660
+ const from = items.findIndex(
661
+ (item) => item === event.target || item.contains(event.target)
662
+ );
663
+ const move = (index) => {
664
+ event.preventDefault();
665
+ items[(index + items.length) % items.length]?.focus();
666
+ };
667
+ if (event.key === "ArrowDown") move(from + 1);
668
+ else if (event.key === "ArrowUp") move(from - 1);
669
+ else if (event.key === "Home") move(0);
670
+ else if (event.key === "End") move(items.length - 1);
671
+ else if (event.key === "Escape" || event.key === "Tab") {
672
+ if (event.key === "Escape") event.preventDefault();
673
+ this.close();
674
+ }
675
+ }
676
+ }
677
+ return OfficeUiContextMenu;
678
+ });
679
+
356
680
  // src/menu.ts
357
681
  var BUTTON_CSS = `
358
682
  :host { display: inline-flex; vertical-align: middle; }
@@ -798,7 +1122,7 @@ var defineToolbar = definer("office-ui-toolbar", () => {
798
1122
  });
799
1123
 
800
1124
  // src/select.ts
801
- var CSS3 = `
1125
+ var CSS5 = `
802
1126
  :host { display: inline-block; position: relative; vertical-align: middle; }
803
1127
  button { box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: 8px;
804
1128
  width: 100%; min-height: var(--office-target-size, 28px); padding: 0 8px; font: inherit; font-size: 12px;
@@ -820,7 +1144,7 @@ li[aria-disabled="true"] { opacity: .5; cursor: not-allowed; }
820
1144
  li[data-active] { background: Highlight; color: HighlightText; }
821
1145
  }
822
1146
  `;
823
- var uid = 0;
1147
+ var uid2 = 0;
824
1148
  var defineSelect = definer("office-ui-select", () => {
825
1149
  class OfficeUiSelect extends HTMLElement {
826
1150
  static observedAttributes = ["value", "disabled", "aria-label"];
@@ -832,12 +1156,12 @@ var defineSelect = definer("office-ui-select", () => {
832
1156
  trigger;
833
1157
  list;
834
1158
  label;
835
- domId = `office-ui-select-${++uid}`;
1159
+ domId = `office-ui-select-${++uid2}`;
836
1160
  constructor() {
837
1161
  super();
838
1162
  const doc = this.ownerDocument;
839
1163
  const root = this.attachShadow({ mode: "open", delegatesFocus: true });
840
- attachStyles(root, controlCss(CSS3));
1164
+ attachStyles(root, controlCss(CSS5));
841
1165
  this.trigger = doc.createElement("button");
842
1166
  this.trigger.type = "button";
843
1167
  this.trigger.setAttribute("role", "combobox");
@@ -1088,7 +1412,7 @@ var defineStatusItem = definer("office-ui-status-item", () => {
1088
1412
  });
1089
1413
 
1090
1414
  // src/tab-strip.ts
1091
- var CSS4 = `
1415
+ var CSS6 = `
1092
1416
  :host { display: flex; align-items: stretch; min-width: 0; min-height: 28px; gap: 2px; padding: 0 6px;
1093
1417
  background: var(--office-surface, #f3f4f6); color: var(--office-muted-foreground, #6b7280);
1094
1418
  border-top: 1px solid var(--office-border, #d1d5db); font-size: 12px;
@@ -1097,6 +1421,8 @@ var CSS4 = `
1097
1421
  min-height: var(--office-target-size, 26px);
1098
1422
  padding: 0; border: 0; background: transparent; color: inherit; cursor: pointer; }
1099
1423
  .step:disabled { opacity: .4; cursor: default; }
1424
+ .add { font-size: 18px; line-height: 1; }
1425
+ .add[hidden] { display: none; }
1100
1426
  .list { display: flex; align-items: stretch; min-width: 0; overflow-x: auto; scrollbar-width: thin; }
1101
1427
  .tab { flex: none; max-width: 220px; min-height: var(--office-target-size, 26px); margin-top: -1px;
1102
1428
  padding: 2px 14px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
@@ -1115,16 +1441,25 @@ svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width:
1115
1441
  `;
1116
1442
  var defineTabStrip = definer("office-ui-tab-strip", () => {
1117
1443
  class OfficeUiTabStrip extends HTMLElement {
1118
- static observedAttributes = ["label", "disabled", "selected", "previous-label", "next-label"];
1444
+ static observedAttributes = [
1445
+ "label",
1446
+ "disabled",
1447
+ "selected",
1448
+ "previous-label",
1449
+ "next-label",
1450
+ "add-label",
1451
+ "add-disabled"
1452
+ ];
1119
1453
  items = [];
1120
1454
  list;
1121
1455
  previous;
1122
1456
  next;
1457
+ add;
1123
1458
  constructor() {
1124
1459
  super();
1125
1460
  const doc = this.ownerDocument;
1126
1461
  const root = this.attachShadow({ mode: "open" });
1127
- attachStyles(root, controlCss(CSS4));
1462
+ attachStyles(root, controlCss(CSS6));
1128
1463
  const step = (name, icon) => {
1129
1464
  const button = doc.createElement("button");
1130
1465
  button.type = "button";
@@ -1141,7 +1476,14 @@ var defineTabStrip = definer("office-ui-tab-strip", () => {
1141
1476
  this.list = doc.createElement("div");
1142
1477
  this.list.className = "list";
1143
1478
  this.list.setAttribute("role", "tablist");
1144
- root.append(this.previous, this.next, this.list);
1479
+ this.add = doc.createElement("button");
1480
+ this.add.type = "button";
1481
+ this.add.className = "step add";
1482
+ this.add.textContent = "+";
1483
+ this.add.addEventListener("click", () => {
1484
+ if (!this.add.disabled) emit(this, "office-command", { command: "tab-add" });
1485
+ });
1486
+ root.append(this.previous, this.next, this.list, this.add);
1145
1487
  this.previous.addEventListener("click", () => this.move(-1, false));
1146
1488
  this.next.addEventListener("click", () => this.move(1, false));
1147
1489
  this.list.addEventListener("click", (event) => {
@@ -1244,6 +1586,11 @@ var defineTabStrip = definer("office-ui-tab-strip", () => {
1244
1586
  if (selected) tab.scrollIntoView?.({ block: "nearest", inline: "nearest" });
1245
1587
  }
1246
1588
  this.previous.disabled = disabled || index <= 0;
1589
+ const addLabel = this.getAttribute("add-label");
1590
+ this.add.hidden = !addLabel;
1591
+ this.add.disabled = disabled || this.hasAttribute("add-disabled");
1592
+ this.add.setAttribute("aria-label", addLabel ?? "Add");
1593
+ this.add.title = this.getAttribute("add-title") ?? addLabel ?? "";
1247
1594
  this.next.disabled = disabled || index < 0 || index >= this.items.length - 1;
1248
1595
  }
1249
1596
  }
@@ -1251,7 +1598,7 @@ var defineTabStrip = definer("office-ui-tab-strip", () => {
1251
1598
  });
1252
1599
 
1253
1600
  // src/zoom-slider.ts
1254
- var CSS5 = `
1601
+ var CSS7 = `
1255
1602
  :host { display: inline-flex; align-items: center; gap: 2px; font-size: 11px;
1256
1603
  color: var(--office-foreground, #1f2937); font-family: var(--office-font, system-ui, sans-serif); }
1257
1604
  button { box-sizing: border-box; display: inline-grid; place-items: center; min-width: var(--office-target-size, 24px);
@@ -1287,7 +1634,7 @@ var defineZoomSlider = definer("office-ui-zoom-slider", () => {
1287
1634
  super();
1288
1635
  const doc = this.ownerDocument;
1289
1636
  const root = this.attachShadow({ mode: "open" });
1290
- attachStyles(root, controlCss(CSS5));
1637
+ attachStyles(root, controlCss(CSS7));
1291
1638
  const button = (name, text) => {
1292
1639
  const element = doc.createElement("button");
1293
1640
  element.type = "button";
@@ -1391,7 +1738,10 @@ var CONTROL_DEFINERS = [
1391
1738
  defineRibbonGroup,
1392
1739
  defineRibbonStack,
1393
1740
  defineMenuItem,
1741
+ defineMenuSeparator,
1394
1742
  defineMenuButton,
1743
+ defineContextMenu,
1744
+ defineCommandSearch,
1395
1745
  defineToolbar,
1396
1746
  defineDialog,
1397
1747
  defineStatusBar,
@@ -1403,4 +1753,4 @@ function registerControls(registry) {
1403
1753
  for (const define of CONTROL_DEFINERS) define(registry);
1404
1754
  }
1405
1755
 
1406
- export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineDialog, defineMenuButton, defineMenuItem, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls };
1756
+ export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineMenuButton, defineMenuItem, defineMenuSeparator, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls };
@@ -0,0 +1,16 @@
1
+ /** One searchable command: label and optional keywords; disabled commands show their reason. */
2
+ export interface OfficeSearchCommand {
3
+ id: string;
4
+ label: string;
5
+ keywords?: string;
6
+ disabled?: boolean;
7
+ title?: string;
8
+ }
9
+ /**
10
+ * Office's "Tell me what you want to do" box. Set `commands` (`OfficeSearchCommand[]`); typing
11
+ * lists up to `limit` (default 8) matches by label or keywords in a top-layer listbox.
12
+ * ArrowUp/Down move, Enter or click run an enabled match (`office-command` `{ command }`),
13
+ * Escape closes then clears. Disabled matches stay visible with their reason. Attributes:
14
+ * `placeholder`, `label` (accessible name), `limit`. Setting `commands` never emits.
15
+ */
16
+ export declare const defineCommandSearch: import("./registry.js").Definer;
@@ -0,0 +1,10 @@
1
+ /** A `role="separator"` rule between groups of menu items. */
2
+ export declare const defineMenuSeparator: import("./registry.js").Definer;
3
+ /**
4
+ * Right-click menu of `office-ui-menu-item` and `office-ui-menu-separator` children (Office's
5
+ * shape and text context menus). `openAt(x, y)` shows it at client coordinates in the popover
6
+ * top layer, kept inside the viewport; `close()` hides it. Arrow keys, Home and End move between
7
+ * enabled items; Escape and Tab close and return focus to the element that was focused. Choosing
8
+ * an item closes the menu and its `office-command` bubbles to the host. `label` names the menu.
9
+ */
10
+ export declare const defineContextMenu: import("./registry.js").Definer;
@@ -2,19 +2,22 @@ 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 { defineCommandSearch } from './command-search.js';
6
+ import { defineContextMenu, defineMenuSeparator } from './context-menu.js';
5
7
  import { defineMenuButton, defineMenuItem } from './menu.js';
6
8
  import { defineRibbonGroup, defineRibbonStack, defineToolbar } from './ribbon.js';
7
9
  import { defineSelect } from './select.js';
8
10
  import { defineStatusBar, defineStatusItem } from './status-bar.js';
9
11
  import { defineTabStrip } from './tab-strip.js';
10
12
  import { defineZoomSlider } from './zoom-slider.js';
11
- export { defineButton, defineCheckbox, defineDialog, defineIcon, defineMenuButton, defineMenuItem, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, };
13
+ export { defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineIcon, defineMenuButton, defineMenuItem, defineMenuSeparator, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, };
12
14
  export type { OfficeCommandEvent } from './button.js';
13
15
  export type { OfficeDialogCloseEvent, OfficeDialogCloseReason } from './dialog.js';
14
16
  export type { OfficeSelectOption } from './select.js';
15
17
  export type { OfficeStatusActivateEvent } from './status-bar.js';
16
18
  export type { OfficeTab, OfficeTabSelectEvent } from './tab-strip.js';
19
+ export type { OfficeSearchCommand } from './command-search.js';
17
20
  /** Every control of this entry. */
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];
21
+ 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];
19
22
  /** Idempotent, SSR-safe (no-op without a DOM). */
20
23
  export declare function registerControls(registry?: CustomElementRegistry): void;
package/dist/controls.js CHANGED
@@ -1,3 +1,3 @@
1
- export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineDialog, defineMenuButton, defineMenuItem, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls } from './chunk-RYUUWQ7F.js';
1
+ export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineMenuButton, defineMenuItem, defineMenuSeparator, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls } from './chunk-RT6XKAM4.js';
2
2
  export { defineIcon } from './chunk-PCSUQPMC.js';
3
3
  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-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"];
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-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-RYUUWQ7F.js';
4
- export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineDialog, defineMenuButton, defineMenuItem, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls } from './chunk-RYUUWQ7F.js';
3
+ import { registerControls } from './chunk-RT6XKAM4.js';
4
+ export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineMenuButton, defineMenuItem, defineMenuSeparator, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls } from './chunk-RT6XKAM4.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';
@@ -20,6 +20,9 @@ var OFFICE_UI_TAGS = [
20
20
  "office-ui-ribbon-stack",
21
21
  "office-ui-menu-item",
22
22
  "office-ui-menu-button",
23
+ "office-ui-menu-separator",
24
+ "office-ui-context-menu",
25
+ "office-ui-command-search",
23
26
  "office-ui-toolbar",
24
27
  "office-ui-dialog",
25
28
  "office-ui-status-bar",
@@ -13,6 +13,8 @@ export type OfficeTabSelectEvent = CustomEvent<{
13
13
  * keys move the selection; the tab list uses a roving tabindex. User selection emits
14
14
  * `office-tab-select` `{ id }` (cancelable); setting properties never emits.
15
15
  * Attributes: `label` (tab list name, default "Tabs"), `previous-label` and `next-label`
16
- * (step button names, default "Previous" and "Next"), `disabled`.
16
+ * (step button names, default "Previous" and "Next"), `disabled`, and `add-label` to show an
17
+ * add button after the tabs (Insert Page, New Sheet) that emits `office-command`
18
+ * `{ command: 'tab-add' }`; `add-disabled` and `add-title` disable and explain it.
17
19
  */
18
20
  export declare const defineTabStrip: import("./registry.js").Definer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ooxml-ui",
3
- "version": "0.4.1",
3
+ "version": "0.6.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",