domma-cms 1.8.33 → 1.8.35

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +1 -1
  3. package/plugins/docs/CLAUDE.md +318 -0
  4. package/plugins/docs/admin/css/docs.css +783 -0
  5. package/plugins/docs/admin/lib/api.js +340 -0
  6. package/plugins/docs/admin/lib/convert.js +482 -0
  7. package/plugins/docs/admin/lib/drafts.js +115 -0
  8. package/plugins/docs/admin/lib/editor.js +851 -0
  9. package/plugins/docs/admin/lib/find-replace.js +426 -0
  10. package/plugins/docs/admin/lib/folders.js +203 -0
  11. package/plugins/docs/admin/lib/panel-guard.js +73 -0
  12. package/plugins/docs/admin/lib/pdf-view.js +126 -0
  13. package/plugins/docs/admin/lib/send.js +156 -0
  14. package/plugins/docs/admin/lib/settings.js +142 -0
  15. package/plugins/docs/admin/lib/share.js +323 -0
  16. package/plugins/docs/admin/lib/shortcuts.js +44 -0
  17. package/plugins/docs/admin/lib/templates.js +194 -0
  18. package/plugins/docs/admin/lib/versions.js +192 -0
  19. package/plugins/docs/admin/templates/docs.html +92 -0
  20. package/plugins/docs/admin/templates/editor.html +86 -0
  21. package/plugins/docs/admin/views/docs.js +777 -0
  22. package/plugins/docs/admin/views/editor.js +978 -0
  23. package/plugins/docs/config.js +24 -0
  24. package/plugins/docs/docs/guide.md +270 -0
  25. package/plugins/docs/lib/extensions.js +213 -0
  26. package/plugins/docs/lib/importer.js +72 -0
  27. package/plugins/docs/lib/pdf-worker.js +48 -0
  28. package/plugins/docs/lib/pdf.js +222 -0
  29. package/plugins/docs/lib/store.js +748 -0
  30. package/plugins/docs/plugin.js +1108 -0
  31. package/plugins/docs/plugin.json +89 -0
  32. package/plugins/docs/plugin.public.js +114 -0
  33. package/plugins/docs/templates/public-doc.html +36 -0
  34. package/plugins/docs/tests/_env.js +10 -0
  35. package/plugins/docs/tests/_pdf.js +73 -0
  36. package/plugins/docs/tests/convert.test.js +95 -0
  37. package/plugins/docs/tests/drafts.test.js +82 -0
  38. package/plugins/docs/tests/pdf.test.js +86 -0
  39. package/plugins/docs/tests/plugin.test.js +816 -0
  40. package/plugins/docs/tests/shortcuts.test.js +30 -0
  41. package/plugins/free-tier.lock.json +105 -1
  42. package/plugins/garage/CLAUDE.md +51 -0
  43. package/plugins/garage/admin/css/garage.css +73 -0
  44. package/plugins/garage/admin/lib/garage.js +236 -0
  45. package/plugins/garage/admin/templates/garage.html +86 -0
  46. package/plugins/garage/admin/views/garage.js +581 -0
  47. package/plugins/garage/collections/garage-vehicles/schema.json +101 -0
  48. package/plugins/garage/config.js +18 -0
  49. package/plugins/garage/docs/guide.md +118 -0
  50. package/plugins/garage/plugin.js +471 -0
  51. package/plugins/garage/plugin.json +68 -0
  52. package/plugins/garage/tests/access.test.js +134 -0
  53. package/plugins/garage/tests/garage.test.js +106 -0
  54. package/plugins/messages/CLAUDE.md +230 -0
  55. package/plugins/messages/admin/css/messages.css +117 -0
  56. package/plugins/messages/admin/lib/messages.js +452 -0
  57. package/plugins/messages/admin/templates/messages.html +76 -0
  58. package/plugins/messages/admin/views/messages.js +1219 -0
  59. package/plugins/messages/collections/user-message-threads/schema.json +22 -0
  60. package/plugins/messages/collections/user-messages/schema.json +23 -0
  61. package/plugins/messages/config.js +14 -0
  62. package/plugins/messages/docs/guide.md +176 -0
  63. package/plugins/messages/lib/call.js +138 -0
  64. package/plugins/messages/plugin.js +994 -0
  65. package/plugins/messages/plugin.json +59 -0
  66. package/plugins/messages/plugin.public.js +52 -0
  67. package/plugins/messages/templates/call.html +6 -0
  68. package/plugins/messages/tests/after-send.test.js +22 -0
  69. package/plugins/messages/tests/api.test.js +187 -0
  70. package/plugins/messages/tests/call.test.js +497 -0
  71. package/plugins/messages/tests/drafts.test.js +25 -0
  72. package/plugins/messages/tests/messages.test.js +167 -0
  73. package/plugins/theme-roller/CLAUDE.md +65 -0
  74. package/plugins/theme-roller/admin/css/theme-roller.css +43 -0
  75. package/plugins/theme-roller/admin/lib/prompt.js +22 -0
  76. package/plugins/theme-roller/admin/lib/themes.js +196 -0
  77. package/plugins/theme-roller/admin/lib/view.js +88 -0
  78. package/plugins/theme-roller/admin/templates/theme-roller.html +73 -0
  79. package/plugins/theme-roller/admin/views/theme-roller-view.js +639 -0
  80. package/plugins/theme-roller/config.js +1 -0
  81. package/plugins/theme-roller/docs/guide.md +104 -0
  82. package/plugins/theme-roller/plugin.js +263 -0
  83. package/plugins/theme-roller/plugin.json +42 -0
  84. package/plugins/theme-roller/tests/api.test.js +161 -0
  85. package/plugins/theme-roller/tests/prompt-values.test.js +22 -0
  86. package/plugins/theme-roller/tests/themes.test.js +77 -0
  87. package/plugins/theme-roller/tests/view.test.js +55 -0
  88. package/server/routes/public.js +16 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Version history: every earlier version of a document, who saved it and
3
+ * when, a preview, and restore.
4
+ *
5
+ * A modal rather than a panel of the screen, because it is a detour: look,
6
+ * perhaps restore, go back to writing. The screen reloads the document off
7
+ * the modal's `close` event when something was restored.
8
+ *
9
+ * @module docs/admin/lib/versions
10
+ */
11
+
12
+ import {docsApi, esc, sanitizeHtml, fetchBlob, downloadBlob} from '/plugins/docs/admin/lib/api.js';
13
+ import {fileName} from '/plugins/docs/admin/lib/convert.js';
14
+ import {pdfFacts} from '/plugins/docs/admin/lib/pdf-view.js';
15
+ import {fmtDateTime} from '/plugins/_lib/admin/ui/dates.js';
16
+
17
+ export class VersionHistory {
18
+ constructor() {
19
+ this.modal = null;
20
+ this.restored = false;
21
+ }
22
+
23
+ /**
24
+ * @param {string} docId
25
+ * @param {object} [opts]
26
+ * @param {string} [opts.session] this tab's edit session (restore needs the lock)
27
+ * @param {boolean} [opts.canRestore] false when reading only
28
+ * @returns {Promise<object|null>} the modal
29
+ */
30
+ async show(docId, {session = '', canRestore = true} = {}) {
31
+ let versions;
32
+ try {
33
+ versions = await docsApi.versions(docId);
34
+ } catch (err) {
35
+ E.toast(err.message || 'Could not load version history', {type: 'error'});
36
+ return null;
37
+ }
38
+ this.restored = false;
39
+ const modal = E.modal({
40
+ title: 'Version history',
41
+ size: 'large',
42
+ scrollable: true,
43
+ buttons: [{id: 'close', text: 'Close', variant: 'secondary'}]
44
+ });
45
+ modal.element.appendChild(this._build(docId, versions, modal, {session, canRestore}));
46
+ // A closed modal is hidden, not removed: take it out once its close has run (2.10).
47
+ modal.element.addEventListener('close', () => setTimeout(() => {
48
+ try { modal.destroy?.(); } catch { /* gone */ }
49
+ modal.element?.remove();
50
+ }, 400), {once: true});
51
+ modal.open();
52
+ this.modal = modal;
53
+ return modal;
54
+ }
55
+
56
+ _build(docId, versions, modal, {session, canRestore}) {
57
+ const wrap = document.createElement('div');
58
+ wrap.className = 'docs-versions';
59
+ if (!versions.length) {
60
+ const empty = document.createElement('p');
61
+ empty.className = 'docs-versions__empty';
62
+ empty.textContent = 'No earlier versions yet. One is kept each time the content changes.';
63
+ wrap.appendChild(empty);
64
+ return wrap;
65
+ }
66
+ if (!canRestore) {
67
+ const note = document.createElement('p');
68
+ note.className = 'docs-versions__note';
69
+ note.textContent = 'You are reading this document, so you can look at earlier versions but not restore them.';
70
+ wrap.appendChild(note);
71
+ }
72
+
73
+ const list = document.createElement('div');
74
+ list.className = 'docs-versions__list';
75
+ const preview = document.createElement('div');
76
+ preview.className = 'docs-versions__preview';
77
+ preview.innerHTML = '<p class="docs-versions__hint">Choose a version to see it here.</p>';
78
+ wrap.append(list, preview);
79
+
80
+ versions.forEach((v, i) => {
81
+ const row = document.createElement('button');
82
+ row.type = 'button';
83
+ row.className = 'docs-version';
84
+ row.dataset.num = String(v.num);
85
+ const name = document.createElement('strong');
86
+ name.textContent = v.createdAt ? fmtDateTime(v.createdAt) : `Version ${v.num}`;
87
+ const meta = document.createElement('span');
88
+ meta.className = 'docs-version__when';
89
+ meta.textContent = [v.createdAt ? D(v.createdAt).fromNow() : '', v.savedByName ? `by ${v.savedByName}` : ''].filter(Boolean).join(' · ');
90
+ const excerpt = document.createElement('span');
91
+ excerpt.className = 'docs-version__preview';
92
+ // A PDF document's version is a file (2.13): say what it was, then what it said.
93
+ excerpt.textContent = v.format === 'pdf' ? [`PDF${pdfFacts(v.file) ? ` · ${pdfFacts(v.file)}` : ''}`, this._excerpt(v.content, 100)].filter(x => x && x !== 'Empty').join(' - ') : this._excerpt(v.content);
94
+ row.append(name, meta, excerpt);
95
+ row.addEventListener('click', () => {
96
+ list.querySelectorAll('.docs-version').forEach(r => r.classList.toggle('is-active', r === row));
97
+ this._preview(preview, docId, v, modal, {session, canRestore});
98
+ });
99
+ list.appendChild(row);
100
+ if (i === 0) queueMicrotask(() => row.click());
101
+ });
102
+
103
+ // Each version's menu (2.10): right-click a row or Shift+F10 on it. Click previews.
104
+ const byNum = new Map(versions.map(v => [String(v.num), v]));
105
+ const menu = E.contextMenu(list, {
106
+ cursor: 'glyph', match: '.docs-version', inherit: false, density: 'compact',
107
+ items: (el) => {
108
+ const v = byNum.get(el.dataset.num);
109
+ if (!v) return [];
110
+ const items = [
111
+ {header: v.createdAt ? fmtDateTime(v.createdAt) : `Version ${v.num}`},
112
+ {label: 'Preview', icon: 'eye', action: () => el.click()}
113
+ ];
114
+ if (v.format === 'pdf') items.push({label: 'Download this version', icon: 'download', action: () => this._downloadPdf(docId, v)});
115
+ if (canRestore) items.push({label: 'Restore this version…', icon: 'rotate-ccw', action: () => this._restore(docId, v, modal, session)});
116
+ return items;
117
+ }
118
+ });
119
+ modal.element.addEventListener('close', () => menu?.destroy?.(), {once: true});
120
+ return wrap;
121
+ }
122
+
123
+ _preview(host, docId, v, modal, {session, canRestore}) {
124
+ host.replaceChildren();
125
+ const bar = document.createElement('div');
126
+ bar.className = 'docs-versions__bar';
127
+ const label = document.createElement('span');
128
+ label.textContent = `Version ${v.num}${v.title ? ` - ${v.title}` : ''}`;
129
+ bar.appendChild(label);
130
+ if (canRestore) {
131
+ const restore = document.createElement('button');
132
+ restore.type = 'button';
133
+ restore.className = 'btn btn-sm btn-primary';
134
+ restore.textContent = 'Restore this version';
135
+ restore.addEventListener('click', () => this._restore(docId, v, modal, session));
136
+ bar.appendChild(restore);
137
+ }
138
+ const body = document.createElement('div');
139
+ if (v.format === 'pdf') {
140
+ // A PDF version (2.13): the file's facts, a download, and the text read out of it.
141
+ body.className = 'docs-versions__pdf';
142
+ const facts = document.createElement('div');
143
+ facts.className = 'docs-versions__facts';
144
+ facts.textContent = `PDF${pdfFacts(v.file) ? ` · ${pdfFacts(v.file)}` : ''}${v.file?.name ? ` · ${v.file.name}` : ''}`;
145
+ const dl = document.createElement('button');
146
+ dl.type = 'button';
147
+ dl.className = 'btn btn-sm btn-outline';
148
+ dl.textContent = 'Download this version';
149
+ dl.addEventListener('click', () => this._downloadPdf(docId, v));
150
+ const extract = document.createElement('div');
151
+ extract.className = 'docs-versions__extract';
152
+ extract.textContent = this._excerpt(v.content, 4000) === 'Empty' ? 'No text could be read from this file.' : this._excerpt(v.content, 4000);
153
+ body.append(facts, dl, extract);
154
+ host.append(bar, body);
155
+ return;
156
+ }
157
+ body.className = 'docs-versions__body docs-surface';
158
+ // Sanitised like any document going into the admin.
159
+ const html = String(v.content || '');
160
+ if (window.DOMPurify?.sanitize) body.innerHTML = sanitizeHtml(html);
161
+ else body.textContent = this._excerpt(html, 100000);
162
+ host.append(bar, body);
163
+ }
164
+
165
+ async _downloadPdf(docId, v) {
166
+ try {
167
+ downloadBlob(fileName(`${v.title || 'Untitled'} (version ${v.num})`, 'pdf'), await fetchBlob(docsApi.versionFileUrl(docId, v.num, {download: true})));
168
+ } catch (err) {
169
+ E.toast(err.message || 'Could not download that version', {type: 'error'});
170
+ }
171
+ }
172
+
173
+ async _restore(docId, version, modal, session) {
174
+ const ok = await E.confirm(`Restore the version from <strong>${esc(version.createdAt ? fmtDateTime(version.createdAt) : `#${version.num}`)}</strong>? What the document says now is kept as a version first.`,
175
+ {title: 'Restore version', confirmText: 'Restore'});
176
+ if (!ok) return;
177
+ try {
178
+ await docsApi.restoreVersion(docId, version.num, session);
179
+ this.restored = true;
180
+ E.toast('Version restored', {type: 'success'});
181
+ modal.close();
182
+ } catch (err) {
183
+ E.toast(err.message || 'Could not restore', {type: 'error'});
184
+ }
185
+ }
186
+
187
+ _excerpt(content, max = 160) {
188
+ const text = String(content || '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
189
+ if (!text) return 'Empty';
190
+ return text.length > max ? `${text.slice(0, max)}…` : text;
191
+ }
192
+ }
@@ -0,0 +1,92 @@
1
+ <!--
2
+ Docs - Home: a rail of places (Recent, Starred, Shared with me, My documents
3
+ and its folders, Templates, Trash) and the documents in the one chosen.
4
+
5
+ Bound with M.applyBindings (admin/views/docs.js). The card and folder
6
+ templates are inserted by the view: the router renders this file through
7
+ _.render before mounting it, which would replace any {{…}} here with nothing.
8
+ -->
9
+ <div class="docs-home" data-bind-class="railCollapsed.value && 'is-rail-collapsed'">
10
+
11
+ <aside class="docs-rail" aria-label="Docs places">
12
+ <button type="button" class="btn btn-primary docs-rail__new" data-on-click="openNewMenu" aria-haspopup="menu">
13
+ <span data-icon="plus" data-icon-size="16"></span> <span class="docs-rail__label">New</span>
14
+ </button>
15
+ <nav class="docs-rail__nav">
16
+ <div class="docs-rail__places">
17
+ <button type="button" class="docs-place" data-place="recent" data-bind-class="view.value === 'recent' && 'is-active'"><span data-icon="clock" data-icon-size="16"></span><span class="docs-rail__label">Recent</span></button>
18
+ <button type="button" class="docs-place" data-place="starred" data-bind-class="view.value === 'starred' && 'is-active'"><span data-icon="star" data-icon-size="16"></span><span class="docs-rail__label">Starred</span><span class="docs-place__count" data-bind-text="placeCounts.value.starred"></span></button>
19
+ <button type="button" class="docs-place" data-place="shared" data-bind-class="view.value === 'shared' && 'is-active'"><span data-icon="users" data-icon-size="16"></span><span class="docs-rail__label">Shared with me</span><span class="docs-place__count" data-bind-text="placeCounts.value.shared"></span></button>
20
+ <button type="button" class="docs-place" data-place="mine" data-bind-class="view.value === 'mine' && !folderId.value && 'is-active'"><span data-icon="folder-open" data-icon-size="16"></span><span class="docs-rail__label">My documents</span><span class="docs-place__count" data-bind-text="placeCounts.value.mine" title="Documents not in a folder"></span></button>
21
+ <button type="button" class="docs-place" data-place="templates" data-bind-class="view.value === 'templates' && 'is-active'"><span data-icon="layout" data-icon-size="16"></span><span class="docs-rail__label">Templates</span></button>
22
+ <button type="button" class="docs-place" data-place="trash" data-bind-class="view.value === 'trash' && 'is-active'"><span data-icon="trash" data-icon-size="16"></span><span class="docs-rail__label">Trash</span><span class="docs-place__count" data-bind-text="placeCounts.value.trash"></span></button>
23
+ <div class="docs-rail__ext"></div>
24
+ </div>
25
+ <div class="docs-rail__section">
26
+ <span class="docs-rail__label">Folders</span>
27
+ <span data-help-title="Folders" data-help="Folders are yours alone - documents shared with you appear under Shared with me instead. Right-click a folder (or use its menu) to add a subfolder, rename or delete it. Deleting a folder never deletes documents: they move up a level."></span>
28
+ <button type="button" class="btn btn-xs btn-ghost docs-rail__add" title="New folder" aria-label="New folder" data-on-click="newFolder">
29
+ <span data-icon="folder-plus" data-icon-size="14"></span>
30
+ </button>
31
+ </div>
32
+ <div class="docs-rail__folders" data-each="folderRows.value key=key"></div>
33
+ </nav>
34
+ </aside>
35
+
36
+ <section class="docs-main" aria-labelledby="docs-main-title">
37
+ <div class="docs-main__bar">
38
+ <button type="button" class="btn btn-sm btn-ghost docs-main__rail-toggle" data-on-click="toggleRail"
39
+ title="Show or hide the side panel" aria-label="Show or hide the side panel">
40
+ <span data-icon="sidebar" data-icon-size="16"></span>
41
+ </button>
42
+ <h2 class="docs-main__title" id="docs-main-title">
43
+ <span data-bind-text="viewTitle.value"></span>
44
+ <span class="docs-main__count" data-bind-text="countText.value"></span>
45
+ </h2>
46
+ <span class="docs-main__help" data-help-title="Places" data-help="Recent is what you opened lately; Starred what you starred (the star on a card or in a document). Shared with me holds what other people shared with you, to view, suggest in or edit. My documents and its folders are yours. Templates start new documents. Trash keeps what you threw away for a while. Right-click a document, template or folder - or its ⋮, or Shift+F10 - for everything you can do to it. New and Import are in the banner above; you can also drop files on the list. A PDF you add is kept as it is - shared, linked, filed and versioned like any document, and shown in the browser's own viewer."></span>
47
+ <div class="docs-main__search" data-bind-hidden="isExt.value">
48
+ <span data-icon="search" data-icon-size="14"></span>
49
+ <input type="search" class="form-input form-input-sm" placeholder="Search documents"
50
+ aria-label="Search documents" data-model="query.value">
51
+ <span data-help-title="Search" data-help="Matches titles and the text of documents in the place you are looking at. Press / to jump here."></span>
52
+ </div>
53
+ <select class="form-input form-input-sm docs-main__sort" aria-label="Sort by" data-model="sort.value" data-bind-hidden="isTemplates.value || isExt.value">
54
+ <option value="updated">Last modified</option>
55
+ <option value="opened">Last opened by me</option>
56
+ <option value="title">Title</option>
57
+ <option value="created">Created</option>
58
+ </select>
59
+ <div class="docs-main__layout" role="group" aria-label="Layout" data-bind-hidden="isExt.value">
60
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="gridLayout" title="Grid" aria-label="Grid"
61
+ data-bind-class="layout.value === 'grid' && 'is-on'"><span data-icon="grid" data-icon-size="16"></span></button>
62
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="listLayout" title="List" aria-label="List"
63
+ data-bind-class="layout.value === 'list' && 'is-on'"><span data-icon="list" data-icon-size="16"></span></button>
64
+ </div>
65
+ </div>
66
+
67
+ <div class="docs-trash-note" data-bind-hidden="!isTrash.value" hidden>
68
+ <span data-bind-text="trashNote.value"></span>
69
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="emptyTrash" data-bind-disabled="!rows.value.length">Empty Trash</button>
70
+ </div>
71
+
72
+ <div class="docs-main__body" data-bind-class="bodyClass.value">
73
+ <div class="docs-drop" aria-hidden="true">
74
+ <span data-icon="upload" data-icon-size="32"></span>
75
+ <span>Drop .md, .html, .txt or .pdf files to add them</span>
76
+ </div>
77
+ <div class="docs-empty" data-bind-hidden="!showEmpty.value" hidden>
78
+ <span data-icon="file-text" data-icon-size="40"></span>
79
+ <p data-bind-text="emptyText.value"></p>
80
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="newDoc" data-bind-hidden="!canCreateHere.value">
81
+ <span data-icon="plus" data-icon-size="14"></span> New document
82
+ </button>
83
+ </div>
84
+ <!-- A skeleton while a place's list first loads (admin/js/lib/loading.js); a reload keeps the list on screen. -->
85
+ <div class="docs-loading dm-loading-slot" data-bind-hidden="!skeleton.value">Loading…</div>
86
+ <div class="docs-cards" data-bind-hidden="isTemplates.value || isExt.value" data-each="rows.value key=id"></div>
87
+ <div class="docs-cards docs-cards--templates" data-bind-hidden="!isTemplates.value" data-each="templateRows.value key=id"></div>
88
+ <div class="docs-ext-view" data-bind-hidden="!isExt.value"></div>
89
+ </div>
90
+ <input type="file" class="docs-import-input" accept=".md,.markdown,.html,.htm,.txt,.pdf,text/markdown,text/html,text/plain,application/pdf" multiple hidden>
91
+ </section>
92
+ </div>
@@ -0,0 +1,86 @@
1
+ <!--
2
+ Docs - one document. Top bar, formatting toolbar, the page, an Outline
3
+ panel and a status line. Bound with M.applyBindings (admin/views/editor.js);
4
+ the Outline's item template is inserted by the view (the router's _.render
5
+ would empty any {{…}} here).
6
+ -->
7
+ <div class="docs-ed" data-bind-class="edClass.value">
8
+
9
+ <header class="docs-ed__top">
10
+ <button type="button" class="btn btn-sm btn-ghost docs-ed__back" data-on-click="back" title="All documents" aria-label="All documents">
11
+ <span data-icon="arrow-left" data-icon-size="18"></span>
12
+ </button>
13
+ <div class="docs-ed__titlebox">
14
+ <input class="docs-ed__title" id="docs-ed-title" aria-label="Document title" placeholder="Untitled"
15
+ data-model="title.value" data-bind-readonly="titleLocked.value" maxlength="200" autocomplete="off">
16
+ <div class="docs-ed__crumbs">
17
+ <span data-bind-text="whereText.value"></span>
18
+ <span class="docs-ed__save" data-bind-class="saveClass.value" data-bind-text="saveState.value" aria-live="polite"></span>
19
+ </div>
20
+ </div>
21
+ <button type="button" class="btn btn-sm btn-ghost docs-ed__star" data-on-click="toggleStar"
22
+ data-bind-class="starred.value && 'is-on'" data-bind-title="starTitle.value" aria-label="Star">
23
+ <span data-icon="star" data-icon-size="18"></span>
24
+ </button>
25
+
26
+ <div class="docs-ed__status-chips">
27
+ <span class="docs-chip docs-chip--locked" data-bind-hidden="!lockedBy.value" hidden>
28
+ <span data-icon="lock" data-icon-size="12"></span> <span data-bind-text="lockText.value"></span>
29
+ </span>
30
+ <button type="button" class="btn btn-xs btn-outline" data-on-click="takeOver" data-bind-hidden="!canTakeOver.value" hidden data-bind-text="takeOverText.value">Take over</button>
31
+ <button type="button" class="btn btn-xs btn-primary" data-on-click="editNow" data-bind-hidden="!canEditNow.value" hidden>Edit now</button>
32
+ <span class="docs-chip" data-bind-hidden="!viewOnly.value" hidden>
33
+ <span data-icon="eye" data-icon-size="12"></span> View only
34
+ </span>
35
+ <span class="docs-chip docs-chip--locked" data-bind-hidden="!frozenText.value" data-bind-title="frozenText.value" hidden>
36
+ <span data-icon="lock" data-icon-size="12"></span> Protected
37
+ </span>
38
+ <span class="docs-chip docs-chip--pdf" data-bind-hidden="!isPdf.value" data-bind-title="pdfName.value" hidden>
39
+ <span data-icon="file" data-icon-size="12"></span> <span data-bind-text="pdfText.value"></span>
40
+ </span>
41
+ <span class="docs-chip docs-chip--danger" data-bind-hidden="!inTrash.value" hidden>In Trash</span>
42
+ <button type="button" class="btn btn-xs btn-outline" data-on-click="restoreFromTrash" data-bind-hidden="!canRestore.value" hidden>Restore</button>
43
+ <span data-help-title="Editing" data-help="One person edits a document at a time. While someone else has it open to edit, you see it read only, with their name here - it refreshes by itself, and Edit now appears when they close it. The owner can Take over; whatever the other person had not saved is kept for them as a copy."></span>
44
+ </div>
45
+
46
+ <div class="docs-ed__actions">
47
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="toggleOutline" title="Outline" aria-label="Outline"
48
+ data-bind-class="outlineOpen.value && 'is-on'"><span data-icon="list" data-icon-size="16"></span></button>
49
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="find" title="Find and replace (Ctrl+F)" aria-label="Find and replace">
50
+ <span data-icon="search" data-icon-size="16"></span></button>
51
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="history" title="Version history" aria-label="Version history">
52
+ <span data-icon="clock" data-icon-size="16"></span></button>
53
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="toggleFocus" title="Focus mode (Ctrl+Shift+F)" aria-label="Focus mode"
54
+ data-bind-class="focus.value && 'is-on'"><span data-icon="maximize" data-icon-size="16"></span></button>
55
+ <button type="button" class="btn btn-sm btn-primary docs-ed__share" data-on-click="share" data-bind-hidden="!canShare.value" hidden>
56
+ <span data-icon="user-plus" data-icon-size="14"></span> Share
57
+ </button>
58
+ <button type="button" class="btn btn-sm btn-ghost docs-ed__more" title="More" aria-label="More actions" aria-haspopup="menu">
59
+ <span data-icon="more-vertical" data-icon-size="16"></span>
60
+ </button>
61
+ </div>
62
+ </header>
63
+
64
+ <div class="docs-ed__toolbar"></div>
65
+
66
+ <div class="docs-ed__body">
67
+ <aside class="docs-outline" aria-label="Outline" data-bind-hidden="!outlineOpen.value">
68
+ <div class="docs-outline__head">Outline
69
+ <span data-help="The document's headings. Click one to jump to it. Headings come from the text style menu, or type # and a space at the start of a line."></span>
70
+ </div>
71
+ <p class="docs-outline__empty" data-bind-hidden="outlineRows.value.length" hidden>Headings you add appear here.</p>
72
+ <div class="docs-outline__list" data-each="outlineRows.value key=key"></div>
73
+ </aside>
74
+ <div class="docs-ed__canvas">
75
+ <!-- Shown while the document loads (a skeleton through admin/js/lib/loading.js); the view hides it after. -->
76
+ <div class="docs-ed__loading dm-loading-slot">Loading…</div>
77
+ <div class="docs-page"></div>
78
+ </div>
79
+ <aside class="docs-ed__side" data-bind-hidden="!sideOpen.value" hidden></aside>
80
+ </div>
81
+
82
+ <footer class="docs-ed__foot">
83
+ <span data-bind-text="statsText.value"></span>
84
+ <span class="docs-ed__foot-right" data-bind-text="editedText.value"></span>
85
+ </footer>
86
+ </div>