@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.
@@ -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