@honeydeck/honeydeck 0.3.0 → 0.5.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 (66) hide show
  1. package/DEVELOPMENT.md +4 -1
  2. package/Readme.md +2 -2
  3. package/SPEC.md +3 -3
  4. package/docs/components-browser-frame.md +34 -0
  5. package/docs/components-keyboard.md +31 -0
  6. package/docs/components-list-style.md +49 -0
  7. package/docs/components-notes.md +36 -0
  8. package/docs/components-reveal-group.md +58 -0
  9. package/docs/components-reveal-with.md +37 -0
  10. package/docs/components-reveal.md +33 -0
  11. package/docs/components-timeline-steps.md +48 -0
  12. package/docs/components.md +13 -54
  13. package/docs/configuration.md +11 -0
  14. package/docs/deeper-dive.md +30 -7
  15. package/docs/getting-started.md +2 -2
  16. package/docs/navigation.md +1 -1
  17. package/docs/pdf-export.md +4 -2
  18. package/docs/presenter-mode.md +6 -3
  19. package/docs/skills.md +3 -3
  20. package/docs/slidev-migration.md +3 -0
  21. package/docs/steps-and-reveals.md +143 -8
  22. package/package.json +4 -1
  23. package/skills/SPEC.md +2 -2
  24. package/skills/honeydeck/SKILL.md +2 -2
  25. package/skills/slidev-migration/SKILL.md +1 -0
  26. package/src/SPEC.md +8 -3
  27. package/src/cli/SPEC.md +3 -2
  28. package/src/cli/pdf.ts +11 -4
  29. package/src/layouts/SPEC.md +1 -1
  30. package/src/remark/SPEC.md +102 -2
  31. package/src/remark/code-utils.ts +151 -0
  32. package/src/remark/shiki-code-blocks.ts +329 -136
  33. package/src/remark/step-numbering.ts +408 -103
  34. package/src/runtime/Deck.tsx +133 -116
  35. package/src/runtime/EffectiveColorModeContext.tsx +37 -0
  36. package/src/runtime/SPEC.md +21 -8
  37. package/src/runtime/SlideCanvas.tsx +19 -16
  38. package/src/runtime/SlideScaleContext.tsx +23 -0
  39. package/src/runtime/components/CodeBlock.tsx +19 -202
  40. package/src/runtime/components/CodeBlockCopyButton.tsx +64 -0
  41. package/src/runtime/components/CodeBlockShared.ts +17 -0
  42. package/src/runtime/components/Fade.tsx +51 -0
  43. package/src/runtime/components/FadeGroup.tsx +175 -0
  44. package/src/runtime/components/FadeWith.tsx +54 -0
  45. package/src/runtime/components/MagicCodeBlock.tsx +223 -0
  46. package/src/runtime/components/NavBar.tsx +1 -1
  47. package/src/runtime/components/NormalCodeBlock.tsx +128 -0
  48. package/src/runtime/components/Reveal.tsx +27 -27
  49. package/src/runtime/components/RevealGroup.tsx +143 -41
  50. package/src/runtime/components/RevealWith.tsx +63 -0
  51. package/src/runtime/components/SPEC.md +112 -7
  52. package/src/runtime/components/TimelineReveal.tsx +81 -0
  53. package/src/runtime/components/index.ts +13 -5
  54. package/src/runtime/components/timelineVisibility.ts +45 -0
  55. package/src/runtime/index.ts +9 -1
  56. package/src/runtime/navigation.ts +6 -4
  57. package/src/runtime/presentationApi.ts +449 -0
  58. package/src/runtime/views/PresenterCastButton.tsx +39 -0
  59. package/src/runtime/views/PresenterView.tsx +21 -4
  60. package/src/runtime/views/SPEC.md +7 -5
  61. package/src/theme/base.css +67 -2
  62. package/src/vite-plugin/SPEC.md +20 -2
  63. package/src/vite-plugin/index.ts +16 -2
  64. package/src/vite-plugin/layout-demo-crawler.ts +304 -33
  65. package/src/vite-plugin/splitter.ts +1 -0
  66. package/src/vite-plugin/virtual-modules.ts +16 -6
@@ -14,6 +14,8 @@
14
14
  * [data-honeydeck-color-mode="light"] { --honeydeck-primary: oklch(55% 0.25 145); }
15
15
  */
16
16
 
17
+ @import "@shikijs/magic-move/style.css";
18
+
17
19
  /* ── Source scanning ────────────────────────────────────────────────────────
18
20
  Tell Tailwind to scan the built-in layouts for utility class candidates.
19
21
  Without this, layouts live outside the Vite root (user's project dir) and
@@ -407,6 +409,59 @@
407
409
  color: inherit;
408
410
  }
409
411
 
412
+ .honeydeck-code-block .shiki-magic-move-container {
413
+ box-sizing: border-box;
414
+ min-height: 0;
415
+ margin: 0;
416
+ padding: 1.5em;
417
+ overflow: auto;
418
+ border-radius: 0;
419
+ font-family: var(--honeydeck-font-mono);
420
+ font-size: inherit;
421
+ line-height: inherit;
422
+ }
423
+
424
+ .honeydeck-code-block .shiki-magic-move-item {
425
+ opacity: var(--honeydeck-magic-code-token-opacity, 1);
426
+ transition-property: color, opacity, background-color, transform;
427
+ }
428
+
429
+ @keyframes honeydeck-magic-code-token-enter {
430
+ from {
431
+ opacity: 0;
432
+ }
433
+
434
+ to {
435
+ opacity: var(--honeydeck-magic-code-token-opacity, 1);
436
+ }
437
+ }
438
+
439
+ @keyframes honeydeck-magic-code-token-leave {
440
+ from {
441
+ opacity: var(--honeydeck-magic-code-token-opacity, 1);
442
+ }
443
+
444
+ to {
445
+ opacity: 0;
446
+ }
447
+ }
448
+
449
+ .honeydeck-code-block .shiki-magic-move-enter-active {
450
+ animation-name: honeydeck-magic-code-token-enter;
451
+ animation-duration: var(--smm-duration, 500ms);
452
+ animation-timing-function: var(--smm-easing, ease);
453
+ animation-delay: var(--smm-stagger, 0ms);
454
+ animation-fill-mode: both;
455
+ }
456
+
457
+ .honeydeck-code-block .shiki-magic-move-leave-active {
458
+ animation-name: honeydeck-magic-code-token-leave;
459
+ animation-duration: var(--smm-duration, 500ms);
460
+ animation-timing-function: var(--smm-easing, ease);
461
+ animation-delay: var(--smm-stagger, 0ms);
462
+ animation-fill-mode: both;
463
+ }
464
+
410
465
  .honeydeck-code-copy-button {
411
466
  position: absolute;
412
467
  top: 0.75em;
@@ -446,12 +501,22 @@
446
501
  color: var(--honeydeck-primary-foreground);
447
502
  }
448
503
 
504
+ @keyframes honeydeck-code-line-highlight-enter {
505
+ from {
506
+ opacity: var(--honeydeck-code-line-dim-opacity);
507
+ }
508
+
509
+ to {
510
+ opacity: 1;
511
+ }
512
+ }
513
+
449
514
  .honeydeck-code-block .line {
450
515
  transition: opacity 150ms ease-in;
451
516
  }
452
517
 
453
- .honeydeck-code-block .line[data-dim="1"] {
454
- opacity: var(--honeydeck-code-line-dim-opacity);
518
+ .honeydeck-code-block .line[data-highlight="1"] {
519
+ animation: honeydeck-code-line-highlight-enter 300ms ease-out both;
455
520
  }
456
521
 
457
522
  /* ── Plain code fallback (no shiki, unsupported lang) ────────────────────── */
@@ -67,6 +67,21 @@ Static files live in the project `public/` directory and are served from the web
67
67
 
68
68
  React components and built-in layouts may also import image assets through Vite (`webp`, `png`, `jpg`, `jpeg`, `svg`, `gif`). The built-in `Image` layout uses this for its bundled placeholder image.
69
69
 
70
+ ### Timeline-aware MDX Components
71
+
72
+ Honeydeck's MDX compilation assigns slide-local timeline steps to built-in timeline components.
73
+
74
+ Rules:
75
+
76
+ - `<Reveal>` adds one step to the slide timeline and receives an internal `at` prop during compilation.
77
+ - `<Reveal name="...">` exposes that reveal step as a same-slide target for `<RevealWith target="...">`.
78
+ - Reveal names must be literal non-empty strings. Dynamic `name` expressions are unsupported.
79
+ - Duplicate reveal names within one slide are compile errors. The same name may be reused on different slides.
80
+ - `<RevealWith>` does not add a step. It requires exactly one literal prop: `target="name"` or `at={n}`.
81
+ - `<RevealWith target="name">` may reference a named `<Reveal>` before or after it on the same slide. Missing targets are compile errors.
82
+ - `<RevealWith at={n}>` targets an existing 1-based step on the same slide. Non-literal, non-positive, and out-of-range values are compile errors.
83
+ - In development, invalid timeline component usage is surfaced with clear terminal and browser diagnostics without permanently killing the dev server. Production builds fail.
84
+
70
85
  ### Markdown Features
71
86
 
72
87
  Slide MDX supports GitHub-flavored Markdown pipe tables. Pipe tables render as real HTML tables in slides. The base theme styles slide tables with compact, full-width, token-based horizontal rules, bold headers, and light horizontal cell spacing so table Markdown is presentation-ready without custom CSS.
@@ -94,6 +109,7 @@ All settings use **camelCase**. No separate config file exists. Frontmatter pars
94
109
  | `pdfColorMode` | `"light" \| "dark"` | unset | Optional explicit PDF color mode; when unset, PDF falls back to pinned deck `colorMode`, then `light` |
95
110
  | `pdfSteps` | `"final" \| "all"` | `"final"` | Whether PDF includes all steps or final state |
96
111
  | `transition` | `boolean` | `true` | Enable crossfade transition between slides |
112
+ | `magicCodeDuration` | `number` | `800` | Default Magic Code animation duration in milliseconds |
97
113
  | `layouts` | `string` | built-in `@honeydeck/honeydeck/layouts` | Layout map module path |
98
114
  | `defaultLayout` | `string` | `"Default"` | Layout used when slide has no `layout:` |
99
115
  | `showSlideNumbers` | `boolean` | `false` | Show the current slide number in the bottom-right corner of slides |
@@ -109,6 +125,8 @@ All settings use **camelCase**. No separate config file exists. Frontmatter pars
109
125
 
110
126
  The first frontmatter block in the deck entry file is parsed as deck config. Deck-level keys are not copied into slide frontmatter. If that block also contains `layout:` plus layout-specific keys, those non-deck keys are emitted as first-slide frontmatter.
111
127
 
112
- Slide-level frontmatter is a frontmatter-only block after a slide separator and applies to the following slide. Imported MDX files are normal MDX modules and cannot set deck-level properties.
128
+ Slide-level frontmatter is a frontmatter-only block after a slide separator and applies to the following slide. Imported MDX files are normal MDX modules and cannot set deck-level properties. `magicCodeDuration` is deck-level only; the same key in slide-level frontmatter is treated as a normal layout prop and does not configure Magic Code.
129
+
130
+ Invalid `aspectRatio`, `colorMode`, and `pdfSteps` values fall back to defaults. Invalid `pdfColorMode` is ignored as unset, allowing the pinned `colorMode` fallback. `showSlideNumbers` is enabled only by literal `true`; `transition` is enabled unless literal `false`. Invalid explicit Magic Code block `duration` values are compile errors; invalid deck-level `magicCodeDuration` falls back to the default Magic Code duration.
113
131
 
114
- Invalid `aspectRatio`, `colorMode`, and `pdfSteps` values fall back to defaults. Invalid `pdfColorMode` is ignored as unset, allowing the pinned `colorMode` fallback. `showSlideNumbers` is enabled only by literal `true`; `transition` is enabled unless literal `false`.
132
+ During development, changes to deck-level frontmatter invalidate the virtual config and every compiled virtual slide module, because slide compilation can depend on deck settings such as `magicCodeDuration`. Layout-related virtual modules are invalidated as before so layout map and demo previews stay current.
@@ -136,6 +136,20 @@ export function honeydeckPlugin(
136
136
  // Use an array so more-specific subpath entries are matched before
137
137
  // the bare 'honeydeck' entry (Vite processes array aliases in order).
138
138
  alias: [
139
+ {
140
+ find: "@honeydeck/honeydeck/components/code-block/normal",
141
+ replacement: resolve(
142
+ __dirname,
143
+ "../runtime/components/NormalCodeBlock.tsx",
144
+ ),
145
+ },
146
+ {
147
+ find: "@honeydeck/honeydeck/components/code-block/magic",
148
+ replacement: resolve(
149
+ __dirname,
150
+ "../runtime/components/MagicCodeBlock.tsx",
151
+ ),
152
+ },
139
153
  {
140
154
  find: "@honeydeck/honeydeck/components/code-block",
141
155
  replacement: resolve(
@@ -350,8 +364,8 @@ export function honeydeckPlugin(
350
364
  mdx({
351
365
  // remarkFrontmatter strips YAML front-matter blocks so they don't
352
366
  // appear as raw text in the rendered output.
353
- // remarkStepNumbering assigns at={n} props to <Reveal>/<RevealGroup>
354
- // and records vfile.data.stepCount for each slide.
367
+ // remarkStepNumbering assigns timeline metadata to built-in MDX
368
+ // timeline components and records vfile.data.stepCount for each slide.
355
369
  remarkPlugins: [
356
370
  remarkFrontmatter,
357
371
  remarkGfm,
@@ -168,37 +168,18 @@ function crawlLayoutMapFile(
168
168
  continue;
169
169
 
170
170
  const layoutName = getLayoutName(property);
171
- const localName = getLayoutLocalIdentifier(property);
172
- if (!layoutName || !localName) continue;
171
+ if (!layoutName) continue;
173
172
 
174
- const binding = bindings.get(localName);
175
- if (!binding) {
176
- context.warnings.push(
177
- `Layout "${layoutName}" is not backed by a static import; demo auto-discovery skipped.`,
178
- );
179
- continue;
180
- }
181
-
182
- const modulePath = resolveImportedModule(
183
- mapPath,
184
- binding.moduleSpecifier,
185
- context.packageRoot,
186
- );
187
- if (!modulePath) {
188
- context.warnings.push(
189
- `Could not resolve layout module "${binding.moduleSpecifier}" for layout "${layoutName}".`,
190
- );
191
- continue;
192
- }
193
-
194
- context.watchedFiles.add(modulePath);
195
- const publicModuleSpecifier = toPublicSpecifier({
196
- entryPath: context.entryPath,
197
- packageRoot: context.packageRoot,
173
+ const reference = resolveLayoutModuleReference({
174
+ property,
175
+ layoutName,
176
+ bindings,
198
177
  mapPath,
199
- modulePath,
200
- originalSpecifier: binding.moduleSpecifier,
178
+ context,
201
179
  });
180
+ if (!reference) continue;
181
+
182
+ const { modulePath, publicModuleSpecifier } = reference;
202
183
 
203
184
  let demoMetadata: StaticDemoMetadata | undefined;
204
185
  try {
@@ -372,13 +353,303 @@ function getLayoutName(
372
353
  return null;
373
354
  }
374
355
 
375
- function getLayoutLocalIdentifier(
376
- property: ts.PropertyAssignment | ts.ShorthandPropertyAssignment,
356
+ type LayoutModuleReference = Pick<
357
+ DiscoveredLayoutDemo,
358
+ "modulePath" | "publicModuleSpecifier"
359
+ >;
360
+
361
+ function resolveLayoutModuleReference({
362
+ property,
363
+ layoutName,
364
+ bindings,
365
+ mapPath,
366
+ context,
367
+ }: {
368
+ property: ts.PropertyAssignment | ts.ShorthandPropertyAssignment;
369
+ layoutName: string;
370
+ bindings: Map<string, ImportBinding>;
371
+ mapPath: string;
372
+ context: CrawlContext;
373
+ }): LayoutModuleReference | null {
374
+ const value = ts.isShorthandPropertyAssignment(property)
375
+ ? property.name
376
+ : unwrapExpression(property.initializer);
377
+
378
+ if (ts.isIdentifier(value)) {
379
+ return resolveImportedLayoutReference(
380
+ value.text,
381
+ layoutName,
382
+ bindings,
383
+ mapPath,
384
+ context,
385
+ );
386
+ }
387
+
388
+ if (ts.isPropertyAccessExpression(value)) {
389
+ return resolveLayoutMapMemberReference(
390
+ value,
391
+ layoutName,
392
+ bindings,
393
+ mapPath,
394
+ context,
395
+ );
396
+ }
397
+
398
+ context.warnings.push(
399
+ `Layout "${layoutName}" is not backed by a static import; demo auto-discovery skipped.`,
400
+ );
401
+ return null;
402
+ }
403
+
404
+ function resolveImportedLayoutReference(
405
+ localName: string,
406
+ layoutName: string,
407
+ bindings: Map<string, ImportBinding>,
408
+ mapPath: string,
409
+ context: CrawlContext,
410
+ ): LayoutModuleReference | null {
411
+ const binding = bindings.get(localName);
412
+ if (!binding) {
413
+ context.warnings.push(
414
+ `Layout "${layoutName}" is not backed by a static import; demo auto-discovery skipped.`,
415
+ );
416
+ return null;
417
+ }
418
+
419
+ const importedModulePath = resolveImportedModule(
420
+ mapPath,
421
+ binding.moduleSpecifier,
422
+ context.packageRoot,
423
+ );
424
+ if (!importedModulePath) {
425
+ context.warnings.push(
426
+ `Could not resolve layout module "${binding.moduleSpecifier}" for layout "${layoutName}".`,
427
+ );
428
+ return null;
429
+ }
430
+
431
+ context.watchedFiles.add(importedModulePath);
432
+ const modulePath =
433
+ binding.importedName === "default"
434
+ ? importedModulePath
435
+ : (resolveNamedExportModulePath(
436
+ importedModulePath,
437
+ binding.importedName,
438
+ context.packageRoot,
439
+ context.watchedFiles,
440
+ ) ?? importedModulePath);
441
+ context.watchedFiles.add(modulePath);
442
+
443
+ return {
444
+ modulePath,
445
+ publicModuleSpecifier: toPublicSpecifier({
446
+ entryPath: context.entryPath,
447
+ packageRoot: context.packageRoot,
448
+ mapPath,
449
+ modulePath,
450
+ originalSpecifier: binding.moduleSpecifier,
451
+ }),
452
+ };
453
+ }
454
+
455
+ function resolveLayoutMapMemberReference(
456
+ memberExpression: ts.PropertyAccessExpression,
457
+ layoutName: string,
458
+ bindings: Map<string, ImportBinding>,
459
+ mapPath: string,
460
+ context: CrawlContext,
461
+ ): LayoutModuleReference | null {
462
+ const mapIdentifier = unwrapExpression(memberExpression.expression);
463
+ if (
464
+ !ts.isIdentifier(mapIdentifier) ||
465
+ !ts.isIdentifier(memberExpression.name)
466
+ ) {
467
+ context.warnings.push(
468
+ `Layout "${layoutName}" is not backed by a static import; demo auto-discovery skipped.`,
469
+ );
470
+ return null;
471
+ }
472
+
473
+ const binding = bindings.get(mapIdentifier.text);
474
+ if (!binding) {
475
+ context.warnings.push(
476
+ `Layout "${layoutName}" references layout map "${mapIdentifier.text}" without a static import; demo auto-discovery skipped.`,
477
+ );
478
+ return null;
479
+ }
480
+
481
+ if (binding.importedName !== "default") {
482
+ context.warnings.push(
483
+ `Layout "${layoutName}" references layout map "${mapIdentifier.text}", but only default-imported layout maps can be inspected.`,
484
+ );
485
+ return null;
486
+ }
487
+
488
+ const memberMapPath = resolveImportedModule(
489
+ mapPath,
490
+ binding.moduleSpecifier,
491
+ context.packageRoot,
492
+ );
493
+ if (!memberMapPath) {
494
+ context.warnings.push(
495
+ `Could not resolve layout map "${binding.moduleSpecifier}" for layout "${layoutName}".`,
496
+ );
497
+ return null;
498
+ }
499
+
500
+ const memberName = memberExpression.name.text;
501
+ const lookupContext = {
502
+ ...context,
503
+ visitedMaps: new Set<string>(),
504
+ };
505
+ const reference = crawlLayoutMapFile(memberMapPath, lookupContext).find(
506
+ (demo) => demo.layoutName === memberName,
507
+ );
508
+
509
+ if (!reference) {
510
+ context.warnings.push(
511
+ `Could not statically find layout "${memberName}" in layout map "${binding.moduleSpecifier}" for layout "${layoutName}".`,
512
+ );
513
+ }
514
+
515
+ return reference ?? null;
516
+ }
517
+
518
+ function resolveNamedExportModulePath(
519
+ modulePath: string,
520
+ exportedName: string,
521
+ packageRoot: string,
522
+ watchedFiles?: Set<string>,
523
+ visited = new Set<string>(),
377
524
  ): string | null {
378
- if (ts.isShorthandPropertyAssignment(property)) return property.name.text;
525
+ const visitKey = `${modulePath}#${exportedName}`;
526
+ if (visited.has(visitKey)) return null;
527
+ visited.add(visitKey);
528
+ watchedFiles?.add(modulePath);
529
+
530
+ let sourceFile: ts.SourceFile;
531
+ try {
532
+ sourceFile = parseFile(modulePath);
533
+ } catch {
534
+ return null;
535
+ }
536
+
537
+ for (const statement of sourceFile.statements) {
538
+ if (!ts.isExportDeclaration(statement)) continue;
539
+
540
+ if (!statement.exportClause) {
541
+ if (
542
+ !statement.moduleSpecifier ||
543
+ !ts.isStringLiteral(statement.moduleSpecifier)
544
+ )
545
+ continue;
546
+
547
+ const starModulePath = resolveImportedModule(
548
+ modulePath,
549
+ statement.moduleSpecifier.text,
550
+ packageRoot,
551
+ );
552
+ if (!starModulePath) continue;
553
+ watchedFiles?.add(starModulePath);
554
+ const resolved = resolveNamedExportModulePath(
555
+ starModulePath,
556
+ exportedName,
557
+ packageRoot,
558
+ watchedFiles,
559
+ visited,
560
+ );
561
+ if (resolved) return resolved;
562
+ continue;
563
+ }
564
+
565
+ if (!ts.isNamedExports(statement.exportClause)) continue;
566
+
567
+ for (const element of statement.exportClause.elements) {
568
+ if (element.name.text !== exportedName) continue;
569
+
570
+ if (
571
+ statement.moduleSpecifier &&
572
+ ts.isStringLiteral(statement.moduleSpecifier)
573
+ ) {
574
+ const importedModulePath = resolveImportedModule(
575
+ modulePath,
576
+ statement.moduleSpecifier.text,
577
+ packageRoot,
578
+ );
579
+ if (!importedModulePath) return null;
580
+ watchedFiles?.add(importedModulePath);
581
+
582
+ const importedName = element.propertyName?.text ?? element.name.text;
583
+ if (importedName === "default") return importedModulePath;
584
+
585
+ return (
586
+ resolveNamedExportModulePath(
587
+ importedModulePath,
588
+ importedName,
589
+ packageRoot,
590
+ watchedFiles,
591
+ visited,
592
+ ) ?? importedModulePath
593
+ );
594
+ }
595
+
596
+ const localName = element.propertyName?.text ?? element.name.text;
597
+ const binding = collectImportBindings(sourceFile).get(localName);
598
+ if (!binding) return modulePath;
599
+
600
+ const importedModulePath = resolveImportedModule(
601
+ modulePath,
602
+ binding.moduleSpecifier,
603
+ packageRoot,
604
+ );
605
+ if (!importedModulePath) return null;
606
+ if (binding.importedName === "default") return importedModulePath;
607
+
608
+ watchedFiles?.add(importedModulePath);
609
+ return (
610
+ resolveNamedExportModulePath(
611
+ importedModulePath,
612
+ binding.importedName,
613
+ packageRoot,
614
+ watchedFiles,
615
+ visited,
616
+ ) ?? importedModulePath
617
+ );
618
+ }
619
+ }
379
620
 
380
- const initializer = unwrapExpression(property.initializer);
381
- return ts.isIdentifier(initializer) ? initializer.text : null;
621
+ for (const statement of sourceFile.statements) {
622
+ if (!hasExportModifier(statement)) continue;
623
+ if (
624
+ (ts.isFunctionDeclaration(statement) ||
625
+ ts.isClassDeclaration(statement)) &&
626
+ statement.name?.text === exportedName
627
+ ) {
628
+ return modulePath;
629
+ }
630
+
631
+ if (ts.isVariableStatement(statement)) {
632
+ for (const declaration of statement.declarationList.declarations) {
633
+ if (
634
+ ts.isIdentifier(declaration.name) &&
635
+ declaration.name.text === exportedName
636
+ )
637
+ return modulePath;
638
+ }
639
+ }
640
+ }
641
+
642
+ return null;
643
+ }
644
+
645
+ function hasExportModifier(node: ts.Node): boolean {
646
+ return (
647
+ ts.canHaveModifiers(node) &&
648
+ (ts
649
+ .getModifiers(node)
650
+ ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ??
651
+ false)
652
+ );
382
653
  }
383
654
 
384
655
  function resolveImportedModule(
@@ -65,6 +65,7 @@ const DECK_FRONTMATTER_KEYS = new Set([
65
65
  "pdfColorMode",
66
66
  "pdfSteps",
67
67
  "transition",
68
+ "magicCodeDuration",
68
69
  "layouts",
69
70
  "defaultLayout",
70
71
  "showSlideNumbers",
@@ -349,7 +349,7 @@ export function virtualModulesPlugin(options: VirtualModulesOptions): Plugin {
349
349
  // e.g. '\0virtual:honeydeck/slide/2.mdx' → index 2
350
350
  const suffix = id.slice(RESOLVED_SLIDE_PREFIX.length); // '2.mdx'
351
351
  const index = parseInt(suffix.replace(".mdx", ""), 10);
352
- const { slides } = getResult();
352
+ const { deckFrontmatter, slides } = getResult();
353
353
  const slide = slides[index];
354
354
 
355
355
  if (!slide) {
@@ -368,7 +368,10 @@ export function virtualModulesPlugin(options: VirtualModulesOptions): Plugin {
368
368
  remarkGfm,
369
369
  remarkH1Extract,
370
370
  remarkStepNumbering,
371
- remarkShikiCodeBlocks,
371
+ [
372
+ remarkShikiCodeBlocks,
373
+ { magicCodeDuration: deckFrontmatter.magicCodeDuration },
374
+ ],
372
375
  ],
373
376
  jsxImportSource: "react",
374
377
  outputFormat: "program",
@@ -529,26 +532,33 @@ export function virtualModulesPlugin(options: VirtualModulesOptions): Plugin {
529
532
  splitResult = newResult; // commit new cache
530
533
 
531
534
  const affected: ModuleNode[] = [];
535
+ const invalidatedIds = new Set<string>();
532
536
 
533
537
  /** Invalidate a virtual module by its resolved ID, if it's in the graph. */
534
538
  const invalidate = (resolvedId: string): void => {
539
+ if (invalidatedIds.has(resolvedId)) return;
535
540
  const mod = ctx.server.moduleGraph.getModuleById(resolvedId);
536
541
  if (mod) {
542
+ invalidatedIds.add(resolvedId);
537
543
  ctx.server.moduleGraph.invalidateModule(mod);
538
544
  affected.push(mod);
539
545
  }
540
546
  };
541
547
 
542
- // Config changed?
543
- if (
548
+ const deckFrontmatterChanged =
544
549
  JSON.stringify(oldResult.deckFrontmatter) !==
545
- JSON.stringify(newResult.deckFrontmatter)
546
- ) {
550
+ JSON.stringify(newResult.deckFrontmatter);
551
+
552
+ // Config changed?
553
+ if (deckFrontmatterChanged) {
547
554
  invalidate(RESOLVED_CONFIG_ID);
548
555
  invalidate(RESOLVED_LAYOUTS_ID);
549
556
  for (let i = 0; i < layoutDemoSources.length; i++) {
550
557
  invalidate(`${RESOLVED_LAYOUT_DEMO_PREFIX}${i}.mdx`);
551
558
  }
559
+ for (let i = 0; i < newResult.slides.length; i++) {
560
+ invalidate(`${RESOLVED_SLIDE_PREFIX}${i}.mdx`);
561
+ }
552
562
  }
553
563
 
554
564
  // Per-slide diff — invalidate only slides that changed or were added/removed.