@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,178 @@
1
+ import * as componentReference from "virtual:honeydeck/components";
2
+ import type { ComponentDoc, ComponentPropDoc } from "../../types.ts";
3
+ import { Intro } from "./Intro.tsx";
4
+
5
+ const { componentDocWarnings, componentDocs, componentMap, componentNames } =
6
+ componentReference;
7
+
8
+ type ComponentEntry = {
9
+ name: string;
10
+ doc?: ComponentDoc;
11
+ };
12
+
13
+ function buildComponentEntries(
14
+ names: string[],
15
+ docs: Record<string, ComponentDoc>,
16
+ ): ComponentEntry[] {
17
+ const entries: ComponentEntry[] = [];
18
+
19
+ for (const name of names) {
20
+ if (!componentMap[name]) continue;
21
+
22
+ const entry: ComponentEntry = { name };
23
+ const doc = docs[name];
24
+ if (doc) entry.doc = doc;
25
+ entries.push(entry);
26
+ }
27
+
28
+ return entries;
29
+ }
30
+
31
+ function ComponentPropsTable({ props }: { props: ComponentPropDoc[] }) {
32
+ if (props.length === 0) {
33
+ return (
34
+ <div className="rounded-md border border-dashed border-border bg-background p-4 text-sm leading-6 text-surface-foreground/65 shadow-sm">
35
+ <div className="font-medium text-surface-foreground">
36
+ No props documented
37
+ </div>
38
+ <div className="mt-1">
39
+ Add an exported props type with JSDoc comments to document component
40
+ params here.
41
+ </div>
42
+ </div>
43
+ );
44
+ }
45
+
46
+ return (
47
+ <div className="overflow-x-auto rounded-md border border-border bg-background shadow-sm">
48
+ <table className="w-full min-w-180 table-fixed border-collapse text-left text-sm">
49
+ <thead className="border-b border-border bg-surface/60 text-xs uppercase tracking-wider text-surface-foreground/60">
50
+ <tr>
51
+ <th className="w-[16%] px-4 py-3 font-medium">Param</th>
52
+ <th className="w-[20%] px-4 py-3 font-medium">Type</th>
53
+ <th className="w-[12%] px-4 py-3 font-medium">Default</th>
54
+ <th className="w-[52%] px-4 py-3 font-medium">Description</th>
55
+ </tr>
56
+ </thead>
57
+ <tbody className="divide-y divide-border">
58
+ {props.map((prop) => (
59
+ <tr key={prop.name}>
60
+ <td className="px-4 py-3 align-top">
61
+ <code className="font-mono text-sm font-medium text-surface-foreground">
62
+ {prop.name}
63
+ {prop.required ? "" : "?"}
64
+ </code>
65
+ </td>
66
+ <td className="px-4 py-3 align-top">
67
+ <code className="break-words font-mono text-xs text-surface-foreground/70">
68
+ {prop.type}
69
+ </code>
70
+ </td>
71
+ <td className="px-4 py-3 align-top">
72
+ {prop.defaultValue !== undefined ? (
73
+ <code className="break-words font-mono text-xs text-surface-foreground/70">
74
+ {prop.defaultValue}
75
+ </code>
76
+ ) : (
77
+ <span className="text-surface-foreground/40">—</span>
78
+ )}
79
+ </td>
80
+ <td className="px-4 py-3 align-top leading-5 text-surface-foreground/70">
81
+ {prop.description || "—"}
82
+ </td>
83
+ </tr>
84
+ ))}
85
+ </tbody>
86
+ </table>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ export function ComponentsTab() {
92
+ const components = buildComponentEntries(componentNames, componentDocs);
93
+
94
+ function scrollToComponent(name: string) {
95
+ document
96
+ .getElementById(componentSectionId(name))
97
+ ?.scrollIntoView({ block: "start" });
98
+ }
99
+
100
+ return (
101
+ <>
102
+ <Intro title="Components">
103
+ Built-in components exported from{" "}
104
+ <code className="rounded-xs bg-surface px-1 py-0.5 font-mono">
105
+ honeydeck/components
106
+ </code>
107
+ , documented from their exported JSDoc comments and props types.
108
+ </Intro>
109
+
110
+ {componentDocWarnings.length > 0 && (
111
+ <div className="mb-6 rounded-md border border-border bg-surface p-4 text-sm text-foreground/70">
112
+ <div className="font-medium text-foreground">
113
+ Component docs discovery warnings
114
+ </div>
115
+ <ul className="mb-0 mt-2 list-disc pl-5">
116
+ {componentDocWarnings.map((warning) => (
117
+ <li key={warning}>{warning}</li>
118
+ ))}
119
+ </ul>
120
+ </div>
121
+ )}
122
+
123
+ <div className="grid gap-8 xl:grid-cols-[12rem_minmax(0,1fr)]">
124
+ <aside className="xl:block">
125
+ <nav
126
+ className="sticky top-28 flex gap-1 overflow-x-auto pb-2 xl:block xl:space-y-1 xl:overflow-visible xl:pb-0"
127
+ aria-label="Built-in components"
128
+ >
129
+ {components.map((entry) => (
130
+ <button
131
+ key={entry.name}
132
+ type="button"
133
+ onClick={() => scrollToComponent(entry.name)}
134
+ className="whitespace-nowrap rounded-sm px-2.5 py-2 text-left text-sm font-medium text-foreground/60 hover:bg-surface hover:text-foreground xl:block xl:w-full"
135
+ >
136
+ {entry.name}
137
+ </button>
138
+ ))}
139
+ </nav>
140
+ </aside>
141
+
142
+ <div className="min-w-0 ">
143
+ {components.map((entry) => (
144
+ <section
145
+ key={entry.name}
146
+ id={componentSectionId(entry.name)}
147
+ className="scroll-mt-28 py-8 first:pt-0"
148
+ >
149
+ <h2 className="m-0 text-3xl font-semibold tracking-tight text-foreground">
150
+ {entry.name}
151
+ </h2>
152
+
153
+ <div className="mt-5">
154
+ {entry.doc ? (
155
+ <div className="honeydeck-docs-content">
156
+ <entry.doc.Component />
157
+ </div>
158
+ ) : (
159
+ <div className="text-sm leading-6 text-surface-foreground/65">
160
+ No documentation comment found for this component.
161
+ </div>
162
+ )}
163
+ </div>
164
+
165
+ <div className="mt-6">
166
+ <ComponentPropsTable props={entry.doc?.props ?? []} />
167
+ </div>
168
+ </section>
169
+ ))}
170
+ </div>
171
+ </div>
172
+ </>
173
+ );
174
+ }
175
+
176
+ function componentSectionId(name: string): string {
177
+ return `component-${name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
178
+ }
@@ -0,0 +1,101 @@
1
+ import { ExternalLinkIcon } from "lucide-react";
2
+ import {
3
+ type ColorMode,
4
+ ColorModeCycleButton,
5
+ } from "../../components/ColorModeCycleButton.tsx";
6
+ import { getRememberedSlideRoute } from "../../lastSlideRoute.ts";
7
+ import type { KitTab } from "../../router.ts";
8
+ import { navigate } from "../../router.ts";
9
+
10
+ export type DocsHeaderProps = {
11
+ tab: KitTab;
12
+ colorMode: ColorMode;
13
+ onSetColorMode: (mode: ColorMode) => void;
14
+ };
15
+
16
+ function tabClass(active: boolean): string {
17
+ return active
18
+ ? "border-primary text-foreground"
19
+ : "border-transparent text-foreground/55 hover:text-foreground";
20
+ }
21
+
22
+ export function DocsHeader({
23
+ tab,
24
+ colorMode,
25
+ onSetColorMode,
26
+ }: DocsHeaderProps) {
27
+ return (
28
+ <header className="sticky top-0 z-10 border-b border-border bg-background/95 backdrop-blur">
29
+ <div className="mx-auto flex max-w-7xl flex-col gap-3 px-5 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-8">
30
+ <div>
31
+ <div className="text-lg font-semibold tracking-tight">
32
+ Honeydeck Reference
33
+ </div>
34
+ <div className="text-xs text-foreground/50">/#/theme</div>
35
+ </div>
36
+
37
+ <nav
38
+ className="flex flex-wrap gap-x-6 gap-y-1"
39
+ aria-label="Docs reference sections"
40
+ >
41
+ <button
42
+ type="button"
43
+ onClick={() =>
44
+ navigate({ view: "kit", slide: 1, step: 0, kitTab: "theme" })
45
+ }
46
+ className={`border-b-2 py-2 text-sm font-medium ${tabClass(tab === "theme")}`}
47
+ >
48
+ Theme tokens
49
+ </button>
50
+ <button
51
+ type="button"
52
+ onClick={() =>
53
+ navigate({ view: "kit", slide: 1, step: 0, kitTab: "layouts" })
54
+ }
55
+ className={`border-b-2 py-2 text-sm font-medium ${tabClass(tab === "layouts")}`}
56
+ >
57
+ Layouts
58
+ </button>
59
+ <button
60
+ type="button"
61
+ onClick={() =>
62
+ navigate({
63
+ view: "kit",
64
+ slide: 1,
65
+ step: 0,
66
+ kitTab: "components",
67
+ })
68
+ }
69
+ className={`border-b-2 py-2 text-sm font-medium ${tabClass(tab === "components")}`}
70
+ >
71
+ Components
72
+ </button>
73
+ </nav>
74
+
75
+ <div className="flex items-center gap-4">
76
+ <a
77
+ href="https://honeydeck.dev"
78
+ target="_blank"
79
+ rel="noreferrer"
80
+ className="inline-flex items-center gap-1.5 text-sm font-medium text-foreground/65 underline underline-offset-4 hover:text-foreground"
81
+ >
82
+ Docs
83
+ <ExternalLinkIcon aria-hidden="true" size={14} />
84
+ </a>
85
+ <ColorModeCycleButton
86
+ colorMode={colorMode}
87
+ onSetColorMode={onSetColorMode}
88
+ className="flex h-8 w-8 items-center justify-center rounded-sm border border-border bg-background text-foreground"
89
+ />
90
+ <button
91
+ type="button"
92
+ onClick={() => navigate(getRememberedSlideRoute())}
93
+ className="rounded-sm border border-border bg-surface px-3 py-1.5 text-sm font-medium text-surface-foreground hover:border-primary/50 hover:bg-primary/10 hover:text-foreground"
94
+ >
95
+ Back to slides
96
+ </button>
97
+ </div>
98
+ </div>
99
+ </header>
100
+ );
101
+ }
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export function Intro({
4
+ title,
5
+ children,
6
+ }: {
7
+ title: string;
8
+ children: ReactNode;
9
+ }) {
10
+ return (
11
+ <div className="mb-8 pb-6">
12
+ <h1 className="m-0 text-4xl font-semibold tracking-tight text-foreground">
13
+ {title}
14
+ </h1>
15
+ <p className="mt-3 max-w-3xl text-md leading-6 text-foreground/70">
16
+ {children}
17
+ </p>
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,324 @@
1
+ import * as layoutReference from "virtual:honeydeck/layouts";
2
+ import { CheckIcon, CopyIcon } from "lucide-react";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { BASE_HEIGHT, BASE_WIDTH, resolveLayout } from "../../slideData.ts";
5
+ import { TimelineProvider } from "../../TimelineContext.tsx";
6
+ import type { CompiledLayoutDemo, LayoutPropDoc } from "../../types.ts";
7
+ import { Intro } from "./Intro.tsx";
8
+
9
+ const { layoutDemos, layoutDemoWarnings, layoutNames, layoutPropDocs } =
10
+ layoutReference;
11
+
12
+ type LayoutEntry = {
13
+ name: string;
14
+ demo?: CompiledLayoutDemo;
15
+ propDocs: LayoutPropDoc[];
16
+ snippet?: string;
17
+ };
18
+
19
+ function buildLayoutEntries(
20
+ names: string[],
21
+ demos: Record<string, CompiledLayoutDemo>,
22
+ propDocs: Record<string, LayoutPropDoc[]>,
23
+ ): LayoutEntry[] {
24
+ const entries: LayoutEntry[] = [];
25
+
26
+ for (const name of names) {
27
+ const demo = demos[name] as CompiledLayoutDemo | undefined;
28
+ const snippet =
29
+ typeof demo?.mdx === "string" && demo.mdx.trim().length > 0
30
+ ? demo.mdx.trim()
31
+ : undefined;
32
+ const entry: LayoutEntry = {
33
+ name,
34
+ propDocs: propDocs[name] ?? [],
35
+ };
36
+ if (demo) entry.demo = demo;
37
+ if (snippet) entry.snippet = snippet;
38
+ entries.push(entry);
39
+ }
40
+
41
+ return entries;
42
+ }
43
+
44
+ function UsageCode({ code }: { code: string }) {
45
+ return (
46
+ <pre className="m-0 min-h-0 max-w-full flex-1 overflow-x-auto overflow-y-auto p-3 font-mono text-sm leading-6 text-surface-foreground">
47
+ {code}
48
+ </pre>
49
+ );
50
+ }
51
+
52
+ function PropsTable({ entry }: { entry: LayoutEntry }) {
53
+ const props: LayoutPropDoc[] = [
54
+ {
55
+ name: "layout",
56
+ type: JSON.stringify(entry.name),
57
+ required: true,
58
+ description: `Selects the ${entry.name} layout for this slide.`,
59
+ },
60
+ ...entry.propDocs,
61
+ ];
62
+
63
+ return (
64
+ <div className="min-h-0 flex-1 overflow-auto p-3">
65
+ <table className="w-full min-w-0 table-fixed border-collapse text-left text-sm">
66
+ <thead className="text-xs uppercase tracking-wider text-surface-foreground/55">
67
+ <tr className="border-b border-border">
68
+ <th className="w-[30%] px-2 py-2 font-medium">Prop</th>
69
+ <th className="w-[25%] px-2 py-2 font-medium">Type</th>
70
+ <th className="w-[45%] px-2 py-2 font-medium">Description</th>
71
+ </tr>
72
+ </thead>
73
+ <tbody className="divide-y divide-border">
74
+ {props.map((prop) => (
75
+ <tr key={prop.name}>
76
+ <td className="px-2 py-2 align-top">
77
+ <code className="font-mono text-sm font-medium text-surface-foreground">
78
+ {prop.name}
79
+ {prop.required ? "" : "?"}
80
+ </code>
81
+ </td>
82
+ <td className="px-2 py-2 align-top">
83
+ <code className="break-words font-mono text-xs text-surface-foreground/70">
84
+ {prop.type}
85
+ </code>
86
+ </td>
87
+ <td className="px-2 py-2 align-top leading-5 text-surface-foreground/70">
88
+ {prop.description || "—"}
89
+ </td>
90
+ </tr>
91
+ ))}
92
+ </tbody>
93
+ </table>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ function LayoutReferenceTabs({ entry }: { entry: LayoutEntry }) {
99
+ const [copied, setCopied] = useState(false);
100
+ const [tab, setTab] = useState<"usage" | "props">("usage");
101
+
102
+ async function copy() {
103
+ if (!entry.snippet) return;
104
+ try {
105
+ await navigator.clipboard.writeText(entry.snippet);
106
+ setCopied(true);
107
+ window.setTimeout(() => setCopied(false), 1200);
108
+ } catch {
109
+ // Ignore clipboard failures; the snippet remains selectable.
110
+ }
111
+ }
112
+
113
+ return (
114
+ <div className="flex h-full min-h-0 min-w-0 w-full flex-col rounded-md border border-border bg-background shadow-sm">
115
+ <div className="flex flex-shrink-0 items-center justify-between gap-3 border-b border-border bg-surface/60 px-3 py-2">
116
+ <div
117
+ className="inline-flex rounded-sm border border-border bg-background p-1 shadow-xs"
118
+ role="tablist"
119
+ aria-label={`${entry.name} reference`}
120
+ >
121
+ <button
122
+ type="button"
123
+ role="tab"
124
+ aria-selected={tab === "usage"}
125
+ onClick={() => setTab("usage")}
126
+ className={`rounded-xs px-3 py-1.5 text-xs font-semibold uppercase tracking-wider transition ${
127
+ tab === "usage"
128
+ ? "bg-primary text-primary-foreground shadow-sm"
129
+ : "text-surface-foreground/60 hover:bg-surface hover:text-surface-foreground"
130
+ }`}
131
+ >
132
+ Usage
133
+ </button>
134
+ <button
135
+ type="button"
136
+ role="tab"
137
+ aria-selected={tab === "props"}
138
+ onClick={() => setTab("props")}
139
+ className={`rounded-xs px-3 py-1.5 text-xs font-semibold uppercase tracking-wider transition ${
140
+ tab === "props"
141
+ ? "bg-primary text-primary-foreground shadow-sm"
142
+ : "text-surface-foreground/60 hover:bg-surface hover:text-surface-foreground"
143
+ }`}
144
+ >
145
+ Props
146
+ </button>
147
+ </div>
148
+ {tab === "usage" && entry.snippet && (
149
+ <button
150
+ type="button"
151
+ onClick={copy}
152
+ className="inline-flex items-center gap-1.5 rounded-sm border border-border bg-background px-3 py-1.5 text-xs font-semibold text-surface-foreground shadow-xs transition hover:border-primary/55 hover:bg-primary hover:text-primary-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
153
+ >
154
+ {copied ? (
155
+ <CheckIcon className="size-3.5" aria-hidden="true" />
156
+ ) : (
157
+ <CopyIcon className="size-3.5" aria-hidden="true" />
158
+ )}
159
+ <span>{copied ? "Copied" : "Copy"}</span>
160
+ </button>
161
+ )}
162
+ </div>
163
+ {tab === "usage" ? (
164
+ entry.snippet ? (
165
+ <UsageCode code={entry.snippet} />
166
+ ) : (
167
+ <MissingSnippet />
168
+ )
169
+ ) : (
170
+ <PropsTable entry={entry} />
171
+ )}
172
+ </div>
173
+ );
174
+ }
175
+
176
+ function MissingSnippet() {
177
+ return (
178
+ <div className="h-full min-h-0 min-w-0 w-full overflow-y-auto rounded-md border border-dashed border-border bg-background p-4 text-sm leading-6 text-surface-foreground/65 shadow-sm">
179
+ <div className="font-medium text-surface-foreground">
180
+ No demo MDX provided
181
+ </div>
182
+ <div className="mt-1 max-w-full">
183
+ Add{" "}
184
+ <code className="rounded-xs bg-surface px-1 py-0.5 font-mono">mdx</code>{" "}
185
+ to this layout's{" "}
186
+ <code className="rounded-xs bg-surface px-1 py-0.5 font-mono">
187
+ demo
188
+ </code>{" "}
189
+ export to show copyable usage here.
190
+ </div>
191
+ </div>
192
+ );
193
+ }
194
+
195
+ function LayoutPreview({ entry }: { entry: LayoutEntry }) {
196
+ const boxRef = useRef<HTMLDivElement>(null);
197
+ const [scale, setScale] = useState(0.2);
198
+
199
+ useEffect(() => {
200
+ const box = boxRef.current;
201
+ if (!box) return;
202
+
203
+ const observer = new ResizeObserver(([entry]) => {
204
+ if (!entry) return;
205
+ const { width, height } = entry.contentRect;
206
+ setScale(Math.min(width / BASE_WIDTH, height / BASE_HEIGHT));
207
+ });
208
+
209
+ observer.observe(box);
210
+ return () => observer.disconnect();
211
+ }, []);
212
+
213
+ if (!entry.demo) {
214
+ return (
215
+ <div className="flex aspect-video w-full min-w-0 items-center justify-center overflow-hidden rounded-md border border-dashed border-border bg-background text-center text-sm text-foreground/55">
216
+ <div>
217
+ <div className="font-medium text-foreground/70">
218
+ No demo MDX provided
219
+ </div>
220
+ <div className="mt-1">
221
+ Export{" "}
222
+ <code className="rounded-xs bg-surface px-1 py-0.5 font-mono">
223
+ demo.mdx
224
+ </code>{" "}
225
+ from this layout to preview it here.
226
+ </div>
227
+ </div>
228
+ </div>
229
+ );
230
+ }
231
+
232
+ const DemoComponent = entry.demo.Component;
233
+ const LayoutComponent = resolveLayout(entry.demo.layoutName || entry.name);
234
+
235
+ return (
236
+ <div
237
+ ref={boxRef}
238
+ className="flex aspect-video w-full min-w-0 items-center justify-center overflow-hidden rounded-md"
239
+ >
240
+ <div
241
+ className="relative overflow-hidden bg-background"
242
+ style={{ width: BASE_WIDTH * scale, height: BASE_HEIGHT * scale }}
243
+ >
244
+ <div
245
+ className="honeydeck-slide-canvas absolute left-0 top-0"
246
+ style={{
247
+ width: BASE_WIDTH,
248
+ height: BASE_HEIGHT,
249
+ transform: `scale(${scale})`,
250
+ transformOrigin: "top left",
251
+ }}
252
+ >
253
+ <TimelineProvider
254
+ stepIndex={0}
255
+ stepCount={entry.demo.stepCount}
256
+ showFutureSteps={true}
257
+ >
258
+ <LayoutComponent
259
+ title={entry.demo.title || null}
260
+ frontmatter={entry.demo.frontmatter}
261
+ rawChildren={<DemoComponent />}
262
+ >
263
+ <DemoComponent />
264
+ </LayoutComponent>
265
+ </TimelineProvider>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ );
270
+ }
271
+
272
+ export function LayoutsTab() {
273
+ const layouts = buildLayoutEntries(layoutNames, layoutDemos, layoutPropDocs);
274
+
275
+ return (
276
+ <>
277
+ <Intro title="Layouts">
278
+ Currently available layouts rendered with their{" "}
279
+ <code className="rounded-xs bg-surface px-1 py-0.5 font-mono">
280
+ demo.mdx
281
+ </code>{" "}
282
+ source and explicit copyable MDX snippets.
283
+ </Intro>
284
+
285
+ {layoutDemoWarnings.length > 0 && (
286
+ <div className="mb-6 rounded-md border border-border bg-surface p-4 text-sm text-foreground/70">
287
+ <div className="font-medium text-foreground">
288
+ Demo discovery warnings
289
+ </div>
290
+ <ul className="mb-0 mt-2 list-disc pl-5">
291
+ {layoutDemoWarnings.map((warning) => (
292
+ <li key={warning}>{warning}</li>
293
+ ))}
294
+ </ul>
295
+ </div>
296
+ )}
297
+
298
+ <div className="w-full min-w-0 space-y-6">
299
+ {layouts.map((entry) => (
300
+ <section
301
+ key={entry.name}
302
+ className="w-full min-w-0 overflow-hidden rounded-md border border-border bg-surface"
303
+ >
304
+ <header className="border-b border-border px-4 py-3">
305
+ <h2 className="m-0 text-2xl font-semibold text-foreground">
306
+ {entry.name}
307
+ </h2>
308
+ </header>
309
+ <div className="grid min-w-0 items-stretch gap-6 p-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,0.7fr)]">
310
+ <div className="min-w-0">
311
+ <LayoutPreview entry={entry} />
312
+ </div>
313
+ <div className="relative h-80 min-w-0 lg:h-auto lg:min-h-0">
314
+ <div className="h-full min-h-0 min-w-0 lg:absolute lg:inset-0">
315
+ <LayoutReferenceTabs entry={entry} />
316
+ </div>
317
+ </div>
318
+ </div>
319
+ </section>
320
+ ))}
321
+ </div>
322
+ </>
323
+ );
324
+ }