ooxml-ui 0.25.0 → 0.27.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.
@@ -1,7 +1,7 @@
1
1
  import { createIconSvg, paintIcon, getIcon } from './chunk-HZ422LTR.js';
2
- import { initialsOf } from './chunk-XQWRKHC2.js';
3
- import { OfficeElement, controlStyles, definer, valueOn, flag, present, triState, withTokens } from './chunk-PCZJV6HQ.js';
4
- import { tok, OFFICE_TOKENS } from './chunk-HSJH6SUW.js';
2
+ import { initialsOf } from './chunk-V6W5DKJ6.js';
3
+ import { OfficeElement, controlStyles, definer, valueOn, flag, present, triState, withTokens } from './chunk-ZWBP2WTC.js';
4
+ import { tok, OFFICE_TOKENS } from './chunk-7Q7ZS2CC.js';
5
5
  import { html, LitElement, nothing, render, svg } from 'lit';
6
6
  import { ifDefined } from 'lit/directives/if-defined.js';
7
7
  import { repeat } from 'lit/directives/repeat.js';
@@ -1059,7 +1059,7 @@ var OfficeUiPrintPreview = class extends OfficeElement {
1059
1059
  var definePrintPreview = definer("office-ui-print-preview", () => OfficeUiPrintPreview);
1060
1060
 
1061
1061
  // raw-css:/home/runner/work/ooxml/ooxml/packages/ui/src/ribbon/ribbon-tabs.css?raw
1062
- var ribbon_tabs_default = ":host {\n display: block;\n min-width: 0;\n}\n.head {\n display: flex;\n align-items: stretch;\n min-width: 0;\n border-bottom: var(--office-border-width) solid var(--office-border);\n}\n.file {\n flex: none;\n min-height: var(--office-ribbon-tab-height);\n margin: 0 var(--office-space-1) 0 0;\n padding: var(--office-space-1-5) var(--office-space-4);\n border: 0;\n border-radius: 0;\n background: var(--office-accent);\n color: var(--office-accent-foreground);\n font: inherit;\n font-size: var(--office-font-size);\n cursor: pointer;\n}\n.file:hover {\n background: var(--office-accent-hover);\n}\n.file[hidden] {\n display: none;\n}\n[role='tablist'] {\n display: flex;\n flex: 0 1 auto;\n align-items: stretch;\n min-width: 0;\n overflow-x: auto;\n scrollbar-width: none;\n}\n[role='tab'] {\n position: relative;\n flex: none;\n min-height: var(--office-ribbon-tab-height);\n padding: var(--office-space-1-5) var(--office-space-3);\n border: 0;\n border-radius: 0;\n background: transparent;\n color: var(--office-muted-foreground);\n font: inherit;\n font-size: var(--office-font-size);\n cursor: pointer;\n}\n[role='tab']:hover {\n color: var(--office-foreground);\n background: var(--office-surface);\n}\n[role='tab'][aria-selected='true'] {\n color: var(--office-accent);\n}\n[role='tab'][aria-selected='true']::after {\n content: '';\n position: absolute;\n inset: auto 0 0;\n height: var(--office-tab-indicator-width);\n background: var(--office-accent);\n}\n.file:focus-visible,\n[role='tab']:focus-visible {\n outline: var(--office-focus-width) solid var(--office-ring);\n outline-offset: calc(-1 * var(--office-focus-width));\n}\n::slotted([slot='search']) {\n align-self: center;\n}\n\n@media (max-width: 760px), (pointer: coarse) {\n .file,\n [role='tab'] {\n min-height: var(--office-target-size-touch);\n }\n .file {\n min-width: var(--office-ribbon-file-min-width);\n }\n}\n@media (forced-colors: active) {\n .file {\n border: var(--office-border-width) solid ButtonText;\n }\n [role='tab'][aria-selected='true']::after {\n background: Highlight;\n }\n}\n";
1062
+ var ribbon_tabs_default = ":host {\n display: block;\n min-width: 0;\n}\n.head {\n display: flex;\n align-items: stretch;\n min-width: 0;\n border-bottom: var(--office-border-width) solid var(--office-border);\n}\n.file {\n flex: none;\n min-height: var(--office-ribbon-tab-height);\n margin: 0 var(--office-space-1) 0 0;\n padding: var(--office-space-1-5) var(--office-space-4);\n border: 0;\n border-radius: 0;\n background: var(--office-accent);\n color: var(--office-accent-foreground);\n font: inherit;\n font-size: var(--office-font-size);\n cursor: pointer;\n}\n.file:hover {\n background: var(--office-accent-hover);\n}\n.file[hidden] {\n display: none;\n}\n[role='tablist'] {\n display: flex;\n flex: 0 1 auto;\n align-items: stretch;\n min-width: 0;\n overflow-x: auto;\n scrollbar-width: none;\n}\n[role='tab'] {\n position: relative;\n flex: none;\n min-height: var(--office-ribbon-tab-height);\n padding: var(--office-space-1-5) var(--office-space-3);\n border: 0;\n border-radius: 0;\n background: transparent;\n color: var(--office-muted-foreground);\n font: inherit;\n font-size: var(--office-font-size);\n cursor: pointer;\n}\n[role='tab']:hover {\n color: var(--office-foreground);\n background: var(--office-surface);\n}\n[role='tab'][aria-selected='true'] {\n color: var(--office-accent);\n}\n[role='tab'][aria-selected='true']::after {\n content: '';\n position: absolute;\n inset: auto 0 0;\n height: var(--office-tab-indicator-width);\n background: var(--office-accent);\n}\n[role='tab'][data-contextual],\n[role='tab'][data-contextual][aria-selected='true'] {\n color: var(--office-ribbon-contextual);\n}\n[role='tab'][data-contextual][aria-selected='true']::after {\n background: var(--office-ribbon-contextual);\n}\n.file:focus-visible,\n[role='tab']:focus-visible {\n outline: var(--office-focus-width) solid var(--office-ring);\n outline-offset: calc(-1 * var(--office-focus-width));\n}\n::slotted([slot='search']) {\n align-self: center;\n}\n\n.collapse {\n flex: none;\n align-self: center;\n width: var(--office-target-size);\n height: var(--office-target-size);\n margin-inline: auto var(--office-space-1);\n padding: 0;\n border: 0;\n border-radius: var(--office-radius-sm);\n background: none;\n color: var(--office-muted-foreground);\n cursor: pointer;\n}\n.collapse[hidden] {\n display: none;\n}\n.collapse:hover {\n background: var(--office-surface);\n}\n.collapse:focus-visible {\n outline: var(--office-focus-width) solid var(--office-ring);\n}\n.collapse .glyph {\n width: var(--office-icon-size);\n height: var(--office-icon-size);\n fill: none;\n stroke: currentColor;\n stroke-width: 1.5;\n transform: rotate(90deg);\n}\n.collapse[aria-pressed='true'] .glyph {\n transform: rotate(-90deg);\n}\n:host([collapsed][peek]) {\n position: relative;\n}\n:host([collapsed][peek]) ::slotted([data-ribbon-tab]:not([hidden])) {\n position: absolute;\n inset: 100% 0 auto;\n z-index: 60;\n border-bottom: var(--office-border-width) solid var(--office-border);\n background: var(--office-background);\n box-shadow: var(--office-shadow);\n}\n\n@media (max-width: 760px), (pointer: coarse) {\n .file,\n [role='tab'] {\n min-height: var(--office-target-size-touch);\n }\n .file {\n min-width: var(--office-ribbon-file-min-width);\n }\n}\n@media (forced-colors: active) {\n .file {\n border: var(--office-border-width) solid ButtonText;\n }\n [role='tab'][aria-selected='true']::after {\n background: Highlight;\n }\n}\n";
1063
1063
 
1064
1064
  // src/ribbon/ribbon-tabs.ts
1065
1065
  var OfficeUiRibbon = class extends OfficeElement {
@@ -1072,6 +1072,10 @@ var OfficeUiRibbon = class extends OfficeElement {
1072
1072
  noFile: { attribute: "no-file", ...flag },
1073
1073
  fileExpanded: { attribute: "file-expanded", type: String },
1074
1074
  fileKeytip: { attribute: "file-keytip", type: String },
1075
+ collapsible: flag,
1076
+ collapsed: flag,
1077
+ peek: flag,
1078
+ collapseLabel: { attribute: "collapse-label", type: String },
1075
1079
  tabs: { state: true }
1076
1080
  };
1077
1081
  chosen = null;
@@ -1082,6 +1086,10 @@ var OfficeUiRibbon = class extends OfficeElement {
1082
1086
  this.noFile = false;
1083
1087
  this.fileExpanded = null;
1084
1088
  this.fileKeytip = null;
1089
+ this.collapsible = false;
1090
+ this.collapsed = false;
1091
+ this.peek = false;
1092
+ this.collapseLabel = null;
1085
1093
  this.tabs = [];
1086
1094
  }
1087
1095
  get selected() {
@@ -1102,6 +1110,28 @@ var OfficeUiRibbon = class extends OfficeElement {
1102
1110
  focusTab() {
1103
1111
  this.renderRoot.querySelector('[role="tab"][aria-selected="true"]')?.focus();
1104
1112
  }
1113
+ /** The rendered tab button of `tab`, or null before the first render. */
1114
+ tabButton(tab) {
1115
+ return this.renderRoot.querySelector(`[role="tab"][data-tab="${tab}"]`);
1116
+ }
1117
+ /** The rendered File button, or null before the first render. */
1118
+ fileButton() {
1119
+ return this.renderRoot.querySelector(".file");
1120
+ }
1121
+ /**
1122
+ * Hide or show the tab of panel `tab` for one reason (`contextual`, `custom`, ...). The tab shows
1123
+ * only while no reason hides it, so a table's tools and a customisation do not undo each other.
1124
+ */
1125
+ setTabHidden(tab, reason, hidden) {
1126
+ const panel = this.panels().find((item) => item.dataset.ribbonTab === tab);
1127
+ if (!panel) return;
1128
+ const reasons = new Set((panel.dataset.tabHiddenBy ?? "").split(" ").filter(Boolean));
1129
+ if (hidden) reasons.add(reason);
1130
+ else reasons.delete(reason);
1131
+ if (reasons.size) panel.dataset.tabHiddenBy = [...reasons].join(" ");
1132
+ else delete panel.dataset.tabHiddenBy;
1133
+ panel.toggleAttribute("data-tab-hidden", reasons.size > 0);
1134
+ }
1105
1135
  panels() {
1106
1136
  return [...this.children].filter(
1107
1137
  (child) => child instanceof HTMLElement && !!child.dataset.ribbonTab
@@ -1120,6 +1150,7 @@ var OfficeUiRibbon = class extends OfficeElement {
1120
1150
  return {
1121
1151
  id,
1122
1152
  label: panel.dataset.label ?? id,
1153
+ contextual: panel.dataset.contextual !== void 0,
1123
1154
  ...panel.dataset.tabKeytip ? {
1124
1155
  keytip: panel.dataset.tabKeytip,
1125
1156
  ...panel.id ? { keytipPanel: panel.id } : {}
@@ -1127,7 +1158,35 @@ var OfficeUiRibbon = class extends OfficeElement {
1127
1158
  };
1128
1159
  });
1129
1160
  }
1161
+ setCollapsed(value) {
1162
+ if (value === this.collapsed) return;
1163
+ this.collapsed = value;
1164
+ this.peek = false;
1165
+ this.fire("office-ribbon-collapse", { collapsed: value });
1166
+ }
1167
+ onOutside = (event) => {
1168
+ if (!event.composedPath().includes(this)) this.peek = false;
1169
+ };
1170
+ onTabDoubleClick(event) {
1171
+ if (this.collapsible && event.target.closest?.('[role="tab"]'))
1172
+ this.setCollapsed(!this.collapsed);
1173
+ }
1174
+ /** A command run from an overlaid panel closes it; a menu opener keeps it open. */
1175
+ onPanelClick(event) {
1176
+ if (!this.collapsed || !this.peek) return;
1177
+ const button = event.target.closest?.("button");
1178
+ if (button && !button.hasAttribute("aria-haspopup") && !button.closest('[role="tablist"]'))
1179
+ this.peek = false;
1180
+ }
1181
+ willUpdate(changed) {
1182
+ if (changed.has("peek")) {
1183
+ const doc = this.ownerDocument;
1184
+ if (this.peek) doc.addEventListener("pointerdown", this.onOutside, true);
1185
+ else doc.removeEventListener("pointerdown", this.onOutside, true);
1186
+ }
1187
+ }
1130
1188
  choose(tab) {
1189
+ if (this.collapsible && this.collapsed) this.peek = true;
1131
1190
  if (tab === this.selected) return;
1132
1191
  if (this.fire("office-ribbon-select", { tab }, true)) this.selected = tab;
1133
1192
  }
@@ -1135,6 +1194,13 @@ var OfficeUiRibbon = class extends OfficeElement {
1135
1194
  const tab = event.target.closest?.('[role="tab"]');
1136
1195
  if (tab) this.choose(tab.dataset.tab);
1137
1196
  }
1197
+ /** Ctrl+F1 and Escape work from anywhere in the ribbon, panels included. */
1198
+ onHostKey = (event) => {
1199
+ if (this.collapsible && event.key === "F1" && event.ctrlKey) {
1200
+ event.preventDefault();
1201
+ this.setCollapsed(!this.collapsed);
1202
+ } else if (event.key === "Escape" && this.peek) this.peek = false;
1203
+ };
1138
1204
  onKey(event) {
1139
1205
  const tabs = [...this.renderRoot.querySelectorAll('[role="tab"]')];
1140
1206
  const focused = event.target.closest?.('[role="tab"]');
@@ -1151,6 +1217,7 @@ var OfficeUiRibbon = class extends OfficeElement {
1151
1217
  connectedCallback() {
1152
1218
  this.build();
1153
1219
  super.connectedCallback();
1220
+ this.addEventListener("keydown", this.onHostKey);
1154
1221
  const Observer = this.ownerDocument.defaultView?.MutationObserver;
1155
1222
  this.watcher = Observer ? new Observer(() => this.build()) : void 0;
1156
1223
  this.watcher?.observe(this, {
@@ -1161,13 +1228,17 @@ var OfficeUiRibbon = class extends OfficeElement {
1161
1228
  }
1162
1229
  disconnectedCallback() {
1163
1230
  super.disconnectedCallback();
1231
+ this.removeEventListener("keydown", this.onHostKey);
1164
1232
  this.watcher?.disconnect();
1233
+ this.ownerDocument.removeEventListener("pointerdown", this.onOutside, true);
1165
1234
  this.watcher = void 0;
1166
1235
  }
1167
1236
  /** Exactly one panel shows. */
1168
1237
  updated() {
1169
1238
  const selected = this.selected;
1170
- for (const panel of this.panels()) panel.hidden = panel.dataset.ribbonTab !== selected;
1239
+ const hideAll = this.collapsed && !this.peek;
1240
+ for (const panel of this.panels())
1241
+ panel.hidden = hideAll || panel.dataset.ribbonTab !== selected;
1171
1242
  }
1172
1243
  render() {
1173
1244
  const selected = this.selected;
@@ -1191,6 +1262,7 @@ var OfficeUiRibbon = class extends OfficeElement {
1191
1262
  aria-label=${this.label ?? "Ribbon"}
1192
1263
  @click=${this.onTabClick}
1193
1264
  @keydown=${this.onKey}
1265
+ @dblclick=${this.onTabDoubleClick}
1194
1266
  >
1195
1267
  ${this.tabs.map(
1196
1268
  (tab) => html`<button
@@ -1198,6 +1270,7 @@ var OfficeUiRibbon = class extends OfficeElement {
1198
1270
  role="tab"
1199
1271
  id="tab-${tab.id}"
1200
1272
  data-tab=${tab.id}
1273
+ data-contextual=${ifDefined(tab.contextual ? "" : void 0)}
1201
1274
  data-keytip=${ifDefined(tab.keytip)}
1202
1275
  data-keytip-panel=${ifDefined(tab.keytipPanel)}
1203
1276
  aria-selected=${String(tab.id === selected)}
@@ -1208,8 +1281,20 @@ var OfficeUiRibbon = class extends OfficeElement {
1208
1281
  </div>
1209
1282
  <slot name="search"></slot>
1210
1283
  <slot name="end"></slot>
1284
+ <button
1285
+ class="collapse"
1286
+ part="collapse"
1287
+ type="button"
1288
+ aria-pressed=${String(this.collapsed)}
1289
+ aria-label=${this.collapseLabel ?? "Collapse the ribbon"}
1290
+ title=${this.collapseLabel ?? "Collapse the ribbon"}
1291
+ ?hidden=${!this.collapsible}
1292
+ @click=${() => this.setCollapsed(!this.collapsed)}
1293
+ >
1294
+ ${glyph("chevronLeft", "glyph")}
1295
+ </button>
1211
1296
  </div>
1212
- <slot @slotchange=${this.build}></slot>
1297
+ <slot @slotchange=${this.build} @click=${this.onPanelClick}></slot>
1213
1298
  `;
1214
1299
  }
1215
1300
  };
@@ -4517,6 +4602,12 @@ function attachKeyTips(scope) {
4517
4602
  view?.addEventListener("blur", onPointer);
4518
4603
  const handle = {
4519
4604
  active: false,
4605
+ start() {
4606
+ if (!handle.active) start();
4607
+ },
4608
+ stop() {
4609
+ stop();
4610
+ },
4520
4611
  dispose() {
4521
4612
  stop();
4522
4613
  target.removeEventListener("keydown", onKeyDown, true);
@@ -4529,6 +4620,197 @@ function attachKeyTips(scope) {
4529
4620
  return handle;
4530
4621
  }
4531
4622
 
4623
+ // src/ribbon/keytip-run.ts
4624
+ var clash = (used, key) => used.some((other) => other.startsWith(key) || key.startsWith(other));
4625
+ function assignKeyTips(targets, label) {
4626
+ const used = [];
4627
+ for (const target of targets) {
4628
+ if (target.key && !clash(used, target.key)) used.push(target.key);
4629
+ else target.key = "";
4630
+ }
4631
+ for (const target of targets.filter((item) => !item.key)) {
4632
+ const source = label(target).toUpperCase().replace(/[^A-Z0-9]/g, "") || "ZZ";
4633
+ let key = "";
4634
+ for (let a = 0; a < source.length && !key; a++)
4635
+ for (let b = a + 1; b < source.length && !key; b++) {
4636
+ const candidate = source[a] + source[b];
4637
+ if (!clash(used, candidate)) key = candidate;
4638
+ }
4639
+ for (let n = 10; !key; n++) if (!clash(used, String(n))) key = String(n);
4640
+ used.push(key);
4641
+ target.key = key;
4642
+ }
4643
+ }
4644
+ function runKeyTips(root, targets, onDone, className) {
4645
+ const doc = root.ownerDocument;
4646
+ const shown = targets.map((target) => {
4647
+ const node = doc.createElement("span");
4648
+ node.className = className;
4649
+ node.textContent = target.key;
4650
+ node.setAttribute("aria-hidden", "true");
4651
+ const box = target.element.getBoundingClientRect();
4652
+ node.style.position = "fixed";
4653
+ node.style.left = `${box.left + box.width / 2}px`;
4654
+ node.style.top = `${box.bottom - 10}px`;
4655
+ root.append(node);
4656
+ return { node, target };
4657
+ });
4658
+ let typed = "";
4659
+ const stop = () => {
4660
+ for (const { node } of shown) node.remove();
4661
+ root.removeEventListener("keydown", onKey, true);
4662
+ root.removeEventListener("pointerdown", stop, true);
4663
+ onDone();
4664
+ };
4665
+ function onKey(event) {
4666
+ if (event.key === "Alt" || event.key === "Shift") return;
4667
+ event.preventDefault();
4668
+ event.stopPropagation();
4669
+ if (event.key === "Escape" || event.ctrlKey || event.metaKey) return stop();
4670
+ typed += event.key.toUpperCase();
4671
+ const matches = shown.filter(({ target }) => target.key.startsWith(typed));
4672
+ if (!matches.length) return stop();
4673
+ for (const { node, target } of shown) node.hidden = !target.key.startsWith(typed);
4674
+ const exact = matches.find(({ target }) => target.key === typed);
4675
+ if (exact) {
4676
+ stop();
4677
+ exact.target.activate();
4678
+ }
4679
+ }
4680
+ root.addEventListener("keydown", onKey, true);
4681
+ root.addEventListener("pointerdown", stop, true);
4682
+ return stop;
4683
+ }
4684
+
4685
+ // src/ribbon/overflow.ts
4686
+ function createRibbonOverflow(options) {
4687
+ let closeOpen;
4688
+ function overflowButton(group) {
4689
+ const doc = group.ownerDocument;
4690
+ const button = doc.createElement("button");
4691
+ button.type = "button";
4692
+ button.className = "ribbon-overflow-button ribbon-large";
4693
+ button.setAttribute("aria-haspopup", "true");
4694
+ const name = options.groupIcons[group.dataset.label ?? ""] ?? options.fallbackIcon;
4695
+ button.append(
4696
+ options.icon(doc, name, 28),
4697
+ doc.createElement("span"),
4698
+ options.icon(doc, "caret", 12)
4699
+ );
4700
+ button.addEventListener("mousedown", (event) => event.preventDefault());
4701
+ button.addEventListener("click", () => toggleGroup(group, button));
4702
+ return button;
4703
+ }
4704
+ function toggleGroup(group, button) {
4705
+ const wasOpen = button.getAttribute("aria-expanded") === "true";
4706
+ closeOpen?.();
4707
+ if (wasOpen) return;
4708
+ const ribbon = group.closest(options.ribbon);
4709
+ if (!ribbon) return;
4710
+ const doc = group.ownerDocument;
4711
+ const pop = doc.createElement("div");
4712
+ pop.className = "ribbon-popover ribbon-overflow-panel";
4713
+ const content = doc.createElement("div");
4714
+ content.className = "ribbon-group ribbon-overflow-content";
4715
+ content.dataset.label = group.dataset.label ?? "";
4716
+ content.dataset.caption = group.dataset.caption ?? "";
4717
+ content.setAttribute("role", "group");
4718
+ content.setAttribute("aria-label", group.getAttribute("aria-label") ?? "");
4719
+ if (options.naturalWidth) content.style.width = `${group.dataset.natural ?? 200}px`;
4720
+ const moved = [...group.children].filter((child) => child !== button);
4721
+ content.append(...moved);
4722
+ pop.append(content);
4723
+ ribbon.append(pop);
4724
+ const box = button.getBoundingClientRect();
4725
+ pop.style.top = `${box.bottom + 2}px`;
4726
+ const width = doc.defaultView?.innerWidth ?? 1024;
4727
+ pop.style.left = `${Math.max(4, Math.min(box.left, width - pop.offsetWidth - 8))}px`;
4728
+ button.setAttribute("aria-expanded", "true");
4729
+ const outside = (event) => {
4730
+ const path = event.composedPath();
4731
+ const inside = path.some(
4732
+ (node) => node instanceof HTMLElement && node.classList.contains("ribbon-popover")
4733
+ );
4734
+ if (!inside && !path.includes(button)) closeOpen?.();
4735
+ };
4736
+ const escape = (event) => {
4737
+ if (event.key !== "Escape") return;
4738
+ closeOpen?.();
4739
+ button.focus();
4740
+ };
4741
+ pop.addEventListener("click", (event) => {
4742
+ if (event.target.closest(options.command)) setTimeout(() => closeOpen?.(), 0);
4743
+ });
4744
+ doc.addEventListener("pointerdown", outside, true);
4745
+ doc.addEventListener("keydown", escape, true);
4746
+ closeOpen = () => {
4747
+ doc.removeEventListener("pointerdown", outside, true);
4748
+ doc.removeEventListener("keydown", escape, true);
4749
+ group.prepend(...moved);
4750
+ pop.remove();
4751
+ button.removeAttribute("aria-expanded");
4752
+ closeOpen = void 0;
4753
+ };
4754
+ }
4755
+ function reserveCaptions(panel) {
4756
+ const sizer = panel.ownerDocument.createElement("span");
4757
+ sizer.className = "ribbon-caption-sizer";
4758
+ panel.append(sizer);
4759
+ for (const group of panel.querySelectorAll(":scope > .ribbon-group")) {
4760
+ sizer.textContent = group.dataset.caption ?? "";
4761
+ const launcher = group.querySelector(":scope > .ribbon-launcher") ? 32 : 12;
4762
+ group.style.minWidth = `${Math.ceil(sizer.offsetWidth) + launcher}px`;
4763
+ }
4764
+ sizer.remove();
4765
+ }
4766
+ function fitPanel(panel) {
4767
+ if (panel.hidden || !panel.clientWidth) return;
4768
+ if (closeOpen && panel.dataset.fittedWidth === String(panel.clientWidth)) return;
4769
+ panel.dataset.fittedWidth = String(panel.clientWidth);
4770
+ closeOpen?.();
4771
+ if (options.reserveCaptions) reserveCaptions(panel);
4772
+ for (const group of panel.querySelectorAll(".ribbon-group[data-collapsed]"))
4773
+ group.removeAttribute("data-collapsed");
4774
+ const groups = [...panel.querySelectorAll(":scope > .ribbon-group")].filter(
4775
+ (group) => group.offsetWidth > 0
4776
+ );
4777
+ if (options.naturalWidth)
4778
+ for (const group of groups) group.dataset.natural = String(Math.ceil(group.offsetWidth));
4779
+ for (let at = groups.length - 1; at >= 0 && panel.scrollWidth > panel.clientWidth + 1; at--) {
4780
+ const group = groups[at];
4781
+ let button = group.querySelector(":scope > .ribbon-overflow-button");
4782
+ if (!button) {
4783
+ button = overflowButton(group);
4784
+ group.append(button);
4785
+ }
4786
+ const label = group.dataset.caption ?? group.dataset.label ?? "";
4787
+ const span = button.querySelector("span");
4788
+ if (options.setCaption) options.setCaption(span, label);
4789
+ else span.textContent = label;
4790
+ button.setAttribute("aria-label", label);
4791
+ button.title = label;
4792
+ group.setAttribute("data-collapsed", "");
4793
+ }
4794
+ }
4795
+ const panelsOf = (root) => root.querySelectorAll(".ribbon-panel");
4796
+ return {
4797
+ fitPanel,
4798
+ attach(root) {
4799
+ if (typeof ResizeObserver === "undefined") return () => {
4800
+ };
4801
+ const observer = new ResizeObserver((entries) => {
4802
+ for (const entry of entries) fitPanel(entry.target);
4803
+ });
4804
+ for (const panel of panelsOf(root)) observer.observe(panel);
4805
+ return () => observer.disconnect();
4806
+ },
4807
+ refit(root) {
4808
+ for (const panel of panelsOf(root)) fitPanel(panel);
4809
+ },
4810
+ close: () => closeOpen?.()
4811
+ };
4812
+ }
4813
+
4532
4814
  // src/controls.ts
4533
4815
  var CONTROL_DEFINERS = [
4534
4816
  defineIcon,
@@ -4571,4 +4853,4 @@ function registerControls(registry) {
4571
4853
  for (const define of CONTROL_DEFINERS) define(registry);
4572
4854
  }
4573
4855
 
4574
- export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, EMPTY_MENU_STATE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, OFFICE_TITLE_BAR_SEARCH_LIMIT, OFFICE_TOAST_VISIBLE_LIMIT, attachKeyTips, clampFlyoutPosition, clampOptionNumber, clearOfficeProfile, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineDialogFooter, defineFindBar, defineGallery, defineIcon, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePasteOptions, definePrintPreview, defineRadio, defineReadOnlyBanner, defineRibbon, defineRibbonGroup, defineRibbonSection, defineRibbonStack, defineRibbonToggle, defineRuler, defineSearchField, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineTitleBar, defineToasts, defineToolbar, defineZoomSlider, glyph, nextEnabledIndex, parseSvgPreview, profileInitials, readOfficeProfile, registerControls, rulerDivisions, sanitizeOfficeProfile, typeAheadIndex, writeOfficeProfile };
4856
+ export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, EMPTY_MENU_STATE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, OFFICE_TITLE_BAR_SEARCH_LIMIT, OFFICE_TOAST_VISIBLE_LIMIT, assignKeyTips, attachKeyTips, clampFlyoutPosition, clampOptionNumber, clearOfficeProfile, createRibbonOverflow, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineDialogFooter, defineFindBar, defineGallery, defineIcon, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePasteOptions, definePrintPreview, defineRadio, defineReadOnlyBanner, defineRibbon, defineRibbonGroup, defineRibbonSection, defineRibbonStack, defineRibbonToggle, defineRuler, defineSearchField, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineTitleBar, defineToasts, defineToolbar, defineZoomSlider, glyph, nextEnabledIndex, parseSvgPreview, profileInitials, readOfficeProfile, registerControls, rulerDivisions, runKeyTips, sanitizeOfficeProfile, typeAheadIndex, writeOfficeProfile };
@@ -1,4 +1,4 @@
1
- import { OfficeElement, controlStyles, definer } from './chunk-PCZJV6HQ.js';
1
+ import { OfficeElement, controlStyles, definer } from './chunk-ZWBP2WTC.js';
2
2
  import { html } from 'lit';
3
3
  import { normalizeHexColor } from 'ooxml-core/color';
4
4
 
@@ -126,6 +126,7 @@ var OFFICE_TOKENS = {
126
126
  "--office-switch-thumb-on": "var(--office-accent-foreground, #ffffff)",
127
127
  // Component: ribbon, status bar, tabs
128
128
  "--office-ribbon-tab-height": "35px",
129
+ "--office-ribbon-contextual": "var(--office-accent, #2563eb)",
129
130
  "--office-ribbon-group-gap": "2px",
130
131
  "--office-ribbon-group-height": "78px",
131
132
  "--office-ribbon-group-padding": "4px 8px 2px",
@@ -1,4 +1,4 @@
1
- import { OFFICE_TOKENS, TOUCH } from './chunk-HSJH6SUW.js';
1
+ import { OFFICE_TOKENS, TOUCH } from './chunk-7Q7ZS2CC.js';
2
2
 
3
3
  // src/theme-bridge.ts
4
4
  var SHADCN_NAMES = [
@@ -1,6 +1,6 @@
1
1
  import { registerIcon, getIcon } from './chunk-HZ422LTR.js';
2
- import { initialsOf } from './chunk-XQWRKHC2.js';
3
- import { definer, OfficeElement, controlStyles } from './chunk-PCZJV6HQ.js';
2
+ import { initialsOf } from './chunk-V6W5DKJ6.js';
3
+ import { definer, OfficeElement, controlStyles } from './chunk-ZWBP2WTC.js';
4
4
  import { html, nothing, unsafeCSS, svg } from 'lit';
5
5
  import { repeat } from 'lit/directives/repeat.js';
6
6
  import { live } from 'lit/directives/live.js';
@@ -1,4 +1,4 @@
1
- import { OfficeElement, controlStyles, definer } from './chunk-PCZJV6HQ.js';
1
+ import { OfficeElement, controlStyles, definer } from './chunk-ZWBP2WTC.js';
2
2
  import { html } from 'lit';
3
3
 
4
4
  // raw-css:/home/runner/work/ooxml/ooxml/packages/ui/src/presence/presence.css?raw
@@ -1,4 +1,4 @@
1
- import { TOUCH, tok, OFFICE_TOKENS } from './chunk-HSJH6SUW.js';
1
+ import { TOUCH, tok, OFFICE_TOKENS } from './chunk-7Q7ZS2CC.js';
2
2
  import { unsafeCSS, LitElement } from 'lit';
3
3
 
4
4
  // src/registry.ts
@@ -45,6 +45,10 @@ export { attachKeyTips, type KeyTipsHandle } from './keytips.js';
45
45
  export type { OfficeBackstageCloseEvent, OfficeBackstageItem, OfficeBackstageSelectEvent, } from './chrome/backstage.js';
46
46
  export type { OfficeFindInputEvent, OfficeFindStepEvent } from './chrome/find-bar.js';
47
47
  export type { OfficePrintPreviewPageEvent } from './chrome/print-preview.js';
48
+ export { assignKeyTips, runKeyTips } from './ribbon/keytip-run.js';
49
+ export type { KeyTipTarget } from './ribbon/keytip-run.js';
50
+ export { createRibbonOverflow } from './ribbon/overflow.js';
51
+ export type { RibbonOverflow, RibbonOverflowOptions } from './ribbon/overflow.js';
48
52
  export type { OfficeRibbonSelectEvent } from './ribbon/ribbon-tabs.js';
49
53
  export type { OfficeRibbonCommandView, OfficeRibbonGroupView } from './ribbon/ribbon-section.js';
50
54
  export { parseSvgPreview, type OfficeGalleryItem, type OfficeGalleryPickEvent, type OfficeGallerySection, type OfficeGalleryState, } from './ribbon/gallery.js';
package/dist/controls.js CHANGED
@@ -1,5 +1,5 @@
1
- export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, EMPTY_MENU_STATE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, OFFICE_TITLE_BAR_SEARCH_LIMIT, OFFICE_TOAST_VISIBLE_LIMIT, attachKeyTips, clampFlyoutPosition, clampOptionNumber, clearOfficeProfile, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineDialogFooter, defineFindBar, defineGallery, defineIcon, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePasteOptions, definePrintPreview, defineRadio, defineReadOnlyBanner, defineRibbon, defineRibbonGroup, defineRibbonSection, defineRibbonStack, defineRibbonToggle, defineRuler, defineSearchField, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineTitleBar, defineToasts, defineToolbar, defineZoomSlider, nextEnabledIndex, parseSvgPreview, profileInitials, readOfficeProfile, registerControls, rulerDivisions, sanitizeOfficeProfile, typeAheadIndex, writeOfficeProfile } from './chunk-SXCANAX7.js';
1
+ export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, EMPTY_MENU_STATE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, OFFICE_TITLE_BAR_SEARCH_LIMIT, OFFICE_TOAST_VISIBLE_LIMIT, assignKeyTips, attachKeyTips, clampFlyoutPosition, clampOptionNumber, clearOfficeProfile, createRibbonOverflow, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineDialogFooter, defineFindBar, defineGallery, defineIcon, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePasteOptions, definePrintPreview, defineRadio, defineReadOnlyBanner, defineRibbon, defineRibbonGroup, defineRibbonSection, defineRibbonStack, defineRibbonToggle, defineRuler, defineSearchField, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineTitleBar, defineToasts, defineToolbar, defineZoomSlider, nextEnabledIndex, parseSvgPreview, profileInitials, readOfficeProfile, registerControls, rulerDivisions, runKeyTips, sanitizeOfficeProfile, typeAheadIndex, writeOfficeProfile } from './chunk-4PBFBPOH.js';
2
2
  import './chunk-HZ422LTR.js';
3
- import './chunk-XQWRKHC2.js';
4
- import './chunk-PCZJV6HQ.js';
5
- import './chunk-HSJH6SUW.js';
3
+ import './chunk-V6W5DKJ6.js';
4
+ import './chunk-ZWBP2WTC.js';
5
+ import './chunk-7Q7ZS2CC.js';
package/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
- import { installOfficeUiTheme } from './chunk-PAPKST6W.js';
2
- export { THEME_CSS, THEME_STYLE_ID, installOfficeUiTheme, shadcnBridge, themeBridge } from './chunk-PAPKST6W.js';
3
- import { registerControls } from './chunk-SXCANAX7.js';
4
- export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, EMPTY_MENU_STATE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, OFFICE_TITLE_BAR_SEARCH_LIMIT, OFFICE_TOAST_VISIBLE_LIMIT, attachKeyTips, clampFlyoutPosition, clampOptionNumber, clearOfficeProfile, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineDialogFooter, defineFindBar, defineGallery, defineIcon, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePasteOptions, definePrintPreview, defineRadio, defineReadOnlyBanner, defineRibbon, defineRibbonGroup, defineRibbonSection, defineRibbonStack, defineRibbonToggle, defineRuler, defineSearchField, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineTitleBar, defineToasts, defineToolbar, defineZoomSlider, glyph, nextEnabledIndex, parseSvgPreview, profileInitials, readOfficeProfile, registerControls, rulerDivisions, sanitizeOfficeProfile, typeAheadIndex, writeOfficeProfile } from './chunk-SXCANAX7.js';
5
- import { defineSmartArt } from './chunk-RVZAYXTI.js';
6
- export { OfficeUiSmartArt, defineSmartArt, renderDiagramDrawing } from './chunk-RVZAYXTI.js';
7
- import { TEAMS_TAGS, registerTeams } from './chunk-TCBUC6BI.js';
8
- export { EMOJI, OfficeUiAppRail, OfficeUiAvatar, OfficeUiCallControls, OfficeUiCallGrid, OfficeUiChannelList, OfficeUiChatComposer, OfficeUiChatList, OfficeUiPrejoin, QUICK_REACTIONS, TEAMS_TAGS, TeamsElement, colorFor, dayLabel, defineAppRail, defineAvatar, defineCallControls, defineCallGrid, defineChannelList, defineChatComposer, defineChatList, definePrejoin, formatElapsed, formatSize, formatTime, icon, registerTeams, withStyles } from './chunk-TCBUC6BI.js';
1
+ import { installOfficeUiTheme } from './chunk-7QLJS57B.js';
2
+ export { THEME_CSS, THEME_STYLE_ID, installOfficeUiTheme, shadcnBridge, themeBridge } from './chunk-7QLJS57B.js';
3
+ import { registerControls } from './chunk-4PBFBPOH.js';
4
+ export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, EMPTY_MENU_STATE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, OFFICE_TITLE_BAR_SEARCH_LIMIT, OFFICE_TOAST_VISIBLE_LIMIT, assignKeyTips, attachKeyTips, clampFlyoutPosition, clampOptionNumber, clearOfficeProfile, createRibbonOverflow, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineDialogFooter, defineFindBar, defineGallery, defineIcon, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePasteOptions, definePrintPreview, defineRadio, defineReadOnlyBanner, defineRibbon, defineRibbonGroup, defineRibbonSection, defineRibbonStack, defineRibbonToggle, defineRuler, defineSearchField, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineTitleBar, defineToasts, defineToolbar, defineZoomSlider, glyph, nextEnabledIndex, parseSvgPreview, profileInitials, readOfficeProfile, registerControls, rulerDivisions, runKeyTips, sanitizeOfficeProfile, typeAheadIndex, writeOfficeProfile } from './chunk-4PBFBPOH.js';
5
+ import { defineSmartArt } from './chunk-4VQ6S7QJ.js';
6
+ export { OfficeUiSmartArt, defineSmartArt, renderDiagramDrawing } from './chunk-4VQ6S7QJ.js';
7
+ import { TEAMS_TAGS, registerTeams } from './chunk-LE4FTRWB.js';
8
+ export { EMOJI, OfficeUiAppRail, OfficeUiAvatar, OfficeUiCallControls, OfficeUiCallGrid, OfficeUiChannelList, OfficeUiChatComposer, OfficeUiChatList, OfficeUiPrejoin, QUICK_REACTIONS, TEAMS_TAGS, TeamsElement, colorFor, dayLabel, defineAppRail, defineAvatar, defineCallControls, defineCallGrid, defineChannelList, defineChatComposer, defineChatList, definePrejoin, formatElapsed, formatSize, formatTime, icon, registerTeams, withStyles } from './chunk-LE4FTRWB.js';
9
9
  export { createIconSvg, getIcon, listIcons, paintIcon, registerIcon } from './chunk-HZ422LTR.js';
10
- import { definePresence } from './chunk-XQWRKHC2.js';
11
- export { OfficeUiPresence, definePresence, initialsOf, participantsFromAwareness } from './chunk-XQWRKHC2.js';
12
- export { CONTRACT_REVISION, OfficeElement, controlStyles, flag, readFlag, triState, valueOn, withTokens } from './chunk-PCZJV6HQ.js';
13
- export { COMPACT, FOCUS_RING, OFFICE_TOKENS, TOUCH, tok } from './chunk-HSJH6SUW.js';
10
+ import { definePresence } from './chunk-V6W5DKJ6.js';
11
+ export { OfficeUiPresence, definePresence, initialsOf, participantsFromAwareness } from './chunk-V6W5DKJ6.js';
12
+ export { CONTRACT_REVISION, OfficeElement, controlStyles, flag, readFlag, triState, valueOn, withTokens } from './chunk-ZWBP2WTC.js';
13
+ export { COMPACT, FOCUS_RING, OFFICE_TOKENS, TOUCH, tok } from './chunk-7Q7ZS2CC.js';
14
14
 
15
15
  // src/index.ts
16
16
  var OFFICE_UI_TAGS = [
package/dist/keytips.d.ts CHANGED
@@ -12,6 +12,10 @@
12
12
  export interface KeyTipsHandle {
13
13
  /** Whether badges are currently shown. */
14
14
  readonly active: boolean;
15
+ /** Show the first level, as Alt does; for products that also start KeyTips from F10. */
16
+ start(): void;
17
+ /** Hide the badges. */
18
+ stop(): void;
15
19
  dispose(): void;
16
20
  }
17
21
  type Root = ShadowRoot | HTMLElement;
package/dist/presence.js CHANGED
@@ -1,3 +1,3 @@
1
- export { OfficeUiPresence, definePresence, initialsOf, participantsFromAwareness } from './chunk-XQWRKHC2.js';
2
- import './chunk-PCZJV6HQ.js';
3
- import './chunk-HSJH6SUW.js';
1
+ export { OfficeUiPresence, definePresence, initialsOf, participantsFromAwareness } from './chunk-V6W5DKJ6.js';
2
+ import './chunk-ZWBP2WTC.js';
3
+ import './chunk-7Q7ZS2CC.js';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The second level of Office KeyTips for products that derive their tips from a ribbon panel: give
3
+ * each visible command a key (the product's own Office keys where the command exists, otherwise two
4
+ * letters of its label, never the prefix of another), show a badge over each and run the one whose
5
+ * letters are typed. Word and Excel use the same loop and differ only in the key table and in how a
6
+ * control is found and operated, so those stay with the product.
7
+ */
8
+ export interface KeyTipTarget {
9
+ /** The letters or digits to type; empty before {@link assignKeyTips}. */
10
+ key: string;
11
+ /** The element the badge sits under. */
12
+ element: HTMLElement;
13
+ /** What the tip does: open the control, or click it. */
14
+ activate(): void;
15
+ }
16
+ /**
17
+ * Makes every key unique and unambiguous. A key that is already set stays unless an earlier one
18
+ * clashes with it (Office's own keys differ per tab, so two commands can share one); the others get
19
+ * two letters from `label(target)`, then a number.
20
+ */
21
+ export declare function assignKeyTips(targets: readonly KeyTipTarget[], label: (target: KeyTipTarget) => string): void;
22
+ /**
23
+ * Shows fixed badges over `targets` (in `root`, so product CSS styles them through `className`) and
24
+ * runs the one whose letters are typed. Escape, a pointer press or a key with no match ends it;
25
+ * `onDone` runs after any end. Returns a function that stops it.
26
+ */
27
+ export declare function runKeyTips(root: HTMLElement, targets: readonly KeyTipTarget[], onDone: () => void, className: string): () => void;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Ribbon overflow: when a panel is narrower than its groups, collapse groups from the right, each
3
+ * into one dropdown button that shows the group's live controls in a panel under it. The panel
4
+ * scrolls only when even that is not enough, as Word and Excel do when a window narrows.
5
+ *
6
+ * A product supplies how its icons are drawn and which command buttons end the dropdown; the
7
+ * folding itself is the same for every product. Panels are `.ribbon-panel` elements holding
8
+ * `.ribbon-group` children, each with `data-label` and optionally `data-caption`.
9
+ */
10
+ export interface RibbonOverflowOptions {
11
+ /** Draws the icon `name` at `size` pixels (the product's own glyph set). */
12
+ icon(doc: Document, name: string, size: number): Node;
13
+ /** Icon of a folded group's button, by the group's `data-label`. */
14
+ groupIcons: Readonly<Record<string, string>>;
15
+ /** Icon for a group missing from `groupIcons`. */
16
+ fallbackIcon: string;
17
+ /** Selector of the ribbon element that owns the dropdown panel. */
18
+ ribbon: string;
19
+ /** Selector of the command buttons whose click ends the dropdown (selects keep it open). */
20
+ command: string;
21
+ /** Writes the caption under a folded group's button (default: its text content). */
22
+ setCaption?(span: HTMLElement, label: string): void;
23
+ /** Give the dropdown the group's natural width (for groups whose layout depends on it). */
24
+ naturalWidth?: boolean;
25
+ /** Reserve room for painted (`::after`) group captions so they never run into the next group. */
26
+ reserveCaptions?: boolean;
27
+ }
28
+ export interface RibbonOverflow {
29
+ /** Folds groups of `panel` from the right until it fits its width. */
30
+ fitPanel(panel: HTMLElement): void;
31
+ /** Keeps each panel of `root` fitted as it resizes. Returns a disposer. */
32
+ attach(root: HTMLElement): () => void;
33
+ /** Re-measures every panel of `root` (after a tab switch or a locale change). */
34
+ refit(root: HTMLElement): void;
35
+ /** Closes the open dropdown, putting its controls back. */
36
+ close(): void;
37
+ }
38
+ export declare function createRibbonOverflow(options: RibbonOverflowOptions): RibbonOverflow;