ooxml-ui 0.24.0 → 0.26.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.
@@ -4,8 +4,8 @@ import { type OfficeTitleBarCommand, type OfficeTitleBarPlacement, type OfficeTi
4
4
  export * from './title-bar-types.js';
5
5
  /**
6
6
  * `<office-ui-title-bar>`: Office's title bar above the ribbon (app mark, AutoSave, Quick
7
- * Access Toolbar, file name and status, centred command search, `collaboration` and `account`
8
- * slots). Controlled: set `state` (`OfficeTitleBarState`, every string translated); the bar
7
+ * Access Toolbar, file name and status, centred command search, `actions`, `collaboration` and
8
+ * `account` slots). Controlled: set `state` (`OfficeTitleBarState`, every string translated); the bar
9
9
  * owns no effects. `placement="belowRibbon"` renders only the Quick Access row and hides
10
10
  * itself (`data-empty`) while that row is empty. Events (bubbling, composed; static names let a
11
11
  * product subclass keep its published ones): `office-autosave-toggle`, `office-command`
@@ -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,10 +1086,16 @@ 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() {
1088
- return this.chosen ?? this.panels()[0]?.dataset.ribbonTab ?? "";
1096
+ const shown = this.shown();
1097
+ const chosen = shown.find((panel) => panel.dataset.ribbonTab === this.chosen);
1098
+ return chosen?.dataset.ribbonTab ?? shown[0]?.dataset.ribbonTab ?? "";
1089
1099
  }
1090
1100
  set selected(tab) {
1091
1101
  this.chosen = tab;
@@ -1100,20 +1110,47 @@ var OfficeUiRibbon = class extends OfficeElement {
1100
1110
  focusTab() {
1101
1111
  this.renderRoot.querySelector('[role="tab"][aria-selected="true"]')?.focus();
1102
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
+ }
1103
1135
  panels() {
1104
1136
  return [...this.children].filter(
1105
1137
  (child) => child instanceof HTMLElement && !!child.dataset.ribbonTab
1106
1138
  );
1107
1139
  }
1140
+ /** The panels that have a tab: those not marked `data-tab-hidden`. */
1141
+ shown() {
1142
+ return this.panels().filter((panel) => panel.dataset.tabHidden === void 0);
1143
+ }
1108
1144
  /** Read the tabs off the panels (on connection and whenever the slotted panels change). */
1109
1145
  build() {
1110
- this.tabs = this.panels().map((panel) => {
1146
+ this.tabs = this.shown().map((panel) => {
1111
1147
  const id = panel.dataset.ribbonTab;
1112
1148
  panel.setAttribute("role", "tabpanel");
1113
1149
  panel.setAttribute("aria-label", panel.dataset.label ?? id);
1114
1150
  return {
1115
1151
  id,
1116
1152
  label: panel.dataset.label ?? id,
1153
+ contextual: panel.dataset.contextual !== void 0,
1117
1154
  ...panel.dataset.tabKeytip ? {
1118
1155
  keytip: panel.dataset.tabKeytip,
1119
1156
  ...panel.id ? { keytipPanel: panel.id } : {}
@@ -1121,7 +1158,35 @@ var OfficeUiRibbon = class extends OfficeElement {
1121
1158
  };
1122
1159
  });
1123
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
+ }
1124
1188
  choose(tab) {
1189
+ if (this.collapsible && this.collapsed) this.peek = true;
1125
1190
  if (tab === this.selected) return;
1126
1191
  if (this.fire("office-ribbon-select", { tab }, true)) this.selected = tab;
1127
1192
  }
@@ -1129,6 +1194,13 @@ var OfficeUiRibbon = class extends OfficeElement {
1129
1194
  const tab = event.target.closest?.('[role="tab"]');
1130
1195
  if (tab) this.choose(tab.dataset.tab);
1131
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
+ };
1132
1204
  onKey(event) {
1133
1205
  const tabs = [...this.renderRoot.querySelectorAll('[role="tab"]')];
1134
1206
  const focused = event.target.closest?.('[role="tab"]');
@@ -1140,14 +1212,33 @@ var OfficeUiRibbon = class extends OfficeElement {
1140
1212
  this.choose(next.dataset.tab);
1141
1213
  this.focusTab();
1142
1214
  }
1215
+ /** Rebuild the tab row when a panel's label or visibility flag changes. */
1216
+ watcher;
1143
1217
  connectedCallback() {
1144
1218
  this.build();
1145
1219
  super.connectedCallback();
1220
+ this.addEventListener("keydown", this.onHostKey);
1221
+ const Observer = this.ownerDocument.defaultView?.MutationObserver;
1222
+ this.watcher = Observer ? new Observer(() => this.build()) : void 0;
1223
+ this.watcher?.observe(this, {
1224
+ attributes: true,
1225
+ subtree: true,
1226
+ attributeFilter: ["data-tab-hidden", "data-label"]
1227
+ });
1228
+ }
1229
+ disconnectedCallback() {
1230
+ super.disconnectedCallback();
1231
+ this.removeEventListener("keydown", this.onHostKey);
1232
+ this.watcher?.disconnect();
1233
+ this.ownerDocument.removeEventListener("pointerdown", this.onOutside, true);
1234
+ this.watcher = void 0;
1146
1235
  }
1147
1236
  /** Exactly one panel shows. */
1148
1237
  updated() {
1149
1238
  const selected = this.selected;
1150
- 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;
1151
1242
  }
1152
1243
  render() {
1153
1244
  const selected = this.selected;
@@ -1171,6 +1262,7 @@ var OfficeUiRibbon = class extends OfficeElement {
1171
1262
  aria-label=${this.label ?? "Ribbon"}
1172
1263
  @click=${this.onTabClick}
1173
1264
  @keydown=${this.onKey}
1265
+ @dblclick=${this.onTabDoubleClick}
1174
1266
  >
1175
1267
  ${this.tabs.map(
1176
1268
  (tab) => html`<button
@@ -1178,6 +1270,7 @@ var OfficeUiRibbon = class extends OfficeElement {
1178
1270
  role="tab"
1179
1271
  id="tab-${tab.id}"
1180
1272
  data-tab=${tab.id}
1273
+ data-contextual=${ifDefined(tab.contextual ? "" : void 0)}
1181
1274
  data-keytip=${ifDefined(tab.keytip)}
1182
1275
  data-keytip-panel=${ifDefined(tab.keytipPanel)}
1183
1276
  aria-selected=${String(tab.id === selected)}
@@ -1188,8 +1281,20 @@ var OfficeUiRibbon = class extends OfficeElement {
1188
1281
  </div>
1189
1282
  <slot name="search"></slot>
1190
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>
1191
1296
  </div>
1192
- <slot @slotchange=${this.build}></slot>
1297
+ <slot @slotchange=${this.build} @click=${this.onPanelClick}></slot>
1193
1298
  `;
1194
1299
  }
1195
1300
  };
@@ -4232,6 +4337,7 @@ var OfficeUiTitleBar = class extends OfficeElement {
4232
4337
  </span>
4233
4338
  ${this.searchBox()}
4234
4339
  <div class="end">
4340
+ <slot name="actions"></slot>
4235
4341
  <slot name="collaboration"></slot>
4236
4342
  <slot name="account"></slot>
4237
4343
  </div>
@@ -4496,6 +4602,12 @@ function attachKeyTips(scope) {
4496
4602
  view?.addEventListener("blur", onPointer);
4497
4603
  const handle = {
4498
4604
  active: false,
4605
+ start() {
4606
+ if (!handle.active) start();
4607
+ },
4608
+ stop() {
4609
+ stop();
4610
+ },
4499
4611
  dispose() {
4500
4612
  stop();
4501
4613
  target.removeEventListener("keydown", onKeyDown, true);
@@ -4508,6 +4620,135 @@ function attachKeyTips(scope) {
4508
4620
  return handle;
4509
4621
  }
4510
4622
 
4623
+ // src/ribbon/overflow.ts
4624
+ function createRibbonOverflow(options) {
4625
+ let closeOpen;
4626
+ function overflowButton(group) {
4627
+ const doc = group.ownerDocument;
4628
+ const button = doc.createElement("button");
4629
+ button.type = "button";
4630
+ button.className = "ribbon-overflow-button ribbon-large";
4631
+ button.setAttribute("aria-haspopup", "true");
4632
+ const name = options.groupIcons[group.dataset.label ?? ""] ?? options.fallbackIcon;
4633
+ button.append(
4634
+ options.icon(doc, name, 28),
4635
+ doc.createElement("span"),
4636
+ options.icon(doc, "caret", 12)
4637
+ );
4638
+ button.addEventListener("mousedown", (event) => event.preventDefault());
4639
+ button.addEventListener("click", () => toggleGroup(group, button));
4640
+ return button;
4641
+ }
4642
+ function toggleGroup(group, button) {
4643
+ const wasOpen = button.getAttribute("aria-expanded") === "true";
4644
+ closeOpen?.();
4645
+ if (wasOpen) return;
4646
+ const ribbon = group.closest(options.ribbon);
4647
+ if (!ribbon) return;
4648
+ const doc = group.ownerDocument;
4649
+ const pop = doc.createElement("div");
4650
+ pop.className = "ribbon-popover ribbon-overflow-panel";
4651
+ const content = doc.createElement("div");
4652
+ content.className = "ribbon-group ribbon-overflow-content";
4653
+ content.dataset.label = group.dataset.label ?? "";
4654
+ content.dataset.caption = group.dataset.caption ?? "";
4655
+ content.setAttribute("role", "group");
4656
+ content.setAttribute("aria-label", group.getAttribute("aria-label") ?? "");
4657
+ if (options.naturalWidth) content.style.width = `${group.dataset.natural ?? 200}px`;
4658
+ const moved = [...group.children].filter((child) => child !== button);
4659
+ content.append(...moved);
4660
+ pop.append(content);
4661
+ ribbon.append(pop);
4662
+ const box = button.getBoundingClientRect();
4663
+ pop.style.top = `${box.bottom + 2}px`;
4664
+ const width = doc.defaultView?.innerWidth ?? 1024;
4665
+ pop.style.left = `${Math.max(4, Math.min(box.left, width - pop.offsetWidth - 8))}px`;
4666
+ button.setAttribute("aria-expanded", "true");
4667
+ const outside = (event) => {
4668
+ const path = event.composedPath();
4669
+ const inside = path.some(
4670
+ (node) => node instanceof HTMLElement && node.classList.contains("ribbon-popover")
4671
+ );
4672
+ if (!inside && !path.includes(button)) closeOpen?.();
4673
+ };
4674
+ const escape = (event) => {
4675
+ if (event.key !== "Escape") return;
4676
+ closeOpen?.();
4677
+ button.focus();
4678
+ };
4679
+ pop.addEventListener("click", (event) => {
4680
+ if (event.target.closest(options.command)) setTimeout(() => closeOpen?.(), 0);
4681
+ });
4682
+ doc.addEventListener("pointerdown", outside, true);
4683
+ doc.addEventListener("keydown", escape, true);
4684
+ closeOpen = () => {
4685
+ doc.removeEventListener("pointerdown", outside, true);
4686
+ doc.removeEventListener("keydown", escape, true);
4687
+ group.prepend(...moved);
4688
+ pop.remove();
4689
+ button.removeAttribute("aria-expanded");
4690
+ closeOpen = void 0;
4691
+ };
4692
+ }
4693
+ function reserveCaptions(panel) {
4694
+ const sizer = panel.ownerDocument.createElement("span");
4695
+ sizer.className = "ribbon-caption-sizer";
4696
+ panel.append(sizer);
4697
+ for (const group of panel.querySelectorAll(":scope > .ribbon-group")) {
4698
+ sizer.textContent = group.dataset.caption ?? "";
4699
+ const launcher = group.querySelector(":scope > .ribbon-launcher") ? 32 : 12;
4700
+ group.style.minWidth = `${Math.ceil(sizer.offsetWidth) + launcher}px`;
4701
+ }
4702
+ sizer.remove();
4703
+ }
4704
+ function fitPanel(panel) {
4705
+ if (panel.hidden || !panel.clientWidth) return;
4706
+ if (closeOpen && panel.dataset.fittedWidth === String(panel.clientWidth)) return;
4707
+ panel.dataset.fittedWidth = String(panel.clientWidth);
4708
+ closeOpen?.();
4709
+ if (options.reserveCaptions) reserveCaptions(panel);
4710
+ for (const group of panel.querySelectorAll(".ribbon-group[data-collapsed]"))
4711
+ group.removeAttribute("data-collapsed");
4712
+ const groups = [...panel.querySelectorAll(":scope > .ribbon-group")].filter(
4713
+ (group) => group.offsetWidth > 0
4714
+ );
4715
+ if (options.naturalWidth)
4716
+ for (const group of groups) group.dataset.natural = String(Math.ceil(group.offsetWidth));
4717
+ for (let at = groups.length - 1; at >= 0 && panel.scrollWidth > panel.clientWidth + 1; at--) {
4718
+ const group = groups[at];
4719
+ let button = group.querySelector(":scope > .ribbon-overflow-button");
4720
+ if (!button) {
4721
+ button = overflowButton(group);
4722
+ group.append(button);
4723
+ }
4724
+ const label = group.dataset.caption ?? group.dataset.label ?? "";
4725
+ const span = button.querySelector("span");
4726
+ if (options.setCaption) options.setCaption(span, label);
4727
+ else span.textContent = label;
4728
+ button.setAttribute("aria-label", label);
4729
+ button.title = label;
4730
+ group.setAttribute("data-collapsed", "");
4731
+ }
4732
+ }
4733
+ const panelsOf = (root) => root.querySelectorAll(".ribbon-panel");
4734
+ return {
4735
+ fitPanel,
4736
+ attach(root) {
4737
+ if (typeof ResizeObserver === "undefined") return () => {
4738
+ };
4739
+ const observer = new ResizeObserver((entries) => {
4740
+ for (const entry of entries) fitPanel(entry.target);
4741
+ });
4742
+ for (const panel of panelsOf(root)) observer.observe(panel);
4743
+ return () => observer.disconnect();
4744
+ },
4745
+ refit(root) {
4746
+ for (const panel of panelsOf(root)) fitPanel(panel);
4747
+ },
4748
+ close: () => closeOpen?.()
4749
+ };
4750
+ }
4751
+
4511
4752
  // src/controls.ts
4512
4753
  var CONTROL_DEFINERS = [
4513
4754
  defineIcon,
@@ -4550,4 +4791,4 @@ function registerControls(registry) {
4550
4791
  for (const define of CONTROL_DEFINERS) define(registry);
4551
4792
  }
4552
4793
 
4553
- 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 };
4794
+ 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, 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, 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,38 @@
1
- import { OFFICE_TOKENS, TOUCH } from './chunk-HSJH6SUW.js';
1
+ import { OFFICE_TOKENS, TOUCH } from './chunk-7Q7ZS2CC.js';
2
+
3
+ // src/theme-bridge.ts
4
+ var SHADCN_NAMES = [
5
+ ["--office-foreground", "foreground"],
6
+ ["--office-muted-foreground", "muted-foreground"],
7
+ ["--office-background", "background"],
8
+ ["--office-surface", "card"],
9
+ ["--office-selected", "accent"],
10
+ ["--office-border", "border"],
11
+ ["--office-accent", "primary"],
12
+ ["--office-accent-foreground", "primary-foreground"],
13
+ ["--office-ring", "ring"],
14
+ ["--office-popover", "popover"],
15
+ // Popover text falls back to the plain foreground, as it does in the shadcn themes.
16
+ ["--office-popover-foreground", "popover-foreground", "foreground"],
17
+ ["--office-danger", "destructive"],
18
+ ["--office-danger-foreground", "destructive-foreground"]
19
+ ];
20
+ function themeBridge(selector, map) {
21
+ const lines = Object.entries(map).filter(([, value]) => value).map(([name, value]) => ` ${name}: ${value};`);
22
+ return `${selector} {
23
+ ${lines.join("\n")}
24
+ }
25
+ `;
26
+ }
27
+ function shadcnBridge(selector, prefix, options = {}) {
28
+ const map = {};
29
+ for (const [token, name, fallbackName] of SHADCN_NAMES) {
30
+ const fallback = options.fallbacks?.[name];
31
+ const inner = fallbackName ? `var(${prefix}${fallbackName})` : fallback;
32
+ map[token] = `var(${prefix}${name}${inner ? `, ${inner}` : ""})`;
33
+ }
34
+ return themeBridge(selector, { ...map, ...options.extra });
35
+ }
2
36
 
3
37
  // src/theme.ts
4
38
  var THEME_STYLE_ID = "office-ui-theme";
@@ -67,4 +101,4 @@ function installOfficeUiTheme(doc = globalThis.document) {
67
101
  return true;
68
102
  }
69
103
 
70
- export { THEME_CSS, THEME_STYLE_ID, installOfficeUiTheme };
104
+ export { THEME_CSS, THEME_STYLE_ID, installOfficeUiTheme, shadcnBridge, themeBridge };
@@ -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,8 @@ 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 { createRibbonOverflow } from './ribbon/overflow.js';
49
+ export type { RibbonOverflow, RibbonOverflowOptions } from './ribbon/overflow.js';
48
50
  export type { OfficeRibbonSelectEvent } from './ribbon/ribbon-tabs.js';
49
51
  export type { OfficeRibbonCommandView, OfficeRibbonGroupView } from './ribbon/ribbon-section.js';
50
52
  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-FWZIVXAV.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, 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, sanitizeOfficeProfile, typeAheadIndex, writeOfficeProfile } from './chunk-4R4CHY5X.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-2X6QYQ3Q.js';
2
- export { THEME_CSS, THEME_STYLE_ID, installOfficeUiTheme } from './chunk-2X6QYQ3Q.js';
3
- import { registerControls } from './chunk-FWZIVXAV.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-FWZIVXAV.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-4R4CHY5X.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, 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, sanitizeOfficeProfile, typeAheadIndex, writeOfficeProfile } from './chunk-4R4CHY5X.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,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;