@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.
@@ -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, jsx } 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, useRef, useMemo, useCallback, useEffect } from 'react';
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 DeckEditor({ source: sourceProp, onChange, debounceMs = 300 }) {
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 setSource = useCallback(
143
- (next) => {
144
- if (isControlled) onChange(next);
145
- else setInternalSource(next);
146
- },
147
- [isControlled, onChange]
519
+ const handleChange = useCallback(
520
+ (e) => onChange(e.target.value),
521
+ [onChange]
148
522
  );
149
- useEffect(() => {
150
- let cancelled = false;
151
- const handle = setTimeout(async () => {
152
- const result = await compileDeck(source);
153
- if (cancelled) return;
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
- }, debounceMs);
161
- return () => {
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 handleInsertTemplate = useCallback(
532
+ const insert = useCallback(
171
533
  (name) => {
172
534
  const snippet = templateSnippets[name];
173
535
  if (!snippet) return;
174
- const next = `${source.replace(/\s*$/, "")}
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, setSource]
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
- // Remount on theme change so the player picks up the canvas theme.
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__ */ jsx(
207
- "button",
208
- {
209
- type: "button",
210
- className: "a63-editor__theme-toggle",
211
- onClick: () => setTheme((t) => t === "light" ? "dark" : "light"),
212
- "aria-pressed": theme === "dark",
213
- children: theme === "light" ? "\u2600\uFE0E Light" : "\u263E Dark"
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__ */ jsx(
218
- "textarea",
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
- type: "button",
234
- className: "a63-editor__chip",
235
- onClick: () => handleInsertTemplate(t.name),
236
- title: `Insert ${t.name}`,
237
- children: [
238
- /* @__PURE__ */ jsx("span", { className: "a63-editor__chip-name", children: t.name }),
239
- /* @__PURE__ */ jsx("span", { className: "a63-editor__chip-meta", children: t.label })
240
- ]
241
- },
242
- t.name
243
- )) }),
244
- /* @__PURE__ */ jsx("p", { className: "a63-editor__footnote", children: "v0.1 \xB7 plain-textarea editing (CodeMirror is a future upgrade). Per-slide form editing & template-switch are deferred to v2." })
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