@honeydeck/honeydeck 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/AGENTS.md +25 -0
  2. package/DEVELOPMENT.md +522 -0
  3. package/LICENSE +21 -0
  4. package/Readme.md +49 -0
  5. package/SPEC.md +88 -0
  6. package/docs/components.md +63 -0
  7. package/docs/configuration.md +91 -0
  8. package/docs/getting-started.md +116 -0
  9. package/docs/kit-authoring.md +207 -0
  10. package/docs/kits.md +387 -0
  11. package/docs/local-development.md +95 -0
  12. package/docs/mermaid.md +198 -0
  13. package/docs/mobile.md +108 -0
  14. package/docs/navigation.md +93 -0
  15. package/docs/next-steps.md +377 -0
  16. package/docs/pdf-export.md +91 -0
  17. package/docs/presenter-mode.md +104 -0
  18. package/docs/slides.md +130 -0
  19. package/docs/slidev-migration.md +42 -0
  20. package/docs/steps-and-reveals.md +171 -0
  21. package/package.json +134 -0
  22. package/skills/SPEC.md +21 -0
  23. package/skills/honeydeck/SKILL.md +65 -0
  24. package/skills/presentation-writing/SKILL.md +75 -0
  25. package/skills/slidev-migration/SKILL.md +153 -0
  26. package/src/SPEC.md +89 -0
  27. package/src/assets.d.ts +30 -0
  28. package/src/cli/SPEC.md +230 -0
  29. package/src/cli/args.ts +3 -0
  30. package/src/cli/banner.ts +9 -0
  31. package/src/cli/bin.js +5 -0
  32. package/src/cli/build.ts +229 -0
  33. package/src/cli/deck-path.ts +32 -0
  34. package/src/cli/dev.ts +263 -0
  35. package/src/cli/index.ts +126 -0
  36. package/src/cli/init.ts +369 -0
  37. package/src/cli/pdf.ts +923 -0
  38. package/src/cli/skill.ts +75 -0
  39. package/src/cli/templates/SPEC.md +70 -0
  40. package/src/cli/templates/deck-mdx.ts +15 -0
  41. package/src/cli/templates/package-json.ts +36 -0
  42. package/src/cli/templates/sparkle-button.ts +15 -0
  43. package/src/cli/templates/starter/components/SparkleButton.tsx +84 -0
  44. package/src/cli/templates/starter/deck.mdx +153 -0
  45. package/src/cli/templates/starter/styles.css +14 -0
  46. package/src/cli/templates/styles-css.ts +14 -0
  47. package/src/defaults.ts +1 -0
  48. package/src/layouts/ColorModeImage.tsx +55 -0
  49. package/src/layouts/SPEC.md +393 -0
  50. package/src/layouts/SlideFrame.tsx +48 -0
  51. package/src/layouts/bee/Blank.tsx +12 -0
  52. package/src/layouts/bee/Cover.tsx +70 -0
  53. package/src/layouts/bee/Default.tsx +42 -0
  54. package/src/layouts/bee/Image/Image.tsx +151 -0
  55. package/src/layouts/bee/Image/placeholder-dark.webp +0 -0
  56. package/src/layouts/bee/Image/placeholder-vertical-dark.webp +0 -0
  57. package/src/layouts/bee/Image/placeholder-vertical.webp +0 -0
  58. package/src/layouts/bee/Image/placeholder.webp +0 -0
  59. package/src/layouts/bee/ImageLeft.tsx +27 -0
  60. package/src/layouts/bee/ImageRight.tsx +27 -0
  61. package/src/layouts/bee/ImageSide.tsx +107 -0
  62. package/src/layouts/bee/Section.tsx +40 -0
  63. package/src/layouts/bee/TwoCol.tsx +108 -0
  64. package/src/layouts/bee/index.ts +40 -0
  65. package/src/layouts/clean/Blank.tsx +12 -0
  66. package/src/layouts/clean/Cover.tsx +58 -0
  67. package/src/layouts/clean/Default.tsx +33 -0
  68. package/src/layouts/clean/Image/Image.tsx +103 -0
  69. package/src/layouts/clean/ImageLeft.tsx +27 -0
  70. package/src/layouts/clean/ImageRight.tsx +27 -0
  71. package/src/layouts/clean/ImageSide.tsx +113 -0
  72. package/src/layouts/clean/Section.tsx +35 -0
  73. package/src/layouts/clean/TwoCol.tsx +63 -0
  74. package/src/layouts/clean/index.ts +40 -0
  75. package/src/layouts/index.ts +60 -0
  76. package/src/layouts/placeholders.ts +9 -0
  77. package/src/layouts/utils.ts +13 -0
  78. package/src/remark/SPEC.md +49 -0
  79. package/src/remark/h1-extract.ts +124 -0
  80. package/src/remark/index.ts +4 -0
  81. package/src/remark/shiki-code-blocks.ts +325 -0
  82. package/src/remark/step-numbering.ts +412 -0
  83. package/src/runtime/Deck.tsx +533 -0
  84. package/src/runtime/SPEC.md +256 -0
  85. package/src/runtime/SlideCanvas.tsx +95 -0
  86. package/src/runtime/TimelineContext.tsx +122 -0
  87. package/src/runtime/app-shell/index.html +31 -0
  88. package/src/runtime/app-shell/main.tsx +42 -0
  89. package/src/runtime/aspectRatio.ts +34 -0
  90. package/src/runtime/colorMode.ts +23 -0
  91. package/src/runtime/components/BrowserFrame.tsx +233 -0
  92. package/src/runtime/components/Button.tsx +57 -0
  93. package/src/runtime/components/CodeBlock.tsx +210 -0
  94. package/src/runtime/components/ColorModeCycleButton.tsx +59 -0
  95. package/src/runtime/components/ErrorBoundary.tsx +125 -0
  96. package/src/runtime/components/Keyboard.tsx +87 -0
  97. package/src/runtime/components/ListStyle.tsx +203 -0
  98. package/src/runtime/components/NavBar.tsx +223 -0
  99. package/src/runtime/components/NavBarButton.tsx +47 -0
  100. package/src/runtime/components/NavBarDivider.tsx +3 -0
  101. package/src/runtime/components/Notes.tsx +171 -0
  102. package/src/runtime/components/Reveal.tsx +82 -0
  103. package/src/runtime/components/RevealGroup.tsx +193 -0
  104. package/src/runtime/components/SPEC.md +263 -0
  105. package/src/runtime/components/SlideNumberBadge.tsx +11 -0
  106. package/src/runtime/components/TimelineSteps.tsx +115 -0
  107. package/src/runtime/components/index.ts +55 -0
  108. package/src/runtime/index.ts +42 -0
  109. package/src/runtime/inputOwnership.ts +68 -0
  110. package/src/runtime/keyboardTarget.ts +7 -0
  111. package/src/runtime/lastSlideRoute.ts +56 -0
  112. package/src/runtime/navigation.ts +211 -0
  113. package/src/runtime/router.ts +157 -0
  114. package/src/runtime/slideData.ts +137 -0
  115. package/src/runtime/sync.ts +267 -0
  116. package/src/runtime/types.ts +182 -0
  117. package/src/runtime/useKeyboardNav.ts +138 -0
  118. package/src/runtime/useSwipeNav.ts +257 -0
  119. package/src/runtime/views/DocsView.tsx +74 -0
  120. package/src/runtime/views/OverviewView.tsx +386 -0
  121. package/src/runtime/views/PresenterNotesPanel.tsx +76 -0
  122. package/src/runtime/views/PresenterView.tsx +340 -0
  123. package/src/runtime/views/SPEC.md +152 -0
  124. package/src/runtime/views/docs/ComponentsTab.tsx +178 -0
  125. package/src/runtime/views/docs/DocsHeader.tsx +101 -0
  126. package/src/runtime/views/docs/Intro.tsx +20 -0
  127. package/src/runtime/views/docs/LayoutsTab.tsx +324 -0
  128. package/src/runtime/views/docs/ThemeTab.tsx +110 -0
  129. package/src/runtime/views/index.ts +7 -0
  130. package/src/runtime/views/overviewGrid.ts +106 -0
  131. package/src/runtime/views/presenterPreview.ts +27 -0
  132. package/src/runtime/virtual-modules.d.ts +98 -0
  133. package/src/theme/SPEC.md +179 -0
  134. package/src/theme/base.css +623 -0
  135. package/src/theme/bee.css +35 -0
  136. package/src/theme/clean.css +38 -0
  137. package/src/vite-plugin/SPEC.md +114 -0
  138. package/src/vite-plugin/component-doc-crawler.ts +350 -0
  139. package/src/vite-plugin/deck-loader.ts +148 -0
  140. package/src/vite-plugin/index.ts +373 -0
  141. package/src/vite-plugin/layout-demo-crawler.ts +802 -0
  142. package/src/vite-plugin/splitter.ts +353 -0
  143. package/src/vite-plugin/token-manifest.ts +163 -0
  144. package/src/vite-plugin/virtual-modules.ts +587 -0
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Remark plugin: replace fenced code blocks with `<HoneydeckCodeBlock>` JSX elements.
3
+ *
4
+ * ### Pipeline position
5
+ * Must run AFTER `remarkStepNumbering` so it can read the `honeydeckStartAt` value
6
+ * that step-numbering annotates onto code MDAST nodes.
7
+ *
8
+ * ### What it does
9
+ * 1. Collects all `code` MDAST nodes.
10
+ * 2. For each code node:
11
+ * a. Loads the language grammar lazily into the shared shiki singleton.
12
+ * b. Highlights the code with dual themes (github-light + github-dark) using
13
+ * CSS custom properties (`defaultColor: false`). Each `.line` span gets a
14
+ * `data-line` attribute for runtime dimming.
15
+ * c. Parses the fence meta string (`{2|4-5|all}`) into step groups.
16
+ * d. Replaces the code node with a `mdxJsxFlowElement` for `<HoneydeckCodeBlock>`
17
+ * carrying three props: `html` (highlighted HTML string), `stepsJson`
18
+ * (JSON-encoded step groups), and `startAt` (1-based timeline step where
19
+ * the second group activates, from `node.data.honeydeckStartAt`), plus
20
+ * `source` (the original fenced code text for clipboard copying).
21
+ * 3. Injects a single `import { HoneydeckCodeBlock } from '@honeydeck/honeydeck/components/code-block'`
22
+ * declaration into the MDAST root when at least one code block was transformed.
23
+ *
24
+ * ### CSS variable activation
25
+ * The highlighted HTML uses shiki's dual-theme variable names
26
+ * (`--shiki-light`, `--shiki-dark`, `--shiki-light-bg`, `--shiki-dark-bg`).
27
+ * `src/theme/base.css` activates the correct variable set based on the
28
+ * `[data-honeydeck-color-mode]` attribute set by the Deck component.
29
+ *
30
+ * ### Step meta syntax
31
+ * - `{2}` — highlight line 2 immediately; consumes no timeline steps
32
+ * - `{2|4-5|all}` — three step groups; first group is baseline, then later groups activate at `startAt`, `startAt+1`
33
+ *
34
+ * @see HoneydeckCodeBlock in `src/runtime/components/CodeBlock.tsx`
35
+ * @see remarkStepNumbering for the counter that provides `honeydeckStartAt`
36
+ */
37
+
38
+ import type { Program } from "estree";
39
+ import type { Code, Root } from "mdast";
40
+ import type {
41
+ MdxJsxAttribute,
42
+ MdxJsxAttributeValueExpression,
43
+ MdxJsxFlowElement,
44
+ } from "mdast-util-mdx-jsx";
45
+ import type { MdxjsEsm } from "mdast-util-mdxjs-esm";
46
+ import { getSingletonHighlighter } from "shiki";
47
+ import type { Plugin } from "unified";
48
+ import { visit } from "unist-util-visit";
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Types
52
+ // ---------------------------------------------------------------------------
53
+
54
+ /** A single step-through group: either an array of 1-based line numbers or 'all'. */
55
+ export type StepGroup = number[] | "all";
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Singleton shiki highlighter
59
+ // ---------------------------------------------------------------------------
60
+
61
+ let highlighterPromise: Promise<
62
+ Awaited<ReturnType<typeof getSingletonHighlighter>>
63
+ > | null = null;
64
+
65
+ function getHighlighter() {
66
+ if (!highlighterPromise) {
67
+ highlighterPromise = getSingletonHighlighter({
68
+ themes: ["github-light", "github-dark"],
69
+ langs: [], // languages loaded lazily per code block
70
+ });
71
+ }
72
+ return highlighterPromise;
73
+ }
74
+
75
+ async function loadLang(
76
+ highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
77
+ lang: string,
78
+ ): Promise<string> {
79
+ if (!lang || lang === "text" || lang === "plaintext") return "text";
80
+ try {
81
+ await highlighter.loadLanguage(
82
+ lang as Parameters<typeof highlighter.loadLanguage>[0],
83
+ );
84
+ return lang;
85
+ } catch {
86
+ // Language not in shiki's bundle — fall back to plain text
87
+ return "text";
88
+ }
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Step meta parsing
93
+ // ---------------------------------------------------------------------------
94
+
95
+ /**
96
+ * Parse a code fence meta string like `{2|4-5|all}` into an array of step groups.
97
+ *
98
+ * Returns `[]` when there is no `{…}` block in the meta string.
99
+ * Even a single group like `{2}` produces `[[2]]` — a baseline highlight that
100
+ * consumes no timeline steps.
101
+ *
102
+ * @example
103
+ * parseStepMeta('{2|4-5|all}') // → [[2], [4,5], 'all']
104
+ * parseStepMeta('{2}') // → [[2]] (baseline highlight, no timeline step)
105
+ * parseStepMeta(null) // → []
106
+ */
107
+ export function parseStepMeta(meta: string | null | undefined): StepGroup[] {
108
+ if (!meta) return [];
109
+
110
+ const match = meta.match(/\{([^}]+)\}/);
111
+ if (!match?.[1]) return [];
112
+
113
+ const groupStrings = match[1].split("|").filter(Boolean);
114
+ if (groupStrings.length === 0) return [];
115
+ // Note: single groups are valid — they represent a baseline highlight and
116
+ // consume no timeline steps.
117
+
118
+ return groupStrings.map((group): StepGroup => {
119
+ const trimmed = group.trim();
120
+ if (trimmed === "all") return "all";
121
+
122
+ const lines: number[] = [];
123
+ for (const part of trimmed.split(",")) {
124
+ const dashIdx = part.indexOf("-");
125
+ if (dashIdx !== -1) {
126
+ const start = parseInt(part.slice(0, dashIdx).trim(), 10);
127
+ const end = parseInt(part.slice(dashIdx + 1).trim(), 10);
128
+ if (!Number.isNaN(start) && !Number.isNaN(end)) {
129
+ for (let i = start; i <= end; i++) lines.push(i);
130
+ }
131
+ } else {
132
+ const n = parseInt(part.trim(), 10);
133
+ if (!Number.isNaN(n)) lines.push(n);
134
+ }
135
+ }
136
+ return lines;
137
+ });
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // AST helpers
142
+ // ---------------------------------------------------------------------------
143
+
144
+ /** Build a string-valued JSX attribute (prop="value"). */
145
+ function makeStringAttr(name: string, value: string): MdxJsxAttribute {
146
+ return { type: "mdxJsxAttribute", name, value };
147
+ }
148
+
149
+ /** Build a numeric JSX attribute (prop={N}). */
150
+ function makeNumericAttr(name: string, value: number): MdxJsxAttribute {
151
+ const estree: Program = {
152
+ type: "Program",
153
+ sourceType: "module",
154
+ comments: [],
155
+ body: [
156
+ {
157
+ type: "ExpressionStatement",
158
+ expression: { type: "Literal", value, raw: String(value) },
159
+ },
160
+ ],
161
+ };
162
+ const valueExpr: MdxJsxAttributeValueExpression = {
163
+ type: "mdxJsxAttributeValueExpression",
164
+ value: String(value),
165
+ data: { estree },
166
+ };
167
+ return { type: "mdxJsxAttribute", name, value: valueExpr };
168
+ }
169
+
170
+ /**
171
+ * Prepend an `import { specifier } from 'source'` declaration to the MDAST
172
+ * root, placed after any leading `yaml` / `mdxjsEsm` nodes so it sits with
173
+ * the other imports.
174
+ *
175
+ * Skips injection if an import for `specifier` already exists.
176
+ */
177
+ function injectImport(tree: Root, specifier: string, source: string): void {
178
+ // Avoid duplicate imports
179
+ const alreadyImported = tree.children.some(
180
+ (n) =>
181
+ n.type === "mdxjsEsm" &&
182
+ (n as unknown as { value: string }).value?.includes(specifier),
183
+ );
184
+ if (alreadyImported) return;
185
+
186
+ // Find insertion point: after any leading yaml + mdxjsEsm nodes
187
+ let insertAt = 0;
188
+ for (let i = 0; i < tree.children.length; i++) {
189
+ const t = tree.children[i]?.type;
190
+ if (t === "yaml" || t === "mdxjsEsm") {
191
+ insertAt = i + 1;
192
+ } else {
193
+ break;
194
+ }
195
+ }
196
+
197
+ const importNode: MdxjsEsm = {
198
+ type: "mdxjsEsm",
199
+ value: `import { ${specifier} } from '${source}'`,
200
+ data: {
201
+ estree: {
202
+ type: "Program",
203
+ sourceType: "module",
204
+ comments: [],
205
+ body: [
206
+ {
207
+ type: "ImportDeclaration",
208
+ specifiers: [
209
+ {
210
+ type: "ImportSpecifier",
211
+ imported: { type: "Identifier", name: specifier },
212
+ local: { type: "Identifier", name: specifier },
213
+ },
214
+ ],
215
+ source: { type: "Literal", value: source, raw: `'${source}'` },
216
+ },
217
+ ],
218
+ } as unknown as Program,
219
+ },
220
+ };
221
+
222
+ tree.children.splice(
223
+ insertAt,
224
+ 0,
225
+ importNode as unknown as (typeof tree.children)[number],
226
+ );
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Plugin
231
+ // ---------------------------------------------------------------------------
232
+
233
+ const COMPONENT_NAME = "HoneydeckCodeBlock";
234
+ const IMPORT_SOURCE = "@honeydeck/honeydeck/components/code-block";
235
+
236
+ /**
237
+ * Async remark plugin that transforms fenced code blocks to `<HoneydeckCodeBlock>`
238
+ * JSX elements with pre-highlighted HTML (via shiki dual themes).
239
+ *
240
+ * Plugin ordering: `[remarkFrontmatter, remarkH1Extract, remarkStepNumbering, remarkShikiCodeBlocks]`
241
+ */
242
+ export const remarkShikiCodeBlocks: Plugin<[], Root> = () => async (tree) => {
243
+ // Collect code nodes before mutating the tree (avoids iterator invalidation).
244
+ type CodeEntry = {
245
+ node: Code;
246
+ index: number;
247
+ parent: { children: unknown[] };
248
+ };
249
+ const codeEntries: CodeEntry[] = [];
250
+
251
+ visit(tree, "code", (node, index, parent) => {
252
+ if (index !== null && index !== undefined && parent) {
253
+ codeEntries.push({
254
+ node: node as unknown as Code,
255
+ index: index as number,
256
+ parent: parent as unknown as { children: unknown[] },
257
+ });
258
+ }
259
+ });
260
+
261
+ if (codeEntries.length === 0) return;
262
+
263
+ const highlighter = await getHighlighter();
264
+ let didTransform = false;
265
+
266
+ for (const { node, index, parent } of codeEntries) {
267
+ const rawLang = node.lang ?? "text";
268
+ const meta = node.meta;
269
+ const steps = parseStepMeta(meta);
270
+ const startAt: number =
271
+ ((node.data as Record<string, unknown> | undefined)
272
+ ?.honeydeckStartAt as number) ?? 0;
273
+
274
+ // Load language grammar (no-op if already cached)
275
+ const lang = await loadLang(highlighter, rawLang);
276
+
277
+ // Highlight with dual themes (CSS variable approach)
278
+ let html: string;
279
+ try {
280
+ html = highlighter.codeToHtml(node.value, {
281
+ lang,
282
+ themes: { light: "github-light", dark: "github-dark" },
283
+ defaultColor: false,
284
+ transformers: [
285
+ {
286
+ line(lineNode, lineNumber) {
287
+ // Ensure properties bag exists, then stamp data-line
288
+ if (!lineNode.properties) lineNode.properties = {};
289
+ lineNode.properties["data-line"] = lineNumber;
290
+ },
291
+ },
292
+ ],
293
+ });
294
+ } catch {
295
+ // Fallback: plain pre/code block without syntax colours
296
+ const escaped = node.value
297
+ .replace(/&/g, "&amp;")
298
+ .replace(/</g, "&lt;")
299
+ .replace(/>/g, "&gt;");
300
+ html = `<pre class="honeydeck-code-plain"><code>${escaped}</code></pre>`;
301
+ }
302
+
303
+ // Build <HoneydeckCodeBlock html="..." stepsJson="..." startAt={N} source="..." />
304
+ const jsxNode: MdxJsxFlowElement = {
305
+ type: "mdxJsxFlowElement",
306
+ name: COMPONENT_NAME,
307
+ attributes: [
308
+ makeStringAttr("html", html),
309
+ makeStringAttr("stepsJson", JSON.stringify(steps)),
310
+ makeNumericAttr("startAt", startAt),
311
+ makeStringAttr("source", node.value),
312
+ ],
313
+ children: [],
314
+ };
315
+
316
+ // Replace code node in-place (1 → 1, so sibling indices stay valid)
317
+ parent.children.splice(index, 1, jsxNode);
318
+ didTransform = true;
319
+ }
320
+
321
+ // Inject the import once for the whole slide
322
+ if (didTransform) {
323
+ injectImport(tree, COMPONENT_NAME, IMPORT_SOURCE);
324
+ }
325
+ };