@atom63/slides 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editor/index.d.ts +50 -32
- package/dist/editor/index.js +629 -89
- package/dist/editor/index.js.map +1 -1
- package/dist/vite/index.d.ts +10 -1
- package/dist/vite/index.js +40 -4
- package/dist/vite/index.js.map +1 -1
- package/package.json +11 -4
- package/src/editor/styles.css +277 -0
package/dist/editor/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { listTemplates, slideMdxComponents, SlidesPlayer } from '../chunk-AD3ZOVWR.js';
|
|
1
|
+
import { listTemplates, slideMdxComponents, SlidesPlayer, templateNames, getTemplate } from '../chunk-AD3ZOVWR.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 { jsxs,
|
|
5
|
+
import { jsx as jsx$1, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
7
|
-
import { useState,
|
|
7
|
+
import { useState, useEffect, useMemo, useRef, useCallback } 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,50 +510,31 @@ ${synthExample(t)}
|
|
|
130
510
|
var templateSnippets = Object.fromEntries(
|
|
131
511
|
listTemplates().map((t) => [t.name, toInsertSnippet(t)])
|
|
132
512
|
);
|
|
133
|
-
function
|
|
134
|
-
const isControlled = onChange !== void 0;
|
|
135
|
-
const [internalSource, setInternalSource] = useState(sourceProp);
|
|
136
|
-
const source = isControlled ? sourceProp : internalSource;
|
|
137
|
-
const [preview, setPreview] = useState(null);
|
|
138
|
-
const [error, setError] = useState(null);
|
|
513
|
+
function EditPane({ source, onChange, onSave, deck, error, onPresent }) {
|
|
139
514
|
const [theme, setTheme] = useState("light");
|
|
515
|
+
const [rightTab, setRightTab] = useState("source");
|
|
516
|
+
const [formSlideIdx, setFormSlideIdx] = useState(0);
|
|
140
517
|
const textareaRef = useRef(null);
|
|
141
518
|
const templates = useMemo(() => listTemplates(), []);
|
|
142
|
-
const
|
|
143
|
-
(
|
|
144
|
-
|
|
145
|
-
else setInternalSource(next);
|
|
146
|
-
},
|
|
147
|
-
[isControlled, onChange]
|
|
519
|
+
const handleChange = useCallback(
|
|
520
|
+
(e) => onChange(e.target.value),
|
|
521
|
+
[onChange]
|
|
148
522
|
);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (result.ok) {
|
|
155
|
-
setPreview({ Content: result.Content, meta: result.meta });
|
|
156
|
-
setError(null);
|
|
157
|
-
} else {
|
|
158
|
-
setError(result.error);
|
|
523
|
+
const handleKeyDown = useCallback(
|
|
524
|
+
(e) => {
|
|
525
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
526
|
+
e.preventDefault();
|
|
527
|
+
onSave?.(source);
|
|
159
528
|
}
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
cancelled = true;
|
|
163
|
-
clearTimeout(handle);
|
|
164
|
-
};
|
|
165
|
-
}, [source, debounceMs]);
|
|
166
|
-
const handleChange = useCallback(
|
|
167
|
-
(e) => setSource(e.target.value),
|
|
168
|
-
[setSource]
|
|
529
|
+
},
|
|
530
|
+
[onSave, source]
|
|
169
531
|
);
|
|
170
|
-
const
|
|
532
|
+
const insert = useCallback(
|
|
171
533
|
(name) => {
|
|
172
534
|
const snippet = templateSnippets[name];
|
|
173
535
|
if (!snippet) return;
|
|
174
|
-
|
|
175
|
-
${snippet}
|
|
176
|
-
setSource(next);
|
|
536
|
+
onChange(`${source.replace(/\s*$/, "")}
|
|
537
|
+
${snippet}`);
|
|
177
538
|
requestAnimationFrame(() => {
|
|
178
539
|
const el = textareaRef.current;
|
|
179
540
|
if (!el) return;
|
|
@@ -182,71 +543,250 @@ ${snippet}`;
|
|
|
182
543
|
el.scrollTop = el.scrollHeight;
|
|
183
544
|
});
|
|
184
545
|
},
|
|
185
|
-
[source,
|
|
546
|
+
[source, onChange]
|
|
547
|
+
);
|
|
548
|
+
const slideCount = useMemo(() => slideBlockIndices(source).length, [source]);
|
|
549
|
+
const clampedIdx = Math.min(formSlideIdx, Math.max(0, slideCount - 1));
|
|
550
|
+
const currentBlock = useMemo(() => getSlideBlock(source, clampedIdx), [source, clampedIdx]);
|
|
551
|
+
const parsedSlide = useMemo(
|
|
552
|
+
() => currentBlock ? parseSlide(currentBlock.text) : null,
|
|
553
|
+
[currentBlock]
|
|
554
|
+
);
|
|
555
|
+
const handleSlotChange = useCallback(
|
|
556
|
+
(key, value) => {
|
|
557
|
+
onChange(setSlideProp(source, clampedIdx, key, value));
|
|
558
|
+
},
|
|
559
|
+
[source, clampedIdx, onChange]
|
|
560
|
+
);
|
|
561
|
+
const handleTemplateSwitch = useCallback(
|
|
562
|
+
(next, mapped) => {
|
|
563
|
+
onChange(switchSlideTemplate(source, clampedIdx, next, mapped.props));
|
|
564
|
+
},
|
|
565
|
+
[source, clampedIdx, onChange]
|
|
186
566
|
);
|
|
187
|
-
const deck = useMemo(() => {
|
|
188
|
-
if (!preview) return null;
|
|
189
|
-
return { slug: "draft", meta: preview.meta, content: preview.Content };
|
|
190
|
-
}, [preview]);
|
|
191
567
|
return /* @__PURE__ */ jsxs("div", { className: "a63-editor", children: [
|
|
192
568
|
/* @__PURE__ */ jsxs("section", { className: "a63-editor__preview", "data-theme": theme, children: [
|
|
193
569
|
error ? /* @__PURE__ */ jsxs("div", { className: "a63-editor__error", role: "alert", children: [
|
|
194
|
-
/* @__PURE__ */ jsx("span", { className: "a63-editor__error-tag", children: "MDX error" }),
|
|
195
|
-
/* @__PURE__ */ jsx("span", { className: "a63-editor__error-msg", children: error })
|
|
570
|
+
/* @__PURE__ */ jsx$1("span", { className: "a63-editor__error-tag", children: "MDX error" }),
|
|
571
|
+
/* @__PURE__ */ jsx$1("span", { className: "a63-editor__error-msg", children: error })
|
|
196
572
|
] }) : null,
|
|
197
|
-
deck ? (
|
|
198
|
-
|
|
199
|
-
/* @__PURE__ */ jsx("div", { className: "a63-editor__preview-stage", children: /* @__PURE__ */ jsx(SlidesPlayer, { deck, onBack: () => {
|
|
200
|
-
} }) }, `${theme}`)
|
|
201
|
-
) : /* @__PURE__ */ jsx("div", { className: "a63-editor__preview-empty", children: error ? "Fix the MDX error to render a preview." : "Compiling preview\u2026" })
|
|
573
|
+
deck ? /* @__PURE__ */ jsx$1("div", { className: "a63-editor__preview-stage", children: /* @__PURE__ */ jsx$1(SlidesPlayer, { deck, onBack: () => {
|
|
574
|
+
} }) }, theme) : /* @__PURE__ */ jsx$1("div", { className: "a63-editor__preview-empty", children: error ? "Fix the MDX error to render a preview." : "Compiling preview\u2026" })
|
|
202
575
|
] }),
|
|
203
576
|
/* @__PURE__ */ jsxs("section", { className: "a63-editor__source", children: [
|
|
204
577
|
/* @__PURE__ */ jsxs("div", { className: "a63-editor__toolbar", children: [
|
|
205
|
-
/* @__PURE__ */ jsx("span", { className: "a63-editor__title", children: "Deck source \xB7 MDX" }),
|
|
206
|
-
/* @__PURE__ */
|
|
207
|
-
"
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
578
|
+
/* @__PURE__ */ jsx$1("span", { className: "a63-editor__title", children: "Deck source \xB7 MDX" }),
|
|
579
|
+
/* @__PURE__ */ jsxs("div", { className: "a63-editor__toolbar-actions", children: [
|
|
580
|
+
/* @__PURE__ */ jsxs("div", { className: "a63-editor__subtoggle", role: "group", "aria-label": "Edit mode", children: [
|
|
581
|
+
/* @__PURE__ */ jsx$1(
|
|
582
|
+
"button",
|
|
583
|
+
{
|
|
584
|
+
type: "button",
|
|
585
|
+
className: "a63-editor__subtoggle-btn",
|
|
586
|
+
"aria-pressed": rightTab === "source",
|
|
587
|
+
onClick: () => setRightTab("source"),
|
|
588
|
+
children: "Source"
|
|
589
|
+
}
|
|
590
|
+
),
|
|
591
|
+
/* @__PURE__ */ jsx$1(
|
|
592
|
+
"button",
|
|
593
|
+
{
|
|
594
|
+
type: "button",
|
|
595
|
+
className: "a63-editor__subtoggle-btn",
|
|
596
|
+
"aria-pressed": rightTab === "form",
|
|
597
|
+
onClick: () => setRightTab("form"),
|
|
598
|
+
children: "Form"
|
|
599
|
+
}
|
|
600
|
+
)
|
|
601
|
+
] }),
|
|
602
|
+
onSave ? /* @__PURE__ */ jsx$1("button", { type: "button", className: "a63-editor__save", onClick: () => onSave(source), children: "Save" }) : null,
|
|
603
|
+
/* @__PURE__ */ jsx$1(
|
|
604
|
+
"button",
|
|
605
|
+
{
|
|
606
|
+
type: "button",
|
|
607
|
+
className: "a63-editor__theme-toggle",
|
|
608
|
+
onClick: () => setTheme((t) => t === "light" ? "dark" : "light"),
|
|
609
|
+
"aria-pressed": theme === "dark",
|
|
610
|
+
children: theme === "light" ? "\u2600\uFE0E Light" : "\u263E Dark"
|
|
611
|
+
}
|
|
612
|
+
),
|
|
613
|
+
/* @__PURE__ */ jsx$1("button", { type: "button", className: "a63-editor__present", onClick: onPresent, children: "Present" })
|
|
614
|
+
] })
|
|
216
615
|
] }),
|
|
217
|
-
/* @__PURE__ */
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
ref: textareaRef,
|
|
221
|
-
className: "a63-editor__textarea",
|
|
222
|
-
value: source,
|
|
223
|
-
onChange: handleChange,
|
|
224
|
-
spellCheck: false,
|
|
225
|
-
"aria-label": "Deck MDX source"
|
|
226
|
-
}
|
|
227
|
-
),
|
|
228
|
-
/* @__PURE__ */ jsxs("div", { className: "a63-editor__palette", children: [
|
|
229
|
-
/* @__PURE__ */ jsx("div", { className: "a63-editor__palette-label", children: "Templates \xB7 click to append" }),
|
|
230
|
-
/* @__PURE__ */ jsx("div", { className: "a63-editor__palette-grid", children: templates.map((t) => /* @__PURE__ */ jsxs(
|
|
231
|
-
"button",
|
|
616
|
+
rightTab === "source" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
617
|
+
/* @__PURE__ */ jsx$1(
|
|
618
|
+
"textarea",
|
|
232
619
|
{
|
|
233
|
-
|
|
234
|
-
className: "a63-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
620
|
+
ref: textareaRef,
|
|
621
|
+
className: "a63-editor__textarea",
|
|
622
|
+
value: source,
|
|
623
|
+
onChange: handleChange,
|
|
624
|
+
onKeyDown: handleKeyDown,
|
|
625
|
+
spellCheck: false,
|
|
626
|
+
"aria-label": "Deck MDX source"
|
|
627
|
+
}
|
|
628
|
+
),
|
|
629
|
+
/* @__PURE__ */ jsxs("div", { className: "a63-editor__palette", children: [
|
|
630
|
+
/* @__PURE__ */ jsx$1("div", { className: "a63-editor__palette-label", children: "Templates \xB7 click to append" }),
|
|
631
|
+
/* @__PURE__ */ jsx$1("div", { className: "a63-editor__palette-grid", children: templates.map((t) => /* @__PURE__ */ jsxs(
|
|
632
|
+
"button",
|
|
633
|
+
{
|
|
634
|
+
type: "button",
|
|
635
|
+
className: "a63-editor__chip",
|
|
636
|
+
onClick: () => insert(t.name),
|
|
637
|
+
title: `Insert ${t.name}`,
|
|
638
|
+
children: [
|
|
639
|
+
/* @__PURE__ */ jsx$1("span", { className: "a63-editor__chip-name", children: t.name }),
|
|
640
|
+
/* @__PURE__ */ jsx$1("span", { className: "a63-editor__chip-meta", children: t.label })
|
|
641
|
+
]
|
|
642
|
+
},
|
|
643
|
+
t.name
|
|
644
|
+
)) })
|
|
645
|
+
] })
|
|
646
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "a63-editor__form-panel", children: [
|
|
647
|
+
/* @__PURE__ */ jsxs("div", { className: "a63-editor__slide-stepper", children: [
|
|
648
|
+
/* @__PURE__ */ jsx$1(
|
|
649
|
+
"button",
|
|
650
|
+
{
|
|
651
|
+
type: "button",
|
|
652
|
+
className: "a63-editor__stepper-btn",
|
|
653
|
+
"aria-label": "Previous slide",
|
|
654
|
+
disabled: clampedIdx <= 0,
|
|
655
|
+
onClick: () => setFormSlideIdx((i) => Math.max(0, i - 1)),
|
|
656
|
+
children: "\u2039"
|
|
657
|
+
}
|
|
658
|
+
),
|
|
659
|
+
/* @__PURE__ */ jsxs("span", { className: "a63-editor__stepper-label", children: [
|
|
660
|
+
"Slide ",
|
|
661
|
+
clampedIdx + 1,
|
|
662
|
+
" / ",
|
|
663
|
+
slideCount
|
|
664
|
+
] }),
|
|
665
|
+
/* @__PURE__ */ jsx$1(
|
|
666
|
+
"button",
|
|
667
|
+
{
|
|
668
|
+
type: "button",
|
|
669
|
+
className: "a63-editor__stepper-btn",
|
|
670
|
+
"aria-label": "Next slide",
|
|
671
|
+
disabled: clampedIdx >= slideCount - 1,
|
|
672
|
+
onClick: () => setFormSlideIdx((i) => Math.min(slideCount - 1, i + 1)),
|
|
673
|
+
children: "\u203A"
|
|
674
|
+
}
|
|
675
|
+
)
|
|
676
|
+
] }),
|
|
677
|
+
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: [
|
|
678
|
+
/* @__PURE__ */ jsx$1("p", { children: "This slide is only editable in Source." }),
|
|
679
|
+
/* @__PURE__ */ jsx$1(
|
|
680
|
+
"button",
|
|
681
|
+
{
|
|
682
|
+
type: "button",
|
|
683
|
+
className: "a63-editor__subtoggle-btn",
|
|
684
|
+
onClick: () => setRightTab("source"),
|
|
685
|
+
children: "Switch to Source"
|
|
686
|
+
}
|
|
687
|
+
)
|
|
688
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "a63-editor__form-fields", children: [
|
|
689
|
+
/* @__PURE__ */ jsx$1(
|
|
690
|
+
TemplatePicker,
|
|
691
|
+
{
|
|
692
|
+
name: parsedSlide.name,
|
|
693
|
+
props: parsedSlide.props,
|
|
694
|
+
onSwitch: handleTemplateSwitch
|
|
695
|
+
}
|
|
696
|
+
),
|
|
697
|
+
/* @__PURE__ */ jsx$1(
|
|
698
|
+
SlotForm,
|
|
699
|
+
{
|
|
700
|
+
name: parsedSlide.name,
|
|
701
|
+
props: parsedSlide.props,
|
|
702
|
+
onChange: handleSlotChange
|
|
703
|
+
}
|
|
704
|
+
)
|
|
705
|
+
] })
|
|
245
706
|
] })
|
|
246
707
|
] })
|
|
247
708
|
] });
|
|
248
709
|
}
|
|
710
|
+
function DeckSurface({
|
|
711
|
+
source: sourceProp,
|
|
712
|
+
onChange,
|
|
713
|
+
onSave,
|
|
714
|
+
initialMode = "present",
|
|
715
|
+
debounceMs = 300
|
|
716
|
+
}) {
|
|
717
|
+
const editable = onChange !== void 0;
|
|
718
|
+
const [internalSource, setInternalSource] = useState(sourceProp);
|
|
719
|
+
const source = editable ? sourceProp : internalSource;
|
|
720
|
+
const setSource = (next) => {
|
|
721
|
+
if (editable) onChange(next);
|
|
722
|
+
else setInternalSource(next);
|
|
723
|
+
};
|
|
724
|
+
const [mode, setMode] = useState(editable ? initialMode : "present");
|
|
725
|
+
const [preview, setPreview] = useState(null);
|
|
726
|
+
const [error, setError] = useState(null);
|
|
727
|
+
useEffect(() => {
|
|
728
|
+
let cancelled = false;
|
|
729
|
+
const h = setTimeout(async () => {
|
|
730
|
+
const r = await compileDeck(source);
|
|
731
|
+
if (cancelled) return;
|
|
732
|
+
if (r.ok) {
|
|
733
|
+
setPreview({ Content: r.Content, meta: r.meta });
|
|
734
|
+
setError(null);
|
|
735
|
+
} else {
|
|
736
|
+
setError(r.error);
|
|
737
|
+
}
|
|
738
|
+
}, debounceMs);
|
|
739
|
+
return () => {
|
|
740
|
+
cancelled = true;
|
|
741
|
+
clearTimeout(h);
|
|
742
|
+
};
|
|
743
|
+
}, [source, debounceMs]);
|
|
744
|
+
const deck = useMemo(
|
|
745
|
+
() => preview ? { slug: "draft", meta: preview.meta, content: preview.Content } : null,
|
|
746
|
+
[preview]
|
|
747
|
+
);
|
|
748
|
+
useEffect(() => {
|
|
749
|
+
if (!editable) return;
|
|
750
|
+
const onKey = (e) => {
|
|
751
|
+
const typing = e.target?.tagName === "TEXTAREA";
|
|
752
|
+
if (e.key === "e" && !typing && mode === "present") setMode("edit");
|
|
753
|
+
if (e.key === "Escape" && mode === "edit") setMode("present");
|
|
754
|
+
};
|
|
755
|
+
window.addEventListener("keydown", onKey);
|
|
756
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
757
|
+
}, [editable, mode]);
|
|
758
|
+
if (editable && mode === "edit") {
|
|
759
|
+
return /* @__PURE__ */ jsx$1(
|
|
760
|
+
EditPane,
|
|
761
|
+
{
|
|
762
|
+
source,
|
|
763
|
+
onChange: setSource,
|
|
764
|
+
onSave,
|
|
765
|
+
deck,
|
|
766
|
+
error,
|
|
767
|
+
onPresent: () => setMode("present")
|
|
768
|
+
}
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
return /* @__PURE__ */ jsxs("div", { className: "a63-surface", children: [
|
|
772
|
+
editable && /* @__PURE__ */ jsx$1("button", { type: "button", className: "a63-surface__edit", onClick: () => setMode("edit"), children: "Edit" }),
|
|
773
|
+
deck ? /* @__PURE__ */ jsx$1(SlidesPlayer, { deck, onBack: () => {
|
|
774
|
+
} }) : /* @__PURE__ */ jsx$1("div", { className: "a63-editor__preview-empty", children: error ? "Fix the MDX error to render a preview." : "Compiling preview\u2026" })
|
|
775
|
+
] });
|
|
776
|
+
}
|
|
777
|
+
function DeckEditor({ source, onChange, debounceMs }) {
|
|
778
|
+
return /* @__PURE__ */ jsx$1(
|
|
779
|
+
DeckSurface,
|
|
780
|
+
{
|
|
781
|
+
source,
|
|
782
|
+
onChange: onChange ?? (() => {
|
|
783
|
+
}),
|
|
784
|
+
initialMode: "edit",
|
|
785
|
+
debounceMs
|
|
786
|
+
}
|
|
787
|
+
);
|
|
788
|
+
}
|
|
249
789
|
|
|
250
|
-
export { DeckEditor, compileDeck, stripImports, synthExample, templateSnippets, toInsertSnippet };
|
|
790
|
+
export { DeckEditor, DeckSurface, compileDeck, stripImports, synthExample, templateSnippets, toInsertSnippet };
|
|
251
791
|
//# sourceMappingURL=index.js.map
|
|
252
792
|
//# sourceMappingURL=index.js.map
|