@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,17 @@
|
|
|
1
|
+
import { P as ParsedDeck } from '../types-Dt_A9RNg.js';
|
|
2
|
+
export { C as CreateDeckPluginOptions, b as DeckError, c as DeckSegment, D as DeckThemeOptions, a as DeckWidgetDefinition, d as DeckWidgetRenderContext, e as DeckWidgetRenderProps, f as ParsedSlide } from '../types-Dt_A9RNg.js';
|
|
3
|
+
import 'react';
|
|
4
|
+
|
|
5
|
+
declare const DECK_PLUGIN_ID = "deck";
|
|
6
|
+
declare const DECK_PANEL_ID = "deck";
|
|
7
|
+
declare const DECK_LABEL = "Deck";
|
|
8
|
+
declare const DECK_PATH_PREFIX = "deck/";
|
|
9
|
+
|
|
10
|
+
declare function normalizeDeckPath(path: string): string;
|
|
11
|
+
declare function isDeckMarkdownPath(path: string, pathPrefix?: string): boolean;
|
|
12
|
+
|
|
13
|
+
declare function splitSlides(input: string): string[];
|
|
14
|
+
declare function parseWidgetAttrs(raw: string): Record<string, string>;
|
|
15
|
+
declare function parseDeckMarkdown(input: string): ParsedDeck;
|
|
16
|
+
|
|
17
|
+
export { DECK_LABEL, DECK_PANEL_ID, DECK_PATH_PREFIX, DECK_PLUGIN_ID, ParsedDeck, isDeckMarkdownPath, normalizeDeckPath, parseDeckMarkdown, parseWidgetAttrs, splitSlides };
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// src/shared/constants.ts
|
|
2
|
+
var DECK_PLUGIN_ID = "deck";
|
|
3
|
+
var DECK_PANEL_ID = "deck";
|
|
4
|
+
var DECK_LABEL = "Deck";
|
|
5
|
+
var DECK_PATH_PREFIX = "deck/";
|
|
6
|
+
|
|
7
|
+
// src/shared/path.ts
|
|
8
|
+
function normalizeDeckPath(path) {
|
|
9
|
+
const trimmed = path.trim().replace(/\\/g, "/");
|
|
10
|
+
const noLeadingDot = trimmed.replace(/^\.\//, "");
|
|
11
|
+
return noLeadingDot.replace(/\/+/g, "/");
|
|
12
|
+
}
|
|
13
|
+
function isDeckMarkdownPath(path, pathPrefix = "deck/") {
|
|
14
|
+
const normalized = normalizeDeckPath(path);
|
|
15
|
+
const normalizedPathPrefix = normalizeDeckPath(pathPrefix);
|
|
16
|
+
const normalizedPrefix = normalizedPathPrefix.endsWith("/") ? normalizedPathPrefix : `${normalizedPathPrefix}/`;
|
|
17
|
+
if (!normalized.endsWith(".md")) return false;
|
|
18
|
+
if (normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized)) return false;
|
|
19
|
+
if (normalized.split("/").some((segment) => segment === "..")) return false;
|
|
20
|
+
return normalized.startsWith(normalizedPrefix);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/shared/parser.ts
|
|
24
|
+
var TITLE_RX = /^##\s+title:\s*(.+)$/i;
|
|
25
|
+
var YAML_TITLE_RX = /^title:\s*(.+)$/i;
|
|
26
|
+
var YAML_KV_RX = /^[A-Za-z][\w-]*:\s*.*$/;
|
|
27
|
+
var FENCE_RX = /^(```|~~~)/;
|
|
28
|
+
var WIDGET_OPEN = "{{";
|
|
29
|
+
var WIDGET_CLOSE = "}}";
|
|
30
|
+
var WIDGET_NAME_RX = /^[A-Za-z][\w-]*$/;
|
|
31
|
+
var ATTR_KEY_RX = /^[A-Za-z][\w-]*$/;
|
|
32
|
+
function splitSlides(input) {
|
|
33
|
+
const slides = [];
|
|
34
|
+
let current = [];
|
|
35
|
+
let fenceMarker = null;
|
|
36
|
+
for (const line of input.split("\n")) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
const fence = FENCE_RX.exec(trimmed);
|
|
39
|
+
if (fence) {
|
|
40
|
+
if (fenceMarker === fence[1]) fenceMarker = null;
|
|
41
|
+
else if (fenceMarker === null) fenceMarker = fence[1];
|
|
42
|
+
}
|
|
43
|
+
if (fenceMarker === null && trimmed === "---") {
|
|
44
|
+
slides.push(current.join("\n").trim());
|
|
45
|
+
current = [];
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
current.push(line);
|
|
49
|
+
}
|
|
50
|
+
slides.push(current.join("\n").trim());
|
|
51
|
+
const nonEmpty = slides.filter((slide) => slide.length > 0);
|
|
52
|
+
if (nonEmpty.length > 0) return nonEmpty;
|
|
53
|
+
return [""];
|
|
54
|
+
}
|
|
55
|
+
function parseWidgetAttrs(raw) {
|
|
56
|
+
const parsed = tryParseWidgetAttrs(raw);
|
|
57
|
+
if (parsed == null) throw new Error(`Malformed widget attrs: ${raw}`);
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
function parseDeckMarkdown(input) {
|
|
61
|
+
const lines = input.split("\n");
|
|
62
|
+
let start = 0;
|
|
63
|
+
while (start < lines.length && lines[start].trim() === "") start += 1;
|
|
64
|
+
let title = stripYamlFrontmatter(lines, start);
|
|
65
|
+
while (start < lines.length && lines[start].trim() === "") start += 1;
|
|
66
|
+
if (!title && lines[start]?.trim() === "---") {
|
|
67
|
+
lines.splice(start, 1);
|
|
68
|
+
}
|
|
69
|
+
let sawFence = false;
|
|
70
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
71
|
+
const trimmed = lines[i].trim();
|
|
72
|
+
if (FENCE_RX.test(trimmed)) sawFence = !sawFence;
|
|
73
|
+
if (!sawFence && trimmed === "---") break;
|
|
74
|
+
const match = TITLE_RX.exec(trimmed);
|
|
75
|
+
if (match) {
|
|
76
|
+
title = title ?? stripQuotes(match[1].trim());
|
|
77
|
+
lines.splice(i, 1);
|
|
78
|
+
if (lines[i]?.trim() === "") lines.splice(i, 1);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const slides = splitSlides(lines.join("\n")).map((raw, index, all) => ({
|
|
83
|
+
index,
|
|
84
|
+
raw,
|
|
85
|
+
segments: tokenize(raw, index, all.length)
|
|
86
|
+
}));
|
|
87
|
+
return title ? { title, slides } : { slides };
|
|
88
|
+
}
|
|
89
|
+
function stripYamlFrontmatter(lines, start) {
|
|
90
|
+
if (lines[start]?.trim() !== "---") return void 0;
|
|
91
|
+
let end = -1;
|
|
92
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
93
|
+
if (lines[i].trim() === "---") {
|
|
94
|
+
end = i;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (end === -1) return void 0;
|
|
99
|
+
const fields = lines.slice(start + 1, end);
|
|
100
|
+
if (fields.length === 0 || !fields.every((line) => YAML_KV_RX.test(line.trim()))) return void 0;
|
|
101
|
+
const titleLine = fields.find((line) => YAML_TITLE_RX.test(line.trim()));
|
|
102
|
+
const title = titleLine?.trim().match(YAML_TITLE_RX)?.[1]?.trim();
|
|
103
|
+
lines.splice(start, end - start + 1);
|
|
104
|
+
while (start < lines.length && lines[start]?.trim() === "") lines.splice(start, 1);
|
|
105
|
+
return title ? stripQuotes(title) : void 0;
|
|
106
|
+
}
|
|
107
|
+
function tokenize(markdown, slideIndex, slideCount) {
|
|
108
|
+
const segments = [];
|
|
109
|
+
const lines = markdown.split("\n");
|
|
110
|
+
let fenceMarker = null;
|
|
111
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
112
|
+
const line = lines[lineIndex];
|
|
113
|
+
const trimmed = line.trim();
|
|
114
|
+
const fence = FENCE_RX.exec(trimmed);
|
|
115
|
+
if (fence) {
|
|
116
|
+
if (fenceMarker === fence[1]) fenceMarker = null;
|
|
117
|
+
else if (fenceMarker === null) fenceMarker = fence[1];
|
|
118
|
+
pushMarkdown(segments, line);
|
|
119
|
+
if (lineIndex < lines.length - 1) pushMarkdown(segments, "\n");
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (fenceMarker) {
|
|
123
|
+
pushMarkdown(segments, line);
|
|
124
|
+
if (lineIndex < lines.length - 1) pushMarkdown(segments, "\n");
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
for (const segment of tokenizeLine(line, slideIndex, slideCount)) {
|
|
128
|
+
if (segment.type === "markdown") pushMarkdown(segments, segment.text);
|
|
129
|
+
else segments.push(segment);
|
|
130
|
+
}
|
|
131
|
+
if (lineIndex < lines.length - 1) pushMarkdown(segments, "\n");
|
|
132
|
+
}
|
|
133
|
+
return segments.length > 0 ? segments : [{ type: "markdown", text: "" }];
|
|
134
|
+
}
|
|
135
|
+
function tokenizeLine(line, _slideIndex, _slideCount) {
|
|
136
|
+
const segments = [];
|
|
137
|
+
let markdown = "";
|
|
138
|
+
let i = 0;
|
|
139
|
+
while (i < line.length) {
|
|
140
|
+
if (line[i] === "`") {
|
|
141
|
+
const tickCount = countRepeated(line, i, "`");
|
|
142
|
+
const close = line.indexOf("`".repeat(tickCount), i + tickCount);
|
|
143
|
+
if (close === -1) {
|
|
144
|
+
markdown += line.slice(i);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
markdown += line.slice(i, close + tickCount);
|
|
148
|
+
i = close + tickCount;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (line.startsWith(WIDGET_OPEN, i)) {
|
|
152
|
+
const close = line.indexOf(WIDGET_CLOSE, i + WIDGET_OPEN.length);
|
|
153
|
+
if (close === -1) {
|
|
154
|
+
markdown += line.slice(i);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
const raw = line.slice(i, close + WIDGET_CLOSE.length);
|
|
158
|
+
const parsed = parseWidget(raw, line);
|
|
159
|
+
if (!parsed) {
|
|
160
|
+
markdown += raw;
|
|
161
|
+
i = close + WIDGET_CLOSE.length;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (markdown) {
|
|
165
|
+
segments.push({ type: "markdown", text: markdown });
|
|
166
|
+
markdown = "";
|
|
167
|
+
}
|
|
168
|
+
segments.push(parsed);
|
|
169
|
+
i = close + WIDGET_CLOSE.length;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
markdown += line[i];
|
|
173
|
+
i += 1;
|
|
174
|
+
}
|
|
175
|
+
if (markdown) segments.push({ type: "markdown", text: markdown });
|
|
176
|
+
return segments;
|
|
177
|
+
}
|
|
178
|
+
function parseWidget(raw, line) {
|
|
179
|
+
const inner = raw.slice(WIDGET_OPEN.length, -WIDGET_CLOSE.length).trim();
|
|
180
|
+
if (!inner) return null;
|
|
181
|
+
const firstSpace = inner.search(/\s/);
|
|
182
|
+
const name = firstSpace === -1 ? inner : inner.slice(0, firstSpace);
|
|
183
|
+
const attrsRaw = firstSpace === -1 ? "" : inner.slice(firstSpace).trim();
|
|
184
|
+
if (!WIDGET_NAME_RX.test(name)) return null;
|
|
185
|
+
let attrs;
|
|
186
|
+
try {
|
|
187
|
+
attrs = attrsRaw ? parseWidgetAttrs(attrsRaw) : {};
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
type: "widget",
|
|
193
|
+
name,
|
|
194
|
+
attrs,
|
|
195
|
+
raw,
|
|
196
|
+
position: line.trim() === raw.trim() ? "block" : "inline"
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function tryParseWidgetAttrs(raw) {
|
|
200
|
+
const attrs = {};
|
|
201
|
+
let i = 0;
|
|
202
|
+
while (i < raw.length) {
|
|
203
|
+
while (i < raw.length && /\s/.test(raw[i])) i += 1;
|
|
204
|
+
if (i >= raw.length) break;
|
|
205
|
+
const keyStart = i;
|
|
206
|
+
while (i < raw.length && /[A-Za-z0-9_-]/.test(raw[i])) i += 1;
|
|
207
|
+
const key = raw.slice(keyStart, i);
|
|
208
|
+
if (!ATTR_KEY_RX.test(key)) return null;
|
|
209
|
+
while (i < raw.length && /\s/.test(raw[i])) i += 1;
|
|
210
|
+
if (raw[i] !== "=") return null;
|
|
211
|
+
i += 1;
|
|
212
|
+
while (i < raw.length && /\s/.test(raw[i])) i += 1;
|
|
213
|
+
if (raw[i] !== '"') return null;
|
|
214
|
+
i += 1;
|
|
215
|
+
let value = "";
|
|
216
|
+
let closed = false;
|
|
217
|
+
while (i < raw.length) {
|
|
218
|
+
const char = raw[i];
|
|
219
|
+
if (char === "\\") {
|
|
220
|
+
const next = raw[i + 1];
|
|
221
|
+
if (next === void 0) return null;
|
|
222
|
+
value += next;
|
|
223
|
+
i += 2;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (char === '"') {
|
|
227
|
+
i += 1;
|
|
228
|
+
closed = true;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
value += char;
|
|
232
|
+
i += 1;
|
|
233
|
+
}
|
|
234
|
+
if (!closed) return null;
|
|
235
|
+
attrs[key] = value;
|
|
236
|
+
}
|
|
237
|
+
while (i < raw.length && /\s/.test(raw[i])) i += 1;
|
|
238
|
+
if (i !== raw.length) return null;
|
|
239
|
+
return attrs;
|
|
240
|
+
}
|
|
241
|
+
function pushMarkdown(segments, text) {
|
|
242
|
+
if (text.length === 0) return;
|
|
243
|
+
const last = segments[segments.length - 1];
|
|
244
|
+
if (last?.type === "markdown") last.text += text;
|
|
245
|
+
else segments.push({ type: "markdown", text });
|
|
246
|
+
}
|
|
247
|
+
function stripQuotes(value) {
|
|
248
|
+
return value.replace(/^['"]|['"]$/g, "");
|
|
249
|
+
}
|
|
250
|
+
function countRepeated(value, start, needle) {
|
|
251
|
+
let count = 0;
|
|
252
|
+
while (value[start + count] === needle) count += 1;
|
|
253
|
+
return count;
|
|
254
|
+
}
|
|
255
|
+
export {
|
|
256
|
+
DECK_LABEL,
|
|
257
|
+
DECK_PANEL_ID,
|
|
258
|
+
DECK_PATH_PREFIX,
|
|
259
|
+
DECK_PLUGIN_ID,
|
|
260
|
+
isDeckMarkdownPath,
|
|
261
|
+
normalizeDeckPath,
|
|
262
|
+
parseDeckMarkdown,
|
|
263
|
+
parseWidgetAttrs,
|
|
264
|
+
splitSlides
|
|
265
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface DeckError {
|
|
4
|
+
type: "storage" | "parse" | "render" | "widget" | "conflict";
|
|
5
|
+
path?: string;
|
|
6
|
+
message: string;
|
|
7
|
+
cause?: unknown;
|
|
8
|
+
}
|
|
9
|
+
interface DeckThemeOptions {
|
|
10
|
+
aspectRatio?: "16:9" | "4:3";
|
|
11
|
+
className?: string;
|
|
12
|
+
slideClassName?: string;
|
|
13
|
+
}
|
|
14
|
+
interface ParsedDeck {
|
|
15
|
+
title?: string;
|
|
16
|
+
slides: ParsedSlide[];
|
|
17
|
+
}
|
|
18
|
+
interface ParsedSlide {
|
|
19
|
+
index: number;
|
|
20
|
+
raw: string;
|
|
21
|
+
segments: DeckSegment[];
|
|
22
|
+
}
|
|
23
|
+
type DeckSegment = {
|
|
24
|
+
type: "markdown";
|
|
25
|
+
text: string;
|
|
26
|
+
} | {
|
|
27
|
+
type: "widget";
|
|
28
|
+
name: string;
|
|
29
|
+
attrs: Record<string, string>;
|
|
30
|
+
raw: string;
|
|
31
|
+
position: "block" | "inline";
|
|
32
|
+
};
|
|
33
|
+
interface DeckWidgetRenderContext {
|
|
34
|
+
path?: string;
|
|
35
|
+
slideIndex: number;
|
|
36
|
+
slideCount: number;
|
|
37
|
+
mode: "read" | "edit" | "present";
|
|
38
|
+
}
|
|
39
|
+
interface DeckWidgetRenderProps<TAttrs = Record<string, string>> {
|
|
40
|
+
attrs: TAttrs;
|
|
41
|
+
rawAttrs: Record<string, string>;
|
|
42
|
+
context: DeckWidgetRenderContext;
|
|
43
|
+
}
|
|
44
|
+
interface DeckWidgetDefinition<TAttrs = Record<string, string>> {
|
|
45
|
+
name: string;
|
|
46
|
+
display?: "block" | "inline";
|
|
47
|
+
parse?: (attrs: Record<string, string>) => TAttrs;
|
|
48
|
+
render: (props: DeckWidgetRenderProps<TAttrs>) => ReactNode;
|
|
49
|
+
}
|
|
50
|
+
interface CreateDeckPluginOptions {
|
|
51
|
+
pathPrefix?: string;
|
|
52
|
+
widgets?: DeckWidgetDefinition[];
|
|
53
|
+
theme?: DeckThemeOptions;
|
|
54
|
+
onError?: (error: DeckError) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type { CreateDeckPluginOptions as C, DeckThemeOptions as D, ParsedDeck as P, DeckWidgetDefinition as a, DeckError as b, DeckSegment as c, DeckWidgetRenderContext as d, DeckWidgetRenderProps as e, ParsedSlide as f };
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hachej/boring-deck",
|
|
3
|
+
"version": "0.1.20",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Front-only markdown deck plugin scaffold for Boring workspace.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/hachej/boring-ui"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/hachej/boring-ui",
|
|
12
|
+
"boring": {
|
|
13
|
+
"label": "Deck",
|
|
14
|
+
"front": "dist/front/index.js"
|
|
15
|
+
},
|
|
16
|
+
"pi": {
|
|
17
|
+
"skills": [
|
|
18
|
+
"skills/deck-authoring"
|
|
19
|
+
],
|
|
20
|
+
"systemPrompt": "Deck files live under deck/*.md. When the user asks to open a deck, create or locate the deck markdown file, then call exec_ui with { kind: 'openSurface', params: { kind: 'workspace.open.path', target: '<deck/path.md>' } }. Use the deck-authoring skill when writing or editing deck content."
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"skills",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/front/index.d.ts",
|
|
33
|
+
"import": "./dist/front/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./front": {
|
|
36
|
+
"types": "./dist/front/index.d.ts",
|
|
37
|
+
"import": "./dist/front/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./shared": {
|
|
40
|
+
"types": "./dist/shared/index.d.ts",
|
|
41
|
+
"import": "./dist/shared/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"sideEffects": false,
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@hachej/boring-ui-kit": "workspace:*",
|
|
48
|
+
"lucide-react": "^1.8.0",
|
|
49
|
+
"react-markdown": "^10.1.0",
|
|
50
|
+
"remark-gfm": "^4.0.1"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"typecheck": "tsc --noEmit",
|
|
55
|
+
"test": "vitest run --passWithNoTests",
|
|
56
|
+
"lint": "pnpm run typecheck",
|
|
57
|
+
"clean": "rm -rf dist .tsbuildinfo"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@hachej/boring-workspace": "workspace:*",
|
|
61
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
62
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@hachej/boring-workspace": "workspace:*",
|
|
66
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
67
|
+
"@testing-library/react": "^16.3.2",
|
|
68
|
+
"@types/react": "^19.0.0",
|
|
69
|
+
"@types/react-dom": "^19.0.0",
|
|
70
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
71
|
+
"jsdom": "^29.0.2",
|
|
72
|
+
"react": "^19.0.0",
|
|
73
|
+
"react-dom": "^19.0.0",
|
|
74
|
+
"tsup": "^8.0.0",
|
|
75
|
+
"typescript": "^5.4.0",
|
|
76
|
+
"vitest": "^3.1.3"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Author, edit, or open markdown slide decks in Boring workspaces.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# deck-authoring
|
|
6
|
+
|
|
7
|
+
Use this skill when authoring or editing markdown slide decks for
|
|
8
|
+
`@hachej/boring-deck`.
|
|
9
|
+
|
|
10
|
+
## File location and opening decks
|
|
11
|
+
|
|
12
|
+
- deck files live under `deck/*.md` by default
|
|
13
|
+
- the host app may configure a different prefix, but you should preserve the
|
|
14
|
+
existing project convention you see in the workspace
|
|
15
|
+
- when the user says “open me a deck” or asks to view a deck, do not say you
|
|
16
|
+
lack UI tools if `exec_ui` is available; create or locate the deck markdown
|
|
17
|
+
file, then call:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"kind": "openSurface",
|
|
22
|
+
"params": {
|
|
23
|
+
"kind": "workspace.open.path",
|
|
24
|
+
"target": "deck/intro.md"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- use the exact deck path as `target`; the deck plugin's surface resolver opens
|
|
30
|
+
matching markdown paths in the deck panel
|
|
31
|
+
|
|
32
|
+
## Slide structure
|
|
33
|
+
|
|
34
|
+
- write decks as normal markdown
|
|
35
|
+
- `---` on its own line splits slides
|
|
36
|
+
- keep one deck-wide canvas in mind (`16:9` by default; some hosts use `4:3`)
|
|
37
|
+
- slides should stay concise and presentation-friendly
|
|
38
|
+
|
|
39
|
+
## Widget syntax
|
|
40
|
+
|
|
41
|
+
Custom components use moustache syntax:
|
|
42
|
+
|
|
43
|
+
```md
|
|
44
|
+
{{WidgetName key="value"}}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
|
|
49
|
+
```md
|
|
50
|
+
Welcome {{Badge text="draft"}}
|
|
51
|
+
|
|
52
|
+
{{Kpi label="Revenue" value="$12.4M"}}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Rules:
|
|
56
|
+
- preserve any existing host-provided widget names and attrs
|
|
57
|
+
- do not silently rewrite host-specific widget syntax into a different format
|
|
58
|
+
- keep inline widgets inline when they appear inside a sentence or paragraph
|
|
59
|
+
|
|
60
|
+
## Authoring guidance
|
|
61
|
+
|
|
62
|
+
- prefer short slide titles and tight bullet lists
|
|
63
|
+
- avoid wall-of-text slides
|
|
64
|
+
- keep markdown valid and simple
|
|
65
|
+
- use fenced code blocks for code samples
|
|
66
|
+
- do not use MDX or raw HTML as a replacement for widgets
|
|
67
|
+
|
|
68
|
+
## Safety / compatibility
|
|
69
|
+
|
|
70
|
+
- preserve existing slide separators when editing an existing deck
|
|
71
|
+
- do not delete custom widgets just because you do not understand them
|
|
72
|
+
- if a deck already has app-specific components, keep their syntax intact unless
|
|
73
|
+
the user explicitly asks for a migration
|