@a83/orbiter-admin 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1569 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Orbiter Admin — Editor</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Space+Grotesk:wght@300;400;500;600&family=Noto+Serif+JP:wght@200;300&family=DM+Mono:wght@300;400&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="/style.css" />
11
+ <script src="/theme.js"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
13
+ <style>
14
+ html,body,.app { height:100%; overflow:hidden; }
15
+ .app { display:flex; flex-direction:column; }
16
+ .editor-shell { display:grid; grid-template-columns:1fr 280px; flex:1; overflow:hidden; }
17
+ .editor-shell.mode-split { grid-template-columns:1fr 1fr 280px; }
18
+ .editor-shell.mode-preview { grid-template-columns:0 1fr 280px; }
19
+
20
+ /* Main writing area */
21
+ .editor-main { display:flex; flex-direction:column; overflow:hidden; background:var(--bg0); }
22
+
23
+ /* Toolbar */
24
+ .editor-toolbar { display:flex; align-items:center; gap:1px; padding:0 20px; height:40px; border-bottom:1px solid var(--line); background:var(--bg2); flex-shrink:0; }
25
+ .tool-group { display:flex; align-items:center; gap:3px; }
26
+ .tool-btn { height:26px; min-width:26px; padding:0 7px; display:flex; align-items:center; justify-content:center; background:transparent; border:1px solid var(--line); color:var(--muted); font-family:var(--mono); font-size:10px; cursor:pointer; transition:all .12s; border-radius:4px; }
27
+ .tool-btn:hover { color:var(--heading); background:var(--bg3); border-color:var(--mid); }
28
+ .tool-btn.active { background:var(--accent-bg); color:var(--accent); border-color:var(--accent); }
29
+ .tool-sep { width:1px; height:16px; background:var(--line); margin:0 6px; flex-shrink:0; }
30
+ .view-toggle { display:flex; gap:3px; margin-left:0; }
31
+ .view-btn { height:24px; padding:0 10px; border:1px solid var(--line); border-radius:4px; background:transparent; color:var(--muted); font-family:var(--mono); font-size:9px; letter-spacing:.06em; cursor:pointer; transition:all .12s; }
32
+ .view-btn:hover { color:var(--text); border-color:var(--mid); }
33
+ .view-btn.active { background:var(--gold); color:var(--bg0); border-color:var(--gold); }
34
+
35
+ /* Writing page */
36
+ .editor-scroll { flex:1; overflow-y:auto; }
37
+ .editor-page { max-width:680px; margin:0 auto; padding:48px 40px 120px; }
38
+ .editor-title-input { width:100%; background:transparent; border:none; outline:none; font-family:var(--serif); font-weight:200; font-size:32px; color:var(--heading); letter-spacing:.02em; line-height:1.2; margin-bottom:8px; resize:none; overflow:hidden; }
39
+ .editor-title-input::placeholder { color:var(--line); }
40
+ .editor-slug-line { font-size:10px; color:var(--muted); margin-bottom:32px; padding-bottom:14px; border-bottom:1px solid var(--line); }
41
+ .editor-slug-line span { color:var(--accent); }
42
+ .word-count { font-size:10px; color:var(--muted); margin-top:28px; padding-top:12px; border-top:1px solid var(--line2); }
43
+
44
+ /* Block editor */
45
+ .be-editor { min-height:400px; cursor:text; }
46
+ .be-block { outline:none; font-family:var(--serif); font-weight:300; font-size:16px; color:var(--text); line-height:2; letter-spacing:.01em; padding:1px 0; min-height:1.6em; position:relative; word-break:break-word; }
47
+ .be-block:focus { outline:none; }
48
+ .be-block:empty::before { content:attr(data-ph); color:var(--line); pointer-events:none; position:absolute; }
49
+ .be-block[data-type="h1"] { font-size:26px; color:var(--heading); line-height:1.3; padding:4px 0 2px; font-family:var(--serif); font-weight:200; }
50
+ .be-block[data-type="h2"] { font-size:20px; color:var(--heading); line-height:1.4; padding:3px 0 2px; font-family:var(--serif); font-weight:200; }
51
+ .be-block[data-type="h3"] { font-size:13px; color:var(--text); line-height:1.6; padding:2px 0; font-family:var(--mono); }
52
+ .be-block[data-type="blockquote"] { border-left:2px solid var(--gold); padding:2px 0 2px 16px; color:var(--mid); font-style:italic; }
53
+ .be-block[data-type="pre"] { font-family:var(--mono); font-size:12px; background:var(--bg1); border:1px solid var(--line); padding:14px 16px; white-space:pre-wrap; line-height:1.6; margin:4px 0; }
54
+ .be-block[data-type="ul"] { padding-left:20px; }
55
+ .be-block[data-type="ul"]::before { content:"·"; position:absolute; left:6px; color:var(--muted); }
56
+ .be-block[data-type="ol"] { padding-left:20px; }
57
+ .be-block[data-type="hr"] { height:1px; min-height:1px; border:none; border-top:1px solid var(--line); margin:12px 0; padding:0; pointer-events:none; }
58
+ .be-block code { font-family:var(--mono); font-size:12px; background:var(--bg3); padding:1px 5px; }
59
+ .be-block strong { font-weight:600; color:var(--heading); }
60
+ .be-block em { color:var(--mid); font-style:italic; }
61
+
62
+ /* Saved indicator */
63
+ .saved-flash { background:var(--jade-bg); border-bottom:1px solid rgba(45,139,106,.15); padding:6px 20px; font-size:10px; color:var(--jade); display:flex; align-items:center; gap:6px; flex-shrink:0; }
64
+ .saved-flash::before { content:"✓"; }
65
+
66
+ /* Preview panel */
67
+ .preview-panel { display:none; border-left:1px solid var(--line); flex-direction:column; overflow:hidden; background:var(--bg2); }
68
+ .editor-shell.mode-split .preview-panel,
69
+ .editor-shell.mode-preview .preview-panel { display:flex; }
70
+ .preview-src-bar { display:flex; align-items:center; gap:4px; padding:6px 12px; border-bottom:1px solid var(--line); background:var(--bg1); flex-shrink:0; }
71
+ .src-btn { height:22px; padding:0 10px; border:1px solid var(--line); border-radius:4px; background:transparent; color:var(--muted); font-family:var(--mono); font-size:9px; cursor:pointer; transition:all .12s; }
72
+ .src-btn:hover { color:var(--text); border-color:var(--mid); }
73
+ .src-btn.active { background:var(--accent-bg); color:var(--accent); border-color:var(--accent); }
74
+ .preview-scroll { flex:1; overflow-y:auto; padding:36px 40px 80px; }
75
+ .md-preview h1 { font-family:var(--serif); font-weight:200; font-size:28px; color:var(--heading); margin:0 0 16px; }
76
+ .md-preview h2 { font-family:var(--serif); font-weight:200; font-size:22px; color:var(--heading); margin:28px 0 12px; padding-bottom:8px; border-bottom:1px solid var(--line); }
77
+ .md-preview h3 { font-family:var(--mono); font-size:13px; color:var(--text); margin:20px 0 8px; }
78
+ .md-preview p { font-family:var(--serif); font-size:15px; color:var(--text); line-height:1.9; margin:0 0 16px; }
79
+ .md-preview blockquote { border-left:2px solid var(--gold); padding:4px 16px; margin:16px 0; color:var(--mid); font-style:italic; }
80
+ .md-preview code { font-family:var(--mono); font-size:12px; background:var(--bg3); padding:1px 5px; }
81
+ .md-preview pre { background:var(--bg1); border:1px solid var(--line); padding:16px; margin:16px 0; }
82
+ .md-preview strong { font-weight:600; color:var(--heading); }
83
+ .md-preview ul,.md-preview ol { padding-left:20px; margin:0 0 16px; }
84
+ .md-preview li { font-family:var(--serif); font-size:15px; line-height:1.9; }
85
+ .md-preview hr { border:none; border-top:1px solid var(--line); margin:28px 0; }
86
+ .md-preview-title { font-family:var(--serif); font-weight:200; font-size:30px; color:var(--heading); margin-bottom:6px; }
87
+ .md-preview-slug { font-size:10px; color:var(--muted); margin-bottom:28px; padding-bottom:16px; border-bottom:1px solid var(--line); }
88
+ .md-preview-slug span { color:var(--accent); }
89
+ .preview-iframe { flex:1; border:none; width:100%; }
90
+
91
+ /* Meta panel */
92
+ .meta-panel { background:var(--bg1); border-left:1px solid var(--line); display:flex; flex-direction:column; overflow-y:auto; width:280px; flex-shrink:0; }
93
+ .meta-section { padding:14px 16px; border-bottom:1px solid var(--line); }
94
+ .meta-section:last-child { border-bottom:none; }
95
+ .meta-label { font-size:9px; letter-spacing:.28em; text-transform:uppercase; color:var(--muted); margin-bottom:10px; }
96
+ .meta-field { margin-bottom:10px; }
97
+ .meta-field:last-child { margin-bottom:0; }
98
+ .field-label { font-size:9px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); margin-bottom:4px; }
99
+ .field-readonly { font-size:11px; color:var(--muted); padding:6px 0; }
100
+
101
+ /* Status */
102
+ .status-bar { display:flex; align-items:center; gap:8px; padding:8px 0; margin-bottom:10px; }
103
+ .status-dot { width:7px; height:7px; border-radius:50%; flex-shrink:0; }
104
+ .status-dot.published { background:var(--jade); }
105
+ .status-dot.draft { background:var(--gold); }
106
+ .status-lbl { font-size:11px; }
107
+ .status-lbl.published { color:var(--jade); }
108
+ .status-lbl.draft { color:var(--gold); }
109
+ .btn-publish { display:block; width:100%; padding:9px; background:var(--gold); border:none; color:var(--bg0); font-family:var(--mono); font-size:10px; letter-spacing:.12em; cursor:pointer; transition:background .15s; margin-bottom:5px; border-radius:var(--radius); }
110
+ .btn-publish:hover { background:#7a5520; }
111
+ .btn-draft { display:block; width:100%; padding:8px; background:transparent; border:1px solid var(--line); color:var(--mid); font-family:var(--mono); font-size:10px; cursor:pointer; transition:all .12s; border-radius:var(--radius); }
112
+ .btn-draft:hover { border-color:var(--mid); color:var(--text); }
113
+
114
+ /* Schema fields in sidebar */
115
+ .field-input,.field-select { width:100%; background:var(--bg0); border:1px solid var(--line); padding:6px 8px; color:var(--heading); font-family:var(--mono); font-size:11px; outline:none; appearance:none; transition:border-color .15s; border-radius:var(--radius); box-sizing:border-box; }
116
+ .field-input:focus,.field-select:focus { border-color:var(--accent); }
117
+ .media-drop-zone { border:1px dashed var(--line); background:var(--bg0); height:72px; cursor:pointer; transition:border-color .12s,background .12s; overflow:hidden; display:flex; align-items:center; justify-content:center; position:relative; border-radius:var(--radius); margin-top:5px; }
118
+ .media-drop-zone:hover { border-color:var(--gold); background:var(--gold-bg); }
119
+ .media-drop-zone.has-image { border-style:solid; }
120
+ .media-drop-zone img { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
121
+ .media-drop-label { font-size:9px; color:var(--muted); letter-spacing:.08em; position:relative; z-index:1; }
122
+ .media-drop-zone.has-image .media-drop-label { display:none; }
123
+ .weekday-grid { display:flex; gap:4px; flex-wrap:wrap; margin-top:4px; }
124
+ .wd-btn { height:26px; min-width:32px; padding:0 4px; background:var(--bg0); border:1px solid var(--line); color:var(--muted); font-family:var(--mono); font-size:9px; cursor:pointer; transition:all .1s; border-radius:2px; }
125
+ .wd-btn.active { background:var(--accent-bg); border-color:var(--accent); color:var(--accent); }
126
+ .tag-input { width:100%; background:var(--bg0); border:1px solid var(--line); padding:6px 8px; color:var(--heading); font-family:var(--mono); font-size:11px; outline:none; transition:border-color .15s; border-radius:var(--radius); box-sizing:border-box; }
127
+ .tag-input:focus { border-color:var(--accent); }
128
+ .tag-preview { display:flex; flex-wrap:wrap; gap:3px; margin-top:4px; }
129
+ .tag-chip { font-size:9px; padding:2px 6px; background:var(--accent-bg); color:var(--accent); border:1px solid rgba(90,122,240,.2); border-radius:2px; }
130
+ .rel-picker { display:flex; flex-direction:column; gap:4px; }
131
+ .rel-search { width:100%; background:var(--bg0); border:1px solid var(--line); padding:5px 8px; color:var(--heading); font-family:var(--mono); font-size:11px; outline:none; transition:border-color .15s; border-radius:var(--radius); box-sizing:border-box; }
132
+ .rel-search:focus { border-color:var(--accent); }
133
+ .rel-list { max-height:140px; overflow-y:auto; border:1px solid var(--line); background:var(--bg0); border-radius:var(--radius); }
134
+ .rel-item { display:flex; align-items:center; gap:6px; padding:5px 8px; cursor:pointer; border-bottom:1px solid var(--line2); }
135
+ .rel-item:last-child { border-bottom:none; }
136
+ .rel-item:hover { background:var(--line2); }
137
+ .rel-item input[type=checkbox] { display:none; }
138
+ .rel-check { width:13px; height:13px; flex-shrink:0; border:1px solid var(--line); background:var(--bg0); display:flex; align-items:center; justify-content:center; font-size:8px; color:transparent; transition:all .12s; }
139
+ .rel-item:has(input:checked) .rel-check { background:var(--accent); border-color:var(--accent); color:var(--bg0); }
140
+ .rel-name { font-size:11px; color:var(--text); }
141
+ .rel-selected { display:flex; flex-wrap:wrap; gap:3px; margin-bottom:4px; min-height:18px; }
142
+ .rel-tag { font-size:9px; padding:2px 6px; background:var(--accent-bg); color:var(--accent); border:1px solid rgba(90,122,240,.2); border-radius:2px; }
143
+
144
+ /* Version history */
145
+ .version-row { display:flex; align-items:center; gap:8px; padding:5px 0; border-bottom:1px solid var(--line2); }
146
+ .version-row:last-child { border-bottom:none; }
147
+ .v-dot { width:6px; height:6px; background:var(--line); border-radius:50%; flex-shrink:0; }
148
+ .v-dot.cur { background:var(--gold); }
149
+ .v-hash { font-size:10px; color:var(--mid); font-family:var(--mono); flex:1; }
150
+ .v-time { font-size:9px; color:var(--muted); }
151
+
152
+ /* Block picker */
153
+ .block-picker { position:fixed; z-index:9999; width:220px; background:var(--bg2); border:1px solid var(--line); box-shadow:0 4px 16px rgba(0,0,0,.12); border-radius:2px; overflow:hidden; display:none; font-family:var(--mono); }
154
+ .bp-header { padding:7px 12px; border-bottom:1px solid var(--line); font-size:10px; color:var(--muted); letter-spacing:.06em; }
155
+ .bp-list { max-height:240px; overflow-y:auto; }
156
+ .bp-item { display:flex; align-items:center; gap:10px; padding:7px 12px; cursor:pointer; }
157
+ .bp-item:hover,.bp-item.active { background:var(--accent-bg); }
158
+ .bp-icon { width:24px; height:24px; display:flex; align-items:center; justify-content:center; background:var(--bg3); border:1px solid var(--line); font-size:10px; flex-shrink:0; }
159
+ .bp-label { font-size:11px; color:var(--text); flex:1; }
160
+ .bp-hint { font-size:9px; color:var(--muted); }
161
+
162
+ /* Autosave indicator */
163
+ #autosave-indicator { font-size:10px; color:var(--muted); padding:0 12px 0 8px; border-left:1px solid var(--line); margin-left:8px; }
164
+
165
+ /* SEO char counter */
166
+ .seo-counter { font-size:9px; float:right; color:var(--muted); transition:color .12s; }
167
+ .seo-counter.ok { color:var(--jade); }
168
+ .seo-counter.warn { color:var(--gold); }
169
+ .seo-counter.over { color:var(--red); }
170
+
171
+ /* ── Image blocks ───────────────────────────────────────────── */
172
+ .be-block[data-type="image"] { position:relative; min-height:auto; padding:6px 0; line-height:1; cursor:default; user-select:none; outline:none; }
173
+ .be-block[data-type="image"] > .be-img { display:block; max-width:100%; border-radius:6px; border:2px solid transparent; transition:border-color .15s; }
174
+ .be-block[data-type="image"].img-sel > .be-img { border-color:var(--accent); }
175
+ .be-block[data-type="image"][data-align="left"] { float:left; max-width:48%; margin:4px 20px 12px 0; }
176
+ .be-block[data-type="image"][data-align="right"] { float:right; max-width:48%; margin:4px 0 12px 20px; }
177
+ .be-block[data-type="image"][data-align="center"] { clear:both; }
178
+ .be-block[data-type="image"][data-align="center"] > .be-img { margin:0 auto; }
179
+ .be-block[data-type="image"][data-align="full"] { clear:both; }
180
+ .be-block[data-type="image"][data-align="full"] > .be-img { width:100%; max-width:100%; }
181
+ .be-img-ctrl {
182
+ position:absolute; top:10px; left:50%; transform:translateX(-50%);
183
+ display:flex; align-items:center; gap:2px;
184
+ background:var(--bg2); border:1px solid var(--mid); border-radius:6px;
185
+ padding:3px 4px; box-shadow:0 4px 14px rgba(0,0,0,.35);
186
+ z-index:20; opacity:0; pointer-events:none; transition:opacity .15s; white-space:nowrap;
187
+ }
188
+ .be-block[data-type="image"].img-sel .be-img-ctrl { opacity:1; pointer-events:auto; }
189
+ .img-ab {
190
+ height:22px; min-width:28px; padding:0 6px;
191
+ background:transparent; border:1px solid transparent; border-radius:3px;
192
+ color:var(--muted); font-size:9px; font-family:var(--mono);
193
+ cursor:pointer; transition:all .1s;
194
+ display:flex; align-items:center; justify-content:center; gap:3px;
195
+ }
196
+ .img-ab:hover { background:var(--bg3); color:var(--text); border-color:var(--line); }
197
+ .img-ab.on { background:var(--accent-bg); color:var(--accent); border-color:var(--accent); }
198
+ .img-ab.del:hover { background:var(--red); color:#fff; border-color:var(--red); }
199
+ .img-ctrl-sep { width:1px; height:14px; background:var(--line); margin:0 2px; }
200
+ .be-img-cap {
201
+ display:block; width:100%; margin-top:5px;
202
+ background:transparent; border:none; border-bottom:1px dashed transparent;
203
+ color:var(--muted); font-family:var(--mono); font-size:10px; letter-spacing:.02em;
204
+ padding:2px 0; outline:none; text-align:center; transition:border-color .12s; box-sizing:border-box;
205
+ }
206
+ .be-img-cap:focus { border-color:var(--line); color:var(--text); }
207
+ .be-block[data-type="image"].img-sel .be-img-cap { border-color:var(--line2); }
208
+
209
+ /* ── Image picker sheet ──────────────────────────────────────── */
210
+ .img-pick-overlay {
211
+ position:fixed; inset:0; background:rgba(0,0,0,.55); backdrop-filter:blur(4px);
212
+ z-index:8000; display:none; align-items:flex-end;
213
+ }
214
+ .img-pick-overlay.open { display:flex; }
215
+ .img-pick-sheet {
216
+ width:100%; max-height:64vh;
217
+ background:var(--bg1); border-top:1px solid var(--mid);
218
+ border-radius:12px 12px 0 0; display:flex; flex-direction:column; overflow:hidden;
219
+ }
220
+ .img-pick-head {
221
+ display:flex; align-items:center; gap:8px; padding:10px 16px;
222
+ border-bottom:1px solid var(--line); background:var(--bg2); flex-shrink:0;
223
+ }
224
+ .img-pick-title { font-family:var(--mono); font-size:10px; letter-spacing:.1em; color:var(--muted); text-transform:uppercase; flex:1; }
225
+ .img-pick-upload {
226
+ height:26px; padding:0 12px; background:var(--accent-bg); border:1px solid var(--accent);
227
+ color:var(--accent); font-family:var(--mono); font-size:10px; border-radius:4px; cursor:pointer; transition:background .12s;
228
+ }
229
+ .img-pick-upload:hover { background:rgba(90,122,240,.2); }
230
+ .img-pick-close {
231
+ width:26px; height:26px; background:transparent; border:1px solid var(--line);
232
+ border-radius:4px; color:var(--muted); font-size:16px; cursor:pointer;
233
+ display:flex; align-items:center; justify-content:center; line-height:1;
234
+ }
235
+ .img-pick-close:hover { color:var(--text); border-color:var(--mid); }
236
+ .img-pick-body { flex:1; overflow-y:auto; padding:12px 16px 24px; }
237
+ .img-pick-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(110px,1fr)); gap:8px; }
238
+ .img-pick-thumb {
239
+ aspect-ratio:4/3; border:1px solid var(--line); border-radius:6px;
240
+ overflow:hidden; cursor:pointer; position:relative; background:var(--bg0);
241
+ transition:border-color .12s, transform .1s;
242
+ }
243
+ .img-pick-thumb:hover { border-color:var(--accent); transform:scale(1.03); }
244
+ .img-pick-thumb img { width:100%; height:100%; object-fit:cover; }
245
+ .img-pick-name {
246
+ position:absolute; bottom:0; left:0; right:0;
247
+ padding:3px 6px; background:rgba(0,0,0,.6);
248
+ font-size:9px; color:rgba(255,255,255,.75);
249
+ white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
250
+ }
251
+ .img-pick-empty { padding:40px; text-align:center; font-size:12px; color:var(--muted); }
252
+ .img-pick-tabs { display:flex; gap:0; border-bottom:1px solid var(--line); flex-shrink:0; }
253
+ .img-pick-tab { flex:1; padding:8px; background:transparent; border:none; font-family:var(--mono); font-size:9px; letter-spacing:.08em; text-transform:uppercase; color:var(--muted); cursor:pointer; transition:all .12s; border-bottom:2px solid transparent; margin-bottom:-1px; }
254
+ .img-pick-tab:hover { color:var(--text); }
255
+ .img-pick-tab.active { color:var(--accent); border-bottom-color:var(--accent); }
256
+ .img-pick-pane { display:none; flex:1; overflow-y:auto; }
257
+ .img-pick-pane.active { display:block; }
258
+ .img-url-pane { padding:16px; display:flex; flex-direction:column; gap:10px; }
259
+ .img-url-inp { width:100%; background:var(--bg2); border:1px solid var(--line); color:var(--text); font-family:var(--mono); font-size:11px; padding:8px 10px; border-radius:4px; box-sizing:border-box; }
260
+ .img-url-inp:focus { outline:none; border-color:var(--accent); }
261
+ .img-url-hint { font-size:10px; color:var(--muted); line-height:1.6; }
262
+ .img-url-hint strong { color:var(--text); font-weight:500; }
263
+ .img-url-btn { align-self:flex-end; padding:6px 16px; background:var(--accent-bg); border:1px solid var(--accent); color:var(--accent); font-family:var(--mono); font-size:10px; border-radius:4px; cursor:pointer; transition:all .12s; }
264
+ .img-url-btn:hover { background:rgba(90,122,240,.25); }
265
+ .img-url-btn:disabled { opacity:.5; cursor:default; }
266
+ .img-url-status { font-size:10px; color:var(--muted); min-height:14px; }
267
+
268
+ /* ── Video blocks ───────────────────────────────────────────── */
269
+ .be-block[data-type="video"] {
270
+ position:relative; min-height:auto; padding:4px 0;
271
+ line-height:1; cursor:default; user-select:none; outline:none; clear:both;
272
+ }
273
+ .be-video-wrap {
274
+ position:relative; width:100%; padding-bottom:56.25%; /* 16:9 */
275
+ border-radius:8px; overflow:hidden;
276
+ border:2px solid transparent; transition:border-color .15s; background:var(--bg1);
277
+ }
278
+ .be-block[data-type="video"].vid-sel .be-video-wrap { border-color:var(--accent); }
279
+ .be-video-wrap iframe, .be-video-wrap video {
280
+ position:absolute; inset:0; width:100%; height:100%; border:none; border-radius:6px;
281
+ }
282
+ .be-video-overlay {
283
+ position:absolute; inset:0; cursor:default; z-index:5; border-radius:6px;
284
+ }
285
+ .be-block[data-type="video"].vid-sel .be-video-overlay { pointer-events:none; }
286
+ .be-vid-ctrl {
287
+ position:absolute; top:8px; left:50%; transform:translateX(-50%);
288
+ display:flex; align-items:center; gap:2px;
289
+ background:var(--bg2); border:1px solid var(--mid); border-radius:6px;
290
+ padding:3px 4px; box-shadow:0 4px 14px rgba(0,0,0,.35);
291
+ z-index:20; opacity:0; pointer-events:none; transition:opacity .15s; white-space:nowrap;
292
+ }
293
+ .be-block[data-type="video"].vid-sel .be-vid-ctrl { opacity:1; pointer-events:auto; }
294
+ .be-vid-url {
295
+ display:block; width:100%; margin-top:5px;
296
+ background:transparent; border:none; border-bottom:1px dashed transparent;
297
+ color:var(--muted); font-family:var(--mono); font-size:10px;
298
+ padding:2px 0; outline:none; text-align:center; transition:border-color .12s; box-sizing:border-box;
299
+ }
300
+ .be-vid-url:focus { border-color:var(--line); color:var(--text); }
301
+ .be-block[data-type="video"].vid-sel .be-vid-url { border-color:var(--line2); }
302
+
303
+ /* Video URL input modal */
304
+ .vid-url-overlay {
305
+ position:fixed; inset:0; background:rgba(0,0,0,.55); backdrop-filter:blur(4px);
306
+ z-index:8000; display:none; align-items:center; justify-content:center;
307
+ }
308
+ .vid-url-overlay.open { display:flex; }
309
+ .vid-url-box {
310
+ background:var(--bg2); border:1px solid var(--mid); border-radius:12px;
311
+ padding:24px 28px; width:460px; max-width:90vw;
312
+ box-shadow:0 8px 40px rgba(0,0,0,.5);
313
+ }
314
+ .vid-url-title { font-family:var(--mono); font-size:10px; letter-spacing:.1em; color:var(--muted); text-transform:uppercase; margin-bottom:12px; }
315
+ .vid-url-inp {
316
+ width:100%; background:var(--bg0); border:1px solid var(--line); border-radius:6px;
317
+ padding:9px 12px; color:var(--heading); font-family:var(--mono); font-size:12px;
318
+ outline:none; box-sizing:border-box; transition:border-color .15s;
319
+ }
320
+ .vid-url-inp:focus { border-color:var(--accent); }
321
+ .vid-url-hint { font-size:10px; color:var(--muted); margin-top:6px; }
322
+ .vid-url-actions { display:flex; gap:8px; margin-top:14px; justify-content:flex-end; }
323
+ .vid-url-ok {
324
+ height:30px; padding:0 16px; background:var(--accent-bg); border:1px solid var(--accent);
325
+ color:var(--accent); font-family:var(--mono); font-size:10px; border-radius:4px; cursor:pointer;
326
+ }
327
+ .vid-url-ok:hover { background:rgba(90,122,240,.2); }
328
+ .vid-url-cancel {
329
+ height:30px; padding:0 12px; background:transparent; border:1px solid var(--line);
330
+ color:var(--muted); font-family:var(--mono); font-size:10px; border-radius:4px; cursor:pointer;
331
+ }
332
+ .vid-url-cancel:hover { border-color:var(--mid); color:var(--text); }
333
+
334
+ /* SERP preview */
335
+ .serp-preview { background:var(--bg0); border:1px solid var(--line); border-radius:var(--radius); padding:12px 14px; margin-top:12px; }
336
+ .serp-url { font-size:9px; color:var(--jade); margin-bottom:3px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
337
+ .serp-title { font-size:13px; color:#8ab4f8; margin-bottom:4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
338
+ .serp-desc { font-size:10px; color:var(--mid); line-height:1.5; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
339
+ </style>
340
+ </head>
341
+ <body>
342
+ <div class="app">
343
+ <header class="topbar">
344
+ <a class="logo" href="/dashboard.html"><div class="logo-mark">OR</div>Orbiter</a>
345
+ <div id="editor-breadcrumb" style="font-size:11px;color:var(--muted);margin-left:16px;padding-left:16px;border-left:1px solid var(--line);display:flex;align-items:center;gap:6px;">
346
+ <a href="/collections.html" style="color:var(--muted);text-decoration:none;">Collections</a>
347
+ <span style="color:var(--line);">/</span>
348
+ <a id="back-to-col" href="/collections.html" style="color:var(--mid);text-decoration:none;"></a>
349
+ <span style="color:var(--line);">/</span>
350
+ <span id="breadcrumb-slug" style="color:var(--text);"></span>
351
+ </div>
352
+ <div class="topbar-right">
353
+ <span id="autosave-indicator"></span>
354
+ <span id="wc" style="font-size:10px;color:var(--muted);padding:0 8px;"></span>
355
+ <div class="view-toggle" onmousedown="event.preventDefault()" style="margin-left:0;margin-right:12px;">
356
+ <button type="button" class="view-btn active" id="vbtn-edit" onclick="setViewMode('edit')">Edit</button>
357
+ <button type="button" class="view-btn" id="vbtn-split" onclick="setViewMode('split')">Split</button>
358
+ <button type="button" class="view-btn" id="vbtn-preview" onclick="setViewMode('preview')">Preview</button>
359
+ </div>
360
+ <button class="scheme-toggle" id="scheme-toggle" title="Toggle scheme">◐</button>
361
+ <span class="user" id="topbar-user"></span>
362
+ <span class="logout" id="logout-btn">Sign out</span>
363
+ </div>
364
+ </header>
365
+ <div class="editor-shell" id="editor-shell">
366
+ <div class="editor-main" id="editor-main">
367
+ <div id="saved-flash" style="display:none;" class="saved-flash">Entry saved</div>
368
+ <div class="editor-toolbar">
369
+ <div class="tool-group" onmousedown="event.preventDefault()">
370
+ <button type="button" class="tool-btn" onclick="beCmd('bold')" title="Bold"><b style="font-size:11px;">B</b></button>
371
+ <button type="button" class="tool-btn" onclick="beCmd('italic')" title="Italic"><i style="font-size:11px;">I</i></button>
372
+ <button type="button" class="tool-btn" onclick="beCmd('code')" title="Inline code" style="font-size:9px;">&lt;/&gt;</button>
373
+ </div>
374
+ <div class="tool-sep"></div>
375
+ <div class="tool-group" onmousedown="event.preventDefault()">
376
+ <button type="button" class="tool-btn" onclick="changeBlockType('h1')" style="font-size:9px;">H1</button>
377
+ <button type="button" class="tool-btn" onclick="changeBlockType('h2')" style="font-size:9px;">H2</button>
378
+ <button type="button" class="tool-btn" onclick="changeBlockType('h3')" style="font-size:9px;">H3</button>
379
+ <button type="button" class="tool-btn" onclick="changeBlockType('blockquote')" >❝</button>
380
+ <button type="button" class="tool-btn" onclick="changeBlockType('ul')" style="font-size:12px;">·</button>
381
+ <button type="button" class="tool-btn" onclick="insertHr()" style="font-size:9px;">—</button>
382
+ </div>
383
+ <div class="tool-sep"></div>
384
+ <div class="tool-group" onmousedown="event.preventDefault()">
385
+ <button type="button" class="tool-btn" onclick="openImgPicker()" title="Insert image">
386
+ <svg width="13" height="13" viewBox="0 0 13 13" fill="none" style="display:block"><rect x=".75" y="1.75" width="11.5" height="9.5" rx="1.5" stroke="currentColor" stroke-width="1.1"/><circle cx="4.25" cy="5" r=".9" fill="currentColor"/><path d="M.75 9l2.9-2.9L6 8.45l1.85-1.85L12.25 10.5" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/></svg>
387
+ </button>
388
+ <button type="button" class="tool-btn" onclick="openVideoPicker()" title="Insert video (YouTube, Vimeo, mp4)">
389
+ <svg width="13" height="13" viewBox="0 0 13 13" fill="none" style="display:block"><rect x=".75" y="2.25" width="11.5" height="8.5" rx="1.5" stroke="currentColor" stroke-width="1.1"/><path d="M5.25 4.75l3.5 1.75-3.5 1.75V4.75z" fill="currentColor"/></svg>
390
+ </button>
391
+ </div>
392
+ </div>
393
+ <div class="editor-scroll">
394
+ <div class="editor-page">
395
+ <textarea id="title-input" class="editor-title-input" placeholder="Untitled" rows="1"
396
+ oninput="this.style.height='auto';this.style.height=this.scrollHeight+'px';onTitleInput()"></textarea>
397
+ <div class="editor-slug-line">/<span id="collection-id-display"></span>/<span id="slug-preview">…</span></div>
398
+ <div id="be-editor" class="be-editor" spellcheck="false"></div>
399
+ <textarea id="body-input" style="display:none"></textarea>
400
+ <div class="word-count" id="word-count-display"></div>
401
+ </div>
402
+ </div>
403
+ </div>
404
+
405
+ <!-- Preview panel -->
406
+ <div class="preview-panel" id="preview-panel">
407
+ <div class="preview-src-bar">
408
+ <button class="src-btn active" id="src-md" onclick="setPreviewSrc('md')">Markdown</button>
409
+ <button class="src-btn" id="src-site" onclick="setPreviewSrc('site')">Site</button>
410
+ </div>
411
+ <div class="preview-scroll" id="preview-md-pane">
412
+ <div class="md-preview-title" id="preview-title"></div>
413
+ <div class="md-preview-slug" id="preview-slug-line"></div>
414
+ <div class="md-preview" id="preview-body"></div>
415
+ </div>
416
+ <iframe class="preview-iframe" id="preview-iframe" style="display:none;" src="about:blank"></iframe>
417
+ </div>
418
+
419
+ <!-- Meta / sidebar -->
420
+ <div class="meta-panel" id="meta-panel">
421
+ <!-- filled by JS -->
422
+ </div>
423
+ </div>
424
+ </div>
425
+
426
+ <div class="block-picker" id="block-picker">
427
+ <div class="bp-header">/ Blocks — type to filter</div>
428
+ <div class="bp-list" id="bp-list"></div>
429
+ </div>
430
+
431
+ <script type="module">
432
+ // ── Bootstrap ─────────────────────────────────────────────────────
433
+ const me = await fetch('/api/auth/me',{credentials:'include'}).then(r=>r.json()).catch(()=>null);
434
+ if (!me?.user) { location.replace('/login.html'); }
435
+ document.getElementById('topbar-user').textContent = me.user.username;
436
+ document.getElementById('logout-btn').addEventListener('click',async()=>{
437
+ await fetch('/api/auth/logout',{method:'POST',credentials:'include'});
438
+ location.replace('/login.html');
439
+ });
440
+
441
+ // Parse ?collection=X&slug=Y from URL
442
+ const params = new URLSearchParams(location.search);
443
+ const COLLECTION = params.get('collection');
444
+ const SLUG = params.get('slug') ?? 'new';
445
+ const IS_NEW = SLUG === 'new';
446
+
447
+ if (!COLLECTION) { location.replace('/collections.html'); }
448
+ document.getElementById('collection-id-display').textContent = COLLECTION;
449
+
450
+ // Load collection schema + entry
451
+ const [colData, entryData, versionsData, mediaData] = await Promise.all([
452
+ fetch(`/api/collections/${COLLECTION}`,{credentials:'include'}).then(r=>r.ok?r.json():null),
453
+ IS_NEW ? null : fetch(`/api/collections/${COLLECTION}/entries/${SLUG}`,{credentials:'include'}).then(r=>r.ok?r.json():null),
454
+ IS_NEW ? [] : fetch(`/api/collections/${COLLECTION}/entries/${SLUG}/versions`,{credentials:'include'}).then(r=>r.ok?r.json():[]).catch(()=>[]),
455
+ fetch('/api/media',{credentials:'include'}).then(r=>r.json()).catch(()=>[]),
456
+ ]);
457
+
458
+ if (!colData) { location.replace('/collections.html'); }
459
+
460
+ // Breadcrumb
461
+ document.getElementById('back-to-col').textContent = colData.label;
462
+ document.getElementById('back-to-col').href = `/entries.html?col=${COLLECTION}&label=${encodeURIComponent(colData.label)}`;
463
+ document.getElementById('breadcrumb-slug').textContent = IS_NEW ? 'New entry' : SLUG;
464
+ document.title = `${IS_NEW ? 'New entry' : SLUG} — ${colData.label} — Orbiter`;
465
+
466
+ const schema = colData.schema ? (typeof colData.schema==='string'?JSON.parse(colData.schema):colData.schema) : {};
467
+ const extraFields = Object.entries(schema).filter(([k])=>k!=='title'&&k!=='body');
468
+
469
+ // Load relation entries for any relation fields
470
+ const relationEntries = {};
471
+ for (const [,f] of extraFields) {
472
+ if (f.type==='relation' && f.collection && !relationEntries[f.collection]) {
473
+ relationEntries[f.collection] = await fetch(`/api/collections/${f.collection}/entries`,{credentials:'include'}).then(r=>r.ok?r.json():[]).catch(()=>[]);
474
+ }
475
+ }
476
+
477
+ // Set title + slug
478
+ const titleInput = document.getElementById('title-input');
479
+ titleInput.value = IS_NEW ? '' : (entryData?.data?.title ?? SLUG);
480
+ titleInput.style.height = 'auto';
481
+ titleInput.style.height = titleInput.scrollHeight + 'px';
482
+
483
+ let currentSlug = IS_NEW ? '' : SLUG;
484
+ document.getElementById('slug-preview').textContent = currentSlug || '…';
485
+
486
+ // ── Meta panel ────────────────────────────────────────────────────
487
+ const SEO_TITLE_KEYS = new Set(['meta_title','seo_title','og_title']);
488
+ const SEO_DESC_KEYS = new Set(['meta_description','seo_description','og_description']);
489
+
490
+ function renderMetaPanel() {
491
+ const panel = document.getElementById('meta-panel');
492
+ const status = IS_NEW ? 'draft' : (entryData?.status ?? 'draft');
493
+ const updatedAt = entryData?.updated_at;
494
+
495
+ let fieldsHtml = '';
496
+ const seoFields = {};
497
+ for (const [key, field] of extraFields) {
498
+ const val = entryData?.data?.[key];
499
+ if (SEO_TITLE_KEYS.has(key)) seoFields.titleKey = key;
500
+ if (SEO_DESC_KEYS.has(key)) seoFields.descKey = key;
501
+ const isSeoTitle = SEO_TITLE_KEYS.has(key);
502
+ const isSeoDesc = SEO_DESC_KEYS.has(key);
503
+ if (isSeoTitle || isSeoDesc) {
504
+ fieldsHtml += `<div class="meta-field" data-field-key="${key}" ${field.showWhen?`data-show-when="${field.showWhen}"`:''}><div class="field-label" style="display:flex;justify-content:space-between;">${escHtml(field.label??key)}<span class="seo-counter" id="seo-ctr-${key}">${(val??'').length}</span></div>`;
505
+ } else {
506
+ fieldsHtml += `<div class="meta-field" data-field-key="${key}" ${field.showWhen?`data-show-when="${field.showWhen}"`:''}><div class="field-label">${escHtml(field.label??key)}</div>`;
507
+ }
508
+
509
+ if (field.type==='datetime') {
510
+ fieldsHtml += `<input type="datetime-local" class="field-input" name="${key}" value="${escHtml(val??'')}" />`;
511
+ } else if (field.type==='date') {
512
+ fieldsHtml += `<input type="date" class="field-input" name="${key}" value="${escHtml(val??'')}" />`;
513
+ } else if (field.type==='select') {
514
+ fieldsHtml += `<select class="field-select" name="${key}">${(field.options??[]).map(o=>`<option value="${escHtml(o)}"${val===o?' selected':''}>${escHtml(o)}</option>`).join('')}</select>`;
515
+ } else if (field.type==='weekdays') {
516
+ const active = Array.isArray(val) ? val : [];
517
+ fieldsHtml += `<div class="weekday-grid" id="wd-grid-${key}">
518
+ ${[['Mon','Mo'],['Tue','Di'],['Wed','Mi'],['Thu','Do'],['Fri','Fr'],['Sat','Sa'],['Sun','So']].map(([d,l])=>`<button type="button" class="wd-btn${active.includes(d)?' active':''}" data-day="${d}" data-key="${key}">${l}</button>`).join('')}
519
+ <input type="hidden" name="${key}" id="wd-val-${key}" value="${active.join(',')}" />
520
+ </div>`;
521
+ } else if (field.type==='media') {
522
+ const hasVal = !!(val);
523
+ fieldsHtml += `<div>
524
+ <select class="field-select" name="${key}" id="media-sel-${key}">
525
+ <option value="">— none —</option>
526
+ ${mediaData.map(m=>`<option value="${m.id}"${val===m.id?' selected':''}>${escHtml(m.filename)}${m.alt?` (${m.alt})`:''}</option>`).join('')}
527
+ </select>
528
+ <div class="media-drop-zone${hasVal?' has-image':''}" id="media-drop-${key}">
529
+ <img src="${hasVal?`/api/media/${val}/raw`:''}" alt="" id="media-img-${key}" style="${hasVal?'':'display:none'}" />
530
+ <div class="media-drop-label" id="media-drop-label-${key}">↑ drop or click</div>
531
+ </div>
532
+ <div style="margin-top:4px;"><span class="field-readonly" id="media-status-${key}"></span></div>
533
+ </div>`;
534
+ } else if (field.type==='array') {
535
+ const arrVal = Array.isArray(val) ? val.join(', ') : (val??'');
536
+ fieldsHtml += `<div>
537
+ <input type="text" class="tag-input" name="${key}" id="arr-${key}" value="${escHtml(arrVal)}" placeholder="tag1, tag2, …" />
538
+ <div class="tag-preview" id="tags-${key}"></div>
539
+ </div>`;
540
+ } else if (field.type==='relation') {
541
+ const selected = Array.isArray(val) ? val : (val ? [val] : []);
542
+ const entries = relationEntries[field.collection] ?? [];
543
+ const selHtml = selected.length
544
+ ? selected.map(id=>{ const e=entries.find(x=>x.id===id); return e?`<span class="rel-tag">${escHtml(e.data?.title??e.slug)}</span>`:''; }).join('')
545
+ : '<span style="font-size:10px;color:var(--muted);">—</span>';
546
+ fieldsHtml += `<div class="rel-picker">
547
+ <div class="rel-selected" id="rel-selected-${key}">${selHtml}</div>
548
+ <input class="rel-search" type="text" placeholder="Filter…" id="rel-search-${key}" />
549
+ <div class="rel-list" id="rel-list-${key}">
550
+ ${entries.length ? entries.map(e=>{
551
+ const checked = selected.includes(e.id);
552
+ return `<label class="rel-item" data-key="${key}">
553
+ <input type="checkbox" name="${key}" value="${e.id}"${checked?' checked':''} />
554
+ <span class="rel-check">${checked?'✓':''}</span>
555
+ <span class="rel-name">${escHtml(e.data?.title??e.data?.name??e.slug)}</span>
556
+ </label>`;
557
+ }).join('') : '<div style="padding:10px;font-size:10px;color:var(--muted);">No entries</div>'}
558
+ </div>
559
+ </div>`;
560
+ } else if (field.type==='url') {
561
+ fieldsHtml += `<input type="url" class="field-input" name="${key}" value="${escHtml(val??'')}" placeholder="https://…" />`;
562
+ } else if (field.type==='number') {
563
+ fieldsHtml += `<input type="number" class="field-input" name="${key}" value="${escHtml(val??'')}" />`;
564
+ } else if (SEO_TITLE_KEYS.has(key)) {
565
+ fieldsHtml += `<input type="text" class="field-input" name="${key}" id="seo-inp-${key}" value="${escHtml(val??'')}" maxlength="120" />`;
566
+ } else if (SEO_DESC_KEYS.has(key)) {
567
+ fieldsHtml += `<textarea class="field-input" name="${key}" id="seo-inp-${key}" rows="3" maxlength="300" style="resize:vertical;">${escHtml(val??'')}</textarea>`;
568
+ } else {
569
+ fieldsHtml += `<input type="text" class="field-input" name="${key}" value="${escHtml(val??'')}" />`;
570
+ }
571
+ fieldsHtml += '</div>';
572
+ }
573
+
574
+ const versHtml = versionsData.slice(0,8).map((v,i)=>`
575
+ <div class="version-row">
576
+ <div class="v-dot${i===0?' cur':''}"></div>
577
+ <div class="v-hash">${v.id.slice(0,7)}</div>
578
+ <div class="v-time">${new Date(v.created_at).toLocaleDateString()}</div>
579
+ </div>
580
+ `).join('');
581
+
582
+ const hasSeo = !!(seoFields.titleKey || seoFields.descKey);
583
+ const serpHtml = hasSeo ? `<div class="serp-preview" id="serp-preview">
584
+ <div class="serp-url" id="serp-url">${location.host}/${COLLECTION}/<span id="serp-slug-part">${escHtml(IS_NEW?'…':SLUG)}</span></div>
585
+ <div class="serp-title" id="serp-title">${escHtml(document.getElementById('title-input')?.value||'Untitled')}</div>
586
+ <div class="serp-desc" id="serp-desc">—</div>
587
+ </div>` : '';
588
+
589
+ panel.innerHTML = `
590
+ ${extraFields.length ? `<div class="meta-section"><div class="meta-label">Fields</div>${fieldsHtml}${serpHtml}</div>` : ''}
591
+ <div class="meta-section">
592
+ <div class="status-bar">
593
+ <div class="status-dot ${status}" id="status-dot"></div>
594
+ <div class="status-lbl ${status}" id="status-lbl">${status==='published'?'Published':'Draft'}</div>
595
+ </div>
596
+ <button type="button" class="btn-publish" id="btn-publish">Publish</button>
597
+ <button type="button" class="btn-draft" id="btn-draft">Save as draft</button>
598
+ </div>
599
+ <div class="meta-section">
600
+ <div class="meta-label">Details</div>
601
+ <div class="meta-field">
602
+ <div class="field-label">Slug</div>
603
+ <input class="field-input" id="slug-input" value="${escHtml(IS_NEW?'':SLUG)}" placeholder="entry-slug" />
604
+ </div>
605
+ <div class="meta-field">
606
+ <div class="field-label">Status</div>
607
+ <select class="field-select" id="status-select">
608
+ <option value="draft" ${status==='draft' ?'selected':''}>Draft</option>
609
+ <option value="published" ${status==='published' ?'selected':''}>Published</option>
610
+ </select>
611
+ </div>
612
+ ${updatedAt ? `<div class="meta-field"><div class="field-label">Modified</div><div class="field-readonly">${new Date(updatedAt).toLocaleDateString()}</div></div>` : ''}
613
+ </div>
614
+ ${versionsData.length ? `<div class="meta-section"><div class="meta-label">History</div>${versHtml}</div>` : ''}
615
+ `;
616
+
617
+ // Wire up weekday toggles
618
+ panel.querySelectorAll('.wd-btn').forEach(btn=>{
619
+ btn.addEventListener('click',()=>{
620
+ btn.classList.toggle('active');
621
+ const key = btn.dataset.key;
622
+ const hidden = document.getElementById('wd-val-'+key);
623
+ const cur = hidden.value ? hidden.value.split(',') : [];
624
+ const idx = cur.indexOf(btn.dataset.day);
625
+ if (idx===-1) cur.push(btn.dataset.day); else cur.splice(idx,1);
626
+ hidden.value = cur.join(',');
627
+ });
628
+ });
629
+
630
+ // Wire up tag previews
631
+ panel.querySelectorAll('[id^="arr-"]').forEach(el=>{
632
+ const key = el.id.replace('arr-','');
633
+ renderTags(key);
634
+ el.addEventListener('input',()=>renderTags(key));
635
+ });
636
+
637
+ // Wire up relation search + selection
638
+ panel.querySelectorAll('.rel-search').forEach(search=>{
639
+ const key = search.id.replace('rel-search-','');
640
+ search.addEventListener('input',()=>{
641
+ const q = search.value.toLowerCase();
642
+ document.querySelectorAll(`#rel-list-${key} .rel-item`).forEach(item=>{
643
+ item.style.display = item.querySelector('.rel-name').textContent.toLowerCase().includes(q) ? '' : 'none';
644
+ });
645
+ });
646
+ });
647
+ panel.querySelectorAll('.rel-item').forEach(item=>{
648
+ item.addEventListener('change',()=>updateRelSelected(item.dataset.key));
649
+ });
650
+
651
+ // Wire up media drop zones
652
+ panel.querySelectorAll('.media-drop-zone').forEach(zone=>{
653
+ const key = zone.id.replace('media-drop-','');
654
+ zone.addEventListener('click',()=>{ const fi=document.createElement('input');fi.type='file';fi.accept='image/*,video/*,application/pdf';fi.onchange=()=>uploadMediaField(key,fi);fi.click(); });
655
+ zone.addEventListener('dragover',e=>{e.preventDefault();zone.classList.add('drag-over');});
656
+ zone.addEventListener('dragleave',()=>zone.classList.remove('drag-over'));
657
+ zone.addEventListener('drop',e=>{e.preventDefault();zone.classList.remove('drag-over');if(e.dataTransfer.files.length){const fi={files:e.dataTransfer.files};uploadMediaField(key,fi);}});
658
+ });
659
+
660
+ // Publish / draft buttons
661
+ document.getElementById('btn-publish').addEventListener('click',()=>saveEntry('published'));
662
+ document.getElementById('btn-draft').addEventListener('click',()=>saveEntry('draft'));
663
+
664
+ // Slug input → slug preview
665
+ document.getElementById('slug-input').addEventListener('input', e=>{
666
+ e.target.dataset.manual='1';
667
+ document.getElementById('slug-preview').textContent = e.target.value || '…';
668
+ });
669
+
670
+ // Status select sync
671
+ document.getElementById('status-select').addEventListener('change',e=>{
672
+ updateStatusUI(e.target.value);
673
+ });
674
+
675
+ // showWhen conditional fields
676
+ initConditional();
677
+
678
+ // SEO char counters + SERP preview live update
679
+ if (hasSeo) {
680
+ function seoCounterClass(len, isTitle) {
681
+ const [lo, hi] = isTitle ? [30, 60] : [120, 160];
682
+ if (len === 0) return '';
683
+ if (len < lo) return 'warn';
684
+ if (len <= hi) return 'ok';
685
+ return 'over';
686
+ }
687
+ function updateSerp() {
688
+ const titleEl = document.getElementById('serp-title');
689
+ const descEl = document.getElementById('serp-desc');
690
+ const slugEl = document.getElementById('serp-slug-part');
691
+ const slugVal = document.getElementById('slug-input')?.value || document.getElementById('slug-preview')?.textContent || '…';
692
+ if (slugEl) slugEl.textContent = slugVal;
693
+ if (titleEl) {
694
+ const seoTitle = seoFields.titleKey ? document.getElementById('seo-inp-'+seoFields.titleKey)?.value : '';
695
+ titleEl.textContent = seoTitle || document.getElementById('title-input')?.value || 'Untitled';
696
+ }
697
+ if (descEl) {
698
+ const seoDesc = seoFields.descKey ? document.getElementById('seo-inp-'+seoFields.descKey)?.value : '';
699
+ descEl.textContent = seoDesc || '—';
700
+ }
701
+ }
702
+ if (seoFields.titleKey) {
703
+ const inp = document.getElementById('seo-inp-'+seoFields.titleKey);
704
+ const ctr = document.getElementById('seo-ctr-'+seoFields.titleKey);
705
+ inp?.addEventListener('input',()=>{
706
+ const len = inp.value.length;
707
+ if (ctr) { ctr.textContent=len; ctr.className='seo-counter '+seoCounterClass(len,true); }
708
+ updateSerp();
709
+ });
710
+ if (ctr && inp) { const l=inp.value.length; ctr.textContent=l; ctr.className='seo-counter '+seoCounterClass(l,true); }
711
+ }
712
+ if (seoFields.descKey) {
713
+ const inp = document.getElementById('seo-inp-'+seoFields.descKey);
714
+ const ctr = document.getElementById('seo-ctr-'+seoFields.descKey);
715
+ inp?.addEventListener('input',()=>{
716
+ const len = inp.value.length;
717
+ if (ctr) { ctr.textContent=len; ctr.className='seo-counter '+seoCounterClass(len,false); }
718
+ updateSerp();
719
+ });
720
+ if (ctr && inp) { const l=inp.value.length; ctr.textContent=l; ctr.className='seo-counter '+seoCounterClass(l,false); }
721
+ }
722
+ // Also update SERP when title or slug changes
723
+ document.getElementById('title-input')?.addEventListener('input', updateSerp);
724
+ document.getElementById('slug-input')?.addEventListener('input', updateSerp);
725
+ updateSerp();
726
+ }
727
+ }
728
+
729
+ function updateStatusUI(status) {
730
+ document.getElementById('status-dot').className = 'status-dot ' + status;
731
+ document.getElementById('status-lbl').className = 'status-lbl ' + status;
732
+ document.getElementById('status-lbl').textContent = status==='published'?'Published':'Draft';
733
+ document.getElementById('status-select').value = status;
734
+ }
735
+
736
+ function renderTags(key) {
737
+ const input = document.getElementById('arr-'+key);
738
+ const preview = document.getElementById('tags-'+key);
739
+ if (!preview) return;
740
+ const tags = input.value.split(',').map(t=>t.trim()).filter(Boolean);
741
+ preview.innerHTML = tags.map(t=>`<span class="tag-chip">${escHtml(t)}</span>`).join('');
742
+ }
743
+
744
+ function updateRelSelected(key) {
745
+ const list = document.getElementById('rel-list-'+key);
746
+ const selected = document.getElementById('rel-selected-'+key);
747
+ if (!list||!selected) return;
748
+ setTimeout(()=>{
749
+ const checked = [...list.querySelectorAll('input[type=checkbox]:checked')];
750
+ selected.innerHTML = checked.length
751
+ ? checked.map(cb=>{ const name=cb.closest('.rel-item')?.querySelector('.rel-name')?.textContent??cb.value; return `<span class="rel-tag">${escHtml(name)}</span>`; }).join('')
752
+ : '<span style="font-size:10px;color:var(--muted);">—</span>';
753
+ list.querySelectorAll('.rel-item').forEach(item=>{
754
+ const cb=item.querySelector('input[type=checkbox]');
755
+ const box=item.querySelector('.rel-check');
756
+ if (box) { box.textContent=cb?.checked?'✓':''; box.style.color=cb?.checked?'var(--bg0)':'transparent'; box.style.background=cb?.checked?'var(--accent)':'var(--bg0)'; box.style.borderColor=cb?.checked?'var(--accent)':'var(--line)'; }
757
+ });
758
+ }, 0);
759
+ }
760
+
761
+ async function uploadMediaField(key, input) {
762
+ if (!input.files?.length) return;
763
+ const file = input.files[0];
764
+ const statusEl = document.getElementById('media-status-'+key);
765
+ const sel = document.getElementById('media-sel-'+key);
766
+ const zone = document.getElementById('media-drop-'+key);
767
+ const img = document.getElementById('media-img-'+key);
768
+ statusEl.textContent = '↑ uploading…';
769
+ const fd = new FormData();
770
+ fd.append('file',file); fd.append('alt',''); fd.append('folder','');
771
+ try {
772
+ const res = await fetch('/api/media',{method:'POST',credentials:'include',body:fd});
773
+ const data = await res.json();
774
+ if (data.id) {
775
+ const opt = document.createElement('option');
776
+ opt.value=data.id; opt.textContent=data.filename; opt.selected=true;
777
+ sel.appendChild(opt);
778
+ img.src='/api/media/'+data.id+'/raw'; img.style.display='block';
779
+ zone.classList.add('has-image');
780
+ statusEl.textContent='✓ '+data.filename;
781
+ setTimeout(()=>{statusEl.textContent='';},3000);
782
+ }
783
+ } catch(e) { statusEl.textContent='✗ '+e.message; }
784
+ }
785
+
786
+ function initConditional() {
787
+ const conditionals = document.querySelectorAll('[data-show-when]');
788
+ if (!conditionals.length) return;
789
+ function evalCond(cond) {
790
+ const [key,rawVal] = cond.split(':');
791
+ const negate = rawVal.startsWith('!');
792
+ const expected = negate ? rawVal.slice(1) : rawVal;
793
+ const ctrl = document.querySelector(`[name="${key}"]`);
794
+ const current = ctrl ? ctrl.value : 'none';
795
+ return negate ? current!==expected : current===expected;
796
+ }
797
+ function refreshAll() { conditionals.forEach(el=>{ el.style.display=evalCond(el.dataset.showWhen)?'':'none'; }); }
798
+ const controlKeys = new Set([...conditionals].map(el=>el.dataset.showWhen.split(':')[0]));
799
+ controlKeys.forEach(key=>{
800
+ const ctrl = document.querySelector(`[name="${key}"]`);
801
+ if (ctrl) ctrl.addEventListener('change',refreshAll);
802
+ });
803
+ refreshAll();
804
+ }
805
+
806
+ // ── Save entry ────────────────────────────────────────────────────
807
+ let currentPath = location.pathname + location.search;
808
+
809
+ async function saveEntry(status, isAutosave=false) {
810
+ const title = document.getElementById('title-input').value;
811
+ syncToHidden();
812
+ const body = document.getElementById('body-input').value;
813
+ const slugInput = document.getElementById('slug-input');
814
+ const slug = isAutosave ? (currentSlug || slugify(title) || 'untitled') : (slugInput.value.trim() || slugify(title) || 'untitled');
815
+
816
+ // Gather extra field values
817
+ const data = { title, body };
818
+ for (const [key, field] of extraFields) {
819
+ if (field.type==='array'||field.type==='weekdays') {
820
+ const raw = document.getElementById('wd-val-'+key)?.value || document.querySelector(`[name="${key}"]`)?.value || '';
821
+ data[key] = raw ? raw.split(',').map(s=>s.trim()).filter(Boolean) : [];
822
+ } else if (field.type==='relation') {
823
+ data[key] = [...document.querySelectorAll(`[name="${key}"]:checked`)].map(cb=>cb.value);
824
+ } else {
825
+ const el = document.querySelector(`[name="${key}"]`);
826
+ data[key] = el ? el.value : '';
827
+ }
828
+ }
829
+
830
+ if (IS_NEW && !currentSlug) {
831
+ // Create new entry
832
+ const res = await fetch(`/api/collections/${COLLECTION}/entries`,{
833
+ method:'POST', credentials:'include',
834
+ headers:{'Content-Type':'application/json'},
835
+ body: JSON.stringify({ slug, data, status }),
836
+ });
837
+ const json = await res.json();
838
+ if (json.slug || json.id) {
839
+ currentSlug = json.slug ?? slug;
840
+ document.getElementById('slug-preview').textContent = currentSlug;
841
+ slugInput.value = currentSlug;
842
+ const newUrl = `/editor.html?collection=${COLLECTION}&slug=${currentSlug}`;
843
+ history.replaceState(null,'',newUrl);
844
+ currentPath = newUrl;
845
+ setIndicator('saved');
846
+ updateStatusUI(status);
847
+ if (!isAutosave) showFlash();
848
+ } else { setIndicator('error'); }
849
+ } else {
850
+ const targetSlug = currentSlug || SLUG;
851
+ const res = await fetch(`/api/collections/${COLLECTION}/entries/${targetSlug}`,{
852
+ method:'PUT', credentials:'include',
853
+ headers:{'Content-Type':'application/json'},
854
+ body: JSON.stringify({ slug, data, status }),
855
+ });
856
+ if (res.ok) {
857
+ const json = await res.json();
858
+ if (json.slug && json.slug!==currentSlug) {
859
+ currentSlug = json.slug;
860
+ const newUrl = `/editor.html?collection=${COLLECTION}&slug=${currentSlug}`;
861
+ history.replaceState(null,'',newUrl);
862
+ document.getElementById('slug-preview').textContent = currentSlug;
863
+ slugInput.value = currentSlug;
864
+ }
865
+ setIndicator('saved'); updateStatusUI(status);
866
+ if (!isAutosave) showFlash();
867
+ } else { setIndicator('error'); }
868
+ }
869
+ }
870
+
871
+ function slugify(s) {
872
+ return s.toLowerCase().replace(/\s+/g,'-').replace(/[^a-z0-9-]/g,'').slice(0,60);
873
+ }
874
+
875
+ function showFlash() {
876
+ const f = document.getElementById('saved-flash');
877
+ f.style.display=''; setTimeout(()=>f.style.display='none',2000);
878
+ }
879
+
880
+ // ── Autosave ──────────────────────────────────────────────────────
881
+ const indicator = document.getElementById('autosave-indicator');
882
+ let autosaveTimer = null;
883
+
884
+ function setIndicator(state) {
885
+ const states = { pending:{text:'Unsaved',color:'var(--gold)'}, saving:{text:'Saving…',color:'var(--muted)'}, saved:{text:'✓ Saved',color:'var(--jade)'}, error:{text:'Error',color:'var(--red)'} };
886
+ const s = states[state] ?? {};
887
+ indicator.textContent = s.text ?? '';
888
+ indicator.style.color = s.color ?? '';
889
+ }
890
+
891
+ function scheduleAutosave() {
892
+ clearTimeout(autosaveTimer);
893
+ setIndicator('pending');
894
+ autosaveTimer = setTimeout(()=>{ setIndicator('saving'); saveEntry(document.getElementById('status-select')?.value??'draft', true); }, 2000);
895
+ }
896
+
897
+ window.onTitleInput = function() {
898
+ const slugInput = document.getElementById('slug-input');
899
+ if (!slugInput.dataset.manual) {
900
+ const s = slugify(document.getElementById('title-input').value);
901
+ slugInput.value = s;
902
+ document.getElementById('slug-preview').textContent = s || '…';
903
+ }
904
+ updateWc(); scheduleAutosave(); updatePreview();
905
+ };
906
+
907
+ // Cmd+S
908
+ document.addEventListener('keydown', e=>{
909
+ if ((e.metaKey||e.ctrlKey) && e.key==='s') {
910
+ e.preventDefault();
911
+ saveEntry(document.getElementById('status-select')?.value??'draft');
912
+ }
913
+ });
914
+
915
+ // ── Block editor ──────────────────────────────────────────────────
916
+ const beEditor = document.getElementById('be-editor');
917
+ const bodyInput = document.getElementById('body-input');
918
+
919
+ function inlineMd(text) {
920
+ return text
921
+ .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
922
+ .replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
923
+ .replace(/__(.+?)__/g,'<strong>$1</strong>')
924
+ .replace(/\*(.+?)\*/g,'<em>$1</em>')
925
+ .replace(/_(.+?)_/g,'<em>$1</em>')
926
+ .replace(/`(.+?)`/g,'<code>$1</code>');
927
+ }
928
+
929
+ function htmlToMd(html) {
930
+ return html
931
+ .replace(/<strong>([\s\S]*?)<\/strong>/gi,'**$1**')
932
+ .replace(/<b>([\s\S]*?)<\/b>/gi,'**$1**')
933
+ .replace(/<em>([\s\S]*?)<\/em>/gi,'_$1_')
934
+ .replace(/<i>([\s\S]*?)<\/i>/gi,'_$1_')
935
+ .replace(/<code>([\s\S]*?)<\/code>/gi,'`$1`')
936
+ .replace(/<br\s*\/?>/gi,' ')
937
+ .replace(/<[^>]+>/g,'')
938
+ .replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&nbsp;/g,' ');
939
+ }
940
+
941
+ function serialize() {
942
+ const lines = [];
943
+ beEditor.querySelectorAll('.be-block').forEach(b=>{
944
+ const type = b.dataset.type||'p';
945
+ if (type==='image') {
946
+ const id = b.dataset.mediaId ?? '';
947
+ const alt = b.querySelector('.be-img-cap')?.value ?? b.dataset.alt ?? '';
948
+ const align = b.dataset.align ?? 'center';
949
+ const suf = (align && align!=='center') ? `{.${align}}` : '';
950
+ lines.push(`![${alt}](${id})${suf}`);
951
+ return;
952
+ }
953
+ if (type==='video') {
954
+ lines.push(`::video[${b.dataset.videoUrl ?? ''}]`);
955
+ return;
956
+ }
957
+ const text = htmlToMd(b.innerHTML);
958
+ if (type==='h1') lines.push('# '+text);
959
+ else if (type==='h2') lines.push('## '+text);
960
+ else if (type==='h3') lines.push('### '+text);
961
+ else if (type==='blockquote') lines.push('> '+text);
962
+ else if (type==='pre') lines.push('```\n'+text+'\n```');
963
+ else if (type==='ul') lines.push('- '+text);
964
+ else if (type==='ol') lines.push('1. '+text);
965
+ else if (type==='hr') lines.push('---');
966
+ else lines.push(text);
967
+ });
968
+ return lines.join('\n\n');
969
+ }
970
+
971
+ function syncToHidden() { bodyInput.value = serialize(); updateWc(); }
972
+
973
+ const BLK_PH = { h1:'Heading 1',h2:'Heading 2',h3:'Heading 3',blockquote:'Quote…',pre:'Code…',ul:'List item',ol:'List item',p:'Write something…' };
974
+
975
+ function createBlock(type, content) {
976
+ const el = document.createElement('div');
977
+ el.className='be-block'; el.dataset.type=type||'p'; el.dataset.ph=BLK_PH[type]||'Write something…';
978
+ if (type==='hr') { el.contentEditable='false'; }
979
+ else { el.contentEditable='true'; el.innerHTML=content||''; }
980
+ return el;
981
+ }
982
+
983
+ function parseMd(md) {
984
+ beEditor.innerHTML='';
985
+ if (!md) { beEditor.appendChild(createBlock('p','')); return; }
986
+ md.split(/\n{2,}/).forEach(chunk=>{
987
+ chunk=chunk.trim(); if (!chunk) return;
988
+ // Video block: ::video[url]
989
+ const vidM = chunk.match(/^::video\[(.+)\]$/);
990
+ if (vidM) { beEditor.appendChild(createVideoBlock(vidM[1])); return; }
991
+ // Image block: ![alt](mediaId) or ![alt](mediaId){.align}
992
+ const imgM = chunk.match(/^!\[([^\]]*)\]\(([^)]+)\)(?:\{\.(\w+)\})?$/);
993
+ if (imgM) { beEditor.appendChild(createImgBlock(imgM[2], imgM[1], imgM[3]||'center')); return; }
994
+ let type='p', content=chunk;
995
+ if (chunk.startsWith('# ')) { type='h1'; content=inlineMd(chunk.slice(2)); }
996
+ else if (chunk.startsWith('## ')) { type='h2'; content=inlineMd(chunk.slice(3)); }
997
+ else if (chunk.startsWith('### ')) { type='h3'; content=inlineMd(chunk.slice(4)); }
998
+ else if (chunk.startsWith('> ')) { type='blockquote'; content=inlineMd(chunk.slice(2)); }
999
+ else if (/^```/.test(chunk)) { type='pre'; content=chunk.replace(/^```[^\n]*\n?/,'').replace(/\n?```$/,''); }
1000
+ else if (chunk.startsWith('- ')) { type='ul'; content=inlineMd(chunk.slice(2)); }
1001
+ else if (/^\d+\.\s/.test(chunk)) { type='ol'; content=inlineMd(chunk.replace(/^\d+\.\s/,'')); }
1002
+ else if (chunk==='---') { type='hr'; content=''; }
1003
+ else { type='p'; content=inlineMd(chunk); }
1004
+ beEditor.appendChild(createBlock(type,content));
1005
+ });
1006
+ if (!beEditor.querySelector('.be-block')) beEditor.appendChild(createBlock('p',''));
1007
+ }
1008
+
1009
+ function focusStart(el) { if (!el||el.contentEditable==='false') return; el.focus(); const r=document.createRange();r.setStart(el,0);r.collapse(true);const s=window.getSelection();s.removeAllRanges();s.addRange(r); }
1010
+ function focusEnd(el) { if (!el||el.contentEditable==='false') return; el.focus(); const r=document.createRange();r.selectNodeContents(el);r.collapse(false);const s=window.getSelection();s.removeAllRanges();s.addRange(r); }
1011
+
1012
+ function getFocusedBlock() {
1013
+ const sel=window.getSelection(); if (!sel||!sel.rangeCount) return null;
1014
+ let node=sel.getRangeAt(0).commonAncestorContainer;
1015
+ while (node&&node!==beEditor) { if (node.classList?.contains('be-block')) return node; node=node.parentNode; }
1016
+ return null;
1017
+ }
1018
+
1019
+ window.changeBlockType = function(type, block) {
1020
+ const b=block||getFocusedBlock(); if (!b) return;
1021
+ b.dataset.type=type; b.dataset.ph=BLK_PH[type]||'Write something…';
1022
+ if (type==='hr') { b.innerHTML=''; b.contentEditable='false'; }
1023
+ else { b.contentEditable='true'; focusEnd(b); }
1024
+ syncToHidden(); scheduleAutosave(); updatePreview();
1025
+ };
1026
+
1027
+ window.insertHr = function() {
1028
+ const b=getFocusedBlock(); const hr=createBlock('hr',''); const next=createBlock('p','');
1029
+ if (b) { b.after(hr); hr.after(next); } else { beEditor.appendChild(hr); beEditor.appendChild(next); }
1030
+ focusStart(next); syncToHidden(); scheduleAutosave(); updatePreview();
1031
+ };
1032
+
1033
+ window.beCmd = function(cmd) {
1034
+ if (cmd==='code') {
1035
+ const sel=window.getSelection(); if (!sel||!sel.rangeCount) return;
1036
+ const range=sel.getRangeAt(0); const text=range.toString();
1037
+ if (text) { const code=document.createElement('code'); code.textContent=text; range.deleteContents(); range.insertNode(code); }
1038
+ } else { document.execCommand(cmd,false,null); }
1039
+ syncToHidden(); scheduleAutosave(); updatePreview();
1040
+ };
1041
+
1042
+ beEditor.addEventListener('keydown',e=>{
1043
+ if (bpOpen) {
1044
+ const items=bpFiltered();
1045
+ if (e.key==='ArrowDown') { e.preventDefault(); bpIdx=(bpIdx+1)%(items.length||1); bpRender(); return; }
1046
+ if (e.key==='ArrowUp') { e.preventDefault(); bpIdx=(bpIdx-1+(items.length||1))%(items.length||1); bpRender(); return; }
1047
+ if (e.key==='Enter') { e.preventDefault(); bpInsert(bpIdx); return; }
1048
+ if (e.key==='Escape') { e.preventDefault(); bpClose(); return; }
1049
+ }
1050
+ const b=getFocusedBlock(); if (!b) return;
1051
+ if (e.key==='Enter'&&!e.shiftKey) {
1052
+ e.preventDefault();
1053
+ const type=b.dataset.type; const newType=(type==='ul'||type==='ol')?type:'p';
1054
+ const next=createBlock(newType,''); b.after(next); focusStart(next);
1055
+ syncToHidden(); scheduleAutosave(); updatePreview(); return;
1056
+ }
1057
+ if (e.key==='Backspace') {
1058
+ const isEmpty=b.innerText.trim()===''; const isFirst=b===beEditor.firstElementChild;
1059
+ if (isEmpty&&b.dataset.type!=='p') { e.preventDefault(); b.dataset.type='p'; b.dataset.ph=BLK_PH.p; b.innerHTML=''; b.contentEditable='true'; focusStart(b); syncToHidden(); scheduleAutosave(); updatePreview(); return; }
1060
+ if (isEmpty&&!isFirst) { e.preventDefault(); const prev=b.previousElementSibling; b.remove(); if (prev&&prev.contentEditable!=='false') focusEnd(prev); syncToHidden(); scheduleAutosave(); updatePreview(); return; }
1061
+ }
1062
+ });
1063
+
1064
+ beEditor.addEventListener('input',()=>{
1065
+ const b=getFocusedBlock(); if (!b||b.dataset.type==='hr') { syncToHidden(); scheduleAutosave(); updatePreview(); return; }
1066
+ if (bpOpen&&bpTargetBlock) {
1067
+ const text=bpTargetBlock.innerText; const typed=text.substring(1);
1068
+ if (typed.includes('\n')) bpClose(); else { bpQuery=typed; bpIdx=0; bpRender(); } return;
1069
+ }
1070
+ const text=b.innerText;
1071
+ if (text==='# ') { b.innerHTML=''; changeBlockType('h1',b); return; }
1072
+ if (text==='## ') { b.innerHTML=''; changeBlockType('h2',b); return; }
1073
+ if (text==='### ') { b.innerHTML=''; changeBlockType('h3',b); return; }
1074
+ if (text==='> ') { b.innerHTML=''; changeBlockType('blockquote',b); return; }
1075
+ if (text==='- ') { b.innerHTML=''; changeBlockType('ul',b); return; }
1076
+ if (text==='---') { const hr=createBlock('hr',''); const next=createBlock('p',''); b.replaceWith(hr); hr.after(next); focusStart(next); syncToHidden(); scheduleAutosave(); updatePreview(); return; }
1077
+ if (text==='/') { bpOpen_(b); return; }
1078
+ syncToHidden(); scheduleAutosave(); updatePreview();
1079
+ });
1080
+
1081
+ beEditor.addEventListener('paste',e=>{
1082
+ e.preventDefault();
1083
+ const text=(e.clipboardData||window.clipboardData).getData('text/plain').trim();
1084
+ if (text && /^https?:\/\//i.test(text) && /youtube\.com|youtu\.be|vimeo\.com|wistia\.(com|net)|\.(?:mp4|webm|ogg|mov)(\?|$)/i.test(text)) {
1085
+ const b=getFocusedBlock();
1086
+ const block=createVideoBlock(text);
1087
+ if (b) {
1088
+ b.after(block);
1089
+ if (b.innerText.trim()==='') b.remove();
1090
+ if (!block.nextElementSibling||block.nextElementSibling.contentEditable==='false') block.after(createBlock('p',''));
1091
+ } else {
1092
+ beEditor.appendChild(block);
1093
+ beEditor.appendChild(createBlock('p',''));
1094
+ }
1095
+ syncToHidden(); scheduleAutosave(); updatePreview(); return;
1096
+ }
1097
+ if (text) document.execCommand('insertText',false,text);
1098
+ syncToHidden(); scheduleAutosave(); updatePreview();
1099
+ });
1100
+
1101
+ beEditor.addEventListener('click',e=>{
1102
+ if (e.target===beEditor) {
1103
+ const last=beEditor.lastElementChild;
1104
+ if (last&&last.contentEditable!=='false') focusEnd(last);
1105
+ else { const p=createBlock('p',''); beEditor.appendChild(p); focusStart(p); }
1106
+ }
1107
+ });
1108
+
1109
+ function updateWc() {
1110
+ const body = bodyInput.value;
1111
+ const words = body.trim() ? body.trim().split(/\s+/).filter(w=>w).length : 0;
1112
+ const chars = body.length;
1113
+ const mins = Math.max(1, Math.round(words / 200));
1114
+ const wcEl = document.getElementById('wc');
1115
+ const detEl = document.getElementById('word-count-display');
1116
+ if (wcEl) wcEl.textContent = words ? `${words.toLocaleString()} w · ${mins} min` : '';
1117
+ if (detEl) detEl.textContent = words ? `${words.toLocaleString()} words · ${chars.toLocaleString()} chars · ~${mins} min read` : '';
1118
+ }
1119
+
1120
+ // ── View modes ────────────────────────────────────────────────────
1121
+ let viewMode='edit'; let previewSrc='md';
1122
+ const shell=document.getElementById('editor-shell');
1123
+
1124
+ window.setViewMode = function(mode) {
1125
+ viewMode=mode;
1126
+ shell.classList.remove('mode-split','mode-preview');
1127
+ if (mode!=='edit') shell.classList.add('mode-'+mode);
1128
+ ['edit','split','preview'].forEach(m=>document.getElementById('vbtn-'+m).classList.toggle('active',m===mode));
1129
+ if (mode!=='edit') updatePreview();
1130
+ };
1131
+
1132
+ window.setPreviewSrc = function(src) {
1133
+ previewSrc=src;
1134
+ document.getElementById('src-md').classList.toggle('active',src==='md');
1135
+ document.getElementById('src-site').classList.toggle('active',src==='site');
1136
+ document.getElementById('preview-md-pane').style.display = src==='md' ? '' : 'none';
1137
+ document.getElementById('preview-iframe').style.display = src==='site' ? '' : 'none';
1138
+ updatePreview();
1139
+ };
1140
+
1141
+ function updatePreview() {
1142
+ if (viewMode==='edit') return;
1143
+ if (previewSrc==='md') renderMarkdown(); else renderIframe();
1144
+ }
1145
+
1146
+ function renderMarkdown() {
1147
+ const title=document.getElementById('title-input').value;
1148
+ const slug=document.getElementById('slug-preview').textContent;
1149
+ document.getElementById('preview-title').textContent=title||'Untitled';
1150
+ document.getElementById('preview-slug-line').innerHTML='/'+COLLECTION+'/<span>'+slug+'</span>';
1151
+ if (window.marked) {
1152
+ // Expand image blocks: ![alt](id){.align} → <img> with alignment style
1153
+ const prepped = (bodyInput.value||'').replace(
1154
+ /!\[([^\]]*)\]\(([0-9a-f-]{32,36})\)(?:\{\.(\w+)\})?/g,
1155
+ (_,alt,id,align)=>{
1156
+ const st = align==='left' ? 'float:left;max-width:48%;margin:4px 20px 12px 0;'
1157
+ : align==='right' ? 'float:right;max-width:48%;margin:4px 0 12px 20px;'
1158
+ : align==='full' ? 'width:100%;display:block;'
1159
+ : 'display:block;margin:0 auto;';
1160
+ return `<img src="/api/media/${id}/raw" alt="${alt}" style="border-radius:6px;max-width:100%;${st}">`;
1161
+ }
1162
+ );
1163
+ const prepped2 = prepped.replace(/::video\[([^\]]+)\]/g, (_,url)=>{
1164
+ const embed = parseVideoUrl(url);
1165
+ if (!embed) return `<video src="${url}" controls style="width:100%;border-radius:8px;margin:16px 0;"></video>`;
1166
+ return `<div style="position:relative;padding-bottom:56.25%;height:0;margin:16px 0;overflow:hidden;border-radius:8px;"><iframe src="${embed}" frameborder="0" allowfullscreen allow="autoplay;encrypted-media" style="position:absolute;top:0;left:0;width:100%;height:100%;"></iframe></div>`;
1167
+ });
1168
+ document.getElementById('preview-body').innerHTML=marked.parse(prepped2);
1169
+ }
1170
+ }
1171
+
1172
+ function renderIframe() {
1173
+ const slug=document.getElementById('slug-preview').textContent||currentSlug;
1174
+ document.getElementById('preview-iframe').src=window.location.origin+'/'+COLLECTION+'/'+slug;
1175
+ }
1176
+
1177
+ // ── Block picker ──────────────────────────────────────────────────
1178
+ const BP_BLOCKS = [
1179
+ {label:'Heading 1', icon:'H1', hint:'#', type:'h1'},
1180
+ {label:'Heading 2', icon:'H2', hint:'##', type:'h2'},
1181
+ {label:'Heading 3', icon:'H3', hint:'###', type:'h3'},
1182
+ {label:'Quote', icon:'❝', hint:'>', type:'blockquote'},
1183
+ {label:'Code block', icon:'{}', hint:'```', type:'pre'},
1184
+ {label:'List', icon:'·', hint:'-', type:'ul'},
1185
+ {label:'Numbered', icon:'1.', hint:'1.', type:'ol'},
1186
+ {label:'Divider', icon:'—', hint:'---', type:'hr'},
1187
+ {label:'Image', icon:'⊡', hint:'img', type:'image'},
1188
+ {label:'Video', icon:'▶', hint:'vid', type:'video'},
1189
+ ];
1190
+ let bpOpen=false, bpIdx=0, bpQuery='', bpTargetBlock=null;
1191
+ const bpEl=document.getElementById('block-picker');
1192
+ const bpListEl=document.getElementById('bp-list');
1193
+
1194
+ function bpFiltered() {
1195
+ if (!bpQuery) return BP_BLOCKS;
1196
+ const q=bpQuery.toLowerCase();
1197
+ return BP_BLOCKS.filter(b=>b.label.toLowerCase().includes(q)||b.hint.includes(q));
1198
+ }
1199
+
1200
+ function bpRender() {
1201
+ bpListEl.innerHTML='';
1202
+ const items=bpFiltered();
1203
+ if (!items.length) { bpListEl.innerHTML='<div style="padding:12px;font-size:11px;color:var(--muted);text-align:center;">No blocks</div>'; return; }
1204
+ items.forEach((b,i)=>{
1205
+ const row=document.createElement('div');
1206
+ row.className='bp-item'+(i===bpIdx?' active':'');
1207
+ row.innerHTML=`<div class="bp-icon">${b.icon}</div><div class="bp-label">${b.label}</div><div class="bp-hint">${b.hint}</div>`;
1208
+ row.addEventListener('mousedown',ev=>{ev.preventDefault();bpInsert(i);});
1209
+ row.addEventListener('mouseover',()=>{bpIdx=i;bpRender();});
1210
+ bpListEl.appendChild(row);
1211
+ });
1212
+ }
1213
+
1214
+ function bpOpen_(block) {
1215
+ bpTargetBlock=block; bpQuery=''; bpIdx=0; bpOpen=true;
1216
+ const rect=block.getBoundingClientRect();
1217
+ bpEl.style.top=Math.min(rect.bottom+6,window.innerHeight-280)+'px';
1218
+ bpEl.style.left=Math.min(rect.left,window.innerWidth-240)+'px';
1219
+ bpEl.style.display='block'; bpRender();
1220
+ }
1221
+
1222
+ function bpClose() { bpOpen=false; bpEl.style.display='none'; }
1223
+
1224
+ function bpInsert(relIdx) {
1225
+ const items=bpFiltered(); const blk=items[relIdx];
1226
+ if (!blk||!bpTargetBlock) { bpClose(); return; }
1227
+ if (blk.type==='image') {
1228
+ pickerAnchor = bpTargetBlock;
1229
+ bpTargetBlock.innerHTML=''; bpClose(); openImgPicker(); return;
1230
+ }
1231
+ if (blk.type==='video') {
1232
+ vidPickerAnchor = bpTargetBlock;
1233
+ bpTargetBlock.innerHTML=''; bpClose(); openVideoPicker(); return;
1234
+ }
1235
+ bpTargetBlock.innerHTML='';
1236
+ if (blk.type==='hr') {
1237
+ const hr=createBlock('hr',''); const next=createBlock('p','');
1238
+ bpTargetBlock.replaceWith(hr); hr.after(next); focusStart(next);
1239
+ } else { changeBlockType(blk.type,bpTargetBlock); }
1240
+ syncToHidden(); scheduleAutosave(); updatePreview(); bpClose();
1241
+ }
1242
+
1243
+ document.addEventListener('mousedown',e=>{if(bpOpen&&!bpEl.contains(e.target))bpClose();});
1244
+
1245
+ // ── Image blocks ──────────────────────────────────────────────────
1246
+ let pickerAnchor = null;
1247
+
1248
+ function refreshAlignBtns(block, align) {
1249
+ block.querySelectorAll('.img-ab[data-align]').forEach(b=>b.classList.toggle('on', b.dataset.align===align));
1250
+ }
1251
+
1252
+ function createImgBlock(mediaId, alt, align) {
1253
+ alt = alt ?? '';
1254
+ align = align ?? 'center';
1255
+ const el = document.createElement('div');
1256
+ el.className='be-block'; el.dataset.type='image';
1257
+ el.dataset.mediaId=mediaId; el.dataset.alt=alt; el.dataset.align=align;
1258
+ el.contentEditable='false';
1259
+ el.innerHTML = `
1260
+ <div class="be-img-ctrl">
1261
+ <button class="img-ab" data-align="left" title="Float left" onclick="imgAlign(this)">◧ left</button>
1262
+ <button class="img-ab" data-align="center" title="Center" onclick="imgAlign(this)">▣ center</button>
1263
+ <button class="img-ab" data-align="right" title="Float right" onclick="imgAlign(this)">◨ right</button>
1264
+ <button class="img-ab" data-align="full" title="Full width" onclick="imgAlign(this)">▭ full</button>
1265
+ <div class="img-ctrl-sep"></div>
1266
+ <button class="img-ab del" title="Remove image" onclick="imgDelete(this)">✕</button>
1267
+ </div>
1268
+ <img class="be-img" src="/api/media/${mediaId}/raw" alt="">
1269
+ <input class="be-img-cap" type="text" placeholder="Caption / alt text…" value="">
1270
+ `;
1271
+ el.querySelector('.be-img').alt = alt;
1272
+ el.querySelector('.be-img-cap').value = alt;
1273
+ refreshAlignBtns(el, align);
1274
+
1275
+ el.querySelector('.be-img-cap').addEventListener('input', function() {
1276
+ el.dataset.alt = this.value;
1277
+ el.querySelector('.be-img').alt = this.value;
1278
+ syncToHidden(); scheduleAutosave();
1279
+ });
1280
+ el.querySelector('.be-img-cap').addEventListener('click', e=>e.stopPropagation());
1281
+
1282
+ el.addEventListener('click', e=>{
1283
+ if (e.target.closest('.img-ab')) return;
1284
+ e.stopPropagation();
1285
+ beEditor.querySelectorAll('.be-block[data-type="image"]').forEach(b=>b.classList.remove('img-sel'));
1286
+ el.classList.add('img-sel');
1287
+ });
1288
+ return el;
1289
+ }
1290
+
1291
+ window.imgAlign = function(btn) {
1292
+ const block=btn.closest('.be-block[data-type="image"]'); if(!block) return;
1293
+ block.dataset.align=btn.dataset.align;
1294
+ refreshAlignBtns(block, btn.dataset.align);
1295
+ syncToHidden(); scheduleAutosave(); updatePreview();
1296
+ };
1297
+
1298
+ window.imgDelete = function(btn) {
1299
+ const block=btn.closest('.be-block[data-type="image"]'); if(!block) return;
1300
+ const prev=block.previousElementSibling, next=block.nextElementSibling;
1301
+ block.remove();
1302
+ if (next&&next.contentEditable!=='false') focusStart(next);
1303
+ else if (prev&&prev.contentEditable!=='false') focusEnd(prev);
1304
+ else { const p=createBlock('p',''); beEditor.appendChild(p); focusStart(p); }
1305
+ syncToHidden(); scheduleAutosave(); updatePreview();
1306
+ };
1307
+
1308
+ // Deselect image blocks on outside click
1309
+ document.addEventListener('click', e=>{
1310
+ if (!e.target.closest('.be-block[data-type="image"]'))
1311
+ beEditor.querySelectorAll('.be-block[data-type="image"]').forEach(b=>b.classList.remove('img-sel'));
1312
+ });
1313
+
1314
+ // ── Image picker ──────────────────────────────────────────────────
1315
+ window.openImgPicker = function() {
1316
+ pickerAnchor = getFocusedBlock();
1317
+ document.getElementById('img-pick-overlay').classList.add('open');
1318
+ renderPickerGrid();
1319
+ };
1320
+
1321
+ window.closeImgPicker = function() {
1322
+ document.getElementById('img-pick-overlay').classList.remove('open');
1323
+ };
1324
+
1325
+ function renderPickerGrid() {
1326
+ const grid=document.getElementById('img-pick-grid');
1327
+ const images=mediaData.filter(m=>m.mime_type?.startsWith('image/'));
1328
+ if (!images.length) {
1329
+ grid.innerHTML='<div class="img-pick-empty">No images yet.<br>Upload one to get started.</div>'; return;
1330
+ }
1331
+ grid.innerHTML=images.map(m=>`
1332
+ <div class="img-pick-thumb" onclick="pickInsert('${m.id}','${escHtml(m.alt??m.filename)}')">
1333
+ <img src="/api/media/${m.id}/raw" alt="${escHtml(m.alt??m.filename)}" loading="lazy">
1334
+ <div class="img-pick-name">${escHtml(m.filename)}</div>
1335
+ </div>`).join('');
1336
+ }
1337
+
1338
+ window.pickInsert = function(mediaId, alt) {
1339
+ closeImgPicker();
1340
+ doInsertImg(mediaId, alt||'');
1341
+ };
1342
+
1343
+ function doInsertImg(mediaId, alt) {
1344
+ const block=createImgBlock(mediaId, alt, 'center');
1345
+ const anchor=pickerAnchor;
1346
+ if (anchor) {
1347
+ anchor.after(block);
1348
+ if (!block.nextElementSibling||block.nextElementSibling.dataset.type==='image')
1349
+ block.after(createBlock('p',''));
1350
+ } else {
1351
+ beEditor.appendChild(block);
1352
+ beEditor.appendChild(createBlock('p',''));
1353
+ }
1354
+ syncToHidden(); scheduleAutosave(); updatePreview();
1355
+ }
1356
+
1357
+ // Upload from file (native picker — supports iCloud Drive, Dropbox, Google Drive via OS)
1358
+ document.getElementById('img-file-inp').addEventListener('change', async function() {
1359
+ const file=this.files[0]; if (!file) return;
1360
+ this.value='';
1361
+ const btn=document.querySelector('.img-pick-upload');
1362
+ const orig=btn.textContent; btn.textContent='Uploading…'; btn.disabled=true;
1363
+ const fd=new FormData(); fd.append('file',file); fd.append('alt',''); fd.append('folder','');
1364
+ try {
1365
+ const res=await fetch('/api/media',{method:'POST',credentials:'include',body:fd});
1366
+ if (res.ok) {
1367
+ const media=await res.json();
1368
+ mediaData.unshift(media);
1369
+ closeImgPicker();
1370
+ doInsertImg(media.id, media.alt??'');
1371
+ } else { alert('Upload failed'); }
1372
+ } catch { alert('Upload error'); }
1373
+ finally { btn.textContent=orig; btn.disabled=false; }
1374
+ });
1375
+
1376
+ window.imgPickTab = function(tab, btn) {
1377
+ document.querySelectorAll('.img-pick-tab').forEach(b=>b.classList.remove('active'));
1378
+ document.querySelectorAll('.img-pick-pane').forEach(p=>p.classList.remove('active'));
1379
+ btn.classList.add('active');
1380
+ document.getElementById('img-pick-pane-'+tab).classList.add('active');
1381
+ if (tab==='url') setTimeout(()=>document.getElementById('img-url-inp').focus(), 40);
1382
+ };
1383
+
1384
+ window.importFromUrl = async function() {
1385
+ const inp = document.getElementById('img-url-inp');
1386
+ const url = inp.value.trim();
1387
+ if (!url) return;
1388
+ const btn = document.getElementById('img-url-btn');
1389
+ const status = document.getElementById('img-url-status');
1390
+ btn.disabled=true; btn.textContent='Wird geladen…'; status.textContent='';
1391
+ try {
1392
+ const res = await fetch('/api/media/import-url', {
1393
+ method:'POST', credentials:'include',
1394
+ headers:{'Content-Type':'application/json'},
1395
+ body: JSON.stringify({ url, alt:'', folder:'' })
1396
+ });
1397
+ if (!res.ok) { const e=await res.json().catch(()=>({})); status.textContent='Fehler: '+(e.error||res.status); return; }
1398
+ const media = await res.json();
1399
+ mediaData.unshift(media);
1400
+ closeImgPicker();
1401
+ doInsertImg(media.id, media.alt??'');
1402
+ inp.value='';
1403
+ } catch(e) {
1404
+ status.textContent='Netzwerkfehler: '+e.message;
1405
+ } finally {
1406
+ btn.disabled=false; btn.textContent='Importieren';
1407
+ }
1408
+ };
1409
+
1410
+ document.getElementById('img-url-inp').addEventListener('keydown', e=>{
1411
+ if (e.key==='Enter') { e.preventDefault(); importFromUrl(); }
1412
+ });
1413
+
1414
+ function escHtml(str) {
1415
+ return String(str??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1416
+ }
1417
+
1418
+ // ── Video blocks ──────────────────────────────────────────────────
1419
+ let vidPickerAnchor = null;
1420
+
1421
+ function parseVideoUrl(url) {
1422
+ url = (url||'').trim();
1423
+ let m = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([A-Za-z0-9_-]{11})/);
1424
+ if (m) return `https://www.youtube.com/embed/${m[1]}?rel=0`;
1425
+ m = url.match(/vimeo\.com\/(?:video\/)?(\d+)/);
1426
+ if (m) return `https://player.vimeo.com/video/${m[1]}`;
1427
+ m = url.match(/wistia\.(?:com|net)\/(?:medias|embed\/iframe)\/(\w+)/);
1428
+ if (m) return `https://fast.wistia.net/embed/iframe/${m[1]}`;
1429
+ return null; // direct file URL — use <video> tag
1430
+ }
1431
+
1432
+ function createVideoBlock(url) {
1433
+ const embed = parseVideoUrl(url);
1434
+ const isDirect = /\.(mp4|webm|ogg|mov)(\?.*)?$/i.test(url);
1435
+ const el = document.createElement('div');
1436
+ el.className='be-block'; el.dataset.type='video';
1437
+ el.dataset.videoUrl = url;
1438
+ el.contentEditable='false';
1439
+ let mediaHtml;
1440
+ if (embed) {
1441
+ mediaHtml = `<iframe src="${escHtml(embed)}" frameborder="0" allowfullscreen allow="autoplay; encrypted-media"></iframe><div class="be-video-overlay"></div>`;
1442
+ } else if (isDirect) {
1443
+ mediaHtml = `<video src="${escHtml(url)}" controls></video>`;
1444
+ } else {
1445
+ mediaHtml = `<div style="padding:20px;color:var(--muted);font-size:11px;font-family:var(--mono);text-align:center;">Unsupported video URL</div>`;
1446
+ }
1447
+ el.innerHTML = `
1448
+ <div class="be-video-wrap">${mediaHtml}</div>
1449
+ <div class="be-vid-ctrl">
1450
+ <input class="be-vid-url" type="text" value="${escHtml(url)}" readonly tabindex="-1">
1451
+ <button class="img-ab del" onclick="vidDelete(this)" title="Remove video">✕ remove</button>
1452
+ </div>`;
1453
+ el.addEventListener('click', e=>{
1454
+ if (e.target.closest('.img-ab')) return;
1455
+ e.stopPropagation();
1456
+ beEditor.querySelectorAll('.be-block[data-type="video"]').forEach(b=>b.classList.remove('vid-sel'));
1457
+ el.classList.add('vid-sel');
1458
+ });
1459
+ return el;
1460
+ }
1461
+
1462
+ window.vidDelete = function(btn) {
1463
+ const block=btn.closest('.be-block[data-type="video"]'); if(!block) return;
1464
+ const prev=block.previousElementSibling, next=block.nextElementSibling;
1465
+ block.remove();
1466
+ if (next&&next.contentEditable!=='false') focusStart(next);
1467
+ else if (prev&&prev.contentEditable!=='false') focusEnd(prev);
1468
+ else { const p=createBlock('p',''); beEditor.appendChild(p); focusStart(p); }
1469
+ syncToHidden(); scheduleAutosave(); updatePreview();
1470
+ };
1471
+
1472
+ document.addEventListener('click', e=>{
1473
+ if (!e.target.closest('.be-block[data-type="video"]'))
1474
+ beEditor.querySelectorAll('.be-block[data-type="video"]').forEach(b=>b.classList.remove('vid-sel'));
1475
+ });
1476
+
1477
+ window.openVideoPicker = function() {
1478
+ vidPickerAnchor = getFocusedBlock();
1479
+ const inp = document.getElementById('vid-url-inp');
1480
+ inp.value = '';
1481
+ document.getElementById('vid-url-overlay').classList.add('open');
1482
+ setTimeout(()=>inp.focus(), 60);
1483
+ };
1484
+
1485
+ window.closeVideoPicker = function() {
1486
+ document.getElementById('vid-url-overlay').classList.remove('open');
1487
+ };
1488
+
1489
+ window.confirmVideoUrl = function() {
1490
+ const url = document.getElementById('vid-url-inp').value.trim();
1491
+ if (!url) return;
1492
+ closeVideoPicker();
1493
+ const block = createVideoBlock(url);
1494
+ const anchor = vidPickerAnchor;
1495
+ if (anchor) {
1496
+ anchor.after(block);
1497
+ if (!block.nextElementSibling||block.nextElementSibling.contentEditable==='false')
1498
+ block.after(createBlock('p',''));
1499
+ if (anchor.innerText.trim()==='') anchor.remove();
1500
+ } else {
1501
+ beEditor.appendChild(block);
1502
+ beEditor.appendChild(createBlock('p',''));
1503
+ }
1504
+ syncToHidden(); scheduleAutosave(); updatePreview();
1505
+ };
1506
+
1507
+ document.getElementById('vid-url-inp').addEventListener('keydown', e=>{
1508
+ if (e.key==='Enter') { e.preventDefault(); confirmVideoUrl(); }
1509
+ if (e.key==='Escape') { e.preventDefault(); closeVideoPicker(); }
1510
+ });
1511
+
1512
+ // ── Init ──────────────────────────────────────────────────────────
1513
+ renderMetaPanel();
1514
+ parseMd(IS_NEW ? '' : (entryData?.data?.body ?? ''));
1515
+ syncToHidden(); // serializes blocks → bodyInput, then calls updateWc()
1516
+ updatePreview();
1517
+ if (!IS_NEW) setIndicator('saved');
1518
+ </script>
1519
+ <!-- Video URL dialog -->
1520
+ <div id="vid-url-overlay" class="vid-url-overlay" onclick="if(event.target===this)closeVideoPicker()">
1521
+ <div class="vid-url-box">
1522
+ <div class="vid-url-title">Insert Video</div>
1523
+ <input id="vid-url-inp" class="vid-url-inp" type="url" placeholder="https://youtu.be/… or https://vimeo.com/… or .mp4 URL" />
1524
+ <div class="vid-url-hint">YouTube · Vimeo · Wistia · direct MP4/WebM URL</div>
1525
+ <div class="vid-url-actions">
1526
+ <button class="vid-url-cancel" onclick="closeVideoPicker()">Cancel</button>
1527
+ <button class="vid-url-ok" onclick="confirmVideoUrl()">Insert</button>
1528
+ </div>
1529
+ </div>
1530
+ </div>
1531
+
1532
+ <!-- Image picker sheet -->
1533
+ <div id="img-pick-overlay" class="img-pick-overlay" onclick="if(event.target===this)closeImgPicker()">
1534
+ <div class="img-pick-sheet">
1535
+ <div class="img-pick-head">
1536
+ <span class="img-pick-title">Insert Image</span>
1537
+ <button class="img-pick-upload" onclick="document.getElementById('img-file-inp').click()">↑ Upload</button>
1538
+ <button class="img-pick-close" onclick="closeImgPicker()">×</button>
1539
+ </div>
1540
+ <div class="img-pick-tabs">
1541
+ <button class="img-pick-tab active" onclick="imgPickTab('library',this)">Library</button>
1542
+ <button class="img-pick-tab" onclick="imgPickTab('url',this)">From URL</button>
1543
+ </div>
1544
+ <div id="img-pick-pane-library" class="img-pick-pane active">
1545
+ <div class="img-pick-body">
1546
+ <div id="img-pick-grid" class="img-pick-grid"></div>
1547
+ </div>
1548
+ </div>
1549
+ <div id="img-pick-pane-url" class="img-pick-pane">
1550
+ <div class="img-url-pane">
1551
+ <input id="img-url-inp" class="img-url-inp" type="url" placeholder="https://…" />
1552
+ <div class="img-url-hint">
1553
+ <strong>Dropbox</strong> — Teilen → Link kopieren<br>
1554
+ <strong>Google Drive</strong> — Freigeben → Link abrufen<br>
1555
+ <strong>OneDrive</strong> — Teilen → Link kopieren<br>
1556
+ <strong>Jede öffentliche Bild-URL</strong>
1557
+ </div>
1558
+ <div id="img-url-status" class="img-url-status"></div>
1559
+ <button id="img-url-btn" class="img-url-btn" onclick="importFromUrl()">Importieren</button>
1560
+ </div>
1561
+ </div>
1562
+ </div>
1563
+ </div>
1564
+ <input type="file" id="img-file-inp" accept="image/*" style="display:none">
1565
+
1566
+ <script src="/sidebar.js"></script>
1567
+ <script src="/router.js"></script>
1568
+ </body>
1569
+ </html>