deckrun 1.4.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,11 +26,13 @@ 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">
@@ -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; }
@@ -341,7 +349,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
341
349
  outline: none;
342
350
  }
343
351
 
344
- #src::selection { background: var(--surface2); 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; }
345
354
 
346
355
  /* Markdown tokens — color only to guarantee pixel-identical alignment */
347
356
  .t-h1 { color: var(--accent); }
@@ -394,6 +403,22 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
394
403
  color: var(--overlay1);
395
404
  }
396
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
+
397
422
  /* ── Dropdown menu ────────────────────────────────────────────────────── */
398
423
  .menu { position: relative; display: inline-flex; }
399
424
  .menu__chev { font-size: 9px; color: var(--overlay0); }
@@ -443,6 +468,21 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
443
468
  white-space: nowrap;
444
469
  }
445
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
+
446
486
  .seg { display: flex; border: 1px solid var(--surface0); border-radius: 7px; overflow: hidden; }
447
487
  .seg button { font-size: 11px; padding: 3px 10px; color: var(--overlay1); }
448
488
  .seg button.is-on { background: var(--surface0); color: var(--text); }
@@ -529,6 +569,19 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
529
569
  #tipbar button { color: var(--overlay0); padding: 0 3px; }
530
570
  #tipbar button:hover { color: var(--text); }
531
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
+
532
585
  /* ── Command palette ──────────────────────────────────────────────────── */
533
586
  #palette, #guide, #library { position: fixed; inset: 0; z-index: 40; display: none; }
534
587
  #palette.is-on, #guide.is-on, #library.is-on { display: block; }
@@ -1014,7 +1067,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1014
1067
  }
1015
1068
 
1016
1069
  [data-doc-kind="html"] #btn-guide,
1017
- [data-doc-kind="html"] #btn-palette {
1070
+ [data-doc-kind="html"] #btn-palette,
1071
+ [data-doc-kind="html"] #template-wrap {
1018
1072
  display: none !important;
1019
1073
  }
1020
1074
 
@@ -1067,6 +1121,30 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1067
1121
  }
1068
1122
  .start-card__acts button:hover { border-color: var(--accent); color: var(--text); }
1069
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
+
1070
1148
  .start-card__url { display: flex; gap: 6px; flex: 1 1 auto; min-width: 0; }
1071
1149
  .start-card__url input {
1072
1150
  flex: 1 1 auto;
@@ -1122,6 +1200,21 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1122
1200
  </button>
1123
1201
  </div>
1124
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>
1125
1218
  <button class="btn" id="btn-theme" title="Pick a theme">
1126
1219
  <span class="th-dot" id="theme-dot"></span>
1127
1220
  <span id="theme-label">theme</span>
@@ -1167,6 +1260,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1167
1260
  <button class="step" id="btn-prev" title="Previous slide">&#8592;</button>
1168
1261
  <span id="prev-count">slide 0 / 0</span>
1169
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>
1170
1264
  <span class="spacer"></span>
1171
1265
  <span id="prev-scale"></span>
1172
1266
  <div class="seg">
@@ -1192,6 +1286,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1192
1286
  <span id="words">0 words</span>
1193
1287
  <span id="chip-slides">0 slides</span>
1194
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>
1195
1290
  <span id="tipbar">
1196
1291
  <span class="tag">tip</span>
1197
1292
  <span id="tip-text"></span>
@@ -1275,6 +1370,16 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1275
1370
  <button id="start-md-blank">start blank</button>
1276
1371
  <button id="start-md-upload">upload .md</button>
1277
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>
1278
1383
  </div>
1279
1384
  <div class="start-card" id="start-html-card">
1280
1385
  <span class="start-card__title">HTML document</span>
@@ -1298,7 +1403,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1298
1403
  <div id="toasts"></div>
1299
1404
  <input type="file" id="file-any" accept=".md,.markdown,text/markdown,text/plain,.html,.htm,text/html" hidden>
1300
1405
 
1301
- <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>
1302
1407
  <script>
1303
1408
  (function () {
1304
1409
  'use strict';
@@ -1315,6 +1420,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1315
1420
  size: 'deckrun.size.v1',
1316
1421
  head: 'deckrun.font.head.v1',
1317
1422
  body: 'deckrun.font.body.v1',
1423
+ template:'deckrun.template.v1',
1424
+ transition:'deckrun.transition.v1',
1318
1425
  split: 'deckrun.split.v1',
1319
1426
  mode: 'deckrun.mode.v1',
1320
1427
  nudge: 'deckrun.nudges.v1',
@@ -1399,11 +1506,20 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1399
1506
  }
1400
1507
 
1401
1508
  /** Returns the new deck's id, or null if this browser refused to store it. */
1402
- function createDeck(name, content, kind) {
1509
+ function createDeck(name, content, kind, template, transition) {
1403
1510
  var id = newDeckId();
1404
1511
  if (!lsSet(K.deck + id, content)) return null;
1405
1512
  var list = loadIndex();
1406
- 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
+ });
1407
1523
  if (!saveIndex(list)) { lsDel(K.deck + id); return null; }
1408
1524
  return id;
1409
1525
  }
@@ -1447,6 +1563,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1447
1563
  if (list[i].id === state.deckId) {
1448
1564
  list[i].name = $('docname').value.trim() || 'untitled';
1449
1565
  list[i].kind = state.kind;
1566
+ list[i].template = state.template;
1567
+ list[i].transition = state.transition;
1450
1568
  if (state.kind === 'html') {
1451
1569
  list[i].slides = 0;
1452
1570
  list[i].chars = srcHtml.value.length;
@@ -1513,6 +1631,22 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1513
1631
  return SIZE_BY_ID[id] ? id : (SIZE_BY_ID[D.size] ? D.size : 'm');
1514
1632
  }
1515
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
+
1516
1650
  var FACES = D.faces;
1517
1651
  var FACE_BY_ID = {};
1518
1652
  FACES.forEach(function (f) { FACE_BY_ID[f.id] = f; });
@@ -1546,6 +1680,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1546
1680
  size: pickSize(lsGet(K.size, D.size)),
1547
1681
  head: pickFont(lsGet(K.head, D.fonts.head)),
1548
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)),
1549
1685
  frameReady: false,
1550
1686
  overflow: {},
1551
1687
  dismissed: {},
@@ -1561,6 +1697,75 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1561
1697
  if (saved && saved.length) saved.forEach(function (id) { state.dismissed[id] = true; });
1562
1698
  } catch (e) {}
1563
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
+
1564
1769
  // ── Toasts ─────────────────────────────────────────────────────────────
1565
1770
  function toast(message, kind, actionLabel, action) {
1566
1771
  var el = document.createElement('div');
@@ -1768,6 +1973,10 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1768
1973
  elCount.textContent = 'slide ' + (n ? state.index + 1 : 0) + ' / ' + n;
1769
1974
  $('btn-prev').disabled = state.index <= 0;
1770
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
+ );
1771
1980
 
1772
1981
  var note = state.notes[state.index];
1773
1982
  if (note) { elNotesText.textContent = note; elNotes.classList.add('is-on'); }
@@ -1795,6 +2004,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
1795
2004
  }
1796
2005
  }
1797
2006
  post({ type: 'index', index: state.index });
2007
+ announceIndex(state.index);
1798
2008
  renderCounts();
1799
2009
  }
1800
2010
 
@@ -2269,7 +2479,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2269
2479
  function exportHtml() {
2270
2480
  if (isEmpty()) { toast('Nothing to export yet.', 'warn'); return; }
2271
2481
  var builder = state.kind === 'html' ? buildHtmlDoc : buildDeck;
2272
- builder(false)
2482
+ builder(false, true)
2273
2483
  .then(function (data) { return fetch(data.path); })
2274
2484
  .then(function (r) {
2275
2485
  if (!r.ok) throw new Error('server said ' + r.status);
@@ -2300,7 +2510,16 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2300
2510
  var pdfUrl = state.kind === 'html' ? '/__pdf-doc' : '/__pdf';
2301
2511
  var pdfBody = state.kind === 'html'
2302
2512
  ? { html: srcHtml.value, title: $('docname').value }
2303
- : { 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
+ };
2304
2523
 
2305
2524
  fetch(pdfUrl, {
2306
2525
  method: 'POST',
@@ -2357,7 +2576,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2357
2576
  });
2358
2577
  }
2359
2578
 
2360
- function buildDeck(forPrint) {
2579
+ function buildDeck(forPrint, standalone) {
2361
2580
  return fetch('/__present', {
2362
2581
  method: 'POST',
2363
2582
  headers: { 'Content-Type': 'application/json' },
@@ -2367,8 +2586,11 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2367
2586
  size: state.size,
2368
2587
  head: state.head,
2369
2588
  body: state.body,
2589
+ template: state.template,
2590
+ transition: state.transition,
2370
2591
  title: $('docname').value,
2371
- print: !!forPrint
2592
+ print: !!forPrint,
2593
+ standalone: !!standalone
2372
2594
  })
2373
2595
  }).then(function (r) {
2374
2596
  if (!r.ok) throw new Error('server said ' + r.status);
@@ -2394,11 +2616,13 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2394
2616
 
2395
2617
  function present() {
2396
2618
  if (isEmpty()) { toast('Nothing to present yet.', 'warn'); return; }
2619
+ expectingDeck = true;
2397
2620
  var builder = state.kind === 'html' ? buildHtmlDoc : buildDeck;
2398
2621
  var tab = window.open('about:blank', '_blank');
2399
2622
  builder(false)
2400
2623
  .then(function (data) {
2401
- var url = location.origin + data.path;
2624
+ var url = location.origin + data.path +
2625
+ (data.path.indexOf('?') >= 0 ? '&' : '?') + 'ps=' + followSid;
2402
2626
  if (tab) tab.location.replace(url);
2403
2627
  else toast('Allow pop-ups to present in a new tab.', 'warn', 'present here', function () { location.href = url; });
2404
2628
  })
@@ -2435,6 +2659,30 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2435
2659
  paintSizeSeg();
2436
2660
  }
2437
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
+
2438
2686
  /**
2439
2687
  * Heading and body faces are chosen separately, and either can be handed
2440
2688
  * back to the theme by picking nothing. Passing null is what clears it.
@@ -2460,6 +2708,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2460
2708
  root.dataset.theme = state.theme;
2461
2709
  root.dataset.decor = decorOf(state.theme);
2462
2710
  root.dataset.size = state.size;
2711
+ root.dataset.template = state.template;
2712
+ root.dataset.transition = state.transition;
2463
2713
  if (state.head) root.dataset.head = state.head; else delete root.dataset.head;
2464
2714
  if (state.body) root.dataset.body = state.body; else delete root.dataset.body;
2465
2715
  }
@@ -2471,7 +2721,9 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2471
2721
  theme: state.theme,
2472
2722
  size: state.size,
2473
2723
  head: state.head,
2474
- body: state.body
2724
+ body: state.body,
2725
+ template: state.template,
2726
+ transition: state.transition
2475
2727
  }, '*');
2476
2728
  }
2477
2729
  } catch (e) {}
@@ -2484,10 +2736,13 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2484
2736
  theme: state.theme,
2485
2737
  size: state.size,
2486
2738
  head: state.head,
2487
- body: state.body
2739
+ body: state.body,
2740
+ template: state.template,
2741
+ transition: state.transition
2488
2742
  });
2489
2743
  syncHtmlFrameTheme();
2490
2744
  paintThemeButton();
2745
+ paintTemplateMenu();
2491
2746
  }
2492
2747
 
2493
2748
  function faceLabel(id) {
@@ -2551,6 +2806,45 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2551
2806
  $('btn-theme').title = t ? t.label + ' — ' + t.blurb : 'Pick a theme';
2552
2807
  }
2553
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
+
2554
2848
  // ── Theme picker ───────────────────────────────────────────────────────
2555
2849
  function buildSizeSeg(hostId, showBlurb) {
2556
2850
  var host = $(hostId);
@@ -2772,6 +3066,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2772
3066
  state.deckId = id;
2773
3067
  lsSet(K.current, id);
2774
3068
  setDocKind(meta.kind || 'markdown');
3069
+ setTemplate(meta.template || state.template, true);
3070
+ setTransition(meta.transition || state.transition, true);
2775
3071
  $('docname').value = meta.name;
2776
3072
  updateDocTitle();
2777
3073
  updateDeckCount();
@@ -2894,7 +3190,13 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2894
3190
  dupe.addEventListener('click', function (e) {
2895
3191
  e.stopPropagation();
2896
3192
  if (deck.id === state.deckId) { duplicateDeck(); renderLibrary(); return; }
2897
- 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
+ );
2898
3200
  if (!id) { noRoom(); return; }
2899
3201
  updateDeckCount();
2900
3202
  renderLibrary();
@@ -2937,7 +3239,48 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2937
3239
  // ── Start screen ─────────────────────────────────────────────────────
2938
3240
  // Shown on a true first run, and whenever "new" is chosen explicitly from
2939
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
+
2940
3280
  function showStartScreen() {
3281
+ startTemplate = state.template;
3282
+ startTransition = state.transition;
3283
+ paintStartOptions();
2941
3284
  $('start-lib').classList.toggle('is-disabled', !loadIndex().length);
2942
3285
  $('screen-start').classList.add('is-on');
2943
3286
  updateDocTitle();
@@ -2956,7 +3299,9 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
2956
3299
  }
2957
3300
 
2958
3301
  function startNewMarkdown() {
2959
- 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);
2960
3305
  hideStartScreenSilently();
2961
3306
  if (!id) {
2962
3307
  // Storage refuses even a first write: use the deck unsaved rather than
@@ -3021,7 +3366,13 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3021
3366
  var name = uniqueName(body.title || url.hostname);
3022
3367
  var kind = body.kind || (body.markdown ? 'markdown' : 'html');
3023
3368
  var content = kind === 'markdown' ? (body.markdown || body.content || '') : (body.html || body.content || '');
3024
- var id = createDeck(name, content, kind);
3369
+ var id = createDeck(
3370
+ name,
3371
+ content,
3372
+ kind,
3373
+ kind === 'markdown' ? startTemplate : state.template,
3374
+ kind === 'markdown' ? startTransition : state.transition
3375
+ );
3025
3376
  if (!id) { noRoom(); return; }
3026
3377
  input.value = '';
3027
3378
  hideStartScreenSilently();
@@ -3062,6 +3413,8 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3062
3413
  var parseTimer = null;
3063
3414
 
3064
3415
  function onInput() {
3416
+ // Typing takes the editor back from the deck.
3417
+ if (following) { following = false; followChipEl.classList.add('hidden'); }
3065
3418
  paint();
3066
3419
  syncScroll();
3067
3420
  updateCaretUi();
@@ -3145,13 +3498,19 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3145
3498
  });
3146
3499
 
3147
3500
  /** An opened file lands as a new deck, so nothing in the library is lost. */
3148
- function loadMarkdownFile(file) {
3501
+ function loadMarkdownFile(file, chosenTemplate, chosenTransition) {
3149
3502
  var fr = new FileReader();
3150
3503
  fr.onload = function () {
3151
3504
  saveNow();
3152
3505
  var markdown = String(fr.result).replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');
3153
3506
  var name = uniqueName(file.name.replace(/\\.(md|markdown|txt)$/i, '') || 'untitled');
3154
- 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
+ );
3155
3514
  if (!id) { noRoom(); return; }
3156
3515
  hideStartScreenSilently();
3157
3516
  openDeck(id);
@@ -3181,7 +3540,14 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3181
3540
  $('file-any').addEventListener('change', function (e) {
3182
3541
  var f = e.target.files[0];
3183
3542
  if (f) {
3184
- 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
+ }
3185
3551
  else if (/\\.html?$/i.test(f.name)) loadHtmlFile(f);
3186
3552
  else toast('Unrecognized file type.', 'warn');
3187
3553
  }
@@ -3267,6 +3633,13 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3267
3633
  setFont(btn.dataset.slot, btn.dataset.font || null, true);
3268
3634
  }, 'button[data-slot]');
3269
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
+
3270
3643
  document.addEventListener('click', function (e) {
3271
3644
  if (openMenu && !openMenu.contains(e.target)) closeMenu();
3272
3645
  });
@@ -3366,7 +3739,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3366
3739
  if (key === 'k' && !e.shiftKey && md) { e.preventDefault(); openPalette(); }
3367
3740
  else if (key === 'o') { e.preventDefault(); openLibrary(); }
3368
3741
  else if (key === '/' && md) { e.preventDefault(); runAction('guide'); }
3369
- else if (key === 's' && e.shiftKey) { e.preventDefault(); exportPdf(); }
3742
+ else if (key === 'p' || (key === 's' && e.shiftKey)) { e.preventDefault(); exportPdf(); }
3370
3743
  else if (key === 's') { e.preventDefault(); saveNow(); download(); }
3371
3744
  else if (key === 'enter') { e.preventDefault(); present(); }
3372
3745
  else if (key === 'd' && md) { e.preventDefault(); insertSnippet(bySnippetId['slide-break']); }
@@ -3476,6 +3849,7 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3476
3849
  } else if (m.type === 'overflow') {
3477
3850
  var had = !!state.overflow[m.index];
3478
3851
  state.overflow[m.index] = m.overflow;
3852
+ renderCounts();
3479
3853
  if (had !== m.overflow) evalNudges();
3480
3854
  }
3481
3855
  });
@@ -3493,8 +3867,12 @@ button { font: inherit; color: inherit; background: none; border: none; cursor:
3493
3867
 
3494
3868
  document.documentElement.dataset.theme = state.theme;
3495
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;
3496
3872
  buildSizeSeg('topbar-size', false);
3497
3873
  buildFontMenu();
3874
+ buildTemplateMenu();
3875
+ buildStartOptions();
3498
3876
  paintSizeSeg();
3499
3877
  paintThemeButton();
3500
3878
  $('seg-single').classList.toggle('is-on', state.mode === 'single');