@atom63/slides 0.2.0 → 0.4.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 +34 -0
- package/dist/{chunk-AD3ZOVWR.js → chunk-FNPEZIX4.js} +3332 -3322
- package/dist/chunk-FNPEZIX4.js.map +1 -0
- package/dist/editor/index.js +623 -47
- package/dist/editor/index.js.map +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +1 -1
- package/package.json +11 -5
- package/src/editor/styles.css +259 -0
- package/src/styles/themes.css +245 -0
- package/dist/chunk-AD3ZOVWR.js.map +0 -1
package/dist/editor/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { listTemplates, slideMdxComponents, SlidesPlayer } from '../chunk-
|
|
1
|
+
import { listTemplates, slideMdxComponents, resolveTheme, SlidesPlayer, templateNames, BUILTIN_THEMES, getTemplate } from '../chunk-FNPEZIX4.js';
|
|
2
2
|
import { evaluate } from '@mdx-js/mdx';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
import * as runtime from 'react/jsx-runtime';
|
|
5
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
|
+
import { jsx as jsx$1, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
7
|
-
import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
|
7
|
+
import { useState, useEffect, useMemo, useRef, useCallback, useId } from 'react';
|
|
8
|
+
import { Parser } from 'acorn';
|
|
9
|
+
import jsx from 'acorn-jsx';
|
|
8
10
|
|
|
9
11
|
function parseFrontmatter(source) {
|
|
10
12
|
const normalized = source.replace(/^/, "");
|
|
@@ -52,6 +54,384 @@ async function compileDeck(source) {
|
|
|
52
54
|
return { ok: false, error };
|
|
53
55
|
}
|
|
54
56
|
}
|
|
57
|
+
var JsxParser = Parser.extend(jsx());
|
|
58
|
+
var IMPORT_RE2 = /^[ \t]*import\b[\s\S]*?(?:from\s*['"][^'"]*['"]|['"][^'"]*['"])[ \t]*;?[ \t]*$/gm;
|
|
59
|
+
function blankImports(block) {
|
|
60
|
+
return block.replace(IMPORT_RE2, (m) => m.replace(/[^\n]/g, " "));
|
|
61
|
+
}
|
|
62
|
+
function findTemplateElement(block) {
|
|
63
|
+
let ast;
|
|
64
|
+
try {
|
|
65
|
+
ast = JsxParser.parse(blankImports(block), { ecmaVersion: "latest", sourceType: "module" });
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const body = ast.body;
|
|
70
|
+
let jsxElement = null;
|
|
71
|
+
for (const node of body) {
|
|
72
|
+
if (node.type === "ImportDeclaration") {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (node.type === "ExpressionStatement" && node.expression?.type === "JSXElement") {
|
|
76
|
+
if (jsxElement !== null) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
jsxElement = node.expression;
|
|
80
|
+
} else {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return jsxElement;
|
|
85
|
+
}
|
|
86
|
+
function parseSlide(block) {
|
|
87
|
+
const element = findTemplateElement(block);
|
|
88
|
+
if (element === null) {
|
|
89
|
+
return { kind: "opaque", reason: "block contains no single JSX template element" };
|
|
90
|
+
}
|
|
91
|
+
const openingEl = element.openingElement;
|
|
92
|
+
const nameNode = openingEl.name;
|
|
93
|
+
if (nameNode.type !== "JSXIdentifier") {
|
|
94
|
+
return { kind: "opaque", reason: "JSX element name is not a plain identifier" };
|
|
95
|
+
}
|
|
96
|
+
const componentName = nameNode.name;
|
|
97
|
+
if (!templateNames.includes(componentName)) {
|
|
98
|
+
return { kind: "opaque", reason: `"${componentName}" is not a registered template` };
|
|
99
|
+
}
|
|
100
|
+
const children = element.children ?? [];
|
|
101
|
+
for (const child of children) {
|
|
102
|
+
if (child.type === "JSXText") {
|
|
103
|
+
if (child.value.trim() !== "") {
|
|
104
|
+
return { kind: "opaque", reason: "element has non-whitespace children" };
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
return { kind: "opaque", reason: "element has non-whitespace children" };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const props = {};
|
|
111
|
+
const attributes = openingEl.attributes ?? [];
|
|
112
|
+
for (const attr of attributes) {
|
|
113
|
+
if (attr.type === "JSXSpreadAttribute") {
|
|
114
|
+
return { kind: "opaque", reason: "element has spread attribute" };
|
|
115
|
+
}
|
|
116
|
+
if (attr.type !== "JSXAttribute") {
|
|
117
|
+
return { kind: "opaque", reason: "unexpected attribute node type" };
|
|
118
|
+
}
|
|
119
|
+
const attrName = attr.name.name;
|
|
120
|
+
const attrValue = attr.value;
|
|
121
|
+
if (attrValue === null) {
|
|
122
|
+
return { kind: "opaque", reason: `attribute "${attrName}" has no string value (boolean shorthand)` };
|
|
123
|
+
}
|
|
124
|
+
if (attrValue.type !== "Literal" || typeof attrValue.value !== "string") {
|
|
125
|
+
return {
|
|
126
|
+
kind: "opaque",
|
|
127
|
+
reason: `attribute "${attrName}" is not a string literal`
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
props[attrName] = attrValue.value;
|
|
131
|
+
}
|
|
132
|
+
return { kind: "template", name: componentName, props };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/editor/serialize-slot.ts
|
|
136
|
+
function renderJsxAttr(name, value) {
|
|
137
|
+
const encoded = value.replaceAll("&", "&").replaceAll('"', """);
|
|
138
|
+
return `${name}="${encoded}"`;
|
|
139
|
+
}
|
|
140
|
+
function renderValue(value) {
|
|
141
|
+
const encoded = value.replaceAll("&", "&").replaceAll('"', """);
|
|
142
|
+
return `"${encoded}"`;
|
|
143
|
+
}
|
|
144
|
+
function setProp(block, name, value) {
|
|
145
|
+
const element = findTemplateElement(block);
|
|
146
|
+
if (element === null) {
|
|
147
|
+
return block;
|
|
148
|
+
}
|
|
149
|
+
const openingEl = element.openingElement;
|
|
150
|
+
const attributes = openingEl.attributes ?? [];
|
|
151
|
+
const existing = attributes.find(
|
|
152
|
+
(attr) => attr.type === "JSXAttribute" && attr.name?.name === name
|
|
153
|
+
);
|
|
154
|
+
if (existing !== void 0) {
|
|
155
|
+
const attrValue = existing.value;
|
|
156
|
+
if (attrValue === null || attrValue.type !== "Literal") {
|
|
157
|
+
return block;
|
|
158
|
+
}
|
|
159
|
+
const existingRaw = block.slice(attrValue.start, attrValue.end);
|
|
160
|
+
const rendered2 = renderValue(value);
|
|
161
|
+
if (existingRaw === rendered2) {
|
|
162
|
+
return block;
|
|
163
|
+
}
|
|
164
|
+
return block.slice(0, attrValue.start) + rendered2 + block.slice(attrValue.end);
|
|
165
|
+
}
|
|
166
|
+
const tagEnd = openingEl.end;
|
|
167
|
+
const rendered = renderValue(value);
|
|
168
|
+
let insertAt;
|
|
169
|
+
if (block[tagEnd - 1] === ">" && block[tagEnd - 2] === "/") {
|
|
170
|
+
insertAt = tagEnd - 2;
|
|
171
|
+
} else if (block[tagEnd - 1] === ">") {
|
|
172
|
+
insertAt = tagEnd - 1;
|
|
173
|
+
} else {
|
|
174
|
+
insertAt = tagEnd;
|
|
175
|
+
}
|
|
176
|
+
const insertion = ` ${name}=${rendered}`;
|
|
177
|
+
return block.slice(0, insertAt) + insertion + block.slice(insertAt);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/editor/slide-blocks.ts
|
|
181
|
+
function isFenceMarker(trimmed) {
|
|
182
|
+
return trimmed.startsWith("```") || trimmed.startsWith("~~~");
|
|
183
|
+
}
|
|
184
|
+
function splitBlocks(source) {
|
|
185
|
+
const blocks = [];
|
|
186
|
+
let idx = 0;
|
|
187
|
+
const normalized = source.replace(/^/, "");
|
|
188
|
+
const allLines = normalized.split("\n");
|
|
189
|
+
let remainder = source;
|
|
190
|
+
let frontmatterConsumed = 0;
|
|
191
|
+
if (allLines[0]?.trim() === "---") {
|
|
192
|
+
let closeIdx = -1;
|
|
193
|
+
for (let i = 1; i < allLines.length; i++) {
|
|
194
|
+
if (allLines[i].trim() === "---") {
|
|
195
|
+
closeIdx = i;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (closeIdx !== -1) {
|
|
200
|
+
const fmLines = allLines.slice(0, closeIdx + 1);
|
|
201
|
+
const fmText = `${fmLines.join("\n")}
|
|
202
|
+
`;
|
|
203
|
+
if (source.startsWith(fmText) || normalized.startsWith(fmText)) {
|
|
204
|
+
frontmatterConsumed = fmText.length;
|
|
205
|
+
blocks.push({ index: idx++, kind: "frontmatter", text: fmText });
|
|
206
|
+
remainder = source.slice(frontmatterConsumed);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const remLines = remainder.split("\n");
|
|
211
|
+
const lineRaws = [];
|
|
212
|
+
for (let i = 0; i < remLines.length; i++) {
|
|
213
|
+
const isLast = i === remLines.length - 1;
|
|
214
|
+
if (isLast && remLines[i] === "") {
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
lineRaws.push(isLast ? remLines[i] : `${remLines[i]}
|
|
218
|
+
`);
|
|
219
|
+
}
|
|
220
|
+
let inFence = false;
|
|
221
|
+
let slideText = "";
|
|
222
|
+
const flushSlide = () => {
|
|
223
|
+
blocks.push({ index: idx++, kind: "slide", text: slideText });
|
|
224
|
+
slideText = "";
|
|
225
|
+
};
|
|
226
|
+
for (let i = 0; i < lineRaws.length; i++) {
|
|
227
|
+
const raw = lineRaws[i];
|
|
228
|
+
const trimmed = raw.trimEnd().replace(/\r$/, "");
|
|
229
|
+
if (!inFence && trimmed === "---") {
|
|
230
|
+
flushSlide();
|
|
231
|
+
blocks.push({ index: idx++, kind: "separator", text: raw });
|
|
232
|
+
} else {
|
|
233
|
+
if (isFenceMarker(trimmed)) {
|
|
234
|
+
inFence = !inFence;
|
|
235
|
+
}
|
|
236
|
+
slideText += raw;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (slideText !== "" || blocks.filter((b) => b.kind === "slide").length === 0) {
|
|
240
|
+
flushSlide();
|
|
241
|
+
}
|
|
242
|
+
return blocks;
|
|
243
|
+
}
|
|
244
|
+
function joinBlocks(blocks) {
|
|
245
|
+
return blocks.map((b) => b.text).join("");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/editor/slide-edit.ts
|
|
249
|
+
function slideBlockIndices(source) {
|
|
250
|
+
const blocks = splitBlocks(source);
|
|
251
|
+
const indices = [];
|
|
252
|
+
for (const block of blocks) {
|
|
253
|
+
if (block.kind === "slide" && block.text.trim() !== "") {
|
|
254
|
+
indices.push(block.index);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return indices;
|
|
258
|
+
}
|
|
259
|
+
function getSlideBlock(source, slideOrdinal) {
|
|
260
|
+
const indices = slideBlockIndices(source);
|
|
261
|
+
if (slideOrdinal < 0 || slideOrdinal >= indices.length) return null;
|
|
262
|
+
const blockIndex = indices[slideOrdinal];
|
|
263
|
+
const blocks = splitBlocks(source);
|
|
264
|
+
const block = blocks.find((b) => b.index === blockIndex);
|
|
265
|
+
if (!block) return null;
|
|
266
|
+
return { blockIndex, text: block.text };
|
|
267
|
+
}
|
|
268
|
+
function setSlideProp(source, slideOrdinal, key, value) {
|
|
269
|
+
const indices = slideBlockIndices(source);
|
|
270
|
+
if (slideOrdinal < 0 || slideOrdinal >= indices.length) return source;
|
|
271
|
+
const blockIndex = indices[slideOrdinal];
|
|
272
|
+
const blocks = splitBlocks(source);
|
|
273
|
+
const updated = blocks.map((b) => {
|
|
274
|
+
if (b.index !== blockIndex) return b;
|
|
275
|
+
if (parseSlide(b.text).kind !== "template") return b;
|
|
276
|
+
return { ...b, text: setProp(b.text, key, value) };
|
|
277
|
+
});
|
|
278
|
+
return joinBlocks(updated);
|
|
279
|
+
}
|
|
280
|
+
function renderProps(props) {
|
|
281
|
+
return Object.entries(props).map(([k, v]) => renderJsxAttr(k, v)).join(" ");
|
|
282
|
+
}
|
|
283
|
+
function switchSlideTemplate(source, slideOrdinal, nextName, nextProps) {
|
|
284
|
+
const indices = slideBlockIndices(source);
|
|
285
|
+
if (slideOrdinal < 0 || slideOrdinal >= indices.length) return source;
|
|
286
|
+
const blockIndex = indices[slideOrdinal];
|
|
287
|
+
const blocks = splitBlocks(source);
|
|
288
|
+
const updated = blocks.map((b) => {
|
|
289
|
+
if (b.index !== blockIndex) return b;
|
|
290
|
+
if (parseSlide(b.text).kind !== "template") return b;
|
|
291
|
+
const element = findTemplateElement(b.text);
|
|
292
|
+
if (element === null) return b;
|
|
293
|
+
const propsStr = renderProps(nextProps);
|
|
294
|
+
const replacement = propsStr.length > 0 ? `<${nextName} ${propsStr} />` : `<${nextName} />`;
|
|
295
|
+
const newText = b.text.slice(0, element.start) + replacement + b.text.slice(element.end);
|
|
296
|
+
return { ...b, text: newText };
|
|
297
|
+
});
|
|
298
|
+
return joinBlocks(updated);
|
|
299
|
+
}
|
|
300
|
+
function fieldId(templateName, key) {
|
|
301
|
+
return `a63-slot-${templateName}-${key}`;
|
|
302
|
+
}
|
|
303
|
+
function isMultiline(kind) {
|
|
304
|
+
return kind === "richtext" || kind === "list";
|
|
305
|
+
}
|
|
306
|
+
function SlotField({
|
|
307
|
+
templateName,
|
|
308
|
+
slot,
|
|
309
|
+
value,
|
|
310
|
+
onChange
|
|
311
|
+
}) {
|
|
312
|
+
const id = fieldId(templateName, slot.key);
|
|
313
|
+
const multiline = isMultiline(slot.kind);
|
|
314
|
+
return /* @__PURE__ */ jsxs("div", { className: "a63-form__field", children: [
|
|
315
|
+
/* @__PURE__ */ jsxs("div", { className: "a63-form__label-row", children: [
|
|
316
|
+
/* @__PURE__ */ jsx$1("label", { className: "a63-form__label", htmlFor: id, children: slot.label }),
|
|
317
|
+
slot.required && /* @__PURE__ */ jsx$1("span", { className: "a63-form__required", "aria-hidden": "true", children: "*" })
|
|
318
|
+
] }),
|
|
319
|
+
multiline ? /* @__PURE__ */ jsx$1(
|
|
320
|
+
"textarea",
|
|
321
|
+
{
|
|
322
|
+
id,
|
|
323
|
+
className: "a63-form__textarea",
|
|
324
|
+
value,
|
|
325
|
+
required: slot.required,
|
|
326
|
+
"aria-required": slot.required,
|
|
327
|
+
onChange: (e) => onChange(slot.key, e.target.value)
|
|
328
|
+
}
|
|
329
|
+
) : /* @__PURE__ */ jsx$1(
|
|
330
|
+
"input",
|
|
331
|
+
{
|
|
332
|
+
type: "text",
|
|
333
|
+
id,
|
|
334
|
+
className: "a63-form__input",
|
|
335
|
+
value,
|
|
336
|
+
required: slot.required,
|
|
337
|
+
"aria-required": slot.required,
|
|
338
|
+
onChange: (e) => onChange(slot.key, e.target.value)
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
] });
|
|
342
|
+
}
|
|
343
|
+
function SlotForm({ name, props, onChange }) {
|
|
344
|
+
const tpl = getTemplate(name);
|
|
345
|
+
if (!tpl) {
|
|
346
|
+
return /* @__PURE__ */ jsxs("p", { className: "a63-form__unknown", children: [
|
|
347
|
+
"Unknown template: ",
|
|
348
|
+
/* @__PURE__ */ jsx$1("code", { children: name })
|
|
349
|
+
] });
|
|
350
|
+
}
|
|
351
|
+
const slotGroupNames = tpl.slots.map((s) => s.name);
|
|
352
|
+
return /* @__PURE__ */ jsxs("div", { className: "a63-form", children: [
|
|
353
|
+
tpl.props.map((slot) => /* @__PURE__ */ jsx$1(
|
|
354
|
+
SlotField,
|
|
355
|
+
{
|
|
356
|
+
templateName: name,
|
|
357
|
+
slot,
|
|
358
|
+
value: props[slot.key] ?? "",
|
|
359
|
+
onChange
|
|
360
|
+
},
|
|
361
|
+
slot.key
|
|
362
|
+
)),
|
|
363
|
+
slotGroupNames.length > 0 && /* @__PURE__ */ jsxs("p", { className: "a63-form__slots-note", children: [
|
|
364
|
+
"Repeatable sections (",
|
|
365
|
+
slotGroupNames.join(", "),
|
|
366
|
+
") are edited in Source."
|
|
367
|
+
] })
|
|
368
|
+
] });
|
|
369
|
+
}
|
|
370
|
+
function TemplatePicker({ name, props, onSwitch }) {
|
|
371
|
+
const [warning, setWarning] = useState(
|
|
372
|
+
null
|
|
373
|
+
);
|
|
374
|
+
function handleChange(next) {
|
|
375
|
+
if (next === name) return;
|
|
376
|
+
const nextDef = getTemplate(next);
|
|
377
|
+
if (!nextDef) return;
|
|
378
|
+
const nextKeys = new Set(nextDef.props.map((s) => s.key));
|
|
379
|
+
const carried = {};
|
|
380
|
+
for (const [k, v] of Object.entries(props)) {
|
|
381
|
+
if (nextKeys.has(k)) {
|
|
382
|
+
carried[k] = v;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const dropped = Object.entries(props).filter(([k, v]) => !nextKeys.has(k) && v !== "").map(([k]) => k);
|
|
386
|
+
for (const slot of nextDef.props) {
|
|
387
|
+
if (!(slot.key in carried)) {
|
|
388
|
+
carried[slot.key] = "";
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
onSwitch(next, { props: carried, dropped });
|
|
392
|
+
if (dropped.length > 0) {
|
|
393
|
+
setWarning({ from: name, to: next, dropped });
|
|
394
|
+
} else {
|
|
395
|
+
setWarning(null);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const templates = listTemplates();
|
|
399
|
+
return /* @__PURE__ */ jsxs("div", { className: "a63-form__field", children: [
|
|
400
|
+
/* @__PURE__ */ jsx$1("label", { className: "a63-form__label", htmlFor: "a63-template-picker", children: "Template" }),
|
|
401
|
+
/* @__PURE__ */ jsx$1(
|
|
402
|
+
"select",
|
|
403
|
+
{
|
|
404
|
+
id: "a63-template-picker",
|
|
405
|
+
className: "a63-form__input",
|
|
406
|
+
"aria-label": "Template",
|
|
407
|
+
value: name,
|
|
408
|
+
onChange: (e) => handleChange(e.target.value),
|
|
409
|
+
children: templates.map((tpl) => /* @__PURE__ */ jsx$1("option", { value: tpl.name, children: tpl.label ?? tpl.name }, tpl.name))
|
|
410
|
+
}
|
|
411
|
+
),
|
|
412
|
+
warning && /* @__PURE__ */ jsxs("output", { className: "a63-form__picker-warning", children: [
|
|
413
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
414
|
+
"Switched ",
|
|
415
|
+
warning.from,
|
|
416
|
+
" \u2192 ",
|
|
417
|
+
warning.to,
|
|
418
|
+
"; dropped: ",
|
|
419
|
+
warning.dropped.join(", "),
|
|
420
|
+
"."
|
|
421
|
+
] }),
|
|
422
|
+
/* @__PURE__ */ jsx$1(
|
|
423
|
+
"button",
|
|
424
|
+
{
|
|
425
|
+
type: "button",
|
|
426
|
+
className: "a63-form__picker-dismiss",
|
|
427
|
+
"aria-label": "Dismiss",
|
|
428
|
+
onClick: () => setWarning(null),
|
|
429
|
+
children: "\xD7"
|
|
430
|
+
}
|
|
431
|
+
)
|
|
432
|
+
] })
|
|
433
|
+
] });
|
|
434
|
+
}
|
|
55
435
|
|
|
56
436
|
// src/editor/template-snippets.ts
|
|
57
437
|
var PLACEHOLDER_IMG = "/images/placeholder-1920x1080.webp";
|
|
@@ -130,8 +510,98 @@ ${synthExample(t)}
|
|
|
130
510
|
var templateSnippets = Object.fromEntries(
|
|
131
511
|
listTemplates().map((t) => [t.name, toInsertSnippet(t)])
|
|
132
512
|
);
|
|
133
|
-
|
|
513
|
+
|
|
514
|
+
// src/editor/frontmatter-edit.ts
|
|
515
|
+
var BOM = "\uFEFF";
|
|
516
|
+
function detectNewline(text) {
|
|
517
|
+
const crlf = (text.match(/\r\n/g) ?? []).length;
|
|
518
|
+
const lf = (text.match(/(?<!\r)\n/g) ?? []).length;
|
|
519
|
+
return crlf >= lf && crlf > 0 ? "\r\n" : "\n";
|
|
520
|
+
}
|
|
521
|
+
function setFrontmatterField(source, key, value) {
|
|
522
|
+
const hasBom = source.startsWith(BOM);
|
|
523
|
+
const withoutBom = hasBom ? source.slice(1) : source;
|
|
524
|
+
const nl = detectNewline(withoutBom);
|
|
525
|
+
const lines = withoutBom.split("\n").map((l) => l.replace(/\r$/, ""));
|
|
526
|
+
const hasFrontmatter = lines[0]?.trim() === "---";
|
|
527
|
+
if (!hasFrontmatter) {
|
|
528
|
+
if (!value) return source;
|
|
529
|
+
const prefix = `---${nl}${key}: ${value}${nl}---${nl}`;
|
|
530
|
+
return hasBom ? BOM + prefix + withoutBom : prefix + withoutBom;
|
|
531
|
+
}
|
|
532
|
+
let closingIndex = -1;
|
|
533
|
+
for (let i = 1; i < lines.length; i++) {
|
|
534
|
+
if (lines[i].trim() === "---") {
|
|
535
|
+
closingIndex = i;
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (closingIndex === -1) {
|
|
540
|
+
if (!value) return source;
|
|
541
|
+
const prefix = `---${nl}${key}: ${value}${nl}---${nl}`;
|
|
542
|
+
return hasBom ? BOM + prefix + withoutBom : prefix + withoutBom;
|
|
543
|
+
}
|
|
544
|
+
const innerLines = lines.slice(1, closingIndex);
|
|
545
|
+
const bodyLines = lines.slice(closingIndex + 1);
|
|
546
|
+
const keyRe = new RegExp(`^${escapeRegExp(key)}:`);
|
|
547
|
+
const existingIdx = innerLines.findIndex((l) => keyRe.test(l));
|
|
548
|
+
let newInnerLines;
|
|
549
|
+
if (existingIdx !== -1) {
|
|
550
|
+
const existing = innerLines[existingIdx];
|
|
551
|
+
if (value) {
|
|
552
|
+
const desired = `${key}: ${value}`;
|
|
553
|
+
if (existing === desired) return source;
|
|
554
|
+
newInnerLines = [...innerLines];
|
|
555
|
+
newInnerLines[existingIdx] = desired;
|
|
556
|
+
} else {
|
|
557
|
+
newInnerLines = innerLines.filter((_, i) => i !== existingIdx);
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
if (!value) {
|
|
561
|
+
return source;
|
|
562
|
+
}
|
|
563
|
+
newInnerLines = [...innerLines, `${key}: ${value}`];
|
|
564
|
+
}
|
|
565
|
+
const innerBlock = newInnerLines.join(nl);
|
|
566
|
+
const bodyBlock = bodyLines.join(nl);
|
|
567
|
+
const rebuilt = `---${nl}${innerBlock}${nl}---${nl}${bodyBlock}`;
|
|
568
|
+
return hasBom ? BOM + rebuilt : rebuilt;
|
|
569
|
+
}
|
|
570
|
+
function escapeRegExp(s) {
|
|
571
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
572
|
+
}
|
|
573
|
+
function ThemePicker({ source, theme, onChange }) {
|
|
574
|
+
const id = useId();
|
|
575
|
+
return /* @__PURE__ */ jsxs("label", { htmlFor: id, className: "a63-editor__theme-picker-label", children: [
|
|
576
|
+
/* @__PURE__ */ jsx$1("span", { className: "a63-editor__theme-picker-text", children: "Theme" }),
|
|
577
|
+
/* @__PURE__ */ jsxs(
|
|
578
|
+
"select",
|
|
579
|
+
{
|
|
580
|
+
id,
|
|
581
|
+
"aria-label": "Theme",
|
|
582
|
+
className: "a63-editor__theme-select",
|
|
583
|
+
value: theme ?? "",
|
|
584
|
+
onChange: (e) => onChange(setFrontmatterField(source, "theme", e.target.value)),
|
|
585
|
+
children: [
|
|
586
|
+
/* @__PURE__ */ jsx$1("option", { value: "", children: "Default" }),
|
|
587
|
+
BUILTIN_THEMES.map((t) => /* @__PURE__ */ jsx$1("option", { value: t, children: t.charAt(0).toUpperCase() + t.slice(1) }, t))
|
|
588
|
+
]
|
|
589
|
+
}
|
|
590
|
+
)
|
|
591
|
+
] });
|
|
592
|
+
}
|
|
593
|
+
function EditPane({
|
|
594
|
+
source,
|
|
595
|
+
onChange,
|
|
596
|
+
onSave,
|
|
597
|
+
deck,
|
|
598
|
+
error,
|
|
599
|
+
onPresent,
|
|
600
|
+
themeValue
|
|
601
|
+
}) {
|
|
134
602
|
const [theme, setTheme] = useState("light");
|
|
603
|
+
const [rightTab, setRightTab] = useState("source");
|
|
604
|
+
const [formSlideIdx, setFormSlideIdx] = useState(0);
|
|
135
605
|
const textareaRef = useRef(null);
|
|
136
606
|
const templates = useMemo(() => listTemplates(), []);
|
|
137
607
|
const handleChange = useCallback(
|
|
@@ -163,21 +633,63 @@ ${snippet}`);
|
|
|
163
633
|
},
|
|
164
634
|
[source, onChange]
|
|
165
635
|
);
|
|
636
|
+
const slideCount = useMemo(() => slideBlockIndices(source).length, [source]);
|
|
637
|
+
const clampedIdx = Math.min(formSlideIdx, Math.max(0, slideCount - 1));
|
|
638
|
+
const currentBlock = useMemo(() => getSlideBlock(source, clampedIdx), [source, clampedIdx]);
|
|
639
|
+
const parsedSlide = useMemo(
|
|
640
|
+
() => currentBlock ? parseSlide(currentBlock.text) : null,
|
|
641
|
+
[currentBlock]
|
|
642
|
+
);
|
|
643
|
+
const handleSlotChange = useCallback(
|
|
644
|
+
(key, value) => {
|
|
645
|
+
onChange(setSlideProp(source, clampedIdx, key, value));
|
|
646
|
+
},
|
|
647
|
+
[source, clampedIdx, onChange]
|
|
648
|
+
);
|
|
649
|
+
const handleTemplateSwitch = useCallback(
|
|
650
|
+
(next, mapped) => {
|
|
651
|
+
onChange(switchSlideTemplate(source, clampedIdx, next, mapped.props));
|
|
652
|
+
},
|
|
653
|
+
[source, clampedIdx, onChange]
|
|
654
|
+
);
|
|
166
655
|
return /* @__PURE__ */ jsxs("div", { className: "a63-editor", children: [
|
|
167
656
|
/* @__PURE__ */ jsxs("section", { className: "a63-editor__preview", "data-theme": theme, children: [
|
|
168
657
|
error ? /* @__PURE__ */ jsxs("div", { className: "a63-editor__error", role: "alert", children: [
|
|
169
|
-
/* @__PURE__ */ jsx("span", { className: "a63-editor__error-tag", children: "MDX error" }),
|
|
170
|
-
/* @__PURE__ */ jsx("span", { className: "a63-editor__error-msg", children: error })
|
|
658
|
+
/* @__PURE__ */ jsx$1("span", { className: "a63-editor__error-tag", children: "MDX error" }),
|
|
659
|
+
/* @__PURE__ */ jsx$1("span", { className: "a63-editor__error-msg", children: error })
|
|
171
660
|
] }) : null,
|
|
172
|
-
deck ? /* @__PURE__ */ jsx("div", { className: "a63-editor__preview-stage", children: /* @__PURE__ */ jsx(SlidesPlayer, { deck, onBack: () => {
|
|
173
|
-
} }) }, theme) : /* @__PURE__ */ jsx("div", { className: "a63-editor__preview-empty", children: error ? "Fix the MDX error to render a preview." : "Compiling preview\u2026" })
|
|
661
|
+
deck ? /* @__PURE__ */ jsx$1("div", { className: "a63-editor__preview-stage", children: /* @__PURE__ */ jsx$1(SlidesPlayer, { deck, onBack: () => {
|
|
662
|
+
} }) }, theme) : /* @__PURE__ */ jsx$1("div", { className: "a63-editor__preview-empty", children: error ? "Fix the MDX error to render a preview." : "Compiling preview\u2026" })
|
|
174
663
|
] }),
|
|
175
664
|
/* @__PURE__ */ jsxs("section", { className: "a63-editor__source", children: [
|
|
176
665
|
/* @__PURE__ */ jsxs("div", { className: "a63-editor__toolbar", children: [
|
|
177
|
-
/* @__PURE__ */ jsx("span", { className: "a63-editor__title", children: "Deck source \xB7 MDX" }),
|
|
666
|
+
/* @__PURE__ */ jsx$1("span", { className: "a63-editor__title", children: "Deck source \xB7 MDX" }),
|
|
178
667
|
/* @__PURE__ */ jsxs("div", { className: "a63-editor__toolbar-actions", children: [
|
|
179
|
-
|
|
180
|
-
|
|
668
|
+
/* @__PURE__ */ jsxs("div", { className: "a63-editor__subtoggle", role: "group", "aria-label": "Edit mode", children: [
|
|
669
|
+
/* @__PURE__ */ jsx$1(
|
|
670
|
+
"button",
|
|
671
|
+
{
|
|
672
|
+
type: "button",
|
|
673
|
+
className: "a63-editor__subtoggle-btn",
|
|
674
|
+
"aria-pressed": rightTab === "source",
|
|
675
|
+
onClick: () => setRightTab("source"),
|
|
676
|
+
children: "Source"
|
|
677
|
+
}
|
|
678
|
+
),
|
|
679
|
+
/* @__PURE__ */ jsx$1(
|
|
680
|
+
"button",
|
|
681
|
+
{
|
|
682
|
+
type: "button",
|
|
683
|
+
className: "a63-editor__subtoggle-btn",
|
|
684
|
+
"aria-pressed": rightTab === "form",
|
|
685
|
+
onClick: () => setRightTab("form"),
|
|
686
|
+
children: "Form"
|
|
687
|
+
}
|
|
688
|
+
)
|
|
689
|
+
] }),
|
|
690
|
+
/* @__PURE__ */ jsx$1(ThemePicker, { source, theme: themeValue, onChange }),
|
|
691
|
+
onSave ? /* @__PURE__ */ jsx$1("button", { type: "button", className: "a63-editor__save", onClick: () => onSave(source), children: "Save" }) : null,
|
|
692
|
+
/* @__PURE__ */ jsx$1(
|
|
181
693
|
"button",
|
|
182
694
|
{
|
|
183
695
|
type: "button",
|
|
@@ -187,37 +699,99 @@ ${snippet}`);
|
|
|
187
699
|
children: theme === "light" ? "\u2600\uFE0E Light" : "\u263E Dark"
|
|
188
700
|
}
|
|
189
701
|
),
|
|
190
|
-
/* @__PURE__ */ jsx("button", { type: "button", className: "a63-editor__present", onClick: onPresent, children: "Present" })
|
|
702
|
+
/* @__PURE__ */ jsx$1("button", { type: "button", className: "a63-editor__present", onClick: onPresent, children: "Present" })
|
|
191
703
|
] })
|
|
192
704
|
] }),
|
|
193
|
-
/* @__PURE__ */
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
ref: textareaRef,
|
|
197
|
-
className: "a63-editor__textarea",
|
|
198
|
-
value: source,
|
|
199
|
-
onChange: handleChange,
|
|
200
|
-
onKeyDown: handleKeyDown,
|
|
201
|
-
spellCheck: false,
|
|
202
|
-
"aria-label": "Deck MDX source"
|
|
203
|
-
}
|
|
204
|
-
),
|
|
205
|
-
/* @__PURE__ */ jsxs("div", { className: "a63-editor__palette", children: [
|
|
206
|
-
/* @__PURE__ */ jsx("div", { className: "a63-editor__palette-label", children: "Templates \xB7 click to append" }),
|
|
207
|
-
/* @__PURE__ */ jsx("div", { className: "a63-editor__palette-grid", children: templates.map((t) => /* @__PURE__ */ jsxs(
|
|
208
|
-
"button",
|
|
705
|
+
rightTab === "source" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
706
|
+
/* @__PURE__ */ jsx$1(
|
|
707
|
+
"textarea",
|
|
209
708
|
{
|
|
210
|
-
|
|
211
|
-
className: "a63-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
709
|
+
ref: textareaRef,
|
|
710
|
+
className: "a63-editor__textarea",
|
|
711
|
+
value: source,
|
|
712
|
+
onChange: handleChange,
|
|
713
|
+
onKeyDown: handleKeyDown,
|
|
714
|
+
spellCheck: false,
|
|
715
|
+
"aria-label": "Deck MDX source"
|
|
716
|
+
}
|
|
717
|
+
),
|
|
718
|
+
/* @__PURE__ */ jsxs("div", { className: "a63-editor__palette", children: [
|
|
719
|
+
/* @__PURE__ */ jsx$1("div", { className: "a63-editor__palette-label", children: "Templates \xB7 click to append" }),
|
|
720
|
+
/* @__PURE__ */ jsx$1("div", { className: "a63-editor__palette-grid", children: templates.map((t) => /* @__PURE__ */ jsxs(
|
|
721
|
+
"button",
|
|
722
|
+
{
|
|
723
|
+
type: "button",
|
|
724
|
+
className: "a63-editor__chip",
|
|
725
|
+
onClick: () => insert(t.name),
|
|
726
|
+
title: `Insert ${t.name}`,
|
|
727
|
+
children: [
|
|
728
|
+
/* @__PURE__ */ jsx$1("span", { className: "a63-editor__chip-name", children: t.name }),
|
|
729
|
+
/* @__PURE__ */ jsx$1("span", { className: "a63-editor__chip-meta", children: t.label })
|
|
730
|
+
]
|
|
731
|
+
},
|
|
732
|
+
t.name
|
|
733
|
+
)) })
|
|
734
|
+
] })
|
|
735
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "a63-editor__form-panel", children: [
|
|
736
|
+
/* @__PURE__ */ jsxs("div", { className: "a63-editor__slide-stepper", children: [
|
|
737
|
+
/* @__PURE__ */ jsx$1(
|
|
738
|
+
"button",
|
|
739
|
+
{
|
|
740
|
+
type: "button",
|
|
741
|
+
className: "a63-editor__stepper-btn",
|
|
742
|
+
"aria-label": "Previous slide",
|
|
743
|
+
disabled: clampedIdx <= 0,
|
|
744
|
+
onClick: () => setFormSlideIdx((i) => Math.max(0, i - 1)),
|
|
745
|
+
children: "\u2039"
|
|
746
|
+
}
|
|
747
|
+
),
|
|
748
|
+
/* @__PURE__ */ jsxs("span", { className: "a63-editor__stepper-label", children: [
|
|
749
|
+
"Slide ",
|
|
750
|
+
clampedIdx + 1,
|
|
751
|
+
" / ",
|
|
752
|
+
slideCount
|
|
753
|
+
] }),
|
|
754
|
+
/* @__PURE__ */ jsx$1(
|
|
755
|
+
"button",
|
|
756
|
+
{
|
|
757
|
+
type: "button",
|
|
758
|
+
className: "a63-editor__stepper-btn",
|
|
759
|
+
"aria-label": "Next slide",
|
|
760
|
+
disabled: clampedIdx >= slideCount - 1,
|
|
761
|
+
onClick: () => setFormSlideIdx((i) => Math.min(slideCount - 1, i + 1)),
|
|
762
|
+
children: "\u203A"
|
|
763
|
+
}
|
|
764
|
+
)
|
|
765
|
+
] }),
|
|
766
|
+
parsedSlide === null ? /* @__PURE__ */ jsx$1("p", { className: "a63-editor__form-empty", children: "No slides found." }) : parsedSlide.kind === "opaque" ? /* @__PURE__ */ jsxs("div", { className: "a63-editor__form-opaque", children: [
|
|
767
|
+
/* @__PURE__ */ jsx$1("p", { children: "This slide is only editable in Source." }),
|
|
768
|
+
/* @__PURE__ */ jsx$1(
|
|
769
|
+
"button",
|
|
770
|
+
{
|
|
771
|
+
type: "button",
|
|
772
|
+
className: "a63-editor__subtoggle-btn",
|
|
773
|
+
onClick: () => setRightTab("source"),
|
|
774
|
+
children: "Switch to Source"
|
|
775
|
+
}
|
|
776
|
+
)
|
|
777
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "a63-editor__form-fields", children: [
|
|
778
|
+
/* @__PURE__ */ jsx$1(
|
|
779
|
+
TemplatePicker,
|
|
780
|
+
{
|
|
781
|
+
name: parsedSlide.name,
|
|
782
|
+
props: parsedSlide.props,
|
|
783
|
+
onSwitch: handleTemplateSwitch
|
|
784
|
+
}
|
|
785
|
+
),
|
|
786
|
+
/* @__PURE__ */ jsx$1(
|
|
787
|
+
SlotForm,
|
|
788
|
+
{
|
|
789
|
+
name: parsedSlide.name,
|
|
790
|
+
props: parsedSlide.props,
|
|
791
|
+
onChange: handleSlotChange
|
|
792
|
+
}
|
|
793
|
+
)
|
|
794
|
+
] })
|
|
221
795
|
] })
|
|
222
796
|
] })
|
|
223
797
|
] });
|
|
@@ -260,6 +834,7 @@ function DeckSurface({
|
|
|
260
834
|
() => preview ? { slug: "draft", meta: preview.meta, content: preview.Content } : null,
|
|
261
835
|
[preview]
|
|
262
836
|
);
|
|
837
|
+
const theme = resolveTheme(deck?.meta);
|
|
263
838
|
useEffect(() => {
|
|
264
839
|
if (!editable) return;
|
|
265
840
|
const onKey = (e) => {
|
|
@@ -271,7 +846,7 @@ function DeckSurface({
|
|
|
271
846
|
return () => window.removeEventListener("keydown", onKey);
|
|
272
847
|
}, [editable, mode]);
|
|
273
848
|
if (editable && mode === "edit") {
|
|
274
|
-
return /* @__PURE__ */ jsx(
|
|
849
|
+
return /* @__PURE__ */ jsx$1("div", { className: "a63-surface-edit", "data-slides-theme": theme, children: /* @__PURE__ */ jsx$1(
|
|
275
850
|
EditPane,
|
|
276
851
|
{
|
|
277
852
|
source,
|
|
@@ -279,18 +854,19 @@ function DeckSurface({
|
|
|
279
854
|
onSave,
|
|
280
855
|
deck,
|
|
281
856
|
error,
|
|
282
|
-
onPresent: () => setMode("present")
|
|
857
|
+
onPresent: () => setMode("present"),
|
|
858
|
+
themeValue: theme
|
|
283
859
|
}
|
|
284
|
-
);
|
|
860
|
+
) });
|
|
285
861
|
}
|
|
286
|
-
return /* @__PURE__ */ jsxs("div", { className: "a63-surface", children: [
|
|
287
|
-
editable && /* @__PURE__ */ jsx("button", { type: "button", className: "a63-surface__edit", onClick: () => setMode("edit"), children: "Edit" }),
|
|
288
|
-
deck ? /* @__PURE__ */ jsx(SlidesPlayer, { deck, onBack: () => {
|
|
289
|
-
} }) : /* @__PURE__ */ jsx("div", { className: "a63-editor__preview-empty", children: error ? "Fix the MDX error to render a preview." : "Compiling preview\u2026" })
|
|
862
|
+
return /* @__PURE__ */ jsxs("div", { className: "a63-surface", "data-slides-theme": theme, children: [
|
|
863
|
+
editable && /* @__PURE__ */ jsx$1("button", { type: "button", className: "a63-surface__edit", onClick: () => setMode("edit"), children: "Edit" }),
|
|
864
|
+
deck ? /* @__PURE__ */ jsx$1(SlidesPlayer, { deck, onBack: () => {
|
|
865
|
+
} }) : /* @__PURE__ */ jsx$1("div", { className: "a63-editor__preview-empty", children: error ? "Fix the MDX error to render a preview." : "Compiling preview\u2026" })
|
|
290
866
|
] });
|
|
291
867
|
}
|
|
292
868
|
function DeckEditor({ source, onChange, debounceMs }) {
|
|
293
|
-
return /* @__PURE__ */ jsx(
|
|
869
|
+
return /* @__PURE__ */ jsx$1(
|
|
294
870
|
DeckSurface,
|
|
295
871
|
{
|
|
296
872
|
source,
|