@hachej/boring-deck 0.1.20
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 +265 -0
- package/dist/front/index.d.ts +42 -0
- package/dist/front/index.js +1159 -0
- package/dist/shared/index.d.ts +17 -0
- package/dist/shared/index.js +265 -0
- package/dist/types-Dt_A9RNg.d.ts +57 -0
- package/package.json +78 -0
- package/skills/deck-authoring/SKILL.md +73 -0
|
@@ -0,0 +1,1159 @@
|
|
|
1
|
+
// src/front/index.tsx
|
|
2
|
+
import {
|
|
3
|
+
WorkspaceFilesProvider,
|
|
4
|
+
useHasWorkspaceFilesProvider
|
|
5
|
+
} from "@hachej/boring-workspace";
|
|
6
|
+
import { definePlugin } from "@hachej/boring-workspace/plugin";
|
|
7
|
+
|
|
8
|
+
// src/shared/constants.ts
|
|
9
|
+
var DECK_PLUGIN_ID = "deck";
|
|
10
|
+
var DECK_PANEL_ID = "deck";
|
|
11
|
+
var DECK_LABEL = "Deck";
|
|
12
|
+
var DECK_PATH_PREFIX = "deck/";
|
|
13
|
+
|
|
14
|
+
// src/shared/path.ts
|
|
15
|
+
function normalizeDeckPath(path) {
|
|
16
|
+
const trimmed = path.trim().replace(/\\/g, "/");
|
|
17
|
+
const noLeadingDot = trimmed.replace(/^\.\//, "");
|
|
18
|
+
return noLeadingDot.replace(/\/+/g, "/");
|
|
19
|
+
}
|
|
20
|
+
function isDeckMarkdownPath(path, pathPrefix = "deck/") {
|
|
21
|
+
const normalized = normalizeDeckPath(path);
|
|
22
|
+
const normalizedPathPrefix = normalizeDeckPath(pathPrefix);
|
|
23
|
+
const normalizedPrefix = normalizedPathPrefix.endsWith("/") ? normalizedPathPrefix : `${normalizedPathPrefix}/`;
|
|
24
|
+
if (!normalized.endsWith(".md")) return false;
|
|
25
|
+
if (normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized)) return false;
|
|
26
|
+
if (normalized.split("/").some((segment) => segment === "..")) return false;
|
|
27
|
+
return normalized.startsWith(normalizedPrefix);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/shared/parser.ts
|
|
31
|
+
var TITLE_RX = /^##\s+title:\s*(.+)$/i;
|
|
32
|
+
var YAML_TITLE_RX = /^title:\s*(.+)$/i;
|
|
33
|
+
var YAML_KV_RX = /^[A-Za-z][\w-]*:\s*.*$/;
|
|
34
|
+
var FENCE_RX = /^(```|~~~)/;
|
|
35
|
+
var WIDGET_OPEN = "{{";
|
|
36
|
+
var WIDGET_CLOSE = "}}";
|
|
37
|
+
var WIDGET_NAME_RX = /^[A-Za-z][\w-]*$/;
|
|
38
|
+
var ATTR_KEY_RX = /^[A-Za-z][\w-]*$/;
|
|
39
|
+
function splitSlides(input) {
|
|
40
|
+
const slides = [];
|
|
41
|
+
let current = [];
|
|
42
|
+
let fenceMarker = null;
|
|
43
|
+
for (const line of input.split("\n")) {
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
const fence = FENCE_RX.exec(trimmed);
|
|
46
|
+
if (fence) {
|
|
47
|
+
if (fenceMarker === fence[1]) fenceMarker = null;
|
|
48
|
+
else if (fenceMarker === null) fenceMarker = fence[1];
|
|
49
|
+
}
|
|
50
|
+
if (fenceMarker === null && trimmed === "---") {
|
|
51
|
+
slides.push(current.join("\n").trim());
|
|
52
|
+
current = [];
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
current.push(line);
|
|
56
|
+
}
|
|
57
|
+
slides.push(current.join("\n").trim());
|
|
58
|
+
const nonEmpty = slides.filter((slide) => slide.length > 0);
|
|
59
|
+
if (nonEmpty.length > 0) return nonEmpty;
|
|
60
|
+
return [""];
|
|
61
|
+
}
|
|
62
|
+
function parseWidgetAttrs(raw) {
|
|
63
|
+
const parsed = tryParseWidgetAttrs(raw);
|
|
64
|
+
if (parsed == null) throw new Error(`Malformed widget attrs: ${raw}`);
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
function parseDeckMarkdown(input) {
|
|
68
|
+
const lines = input.split("\n");
|
|
69
|
+
let start = 0;
|
|
70
|
+
while (start < lines.length && lines[start].trim() === "") start += 1;
|
|
71
|
+
let title = stripYamlFrontmatter(lines, start);
|
|
72
|
+
while (start < lines.length && lines[start].trim() === "") start += 1;
|
|
73
|
+
if (!title && lines[start]?.trim() === "---") {
|
|
74
|
+
lines.splice(start, 1);
|
|
75
|
+
}
|
|
76
|
+
let sawFence = false;
|
|
77
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
78
|
+
const trimmed = lines[i].trim();
|
|
79
|
+
if (FENCE_RX.test(trimmed)) sawFence = !sawFence;
|
|
80
|
+
if (!sawFence && trimmed === "---") break;
|
|
81
|
+
const match = TITLE_RX.exec(trimmed);
|
|
82
|
+
if (match) {
|
|
83
|
+
title = title ?? stripQuotes(match[1].trim());
|
|
84
|
+
lines.splice(i, 1);
|
|
85
|
+
if (lines[i]?.trim() === "") lines.splice(i, 1);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const slides = splitSlides(lines.join("\n")).map((raw, index, all) => ({
|
|
90
|
+
index,
|
|
91
|
+
raw,
|
|
92
|
+
segments: tokenize(raw, index, all.length)
|
|
93
|
+
}));
|
|
94
|
+
return title ? { title, slides } : { slides };
|
|
95
|
+
}
|
|
96
|
+
function stripYamlFrontmatter(lines, start) {
|
|
97
|
+
if (lines[start]?.trim() !== "---") return void 0;
|
|
98
|
+
let end = -1;
|
|
99
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
100
|
+
if (lines[i].trim() === "---") {
|
|
101
|
+
end = i;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (end === -1) return void 0;
|
|
106
|
+
const fields = lines.slice(start + 1, end);
|
|
107
|
+
if (fields.length === 0 || !fields.every((line) => YAML_KV_RX.test(line.trim()))) return void 0;
|
|
108
|
+
const titleLine = fields.find((line) => YAML_TITLE_RX.test(line.trim()));
|
|
109
|
+
const title = titleLine?.trim().match(YAML_TITLE_RX)?.[1]?.trim();
|
|
110
|
+
lines.splice(start, end - start + 1);
|
|
111
|
+
while (start < lines.length && lines[start]?.trim() === "") lines.splice(start, 1);
|
|
112
|
+
return title ? stripQuotes(title) : void 0;
|
|
113
|
+
}
|
|
114
|
+
function tokenize(markdown, slideIndex, slideCount) {
|
|
115
|
+
const segments = [];
|
|
116
|
+
const lines = markdown.split("\n");
|
|
117
|
+
let fenceMarker = null;
|
|
118
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
119
|
+
const line = lines[lineIndex];
|
|
120
|
+
const trimmed = line.trim();
|
|
121
|
+
const fence = FENCE_RX.exec(trimmed);
|
|
122
|
+
if (fence) {
|
|
123
|
+
if (fenceMarker === fence[1]) fenceMarker = null;
|
|
124
|
+
else if (fenceMarker === null) fenceMarker = fence[1];
|
|
125
|
+
pushMarkdown(segments, line);
|
|
126
|
+
if (lineIndex < lines.length - 1) pushMarkdown(segments, "\n");
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (fenceMarker) {
|
|
130
|
+
pushMarkdown(segments, line);
|
|
131
|
+
if (lineIndex < lines.length - 1) pushMarkdown(segments, "\n");
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
for (const segment of tokenizeLine(line, slideIndex, slideCount)) {
|
|
135
|
+
if (segment.type === "markdown") pushMarkdown(segments, segment.text);
|
|
136
|
+
else segments.push(segment);
|
|
137
|
+
}
|
|
138
|
+
if (lineIndex < lines.length - 1) pushMarkdown(segments, "\n");
|
|
139
|
+
}
|
|
140
|
+
return segments.length > 0 ? segments : [{ type: "markdown", text: "" }];
|
|
141
|
+
}
|
|
142
|
+
function tokenizeLine(line, _slideIndex, _slideCount) {
|
|
143
|
+
const segments = [];
|
|
144
|
+
let markdown = "";
|
|
145
|
+
let i = 0;
|
|
146
|
+
while (i < line.length) {
|
|
147
|
+
if (line[i] === "`") {
|
|
148
|
+
const tickCount = countRepeated(line, i, "`");
|
|
149
|
+
const close = line.indexOf("`".repeat(tickCount), i + tickCount);
|
|
150
|
+
if (close === -1) {
|
|
151
|
+
markdown += line.slice(i);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
markdown += line.slice(i, close + tickCount);
|
|
155
|
+
i = close + tickCount;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (line.startsWith(WIDGET_OPEN, i)) {
|
|
159
|
+
const close = line.indexOf(WIDGET_CLOSE, i + WIDGET_OPEN.length);
|
|
160
|
+
if (close === -1) {
|
|
161
|
+
markdown += line.slice(i);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
const raw = line.slice(i, close + WIDGET_CLOSE.length);
|
|
165
|
+
const parsed = parseWidget(raw, line);
|
|
166
|
+
if (!parsed) {
|
|
167
|
+
markdown += raw;
|
|
168
|
+
i = close + WIDGET_CLOSE.length;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (markdown) {
|
|
172
|
+
segments.push({ type: "markdown", text: markdown });
|
|
173
|
+
markdown = "";
|
|
174
|
+
}
|
|
175
|
+
segments.push(parsed);
|
|
176
|
+
i = close + WIDGET_CLOSE.length;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
markdown += line[i];
|
|
180
|
+
i += 1;
|
|
181
|
+
}
|
|
182
|
+
if (markdown) segments.push({ type: "markdown", text: markdown });
|
|
183
|
+
return segments;
|
|
184
|
+
}
|
|
185
|
+
function parseWidget(raw, line) {
|
|
186
|
+
const inner = raw.slice(WIDGET_OPEN.length, -WIDGET_CLOSE.length).trim();
|
|
187
|
+
if (!inner) return null;
|
|
188
|
+
const firstSpace = inner.search(/\s/);
|
|
189
|
+
const name = firstSpace === -1 ? inner : inner.slice(0, firstSpace);
|
|
190
|
+
const attrsRaw = firstSpace === -1 ? "" : inner.slice(firstSpace).trim();
|
|
191
|
+
if (!WIDGET_NAME_RX.test(name)) return null;
|
|
192
|
+
let attrs;
|
|
193
|
+
try {
|
|
194
|
+
attrs = attrsRaw ? parseWidgetAttrs(attrsRaw) : {};
|
|
195
|
+
} catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
type: "widget",
|
|
200
|
+
name,
|
|
201
|
+
attrs,
|
|
202
|
+
raw,
|
|
203
|
+
position: line.trim() === raw.trim() ? "block" : "inline"
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function tryParseWidgetAttrs(raw) {
|
|
207
|
+
const attrs = {};
|
|
208
|
+
let i = 0;
|
|
209
|
+
while (i < raw.length) {
|
|
210
|
+
while (i < raw.length && /\s/.test(raw[i])) i += 1;
|
|
211
|
+
if (i >= raw.length) break;
|
|
212
|
+
const keyStart = i;
|
|
213
|
+
while (i < raw.length && /[A-Za-z0-9_-]/.test(raw[i])) i += 1;
|
|
214
|
+
const key = raw.slice(keyStart, i);
|
|
215
|
+
if (!ATTR_KEY_RX.test(key)) return null;
|
|
216
|
+
while (i < raw.length && /\s/.test(raw[i])) i += 1;
|
|
217
|
+
if (raw[i] !== "=") return null;
|
|
218
|
+
i += 1;
|
|
219
|
+
while (i < raw.length && /\s/.test(raw[i])) i += 1;
|
|
220
|
+
if (raw[i] !== '"') return null;
|
|
221
|
+
i += 1;
|
|
222
|
+
let value = "";
|
|
223
|
+
let closed = false;
|
|
224
|
+
while (i < raw.length) {
|
|
225
|
+
const char = raw[i];
|
|
226
|
+
if (char === "\\") {
|
|
227
|
+
const next = raw[i + 1];
|
|
228
|
+
if (next === void 0) return null;
|
|
229
|
+
value += next;
|
|
230
|
+
i += 2;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (char === '"') {
|
|
234
|
+
i += 1;
|
|
235
|
+
closed = true;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
value += char;
|
|
239
|
+
i += 1;
|
|
240
|
+
}
|
|
241
|
+
if (!closed) return null;
|
|
242
|
+
attrs[key] = value;
|
|
243
|
+
}
|
|
244
|
+
while (i < raw.length && /\s/.test(raw[i])) i += 1;
|
|
245
|
+
if (i !== raw.length) return null;
|
|
246
|
+
return attrs;
|
|
247
|
+
}
|
|
248
|
+
function pushMarkdown(segments, text) {
|
|
249
|
+
if (text.length === 0) return;
|
|
250
|
+
const last = segments[segments.length - 1];
|
|
251
|
+
if (last?.type === "markdown") last.text += text;
|
|
252
|
+
else segments.push({ type: "markdown", text });
|
|
253
|
+
}
|
|
254
|
+
function stripQuotes(value) {
|
|
255
|
+
return value.replace(/^['"]|['"]$/g, "");
|
|
256
|
+
}
|
|
257
|
+
function countRepeated(value, start, needle) {
|
|
258
|
+
let count = 0;
|
|
259
|
+
while (value[start + count] === needle) count += 1;
|
|
260
|
+
return count;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/front/DeckPane.tsx
|
|
264
|
+
import {
|
|
265
|
+
cn as cn2,
|
|
266
|
+
MarkdownEditor,
|
|
267
|
+
useFilePane,
|
|
268
|
+
useFullPagePanelHref,
|
|
269
|
+
useIsFullPagePanel
|
|
270
|
+
} from "@hachej/boring-workspace";
|
|
271
|
+
import { Button as Button2 } from "@hachej/boring-ui-kit";
|
|
272
|
+
import { ExternalLink } from "lucide-react";
|
|
273
|
+
import { Fragment, useEffect, useMemo, useState } from "react";
|
|
274
|
+
import ReactMarkdown from "react-markdown";
|
|
275
|
+
import remarkGfm from "remark-gfm";
|
|
276
|
+
|
|
277
|
+
// src/front/components.tsx
|
|
278
|
+
import { cn } from "@hachej/boring-workspace";
|
|
279
|
+
import { Button, SegmentedControl, SegmentedControlItem, Separator } from "@hachej/boring-ui-kit";
|
|
280
|
+
import { ChevronLeft, ChevronRight, Maximize2, Minimize2 } from "lucide-react";
|
|
281
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
282
|
+
function DeckScaffoldState({ children }) {
|
|
283
|
+
return /* @__PURE__ */ jsx("div", { className: "p-4 text-sm text-muted-foreground", children });
|
|
284
|
+
}
|
|
285
|
+
function DeckErrorState({ title, description }) {
|
|
286
|
+
return /* @__PURE__ */ jsx(
|
|
287
|
+
"div",
|
|
288
|
+
{
|
|
289
|
+
className: "flex min-h-0 flex-1 items-center justify-center p-6",
|
|
290
|
+
"data-testid": "deck-error-state",
|
|
291
|
+
children: /* @__PURE__ */ jsxs("div", { className: "max-w-lg rounded-xl border border-destructive/20 bg-destructive/5 p-4 text-sm", children: [
|
|
292
|
+
/* @__PURE__ */ jsx("div", { className: "font-medium text-foreground", children: title }),
|
|
293
|
+
/* @__PURE__ */ jsx("div", { className: "mt-1 text-muted-foreground", children: description })
|
|
294
|
+
] })
|
|
295
|
+
}
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
function DeckShell({ children, theme, presentMode = false }) {
|
|
299
|
+
return /* @__PURE__ */ jsxs(
|
|
300
|
+
"div",
|
|
301
|
+
{
|
|
302
|
+
className: cn(
|
|
303
|
+
"deck-root flex min-h-0 flex-1 flex-col bg-background text-foreground",
|
|
304
|
+
presentMode && "min-h-screen bg-background",
|
|
305
|
+
theme?.className
|
|
306
|
+
),
|
|
307
|
+
"data-testid": presentMode ? "deck-shell-present" : "deck-shell-read",
|
|
308
|
+
children: [
|
|
309
|
+
children,
|
|
310
|
+
/* @__PURE__ */ jsx("style", { children: `
|
|
311
|
+
.deck-root { --deck-accent: oklch(0.62 0.14 65); }
|
|
312
|
+
.dark .deck-root { --deck-accent: oklch(0.76 0.16 68); }
|
|
313
|
+
.deck-slide-frame-shell {
|
|
314
|
+
animation: deck-slide-enter 240ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
315
|
+
}
|
|
316
|
+
@keyframes deck-slide-enter {
|
|
317
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
318
|
+
to { opacity: 1; transform: translateY(0); }
|
|
319
|
+
}
|
|
320
|
+
@media (prefers-reduced-motion: reduce) {
|
|
321
|
+
.deck-slide-frame-shell { animation: none; }
|
|
322
|
+
}
|
|
323
|
+
` })
|
|
324
|
+
]
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
function DeckSlideFrame({ children, theme }) {
|
|
329
|
+
const aspectRatio = theme?.aspectRatio === "4:3" ? "4 / 3" : "16 / 9";
|
|
330
|
+
return /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 items-center justify-center p-3 sm:p-6", children: /* @__PURE__ */ jsx(
|
|
331
|
+
"div",
|
|
332
|
+
{
|
|
333
|
+
className: cn(
|
|
334
|
+
"deck-slide-frame-shell flex w-full max-w-6xl overflow-hidden rounded-2xl border border-border/70 bg-card shadow-[0_1px_0_oklch(from_var(--foreground)_l_c_h/0.04),0_24px_60px_-30px_oklch(from_var(--foreground)_l_c_h/0.45)]",
|
|
335
|
+
theme?.slideClassName
|
|
336
|
+
),
|
|
337
|
+
"data-testid": "deck-slide-frame",
|
|
338
|
+
style: { aspectRatio },
|
|
339
|
+
children: /* @__PURE__ */ jsx("div", { className: "min-h-0 min-w-0 flex-1 overflow-auto p-6 sm:p-10", children })
|
|
340
|
+
}
|
|
341
|
+
) });
|
|
342
|
+
}
|
|
343
|
+
function DeckToolbar({
|
|
344
|
+
title,
|
|
345
|
+
path,
|
|
346
|
+
mode,
|
|
347
|
+
onModeChange,
|
|
348
|
+
presentMode,
|
|
349
|
+
slideIndex,
|
|
350
|
+
slideCount,
|
|
351
|
+
onTogglePresentMode,
|
|
352
|
+
actions
|
|
353
|
+
}) {
|
|
354
|
+
return /* @__PURE__ */ jsxs("header", { className: "flex items-center justify-between gap-3 border-b border-border/60 bg-background/60 px-4 py-2 backdrop-blur-[2px]", children: [
|
|
355
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-baseline gap-3", children: [
|
|
356
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] uppercase tracking-[0.22em] text-[color:var(--deck-accent)]", children: "Deck" }),
|
|
357
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-[13px] font-medium tracking-tight text-foreground", children: title || "Deck" }),
|
|
358
|
+
path ? /* @__PURE__ */ jsx("span", { className: "hidden truncate font-mono text-[11px] text-muted-foreground/70 sm:inline", children: path }) : null
|
|
359
|
+
] }),
|
|
360
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
|
|
361
|
+
onModeChange ? /* @__PURE__ */ jsx(SegmentedControl, { "aria-label": "Deck mode", className: "bg-background", children: ["read", "edit"].map((nextMode) => /* @__PURE__ */ jsx(
|
|
362
|
+
SegmentedControlItem,
|
|
363
|
+
{
|
|
364
|
+
type: "button",
|
|
365
|
+
selected: mode === nextMode,
|
|
366
|
+
onClick: () => onModeChange(nextMode),
|
|
367
|
+
"aria-pressed": mode === nextMode,
|
|
368
|
+
className: "px-2.5 py-1 text-[11px]",
|
|
369
|
+
"data-testid": nextMode === "read" ? "deck-mode-read" : "deck-mode-edit",
|
|
370
|
+
children: nextMode === "read" ? "Read" : "Edit"
|
|
371
|
+
},
|
|
372
|
+
nextMode
|
|
373
|
+
)) }) : null,
|
|
374
|
+
actions,
|
|
375
|
+
slideCount > 0 ? /* @__PURE__ */ jsxs("span", { className: "hidden font-mono text-[11px] tabular-nums tracking-tight text-muted-foreground sm:inline", children: [
|
|
376
|
+
"Slide ",
|
|
377
|
+
slideIndex + 1,
|
|
378
|
+
" of ",
|
|
379
|
+
slideCount
|
|
380
|
+
] }) : null,
|
|
381
|
+
onTogglePresentMode ? /* @__PURE__ */ jsx(
|
|
382
|
+
Button,
|
|
383
|
+
{
|
|
384
|
+
type: "button",
|
|
385
|
+
variant: "ghost",
|
|
386
|
+
size: "icon-xs",
|
|
387
|
+
onClick: onTogglePresentMode,
|
|
388
|
+
"aria-label": presentMode ? "Exit present mode" : "Present",
|
|
389
|
+
title: presentMode ? "Exit present mode" : "Present",
|
|
390
|
+
"data-testid": "deck-toggle-present",
|
|
391
|
+
children: presentMode ? /* @__PURE__ */ jsx(Minimize2, { className: "size-3.5" }) : /* @__PURE__ */ jsx(Maximize2, { className: "size-3.5" })
|
|
392
|
+
}
|
|
393
|
+
) : null
|
|
394
|
+
] })
|
|
395
|
+
] });
|
|
396
|
+
}
|
|
397
|
+
function DeckSlideRail({
|
|
398
|
+
slideIndex,
|
|
399
|
+
slideCount,
|
|
400
|
+
canGoPrevious,
|
|
401
|
+
canGoNext,
|
|
402
|
+
onPrevious,
|
|
403
|
+
onNext,
|
|
404
|
+
onSelect
|
|
405
|
+
}) {
|
|
406
|
+
if (slideCount <= 1) return null;
|
|
407
|
+
return /* @__PURE__ */ jsxs("footer", { className: "flex shrink-0 items-center gap-3 border-t border-border/60 bg-background/60 px-4 py-2", children: [
|
|
408
|
+
/* @__PURE__ */ jsxs(
|
|
409
|
+
Button,
|
|
410
|
+
{
|
|
411
|
+
type: "button",
|
|
412
|
+
variant: "ghost",
|
|
413
|
+
size: "xs",
|
|
414
|
+
onClick: onPrevious,
|
|
415
|
+
disabled: !canGoPrevious,
|
|
416
|
+
"aria-label": "Previous slide",
|
|
417
|
+
"data-testid": "deck-prev",
|
|
418
|
+
children: [
|
|
419
|
+
/* @__PURE__ */ jsx(ChevronLeft, { className: "size-3.5" }),
|
|
420
|
+
"Prev"
|
|
421
|
+
]
|
|
422
|
+
}
|
|
423
|
+
),
|
|
424
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-1 items-center justify-center gap-2.5", children: [
|
|
425
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-[11px] tabular-nums tracking-tight text-foreground", children: String(slideIndex + 1).padStart(2, "0") }),
|
|
426
|
+
/* @__PURE__ */ jsx(
|
|
427
|
+
"div",
|
|
428
|
+
{
|
|
429
|
+
role: "group",
|
|
430
|
+
"aria-label": "Slide navigation",
|
|
431
|
+
className: "flex max-w-[60%] flex-1 items-center gap-0.5 overflow-x-auto",
|
|
432
|
+
children: Array.from({ length: slideCount }, (_, index) => {
|
|
433
|
+
const isActive = index === slideIndex;
|
|
434
|
+
const label = `Slide ${index + 1}`;
|
|
435
|
+
return /* @__PURE__ */ jsxs(
|
|
436
|
+
Button,
|
|
437
|
+
{
|
|
438
|
+
type: "button",
|
|
439
|
+
variant: "ghost",
|
|
440
|
+
size: "icon-xs",
|
|
441
|
+
"aria-current": isActive ? "true" : void 0,
|
|
442
|
+
"aria-label": label,
|
|
443
|
+
onClick: () => onSelect?.(index),
|
|
444
|
+
className: "group flex flex-1 min-w-[28px] max-w-[56px] cursor-pointer items-center justify-center px-0.5 py-2.5",
|
|
445
|
+
children: [
|
|
446
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: label }),
|
|
447
|
+
/* @__PURE__ */ jsx(
|
|
448
|
+
"span",
|
|
449
|
+
{
|
|
450
|
+
"aria-hidden": true,
|
|
451
|
+
className: cn(
|
|
452
|
+
"block h-1 w-full rounded-full transition-colors",
|
|
453
|
+
isActive ? "bg-[color:var(--deck-accent)]" : "bg-border group-hover:bg-muted-foreground/40 group-focus-visible:bg-muted-foreground/40"
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
)
|
|
457
|
+
]
|
|
458
|
+
},
|
|
459
|
+
index
|
|
460
|
+
);
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
),
|
|
464
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-[11px] tabular-nums tracking-tight text-muted-foreground", children: String(slideCount).padStart(2, "0") })
|
|
465
|
+
] }),
|
|
466
|
+
/* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "!h-5" }),
|
|
467
|
+
/* @__PURE__ */ jsxs(
|
|
468
|
+
Button,
|
|
469
|
+
{
|
|
470
|
+
type: "button",
|
|
471
|
+
variant: "ghost",
|
|
472
|
+
size: "xs",
|
|
473
|
+
onClick: onNext,
|
|
474
|
+
disabled: !canGoNext,
|
|
475
|
+
"aria-label": "Next slide",
|
|
476
|
+
"data-testid": "deck-next",
|
|
477
|
+
children: [
|
|
478
|
+
"Next",
|
|
479
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "size-3.5" })
|
|
480
|
+
]
|
|
481
|
+
}
|
|
482
|
+
)
|
|
483
|
+
] });
|
|
484
|
+
}
|
|
485
|
+
function DeckNotice({
|
|
486
|
+
title,
|
|
487
|
+
description,
|
|
488
|
+
actions,
|
|
489
|
+
tone = "warning",
|
|
490
|
+
testId
|
|
491
|
+
}) {
|
|
492
|
+
return /* @__PURE__ */ jsxs(
|
|
493
|
+
"div",
|
|
494
|
+
{
|
|
495
|
+
className: cn(
|
|
496
|
+
"mx-3 mt-3 rounded-xl border p-3 text-sm",
|
|
497
|
+
tone === "error" ? "border-destructive/20 bg-destructive/5" : "border-amber-500/20 bg-amber-500/5"
|
|
498
|
+
),
|
|
499
|
+
"data-testid": testId,
|
|
500
|
+
children: [
|
|
501
|
+
/* @__PURE__ */ jsx("div", { className: "font-medium text-foreground", children: title }),
|
|
502
|
+
/* @__PURE__ */ jsx("div", { className: "mt-1 text-muted-foreground", children: description }),
|
|
503
|
+
actions ? /* @__PURE__ */ jsx("div", { className: "mt-3 flex items-center gap-2", children: actions }) : null
|
|
504
|
+
]
|
|
505
|
+
}
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/front/widgets.tsx
|
|
510
|
+
import { Component } from "react";
|
|
511
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
512
|
+
function validateDeckWidgets(widgets) {
|
|
513
|
+
const seen = /* @__PURE__ */ new Set();
|
|
514
|
+
for (const widget of widgets) {
|
|
515
|
+
if (seen.has(widget.name)) {
|
|
516
|
+
throw new Error(`Duplicate deck widget name: ${widget.name}`);
|
|
517
|
+
}
|
|
518
|
+
seen.add(widget.name);
|
|
519
|
+
}
|
|
520
|
+
return widgets;
|
|
521
|
+
}
|
|
522
|
+
function indexDeckWidgets(widgets) {
|
|
523
|
+
const indexed = /* @__PURE__ */ new Map();
|
|
524
|
+
for (const widget of validateDeckWidgets(widgets)) indexed.set(widget.name, widget);
|
|
525
|
+
return {
|
|
526
|
+
get(name) {
|
|
527
|
+
return indexed.get(name);
|
|
528
|
+
},
|
|
529
|
+
entries() {
|
|
530
|
+
return indexed.entries();
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function DeckWidgetSlot({ segment, widgets, context }) {
|
|
535
|
+
const widget = widgets.get(segment.name);
|
|
536
|
+
if (!widget) {
|
|
537
|
+
return /* @__PURE__ */ jsx2(DeckWidgetPlaceholder, { name: segment.name, position: segment.position, reason: "Unknown widget" });
|
|
538
|
+
}
|
|
539
|
+
const display = widget.display ?? segment.position;
|
|
540
|
+
try {
|
|
541
|
+
const attrs = widget.parse ? widget.parse(segment.attrs) : segment.attrs;
|
|
542
|
+
return /* @__PURE__ */ jsx2(DeckWidgetErrorBoundary, { name: segment.name, position: display, children: /* @__PURE__ */ jsx2(DeckWidgetFrame, { position: display, children: widget.render({ attrs, rawAttrs: segment.attrs, context }) }) });
|
|
543
|
+
} catch (error) {
|
|
544
|
+
return /* @__PURE__ */ jsx2(
|
|
545
|
+
DeckWidgetPlaceholder,
|
|
546
|
+
{
|
|
547
|
+
name: segment.name,
|
|
548
|
+
position: display,
|
|
549
|
+
reason: error instanceof Error ? error.message : "Widget failed"
|
|
550
|
+
}
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function DeckWidgetFrame({ position, children }) {
|
|
555
|
+
if (position === "inline") {
|
|
556
|
+
return /* @__PURE__ */ jsx2("span", { "data-testid": "deck-widget-inline", children });
|
|
557
|
+
}
|
|
558
|
+
return /* @__PURE__ */ jsx2("div", { "data-testid": "deck-widget-block", children });
|
|
559
|
+
}
|
|
560
|
+
function DeckWidgetPlaceholder({
|
|
561
|
+
name,
|
|
562
|
+
position,
|
|
563
|
+
reason
|
|
564
|
+
}) {
|
|
565
|
+
const content = /* @__PURE__ */ jsxs2("span", { className: "text-xs text-muted-foreground", "data-testid": "deck-widget-placeholder", children: [
|
|
566
|
+
reason,
|
|
567
|
+
": ",
|
|
568
|
+
name
|
|
569
|
+
] });
|
|
570
|
+
return position === "inline" ? /* @__PURE__ */ jsx2("span", { children: content }) : /* @__PURE__ */ jsx2("div", { children: content });
|
|
571
|
+
}
|
|
572
|
+
var DeckWidgetErrorBoundary = class extends Component {
|
|
573
|
+
state = { error: null };
|
|
574
|
+
static getDerivedStateFromError(error) {
|
|
575
|
+
return { error };
|
|
576
|
+
}
|
|
577
|
+
componentDidCatch(_error, _info) {
|
|
578
|
+
}
|
|
579
|
+
componentDidUpdate(prevProps) {
|
|
580
|
+
if (this.state.error && prevProps.children !== this.props.children) {
|
|
581
|
+
this.setState({ error: null });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
render() {
|
|
585
|
+
if (this.state.error) {
|
|
586
|
+
return /* @__PURE__ */ jsx2(
|
|
587
|
+
DeckWidgetPlaceholder,
|
|
588
|
+
{
|
|
589
|
+
name: this.props.name,
|
|
590
|
+
position: this.props.position,
|
|
591
|
+
reason: this.state.error.message || "Widget failed"
|
|
592
|
+
}
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
return this.props.children;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// src/front/DeckPane.tsx
|
|
600
|
+
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
601
|
+
var DEFAULT_SCAFFOLD_CONTENT = `# Deck scaffold
|
|
602
|
+
|
|
603
|
+
Deck rendering shell is ready.`;
|
|
604
|
+
function useDeckKeyboardNavigation({
|
|
605
|
+
enabled,
|
|
606
|
+
canGoPrevious,
|
|
607
|
+
canGoNext,
|
|
608
|
+
onPrevious,
|
|
609
|
+
onNext
|
|
610
|
+
}) {
|
|
611
|
+
useEffect(() => {
|
|
612
|
+
if (!enabled) return;
|
|
613
|
+
const onKeyDown = (event) => {
|
|
614
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
615
|
+
const target = event.target;
|
|
616
|
+
if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if ((event.key === "ArrowRight" || event.key === "PageDown") && canGoNext) {
|
|
620
|
+
event.preventDefault();
|
|
621
|
+
onNext();
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if ((event.key === "ArrowLeft" || event.key === "PageUp") && canGoPrevious) {
|
|
625
|
+
event.preventDefault();
|
|
626
|
+
onPrevious();
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
window.addEventListener("keydown", onKeyDown);
|
|
630
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
631
|
+
}, [enabled, canGoNext, canGoPrevious, onNext, onPrevious]);
|
|
632
|
+
}
|
|
633
|
+
function DeckPane(props) {
|
|
634
|
+
if (props.content === void 0 && props.params?.path) {
|
|
635
|
+
return /* @__PURE__ */ jsx3(FileBackedDeckPane, { ...props });
|
|
636
|
+
}
|
|
637
|
+
return /* @__PURE__ */ jsx3(
|
|
638
|
+
DeckRenderedPane,
|
|
639
|
+
{
|
|
640
|
+
...props,
|
|
641
|
+
content: props.content ?? DEFAULT_SCAFFOLD_CONTENT,
|
|
642
|
+
initialMode: props.initialMode === "edit" ? "read" : props.initialMode ?? "read"
|
|
643
|
+
}
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
function DeckRenderedPane({
|
|
647
|
+
params,
|
|
648
|
+
content,
|
|
649
|
+
pathPrefix = "deck/",
|
|
650
|
+
theme,
|
|
651
|
+
widgets = [],
|
|
652
|
+
onError,
|
|
653
|
+
initialMode = "read"
|
|
654
|
+
}) {
|
|
655
|
+
const isFullPagePanel = useIsFullPagePanel();
|
|
656
|
+
const resolvedInitialMode = isFullPagePanel ? "present" : initialMode;
|
|
657
|
+
const [mode, setMode] = useState(resolvedInitialMode);
|
|
658
|
+
const [slideIndex, setSlideIndex] = useState(0);
|
|
659
|
+
const indexedWidgets = useMemo(() => indexDeckWidgets(widgets), [widgets]);
|
|
660
|
+
const parsed = useMemo(() => parseDeckContent(content, params?.path), [content, params?.path]);
|
|
661
|
+
useEffect(() => {
|
|
662
|
+
if (!parsed.ok) onError?.(parsed.error);
|
|
663
|
+
}, [onError, parsed]);
|
|
664
|
+
useEffect(() => {
|
|
665
|
+
setMode(resolvedInitialMode);
|
|
666
|
+
}, [resolvedInitialMode]);
|
|
667
|
+
useEffect(() => {
|
|
668
|
+
setSlideIndex(0);
|
|
669
|
+
}, [content, params?.path]);
|
|
670
|
+
if (!content) {
|
|
671
|
+
return /* @__PURE__ */ jsx3(DeckShell, { theme, presentMode: mode === "present", children: /* @__PURE__ */ jsxs3(DeckScaffoldState, { children: [
|
|
672
|
+
"Deck shell for ",
|
|
673
|
+
pathPrefix,
|
|
674
|
+
params?.path ? ` (${params.path})` : ""
|
|
675
|
+
] }) });
|
|
676
|
+
}
|
|
677
|
+
if (!parsed.ok) {
|
|
678
|
+
return /* @__PURE__ */ jsx3(DeckShell, { theme, presentMode: mode === "present", children: /* @__PURE__ */ jsx3(DeckErrorState, { title: "Failed to render deck", description: parsed.error.message }) });
|
|
679
|
+
}
|
|
680
|
+
const slides = parsed.deck.slides;
|
|
681
|
+
const safeIndex = Math.min(Math.max(slideIndex, 0), Math.max(slides.length - 1, 0));
|
|
682
|
+
const currentSlide = slides[safeIndex];
|
|
683
|
+
const title = parsed.deck.title ?? params?.path ?? "Deck";
|
|
684
|
+
const presentMode = mode === "present";
|
|
685
|
+
const hidePresentationControls = shouldHidePresentationControls({
|
|
686
|
+
isFullPagePanel,
|
|
687
|
+
params,
|
|
688
|
+
presentMode
|
|
689
|
+
});
|
|
690
|
+
useDeckKeyboardNavigation({
|
|
691
|
+
enabled: true,
|
|
692
|
+
canGoPrevious: safeIndex > 0,
|
|
693
|
+
canGoNext: safeIndex < slides.length - 1,
|
|
694
|
+
onPrevious: () => setSlideIndex((current) => Math.max(current - 1, 0)),
|
|
695
|
+
onNext: () => setSlideIndex((current) => Math.min(current + 1, slides.length - 1))
|
|
696
|
+
});
|
|
697
|
+
return /* @__PURE__ */ jsxs3(DeckShell, { theme, presentMode, children: [
|
|
698
|
+
!hidePresentationControls ? /* @__PURE__ */ jsx3(
|
|
699
|
+
DeckToolbar,
|
|
700
|
+
{
|
|
701
|
+
title,
|
|
702
|
+
path: params?.path,
|
|
703
|
+
presentMode,
|
|
704
|
+
slideIndex: safeIndex,
|
|
705
|
+
slideCount: slides.length,
|
|
706
|
+
onTogglePresentMode: () => setMode((current) => current === "present" ? "read" : "present")
|
|
707
|
+
}
|
|
708
|
+
) : null,
|
|
709
|
+
/* @__PURE__ */ jsx3(DeckSlideFrame, { theme, children: /* @__PURE__ */ jsx3(
|
|
710
|
+
"article",
|
|
711
|
+
{
|
|
712
|
+
className: cn2("prose prose-slate max-w-none dark:prose-invert", presentMode && "text-base"),
|
|
713
|
+
"data-testid": "deck-slide-content",
|
|
714
|
+
children: /* @__PURE__ */ jsx3(
|
|
715
|
+
DeckSlideContent,
|
|
716
|
+
{
|
|
717
|
+
slide: currentSlide,
|
|
718
|
+
slideCount: slides.length,
|
|
719
|
+
path: params?.path,
|
|
720
|
+
mode,
|
|
721
|
+
widgets: indexedWidgets
|
|
722
|
+
}
|
|
723
|
+
)
|
|
724
|
+
}
|
|
725
|
+
) }),
|
|
726
|
+
!hidePresentationControls ? /* @__PURE__ */ jsx3(
|
|
727
|
+
DeckSlideRail,
|
|
728
|
+
{
|
|
729
|
+
slideIndex: safeIndex,
|
|
730
|
+
slideCount: slides.length,
|
|
731
|
+
canGoPrevious: safeIndex > 0,
|
|
732
|
+
canGoNext: safeIndex < slides.length - 1,
|
|
733
|
+
onPrevious: () => setSlideIndex((current) => Math.max(current - 1, 0)),
|
|
734
|
+
onNext: () => setSlideIndex((current) => Math.min(current + 1, slides.length - 1)),
|
|
735
|
+
onSelect: setSlideIndex
|
|
736
|
+
}
|
|
737
|
+
) : null
|
|
738
|
+
] });
|
|
739
|
+
}
|
|
740
|
+
function FileBackedDeckPane({
|
|
741
|
+
params,
|
|
742
|
+
api,
|
|
743
|
+
theme,
|
|
744
|
+
widgets = [],
|
|
745
|
+
onError,
|
|
746
|
+
initialMode = "read",
|
|
747
|
+
getPresentHref
|
|
748
|
+
}) {
|
|
749
|
+
const path = params?.path ?? "";
|
|
750
|
+
const hasSelectedPath = /\S/.test(path);
|
|
751
|
+
const isFullPagePanel = useIsFullPagePanel();
|
|
752
|
+
const resolvedInitialMode = isFullPagePanel ? "present" : initialMode;
|
|
753
|
+
const [mode, setMode] = useState(resolvedInitialMode);
|
|
754
|
+
const [slideIndex, setSlideIndex] = useState(0);
|
|
755
|
+
const indexedWidgets = useMemo(() => indexDeckWidgets(widgets), [widgets]);
|
|
756
|
+
const {
|
|
757
|
+
content,
|
|
758
|
+
conflict,
|
|
759
|
+
error,
|
|
760
|
+
fileName,
|
|
761
|
+
isLoading,
|
|
762
|
+
onOverwrite,
|
|
763
|
+
onReloadFromServer,
|
|
764
|
+
setContent,
|
|
765
|
+
tabTitle
|
|
766
|
+
} = useFilePane({ path, panelId: api?.id });
|
|
767
|
+
const parsed = useMemo(() => content == null ? null : parseDeckContent(content, path), [content, path]);
|
|
768
|
+
useEffect(() => {
|
|
769
|
+
if (error) {
|
|
770
|
+
onError?.({
|
|
771
|
+
type: "storage",
|
|
772
|
+
path,
|
|
773
|
+
message: error.message,
|
|
774
|
+
cause: error
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}, [error, onError, path]);
|
|
778
|
+
useEffect(() => {
|
|
779
|
+
if (conflict) {
|
|
780
|
+
onError?.({
|
|
781
|
+
type: "conflict",
|
|
782
|
+
path,
|
|
783
|
+
message: conflict.message,
|
|
784
|
+
cause: conflict
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
}, [conflict, onError, path]);
|
|
788
|
+
useEffect(() => {
|
|
789
|
+
if (parsed && !parsed.ok) onError?.(parsed.error);
|
|
790
|
+
}, [onError, parsed]);
|
|
791
|
+
useEffect(() => {
|
|
792
|
+
setMode(resolvedInitialMode);
|
|
793
|
+
}, [resolvedInitialMode]);
|
|
794
|
+
useEffect(() => {
|
|
795
|
+
setSlideIndex(0);
|
|
796
|
+
}, [content, path]);
|
|
797
|
+
useEffect(() => {
|
|
798
|
+
if (api && tabTitle) {
|
|
799
|
+
api.setTitle(tabTitle);
|
|
800
|
+
}
|
|
801
|
+
}, [api, tabTitle]);
|
|
802
|
+
const deck = parsed && parsed.ok ? parsed.deck : null;
|
|
803
|
+
const fallbackContent = content ?? "";
|
|
804
|
+
const slides = deck?.slides ?? [{ index: 0, raw: fallbackContent, segments: [{ type: "markdown", text: fallbackContent }] }];
|
|
805
|
+
const slideCount = slides.length;
|
|
806
|
+
const safeIndex = Math.min(Math.max(slideIndex, 0), Math.max(slideCount - 1, 0));
|
|
807
|
+
const currentSlide = slides[safeIndex];
|
|
808
|
+
const title = deck?.title ?? fileName ?? path;
|
|
809
|
+
const canNavigateSlides = mode !== "edit";
|
|
810
|
+
const fullPageHref = useFullPagePanelHref({
|
|
811
|
+
componentId: DECK_PANEL_ID,
|
|
812
|
+
params: path ? { path } : void 0
|
|
813
|
+
});
|
|
814
|
+
const presentHref = isFullPagePanel ? null : path ? getPresentHref?.(path) ?? fullPageHref : null;
|
|
815
|
+
const hidePresentationControls = shouldHidePresentationControls({
|
|
816
|
+
isFullPagePanel,
|
|
817
|
+
params,
|
|
818
|
+
presentMode: mode === "present"
|
|
819
|
+
});
|
|
820
|
+
useDeckKeyboardNavigation({
|
|
821
|
+
enabled: hasSelectedPath && !error && content != null && mode !== "edit",
|
|
822
|
+
canGoPrevious: safeIndex > 0,
|
|
823
|
+
canGoNext: safeIndex < slideCount - 1,
|
|
824
|
+
onPrevious: () => setSlideIndex((current) => Math.max(current - 1, 0)),
|
|
825
|
+
onNext: () => setSlideIndex((current) => Math.min(current + 1, slideCount - 1))
|
|
826
|
+
});
|
|
827
|
+
if (!hasSelectedPath) {
|
|
828
|
+
return /* @__PURE__ */ jsx3(DeckShell, { theme, children: /* @__PURE__ */ jsx3(DeckScaffoldState, { children: "No deck file selected." }) });
|
|
829
|
+
}
|
|
830
|
+
if (isLoading && content == null) {
|
|
831
|
+
return /* @__PURE__ */ jsx3(DeckShell, { theme, children: /* @__PURE__ */ jsx3(DeckScaffoldState, { children: "Loading deck\u2026" }) });
|
|
832
|
+
}
|
|
833
|
+
if (error || content == null) {
|
|
834
|
+
return /* @__PURE__ */ jsx3(DeckShell, { theme, children: /* @__PURE__ */ jsx3(DeckErrorState, { title: "Failed to load deck", description: error?.message ?? "Preview unavailable." }) });
|
|
835
|
+
}
|
|
836
|
+
return /* @__PURE__ */ jsxs3(DeckShell, { theme, presentMode: mode === "present", children: [
|
|
837
|
+
!hidePresentationControls ? /* @__PURE__ */ jsx3(
|
|
838
|
+
DeckToolbar,
|
|
839
|
+
{
|
|
840
|
+
title,
|
|
841
|
+
path,
|
|
842
|
+
mode: mode === "present" ? "read" : mode,
|
|
843
|
+
onModeChange: isFullPagePanel ? void 0 : setMode,
|
|
844
|
+
presentMode: mode === "present",
|
|
845
|
+
slideIndex: safeIndex,
|
|
846
|
+
slideCount,
|
|
847
|
+
onTogglePresentMode: mode === "edit" ? void 0 : () => setMode((current) => current === "present" ? "read" : "present"),
|
|
848
|
+
actions: /* @__PURE__ */ jsx3(Fragment2, { children: presentHref ? /* @__PURE__ */ jsx3(
|
|
849
|
+
Button2,
|
|
850
|
+
{
|
|
851
|
+
variant: "ghost",
|
|
852
|
+
size: "icon-xs",
|
|
853
|
+
asChild: true,
|
|
854
|
+
"aria-label": "Open in new tab",
|
|
855
|
+
title: "Open deck in new tab",
|
|
856
|
+
children: /* @__PURE__ */ jsx3("a", { href: presentHref, target: "_blank", rel: "noopener noreferrer", "data-testid": "deck-open-present", children: /* @__PURE__ */ jsx3(ExternalLink, { className: "size-3.5" }) })
|
|
857
|
+
}
|
|
858
|
+
) : null })
|
|
859
|
+
}
|
|
860
|
+
) : null,
|
|
861
|
+
conflict ? /* @__PURE__ */ jsx3(
|
|
862
|
+
DeckNotice,
|
|
863
|
+
{
|
|
864
|
+
title: "Deck changed on disk",
|
|
865
|
+
description: "Choose whether to reload the server version or overwrite it with your current draft.",
|
|
866
|
+
testId: "deck-conflict-notice",
|
|
867
|
+
actions: /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
868
|
+
/* @__PURE__ */ jsx3(
|
|
869
|
+
Button2,
|
|
870
|
+
{
|
|
871
|
+
type: "button",
|
|
872
|
+
variant: "outline",
|
|
873
|
+
size: "xs",
|
|
874
|
+
onClick: () => void onReloadFromServer(),
|
|
875
|
+
"data-testid": "deck-reload",
|
|
876
|
+
children: "Reload"
|
|
877
|
+
}
|
|
878
|
+
),
|
|
879
|
+
/* @__PURE__ */ jsx3(
|
|
880
|
+
Button2,
|
|
881
|
+
{
|
|
882
|
+
type: "button",
|
|
883
|
+
variant: "outline",
|
|
884
|
+
size: "xs",
|
|
885
|
+
onClick: () => void onOverwrite(),
|
|
886
|
+
"data-testid": "deck-overwrite",
|
|
887
|
+
children: "Overwrite"
|
|
888
|
+
}
|
|
889
|
+
)
|
|
890
|
+
] })
|
|
891
|
+
}
|
|
892
|
+
) : null,
|
|
893
|
+
mode === "edit" ? /* @__PURE__ */ jsxs3("div", { className: "min-h-0 flex-1 overflow-hidden", "data-testid": "deck-edit-mode", children: [
|
|
894
|
+
parsed && !parsed.ok ? /* @__PURE__ */ jsx3(
|
|
895
|
+
DeckNotice,
|
|
896
|
+
{
|
|
897
|
+
title: "Deck markdown has parse errors",
|
|
898
|
+
description: parsed.error.message,
|
|
899
|
+
tone: "error",
|
|
900
|
+
testId: "deck-parse-notice"
|
|
901
|
+
}
|
|
902
|
+
) : null,
|
|
903
|
+
/* @__PURE__ */ jsx3(
|
|
904
|
+
MarkdownEditor,
|
|
905
|
+
{
|
|
906
|
+
content,
|
|
907
|
+
onChange: setContent,
|
|
908
|
+
documentPath: path,
|
|
909
|
+
className: "min-h-0 h-full"
|
|
910
|
+
}
|
|
911
|
+
)
|
|
912
|
+
] }) : parsed && !parsed.ok ? /* @__PURE__ */ jsx3(DeckErrorState, { title: "Failed to render deck", description: parsed.error.message }) : /* @__PURE__ */ jsx3(DeckSlideFrame, { theme, children: /* @__PURE__ */ jsx3(
|
|
913
|
+
"article",
|
|
914
|
+
{
|
|
915
|
+
className: cn2("prose prose-slate max-w-none dark:prose-invert", mode === "present" && "text-base"),
|
|
916
|
+
"data-testid": "deck-slide-content",
|
|
917
|
+
children: /* @__PURE__ */ jsx3(
|
|
918
|
+
DeckSlideContent,
|
|
919
|
+
{
|
|
920
|
+
slide: currentSlide,
|
|
921
|
+
slideCount,
|
|
922
|
+
path,
|
|
923
|
+
mode: mode === "present" ? "present" : "read",
|
|
924
|
+
widgets: indexedWidgets
|
|
925
|
+
}
|
|
926
|
+
)
|
|
927
|
+
}
|
|
928
|
+
) }),
|
|
929
|
+
mode !== "edit" && !hidePresentationControls ? /* @__PURE__ */ jsx3(
|
|
930
|
+
DeckSlideRail,
|
|
931
|
+
{
|
|
932
|
+
slideIndex: safeIndex,
|
|
933
|
+
slideCount,
|
|
934
|
+
canGoPrevious: canNavigateSlides && safeIndex > 0,
|
|
935
|
+
canGoNext: canNavigateSlides && safeIndex < slideCount - 1,
|
|
936
|
+
onPrevious: () => setSlideIndex((current) => Math.max(current - 1, 0)),
|
|
937
|
+
onNext: () => setSlideIndex((current) => Math.min(current + 1, slideCount - 1)),
|
|
938
|
+
onSelect: setSlideIndex
|
|
939
|
+
}
|
|
940
|
+
) : null
|
|
941
|
+
] });
|
|
942
|
+
}
|
|
943
|
+
function shouldHidePresentationControls({
|
|
944
|
+
isFullPagePanel,
|
|
945
|
+
params,
|
|
946
|
+
presentMode
|
|
947
|
+
}) {
|
|
948
|
+
if (!presentMode) return false;
|
|
949
|
+
if (params?.showControls === true || params?.controls === "visible") return false;
|
|
950
|
+
if (params?.showControls === false || params?.controls === "hidden") return true;
|
|
951
|
+
return isFullPagePanel;
|
|
952
|
+
}
|
|
953
|
+
function DeckSlideContent({ slide, slideCount, path, mode, widgets }) {
|
|
954
|
+
const blocks = [];
|
|
955
|
+
let inlineRun = [];
|
|
956
|
+
const flushInlineRun = () => {
|
|
957
|
+
if (inlineRun.length === 0) return;
|
|
958
|
+
blocks.push(/* @__PURE__ */ jsx3("p", { children: inlineRun }, `inline-run-${slide.index}-${blocks.length}`));
|
|
959
|
+
inlineRun = [];
|
|
960
|
+
};
|
|
961
|
+
slide.segments.forEach((segment, index) => {
|
|
962
|
+
const context = {
|
|
963
|
+
path,
|
|
964
|
+
slideIndex: slide.index,
|
|
965
|
+
slideCount,
|
|
966
|
+
mode
|
|
967
|
+
};
|
|
968
|
+
if (segment.type === "markdown") {
|
|
969
|
+
if (isInlineCompatibleMarkdown(segment.text)) {
|
|
970
|
+
inlineRun.push(
|
|
971
|
+
/* @__PURE__ */ jsx3(InlineMarkdownSegment, { text: segment.text }, `inline-markdown-${slide.index}-${index}`)
|
|
972
|
+
);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
flushInlineRun();
|
|
976
|
+
blocks.push(/* @__PURE__ */ jsx3(MarkdownSegment, { text: segment.text }, `markdown-${slide.index}-${index}`));
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const widget = widgets.get(segment.name);
|
|
980
|
+
const display = widget?.display ?? segment.position;
|
|
981
|
+
if (display === "inline") {
|
|
982
|
+
inlineRun.push(
|
|
983
|
+
/* @__PURE__ */ jsx3(
|
|
984
|
+
DeckWidgetSlot,
|
|
985
|
+
{
|
|
986
|
+
segment,
|
|
987
|
+
widgets,
|
|
988
|
+
context
|
|
989
|
+
},
|
|
990
|
+
`widget-${slide.index}-${index}`
|
|
991
|
+
)
|
|
992
|
+
);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
flushInlineRun();
|
|
996
|
+
blocks.push(
|
|
997
|
+
/* @__PURE__ */ jsx3(
|
|
998
|
+
DeckWidgetSlot,
|
|
999
|
+
{
|
|
1000
|
+
segment,
|
|
1001
|
+
widgets,
|
|
1002
|
+
context
|
|
1003
|
+
},
|
|
1004
|
+
`widget-${slide.index}-${index}`
|
|
1005
|
+
)
|
|
1006
|
+
);
|
|
1007
|
+
});
|
|
1008
|
+
flushInlineRun();
|
|
1009
|
+
return /* @__PURE__ */ jsx3("div", { className: "space-y-4", "data-testid": `deck-slide-${slide.index}`, children: blocks });
|
|
1010
|
+
}
|
|
1011
|
+
function MarkdownSegment({ text }) {
|
|
1012
|
+
return /* @__PURE__ */ jsx3(ReactMarkdown, { remarkPlugins: [remarkGfm], children: text });
|
|
1013
|
+
}
|
|
1014
|
+
function InlineMarkdownSegment({ text }) {
|
|
1015
|
+
return /* @__PURE__ */ jsx3("span", { className: "whitespace-pre-wrap", children: /* @__PURE__ */ jsx3(
|
|
1016
|
+
ReactMarkdown,
|
|
1017
|
+
{
|
|
1018
|
+
remarkPlugins: [remarkGfm],
|
|
1019
|
+
components: {
|
|
1020
|
+
p: ({ children }) => /* @__PURE__ */ jsx3(Fragment, { children })
|
|
1021
|
+
},
|
|
1022
|
+
children: text
|
|
1023
|
+
}
|
|
1024
|
+
) });
|
|
1025
|
+
}
|
|
1026
|
+
function isInlineCompatibleMarkdown(text) {
|
|
1027
|
+
const trimmed = text.trim();
|
|
1028
|
+
if (!trimmed) return false;
|
|
1029
|
+
if (trimmed.includes("\n\n")) return false;
|
|
1030
|
+
return !/^(#{1,6}\s|[-*+]\s|\d+\.\s|>|```|~~~|\|)|^---$/m.test(trimmed);
|
|
1031
|
+
}
|
|
1032
|
+
function parseDeckContent(content, path) {
|
|
1033
|
+
try {
|
|
1034
|
+
return { ok: true, deck: parseDeckMarkdown(content) };
|
|
1035
|
+
} catch (cause) {
|
|
1036
|
+
return {
|
|
1037
|
+
ok: false,
|
|
1038
|
+
error: {
|
|
1039
|
+
type: "parse",
|
|
1040
|
+
path,
|
|
1041
|
+
message: cause instanceof Error ? cause.message : "Failed to parse deck",
|
|
1042
|
+
cause
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/front/StandaloneDeckRoute.tsx
|
|
1049
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1050
|
+
function StandaloneDeckRoute({ path, content, theme, widgets, onError, getPresentHref }) {
|
|
1051
|
+
return /* @__PURE__ */ jsx4("div", { className: "min-h-screen bg-background text-foreground", children: /* @__PURE__ */ jsx4(
|
|
1052
|
+
DeckPane,
|
|
1053
|
+
{
|
|
1054
|
+
params: path ? { path } : {},
|
|
1055
|
+
content,
|
|
1056
|
+
theme,
|
|
1057
|
+
widgets,
|
|
1058
|
+
onError,
|
|
1059
|
+
getPresentHref,
|
|
1060
|
+
initialMode: "present"
|
|
1061
|
+
}
|
|
1062
|
+
) });
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/front/surfaceResolver.ts
|
|
1066
|
+
import { WORKSPACE_OPEN_PATH_SURFACE_KIND } from "@hachej/boring-workspace/plugin";
|
|
1067
|
+
function basename(path) {
|
|
1068
|
+
const normalized = normalizeDeckPath(path);
|
|
1069
|
+
return normalized.split("/").pop() ?? path;
|
|
1070
|
+
}
|
|
1071
|
+
function createDeckSurfaceResolver(pathPrefix) {
|
|
1072
|
+
const normalizedPrefix = normalizeDeckPath(pathPrefix);
|
|
1073
|
+
return {
|
|
1074
|
+
id: "deck.open-path",
|
|
1075
|
+
kind: WORKSPACE_OPEN_PATH_SURFACE_KIND,
|
|
1076
|
+
source: "app",
|
|
1077
|
+
resolve: (request) => {
|
|
1078
|
+
if (request.kind !== WORKSPACE_OPEN_PATH_SURFACE_KIND) return null;
|
|
1079
|
+
const target = normalizeDeckPath(request.target);
|
|
1080
|
+
if (!isDeckMarkdownPath(target, normalizedPrefix)) return null;
|
|
1081
|
+
return {
|
|
1082
|
+
component: DECK_PANEL_ID,
|
|
1083
|
+
title: basename(target),
|
|
1084
|
+
params: { path: target },
|
|
1085
|
+
score: 100
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
var deckSurfaceResolver = createDeckSurfaceResolver("deck/");
|
|
1091
|
+
|
|
1092
|
+
// src/front/index.tsx
|
|
1093
|
+
import { Fragment as Fragment3, jsx as jsx5 } from "react/jsx-runtime";
|
|
1094
|
+
function DeckFilesProvider({
|
|
1095
|
+
apiBaseUrl,
|
|
1096
|
+
authHeaders,
|
|
1097
|
+
onAuthError,
|
|
1098
|
+
apiTimeout,
|
|
1099
|
+
children
|
|
1100
|
+
}) {
|
|
1101
|
+
const hasWorkspaceFilesProvider = useHasWorkspaceFilesProvider();
|
|
1102
|
+
if (hasWorkspaceFilesProvider) {
|
|
1103
|
+
return /* @__PURE__ */ jsx5(Fragment3, { children });
|
|
1104
|
+
}
|
|
1105
|
+
return /* @__PURE__ */ jsx5(
|
|
1106
|
+
WorkspaceFilesProvider,
|
|
1107
|
+
{
|
|
1108
|
+
apiBaseUrl,
|
|
1109
|
+
authHeaders,
|
|
1110
|
+
onAuthError,
|
|
1111
|
+
timeout: apiTimeout,
|
|
1112
|
+
children
|
|
1113
|
+
}
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
function createDeckPlugin(options = {}) {
|
|
1117
|
+
const pathPrefix = normalizeDeckPath(options.pathPrefix ?? DECK_PATH_PREFIX);
|
|
1118
|
+
validateDeckWidgets(options.widgets ?? []);
|
|
1119
|
+
return definePlugin({
|
|
1120
|
+
id: DECK_PLUGIN_ID,
|
|
1121
|
+
label: DECK_LABEL,
|
|
1122
|
+
providers: [
|
|
1123
|
+
{
|
|
1124
|
+
id: "deck-files",
|
|
1125
|
+
component: DeckFilesProvider
|
|
1126
|
+
}
|
|
1127
|
+
],
|
|
1128
|
+
panels: [
|
|
1129
|
+
{
|
|
1130
|
+
id: DECK_PANEL_ID,
|
|
1131
|
+
label: DECK_LABEL,
|
|
1132
|
+
component: (props) => /* @__PURE__ */ jsx5(
|
|
1133
|
+
DeckPane,
|
|
1134
|
+
{
|
|
1135
|
+
...props,
|
|
1136
|
+
pathPrefix,
|
|
1137
|
+
theme: options.theme,
|
|
1138
|
+
widgets: options.widgets,
|
|
1139
|
+
onError: options.onError
|
|
1140
|
+
}
|
|
1141
|
+
),
|
|
1142
|
+
placement: "center",
|
|
1143
|
+
source: "app",
|
|
1144
|
+
supportsFullPage: true
|
|
1145
|
+
}
|
|
1146
|
+
],
|
|
1147
|
+
surfaceResolvers: [createDeckSurfaceResolver(pathPrefix)]
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
var deckPlugin = createDeckPlugin({ pathPrefix: DECK_PATH_PREFIX });
|
|
1151
|
+
var front_default = deckPlugin;
|
|
1152
|
+
export {
|
|
1153
|
+
DeckPane,
|
|
1154
|
+
StandaloneDeckRoute,
|
|
1155
|
+
createDeckPlugin,
|
|
1156
|
+
createDeckSurfaceResolver,
|
|
1157
|
+
deckSurfaceResolver,
|
|
1158
|
+
front_default as default
|
|
1159
|
+
};
|