deckrun 1.3.0 → 1.6.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/dist/editor.js CHANGED
@@ -2,7 +2,8 @@ import { RESET_CSS } from "./generate.js";
2
2
  import { DEFAULT_SIZE, DEFAULT_THEME, decorOf, findFont, fontSummaries, googleFontsHref, resolveSizeName, sizeSummaries, themeSummaries, themeSwitchableCss, } from "./themes.js";
3
3
  import { SNIPPETS, SNIPPET_GROUPS, TIPS, WELCOME_DECK, } from "./editor-content.js";
4
4
  import { PREVIEW_WIDTH, PREVIEW_HEIGHT } from "./preview.js";
5
- function bootstrapJson(theme, size, fonts) {
5
+ import { DEFAULT_TEMPLATE, DEFAULT_TRANSITION, resolveTemplateName, resolveTransitionName, templateSummaries, transitionSummaries, } from "./presentation-options.js";
6
+ function bootstrapJson(theme, size, fonts, template, transition) {
6
7
  const payload = {
7
8
  theme,
8
9
  themes: themeSummaries(),
@@ -10,6 +11,10 @@ function bootstrapJson(theme, size, fonts) {
10
11
  sizes: sizeSummaries(),
11
12
  fonts,
12
13
  faces: fontSummaries(),
14
+ template,
15
+ templates: templateSummaries(),
16
+ transition,
17
+ transitions: transitionSummaries(),
13
18
  width: PREVIEW_WIDTH,
14
19
  height: PREVIEW_HEIGHT,
15
20
  groups: SNIPPET_GROUPS,
@@ -21,15 +26,17 @@ function bootstrapJson(theme, size, fonts) {
21
26
  return JSON.stringify(payload).replace(/</g, "\\u003c");
22
27
  }
23
28
  /** The Markdown editor served when `deckrun` is launched without a file. */
24
- export function generateEditorHtml(theme = DEFAULT_THEME, sizeInput = DEFAULT_SIZE, fontInput = {}) {
29
+ export function generateEditorHtml(theme = DEFAULT_THEME, sizeInput = DEFAULT_SIZE, fontInput = {}, templateInput = DEFAULT_TEMPLATE, transitionInput = DEFAULT_TRANSITION) {
25
30
  const size = resolveSizeName(sizeInput);
31
+ const template = resolveTemplateName(templateInput);
32
+ const transition = resolveTransitionName(transitionInput);
26
33
  const fonts = { head: findFont(fontInput.head), body: findFont(fontInput.body) };
27
34
  return `<!DOCTYPE html>
28
- <html lang="en" data-theme="${theme}" data-decor="${decorOf(theme)}">
35
+ <html lang="en" data-theme="${theme}" data-decor="${decorOf(theme)}" data-size="${size}" data-template="${template}" data-transition="${transition}">
29
36
  <head>
30
37
  <meta charset="UTF-8">
31
38
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
32
- <title>deckrun editor</title>
39
+ <title>deckrun · editor</title>
33
40
  <link rel="preconnect" href="https://fonts.googleapis.com">
34
41
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
35
42
  <link href="${googleFontsHref()}" rel="stylesheet">
@@ -50,7 +57,8 @@ html, body {
50
57
  }
51
58
 
52
59
  button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
53
- ::selection { background: var(--surface1); }
60
+ ::selection { background: var(--selection-bg, var(--accent-line, rgba(56, 139, 253, 0.35))); color: var(--selection-text, inherit); }
61
+ ::-moz-selection { background: var(--selection-bg, var(--accent-line, rgba(56, 139, 253, 0.35))); color: var(--selection-text, inherit); }
54
62
 
55
63
  ::-webkit-scrollbar { width: 9px; height: 9px; }
56
64
  ::-webkit-scrollbar-track { background: transparent; }
@@ -305,21 +313,30 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
305
313
  position: absolute;
306
314
  inset: 0;
307
315
  margin: 0;
308
- padding: 18px 20px 45vh 20px;
316
+ padding: 18px 20px;
309
317
  font-family: var(--font-mono);
310
318
  font-size: 13.5px;
311
319
  font-weight: 400;
320
+ font-style: normal;
312
321
  line-height: 1.75;
313
322
  letter-spacing: 0;
323
+ word-spacing: 0;
314
324
  white-space: pre-wrap;
315
325
  overflow-wrap: break-word;
316
326
  word-break: break-word;
317
327
  tab-size: 2;
318
328
  border: 0;
329
+ box-sizing: border-box;
319
330
  background: transparent;
331
+ scrollbar-width: none;
332
+ -ms-overflow-style: none;
333
+ }
334
+
335
+ #hl::-webkit-scrollbar, #src::-webkit-scrollbar {
336
+ display: none;
320
337
  }
321
338
 
322
- #hl { z-index: 1; overflow: hidden; pointer-events: none; color: var(--subtext0); counter-reset: sld 1; }
339
+ #hl { z-index: 1; overflow: hidden; pointer-events: none; color: var(--subtext0); }
323
340
  #measure { z-index: 0; visibility: hidden; overflow: hidden; }
324
341
 
325
342
  #src {
@@ -332,38 +349,29 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
332
349
  outline: none;
333
350
  }
334
351
 
335
- #src::selection { background: var(--surface1); color: transparent; }
352
+ #src::selection { background: var(--selection-bg, var(--accent-line, rgba(56, 139, 253, 0.35))); color: transparent; }
353
+ #src::-moz-selection { background: var(--selection-bg, var(--accent-line, rgba(56, 139, 253, 0.35))); color: transparent; }
336
354
 
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; }
355
+ /* Markdown tokens — color only to guarantee pixel-identical alignment */
356
+ .t-h1 { color: var(--accent); }
357
+ .t-h2 { color: var(--accent-2); }
358
+ .t-h3 { color: var(--accent-3); }
341
359
  .t-h4 { color: var(--teal); }
342
- .t-strong { color: var(--peach); font-weight: 600; }
343
- .t-em { color: var(--subtext1); font-style: italic; }
360
+ .t-strong { color: var(--peach); }
361
+ .t-em { color: var(--subtext1); }
344
362
  .t-code { color: var(--green); }
345
363
  .t-fence { color: var(--overlay1); }
346
364
  .t-codeline { color: var(--subtext1); }
347
- .t-quote { color: var(--subtext0); font-style: italic; }
365
+ .t-quote { color: var(--subtext0); }
348
366
  .t-marker { color: var(--accent); }
349
367
  .t-link { color: var(--blue); }
350
368
  .t-url { color: var(--overlay0); }
351
369
  .t-img { color: var(--sapphire); }
352
- .t-dir { color: var(--yellow); font-weight: 600; }
353
- .t-note { color: var(--overlay0); font-style: italic; }
370
+ .t-dir { color: var(--yellow); }
371
+ .t-note { color: var(--overlay0); }
354
372
  .t-html { color: var(--pink); }
355
373
  .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
- }
374
+ .t-sep { color: var(--accent); }
367
375
 
368
376
  /* ── Drop target ──────────────────────────────────────────────────────── */
369
377
  #pane-edit.is-dropping::after {
@@ -382,56 +390,6 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
382
390
  pointer-events: none;
383
391
  }
384
392
 
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
393
  /* ── Preview ──────────────────────────────────────────────────────────── */
436
394
  #prev-head {
437
395
  display: flex;
@@ -445,6 +403,22 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
445
403
  color: var(--overlay1);
446
404
  }
447
405
 
406
+ #overflow-badge {
407
+ display: none;
408
+ align-items: center;
409
+ gap: 5px;
410
+ padding: 2px 7px;
411
+ border: 1px solid var(--yellow);
412
+ border-radius: 999px;
413
+ color: var(--yellow);
414
+ background: var(--surface-soft);
415
+ font-size: 9px;
416
+ letter-spacing: 0.1em;
417
+ text-transform: uppercase;
418
+ }
419
+
420
+ #overflow-badge.is-on { display: inline-flex; }
421
+
448
422
  /* ── Dropdown menu ────────────────────────────────────────────────────── */
449
423
  .menu { position: relative; display: inline-flex; }
450
424
  .menu__chev { font-size: 9px; color: var(--overlay0); }
@@ -494,6 +468,21 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
494
468
  white-space: nowrap;
495
469
  }
496
470
 
471
+ .menu__pop--template { min-width: 390px; padding: 7px; }
472
+ .menu-section + .menu-section { margin-top: 6px; padding-top: 7px; border-top: 1px solid var(--surface0); }
473
+ .menu-section__title {
474
+ padding: 2px 10px 5px;
475
+ font-size: 9px;
476
+ letter-spacing: 0.16em;
477
+ text-transform: uppercase;
478
+ color: var(--overlay0);
479
+ }
480
+ .menu__pop--template button.is-on {
481
+ background: var(--accent-soft);
482
+ border-left-color: var(--accent);
483
+ }
484
+ .menu__pop--template button.is-on .menu__name { color: var(--accent); }
485
+
497
486
  .seg { display: flex; border: 1px solid var(--surface0); border-radius: 7px; overflow: hidden; }
498
487
  .seg button { font-size: 11px; padding: 3px 10px; color: var(--overlay1); }
499
488
  .seg button.is-on { background: var(--surface0); color: var(--text); }
@@ -580,6 +569,19 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
580
569
  #tipbar button { color: var(--overlay0); padding: 0 3px; }
581
570
  #tipbar button:hover { color: var(--text); }
582
571
 
572
+ #follow-chip {
573
+ flex: 0 0 auto;
574
+ border: 1px solid var(--surface0);
575
+ border-radius: 5px;
576
+ padding: 1px 7px;
577
+ color: var(--accent2);
578
+ cursor: pointer;
579
+ user-select: none;
580
+ }
581
+ #follow-chip:hover { border-color: var(--accent2); }
582
+ #follow-chip b { font-weight: 600; }
583
+ #follow-chip.hidden { display: none; }
584
+
583
585
  /* ── Command palette ──────────────────────────────────────────────────── */
584
586
  #palette, #guide, #library { position: fixed; inset: 0; z-index: 40; display: none; }
585
587
  #palette.is-on, #guide.is-on, #library.is-on { display: block; }
@@ -1065,7 +1067,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1065
1067
  }
1066
1068
 
1067
1069
  [data-doc-kind="html"] #btn-guide,
1068
- [data-doc-kind="html"] #btn-palette {
1070
+ [data-doc-kind="html"] #btn-palette,
1071
+ [data-doc-kind="html"] #template-wrap {
1069
1072
  display: none !important;
1070
1073
  }
1071
1074
 
@@ -1118,6 +1121,30 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1118
1121
  }
1119
1122
  .start-card__acts button:hover { border-color: var(--accent); color: var(--text); }
1120
1123
 
1124
+ .start-options {
1125
+ margin-top: 12px;
1126
+ padding-top: 11px;
1127
+ border-top: 1px solid var(--surface0);
1128
+ }
1129
+ .start-options__row { display: flex; align-items: center; gap: 8px; margin-top: 7px; }
1130
+ .start-options__label {
1131
+ flex: 0 0 72px;
1132
+ font-size: 9px;
1133
+ letter-spacing: 0.12em;
1134
+ text-transform: uppercase;
1135
+ color: var(--overlay0);
1136
+ }
1137
+ .start-options__choices { display: flex; flex-wrap: wrap; gap: 5px; }
1138
+ .start-choice {
1139
+ padding: 4px 8px;
1140
+ font-size: 10.5px;
1141
+ color: var(--subtext0);
1142
+ border: 1px solid var(--surface1);
1143
+ border-radius: 6px;
1144
+ }
1145
+ .start-choice:hover { color: var(--text); border-color: var(--accent); }
1146
+ .start-choice.is-on { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
1147
+
1121
1148
  .start-card__url { display: flex; gap: 6px; flex: 1 1 auto; min-width: 0; }
1122
1149
  .start-card__url input {
1123
1150
  flex: 1 1 auto;
@@ -1173,6 +1200,21 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1173
1200
  </button>
1174
1201
  </div>
1175
1202
  </span>
1203
+ <span class="menu" id="template-wrap">
1204
+ <button class="btn" id="btn-template" aria-haspopup="true" aria-expanded="false" title="Composition template and slide transition">
1205
+ <span id="template-label">template</span> <span class="menu__chev">&#9662;</span>
1206
+ </button>
1207
+ <div class="menu__pop menu__pop--template" id="template-menu">
1208
+ <div class="menu-section">
1209
+ <div class="menu-section__title">template</div>
1210
+ <div id="template-list"></div>
1211
+ </div>
1212
+ <div class="menu-section">
1213
+ <div class="menu-section__title">transition</div>
1214
+ <div id="transition-list"></div>
1215
+ </div>
1216
+ </div>
1217
+ </span>
1176
1218
  <button class="btn" id="btn-theme" title="Pick a theme">
1177
1219
  <span class="th-dot" id="theme-dot"></span>
1178
1220
  <span id="theme-label">theme</span>
@@ -1208,16 +1250,6 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1208
1250
  <pre id="measure" aria-hidden="true"></pre>
1209
1251
  <textarea id="src" spellcheck="false" autocomplete="off" autocapitalize="off" wrap="soft" aria-label="Markdown source"></textarea>
1210
1252
  </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">&times;</button>
1219
- </div>
1220
- </div>
1221
1253
  <textarea id="src-html" spellcheck="false" autocomplete="off" autocapitalize="off" wrap="off" aria-label="HTML source"></textarea>
1222
1254
  </section>
1223
1255
 
@@ -1228,6 +1260,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1228
1260
  <button class="step" id="btn-prev" title="Previous slide">&#8592;</button>
1229
1261
  <span id="prev-count">slide 0 / 0</span>
1230
1262
  <button class="step" id="btn-next" title="Next slide">&#8594;</button>
1263
+ <span id="overflow-badge" title="This slide is clipped. Shorten it or split it with three dashes.">overflow</span>
1231
1264
  <span class="spacer"></span>
1232
1265
  <span id="prev-scale"></span>
1233
1266
  <div class="seg">
@@ -1253,6 +1286,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1253
1286
  <span id="words">0 words</span>
1254
1287
  <span id="chip-slides">0 slides</span>
1255
1288
  <span id="save-state"></span>
1289
+ <span id="follow-chip" class="hidden" title="The editor is mirroring the presented deck. Click to stop following.">following deck &middot; <b id="follow-slide">1</b></span>
1256
1290
  <span id="tipbar">
1257
1291
  <span class="tag">tip</span>
1258
1292
  <span id="tip-text"></span>
@@ -1336,6 +1370,16 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1336
1370
  <button id="start-md-blank">start blank</button>
1337
1371
  <button id="start-md-upload">upload .md</button>
1338
1372
  </span>
1373
+ <div class="start-options">
1374
+ <div class="start-options__row">
1375
+ <span class="start-options__label">template</span>
1376
+ <span class="start-options__choices" id="start-template-list"></span>
1377
+ </div>
1378
+ <div class="start-options__row">
1379
+ <span class="start-options__label">transition</span>
1380
+ <span class="start-options__choices" id="start-transition-list"></span>
1381
+ </div>
1382
+ </div>
1339
1383
  </div>
1340
1384
  <div class="start-card" id="start-html-card">
1341
1385
  <span class="start-card__title">HTML document</span>
@@ -1359,7 +1403,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1359
1403
  <div id="toasts"></div>
1360
1404
  <input type="file" id="file-any" accept=".md,.markdown,text/markdown,text/plain,.html,.htm,text/html" hidden>
1361
1405
 
1362
- <script type="application/json" id="bootstrap">${bootstrapJson(theme, size, fonts)}</script>
1406
+ <script type="application/json" id="bootstrap">${bootstrapJson(theme, size, fonts, template, transition)}</script>
1363
1407
  <script>
1364
1408
  (function () {
1365
1409
  'use strict';
@@ -1376,6 +1420,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1376
1420
  size: 'deckrun.size.v1',
1377
1421
  head: 'deckrun.font.head.v1',
1378
1422
  body: 'deckrun.font.body.v1',
1423
+ template:'deckrun.template.v1',
1424
+ transition:'deckrun.transition.v1',
1379
1425
  split: 'deckrun.split.v1',
1380
1426
  mode: 'deckrun.mode.v1',
1381
1427
  nudge: 'deckrun.nudges.v1',
@@ -1460,11 +1506,20 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1460
1506
  }
1461
1507
 
1462
1508
  /** Returns the new deck's id, or null if this browser refused to store it. */
1463
- function createDeck(name, content, kind) {
1509
+ function createDeck(name, content, kind, template, transition) {
1464
1510
  var id = newDeckId();
1465
1511
  if (!lsSet(K.deck + id, content)) return null;
1466
1512
  var list = loadIndex();
1467
- list.unshift({ id: id, name: name || 'untitled', kind: kind || 'markdown', slides: 0, chars: content.length, at: Date.now() });
1513
+ list.unshift({
1514
+ id: id,
1515
+ name: name || 'untitled',
1516
+ kind: kind || 'markdown',
1517
+ slides: 0,
1518
+ chars: content.length,
1519
+ template: template || (typeof state !== 'undefined' && state.template ? state.template : D.template),
1520
+ transition: transition || (typeof state !== 'undefined' && state.transition ? state.transition : D.transition),
1521
+ at: Date.now()
1522
+ });
1468
1523
  if (!saveIndex(list)) { lsDel(K.deck + id); return null; }
1469
1524
  return id;
1470
1525
  }
@@ -1488,6 +1543,19 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1488
1543
  return days + 'd ago';
1489
1544
  }
1490
1545
 
1546
+ function updateDocTitle() {
1547
+ if ($('screen-start') && $('screen-start').classList.contains('is-on')) {
1548
+ document.title = 'deckrun \u00b7 start';
1549
+ return;
1550
+ }
1551
+ if ($('library') && $('library').classList.contains('is-on')) {
1552
+ document.title = 'deckrun \u00b7 library';
1553
+ return;
1554
+ }
1555
+ var name = ($('docname').value || '').trim();
1556
+ document.title = name ? name + ' \u00b7 deckrun' : 'deckrun \u00b7 editor';
1557
+ }
1558
+
1491
1559
  /** One deck's metadata is refreshed from the editor on every save. */
1492
1560
  function touchCurrent() {
1493
1561
  var list = loadIndex();
@@ -1495,6 +1563,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1495
1563
  if (list[i].id === state.deckId) {
1496
1564
  list[i].name = $('docname').value.trim() || 'untitled';
1497
1565
  list[i].kind = state.kind;
1566
+ list[i].template = state.template;
1567
+ list[i].transition = state.transition;
1498
1568
  if (state.kind === 'html') {
1499
1569
  list[i].slides = 0;
1500
1570
  list[i].chars = srcHtml.value.length;
@@ -1506,6 +1576,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1506
1576
  break;
1507
1577
  }
1508
1578
  }
1579
+ updateDocTitle();
1509
1580
  return saveIndex(list);
1510
1581
  }
1511
1582
 
@@ -1560,6 +1631,22 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1560
1631
  return SIZE_BY_ID[id] ? id : (SIZE_BY_ID[D.size] ? D.size : 'm');
1561
1632
  }
1562
1633
 
1634
+ var TEMPLATES = D.templates;
1635
+ var TEMPLATE_BY_ID = {};
1636
+ TEMPLATES.forEach(function (item) { TEMPLATE_BY_ID[item.id] = item; });
1637
+
1638
+ function pickTemplate(id) {
1639
+ return TEMPLATE_BY_ID[id] ? id : (TEMPLATE_BY_ID[D.template] ? D.template : 'classic');
1640
+ }
1641
+
1642
+ var TRANSITIONS = D.transitions;
1643
+ var TRANSITION_BY_ID = {};
1644
+ TRANSITIONS.forEach(function (item) { TRANSITION_BY_ID[item.id] = item; });
1645
+
1646
+ function pickTransition(id) {
1647
+ return TRANSITION_BY_ID[id] ? id : (TRANSITION_BY_ID[D.transition] ? D.transition : 'slide');
1648
+ }
1649
+
1563
1650
  var FACES = D.faces;
1564
1651
  var FACE_BY_ID = {};
1565
1652
  FACES.forEach(function (f) { FACE_BY_ID[f.id] = f; });
@@ -1577,7 +1664,6 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1577
1664
  var elChipSlides = $('chip-slides'), elSave = $('save-state'), elPos = $('pos'), elWords = $('words');
1578
1665
  var elCount = $('prev-count'), elScale = $('prev-scale');
1579
1666
  var elNotes = $('notes'), elNotesText = $('notes__text');
1580
- var elNudge = $('nudge'), elNudgeText = $('nudge__text'), elNudgeDo = $('nudge-do'), elNudgeX = $('nudge-x');
1581
1667
  var palette = $('palette'), palInput = $('pal-input'), palList = $('pal-list');
1582
1668
  var guide = $('guide'), guideBody = $('guide-body');
1583
1669
  var srcHtml = $('src-html'), frameHtml = $('frame-html');
@@ -1594,6 +1680,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1594
1680
  size: pickSize(lsGet(K.size, D.size)),
1595
1681
  head: pickFont(lsGet(K.head, D.fonts.head)),
1596
1682
  body: pickFont(lsGet(K.body, D.fonts.body)),
1683
+ template: pickTemplate(lsGet(K.template, D.template)),
1684
+ transition: pickTransition(lsGet(K.transition, D.transition)),
1597
1685
  frameReady: false,
1598
1686
  overflow: {},
1599
1687
  dismissed: {},
@@ -1609,6 +1697,75 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1609
1697
  if (saved && saved.length) saved.forEach(function (id) { state.dismissed[id] = true; });
1610
1698
  } catch (e) {}
1611
1699
 
1700
+ // ── Following the presented deck ───────────────────────────────────────
1701
+ // Presenting hands the deck tab this editor's session id (ps=... in the
1702
+ // URL), and the editor listens on that same channel: the preview and the
1703
+ // notes panel mirror the deck's current slide, so the editor window is the
1704
+ // presenter's screen and the deck tab is the projector. No second window
1705
+ // is involved.
1706
+ var FOLLOW_KEY = 'deckrun.presenter.session';
1707
+ var followSid = null;
1708
+ try { followSid = sessionStorage.getItem(FOLLOW_KEY); } catch (e) {}
1709
+ if (!followSid) {
1710
+ followSid = (window.crypto && crypto.randomUUID)
1711
+ ? crypto.randomUUID()
1712
+ : 'e' + Date.now().toString(36) + Math.random().toString(36).slice(2);
1713
+ try { sessionStorage.setItem(FOLLOW_KEY, followSid); } catch (e) {}
1714
+ }
1715
+ var followChan = (typeof BroadcastChannel !== 'undefined')
1716
+ ? new BroadcastChannel('deckrun:' + followSid)
1717
+ : null;
1718
+
1719
+ var following = false; // the editor is mirroring the deck right now
1720
+ var expectingDeck = false; // a present just went out: follow the deck that answers
1721
+ var deckNotes = []; // the deck's own notes, authoritative while following
1722
+ var followChipEl = $('follow-chip');
1723
+ var followSlideEl = $('follow-slide');
1724
+
1725
+ /** The editor drives the deck as well as the preview: wherever the
1726
+ presenter lands in the editor, the deck goes there too. The deck echoes
1727
+ its state back over the same channel, which is what keeps both in step. */
1728
+ function announceIndex(i) {
1729
+ if (followChan) followChan.postMessage({ id: followSid, type: 'goto', index: i });
1730
+ }
1731
+
1732
+ function followDeck(i) {
1733
+ var n = state.slides.length;
1734
+ if (!n) return;
1735
+ i = Math.max(0, Math.min(n - 1, i));
1736
+ var changed = i !== state.index;
1737
+ state.index = i;
1738
+ state.notes = deckNotes;
1739
+ followSlideEl.textContent = String(i + 1);
1740
+ if (changed) pushFrame();
1741
+ renderCounts();
1742
+ }
1743
+
1744
+ if (followChan) {
1745
+ followChan.onmessage = function (e) {
1746
+ var m = e.data || {};
1747
+ if (m.id !== followSid) return;
1748
+ if (m.type === 'init') {
1749
+ deckNotes = m.notes || [];
1750
+ if (expectingDeck) {
1751
+ expectingDeck = false;
1752
+ following = true;
1753
+ }
1754
+ followChipEl.classList.toggle('hidden', !following);
1755
+ } else if (m.type === 'state') {
1756
+ if (following) followDeck(m.index);
1757
+ }
1758
+ };
1759
+ setInterval(function () {
1760
+ followChan.postMessage({ id: followSid, type: 'ready' });
1761
+ }, 2000);
1762
+ }
1763
+
1764
+ followChipEl.addEventListener('click', function () {
1765
+ following = false;
1766
+ followChipEl.classList.add('hidden');
1767
+ });
1768
+
1612
1769
  // ── Toasts ─────────────────────────────────────────────────────────────
1613
1770
  function toast(message, kind, actionLabel, action) {
1614
1771
  var el = document.createElement('div');
@@ -1701,7 +1858,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1701
1858
  out.push('<span class="t-sep">' + esc(line) + '</span>');
1702
1859
  continue;
1703
1860
  }
1704
- if (/^\\s*&lt;!--/.test(esc(line)) || /^\\s*<!--/.test(line)) {
1861
+ if (/^\\s*<!--/.test(line)) {
1705
1862
  out.push('<span class="t-note">' + esc(line) + '</span>');
1706
1863
  continue;
1707
1864
  }
@@ -1712,7 +1869,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1712
1869
  out.push('<span class="t-h' + lvl + '">' + inlineTokens(line) + '</span>');
1713
1870
  continue;
1714
1871
  }
1715
- if (/^\\s*&gt;/.test(esc(line))) {
1872
+ if (/^\\s*>/.test(line)) {
1716
1873
  out.push('<span class="t-quote">' + inlineTokens(line) + '</span>');
1717
1874
  continue;
1718
1875
  }
@@ -1816,6 +1973,10 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1816
1973
  elCount.textContent = 'slide ' + (n ? state.index + 1 : 0) + ' / ' + n;
1817
1974
  $('btn-prev').disabled = state.index <= 0;
1818
1975
  $('btn-next').disabled = state.index >= n - 1;
1976
+ $('overflow-badge').classList.toggle(
1977
+ 'is-on',
1978
+ state.mode === 'single' && !!state.overflow[state.index]
1979
+ );
1819
1980
 
1820
1981
  var note = state.notes[state.index];
1821
1982
  if (note) { elNotesText.textContent = note; elNotes.classList.add('is-on'); }
@@ -1843,6 +2004,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1843
2004
  }
1844
2005
  }
1845
2006
  post({ type: 'index', index: state.index });
2007
+ announceIndex(state.index);
1846
2008
  renderCounts();
1847
2009
  }
1848
2010
 
@@ -1850,12 +2012,6 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1850
2012
  var before = src.value.slice(0, src.selectionStart);
1851
2013
  var lines = before.split('\\n');
1852
2014
  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
2015
  }
1860
2016
 
1861
2017
  // ── Parse round-trip: the server owns the Markdown, so what you see here
@@ -2081,58 +2237,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2081
2237
  var bySnippetId = {};
2082
2238
  D.snippets.forEach(function (s) { bySnippetId[s.id] = s; });
2083
2239
 
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
- });
2240
+ function evalNudges() {}
2241
+ function hideNudge() {}
2136
2242
 
2137
2243
  // ── Tips carousel ──────────────────────────────────────────────────────
2138
2244
  var tipTimer = null;
@@ -2330,6 +2436,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2330
2436
  guide.classList.remove('is-on');
2331
2437
  $('library').classList.remove('is-on');
2332
2438
  if ($('themes').classList.contains('is-on')) closeThemes(true);
2439
+ updateDocTitle();
2333
2440
  if (!keepFocus) src.focus();
2334
2441
  }
2335
2442
 
@@ -2372,7 +2479,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2372
2479
  function exportHtml() {
2373
2480
  if (isEmpty()) { toast('Nothing to export yet.', 'warn'); return; }
2374
2481
  var builder = state.kind === 'html' ? buildHtmlDoc : buildDeck;
2375
- builder(false)
2482
+ builder(false, true)
2376
2483
  .then(function (data) { return fetch(data.path); })
2377
2484
  .then(function (r) {
2378
2485
  if (!r.ok) throw new Error('server said ' + r.status);
@@ -2403,7 +2510,16 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2403
2510
  var pdfUrl = state.kind === 'html' ? '/__pdf-doc' : '/__pdf';
2404
2511
  var pdfBody = state.kind === 'html'
2405
2512
  ? { 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 };
2513
+ : {
2514
+ markdown: src.value,
2515
+ theme: state.theme,
2516
+ size: state.size,
2517
+ head: state.head,
2518
+ body: state.body,
2519
+ template: state.template,
2520
+ transition: state.transition,
2521
+ title: $('docname').value
2522
+ };
2407
2523
 
2408
2524
  fetch(pdfUrl, {
2409
2525
  method: 'POST',
@@ -2460,7 +2576,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2460
2576
  });
2461
2577
  }
2462
2578
 
2463
- function buildDeck(forPrint) {
2579
+ function buildDeck(forPrint, standalone) {
2464
2580
  return fetch('/__present', {
2465
2581
  method: 'POST',
2466
2582
  headers: { 'Content-Type': 'application/json' },
@@ -2470,8 +2586,11 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2470
2586
  size: state.size,
2471
2587
  head: state.head,
2472
2588
  body: state.body,
2589
+ template: state.template,
2590
+ transition: state.transition,
2473
2591
  title: $('docname').value,
2474
- print: !!forPrint
2592
+ print: !!forPrint,
2593
+ standalone: !!standalone
2475
2594
  })
2476
2595
  }).then(function (r) {
2477
2596
  if (!r.ok) throw new Error('server said ' + r.status);
@@ -2497,11 +2616,13 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2497
2616
 
2498
2617
  function present() {
2499
2618
  if (isEmpty()) { toast('Nothing to present yet.', 'warn'); return; }
2619
+ expectingDeck = true;
2500
2620
  var builder = state.kind === 'html' ? buildHtmlDoc : buildDeck;
2501
2621
  var tab = window.open('about:blank', '_blank');
2502
2622
  builder(false)
2503
2623
  .then(function (data) {
2504
- var url = location.origin + data.path;
2624
+ var url = location.origin + data.path +
2625
+ (data.path.indexOf('?') >= 0 ? '&' : '?') + 'ps=' + followSid;
2505
2626
  if (tab) tab.location.replace(url);
2506
2627
  else toast('Allow pop-ups to present in a new tab.', 'warn', 'present here', function () { location.href = url; });
2507
2628
  })
@@ -2538,6 +2659,30 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2538
2659
  paintSizeSeg();
2539
2660
  }
2540
2661
 
2662
+ function setTemplate(next, remember) {
2663
+ var id = pickTemplate(next);
2664
+ state.template = id;
2665
+ document.documentElement.dataset.template = id;
2666
+ if (remember !== false) {
2667
+ lsSet(K.template, id);
2668
+ scheduleSave();
2669
+ }
2670
+ pushLook();
2671
+ paintTemplateMenu();
2672
+ }
2673
+
2674
+ function setTransition(next, remember) {
2675
+ var id = pickTransition(next);
2676
+ state.transition = id;
2677
+ document.documentElement.dataset.transition = id;
2678
+ if (remember !== false) {
2679
+ lsSet(K.transition, id);
2680
+ scheduleSave();
2681
+ }
2682
+ pushLook();
2683
+ paintTemplateMenu();
2684
+ }
2685
+
2541
2686
  /**
2542
2687
  * Heading and body faces are chosen separately, and either can be handed
2543
2688
  * back to the theme by picking nothing. Passing null is what clears it.
@@ -2563,6 +2708,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2563
2708
  root.dataset.theme = state.theme;
2564
2709
  root.dataset.decor = decorOf(state.theme);
2565
2710
  root.dataset.size = state.size;
2711
+ root.dataset.template = state.template;
2712
+ root.dataset.transition = state.transition;
2566
2713
  if (state.head) root.dataset.head = state.head; else delete root.dataset.head;
2567
2714
  if (state.body) root.dataset.body = state.body; else delete root.dataset.body;
2568
2715
  }
@@ -2574,7 +2721,9 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2574
2721
  theme: state.theme,
2575
2722
  size: state.size,
2576
2723
  head: state.head,
2577
- body: state.body
2724
+ body: state.body,
2725
+ template: state.template,
2726
+ transition: state.transition
2578
2727
  }, '*');
2579
2728
  }
2580
2729
  } catch (e) {}
@@ -2587,10 +2736,13 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2587
2736
  theme: state.theme,
2588
2737
  size: state.size,
2589
2738
  head: state.head,
2590
- body: state.body
2739
+ body: state.body,
2740
+ template: state.template,
2741
+ transition: state.transition
2591
2742
  });
2592
2743
  syncHtmlFrameTheme();
2593
2744
  paintThemeButton();
2745
+ paintTemplateMenu();
2594
2746
  }
2595
2747
 
2596
2748
  function faceLabel(id) {
@@ -2654,6 +2806,45 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2654
2806
  $('btn-theme').title = t ? t.label + ' — ' + t.blurb : 'Pick a theme';
2655
2807
  }
2656
2808
 
2809
+ function optionRow(kind, item) {
2810
+ var b = document.createElement('button');
2811
+ b.dataset[kind] = item.id;
2812
+ var name = document.createElement('span');
2813
+ name.className = 'menu__name';
2814
+ name.textContent = item.label;
2815
+ var hint = document.createElement('span');
2816
+ hint.className = 'menu__hint';
2817
+ hint.textContent = item.blurb;
2818
+ b.appendChild(name);
2819
+ b.appendChild(hint);
2820
+ return b;
2821
+ }
2822
+
2823
+ function buildTemplateMenu() {
2824
+ var templateHost = $('template-list');
2825
+ var transitionHost = $('transition-list');
2826
+ templateHost.innerHTML = '';
2827
+ transitionHost.innerHTML = '';
2828
+ TEMPLATES.forEach(function (item) { templateHost.appendChild(optionRow('template', item)); });
2829
+ TRANSITIONS.forEach(function (item) { transitionHost.appendChild(optionRow('transition', item)); });
2830
+ paintTemplateMenu();
2831
+ }
2832
+
2833
+ function paintTemplateMenu() {
2834
+ var menu = $('template-menu');
2835
+ if (!menu) return;
2836
+ Array.prototype.forEach.call(menu.querySelectorAll('[data-template]'), function (b) {
2837
+ b.classList.toggle('is-on', b.dataset.template === state.template);
2838
+ });
2839
+ Array.prototype.forEach.call(menu.querySelectorAll('[data-transition]'), function (b) {
2840
+ b.classList.toggle('is-on', b.dataset.transition === state.transition);
2841
+ });
2842
+ var current = TEMPLATE_BY_ID[state.template];
2843
+ $('template-label').textContent = current ? current.label : 'template';
2844
+ $('btn-template').title = (current ? current.blurb : 'Composition template') +
2845
+ ' · transition ' + state.transition;
2846
+ }
2847
+
2657
2848
  // ── Theme picker ───────────────────────────────────────────────────────
2658
2849
  function buildSizeSeg(hostId, showBlurb) {
2659
2850
  var host = $(hostId);
@@ -2801,7 +2992,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2801
2992
  // Hovering previews too, so the mouse gets the same instant feedback
2802
2993
  // as the arrow keys.
2803
2994
  card.addEventListener('mouseenter', function () { selectTheme(themeCards.indexOf(card)); });
2804
- card.addEventListener('click', function () { commitTheme(); });
2995
+ card.addEventListener('click', function () { selectTheme(themeCards.indexOf(card)); commitTheme(); });
2805
2996
  grid.appendChild(card);
2806
2997
  themeCards.push(card);
2807
2998
  });
@@ -2875,7 +3066,10 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2875
3066
  state.deckId = id;
2876
3067
  lsSet(K.current, id);
2877
3068
  setDocKind(meta.kind || 'markdown');
3069
+ setTemplate(meta.template || state.template, true);
3070
+ setTransition(meta.transition || state.transition, true);
2878
3071
  $('docname').value = meta.name;
3072
+ updateDocTitle();
2879
3073
  updateDeckCount();
2880
3074
  elSave.className = '';
2881
3075
  elSave.textContent = '';
@@ -2996,7 +3190,13 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2996
3190
  dupe.addEventListener('click', function (e) {
2997
3191
  e.stopPropagation();
2998
3192
  if (deck.id === state.deckId) { duplicateDeck(); renderLibrary(); return; }
2999
- var id = createDeck(uniqueName(deck.name + ' copy'), readDeck(deck.id), deck.kind || 'markdown');
3193
+ var id = createDeck(
3194
+ uniqueName(deck.name + ' copy'),
3195
+ readDeck(deck.id),
3196
+ deck.kind || 'markdown',
3197
+ deck.template || state.template,
3198
+ deck.transition || state.transition
3199
+ );
3000
3200
  if (!id) { noRoom(); return; }
3001
3201
  updateDeckCount();
3002
3202
  renderLibrary();
@@ -3029,6 +3229,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3029
3229
  for (var i = 0; i < list.length; i++) if (list[i].id === state.deckId) libSel = i;
3030
3230
  renderLibrary();
3031
3231
  $('library').classList.add('is-on');
3232
+ updateDocTitle();
3032
3233
  }
3033
3234
 
3034
3235
  $('lib-new').addEventListener('click', function () { closeOverlays(); showStartScreen(); });
@@ -3038,13 +3239,56 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3038
3239
  // ── Start screen ─────────────────────────────────────────────────────
3039
3240
  // Shown on a true first run, and whenever "new" is chosen explicitly from
3040
3241
  // the library — never on an ordinary launch that has a deck to resume.
3242
+ var startTemplate = state.template;
3243
+ var startTransition = state.transition;
3244
+
3245
+ function paintStartOptions() {
3246
+ Array.prototype.forEach.call($('start-template-list').querySelectorAll('[data-template]'), function (b) {
3247
+ b.classList.toggle('is-on', b.dataset.template === startTemplate);
3248
+ });
3249
+ Array.prototype.forEach.call($('start-transition-list').querySelectorAll('[data-transition]'), function (b) {
3250
+ b.classList.toggle('is-on', b.dataset.transition === startTransition);
3251
+ });
3252
+ }
3253
+
3254
+ function buildStartOptions() {
3255
+ var templateHost = $('start-template-list');
3256
+ var transitionHost = $('start-transition-list');
3257
+ templateHost.innerHTML = '';
3258
+ transitionHost.innerHTML = '';
3259
+ TEMPLATES.forEach(function (item) {
3260
+ var b = document.createElement('button');
3261
+ b.className = 'start-choice';
3262
+ b.dataset.template = item.id;
3263
+ b.textContent = item.label;
3264
+ b.title = item.blurb;
3265
+ b.addEventListener('click', function () { startTemplate = item.id; paintStartOptions(); });
3266
+ templateHost.appendChild(b);
3267
+ });
3268
+ TRANSITIONS.forEach(function (item) {
3269
+ var b = document.createElement('button');
3270
+ b.className = 'start-choice';
3271
+ b.dataset.transition = item.id;
3272
+ b.textContent = item.label;
3273
+ b.title = item.blurb;
3274
+ b.addEventListener('click', function () { startTransition = item.id; paintStartOptions(); });
3275
+ transitionHost.appendChild(b);
3276
+ });
3277
+ paintStartOptions();
3278
+ }
3279
+
3041
3280
  function showStartScreen() {
3281
+ startTemplate = state.template;
3282
+ startTransition = state.transition;
3283
+ paintStartOptions();
3042
3284
  $('start-lib').classList.toggle('is-disabled', !loadIndex().length);
3043
3285
  $('screen-start').classList.add('is-on');
3286
+ updateDocTitle();
3044
3287
  }
3045
3288
 
3046
3289
  function hideStartScreenSilently() {
3047
3290
  $('screen-start').classList.remove('is-on');
3291
+ updateDocTitle();
3048
3292
  }
3049
3293
 
3050
3294
  /** Escape/backdrop close. With nothing open yet, that would be a dead end,
@@ -3055,7 +3299,9 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3055
3299
  }
3056
3300
 
3057
3301
  function startNewMarkdown() {
3058
- var id = createDeck(uniqueName('untitled'), D.welcome, 'markdown');
3302
+ setTemplate(startTemplate, true);
3303
+ setTransition(startTransition, true);
3304
+ var id = createDeck(uniqueName('untitled'), D.welcome, 'markdown', startTemplate, startTransition);
3059
3305
  hideStartScreenSilently();
3060
3306
  if (!id) {
3061
3307
  // Storage refuses even a first write: use the deck unsaved rather than
@@ -3091,7 +3337,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3091
3337
  openLibrary();
3092
3338
  }
3093
3339
 
3094
- /** Fetches a public HTML page server-side (sidesteps CORS) and opens it as a new HTML doc. */
3340
+ /** Fetches a public URL server-side (sidesteps CORS) and opens it as a new Markdown deck or HTML doc. */
3095
3341
  function loadHtmlUrl() {
3096
3342
  var input = $('start-html-url');
3097
3343
  var raw = input.value.trim();
@@ -3118,12 +3364,20 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3118
3364
  .then(function (body) {
3119
3365
  saveNow();
3120
3366
  var name = uniqueName(body.title || url.hostname);
3121
- var id = createDeck(name, body.html, 'html');
3367
+ var kind = body.kind || (body.markdown ? 'markdown' : 'html');
3368
+ var content = kind === 'markdown' ? (body.markdown || body.content || '') : (body.html || body.content || '');
3369
+ var id = createDeck(
3370
+ name,
3371
+ content,
3372
+ kind,
3373
+ kind === 'markdown' ? startTemplate : state.template,
3374
+ kind === 'markdown' ? startTransition : state.transition
3375
+ );
3122
3376
  if (!id) { noRoom(); return; }
3123
3377
  input.value = '';
3124
3378
  hideStartScreenSilently();
3125
3379
  openDeck(id);
3126
- toast('Loaded ' + url.hostname + ' as a new HTML doc');
3380
+ toast('Loaded ' + (kind === 'markdown' ? 'Markdown deck' : 'HTML doc') + ' from ' + url.hostname);
3127
3381
  })
3128
3382
  .catch(function (err) { toast('Could not load URL: ' + err.message, 'err'); })
3129
3383
  .then(function () { btn.disabled = false; });
@@ -3148,6 +3402,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3148
3402
  else if (name === 'grid') setMode(state.mode === 'grid' ? 'single' : 'grid');
3149
3403
  else if (name === 'theme') openThemes();
3150
3404
  else if (name === 'guide') { guide.classList.add('is-on'); }
3405
+ else if (name === 'palette') openPalette();
3151
3406
  else if (name === 'decks') openLibrary();
3152
3407
  else if (name === 'new') newDeck();
3153
3408
  else if (name === 'duplicate') duplicateDeck();
@@ -3158,6 +3413,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3158
3413
  var parseTimer = null;
3159
3414
 
3160
3415
  function onInput() {
3416
+ // Typing takes the editor back from the deck.
3417
+ if (following) { following = false; followChipEl.classList.add('hidden'); }
3161
3418
  paint();
3162
3419
  syncScroll();
3163
3420
  updateCaretUi();
@@ -3179,26 +3436,6 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3179
3436
  e.preventDefault();
3180
3437
  typeText(' ');
3181
3438
  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
3439
  }
3203
3440
  });
3204
3441
 
@@ -3261,13 +3498,19 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3261
3498
  });
3262
3499
 
3263
3500
  /** An opened file lands as a new deck, so nothing in the library is lost. */
3264
- function loadMarkdownFile(file) {
3501
+ function loadMarkdownFile(file, chosenTemplate, chosenTransition) {
3265
3502
  var fr = new FileReader();
3266
3503
  fr.onload = function () {
3267
3504
  saveNow();
3268
3505
  var markdown = String(fr.result).replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');
3269
3506
  var name = uniqueName(file.name.replace(/\\.(md|markdown|txt)$/i, '') || 'untitled');
3270
- var id = createDeck(name, markdown, 'markdown');
3507
+ var id = createDeck(
3508
+ name,
3509
+ markdown,
3510
+ 'markdown',
3511
+ chosenTemplate || state.template,
3512
+ chosenTransition || state.transition
3513
+ );
3271
3514
  if (!id) { noRoom(); return; }
3272
3515
  hideStartScreenSilently();
3273
3516
  openDeck(id);
@@ -3297,14 +3540,24 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3297
3540
  $('file-any').addEventListener('change', function (e) {
3298
3541
  var f = e.target.files[0];
3299
3542
  if (f) {
3300
- if (/\\.(md|markdown|txt)$/i.test(f.name)) loadMarkdownFile(f);
3543
+ var fromStart = $('screen-start').classList.contains('is-on');
3544
+ if (/\\.(md|markdown|txt)$/i.test(f.name)) {
3545
+ loadMarkdownFile(
3546
+ f,
3547
+ fromStart ? startTemplate : state.template,
3548
+ fromStart ? startTransition : state.transition
3549
+ );
3550
+ }
3301
3551
  else if (/\\.html?$/i.test(f.name)) loadHtmlFile(f);
3302
3552
  else toast('Unrecognized file type.', 'warn');
3303
3553
  }
3304
3554
  e.target.value = '';
3305
3555
  });
3306
3556
 
3307
- $('docname').addEventListener('input', scheduleSave);
3557
+ $('docname').addEventListener('input', function () {
3558
+ updateDocTitle();
3559
+ scheduleSave();
3560
+ });
3308
3561
 
3309
3562
  $('btn-guide').addEventListener('click', function () { runAction('guide'); });
3310
3563
  $('btn-palette').addEventListener('click', openPalette);
@@ -3380,6 +3633,13 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3380
3633
  setFont(btn.dataset.slot, btn.dataset.font || null, true);
3381
3634
  }, 'button[data-slot]');
3382
3635
 
3636
+ // Template and transition compose, so the menu remains open while either
3637
+ // choice is changed and the existing Markdown reflows immediately.
3638
+ bindMenu('btn-template', 'template-menu', function (btn) {
3639
+ if (btn.dataset.template) setTemplate(btn.dataset.template, true);
3640
+ if (btn.dataset.transition) setTransition(btn.dataset.transition, true);
3641
+ }, 'button[data-template], button[data-transition]');
3642
+
3383
3643
  document.addEventListener('click', function (e) {
3384
3644
  if (openMenu && !openMenu.contains(e.target)) closeMenu();
3385
3645
  });
@@ -3415,12 +3675,25 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3415
3675
  themeCards[next].scrollIntoView({ block: 'nearest' });
3416
3676
  }, true);
3417
3677
  $('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); });
3678
+ $('btn-prev').addEventListener('click', function () { setIndex(state.index - 1, false); });
3679
+ $('btn-next').addEventListener('click', function () { setIndex(state.index + 1, false); });
3420
3680
  $('seg-single').addEventListener('click', function () { setMode('single'); });
3421
3681
  $('seg-grid').addEventListener('click', function () { setMode('grid'); });
3422
3682
 
3683
+ $('pane-prev').addEventListener('mousedown', function () {
3684
+ if (document.activeElement && isEditingText()) {
3685
+ document.activeElement.blur();
3686
+ }
3687
+ });
3688
+
3423
3689
  // ── Global keys ────────────────────────────────────────────────────────
3690
+ function isEditingText() {
3691
+ var el = document.activeElement;
3692
+ if (!el) return false;
3693
+ var tag = el.tagName ? el.tagName.toLowerCase() : '';
3694
+ return tag === 'textarea' || tag === 'input' || el.isContentEditable;
3695
+ }
3696
+
3424
3697
  document.addEventListener('keydown', function (e) {
3425
3698
  var mod = e.metaKey || e.ctrlKey;
3426
3699
 
@@ -3458,31 +3731,59 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3458
3731
  }
3459
3732
  return;
3460
3733
  }
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
3734
 
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); }
3735
+ if (mod) {
3736
+ var key = e.key.toLowerCase();
3737
+ var md = state.kind === 'markdown';
3738
+
3739
+ if (key === 'k' && !e.shiftKey && md) { e.preventDefault(); openPalette(); }
3740
+ else if (key === 'o') { e.preventDefault(); openLibrary(); }
3741
+ else if (key === '/' && md) { e.preventDefault(); runAction('guide'); }
3742
+ else if (key === 'p' || (key === 's' && e.shiftKey)) { e.preventDefault(); exportPdf(); }
3743
+ else if (key === 's') { e.preventDefault(); saveNow(); download(); }
3744
+ else if (key === 'enter') { e.preventDefault(); present(); }
3745
+ else if (key === 'd' && md) { e.preventDefault(); insertSnippet(bySnippetId['slide-break']); }
3746
+ else if (key === 'b' && md) { e.preventDefault(); insertSnippet(bySnippetId.bold); }
3747
+ else if (key === 'i' && md) { e.preventDefault(); insertSnippet(bySnippetId.italic); }
3748
+ else if (key === 'e' && md) { e.preventDefault(); insertSnippet(bySnippetId['inline-code']); }
3749
+ else if (key === 'g' && md) { e.preventDefault(); runAction('grid'); }
3750
+ else if (key === 'l' && e.shiftKey) { e.preventDefault(); runAction('theme'); }
3751
+ return;
3752
+ }
3753
+
3754
+ if (palette.classList.contains('is-on') || guide.classList.contains('is-on') ||
3755
+ $('library').classList.contains('is-on') || menuIsOpen()) {
3756
+ return;
3757
+ }
3758
+
3759
+ // Alt + arrows/page keys hops between slides (works anywhere, including inside the editor).
3760
+ if (e.altKey) {
3761
+ if (e.key === 'ArrowDown' || e.key === 'ArrowRight' || e.key === 'PageDown') {
3762
+ e.preventDefault();
3763
+ setIndex(state.index + 1, false);
3764
+ } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft' || e.key === 'PageUp') {
3765
+ e.preventDefault();
3766
+ setIndex(state.index - 1, false);
3767
+ }
3768
+ return;
3769
+ }
3770
+
3771
+ // Direct arrow keys / page keys when not typing into a text field.
3772
+ if (!isEditingText()) {
3773
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ' || e.key === 'PageDown') {
3774
+ e.preventDefault();
3775
+ setIndex(state.index + 1, false);
3776
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp') {
3777
+ e.preventDefault();
3778
+ setIndex(state.index - 1, false);
3779
+ } else if (e.key === 'Home') {
3780
+ e.preventDefault();
3781
+ setIndex(0, false);
3782
+ } else if (e.key === 'End') {
3783
+ e.preventDefault();
3784
+ setIndex(state.slides.length - 1, false);
3785
+ }
3786
+ }
3486
3787
  });
3487
3788
 
3488
3789
  // ── Split pane ─────────────────────────────────────────────────────────
@@ -3539,9 +3840,16 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3539
3840
  } else if (m.type === 'goto') {
3540
3841
  setMode('single');
3541
3842
  setIndex(m.index, true);
3843
+ } else if (m.type === 'nav') {
3844
+ setIndex(state.index + m.delta, false);
3845
+ } else if (m.type === 'index-select') {
3846
+ setIndex(m.index, false);
3847
+ } else if (m.type === 'action') {
3848
+ runAction(m.action);
3542
3849
  } else if (m.type === 'overflow') {
3543
3850
  var had = !!state.overflow[m.index];
3544
3851
  state.overflow[m.index] = m.overflow;
3852
+ renderCounts();
3545
3853
  if (had !== m.overflow) evalNudges();
3546
3854
  }
3547
3855
  });
@@ -3559,8 +3867,12 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3559
3867
 
3560
3868
  document.documentElement.dataset.theme = state.theme;
3561
3869
  document.documentElement.dataset.decor = THEME_BY_ID[state.theme].decor;
3870
+ document.documentElement.dataset.template = state.template;
3871
+ document.documentElement.dataset.transition = state.transition;
3562
3872
  buildSizeSeg('topbar-size', false);
3563
3873
  buildFontMenu();
3874
+ buildTemplateMenu();
3875
+ buildStartOptions();
3564
3876
  paintSizeSeg();
3565
3877
  paintThemeButton();
3566
3878
  $('seg-single').classList.toggle('is-on', state.mode === 'single');