deckrun 1.3.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +217 -35
- package/dist/editor-content.js +86 -1
- package/dist/editor.js +528 -216
- package/dist/fragments.js +71 -0
- package/dist/generate.js +1040 -45
- package/dist/index.js +353 -53
- 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 +125 -7
- package/dist/rich-content.js +170 -0
- package/dist/themes.js +8 -5
- 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%;
|
|
@@ -72,7 +83,8 @@ export const SLIDE_CSS = `html, body {
|
|
|
72
83
|
position: absolute;
|
|
73
84
|
inset: 0;
|
|
74
85
|
display: flex;
|
|
75
|
-
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
align-items: stretch;
|
|
76
88
|
justify-content: flex-start;
|
|
77
89
|
padding: var(--slide-pad-y) var(--slide-pad-x);
|
|
78
90
|
opacity: 0;
|
|
@@ -116,7 +128,7 @@ export const SLIDE_CSS = `html, body {
|
|
|
116
128
|
position: relative;
|
|
117
129
|
z-index: 1;
|
|
118
130
|
width: 100%;
|
|
119
|
-
max-width:
|
|
131
|
+
max-width: 100%;
|
|
120
132
|
max-height: calc(100vh - var(--slide-pad-y) * 2);
|
|
121
133
|
overflow: hidden;
|
|
122
134
|
}
|
|
@@ -151,7 +163,7 @@ export const SLIDE_CSS = `html, body {
|
|
|
151
163
|
z-index: 1;
|
|
152
164
|
display: flex;
|
|
153
165
|
width: 100%;
|
|
154
|
-
max-width:
|
|
166
|
+
max-width: 100%;
|
|
155
167
|
height: calc(100vh - var(--slide-pad-y) * 2);
|
|
156
168
|
align-items: center;
|
|
157
169
|
gap: 3.2rem;
|
|
@@ -243,7 +255,6 @@ export const SLIDE_CSS = `html, body {
|
|
|
243
255
|
line-height: calc(1.72 * var(--type-lead));
|
|
244
256
|
margin-bottom: 1rem;
|
|
245
257
|
color: var(--subtext1);
|
|
246
|
-
max-width: 62ch;
|
|
247
258
|
}
|
|
248
259
|
|
|
249
260
|
.slide__content strong {
|
|
@@ -266,7 +277,6 @@ export const SLIDE_CSS = `html, body {
|
|
|
266
277
|
line-height: calc(1.72 * var(--type-lead));
|
|
267
278
|
margin-bottom: 1rem;
|
|
268
279
|
color: var(--subtext1);
|
|
269
|
-
max-width: 64ch;
|
|
270
280
|
}
|
|
271
281
|
|
|
272
282
|
.slide__content ul { list-style: none; padding-left: 1.35em; }
|
|
@@ -379,7 +389,6 @@ export const SLIDE_CSS = `html, body {
|
|
|
379
389
|
border-radius: 0 10px 10px 0;
|
|
380
390
|
color: var(--subtext1);
|
|
381
391
|
font-size: calc(clamp(1rem, 1.5vw, 1.3rem) * var(--type-body));
|
|
382
|
-
max-width: 60ch;
|
|
383
392
|
}
|
|
384
393
|
|
|
385
394
|
.slide__content blockquote::before {
|
|
@@ -789,7 +798,7 @@ const CHROME_CSS = `/* ── HUD (progress + counter) ────────
|
|
|
789
798
|
.slide__content iframe, .slide__content video { max-height: 4in !important; }
|
|
790
799
|
|
|
791
800
|
#hud, .nav-arrow, #overview, #kbd-hint, #cursor, #fs-hint, .pet,
|
|
792
|
-
#board, #laser, #blackout, #help {
|
|
801
|
+
#board, #laser, #blackout, #help, #themes {
|
|
793
802
|
display: none !important;
|
|
794
803
|
}
|
|
795
804
|
}`;
|
|
@@ -892,6 +901,38 @@ const PRESENTER_CSS = `/* ── HUD tool strip ──────────
|
|
|
892
901
|
text-align: center;
|
|
893
902
|
}
|
|
894
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
|
+
|
|
895
936
|
/* ── Annotation canvas ────────────────────────────────────────────────── */
|
|
896
937
|
#board {
|
|
897
938
|
position: fixed;
|
|
@@ -947,6 +988,215 @@ body.laser-on, body.laser-on #board.is-drawing { cursor: none; }
|
|
|
947
988
|
|
|
948
989
|
#blackout.is-on { display: block; }
|
|
949
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
|
+
|
|
950
1200
|
/* ── Controls overlay ─────────────────────────────────────────────────── */
|
|
951
1201
|
#help {
|
|
952
1202
|
position: fixed;
|
|
@@ -1057,30 +1307,43 @@ body.laser-on, body.laser-on #board.is-drawing { cursor: none; }
|
|
|
1057
1307
|
font-size: 0.66rem;
|
|
1058
1308
|
color: var(--overlay1);
|
|
1059
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;
|
|
1060
1315
|
}`;
|
|
1061
|
-
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 = {}) {
|
|
1062
1317
|
const theme = resolveThemeName(themeInput);
|
|
1063
1318
|
const size = resolveSizeName(sizeInput);
|
|
1319
|
+
const template = resolveTemplateName(presentation.template ?? DEFAULT_TEMPLATE);
|
|
1320
|
+
const transition = resolveTransitionName(presentation.transition ?? DEFAULT_TRANSITION);
|
|
1064
1321
|
const head = findFont(fonts.head);
|
|
1065
1322
|
const body = findFont(fonts.body);
|
|
1066
1323
|
const fontAttrs = (head ? ` data-head="${head}"` : "") + (body ? ` data-body="${body}"` : "");
|
|
1067
1324
|
const slideHtml = slides.map((s, i) => renderSlide(s, i)).join("\n");
|
|
1068
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");
|
|
1330
|
+
const pageTitle = title ? (title.toLowerCase().includes("deckrun") ? title : `${title} · deckrun`) : "deckrun";
|
|
1069
1331
|
return `<!DOCTYPE html>
|
|
1070
|
-
<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}>
|
|
1071
1333
|
<head>
|
|
1072
1334
|
<meta charset="UTF-8">
|
|
1073
1335
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1074
|
-
<title>${escAttr(
|
|
1336
|
+
<title>${escAttr(pageTitle)}</title>
|
|
1075
1337
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1076
1338
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
1077
|
-
<link href="${googleFontsHref(
|
|
1078
|
-
<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)}">
|
|
1079
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")}
|
|
1080
1343
|
<style>
|
|
1081
1344
|
${RESET_CSS}
|
|
1082
1345
|
|
|
1083
|
-
${
|
|
1346
|
+
${themeSwitchableCss()}
|
|
1084
1347
|
|
|
1085
1348
|
${sizeRootCss(size)}
|
|
1086
1349
|
|
|
@@ -1088,6 +1351,14 @@ ${fontOverrideCss()}
|
|
|
1088
1351
|
|
|
1089
1352
|
${SLIDE_CSS}
|
|
1090
1353
|
|
|
1354
|
+
${TEMPLATE_CSS}
|
|
1355
|
+
|
|
1356
|
+
${TRANSITION_CSS}
|
|
1357
|
+
|
|
1358
|
+
${FRAGMENT_CSS}
|
|
1359
|
+
|
|
1360
|
+
${RICH_CONTENT_CSS}
|
|
1361
|
+
|
|
1091
1362
|
${DECOR_CSS}
|
|
1092
1363
|
|
|
1093
1364
|
${CHROME_CSS}
|
|
@@ -1111,6 +1382,7 @@ ${slideHtml}
|
|
|
1111
1382
|
<button class="hud-btn" id="btn-pen" title="Draw on the slide (D)">pen <kbd>D</kbd></button>
|
|
1112
1383
|
<button class="hud-btn" id="btn-blank" title="Blank canvas over the slide (C)">canvas <kbd>C</kbd></button>
|
|
1113
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>
|
|
1114
1386
|
<div id="pen-bar">
|
|
1115
1387
|
<span id="hud-sep"></span>
|
|
1116
1388
|
<span id="pen-swatches"></span>
|
|
@@ -1122,7 +1394,10 @@ ${slideHtml}
|
|
|
1122
1394
|
</div>
|
|
1123
1395
|
<button class="hud-btn" id="btn-help" title="Show every control (?)">? controls</button>
|
|
1124
1396
|
</div>
|
|
1125
|
-
<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>
|
|
1126
1401
|
</div>
|
|
1127
1402
|
</div>
|
|
1128
1403
|
|
|
@@ -1130,6 +1405,23 @@ ${slideHtml}
|
|
|
1130
1405
|
<div id="laser" aria-hidden="true"></div>
|
|
1131
1406
|
<div id="blackout" title="Click or press B to come back"></div>
|
|
1132
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
|
+
|
|
1133
1425
|
<div id="help" role="dialog" aria-modal="true" aria-label="Presenter controls">
|
|
1134
1426
|
<div id="help__backdrop" data-close="help"></div>
|
|
1135
1427
|
<div id="help__panel">
|
|
@@ -1140,8 +1432,8 @@ ${slideHtml}
|
|
|
1140
1432
|
</div>
|
|
1141
1433
|
<div id="help__grid"></div>
|
|
1142
1434
|
<div id="help__foot">
|
|
1143
|
-
Annotations live per slide and survive navigation
|
|
1144
|
-
|
|
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>
|
|
1145
1437
|
</div>
|
|
1146
1438
|
</div>
|
|
1147
1439
|
</div>
|
|
@@ -1153,12 +1445,20 @@ ${slideHtml}
|
|
|
1153
1445
|
|
|
1154
1446
|
<div id="overview" class="hidden"></div>
|
|
1155
1447
|
|
|
1156
|
-
<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>
|
|
1157
1449
|
|
|
1158
1450
|
${autoFullscreen ? `<div id="fs-hint">
|
|
1159
1451
|
<div id="fs-hint__inner">Press any key or click to enter fullscreen</div>
|
|
1160
1452
|
</div>` : ''}
|
|
1161
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>
|
|
1162
1462
|
<script>
|
|
1163
1463
|
(function () {
|
|
1164
1464
|
'use strict';
|
|
@@ -1178,18 +1478,78 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1178
1478
|
const elLaser = document.getElementById('laser');
|
|
1179
1479
|
const elBlack = document.getElementById('blackout');
|
|
1180
1480
|
const elHelp = document.getElementById('help');
|
|
1481
|
+
const elThemes = document.getElementById('themes');
|
|
1181
1482
|
const elPenBar = document.getElementById('pen-bar');
|
|
1182
1483
|
const elPenWidth = document.getElementById('pen-width');
|
|
1183
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
|
+
|
|
1184
1523
|
// ── Syntax highlighting ──────────────────────────────────────────────
|
|
1185
1524
|
// Guarded: the highlighter comes off a CDN, and a deck presented offline
|
|
1186
1525
|
// should still navigate rather than die on a missing global.
|
|
1187
|
-
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();
|
|
1188
1535
|
|
|
1189
1536
|
// ── Slide navigation ─────────────────────────────────────────────────
|
|
1190
|
-
function showSlide(next, direction) {
|
|
1537
|
+
function showSlide(next, direction, fragmentMode) {
|
|
1191
1538
|
const prev = cur;
|
|
1192
|
-
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
|
+
}
|
|
1193
1553
|
|
|
1194
1554
|
const slideOut = slides[prev];
|
|
1195
1555
|
const slideIn = slides[next];
|
|
@@ -1212,10 +1572,18 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1212
1572
|
slideOut.classList.add(exitClass);
|
|
1213
1573
|
|
|
1214
1574
|
// Clean up exit class after transition
|
|
1215
|
-
|
|
1575
|
+
var cleaned = false;
|
|
1576
|
+
function cleanup() {
|
|
1577
|
+
if (cleaned) return;
|
|
1578
|
+
cleaned = true;
|
|
1216
1579
|
slideOut.classList.remove(exitClass, 'exit-left', 'exit-right');
|
|
1217
1580
|
slideOut.removeEventListener('transitionend', cleanup);
|
|
1218
|
-
}
|
|
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);
|
|
1219
1587
|
|
|
1220
1588
|
cur = next;
|
|
1221
1589
|
updateHud();
|
|
@@ -1226,12 +1594,62 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1226
1594
|
elCur.textContent = String(cur + 1);
|
|
1227
1595
|
const pct = total > 1 ? (cur / (total - 1)) * 100 : 100;
|
|
1228
1596
|
elFill.style.width = pct + '%';
|
|
1229
|
-
|
|
1230
|
-
|
|
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');
|
|
1231
1610
|
}
|
|
1232
1611
|
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
+
}
|
|
1235
1653
|
|
|
1236
1654
|
// ── Overview mode ────────────────────────────────────────────────────
|
|
1237
1655
|
function buildOverview() {
|
|
@@ -1252,6 +1670,10 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1252
1670
|
const clone = slide.cloneNode(true);
|
|
1253
1671
|
clone.classList.add('is-active');
|
|
1254
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
|
+
});
|
|
1255
1677
|
inner.appendChild(clone);
|
|
1256
1678
|
|
|
1257
1679
|
thumb.appendChild(num);
|
|
@@ -1317,6 +1739,7 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1317
1739
|
pen: document.getElementById('btn-pen'),
|
|
1318
1740
|
blank: document.getElementById('btn-blank'),
|
|
1319
1741
|
black: document.getElementById('btn-black'),
|
|
1742
|
+
theme: document.getElementById('btn-theme'),
|
|
1320
1743
|
erase: document.getElementById('btn-erase'),
|
|
1321
1744
|
};
|
|
1322
1745
|
|
|
@@ -1504,6 +1927,7 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1504
1927
|
tools.pen.classList.toggle('is-on', penOn);
|
|
1505
1928
|
tools.blank.classList.toggle('is-on', blankOn);
|
|
1506
1929
|
tools.black.classList.toggle('is-on', blackOn);
|
|
1930
|
+
if (tools.theme) tools.theme.classList.toggle('is-on', themesOpen());
|
|
1507
1931
|
tools.erase.classList.toggle('is-on', eraseOn);
|
|
1508
1932
|
|
|
1509
1933
|
elBoard.classList.toggle('is-drawing', penOn);
|
|
@@ -1522,6 +1946,200 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1522
1946
|
elHelp.classList.toggle('is-on', helpOn);
|
|
1523
1947
|
}
|
|
1524
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
|
+
|
|
1525
2143
|
// ── Laser pointer ────────────────────────────────────────────────────
|
|
1526
2144
|
document.addEventListener('pointermove', function (e) {
|
|
1527
2145
|
if (!laserOn) return;
|
|
@@ -1531,8 +2149,8 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1531
2149
|
// ── Controls overlay ─────────────────────────────────────────────────
|
|
1532
2150
|
const HELP_GROUPS = [
|
|
1533
2151
|
{ title: 'navigate', rows: [
|
|
1534
|
-
{ keys: ['→', '↓', 'Space'], desc: 'Next slide' },
|
|
1535
|
-
{ keys: ['←', '↑', 'Backspace'], desc: 'Previous slide' },
|
|
2152
|
+
{ keys: ['→', '↓', 'Space'], desc: 'Next reveal or slide' },
|
|
2153
|
+
{ keys: ['←', '↑', 'Backspace'], desc: 'Previous reveal or slide' },
|
|
1536
2154
|
{ keys: ['Home'], desc: 'First slide' },
|
|
1537
2155
|
{ keys: ['End'], desc: 'Last slide' },
|
|
1538
2156
|
{ keys: ['O'], desc: 'Overview grid' },
|
|
@@ -1540,6 +2158,7 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1540
2158
|
]},
|
|
1541
2159
|
{ title: 'screen', rows: [
|
|
1542
2160
|
{ keys: ['F'], desc: 'Fullscreen' },
|
|
2161
|
+
{ keys: ['T'], desc: 'Theme picker' },
|
|
1543
2162
|
{ keys: ['B'], desc: 'Black out the screen' },
|
|
1544
2163
|
{ keys: ['?'], desc: 'These controls' },
|
|
1545
2164
|
]},
|
|
@@ -1609,12 +2228,17 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1609
2228
|
onClick(tools.blank, function () { setBlank(!blankOn); });
|
|
1610
2229
|
onClick(tools.black, function () { setBlack(true); });
|
|
1611
2230
|
onClick(tools.erase, function () { setEraser(!eraseOn); });
|
|
2231
|
+
onClick(tools.theme, function () { themesOpen() ? closeThemes(false) : openThemes(); });
|
|
1612
2232
|
onClick(document.getElementById('btn-thin'), function () { nudgeWidth(-1); });
|
|
1613
2233
|
onClick(document.getElementById('btn-thick'), function () { nudgeWidth(1); });
|
|
1614
2234
|
onClick(document.getElementById('btn-clear'), function () { clearSlide(); });
|
|
1615
2235
|
onClick(document.getElementById('btn-help'), function () { setHelp(!helpOn); });
|
|
1616
2236
|
onClick(elBlack, function () { setBlack(false); });
|
|
1617
2237
|
|
|
2238
|
+
Array.prototype.forEach.call(elThemes.querySelectorAll('[data-close="themes"]'), function (el) {
|
|
2239
|
+
onClick(el, function () { closeThemes(false); });
|
|
2240
|
+
});
|
|
2241
|
+
|
|
1618
2242
|
Array.prototype.forEach.call(elHelp.querySelectorAll('[data-close="help"]'), function (el) {
|
|
1619
2243
|
onClick(el, function () { setHelp(false); });
|
|
1620
2244
|
});
|
|
@@ -1625,16 +2249,55 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1625
2249
|
document.addEventListener('keydown', function (e) {
|
|
1626
2250
|
const k = e.key;
|
|
1627
2251
|
|
|
1628
|
-
// Undo
|
|
2252
|
+
// Undo and PDF/Print modifier combos
|
|
1629
2253
|
if (e.metaKey || e.ctrlKey || e.altKey) {
|
|
1630
2254
|
if ((k === 'z' || k === 'Z') && hasInk()) {
|
|
1631
2255
|
e.preventDefault();
|
|
1632
2256
|
undoStroke();
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
if (k === 'p' || k === 'P') {
|
|
2260
|
+
e.preventDefault();
|
|
2261
|
+
triggerPdfExport();
|
|
2262
|
+
return;
|
|
1633
2263
|
}
|
|
1634
2264
|
return;
|
|
1635
2265
|
}
|
|
1636
2266
|
|
|
1637
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
|
+
|
|
1638
2301
|
if (helpOn) {
|
|
1639
2302
|
if (k === 'Escape' || k === '?' || k === 'h' || k === 'H') {
|
|
1640
2303
|
e.preventDefault();
|
|
@@ -1695,17 +2358,22 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1695
2358
|
break;
|
|
1696
2359
|
case 'Home':
|
|
1697
2360
|
e.preventDefault();
|
|
1698
|
-
showSlide(0, 'backward');
|
|
2361
|
+
showSlide(0, 'backward', 'start');
|
|
1699
2362
|
break;
|
|
1700
2363
|
case 'End':
|
|
1701
2364
|
e.preventDefault();
|
|
1702
|
-
showSlide(total - 1, 'forward');
|
|
2365
|
+
showSlide(total - 1, 'forward', 'end');
|
|
1703
2366
|
break;
|
|
1704
2367
|
case 'f':
|
|
1705
2368
|
case 'F':
|
|
1706
2369
|
e.preventDefault();
|
|
1707
2370
|
toggleFullscreen();
|
|
1708
2371
|
break;
|
|
2372
|
+
case 't':
|
|
2373
|
+
case 'T':
|
|
2374
|
+
e.preventDefault();
|
|
2375
|
+
openThemes();
|
|
2376
|
+
break;
|
|
1709
2377
|
case 'l':
|
|
1710
2378
|
case 'L':
|
|
1711
2379
|
e.preventDefault();
|
|
@@ -1728,8 +2396,9 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1728
2396
|
break;
|
|
1729
2397
|
case 'Escape':
|
|
1730
2398
|
e.preventDefault();
|
|
1731
|
-
// Peel one layer at a time: canvas, pen, laser, then the overview.
|
|
1732
|
-
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);
|
|
1733
2402
|
else if (penOn) setPen(false);
|
|
1734
2403
|
else if (laserOn) setLaser(false);
|
|
1735
2404
|
else toggleOverview();
|
|
@@ -1814,6 +2483,32 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1814
2483
|
}, { once: true });
|
|
1815
2484
|
}
|
|
1816
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
|
+
|
|
1817
2512
|
// ── Print export ─────────────────────────────────────────────────────
|
|
1818
2513
|
// Loading the deck with ?print=1 opens the print dialog once fonts and
|
|
1819
2514
|
// highlighting have settled. The editor's PDF export uses this.
|
|
@@ -1821,8 +2516,8 @@ ${autoFullscreen ? `<div id="fs-hint">
|
|
|
1821
2516
|
try { wantsPrint = new URLSearchParams(location.search).has('print'); } catch (e) {}
|
|
1822
2517
|
if (wantsPrint) {
|
|
1823
2518
|
var openPrint = function () { setTimeout(function () { window.print(); }, 350); };
|
|
1824
|
-
|
|
1825
|
-
|
|
2519
|
+
var fontsReady = document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve();
|
|
2520
|
+
Promise.all([richReady, fontsReady]).then(openPrint, openPrint);
|
|
1826
2521
|
}
|
|
1827
2522
|
|
|
1828
2523
|
// ── Init ─────────────────────────────────────────────────────────────
|
|
@@ -1861,19 +2556,21 @@ const DOC_CSS = `html, body {
|
|
|
1861
2556
|
*/
|
|
1862
2557
|
export function generateDocHtml(docUrl, title, autoFullscreen = false, themeInput = DEFAULT_THEME) {
|
|
1863
2558
|
const theme = resolveThemeName(themeInput);
|
|
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
|
-
<title>${escAttr(
|
|
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;
|