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/README.md +205 -32
- package/dist/editor-content.js +85 -0
- package/dist/editor.js +401 -23
- package/dist/fragments.js +71 -0
- package/dist/generate.js +1032 -37
- package/dist/index.js +186 -20
- package/dist/lint.js +218 -0
- package/dist/parser.js +85 -0
- package/dist/pdf.js +5 -1
- package/dist/presentation-options.js +287 -0
- package/dist/preview.js +45 -7
- package/dist/rich-content.js +170 -0
- package/dist/themes.js +2 -0
- package/package.json +5 -2
package/dist/generate.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { DECOR_CSS, DEFAULT_SIZE, DEFAULT_THEME, decorOf, findFont, fontOverrideCss, googleFontsHref, hljsHref, resolveSizeName, resolveThemeName, sizeRootCss,
|
|
1
|
+
import { DECOR_CSS, DEFAULT_SIZE, DEFAULT_THEME, THEME_IDS, decorMapJson, decorOf, findFont, fontOverrideCss, googleFontsHref, hljsHref, hljsMapJson, resolveSizeName, resolveThemeName, sizeRootCss, themeSummaries, themeSwitchableCss, } from "./themes.js";
|
|
2
|
+
import { DEFAULT_TEMPLATE, DEFAULT_TRANSITION, resolveTemplateName, resolveTransitionName, TEMPLATE_CSS, TRANSITION_CSS, } from "./presentation-options.js";
|
|
3
|
+
import { RICH_CONTENT_CSS, RICH_CONTENT_RUNTIME, richContentFeatures, richContentHead, } from "./rich-content.js";
|
|
4
|
+
import { FRAGMENT_CSS, FRAGMENT_RUNTIME } from "./fragments.js";
|
|
2
5
|
export { DECOR_CSS, decorOf, googleFontsHref, hljsHref, hljsMapJson, decorMapJson, themeRootCss, themeSwitchableCss, themeSummaries, resolveThemeName, findTheme, THEME_IDS, THEMES, DEFAULT_THEME, sizeRootCss, sizeSwitchableCss, sizeSummaries, resolveSizeName, findSize, SIZE_IDS, DEFAULT_SIZE, fontOverrideCss, fontSummaries, fontListing, fontName, findFont, FONT_IDS, FONTS, } from "./themes.js";
|
|
3
6
|
function escAttr(str) {
|
|
4
7
|
return str
|
|
@@ -43,7 +46,15 @@ export function renderSlide(slide, index) {
|
|
|
43
46
|
}
|
|
44
47
|
/** Box-model reset shared by the deck and the editor preview. */
|
|
45
48
|
export const RESET_CSS = `/* ── Reset & base ─────────────────────────────────────────────────────── */
|
|
46
|
-
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
49
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
50
|
+
::selection {
|
|
51
|
+
background: var(--selection-bg, var(--accent-line, rgba(56, 139, 253, 0.22)));
|
|
52
|
+
color: var(--selection-text, inherit);
|
|
53
|
+
}
|
|
54
|
+
::-moz-selection {
|
|
55
|
+
background: var(--selection-bg, var(--accent-line, rgba(56, 139, 253, 0.22)));
|
|
56
|
+
color: var(--selection-text, inherit);
|
|
57
|
+
}`;
|
|
47
58
|
/** Slide rendering rules. Shared verbatim by the deck and the editor preview. */
|
|
48
59
|
export const SLIDE_CSS = `html, body {
|
|
49
60
|
height: 100%;
|
|
@@ -787,7 +798,7 @@ const CHROME_CSS = `/* ── HUD (progress + counter) ────────
|
|
|
787
798
|
.slide__content iframe, .slide__content video { max-height: 4in !important; }
|
|
788
799
|
|
|
789
800
|
#hud, .nav-arrow, #overview, #kbd-hint, #cursor, #fs-hint, .pet,
|
|
790
|
-
#board, #laser, #blackout, #help {
|
|
801
|
+
#board, #laser, #blackout, #help, #themes {
|
|
791
802
|
display: none !important;
|
|
792
803
|
}
|
|
793
804
|
}`;
|
|
@@ -890,6 +901,38 @@ const PRESENTER_CSS = `/* ── HUD tool strip ──────────
|
|
|
890
901
|
text-align: center;
|
|
891
902
|
}
|
|
892
903
|
|
|
904
|
+
#hud-right {
|
|
905
|
+
display: flex;
|
|
906
|
+
align-items: center;
|
|
907
|
+
gap: 1.1rem;
|
|
908
|
+
flex: 0 0 auto;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.hud-brand {
|
|
912
|
+
pointer-events: all;
|
|
913
|
+
font-size: 0.64rem;
|
|
914
|
+
color: var(--overlay0);
|
|
915
|
+
text-decoration: none;
|
|
916
|
+
letter-spacing: 0.06em;
|
|
917
|
+
opacity: 0.72;
|
|
918
|
+
transition: opacity 0.15s ease, color 0.15s ease;
|
|
919
|
+
white-space: nowrap;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
.hud-brand:hover {
|
|
923
|
+
opacity: 1;
|
|
924
|
+
color: var(--accent);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
.hud-brand span {
|
|
928
|
+
font-weight: 600;
|
|
929
|
+
color: var(--subtext1);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
.hud-brand:hover span {
|
|
933
|
+
color: var(--accent);
|
|
934
|
+
}
|
|
935
|
+
|
|
893
936
|
/* ── Annotation canvas ────────────────────────────────────────────────── */
|
|
894
937
|
#board {
|
|
895
938
|
position: fixed;
|
|
@@ -945,6 +988,215 @@ body.laser-on, body.laser-on #board.is-drawing { cursor: none; }
|
|
|
945
988
|
|
|
946
989
|
#blackout.is-on { display: block; }
|
|
947
990
|
|
|
991
|
+
/* ── Theme picker overlay ─────────────────────────────────────────────── */
|
|
992
|
+
#themes {
|
|
993
|
+
position: fixed;
|
|
994
|
+
inset: 0;
|
|
995
|
+
z-index: 450;
|
|
996
|
+
display: none;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
#themes.is-on { display: block; }
|
|
1000
|
+
|
|
1001
|
+
#themes__backdrop {
|
|
1002
|
+
position: absolute;
|
|
1003
|
+
inset: 0;
|
|
1004
|
+
background: var(--crust-overlay);
|
|
1005
|
+
backdrop-filter: blur(4px);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
#themes__box {
|
|
1009
|
+
font-family: var(--font-mono);
|
|
1010
|
+
position: relative;
|
|
1011
|
+
width: min(980px, 94vw);
|
|
1012
|
+
max-height: 84vh;
|
|
1013
|
+
margin: 6vh auto 0;
|
|
1014
|
+
background: var(--mantle);
|
|
1015
|
+
border: 1px solid var(--surface1);
|
|
1016
|
+
border-radius: 14px;
|
|
1017
|
+
box-shadow: var(--shadow-lg);
|
|
1018
|
+
overflow: hidden;
|
|
1019
|
+
display: flex;
|
|
1020
|
+
flex-direction: column;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
#themes__head {
|
|
1024
|
+
display: flex;
|
|
1025
|
+
align-items: baseline;
|
|
1026
|
+
justify-content: space-between;
|
|
1027
|
+
gap: 0.8rem;
|
|
1028
|
+
padding: 1rem 1.4rem;
|
|
1029
|
+
border-bottom: 1px solid var(--surface0);
|
|
1030
|
+
background: var(--mantle);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
.th-head__title {
|
|
1034
|
+
font-size: 0.88rem;
|
|
1035
|
+
font-weight: 600;
|
|
1036
|
+
letter-spacing: 0.1em;
|
|
1037
|
+
text-transform: uppercase;
|
|
1038
|
+
color: var(--text);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
.th-head__sub {
|
|
1042
|
+
flex: 1 1 auto;
|
|
1043
|
+
font-size: 0.68rem;
|
|
1044
|
+
color: var(--overlay1);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
.th-head__sub kbd {
|
|
1048
|
+
display: inline-block;
|
|
1049
|
+
font: inherit;
|
|
1050
|
+
font-size: 0.6rem;
|
|
1051
|
+
background: var(--surface0);
|
|
1052
|
+
border: 1px solid var(--surface1);
|
|
1053
|
+
border-radius: 3px;
|
|
1054
|
+
padding: 0.05em 0.35em;
|
|
1055
|
+
color: var(--lavender);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
.th-head__brand {
|
|
1059
|
+
font-size: 0.65rem;
|
|
1060
|
+
color: var(--overlay0);
|
|
1061
|
+
text-decoration: none;
|
|
1062
|
+
padding: 0.2rem 0.5rem;
|
|
1063
|
+
border-radius: 4px;
|
|
1064
|
+
background: var(--surface0);
|
|
1065
|
+
border: 1px solid var(--surface1);
|
|
1066
|
+
transition: color 0.15s ease, border-color 0.15s ease;
|
|
1067
|
+
}
|
|
1068
|
+
.th-head__brand:hover { color: var(--accent); border-color: var(--accent-line); }
|
|
1069
|
+
|
|
1070
|
+
#themes__close {
|
|
1071
|
+
background: transparent;
|
|
1072
|
+
border: none;
|
|
1073
|
+
color: var(--overlay0);
|
|
1074
|
+
font-size: 1.25rem;
|
|
1075
|
+
line-height: 1;
|
|
1076
|
+
cursor: pointer;
|
|
1077
|
+
padding: 0 0.2rem;
|
|
1078
|
+
}
|
|
1079
|
+
#themes__close:hover { color: var(--text); }
|
|
1080
|
+
|
|
1081
|
+
#themes__list {
|
|
1082
|
+
flex: 1 1 auto;
|
|
1083
|
+
overflow-y: auto;
|
|
1084
|
+
padding: 1.1rem 1.4rem 1.4rem;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
.th-group {
|
|
1088
|
+
padding: 0.4rem 0.2rem 0.6rem;
|
|
1089
|
+
font-size: 0.62rem;
|
|
1090
|
+
letter-spacing: 0.16em;
|
|
1091
|
+
text-transform: uppercase;
|
|
1092
|
+
color: var(--accent);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.th-grid {
|
|
1096
|
+
display: grid;
|
|
1097
|
+
grid-template-columns: repeat(auto-fill, minmax(195px, 1fr));
|
|
1098
|
+
gap: 0.85rem;
|
|
1099
|
+
margin-bottom: 1.2rem;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
.th-card {
|
|
1103
|
+
display: flex;
|
|
1104
|
+
flex-direction: column;
|
|
1105
|
+
padding: 0;
|
|
1106
|
+
overflow: hidden;
|
|
1107
|
+
border: 1px solid var(--surface1);
|
|
1108
|
+
border-radius: 10px;
|
|
1109
|
+
background: var(--base);
|
|
1110
|
+
cursor: pointer;
|
|
1111
|
+
text-align: left;
|
|
1112
|
+
font-family: inherit;
|
|
1113
|
+
transition: border-color 0.14s ease, transform 0.14s ease, box-shadow 0.14s ease;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
.th-card:hover, .th-card.is-sel {
|
|
1117
|
+
border-color: var(--accent);
|
|
1118
|
+
transform: translateY(-2px);
|
|
1119
|
+
box-shadow: var(--shadow-md);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
.th-card.is-current { box-shadow: inset 0 0 0 1px var(--accent); }
|
|
1123
|
+
|
|
1124
|
+
.th-thumb {
|
|
1125
|
+
position: relative;
|
|
1126
|
+
aspect-ratio: 16 / 9;
|
|
1127
|
+
padding: 11px 12px;
|
|
1128
|
+
overflow: hidden;
|
|
1129
|
+
border-bottom: 1px solid var(--surface0);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
.th-thumb__glow {
|
|
1133
|
+
position: absolute;
|
|
1134
|
+
width: 150%;
|
|
1135
|
+
height: 150%;
|
|
1136
|
+
right: -55%;
|
|
1137
|
+
top: -60%;
|
|
1138
|
+
border-radius: 50%;
|
|
1139
|
+
pointer-events: none;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
.th-thumb__title {
|
|
1143
|
+
position: relative;
|
|
1144
|
+
font-size: 13px;
|
|
1145
|
+
line-height: 1.1;
|
|
1146
|
+
margin-bottom: 5px;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.th-thumb__rule { position: relative; width: 30px; height: 2px; border-radius: 2px; margin-bottom: 7px; }
|
|
1150
|
+
.th-thumb__line { position: relative; height: 2.5px; border-radius: 2px; margin-bottom: 4px; }
|
|
1151
|
+
.th-thumb__code {
|
|
1152
|
+
position: relative;
|
|
1153
|
+
margin-top: 6px;
|
|
1154
|
+
border-radius: 4px;
|
|
1155
|
+
padding: 3px 5px;
|
|
1156
|
+
font-size: 7.5px;
|
|
1157
|
+
letter-spacing: 0;
|
|
1158
|
+
white-space: nowrap;
|
|
1159
|
+
overflow: hidden;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
.th-meta { padding: 8px 10px 10px; }
|
|
1163
|
+
.th-meta__top { display: flex; align-items: baseline; gap: 6px; }
|
|
1164
|
+
.th-meta__name { flex: 1 1 auto; font-size: 11.5px; color: var(--text); }
|
|
1165
|
+
.th-meta__mood {
|
|
1166
|
+
flex: 0 0 auto;
|
|
1167
|
+
font-size: 8px;
|
|
1168
|
+
letter-spacing: 0.12em;
|
|
1169
|
+
text-transform: uppercase;
|
|
1170
|
+
color: var(--overlay0);
|
|
1171
|
+
}
|
|
1172
|
+
.th-meta__blurb {
|
|
1173
|
+
font-size: 9.5px;
|
|
1174
|
+
line-height: 1.5;
|
|
1175
|
+
color: var(--overlay1);
|
|
1176
|
+
margin-top: 3px;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
#themes__foot {
|
|
1180
|
+
display: flex;
|
|
1181
|
+
align-items: center;
|
|
1182
|
+
justify-content: space-between;
|
|
1183
|
+
gap: 1rem;
|
|
1184
|
+
padding: 0.8rem 1.4rem;
|
|
1185
|
+
border-top: 1px solid var(--surface0);
|
|
1186
|
+
background: var(--mantle);
|
|
1187
|
+
font-size: 0.65rem;
|
|
1188
|
+
color: var(--overlay1);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
.th-foot__brand a, #help__foot a {
|
|
1192
|
+
color: var(--accent);
|
|
1193
|
+
text-decoration: none;
|
|
1194
|
+
font-weight: 600;
|
|
1195
|
+
}
|
|
1196
|
+
.th-foot__brand a:hover, #help__foot a:hover {
|
|
1197
|
+
text-decoration: underline;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
948
1200
|
/* ── Controls overlay ─────────────────────────────────────────────────── */
|
|
949
1201
|
#help {
|
|
950
1202
|
position: fixed;
|
|
@@ -1055,31 +1307,43 @@ body.laser-on, body.laser-on #board.is-drawing { cursor: none; }
|
|
|
1055
1307
|
font-size: 0.66rem;
|
|
1056
1308
|
color: var(--overlay1);
|
|
1057
1309
|
line-height: 1.7;
|
|
1310
|
+
display: flex;
|
|
1311
|
+
justify-content: space-between;
|
|
1312
|
+
align-items: center;
|
|
1313
|
+
flex-wrap: wrap;
|
|
1314
|
+
gap: 0.5rem;
|
|
1058
1315
|
}`;
|
|
1059
|
-
export function generateHtml(slides, title, autoFullscreen = false, themeInput = DEFAULT_THEME, sizeInput = DEFAULT_SIZE, fonts = {}) {
|
|
1316
|
+
export function generateHtml(slides, title, autoFullscreen = false, themeInput = DEFAULT_THEME, sizeInput = DEFAULT_SIZE, fonts = {}, presentation = {}) {
|
|
1060
1317
|
const theme = resolveThemeName(themeInput);
|
|
1061
1318
|
const size = resolveSizeName(sizeInput);
|
|
1319
|
+
const template = resolveTemplateName(presentation.template ?? DEFAULT_TEMPLATE);
|
|
1320
|
+
const transition = resolveTransitionName(presentation.transition ?? DEFAULT_TRANSITION);
|
|
1062
1321
|
const head = findFont(fonts.head);
|
|
1063
1322
|
const body = findFont(fonts.body);
|
|
1064
1323
|
const fontAttrs = (head ? ` data-head="${head}"` : "") + (body ? ` data-body="${body}"` : "");
|
|
1065
1324
|
const slideHtml = slides.map((s, i) => renderSlide(s, i)).join("\n");
|
|
1066
1325
|
const total = slides.length;
|
|
1326
|
+
const rich = richContentFeatures(slides);
|
|
1327
|
+
// Speaker notes ride along as JSON so the editor's notes panel can show
|
|
1328
|
+
// them; they are never rendered into the deck itself.
|
|
1329
|
+
const notesJson = JSON.stringify(slides.map((s) => s.notes ?? "")).replace(/</g, "\\u003c");
|
|
1067
1330
|
const pageTitle = title ? (title.toLowerCase().includes("deckrun") ? title : `${title} · deckrun`) : "deckrun";
|
|
1068
1331
|
return `<!DOCTYPE html>
|
|
1069
|
-
<html lang="en" data-theme="${theme}" data-decor="${decorOf(theme)}" data-size="${size}"${fontAttrs}>
|
|
1332
|
+
<html lang="en" data-theme="${theme}" data-decor="${decorOf(theme)}" data-size="${size}" data-template="${template}" data-transition="${transition}"${fontAttrs}>
|
|
1070
1333
|
<head>
|
|
1071
1334
|
<meta charset="UTF-8">
|
|
1072
1335
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1073
1336
|
<title>${escAttr(pageTitle)}</title>
|
|
1074
1337
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1075
1338
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
1076
|
-
<link href="${googleFontsHref(
|
|
1077
|
-
<link rel="stylesheet" href="${hljsHref(theme)}">
|
|
1339
|
+
<link href="${googleFontsHref(THEME_IDS.slice(), [head, body])}" rel="stylesheet">
|
|
1340
|
+
<link rel="stylesheet" id="hljs-theme" href="${hljsHref(theme)}">
|
|
1078
1341
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
1342
|
+
${richContentHead(rich, presentation.standalone ? "cdn" : "local")}
|
|
1079
1343
|
<style>
|
|
1080
1344
|
${RESET_CSS}
|
|
1081
1345
|
|
|
1082
|
-
${
|
|
1346
|
+
${themeSwitchableCss()}
|
|
1083
1347
|
|
|
1084
1348
|
${sizeRootCss(size)}
|
|
1085
1349
|
|
|
@@ -1087,6 +1351,14 @@ ${fontOverrideCss()}
|
|
|
1087
1351
|
|
|
1088
1352
|
${SLIDE_CSS}
|
|
1089
1353
|
|
|
1354
|
+
${TEMPLATE_CSS}
|
|
1355
|
+
|
|
1356
|
+
${TRANSITION_CSS}
|
|
1357
|
+
|
|
1358
|
+
${FRAGMENT_CSS}
|
|
1359
|
+
|
|
1360
|
+
${RICH_CONTENT_CSS}
|
|
1361
|
+
|
|
1090
1362
|
${DECOR_CSS}
|
|
1091
1363
|
|
|
1092
1364
|
${CHROME_CSS}
|
|
@@ -1110,6 +1382,7 @@ ${slideHtml}
|
|
|
1110
1382
|
<button class="hud-btn" id="btn-pen" title="Draw on the slide (D)">pen <kbd>D</kbd></button>
|
|
1111
1383
|
<button class="hud-btn" id="btn-blank" title="Blank canvas over the slide (C)">canvas <kbd>C</kbd></button>
|
|
1112
1384
|
<button class="hud-btn" id="btn-black" title="Black out the screen (B)">black <kbd>B</kbd></button>
|
|
1385
|
+
<button class="hud-btn" id="btn-theme" title="Change theme (T)">theme <kbd>T</kbd></button>
|
|
1113
1386
|
<div id="pen-bar">
|
|
1114
1387
|
<span id="hud-sep"></span>
|
|
1115
1388
|
<span id="pen-swatches"></span>
|
|
@@ -1121,7 +1394,10 @@ ${slideHtml}
|
|
|
1121
1394
|
</div>
|
|
1122
1395
|
<button class="hud-btn" id="btn-help" title="Show every control (?)">? controls</button>
|
|
1123
1396
|
</div>
|
|
1124
|
-
<div id="
|
|
1397
|
+
<div id="hud-right">
|
|
1398
|
+
<a href="https://github.com/arpitbbhayani/deckrun" target="_blank" rel="noopener noreferrer" class="hud-brand" id="hud-brand" title="deckrun — Markdown presentations">powered by <span>deckrun</span></a>
|
|
1399
|
+
<div id="slide-counter"><span id="cur">1</span> / <span id="tot">${total}</span></div>
|
|
1400
|
+
</div>
|
|
1125
1401
|
</div>
|
|
1126
1402
|
</div>
|
|
1127
1403
|
|
|
@@ -1129,6 +1405,23 @@ ${slideHtml}
|
|
|
1129
1405
|
<div id="laser" aria-hidden="true"></div>
|
|
1130
1406
|
<div id="blackout" title="Click or press B to come back"></div>
|
|
1131
1407
|
|
|
1408
|
+
<div id="themes" role="dialog" aria-modal="true" aria-label="Theme picker">
|
|
1409
|
+
<div id="themes__backdrop" data-close="themes"></div>
|
|
1410
|
+
<div id="themes__box">
|
|
1411
|
+
<div id="themes__head">
|
|
1412
|
+
<span class="th-head__title">themes</span>
|
|
1413
|
+
<span class="th-head__sub">Arrow keys preview live · enter selects · esc closes</span>
|
|
1414
|
+
<a href="https://github.com/arpitbbhayani/deckrun" target="_blank" rel="noopener noreferrer" class="th-head__brand" title="deckrun">deckrun</a>
|
|
1415
|
+
<button id="themes__close" data-close="themes" title="Close (Esc)">×</button>
|
|
1416
|
+
</div>
|
|
1417
|
+
<div id="themes__list"></div>
|
|
1418
|
+
<div id="themes__foot">
|
|
1419
|
+
<span class="th-foot__brand">powered by <a href="https://github.com/arpitbbhayani/deckrun" target="_blank" rel="noopener noreferrer">deckrun</a></span>
|
|
1420
|
+
<span class="th-foot__hint">Switch themes on the fly</span>
|
|
1421
|
+
</div>
|
|
1422
|
+
</div>
|
|
1423
|
+
</div>
|
|
1424
|
+
|
|
1132
1425
|
<div id="help" role="dialog" aria-modal="true" aria-label="Presenter controls">
|
|
1133
1426
|
<div id="help__backdrop" data-close="help"></div>
|
|
1134
1427
|
<div id="help__panel">
|
|
@@ -1139,8 +1432,8 @@ ${slideHtml}
|
|
|
1139
1432
|
</div>
|
|
1140
1433
|
<div id="help__grid"></div>
|
|
1141
1434
|
<div id="help__foot">
|
|
1142
|
-
Annotations live per slide and survive navigation
|
|
1143
|
-
|
|
1435
|
+
<span>Annotations live per slide and survive navigation.</span>
|
|
1436
|
+
<span>powered by <a href="https://github.com/arpitbbhayani/deckrun" target="_blank" rel="noopener noreferrer">deckrun</a></span>
|
|
1144
1437
|
</div>
|
|
1145
1438
|
</div>
|
|
1146
1439
|
</div>
|
|
@@ -1152,12 +1445,20 @@ ${slideHtml}
|
|
|
1152
1445
|
|
|
1153
1446
|
<div id="overview" class="hidden"></div>
|
|
1154
1447
|
|
|
1155
|
-
<div id="kbd-hint">← → navigate · O overview · F fullscreen · L laser · D draw · ? controls</div>
|
|
1448
|
+
<div id="kbd-hint">← → reveal / navigate · O overview · F fullscreen · T theme · L laser · D draw · ? controls</div>
|
|
1156
1449
|
|
|
1157
1450
|
${autoFullscreen ? `<div id="fs-hint">
|
|
1158
1451
|
<div id="fs-hint__inner">Press any key or click to enter fullscreen</div>
|
|
1159
1452
|
</div>` : ''}
|
|
1160
1453
|
|
|
1454
|
+
<script id="deck-notes" type="application/json">${notesJson}</script>
|
|
1455
|
+
<script id="deck-themes" type="application/json">${JSON.stringify({ themes: themeSummaries(), hljsMap: JSON.parse(hljsMapJson()), decorMap: JSON.parse(decorMapJson()) })}</script>
|
|
1456
|
+
|
|
1457
|
+
<script>
|
|
1458
|
+
${FRAGMENT_RUNTIME}
|
|
1459
|
+
|
|
1460
|
+
${RICH_CONTENT_RUNTIME}
|
|
1461
|
+
</script>
|
|
1161
1462
|
<script>
|
|
1162
1463
|
(function () {
|
|
1163
1464
|
'use strict';
|
|
@@ -1177,18 +1478,78 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1177
1478
|
const elLaser = document.getElementById('laser');
|
|
1178
1479
|
const elBlack = document.getElementById('blackout');
|
|
1179
1480
|
const elHelp = document.getElementById('help');
|
|
1481
|
+
const elThemes = document.getElementById('themes');
|
|
1180
1482
|
const elPenBar = document.getElementById('pen-bar');
|
|
1181
1483
|
const elPenWidth = document.getElementById('pen-width');
|
|
1182
1484
|
|
|
1485
|
+
if (window.deckrunPrepareFragments) {
|
|
1486
|
+
window.deckrunPrepareFragments(document.getElementById('presentation'), false);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
const fragmentSteps = slides.map(() => 0);
|
|
1490
|
+
|
|
1491
|
+
function fragmentsAt(index) {
|
|
1492
|
+
return Array.from(slides[index].querySelectorAll('.fragment'));
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function applyFragmentStep(index) {
|
|
1496
|
+
const fragments = fragmentsAt(index);
|
|
1497
|
+
const step = Math.max(0, Math.min(fragmentSteps[index] || 0, fragments.length));
|
|
1498
|
+
fragmentSteps[index] = step;
|
|
1499
|
+
fragments.forEach(function (fragment, i) {
|
|
1500
|
+
const shown = i < step;
|
|
1501
|
+
fragment.classList.toggle('is-revealed', shown);
|
|
1502
|
+
fragment.setAttribute('aria-hidden', shown ? 'false' : 'true');
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function revealNext() {
|
|
1507
|
+
const count = fragmentsAt(cur).length;
|
|
1508
|
+
if ((fragmentSteps[cur] || 0) >= count) return false;
|
|
1509
|
+
fragmentSteps[cur] = (fragmentSteps[cur] || 0) + 1;
|
|
1510
|
+
applyFragmentStep(cur);
|
|
1511
|
+
updateHud();
|
|
1512
|
+
return true;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function concealPrevious() {
|
|
1516
|
+
if ((fragmentSteps[cur] || 0) <= 0) return false;
|
|
1517
|
+
fragmentSteps[cur] -= 1;
|
|
1518
|
+
applyFragmentStep(cur);
|
|
1519
|
+
updateHud();
|
|
1520
|
+
return true;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1183
1523
|
// ── Syntax highlighting ──────────────────────────────────────────────
|
|
1184
1524
|
// Guarded: the highlighter comes off a CDN, and a deck presented offline
|
|
1185
1525
|
// should still navigate rather than die on a missing global.
|
|
1186
|
-
if (window.hljs)
|
|
1526
|
+
if (window.hljs) {
|
|
1527
|
+
document.querySelectorAll('pre code:not(.language-mermaid):not(.lang-mermaid)').forEach(function (block) {
|
|
1528
|
+
try { window.hljs.highlightElement(block); } catch (e) {}
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
const richReady = window.deckrunRenderRichContent
|
|
1533
|
+
? window.deckrunRenderRichContent(document.getElementById('presentation'))
|
|
1534
|
+
: Promise.resolve();
|
|
1187
1535
|
|
|
1188
1536
|
// ── Slide navigation ─────────────────────────────────────────────────
|
|
1189
|
-
function showSlide(next, direction) {
|
|
1537
|
+
function showSlide(next, direction, fragmentMode) {
|
|
1190
1538
|
const prev = cur;
|
|
1191
|
-
if (next < 0 || next >= total
|
|
1539
|
+
if (next < 0 || next >= total) return;
|
|
1540
|
+
|
|
1541
|
+
if (next === prev && !fragmentMode) return;
|
|
1542
|
+
|
|
1543
|
+
const nextFragments = fragmentsAt(next).length;
|
|
1544
|
+
fragmentSteps[next] = fragmentMode === 'start' || (fragmentMode !== 'end' && direction === 'forward')
|
|
1545
|
+
? 0
|
|
1546
|
+
: nextFragments;
|
|
1547
|
+
applyFragmentStep(next);
|
|
1548
|
+
|
|
1549
|
+
if (next === prev) {
|
|
1550
|
+
updateHud();
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1192
1553
|
|
|
1193
1554
|
const slideOut = slides[prev];
|
|
1194
1555
|
const slideIn = slides[next];
|
|
@@ -1211,10 +1572,18 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1211
1572
|
slideOut.classList.add(exitClass);
|
|
1212
1573
|
|
|
1213
1574
|
// Clean up exit class after transition
|
|
1214
|
-
|
|
1575
|
+
var cleaned = false;
|
|
1576
|
+
function cleanup() {
|
|
1577
|
+
if (cleaned) return;
|
|
1578
|
+
cleaned = true;
|
|
1215
1579
|
slideOut.classList.remove(exitClass, 'exit-left', 'exit-right');
|
|
1216
1580
|
slideOut.removeEventListener('transitionend', cleanup);
|
|
1217
|
-
}
|
|
1581
|
+
}
|
|
1582
|
+
slideOut.addEventListener('transitionend', cleanup);
|
|
1583
|
+
// The no-motion preset has no transitionend event. The timeout also guards interrupted
|
|
1584
|
+
// transitions, so no slide can retain an exit class indefinitely.
|
|
1585
|
+
if (document.documentElement.dataset.transition === 'none') cleanup();
|
|
1586
|
+
else setTimeout(cleanup, 650);
|
|
1218
1587
|
|
|
1219
1588
|
cur = next;
|
|
1220
1589
|
updateHud();
|
|
@@ -1225,12 +1594,62 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1225
1594
|
elCur.textContent = String(cur + 1);
|
|
1226
1595
|
const pct = total > 1 ? (cur / (total - 1)) * 100 : 100;
|
|
1227
1596
|
elFill.style.width = pct + '%';
|
|
1228
|
-
|
|
1229
|
-
|
|
1597
|
+
const step = fragmentSteps[cur] || 0;
|
|
1598
|
+
const count = fragmentsAt(cur).length;
|
|
1599
|
+
elBtnPrev.style.opacity = cur === 0 && step === 0 ? '0.2' : '1';
|
|
1600
|
+
elBtnNext.style.opacity = cur === total - 1 && step >= count ? '0.2' : '1';
|
|
1601
|
+
post({ type: 'state', index: cur, total: total });
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function next() {
|
|
1605
|
+
if (!revealNext()) showSlide(cur + 1, 'forward');
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function prev() {
|
|
1609
|
+
if (!concealPrevious()) showSlide(cur - 1, 'backward');
|
|
1230
1610
|
}
|
|
1231
1611
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1612
|
+
// ── Editor mirror ─────────────────────────────────────────────────────
|
|
1613
|
+
// The editor that presented this tab is a peer of it: this tab feeds it,
|
|
1614
|
+
// over BroadcastChannel, the slides on screen plus the speaker notes. The
|
|
1615
|
+
// session id comes from the URL the editor opened (ps=...), so only a deck
|
|
1616
|
+
// an editor actually presented listens; one opened straight from a file
|
|
1617
|
+
// skips the machinery entirely.
|
|
1618
|
+
const psParam = new URLSearchParams(location.search).get('ps');
|
|
1619
|
+
const sid = (psParam && /^[A-Za-z0-9-]{1,64}$/.test(psParam)) ? psParam : null;
|
|
1620
|
+
const channel = (sid && typeof BroadcastChannel !== 'undefined')
|
|
1621
|
+
? new BroadcastChannel('deckrun:' + sid)
|
|
1622
|
+
: null;
|
|
1623
|
+
|
|
1624
|
+
function post(msg) {
|
|
1625
|
+
if (!channel) return;
|
|
1626
|
+
msg.id = sid;
|
|
1627
|
+
channel.postMessage(msg);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (channel) {
|
|
1631
|
+
const NOTES = (function readNotes() {
|
|
1632
|
+
try {
|
|
1633
|
+
return JSON.parse(document.getElementById('deck-notes').textContent);
|
|
1634
|
+
} catch (e) {
|
|
1635
|
+
return [];
|
|
1636
|
+
}
|
|
1637
|
+
})();
|
|
1638
|
+
|
|
1639
|
+
channel.onmessage = function (e) {
|
|
1640
|
+
const m = e.data || {};
|
|
1641
|
+
if (m.id !== sid) return;
|
|
1642
|
+
if (m.type === 'ready') {
|
|
1643
|
+
post({ type: 'init', notes: NOTES });
|
|
1644
|
+
post({ type: 'state', index: cur, total: total });
|
|
1645
|
+
} else if (m.type === 'goto') {
|
|
1646
|
+
const i = parseInt(m.index, 10);
|
|
1647
|
+
if (!isNaN(i) && i >= 0 && i < total && i !== cur) {
|
|
1648
|
+
showSlide(i, i > cur ? 'forward' : 'backward');
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1234
1653
|
|
|
1235
1654
|
// ── Overview mode ────────────────────────────────────────────────────
|
|
1236
1655
|
function buildOverview() {
|
|
@@ -1251,6 +1670,10 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1251
1670
|
const clone = slide.cloneNode(true);
|
|
1252
1671
|
clone.classList.add('is-active');
|
|
1253
1672
|
clone.style.transition = 'none';
|
|
1673
|
+
clone.querySelectorAll('.fragment').forEach(function (fragment) {
|
|
1674
|
+
fragment.classList.add('is-revealed');
|
|
1675
|
+
fragment.setAttribute('aria-hidden', 'false');
|
|
1676
|
+
});
|
|
1254
1677
|
inner.appendChild(clone);
|
|
1255
1678
|
|
|
1256
1679
|
thumb.appendChild(num);
|
|
@@ -1316,6 +1739,7 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1316
1739
|
pen: document.getElementById('btn-pen'),
|
|
1317
1740
|
blank: document.getElementById('btn-blank'),
|
|
1318
1741
|
black: document.getElementById('btn-black'),
|
|
1742
|
+
theme: document.getElementById('btn-theme'),
|
|
1319
1743
|
erase: document.getElementById('btn-erase'),
|
|
1320
1744
|
};
|
|
1321
1745
|
|
|
@@ -1503,6 +1927,7 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1503
1927
|
tools.pen.classList.toggle('is-on', penOn);
|
|
1504
1928
|
tools.blank.classList.toggle('is-on', blankOn);
|
|
1505
1929
|
tools.black.classList.toggle('is-on', blackOn);
|
|
1930
|
+
if (tools.theme) tools.theme.classList.toggle('is-on', themesOpen());
|
|
1506
1931
|
tools.erase.classList.toggle('is-on', eraseOn);
|
|
1507
1932
|
|
|
1508
1933
|
elBoard.classList.toggle('is-drawing', penOn);
|
|
@@ -1521,6 +1946,200 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1521
1946
|
elHelp.classList.toggle('is-on', helpOn);
|
|
1522
1947
|
}
|
|
1523
1948
|
|
|
1949
|
+
// ── Theme picker ─────────────────────────────────────────────────────
|
|
1950
|
+
let themeBootstrap = { themes: [], hljsMap: {}, decorMap: {} };
|
|
1951
|
+
try {
|
|
1952
|
+
const elBt = document.getElementById('deck-themes');
|
|
1953
|
+
if (elBt) themeBootstrap = JSON.parse(elBt.textContent || '{}');
|
|
1954
|
+
} catch (e) {}
|
|
1955
|
+
const themeList = themeBootstrap.themes || [];
|
|
1956
|
+
const hljsMap = themeBootstrap.hljsMap || {};
|
|
1957
|
+
const decorMap = themeBootstrap.decorMap || {};
|
|
1958
|
+
const THEME_DATA = Object.fromEntries(themeList.map(function (t) { return [t.id, t]; }));
|
|
1959
|
+
let activeTheme = document.documentElement.dataset.theme || 'nord';
|
|
1960
|
+
let themeCommitted = activeTheme;
|
|
1961
|
+
let themeSel = 0;
|
|
1962
|
+
let themeCards = [];
|
|
1963
|
+
|
|
1964
|
+
function themeThumb(t) {
|
|
1965
|
+
const c = t.colors;
|
|
1966
|
+
const wrap = document.createElement('div');
|
|
1967
|
+
wrap.className = 'th-thumb';
|
|
1968
|
+
wrap.style.background = c.crust;
|
|
1969
|
+
wrap.style.color = c.text;
|
|
1970
|
+
|
|
1971
|
+
const glow = document.createElement('div');
|
|
1972
|
+
glow.className = 'th-thumb__glow';
|
|
1973
|
+
glow.style.background = 'radial-gradient(circle, ' + c.accent + ' 0%, transparent 70%)';
|
|
1974
|
+
glow.style.opacity = t.mood === 'dark' ? '0.22' : '0.12';
|
|
1975
|
+
wrap.appendChild(glow);
|
|
1976
|
+
|
|
1977
|
+
const title = document.createElement('div');
|
|
1978
|
+
title.className = 'th-thumb__title';
|
|
1979
|
+
title.textContent = t.label;
|
|
1980
|
+
title.style.fontFamily = t.fonts.display;
|
|
1981
|
+
title.style.color = c.accent;
|
|
1982
|
+
wrap.appendChild(title);
|
|
1983
|
+
|
|
1984
|
+
const rule = document.createElement('div');
|
|
1985
|
+
rule.className = 'th-thumb__rule';
|
|
1986
|
+
rule.style.background = c.accent2;
|
|
1987
|
+
wrap.appendChild(rule);
|
|
1988
|
+
|
|
1989
|
+
[75, 55].forEach(function (w) {
|
|
1990
|
+
const line = document.createElement('div');
|
|
1991
|
+
line.className = 'th-thumb__line';
|
|
1992
|
+
line.style.width = w + '%';
|
|
1993
|
+
line.style.background = c.surface0;
|
|
1994
|
+
wrap.appendChild(line);
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
const code = document.createElement('div');
|
|
1998
|
+
code.className = 'th-thumb__code';
|
|
1999
|
+
code.textContent = 'const deck = md';
|
|
2000
|
+
code.style.fontFamily = t.fonts.mono;
|
|
2001
|
+
code.style.background = c.mantle;
|
|
2002
|
+
code.style.color = c.accent3;
|
|
2003
|
+
code.style.border = '1px solid ' + c.surface0;
|
|
2004
|
+
wrap.appendChild(code);
|
|
2005
|
+
|
|
2006
|
+
return wrap;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
function themeCard(t) {
|
|
2010
|
+
const card = document.createElement('button');
|
|
2011
|
+
card.className = 'th-card' + (t.id === activeTheme ? ' is-current' : '');
|
|
2012
|
+
card.dataset.theme = t.id;
|
|
2013
|
+
card.appendChild(themeThumb(t));
|
|
2014
|
+
|
|
2015
|
+
const meta = document.createElement('div');
|
|
2016
|
+
meta.className = 'th-meta';
|
|
2017
|
+
|
|
2018
|
+
const top = document.createElement('div');
|
|
2019
|
+
top.className = 'th-meta__top';
|
|
2020
|
+
const name = document.createElement('span');
|
|
2021
|
+
name.className = 'th-meta__name';
|
|
2022
|
+
name.textContent = t.label;
|
|
2023
|
+
const mood = document.createElement('span');
|
|
2024
|
+
mood.className = 'th-meta__mood';
|
|
2025
|
+
mood.textContent = t.mood;
|
|
2026
|
+
top.appendChild(name);
|
|
2027
|
+
top.appendChild(mood);
|
|
2028
|
+
|
|
2029
|
+
const blurb = document.createElement('div');
|
|
2030
|
+
blurb.className = 'th-meta__blurb';
|
|
2031
|
+
blurb.textContent = t.blurb;
|
|
2032
|
+
|
|
2033
|
+
meta.appendChild(top);
|
|
2034
|
+
meta.appendChild(blurb);
|
|
2035
|
+
card.appendChild(meta);
|
|
2036
|
+
return card;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
function buildThemePicker() {
|
|
2040
|
+
const host = document.getElementById('themes__list');
|
|
2041
|
+
if (!host) return;
|
|
2042
|
+
host.innerHTML = '';
|
|
2043
|
+
themeCards = [];
|
|
2044
|
+
|
|
2045
|
+
[['dark', 'dark palettes'], ['light', 'light palettes']].forEach(function (pair) {
|
|
2046
|
+
const list = themeList.filter(function (t) { return t.mood === pair[0]; });
|
|
2047
|
+
if (!list.length) return;
|
|
2048
|
+
|
|
2049
|
+
const label = document.createElement('div');
|
|
2050
|
+
label.className = 'th-group';
|
|
2051
|
+
label.textContent = pair[1];
|
|
2052
|
+
host.appendChild(label);
|
|
2053
|
+
|
|
2054
|
+
const grid = document.createElement('div');
|
|
2055
|
+
grid.className = 'th-grid';
|
|
2056
|
+
list.forEach(function (t) {
|
|
2057
|
+
const card = themeCard(t);
|
|
2058
|
+
card.addEventListener('mouseenter', function () { selectTheme(themeCards.indexOf(card)); });
|
|
2059
|
+
card.addEventListener('click', function () { selectTheme(themeCards.indexOf(card)); commitTheme(); });
|
|
2060
|
+
grid.appendChild(card);
|
|
2061
|
+
themeCards.push(card);
|
|
2062
|
+
});
|
|
2063
|
+
host.appendChild(grid);
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
function updateThemePenColors() {
|
|
2068
|
+
const rs = getComputedStyle(document.documentElement);
|
|
2069
|
+
function tc(name, fb) {
|
|
2070
|
+
const v = rs.getPropertyValue('--' + name).trim();
|
|
2071
|
+
return v || fb;
|
|
2072
|
+
}
|
|
2073
|
+
PEN_COLORS[0] = tc('red', '#f38ba8');
|
|
2074
|
+
PEN_COLORS[1] = tc('yellow', '#f9e2af');
|
|
2075
|
+
PEN_COLORS[2] = tc('green', '#a6e3a1');
|
|
2076
|
+
PEN_COLORS[3] = tc('blue', '#89b4fa');
|
|
2077
|
+
PEN_COLORS[4] = tc('text', '#cdd6f4');
|
|
2078
|
+
swatches.forEach(function (sw, i) {
|
|
2079
|
+
if (sw && PEN_COLORS[i]) sw.style.background = PEN_COLORS[i];
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
function setTheme(id, remember) {
|
|
2084
|
+
if (!id || !THEME_DATA[id]) return;
|
|
2085
|
+
activeTheme = id;
|
|
2086
|
+
document.documentElement.dataset.theme = id;
|
|
2087
|
+
document.documentElement.dataset.decor = decorMap[id] || 'orbs';
|
|
2088
|
+
const hljsLink = document.getElementById('hljs-theme');
|
|
2089
|
+
if (hljsLink && hljsMap[id]) {
|
|
2090
|
+
hljsLink.href = hljsMap[id];
|
|
2091
|
+
}
|
|
2092
|
+
updateThemePenColors();
|
|
2093
|
+
if (remember !== false) {
|
|
2094
|
+
try { localStorage.setItem('deckrun.theme.v1', id); } catch (e) {}
|
|
2095
|
+
}
|
|
2096
|
+
if (themeCards.length) {
|
|
2097
|
+
themeCards.forEach(function (card) {
|
|
2098
|
+
card.classList.toggle('is-current', card.dataset.theme === id);
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
post({ type: 'theme', theme: id });
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
function selectTheme(i) {
|
|
2105
|
+
if (i < 0 || i >= themeCards.length) return;
|
|
2106
|
+
themeSel = i;
|
|
2107
|
+
themeCards.forEach(function (c, n) { c.classList.toggle('is-sel', n === i); });
|
|
2108
|
+
setTheme(themeCards[i].dataset.theme, false);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
function openThemes() {
|
|
2112
|
+
if (helpOn) setHelp(false);
|
|
2113
|
+
if (inOverview) toggleOverview(false);
|
|
2114
|
+
themeCommitted = activeTheme;
|
|
2115
|
+
buildThemePicker();
|
|
2116
|
+
let at = 0;
|
|
2117
|
+
themeCards.forEach(function (c, n) { if (c.dataset.theme === activeTheme) at = n; });
|
|
2118
|
+
elThemes.classList.add('is-on');
|
|
2119
|
+
syncTools();
|
|
2120
|
+
selectTheme(at);
|
|
2121
|
+
if (themeCards[themeSel]) themeCards[themeSel].scrollIntoView({ block: 'nearest' });
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
function commitTheme() {
|
|
2125
|
+
themeCommitted = activeTheme;
|
|
2126
|
+
setTheme(activeTheme, true);
|
|
2127
|
+
closeThemes(false);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
function closeThemes(restore) {
|
|
2131
|
+
if (restore && themeCommitted && themeCommitted !== activeTheme) {
|
|
2132
|
+
setTheme(themeCommitted, true);
|
|
2133
|
+
}
|
|
2134
|
+
elThemes.classList.remove('is-on');
|
|
2135
|
+
syncTools();
|
|
2136
|
+
themeCards.forEach(function (c) { c.classList.remove('is-sel'); });
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
function themesOpen() {
|
|
2140
|
+
return elThemes ? elThemes.classList.contains('is-on') : false;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
1524
2143
|
// ── Laser pointer ────────────────────────────────────────────────────
|
|
1525
2144
|
document.addEventListener('pointermove', function (e) {
|
|
1526
2145
|
if (!laserOn) return;
|
|
@@ -1530,8 +2149,8 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1530
2149
|
// ── Controls overlay ─────────────────────────────────────────────────
|
|
1531
2150
|
const HELP_GROUPS = [
|
|
1532
2151
|
{ title: 'navigate', rows: [
|
|
1533
|
-
{ keys: ['→', '↓', 'Space'], desc: 'Next slide' },
|
|
1534
|
-
{ keys: ['←', '↑', 'Backspace'], desc: 'Previous slide' },
|
|
2152
|
+
{ keys: ['→', '↓', 'Space'], desc: 'Next reveal or slide' },
|
|
2153
|
+
{ keys: ['←', '↑', 'Backspace'], desc: 'Previous reveal or slide' },
|
|
1535
2154
|
{ keys: ['Home'], desc: 'First slide' },
|
|
1536
2155
|
{ keys: ['End'], desc: 'Last slide' },
|
|
1537
2156
|
{ keys: ['O'], desc: 'Overview grid' },
|
|
@@ -1539,6 +2158,7 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1539
2158
|
]},
|
|
1540
2159
|
{ title: 'screen', rows: [
|
|
1541
2160
|
{ keys: ['F'], desc: 'Fullscreen' },
|
|
2161
|
+
{ keys: ['T'], desc: 'Theme picker' },
|
|
1542
2162
|
{ keys: ['B'], desc: 'Black out the screen' },
|
|
1543
2163
|
{ keys: ['?'], desc: 'These controls' },
|
|
1544
2164
|
]},
|
|
@@ -1608,12 +2228,17 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1608
2228
|
onClick(tools.blank, function () { setBlank(!blankOn); });
|
|
1609
2229
|
onClick(tools.black, function () { setBlack(true); });
|
|
1610
2230
|
onClick(tools.erase, function () { setEraser(!eraseOn); });
|
|
2231
|
+
onClick(tools.theme, function () { themesOpen() ? closeThemes(false) : openThemes(); });
|
|
1611
2232
|
onClick(document.getElementById('btn-thin'), function () { nudgeWidth(-1); });
|
|
1612
2233
|
onClick(document.getElementById('btn-thick'), function () { nudgeWidth(1); });
|
|
1613
2234
|
onClick(document.getElementById('btn-clear'), function () { clearSlide(); });
|
|
1614
2235
|
onClick(document.getElementById('btn-help'), function () { setHelp(!helpOn); });
|
|
1615
2236
|
onClick(elBlack, function () { setBlack(false); });
|
|
1616
2237
|
|
|
2238
|
+
Array.prototype.forEach.call(elThemes.querySelectorAll('[data-close="themes"]'), function (el) {
|
|
2239
|
+
onClick(el, function () { closeThemes(false); });
|
|
2240
|
+
});
|
|
2241
|
+
|
|
1617
2242
|
Array.prototype.forEach.call(elHelp.querySelectorAll('[data-close="help"]'), function (el) {
|
|
1618
2243
|
onClick(el, function () { setHelp(false); });
|
|
1619
2244
|
});
|
|
@@ -1624,16 +2249,55 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1624
2249
|
document.addEventListener('keydown', function (e) {
|
|
1625
2250
|
const k = e.key;
|
|
1626
2251
|
|
|
1627
|
-
// Undo
|
|
2252
|
+
// Undo and PDF/Print modifier combos
|
|
1628
2253
|
if (e.metaKey || e.ctrlKey || e.altKey) {
|
|
1629
2254
|
if ((k === 'z' || k === 'Z') && hasInk()) {
|
|
1630
2255
|
e.preventDefault();
|
|
1631
2256
|
undoStroke();
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
if (k === 'p' || k === 'P') {
|
|
2260
|
+
e.preventDefault();
|
|
2261
|
+
triggerPdfExport();
|
|
2262
|
+
return;
|
|
1632
2263
|
}
|
|
1633
2264
|
return;
|
|
1634
2265
|
}
|
|
1635
2266
|
|
|
1636
2267
|
// Each overlay swallows keys until it is dismissed, outermost first.
|
|
2268
|
+
if (themesOpen()) {
|
|
2269
|
+
if (k === 'Escape') {
|
|
2270
|
+
e.preventDefault();
|
|
2271
|
+
closeThemes(true);
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
if (k === 'Enter') {
|
|
2275
|
+
e.preventDefault();
|
|
2276
|
+
commitTheme();
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
if (k === 'ArrowRight' || k === 'ArrowDown') {
|
|
2280
|
+
e.preventDefault();
|
|
2281
|
+
const nextIdx = (themeSel + 1) % themeCards.length;
|
|
2282
|
+
selectTheme(nextIdx);
|
|
2283
|
+
if (themeCards[nextIdx]) themeCards[nextIdx].scrollIntoView({ block: 'nearest' });
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
if (k === 'ArrowLeft' || k === 'ArrowUp') {
|
|
2287
|
+
e.preventDefault();
|
|
2288
|
+
const prevIdx = (themeSel - 1 + themeCards.length) % themeCards.length;
|
|
2289
|
+
selectTheme(prevIdx);
|
|
2290
|
+
if (themeCards[prevIdx]) themeCards[prevIdx].scrollIntoView({ block: 'nearest' });
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
if (k === 't' || k === 'T') {
|
|
2294
|
+
e.preventDefault();
|
|
2295
|
+
closeThemes(false);
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
1637
2301
|
if (helpOn) {
|
|
1638
2302
|
if (k === 'Escape' || k === '?' || k === 'h' || k === 'H') {
|
|
1639
2303
|
e.preventDefault();
|
|
@@ -1694,17 +2358,22 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1694
2358
|
break;
|
|
1695
2359
|
case 'Home':
|
|
1696
2360
|
e.preventDefault();
|
|
1697
|
-
showSlide(0, 'backward');
|
|
2361
|
+
showSlide(0, 'backward', 'start');
|
|
1698
2362
|
break;
|
|
1699
2363
|
case 'End':
|
|
1700
2364
|
e.preventDefault();
|
|
1701
|
-
showSlide(total - 1, 'forward');
|
|
2365
|
+
showSlide(total - 1, 'forward', 'end');
|
|
1702
2366
|
break;
|
|
1703
2367
|
case 'f':
|
|
1704
2368
|
case 'F':
|
|
1705
2369
|
e.preventDefault();
|
|
1706
2370
|
toggleFullscreen();
|
|
1707
2371
|
break;
|
|
2372
|
+
case 't':
|
|
2373
|
+
case 'T':
|
|
2374
|
+
e.preventDefault();
|
|
2375
|
+
openThemes();
|
|
2376
|
+
break;
|
|
1708
2377
|
case 'l':
|
|
1709
2378
|
case 'L':
|
|
1710
2379
|
e.preventDefault();
|
|
@@ -1727,8 +2396,9 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1727
2396
|
break;
|
|
1728
2397
|
case 'Escape':
|
|
1729
2398
|
e.preventDefault();
|
|
1730
|
-
// Peel one layer at a time: canvas, pen, laser, then the overview.
|
|
1731
|
-
if (
|
|
2399
|
+
// Peel one layer at a time: themes, canvas, pen, laser, then the overview.
|
|
2400
|
+
if (themesOpen()) closeThemes(true);
|
|
2401
|
+
else if (blankOn) setBlank(false);
|
|
1732
2402
|
else if (penOn) setPen(false);
|
|
1733
2403
|
else if (laserOn) setLaser(false);
|
|
1734
2404
|
else toggleOverview();
|
|
@@ -1813,6 +2483,32 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1813
2483
|
}, { once: true });
|
|
1814
2484
|
}
|
|
1815
2485
|
|
|
2486
|
+
// ── PDF export / Print intercept ───────────────────────────────────────────
|
|
2487
|
+
function triggerPdfExport() {
|
|
2488
|
+
fetch('/__pdf', { method: 'POST', headers: { 'Content-Type': 'application/json' } })
|
|
2489
|
+
.then(function (r) {
|
|
2490
|
+
if (r.ok) return r.blob();
|
|
2491
|
+
throw new Error('Server PDF unavailable');
|
|
2492
|
+
})
|
|
2493
|
+
.then(function (blob) {
|
|
2494
|
+
var url = URL.createObjectURL(blob);
|
|
2495
|
+
var a = document.createElement('a');
|
|
2496
|
+
a.href = url;
|
|
2497
|
+
var title = (document.title || 'deck').replace(/[\s/\\?%*:|"<>]+/g, '-').toLowerCase();
|
|
2498
|
+
a.download = (title.endsWith('.pdf') ? title : title + '.pdf');
|
|
2499
|
+
document.body.appendChild(a);
|
|
2500
|
+
a.click();
|
|
2501
|
+
document.body.removeChild(a);
|
|
2502
|
+
setTimeout(function () { URL.revokeObjectURL(url); }, 5000);
|
|
2503
|
+
})
|
|
2504
|
+
.catch(function () {
|
|
2505
|
+
var fontsReady = document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve();
|
|
2506
|
+
Promise.all([richReady, fontsReady]).then(function () {
|
|
2507
|
+
setTimeout(function () { window.print(); }, 150);
|
|
2508
|
+
});
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
|
|
1816
2512
|
// ── Print export ─────────────────────────────────────────────────────
|
|
1817
2513
|
// Loading the deck with ?print=1 opens the print dialog once fonts and
|
|
1818
2514
|
// highlighting have settled. The editor's PDF export uses this.
|
|
@@ -1820,8 +2516,8 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1820
2516
|
try { wantsPrint = new URLSearchParams(location.search).has('print'); } catch (e) {}
|
|
1821
2517
|
if (wantsPrint) {
|
|
1822
2518
|
var openPrint = function () { setTimeout(function () { window.print(); }, 350); };
|
|
1823
|
-
|
|
1824
|
-
|
|
2519
|
+
var fontsReady = document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve();
|
|
2520
|
+
Promise.all([richReady, fontsReady]).then(openPrint, openPrint);
|
|
1825
2521
|
}
|
|
1826
2522
|
|
|
1827
2523
|
// ── Init ─────────────────────────────────────────────────────────────
|
|
@@ -1862,18 +2558,19 @@ export function generateDocHtml(docUrl, title, autoFullscreen = false, themeInpu
|
|
|
1862
2558
|
const theme = resolveThemeName(themeInput);
|
|
1863
2559
|
const pageTitle = title ? (title.toLowerCase().includes("deckrun") ? title : `${title} · deckrun`) : "deckrun";
|
|
1864
2560
|
return `<!DOCTYPE html>
|
|
1865
|
-
<html lang="en" data-theme="${theme}">
|
|
2561
|
+
<html lang="en" data-theme="${theme}" data-decor="${decorOf(theme)}">
|
|
1866
2562
|
<head>
|
|
1867
2563
|
<meta charset="UTF-8">
|
|
1868
2564
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1869
2565
|
<title>${escAttr(pageTitle)}</title>
|
|
1870
2566
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1871
2567
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
1872
|
-
<link href="${googleFontsHref(
|
|
2568
|
+
<link href="${googleFontsHref(THEME_IDS.slice())}" rel="stylesheet">
|
|
2569
|
+
<link rel="stylesheet" id="hljs-theme" href="${hljsHref(theme)}">
|
|
1873
2570
|
<style>
|
|
1874
2571
|
${RESET_CSS}
|
|
1875
2572
|
|
|
1876
|
-
${
|
|
2573
|
+
${themeSwitchableCss()}
|
|
1877
2574
|
|
|
1878
2575
|
${DOC_CSS}
|
|
1879
2576
|
|
|
@@ -1893,6 +2590,7 @@ ${PRESENTER_CSS}
|
|
|
1893
2590
|
<button class="hud-btn" id="btn-pen" title="Draw on the doc (D)">pen <kbd>D</kbd></button>
|
|
1894
2591
|
<button class="hud-btn" id="btn-blank" title="Blank canvas over the doc (C)">canvas <kbd>C</kbd></button>
|
|
1895
2592
|
<button class="hud-btn" id="btn-black" title="Black out the screen (B)">black <kbd>B</kbd></button>
|
|
2593
|
+
<button class="hud-btn" id="btn-theme" title="Change theme (T)">theme <kbd>T</kbd></button>
|
|
1896
2594
|
<div id="pen-bar">
|
|
1897
2595
|
<span id="hud-sep"></span>
|
|
1898
2596
|
<span id="pen-swatches"></span>
|
|
@@ -1904,6 +2602,9 @@ ${PRESENTER_CSS}
|
|
|
1904
2602
|
</div>
|
|
1905
2603
|
<button class="hud-btn" id="btn-help" title="Show every control (?)">? controls</button>
|
|
1906
2604
|
</div>
|
|
2605
|
+
<div id="hud-right">
|
|
2606
|
+
<a href="https://github.com/arpitbbhayani/deckrun" target="_blank" rel="noopener noreferrer" class="hud-brand" id="hud-brand" title="deckrun — Markdown presentations">powered by <span>deckrun</span></a>
|
|
2607
|
+
</div>
|
|
1907
2608
|
</div>
|
|
1908
2609
|
</div>
|
|
1909
2610
|
|
|
@@ -1911,6 +2612,23 @@ ${PRESENTER_CSS}
|
|
|
1911
2612
|
<div id="laser" aria-hidden="true"></div>
|
|
1912
2613
|
<div id="blackout" title="Click or press B to come back"></div>
|
|
1913
2614
|
|
|
2615
|
+
<div id="themes" role="dialog" aria-modal="true" aria-label="Theme picker">
|
|
2616
|
+
<div id="themes__backdrop" data-close="themes"></div>
|
|
2617
|
+
<div id="themes__box">
|
|
2618
|
+
<div id="themes__head">
|
|
2619
|
+
<span class="th-head__title">themes</span>
|
|
2620
|
+
<span class="th-head__sub">Arrow keys preview live · enter selects · esc closes</span>
|
|
2621
|
+
<a href="https://github.com/arpitbbhayani/deckrun" target="_blank" rel="noopener noreferrer" class="th-head__brand" title="deckrun">deckrun</a>
|
|
2622
|
+
<button id="themes__close" data-close="themes" title="Close (Esc)">×</button>
|
|
2623
|
+
</div>
|
|
2624
|
+
<div id="themes__list"></div>
|
|
2625
|
+
<div id="themes__foot">
|
|
2626
|
+
<span class="th-foot__brand">powered by <a href="https://github.com/arpitbbhayani/deckrun" target="_blank" rel="noopener noreferrer">deckrun</a></span>
|
|
2627
|
+
<span class="th-foot__hint">Switch themes on the fly</span>
|
|
2628
|
+
</div>
|
|
2629
|
+
</div>
|
|
2630
|
+
</div>
|
|
2631
|
+
|
|
1914
2632
|
<div id="help" role="dialog" aria-modal="true" aria-label="Presenter controls">
|
|
1915
2633
|
<div id="help__backdrop" data-close="help"></div>
|
|
1916
2634
|
<div id="help__panel">
|
|
@@ -1921,7 +2639,8 @@ ${PRESENTER_CSS}
|
|
|
1921
2639
|
</div>
|
|
1922
2640
|
<div id="help__grid"></div>
|
|
1923
2641
|
<div id="help__foot">
|
|
1924
|
-
Annotations are not saved to disk, and reset if the page reloads
|
|
2642
|
+
<span>Annotations are not saved to disk, and reset if the page reloads.</span>
|
|
2643
|
+
<span>powered by <a href="https://github.com/arpitbbhayani/deckrun" target="_blank" rel="noopener noreferrer">deckrun</a></span>
|
|
1925
2644
|
</div>
|
|
1926
2645
|
</div>
|
|
1927
2646
|
</div>
|
|
@@ -1930,6 +2649,8 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1930
2649
|
<div id="fs-hint__inner">Press any key or click to enter fullscreen</div>
|
|
1931
2650
|
</div>` : ''}
|
|
1932
2651
|
|
|
2652
|
+
<script id="deck-themes" type="application/json">${JSON.stringify({ themes: themeSummaries(), hljsMap: JSON.parse(hljsMapJson()), decorMap: JSON.parse(decorMapJson()) })}</script>
|
|
2653
|
+
|
|
1933
2654
|
<script>
|
|
1934
2655
|
(function () {
|
|
1935
2656
|
'use strict';
|
|
@@ -1938,6 +2659,7 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1938
2659
|
const elLaser = document.getElementById('laser');
|
|
1939
2660
|
const elBlack = document.getElementById('blackout');
|
|
1940
2661
|
const elHelp = document.getElementById('help');
|
|
2662
|
+
const elThemes = document.getElementById('themes');
|
|
1941
2663
|
const elPenBar = document.getElementById('pen-bar');
|
|
1942
2664
|
const elPenWidth = document.getElementById('pen-width');
|
|
1943
2665
|
const elFrame = document.getElementById('doc-frame');
|
|
@@ -1980,6 +2702,7 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1980
2702
|
pen: document.getElementById('btn-pen'),
|
|
1981
2703
|
blank: document.getElementById('btn-blank'),
|
|
1982
2704
|
black: document.getElementById('btn-black'),
|
|
2705
|
+
theme: document.getElementById('btn-theme'),
|
|
1983
2706
|
erase: document.getElementById('btn-erase'),
|
|
1984
2707
|
};
|
|
1985
2708
|
|
|
@@ -2164,6 +2887,7 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
2164
2887
|
tools.pen.classList.toggle('is-on', penOn);
|
|
2165
2888
|
tools.blank.classList.toggle('is-on', blankOn);
|
|
2166
2889
|
tools.black.classList.toggle('is-on', blackOn);
|
|
2890
|
+
if (tools.theme) tools.theme.classList.toggle('is-on', themesOpen());
|
|
2167
2891
|
tools.erase.classList.toggle('is-on', eraseOn);
|
|
2168
2892
|
|
|
2169
2893
|
elBoard.classList.toggle('is-drawing', penOn);
|
|
@@ -2182,6 +2906,208 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
2182
2906
|
elHelp.classList.toggle('is-on', helpOn);
|
|
2183
2907
|
}
|
|
2184
2908
|
|
|
2909
|
+
// ── Theme picker ─────────────────────────────────────────────────────
|
|
2910
|
+
let themeBootstrap = { themes: [], hljsMap: {}, decorMap: {} };
|
|
2911
|
+
try {
|
|
2912
|
+
const elBt = document.getElementById('deck-themes');
|
|
2913
|
+
if (elBt) themeBootstrap = JSON.parse(elBt.textContent || '{}');
|
|
2914
|
+
} catch (e) {}
|
|
2915
|
+
const themeList = themeBootstrap.themes || [];
|
|
2916
|
+
const hljsMap = themeBootstrap.hljsMap || {};
|
|
2917
|
+
const decorMap = themeBootstrap.decorMap || {};
|
|
2918
|
+
const THEME_DATA = Object.fromEntries(themeList.map(function (t) { return [t.id, t]; }));
|
|
2919
|
+
let activeTheme = document.documentElement.dataset.theme || 'nord';
|
|
2920
|
+
let themeCommitted = activeTheme;
|
|
2921
|
+
let themeSel = 0;
|
|
2922
|
+
let themeCards = [];
|
|
2923
|
+
|
|
2924
|
+
function themeThumb(t) {
|
|
2925
|
+
const c = t.colors;
|
|
2926
|
+
const wrap = document.createElement('div');
|
|
2927
|
+
wrap.className = 'th-thumb';
|
|
2928
|
+
wrap.style.background = c.crust;
|
|
2929
|
+
wrap.style.color = c.text;
|
|
2930
|
+
|
|
2931
|
+
const glow = document.createElement('div');
|
|
2932
|
+
glow.className = 'th-thumb__glow';
|
|
2933
|
+
glow.style.background = 'radial-gradient(circle, ' + c.accent + ' 0%, transparent 70%)';
|
|
2934
|
+
glow.style.opacity = t.mood === 'dark' ? '0.22' : '0.12';
|
|
2935
|
+
wrap.appendChild(glow);
|
|
2936
|
+
|
|
2937
|
+
const title = document.createElement('div');
|
|
2938
|
+
title.className = 'th-thumb__title';
|
|
2939
|
+
title.textContent = t.label;
|
|
2940
|
+
title.style.fontFamily = t.fonts.display;
|
|
2941
|
+
title.style.color = c.accent;
|
|
2942
|
+
wrap.appendChild(title);
|
|
2943
|
+
|
|
2944
|
+
const rule = document.createElement('div');
|
|
2945
|
+
rule.className = 'th-thumb__rule';
|
|
2946
|
+
rule.style.background = c.accent2;
|
|
2947
|
+
wrap.appendChild(rule);
|
|
2948
|
+
|
|
2949
|
+
[75, 55].forEach(function (w) {
|
|
2950
|
+
const line = document.createElement('div');
|
|
2951
|
+
line.className = 'th-thumb__line';
|
|
2952
|
+
line.style.width = w + '%';
|
|
2953
|
+
line.style.background = c.surface0;
|
|
2954
|
+
wrap.appendChild(line);
|
|
2955
|
+
});
|
|
2956
|
+
|
|
2957
|
+
const code = document.createElement('div');
|
|
2958
|
+
code.className = 'th-thumb__code';
|
|
2959
|
+
code.textContent = 'const deck = md';
|
|
2960
|
+
code.style.fontFamily = t.fonts.mono;
|
|
2961
|
+
code.style.background = c.mantle;
|
|
2962
|
+
code.style.color = c.accent3;
|
|
2963
|
+
code.style.border = '1px solid ' + c.surface0;
|
|
2964
|
+
wrap.appendChild(code);
|
|
2965
|
+
|
|
2966
|
+
return wrap;
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
function themeCard(t) {
|
|
2970
|
+
const card = document.createElement('button');
|
|
2971
|
+
card.className = 'th-card' + (t.id === activeTheme ? ' is-current' : '');
|
|
2972
|
+
card.dataset.theme = t.id;
|
|
2973
|
+
card.appendChild(themeThumb(t));
|
|
2974
|
+
|
|
2975
|
+
const meta = document.createElement('div');
|
|
2976
|
+
meta.className = 'th-meta';
|
|
2977
|
+
|
|
2978
|
+
const top = document.createElement('div');
|
|
2979
|
+
top.className = 'th-meta__top';
|
|
2980
|
+
const name = document.createElement('span');
|
|
2981
|
+
name.className = 'th-meta__name';
|
|
2982
|
+
name.textContent = t.label;
|
|
2983
|
+
const mood = document.createElement('span');
|
|
2984
|
+
mood.className = 'th-meta__mood';
|
|
2985
|
+
mood.textContent = t.mood;
|
|
2986
|
+
top.appendChild(name);
|
|
2987
|
+
top.appendChild(mood);
|
|
2988
|
+
|
|
2989
|
+
const blurb = document.createElement('div');
|
|
2990
|
+
blurb.className = 'th-meta__blurb';
|
|
2991
|
+
blurb.textContent = t.blurb;
|
|
2992
|
+
|
|
2993
|
+
meta.appendChild(top);
|
|
2994
|
+
meta.appendChild(blurb);
|
|
2995
|
+
card.appendChild(meta);
|
|
2996
|
+
return card;
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
function buildThemePicker() {
|
|
3000
|
+
const host = document.getElementById('themes__list');
|
|
3001
|
+
if (!host) return;
|
|
3002
|
+
host.innerHTML = '';
|
|
3003
|
+
themeCards = [];
|
|
3004
|
+
|
|
3005
|
+
[['dark', 'dark palettes'], ['light', 'light palettes']].forEach(function (pair) {
|
|
3006
|
+
const list = themeList.filter(function (t) { return t.mood === pair[0]; });
|
|
3007
|
+
if (!list.length) return;
|
|
3008
|
+
|
|
3009
|
+
const label = document.createElement('div');
|
|
3010
|
+
label.className = 'th-group';
|
|
3011
|
+
label.textContent = pair[1];
|
|
3012
|
+
host.appendChild(label);
|
|
3013
|
+
|
|
3014
|
+
const grid = document.createElement('div');
|
|
3015
|
+
grid.className = 'th-grid';
|
|
3016
|
+
list.forEach(function (t) {
|
|
3017
|
+
const card = themeCard(t);
|
|
3018
|
+
card.addEventListener('mouseenter', function () { selectTheme(themeCards.indexOf(card)); });
|
|
3019
|
+
card.addEventListener('click', function () { selectTheme(themeCards.indexOf(card)); commitTheme(); });
|
|
3020
|
+
grid.appendChild(card);
|
|
3021
|
+
themeCards.push(card);
|
|
3022
|
+
});
|
|
3023
|
+
host.appendChild(grid);
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
function updateThemePenColors() {
|
|
3028
|
+
const rs = getComputedStyle(document.documentElement);
|
|
3029
|
+
function tc(name, fb) {
|
|
3030
|
+
const v = rs.getPropertyValue('--' + name).trim();
|
|
3031
|
+
return v || fb;
|
|
3032
|
+
}
|
|
3033
|
+
PEN_COLORS[0] = tc('red', '#f38ba8');
|
|
3034
|
+
PEN_COLORS[1] = tc('yellow', '#f9e2af');
|
|
3035
|
+
PEN_COLORS[2] = tc('green', '#a6e3a1');
|
|
3036
|
+
PEN_COLORS[3] = tc('blue', '#89b4fa');
|
|
3037
|
+
PEN_COLORS[4] = tc('text', '#cdd6f4');
|
|
3038
|
+
swatches.forEach(function (sw, i) {
|
|
3039
|
+
if (sw && PEN_COLORS[i]) sw.style.background = PEN_COLORS[i];
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
function setTheme(id, remember) {
|
|
3044
|
+
if (!id || !THEME_DATA[id]) return;
|
|
3045
|
+
activeTheme = id;
|
|
3046
|
+
document.documentElement.dataset.theme = id;
|
|
3047
|
+
document.documentElement.dataset.decor = decorMap[id] || 'orbs';
|
|
3048
|
+
const hljsLink = document.getElementById('hljs-theme');
|
|
3049
|
+
if (hljsLink && hljsMap[id]) {
|
|
3050
|
+
hljsLink.href = hljsMap[id];
|
|
3051
|
+
}
|
|
3052
|
+
try {
|
|
3053
|
+
if (elFrame && elFrame.contentDocument && elFrame.contentDocument.documentElement) {
|
|
3054
|
+
elFrame.contentDocument.documentElement.dataset.theme = id;
|
|
3055
|
+
}
|
|
3056
|
+
} catch (e) {}
|
|
3057
|
+
try {
|
|
3058
|
+
if (elFrame && elFrame.contentWindow) {
|
|
3059
|
+
elFrame.contentWindow.postMessage({ type: 'theme', theme: id }, '*');
|
|
3060
|
+
}
|
|
3061
|
+
} catch (e) {}
|
|
3062
|
+
updateThemePenColors();
|
|
3063
|
+
if (remember !== false) {
|
|
3064
|
+
try { localStorage.setItem('deckrun.theme.v1', id); } catch (e) {}
|
|
3065
|
+
}
|
|
3066
|
+
if (themeCards.length) {
|
|
3067
|
+
themeCards.forEach(function (card) {
|
|
3068
|
+
card.classList.toggle('is-current', card.dataset.theme === id);
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
function selectTheme(i) {
|
|
3074
|
+
if (i < 0 || i >= themeCards.length) return;
|
|
3075
|
+
themeSel = i;
|
|
3076
|
+
themeCards.forEach(function (c, n) { c.classList.toggle('is-sel', n === i); });
|
|
3077
|
+
setTheme(themeCards[i].dataset.theme, false);
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
function openThemes() {
|
|
3081
|
+
if (helpOn) setHelp(false);
|
|
3082
|
+
themeCommitted = activeTheme;
|
|
3083
|
+
buildThemePicker();
|
|
3084
|
+
let at = 0;
|
|
3085
|
+
themeCards.forEach(function (c, n) { if (c.dataset.theme === activeTheme) at = n; });
|
|
3086
|
+
elThemes.classList.add('is-on');
|
|
3087
|
+
syncTools();
|
|
3088
|
+
selectTheme(at);
|
|
3089
|
+
if (themeCards[themeSel]) themeCards[themeSel].scrollIntoView({ block: 'nearest' });
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
function commitTheme() {
|
|
3093
|
+
themeCommitted = activeTheme;
|
|
3094
|
+
setTheme(activeTheme, true);
|
|
3095
|
+
closeThemes(false);
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
function closeThemes(restore) {
|
|
3099
|
+
if (restore && themeCommitted && themeCommitted !== activeTheme) {
|
|
3100
|
+
setTheme(themeCommitted, true);
|
|
3101
|
+
}
|
|
3102
|
+
elThemes.classList.remove('is-on');
|
|
3103
|
+
syncTools();
|
|
3104
|
+
themeCards.forEach(function (c) { c.classList.remove('is-sel'); });
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
function themesOpen() {
|
|
3108
|
+
return elThemes ? elThemes.classList.contains('is-on') : false;
|
|
3109
|
+
}
|
|
3110
|
+
|
|
2185
3111
|
// ── Laser pointer ────────────────────────────────────────────────────
|
|
2186
3112
|
// Same-origin doc, no border on the iframe: client coordinates line up
|
|
2187
3113
|
// with the outer viewport, so no translation is needed either way.
|
|
@@ -2195,6 +3121,7 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
2195
3121
|
const HELP_GROUPS = [
|
|
2196
3122
|
{ title: 'screen', rows: [
|
|
2197
3123
|
{ keys: ['F'], desc: 'Fullscreen' },
|
|
3124
|
+
{ keys: ['T'], desc: 'Theme picker' },
|
|
2198
3125
|
{ keys: ['B'], desc: 'Black out the screen' },
|
|
2199
3126
|
{ keys: ['Esc'], desc: 'Close what is open' },
|
|
2200
3127
|
{ keys: ['?'], desc: 'These controls' },
|
|
@@ -2265,12 +3192,17 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
2265
3192
|
onClick(tools.blank, function () { setBlank(!blankOn); });
|
|
2266
3193
|
onClick(tools.black, function () { setBlack(true); });
|
|
2267
3194
|
onClick(tools.erase, function () { setEraser(!eraseOn); });
|
|
3195
|
+
onClick(tools.theme, function () { themesOpen() ? closeThemes(false) : openThemes(); });
|
|
2268
3196
|
onClick(document.getElementById('btn-thin'), function () { nudgeWidth(-1); });
|
|
2269
3197
|
onClick(document.getElementById('btn-thick'), function () { nudgeWidth(1); });
|
|
2270
3198
|
onClick(document.getElementById('btn-clear'), function () { clearBoard(); });
|
|
2271
3199
|
onClick(document.getElementById('btn-help'), function () { setHelp(!helpOn); });
|
|
2272
3200
|
onClick(elBlack, function () { setBlack(false); });
|
|
2273
3201
|
|
|
3202
|
+
Array.prototype.forEach.call(elThemes.querySelectorAll('[data-close="themes"]'), function (el) {
|
|
3203
|
+
onClick(el, function () { closeThemes(false); });
|
|
3204
|
+
});
|
|
3205
|
+
|
|
2274
3206
|
Array.prototype.forEach.call(elHelp.querySelectorAll('[data-close="help"]'), function (el) {
|
|
2275
3207
|
onClick(el, function () { setHelp(false); });
|
|
2276
3208
|
});
|
|
@@ -2281,16 +3213,73 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
2281
3213
|
function onKeydown(e) {
|
|
2282
3214
|
const k = e.key;
|
|
2283
3215
|
|
|
2284
|
-
// Undo
|
|
3216
|
+
// Undo and PDF/Print modifier combos
|
|
2285
3217
|
if (e.metaKey || e.ctrlKey || e.altKey) {
|
|
2286
3218
|
if ((k === 'z' || k === 'Z') && hasInk()) {
|
|
2287
3219
|
e.preventDefault();
|
|
2288
3220
|
undoStroke();
|
|
3221
|
+
return;
|
|
3222
|
+
}
|
|
3223
|
+
if (k === 'p' || k === 'P') {
|
|
3224
|
+
e.preventDefault();
|
|
3225
|
+
fetch('/__pdf-doc', {
|
|
3226
|
+
method: 'POST',
|
|
3227
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3228
|
+
body: JSON.stringify({ title: document.title })
|
|
3229
|
+
})
|
|
3230
|
+
.then(function (r) { if (r.ok) return r.blob(); throw new Error('PDF unavailable'); })
|
|
3231
|
+
.then(function (blob) {
|
|
3232
|
+
var url = URL.createObjectURL(blob);
|
|
3233
|
+
var a = document.createElement('a');
|
|
3234
|
+
a.href = url;
|
|
3235
|
+
a.download = 'document.pdf';
|
|
3236
|
+
document.body.appendChild(a);
|
|
3237
|
+
a.click();
|
|
3238
|
+
document.body.removeChild(a);
|
|
3239
|
+
setTimeout(function () { URL.revokeObjectURL(url); }, 5000);
|
|
3240
|
+
})
|
|
3241
|
+
.catch(function () {
|
|
3242
|
+
window.print();
|
|
3243
|
+
});
|
|
3244
|
+
return;
|
|
2289
3245
|
}
|
|
2290
3246
|
return;
|
|
2291
3247
|
}
|
|
2292
3248
|
|
|
2293
3249
|
// Each overlay swallows keys until it is dismissed, outermost first.
|
|
3250
|
+
if (themesOpen()) {
|
|
3251
|
+
if (k === 'Escape') {
|
|
3252
|
+
e.preventDefault();
|
|
3253
|
+
closeThemes(true);
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
if (k === 'Enter') {
|
|
3257
|
+
e.preventDefault();
|
|
3258
|
+
commitTheme();
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
if (k === 'ArrowRight' || k === 'ArrowDown') {
|
|
3262
|
+
e.preventDefault();
|
|
3263
|
+
const nextIdx = (themeSel + 1) % themeCards.length;
|
|
3264
|
+
selectTheme(nextIdx);
|
|
3265
|
+
if (themeCards[nextIdx]) themeCards[nextIdx].scrollIntoView({ block: 'nearest' });
|
|
3266
|
+
return;
|
|
3267
|
+
}
|
|
3268
|
+
if (k === 'ArrowLeft' || k === 'ArrowUp') {
|
|
3269
|
+
e.preventDefault();
|
|
3270
|
+
const prevIdx = (themeSel - 1 + themeCards.length) % themeCards.length;
|
|
3271
|
+
selectTheme(prevIdx);
|
|
3272
|
+
if (themeCards[prevIdx]) themeCards[prevIdx].scrollIntoView({ block: 'nearest' });
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
if (k === 't' || k === 'T') {
|
|
3276
|
+
e.preventDefault();
|
|
3277
|
+
closeThemes(false);
|
|
3278
|
+
return;
|
|
3279
|
+
}
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
|
|
2294
3283
|
if (helpOn) {
|
|
2295
3284
|
if (k === 'Escape' || k === '?' || k === 'h' || k === 'H') {
|
|
2296
3285
|
e.preventDefault();
|
|
@@ -2332,6 +3321,11 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
2332
3321
|
e.preventDefault();
|
|
2333
3322
|
toggleFullscreen();
|
|
2334
3323
|
break;
|
|
3324
|
+
case 't':
|
|
3325
|
+
case 'T':
|
|
3326
|
+
e.preventDefault();
|
|
3327
|
+
openThemes();
|
|
3328
|
+
break;
|
|
2335
3329
|
case 'l':
|
|
2336
3330
|
case 'L':
|
|
2337
3331
|
e.preventDefault();
|
|
@@ -2349,8 +3343,9 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
2349
3343
|
break;
|
|
2350
3344
|
case 'Escape':
|
|
2351
3345
|
e.preventDefault();
|
|
2352
|
-
// Peel one layer at a time: canvas, pen, laser.
|
|
2353
|
-
if (
|
|
3346
|
+
// Peel one layer at a time: themes, canvas, pen, laser.
|
|
3347
|
+
if (themesOpen()) closeThemes(true);
|
|
3348
|
+
else if (blankOn) setBlank(false);
|
|
2354
3349
|
else if (penOn) setPen(false);
|
|
2355
3350
|
else if (laserOn) setLaser(false);
|
|
2356
3351
|
break;
|