Polish Office canvas document controls

Unify Markdown and LibreOffice document handling around an active-file header in both canvas and modal views. Move creation into a shared + New menu, place Save beside file actions, and expose Rename/Close File from the document dropdown. Update the README with the refreshed Office and Browser screenshots plus the new Office UI behavior.

Alessandro committed May 5, 2026 at 18:17 UTC 0d9a7cd65d205d6f5dc22b46fc67ad9bafbbd23b
4 files changed +426 -90
README.md
+4 -2
@@ -80,16 +80,18 @@ The canvas makes agent work visible. You can watch it browse, inspect what chang
80
81 ## LibreOffice Integration
82
83 -<img alt="Cowork on Documents" src="https://github.com/user-attachments/assets/5ceb8f2c-cb5a-41d0-b7a8-7d5da6375aa1" />
83 +<img alt="Cowork on Documents" width="1406" height="720" src="https://github.com/user-attachments/assets/4ad71888-4f0d-484a-b68b-631ad99187d7" />
84 <br>
85
86 Create, open, and cowork with the AI on documents, spreadsheets, and presentation decks with the LibreOffice stack.
87
88 The document canvas supports Markdown by default, with LibreOffice-native ODT, ODS, and ODP workflows when binary office artifacts are needed. Agents can create substantial deliverables, read their contents, apply precise saved edits, preserve version history, and generate native ODS charts directly inside spreadsheets. Microsoft Office compatibility imports and exports remain available when explicitly requested.
89
90 +Markdown, Writer, Spreadsheet, and Presentation files share a compact active-file header with save, rename, close, and creation controls in both canvas and modal views.
91 +
92 ## Native Browser With Annotations and Extensions
93
92 -<img alt="Native Browser" src="https://github.com/user-attachments/assets/4a2a9f8a-470f-4f24-94b7-c5fbbebe9008" />
94 +<img alt="Native Browser" width="1406" height="720" src="https://github.com/user-attachments/assets/8d425af6-fb27-4d46-ade9-8ffa34c7382c" />
95 <br>
96
97 Agent Zero includes a direct Playwright-powered Browser tool with a visible WebUI viewer. The agent can navigate pages, inspect readable page content, and act through typed page references such as `[link 3]`, `[button 6]`, and `[input text 8]` and use vision.
plugins/_office/extensions/webui/right-canvas-toolbar-start/office-new-menu.html new
+43
@@ -0,0 +1,43 @@
1 +<style>
2 + .right-canvas-toolbar > x-extension:has(.office-canvas-new-menu-host),
3 + .right-canvas-toolbar > x-extension > x-component:has(.office-canvas-new-menu-host) {
4 + display: contents;
5 + }
6 +
7 + .office-canvas-new-menu-host {
8 + display: inline-flex;
9 + align-items: center;
10 + }
11 +
12 + .office-canvas-new-menu-host[hidden] {
13 + display: none;
14 + }
15 +</style>
16 +
17 +<script type="module">
18 + import { store as officeStore } from "/plugins/_office/webui/office-store.js";
19 +
20 + function syncCanvasMenuVisibility(host) {
21 + if (!host?.isConnected) return;
22 + const rightCanvas = globalThis.Alpine?.store?.("rightCanvas");
23 + host.hidden = !(rightCanvas?.isOpen && rightCanvas?.activeSurfaceId === "office");
24 + globalThis.requestAnimationFrame?.(() => syncCanvasMenuVisibility(host));
25 + }
26 +
27 + function mountCanvasNewMenu() {
28 + const hosts = globalThis.document.querySelectorAll("[data-office-canvas-new-menu-host]:not([data-office-mounted])");
29 + for (const host of hosts) {
30 + host.dataset.officeMounted = "1";
31 + officeStore.installHeaderNewMenu(host);
32 + syncCanvasMenuVisibility(host);
33 + }
34 + }
35 +
36 + if (globalThis.requestAnimationFrame) {
37 + globalThis.requestAnimationFrame(mountCanvasNewMenu);
38 + } else {
39 + globalThis.setTimeout(mountCanvasNewMenu, 0);
40 + }
41 +</script>
42 +
43 +<div class="office-canvas-new-menu-host" data-office-canvas-new-menu-host hidden></div>
plugins/_office/webui/office-panel.html
+255 -82
@@ -8,65 +8,66 @@
8 <div class="office-panel" x-data x-create="$store.office.onMount($el, xAttrs($el) || {})" x-destroy="$store.office.cleanup()">
9 <template x-if="$store.office">
10 <div class="office-shell">
11 - <div class="office-toolbar">
12 - <div class="office-toolbar-row is-primary">
13 - <div class="office-tool-group">
14 - <button type="button" class="office-icon-button" title="Open" aria-label="Open" @click="$store.office.openFileBrowser()">
15 - <span class="material-symbols-outlined">folder_open</span>
16 - </button>
17 - </div>
18 -
19 - <div class="office-tool-group">
20 - <button type="button" class="office-icon-button office-command-button" aria-label="New Markdown" @click="$store.office.create('document', 'md')">
21 - <span class="material-symbols-outlined">article</span>
22 - <span class="office-button-label">Markdown</span>
23 - </button>
24 - <button type="button" class="office-icon-button office-command-button" aria-label="New Writer document" @click="$store.office.create('document', 'odt')">
25 - <span class="material-symbols-outlined">description</span>
26 - <span class="office-button-label">Writer</span>
27 - </button>
28 - <button type="button" class="office-icon-button office-command-button" aria-label="New spreadsheet" @click="$store.office.create('spreadsheet', 'ods')">
29 - <span class="material-symbols-outlined">table_chart</span>
30 - <span class="office-button-label">Spreadsheet</span>
31 - </button>
32 - <button type="button" class="office-icon-button office-command-button" aria-label="New presentation" @click="$store.office.create('presentation', 'odp')">
33 - <span class="material-symbols-outlined">co_present</span>
34 - <span class="office-button-label">Presentation</span>
35 - </button>
36 - </div>
37 -
38 - <span class="office-toolbar-spacer"></span>
11 + <div class="office-document-header" x-show="$store.office.hasActiveFile()" style="display: none;">
12 + <div class="office-document-title" :title="$store.office.tabLabel($store.office.session)">
13 + <span class="material-symbols-outlined office-document-icon" aria-hidden="true" x-text="$store.office.tabIcon($store.office.session)"></span>
14 + <span class="office-document-name" x-text="$store.office.tabTitle($store.office.session)"></span>
15 + <span class="office-document-dirty" x-show="$store.office.dirty" aria-hidden="true">*</span>
16 + </div>
17
40 - <div class="office-tool-group office-tool-actions" x-show="$store.office.session && !$store.office.isDesktopSession()" style="display: none;">
41 - <button type="button" class="office-icon-button" title="Rename" aria-label="Rename" :disabled="$store.office.saving" @click="$store.office.renameActiveFile()">
42 - <span class="material-symbols-outlined">edit</span>
18 + <button
19 + type="button"
20 + class="office-icon-button office-document-save-button"
21 + title="Save"
22 + aria-label="Save"
23 + :class="{ 'is-primary': $store.office.dirty }"
24 + :disabled="$store.office.saving"
25 + @click="$store.office.save()"
26 + >
27 + <span class="material-symbols-outlined" :class="{ spinning: $store.office.saving }" x-text="$store.office.saving ? 'progress_activity' : 'save'"></span>
28 + </button>
29 +
30 + <div class="office-file-actions" x-data="{ open: false }" @click.outside="open = false" @keydown.escape.window="open = false">
31 + <button
32 + type="button"
33 + class="office-icon-button office-file-menu-button"
34 + title="File actions"
35 + aria-label="File actions"
36 + aria-haspopup="menu"
37 + :aria-expanded="open.toString()"
38 + :disabled="$store.office.saving"
39 + @click.stop="open = !open"
40 + >
41 + <span class="material-symbols-outlined">more_vert</span>
42 + </button>
43 + <div class="office-new-menu office-file-menu" role="menu" x-show="open" @click.stop>
44 + <button type="button" class="office-new-menu-item" role="menuitem" :disabled="$store.office.saving" @click="open = false; $store.office.renameActiveFile()">
45 + <span class="material-symbols-outlined" aria-hidden="true">edit</span>
46 + <span>Rename</span>
47 </button>
44 - <button type="button" class="office-icon-button" title="Save" aria-label="Save" :class="{ 'is-primary': $store.office.dirty }" :disabled="$store.office.saving" @click="$store.office.save()">
45 - <span class="material-symbols-outlined" :class="{ spinning: $store.office.saving }" x-text="$store.office.saving ? 'progress_activity' : 'save'"></span>
48 + <button type="button" class="office-new-menu-item" role="menuitem" :disabled="$store.office.loading" @click="open = false; $store.office.closeActiveFile()">
49 + <span class="material-symbols-outlined" aria-hidden="true">close</span>
50 + <span>Close File</span>
51 </button>
52 </div>
53 </div>
54 + </div>
55
50 - <div class="office-toolbar-row is-editor" x-show="$store.office.session && $store.office.isMarkdown()" style="display: none;">
51 - <div class="office-tool-group">
56 + <div class="office-toolbar" x-show="$store.office.session && $store.office.isMarkdown()" style="display: none;">
57 + <div class="office-toolbar-row">
58 + <div class="office-tool-group office-editor-tools" x-show="$store.office.session && $store.office.isMarkdown()" style="display: none;">
59 <button type="button" class="office-icon-button" title="Undo" aria-label="Undo" :disabled="!$store.office.canUndo()" @click="$store.office.undo()">
60 <span class="material-symbols-outlined">undo</span>
61 </button>
62 <button type="button" class="office-icon-button" title="Redo" aria-label="Redo" :disabled="!$store.office.canRedo()" @click="$store.office.redo()">
63 <span class="material-symbols-outlined">redo</span>
64 </button>
58 - </div>
59 -
60 - <div class="office-tool-group">
65 <button type="button" class="office-icon-button" title="Bold" aria-label="Bold" @click="$store.office.format('bold')">
66 <span class="material-symbols-outlined">format_bold</span>
67 </button>
68 <button type="button" class="office-icon-button" title="Italic" aria-label="Italic" @click="$store.office.format('italic')">
69 <span class="material-symbols-outlined">format_italic</span>
70 </button>
67 - </div>
68 -
69 - <div class="office-tool-group">
71 <button type="button" class="office-icon-button" title="List" aria-label="List" @click="$store.office.format('list')">
72 <span class="material-symbols-outlined">format_list_bulleted</span>
73 </button>
@@ -77,21 +78,9 @@
78 <span class="material-symbols-outlined">table</span>
79 </button>
80 </div>
80 - </div>
81 - </div>
81
83 - <div class="office-tabs" x-show="$store.office.visibleTabs().length" role="tablist" aria-label="Open documents" style="display: none;">
84 - <template x-for="tab in $store.office.visibleTabs()" :key="tab.tab_id">
85 - <div class="office-tab-shell" :class="{ 'is-active': $store.office.isActiveTab(tab), 'is-dirty': tab.dirty, 'is-system': $store.office.isDesktopSession(tab) }">
86 - <button type="button" class="office-tab" role="tab" :aria-selected="$store.office.isActiveTab(tab).toString()" :title="$store.office.tabLabel(tab)" @click="$store.office.selectTab(tab.tab_id)">
87 - <span class="material-symbols-outlined office-tab-icon" aria-hidden="true" x-text="$store.office.tabIcon(tab)"></span>
88 - <span class="office-tab-title" x-text="$store.office.tabTitle(tab)"></span>
89 - </button>
90 - <button type="button" class="office-tab-close" x-show="!$store.office.isDesktopSession(tab)" :title="'Close ' + $store.office.tabLabel(tab)" :aria-label="'Close ' + $store.office.tabLabel(tab)" :disabled="$store.office.loading" @click.stop="$confirmClick($event, () => $store.office.closeTab(tab.tab_id))">
91 - <span class="material-symbols-outlined">close</span>
92 - </button>
93 - </div>
94 - </template>
82 + <span class="office-toolbar-spacer"></span>
83 + </div>
84 </div>
85
86 <div class="office-state-line" x-show="$store.office.message || $store.office.error || $store.office.loading" style="display: none;">
@@ -189,7 +178,7 @@
178 }
179
180 .modal-inner.office-modal .modal-header {
192 - grid-template-columns: minmax(0, 1fr) auto auto auto;
181 + grid-template-columns: minmax(0, 1fr) repeat(5, auto);
182 }
183
184 .office-modal-input-shield {
@@ -259,13 +248,12 @@
248
249 .office-toolbar {
250 display: flex;
262 - flex-direction: column;
251 + flex-direction: row;
252 align-items: stretch;
253 flex-wrap: nowrap;
265 - gap: 8px;
254 min-height: 0;
267 - padding: 8px 10px;
268 - overflow: visible;
255 + padding: 6px 10px;
256 + overflow: hidden;
257 border-bottom: 1px solid color-mix(in srgb, var(--color-border), transparent 20%);
258 background: color-mix(in srgb, var(--color-background), var(--color-panel) 48%);
259 }
@@ -274,32 +262,208 @@
262 display: flex;
263 align-items: center;
264 flex: 0 0 auto;
277 - flex-wrap: wrap;
278 - gap: 8px;
265 + flex-wrap: nowrap;
266 + gap: 6px;
267 + width: 100%;
268 min-width: 0;
280 - }
281 -
282 - .office-toolbar-row.is-editor {
283 - gap: 6px 8px;
284 - padding-top: 1px;
269 + min-height: 32px;
270 + overflow-x: auto;
271 + overflow-y: hidden;
272 + scrollbar-width: thin;
273 }
274
275 .office-tool-group {
276 display: flex;
277 align-items: center;
290 - flex-wrap: wrap;
278 + flex-wrap: nowrap;
279 flex: 0 0 auto;
292 - gap: 6px;
280 + gap: 4px;
281 min-width: 0;
282 }
283
284 + .office-editor-tools {
285 + gap: 3px;
286 + }
287 +
288 .office-tool-actions {
289 justify-content: flex-end;
290 }
291
292 .office-toolbar-spacer {
301 - flex: 1 1 80px;
293 + flex: 1 1 16px;
294 + min-width: 8px;
295 + }
296 +
297 + .office-toolbar-divider {
298 + flex: 0 0 auto;
299 + width: 1px;
300 + height: 22px;
301 + margin-inline: 2px;
302 + background: color-mix(in srgb, var(--color-border), transparent 18%);
303 + }
304 +
305 + .office-document-header {
306 + display: flex;
307 + align-items: center;
308 + gap: 10px;
309 + min-height: 38px;
310 + padding: 5px 10px 5px 12px;
311 + border-bottom: 1px solid color-mix(in srgb, var(--color-border), transparent 22%);
312 + background: color-mix(in srgb, var(--color-panel), var(--color-background) 30%);
313 + }
314 +
315 + .office-document-title {
316 + display: flex;
317 + align-items: center;
318 + gap: 7px;
319 min-width: 0;
320 + flex: 1 1 auto;
321 + color: var(--color-text);
322 + }
323 +
324 + .office-document-icon {
325 + flex: 0 0 auto;
326 + font-size: 19px;
327 + line-height: 1;
328 + }
329 +
330 + .office-document-name {
331 + min-width: 0;
332 + overflow: hidden;
333 + text-overflow: ellipsis;
334 + white-space: nowrap;
335 + font-size: 13px;
336 + font-weight: 750;
337 + letter-spacing: 0;
338 + line-height: 1.2;
339 + }
340 +
341 + .office-document-dirty {
342 + flex: 0 0 auto;
343 + color: #2ca58d;
344 + font-size: 14px;
345 + font-weight: 800;
346 + line-height: 1;
347 + }
348 +
349 + .office-file-actions {
350 + position: relative;
351 + display: inline-flex;
352 + align-items: center;
353 + flex: 0 0 auto;
354 + }
355 +
356 + .office-document-save-button,
357 + .office-file-menu-button {
358 + width: 30px;
359 + height: 30px;
360 + min-width: 30px;
361 + }
362 +
363 + .office-header-actions {
364 + position: relative;
365 + display: inline-flex;
366 + align-items: center;
367 + flex: 0 0 auto;
368 + }
369 +
370 + .office-header-new-button {
371 + appearance: none;
372 + display: inline-flex;
373 + align-items: center;
374 + justify-content: center;
375 + gap: 4px;
376 + height: 34px;
377 + min-height: 34px;
378 + padding: 0 9px 0 8px;
379 + border: 1px solid transparent;
380 + border-radius: 7px;
381 + background: transparent;
382 + color: var(--color-text);
383 + cursor: pointer;
384 + font: inherit;
385 + font-size: 12px;
386 + font-weight: 750;
387 + letter-spacing: 0;
388 + line-height: 1;
389 + opacity: 0.82;
390 + white-space: nowrap;
391 + transition: background-color 0.16s ease, border-color 0.16s ease, opacity 0.16s ease;
392 + }
393 +
394 + .office-header-new-button:hover,
395 + .office-header-actions.is-open .office-header-new-button {
396 + opacity: 1;
397 + border-color: color-mix(in srgb, var(--color-primary) 28%, var(--color-border));
398 + background: color-mix(in srgb, var(--color-background-hover) 72%, transparent);
399 + }
400 +
401 + .office-header-new-button .material-symbols-outlined {
402 + font-size: 18px;
403 + line-height: 1;
404 + }
405 +
406 + .office-header-new-button .office-new-chevron {
407 + margin-left: -2px;
408 + font-size: 16px;
409 + opacity: 0.8;
410 + }
411 +
412 + .office-new-menu {
413 + position: absolute;
414 + top: calc(100% + 6px);
415 + right: 0;
416 + z-index: 100;
417 + min-width: 184px;
418 + padding: 5px;
419 + border: 1px solid color-mix(in srgb, var(--color-border), transparent 10%);
420 + border-radius: 8px;
421 + background: color-mix(in srgb, var(--color-panel), var(--color-background) 10%);
422 + box-shadow: 0 14px 34px rgba(0, 0, 0, 0.34);
423 + }
424 +
425 + .office-new-menu[hidden] {
426 + display: none;
427 + }
428 +
429 + .office-new-menu-item {
430 + appearance: none;
431 + display: flex;
432 + align-items: center;
433 + gap: 8px;
434 + width: 100%;
435 + height: 32px;
436 + padding: 0 8px;
437 + border: 1px solid transparent;
438 + border-radius: 6px;
439 + background: transparent;
440 + color: var(--color-text);
441 + cursor: pointer;
442 + font: inherit;
443 + font-size: 12px;
444 + font-weight: 650;
445 + letter-spacing: 0;
446 + line-height: 1;
447 + text-align: left;
448 + white-space: nowrap;
449 + }
450 +
451 + .office-new-menu-item:hover {
452 + border-color: color-mix(in srgb, var(--color-primary) 22%, transparent);
453 + background: color-mix(in srgb, var(--color-background-hover) 76%, transparent);
454 + }
455 +
456 + .office-new-menu-item:disabled {
457 + cursor: default;
458 + opacity: 0.46;
459 + }
460 +
461 + .office-new-menu-item .material-symbols-outlined {
462 + flex: 0 0 auto;
463 + width: 18px;
464 + font-size: 18px;
465 + line-height: 1;
466 + text-align: center;
467 }
468
469 .office-icon-button,
@@ -315,9 +479,9 @@
479 .office-icon-button {
480 display: inline-grid;
481 place-items: center;
318 - width: 40px;
319 - height: 40px;
320 - min-width: 40px;
482 + width: 32px;
483 + height: 32px;
484 + min-width: 32px;
485 padding: 0;
486 }
487
@@ -325,10 +489,10 @@
489 display: inline-flex;
490 align-items: center;
491 justify-content: center;
328 - gap: 7px;
492 + gap: 5px;
493 width: auto;
330 - max-width: 152px;
331 - padding: 0 11px;
494 + max-width: 126px;
495 + padding: 0 8px;
496 white-space: nowrap;
497 }
498
@@ -336,7 +500,7 @@
500 min-width: 0;
501 overflow: hidden;
502 text-overflow: ellipsis;
339 - font-size: 12px;
503 + font-size: 11px;
504 font-weight: 700;
505 line-height: 1;
506 }
@@ -365,7 +529,7 @@
529
530 .office-icon-button .material-symbols-outlined,
531 .office-tab-icon {
368 - font-size: 21px;
532 + font-size: 19px;
533 line-height: 1;
534 }
535
@@ -578,17 +742,26 @@
742
743 @container (max-width: 680px) {
744 .office-toolbar {
581 - gap: 6px;
745 padding-inline: 8px;
746 }
747
748 .office-toolbar-row {
586 - gap: 6px;
749 + gap: 5px;
750 }
751
752 .office-command-button {
590 - max-width: 132px;
591 - padding-inline: 9px;
753 + width: 32px;
754 + max-width: 32px;
755 + padding-inline: 0;
756 + }
757 +
758 + .office-command-button .office-button-label {
759 + position: absolute;
760 + width: 1px;
761 + height: 1px;
762 + overflow: hidden;
763 + clip: rect(0 0 0 0);
764 + white-space: nowrap;
765 }
766
767 }
plugins/_office/webui/office-store.js
+124 -6
@@ -558,7 +558,7 @@ const model = {
558
559 installDesktopDocumentSession(session) {
560 this.setDesktopIntentionalShutdown(false);
561 - this.tabs = this.tabs.filter((tab) => this.isVisibleOfficeTab(tab));
561 + this.tabs = this.tabs.filter((tab) => !this.isDesktopOfficeDocument(tab));
562 let desktopTab = this.tabs.find((tab) => this.isDesktopSession(tab));
563 if (!desktopTab) {
564 desktopTab = {
@@ -580,13 +580,24 @@ const model = {
580 };
581 this.tabs.unshift(desktopTab);
582 }
583 - const desktopTabId = desktopTab.tab_id;
584 - this.session = { ...session, tab_id: session.tab_id || uniqueTabId(session) };
585 - this.activeTabId = desktopTabId;
583 + const documentSession = { ...session, tab_id: session.tab_id || uniqueTabId(session) };
584 + const existingIndex = this.tabs.findIndex((tab) => (
585 + (documentSession.file_id && tab.file_id === documentSession.file_id)
586 + || (documentSession.path && tab.path === documentSession.path)
587 + ));
588 + if (existingIndex >= 0) {
589 + this.tabs.splice(existingIndex, 1, documentSession);
590 + } else {
591 + this.tabs.push(documentSession);
592 + }
593 + this.session = documentSession;
594 + this.activeTabId = documentSession.tab_id;
595 this.editorText = "";
596 this.dirty = false;
597 this.resetHistory("");
598 this.queueRender({ focus: true });
599 + this.restoreDesktopFrames();
600 + this.requestDesktopViewportSync({ force: true });
601 this.updateDesktopMonitor();
602 },
603
@@ -657,6 +668,11 @@ const model = {
668 await this.refresh();
669 },
670
671 + async closeActiveFile() {
672 + if (!this.session || this.isDesktopSession() || this.loading) return;
673 + await this.closeTab(this.session.tab_id);
674 + },
675 +
676 async save() {
677 if (!this.session || this.saving) return;
678 if (this.isDesktopSession()) return;
@@ -800,7 +816,7 @@ const model = {
816 replaceSession(previous, next) {
817 this.session = next;
818 const index = this.tabs.findIndex((tab) => tab.tab_id === (previous?.tab_id || next.tab_id));
803 - if (index >= 0 && this.isVisibleOfficeTab(next)) this.tabs.splice(index, 1, next);
819 + if (index >= 0) this.tabs.splice(index, 1, next);
820 this.queueRender();
821 this.updateDesktopMonitor();
822 },
@@ -987,8 +1003,12 @@ const model = {
1003 return Boolean(tab && this.hasOfficialOffice(tab) && !this.isDesktopSession(tab) && this.isBinaryOffice(tab));
1004 },
1005
1006 + hasActiveFile(tab = this.session) {
1007 + return Boolean(tab && !this.isDesktopSession(tab) && (this.isMarkdown(tab) || this.isDesktopOfficeDocument(tab)));
1008 + },
1009 +
1010 isVisibleOfficeTab(tab = {}) {
991 - return Boolean(this.isDesktopSession(tab) || this.isMarkdown(tab));
1011 + return Boolean(this.hasActiveFile(tab));
1012 },
1013
1014 visibleTabs() {
@@ -2259,6 +2279,101 @@ const model = {
2279 return "draft";
2280 },
2281
2282 + async runNewMenuAction(action = "") {
2283 + const normalized = String(action || "").trim().toLowerCase();
2284 + if (normalized === "open") return await this.openFileBrowser();
2285 + if (normalized === "markdown") return await this.create("document", "md");
2286 + if (normalized === "writer") return await this.create("document", "odt");
2287 + if (normalized === "spreadsheet") return await this.create("spreadsheet", "ods");
2288 + if (normalized === "presentation") return await this.create("presentation", "odp");
2289 + return null;
2290 + },
2291 +
2292 + installHeaderNewMenu(header = null) {
2293 + if (!header || header.querySelector(".office-header-actions")) return () => {};
2294 +
2295 + const root = globalThis.document.createElement("div");
2296 + root.className = "office-header-actions";
2297 + root.innerHTML = `
2298 + <button type="button" class="office-header-new-button" aria-haspopup="menu" aria-expanded="false">
2299 + <span class="material-symbols-outlined" aria-hidden="true">add</span>
2300 + <span>New</span>
2301 + <span class="material-symbols-outlined office-new-chevron" aria-hidden="true">expand_more</span>
2302 + </button>
2303 + <div class="office-new-menu" role="menu" hidden>
2304 + <button type="button" class="office-new-menu-item" role="menuitem" data-office-new-action="open">
2305 + <span class="material-symbols-outlined" aria-hidden="true">folder_open</span>
2306 + <span>Open</span>
2307 + </button>
2308 + <button type="button" class="office-new-menu-item" role="menuitem" data-office-new-action="markdown">
2309 + <span class="material-symbols-outlined" aria-hidden="true">article</span>
2310 + <span>Markdown</span>
2311 + </button>
2312 + <button type="button" class="office-new-menu-item" role="menuitem" data-office-new-action="writer">
2313 + <span class="material-symbols-outlined" aria-hidden="true">description</span>
2314 + <span>Writer</span>
2315 + </button>
2316 + <button type="button" class="office-new-menu-item" role="menuitem" data-office-new-action="spreadsheet">
2317 + <span class="material-symbols-outlined" aria-hidden="true">table_chart</span>
2318 + <span>Spreadsheet</span>
2319 + </button>
2320 + <button type="button" class="office-new-menu-item" role="menuitem" data-office-new-action="presentation">
2321 + <span class="material-symbols-outlined" aria-hidden="true">co_present</span>
2322 + <span>Presentation</span>
2323 + </button>
2324 + </div>
2325 + `;
2326 +
2327 + const button = root.querySelector(".office-header-new-button");
2328 + const menu = root.querySelector(".office-new-menu");
2329 + const setOpen = (open) => {
2330 + root.classList.toggle("is-open", open);
2331 + button?.setAttribute("aria-expanded", open.toString());
2332 + if (menu) menu.hidden = !open;
2333 + };
2334 + const onButtonClick = (event) => {
2335 + event.preventDefault();
2336 + event.stopPropagation();
2337 + setOpen(!root.classList.contains("is-open"));
2338 + };
2339 + const onDocumentClick = (event) => {
2340 + if (!root.contains(event.target)) setOpen(false);
2341 + };
2342 + const onDocumentKeydown = (event) => {
2343 + if (event.key === "Escape") setOpen(false);
2344 + };
2345 +
2346 + button?.addEventListener("click", onButtonClick);
2347 + for (const item of root.querySelectorAll("[data-office-new-action]")) {
2348 + item.addEventListener("click", async (event) => {
2349 + event.preventDefault();
2350 + event.stopPropagation();
2351 + const action = event.currentTarget?.dataset?.officeNewAction || "";
2352 + setOpen(false);
2353 + await this.runNewMenuAction(action);
2354 + });
2355 + }
2356 + globalThis.document.addEventListener("click", onDocumentClick);
2357 + globalThis.document.addEventListener("keydown", onDocumentKeydown);
2358 +
2359 + const firstHeaderAction = header.querySelector(
2360 + ".modal-surface-switcher, .modal-dock-button, .office-modal-focus-button, .modal-close",
2361 + );
2362 + if (firstHeaderAction) {
2363 + firstHeaderAction.insertAdjacentElement("beforebegin", root);
2364 + } else {
2365 + header.appendChild(root);
2366 + }
2367 +
2368 + setOpen(false);
2369 + return () => {
2370 + button?.removeEventListener("click", onButtonClick);
2371 + globalThis.document.removeEventListener("click", onDocumentClick);
2372 + globalThis.document.removeEventListener("keydown", onDocumentKeydown);
2373 + root.remove();
2374 + };
2375 + },
2376 +
2377 setupFloatingModal(element = null) {
2378 const root = element || globalThis.document?.querySelector(".office-panel");
2379 const modal = root?.closest?.(".modal");
@@ -2290,6 +2405,8 @@ const model = {
2405 let startHeight = 0;
2406 let resizeMode = "";
2407
2408 + const newMenuCleanup = this.installHeaderNewMenu(header);
2409 +
2410 const currentBounds = () => {
2411 const rect = inner.getBoundingClientRect();
2412 return {
@@ -2516,6 +2633,7 @@ const model = {
2633 globalThis.setTimeout(ensurePosition, 0);
2634 }
2635 this._floatingCleanup = () => {
2636 + newMenuCleanup?.();
2637 cleanup.splice(0).reverse().forEach((entry) => entry());
2638 modal?.classList?.remove("modal-floating", "modal-no-backdrop");
2639 inner.classList.remove("is-dragging", "is-resizing", "is-focus-mode");