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");