@usetheo/ui 0.1.0-next.0 → 0.1.0-next.1
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/CHANGELOG.md +14 -0
- package/README.md +116 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin-Atb0VKtr.d.ts +172 -0
- package/dist/slide/index.d.ts +212 -0
- package/dist/slide/index.js +714 -0
- package/dist/slide/index.js.map +1 -0
- package/dist/slide/plugins/emoji/index.d.ts +29 -0
- package/dist/slide/plugins/emoji/index.js +157 -0
- package/dist/slide/plugins/emoji/index.js.map +1 -0
- package/dist/slide/plugins/math/index.d.ts +13 -0
- package/dist/slide/plugins/math/index.js +145 -0
- package/dist/slide/plugins/math/index.js.map +1 -0
- package/dist/slide/plugins/mermaid/index.d.ts +55 -0
- package/dist/slide/plugins/mermaid/index.js +218 -0
- package/dist/slide/plugins/mermaid/index.js.map +1 -0
- package/dist/slide/plugins/shiki/index.d.ts +18 -0
- package/dist/slide/plugins/shiki/index.js +87 -0
- package/dist/slide/plugins/shiki/index.js.map +1 -0
- package/dist/slide/themes/default.css +256 -0
- package/dist/slide/themes/layouts.css +143 -0
- package/dist/slide/themes/violet-forge.css +256 -0
- package/dist/slide-deck/index.css +52 -0
- package/dist/slide-deck/index.css.map +1 -0
- package/dist/slide-deck/index.d.ts +377 -0
- package/dist/slide-deck/index.js +1797 -0
- package/dist/slide-deck/index.js.map +1 -0
- package/dist/whiteboard/index.d.ts +258 -0
- package/dist/whiteboard/index.js +738 -0
- package/dist/whiteboard/index.js.map +1 -0
- package/package.json +141 -9
- package/registry/r/agent-composer.json +4 -4
- package/registry/r/agent-editor.json +9 -9
- package/registry/r/agent-error-card.json +2 -2
- package/registry/r/agent-event.json +4 -4
- package/registry/r/agent-handoff.json +2 -2
- package/registry/r/agent-profile.json +2 -2
- package/registry/r/agent-starting-state.json +2 -2
- package/registry/r/agent-stream.json +9 -9
- package/registry/r/agent-streaming.json +2 -2
- package/registry/r/agent-timeline.json +4 -4
- package/registry/r/approval-card.json +4 -4
- package/registry/r/artifact-preview.json +2 -2
- package/registry/r/attachment-chip.json +4 -4
- package/registry/r/audit-log-entry.json +3 -3
- package/registry/r/auto-compact-notice.json +2 -2
- package/registry/r/avatar.json +2 -2
- package/registry/r/badge.json +2 -2
- package/registry/r/browser-controls.json +2 -2
- package/registry/r/build-log-stream.json +2 -2
- package/registry/r/button.json +2 -2
- package/registry/r/capability-indicator.json +3 -3
- package/registry/r/card.json +2 -2
- package/registry/r/chat-composer.json +3 -3
- package/registry/r/chat-message.json +3 -3
- package/registry/r/chat-thread.json +2 -2
- package/registry/r/checkbox.json +2 -2
- package/registry/r/command-palette.json +4 -4
- package/registry/r/context-card.json +3 -3
- package/registry/r/context-window-bar.json +2 -2
- package/registry/r/cost-meter.json +2 -2
- package/registry/r/created-files-card.json +3 -3
- package/registry/r/cron-job-card.json +2 -2
- package/registry/r/cron-jobs-list.json +3 -3
- package/registry/r/deployment-row.json +3 -3
- package/registry/r/dialog.json +2 -2
- package/registry/r/diff-viewer.json +2 -2
- package/registry/r/domain-config.json +6 -6
- package/registry/r/empty-state.json +3 -3
- package/registry/r/env-var-editor.json +5 -5
- package/registry/r/folder-context-card.json +3 -3
- package/registry/r/folder-selector.json +2 -2
- package/registry/r/form-field.json +2 -2
- package/registry/r/hook-config.json +2 -2
- package/registry/r/hook-event-log.json +2 -2
- package/registry/r/input.json +2 -2
- package/registry/r/intent-selector.json +3 -3
- package/registry/r/label.json +2 -2
- package/registry/r/lane-board.json +2 -2
- package/registry/r/login-split.json +2 -2
- package/registry/r/mcp-server-card.json +2 -2
- package/registry/r/mcp-server-list.json +3 -3
- package/registry/r/memory-editor.json +3 -3
- package/registry/r/mention-menu.json +3 -3
- package/registry/r/metrics-panel.json +2 -2
- package/registry/r/model-card.json +3 -3
- package/registry/r/model-selector.json +2 -2
- package/registry/r/permission-matrix.json +2 -2
- package/registry/r/permission-modal.json +4 -4
- package/registry/r/preview-env-card.json +5 -5
- package/registry/r/preview-panel.json +3 -3
- package/registry/r/progress-checklist.json +3 -3
- package/registry/r/project-card.json +5 -5
- package/registry/r/project-switcher.json +2 -2
- package/registry/r/quick-action-chips.json +3 -3
- package/registry/r/radio-group.json +2 -2
- package/registry/r/recent-folders-list.json +2 -2
- package/registry/r/rollback-ui.json +4 -4
- package/registry/r/rule-card.json +3 -3
- package/registry/r/rule-editor.json +10 -10
- package/registry/r/rule-types.json +1 -1
- package/registry/r/run-stats.json +2 -2
- package/registry/r/running-tasks-panel.json +2 -2
- package/registry/r/scroll-area.json +2 -2
- package/registry/r/select.json +2 -2
- package/registry/r/session-list-item.json +2 -2
- package/registry/r/session-timeline.json +2 -2
- package/registry/r/sheet.json +2 -2
- package/registry/r/sidebar.json +2 -2
- package/registry/r/skeleton.json +2 -2
- package/registry/r/skill-card.json +4 -4
- package/registry/r/skill-editor.json +10 -10
- package/registry/r/skills-list.json +3 -3
- package/registry/r/social-auth-row.json +3 -3
- package/registry/r/steps-rail.json +2 -2
- package/registry/r/sub-agent-dispatch.json +2 -2
- package/registry/r/switch.json +2 -2
- package/registry/r/system-prompt-editor.json +2 -2
- package/registry/r/tabs.json +2 -2
- package/registry/r/task-header.json +4 -4
- package/registry/r/task-plan.json +2 -2
- package/registry/r/terminal-panel.json +2 -2
- package/registry/r/textarea.json +2 -2
- package/registry/r/theme-provider.json +2 -2
- package/registry/r/theme-script.json +1 -1
- package/registry/r/theo-ui-provider.json +2 -2
- package/registry/r/toast.json +2 -2
- package/registry/r/token-usage-chart.json +2 -2
- package/registry/r/tool-call-card.json +3 -3
- package/registry/r/tool-call.json +2 -2
- package/registry/r/tool-result.json +2 -2
- package/registry/r/tools-list.json +3 -3
- package/registry/r/tooltip.json +2 -2
- package/registry/r/topnav.json +2 -2
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/components/primitives/slide/slide.tsx
|
|
6
|
+
|
|
7
|
+
// src/components/primitives/slide/alerts.ts
|
|
8
|
+
var ALERT_RE = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][ \t]*(?:\r?\n|$)/i;
|
|
9
|
+
function detectAlerts(tree) {
|
|
10
|
+
for (const node of tree.children) {
|
|
11
|
+
if (node.type !== "blockquote") continue;
|
|
12
|
+
transformIfAlert(node);
|
|
13
|
+
}
|
|
14
|
+
return tree;
|
|
15
|
+
}
|
|
16
|
+
function transformIfAlert(node) {
|
|
17
|
+
const firstChild = node.children[0];
|
|
18
|
+
if (!firstChild || firstChild.type !== "paragraph") return;
|
|
19
|
+
const paragraph = firstChild;
|
|
20
|
+
const firstInline = paragraph.children[0];
|
|
21
|
+
if (!firstInline || firstInline.type !== "text") return;
|
|
22
|
+
const text = firstInline;
|
|
23
|
+
const match = ALERT_RE.exec(text.value);
|
|
24
|
+
if (!match) return;
|
|
25
|
+
const matched = match[1];
|
|
26
|
+
if (!matched) return;
|
|
27
|
+
const type = matched.toLowerCase();
|
|
28
|
+
text.value = text.value.replace(ALERT_RE, "");
|
|
29
|
+
if (text.value === "" && paragraph.children.length > 1) {
|
|
30
|
+
paragraph.children.shift();
|
|
31
|
+
}
|
|
32
|
+
node.data = {
|
|
33
|
+
...node.data,
|
|
34
|
+
hName: "aside",
|
|
35
|
+
hProperties: {
|
|
36
|
+
...node.data?.hProperties ?? {},
|
|
37
|
+
className: ["theo-slide-alert"],
|
|
38
|
+
"data-theo-slide-alert-type": type
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/components/primitives/slide/marpit-bg.ts
|
|
44
|
+
var BG_ALT_RE = /^bg(?:\s+(\w+))?/i;
|
|
45
|
+
var VALID_MODIFIERS = /* @__PURE__ */ new Set(["cover", "fit", "left", "right"]);
|
|
46
|
+
function extractMarpitBackgrounds(tree) {
|
|
47
|
+
let background;
|
|
48
|
+
const filteredChildren = tree.children.filter((node) => {
|
|
49
|
+
if (node.type !== "paragraph") return true;
|
|
50
|
+
const p = node;
|
|
51
|
+
if (p.children.length !== 1) return true;
|
|
52
|
+
const child = p.children[0];
|
|
53
|
+
if (!child || child.type !== "image") return true;
|
|
54
|
+
const img = child;
|
|
55
|
+
const match = BG_ALT_RE.exec(img.alt ?? "");
|
|
56
|
+
if (!match) return true;
|
|
57
|
+
if (!background) {
|
|
58
|
+
const modifierRaw = match[1]?.toLowerCase();
|
|
59
|
+
const modifier = modifierRaw && VALID_MODIFIERS.has(modifierRaw) ? modifierRaw : void 0;
|
|
60
|
+
background = {
|
|
61
|
+
url: img.url,
|
|
62
|
+
modifier
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
tree: { ...tree, children: filteredChildren },
|
|
69
|
+
background
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/components/primitives/slide/plugin.ts
|
|
74
|
+
function composePlugins(plugins) {
|
|
75
|
+
return {
|
|
76
|
+
async runMdast(tree, errors) {
|
|
77
|
+
let current = tree;
|
|
78
|
+
for (const p of plugins) {
|
|
79
|
+
if (!p.mdastTransform) continue;
|
|
80
|
+
const previous = current;
|
|
81
|
+
try {
|
|
82
|
+
const result = await p.mdastTransform(current);
|
|
83
|
+
if (!result || result.type !== "root") {
|
|
84
|
+
throw new Error("mdastTransform returned non-Root node");
|
|
85
|
+
}
|
|
86
|
+
current = result;
|
|
87
|
+
} catch (e) {
|
|
88
|
+
current = previous;
|
|
89
|
+
errors.push(makePluginError(p.name, "mdastTransform", e));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return current;
|
|
93
|
+
},
|
|
94
|
+
async runHast(tree, errors) {
|
|
95
|
+
let current = tree;
|
|
96
|
+
for (const p of plugins) {
|
|
97
|
+
if (!p.hastTransform) continue;
|
|
98
|
+
const previous = current;
|
|
99
|
+
try {
|
|
100
|
+
const result = await p.hastTransform(current);
|
|
101
|
+
if (!result || result.type !== "root") {
|
|
102
|
+
throw new Error("hastTransform returned non-Root node");
|
|
103
|
+
}
|
|
104
|
+
current = result;
|
|
105
|
+
} catch (e) {
|
|
106
|
+
current = previous;
|
|
107
|
+
errors.push(makePluginError(p.name, "hastTransform", e));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return current;
|
|
111
|
+
},
|
|
112
|
+
mergedComponents() {
|
|
113
|
+
const out = {};
|
|
114
|
+
for (const p of plugins) {
|
|
115
|
+
if (p.components) Object.assign(out, p.components);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
},
|
|
119
|
+
mergedSanitizeExtensions() {
|
|
120
|
+
const tagNames = /* @__PURE__ */ new Set();
|
|
121
|
+
const attributes = {};
|
|
122
|
+
for (const p of plugins) {
|
|
123
|
+
const ext = p.sanitizeSchemaExtension;
|
|
124
|
+
if (!ext) continue;
|
|
125
|
+
if (ext.tagNames) {
|
|
126
|
+
for (const tag of ext.tagNames) tagNames.add(tag);
|
|
127
|
+
}
|
|
128
|
+
if (ext.attributes) {
|
|
129
|
+
for (const [tag, attrs] of Object.entries(ext.attributes)) {
|
|
130
|
+
if (!attributes[tag]) attributes[tag] = /* @__PURE__ */ new Set();
|
|
131
|
+
for (const a of attrs) attributes[tag].add(a);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const mergedAttrs = {};
|
|
136
|
+
for (const [tag, set] of Object.entries(attributes)) {
|
|
137
|
+
mergedAttrs[tag] = Array.from(set);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
tagNames: Array.from(tagNames),
|
|
141
|
+
attributes: mergedAttrs
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function makePluginError(name, hook, e) {
|
|
147
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
148
|
+
return {
|
|
149
|
+
code: "PLUGIN_ERROR",
|
|
150
|
+
path: [],
|
|
151
|
+
message: `Plugin '${name}' failed in ${hook}: ${msg}`,
|
|
152
|
+
got: name
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/components/primitives/slide/sanitize.ts
|
|
157
|
+
var cachedBuiltSchema;
|
|
158
|
+
var TIER_1_TAG_NAMES = ["aside"];
|
|
159
|
+
var TIER_1_ATTRIBUTES = {
|
|
160
|
+
aside: ["className", "data-theo-slide-alert-type"],
|
|
161
|
+
"*": ["className"]
|
|
162
|
+
};
|
|
163
|
+
async function getSlideSanitizeSchema(extensions) {
|
|
164
|
+
const baseline = await getBaselineSchema();
|
|
165
|
+
if (!extensions || extensions.tagNames.length === 0 && Object.keys(extensions.attributes).length === 0) {
|
|
166
|
+
return baseline;
|
|
167
|
+
}
|
|
168
|
+
return mergeSchema(baseline, extensions);
|
|
169
|
+
}
|
|
170
|
+
async function getBaselineSchema() {
|
|
171
|
+
if (cachedBuiltSchema) return cachedBuiltSchema;
|
|
172
|
+
const { defaultSchema } = await import('hast-util-sanitize');
|
|
173
|
+
cachedBuiltSchema = mergeSchema(defaultSchema, {
|
|
174
|
+
tagNames: TIER_1_TAG_NAMES,
|
|
175
|
+
attributes: TIER_1_ATTRIBUTES
|
|
176
|
+
});
|
|
177
|
+
return cachedBuiltSchema;
|
|
178
|
+
}
|
|
179
|
+
function mergeSchema(base, extensions) {
|
|
180
|
+
const baseTagNames = base.tagNames ?? [];
|
|
181
|
+
const tagSet = new Set(baseTagNames);
|
|
182
|
+
for (const t of extensions.tagNames) tagSet.add(t);
|
|
183
|
+
const baseAttrs = base.attributes ?? {};
|
|
184
|
+
const mergedAttrs = { ...baseAttrs };
|
|
185
|
+
for (const [tag, attrs] of Object.entries(extensions.attributes)) {
|
|
186
|
+
const baseline = mergedAttrs[tag] ?? [];
|
|
187
|
+
const seen = /* @__PURE__ */ new Set();
|
|
188
|
+
const combined = [];
|
|
189
|
+
for (const a of baseline) {
|
|
190
|
+
const key = typeof a === "string" ? a : JSON.stringify(a);
|
|
191
|
+
if (!seen.has(key)) {
|
|
192
|
+
seen.add(key);
|
|
193
|
+
combined.push(a);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const a of attrs) {
|
|
197
|
+
if (!seen.has(a)) {
|
|
198
|
+
seen.add(a);
|
|
199
|
+
combined.push(a);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
mergedAttrs[tag] = combined;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
...base,
|
|
206
|
+
tagNames: Array.from(tagSet),
|
|
207
|
+
attributes: mergedAttrs
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function collectTagCounts(tree) {
|
|
211
|
+
const counts = /* @__PURE__ */ new Map();
|
|
212
|
+
const walk = (node) => {
|
|
213
|
+
if (node.type === "element") {
|
|
214
|
+
const tag = node.tagName;
|
|
215
|
+
counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
216
|
+
}
|
|
217
|
+
const children = node.children;
|
|
218
|
+
if (Array.isArray(children)) {
|
|
219
|
+
for (const child of children) {
|
|
220
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
221
|
+
walk(child);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
walk(tree);
|
|
227
|
+
return counts;
|
|
228
|
+
}
|
|
229
|
+
var slideTheme = z.enum(["default", "violet-forge"]);
|
|
230
|
+
var slideLayout = z.enum([
|
|
231
|
+
"default",
|
|
232
|
+
"title",
|
|
233
|
+
"two-column",
|
|
234
|
+
"image-right",
|
|
235
|
+
"image-left",
|
|
236
|
+
"code-output",
|
|
237
|
+
"section"
|
|
238
|
+
]);
|
|
239
|
+
var cssColor = z.string().max(64);
|
|
240
|
+
var langTag = z.string().regex(/^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/, "Expected BCP-47 language tag").max(35);
|
|
241
|
+
var SAFE_URL_SCHEMES = ["http://", "https://"];
|
|
242
|
+
function sanitizeBgUrl(input) {
|
|
243
|
+
try {
|
|
244
|
+
const trimmed = input.trim();
|
|
245
|
+
const url = trimmed.startsWith("url(") ? trimmed.replace(/^url\(\s*['"]?/, "").replace(/['"]?\s*\)$/, "") : trimmed;
|
|
246
|
+
const lower = url.toLowerCase();
|
|
247
|
+
if (lower.startsWith("javascript:") || lower.startsWith("vbscript:")) return null;
|
|
248
|
+
if (lower.startsWith("data:")) return null;
|
|
249
|
+
if (!SAFE_URL_SCHEMES.some((s) => lower.startsWith(s))) return null;
|
|
250
|
+
new URL(url);
|
|
251
|
+
return url;
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
var slideFrontmatter = z.object({
|
|
257
|
+
theme: slideTheme.optional(),
|
|
258
|
+
/** Rich-content T2: built-in layout (CSS grid). */
|
|
259
|
+
layout: slideLayout.optional(),
|
|
260
|
+
/** Rich-content T3: background URL (sanitized: http(s) only). */
|
|
261
|
+
backgroundImage: z.string().max(5e5).transform((v) => {
|
|
262
|
+
if (!v) return void 0;
|
|
263
|
+
const sanitized = sanitizeBgUrl(v);
|
|
264
|
+
return sanitized ?? void 0;
|
|
265
|
+
}).optional(),
|
|
266
|
+
/** Rich-content T3: CSS gradient string (validated by prefix). */
|
|
267
|
+
backgroundGradient: z.string().max(500).regex(
|
|
268
|
+
/^(linear|radial|conic)-gradient\(/i,
|
|
269
|
+
"Must start with linear-/radial-/conic-gradient("
|
|
270
|
+
).optional(),
|
|
271
|
+
/** Rich-content T5: header overlay text (plain, ≤ 200 chars). */
|
|
272
|
+
header: z.string().max(200).optional(),
|
|
273
|
+
/** Rich-content T5: footer overlay text (plain, ≤ 200 chars). */
|
|
274
|
+
footer: z.string().max(200).optional(),
|
|
275
|
+
/** Rich-content T5: pagination — `true` shows page number, `"skip"` hides. */
|
|
276
|
+
paginate: z.union([z.boolean(), z.literal("skip"), z.literal("hold")]).optional(),
|
|
277
|
+
lang: langTag.optional(),
|
|
278
|
+
color: cssColor.optional(),
|
|
279
|
+
backgroundColor: cssColor.optional()
|
|
280
|
+
}).strict();
|
|
281
|
+
var slideInput = z.object({
|
|
282
|
+
frontmatter: slideFrontmatter,
|
|
283
|
+
body: z.string().max(5e4)
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// src/components/primitives/slide/frontmatter.ts
|
|
287
|
+
var MAX_RAW_FRONTMATTER = 10240;
|
|
288
|
+
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
289
|
+
var BOM = "\uFEFF";
|
|
290
|
+
function extractFrontmatter(md) {
|
|
291
|
+
const normalized = md.startsWith(BOM) ? md.slice(1) : md;
|
|
292
|
+
const match = FRONTMATTER_RE.exec(normalized);
|
|
293
|
+
if (!match) {
|
|
294
|
+
return { rawFrontmatter: null, body: normalized };
|
|
295
|
+
}
|
|
296
|
+
const raw = match[1] ?? "";
|
|
297
|
+
const body = match[2] ?? "";
|
|
298
|
+
if (raw.length > MAX_RAW_FRONTMATTER) {
|
|
299
|
+
return { rawFrontmatter: raw, body, tooLarge: true };
|
|
300
|
+
}
|
|
301
|
+
return { rawFrontmatter: raw, body };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/components/primitives/slide/validate.ts
|
|
305
|
+
var MAX_BODY = 5e4;
|
|
306
|
+
function valueAtPath(input, path) {
|
|
307
|
+
let cursor = input;
|
|
308
|
+
for (const segment of path) {
|
|
309
|
+
if (cursor === null || typeof cursor !== "object") return void 0;
|
|
310
|
+
cursor = cursor[segment];
|
|
311
|
+
}
|
|
312
|
+
return cursor;
|
|
313
|
+
}
|
|
314
|
+
function formatZodIssue(issue, input, pathPrefix) {
|
|
315
|
+
const error = {
|
|
316
|
+
path: [...pathPrefix, ...issue.path],
|
|
317
|
+
message: issue.message,
|
|
318
|
+
code: "INVALID_FRONTMATTER"
|
|
319
|
+
};
|
|
320
|
+
if ("received" in issue && issue.received !== void 0) {
|
|
321
|
+
error.got = issue.received;
|
|
322
|
+
} else if (issue.path.length > 0) {
|
|
323
|
+
const value = valueAtPath(input, issue.path);
|
|
324
|
+
if (value !== void 0) error.got = value;
|
|
325
|
+
}
|
|
326
|
+
return error;
|
|
327
|
+
}
|
|
328
|
+
async function detectMultiSlide(body) {
|
|
329
|
+
const { fromMarkdown } = await import('mdast-util-from-markdown');
|
|
330
|
+
const tree = fromMarkdown(body);
|
|
331
|
+
const hrIdx = tree.children.findIndex((n) => n.type === "thematicBreak");
|
|
332
|
+
if (hrIdx === -1) {
|
|
333
|
+
return { multi: false, firstSlideBody: body };
|
|
334
|
+
}
|
|
335
|
+
const firstHr = tree.children[hrIdx];
|
|
336
|
+
const offset = firstHr?.position?.start.offset;
|
|
337
|
+
if (typeof offset !== "number") {
|
|
338
|
+
return { multi: true, firstSlideBody: body };
|
|
339
|
+
}
|
|
340
|
+
return { multi: true, firstSlideBody: body.slice(0, offset) };
|
|
341
|
+
}
|
|
342
|
+
async function validateSlide(markdown) {
|
|
343
|
+
const errors = [];
|
|
344
|
+
const extracted = extractFrontmatter(markdown);
|
|
345
|
+
if (extracted.tooLarge) {
|
|
346
|
+
errors.push({
|
|
347
|
+
code: "FRONTMATTER_TOO_LARGE",
|
|
348
|
+
path: [],
|
|
349
|
+
message: `Raw frontmatter exceeds ${10240} bytes.`,
|
|
350
|
+
got: extracted.rawFrontmatter?.length
|
|
351
|
+
});
|
|
352
|
+
return { ok: false, errors };
|
|
353
|
+
}
|
|
354
|
+
let frontmatter = {};
|
|
355
|
+
if (extracted.rawFrontmatter !== null) {
|
|
356
|
+
let parsed;
|
|
357
|
+
try {
|
|
358
|
+
const yaml = await import('yaml');
|
|
359
|
+
parsed = yaml.parse(extracted.rawFrontmatter);
|
|
360
|
+
} catch (e) {
|
|
361
|
+
errors.push({
|
|
362
|
+
code: "INVALID_FRONTMATTER",
|
|
363
|
+
path: [],
|
|
364
|
+
message: e instanceof Error ? e.message : "YAML parse failed.",
|
|
365
|
+
got: extracted.rawFrontmatter
|
|
366
|
+
});
|
|
367
|
+
return { ok: false, errors };
|
|
368
|
+
}
|
|
369
|
+
if (parsed === null || parsed === void 0) {
|
|
370
|
+
frontmatter = {};
|
|
371
|
+
} else if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
372
|
+
errors.push({
|
|
373
|
+
code: "INVALID_FRONTMATTER",
|
|
374
|
+
path: [],
|
|
375
|
+
message: "Frontmatter must be a YAML mapping (object).",
|
|
376
|
+
got: parsed
|
|
377
|
+
});
|
|
378
|
+
return { ok: false, errors };
|
|
379
|
+
} else {
|
|
380
|
+
const result = slideFrontmatter.safeParse(parsed);
|
|
381
|
+
if (!result.success) {
|
|
382
|
+
for (const issue of result.error.issues) {
|
|
383
|
+
errors.push(formatZodIssue(issue, parsed, []));
|
|
384
|
+
}
|
|
385
|
+
return { ok: false, errors };
|
|
386
|
+
}
|
|
387
|
+
frontmatter = result.data;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
let body = extracted.body;
|
|
391
|
+
if (body.length > MAX_BODY) {
|
|
392
|
+
errors.push({
|
|
393
|
+
code: "CONTENT_TOO_LARGE",
|
|
394
|
+
path: ["body"],
|
|
395
|
+
message: `Body exceeds ${MAX_BODY} characters.`,
|
|
396
|
+
got: body.length
|
|
397
|
+
});
|
|
398
|
+
return { ok: false, errors };
|
|
399
|
+
}
|
|
400
|
+
const multi = await detectMultiSlide(body);
|
|
401
|
+
if (multi.multi) {
|
|
402
|
+
body = multi.firstSlideBody;
|
|
403
|
+
errors.push({
|
|
404
|
+
code: "MULTIPLE_SLIDES",
|
|
405
|
+
path: ["body"],
|
|
406
|
+
message: "Input contains a top-level thematic break (---). <Slide> renders single-slide markdown only. Only the first slide was rendered."
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
return { ok: true, input: { frontmatter, body }, errors };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/components/primitives/slide/parse.ts
|
|
413
|
+
async function parseBody(body) {
|
|
414
|
+
const [{ fromMarkdown }, { gfmFromMarkdown }, { gfm }] = await Promise.all([
|
|
415
|
+
import('mdast-util-from-markdown'),
|
|
416
|
+
import('mdast-util-gfm'),
|
|
417
|
+
import('micromark-extension-gfm')
|
|
418
|
+
]);
|
|
419
|
+
return fromMarkdown(body, {
|
|
420
|
+
extensions: [gfm()],
|
|
421
|
+
mdastExtensions: [gfmFromMarkdown()]
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
async function mdastToHast(tree) {
|
|
425
|
+
const { toHast } = await import('mdast-util-to-hast');
|
|
426
|
+
const hast = toHast(tree, { allowDangerousHtml: false });
|
|
427
|
+
if (!hast || hast.type !== "root") {
|
|
428
|
+
return { type: "root", children: hast ? [hast] : [] };
|
|
429
|
+
}
|
|
430
|
+
return hast;
|
|
431
|
+
}
|
|
432
|
+
async function sanitizeHast(tree, extensions) {
|
|
433
|
+
const schema = await getSlideSanitizeSchema(extensions);
|
|
434
|
+
const { sanitize } = await import('hast-util-sanitize');
|
|
435
|
+
const preCount = collectTagCounts(tree);
|
|
436
|
+
const safe = sanitize(tree, schema);
|
|
437
|
+
const safeRoot = safe.type === "root" ? safe : { type: "root", children: [safe] };
|
|
438
|
+
const postCount = collectTagCounts(safeRoot);
|
|
439
|
+
const bannedTags = [];
|
|
440
|
+
for (const [tag, before] of preCount) {
|
|
441
|
+
const after = postCount.get(tag) ?? 0;
|
|
442
|
+
if (after < before) {
|
|
443
|
+
bannedTags.push(tag);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return { tree: safeRoot, bannedTags };
|
|
447
|
+
}
|
|
448
|
+
async function hastToReact(tree, components) {
|
|
449
|
+
const { Fragment, jsx: jsx2, jsxs: jsxs2 } = await import('react/jsx-runtime');
|
|
450
|
+
const { toJsxRuntime } = await import('hast-util-to-jsx-runtime');
|
|
451
|
+
return toJsxRuntime(tree, {
|
|
452
|
+
Fragment,
|
|
453
|
+
jsx: jsx2,
|
|
454
|
+
jsxs: jsxs2,
|
|
455
|
+
components
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
async function parseSlide(markdown, opts = {}) {
|
|
459
|
+
const errors = [];
|
|
460
|
+
let frontmatter = {};
|
|
461
|
+
let body = markdown;
|
|
462
|
+
let truncated = false;
|
|
463
|
+
const validation = await validateSlide(markdown);
|
|
464
|
+
if (validation.ok) {
|
|
465
|
+
frontmatter = validation.input.frontmatter;
|
|
466
|
+
body = validation.input.body;
|
|
467
|
+
errors.push(...validation.errors);
|
|
468
|
+
truncated = validation.errors.some((e) => e.code === "MULTIPLE_SLIDES");
|
|
469
|
+
} else {
|
|
470
|
+
errors.push(...validation.errors);
|
|
471
|
+
body = markdown;
|
|
472
|
+
}
|
|
473
|
+
const compose = composePlugins(opts.plugins ?? []);
|
|
474
|
+
const rawMdast = await parseBody(body);
|
|
475
|
+
detectAlerts(rawMdast);
|
|
476
|
+
const { tree: mdastNoBg, background: marpitBg } = extractMarpitBackgrounds(rawMdast);
|
|
477
|
+
let extractedBackground;
|
|
478
|
+
if (marpitBg) {
|
|
479
|
+
const safeUrl = sanitizeBgUrl(marpitBg.url);
|
|
480
|
+
if (safeUrl) {
|
|
481
|
+
extractedBackground = { url: safeUrl, modifier: marpitBg.modifier };
|
|
482
|
+
} else {
|
|
483
|
+
errors.push({
|
|
484
|
+
code: "MARPIT_BG_UNSAFE_URL",
|
|
485
|
+
path: [],
|
|
486
|
+
message: "Marpit  rejected: unsafe scheme or malformed URL.",
|
|
487
|
+
got: marpitBg.url.slice(0, 80)
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const transformedMdast = await compose.runMdast(mdastNoBg, errors);
|
|
492
|
+
const rawHast = await mdastToHast(transformedMdast);
|
|
493
|
+
const transformedHast = await compose.runHast(rawHast, errors);
|
|
494
|
+
const sanitizeExtensions = compose.mergedSanitizeExtensions();
|
|
495
|
+
const { tree: safeTree, bannedTags } = await sanitizeHast(transformedHast, sanitizeExtensions);
|
|
496
|
+
for (const tag of bannedTags) {
|
|
497
|
+
errors.push({
|
|
498
|
+
code: "BANNED_TAG",
|
|
499
|
+
path: ["body"],
|
|
500
|
+
message: `Tag <${tag}> was stripped by the slide sanitizer.`,
|
|
501
|
+
got: tag
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
const mergedComponents = {
|
|
505
|
+
...opts.components ?? {},
|
|
506
|
+
...compose.mergedComponents()
|
|
507
|
+
};
|
|
508
|
+
const tree = await hastToReact(safeTree, mergedComponents);
|
|
509
|
+
return { frontmatter, tree, errors, truncated, extractedBackground };
|
|
510
|
+
}
|
|
511
|
+
function useSlideFit(ref, canvasW, canvasH, opts = {}) {
|
|
512
|
+
const { minScale = 0.1, maxScale = 4 } = opts;
|
|
513
|
+
const [scale, setScale] = useState(1);
|
|
514
|
+
useEffect(() => {
|
|
515
|
+
const el = ref.current;
|
|
516
|
+
if (!el) return;
|
|
517
|
+
if (typeof ResizeObserver === "undefined") {
|
|
518
|
+
const { width, height } = el.getBoundingClientRect();
|
|
519
|
+
if (width > 0 && height > 0) {
|
|
520
|
+
const raw = Math.min(width / canvasW, height / canvasH);
|
|
521
|
+
setScale(Math.max(minScale, Math.min(raw, maxScale)));
|
|
522
|
+
}
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const update = () => {
|
|
526
|
+
const { width, height } = el.getBoundingClientRect();
|
|
527
|
+
if (width <= 0 || height <= 0) return;
|
|
528
|
+
const raw = Math.min(width / canvasW, height / canvasH);
|
|
529
|
+
const clamped = Math.max(minScale, Math.min(raw, maxScale));
|
|
530
|
+
if (Number.isFinite(clamped)) setScale(clamped);
|
|
531
|
+
};
|
|
532
|
+
update();
|
|
533
|
+
const ro = new ResizeObserver(update);
|
|
534
|
+
ro.observe(el);
|
|
535
|
+
return () => ro.disconnect();
|
|
536
|
+
}, [ref, canvasW, canvasH, minScale, maxScale]);
|
|
537
|
+
return scale;
|
|
538
|
+
}
|
|
539
|
+
var ASPECT_PRESETS = {
|
|
540
|
+
"16:9": { width: 1280, height: 720 },
|
|
541
|
+
"4:3": { width: 960, height: 720 }
|
|
542
|
+
};
|
|
543
|
+
function resolveCanvas(ar) {
|
|
544
|
+
if (!ar || ar === "16:9") return ASPECT_PRESETS["16:9"];
|
|
545
|
+
if (ar === "4:3") return ASPECT_PRESETS["4:3"];
|
|
546
|
+
if (ar.width <= 0 || ar.height <= 0 || !Number.isFinite(ar.width) || !Number.isFinite(ar.height)) {
|
|
547
|
+
return { ...ASPECT_PRESETS["16:9"], invalid: true };
|
|
548
|
+
}
|
|
549
|
+
return ar;
|
|
550
|
+
}
|
|
551
|
+
var Slide = ({
|
|
552
|
+
markdown,
|
|
553
|
+
theme = "default",
|
|
554
|
+
aspectRatio = "16:9",
|
|
555
|
+
minScale,
|
|
556
|
+
maxScale,
|
|
557
|
+
onValidationError,
|
|
558
|
+
components,
|
|
559
|
+
plugins,
|
|
560
|
+
className,
|
|
561
|
+
"aria-label": ariaLabel = "Slide"
|
|
562
|
+
}) => {
|
|
563
|
+
const containerRef = useRef(null);
|
|
564
|
+
const canvas = useMemo(() => resolveCanvas(aspectRatio), [aspectRatio]);
|
|
565
|
+
const scale = useSlideFit(containerRef, canvas.width, canvas.height, {
|
|
566
|
+
minScale,
|
|
567
|
+
maxScale
|
|
568
|
+
});
|
|
569
|
+
const [parsed, setParsed] = useState(null);
|
|
570
|
+
useEffect(() => {
|
|
571
|
+
if (canvas.invalid && onValidationError) {
|
|
572
|
+
onValidationError([
|
|
573
|
+
{
|
|
574
|
+
code: "INVALID_ASPECT_RATIO",
|
|
575
|
+
path: ["aspectRatio"],
|
|
576
|
+
message: "aspectRatio width/height must be positive finite numbers; falling back to 16:9.",
|
|
577
|
+
got: aspectRatio
|
|
578
|
+
}
|
|
579
|
+
]);
|
|
580
|
+
}
|
|
581
|
+
}, [canvas.invalid, onValidationError, aspectRatio]);
|
|
582
|
+
const versionRef = useRef(0);
|
|
583
|
+
useEffect(() => {
|
|
584
|
+
const myVersion = ++versionRef.current;
|
|
585
|
+
let cancelled = false;
|
|
586
|
+
parseSlide(markdown, { components, plugins }).then(
|
|
587
|
+
(result) => {
|
|
588
|
+
if (cancelled || myVersion !== versionRef.current) return;
|
|
589
|
+
setParsed(result);
|
|
590
|
+
if (result.errors.length > 0 && onValidationError) {
|
|
591
|
+
onValidationError(result.errors);
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
(err) => {
|
|
595
|
+
if (cancelled || myVersion !== versionRef.current) return;
|
|
596
|
+
if (onValidationError) {
|
|
597
|
+
onValidationError([
|
|
598
|
+
{
|
|
599
|
+
code: "INVALID_FRONTMATTER",
|
|
600
|
+
path: [],
|
|
601
|
+
message: err instanceof Error ? err.message : "parseSlide rejected."
|
|
602
|
+
}
|
|
603
|
+
]);
|
|
604
|
+
}
|
|
605
|
+
setParsed({
|
|
606
|
+
frontmatter: {},
|
|
607
|
+
tree: emptyFragment(),
|
|
608
|
+
errors: [],
|
|
609
|
+
truncated: false
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
);
|
|
613
|
+
return () => {
|
|
614
|
+
cancelled = true;
|
|
615
|
+
};
|
|
616
|
+
}, [markdown, components, plugins, onValidationError]);
|
|
617
|
+
const treeNode = parsed?.tree ?? null;
|
|
618
|
+
const slideThemeAttr = theme;
|
|
619
|
+
const fm = parsed?.frontmatter ?? {};
|
|
620
|
+
const layout = fm.layout;
|
|
621
|
+
const bgUrl = fm.backgroundImage ?? parsed?.extractedBackground?.url;
|
|
622
|
+
const bgModifier = parsed?.extractedBackground?.modifier;
|
|
623
|
+
const bgGradient = fm.backgroundGradient;
|
|
624
|
+
const headerText = fm.header;
|
|
625
|
+
const footerText = fm.footer;
|
|
626
|
+
const paginateValue = fm.paginate;
|
|
627
|
+
const showPaginate = paginateValue === true;
|
|
628
|
+
const backgroundImageStyle = bgGradient ? bgGradient : bgUrl ? `url("${bgUrl}")` : void 0;
|
|
629
|
+
return /* @__PURE__ */ jsx(
|
|
630
|
+
"div",
|
|
631
|
+
{
|
|
632
|
+
ref: containerRef,
|
|
633
|
+
className: ["theo-slide-host", className].filter(Boolean).join(" "),
|
|
634
|
+
"data-theo-slide-host": true,
|
|
635
|
+
style: {
|
|
636
|
+
position: "relative",
|
|
637
|
+
overflow: "hidden",
|
|
638
|
+
width: "100%",
|
|
639
|
+
height: "100%"
|
|
640
|
+
},
|
|
641
|
+
children: /* @__PURE__ */ jsxs(
|
|
642
|
+
"section",
|
|
643
|
+
{
|
|
644
|
+
"aria-roledescription": "slide",
|
|
645
|
+
"aria-label": ariaLabel,
|
|
646
|
+
className: "theo-slide",
|
|
647
|
+
"data-theo-slide-theme": slideThemeAttr,
|
|
648
|
+
"data-theo-slide-layout": layout ?? "default",
|
|
649
|
+
"data-theo-slide-bg-modifier": bgModifier,
|
|
650
|
+
style: {
|
|
651
|
+
position: "absolute",
|
|
652
|
+
top: "50%",
|
|
653
|
+
left: "50%",
|
|
654
|
+
width: canvas.width,
|
|
655
|
+
height: canvas.height,
|
|
656
|
+
transform: `translate(-50%, -50%) scale(${scale})`,
|
|
657
|
+
transformOrigin: "center",
|
|
658
|
+
padding: "var(--theo-slide-padding, 64px)",
|
|
659
|
+
color: "inherit",
|
|
660
|
+
// Background fills the canvas. When neither image nor gradient is set,
|
|
661
|
+
// the slide inherits the parent surface (same pattern as Whiteboard).
|
|
662
|
+
background: "transparent",
|
|
663
|
+
// Only set backgroundImage + ancillary props when one is provided —
|
|
664
|
+
// otherwise the shorthand `background: transparent` is preserved as-is
|
|
665
|
+
// (important for the inherit-from-parent guarantee).
|
|
666
|
+
...backgroundImageStyle ? {
|
|
667
|
+
backgroundImage: backgroundImageStyle,
|
|
668
|
+
backgroundSize: bgModifier === "fit" ? "contain" : "cover",
|
|
669
|
+
backgroundPosition: "center",
|
|
670
|
+
backgroundRepeat: "no-repeat"
|
|
671
|
+
} : {},
|
|
672
|
+
fontFamily: "var(--theo-slide-font-family, system-ui, -apple-system, 'Segoe UI', sans-serif)",
|
|
673
|
+
fontSize: "var(--theo-slide-font-base, 28px)",
|
|
674
|
+
lineHeight: 1.5,
|
|
675
|
+
boxSizing: "border-box",
|
|
676
|
+
overflow: "hidden"
|
|
677
|
+
},
|
|
678
|
+
children: [
|
|
679
|
+
headerText ? /* @__PURE__ */ jsx("div", { className: "theo-slide-header", "aria-hidden": "true", children: headerText }) : null,
|
|
680
|
+
treeNode,
|
|
681
|
+
footerText ? /* @__PURE__ */ jsx("div", { className: "theo-slide-footer", "aria-hidden": "true", children: footerText }) : null,
|
|
682
|
+
showPaginate ? /* @__PURE__ */ jsx("div", { className: "theo-slide-paginate", "aria-hidden": "true", children: "1" }) : null
|
|
683
|
+
]
|
|
684
|
+
}
|
|
685
|
+
)
|
|
686
|
+
}
|
|
687
|
+
);
|
|
688
|
+
};
|
|
689
|
+
function emptyFragment() {
|
|
690
|
+
return { type: "span", props: { children: null }, key: null };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/components/primitives/slide/themes/index.ts
|
|
694
|
+
var slideThemes = ["default", "violet-forge"];
|
|
695
|
+
function isSlideTheme(value) {
|
|
696
|
+
return typeof value === "string" && slideThemes.includes(value);
|
|
697
|
+
}
|
|
698
|
+
var rawSchema = z.toJSONSchema(slideFrontmatter, {
|
|
699
|
+
unrepresentable: "any"
|
|
700
|
+
});
|
|
701
|
+
if (rawSchema.properties) {
|
|
702
|
+
rawSchema.properties.backgroundImage = {
|
|
703
|
+
type: "string",
|
|
704
|
+
format: "uri",
|
|
705
|
+
maxLength: 5e5,
|
|
706
|
+
pattern: "^https?://",
|
|
707
|
+
description: "Slide background image URL. Only http(s) schemes are accepted; data: URLs are rejected at runtime (use a hosted image)."
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
var slideFrontmatterJsonSchema = rawSchema;
|
|
711
|
+
|
|
712
|
+
export { MAX_RAW_FRONTMATTER, Slide, collectTagCounts, composePlugins, extractFrontmatter, getSlideSanitizeSchema, hastToReact, isSlideTheme, mdastToHast, parseBody, parseSlide, sanitizeHast, slideFrontmatter, slideFrontmatterJsonSchema, slideInput, slideTheme, slideThemes, useSlideFit, validateSlide };
|
|
713
|
+
//# sourceMappingURL=index.js.map
|
|
714
|
+
//# sourceMappingURL=index.js.map
|