deckrun 1.3.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.
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/dist/editor-content.js +400 -0
- package/dist/editor.js +3592 -0
- package/dist/generate.js +2394 -0
- package/dist/index.js +538 -0
- package/dist/parser.js +65 -0
- package/dist/pdf.js +196 -0
- package/dist/preview.js +277 -0
- package/dist/themes.js +971 -0
- package/package.json +26 -0
package/dist/editor.js
ADDED
|
@@ -0,0 +1,3592 @@
|
|
|
1
|
+
import { RESET_CSS } from "./generate.js";
|
|
2
|
+
import { DEFAULT_SIZE, DEFAULT_THEME, decorOf, findFont, fontSummaries, googleFontsHref, resolveSizeName, sizeSummaries, themeSummaries, themeSwitchableCss, } from "./themes.js";
|
|
3
|
+
import { SNIPPETS, SNIPPET_GROUPS, TIPS, WELCOME_DECK, } from "./editor-content.js";
|
|
4
|
+
import { PREVIEW_WIDTH, PREVIEW_HEIGHT } from "./preview.js";
|
|
5
|
+
function bootstrapJson(theme, size, fonts) {
|
|
6
|
+
const payload = {
|
|
7
|
+
theme,
|
|
8
|
+
themes: themeSummaries(),
|
|
9
|
+
size,
|
|
10
|
+
sizes: sizeSummaries(),
|
|
11
|
+
fonts,
|
|
12
|
+
faces: fontSummaries(),
|
|
13
|
+
width: PREVIEW_WIDTH,
|
|
14
|
+
height: PREVIEW_HEIGHT,
|
|
15
|
+
groups: SNIPPET_GROUPS,
|
|
16
|
+
snippets: SNIPPETS,
|
|
17
|
+
tips: TIPS,
|
|
18
|
+
welcome: WELCOME_DECK,
|
|
19
|
+
};
|
|
20
|
+
// Keep the JSON inert inside a <script> block.
|
|
21
|
+
return JSON.stringify(payload).replace(/</g, "\\u003c");
|
|
22
|
+
}
|
|
23
|
+
/** The Markdown editor served when `deckrun` is launched without a file. */
|
|
24
|
+
export function generateEditorHtml(theme = DEFAULT_THEME, sizeInput = DEFAULT_SIZE, fontInput = {}) {
|
|
25
|
+
const size = resolveSizeName(sizeInput);
|
|
26
|
+
const fonts = { head: findFont(fontInput.head), body: findFont(fontInput.body) };
|
|
27
|
+
return `<!DOCTYPE html>
|
|
28
|
+
<html lang="en" data-theme="${theme}" data-decor="${decorOf(theme)}">
|
|
29
|
+
<head>
|
|
30
|
+
<meta charset="UTF-8">
|
|
31
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
32
|
+
<title>deckrun editor</title>
|
|
33
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
34
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
35
|
+
<link href="${googleFontsHref()}" rel="stylesheet">
|
|
36
|
+
<style>
|
|
37
|
+
${RESET_CSS}
|
|
38
|
+
|
|
39
|
+
${themeSwitchableCss()}
|
|
40
|
+
|
|
41
|
+
html, body {
|
|
42
|
+
height: 100%;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
background: var(--crust);
|
|
45
|
+
color: var(--text);
|
|
46
|
+
font-family: var(--font-mono);
|
|
47
|
+
font-size: 13px;
|
|
48
|
+
-webkit-font-smoothing: antialiased;
|
|
49
|
+
-moz-osx-font-smoothing: grayscale;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
|
|
53
|
+
::selection { background: var(--surface1); }
|
|
54
|
+
|
|
55
|
+
::-webkit-scrollbar { width: 9px; height: 9px; }
|
|
56
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
57
|
+
::-webkit-scrollbar-thumb { background: var(--surface1); border-radius: 5px; border: 2px solid transparent; background-clip: content-box; }
|
|
58
|
+
::-webkit-scrollbar-thumb:hover { background: var(--surface2); background-clip: content-box; }
|
|
59
|
+
|
|
60
|
+
#app { display: flex; flex-direction: column; height: 100%; }
|
|
61
|
+
|
|
62
|
+
/* ── Top bar ──────────────────────────────────────────────────────────── */
|
|
63
|
+
/* Ten controls live here and there is no room to spare. The bar wraps rather
|
|
64
|
+
than scrolls: a scrollable top bar would clip the dropdowns that hang out of
|
|
65
|
+
it, since overflow-x also clips vertically. */
|
|
66
|
+
#topbar {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-wrap: wrap;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: 7px 9px;
|
|
71
|
+
min-height: 46px;
|
|
72
|
+
flex: 0 0 auto;
|
|
73
|
+
padding: 7px 12px;
|
|
74
|
+
background: var(--mantle);
|
|
75
|
+
border-bottom: 1px solid var(--surface0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* The right-hand controls wrap as a block and stay right-aligned, so a narrow
|
|
79
|
+
window never leaves "present" stranded at the far left of a second row. */
|
|
80
|
+
#topbar-right {
|
|
81
|
+
display: flex;
|
|
82
|
+
flex: 1 1 auto;
|
|
83
|
+
flex-wrap: wrap;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: flex-end;
|
|
86
|
+
gap: 7px 9px;
|
|
87
|
+
min-width: 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Before it wraps, shed what is least load-bearing: the keyboard hints, then
|
|
91
|
+
the two buttons whose action the command palette also carries. */
|
|
92
|
+
#font-label {
|
|
93
|
+
display: inline-block;
|
|
94
|
+
max-width: 136px;
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
text-overflow: ellipsis;
|
|
97
|
+
white-space: nowrap;
|
|
98
|
+
vertical-align: bottom;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@media (max-width: 1460px) {
|
|
102
|
+
.btn kbd { display: none; }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@media (max-width: 1300px) {
|
|
106
|
+
#font-label { max-width: 76px; }
|
|
107
|
+
#docname { width: 130px; }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@media (max-width: 1140px) {
|
|
111
|
+
#btn-new { display: none; }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@media (max-width: 1040px) {
|
|
115
|
+
#btn-guide { display: none; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#brand {
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
gap: 8px;
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
letter-spacing: 0.01em;
|
|
124
|
+
white-space: nowrap;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#brand .caret {
|
|
128
|
+
width: 8px;
|
|
129
|
+
height: 15px;
|
|
130
|
+
background: var(--accent);
|
|
131
|
+
animation: blink 1.1s step-start infinite;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
|
135
|
+
|
|
136
|
+
#docname {
|
|
137
|
+
width: 190px;
|
|
138
|
+
padding: 5px 8px;
|
|
139
|
+
font: inherit;
|
|
140
|
+
color: var(--subtext1);
|
|
141
|
+
background: transparent;
|
|
142
|
+
border: 1px solid transparent;
|
|
143
|
+
border-radius: 6px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#docname:hover { border-color: var(--surface0); }
|
|
147
|
+
#docname:focus { outline: none; border-color: var(--accent); color: var(--text); }
|
|
148
|
+
|
|
149
|
+
/* Both of these sat in the top bar and were the loudest things in it. They
|
|
150
|
+
belong with the other counters, at the bottom. */
|
|
151
|
+
#chip-slides { white-space: nowrap; }
|
|
152
|
+
|
|
153
|
+
#save-state { white-space: nowrap; color: var(--overlay1); }
|
|
154
|
+
#save-state:empty { display: none; }
|
|
155
|
+
#save-state.ok { color: var(--green); }
|
|
156
|
+
#save-state.warn { color: var(--yellow); }
|
|
157
|
+
#save-state.err { color: var(--red); }
|
|
158
|
+
|
|
159
|
+
.spacer { flex: 1 1 auto; }
|
|
160
|
+
|
|
161
|
+
.btn {
|
|
162
|
+
display: inline-flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
gap: 7px;
|
|
165
|
+
height: 30px;
|
|
166
|
+
padding: 0 11px;
|
|
167
|
+
font-size: 12px;
|
|
168
|
+
color: var(--subtext1);
|
|
169
|
+
border: 1px solid var(--surface0);
|
|
170
|
+
border-radius: 7px;
|
|
171
|
+
white-space: nowrap;
|
|
172
|
+
transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.btn:hover { border-color: var(--accent); color: var(--text); background: var(--accent-soft); }
|
|
176
|
+
.btn kbd { font: inherit; font-size: 10px; color: var(--overlay0); }
|
|
177
|
+
|
|
178
|
+
/* The type size, as a segment rather than a menu: four options is few enough
|
|
179
|
+
that hiding them behind a click would cost more than the width it saves. */
|
|
180
|
+
#topbar-size { flex: 0 0 auto; height: 30px; align-items: stretch; }
|
|
181
|
+
#topbar-size .sz-btn {
|
|
182
|
+
min-width: 0;
|
|
183
|
+
padding: 0 9px;
|
|
184
|
+
border: 0;
|
|
185
|
+
border-radius: 0;
|
|
186
|
+
display: inline-flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
color: var(--overlay1);
|
|
189
|
+
}
|
|
190
|
+
#topbar-size .sz-btn:hover { background: var(--accent-soft); color: var(--text); }
|
|
191
|
+
#topbar-size .sz-btn.is-on { background: var(--surface0); color: var(--accent); }
|
|
192
|
+
|
|
193
|
+
/* ── Font menu ────────────────────────────────────────────────────────── */
|
|
194
|
+
.menu__pop--font { min-width: 452px; padding: 0; }
|
|
195
|
+
|
|
196
|
+
#ff-cols { display: flex; }
|
|
197
|
+
.ff-col { flex: 1 1 0; min-width: 0; padding: 7px; }
|
|
198
|
+
.ff-col + .ff-col { border-left: 1px solid var(--surface0); }
|
|
199
|
+
|
|
200
|
+
.ff-col__title {
|
|
201
|
+
padding: 4px 8px 7px;
|
|
202
|
+
font-size: 9px;
|
|
203
|
+
letter-spacing: 0.16em;
|
|
204
|
+
text-transform: uppercase;
|
|
205
|
+
color: var(--overlay0);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.ff-list { max-height: 58vh; overflow-y: auto; }
|
|
209
|
+
|
|
210
|
+
.ff-row {
|
|
211
|
+
display: block;
|
|
212
|
+
width: 100%;
|
|
213
|
+
padding: 5px 8px;
|
|
214
|
+
border-radius: 6px;
|
|
215
|
+
border-left: 2px solid transparent;
|
|
216
|
+
text-align: left;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.ff-row:hover { background: var(--surface-soft); border-left-color: var(--accent); }
|
|
220
|
+
.ff-row.is-on { border-left-color: var(--accent); }
|
|
221
|
+
.ff-row.is-on .ff-row__name { color: var(--accent); }
|
|
222
|
+
|
|
223
|
+
/* Each row is set in the face it offers, which is the only honest preview. */
|
|
224
|
+
.ff-row__name {
|
|
225
|
+
display: block;
|
|
226
|
+
font-size: 13.5px;
|
|
227
|
+
line-height: 1.35;
|
|
228
|
+
color: var(--text);
|
|
229
|
+
white-space: nowrap;
|
|
230
|
+
overflow: hidden;
|
|
231
|
+
text-overflow: ellipsis;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.ff-row--auto .ff-row__name { font-family: var(--font-mono); font-size: 12px; color: var(--subtext0); }
|
|
235
|
+
|
|
236
|
+
.ff-foot {
|
|
237
|
+
padding: 8px 14px;
|
|
238
|
+
border-top: 1px solid var(--surface0);
|
|
239
|
+
font-size: 10px;
|
|
240
|
+
color: var(--overlay0);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* The swatch on the theme button, so the current palette is visible at rest. */
|
|
244
|
+
.th-dot {
|
|
245
|
+
width: 9px;
|
|
246
|
+
height: 9px;
|
|
247
|
+
border-radius: 3px;
|
|
248
|
+
flex: 0 0 auto;
|
|
249
|
+
background: var(--gradient);
|
|
250
|
+
transform: rotate(45deg);
|
|
251
|
+
}
|
|
252
|
+
.btn--icon { padding: 0 9px; }
|
|
253
|
+
|
|
254
|
+
.btn--primary {
|
|
255
|
+
color: var(--crust);
|
|
256
|
+
font-weight: 600;
|
|
257
|
+
border-color: transparent;
|
|
258
|
+
background: var(--gradient);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* The gradient has to be restated: .btn:hover is a class plus a pseudo-class,
|
|
262
|
+
so its faint tint outranks the plain .btn--primary background and the button
|
|
263
|
+
would drop to near-black text on a barely-there surface. */
|
|
264
|
+
.btn--primary:hover {
|
|
265
|
+
background: var(--gradient);
|
|
266
|
+
filter: brightness(1.12);
|
|
267
|
+
border-color: transparent;
|
|
268
|
+
color: var(--crust);
|
|
269
|
+
}
|
|
270
|
+
.btn--primary kbd { color: var(--crust); opacity: 0.7; }
|
|
271
|
+
|
|
272
|
+
/* ── Panes ────────────────────────────────────────────────────────────── */
|
|
273
|
+
#panes { flex: 1 1 auto; display: flex; min-height: 0; }
|
|
274
|
+
|
|
275
|
+
#pane-edit {
|
|
276
|
+
position: relative;
|
|
277
|
+
display: flex;
|
|
278
|
+
flex-direction: column;
|
|
279
|
+
min-width: 240px;
|
|
280
|
+
background: var(--mantle);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#divider { flex: 0 0 7px; position: relative; cursor: col-resize; background: var(--crust); }
|
|
284
|
+
#divider::after {
|
|
285
|
+
content: '';
|
|
286
|
+
position: absolute;
|
|
287
|
+
inset: 0 3px;
|
|
288
|
+
background: var(--surface0);
|
|
289
|
+
transition: background 0.15s ease;
|
|
290
|
+
}
|
|
291
|
+
#divider:hover::after, #divider.is-dragging::after { background: var(--accent); }
|
|
292
|
+
|
|
293
|
+
#pane-prev {
|
|
294
|
+
flex: 1 1 auto;
|
|
295
|
+
display: flex;
|
|
296
|
+
flex-direction: column;
|
|
297
|
+
min-width: 260px;
|
|
298
|
+
background: var(--crust);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* ── Editor surface ───────────────────────────────────────────────────── */
|
|
302
|
+
#edit-wrap { position: relative; flex: 1 1 auto; min-height: 0; }
|
|
303
|
+
|
|
304
|
+
#hl, #src, #measure {
|
|
305
|
+
position: absolute;
|
|
306
|
+
inset: 0;
|
|
307
|
+
margin: 0;
|
|
308
|
+
padding: 18px 20px 45vh 20px;
|
|
309
|
+
font-family: var(--font-mono);
|
|
310
|
+
font-size: 13.5px;
|
|
311
|
+
font-weight: 400;
|
|
312
|
+
line-height: 1.75;
|
|
313
|
+
letter-spacing: 0;
|
|
314
|
+
white-space: pre-wrap;
|
|
315
|
+
overflow-wrap: break-word;
|
|
316
|
+
word-break: break-word;
|
|
317
|
+
tab-size: 2;
|
|
318
|
+
border: 0;
|
|
319
|
+
background: transparent;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
#hl { z-index: 1; overflow: hidden; pointer-events: none; color: var(--subtext0); counter-reset: sld 1; }
|
|
323
|
+
#measure { z-index: 0; visibility: hidden; overflow: hidden; }
|
|
324
|
+
|
|
325
|
+
#src {
|
|
326
|
+
z-index: 2;
|
|
327
|
+
overflow-y: auto;
|
|
328
|
+
overflow-x: hidden;
|
|
329
|
+
color: transparent;
|
|
330
|
+
caret-color: var(--accent);
|
|
331
|
+
resize: none;
|
|
332
|
+
outline: none;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#src::selection { background: var(--surface1); color: transparent; }
|
|
336
|
+
|
|
337
|
+
/* Markdown tokens */
|
|
338
|
+
.t-h1 { color: var(--accent); font-weight: 700; }
|
|
339
|
+
.t-h2 { color: var(--accent-2); font-weight: 600; }
|
|
340
|
+
.t-h3 { color: var(--accent-3); font-weight: 500; }
|
|
341
|
+
.t-h4 { color: var(--teal); }
|
|
342
|
+
.t-strong { color: var(--peach); font-weight: 600; }
|
|
343
|
+
.t-em { color: var(--subtext1); font-style: italic; }
|
|
344
|
+
.t-code { color: var(--green); }
|
|
345
|
+
.t-fence { color: var(--overlay1); }
|
|
346
|
+
.t-codeline { color: var(--subtext1); }
|
|
347
|
+
.t-quote { color: var(--subtext0); font-style: italic; }
|
|
348
|
+
.t-marker { color: var(--accent); }
|
|
349
|
+
.t-link { color: var(--blue); }
|
|
350
|
+
.t-url { color: var(--overlay0); }
|
|
351
|
+
.t-img { color: var(--sapphire); }
|
|
352
|
+
.t-dir { color: var(--yellow); font-weight: 600; }
|
|
353
|
+
.t-note { color: var(--overlay0); font-style: italic; }
|
|
354
|
+
.t-html { color: var(--pink); }
|
|
355
|
+
.t-table { color: var(--lavender); }
|
|
356
|
+
|
|
357
|
+
.t-sep { position: relative; color: var(--accent); counter-increment: sld; }
|
|
358
|
+
.t-sep::after {
|
|
359
|
+
content: 'slide ' counter(sld);
|
|
360
|
+
position: absolute;
|
|
361
|
+
left: calc(100% + 14px);
|
|
362
|
+
top: 0;
|
|
363
|
+
white-space: nowrap;
|
|
364
|
+
color: var(--overlay0);
|
|
365
|
+
font-size: 11px;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* ── Drop target ──────────────────────────────────────────────────────── */
|
|
369
|
+
#pane-edit.is-dropping::after {
|
|
370
|
+
content: 'drop a Markdown or HTML file to open it';
|
|
371
|
+
position: absolute;
|
|
372
|
+
inset: 10px;
|
|
373
|
+
z-index: 9;
|
|
374
|
+
display: flex;
|
|
375
|
+
align-items: center;
|
|
376
|
+
justify-content: center;
|
|
377
|
+
border: 2px dashed var(--accent);
|
|
378
|
+
border-radius: 12px;
|
|
379
|
+
background: var(--accent-soft);
|
|
380
|
+
color: var(--accent);
|
|
381
|
+
letter-spacing: 0.05em;
|
|
382
|
+
pointer-events: none;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* ── Contextual nudge ─────────────────────────────────────────────────── */
|
|
386
|
+
#nudge {
|
|
387
|
+
position: absolute;
|
|
388
|
+
left: 14px;
|
|
389
|
+
right: 14px;
|
|
390
|
+
bottom: 14px;
|
|
391
|
+
z-index: 7;
|
|
392
|
+
display: flex;
|
|
393
|
+
align-items: flex-start;
|
|
394
|
+
gap: 12px;
|
|
395
|
+
padding: 11px 13px;
|
|
396
|
+
background: var(--base);
|
|
397
|
+
border: 1px solid var(--surface1);
|
|
398
|
+
border-left: 2px solid var(--accent);
|
|
399
|
+
border-radius: 9px;
|
|
400
|
+
box-shadow: var(--shadow-md);
|
|
401
|
+
opacity: 0;
|
|
402
|
+
transform: translateY(10px);
|
|
403
|
+
pointer-events: none;
|
|
404
|
+
transition: opacity 0.22s ease, transform 0.22s ease;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
#nudge.is-on { opacity: 1; transform: translateY(0); pointer-events: all; }
|
|
408
|
+
#nudge__body { flex: 1 1 auto; }
|
|
409
|
+
#nudge__label {
|
|
410
|
+
font-size: 10px;
|
|
411
|
+
letter-spacing: 0.14em;
|
|
412
|
+
text-transform: uppercase;
|
|
413
|
+
color: var(--accent);
|
|
414
|
+
margin-bottom: 4px;
|
|
415
|
+
}
|
|
416
|
+
#nudge__text { font-size: 12.5px; color: var(--subtext1); line-height: 1.6; }
|
|
417
|
+
#nudge__text code {
|
|
418
|
+
font: inherit;
|
|
419
|
+
color: var(--green);
|
|
420
|
+
background: var(--surface-soft);
|
|
421
|
+
border-radius: 4px;
|
|
422
|
+
padding: 0.05em 0.3em;
|
|
423
|
+
}
|
|
424
|
+
#nudge__acts { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
|
|
425
|
+
.nudge-btn {
|
|
426
|
+
font-size: 11px;
|
|
427
|
+
color: var(--subtext0);
|
|
428
|
+
border: 1px solid var(--surface1);
|
|
429
|
+
border-radius: 6px;
|
|
430
|
+
padding: 4px 8px;
|
|
431
|
+
}
|
|
432
|
+
.nudge-btn:hover { border-color: var(--accent); color: var(--text); }
|
|
433
|
+
.nudge-btn--x { padding: 4px 7px; color: var(--overlay0); }
|
|
434
|
+
|
|
435
|
+
/* ── Preview ──────────────────────────────────────────────────────────── */
|
|
436
|
+
#prev-head {
|
|
437
|
+
display: flex;
|
|
438
|
+
align-items: center;
|
|
439
|
+
gap: 10px;
|
|
440
|
+
height: 34px;
|
|
441
|
+
flex: 0 0 34px;
|
|
442
|
+
padding: 0 12px;
|
|
443
|
+
border-bottom: 1px solid var(--surface0);
|
|
444
|
+
font-size: 11px;
|
|
445
|
+
color: var(--overlay1);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* ── Dropdown menu ────────────────────────────────────────────────────── */
|
|
449
|
+
.menu { position: relative; display: inline-flex; }
|
|
450
|
+
.menu__chev { font-size: 9px; color: var(--overlay0); }
|
|
451
|
+
.menu.is-open > .btn { border-color: var(--accent); color: var(--text); background: var(--accent-soft); }
|
|
452
|
+
|
|
453
|
+
.menu__pop {
|
|
454
|
+
position: absolute;
|
|
455
|
+
top: calc(100% + 6px);
|
|
456
|
+
right: 0;
|
|
457
|
+
z-index: 45;
|
|
458
|
+
min-width: 316px;
|
|
459
|
+
padding: 5px;
|
|
460
|
+
display: none;
|
|
461
|
+
flex-direction: column;
|
|
462
|
+
background: var(--mantle);
|
|
463
|
+
border: 1px solid var(--surface1);
|
|
464
|
+
border-radius: 10px;
|
|
465
|
+
box-shadow: var(--shadow-lg);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.menu.is-open .menu__pop { display: flex; }
|
|
469
|
+
|
|
470
|
+
.menu__pop button {
|
|
471
|
+
display: flex;
|
|
472
|
+
align-items: center;
|
|
473
|
+
gap: 10px;
|
|
474
|
+
padding: 8px 10px;
|
|
475
|
+
border-radius: 7px;
|
|
476
|
+
border-left: 2px solid transparent;
|
|
477
|
+
text-align: left;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.menu__pop button:hover, .menu__pop button.is-sel {
|
|
481
|
+
background: var(--surface-soft);
|
|
482
|
+
border-left-color: var(--accent);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* A fixed name column keeps the three rows on one grid. */
|
|
486
|
+
.menu__name { flex: 0 0 74px; font-size: 12.5px; color: var(--text); }
|
|
487
|
+
.menu__hint { flex: 1 1 auto; font-size: 11px; color: var(--overlay1); white-space: nowrap; }
|
|
488
|
+
.menu__pop kbd {
|
|
489
|
+
flex: 0 0 auto;
|
|
490
|
+
margin-left: auto;
|
|
491
|
+
font: inherit;
|
|
492
|
+
font-size: 10px;
|
|
493
|
+
color: var(--overlay0);
|
|
494
|
+
white-space: nowrap;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.seg { display: flex; border: 1px solid var(--surface0); border-radius: 7px; overflow: hidden; }
|
|
498
|
+
.seg button { font-size: 11px; padding: 3px 10px; color: var(--overlay1); }
|
|
499
|
+
.seg button.is-on { background: var(--surface0); color: var(--text); }
|
|
500
|
+
|
|
501
|
+
.step { font-size: 13px; color: var(--overlay1); padding: 0 5px; }
|
|
502
|
+
.step:hover { color: var(--text); }
|
|
503
|
+
.step[disabled] { opacity: 0.3; cursor: default; }
|
|
504
|
+
|
|
505
|
+
#stage {
|
|
506
|
+
position: relative;
|
|
507
|
+
flex: 1 1 auto;
|
|
508
|
+
min-height: 0;
|
|
509
|
+
display: flex;
|
|
510
|
+
align-items: center;
|
|
511
|
+
justify-content: center;
|
|
512
|
+
padding: 16px;
|
|
513
|
+
overflow: hidden;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
#frame-box { position: relative; }
|
|
517
|
+
|
|
518
|
+
#frame {
|
|
519
|
+
position: absolute;
|
|
520
|
+
top: 0;
|
|
521
|
+
left: 0;
|
|
522
|
+
width: ${PREVIEW_WIDTH}px;
|
|
523
|
+
height: ${PREVIEW_HEIGHT}px;
|
|
524
|
+
border: 0;
|
|
525
|
+
transform-origin: top left;
|
|
526
|
+
background: var(--base);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
#frame-box.is-single {
|
|
530
|
+
border-radius: 10px;
|
|
531
|
+
overflow: hidden;
|
|
532
|
+
border: 1px solid var(--surface0);
|
|
533
|
+
box-shadow: var(--shadow-lg);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
#notes {
|
|
537
|
+
flex: 0 0 auto;
|
|
538
|
+
max-height: 24%;
|
|
539
|
+
overflow: auto;
|
|
540
|
+
padding: 9px 12px 11px;
|
|
541
|
+
border-top: 1px solid var(--surface0);
|
|
542
|
+
background: var(--mantle);
|
|
543
|
+
display: none;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
#notes.is-on { display: block; }
|
|
547
|
+
#notes__label {
|
|
548
|
+
font-size: 10px;
|
|
549
|
+
letter-spacing: 0.14em;
|
|
550
|
+
text-transform: uppercase;
|
|
551
|
+
color: var(--overlay0);
|
|
552
|
+
margin-bottom: 4px;
|
|
553
|
+
}
|
|
554
|
+
#notes__text { font-size: 12.5px; line-height: 1.65; color: var(--subtext0); white-space: pre-wrap; }
|
|
555
|
+
|
|
556
|
+
/* ── Status bar ───────────────────────────────────────────────────────── */
|
|
557
|
+
#statusbar {
|
|
558
|
+
display: flex;
|
|
559
|
+
align-items: center;
|
|
560
|
+
gap: 12px;
|
|
561
|
+
height: 28px;
|
|
562
|
+
flex: 0 0 28px;
|
|
563
|
+
padding: 0 12px;
|
|
564
|
+
background: var(--mantle);
|
|
565
|
+
border-top: 1px solid var(--surface0);
|
|
566
|
+
font-size: 11px;
|
|
567
|
+
color: var(--overlay0);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
#statusbar > span { flex: 0 0 auto; }
|
|
571
|
+
#tipbar { flex: 1 1 auto !important; display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
572
|
+
#tipbar .tag {
|
|
573
|
+
flex: 0 0 auto;
|
|
574
|
+
color: var(--accent);
|
|
575
|
+
letter-spacing: 0.14em;
|
|
576
|
+
text-transform: uppercase;
|
|
577
|
+
font-size: 9px;
|
|
578
|
+
}
|
|
579
|
+
#tip-text { color: var(--subtext0); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
580
|
+
#tipbar button { color: var(--overlay0); padding: 0 3px; }
|
|
581
|
+
#tipbar button:hover { color: var(--text); }
|
|
582
|
+
|
|
583
|
+
/* ── Command palette ──────────────────────────────────────────────────── */
|
|
584
|
+
#palette, #guide, #library { position: fixed; inset: 0; z-index: 40; display: none; }
|
|
585
|
+
#palette.is-on, #guide.is-on, #library.is-on { display: block; }
|
|
586
|
+
.backdrop { position: absolute; inset: 0; background: var(--scrim); backdrop-filter: blur(3px); }
|
|
587
|
+
|
|
588
|
+
#pal-box, #lib-box {
|
|
589
|
+
position: relative;
|
|
590
|
+
width: min(690px, 92vw);
|
|
591
|
+
margin: 9vh auto 0;
|
|
592
|
+
background: var(--mantle);
|
|
593
|
+
border: 1px solid var(--surface1);
|
|
594
|
+
border-radius: 14px;
|
|
595
|
+
box-shadow: var(--shadow-lg);
|
|
596
|
+
overflow: hidden;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
#pal-input, #lib-head {
|
|
600
|
+
width: 100%;
|
|
601
|
+
padding: 15px 18px;
|
|
602
|
+
font: inherit;
|
|
603
|
+
font-size: 14px;
|
|
604
|
+
color: var(--text);
|
|
605
|
+
background: transparent;
|
|
606
|
+
border: 0;
|
|
607
|
+
border-bottom: 1px solid var(--surface0);
|
|
608
|
+
outline: none;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
#pal-input::placeholder { color: var(--overlay0); }
|
|
612
|
+
#pal-list, #lib-list { max-height: 54vh; overflow-y: auto; padding: 6px; }
|
|
613
|
+
|
|
614
|
+
#lib-head {
|
|
615
|
+
display: flex;
|
|
616
|
+
align-items: center;
|
|
617
|
+
gap: 10px;
|
|
618
|
+
font-size: 13px;
|
|
619
|
+
color: var(--text);
|
|
620
|
+
}
|
|
621
|
+
#lib-head span { flex: 1 1 auto; }
|
|
622
|
+
#lib-head button {
|
|
623
|
+
font-size: 11px;
|
|
624
|
+
color: var(--subtext0);
|
|
625
|
+
border: 1px solid var(--surface1);
|
|
626
|
+
border-radius: 6px;
|
|
627
|
+
padding: 4px 9px;
|
|
628
|
+
}
|
|
629
|
+
#lib-head button:hover { border-color: var(--accent); color: var(--text); background: var(--accent-soft); }
|
|
630
|
+
|
|
631
|
+
.lib-row {
|
|
632
|
+
display: flex;
|
|
633
|
+
align-items: center;
|
|
634
|
+
gap: 12px;
|
|
635
|
+
padding: 9px 12px;
|
|
636
|
+
border-radius: 8px;
|
|
637
|
+
border-left: 2px solid transparent;
|
|
638
|
+
cursor: pointer;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.lib-row.is-sel { background: var(--surface-soft); border-left-color: var(--accent); }
|
|
642
|
+
.lib-row.is-current .lib-row__name { color: var(--accent-2); }
|
|
643
|
+
.lib-row__main { flex: 1 1 auto; min-width: 0; }
|
|
644
|
+
.lib-row__name {
|
|
645
|
+
color: var(--text);
|
|
646
|
+
font-size: 12.5px;
|
|
647
|
+
overflow: hidden;
|
|
648
|
+
text-overflow: ellipsis;
|
|
649
|
+
white-space: nowrap;
|
|
650
|
+
}
|
|
651
|
+
.lib-row__meta { color: var(--overlay1); font-size: 11px; margin-top: 2px; }
|
|
652
|
+
.lib-row__acts { flex: 0 0 auto; display: flex; gap: 5px; opacity: 0; transition: opacity 0.12s ease; }
|
|
653
|
+
.lib-row:hover .lib-row__acts, .lib-row.is-sel .lib-row__acts { opacity: 1; }
|
|
654
|
+
.lib-row__acts button {
|
|
655
|
+
font-size: 10px;
|
|
656
|
+
color: var(--overlay1);
|
|
657
|
+
border: 1px solid var(--surface1);
|
|
658
|
+
border-radius: 5px;
|
|
659
|
+
padding: 3px 7px;
|
|
660
|
+
}
|
|
661
|
+
.lib-row__acts button:hover { border-color: var(--accent); color: var(--text); }
|
|
662
|
+
.lib-row__acts button.danger:hover { border-color: var(--red); color: var(--red); }
|
|
663
|
+
|
|
664
|
+
.pal-group {
|
|
665
|
+
padding: 11px 12px 5px;
|
|
666
|
+
font-size: 9px;
|
|
667
|
+
letter-spacing: 0.16em;
|
|
668
|
+
text-transform: uppercase;
|
|
669
|
+
color: var(--overlay0);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.pal-row {
|
|
673
|
+
display: flex;
|
|
674
|
+
align-items: center;
|
|
675
|
+
gap: 12px;
|
|
676
|
+
padding: 8px 12px;
|
|
677
|
+
border-radius: 8px;
|
|
678
|
+
border-left: 2px solid transparent;
|
|
679
|
+
cursor: pointer;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.pal-row.is-sel { background: var(--surface-soft); border-left-color: var(--accent); }
|
|
683
|
+
.pal-row__main { flex: 1 1 auto; min-width: 0; }
|
|
684
|
+
.pal-row__label { color: var(--text); font-size: 12.5px; }
|
|
685
|
+
.pal-row__hint { color: var(--overlay1); font-size: 11px; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
686
|
+
.pal-row__syntax {
|
|
687
|
+
flex: 0 0 auto;
|
|
688
|
+
max-width: 210px;
|
|
689
|
+
overflow: hidden;
|
|
690
|
+
text-overflow: ellipsis;
|
|
691
|
+
white-space: nowrap;
|
|
692
|
+
font-size: 11px;
|
|
693
|
+
color: var(--green);
|
|
694
|
+
background: var(--surface-soft);
|
|
695
|
+
border-radius: 5px;
|
|
696
|
+
padding: 3px 7px;
|
|
697
|
+
}
|
|
698
|
+
.pal-row__keys { flex: 0 0 auto; font-size: 10px; color: var(--overlay0); }
|
|
699
|
+
#pal-foot {
|
|
700
|
+
display: flex;
|
|
701
|
+
gap: 14px;
|
|
702
|
+
padding: 8px 14px;
|
|
703
|
+
border-top: 1px solid var(--surface0);
|
|
704
|
+
font-size: 10px;
|
|
705
|
+
color: var(--overlay0);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/* ── Guide drawer ─────────────────────────────────────────────────────── */
|
|
709
|
+
#guide-panel {
|
|
710
|
+
position: absolute;
|
|
711
|
+
top: 0;
|
|
712
|
+
right: 0;
|
|
713
|
+
bottom: 0;
|
|
714
|
+
width: min(540px, 94vw);
|
|
715
|
+
display: flex;
|
|
716
|
+
flex-direction: column;
|
|
717
|
+
background: var(--mantle);
|
|
718
|
+
border-left: 1px solid var(--surface1);
|
|
719
|
+
box-shadow: -22px 0 70px var(--scrim);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
#guide-head {
|
|
723
|
+
display: flex;
|
|
724
|
+
align-items: center;
|
|
725
|
+
gap: 10px;
|
|
726
|
+
padding: 14px 16px;
|
|
727
|
+
border-bottom: 1px solid var(--surface0);
|
|
728
|
+
}
|
|
729
|
+
#guide-head h2 { font-size: 14px; font-weight: 600; color: var(--text); }
|
|
730
|
+
#guide-head p { font-size: 11px; color: var(--overlay1); margin-top: 3px; }
|
|
731
|
+
#guide-body { flex: 1 1 auto; overflow-y: auto; padding: 14px 16px 30px; }
|
|
732
|
+
|
|
733
|
+
.gsec { margin-bottom: 22px; }
|
|
734
|
+
.gsec > h3 {
|
|
735
|
+
font-size: 9px;
|
|
736
|
+
letter-spacing: 0.18em;
|
|
737
|
+
text-transform: uppercase;
|
|
738
|
+
color: var(--accent);
|
|
739
|
+
margin-bottom: 9px;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.gcard {
|
|
743
|
+
border: 1px solid var(--surface0);
|
|
744
|
+
border-radius: 10px;
|
|
745
|
+
padding: 11px 12px;
|
|
746
|
+
margin-bottom: 8px;
|
|
747
|
+
background: var(--base);
|
|
748
|
+
transition: border-color 0.15s ease;
|
|
749
|
+
}
|
|
750
|
+
.gcard:hover { border-color: var(--surface2); }
|
|
751
|
+
.gcard__top { display: flex; align-items: baseline; gap: 10px; }
|
|
752
|
+
.gcard__label { flex: 1 1 auto; color: var(--text); font-size: 12.5px; }
|
|
753
|
+
.gcard__keys { font-size: 10px; color: var(--overlay0); }
|
|
754
|
+
.gcard__hint { font-size: 11.5px; color: var(--overlay1); line-height: 1.6; margin-top: 4px; }
|
|
755
|
+
.gcard__syntax {
|
|
756
|
+
margin-top: 8px;
|
|
757
|
+
font-size: 11px;
|
|
758
|
+
color: var(--green);
|
|
759
|
+
background: var(--crust);
|
|
760
|
+
border: 1px solid var(--surface0);
|
|
761
|
+
border-radius: 6px;
|
|
762
|
+
padding: 7px 9px;
|
|
763
|
+
overflow-x: auto;
|
|
764
|
+
white-space: pre;
|
|
765
|
+
}
|
|
766
|
+
.gcard__ins {
|
|
767
|
+
margin-top: 8px;
|
|
768
|
+
font-size: 11px;
|
|
769
|
+
color: var(--subtext0);
|
|
770
|
+
border: 1px solid var(--surface1);
|
|
771
|
+
border-radius: 6px;
|
|
772
|
+
padding: 4px 9px;
|
|
773
|
+
}
|
|
774
|
+
.gcard__ins:hover { border-color: var(--accent); color: var(--text); background: var(--accent-soft); }
|
|
775
|
+
|
|
776
|
+
/* ── Theme picker ─────────────────────────────────────────────────────── */
|
|
777
|
+
#themes { position: fixed; inset: 0; z-index: 44; display: none; }
|
|
778
|
+
#themes.is-on { display: block; }
|
|
779
|
+
|
|
780
|
+
#th-box {
|
|
781
|
+
position: relative;
|
|
782
|
+
width: min(1120px, 95vw);
|
|
783
|
+
margin: 5vh auto 0;
|
|
784
|
+
background: var(--mantle);
|
|
785
|
+
border: 1px solid var(--surface1);
|
|
786
|
+
border-radius: 16px;
|
|
787
|
+
box-shadow: var(--shadow-lg);
|
|
788
|
+
overflow: hidden;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
#th-head {
|
|
792
|
+
display: flex;
|
|
793
|
+
align-items: center;
|
|
794
|
+
gap: 12px;
|
|
795
|
+
padding: 14px 18px;
|
|
796
|
+
border-bottom: 1px solid var(--surface0);
|
|
797
|
+
font-size: 13px;
|
|
798
|
+
color: var(--text);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
#th-head .th-head__sub { flex: 1 1 auto; font-size: 11px; color: var(--overlay1); }
|
|
802
|
+
#th-head .th-head__sub kbd {
|
|
803
|
+
font: inherit;
|
|
804
|
+
font-size: 10px;
|
|
805
|
+
color: var(--subtext0);
|
|
806
|
+
border: 1px solid var(--surface1);
|
|
807
|
+
border-radius: 3px;
|
|
808
|
+
padding: 0 3px;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/* ── Type size ────────────────────────────────────────────────────────── */
|
|
812
|
+
#th-size {
|
|
813
|
+
display: flex;
|
|
814
|
+
align-items: center;
|
|
815
|
+
gap: 10px;
|
|
816
|
+
padding: 11px 18px;
|
|
817
|
+
border-bottom: 1px solid var(--surface0);
|
|
818
|
+
background: var(--surface-soft);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
#th-size__label {
|
|
822
|
+
flex: 0 0 auto;
|
|
823
|
+
font-size: 9px;
|
|
824
|
+
letter-spacing: 0.16em;
|
|
825
|
+
text-transform: uppercase;
|
|
826
|
+
color: var(--overlay0);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
#th-size__blurb {
|
|
830
|
+
flex: 1 1 auto;
|
|
831
|
+
min-width: 0;
|
|
832
|
+
font-size: 11px;
|
|
833
|
+
color: var(--overlay1);
|
|
834
|
+
overflow: hidden;
|
|
835
|
+
text-overflow: ellipsis;
|
|
836
|
+
white-space: nowrap;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
#th-size__seg { flex: 0 0 auto; display: flex; gap: 5px; }
|
|
840
|
+
|
|
841
|
+
#th-size__keys { flex: 0 0 auto; display: flex; gap: 3px; }
|
|
842
|
+
#th-size__keys kbd {
|
|
843
|
+
font: inherit;
|
|
844
|
+
font-size: 10px;
|
|
845
|
+
color: var(--overlay0);
|
|
846
|
+
border: 1px solid var(--surface1);
|
|
847
|
+
border-radius: 3px;
|
|
848
|
+
padding: 0 3px;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.sz-btn {
|
|
852
|
+
min-width: 40px;
|
|
853
|
+
padding: 5px 9px;
|
|
854
|
+
border: 1px solid var(--surface1);
|
|
855
|
+
border-radius: 7px;
|
|
856
|
+
font-size: 11px;
|
|
857
|
+
color: var(--subtext0);
|
|
858
|
+
text-align: center;
|
|
859
|
+
transition: border-color 0.14s ease, color 0.14s ease, background 0.14s ease;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
.sz-btn:hover { border-color: var(--accent); color: var(--text); background: var(--accent-soft); }
|
|
863
|
+
|
|
864
|
+
.sz-btn.is-on {
|
|
865
|
+
border-color: var(--accent);
|
|
866
|
+
color: var(--accent);
|
|
867
|
+
background: var(--accent-soft);
|
|
868
|
+
font-weight: 600;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/* Each button is set at the ratio it stands for, so the row is its own key. */
|
|
872
|
+
.sz-btn__glyph { font-family: var(--font-display); letter-spacing: -0.02em; }
|
|
873
|
+
#th-head button {
|
|
874
|
+
font-size: 11px;
|
|
875
|
+
color: var(--subtext0);
|
|
876
|
+
border: 1px solid var(--surface1);
|
|
877
|
+
border-radius: 6px;
|
|
878
|
+
padding: 4px 9px;
|
|
879
|
+
}
|
|
880
|
+
#th-head button:hover { border-color: var(--accent); color: var(--text); background: var(--accent-soft); }
|
|
881
|
+
|
|
882
|
+
#th-list { max-height: 76vh; overflow-y: auto; padding: 14px 16px 18px; }
|
|
883
|
+
|
|
884
|
+
.th-group {
|
|
885
|
+
padding: 4px 2px 8px;
|
|
886
|
+
font-size: 9px;
|
|
887
|
+
letter-spacing: 0.16em;
|
|
888
|
+
text-transform: uppercase;
|
|
889
|
+
color: var(--overlay0);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.th-grid {
|
|
893
|
+
display: grid;
|
|
894
|
+
grid-template-columns: repeat(auto-fill, minmax(212px, 1fr));
|
|
895
|
+
gap: 12px;
|
|
896
|
+
margin-bottom: 18px;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
.th-card {
|
|
900
|
+
display: flex;
|
|
901
|
+
flex-direction: column;
|
|
902
|
+
gap: 0;
|
|
903
|
+
padding: 0;
|
|
904
|
+
overflow: hidden;
|
|
905
|
+
border: 1px solid var(--surface1);
|
|
906
|
+
border-radius: 12px;
|
|
907
|
+
background: var(--base);
|
|
908
|
+
cursor: pointer;
|
|
909
|
+
text-align: left;
|
|
910
|
+
transition: border-color 0.14s ease, transform 0.14s ease, box-shadow 0.14s ease;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.th-card:hover, .th-card.is-sel {
|
|
914
|
+
border-color: var(--accent);
|
|
915
|
+
transform: translateY(-2px);
|
|
916
|
+
box-shadow: var(--shadow-md);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.th-card.is-current { box-shadow: inset 0 0 0 1px var(--accent); }
|
|
920
|
+
|
|
921
|
+
/* The thumbnail is a real slide in miniature: the theme's own background,
|
|
922
|
+
its own accent, its own faces. Nothing about it is a stand-in. */
|
|
923
|
+
.th-thumb {
|
|
924
|
+
position: relative;
|
|
925
|
+
aspect-ratio: 16 / 9;
|
|
926
|
+
padding: 13px 14px;
|
|
927
|
+
overflow: hidden;
|
|
928
|
+
border-bottom: 1px solid var(--surface0);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
.th-thumb__glow {
|
|
932
|
+
position: absolute;
|
|
933
|
+
width: 150%;
|
|
934
|
+
height: 150%;
|
|
935
|
+
right: -55%;
|
|
936
|
+
top: -60%;
|
|
937
|
+
border-radius: 50%;
|
|
938
|
+
pointer-events: none;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
.th-thumb__title {
|
|
942
|
+
position: relative;
|
|
943
|
+
font-size: 15px;
|
|
944
|
+
line-height: 1.1;
|
|
945
|
+
margin-bottom: 6px;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
.th-thumb__rule { position: relative; width: 34px; height: 2px; border-radius: 2px; margin-bottom: 9px; }
|
|
949
|
+
.th-thumb__line { position: relative; height: 3px; border-radius: 2px; margin-bottom: 5px; }
|
|
950
|
+
.th-thumb__code {
|
|
951
|
+
position: relative;
|
|
952
|
+
margin-top: 8px;
|
|
953
|
+
border-radius: 4px;
|
|
954
|
+
padding: 4px 6px;
|
|
955
|
+
font-size: 8px;
|
|
956
|
+
letter-spacing: 0;
|
|
957
|
+
white-space: nowrap;
|
|
958
|
+
overflow: hidden;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.th-meta { padding: 9px 12px 11px; }
|
|
962
|
+
.th-meta__top { display: flex; align-items: baseline; gap: 7px; }
|
|
963
|
+
.th-meta__name { flex: 1 1 auto; font-size: 12.5px; color: var(--text); }
|
|
964
|
+
.th-meta__mood {
|
|
965
|
+
flex: 0 0 auto;
|
|
966
|
+
font-size: 9px;
|
|
967
|
+
letter-spacing: 0.12em;
|
|
968
|
+
text-transform: uppercase;
|
|
969
|
+
color: var(--overlay0);
|
|
970
|
+
}
|
|
971
|
+
.th-meta__blurb {
|
|
972
|
+
font-size: 10.5px;
|
|
973
|
+
line-height: 1.55;
|
|
974
|
+
color: var(--overlay1);
|
|
975
|
+
margin-top: 4px;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/* ── Toasts ───────────────────────────────────────────────────────────── */
|
|
979
|
+
#toasts {
|
|
980
|
+
position: fixed;
|
|
981
|
+
right: 16px;
|
|
982
|
+
bottom: 42px;
|
|
983
|
+
z-index: 60;
|
|
984
|
+
display: flex;
|
|
985
|
+
flex-direction: column;
|
|
986
|
+
align-items: flex-end;
|
|
987
|
+
gap: 8px;
|
|
988
|
+
pointer-events: none;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
.toast {
|
|
992
|
+
display: flex;
|
|
993
|
+
align-items: center;
|
|
994
|
+
gap: 10px;
|
|
995
|
+
max-width: 380px;
|
|
996
|
+
padding: 9px 12px;
|
|
997
|
+
font-size: 12px;
|
|
998
|
+
color: var(--subtext1);
|
|
999
|
+
background: var(--base);
|
|
1000
|
+
border: 1px solid var(--surface1);
|
|
1001
|
+
border-left: 2px solid var(--teal);
|
|
1002
|
+
border-radius: 9px;
|
|
1003
|
+
box-shadow: var(--shadow-md);
|
|
1004
|
+
pointer-events: all;
|
|
1005
|
+
animation: rise 0.18s ease;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.toast--warn { border-left-color: var(--yellow); }
|
|
1009
|
+
.toast--err { border-left-color: var(--red); }
|
|
1010
|
+
.toast button {
|
|
1011
|
+
font-size: 11px;
|
|
1012
|
+
color: var(--subtext0);
|
|
1013
|
+
border: 1px solid var(--surface1);
|
|
1014
|
+
border-radius: 6px;
|
|
1015
|
+
padding: 3px 8px;
|
|
1016
|
+
}
|
|
1017
|
+
.toast button:hover { border-color: var(--accent); color: var(--text); }
|
|
1018
|
+
|
|
1019
|
+
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
|
1020
|
+
|
|
1021
|
+
/* ── HTML doc source / preview ───────────────────────────────────────────
|
|
1022
|
+
These live inside the same #pane-edit / #pane-prev panes the Markdown
|
|
1023
|
+
editor uses, so the divider drag-resize keeps working for free — only the
|
|
1024
|
+
inner content swaps, via [data-doc-kind]. */
|
|
1025
|
+
#src-html {
|
|
1026
|
+
display: none;
|
|
1027
|
+
position: absolute;
|
|
1028
|
+
inset: 0;
|
|
1029
|
+
margin: 0;
|
|
1030
|
+
padding: 18px 20px;
|
|
1031
|
+
font-family: var(--font-mono);
|
|
1032
|
+
font-size: 13.5px;
|
|
1033
|
+
line-height: 1.75;
|
|
1034
|
+
color: var(--text);
|
|
1035
|
+
caret-color: var(--accent);
|
|
1036
|
+
background: transparent;
|
|
1037
|
+
border: 0;
|
|
1038
|
+
outline: none;
|
|
1039
|
+
resize: none;
|
|
1040
|
+
white-space: pre;
|
|
1041
|
+
overflow: auto;
|
|
1042
|
+
tab-size: 2;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
#frame-html {
|
|
1046
|
+
display: none;
|
|
1047
|
+
flex: 1 1 auto;
|
|
1048
|
+
min-height: 0;
|
|
1049
|
+
width: 100%;
|
|
1050
|
+
border: 0;
|
|
1051
|
+
background: #fff;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
[data-doc-kind="html"] #edit-wrap,
|
|
1055
|
+
[data-doc-kind="html"] #nudge,
|
|
1056
|
+
[data-doc-kind="html"] #prev-head,
|
|
1057
|
+
[data-doc-kind="html"] #stage,
|
|
1058
|
+
[data-doc-kind="html"] #notes {
|
|
1059
|
+
display: none !important;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
[data-doc-kind="html"] #src-html,
|
|
1063
|
+
[data-doc-kind="html"] #frame-html {
|
|
1064
|
+
display: block;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
[data-doc-kind="html"] #btn-guide,
|
|
1068
|
+
[data-doc-kind="html"] #btn-palette {
|
|
1069
|
+
display: none !important;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/* ── Start screen ─────────────────────────────────────────────────────── */
|
|
1073
|
+
#screen-start { position: fixed; inset: 0; z-index: 55; display: none; }
|
|
1074
|
+
#screen-start.is-on { display: block; }
|
|
1075
|
+
|
|
1076
|
+
#start-box {
|
|
1077
|
+
position: relative;
|
|
1078
|
+
width: min(680px, 92vw);
|
|
1079
|
+
margin: 12vh auto 0;
|
|
1080
|
+
padding: 26px 28px 22px;
|
|
1081
|
+
background: var(--mantle);
|
|
1082
|
+
border: 1px solid var(--surface1);
|
|
1083
|
+
border-radius: 14px;
|
|
1084
|
+
box-shadow: var(--shadow-lg);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
#start-head { margin-bottom: 18px; }
|
|
1088
|
+
#start-head #brand { font-size: 15px; }
|
|
1089
|
+
#start-head p { margin-top: 8px; font-size: 12.5px; color: var(--overlay1); }
|
|
1090
|
+
|
|
1091
|
+
#start-cards { display: flex; flex-direction: column; gap: 10px; }
|
|
1092
|
+
|
|
1093
|
+
.start-card {
|
|
1094
|
+
display: block;
|
|
1095
|
+
width: 100%;
|
|
1096
|
+
text-align: left;
|
|
1097
|
+
padding: 14px 16px;
|
|
1098
|
+
background: var(--base);
|
|
1099
|
+
border: 1px solid var(--surface0);
|
|
1100
|
+
border-radius: 10px;
|
|
1101
|
+
transition: border-color 0.15s ease, background 0.15s ease;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
.start-card:hover { border-color: var(--accent); background: var(--accent-soft); }
|
|
1105
|
+
.start-card.is-disabled { opacity: 0.45; pointer-events: none; }
|
|
1106
|
+
|
|
1107
|
+
.start-card__title { display: block; font-size: 13px; font-weight: 600; color: var(--text); }
|
|
1108
|
+
.start-card__desc { display: block; margin-top: 4px; font-size: 11.5px; color: var(--overlay1); }
|
|
1109
|
+
|
|
1110
|
+
.start-card__acts { display: flex; gap: 8px; margin-top: 10px; }
|
|
1111
|
+
.start-card__acts button {
|
|
1112
|
+
padding: 5px 10px;
|
|
1113
|
+
font-size: 11.5px;
|
|
1114
|
+
color: var(--subtext1);
|
|
1115
|
+
border: 1px solid var(--surface1);
|
|
1116
|
+
border-radius: 6px;
|
|
1117
|
+
flex: none;
|
|
1118
|
+
}
|
|
1119
|
+
.start-card__acts button:hover { border-color: var(--accent); color: var(--text); }
|
|
1120
|
+
|
|
1121
|
+
.start-card__url { display: flex; gap: 6px; flex: 1 1 auto; min-width: 0; }
|
|
1122
|
+
.start-card__url input {
|
|
1123
|
+
flex: 1 1 auto;
|
|
1124
|
+
min-width: 0;
|
|
1125
|
+
padding: 5px 8px;
|
|
1126
|
+
font-size: 11.5px;
|
|
1127
|
+
font-family: inherit;
|
|
1128
|
+
color: var(--text);
|
|
1129
|
+
background: var(--mantle);
|
|
1130
|
+
border: 1px solid var(--surface1);
|
|
1131
|
+
border-radius: 6px;
|
|
1132
|
+
}
|
|
1133
|
+
.start-card__url input:focus { border-color: var(--accent); outline: none; }
|
|
1134
|
+
.start-card__url button { flex: none; }
|
|
1135
|
+
|
|
1136
|
+
/* ── Narrow screens ───────────────────────────────────────────────────── */
|
|
1137
|
+
@media (max-width: 900px) {
|
|
1138
|
+
#panes { flex-direction: column; }
|
|
1139
|
+
#pane-edit { flex: 1 1 50%; width: auto !important; min-height: 0; }
|
|
1140
|
+
#pane-prev { flex: 1 1 50%; min-height: 0; }
|
|
1141
|
+
#divider { display: none; }
|
|
1142
|
+
}
|
|
1143
|
+
</style>
|
|
1144
|
+
</head>
|
|
1145
|
+
<body>
|
|
1146
|
+
<div id="app">
|
|
1147
|
+
<header id="topbar">
|
|
1148
|
+
<span id="brand">deckrun<span class="caret"></span></span>
|
|
1149
|
+
<input id="docname" value="deck" spellcheck="false" title="Deck name, also the download filename">
|
|
1150
|
+
<button class="btn" id="btn-decks" title="Switch between the decks in this browser">decks <span id="deck-count">1</span> <kbd>Cmd O</kbd></button>
|
|
1151
|
+
<span class="spacer"></span>
|
|
1152
|
+
<span id="topbar-right">
|
|
1153
|
+
<button class="btn" id="btn-guide" title="Everything you can put on a slide">guide <kbd>Cmd /</kbd></button>
|
|
1154
|
+
<button class="btn" id="btn-palette" title="Insert anything">insert <kbd>Cmd K</kbd></button>
|
|
1155
|
+
<button class="btn" id="btn-new" title="Start a new Markdown deck or HTML doc">new</button>
|
|
1156
|
+
<span class="menu">
|
|
1157
|
+
<button class="btn" id="btn-export" aria-haspopup="true" aria-expanded="false">export <span class="menu__chev">▾</span></button>
|
|
1158
|
+
<div class="menu__pop" id="export-menu">
|
|
1159
|
+
<button data-export="md">
|
|
1160
|
+
<span class="menu__name">Markdown</span>
|
|
1161
|
+
<span class="menu__hint">a plain .md file</span>
|
|
1162
|
+
<kbd>Cmd S</kbd>
|
|
1163
|
+
</button>
|
|
1164
|
+
<button data-export="pdf">
|
|
1165
|
+
<span class="menu__name">PDF</span>
|
|
1166
|
+
<span class="menu__hint">16:9 pages, styling intact</span>
|
|
1167
|
+
<kbd>Cmd Shift S</kbd>
|
|
1168
|
+
</button>
|
|
1169
|
+
<button data-export="html">
|
|
1170
|
+
<span class="menu__name">HTML</span>
|
|
1171
|
+
<span class="menu__hint">one self-contained page</span>
|
|
1172
|
+
<kbd></kbd>
|
|
1173
|
+
</button>
|
|
1174
|
+
</div>
|
|
1175
|
+
</span>
|
|
1176
|
+
<button class="btn" id="btn-theme" title="Pick a theme">
|
|
1177
|
+
<span class="th-dot" id="theme-dot"></span>
|
|
1178
|
+
<span id="theme-label">theme</span>
|
|
1179
|
+
<kbd>Cmd Shift L</kbd>
|
|
1180
|
+
</button>
|
|
1181
|
+
<span class="menu">
|
|
1182
|
+
<button class="btn" id="btn-font" aria-haspopup="true" aria-expanded="false" title="Heading and body faces">
|
|
1183
|
+
<span id="font-label">font</span> <span class="menu__chev">▾</span>
|
|
1184
|
+
</button>
|
|
1185
|
+
<div class="menu__pop menu__pop--font" id="font-menu">
|
|
1186
|
+
<div id="ff-cols">
|
|
1187
|
+
<div class="ff-col">
|
|
1188
|
+
<div class="ff-col__title">heading & title</div>
|
|
1189
|
+
<div class="ff-list" id="ff-head"></div>
|
|
1190
|
+
</div>
|
|
1191
|
+
<div class="ff-col">
|
|
1192
|
+
<div class="ff-col__title">body</div>
|
|
1193
|
+
<div class="ff-list" id="ff-body"></div>
|
|
1194
|
+
</div>
|
|
1195
|
+
</div>
|
|
1196
|
+
<div class="ff-foot">Code keeps the theme's monospace face.</div>
|
|
1197
|
+
</div>
|
|
1198
|
+
</span>
|
|
1199
|
+
<span class="seg" id="topbar-size" title="Type size"></span>
|
|
1200
|
+
<button class="btn btn--primary" id="btn-present" title="Open the real deck in a new tab">present <kbd>Cmd Enter</kbd></button>
|
|
1201
|
+
</span>
|
|
1202
|
+
</header>
|
|
1203
|
+
|
|
1204
|
+
<main id="panes">
|
|
1205
|
+
<section id="pane-edit">
|
|
1206
|
+
<div id="edit-wrap">
|
|
1207
|
+
<pre id="hl" aria-hidden="true"></pre>
|
|
1208
|
+
<pre id="measure" aria-hidden="true"></pre>
|
|
1209
|
+
<textarea id="src" spellcheck="false" autocomplete="off" autocapitalize="off" wrap="soft" aria-label="Markdown source"></textarea>
|
|
1210
|
+
</div>
|
|
1211
|
+
<div id="nudge">
|
|
1212
|
+
<div id="nudge__body">
|
|
1213
|
+
<div id="nudge__label">try this</div>
|
|
1214
|
+
<div id="nudge__text"></div>
|
|
1215
|
+
</div>
|
|
1216
|
+
<div id="nudge__acts">
|
|
1217
|
+
<button class="nudge-btn" id="nudge-do">insert</button>
|
|
1218
|
+
<button class="nudge-btn nudge-btn--x" id="nudge-x" title="Dismiss">×</button>
|
|
1219
|
+
</div>
|
|
1220
|
+
</div>
|
|
1221
|
+
<textarea id="src-html" spellcheck="false" autocomplete="off" autocapitalize="off" wrap="off" aria-label="HTML source"></textarea>
|
|
1222
|
+
</section>
|
|
1223
|
+
|
|
1224
|
+
<div id="divider" title="Drag to resize, double-click to reset"></div>
|
|
1225
|
+
|
|
1226
|
+
<section id="pane-prev">
|
|
1227
|
+
<div id="prev-head">
|
|
1228
|
+
<button class="step" id="btn-prev" title="Previous slide">←</button>
|
|
1229
|
+
<span id="prev-count">slide 0 / 0</span>
|
|
1230
|
+
<button class="step" id="btn-next" title="Next slide">→</button>
|
|
1231
|
+
<span class="spacer"></span>
|
|
1232
|
+
<span id="prev-scale"></span>
|
|
1233
|
+
<div class="seg">
|
|
1234
|
+
<button id="seg-single" class="is-on">single</button>
|
|
1235
|
+
<button id="seg-grid">grid</button>
|
|
1236
|
+
</div>
|
|
1237
|
+
</div>
|
|
1238
|
+
<div id="stage">
|
|
1239
|
+
<div id="frame-box" class="is-single">
|
|
1240
|
+
<iframe id="frame" src="/__preview" title="Slide preview"></iframe>
|
|
1241
|
+
</div>
|
|
1242
|
+
</div>
|
|
1243
|
+
<div id="notes">
|
|
1244
|
+
<div id="notes__label">speaker notes</div>
|
|
1245
|
+
<div id="notes__text"></div>
|
|
1246
|
+
</div>
|
|
1247
|
+
<iframe id="frame-html" title="HTML doc preview"></iframe>
|
|
1248
|
+
</section>
|
|
1249
|
+
</main>
|
|
1250
|
+
|
|
1251
|
+
<footer id="statusbar">
|
|
1252
|
+
<span id="pos">Ln 1, Col 1</span>
|
|
1253
|
+
<span id="words">0 words</span>
|
|
1254
|
+
<span id="chip-slides">0 slides</span>
|
|
1255
|
+
<span id="save-state"></span>
|
|
1256
|
+
<span id="tipbar">
|
|
1257
|
+
<span class="tag">tip</span>
|
|
1258
|
+
<span id="tip-text"></span>
|
|
1259
|
+
<button id="tip-prev" title="Previous tip">‹</button>
|
|
1260
|
+
<button id="tip-next" title="Next tip">›</button>
|
|
1261
|
+
</span>
|
|
1262
|
+
<span>local only, nothing is uploaded</span>
|
|
1263
|
+
</footer>
|
|
1264
|
+
</div>
|
|
1265
|
+
|
|
1266
|
+
<div id="palette">
|
|
1267
|
+
<div class="backdrop" data-close="palette"></div>
|
|
1268
|
+
<div id="pal-box">
|
|
1269
|
+
<input id="pal-input" placeholder="Insert a style, layout, embed, or action" spellcheck="false" autocomplete="off">
|
|
1270
|
+
<div id="pal-list"></div>
|
|
1271
|
+
<div id="pal-foot">
|
|
1272
|
+
<span>↑↓ move</span><span>enter inserts</span><span>esc closes</span>
|
|
1273
|
+
</div>
|
|
1274
|
+
</div>
|
|
1275
|
+
</div>
|
|
1276
|
+
|
|
1277
|
+
<div id="guide">
|
|
1278
|
+
<div class="backdrop" data-close="guide"></div>
|
|
1279
|
+
<div id="guide-panel">
|
|
1280
|
+
<div id="guide-head">
|
|
1281
|
+
<div style="flex:1 1 auto">
|
|
1282
|
+
<h2>Everything you can put on a slide</h2>
|
|
1283
|
+
<p>Click insert on any card to drop it at your caret.</p>
|
|
1284
|
+
</div>
|
|
1285
|
+
<button class="nudge-btn nudge-btn--x" data-close="guide" title="Close">×</button>
|
|
1286
|
+
</div>
|
|
1287
|
+
<div id="guide-body"></div>
|
|
1288
|
+
</div>
|
|
1289
|
+
</div>
|
|
1290
|
+
|
|
1291
|
+
<div id="library">
|
|
1292
|
+
<div class="backdrop" data-close="library"></div>
|
|
1293
|
+
<div id="lib-box">
|
|
1294
|
+
<div id="lib-head">
|
|
1295
|
+
<span>Decks in this browser</span>
|
|
1296
|
+
<button id="lib-new">new deck</button>
|
|
1297
|
+
<button data-close="library">close</button>
|
|
1298
|
+
</div>
|
|
1299
|
+
<div id="lib-list"></div>
|
|
1300
|
+
<div id="pal-foot">
|
|
1301
|
+
<span>↑↓ move</span><span>enter opens</span><span>esc closes</span>
|
|
1302
|
+
</div>
|
|
1303
|
+
</div>
|
|
1304
|
+
</div>
|
|
1305
|
+
|
|
1306
|
+
<div id="themes">
|
|
1307
|
+
<div class="backdrop" data-close="themes"></div>
|
|
1308
|
+
<div id="th-box">
|
|
1309
|
+
<div id="th-head">
|
|
1310
|
+
<span>Themes</span>
|
|
1311
|
+
<span class="th-head__sub">Arrow keys preview a theme live · enter keeps it · esc puts it back</span>
|
|
1312
|
+
<button data-close="themes">close</button>
|
|
1313
|
+
</div>
|
|
1314
|
+
<div id="th-size">
|
|
1315
|
+
<span id="th-size__label">type size</span>
|
|
1316
|
+
<span id="th-size__seg"></span>
|
|
1317
|
+
<span id="th-size__keys"><kbd>[</kbd><kbd>]</kbd></span>
|
|
1318
|
+
<span id="th-size__blurb">Applies to every theme, and sticks right away.</span>
|
|
1319
|
+
</div>
|
|
1320
|
+
<div id="th-list"></div>
|
|
1321
|
+
</div>
|
|
1322
|
+
</div>
|
|
1323
|
+
|
|
1324
|
+
<div id="screen-start">
|
|
1325
|
+
<div class="backdrop"></div>
|
|
1326
|
+
<div id="start-box">
|
|
1327
|
+
<div id="start-head">
|
|
1328
|
+
<span id="brand">deckrun<span class="caret"></span></span>
|
|
1329
|
+
<p>What are you making?</p>
|
|
1330
|
+
</div>
|
|
1331
|
+
<div id="start-cards">
|
|
1332
|
+
<div class="start-card" id="start-md-card">
|
|
1333
|
+
<span class="start-card__title">Markdown deck</span>
|
|
1334
|
+
<span class="start-card__desc">Slides written in Markdown, presented one at a time.</span>
|
|
1335
|
+
<span class="start-card__acts">
|
|
1336
|
+
<button id="start-md-blank">start blank</button>
|
|
1337
|
+
<button id="start-md-upload">upload .md</button>
|
|
1338
|
+
</span>
|
|
1339
|
+
</div>
|
|
1340
|
+
<div class="start-card" id="start-html-card">
|
|
1341
|
+
<span class="start-card__title">HTML document</span>
|
|
1342
|
+
<span class="start-card__desc">One self-contained page, presented with the laser, pen, and canvas — no slide boundaries.</span>
|
|
1343
|
+
<span class="start-card__acts">
|
|
1344
|
+
<button id="start-html-upload">upload .html</button>
|
|
1345
|
+
<span class="start-card__url">
|
|
1346
|
+
<input type="url" id="start-html-url" placeholder="https://a-public-page.html" inputmode="url" autocomplete="off" spellcheck="false">
|
|
1347
|
+
<button id="start-html-url-go">load URL</button>
|
|
1348
|
+
</span>
|
|
1349
|
+
</span>
|
|
1350
|
+
</div>
|
|
1351
|
+
<button class="start-card" id="start-lib">
|
|
1352
|
+
<span class="start-card__title">Open from library</span>
|
|
1353
|
+
<span class="start-card__desc" id="start-lib-desc">Decks and docs already in this browser.</span>
|
|
1354
|
+
</button>
|
|
1355
|
+
</div>
|
|
1356
|
+
</div>
|
|
1357
|
+
</div>
|
|
1358
|
+
|
|
1359
|
+
<div id="toasts"></div>
|
|
1360
|
+
<input type="file" id="file-any" accept=".md,.markdown,text/markdown,text/plain,.html,.htm,text/html" hidden>
|
|
1361
|
+
|
|
1362
|
+
<script type="application/json" id="bootstrap">${bootstrapJson(theme, size, fonts)}</script>
|
|
1363
|
+
<script>
|
|
1364
|
+
(function () {
|
|
1365
|
+
'use strict';
|
|
1366
|
+
|
|
1367
|
+
var D = JSON.parse(document.getElementById('bootstrap').textContent);
|
|
1368
|
+
var MAC = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
1369
|
+
var CMD = MAC ? 'Cmd' : 'Ctrl';
|
|
1370
|
+
|
|
1371
|
+
var K = {
|
|
1372
|
+
index: 'deckrun.decks.v1',
|
|
1373
|
+
deck: 'deckrun.deck.',
|
|
1374
|
+
current: 'deckrun.current.v1',
|
|
1375
|
+
theme: 'deckrun.theme.v1',
|
|
1376
|
+
size: 'deckrun.size.v1',
|
|
1377
|
+
head: 'deckrun.font.head.v1',
|
|
1378
|
+
body: 'deckrun.font.body.v1',
|
|
1379
|
+
split: 'deckrun.split.v1',
|
|
1380
|
+
mode: 'deckrun.mode.v1',
|
|
1381
|
+
nudge: 'deckrun.nudges.v1',
|
|
1382
|
+
// Superseded by the deck library, read once to migrate.
|
|
1383
|
+
oldDoc: 'presentmd.doc.v1',
|
|
1384
|
+
oldName: 'presentmd.name.v1'
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
// present-md was renamed to deckrun — the whole storage namespace moves
|
|
1388
|
+
// with it, so this copies it over once rather than orphaning every deck
|
|
1389
|
+
// already saved under the old prefix.
|
|
1390
|
+
var OLD_NS = {
|
|
1391
|
+
index: 'presentmd.decks.v1', deck: 'presentmd.deck.', current: 'presentmd.current.v1',
|
|
1392
|
+
theme: 'presentmd.theme.v1', size: 'presentmd.size.v1', head: 'presentmd.font.head.v1',
|
|
1393
|
+
body: 'presentmd.font.body.v1', split: 'presentmd.split.v1', mode: 'presentmd.mode.v1',
|
|
1394
|
+
nudge: 'presentmd.nudges.v1'
|
|
1395
|
+
};
|
|
1396
|
+
(function migrateNamespace() {
|
|
1397
|
+
if (lsGet(K.index, null) !== null) return;
|
|
1398
|
+
var oldIndexRaw = lsGet(OLD_NS.index, null);
|
|
1399
|
+
if (oldIndexRaw === null) return;
|
|
1400
|
+
lsSet(K.index, oldIndexRaw);
|
|
1401
|
+
lsDel(OLD_NS.index);
|
|
1402
|
+
try {
|
|
1403
|
+
var oldIndex = JSON.parse(oldIndexRaw);
|
|
1404
|
+
if (Object.prototype.toString.call(oldIndex) === '[object Array]') {
|
|
1405
|
+
oldIndex.forEach(function (entry) {
|
|
1406
|
+
if (!entry || !entry.id) return;
|
|
1407
|
+
var content = lsGet(OLD_NS.deck + entry.id, null);
|
|
1408
|
+
if (content !== null) {
|
|
1409
|
+
lsSet(K.deck + entry.id, content);
|
|
1410
|
+
lsDel(OLD_NS.deck + entry.id);
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
} catch (e) {}
|
|
1415
|
+
['current', 'theme', 'size', 'head', 'body', 'split', 'mode', 'nudge'].forEach(function (slot) {
|
|
1416
|
+
var v = lsGet(OLD_NS[slot], null);
|
|
1417
|
+
if (v !== null) { lsSet(K[slot], v); lsDel(OLD_NS[slot]); }
|
|
1418
|
+
});
|
|
1419
|
+
})();
|
|
1420
|
+
|
|
1421
|
+
function lsGet(k, fallback) {
|
|
1422
|
+
try { var v = localStorage.getItem(k); return v === null ? fallback : v; }
|
|
1423
|
+
catch (e) { return fallback; }
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function lsSet(k, v) {
|
|
1427
|
+
try { localStorage.setItem(k, v); return true; }
|
|
1428
|
+
catch (e) { return false; }
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function lsDel(k) {
|
|
1432
|
+
try { localStorage.removeItem(k); } catch (e) {}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// ── Deck library ───────────────────────────────────────────────────────
|
|
1436
|
+
// The index holds metadata only, so listing decks never reads their text.
|
|
1437
|
+
// Each deck's Markdown lives under its own key.
|
|
1438
|
+
|
|
1439
|
+
function loadIndex() {
|
|
1440
|
+
try {
|
|
1441
|
+
var list = JSON.parse(lsGet(K.index, '[]'));
|
|
1442
|
+
return Object.prototype.toString.call(list) === '[object Array]' ? list : [];
|
|
1443
|
+
} catch (e) { return []; }
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function saveIndex(list) {
|
|
1447
|
+
return lsSet(K.index, JSON.stringify(list));
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function newDeckId() {
|
|
1451
|
+
return 'd' + Date.now().toString(36) + Math.floor(Math.random() * 1e6).toString(36);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function readDeck(id) { return lsGet(K.deck + id, ''); }
|
|
1455
|
+
|
|
1456
|
+
function findDeck(id) {
|
|
1457
|
+
var list = loadIndex();
|
|
1458
|
+
for (var i = 0; i < list.length; i++) if (list[i].id === id) return list[i];
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/** Returns the new deck's id, or null if this browser refused to store it. */
|
|
1463
|
+
function createDeck(name, content, kind) {
|
|
1464
|
+
var id = newDeckId();
|
|
1465
|
+
if (!lsSet(K.deck + id, content)) return null;
|
|
1466
|
+
var list = loadIndex();
|
|
1467
|
+
list.unshift({ id: id, name: name || 'untitled', kind: kind || 'markdown', slides: 0, chars: content.length, at: Date.now() });
|
|
1468
|
+
if (!saveIndex(list)) { lsDel(K.deck + id); return null; }
|
|
1469
|
+
return id;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function uniqueName(base) {
|
|
1473
|
+
var list = loadIndex();
|
|
1474
|
+
var taken = {};
|
|
1475
|
+
list.forEach(function (d) { taken[d.name] = true; });
|
|
1476
|
+
if (!taken[base]) return base;
|
|
1477
|
+
for (var n = 2; n < 500; n++) if (!taken[base + ' ' + n]) return base + ' ' + n;
|
|
1478
|
+
return base;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function timeAgo(ts) {
|
|
1482
|
+
var mins = Math.floor((Date.now() - ts) / 60000);
|
|
1483
|
+
if (mins < 1) return 'just now';
|
|
1484
|
+
if (mins < 60) return mins + 'm ago';
|
|
1485
|
+
var hours = Math.floor(mins / 60);
|
|
1486
|
+
if (hours < 24) return hours + 'h ago';
|
|
1487
|
+
var days = Math.floor(hours / 24);
|
|
1488
|
+
return days + 'd ago';
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/** One deck's metadata is refreshed from the editor on every save. */
|
|
1492
|
+
function touchCurrent() {
|
|
1493
|
+
var list = loadIndex();
|
|
1494
|
+
for (var i = 0; i < list.length; i++) {
|
|
1495
|
+
if (list[i].id === state.deckId) {
|
|
1496
|
+
list[i].name = $('docname').value.trim() || 'untitled';
|
|
1497
|
+
list[i].kind = state.kind;
|
|
1498
|
+
if (state.kind === 'html') {
|
|
1499
|
+
list[i].slides = 0;
|
|
1500
|
+
list[i].chars = srcHtml.value.length;
|
|
1501
|
+
} else {
|
|
1502
|
+
list[i].slides = state.slides.length;
|
|
1503
|
+
list[i].chars = src.value.length;
|
|
1504
|
+
}
|
|
1505
|
+
list[i].at = Date.now();
|
|
1506
|
+
break;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return saveIndex(list);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function updateDeckCount() {
|
|
1513
|
+
$('deck-count').textContent = String(loadIndex().length);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/** The open document's own text, whichever kind it is. */
|
|
1517
|
+
function curValue() { return state.kind === 'html' ? srcHtml.value : src.value; }
|
|
1518
|
+
|
|
1519
|
+
/** Relabels the export menu, since "Markdown" makes no sense for a doc. */
|
|
1520
|
+
function paintExportMenu() {
|
|
1521
|
+
var mdName = document.querySelector('#export-menu [data-export="md"] .menu__name');
|
|
1522
|
+
var mdHint = document.querySelector('#export-menu [data-export="md"] .menu__hint');
|
|
1523
|
+
var htmlName = document.querySelector('#export-menu [data-export="html"] .menu__name');
|
|
1524
|
+
var htmlHint = document.querySelector('#export-menu [data-export="html"] .menu__hint');
|
|
1525
|
+
if (state.kind === 'html') {
|
|
1526
|
+
mdName.textContent = 'Source';
|
|
1527
|
+
mdHint.textContent = 'the raw .html file';
|
|
1528
|
+
htmlName.textContent = 'Presenter Page';
|
|
1529
|
+
htmlHint.textContent = 'standalone, with the tool belt built in';
|
|
1530
|
+
} else {
|
|
1531
|
+
mdName.textContent = 'Markdown';
|
|
1532
|
+
mdHint.textContent = 'a plain .md file';
|
|
1533
|
+
htmlName.textContent = 'HTML';
|
|
1534
|
+
htmlHint.textContent = 'one self-contained page';
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/** Switches which document kind the chrome is dressed for. */
|
|
1539
|
+
function setDocKind(kind) {
|
|
1540
|
+
state.kind = kind === 'html' ? 'html' : 'markdown';
|
|
1541
|
+
document.documentElement.dataset.docKind = state.kind;
|
|
1542
|
+
paintExportMenu();
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// ── Themes ─────────────────────────────────────────────────────────────
|
|
1546
|
+
var THEMES = D.themes;
|
|
1547
|
+
var THEME_BY_ID = {};
|
|
1548
|
+
THEMES.forEach(function (t) { THEME_BY_ID[t.id] = t; });
|
|
1549
|
+
|
|
1550
|
+
/** A theme id we can actually apply, whatever localStorage happens to hold. */
|
|
1551
|
+
function pickTheme(id) {
|
|
1552
|
+
return THEME_BY_ID[id] ? id : (THEME_BY_ID[D.theme] ? D.theme : THEMES[0].id);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
var SIZES = D.sizes;
|
|
1556
|
+
var SIZE_BY_ID = {};
|
|
1557
|
+
SIZES.forEach(function (z) { SIZE_BY_ID[z.id] = z; });
|
|
1558
|
+
|
|
1559
|
+
function pickSize(id) {
|
|
1560
|
+
return SIZE_BY_ID[id] ? id : (SIZE_BY_ID[D.size] ? D.size : 'm');
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
var FACES = D.faces;
|
|
1564
|
+
var FACE_BY_ID = {};
|
|
1565
|
+
FACES.forEach(function (f) { FACE_BY_ID[f.id] = f; });
|
|
1566
|
+
|
|
1567
|
+
/** null means "whatever the theme says", which is not a face id. */
|
|
1568
|
+
function pickFont(id) {
|
|
1569
|
+
return FACE_BY_ID[id] ? id : null;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// ── Elements ───────────────────────────────────────────────────────────
|
|
1573
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
1574
|
+
var src = $('src'), hl = $('hl'), measure = $('measure');
|
|
1575
|
+
var frame = $('frame'), frameBox = $('frame-box'), stage = $('stage');
|
|
1576
|
+
var paneEdit = $('pane-edit'), panePrev = $('pane-prev'), divider = $('divider');
|
|
1577
|
+
var elChipSlides = $('chip-slides'), elSave = $('save-state'), elPos = $('pos'), elWords = $('words');
|
|
1578
|
+
var elCount = $('prev-count'), elScale = $('prev-scale');
|
|
1579
|
+
var elNotes = $('notes'), elNotesText = $('notes__text');
|
|
1580
|
+
var elNudge = $('nudge'), elNudgeText = $('nudge__text'), elNudgeDo = $('nudge-do'), elNudgeX = $('nudge-x');
|
|
1581
|
+
var palette = $('palette'), palInput = $('pal-input'), palList = $('pal-list');
|
|
1582
|
+
var guide = $('guide'), guideBody = $('guide-body');
|
|
1583
|
+
var srcHtml = $('src-html'), frameHtml = $('frame-html');
|
|
1584
|
+
|
|
1585
|
+
// ── State ──────────────────────────────────────────────────────────────
|
|
1586
|
+
var state = {
|
|
1587
|
+
deckId: null,
|
|
1588
|
+
kind: 'markdown',
|
|
1589
|
+
slides: [],
|
|
1590
|
+
notes: [],
|
|
1591
|
+
index: 0,
|
|
1592
|
+
mode: lsGet(K.mode, 'single') === 'grid' ? 'grid' : 'single',
|
|
1593
|
+
theme: pickTheme(lsGet(K.theme, D.theme)),
|
|
1594
|
+
size: pickSize(lsGet(K.size, D.size)),
|
|
1595
|
+
head: pickFont(lsGet(K.head, D.fonts.head)),
|
|
1596
|
+
body: pickFont(lsGet(K.body, D.fonts.body)),
|
|
1597
|
+
frameReady: false,
|
|
1598
|
+
overflow: {},
|
|
1599
|
+
dismissed: {},
|
|
1600
|
+
activeNudge: null,
|
|
1601
|
+
nudgeShownAt: 0,
|
|
1602
|
+
seq: 0,
|
|
1603
|
+
inflight: null,
|
|
1604
|
+
tip: Math.floor(Math.random() * D.tips.length)
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1607
|
+
try {
|
|
1608
|
+
var saved = JSON.parse(lsGet(K.nudge, '[]'));
|
|
1609
|
+
if (saved && saved.length) saved.forEach(function (id) { state.dismissed[id] = true; });
|
|
1610
|
+
} catch (e) {}
|
|
1611
|
+
|
|
1612
|
+
// ── Toasts ─────────────────────────────────────────────────────────────
|
|
1613
|
+
function toast(message, kind, actionLabel, action) {
|
|
1614
|
+
var el = document.createElement('div');
|
|
1615
|
+
el.className = 'toast' + (kind ? ' toast--' + kind : '');
|
|
1616
|
+
var text = document.createElement('span');
|
|
1617
|
+
text.style.flex = '1 1 auto';
|
|
1618
|
+
text.textContent = message;
|
|
1619
|
+
el.appendChild(text);
|
|
1620
|
+
if (actionLabel) {
|
|
1621
|
+
var btn = document.createElement('button');
|
|
1622
|
+
btn.textContent = actionLabel;
|
|
1623
|
+
btn.addEventListener('click', function () { action(); el.remove(); });
|
|
1624
|
+
el.appendChild(btn);
|
|
1625
|
+
}
|
|
1626
|
+
$('toasts').appendChild(el);
|
|
1627
|
+
setTimeout(function () {
|
|
1628
|
+
el.style.transition = 'opacity .3s ease';
|
|
1629
|
+
el.style.opacity = '0';
|
|
1630
|
+
setTimeout(function () { el.remove(); }, 320);
|
|
1631
|
+
}, actionLabel ? 8000 : 4200);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// ── Markdown highlighting for the editor surface ───────────────────────
|
|
1635
|
+
function esc(s) {
|
|
1636
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
var HOLD = '\\u0000';
|
|
1640
|
+
|
|
1641
|
+
function inlineTokens(line) {
|
|
1642
|
+
var held = [];
|
|
1643
|
+
function hold(html) { held.push(html); return HOLD + (held.length - 1) + HOLD; }
|
|
1644
|
+
|
|
1645
|
+
var out = esc(line);
|
|
1646
|
+
|
|
1647
|
+
// Images first: the title attribute carries the layout directive.
|
|
1648
|
+
out = out.replace(/!\\[([^\\]\\n]*)\\]\\(([^\\s)]*)(\\s+"[^"\\n]*")?\\)/g, function (m, alt, url, title) {
|
|
1649
|
+
var html = '<span class="t-img">![' + alt + ']</span><span class="t-url">(' + url;
|
|
1650
|
+
if (title) html += '</span><span class="t-dir">' + title + '</span><span class="t-url">';
|
|
1651
|
+
return hold(html + ')</span>');
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
// Links.
|
|
1655
|
+
out = out.replace(/\\[([^\\]\\n]*)\\]\\(([^\\s)]*)(\\s+"[^"\\n]*")?\\)/g, function (m, label, url, title) {
|
|
1656
|
+
return hold('<span class="t-link">[' + label + ']</span><span class="t-url">(' + url +
|
|
1657
|
+
(title ? '</span><span class="t-dir">' + title + '</span><span class="t-url">' : '') + ')</span>');
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
// Inline code.
|
|
1661
|
+
out = out.replace(/\`[^\`\\n]+\`/g, function (m) { return hold('<span class="t-code">' + m + '</span>'); });
|
|
1662
|
+
|
|
1663
|
+
// Raw HTML tags.
|
|
1664
|
+
out = out.replace(/<\\/?[a-zA-Z][^&]{0,200}?>/g, function (m) {
|
|
1665
|
+
return hold('<span class="t-html">' + m + '</span>');
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
out = out.replace(/\\*\\*[^*\\n]+\\*\\*/g, function (m) { return '<span class="t-strong">' + m + '</span>'; });
|
|
1669
|
+
out = out.replace(/(^|[^*\\w])(\\*[^*\\n]+\\*)/g, function (m, pre, body) {
|
|
1670
|
+
return pre + '<span class="t-em">' + body + '</span>';
|
|
1671
|
+
});
|
|
1672
|
+
out = out.replace(/(^|[^_\\w])(_[^_\\n]+_)/g, function (m, pre, body) {
|
|
1673
|
+
return pre + '<span class="t-em">' + body + '</span>';
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
return out.replace(new RegExp(HOLD + '(\\\\d+)' + HOLD, 'g'), function (m, i) { return held[+i]; });
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function highlightMarkdown(text) {
|
|
1680
|
+
var lines = text.split('\\n');
|
|
1681
|
+
var out = [];
|
|
1682
|
+
var inFence = false;
|
|
1683
|
+
|
|
1684
|
+
for (var i = 0; i < lines.length; i++) {
|
|
1685
|
+
var line = lines[i];
|
|
1686
|
+
|
|
1687
|
+
// A pasted data URI is one enormous line. Colour it in a single pass so
|
|
1688
|
+
// typing stays responsive.
|
|
1689
|
+
if (line.length > 4000) {
|
|
1690
|
+
out.push('<span class="t-url">' + esc(line) + '</span>');
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (/^\\s*\`\`\`/.test(line)) {
|
|
1695
|
+
out.push('<span class="t-fence">' + esc(line) + '</span>');
|
|
1696
|
+
inFence = !inFence;
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
if (inFence) { out.push('<span class="t-codeline">' + esc(line) + '</span>'); continue; }
|
|
1700
|
+
if (/^[ \\t]*---[ \\t]*$/.test(line)) {
|
|
1701
|
+
out.push('<span class="t-sep">' + esc(line) + '</span>');
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
if (/^\\s*<!--/.test(esc(line)) || /^\\s*<!--/.test(line)) {
|
|
1705
|
+
out.push('<span class="t-note">' + esc(line) + '</span>');
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
var head = line.match(/^(#{1,6})\\s/);
|
|
1710
|
+
if (head) {
|
|
1711
|
+
var lvl = Math.min(head[1].length, 4);
|
|
1712
|
+
out.push('<span class="t-h' + lvl + '">' + inlineTokens(line) + '</span>');
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
if (/^\\s*>/.test(esc(line))) {
|
|
1716
|
+
out.push('<span class="t-quote">' + inlineTokens(line) + '</span>');
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
if (/^\\s*\\|.*\\|\\s*$/.test(line)) {
|
|
1720
|
+
out.push('<span class="t-table">' + inlineTokens(line) + '</span>');
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
var marker = line.match(/^(\\s*)([-*+]|\\d+[.)])(\\s+)/);
|
|
1724
|
+
if (marker) {
|
|
1725
|
+
out.push(marker[1] + '<span class="t-marker">' + esc(marker[2]) + '</span>' + marker[3] +
|
|
1726
|
+
inlineTokens(line.slice(marker[0].length)));
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
out.push(inlineTokens(line));
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
return out.join('\\n') + '\\n';
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function paint() { hl.innerHTML = highlightMarkdown(src.value); }
|
|
1736
|
+
|
|
1737
|
+
function syncScroll() { hl.scrollTop = src.scrollTop; hl.scrollLeft = src.scrollLeft; }
|
|
1738
|
+
|
|
1739
|
+
// ── Slide map, mirroring the server-side split ─────────────────────────
|
|
1740
|
+
var SEP = /\\r?\\n[ \\t]*---[ \\t]*\\r?\\n/g;
|
|
1741
|
+
|
|
1742
|
+
function slideMap() {
|
|
1743
|
+
var text = src.value, spans = [], last = 0, m;
|
|
1744
|
+
SEP.lastIndex = 0;
|
|
1745
|
+
while ((m = SEP.exec(text)) !== null) {
|
|
1746
|
+
spans.push([last, m.index]);
|
|
1747
|
+
last = m.index + m[0].length;
|
|
1748
|
+
SEP.lastIndex = last;
|
|
1749
|
+
}
|
|
1750
|
+
spans.push([last, text.length]);
|
|
1751
|
+
return spans.filter(function (s) { return text.slice(s[0], s[1]).trim().length > 0; });
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function caretSlide() {
|
|
1755
|
+
var pos = src.selectionStart, spans = slideMap();
|
|
1756
|
+
for (var i = 0; i < spans.length; i++) {
|
|
1757
|
+
if (pos <= spans[i][1]) return i;
|
|
1758
|
+
}
|
|
1759
|
+
return Math.max(0, spans.length - 1);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function offsetTop(pos) {
|
|
1763
|
+
measure.textContent = src.value.slice(0, pos);
|
|
1764
|
+
var marker = document.createElement('span');
|
|
1765
|
+
marker.textContent = '\\u200b';
|
|
1766
|
+
measure.appendChild(marker);
|
|
1767
|
+
return marker.offsetTop;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function scrollCaretIntoView() {
|
|
1771
|
+
var top = offsetTop(src.selectionStart);
|
|
1772
|
+
var view = src.clientHeight;
|
|
1773
|
+
if (top < src.scrollTop + 40 || top > src.scrollTop + view - 80) {
|
|
1774
|
+
src.scrollTop = Math.max(0, top - view / 3);
|
|
1775
|
+
syncScroll();
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// ── Preview plumbing ───────────────────────────────────────────────────
|
|
1780
|
+
/** Dropped rather than queued until the frame is up: see the ready handler. */
|
|
1781
|
+
function post(msg) {
|
|
1782
|
+
if (!state.frameReady) return;
|
|
1783
|
+
frame.contentWindow.postMessage(msg, '*');
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function pushFrame() {
|
|
1787
|
+
post({ type: 'render', slides: state.slides, index: state.index, mode: state.mode });
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function layout() {
|
|
1791
|
+
var padding = 32;
|
|
1792
|
+
var w = Math.max(120, stage.clientWidth - padding);
|
|
1793
|
+
var h = Math.max(90, stage.clientHeight - padding);
|
|
1794
|
+
var scale;
|
|
1795
|
+
if (state.mode === 'single') {
|
|
1796
|
+
scale = Math.min(w / D.width, h / D.height);
|
|
1797
|
+
frame.style.height = D.height + 'px';
|
|
1798
|
+
frameBox.classList.add('is-single');
|
|
1799
|
+
frameBox.style.width = Math.round(D.width * scale) + 'px';
|
|
1800
|
+
frameBox.style.height = Math.round(D.height * scale) + 'px';
|
|
1801
|
+
} else {
|
|
1802
|
+
scale = w / D.width;
|
|
1803
|
+
var virtualH = Math.round(h / scale);
|
|
1804
|
+
frame.style.height = virtualH + 'px';
|
|
1805
|
+
frameBox.classList.remove('is-single');
|
|
1806
|
+
frameBox.style.width = Math.round(w) + 'px';
|
|
1807
|
+
frameBox.style.height = Math.round(h) + 'px';
|
|
1808
|
+
}
|
|
1809
|
+
frame.style.transform = 'scale(' + scale + ')';
|
|
1810
|
+
elScale.textContent = Math.round(scale * 100) + '%';
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function renderCounts() {
|
|
1814
|
+
var n = state.slides.length;
|
|
1815
|
+
elChipSlides.textContent = n + (n === 1 ? ' slide' : ' slides');
|
|
1816
|
+
elCount.textContent = 'slide ' + (n ? state.index + 1 : 0) + ' / ' + n;
|
|
1817
|
+
$('btn-prev').disabled = state.index <= 0;
|
|
1818
|
+
$('btn-next').disabled = state.index >= n - 1;
|
|
1819
|
+
|
|
1820
|
+
var note = state.notes[state.index];
|
|
1821
|
+
if (note) { elNotesText.textContent = note; elNotes.classList.add('is-on'); }
|
|
1822
|
+
else { elNotes.classList.remove('is-on'); }
|
|
1823
|
+
|
|
1824
|
+
var words = src.value.trim() ? src.value.trim().split(/\\s+/).length : 0;
|
|
1825
|
+
elWords.textContent = words + (words === 1 ? ' word' : ' words');
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function setIndex(i, moveCaret) {
|
|
1829
|
+
var n = state.slides.length;
|
|
1830
|
+
if (!n) return;
|
|
1831
|
+
i = Math.max(0, Math.min(n - 1, i));
|
|
1832
|
+
if (i === state.index && !moveCaret) return;
|
|
1833
|
+
state.index = i;
|
|
1834
|
+
if (moveCaret) {
|
|
1835
|
+
var spans = slideMap();
|
|
1836
|
+
if (spans[i]) {
|
|
1837
|
+
var target = spans[i][0];
|
|
1838
|
+
while (/\\s/.test(src.value.charAt(target)) && target < spans[i][1]) target++;
|
|
1839
|
+
src.focus();
|
|
1840
|
+
src.setSelectionRange(target, target);
|
|
1841
|
+
scrollCaretIntoView();
|
|
1842
|
+
updateCaretUi();
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
post({ type: 'index', index: state.index });
|
|
1846
|
+
renderCounts();
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
function updateCaretUi() {
|
|
1850
|
+
var before = src.value.slice(0, src.selectionStart);
|
|
1851
|
+
var lines = before.split('\\n');
|
|
1852
|
+
elPos.textContent = 'Ln ' + lines.length + ', Col ' + (lines[lines.length - 1].length + 1);
|
|
1853
|
+
var i = caretSlide();
|
|
1854
|
+
if (i !== state.index) {
|
|
1855
|
+
state.index = i;
|
|
1856
|
+
post({ type: 'index', index: i });
|
|
1857
|
+
renderCounts();
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// ── Parse round-trip: the server owns the Markdown, so what you see here
|
|
1862
|
+
// is byte-for-byte what deckrun file.md renders. ───────────────
|
|
1863
|
+
function refresh() {
|
|
1864
|
+
var mine = ++state.seq;
|
|
1865
|
+
if (state.inflight) state.inflight.abort();
|
|
1866
|
+
state.inflight = new AbortController();
|
|
1867
|
+
|
|
1868
|
+
fetch('/__parse', {
|
|
1869
|
+
method: 'POST',
|
|
1870
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
1871
|
+
body: src.value,
|
|
1872
|
+
signal: state.inflight.signal
|
|
1873
|
+
})
|
|
1874
|
+
.then(function (r) { return r.json(); })
|
|
1875
|
+
.then(function (data) {
|
|
1876
|
+
if (mine !== state.seq) return;
|
|
1877
|
+
state.slides = data.slides || [];
|
|
1878
|
+
state.notes = data.notes || [];
|
|
1879
|
+
if (state.index >= state.slides.length) state.index = Math.max(0, state.slides.length - 1);
|
|
1880
|
+
pushFrame();
|
|
1881
|
+
renderCounts();
|
|
1882
|
+
evalNudges();
|
|
1883
|
+
})
|
|
1884
|
+
.catch(function (err) {
|
|
1885
|
+
if (err && err.name === 'AbortError') return;
|
|
1886
|
+
toast('Preview failed: ' + err.message + '. Is the server still running?', 'err');
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// ── Autosave ───────────────────────────────────────────────────────────
|
|
1891
|
+
var saveTimer = null;
|
|
1892
|
+
|
|
1893
|
+
function saveNow() {
|
|
1894
|
+
var ok = state.deckId && lsSet(K.deck + state.deckId, curValue()) && touchCurrent();
|
|
1895
|
+
if (ok) {
|
|
1896
|
+
elSave.className = '';
|
|
1897
|
+
elSave.textContent = '';
|
|
1898
|
+
} else if (state.deckId) {
|
|
1899
|
+
elSave.className = 'err';
|
|
1900
|
+
elSave.textContent = 'not saved: storage full';
|
|
1901
|
+
toast('This browser is out of room. Download this deck, or delete one you no longer need.', 'err', 'download', download);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function scheduleSave() {
|
|
1906
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
1907
|
+
// Writing a megabyte of content costs real time. Back off on big docs.
|
|
1908
|
+
saveTimer = setTimeout(saveNow, curValue().length > 1024 * 1024 ? 1500 : 500);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// ── Text insertion, undo-safe ──────────────────────────────────────────
|
|
1912
|
+
function typeText(text) {
|
|
1913
|
+
src.focus();
|
|
1914
|
+
var inserted = false;
|
|
1915
|
+
try { inserted = document.execCommand('insertText', false, text); } catch (e) {}
|
|
1916
|
+
if (!inserted) {
|
|
1917
|
+
var s = src.selectionStart, e2 = src.selectionEnd;
|
|
1918
|
+
src.setRangeText(text, s, e2, 'end');
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
var MARK = '\\u0001';
|
|
1923
|
+
|
|
1924
|
+
function insertSnippet(sn) {
|
|
1925
|
+
if (!sn.insert) return;
|
|
1926
|
+
var s = src.selectionStart, e = src.selectionEnd;
|
|
1927
|
+
var sel = src.value.slice(s, e);
|
|
1928
|
+
|
|
1929
|
+
var text = sn.insert.split('{caret}').join(MARK);
|
|
1930
|
+
if (text.indexOf(MARK) === -1 && !sel) text = text.split('{sel}').join(MARK);
|
|
1931
|
+
text = text.split('{sel}').join(sel);
|
|
1932
|
+
|
|
1933
|
+
var prefix = '', suffix = '';
|
|
1934
|
+
if (sn.block) {
|
|
1935
|
+
var before = src.value.slice(0, s);
|
|
1936
|
+
if (before && !/\\n[ \\t]*$/.test(before)) prefix = '\\n\\n';
|
|
1937
|
+
else if (before && !/\\n[ \\t]*\\n[ \\t]*$/.test(before)) prefix = '\\n';
|
|
1938
|
+
var after = src.value.slice(e);
|
|
1939
|
+
if (after && !/^[ \\t]*\\n/.test(after)) suffix = '\\n';
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
var caretAt = text.indexOf(MARK);
|
|
1943
|
+
text = text.split(MARK).join('');
|
|
1944
|
+
var full = prefix + text + suffix;
|
|
1945
|
+
|
|
1946
|
+
typeText(full);
|
|
1947
|
+
var pos = s + prefix.length + (caretAt === -1 ? text.length : caretAt);
|
|
1948
|
+
src.setSelectionRange(pos, pos);
|
|
1949
|
+
onInput();
|
|
1950
|
+
scrollCaretIntoView();
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// ── Nudges: rules evaluated against the live document ──────────────────
|
|
1954
|
+
var NUDGE_RULES = [
|
|
1955
|
+
{
|
|
1956
|
+
id: 'overflow',
|
|
1957
|
+
sticky: true,
|
|
1958
|
+
test: function (ctx) {
|
|
1959
|
+
var i = state.index;
|
|
1960
|
+
return state.overflow[i] ? 'Slide ' + (i + 1) + ' overflows and is being clipped. Trim it, or split it with <code>---</code>.' : null;
|
|
1961
|
+
},
|
|
1962
|
+
snippet: 'slide-break'
|
|
1963
|
+
},
|
|
1964
|
+
{
|
|
1965
|
+
id: 'no-split',
|
|
1966
|
+
test: function (ctx) {
|
|
1967
|
+
return ctx.count === 1 && ctx.text.length > 420
|
|
1968
|
+
? 'This is one long slide. Three dashes on their own line start the next one.'
|
|
1969
|
+
: null;
|
|
1970
|
+
},
|
|
1971
|
+
snippet: 'slide-break'
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
id: 'untagged-fence',
|
|
1975
|
+
test: function (ctx) {
|
|
1976
|
+
return ctx.untaggedFence
|
|
1977
|
+
? 'That code block has no language tag. Write <code>```go</code> and it gets highlighted.'
|
|
1978
|
+
: null;
|
|
1979
|
+
},
|
|
1980
|
+
snippet: 'code-go'
|
|
1981
|
+
},
|
|
1982
|
+
{
|
|
1983
|
+
id: 'img-plain',
|
|
1984
|
+
test: function (ctx) {
|
|
1985
|
+
return ctx.plainImage
|
|
1986
|
+
? 'Your image is inline. Add <code>"right"</code> after the URL and the slide splits: text left, image right.'
|
|
1987
|
+
: null;
|
|
1988
|
+
},
|
|
1989
|
+
snippet: 'img-right'
|
|
1990
|
+
},
|
|
1991
|
+
{
|
|
1992
|
+
id: 'no-notes',
|
|
1993
|
+
test: function (ctx) {
|
|
1994
|
+
return ctx.count >= 3 && !ctx.hasNotes
|
|
1995
|
+
? 'Speaker notes live in <code><!-- notes: ... --></code>. They show under the preview and never reach the deck.'
|
|
1996
|
+
: null;
|
|
1997
|
+
},
|
|
1998
|
+
snippet: 'slide-notes'
|
|
1999
|
+
},
|
|
2000
|
+
{
|
|
2001
|
+
id: 'no-image',
|
|
2002
|
+
test: function (ctx) {
|
|
2003
|
+
return ctx.count >= 4 && !ctx.hasImage
|
|
2004
|
+
? 'All text so far. An image path resolves against the folder you launched in: <code></code>.'
|
|
2005
|
+
: null;
|
|
2006
|
+
},
|
|
2007
|
+
snippet: 'img-right'
|
|
2008
|
+
},
|
|
2009
|
+
{
|
|
2010
|
+
id: 'no-table',
|
|
2011
|
+
test: function (ctx) {
|
|
2012
|
+
return ctx.count >= 5 && !ctx.hasTable
|
|
2013
|
+
? 'Numbers land harder in a table than in bullets. Zebra rows and an accented header come for free.'
|
|
2014
|
+
: null;
|
|
2015
|
+
},
|
|
2016
|
+
snippet: 'table'
|
|
2017
|
+
},
|
|
2018
|
+
{
|
|
2019
|
+
id: 'no-embed',
|
|
2020
|
+
test: function (ctx) {
|
|
2021
|
+
return ctx.count >= 6 && !ctx.hasEmbed
|
|
2022
|
+
? 'Raw HTML works, so a YouTube or dashboard <code><iframe></code> can live on a slide, sized to 16:9.'
|
|
2023
|
+
: null;
|
|
2024
|
+
},
|
|
2025
|
+
snippet: 'embed-youtube'
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
id: 'dense',
|
|
2029
|
+
test: function (ctx) {
|
|
2030
|
+
return ctx.denseSlide !== -1
|
|
2031
|
+
? 'Slide ' + (ctx.denseSlide + 1) + ' is dense. Around seven bullets is the ceiling from the back row.'
|
|
2032
|
+
: null;
|
|
2033
|
+
},
|
|
2034
|
+
snippet: 'slide-break'
|
|
2035
|
+
},
|
|
2036
|
+
{
|
|
2037
|
+
id: 'huge',
|
|
2038
|
+
sticky: true,
|
|
2039
|
+
test: function (ctx) {
|
|
2040
|
+
return ctx.text.length > 3.5 * 1024 * 1024
|
|
2041
|
+
? 'This deck is over 3.5 MB, near what a browser will autosave. Download a copy.'
|
|
2042
|
+
: null;
|
|
2043
|
+
},
|
|
2044
|
+
action: 'download'
|
|
2045
|
+
}
|
|
2046
|
+
];
|
|
2047
|
+
|
|
2048
|
+
function docContext() {
|
|
2049
|
+
var text = src.value;
|
|
2050
|
+
var lines = text.split('\\n');
|
|
2051
|
+
var untagged = false, inFence = false;
|
|
2052
|
+
for (var i = 0; i < lines.length; i++) {
|
|
2053
|
+
var m = lines[i].match(/^\\s*\`\`\`(.*)$/);
|
|
2054
|
+
if (!m) continue;
|
|
2055
|
+
if (!inFence && !m[1].trim()) untagged = true;
|
|
2056
|
+
inFence = !inFence;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
var dense = -1;
|
|
2060
|
+
var spans = slideMap();
|
|
2061
|
+
for (var j = 0; j < spans.length; j++) {
|
|
2062
|
+
var body = text.slice(spans[j][0], spans[j][1]);
|
|
2063
|
+
if (body.indexOf('\`\`\`') !== -1) continue;
|
|
2064
|
+
var bullets = body.split('\\n').filter(function (l) { return /^\\s*([-*+]|\\d+[.)])\\s/.test(l); });
|
|
2065
|
+
if (bullets.length > 9) { dense = j; break; }
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
return {
|
|
2069
|
+
text: text,
|
|
2070
|
+
count: state.slides.length,
|
|
2071
|
+
untaggedFence: untagged,
|
|
2072
|
+
plainImage: /!\\[[^\\]]*\\]\\([^\\s)]+\\)(?!\\s*\\{)/.test(text) && !/!\\[[^\\]]*\\]\\([^\\s)]+\\s+"[^"]*"\\)/.test(text),
|
|
2073
|
+
hasNotes: /<!--\\s*notes?:/i.test(text),
|
|
2074
|
+
hasImage: /!\\[[^\\]]*\\]\\(/.test(text),
|
|
2075
|
+
hasTable: /^\\s*\\|.*\\|\\s*$/m.test(text),
|
|
2076
|
+
hasEmbed: /<(iframe|video)\\b/i.test(text),
|
|
2077
|
+
denseSlide: dense
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
var bySnippetId = {};
|
|
2082
|
+
D.snippets.forEach(function (s) { bySnippetId[s.id] = s; });
|
|
2083
|
+
|
|
2084
|
+
function evalNudges() {
|
|
2085
|
+
if (Date.now() - state.bootAt < 5000) return;
|
|
2086
|
+
var ctx = docContext();
|
|
2087
|
+
for (var i = 0; i < NUDGE_RULES.length; i++) {
|
|
2088
|
+
var rule = NUDGE_RULES[i];
|
|
2089
|
+
if (state.dismissed[rule.id]) continue;
|
|
2090
|
+
var message = rule.test(ctx);
|
|
2091
|
+
if (!message) continue;
|
|
2092
|
+
showNudge(rule, message);
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
hideNudge();
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
function showNudge(rule, message) {
|
|
2099
|
+
if (state.activeNudge && state.activeNudge.rule.id === rule.id &&
|
|
2100
|
+
state.activeNudge.message === message) return;
|
|
2101
|
+
if (state.activeNudge && Date.now() - state.nudgeShownAt < 2500) return;
|
|
2102
|
+
state.activeNudge = { rule: rule, message: message };
|
|
2103
|
+
state.nudgeShownAt = Date.now();
|
|
2104
|
+
elNudgeText.innerHTML = message;
|
|
2105
|
+
elNudgeDo.textContent = rule.action === 'download' ? 'download' : 'insert';
|
|
2106
|
+
elNudge.classList.add('is-on');
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function hideNudge() {
|
|
2110
|
+
state.activeNudge = null;
|
|
2111
|
+
elNudge.classList.remove('is-on');
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
elNudgeDo.addEventListener('click', function () {
|
|
2115
|
+
var active = state.activeNudge;
|
|
2116
|
+
if (!active) return;
|
|
2117
|
+
if (active.rule.action) runAction(active.rule.action);
|
|
2118
|
+
else if (bySnippetId[active.rule.snippet]) insertSnippet(bySnippetId[active.rule.snippet]);
|
|
2119
|
+
hideNudge();
|
|
2120
|
+
});
|
|
2121
|
+
|
|
2122
|
+
elNudgeX.addEventListener('click', function () {
|
|
2123
|
+
var active = state.activeNudge;
|
|
2124
|
+
if (active) {
|
|
2125
|
+
state.dismissed[active.rule.id] = true;
|
|
2126
|
+
// A tip stays dismissed for good; a real problem may nag again next session.
|
|
2127
|
+
if (!active.rule.sticky) {
|
|
2128
|
+
var keep = Object.keys(state.dismissed).filter(function (id) {
|
|
2129
|
+
return !NUDGE_RULES.some(function (r) { return r.id === id && r.sticky; });
|
|
2130
|
+
});
|
|
2131
|
+
lsSet(K.nudge, JSON.stringify(keep));
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
hideNudge();
|
|
2135
|
+
});
|
|
2136
|
+
|
|
2137
|
+
// ── Tips carousel ──────────────────────────────────────────────────────
|
|
2138
|
+
var tipTimer = null;
|
|
2139
|
+
|
|
2140
|
+
function showTip(step) {
|
|
2141
|
+
state.tip = (state.tip + step + D.tips.length) % D.tips.length;
|
|
2142
|
+
$('tip-text').textContent = D.tips[state.tip];
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
function startTips() {
|
|
2146
|
+
showTip(0);
|
|
2147
|
+
if (tipTimer) clearInterval(tipTimer);
|
|
2148
|
+
tipTimer = setInterval(function () { showTip(1); }, 13000);
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
$('tip-prev').addEventListener('click', function () { showTip(-1); startTips(); });
|
|
2152
|
+
$('tip-next').addEventListener('click', function () { showTip(1); startTips(); });
|
|
2153
|
+
$('tipbar').addEventListener('mouseenter', function () { if (tipTimer) clearInterval(tipTimer); });
|
|
2154
|
+
$('tipbar').addEventListener('mouseleave', startTips);
|
|
2155
|
+
|
|
2156
|
+
// ── Command palette ────────────────────────────────────────────────────
|
|
2157
|
+
var palRows = [], palSel = 0;
|
|
2158
|
+
|
|
2159
|
+
function palFilter() {
|
|
2160
|
+
var q = palInput.value.trim().toLowerCase();
|
|
2161
|
+
var terms = q ? q.split(/\\s+/) : [];
|
|
2162
|
+
return D.snippets.filter(function (s) {
|
|
2163
|
+
if (!terms.length) return true;
|
|
2164
|
+
var hay = (s.label + ' ' + s.group + ' ' + s.hint + ' ' + s.syntax).toLowerCase();
|
|
2165
|
+
return terms.every(function (t) { return hay.indexOf(t) !== -1; });
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
function renderPalette() {
|
|
2170
|
+
var items = palFilter();
|
|
2171
|
+
palRows = items;
|
|
2172
|
+
if (palSel >= items.length) palSel = Math.max(0, items.length - 1);
|
|
2173
|
+
palList.innerHTML = '';
|
|
2174
|
+
|
|
2175
|
+
if (!items.length) {
|
|
2176
|
+
var none = document.createElement('div');
|
|
2177
|
+
none.className = 'pal-group';
|
|
2178
|
+
none.textContent = 'nothing matches';
|
|
2179
|
+
palList.appendChild(none);
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
var group = null;
|
|
2184
|
+
items.forEach(function (item, i) {
|
|
2185
|
+
if (item.group !== group) {
|
|
2186
|
+
group = item.group;
|
|
2187
|
+
var head = document.createElement('div');
|
|
2188
|
+
head.className = 'pal-group';
|
|
2189
|
+
head.textContent = group;
|
|
2190
|
+
palList.appendChild(head);
|
|
2191
|
+
}
|
|
2192
|
+
var row = document.createElement('div');
|
|
2193
|
+
row.className = 'pal-row' + (i === palSel ? ' is-sel' : '');
|
|
2194
|
+
row.dataset.i = String(i);
|
|
2195
|
+
|
|
2196
|
+
var main = document.createElement('div');
|
|
2197
|
+
main.className = 'pal-row__main';
|
|
2198
|
+
var label = document.createElement('div');
|
|
2199
|
+
label.className = 'pal-row__label';
|
|
2200
|
+
label.textContent = item.label;
|
|
2201
|
+
var hint = document.createElement('div');
|
|
2202
|
+
hint.className = 'pal-row__hint';
|
|
2203
|
+
hint.textContent = item.hint;
|
|
2204
|
+
main.appendChild(label);
|
|
2205
|
+
main.appendChild(hint);
|
|
2206
|
+
row.appendChild(main);
|
|
2207
|
+
|
|
2208
|
+
if (item.syntax) {
|
|
2209
|
+
var syn = document.createElement('code');
|
|
2210
|
+
syn.className = 'pal-row__syntax';
|
|
2211
|
+
syn.textContent = item.syntax.split('\\\\n').join(' ');
|
|
2212
|
+
row.appendChild(syn);
|
|
2213
|
+
}
|
|
2214
|
+
if (item.keys) {
|
|
2215
|
+
var keys = document.createElement('span');
|
|
2216
|
+
keys.className = 'pal-row__keys';
|
|
2217
|
+
keys.textContent = item.keys.replace('Cmd', CMD);
|
|
2218
|
+
row.appendChild(keys);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
row.addEventListener('mousemove', function () {
|
|
2222
|
+
if (palSel === i) return;
|
|
2223
|
+
palSel = i;
|
|
2224
|
+
var sel = palList.querySelector('.pal-row.is-sel');
|
|
2225
|
+
if (sel) sel.classList.remove('is-sel');
|
|
2226
|
+
row.classList.add('is-sel');
|
|
2227
|
+
});
|
|
2228
|
+
row.addEventListener('click', function () { palRun(i); });
|
|
2229
|
+
palList.appendChild(row);
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
function palRun(i) {
|
|
2234
|
+
var item = palRows[i];
|
|
2235
|
+
if (!item) return;
|
|
2236
|
+
closeOverlays();
|
|
2237
|
+
if (item.action) runAction(item.action);
|
|
2238
|
+
else insertSnippet(item);
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
function palMove(step) {
|
|
2242
|
+
if (!palRows.length) return;
|
|
2243
|
+
palSel = (palSel + step + palRows.length) % palRows.length;
|
|
2244
|
+
renderPalette();
|
|
2245
|
+
var sel = palList.querySelector('.pal-row.is-sel');
|
|
2246
|
+
if (sel) sel.scrollIntoView({ block: 'nearest' });
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
function openPalette() {
|
|
2250
|
+
palette.classList.add('is-on');
|
|
2251
|
+
palInput.value = '';
|
|
2252
|
+
palSel = 0;
|
|
2253
|
+
renderPalette();
|
|
2254
|
+
palInput.focus();
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
palInput.addEventListener('input', function () { palSel = 0; renderPalette(); });
|
|
2258
|
+
palInput.addEventListener('keydown', function (e) {
|
|
2259
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); palMove(1); }
|
|
2260
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); palMove(-1); }
|
|
2261
|
+
else if (e.key === 'Enter') { e.preventDefault(); palRun(palSel); }
|
|
2262
|
+
else if (e.key === 'Escape') { e.preventDefault(); closeOverlays(); }
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2265
|
+
// ── Guide drawer ───────────────────────────────────────────────────────
|
|
2266
|
+
function buildGuide() {
|
|
2267
|
+
guideBody.innerHTML = '';
|
|
2268
|
+
D.groups.forEach(function (group) {
|
|
2269
|
+
var items = D.snippets.filter(function (s) { return s.group === group; });
|
|
2270
|
+
if (!items.length) return;
|
|
2271
|
+
|
|
2272
|
+
var sec = document.createElement('section');
|
|
2273
|
+
sec.className = 'gsec';
|
|
2274
|
+
var head = document.createElement('h3');
|
|
2275
|
+
head.textContent = group;
|
|
2276
|
+
sec.appendChild(head);
|
|
2277
|
+
|
|
2278
|
+
items.forEach(function (item) {
|
|
2279
|
+
var card = document.createElement('div');
|
|
2280
|
+
card.className = 'gcard';
|
|
2281
|
+
|
|
2282
|
+
var top = document.createElement('div');
|
|
2283
|
+
top.className = 'gcard__top';
|
|
2284
|
+
var label = document.createElement('span');
|
|
2285
|
+
label.className = 'gcard__label';
|
|
2286
|
+
label.textContent = item.label;
|
|
2287
|
+
top.appendChild(label);
|
|
2288
|
+
if (item.keys) {
|
|
2289
|
+
var keys = document.createElement('span');
|
|
2290
|
+
keys.className = 'gcard__keys';
|
|
2291
|
+
keys.textContent = item.keys.replace('Cmd', CMD);
|
|
2292
|
+
top.appendChild(keys);
|
|
2293
|
+
}
|
|
2294
|
+
card.appendChild(top);
|
|
2295
|
+
|
|
2296
|
+
var hint = document.createElement('div');
|
|
2297
|
+
hint.className = 'gcard__hint';
|
|
2298
|
+
hint.textContent = item.hint;
|
|
2299
|
+
card.appendChild(hint);
|
|
2300
|
+
|
|
2301
|
+
if (item.syntax) {
|
|
2302
|
+
var syn = document.createElement('pre');
|
|
2303
|
+
syn.className = 'gcard__syntax';
|
|
2304
|
+
syn.textContent = item.syntax.split('\\\\n').join('\\n');
|
|
2305
|
+
card.appendChild(syn);
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
var btn = document.createElement('button');
|
|
2309
|
+
btn.className = 'gcard__ins';
|
|
2310
|
+
btn.textContent = item.action ? 'run' : 'insert at caret';
|
|
2311
|
+
btn.addEventListener('click', function () {
|
|
2312
|
+
closeOverlays();
|
|
2313
|
+
if (item.action) runAction(item.action);
|
|
2314
|
+
else insertSnippet(item);
|
|
2315
|
+
});
|
|
2316
|
+
card.appendChild(btn);
|
|
2317
|
+
|
|
2318
|
+
sec.appendChild(card);
|
|
2319
|
+
});
|
|
2320
|
+
|
|
2321
|
+
guideBody.appendChild(sec);
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
function closeOverlays(keepFocus) {
|
|
2326
|
+
// A dropdown outranks the picker in the stacking order, so it has to go
|
|
2327
|
+
// down too or it floats over the modal that just opened.
|
|
2328
|
+
closeMenu();
|
|
2329
|
+
palette.classList.remove('is-on');
|
|
2330
|
+
guide.classList.remove('is-on');
|
|
2331
|
+
$('library').classList.remove('is-on');
|
|
2332
|
+
if ($('themes').classList.contains('is-on')) closeThemes(true);
|
|
2333
|
+
if (!keepFocus) src.focus();
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
Array.prototype.forEach.call(document.querySelectorAll('[data-close]'), function (el) {
|
|
2337
|
+
el.addEventListener('click', function () { closeOverlays(); });
|
|
2338
|
+
});
|
|
2339
|
+
|
|
2340
|
+
// ── Actions ────────────────────────────────────────────────────────────
|
|
2341
|
+
function slugify(s) {
|
|
2342
|
+
return (s || 'deck').trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-|-$/g, '') || 'deck';
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
function download() {
|
|
2346
|
+
var name = slugify($('docname').value);
|
|
2347
|
+
if (state.kind === 'html') {
|
|
2348
|
+
if (!/\\.html?$/.test(name)) name += '.html';
|
|
2349
|
+
saveBlob(name, new Blob([srcHtml.value], { type: 'text/html;charset=utf-8' }));
|
|
2350
|
+
toast('Downloaded ' + name);
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
if (!/\\.(md|markdown)$/.test(name)) name += '.md';
|
|
2354
|
+
saveBlob(name, new Blob([src.value], { type: 'text/markdown;charset=utf-8' }));
|
|
2355
|
+
toast('Downloaded ' + name);
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
function saveBlob(name, blob) {
|
|
2359
|
+
var a = document.createElement('a');
|
|
2360
|
+
a.href = URL.createObjectURL(blob);
|
|
2361
|
+
a.download = name;
|
|
2362
|
+
document.body.appendChild(a);
|
|
2363
|
+
a.click();
|
|
2364
|
+
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
function isEmpty() {
|
|
2368
|
+
return state.kind === 'html' ? !srcHtml.value.trim() : !state.slides.length;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
/** Downloads the built deck or doc as one standalone page. */
|
|
2372
|
+
function exportHtml() {
|
|
2373
|
+
if (isEmpty()) { toast('Nothing to export yet.', 'warn'); return; }
|
|
2374
|
+
var builder = state.kind === 'html' ? buildHtmlDoc : buildDeck;
|
|
2375
|
+
builder(false)
|
|
2376
|
+
.then(function (data) { return fetch(data.path); })
|
|
2377
|
+
.then(function (r) {
|
|
2378
|
+
if (!r.ok) throw new Error('server said ' + r.status);
|
|
2379
|
+
return r.text();
|
|
2380
|
+
})
|
|
2381
|
+
.then(function (html) {
|
|
2382
|
+
var name = slugify($('docname').value).replace(/\\.(md|markdown|html?)$/, '') + '.html';
|
|
2383
|
+
saveBlob(name, new Blob([html], { type: 'text/html;charset=utf-8' }));
|
|
2384
|
+
toast('Downloaded ' + name + '. Fonts and highlighting load from a CDN, so it needs a connection.');
|
|
2385
|
+
})
|
|
2386
|
+
.catch(function (err) { toast('Could not export HTML: ' + err.message, 'err'); });
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
/**
|
|
2390
|
+
* Exports a real PDF file. The server drives a headless browser, so nobody
|
|
2391
|
+
* has to find the landscape and background-graphics settings in a dialog.
|
|
2392
|
+
* Falls back to the dialog only when the machine has no browser to drive.
|
|
2393
|
+
*/
|
|
2394
|
+
var pdfInFlight = false;
|
|
2395
|
+
|
|
2396
|
+
function exportPdf() {
|
|
2397
|
+
if (isEmpty()) { toast('Nothing to export yet.', 'warn'); return; }
|
|
2398
|
+
if (pdfInFlight) { toast('Already building a PDF.'); return; }
|
|
2399
|
+
pdfInFlight = true;
|
|
2400
|
+
elSave.className = 'warn';
|
|
2401
|
+
elSave.textContent = 'building the PDF';
|
|
2402
|
+
|
|
2403
|
+
var pdfUrl = state.kind === 'html' ? '/__pdf-doc' : '/__pdf';
|
|
2404
|
+
var pdfBody = state.kind === 'html'
|
|
2405
|
+
? { html: srcHtml.value, title: $('docname').value }
|
|
2406
|
+
: { markdown: src.value, theme: state.theme, size: state.size, head: state.head, body: state.body, title: $('docname').value };
|
|
2407
|
+
|
|
2408
|
+
fetch(pdfUrl, {
|
|
2409
|
+
method: 'POST',
|
|
2410
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2411
|
+
body: JSON.stringify(pdfBody)
|
|
2412
|
+
})
|
|
2413
|
+
.then(function (r) {
|
|
2414
|
+
if (r.status === 501) {
|
|
2415
|
+
return r.json().then(function (info) {
|
|
2416
|
+
printFallback(info && info.detail);
|
|
2417
|
+
return null;
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
if (!r.ok) {
|
|
2421
|
+
return r.json().then(
|
|
2422
|
+
function (info) { throw new Error((info && info.detail) || 'server said ' + r.status); },
|
|
2423
|
+
function () { throw new Error('server said ' + r.status); }
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
return r.blob();
|
|
2427
|
+
})
|
|
2428
|
+
.then(function (blob) {
|
|
2429
|
+
if (!blob) return;
|
|
2430
|
+
var name = slugify($('docname').value).replace(/\\.(md|markdown|html?)$/, '') + '.pdf';
|
|
2431
|
+
saveBlob(name, blob);
|
|
2432
|
+
toast('Downloaded ' + name + (state.kind === 'html' ? '.' : ', one 16:9 page per slide.'));
|
|
2433
|
+
})
|
|
2434
|
+
.catch(function (err) { toast('Could not export the PDF: ' + err.message, 'err'); })
|
|
2435
|
+
.then(function () {
|
|
2436
|
+
pdfInFlight = false;
|
|
2437
|
+
elSave.className = '';
|
|
2438
|
+
elSave.textContent = '';
|
|
2439
|
+
scheduleSave();
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
/** No local browser to drive: hand the deck or doc to the print dialog instead. */
|
|
2444
|
+
function printFallback(detail) {
|
|
2445
|
+
var tab = window.open('about:blank', '_blank');
|
|
2446
|
+
var builder = state.kind === 'html' ? buildHtmlDoc : buildDeck;
|
|
2447
|
+
builder(true)
|
|
2448
|
+
.then(function (data) {
|
|
2449
|
+
var url = location.origin + data.path;
|
|
2450
|
+
if (!tab) {
|
|
2451
|
+
toast('Allow pop-ups, or press Cmd P, to save a PDF.', 'warn', 'open here', function () { location.href = url; });
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
tab.location.replace(url);
|
|
2455
|
+
toast((detail || 'No local browser to render with.') + ' Using the print dialog instead, which is already set to landscape.', 'warn');
|
|
2456
|
+
})
|
|
2457
|
+
.catch(function (err) {
|
|
2458
|
+
if (tab) tab.close();
|
|
2459
|
+
toast('Could not build the page: ' + err.message, 'err');
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
function buildDeck(forPrint) {
|
|
2464
|
+
return fetch('/__present', {
|
|
2465
|
+
method: 'POST',
|
|
2466
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2467
|
+
body: JSON.stringify({
|
|
2468
|
+
markdown: src.value,
|
|
2469
|
+
theme: state.theme,
|
|
2470
|
+
size: state.size,
|
|
2471
|
+
head: state.head,
|
|
2472
|
+
body: state.body,
|
|
2473
|
+
title: $('docname').value,
|
|
2474
|
+
print: !!forPrint
|
|
2475
|
+
})
|
|
2476
|
+
}).then(function (r) {
|
|
2477
|
+
if (!r.ok) throw new Error('server said ' + r.status);
|
|
2478
|
+
return r.json();
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
function buildHtmlDoc(forPrint) {
|
|
2483
|
+
return fetch('/__present-doc', {
|
|
2484
|
+
method: 'POST',
|
|
2485
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2486
|
+
body: JSON.stringify({
|
|
2487
|
+
html: srcHtml.value,
|
|
2488
|
+
theme: state.theme,
|
|
2489
|
+
title: $('docname').value,
|
|
2490
|
+
print: !!forPrint
|
|
2491
|
+
})
|
|
2492
|
+
}).then(function (r) {
|
|
2493
|
+
if (!r.ok) throw new Error('server said ' + r.status);
|
|
2494
|
+
return r.json();
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
function present() {
|
|
2499
|
+
if (isEmpty()) { toast('Nothing to present yet.', 'warn'); return; }
|
|
2500
|
+
var builder = state.kind === 'html' ? buildHtmlDoc : buildDeck;
|
|
2501
|
+
var tab = window.open('about:blank', '_blank');
|
|
2502
|
+
builder(false)
|
|
2503
|
+
.then(function (data) {
|
|
2504
|
+
var url = location.origin + data.path;
|
|
2505
|
+
if (tab) tab.location.replace(url);
|
|
2506
|
+
else toast('Allow pop-ups to present in a new tab.', 'warn', 'present here', function () { location.href = url; });
|
|
2507
|
+
})
|
|
2508
|
+
.catch(function (err) {
|
|
2509
|
+
if (tab) tab.close();
|
|
2510
|
+
toast('Could not build the page: ' + err.message, 'err');
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
/**
|
|
2515
|
+
* Applies a theme everywhere at once: the editor chrome, the preview iframe,
|
|
2516
|
+
* and the decor attribute the backdrop patterns key off. A remember of false
|
|
2517
|
+
* means the picker is only previewing, so arrowing past a theme never
|
|
2518
|
+
* overwrites the one the user actually chose.
|
|
2519
|
+
*/
|
|
2520
|
+
function setTheme(next, remember) {
|
|
2521
|
+
var id = pickTheme(next);
|
|
2522
|
+
state.theme = id;
|
|
2523
|
+
document.documentElement.dataset.theme = id;
|
|
2524
|
+
document.documentElement.dataset.decor = THEME_BY_ID[id].decor;
|
|
2525
|
+
if (remember !== false) lsSet(K.theme, id);
|
|
2526
|
+
pushLook();
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
/**
|
|
2530
|
+
* Type size is orthogonal to the theme: every theme can be set at any of the
|
|
2531
|
+
* four, so the two choices compose instead of multiplying into presets.
|
|
2532
|
+
*/
|
|
2533
|
+
function setSize(next, remember) {
|
|
2534
|
+
var id = pickSize(next);
|
|
2535
|
+
state.size = id;
|
|
2536
|
+
if (remember !== false) lsSet(K.size, id);
|
|
2537
|
+
pushLook();
|
|
2538
|
+
paintSizeSeg();
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
/**
|
|
2542
|
+
* Heading and body faces are chosen separately, and either can be handed
|
|
2543
|
+
* back to the theme by picking nothing. Passing null is what clears it.
|
|
2544
|
+
*/
|
|
2545
|
+
function setFont(slot, id, remember) {
|
|
2546
|
+
if (slot !== 'head' && slot !== 'body') return;
|
|
2547
|
+
var next = pickFont(id);
|
|
2548
|
+
state[slot] = next;
|
|
2549
|
+
if (remember !== false) {
|
|
2550
|
+
// '' rather than a missing key, so a cleared override survives a reload
|
|
2551
|
+
// instead of falling back to whatever the CLI started with.
|
|
2552
|
+
lsSet(slot === 'head' ? K.head : K.body, next || '');
|
|
2553
|
+
}
|
|
2554
|
+
pushLook();
|
|
2555
|
+
paintFontMenu();
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
function syncHtmlFrameTheme() {
|
|
2559
|
+
if (!frameHtml) return;
|
|
2560
|
+
try {
|
|
2561
|
+
var root = frameHtml.contentDocument && frameHtml.contentDocument.documentElement;
|
|
2562
|
+
if (root) {
|
|
2563
|
+
root.dataset.theme = state.theme;
|
|
2564
|
+
root.dataset.decor = decorOf(state.theme);
|
|
2565
|
+
root.dataset.size = state.size;
|
|
2566
|
+
if (state.head) root.dataset.head = state.head; else delete root.dataset.head;
|
|
2567
|
+
if (state.body) root.dataset.body = state.body; else delete root.dataset.body;
|
|
2568
|
+
}
|
|
2569
|
+
} catch (e) {}
|
|
2570
|
+
try {
|
|
2571
|
+
if (frameHtml.contentWindow) {
|
|
2572
|
+
frameHtml.contentWindow.postMessage({
|
|
2573
|
+
type: 'theme',
|
|
2574
|
+
theme: state.theme,
|
|
2575
|
+
size: state.size,
|
|
2576
|
+
head: state.head,
|
|
2577
|
+
body: state.body
|
|
2578
|
+
}, '*');
|
|
2579
|
+
}
|
|
2580
|
+
} catch (e) {}
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
/** One message for all four, since the preview applies them to one root. */
|
|
2584
|
+
function pushLook() {
|
|
2585
|
+
post({
|
|
2586
|
+
type: 'theme',
|
|
2587
|
+
theme: state.theme,
|
|
2588
|
+
size: state.size,
|
|
2589
|
+
head: state.head,
|
|
2590
|
+
body: state.body
|
|
2591
|
+
});
|
|
2592
|
+
syncHtmlFrameTheme();
|
|
2593
|
+
paintThemeButton();
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
function faceLabel(id) {
|
|
2597
|
+
return id && FACE_BY_ID[id] ? FACE_BY_ID[id].name : 'theme';
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
function buildFontMenu() {
|
|
2601
|
+
[['head', 'ff-head'], ['body', 'ff-body']].forEach(function (pair) {
|
|
2602
|
+
var slot = pair[0];
|
|
2603
|
+
var host = $(pair[1]);
|
|
2604
|
+
host.innerHTML = '';
|
|
2605
|
+
|
|
2606
|
+
host.appendChild(fontRow(slot, null, 'theme default', null));
|
|
2607
|
+
|
|
2608
|
+
var lastKind = null;
|
|
2609
|
+
FACES.forEach(function (f) {
|
|
2610
|
+
if (f.kind !== lastKind) {
|
|
2611
|
+
lastKind = f.kind;
|
|
2612
|
+
var head = document.createElement('div');
|
|
2613
|
+
head.className = 'ff-col__title';
|
|
2614
|
+
head.textContent = f.kind;
|
|
2615
|
+
host.appendChild(head);
|
|
2616
|
+
}
|
|
2617
|
+
host.appendChild(fontRow(slot, f.id, f.name, f));
|
|
2618
|
+
});
|
|
2619
|
+
});
|
|
2620
|
+
paintFontMenu();
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
function fontRow(slot, id, label, face) {
|
|
2624
|
+
var b = document.createElement('button');
|
|
2625
|
+
b.className = 'ff-row' + (id ? '' : ' ff-row--auto');
|
|
2626
|
+
b.dataset.slot = slot;
|
|
2627
|
+
if (id) b.dataset.font = id;
|
|
2628
|
+
|
|
2629
|
+
var name = document.createElement('span');
|
|
2630
|
+
name.className = 'ff-row__name';
|
|
2631
|
+
name.textContent = label;
|
|
2632
|
+
if (face) name.style.fontFamily = face.stack;
|
|
2633
|
+
b.appendChild(name);
|
|
2634
|
+
|
|
2635
|
+
return b;
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
function paintFontMenu() {
|
|
2639
|
+
Array.prototype.forEach.call($('font-menu').querySelectorAll('.ff-row'), function (b) {
|
|
2640
|
+
var slot = b.dataset.slot;
|
|
2641
|
+
b.classList.toggle('is-on', (b.dataset.font || null) === state[slot]);
|
|
2642
|
+
});
|
|
2643
|
+
var custom = state.head || state.body;
|
|
2644
|
+
$('font-label').textContent = custom
|
|
2645
|
+
? faceLabel(state.head) + ' / ' + faceLabel(state.body)
|
|
2646
|
+
: 'font';
|
|
2647
|
+
$('btn-font').title = 'Heading ' + faceLabel(state.head) +
|
|
2648
|
+
' · body ' + faceLabel(state.body);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
function paintThemeButton() {
|
|
2652
|
+
var t = THEME_BY_ID[state.theme];
|
|
2653
|
+
$('theme-label').textContent = t ? t.label : 'theme';
|
|
2654
|
+
$('btn-theme').title = t ? t.label + ' — ' + t.blurb : 'Pick a theme';
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// ── Theme picker ───────────────────────────────────────────────────────
|
|
2658
|
+
function buildSizeSeg(hostId, showBlurb) {
|
|
2659
|
+
var host = $(hostId);
|
|
2660
|
+
host.innerHTML = '';
|
|
2661
|
+
SIZES.forEach(function (z) {
|
|
2662
|
+
var b = document.createElement('button');
|
|
2663
|
+
b.className = 'sz-btn';
|
|
2664
|
+
b.dataset.size = z.id;
|
|
2665
|
+
b.title = z.label + ' — ' + z.blurb;
|
|
2666
|
+
|
|
2667
|
+
var glyph = document.createElement('span');
|
|
2668
|
+
glyph.className = 'sz-btn__glyph';
|
|
2669
|
+
glyph.textContent = z.id.toUpperCase();
|
|
2670
|
+
// Set at the ratio it stands for, so the row doubles as its own legend.
|
|
2671
|
+
glyph.style.fontSize = (10.5 * z.display).toFixed(2) + 'px';
|
|
2672
|
+
b.appendChild(glyph);
|
|
2673
|
+
|
|
2674
|
+
b.addEventListener('click', function () { setSize(z.id, true); });
|
|
2675
|
+
if (showBlurb) {
|
|
2676
|
+
b.addEventListener('mouseenter', function () { showSizeBlurb(z.id); });
|
|
2677
|
+
b.addEventListener('mouseleave', function () { showSizeBlurb(state.size); });
|
|
2678
|
+
}
|
|
2679
|
+
host.appendChild(b);
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
function showSizeBlurb(id) {
|
|
2684
|
+
var z = SIZE_BY_ID[id];
|
|
2685
|
+
$('th-size__blurb').textContent = z ? z.blurb : '';
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
function paintSizeSeg() {
|
|
2689
|
+
Array.prototype.forEach.call(document.querySelectorAll('.sz-btn'), function (b) {
|
|
2690
|
+
b.classList.toggle('is-on', b.dataset.size === state.size);
|
|
2691
|
+
});
|
|
2692
|
+
showSizeBlurb(state.size);
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
/** Steps the size by one, clamped rather than wrapped. */
|
|
2696
|
+
function nudgeSize(delta) {
|
|
2697
|
+
var at = 0;
|
|
2698
|
+
for (var i = 0; i < SIZES.length; i++) if (SIZES[i].id === state.size) at = i;
|
|
2699
|
+
var next = Math.max(0, Math.min(SIZES.length - 1, at + delta));
|
|
2700
|
+
if (SIZES[next].id !== state.size) setSize(SIZES[next].id, true);
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
var themeCards = [];
|
|
2704
|
+
var themeSel = 0;
|
|
2705
|
+
var themeCommitted = null;
|
|
2706
|
+
|
|
2707
|
+
/** A slide in miniature, painted with the theme's own palette and faces. */
|
|
2708
|
+
function themeThumb(t) {
|
|
2709
|
+
var c = t.colors;
|
|
2710
|
+
var wrap = document.createElement('div');
|
|
2711
|
+
wrap.className = 'th-thumb';
|
|
2712
|
+
wrap.style.background = c.crust;
|
|
2713
|
+
|
|
2714
|
+
var glow = document.createElement('div');
|
|
2715
|
+
glow.className = 'th-thumb__glow';
|
|
2716
|
+
glow.style.background = 'radial-gradient(circle, ' + c.accent + '2e 0%, transparent 68%)';
|
|
2717
|
+
wrap.appendChild(glow);
|
|
2718
|
+
|
|
2719
|
+
var title = document.createElement('div');
|
|
2720
|
+
title.className = 'th-thumb__title';
|
|
2721
|
+
title.textContent = 'Aa';
|
|
2722
|
+
title.style.fontFamily = t.fonts.display;
|
|
2723
|
+
title.style.fontWeight = '700';
|
|
2724
|
+
title.style.color = c.accent;
|
|
2725
|
+
wrap.appendChild(title);
|
|
2726
|
+
|
|
2727
|
+
var rule = document.createElement('div');
|
|
2728
|
+
rule.className = 'th-thumb__rule';
|
|
2729
|
+
rule.style.background = 'linear-gradient(90deg, ' + c.accent + ', ' + c.accent2 + ')';
|
|
2730
|
+
wrap.appendChild(rule);
|
|
2731
|
+
|
|
2732
|
+
[88, 70, 52].forEach(function (pct, i) {
|
|
2733
|
+
var line = document.createElement('div');
|
|
2734
|
+
line.className = 'th-thumb__line';
|
|
2735
|
+
line.style.width = pct + '%';
|
|
2736
|
+
line.style.background = i === 0 ? c.subtext1 : c.overlay1;
|
|
2737
|
+
line.style.opacity = i === 0 ? '0.85' : '0.5';
|
|
2738
|
+
wrap.appendChild(line);
|
|
2739
|
+
});
|
|
2740
|
+
|
|
2741
|
+
var code = document.createElement('div');
|
|
2742
|
+
code.className = 'th-thumb__code';
|
|
2743
|
+
code.textContent = 'const deck = md';
|
|
2744
|
+
code.style.fontFamily = t.fonts.mono;
|
|
2745
|
+
code.style.background = c.mantle;
|
|
2746
|
+
code.style.color = c.accent3;
|
|
2747
|
+
code.style.border = '1px solid ' + c.surface0;
|
|
2748
|
+
wrap.appendChild(code);
|
|
2749
|
+
|
|
2750
|
+
return wrap;
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
function themeCard(t) {
|
|
2754
|
+
var card = document.createElement('button');
|
|
2755
|
+
card.className = 'th-card' + (t.id === state.theme ? ' is-current' : '');
|
|
2756
|
+
card.dataset.theme = t.id;
|
|
2757
|
+
card.appendChild(themeThumb(t));
|
|
2758
|
+
|
|
2759
|
+
var meta = document.createElement('div');
|
|
2760
|
+
meta.className = 'th-meta';
|
|
2761
|
+
|
|
2762
|
+
var top = document.createElement('div');
|
|
2763
|
+
top.className = 'th-meta__top';
|
|
2764
|
+
var name = document.createElement('span');
|
|
2765
|
+
name.className = 'th-meta__name';
|
|
2766
|
+
name.textContent = t.label;
|
|
2767
|
+
var mood = document.createElement('span');
|
|
2768
|
+
mood.className = 'th-meta__mood';
|
|
2769
|
+
mood.textContent = t.mood;
|
|
2770
|
+
top.appendChild(name);
|
|
2771
|
+
top.appendChild(mood);
|
|
2772
|
+
|
|
2773
|
+
var blurb = document.createElement('div');
|
|
2774
|
+
blurb.className = 'th-meta__blurb';
|
|
2775
|
+
blurb.textContent = t.blurb;
|
|
2776
|
+
|
|
2777
|
+
meta.appendChild(top);
|
|
2778
|
+
meta.appendChild(blurb);
|
|
2779
|
+
card.appendChild(meta);
|
|
2780
|
+
return card;
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
function buildThemePicker() {
|
|
2784
|
+
var host = $('th-list');
|
|
2785
|
+
host.innerHTML = '';
|
|
2786
|
+
themeCards = [];
|
|
2787
|
+
|
|
2788
|
+
[['dark', 'dark'], ['light', 'light']].forEach(function (pair) {
|
|
2789
|
+
var list = THEMES.filter(function (t) { return t.mood === pair[0]; });
|
|
2790
|
+
if (!list.length) return;
|
|
2791
|
+
|
|
2792
|
+
var label = document.createElement('div');
|
|
2793
|
+
label.className = 'th-group';
|
|
2794
|
+
label.textContent = pair[1];
|
|
2795
|
+
host.appendChild(label);
|
|
2796
|
+
|
|
2797
|
+
var grid = document.createElement('div');
|
|
2798
|
+
grid.className = 'th-grid';
|
|
2799
|
+
list.forEach(function (t) {
|
|
2800
|
+
var card = themeCard(t);
|
|
2801
|
+
// Hovering previews too, so the mouse gets the same instant feedback
|
|
2802
|
+
// as the arrow keys.
|
|
2803
|
+
card.addEventListener('mouseenter', function () { selectTheme(themeCards.indexOf(card)); });
|
|
2804
|
+
card.addEventListener('click', function () { commitTheme(); });
|
|
2805
|
+
grid.appendChild(card);
|
|
2806
|
+
themeCards.push(card);
|
|
2807
|
+
});
|
|
2808
|
+
host.appendChild(grid);
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
function selectTheme(i) {
|
|
2813
|
+
if (i < 0 || i >= themeCards.length) return;
|
|
2814
|
+
themeSel = i;
|
|
2815
|
+
themeCards.forEach(function (c, n) { c.classList.toggle('is-sel', n === i); });
|
|
2816
|
+
setTheme(themeCards[i].dataset.theme, false);
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
function openThemes() {
|
|
2820
|
+
closeOverlays(true);
|
|
2821
|
+
themeCommitted = state.theme;
|
|
2822
|
+
buildSizeSeg('th-size__seg', true);
|
|
2823
|
+
paintSizeSeg();
|
|
2824
|
+
buildThemePicker();
|
|
2825
|
+
var at = 0;
|
|
2826
|
+
themeCards.forEach(function (c, n) { if (c.dataset.theme === state.theme) at = n; });
|
|
2827
|
+
$('themes').classList.add('is-on');
|
|
2828
|
+
selectTheme(at);
|
|
2829
|
+
themeCards[themeSel].scrollIntoView({ block: 'nearest' });
|
|
2830
|
+
src.blur();
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
function commitTheme() {
|
|
2834
|
+
themeCommitted = state.theme;
|
|
2835
|
+
lsSet(K.theme, state.theme);
|
|
2836
|
+
closeThemes();
|
|
2837
|
+
var t = THEME_BY_ID[state.theme];
|
|
2838
|
+
toast('Theme set to ' + (t ? t.label : state.theme) + '.');
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
/** Leaving without choosing puts back whatever was on when the picker opened. */
|
|
2842
|
+
function closeThemes(restore) {
|
|
2843
|
+
if (restore && themeCommitted && themeCommitted !== state.theme) {
|
|
2844
|
+
setTheme(themeCommitted, true);
|
|
2845
|
+
}
|
|
2846
|
+
$('themes').classList.remove('is-on');
|
|
2847
|
+
themeCards.forEach(function (c) { c.classList.remove('is-sel'); });
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
function themesOpen() { return $('themes').classList.contains('is-on'); }
|
|
2851
|
+
|
|
2852
|
+
function setMode(next) {
|
|
2853
|
+
state.mode = next;
|
|
2854
|
+
lsSet(K.mode, next);
|
|
2855
|
+
$('seg-single').classList.toggle('is-on', next === 'single');
|
|
2856
|
+
$('seg-grid').classList.toggle('is-on', next === 'grid');
|
|
2857
|
+
layout();
|
|
2858
|
+
pushFrame();
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
/** The status-bar counters' HTML-doc analog: chars and caret position. */
|
|
2862
|
+
function renderHtmlCounts() {
|
|
2863
|
+
elChipSlides.textContent = srcHtml.value.length + ' chars';
|
|
2864
|
+
var before = srcHtml.value.slice(0, srcHtml.selectionStart || 0);
|
|
2865
|
+
var lines = before.split('\\n');
|
|
2866
|
+
elPos.textContent = 'Ln ' + lines.length + ', Col ' + (lines[lines.length - 1].length + 1);
|
|
2867
|
+
elWords.textContent = '';
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
/** Load a deck or doc into the editor, saving whatever is open first. */
|
|
2871
|
+
function openDeck(id, announce) {
|
|
2872
|
+
var meta = findDeck(id);
|
|
2873
|
+
if (!meta) { toast('That deck is gone.', 'warn'); return; }
|
|
2874
|
+
if (state.deckId && id !== state.deckId) saveNow();
|
|
2875
|
+
state.deckId = id;
|
|
2876
|
+
lsSet(K.current, id);
|
|
2877
|
+
setDocKind(meta.kind || 'markdown');
|
|
2878
|
+
$('docname').value = meta.name;
|
|
2879
|
+
updateDeckCount();
|
|
2880
|
+
elSave.className = '';
|
|
2881
|
+
elSave.textContent = '';
|
|
2882
|
+
|
|
2883
|
+
if (state.kind === 'html') {
|
|
2884
|
+
srcHtml.value = readDeck(id);
|
|
2885
|
+
frameHtml.srcdoc = srcHtml.value;
|
|
2886
|
+
renderHtmlCounts();
|
|
2887
|
+
if (announce) toast('Opened ' + meta.name);
|
|
2888
|
+
srcHtml.focus();
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
src.value = readDeck(id);
|
|
2893
|
+
src.setSelectionRange(0, 0);
|
|
2894
|
+
src.scrollTop = 0;
|
|
2895
|
+
state.index = 0;
|
|
2896
|
+
state.overflow = {};
|
|
2897
|
+
paint();
|
|
2898
|
+
syncScroll();
|
|
2899
|
+
updateCaretUi();
|
|
2900
|
+
refresh();
|
|
2901
|
+
if (announce) toast('Opened ' + meta.name);
|
|
2902
|
+
src.focus();
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
function noRoom() {
|
|
2906
|
+
toast('This browser has no room for another deck. Delete one, or download this one first.', 'err');
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
function newDeck() {
|
|
2910
|
+
saveNow();
|
|
2911
|
+
var id = createDeck(uniqueName('untitled'), '', 'markdown');
|
|
2912
|
+
if (!id) { noRoom(); return; }
|
|
2913
|
+
openDeck(id);
|
|
2914
|
+
toast('New deck. Press ' + CMD + ' K to see what you can add.');
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
function duplicateDeck() {
|
|
2918
|
+
saveNow();
|
|
2919
|
+
var meta = findDeck(state.deckId);
|
|
2920
|
+
var id = createDeck(uniqueName((meta ? meta.name : 'deck') + ' copy'), curValue(), state.kind);
|
|
2921
|
+
if (!id) { noRoom(); return; }
|
|
2922
|
+
openDeck(id);
|
|
2923
|
+
toast('Duplicated');
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
function deleteDeck(id) {
|
|
2927
|
+
var meta = findDeck(id);
|
|
2928
|
+
if (!meta) return;
|
|
2929
|
+
if (!confirm('Delete "' + meta.name + '"? This cannot be undone, and it is only in this browser.')) return;
|
|
2930
|
+
|
|
2931
|
+
var list = loadIndex().filter(function (d) { return d.id !== id; });
|
|
2932
|
+
saveIndex(list);
|
|
2933
|
+
lsDel(K.deck + id);
|
|
2934
|
+
|
|
2935
|
+
if (id === state.deckId) {
|
|
2936
|
+
if (list.length) {
|
|
2937
|
+
openDeck(list[0].id);
|
|
2938
|
+
} else {
|
|
2939
|
+
var fresh = createDeck('deck', '', 'markdown');
|
|
2940
|
+
if (fresh) openDeck(fresh);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
updateDeckCount();
|
|
2944
|
+
renderLibrary();
|
|
2945
|
+
toast('Deleted ' + meta.name);
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
// ── Library panel ──────────────────────────────────────────────────────
|
|
2949
|
+
var libRows = [], libSel = 0;
|
|
2950
|
+
|
|
2951
|
+
/** Most recently touched first, the useful order for a switcher. */
|
|
2952
|
+
function libraryList() {
|
|
2953
|
+
return loadIndex().slice().sort(function (a, b) { return (b.at || 0) - (a.at || 0); });
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
function renderLibrary() {
|
|
2957
|
+
var list = libraryList();
|
|
2958
|
+
libRows = list;
|
|
2959
|
+
if (libSel >= list.length) libSel = Math.max(0, list.length - 1);
|
|
2960
|
+
var host = $('lib-list');
|
|
2961
|
+
host.innerHTML = '';
|
|
2962
|
+
|
|
2963
|
+
list.forEach(function (deck, i) {
|
|
2964
|
+
var row = document.createElement('div');
|
|
2965
|
+
row.className = 'lib-row' + (i === libSel ? ' is-sel' : '') +
|
|
2966
|
+
(deck.id === state.deckId ? ' is-current' : '');
|
|
2967
|
+
|
|
2968
|
+
var main = document.createElement('div');
|
|
2969
|
+
main.className = 'lib-row__main';
|
|
2970
|
+
var name = document.createElement('div');
|
|
2971
|
+
name.className = 'lib-row__name';
|
|
2972
|
+
name.textContent = deck.name + (deck.id === state.deckId ? ' (open)' : '');
|
|
2973
|
+
var meta = document.createElement('div');
|
|
2974
|
+
meta.className = 'lib-row__meta';
|
|
2975
|
+
var kind = deck.kind || 'markdown';
|
|
2976
|
+
if (kind === 'html') {
|
|
2977
|
+
var chars = deck.id === state.deckId ? srcHtml.value.length : deck.chars;
|
|
2978
|
+
meta.textContent = 'html' +
|
|
2979
|
+
' \u00b7 ' + Math.max(1, Math.round((chars || 0) / 1024)) + ' KB' +
|
|
2980
|
+
' \u00b7 ' + timeAgo(deck.at || Date.now());
|
|
2981
|
+
} else {
|
|
2982
|
+
var slides = deck.id === state.deckId ? state.slides.length : deck.slides;
|
|
2983
|
+
meta.textContent = (slides || 0) + (slides === 1 ? ' slide' : ' slides') +
|
|
2984
|
+
' \u00b7 ' + Math.max(1, Math.round((deck.chars || 0) / 1024)) + ' KB' +
|
|
2985
|
+
' \u00b7 ' + timeAgo(deck.at || Date.now());
|
|
2986
|
+
}
|
|
2987
|
+
main.appendChild(name);
|
|
2988
|
+
main.appendChild(meta);
|
|
2989
|
+
row.appendChild(main);
|
|
2990
|
+
|
|
2991
|
+
var acts = document.createElement('div');
|
|
2992
|
+
acts.className = 'lib-row__acts';
|
|
2993
|
+
|
|
2994
|
+
var dupe = document.createElement('button');
|
|
2995
|
+
dupe.textContent = 'duplicate';
|
|
2996
|
+
dupe.addEventListener('click', function (e) {
|
|
2997
|
+
e.stopPropagation();
|
|
2998
|
+
if (deck.id === state.deckId) { duplicateDeck(); renderLibrary(); return; }
|
|
2999
|
+
var id = createDeck(uniqueName(deck.name + ' copy'), readDeck(deck.id), deck.kind || 'markdown');
|
|
3000
|
+
if (!id) { noRoom(); return; }
|
|
3001
|
+
updateDeckCount();
|
|
3002
|
+
renderLibrary();
|
|
3003
|
+
toast('Duplicated ' + deck.name);
|
|
3004
|
+
});
|
|
3005
|
+
acts.appendChild(dupe);
|
|
3006
|
+
|
|
3007
|
+
var del = document.createElement('button');
|
|
3008
|
+
del.className = 'danger';
|
|
3009
|
+
del.textContent = 'delete';
|
|
3010
|
+
del.addEventListener('click', function (e) { e.stopPropagation(); deleteDeck(deck.id); });
|
|
3011
|
+
acts.appendChild(del);
|
|
3012
|
+
|
|
3013
|
+
row.appendChild(acts);
|
|
3014
|
+
row.addEventListener('mousemove', function () {
|
|
3015
|
+
if (libSel === i) return;
|
|
3016
|
+
libSel = i;
|
|
3017
|
+
var sel = host.querySelector('.lib-row.is-sel');
|
|
3018
|
+
if (sel) sel.classList.remove('is-sel');
|
|
3019
|
+
row.classList.add('is-sel');
|
|
3020
|
+
});
|
|
3021
|
+
row.addEventListener('click', function () { closeOverlays(); openDeck(deck.id, true); });
|
|
3022
|
+
host.appendChild(row);
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
function openLibrary() {
|
|
3027
|
+
var list = libraryList();
|
|
3028
|
+
libSel = 0;
|
|
3029
|
+
for (var i = 0; i < list.length; i++) if (list[i].id === state.deckId) libSel = i;
|
|
3030
|
+
renderLibrary();
|
|
3031
|
+
$('library').classList.add('is-on');
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
$('lib-new').addEventListener('click', function () { closeOverlays(); showStartScreen(); });
|
|
3035
|
+
|
|
3036
|
+
$('btn-decks').addEventListener('click', openLibrary);
|
|
3037
|
+
|
|
3038
|
+
// ── Start screen ─────────────────────────────────────────────────────
|
|
3039
|
+
// Shown on a true first run, and whenever "new" is chosen explicitly from
|
|
3040
|
+
// the library — never on an ordinary launch that has a deck to resume.
|
|
3041
|
+
function showStartScreen() {
|
|
3042
|
+
$('start-lib').classList.toggle('is-disabled', !loadIndex().length);
|
|
3043
|
+
$('screen-start').classList.add('is-on');
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
function hideStartScreenSilently() {
|
|
3047
|
+
$('screen-start').classList.remove('is-on');
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
/** Escape/backdrop close. With nothing open yet, that would be a dead end,
|
|
3051
|
+
so the one specified fallback is a blank Markdown deck. */
|
|
3052
|
+
function hideStartScreen(fallbackIfEmpty) {
|
|
3053
|
+
hideStartScreenSilently();
|
|
3054
|
+
if (fallbackIfEmpty && !state.deckId) startNewMarkdown();
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
function startNewMarkdown() {
|
|
3058
|
+
var id = createDeck(uniqueName('untitled'), D.welcome, 'markdown');
|
|
3059
|
+
hideStartScreenSilently();
|
|
3060
|
+
if (!id) {
|
|
3061
|
+
// Storage refuses even a first write: use the deck unsaved rather than
|
|
3062
|
+
// leaving the editor with nothing open.
|
|
3063
|
+
state.deckId = newDeckId();
|
|
3064
|
+
setDocKind('markdown');
|
|
3065
|
+
src.value = D.welcome;
|
|
3066
|
+
$('docname').value = 'deck';
|
|
3067
|
+
src.setSelectionRange(0, 0);
|
|
3068
|
+
src.scrollTop = 0;
|
|
3069
|
+
paint();
|
|
3070
|
+
syncScroll();
|
|
3071
|
+
updateCaretUi();
|
|
3072
|
+
refresh();
|
|
3073
|
+
updateDeckCount();
|
|
3074
|
+
elSave.className = 'err';
|
|
3075
|
+
elSave.textContent = 'this browser blocks local storage';
|
|
3076
|
+
setTimeout(function () {
|
|
3077
|
+
toast('This browser will not let the editor save anything. Download your deck to keep it.', 'err', 'download', download);
|
|
3078
|
+
}, 700);
|
|
3079
|
+
src.focus();
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
openDeck(id);
|
|
3083
|
+
setTimeout(function () {
|
|
3084
|
+
toast('This deck is yours to overwrite. Press ' + CMD + ' K to see every layout and style.', null, 'open the guide', function () { runAction('guide'); });
|
|
3085
|
+
}, 900);
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
function startOpenLibrary() {
|
|
3089
|
+
if (!loadIndex().length) return;
|
|
3090
|
+
hideStartScreenSilently();
|
|
3091
|
+
openLibrary();
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
/** Fetches a public HTML page server-side (sidesteps CORS) and opens it as a new HTML doc. */
|
|
3095
|
+
function loadHtmlUrl() {
|
|
3096
|
+
var input = $('start-html-url');
|
|
3097
|
+
var raw = input.value.trim();
|
|
3098
|
+
if (!raw) { input.focus(); return; }
|
|
3099
|
+
var url;
|
|
3100
|
+
try { url = new URL(raw); } catch (err) { toast('Not a valid URL.', 'err'); return; }
|
|
3101
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
3102
|
+
toast('URL must be http or https.', 'err');
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
3105
|
+
var btn = $('start-html-url-go');
|
|
3106
|
+
btn.disabled = true;
|
|
3107
|
+
fetch('/__fetch-doc', {
|
|
3108
|
+
method: 'POST',
|
|
3109
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3110
|
+
body: JSON.stringify({ url: url.toString() }),
|
|
3111
|
+
})
|
|
3112
|
+
.then(function (res) {
|
|
3113
|
+
return res.json().then(function (body) {
|
|
3114
|
+
if (!res.ok) throw new Error(body.detail || body.error || 'could not load that URL');
|
|
3115
|
+
return body;
|
|
3116
|
+
});
|
|
3117
|
+
})
|
|
3118
|
+
.then(function (body) {
|
|
3119
|
+
saveNow();
|
|
3120
|
+
var name = uniqueName(body.title || url.hostname);
|
|
3121
|
+
var id = createDeck(name, body.html, 'html');
|
|
3122
|
+
if (!id) { noRoom(); return; }
|
|
3123
|
+
input.value = '';
|
|
3124
|
+
hideStartScreenSilently();
|
|
3125
|
+
openDeck(id);
|
|
3126
|
+
toast('Loaded ' + url.hostname + ' as a new HTML doc');
|
|
3127
|
+
})
|
|
3128
|
+
.catch(function (err) { toast('Could not load URL: ' + err.message, 'err'); })
|
|
3129
|
+
.then(function () { btn.disabled = false; });
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
$('start-md-blank').addEventListener('click', startNewMarkdown);
|
|
3133
|
+
$('start-md-upload').addEventListener('click', function () { $('file-any').click(); });
|
|
3134
|
+
$('start-html-upload').addEventListener('click', function () { $('file-any').click(); });
|
|
3135
|
+
$('start-html-url-go').addEventListener('click', loadHtmlUrl);
|
|
3136
|
+
$('start-html-url').addEventListener('keydown', function (e) {
|
|
3137
|
+
if (e.key === 'Enter') { e.preventDefault(); loadHtmlUrl(); }
|
|
3138
|
+
});
|
|
3139
|
+
$('start-lib').addEventListener('click', startOpenLibrary);
|
|
3140
|
+
$('screen-start').querySelector('.backdrop').addEventListener('click', function () { hideStartScreen(true); });
|
|
3141
|
+
|
|
3142
|
+
function runAction(name) {
|
|
3143
|
+
if (name === 'present') present();
|
|
3144
|
+
else if (name === 'download') download();
|
|
3145
|
+
else if (name === 'pdf') exportPdf();
|
|
3146
|
+
else if (name === 'html') exportHtml();
|
|
3147
|
+
else if (name === 'open') $('file-any').click();
|
|
3148
|
+
else if (name === 'grid') setMode(state.mode === 'grid' ? 'single' : 'grid');
|
|
3149
|
+
else if (name === 'theme') openThemes();
|
|
3150
|
+
else if (name === 'guide') { guide.classList.add('is-on'); }
|
|
3151
|
+
else if (name === 'decks') openLibrary();
|
|
3152
|
+
else if (name === 'new') newDeck();
|
|
3153
|
+
else if (name === 'duplicate') duplicateDeck();
|
|
3154
|
+
else if (name === 'delete') deleteDeck(state.deckId);
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
// ── Wiring ─────────────────────────────────────────────────────────────
|
|
3158
|
+
var parseTimer = null;
|
|
3159
|
+
|
|
3160
|
+
function onInput() {
|
|
3161
|
+
paint();
|
|
3162
|
+
syncScroll();
|
|
3163
|
+
updateCaretUi();
|
|
3164
|
+
scheduleSave();
|
|
3165
|
+
if (parseTimer) clearTimeout(parseTimer);
|
|
3166
|
+
parseTimer = setTimeout(refresh, 140);
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
src.addEventListener('input', onInput);
|
|
3170
|
+
src.addEventListener('scroll', syncScroll);
|
|
3171
|
+
src.addEventListener('click', updateCaretUi);
|
|
3172
|
+
src.addEventListener('keyup', function (e) {
|
|
3173
|
+
if (e.key.indexOf('Arrow') === 0 || e.key === 'Home' || e.key === 'End' ||
|
|
3174
|
+
e.key === 'PageUp' || e.key === 'PageDown') updateCaretUi();
|
|
3175
|
+
});
|
|
3176
|
+
|
|
3177
|
+
src.addEventListener('keydown', function (e) {
|
|
3178
|
+
if (e.key === 'Tab') {
|
|
3179
|
+
e.preventDefault();
|
|
3180
|
+
typeText(' ');
|
|
3181
|
+
onInput();
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
// Continue the list you are in.
|
|
3185
|
+
if (e.key === 'Enter' && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
|
|
3186
|
+
var upto = src.value.slice(0, src.selectionStart);
|
|
3187
|
+
var line = upto.slice(upto.lastIndexOf('\\n') + 1);
|
|
3188
|
+
var m = line.match(/^(\\s*)([-*+]|(\\d+)[.)])(\\s+)(.*)$/);
|
|
3189
|
+
if (m && src.selectionStart === src.selectionEnd) {
|
|
3190
|
+
e.preventDefault();
|
|
3191
|
+
if (!m[5]) {
|
|
3192
|
+
// Empty item: end the list instead of nesting further.
|
|
3193
|
+
var start = src.selectionStart - line.length;
|
|
3194
|
+
src.setSelectionRange(start, src.selectionStart);
|
|
3195
|
+
typeText('\\n');
|
|
3196
|
+
} else {
|
|
3197
|
+
var marker = m[3] ? (parseInt(m[3], 10) + 1) + m[2].slice(String(m[3]).length) : m[2];
|
|
3198
|
+
typeText('\\n' + m[1] + marker + m[4]);
|
|
3199
|
+
}
|
|
3200
|
+
onInput();
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
});
|
|
3204
|
+
|
|
3205
|
+
// ── HTML doc source ──────────────────────────────────────────────────
|
|
3206
|
+
var htmlPreviewTimer = null;
|
|
3207
|
+
|
|
3208
|
+
function onHtmlInput() {
|
|
3209
|
+
scheduleSave();
|
|
3210
|
+
renderHtmlCounts();
|
|
3211
|
+
if (htmlPreviewTimer) clearTimeout(htmlPreviewTimer);
|
|
3212
|
+
htmlPreviewTimer = setTimeout(function () { frameHtml.srcdoc = srcHtml.value; }, 140);
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
srcHtml.addEventListener('input', onHtmlInput);
|
|
3216
|
+
frameHtml.addEventListener('load', syncHtmlFrameTheme);
|
|
3217
|
+
srcHtml.addEventListener('click', renderHtmlCounts);
|
|
3218
|
+
srcHtml.addEventListener('keyup', function (e) {
|
|
3219
|
+
if (e.key.indexOf('Arrow') === 0 || e.key === 'Home' || e.key === 'End' ||
|
|
3220
|
+
e.key === 'PageUp' || e.key === 'PageDown') renderHtmlCounts();
|
|
3221
|
+
});
|
|
3222
|
+
srcHtml.addEventListener('keydown', function (e) {
|
|
3223
|
+
if (e.key !== 'Tab') return;
|
|
3224
|
+
e.preventDefault();
|
|
3225
|
+
srcHtml.focus();
|
|
3226
|
+
var inserted = false;
|
|
3227
|
+
try { inserted = document.execCommand('insertText', false, ' '); } catch (err) {}
|
|
3228
|
+
if (!inserted) {
|
|
3229
|
+
var s = srcHtml.selectionStart, e2 = srcHtml.selectionEnd;
|
|
3230
|
+
srcHtml.setRangeText(' ', s, e2, 'end');
|
|
3231
|
+
}
|
|
3232
|
+
onHtmlInput();
|
|
3233
|
+
});
|
|
3234
|
+
|
|
3235
|
+
// Drag and drop a .md file onto the editor to open it.
|
|
3236
|
+
['dragenter', 'dragover'].forEach(function (type) {
|
|
3237
|
+
paneEdit.addEventListener(type, function (e) {
|
|
3238
|
+
if (!e.dataTransfer) return;
|
|
3239
|
+
e.preventDefault();
|
|
3240
|
+
paneEdit.classList.add('is-dropping');
|
|
3241
|
+
});
|
|
3242
|
+
});
|
|
3243
|
+
|
|
3244
|
+
['dragleave', 'dragend'].forEach(function (type) {
|
|
3245
|
+
paneEdit.addEventListener(type, function (e) {
|
|
3246
|
+
if (e.target === paneEdit || type === 'dragend') paneEdit.classList.remove('is-dropping');
|
|
3247
|
+
});
|
|
3248
|
+
});
|
|
3249
|
+
|
|
3250
|
+
paneEdit.addEventListener('drop', function (e) {
|
|
3251
|
+
e.preventDefault();
|
|
3252
|
+
paneEdit.classList.remove('is-dropping');
|
|
3253
|
+
var files = e.dataTransfer ? e.dataTransfer.files : null;
|
|
3254
|
+
if (!files || !files.length) return;
|
|
3255
|
+
for (var i = 0; i < files.length; i++) {
|
|
3256
|
+
var f = files[i];
|
|
3257
|
+
if (/\\.(md|markdown|txt)$/i.test(f.name)) loadMarkdownFile(f);
|
|
3258
|
+
else if (/\\.html?$/i.test(f.name)) loadHtmlFile(f);
|
|
3259
|
+
else toast('Skipped ' + f.name + '. Drop a Markdown or HTML file to open it.', 'warn');
|
|
3260
|
+
}
|
|
3261
|
+
});
|
|
3262
|
+
|
|
3263
|
+
/** An opened file lands as a new deck, so nothing in the library is lost. */
|
|
3264
|
+
function loadMarkdownFile(file) {
|
|
3265
|
+
var fr = new FileReader();
|
|
3266
|
+
fr.onload = function () {
|
|
3267
|
+
saveNow();
|
|
3268
|
+
var markdown = String(fr.result).replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');
|
|
3269
|
+
var name = uniqueName(file.name.replace(/\\.(md|markdown|txt)$/i, '') || 'untitled');
|
|
3270
|
+
var id = createDeck(name, markdown, 'markdown');
|
|
3271
|
+
if (!id) { noRoom(); return; }
|
|
3272
|
+
hideStartScreenSilently();
|
|
3273
|
+
openDeck(id);
|
|
3274
|
+
toast('Loaded ' + file.name + ' as a new deck');
|
|
3275
|
+
};
|
|
3276
|
+
fr.onerror = function () { toast('Could not read ' + file.name, 'err'); };
|
|
3277
|
+
fr.readAsText(file);
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
/** An uploaded HTML doc lands as a new library entry, same as a .md file. */
|
|
3281
|
+
function loadHtmlFile(file) {
|
|
3282
|
+
var fr = new FileReader();
|
|
3283
|
+
fr.onload = function () {
|
|
3284
|
+
saveNow();
|
|
3285
|
+
var html = String(fr.result);
|
|
3286
|
+
var name = uniqueName(file.name.replace(/\\.html?$/i, '') || 'untitled');
|
|
3287
|
+
var id = createDeck(name, html, 'html');
|
|
3288
|
+
if (!id) { noRoom(); return; }
|
|
3289
|
+
hideStartScreenSilently();
|
|
3290
|
+
openDeck(id);
|
|
3291
|
+
toast('Loaded ' + file.name + ' as a new HTML doc');
|
|
3292
|
+
};
|
|
3293
|
+
fr.onerror = function () { toast('Could not read ' + file.name, 'err'); };
|
|
3294
|
+
fr.readAsText(file);
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
$('file-any').addEventListener('change', function (e) {
|
|
3298
|
+
var f = e.target.files[0];
|
|
3299
|
+
if (f) {
|
|
3300
|
+
if (/\\.(md|markdown|txt)$/i.test(f.name)) loadMarkdownFile(f);
|
|
3301
|
+
else if (/\\.html?$/i.test(f.name)) loadHtmlFile(f);
|
|
3302
|
+
else toast('Unrecognized file type.', 'warn');
|
|
3303
|
+
}
|
|
3304
|
+
e.target.value = '';
|
|
3305
|
+
});
|
|
3306
|
+
|
|
3307
|
+
$('docname').addEventListener('input', scheduleSave);
|
|
3308
|
+
|
|
3309
|
+
$('btn-guide').addEventListener('click', function () { runAction('guide'); });
|
|
3310
|
+
$('btn-palette').addEventListener('click', openPalette);
|
|
3311
|
+
$('btn-new').addEventListener('click', function () { closeOverlays(); showStartScreen(); });
|
|
3312
|
+
// ── Dropdown menus ─────────────────────────────────────────────────────
|
|
3313
|
+
// Both the export and font popovers behave the same way, so one open menu is
|
|
3314
|
+
// tracked here rather than each growing its own copy of this.
|
|
3315
|
+
var openMenu = null;
|
|
3316
|
+
|
|
3317
|
+
function menuIsOpen() { return !!openMenu; }
|
|
3318
|
+
|
|
3319
|
+
function closeMenu() {
|
|
3320
|
+
if (!openMenu) return;
|
|
3321
|
+
openMenu.classList.remove('is-open');
|
|
3322
|
+
var btn = openMenu.querySelector('.btn');
|
|
3323
|
+
if (btn) btn.setAttribute('aria-expanded', 'false');
|
|
3324
|
+
openMenu = null;
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
function toggleMenu(el) {
|
|
3328
|
+
var wasOpen = openMenu === el;
|
|
3329
|
+
closeMenu();
|
|
3330
|
+
if (wasOpen) return;
|
|
3331
|
+
openMenu = el;
|
|
3332
|
+
el.classList.add('is-open');
|
|
3333
|
+
var btn = el.querySelector('.btn');
|
|
3334
|
+
if (btn) btn.setAttribute('aria-expanded', 'true');
|
|
3335
|
+
var first = el.querySelector('.menu__pop button');
|
|
3336
|
+
if (first) first.focus();
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
/** Wires one dropdown: its trigger, its arrow keys, and its own Escape. */
|
|
3340
|
+
function bindMenu(triggerId, popId, onPick, pickSelector) {
|
|
3341
|
+
var wrap = $(triggerId).parentNode;
|
|
3342
|
+
var pop = $(popId);
|
|
3343
|
+
|
|
3344
|
+
$(triggerId).addEventListener('click', function (e) {
|
|
3345
|
+
e.stopPropagation();
|
|
3346
|
+
toggleMenu(wrap);
|
|
3347
|
+
});
|
|
3348
|
+
|
|
3349
|
+
pop.addEventListener('click', function (e) {
|
|
3350
|
+
var btn = e.target.closest ? e.target.closest(pickSelector) : null;
|
|
3351
|
+
if (!btn) return;
|
|
3352
|
+
onPick(btn);
|
|
3353
|
+
});
|
|
3354
|
+
|
|
3355
|
+
pop.addEventListener('keydown', function (e) {
|
|
3356
|
+
var items = Array.prototype.slice.call(pop.querySelectorAll('button'));
|
|
3357
|
+
var at = items.indexOf(document.activeElement);
|
|
3358
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
3359
|
+
e.preventDefault();
|
|
3360
|
+
var next = (at + (e.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
|
3361
|
+
items[next].focus();
|
|
3362
|
+
} else if (e.key === 'Escape') {
|
|
3363
|
+
e.preventDefault();
|
|
3364
|
+
closeMenu();
|
|
3365
|
+
src.focus();
|
|
3366
|
+
}
|
|
3367
|
+
});
|
|
3368
|
+
|
|
3369
|
+
return wrap;
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
bindMenu('btn-export', 'export-menu', function (btn) {
|
|
3373
|
+
closeMenu();
|
|
3374
|
+
runExport(btn.dataset.export);
|
|
3375
|
+
}, 'button[data-export]');
|
|
3376
|
+
|
|
3377
|
+
// The font menu stays open across picks: choosing a heading face and then a
|
|
3378
|
+
// body face is one decision, and closing between them would fight the user.
|
|
3379
|
+
bindMenu('btn-font', 'font-menu', function (btn) {
|
|
3380
|
+
setFont(btn.dataset.slot, btn.dataset.font || null, true);
|
|
3381
|
+
}, 'button[data-slot]');
|
|
3382
|
+
|
|
3383
|
+
document.addEventListener('click', function (e) {
|
|
3384
|
+
if (openMenu && !openMenu.contains(e.target)) closeMenu();
|
|
3385
|
+
});
|
|
3386
|
+
|
|
3387
|
+
function runExport(kind) {
|
|
3388
|
+
if (kind === 'md') download();
|
|
3389
|
+
else if (kind === 'pdf') exportPdf();
|
|
3390
|
+
else if (kind === 'html') exportHtml();
|
|
3391
|
+
}
|
|
3392
|
+
$('btn-theme').addEventListener('click', function () { runAction('theme'); });
|
|
3393
|
+
|
|
3394
|
+
// The picker owns the arrow keys while it is up, and Enter keeps the preview.
|
|
3395
|
+
document.addEventListener('keydown', function (e) {
|
|
3396
|
+
if (!themesOpen() || e.metaKey || e.ctrlKey || e.altKey) return;
|
|
3397
|
+
var cols = 0;
|
|
3398
|
+
if (themeCards.length) {
|
|
3399
|
+
var grid = themeCards[themeSel].parentNode;
|
|
3400
|
+
cols = Math.max(1, Math.round(grid.clientWidth / themeCards[themeSel].offsetWidth));
|
|
3401
|
+
}
|
|
3402
|
+
var step = 0;
|
|
3403
|
+
if (e.key === 'ArrowRight') step = 1;
|
|
3404
|
+
else if (e.key === 'ArrowLeft') step = -1;
|
|
3405
|
+
else if (e.key === 'ArrowDown') step = cols;
|
|
3406
|
+
else if (e.key === 'ArrowUp') step = -cols;
|
|
3407
|
+
else if (e.key === 'Enter') { e.preventDefault(); commitTheme(); return; }
|
|
3408
|
+
else if (e.key === 'Escape') { e.preventDefault(); closeOverlays(); return; }
|
|
3409
|
+
else if (e.key === '[' || e.key === '-') { e.preventDefault(); nudgeSize(-1); return; }
|
|
3410
|
+
else if (e.key === ']' || e.key === '+' || e.key === '=') { e.preventDefault(); nudgeSize(1); return; }
|
|
3411
|
+
else return;
|
|
3412
|
+
e.preventDefault();
|
|
3413
|
+
var next = (themeSel + step + themeCards.length) % themeCards.length;
|
|
3414
|
+
selectTheme(next);
|
|
3415
|
+
themeCards[next].scrollIntoView({ block: 'nearest' });
|
|
3416
|
+
}, true);
|
|
3417
|
+
$('btn-present').addEventListener('click', present);
|
|
3418
|
+
$('btn-prev').addEventListener('click', function () { setIndex(state.index - 1, true); });
|
|
3419
|
+
$('btn-next').addEventListener('click', function () { setIndex(state.index + 1, true); });
|
|
3420
|
+
$('seg-single').addEventListener('click', function () { setMode('single'); });
|
|
3421
|
+
$('seg-grid').addEventListener('click', function () { setMode('grid'); });
|
|
3422
|
+
|
|
3423
|
+
// ── Global keys ────────────────────────────────────────────────────────
|
|
3424
|
+
document.addEventListener('keydown', function (e) {
|
|
3425
|
+
var mod = e.metaKey || e.ctrlKey;
|
|
3426
|
+
|
|
3427
|
+
if (themesOpen()) return;
|
|
3428
|
+
|
|
3429
|
+
if ($('screen-start').classList.contains('is-on')) {
|
|
3430
|
+
if (e.key === 'Escape') { e.preventDefault(); hideStartScreen(true); }
|
|
3431
|
+
return;
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
if ($('library').classList.contains('is-on') && !mod) {
|
|
3435
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
3436
|
+
e.preventDefault();
|
|
3437
|
+
if (libRows.length) {
|
|
3438
|
+
libSel = (libSel + (e.key === 'ArrowDown' ? 1 : -1) + libRows.length) % libRows.length;
|
|
3439
|
+
renderLibrary();
|
|
3440
|
+
var sel = $('lib-list').querySelector('.lib-row.is-sel');
|
|
3441
|
+
if (sel) sel.scrollIntoView({ block: 'nearest' });
|
|
3442
|
+
}
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
if (e.key === 'Enter') {
|
|
3446
|
+
e.preventDefault();
|
|
3447
|
+
if (libRows[libSel]) { closeOverlays(); openDeck(libRows[libSel].id, true); }
|
|
3448
|
+
return;
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
if (e.key === 'Escape') {
|
|
3453
|
+
if (menuIsOpen()) { e.preventDefault(); closeMenu(); src.focus(); return; }
|
|
3454
|
+
if (palette.classList.contains('is-on') || guide.classList.contains('is-on') ||
|
|
3455
|
+
$('library').classList.contains('is-on')) {
|
|
3456
|
+
e.preventDefault();
|
|
3457
|
+
closeOverlays();
|
|
3458
|
+
}
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3461
|
+
if (!mod) return;
|
|
3462
|
+
|
|
3463
|
+
var key = e.key.toLowerCase();
|
|
3464
|
+
|
|
3465
|
+
var md = state.kind === 'markdown';
|
|
3466
|
+
|
|
3467
|
+
if (key === 'k' && !e.shiftKey && md) { e.preventDefault(); openPalette(); }
|
|
3468
|
+
else if (key === 'o') { e.preventDefault(); openLibrary(); }
|
|
3469
|
+
else if (key === '/' && md) { e.preventDefault(); runAction('guide'); }
|
|
3470
|
+
else if (key === 's' && e.shiftKey) { e.preventDefault(); exportPdf(); }
|
|
3471
|
+
else if (key === 's') { e.preventDefault(); saveNow(); download(); }
|
|
3472
|
+
else if (key === 'enter') { e.preventDefault(); present(); }
|
|
3473
|
+
else if (key === 'd' && md) { e.preventDefault(); insertSnippet(bySnippetId['slide-break']); }
|
|
3474
|
+
else if (key === 'b' && md) { e.preventDefault(); insertSnippet(bySnippetId.bold); }
|
|
3475
|
+
else if (key === 'i' && md) { e.preventDefault(); insertSnippet(bySnippetId.italic); }
|
|
3476
|
+
else if (key === 'e' && md) { e.preventDefault(); insertSnippet(bySnippetId['inline-code']); }
|
|
3477
|
+
else if (key === 'g' && md) { e.preventDefault(); runAction('grid'); }
|
|
3478
|
+
else if (key === 'l' && e.shiftKey) { e.preventDefault(); runAction('theme'); }
|
|
3479
|
+
});
|
|
3480
|
+
|
|
3481
|
+
// Alt + up/down hops between slides.
|
|
3482
|
+
document.addEventListener('keydown', function (e) {
|
|
3483
|
+
if (!e.altKey || e.metaKey || e.ctrlKey) return;
|
|
3484
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); setIndex(state.index + 1, true); }
|
|
3485
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); setIndex(state.index - 1, true); }
|
|
3486
|
+
});
|
|
3487
|
+
|
|
3488
|
+
// ── Split pane ─────────────────────────────────────────────────────────
|
|
3489
|
+
var splitPct = parseFloat(lsGet(K.split, '48'));
|
|
3490
|
+
if (!(splitPct > 15 && splitPct < 85)) splitPct = 48;
|
|
3491
|
+
|
|
3492
|
+
function applySplit() {
|
|
3493
|
+
paneEdit.style.flex = '0 0 ' + splitPct + '%';
|
|
3494
|
+
layout();
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
divider.addEventListener('mousedown', function (e) {
|
|
3498
|
+
e.preventDefault();
|
|
3499
|
+
divider.classList.add('is-dragging');
|
|
3500
|
+
document.body.style.cursor = 'col-resize';
|
|
3501
|
+
|
|
3502
|
+
function move(ev) {
|
|
3503
|
+
var box = $('panes').getBoundingClientRect();
|
|
3504
|
+
var pct = ((ev.clientX - box.left) / box.width) * 100;
|
|
3505
|
+
splitPct = Math.max(18, Math.min(82, pct));
|
|
3506
|
+
applySplit();
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
function up() {
|
|
3510
|
+
divider.classList.remove('is-dragging');
|
|
3511
|
+
document.body.style.cursor = '';
|
|
3512
|
+
lsSet(K.split, String(Math.round(splitPct)));
|
|
3513
|
+
document.removeEventListener('mousemove', move);
|
|
3514
|
+
document.removeEventListener('mouseup', up);
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
document.addEventListener('mousemove', move);
|
|
3518
|
+
document.addEventListener('mouseup', up);
|
|
3519
|
+
});
|
|
3520
|
+
|
|
3521
|
+
divider.addEventListener('dblclick', function () {
|
|
3522
|
+
splitPct = 48;
|
|
3523
|
+
lsSet(K.split, '48');
|
|
3524
|
+
applySplit();
|
|
3525
|
+
});
|
|
3526
|
+
|
|
3527
|
+
window.addEventListener('resize', layout);
|
|
3528
|
+
|
|
3529
|
+
// ── Frame messages ─────────────────────────────────────────────────────
|
|
3530
|
+
window.addEventListener('message', function (e) {
|
|
3531
|
+
var m = e.data || {};
|
|
3532
|
+
if (m.type === 'ready') {
|
|
3533
|
+
// pushLook and pushFrame between them send the whole of the current
|
|
3534
|
+
// state, so a message dropped while the frame was loading is already
|
|
3535
|
+
// superseded by the time it is up.
|
|
3536
|
+
state.frameReady = true;
|
|
3537
|
+
pushLook();
|
|
3538
|
+
pushFrame();
|
|
3539
|
+
} else if (m.type === 'goto') {
|
|
3540
|
+
setMode('single');
|
|
3541
|
+
setIndex(m.index, true);
|
|
3542
|
+
} else if (m.type === 'overflow') {
|
|
3543
|
+
var had = !!state.overflow[m.index];
|
|
3544
|
+
state.overflow[m.index] = m.overflow;
|
|
3545
|
+
if (had !== m.overflow) evalNudges();
|
|
3546
|
+
}
|
|
3547
|
+
});
|
|
3548
|
+
|
|
3549
|
+
// ── Boot ───────────────────────────────────────────────────────────────
|
|
3550
|
+
state.bootAt = Date.now();
|
|
3551
|
+
|
|
3552
|
+
// Carry over a deck saved before the library existed.
|
|
3553
|
+
var legacy = lsGet(K.oldDoc, null);
|
|
3554
|
+
if (legacy !== null && !loadIndex().length) {
|
|
3555
|
+
createDeck(lsGet(K.oldName, 'deck'), legacy, 'markdown');
|
|
3556
|
+
lsDel(K.oldDoc);
|
|
3557
|
+
lsDel(K.oldName);
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
document.documentElement.dataset.theme = state.theme;
|
|
3561
|
+
document.documentElement.dataset.decor = THEME_BY_ID[state.theme].decor;
|
|
3562
|
+
buildSizeSeg('topbar-size', false);
|
|
3563
|
+
buildFontMenu();
|
|
3564
|
+
paintSizeSeg();
|
|
3565
|
+
paintThemeButton();
|
|
3566
|
+
$('seg-single').classList.toggle('is-on', state.mode === 'single');
|
|
3567
|
+
$('seg-grid').classList.toggle('is-on', state.mode === 'grid');
|
|
3568
|
+
|
|
3569
|
+
Array.prototype.forEach.call(document.querySelectorAll('.btn kbd, .menu__pop kbd'), function (el) {
|
|
3570
|
+
el.textContent = el.textContent.replace('Cmd', CMD);
|
|
3571
|
+
});
|
|
3572
|
+
|
|
3573
|
+
buildGuide();
|
|
3574
|
+
startTips();
|
|
3575
|
+
applySplit();
|
|
3576
|
+
paintExportMenu();
|
|
3577
|
+
updateDeckCount();
|
|
3578
|
+
|
|
3579
|
+
// A deck to resume opens with zero clicks, exactly as before. Only a true
|
|
3580
|
+
// first run — nothing in the library at all — shows the start screen.
|
|
3581
|
+
var index = loadIndex();
|
|
3582
|
+
if (index.length === 0) {
|
|
3583
|
+
showStartScreen();
|
|
3584
|
+
} else {
|
|
3585
|
+
var wanted = lsGet(K.current, null);
|
|
3586
|
+
openDeck(findDeck(wanted) ? wanted : index[0].id);
|
|
3587
|
+
}
|
|
3588
|
+
})();
|
|
3589
|
+
</script>
|
|
3590
|
+
</body>
|
|
3591
|
+
</html>`;
|
|
3592
|
+
}
|