domma-cms 1.8.23 → 1.8.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,370 +1,605 @@
1
- <!-- Page editor. No page heading: the Tool banner above is the heading ("Pages › …", set by
2
- admin/js/views/page-editor.js), with Back, Editor reference, View on the site and Save in it. This bar
3
- carries the unsaved state; Ctrl+S saves; leaving with unsaved changes asks and keeps a draft. The row
4
- above it (#page-hop, lib/editor-hop.js) finds any page or block and lists recent edits - a page opens
5
- in its own editor (this page's changes kept without asking), a block opens HERE. Blocks are edited in
6
- this editor: a tab each above it (.pe-files) - the page's, the footer's, any opened from the bar - their
7
- HTML and CSS in .pe-block-pane, saved with the page.
8
- Bound with M.applyBindings (see the view): every field is a data-model, and what shows - unsaved,
9
- the problem note, the view mode, the CSS pane, Live Preview, History - is read from the state. The
10
- router renders this file through _.render, which eats any {{…}}: bindings only, never mustache. -->
11
- <div id="page-hop" class="dm-hop-slot"></div>
12
- <div class="pex-bar">
13
- <span data-help-title="Pages and blocks" data-help="The blocks this page uses have a tab each above the editor - edit their HTML and CSS right here, with the preview showing the page as you type. Save saves the page and the blocks you changed (a block used elsewhere changes there too - you are asked first). Any other block opens here in a tab too - pick it from the Blocks list, the search or a recent one. Picking a page opens that page; your unsaved changes here are kept while you go. A dot marks anything with changes not saved yet."></span>
14
- <span class="pex-chip" hidden data-bind-hidden="!dirty.value" title="Changes not saved yet">Unsaved</span>
15
- <small class="pex-problem" hidden data-bind-hidden="!(dirty.value && problem.value)" data-bind-text="problem.value"></small>
16
- <small class="pex-held" hidden data-bind-hidden="!heldNote.value" data-bind-text="heldNote.value"
17
- title="Those blocks' changes were kept from the block editor - open the block's tab to see them, Save saves them"></small>
18
- <span class="pex-spacer"></span>
19
- <a class="btn btn-ghost btn-sm" target="_blank" rel="noopener" hidden data-bind-hidden="!isEdit" data-bind-href="savedPath">
20
- <span data-icon="external-link"></span> View page
21
- </a>
22
- <button type="button" id="discard-btn" class="btn btn-ghost btn-sm" hidden data-bind-hidden="!dirty.value" data-on-click="discard">Discard</button>
23
- <button type="button" id="save-btn" class="btn btn-primary btn-sm" data-on-click="save" data-bind-disabled="saving.value"><span data-icon="save"></span>
24
- <span data-bind-text="saving.value ? 'Saving…' : 'Save'">Save</span></button>
25
- </div>
1
+ <!-- The editor workspace (views/page-editor.js): pages, blocks and components edited side by side, like
2
+ a code editor. Left, the explorer - this page's blocks and components, every page as a tree, every
3
+ block, every component - with a filter box (Ctrl+Shift+E hides it). Across the top, a tab for
4
+ everything open; a dot marks unsaved work, × (or a middle-click) closes. The middle is the editor and
5
+ the preview - split, editor only or preview only (the toolbar's buttons, or Ctrl+\ cycles) - and the
6
+ bottom is the status bar: where you are, what is unsaved, Discard, Save (Ctrl+S) and Save all. Right,
7
+ the dock (the sliders button in the tab strip): the page's details, SEO, DConfig and history; on a block's
8
+ tab its details and the sample values its preview fills in; on a component's tab its details and preview
9
+ props. The explorer's + makes a new page, block or component (a name first).
10
+ No page heading: the Tool banner above is the heading ("Pages › …", set by the view), with Back,
11
+ Editor reference, View on the site, Clone and the editor settings in it.
12
+ Bound with M.applyBindings (see the view): every field is a data-model, and what shows is read from
13
+ the state. The router renders this file through _.render, which eats any {{…}}: bindings only. -->
14
+ <div id="pe-workspace" class="pe-ws"
15
+ data-bind-class="(explorerOn.value ? '' : 'pe-ws--no-explorer') + (dockOn.value ? ' pe-ws--dock' : '') + (fullscreen.value ? ' pe-ws--full' : '')">
26
16
 
27
- <div class="tabs" id="editor-meta-tabs">
28
- <div class="tab-list">
29
- <button class="tab-item active" data-on-click="showTab('edit')">Edit</button>
30
- <button class="tab-item" data-on-click="showTab('details')">Page Details</button>
31
- <button class="tab-item" data-on-click="showTab('seo')">SEO</button>
32
- <button class="tab-item" data-on-click="showTab('dconfig')">DConfig</button>
33
- <button class="tab-item" data-on-click="showTab('live')">Live Preview</button>
34
- <button class="tab-item" hidden data-bind-hidden="!isEdit" data-on-click="showTab('history')">History</button>
35
- </div>
36
- <div class="tab-content">
17
+ <!-- Explorer -->
18
+ <aside class="pe-explorer" aria-label="Pages, blocks and components" hidden data-bind-hidden="!explorerOn.value">
19
+ <div class="pe-ex-find">
20
+ <span class="pe-ex-find-icon" data-icon="search" data-icon-size="14"></span>
21
+ <input type="text" class="form-input form-input--sm pe-ex-input" placeholder="Filter…" data-model="filter.value"
22
+ aria-label="Filter pages, blocks and components" autocomplete="off" spellcheck="false">
23
+ </div>
24
+ <div class="pe-ex-scroll">
25
+ <!-- The blocks the open page uses - its text's first, then the footer's (on every page) - and its components. -->
26
+ <section class="pe-ex-section" hidden data-bind-hidden="!thisPage.value.length">
27
+ <div class="pe-ex-head">
28
+ <button type="button" class="pe-ex-toggle" data-on-click="toggleSection('here')" data-bind-aria-expanded="secHere.value ? 'true' : 'false'">
29
+ <span class="pe-ex-chev" data-bind-class="secHere.value && 'is-open'"></span>This page
30
+ </button>
31
+ </div>
32
+ <div class="pe-ex-list" hidden data-bind-hidden="!secHere.value" data-each="thisPage.value key=key">
33
+ <button type="button" class="pe-ex-item is-block" data-bind-data-id="key" data-bind-class="active.value && 'active'" data-bind-title="title">
34
+ <span class="pe-ex-kind" data-bind-text="kindText"></span>
35
+ <span class="pe-ex-label" data-bind-text="label"></span>
36
+ <span class="pe-ex-dot" hidden data-bind-hidden="!dirty.value">•</span>
37
+ </button>
38
+ </div>
39
+ </section>
40
+ <section class="pe-ex-section">
41
+ <div class="pe-ex-head">
42
+ <button type="button" class="pe-ex-toggle" data-on-click="toggleSection('pages')" data-bind-aria-expanded="secPages.value ? 'true' : 'false'">
43
+ <span class="pe-ex-chev" data-bind-class="secPages.value && 'is-open'"></span>Pages
44
+ </button>
45
+ <button type="button" class="pe-ex-add" title="New page" data-on-click="newPage"><span data-icon="plus" data-icon-size="14"></span></button>
46
+ </div>
47
+ <div class="pe-ex-list" hidden data-bind-hidden="!secPages.value" data-each="pageRows.value key=key">
48
+ <button type="button" class="pe-ex-item" data-bind-data-id="key" data-bind-data-depth="depth" data-bind-title="title"
49
+ data-bind-class="(active.value ? 'active' : '') + (folder.value ? ' is-folder' : '') + (status.value && status.value !== 'published' ? ' is-draft' : '')">
50
+ <span class="pe-ex-chev" hidden data-bind-hidden="!folder.value" data-bind-class="!folded.value && 'is-open'"></span>
51
+ <span class="pe-ex-label" data-bind-text="label.value"></span>
52
+ <span class="pe-ex-meta" data-bind-text="meta.value"></span>
53
+ <span class="pe-ex-dot" hidden data-bind-hidden="!dirty.value">•</span>
54
+ </button>
55
+ </div>
56
+ <p class="pe-ex-none" hidden data-bind-hidden="!secPages.value || pageRows.value.length">No page matches.</p>
57
+ </section>
58
+ <section class="pe-ex-section">
59
+ <div class="pe-ex-head">
60
+ <button type="button" class="pe-ex-toggle" data-on-click="toggleSection('blocks')" data-bind-aria-expanded="secBlocks.value ? 'true' : 'false'">
61
+ <span class="pe-ex-chev" data-bind-class="secBlocks.value && 'is-open'"></span>Blocks
62
+ </button>
63
+ <button type="button" class="pe-ex-add" title="New block" data-on-click="newBlock"><span data-icon="plus" data-icon-size="14"></span></button>
64
+ </div>
65
+ <div class="pe-ex-list" hidden data-bind-hidden="!secBlocks.value" data-each="blockRows.value key=key">
66
+ <button type="button" class="pe-ex-item is-block" data-bind-data-id="key" data-bind-class="active.value && 'active'" data-bind-title="title">
67
+ <span class="pe-ex-label" data-bind-text="label.value"></span>
68
+ <span class="pe-ex-meta" data-bind-text="meta.value"></span>
69
+ <span class="pe-ex-dot" hidden data-bind-hidden="!dirty.value">•</span>
70
+ </button>
71
+ </div>
72
+ <p class="pe-ex-none" hidden data-bind-hidden="!secBlocks.value || blockRows.value.length">No block matches.</p>
73
+ </section>
74
+ <section class="pe-ex-section">
75
+ <div class="pe-ex-head">
76
+ <button type="button" class="pe-ex-toggle" data-on-click="toggleSection('components')" data-bind-aria-expanded="secComps.value ? 'true' : 'false'">
77
+ <span class="pe-ex-chev" data-bind-class="secComps.value && 'is-open'"></span>Components
78
+ </button>
79
+ <button type="button" class="pe-ex-add" title="New component" data-on-click="newComponent"><span data-icon="plus" data-icon-size="14"></span></button>
80
+ </div>
81
+ <div class="pe-ex-list" hidden data-bind-hidden="!secComps.value" data-each="compRows.value key=key">
82
+ <button type="button" class="pe-ex-item is-block" data-bind-data-id="key" data-bind-class="(active.value ? 'active' : '') + (readOnly ? ' is-readonly' : '')" data-bind-title="title">
83
+ <span class="pe-ex-label" data-bind-text="label.value"></span>
84
+ <span class="pe-ex-meta" data-bind-text="meta.value"></span>
85
+ <span class="pe-ex-dot" hidden data-bind-hidden="!dirty.value">•</span>
86
+ </button>
87
+ </div>
88
+ <p class="pe-ex-none" hidden data-bind-hidden="!secComps.value || compRows.value.length">No component matches.</p>
89
+ </section>
90
+ </div>
91
+ </aside>
92
+
93
+ <!-- Tabs, editor, status bar -->
94
+ <div class="pe-main">
95
+ <div class="pe-tabs">
96
+ <button type="button" class="pe-icon-btn pe-tabs-explorer" title="Show or hide the list (Ctrl+Shift+E)" data-on-click="toggleExplorer"
97
+ data-bind-class="explorerOn.value && 'active'" data-bind-aria-pressed="explorerOn.value ? 'true' : 'false'"><span data-icon="menu"></span></button>
98
+ <!-- The open tabs: pages, blocks and components in the order opened. The view listens on the strip. -->
99
+ <div class="pe-tabs-list" role="tablist" aria-label="Open pages, blocks and components" data-each="tabs.value key=id">
100
+ <div class="pe-tab" role="tab" tabindex="0" data-bind-data-id="id" data-bind-title="title.value"
101
+ data-bind-class="(active.value ? 'active' : '') + ' is-' + kind" data-bind-aria-selected="active.value ? 'true' : 'false'">
102
+ <span class="pe-tab-kind" data-bind-text="kindText"></span>
103
+ <span class="pe-tab-label" data-bind-text="label.value"></span>
104
+ <span class="pe-tab-dot" hidden data-bind-hidden="!dirty.value">•</span>
105
+ <button type="button" class="pe-tab-close" tabindex="-1" aria-label="Close" title="Close">×</button>
106
+ </div>
107
+ </div>
108
+ <span class="pex-spacer"></span>
109
+ <button type="button" class="pe-icon-btn" title="Settings - the page's details, SEO, DConfig and history; a component's details and preview props" data-on-click="toggleDock"
110
+ data-bind-class="dockOn.value && 'active'" data-bind-aria-pressed="dockOn.value ? 'true' : 'false'"><span data-icon="sliders"></span></button>
111
+ </div>
112
+
113
+ <div class="pe-empty" hidden data-bind-hidden="active.value">
114
+ <p>Nothing open. Pick a page, a block or a component from the list<span hidden data-bind-hidden="explorerOn.value"> (Ctrl+Shift+E shows it)</span>,
115
+ or <button type="button" class="btn btn-ghost btn-sm" data-on-click="newPage">start a new page</button>.</p>
116
+ </div>
37
117
 
38
- <!-- Edit -->
39
- <div class="tab-panel active">
40
- <div class="card editor-card" data-bind-class="fullscreen.value && 'editor-fullscreen'">
41
- <div class="card-body p-0">
42
- <!-- The page and the blocks it uses (its body's, then the footer's), one tab each. The
43
- tabs are buttons with data-name; the view listens on the strip. -->
44
- <div class="pe-files" role="tablist" aria-label="The page and its blocks" hidden data-bind-hidden="!blockTabs.value.length">
45
- <button type="button" class="pe-file" role="tab" data-name="" data-bind-class="!blockTab.value && 'active'"
46
- data-bind-aria-selected="!blockTab.value">
47
- <span class="pe-file-kind">Page</span><span class="pe-file-dot" hidden data-bind-hidden="!pageDirty.value">•</span>
48
- </button>
49
- <span class="pe-files-list" data-each="blockTabs.value key=name">
50
- <button type="button" class="pe-file is-block" role="tab" data-bind-data-name="name" data-bind-title="title"
51
- data-bind-class="active.value && 'active'" data-bind-aria-selected="active.value">
52
- <span class="pe-file-kind" data-bind-text="kindText"></span><span class="pe-file-name" data-bind-text="name"></span><span class="pe-file-dot" hidden data-bind-hidden="!dirty.value">•</span>
53
- </button>
54
- </span>
118
+ <div class="pe-stage" hidden data-bind-hidden="!active.value">
119
+ <!-- On a block's or a component's tab the Markdown tools step aside; the layout buttons and fullscreen stay. -->
120
+ <div class="editor-toolbar" id="editor-toolbar" data-bind-class="(blockTab.value || compTab.value) && 'pe-toolbar--block'"></div>
121
+ <div class="editor-body" id="editor-body" data-bind-class="'editor-mode-' + viewMode.value">
122
+ <div class="editor-pane editor-pane--write">
123
+ <!-- The body is not a data-model: the folding manager owns what the textarea shows. -->
124
+ <textarea id="markdown-editor" class="editor-textarea" data-bind-hidden="cssMode.value || blockTab.value || compTab.value"
125
+ placeholder="Write your page content in Markdown..."></textarea>
126
+ <textarea id="css-editor" class="editor-textarea" hidden
127
+ data-bind-hidden="!cssMode.value || blockTab.value || compTab.value" data-model="css.value" placeholder="/* Custom CSS… */"></textarea>
128
+ <!-- The open block: its HTML or its CSS, in the body's own editor (lib/plain-editor.js, mounted by
129
+ the view). The bar is moved into the toolbar row by the view, where the Markdown tools step aside. -->
130
+ <div class="pe-block-pane" hidden data-bind-hidden="!blockTab.value">
131
+ <div class="pe-block-bar" hidden data-bind-hidden="!blockTab.value">
132
+ <div class="pe-block-parts" role="group" aria-label="Part of the block">
133
+ <button type="button" class="editor-view-btn" data-on-click="setBlockPart('html')"
134
+ data-bind-class="blockPart.value === 'html' && 'active'">HTML</button>
135
+ <button type="button" class="editor-view-btn" data-on-click="setBlockPart('css')"
136
+ data-bind-class="blockPart.value === 'css' && 'active'">CSS</button>
137
+ </div>
138
+ <span class="pex-spacer"></span>
139
+ <button type="button" class="editor-view-btn" title="Tidy the HTML's indentation" data-on-click="formatBlock"
140
+ data-bind-disabled="blockPart.value !== 'html' || !blockReady.value"><span data-icon="code"></span></button>
141
+ </div>
142
+ <p class="pe-block-note" hidden data-bind-hidden="!blockNote.value" data-bind-text="blockNote.value"></p>
143
+ <div class="pe-block-edit" data-part="html" hidden data-bind-hidden="!blockReady.value || blockPart.value !== 'html'"></div>
144
+ <div class="pe-block-edit" data-part="css" hidden data-bind-hidden="!blockReady.value || blockPart.value !== 'css'"></div>
55
145
  </div>
56
- <!-- On a block's tab the Markdown tools step aside; the view modes and fullscreen stay. -->
57
- <div class="editor-toolbar" id="editor-toolbar" data-bind-class="blockTab.value && 'pe-toolbar--block'"></div>
58
- <div class="editor-body" id="editor-body" data-bind-class="'editor-mode-' + viewMode.value">
59
- <div class="editor-pane editor-pane--write">
60
- <!-- The body is not a data-model: the folding manager owns what the textarea shows. -->
61
- <textarea id="markdown-editor" class="editor-textarea" data-bind-hidden="cssMode.value || blockTab.value"
62
- placeholder="Write your page content in Markdown..."></textarea>
63
- <textarea id="css-editor" class="editor-textarea" hidden
64
- data-bind-hidden="!cssMode.value || blockTab.value" data-model="css.value" placeholder="/* Custom CSS… */"></textarea>
65
- <!-- The open block: its HTML or its CSS (two simple-editors, mounted by the view). The
66
- bar is moved into the toolbar row by the view, where the Markdown tools step aside. -->
67
- <div class="pe-block-pane" hidden data-bind-hidden="!blockTab.value">
68
- <div class="pe-block-bar" hidden data-bind-hidden="!blockTab.value">
69
- <div class="pe-block-parts" role="group" aria-label="Part of the block">
70
- <button type="button" class="editor-view-btn" data-on-click="setBlockPart('html')"
71
- data-bind-class="blockPart.value === 'html' && 'active'">HTML</button>
72
- <button type="button" class="editor-view-btn" data-on-click="setBlockPart('css')"
73
- data-bind-class="blockPart.value === 'css' && 'active'">CSS</button>
74
- </div>
75
- <small class="pe-block-uses" data-bind-text="blockUses.value"></small>
76
- <span class="pex-spacer"></span>
77
- <button type="button" class="editor-view-btn" title="Tidy the HTML's indentation" data-on-click="formatBlock"
78
- data-bind-disabled="blockPart.value !== 'html' || !blockReady.value"><span data-icon="code"></span></button>
79
- <button type="button" class="editor-view-btn pe-block-open" title="Open in the Blocks editor - samples, parameters, every page it is on"
80
- data-on-click="openBlockEditor"><span data-icon="external-link"></span></button>
81
- </div>
82
- <p class="pe-block-note" hidden data-bind-hidden="!blockNote.value" data-bind-text="blockNote.value"></p>
83
- <div class="pe-block-edit" data-part="html" hidden data-bind-hidden="!blockReady.value || blockPart.value !== 'html'"></div>
84
- <div class="pe-block-edit" data-part="css" hidden data-bind-hidden="!blockReady.value || blockPart.value !== 'css'"></div>
146
+ <!-- The open component: its four source blocks, each in the body's own editor (mounted by the view),
147
+ recompiled as you type; a plugin's is read only. Its bar goes into the toolbar row like the block's. -->
148
+ <div class="pe-comp-pane" hidden data-bind-hidden="!compTab.value">
149
+ <div class="pe-comp-bar" hidden data-bind-hidden="!compTab.value">
150
+ <div class="pe-block-parts" role="group" aria-label="Part of the component">
151
+ <button type="button" class="editor-view-btn" data-on-click="setCompPart('template')" data-bind-class="compPart.value === 'template' && 'active'">&lt;template&gt;</button>
152
+ <button type="button" class="editor-view-btn" data-on-click="setCompPart('props')" data-bind-class="compPart.value === 'props' && 'active'">&lt;props&gt;</button>
153
+ <button type="button" class="editor-view-btn" data-on-click="setCompPart('script')" data-bind-class="compPart.value === 'script' && 'active'">&lt;script&gt;</button>
154
+ <button type="button" class="editor-view-btn" data-on-click="setCompPart('style')" data-bind-class="compPart.value === 'style' && 'active'">&lt;style&gt;</button>
85
155
  </div>
156
+ <span class="pe-comp-state" data-bind-class="'is-' + compState.value" hidden data-bind-hidden="!compReady.value || compReadOnly.value">
157
+ <span class="pe-comp-dot" aria-hidden="true"></span><span data-bind-text="compStateText.value"></span></span>
158
+ <span class="pex-spacer"></span>
86
159
  </div>
87
- <div class="editor-divider" id="editor-divider" role="separator" aria-orientation="vertical"
88
- tabindex="0" aria-label="Resize editor panes" aria-valuemin="15" aria-valuemax="85"
89
- aria-valuenow="50" title="Drag to resize - double-click to reset"></div>
90
- <div class="editor-pane editor-pane--preview">
91
- <!-- An iframe, not a div: the page background comes from `body {
92
- background-color: var(--dm-background) }`, which cannot apply
93
- to a fragment, and the site stylesheet cannot be loaded into
94
- the admin document without restyling the admin itself. -->
95
- <iframe id="markdown-preview" class="editor-preview" title="Page preview"
96
- sandbox="allow-same-origin allow-scripts allow-forms allow-popups"></iframe>
160
+ <p class="pe-block-note" hidden data-bind-hidden="!compNote.value" data-bind-text="compNote.value"></p>
161
+ <p class="pe-block-note pe-comp-ro" hidden data-bind-hidden="!compReady.value || !compReadOnly.value">
162
+ <span data-icon="lock" data-icon-size="14"></span> This component comes from a plugin, so it cannot be changed here.
163
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="copyComponent">Copy to a new component</button> to make your own version.</p>
164
+ <pre class="pe-comp-errors" hidden data-bind-hidden="!compReady.value || compState.value !== 'error'" data-bind-text="compErrors.value"></pre>
165
+ <div class="pe-comp-edit" data-part="template" hidden data-bind-hidden="!compReady.value || compPart.value !== 'template'"></div>
166
+ <div class="pe-comp-edit" data-part="props" hidden data-bind-hidden="!compReady.value || compPart.value !== 'props'"></div>
167
+ <div class="pe-comp-edit" data-part="script" hidden data-bind-hidden="!compReady.value || compPart.value !== 'script'"></div>
168
+ <div class="pe-comp-edit" data-part="style" hidden data-bind-hidden="!compReady.value || compPart.value !== 'style'"></div>
169
+ </div>
170
+ </div>
171
+ <div class="editor-divider" id="editor-divider" role="separator" aria-orientation="vertical"
172
+ tabindex="0" aria-label="Resize editor panes" aria-valuemin="15" aria-valuemax="85"
173
+ aria-valuenow="50" title="Drag to resize - double-click to reset"></div>
174
+ <div class="editor-pane editor-pane--preview">
175
+ <!-- Preview only: the width presets, Refresh and Share link (the full page, rendered by the
176
+ server from what is in the editor now - what saving would ship). -->
177
+ <div class="pe-preview-bar" hidden data-bind-hidden="viewMode.value !== 'preview'">
178
+ <div class="live-preview-widths" role="group" aria-label="Preview width">
179
+ <button type="button" class="live-preview-width" title="Full width" data-on-click="setWidth('full')"
180
+ data-bind-class="liveWidth.value === 'full' && 'active'"><span data-icon="monitor"></span></button>
181
+ <button type="button" class="live-preview-width" title="Tablet (1024px)" data-on-click="setWidth('1024')"
182
+ data-bind-class="liveWidth.value === '1024' && 'active'"><span data-icon="tablet"></span></button>
183
+ <button type="button" class="live-preview-width" title="Mobile (390px)" data-on-click="setWidth('390')"
184
+ data-bind-class="liveWidth.value === '390' && 'active'"><span data-icon="smartphone"></span></button>
97
185
  </div>
186
+ <span class="live-preview-status" data-bind-text="previewStatus.value"></span>
187
+ <button type="button" class="btn btn-ghost btn-sm" data-on-click="refreshPreview"><span data-icon="refresh-cw"></span> Refresh</button>
188
+ <button type="button" class="btn btn-ghost btn-sm" hidden data-bind-hidden="!canView.value || compTab.value" data-on-click="share"><span data-icon="link"></span> Share link</button>
189
+ </div>
190
+ <!-- An iframe, not a div: the page background comes from `body { background-color:
191
+ var(--dm-background) }`, which cannot apply to a fragment, and the site stylesheet
192
+ cannot be loaded into the admin document without restyling the admin itself. -->
193
+ <div class="pe-preview-stage">
194
+ <iframe id="markdown-preview" class="editor-preview" title="Preview" data-bind-hidden="compTab.value"
195
+ data-bind-data-width="viewMode.value === 'preview' && liveWidth.value !== 'full' ? liveWidth.value : null"
196
+ sandbox="allow-same-origin allow-scripts allow-forms allow-popups"></iframe>
197
+ <!-- A component on its own: the component editor's preview page, which mounts the compiled
198
+ element (admin/preview/component-preview.html) - reloaded per compile, because a custom
199
+ element can be defined once per document. -->
200
+ <iframe id="component-preview" class="editor-preview" title="Component preview" src="/admin/preview/component-preview.html"
201
+ hidden data-bind-hidden="!compTab.value"
202
+ data-bind-data-width="viewMode.value === 'preview' && liveWidth.value !== 'full' ? liveWidth.value : null"></iframe>
98
203
  </div>
99
204
  </div>
100
205
  </div>
101
206
  </div>
102
207
 
103
- <!-- Page Details -->
104
- <div class="tab-panel">
105
- <div class="row mb-3">
106
- <div class="col">
107
- <label class="form-label">URL Path <span data-help-title="URL path" data-help="Where the page lives on the site, starting with /. Changing it moves the page; menu links to the old address are updated. A project whose root URL is a prefix of this address files the page under it."></span></label>
108
- <input id="page-url-path" type="text" data-model="urlPath.value" class="form-input" placeholder="/about or /services/web-dev">
109
- <small class="form-hint">The URL where this page will live. Changing this will update any navigation links
110
- pointing to the old URL.</small>
208
+ <div class="pe-status">
209
+ <span class="pe-status-text" data-bind-text="statusText.value"></span>
210
+ <small class="pex-held" hidden data-bind-hidden="!heldNote.value" data-bind-text="heldNote.value"
211
+ title="Those blocks have changes kept as drafts - open the block to see them, Save saves them"></small>
212
+ <small class="pex-problem" hidden data-bind-hidden="activeKind.value !== 'page' || !(dirty.value && problem.value)" data-bind-text="problem.value"></small>
213
+ <span class="pex-spacer"></span>
214
+ <span data-help-title="The editor" data-help="Pages, blocks and components open side by side, a tab each. On the left: the blocks and components this page uses, every page, every block and every component (Ctrl+Shift+E hides the list); + makes a new one. The toolbar's three buttons - or Ctrl+\ - switch between editor and preview, editor only and preview only; preview only has the width presets and the share link. The sliders button (top right) opens the page's details, SEO, DConfig and history; on a block's tab its details, where it is used and the sample values its preview fills in; on a component's tab its details and preview props. Save (Ctrl+S) saves what you are on - a page with the blocks of its you changed; Save all saves every open page, block and component. A block used elsewhere changes there too - you are asked first. A component that does not compile is not saved. Closing a tab keeps unsaved work as a draft, back when you open it again. A dot marks anything with changes not saved yet."></span>
215
+ <span class="pex-chip" hidden data-bind-hidden="!dirty.value" title="Changes not saved yet">Unsaved</span>
216
+ <a class="btn btn-ghost btn-sm" target="_blank" rel="noopener" hidden data-bind-hidden="!canView.value" data-bind-href="viewHref.value">
217
+ <span data-icon="external-link"></span> View page
218
+ </a>
219
+ <button type="button" id="discard-btn" class="btn btn-ghost btn-sm" hidden data-bind-hidden="!activeDirty.value" data-on-click="discard">Discard</button>
220
+ <button type="button" id="save-btn" class="btn btn-primary btn-sm" data-on-click="save" data-bind-disabled="saving.value || !active.value"><span data-icon="save"></span>
221
+ <span data-bind-text="saving.value ? 'Saving…' : 'Save'">Save</span></button>
222
+ <button type="button" id="save-all-btn" class="btn btn-secondary btn-sm" hidden data-bind-hidden="!saveAll.value" data-bind-disabled="saving.value"
223
+ data-bind-text="saveAll.value" data-on-click="saveAllOpen"></button>
224
+ </div>
225
+ </div>
226
+
227
+ <!-- Dock: the page's settings and history; on a block's tab its details and sample values; on a component's tab its details and preview props -->
228
+ <aside class="pe-dock" aria-label="Settings" hidden data-bind-hidden="!dockOn.value">
229
+ <div class="pe-dock-tabs" role="tablist">
230
+ <!-- A page's -->
231
+ <button type="button" class="pe-dock-tab" role="tab" hidden data-bind-hidden="dockKind.value !== 'page'" data-on-click="showDock('details')" data-bind-class="dockTab.value === 'details' && 'active'">Details</button>
232
+ <button type="button" class="pe-dock-tab" role="tab" hidden data-bind-hidden="dockKind.value !== 'page'" data-on-click="showDock('seo')" data-bind-class="dockTab.value === 'seo' && 'active'">SEO</button>
233
+ <button type="button" class="pe-dock-tab" role="tab" hidden data-bind-hidden="dockKind.value !== 'page'" data-on-click="showDock('dconfig')" data-bind-class="dockTab.value === 'dconfig' && 'active'">DConfig</button>
234
+ <button type="button" class="pe-dock-tab" role="tab" hidden data-bind-hidden="dockKind.value !== 'page' || !canView.value" data-on-click="showDock('history')" data-bind-class="dockTab.value === 'history' && 'active'">History</button>
235
+ <!-- A block's -->
236
+ <button type="button" class="pe-dock-tab" role="tab" hidden data-bind-hidden="dockKind.value !== 'block'" data-on-click="showDock('details')" data-bind-class="dockTab.value === 'details' && 'active'">Block</button>
237
+ <button type="button" class="pe-dock-tab" role="tab" hidden data-bind-hidden="dockKind.value !== 'block'" data-on-click="showDock('samples')" data-bind-class="dockTab.value === 'samples' && 'active'">Samples</button>
238
+ <!-- A component's -->
239
+ <button type="button" class="pe-dock-tab" role="tab" hidden data-bind-hidden="dockKind.value !== 'component'" data-on-click="showDock('details')" data-bind-class="dockTab.value === 'details' && 'active'">Component</button>
240
+ <button type="button" class="pe-dock-tab" role="tab" hidden data-bind-hidden="dockKind.value !== 'component'" data-on-click="showDock('props')" data-bind-class="dockTab.value === 'props' && 'active'">Preview props</button>
241
+ <span class="pex-spacer"></span>
242
+ <button type="button" class="pe-icon-btn" title="Close" aria-label="Close the settings" data-on-click="toggleDock">×</button>
243
+ </div>
244
+ <div class="pe-dock-body">
245
+ <p class="pe-dock-none" hidden data-bind-hidden="dockKind.value">Open a page, a block or a component to see its settings.</p>
246
+
247
+ <!-- A block: its details -->
248
+ <div class="pe-dock-panel" hidden data-bind-hidden="dockKind.value !== 'block' || dockTab.value !== 'details'">
249
+ <div class="pe-field">
250
+ <label class="form-label">Short tag <span data-help-title="Using the block" data-help="On a page, the block's name is its tag - [name field=&quot;value&quot; /] - and a collection display draws it per entry with display=&quot;block&quot; block=&quot;name&quot;. The name is fixed once the block exists."></span></label>
251
+ <div class="pe-comp-tagrow"><code class="pe-comp-tag" data-bind-text="blockTagText.value"></code>
252
+ <button type="button" class="btn btn-ghost btn-sm" data-on-click="copyShortcode" title="Copy the collection shortcode that draws this block"><span data-icon="copy"></span> Copy shortcode</button></div>
111
253
  </div>
112
- </div>
113
- <div class="row mb-3">
114
- <div class="col-6">
115
- <label class="form-label">Title *</label>
116
- <input id="field-title" type="text" data-model="title.value" class="form-input" placeholder="Page title">
254
+ <div class="pe-field">
255
+ <label class="form-label" for="block-project">Project
256
+ <span data-help="File the block under a project - it then shows in that project's lists. Leave it on none for a site-wide one."></span></label>
257
+ <select id="block-project" class="form-select" data-model="blockProject.value" data-bind-disabled="!blockReady.value">
258
+ <option value="">- none -</option>
259
+ </select>
117
260
  </div>
118
- <div class="col-3">
119
- <label class="form-label">Layout <span data-help="The page shell: navbar, width, sidebar, footer. Layouts are managed under System › Layouts."></span></label>
120
- <select id="field-layout" class="form-select" data-model="layout.value"></select>
261
+ <div class="pe-field pe-field-checks">
262
+ <label class="form-check-label"><input id="block-bundled" type="checkbox" class="form-check" data-model="blockBundled.value" data-bind-disabled="!blockReady.value"> Bundled
263
+ <span data-help="A bundled block is included in fresh installs by the seed script. Leave it off for one that belongs to this site only."></span></label>
121
264
  </div>
122
- <div class="col-3">
123
- <label class="form-label">Status <span data-help-title="Page status" data-help="Draft and Review pages are hidden from visitors (404). Anyone who can read pages can preview them, and Live Preview and share links show them before publication."></span></label>
124
- <select id="field-status" class="form-select" data-model="status.value">
125
- <option value="draft">Draft</option>
126
- <option value="published">Published</option>
127
- <option value="review">Review</option>
128
- </select>
265
+ <div class="pe-field">
266
+ <label class="form-label">Where it is used</label>
267
+ <p class="pe-dock-none" data-bind-text="blockWhere.value"></p>
129
268
  </div>
130
269
  </div>
131
- <div class="row mb-3">
132
- <div class="col-8">
133
- <label class="form-label">Description</label>
134
- <input id="field-description" type="text" data-model="description.value" class="form-input" placeholder="Short description">
270
+
271
+ <!-- A block: sample values (rows built by the view, with a context menu) -->
272
+ <div class="pe-dock-panel" hidden data-bind-hidden="dockKind.value !== 'block' || dockTab.value !== 'samples'">
273
+ <p class="pe-dock-none">One per placeholder the template uses, filled in for the preview of the block on its own - nothing here is saved. Right-click one (or ⋮, or Shift+F10) to insert its placeholder, copy it or reset the sample.</p>
274
+ <div id="pe-block-samples" class="pe-comp-props"><p class="pe-dock-none">Open a block to see its placeholders.</p></div>
275
+ </div>
276
+
277
+ <!-- A component: its details -->
278
+ <div class="pe-dock-panel" hidden data-bind-hidden="dockKind.value !== 'component' || dockTab.value !== 'details'">
279
+ <div class="pe-field">
280
+ <label class="form-label">Tag <span data-help-title="The tag" data-help="The component is used on a page as this tag, made from its name - or as [component name=&quot;…&quot; /]. The name cannot be changed once the component exists (every page that uses it names it)."></span></label>
281
+ <code class="pe-comp-tag" data-bind-text="compTagText.value"></code>
135
282
  </div>
136
- <div class="col-4">
137
- <label class="form-label">Sort Order <span data-help="Lower numbers come first in navigation and in page lists sorted by navigation order."></span></label>
138
- <input id="field-sort-order" type="number" class="form-input" min="0" data-model="sortOrder.value">
283
+ <div class="pe-field">
284
+ <label class="form-label" for="comp-project">Project
285
+ <span data-help="File the component under a project - it then shows in that project's lists. Leave it on none for a site-wide one."></span></label>
286
+ <select id="comp-project" class="form-select" data-model="compProject.value" data-bind-disabled="compReadOnly.value || !compReady.value">
287
+ <option value="">- none -</option>
288
+ </select>
139
289
  </div>
290
+ <div class="pe-field pe-field-checks">
291
+ <label class="form-check-label"><input id="comp-bundled" type="checkbox" class="form-check" data-model="compBundled.value" data-bind-disabled="compReadOnly.value || !compReady.value"> Bundled
292
+ <span data-help="A bundled component is included in fresh installs by the seed script. Leave it off for one that belongs to this site only."></span></label>
293
+ </div>
294
+ <p class="pe-dock-none" hidden data-bind-hidden="!compReadOnly.value">From a plugin: seen here, not changed. Copy it to a new component to make your own version.</p>
295
+ </div>
296
+
297
+ <!-- A component: the preview props (rows built by the view, with a context menu) -->
298
+ <div class="pe-dock-panel" hidden data-bind-hidden="dockKind.value !== 'component' || dockTab.value !== 'props'">
299
+ <p class="pe-dock-none">One row per prop from the &lt;props&gt; block. Change a value to see it in the preview - it is not saved. Right-click a row (or ⋮, or Shift+F10) to reset it, copy it as an attribute or a whole tag, or jump to it in the &lt;props&gt; block.</p>
300
+ <div id="pe-comp-props" class="pe-comp-props"><p class="pe-dock-none">Declare props in the &lt;props&gt; block to see them here.</p></div>
140
301
  </div>
141
- <div class="row mb-3">
142
- <div class="col-4">
143
- <label class="form-label">Category <span data-help="A free label for grouping pages - collection displays and search can filter by it."></span></label>
144
- <input id="field-category" type="text" data-model="category.value" class="form-input" placeholder="e.g. news, docs, products">
302
+
303
+ <!-- Details -->
304
+ <div class="pe-dock-panel" hidden data-bind-hidden="dockKind.value !== 'page' || dockTab.value !== 'details'">
305
+ <div class="pe-field">
306
+ <label class="form-label" for="page-url-path">URL Path <span data-help-title="URL path" data-help="Where the page lives on the site, starting with /. Changing it moves the page; menu links to the old address are updated. A project whose root URL is a prefix of this address files the page under it."></span></label>
307
+ <input id="page-url-path" type="text" data-model="urlPath.value" class="form-input" placeholder="/about or /services/web-dev">
308
+ </div>
309
+ <div class="pe-field">
310
+ <label class="form-label" for="field-title">Title *</label>
311
+ <input id="field-title" type="text" data-model="title.value" class="form-input" placeholder="Page title">
312
+ </div>
313
+ <div class="pe-field-row">
314
+ <div class="pe-field">
315
+ <label class="form-label" for="field-layout">Layout <span data-help="The page shell: navbar, width, sidebar, footer. Layouts are managed under System › Layouts."></span></label>
316
+ <select id="field-layout" class="form-select" data-model="layout.value"></select>
317
+ </div>
318
+ <div class="pe-field">
319
+ <label class="form-label" for="field-status">Status <span data-help-title="Page status" data-help="Draft and Review pages are hidden from visitors (404). Anyone who can read pages can preview them, and the preview and share links show them before publication."></span></label>
320
+ <select id="field-status" class="form-select" data-model="status.value">
321
+ <option value="draft">Draft</option>
322
+ <option value="published">Published</option>
323
+ <option value="review">Review</option>
324
+ </select>
325
+ </div>
326
+ </div>
327
+ <div class="pe-field">
328
+ <label class="form-label" for="field-description">Description</label>
329
+ <input id="field-description" type="text" data-model="description.value" class="form-input" placeholder="Short description">
145
330
  </div>
146
- <div class="col-4">
147
- <label class="form-label">Visibility <span data-help-title="Who can see the page" data-help="Public: everyone. A role &quot;and above&quot;: that role and every more senior one. A role &quot;only&quot;: holders of that role (and the super-admin). Private: the super-admin alone. Signed-out visitors are sent to sign in."></span></label>
331
+ <div class="pe-field-row">
332
+ <div class="pe-field">
333
+ <label class="form-label" for="field-sort-order">Sort Order <span data-help="Lower numbers come first in navigation and in page lists sorted by navigation order."></span></label>
334
+ <input id="field-sort-order" type="number" class="form-input" min="0" data-model="sortOrder.value">
335
+ </div>
336
+ <div class="pe-field">
337
+ <label class="form-label" for="field-category">Category <span data-help="A free label for grouping pages - collection displays and search can filter by it."></span></label>
338
+ <input id="field-category" type="text" data-model="category.value" class="form-input" placeholder="e.g. news, docs">
339
+ </div>
340
+ </div>
341
+ <div class="pe-field">
342
+ <label class="form-label" for="field-visibility">Visibility <span data-help-title="Who can see the page" data-help="Public: everyone. A role &quot;and above&quot;: that role and every more senior one. A role &quot;only&quot;: holders of that role (and the super-admin). Private: the super-admin alone. Signed-out visitors are sent to sign in."></span></label>
148
343
  <select id="field-visibility" class="form-select" data-model="visibility.value">
149
344
  <option value="public">Public - everyone</option>
150
345
  <option value="private">Private - super-admin only</option>
151
346
  </select>
152
347
  </div>
153
- <div class="col-4">
154
- <label class="form-label">Project <span data-help="Which project the page belongs to. Inherit from URL files it under the project whose root URL matches its address, else Core."></span></label>
348
+ <div class="pe-field">
349
+ <label class="form-label" for="field-project">Project <span data-help="Which project the page belongs to. Inherit from URL files it under the project whose root URL matches its address, else Core."></span></label>
155
350
  <select id="field-project" class="form-select" data-model="project.value">
156
351
  <option value="">Inherit from URL</option>
157
352
  </select>
158
- <small class="form-hint">Pre-filled from the project you came from, or from URL inheritance; choose explicitly to override.</small>
159
353
  </div>
160
- </div>
161
- <div class="row mb-3">
162
- <div class="col-6">
163
- <label class="form-label">Theme Override <span data-help="Shows this page in a different theme from the rest of the site - built-in or custom."></span></label>
164
- <select id="field-theme" class="form-select" data-model="theme.value"></select>
165
- <small class="form-hint">Overrides the site-wide theme for this page only.</small>
354
+ <div class="pe-field">
355
+ <label class="form-label" for="field-theme">Theme Override <span data-help="Shows this page in a different theme from the rest of the site - built-in or custom."></span></label>
356
+ <select id="field-theme" class="form-select" data-model="theme.value"></select>
166
357
  </div>
167
- </div>
168
- <div class="row mb-3">
169
- <div class="col">
170
- <label class="form-label">Tags <span data-help="Keywords for filtering and search. Type and press Enter; existing tags are suggested."></span></label>
358
+ <div class="pe-field">
359
+ <label class="form-label" for="field-tags">Tags <span data-help="Keywords for filtering and search. Type and press Enter; existing tags are suggested."></span></label>
171
360
  <input id="field-tags" type="text" class="form-input" />
172
- <small class="form-hint">Comma-separated keywords for filtering and search.</small>
173
361
  </div>
174
- </div>
175
- <div class="row mb-3">
176
- <div class="col">
177
- <label class="form-label">Admin tags <span data-help="Labels that link pages together in the Pages list - pick one with the list's cog to show only those pages. Seen only here in the admin, never on the site; the Pages list's right-click menu adds and removes them too."></span></label>
362
+ <div class="pe-field">
363
+ <label class="form-label" for="field-admin-tags">Admin tags <span data-help="Labels that link pages together in the Pages list - pick one with the list's cog to show only those pages. Seen only here in the admin, never on the site; the Pages list's right-click menu adds and removes them too."></span></label>
178
364
  <input id="field-admin-tags" type="text" class="form-input" />
179
- <small class="form-hint">Only seen in the admin - for grouping pages in the Pages list.</small>
180
365
  </div>
181
- </div>
182
- <div class="row">
183
- <div class="col-auto">
184
- <label class="form-check-label">
185
- <input id="field-sidebar" type="checkbox" class="form-check" data-model="sidebar.value"> Show Sidebar
186
- </label>
187
- </div>
188
- <div class="col-auto">
189
- <label class="form-check-label">
190
- <input id="field-show-breadcrumbs" type="checkbox" class="form-check" data-model="breadcrumbs.value"> Show Breadcrumbs
191
- </label>
192
- </div>
193
- <div class="col-auto">
194
- <label class="form-check-label" title="Included in fresh installs via the seed script">
195
- <input id="field-bundled" type="checkbox" class="form-check" data-model="bundled.value"> Bundled
196
- </label>
197
- <span data-help="Included when a fresh site is seeded. Leave off for pages that belong to this site only."></span>
366
+ <div class="pe-field pe-field-checks">
367
+ <label class="form-check-label"><input id="field-sidebar" type="checkbox" class="form-check" data-model="sidebar.value"> Show Sidebar</label>
368
+ <label class="form-check-label"><input id="field-show-breadcrumbs" type="checkbox" class="form-check" data-model="breadcrumbs.value"> Show Breadcrumbs</label>
369
+ <label class="form-check-label" title="Included in fresh installs via the seed script"><input id="field-bundled" type="checkbox" class="form-check" data-model="bundled.value"> Bundled
370
+ <span data-help="Included when a fresh site is seeded. Leave off for pages that belong to this site only."></span></label>
198
371
  </div>
199
372
  </div>
200
- </div>
201
373
 
202
- <!-- SEO -->
203
- <div class="tab-panel">
204
- <div class="row mb-3">
205
- <div class="col">
206
- <label class="form-label">SEO Title <span data-help="The title search engines and browser tabs show. Blank uses the page title."></span></label>
374
+ <!-- SEO -->
375
+ <div class="pe-dock-panel" hidden data-bind-hidden="dockKind.value !== 'page' || dockTab.value !== 'seo'">
376
+ <div class="pe-field">
377
+ <label class="form-label" for="field-seo-title">SEO Title <span data-help="The title search engines and browser tabs show. Blank uses the page title."></span></label>
207
378
  <input id="field-seo-title" type="text" data-model="seoTitle.value" class="form-input" placeholder="Overrides default title">
208
379
  </div>
209
- </div>
210
- <div class="row">
211
- <div class="col">
212
- <label class="form-label">SEO Description <span data-help="The summary search engines show under the title. Blank uses the page description."></span></label>
380
+ <div class="pe-field">
381
+ <label class="form-label" for="field-seo-desc">SEO Description <span data-help="The summary search engines show under the title. Blank uses the page description."></span></label>
213
382
  <input id="field-seo-desc" type="text" data-model="seoDesc.value" class="form-input" placeholder="Overrides default description">
214
383
  </div>
215
384
  </div>
216
- </div>
217
385
 
218
- <!-- DConfig -->
219
- <div class="tab-panel">
220
- <label class="form-label">Config JSON <span data-help-title="DConfig" data-help="Declarative behaviour for elements on this page - click handlers, class toggles - as a JSON object keyed by CSS selector. Must be valid JSON before the page can be saved."></span></label>
221
- <textarea id="field-dconfig" class="form-input dconfig-textarea" data-model="dconfig.value"
222
- placeholder='{ "#my-btn": { "events": { "click": { "target": "#panel", "toggleClass": "hidden" } } } }'></textarea>
223
- <small class="form-hint">Declarative click handlers and class toggles applied on page load. Merged with any <code>[dconfig]</code>
224
- shortcodes in the content body - inline shortcodes win on selector conflict. Leave blank if unused.</small>
225
- </div>
226
-
227
- <!-- Live Preview -->
228
- <div class="tab-panel" id="live-preview-panel">
229
- <!-- The tabs set the panel's display inline, so the filling is done one level in. -->
230
- <div class="live-preview-wrap">
231
- <div class="live-preview-bar">
232
- <div class="live-preview-widths" role="group" aria-label="Preview width">
233
- <button type="button" class="live-preview-width" data-tooltip="Full width" data-on-click="setWidth('full')"
234
- data-bind-class="liveWidth.value === 'full' && 'active'">
235
- <span data-icon="monitor"></span>
236
- </button>
237
- <button type="button" class="live-preview-width" data-tooltip="Tablet (1024px)" data-on-click="setWidth('1024')"
238
- data-bind-class="liveWidth.value === '1024' && 'active'">
239
- <span data-icon="tablet"></span>
240
- </button>
241
- <button type="button" class="live-preview-width" data-tooltip="Mobile (390px)" data-on-click="setWidth('390')"
242
- data-bind-class="liveWidth.value === '390' && 'active'">
243
- <span data-icon="smartphone"></span>
244
- </button>
386
+ <!-- DConfig -->
387
+ <div class="pe-dock-panel" hidden data-bind-hidden="dockKind.value !== 'page' || dockTab.value !== 'dconfig'">
388
+ <div class="pe-field">
389
+ <label class="form-label" for="field-dconfig">Config JSON <span data-help-title="DConfig" data-help="Declarative behaviour for elements on this page - click handlers, class toggles - as a JSON object keyed by CSS selector. Must be valid JSON before the page can be saved."></span></label>
390
+ <textarea id="field-dconfig" class="form-input dconfig-textarea" data-model="dconfig.value"
391
+ placeholder='{ "#my-btn": { "events": { "click": { "target": "#panel", "toggleClass": "hidden" } } } }'></textarea>
392
+ <small class="form-hint">Declarative click handlers and class toggles applied on page load. Merged with any <code>[dconfig]</code>
393
+ shortcodes in the content body - inline shortcodes win on selector conflict. Leave blank if unused.</small>
245
394
  </div>
246
- <span class="live-preview-status" data-bind-text="liveStatus.value">Showing unsaved changes</span>
247
- <span data-help-title="Live Preview" data-help="The whole page - navbar, footer, theme - rendered by the server from what is in the editor now, before saving. Share link makes a private, revocable address for someone without an account."></span>
248
- <div class="live-preview-actions">
249
- <button type="button" class="btn btn-ghost btn-sm" data-on-click="refreshLive">
250
- <span data-icon="refresh-cw"></span> Refresh
251
- </button>
252
- <button type="button" class="btn btn-ghost btn-sm" data-on-click="share">
253
- <span data-icon="link"></span> Share link
254
- </button>
255
- </div>
256
- </div>
257
- <div class="live-preview-stage" id="live-preview-stage">
258
- <iframe id="live-preview-frame" class="page-live-preview-frame" src="about:blank"
259
- data-bind-data-width="liveWidth.value === 'full' ? null : liveWidth.value"
260
- title="Full page preview" sandbox="allow-same-origin allow-scripts allow-forms allow-popups"></iframe>
261
- </div>
262
395
  </div>
263
- </div>
264
396
 
265
- <!-- History - loaded the first time the tab is opened. -->
266
- <div class="tab-panel" id="history-panel">
267
- <div class="pe-hist-head">
268
- <h3>Version History <span data-help="A version is kept on every save. Click one to compare it with the editor; right-click it (or ⋮, or Shift+F10) to restore or delete it. Restoring keeps a snapshot of the current page first."></span></h3>
269
- <div class="pe-hist-actions">
270
- <button class="btn btn-ghost btn-sm pe-danger" hidden data-bind-hidden="!anySelected.value" data-on-click="deleteSelected">
271
- <span data-icon="trash"></span> Delete Selected
272
- </button>
273
- <button class="btn btn-ghost btn-sm" data-on-click="saveNamed"><span data-icon="bookmark"></span> Save Named Version</button>
397
+ <!-- History - loaded the first time it is opened for a page. -->
398
+ <div class="pe-dock-panel" id="history-panel" hidden data-bind-hidden="dockKind.value !== 'page' || dockTab.value !== 'history'">
399
+ <div class="pe-hist-head">
400
+ <h3>Versions <span data-help="A version is kept on every save. Click one to see it beside the editor; right-click it (or ⋮, or Shift+F10) to restore or delete it. Restoring keeps a snapshot of the current page first."></span></h3>
401
+ <div class="pe-hist-actions">
402
+ <button type="button" class="btn btn-ghost btn-sm pe-danger" hidden data-bind-hidden="!anySelected.value" data-on-click="deleteSelected" title="Delete the selected versions">
403
+ <span data-icon="trash"></span></button>
404
+ <button type="button" class="btn btn-ghost btn-sm" data-on-click="saveNamed" title="Save a named version"><span data-icon="bookmark"></span></button>
405
+ </div>
274
406
  </div>
275
- </div>
276
407
 
277
- <div hidden data-bind-hidden="compare.value">
278
- <p class="pe-hist-note" hidden data-bind-hidden="!histLoading.value">Loading…</p>
279
- <p class="pe-hist-note is-bad" hidden data-bind-hidden="!histError.value" data-bind-text="histError.value"></p>
280
- <p class="pe-hist-note" hidden data-bind-hidden="histLoading.value || histError.value || versions.value.length">No versions saved yet.
281
- Versions are created automatically on each save.</p>
282
- <table class="pe-hist-table" hidden data-bind-hidden="!versions.value.length">
283
- <thead><tr>
284
- <th class="pe-hist-check"><input type="checkbox" aria-label="Select all versions" data-on-change="toggleAll"
285
- data-bind-checked="allSelected.value" data-bind-indeterminate="someSelected.value"></th>
286
- <th>Date</th><th>Author</th><th>Type</th><th>Label</th><th>Actions</th>
287
- </tr></thead>
288
- <tbody class="pe-version-list" data-each="versions.value key=filename">
289
- <tr class="pe-version-row" tabindex="0" data-bind-data-file="filename">
290
- <td><input type="checkbox" data-model="checked.value" aria-label="Select this version"></td>
291
- <td data-bind-text="when"></td>
292
- <td data-bind-text="author || '-'"></td>
293
- <td><span class="badge pe-hist-type" data-bind-class="type === 'manual' ? 'badge-primary' : 'badge-secondary'" data-bind-text="type"></span></td>
294
- <td class="pe-muted" data-bind-text="label || '-'"></td>
295
- <td><button type="button" class="pe-kebab" title="Actions" aria-label="Actions for this version">⋮</button></td>
296
- </tr>
297
- </tbody>
298
- </table>
299
- </div>
408
+ <div hidden data-bind-hidden="compare.value">
409
+ <p class="pe-hist-note" hidden data-bind-hidden="!histLoading.value">Loading…</p>
410
+ <p class="pe-hist-note is-bad" hidden data-bind-hidden="!histError.value" data-bind-text="histError.value"></p>
411
+ <p class="pe-hist-note" hidden data-bind-hidden="histLoading.value || histError.value || versions.value.length">No versions saved yet.
412
+ Versions are created automatically on each save.</p>
413
+ <table class="pe-hist-table" hidden data-bind-hidden="!versions.value.length">
414
+ <thead><tr>
415
+ <th class="pe-hist-check"><input type="checkbox" aria-label="Select all versions" data-on-change="toggleAll"
416
+ data-bind-checked="allSelected.value" data-bind-indeterminate="someSelected.value"></th>
417
+ <th>Version</th><th></th>
418
+ </tr></thead>
419
+ <tbody class="pe-version-list" data-each="versions.value key=filename">
420
+ <tr class="pe-version-row" tabindex="0" data-bind-data-file="filename">
421
+ <td><input type="checkbox" data-model="checked.value" aria-label="Select this version"></td>
422
+ <td><div data-bind-text="when"></div>
423
+ <div class="pe-muted"><span class="badge pe-hist-type" data-bind-class="type === 'manual' ? 'badge-primary' : 'badge-secondary'" data-bind-text="type"></span>
424
+ <span data-bind-text="who"></span></div></td>
425
+ <td><button type="button" class="pe-kebab" title="Actions" aria-label="Actions for this version">⋮</button></td>
426
+ </tr>
427
+ </tbody>
428
+ </table>
429
+ </div>
300
430
 
301
- <div hidden data-bind-hidden="!compare.value">
302
- <div class="pe-hist-compare-bar">
303
- <button class="btn btn-ghost btn-sm" data-on-click="closeCompare"><span data-icon="arrow-left"></span> Back to List</button>
304
- <button class="btn btn-primary btn-sm" data-on-click="restoreCompared"><span data-icon="rotate-ccw"></span> Restore This Version</button>
305
- <span class="pe-muted" data-bind-text="compare.value ? compare.value.label : ''"></span>
306
- </div>
307
- <div class="row" style="gap:1rem;">
308
- <div class="col">
309
- <p class="pe-hist-caption">SELECTED VERSION</p>
310
- <pre class="pe-hist-pre" data-bind-text="compare.value ? compare.value.old : ''"></pre>
311
- </div>
312
- <div class="col">
313
- <p class="pe-hist-caption">CURRENT (UNSAVED)</p>
314
- <pre class="pe-hist-pre" data-bind-text="compare.value ? compare.value.current : ''"></pre>
431
+ <div hidden data-bind-hidden="!compare.value">
432
+ <div class="pe-hist-compare-bar">
433
+ <button type="button" class="btn btn-ghost btn-sm" data-on-click="closeCompare"><span data-icon="arrow-left"></span> Back</button>
434
+ <button type="button" class="btn btn-primary btn-sm" data-on-click="restoreCompared"><span data-icon="rotate-ccw"></span> Restore</button>
315
435
  </div>
436
+ <p class="pe-hist-caption" data-bind-text="compare.value ? compare.value.label : ''"></p>
437
+ <pre class="pe-hist-pre" data-bind-text="compare.value ? compare.value.old : ''"></pre>
316
438
  </div>
317
439
  </div>
318
440
  </div>
319
-
320
- </div>
441
+ </aside>
321
442
  </div>
322
443
 
323
444
  <div id="media-picker-modal"></div>
324
445
 
325
446
  <style>
326
- .pex-bar { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-bottom: .75rem; }
327
- .pex-bar .btn { display: inline-flex; align-items: center; gap: .3rem; }
447
+ .pe-ws [hidden] { display: none !important; }
328
448
  .pex-spacer { flex: 1; }
329
449
  .pex-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px;
330
450
  background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
331
451
  .pex-problem { color: var(--dm-danger); font-size: var(--dm-text-sm); }
332
452
  .pex-held { color: var(--dm-warning); font-size: var(--dm-text-sm); }
333
- .pe-files { display: flex; flex-wrap: wrap; gap: .25rem; padding: .4rem .5rem 0; border-bottom: 1px solid var(--dm-border); }
334
- .pe-files-list { display: contents; }
335
- .pe-file { display: inline-flex; align-items: center; gap: .35rem; padding: .3rem .7rem; font-size: var(--dm-text-sm); cursor: pointer;
453
+ .pe-icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 1.9rem; height: 1.9rem; border: 0; border-radius: var(--dm-radius-sm);
454
+ background: none; color: var(--dm-text-muted); cursor: pointer; font-size: var(--dm-text-md); }
455
+ .pe-icon-btn:hover { color: var(--dm-text); background: var(--dm-hover-bg); }
456
+ .pe-icon-btn.active { color: var(--dm-primary); }
457
+
458
+ /* Explorer */
459
+ .pe-ex-find { position: relative; padding: .5rem .6rem; border-bottom: 1px solid var(--dm-border); }
460
+ .pe-ex-find-icon { position: absolute; left: 1.1rem; top: 50%; transform: translateY(-50%); color: var(--dm-text-muted); pointer-events: none; }
461
+ .pe-ex-input { padding-left: 1.8rem !important; }
462
+ .pe-ex-scroll { flex: 1; min-height: 0; overflow: auto; padding-bottom: .5rem; }
463
+ .pe-ex-section { padding-top: .35rem; }
464
+ .pe-ex-head { display: flex; align-items: center; padding: 0 .35rem 0 .5rem; }
465
+ .pe-ex-toggle { flex: 1; display: flex; align-items: center; gap: .3rem; border: 0; background: none; color: var(--dm-text-muted); cursor: pointer;
466
+ font-size: var(--dm-text-xs); font-weight: 700; text-transform: uppercase; letter-spacing: .06em; padding: .3rem .2rem; text-align: left; }
467
+ .pe-ex-toggle:hover { color: var(--dm-text); }
468
+ .pe-ex-add { border: 0; background: none; color: var(--dm-text-muted); cursor: pointer; display: inline-flex; padding: .2rem; border-radius: var(--dm-radius-sm); }
469
+ .pe-ex-add:hover { color: var(--dm-primary); background: var(--dm-hover-bg); }
470
+ .pe-ex-chev { display: inline-block; width: .8rem; flex: none; color: var(--dm-text-muted); font-size: .7rem; transition: transform .12s; }
471
+ .pe-ex-chev::before { content: '▸'; }
472
+ .pe-ex-chev.is-open { transform: rotate(90deg); }
473
+ .pe-ex-list { display: flex; flex-direction: column; }
474
+ .pe-ex-item { display: flex; align-items: center; gap: .35rem; width: 100%; border: 0; background: none; color: var(--dm-text); cursor: pointer; text-align: left;
475
+ padding: .28rem .6rem .28rem calc(.75rem + var(--pe-depth, 0) * .9rem); font-size: var(--dm-text-sm); line-height: 1.3; border-radius: 0; }
476
+ .pe-ex-item[data-depth="1"] { --pe-depth: 1; }
477
+ .pe-ex-item[data-depth="2"] { --pe-depth: 2; }
478
+ .pe-ex-item[data-depth="3"] { --pe-depth: 3; }
479
+ .pe-ex-item[data-depth="4"] { --pe-depth: 4; }
480
+ .pe-ex-item[data-depth="5"] { --pe-depth: 5; }
481
+ .pe-ex-item:hover { background: var(--dm-hover-bg); }
482
+ .pe-ex-item.active { background: color-mix(in srgb, var(--dm-primary) 14%, transparent); color: var(--dm-primary); }
483
+ .pe-ex-item.is-folder:not(.active) .pe-ex-label { color: var(--dm-text-muted); }
484
+ .pe-ex-item.is-draft .pe-ex-label { font-style: italic; }
485
+ .pe-ex-item .pe-ex-chev { width: .8rem; }
486
+ .pe-ex-item:not(.is-folder) .pe-ex-chev { visibility: hidden; }
487
+ .pe-ex-kind { font-size: var(--dm-text-xs); text-transform: uppercase; letter-spacing: .04em; opacity: .6; flex: none; }
488
+ .pe-ex-label { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
489
+ .pe-ex-item.is-block .pe-ex-label { font-family: var(--dm-font-mono, ui-monospace, monospace); font-size: var(--dm-text-xs); }
490
+ .pe-ex-meta { flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
491
+ .pe-ex-item.is-block .pe-ex-meta { flex: 1 1 0; }
492
+ .pe-ex-dot, .pe-tab-dot { color: var(--dm-warning); font-weight: 700; flex: none; }
493
+ .pe-ex-none { margin: 0; padding: .2rem .75rem .4rem; color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
494
+
495
+ /* Tabs */
496
+ .pe-tabs { display: flex; align-items: stretch; gap: .15rem; padding: .3rem .4rem 0; border-bottom: 1px solid var(--dm-border); flex: none; min-width: 0; }
497
+ .pe-tabs > .pe-icon-btn { align-self: center; flex: none; }
498
+ .pe-tabs-list { display: flex; flex: 0 1 auto; min-width: 0; overflow-x: auto; scrollbar-width: thin; gap: .15rem; }
499
+ .pe-tab { display: inline-flex; align-items: center; gap: .35rem; padding: .35rem .3rem .35rem .7rem; font-size: var(--dm-text-sm); cursor: pointer; white-space: nowrap;
336
500
  border: 1px solid transparent; border-bottom: 0; border-radius: var(--dm-radius-sm) var(--dm-radius-sm) 0 0;
337
- background: none; color: var(--dm-text-muted); margin-bottom: -1px; }
338
- .pe-file:hover { color: var(--dm-text); }
339
- .pe-file.active { color: var(--dm-text); background: var(--dm-surface, var(--dm-background)); border-color: var(--dm-border); }
340
- .pe-file-kind { font-size: var(--dm-text-xs); text-transform: uppercase; letter-spacing: .04em; opacity: .7; }
341
- .pe-file:not(.is-block) .pe-file-kind { opacity: 1; text-transform: none; letter-spacing: 0; font-size: var(--dm-text-sm); font-weight: 600; }
342
- .pe-file-name { font-family: var(--dm-font-mono, ui-monospace, monospace); }
343
- .pe-file-dot { color: var(--dm-warning); font-weight: 700; }
344
- .pe-toolbar--block > :not(.editor-toolbar-right):not(.pe-block-bar), .pe-toolbar--block .pe-toolbar-extras { display: none !important; }
345
- .pe-block-pane { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; min-height: 0; }
346
- .pe-block-bar { display: flex; flex: 1 1 auto; flex-wrap: wrap; align-items: center; gap: .5rem; min-width: 0; }
501
+ background: none; color: var(--dm-text-muted); margin-bottom: -1px; max-width: 16rem; user-select: none; }
502
+ .pe-tab:hover { color: var(--dm-text); }
503
+ .pe-tab:focus-visible { outline: 2px solid var(--dm-primary); outline-offset: -2px; }
504
+ .pe-tab.active { color: var(--dm-text); background: var(--dm-surface, var(--dm-background)); border-color: var(--dm-border); }
505
+ .pe-tab-kind { font-size: var(--dm-text-xs); text-transform: uppercase; letter-spacing: .04em; opacity: .6; }
506
+ .pe-tab-label { overflow: hidden; text-overflow: ellipsis; }
507
+ .pe-tab.is-block .pe-tab-label, .pe-tab.is-component .pe-tab-label { font-family: var(--dm-font-mono, ui-monospace, monospace); font-size: var(--dm-text-xs); }
508
+ .pe-tab-close { border: 0; background: none; color: var(--dm-text-muted); cursor: pointer; border-radius: var(--dm-radius-sm); width: 1.25rem; height: 1.25rem;
509
+ line-height: 1; font-size: 1rem; display: inline-flex; align-items: center; justify-content: center; opacity: .6; }
510
+ .pe-tab:hover .pe-tab-close, .pe-tab.active .pe-tab-close { opacity: 1; }
511
+ .pe-tab-close:hover { color: var(--dm-danger); background: var(--dm-hover-bg); }
512
+
513
+ /* Stage, status bar, empty state */
514
+ .pe-stage { display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0; }
515
+ .pe-empty { flex: 1; display: flex; align-items: center; justify-content: center; color: var(--dm-text-muted); text-align: center; padding: 2rem; }
516
+ .pe-empty p { margin: 0; display: inline-flex; align-items: center; gap: .3rem; flex-wrap: wrap; justify-content: center; }
517
+ .pe-status { display: flex; align-items: center; gap: .5rem; padding: .3rem .6rem; border-top: 1px solid var(--dm-border); flex: none; min-height: 2.3rem;
518
+ font-size: var(--dm-text-sm); background: var(--dm-background-alt); }
519
+ .pe-status .btn { display: inline-flex; align-items: center; gap: .3rem; }
520
+ .pe-status-text { color: var(--dm-text-muted); font-size: var(--dm-text-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
521
+ .pe-toolbar--block > :not(.editor-toolbar-right):not(.pe-block-bar):not(.pe-comp-bar), .pe-toolbar--block .pe-toolbar-extras { display: none !important; }
522
+ .pe-block-pane, .pe-comp-pane { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; min-height: 0; }
523
+ .pe-block-bar, .pe-comp-bar { display: flex; flex: 1 1 auto; flex-wrap: wrap; align-items: center; gap: .5rem; min-width: 0; }
347
524
  .pe-block-parts { display: inline-flex; gap: .15rem; }
348
- .pe-block-uses { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
525
+ .pe-comp-bar .pe-block-parts .editor-view-btn { font-family: var(--dm-font-mono, ui-monospace, monospace); font-size: var(--dm-text-xs); }
349
526
  .pe-block-note { margin: 0; padding: .75rem 1rem; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
350
- .pe-block-edit { display: flex; flex: 1 1 auto; min-height: 0; }
351
- .pex-bar [hidden], .editor-card [hidden], #editor-meta-tabs [hidden] { display: none !important; }
352
- .pe-hist-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
353
- .pe-hist-head h3 { margin: 0; font-size: 1rem; }
354
- .pe-hist-actions { display: flex; gap: .5rem; align-items: center; }
527
+ .pe-block-edit, .pe-comp-edit { display: flex; flex: 1 1 auto; min-height: 0; min-width: 0; }
528
+ /* A block's or component's source: the body's own textarea and gutter (lib/plain-editor.js), one line per number.
529
+ The colours are a <pre> behind the textarea with the SAME .editor-textarea class - the lettering rules reach
530
+ both by one selector, so they cannot differ; only what is set here (position, colour) tells them apart. */
531
+ .pe-plain { display: flex; flex: 1 1 auto; min-width: 0; min-height: 0; }
532
+ .pe-plain .editor-textarea { flex: 1 1 auto; min-width: 0; white-space: pre; overflow: auto; tab-size: 2; -moz-tab-size: 2; margin: 0; box-sizing: border-box; }
533
+ .pe-plain-host { position: relative; flex: 1 1 auto; min-width: 0; min-height: 0; display: flex; overflow: hidden; }
534
+ /* The layer scrolls like the textarea (its scrollbar sits under the textarea's), so the two wrap at the same width. */
535
+ .pe-plain-host .pe-plain-colours { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: auto; scrollbar-gutter: stable; color: var(--dm-text); margin: 0; box-sizing: border-box; }
536
+ .pe-plain-host .pe-plain-coloured { position: relative; z-index: 1; width: 100%; color: transparent; caret-color: var(--dm-text, #ddd); scrollbar-gutter: stable; }
537
+ /* The page body and the CSS box hide by their own `hidden`: the host follows. */
538
+ .pe-plain-host:has(> textarea[hidden]) { display: none !important; }
539
+ .pe-plain-host .pe-plain-coloured::placeholder { color: var(--dm-text-muted); }
540
+ .pe-plain-host .pe-plain-coloured::selection { background: color-mix(in srgb, var(--dm-primary) 35%, transparent); }
541
+ .pe-plain-colours .dm-tok-comment { color: #8b949e; font-style: italic; }
542
+ .pe-plain-colours .dm-tok-string { color: #4f9e4f; }
543
+ .pe-plain-colours .dm-tok-keyword { color: #b559c8; }
544
+ .pe-plain-colours .dm-tok-literal { color: #2f9bb3; }
545
+ .pe-plain-colours .dm-tok-number { color: #c98a2c; }
546
+ .pe-plain-colours .dm-tok-func { color: #4078f2; }
547
+ .pe-plain-colours .dm-tok-key { color: #b8762e; }
548
+ .pe-plain-colours .dm-tok-tag { color: #cc5c66; }
549
+ .pe-comp-ro { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; color: var(--dm-text); }
550
+ .pe-comp-state { display: inline-flex; align-items: center; gap: .35rem; font-size: var(--dm-text-xs); color: var(--dm-text-muted); white-space: nowrap; }
551
+ .pe-comp-dot { width: .55rem; height: .55rem; border-radius: 50%; background: var(--dm-text-muted); }
552
+ .pe-comp-state.is-ok .pe-comp-dot { background: var(--dm-success); }
553
+ .pe-comp-state.is-error { color: var(--dm-danger); }
554
+ .pe-comp-state.is-error .pe-comp-dot { background: var(--dm-danger); }
555
+ .pe-comp-errors { margin: 0; padding: .5rem .75rem; white-space: pre-wrap; font-size: var(--dm-text-xs); font-family: var(--dm-font-mono, monospace);
556
+ background: color-mix(in srgb, var(--dm-danger) 10%, transparent); color: var(--dm-danger); max-height: 10rem; overflow: auto; flex: none; }
557
+ .pe-comp-tag { font-size: var(--dm-text-sm); }
558
+ .pe-comp-tagrow { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
559
+ .pe-comp-tagrow .btn { display: inline-flex; align-items: center; gap: .3rem; }
560
+ .pe-comp-props { display: flex; flex-direction: column; }
561
+ .pe-comp-prop { display: flex; align-items: center; gap: .4rem; padding: .3rem .15rem; border-radius: var(--dm-radius-sm); }
562
+ .pe-comp-prop:hover, .pe-comp-prop:focus-within, .pe-comp-prop:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
563
+ .pe-comp-prop-label { flex: 0 0 6rem; font-size: var(--dm-text-xs); color: var(--dm-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
564
+ .pe-comp-prop-input { flex: 1; min-width: 0; font-family: inherit; }
565
+ textarea.pe-comp-prop-input { font-family: var(--dm-font-mono, monospace); }
566
+ .pe-ex-item.is-readonly .pe-ex-label { opacity: .75; }
567
+ .pe-preview-bar { display: flex; align-items: center; gap: .5rem; padding: .3rem .6rem; border-bottom: 1px solid var(--dm-border); flex: none; }
568
+ .pe-preview-bar .btn { display: inline-flex; align-items: center; gap: .3rem; }
569
+ .pe-preview-stage { flex: 1; min-height: 0; display: flex; justify-content: center; background: var(--dm-background-alt); }
570
+ .editor-preview[data-width="1024"] { max-width: 1024px; border-left: 1px solid var(--dm-border); border-right: 1px solid var(--dm-border); }
571
+ .editor-preview[data-width="390"] { max-width: 390px; border-left: 1px solid var(--dm-border); border-right: 1px solid var(--dm-border); }
572
+
573
+ /* Dock */
574
+ .pe-dock-tabs { display: flex; align-items: center; gap: .1rem; padding: .3rem .4rem 0 .6rem; border-bottom: 1px solid var(--dm-border); flex: none; }
575
+ .pe-dock-tab { border: 0; border-bottom: 2px solid transparent; background: none; color: var(--dm-text-muted); cursor: pointer; padding: .4rem .55rem; font-size: var(--dm-text-sm); margin-bottom: -1px; }
576
+ .pe-dock-tab:hover { color: var(--dm-text); }
577
+ .pe-dock-tab.active { color: var(--dm-primary); border-bottom-color: var(--dm-primary); }
578
+ .pe-dock-body { flex: 1; min-height: 0; overflow: auto; padding: .75rem .85rem 1rem; }
579
+ .pe-dock-none { color: var(--dm-text-muted); font-size: var(--dm-text-sm); margin: 0; }
580
+ .pe-dock-panel { display: flex; flex-direction: column; gap: .65rem; }
581
+ .pe-field { display: flex; flex-direction: column; gap: .2rem; min-width: 0; }
582
+ .pe-field .form-label { margin: 0; font-size: var(--dm-text-xs); }
583
+ .pe-field .form-hint { font-size: var(--dm-text-xs); }
584
+ .pe-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; }
585
+ .pe-field-checks { gap: .35rem; }
586
+ .pe-field-checks .form-check-label { display: flex; align-items: center; gap: .4rem; font-size: var(--dm-text-sm); }
587
+ .pe-hist-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: .5rem; }
588
+ .pe-hist-head h3 { margin: 0; font-size: var(--dm-text-sm); }
589
+ .pe-hist-actions { display: flex; gap: .2rem; align-items: center; }
355
590
  .pe-danger { color: var(--dm-danger); }
356
- .pe-hist-note { color: var(--dm-text-muted); }
591
+ .pe-hist-note { color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
357
592
  .pe-hist-note.is-bad { color: var(--dm-danger); }
358
- .pe-hist-table { width: 100%; border-collapse: collapse; font-size: .875rem; }
359
- .pe-hist-table th { text-align: left; padding: .5rem .75rem; border-bottom: 1px solid var(--dm-border); font-size: .8rem; color: var(--dm-text-muted); }
360
- .pe-hist-table th.pe-hist-check { width: 2rem; }
361
- .pe-hist-table td { padding: .5rem .75rem; }
593
+ .pe-hist-table { width: 100%; border-collapse: collapse; font-size: var(--dm-text-sm); }
594
+ .pe-hist-table th { text-align: left; padding: .3rem .4rem; border-bottom: 1px solid var(--dm-border); font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
595
+ .pe-hist-table th.pe-hist-check { width: 1.5rem; }
596
+ .pe-hist-table td { padding: .35rem .4rem; vertical-align: top; }
362
597
  .pe-hist-table tbody tr { border-bottom: 1px solid var(--dm-border); }
363
- .pe-hist-type { font-size: .75rem; padding: .2rem .5rem; border-radius: 4px; }
364
- .pe-muted { color: var(--dm-text-muted); font-size: .875rem; }
365
- .pe-hist-compare-bar { display: flex; align-items: center; gap: .75rem; margin-bottom: 1rem; }
366
- .pe-hist-caption { font-size: .8rem; font-weight: 600; margin-bottom: .5rem; color: var(--dm-text-muted); }
367
- .pe-hist-pre { background: var(--dm-background-alt); padding: 1rem; border-radius: 6px; overflow: auto; white-space: pre-wrap; font-size: .8rem; max-height: 60vh; }
598
+ .pe-hist-type { font-size: .7rem; padding: .1rem .4rem; border-radius: 4px; }
599
+ .pe-muted { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
600
+ .pe-hist-compare-bar { display: flex; align-items: center; gap: .5rem; margin-bottom: .5rem; }
601
+ .pe-hist-caption { font-size: var(--dm-text-xs); font-weight: 600; margin: 0 0 .4rem; color: var(--dm-text-muted); }
602
+ .pe-hist-pre { background: var(--dm-background-alt); padding: .75rem; border-radius: 6px; overflow: auto; white-space: pre-wrap; font-size: .75rem; margin: 0; }
368
603
  #history-panel .pe-version-row { cursor: pointer; }
369
604
  #history-panel .pe-version-row:hover, #history-panel .pe-version-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
370
605
  .pe-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }