@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,87 @@
1
+ import { Children, type ReactNode } from "react";
2
+
3
+ export type KeyboardKey = ReactNode;
4
+
5
+ export type KeyboardProps = {
6
+ /** Key label or ordered shortcut key labels. */
7
+ keys?: KeyboardKey | readonly KeyboardKey[];
8
+ /** Single key label when `keys` is omitted. */
9
+ children?: ReactNode;
10
+ /** Separator rendered between array entries. */
11
+ separator?: ReactNode;
12
+ /** Additional CSS class for the outer element. */
13
+ className?: string;
14
+ };
15
+
16
+ function cn(...classes: (string | undefined | false)[]): string | undefined {
17
+ const value = classes.filter(Boolean).join(" ");
18
+ return value || undefined;
19
+ }
20
+
21
+ function renderKey(key: KeyboardKey): ReactNode {
22
+ return (
23
+ <kbd className="honeydeck-keyboard-key inline-flex min-w-[1.6em] items-center justify-center rounded-[0.28em] border border-b-[0.14em] border-border bg-surface px-[0.42em] pt-[0.18em] pb-[0.14em] font-mono text-[1em] font-semibold leading-none text-surface-foreground">
24
+ {key}
25
+ </kbd>
26
+ );
27
+ }
28
+
29
+ /**
30
+ * Displays one keyboard key or an ordered keyboard shortcut in slide prose.
31
+ *
32
+ * Use `children` for a single inline key label. Use `keys` with an array for
33
+ * ordered shortcuts, and change `separator` when the default `+` is too noisy
34
+ * for the shortcut style.
35
+ *
36
+ * ```mdx
37
+ * import { Keyboard } from '@honeydeck/honeydeck'
38
+ *
39
+ * Press <Keyboard>Esc</Keyboard> to close overview.
40
+ *
41
+ * Open command palette with <Keyboard keys={["Ctrl", "Shift", "P"]} />.
42
+ *
43
+ * Advance with <Keyboard keys="Space" />.
44
+ * ```
45
+ */
46
+ export function Keyboard({
47
+ keys,
48
+ children,
49
+ separator = "+",
50
+ className,
51
+ }: KeyboardProps) {
52
+ const value = keys ?? children;
53
+
54
+ if (Array.isArray(value)) {
55
+ return (
56
+ <span
57
+ className={cn(
58
+ "honeydeck-keyboard inline-flex items-baseline whitespace-nowrap align-baseline text-[0.82em] leading-none",
59
+ className,
60
+ )}
61
+ >
62
+ {Children.map(value, (key, index) => (
63
+ <span className="honeydeck-keyboard-part inline-flex items-center">
64
+ {index > 0 && (
65
+ <span className="honeydeck-keyboard-separator mx-[0.25em] font-body font-semibold text-surface-foreground opacity-[0.72]">
66
+ {separator}
67
+ </span>
68
+ )}
69
+ {renderKey(key)}
70
+ </span>
71
+ ))}
72
+ </span>
73
+ );
74
+ }
75
+
76
+ return (
77
+ <kbd
78
+ className={cn(
79
+ "honeydeck-keyboard inline-flex items-baseline whitespace-nowrap align-baseline text-[0.82em] leading-none",
80
+ "honeydeck-keyboard-key min-w-[1.6em] justify-center rounded-[0.28em] border border-b-[0.14em] border-border bg-surface px-[0.42em] pt-[0.18em] pb-[0.14em] font-mono text-[1em] font-semibold text-surface-foreground",
81
+ className,
82
+ )}
83
+ >
84
+ {value}
85
+ </kbd>
86
+ );
87
+ }
@@ -0,0 +1,203 @@
1
+ import {
2
+ Children,
3
+ type CSSProperties,
4
+ cloneElement,
5
+ isValidElement,
6
+ type ReactElement,
7
+ type ReactNode,
8
+ } from "react";
9
+
10
+ export type ListBullet = ReactNode;
11
+
12
+ export type ListBullets =
13
+ | false
14
+ | "none"
15
+ | null
16
+ | ListBullet
17
+ | readonly ListBullet[];
18
+
19
+ export type ListStyleProps = {
20
+ /**
21
+ * Custom bullet marker(s). Omit, `false`, `null`, or `"none"` to remove
22
+ * markers. Pass one marker for every level or an array by nesting level.
23
+ */
24
+ bullets?: ListBullets;
25
+ className?: string;
26
+ style?: CSSProperties;
27
+ children?: ReactNode;
28
+ };
29
+
30
+ type CommonElement = ReactElement<{
31
+ children?: ReactNode;
32
+ className?: string;
33
+ }>;
34
+
35
+ function cn(...classes: (string | undefined | false)[]): string | undefined {
36
+ const value = classes.filter(Boolean).join(" ");
37
+ return value || undefined;
38
+ }
39
+
40
+ function isCommonElement(node: ReactNode): node is CommonElement {
41
+ return isValidElement(node);
42
+ }
43
+
44
+ function isListItemElement(node: ReactNode): node is CommonElement {
45
+ return (
46
+ isCommonElement(node) && typeof node.type === "string" && node.type === "li"
47
+ );
48
+ }
49
+
50
+ function hasCustomBullets(bullets: ListBullets | undefined): boolean {
51
+ return !(
52
+ bullets === undefined ||
53
+ bullets === null ||
54
+ bullets === false ||
55
+ bullets === "none" ||
56
+ (Array.isArray(bullets) && bullets.length === 0)
57
+ );
58
+ }
59
+
60
+ function bulletForLevel(
61
+ bullets: ListBullets | undefined,
62
+ level: number,
63
+ ): ReactNode {
64
+ if (!hasCustomBullets(bullets)) return null;
65
+
66
+ if (Array.isArray(bullets)) {
67
+ const index = Math.min(level - 1, bullets.length - 1);
68
+ const bullet = bullets[index];
69
+ return bullet === undefined || bullet === false ? null : bullet;
70
+ }
71
+
72
+ return bullets;
73
+ }
74
+
75
+ function transformChildren(
76
+ children: ReactNode,
77
+ level: number,
78
+ bullets: ListBullets,
79
+ ): ReactNode {
80
+ return Children.map(children, (child) =>
81
+ transformNode(child, level, bullets),
82
+ );
83
+ }
84
+
85
+ function transformNode(
86
+ node: ReactNode,
87
+ level: number,
88
+ bullets: ListBullets,
89
+ ): ReactNode {
90
+ if (!isCommonElement(node)) return node;
91
+
92
+ const element = node;
93
+ if (
94
+ typeof element.type === "string" &&
95
+ (element.type === "ul" || element.type === "ol")
96
+ ) {
97
+ return transformList(element, level, bullets);
98
+ }
99
+
100
+ if (element.props.children === undefined) return element;
101
+
102
+ return cloneElement(element, {
103
+ children: transformChildren(element.props.children, level, bullets),
104
+ });
105
+ }
106
+
107
+ function transformList(
108
+ list: CommonElement,
109
+ level: number,
110
+ bullets: ListBullets,
111
+ ): ReactNode {
112
+ return cloneElement(list, {
113
+ className: cn(list.props.className, "honeydeck-list-style-list"),
114
+ children: Children.map(list.props.children, (child) => {
115
+ if (!isListItemElement(child))
116
+ return transformNode(child, level + 1, bullets);
117
+ return transformListItem(child, level, bullets);
118
+ }),
119
+ });
120
+ }
121
+
122
+ function transformListItem(
123
+ item: CommonElement,
124
+ level: number,
125
+ bullets: ListBullets,
126
+ ): ReactNode {
127
+ const bullet = bulletForLevel(bullets, level);
128
+ const hasBullet = bullet !== null && bullet !== undefined && bullet !== false;
129
+ const marker = hasBullet ? (
130
+ <span
131
+ className="honeydeck-list-style-marker inline-flex h-[1.7em] w-[0.9em] flex-[0_0_0.9em] items-center justify-center text-accent font-bold leading-none [&>svg]:h-[0.9em] [&>svg]:w-[0.9em] [&>svg]:stroke-[3]"
132
+ aria-hidden="true"
133
+ >
134
+ {bullet}
135
+ </span>
136
+ ) : null;
137
+
138
+ return cloneElement(item, {
139
+ className: cn(
140
+ item.props.className,
141
+ "honeydeck-list-style-item",
142
+ hasBullet &&
143
+ "honeydeck-list-style-item--with-marker flex items-start gap-[0.35em]",
144
+ ),
145
+ children: hasBullet ? (
146
+ <>
147
+ {marker}
148
+ <div className="honeydeck-list-style-content min-w-0 flex-1">
149
+ {transformChildren(item.props.children, level + 1, bullets)}
150
+ </div>
151
+ </>
152
+ ) : (
153
+ transformChildren(item.props.children, level + 1, bullets)
154
+ ),
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Wrap Markdown/HTML/JSX lists to remove native markers or render custom
160
+ * bullet markers per nesting level.
161
+ *
162
+ * Omit `bullets` to remove native markers while keeping list alignment. Pass
163
+ * one marker to reuse it for every nesting level, or an array to assign markers
164
+ * by level. Markers can be strings, icons, or any React node that fits inline.
165
+ *
166
+ * ```mdx
167
+ * import { ListStyle } from '@honeydeck/honeydeck'
168
+ *
169
+ * <ListStyle>
170
+ * - No marker
171
+ * - Still aligned
172
+ * </ListStyle>
173
+ *
174
+ * <ListStyle bullets={["01", "+"]}>
175
+ * - First level
176
+ * - Nested detail
177
+ * - Aligned custom markers
178
+ * </ListStyle>
179
+ * ```
180
+ */
181
+ export function ListStyle({
182
+ bullets,
183
+ className,
184
+ style,
185
+ children,
186
+ }: ListStyleProps) {
187
+ const customBullets = hasCustomBullets(bullets);
188
+
189
+ return (
190
+ <div
191
+ className={cn(
192
+ "honeydeck-list-style [&_:is(ul,ol)]:list-none [&_:is(ul,ol)]:pl-0 [&_li>:is(ul,ol)]:mt-[0.35em] [&_li>:is(ul,ol)]:pl-[1.5em] [&_.honeydeck-list-style-content>:is(ul,ol)]:mt-[0.35em] [&_.honeydeck-list-style-content>:is(ul,ol)]:pl-[1.5em]",
193
+ customBullets
194
+ ? "honeydeck-list-style--custom"
195
+ : "honeydeck-list-style--plain",
196
+ className,
197
+ )}
198
+ style={style}
199
+ >
200
+ {customBullets ? transformChildren(children, 1, bullets) : children}
201
+ </div>
202
+ );
203
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * NavBar — floating navigation controls for Honeydeck.
3
+ *
4
+ * ### Visibility
5
+ * - **Desktop** (pointer: fine): hidden by default, fades in when the
6
+ * cursor hovers near the bottom edge of the screen (within the hover zone).
7
+ * - **Touch/mobile** (pointer: coarse): always visible.
8
+ *
9
+ * ### Controls
10
+ * - Previous step (←)
11
+ * - Current slide number
12
+ * - Next step (→)
13
+ * - Overview grid toggle
14
+ * - Layouts reference
15
+ * - Docs website (opens new window)
16
+ * - Presenter mode (opens new window)
17
+ * - Fullscreen toggle
18
+ * - Mobile slide text selection toggle
19
+ * - Color mode cycle (system → light → dark → system)
20
+ */
21
+
22
+ import {
23
+ BookOpenTextIcon,
24
+ ChevronLeftIcon,
25
+ ChevronRightIcon,
26
+ ExternalLinkIcon,
27
+ LayoutGridIcon,
28
+ MaximizeIcon,
29
+ MinimizeIcon,
30
+ PresentationIcon,
31
+ RotateCcwIcon,
32
+ TextSelectIcon,
33
+ } from "lucide-react";
34
+ import { useEffect, useState } from "react";
35
+ import {
36
+ getNextStepRoute,
37
+ getPreviousStepRoute,
38
+ nextStep,
39
+ openDocsWebsite,
40
+ openPresenter,
41
+ openReference,
42
+ previousStep,
43
+ } from "../navigation.ts";
44
+ import type { Route } from "../router.ts";
45
+ import { slideData } from "../slideData.ts";
46
+ import type { ColorMode } from "./ColorModeCycleButton.tsx";
47
+ import { ColorModeCycleButton } from "./ColorModeCycleButton.tsx";
48
+ import { NavBarButton, navBarButtonClass } from "./NavBarButton.tsx";
49
+ import { NavBarDivider } from "./NavBarDivider.tsx";
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Types
53
+ // ---------------------------------------------------------------------------
54
+
55
+ export type NavBarProps = {
56
+ route: Route;
57
+ isOverview: boolean;
58
+ colorMode: ColorMode;
59
+ onToggleOverview: () => void;
60
+ onSetColorMode: (mode: ColorMode) => void;
61
+ isZoomed?: boolean;
62
+ onResetZoom?: () => void;
63
+ showTextSelectionToggle?: boolean;
64
+ isTextSelectionEnabled?: boolean;
65
+ onToggleTextSelection?: () => void;
66
+ };
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Component
70
+ // ---------------------------------------------------------------------------
71
+
72
+ export function NavBar({
73
+ route,
74
+ isOverview,
75
+ colorMode,
76
+ onToggleOverview,
77
+ onSetColorMode,
78
+ isZoomed = false,
79
+ onResetZoom,
80
+ showTextSelectionToggle = false,
81
+ isTextSelectionEnabled = false,
82
+ onToggleTextSelection,
83
+ }: NavBarProps) {
84
+ const navigationOptions = {
85
+ slideCount: slideData.length,
86
+ getStepCount: (slideIndex: number) => slideData[slideIndex]?.stepCount ?? 0,
87
+ };
88
+
89
+ // ISSUE-02: track fullscreen reactively via event listener
90
+ const [isFullscreen, setIsFullscreen] = useState(
91
+ !!document.fullscreenElement,
92
+ );
93
+ useEffect(() => {
94
+ function onFullscreenChange() {
95
+ setIsFullscreen(!!document.fullscreenElement);
96
+ }
97
+ document.addEventListener("fullscreenchange", onFullscreenChange);
98
+ return () =>
99
+ document.removeEventListener("fullscreenchange", onFullscreenChange);
100
+ }, []);
101
+
102
+ function goPrev() {
103
+ previousStep(route, navigationOptions);
104
+ }
105
+
106
+ function goNext() {
107
+ nextStep(route, navigationOptions);
108
+ }
109
+
110
+ function toggleFullscreen() {
111
+ if (document.fullscreenElement) {
112
+ document.exitFullscreen();
113
+ } else {
114
+ document.documentElement.requestFullscreen();
115
+ }
116
+ }
117
+
118
+ const canPrev = getPreviousStepRoute(route, navigationOptions) !== null;
119
+ const canNext = getNextStepRoute(route, navigationOptions) !== null;
120
+ const FullscreenIcon = isFullscreen ? MinimizeIcon : MaximizeIcon;
121
+
122
+ return (
123
+ // Hover zone — transparent, occupies the bottom strip
124
+ <div
125
+ className="honeydeck-nav-zone fixed bottom-0 left-0 right-0 h-20 z-50 flex items-end pointer-events-none"
126
+ data-honeydeck-no-swipe="true"
127
+ >
128
+ {/* Actual bar */}
129
+ <div className="honeydeck-nav-bar pointer-events-auto flex items-center gap-1 px-2 py-1.5 ml-6 mb-6 bg-black/70 backdrop-blur rounded-lg border border-white/10 shadow-[0_4px_24px_rgba(0,0,0,0.4)]">
130
+ {/* Prev step */}
131
+ <NavBarButton
132
+ onClick={goPrev}
133
+ label="Previous step (←)"
134
+ disabled={!canPrev}
135
+ >
136
+ <ChevronLeftIcon aria-hidden="true" size={16} />
137
+ </NavBarButton>
138
+
139
+ {/* Slide number */}
140
+ <span className="min-w-6 px-1 text-center font-sans text-sm tabular-nums text-white/60">
141
+ {route.slide}
142
+ </span>
143
+
144
+ {/* Next step */}
145
+ <NavBarButton
146
+ onClick={goNext}
147
+ label="Next step (→)"
148
+ disabled={!canNext}
149
+ >
150
+ <ChevronRightIcon aria-hidden="true" size={16} />
151
+ </NavBarButton>
152
+
153
+ <NavBarDivider />
154
+
155
+ {/* Overview */}
156
+ <NavBarButton
157
+ onClick={onToggleOverview}
158
+ label="Overview (o)"
159
+ active={isOverview}
160
+ >
161
+ <LayoutGridIcon aria-hidden="true" size={14} />
162
+ </NavBarButton>
163
+
164
+ {/* Layouts reference */}
165
+ <NavBarButton
166
+ onClick={() => openReference(route)}
167
+ label="Layouts reference"
168
+ >
169
+ <BookOpenTextIcon aria-hidden="true" size={16} />
170
+ </NavBarButton>
171
+
172
+ {/* Docs website */}
173
+ <NavBarButton onClick={openDocsWebsite} label="Docs website">
174
+ <ExternalLinkIcon aria-hidden="true" size={15} />
175
+ </NavBarButton>
176
+
177
+ {/* Presenter mode */}
178
+ {route.view !== "presenter" && (
179
+ <NavBarButton
180
+ onClick={() => openPresenter(route)}
181
+ label="Presenter mode (p)"
182
+ >
183
+ <PresentationIcon aria-hidden="true" size={16} />
184
+ </NavBarButton>
185
+ )}
186
+
187
+ {/* Fullscreen */}
188
+ <NavBarButton onClick={toggleFullscreen} label="Fullscreen (f)">
189
+ <FullscreenIcon aria-hidden="true" size={16} />
190
+ </NavBarButton>
191
+
192
+ {showTextSelectionToggle && onToggleTextSelection && (
193
+ <NavBarButton
194
+ onClick={onToggleTextSelection}
195
+ label={
196
+ isTextSelectionEnabled
197
+ ? "Disable slide text selection"
198
+ : "Enable slide text selection"
199
+ }
200
+ active={isTextSelectionEnabled}
201
+ >
202
+ <TextSelectIcon aria-hidden="true" size={16} />
203
+ </NavBarButton>
204
+ )}
205
+
206
+ <NavBarDivider />
207
+
208
+ {isZoomed && onResetZoom && (
209
+ <NavBarButton onClick={onResetZoom} label="Reset zoom">
210
+ <RotateCcwIcon aria-hidden="true" size={16} />
211
+ </NavBarButton>
212
+ )}
213
+
214
+ {/* Color mode */}
215
+ <ColorModeCycleButton
216
+ colorMode={colorMode}
217
+ onSetColorMode={onSetColorMode}
218
+ className={navBarButtonClass()}
219
+ />
220
+ </div>
221
+ </div>
222
+ );
223
+ }
@@ -0,0 +1,47 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export function NavBarButton({
4
+ onClick,
5
+ title,
6
+ label,
7
+ "aria-label": ariaLabel,
8
+ disabled,
9
+ active,
10
+ children,
11
+ }: {
12
+ onClick: () => void;
13
+ title?: string;
14
+ label?: string;
15
+ "aria-label"?: string;
16
+ disabled?: boolean;
17
+ active?: boolean;
18
+ children: ReactNode;
19
+ }) {
20
+ const accessibleLabel = ariaLabel ?? label ?? title;
21
+ const buttonTitle = title ?? label ?? ariaLabel;
22
+
23
+ return (
24
+ <button
25
+ type="button"
26
+ onClick={onClick}
27
+ title={buttonTitle}
28
+ aria-label={accessibleLabel}
29
+ disabled={disabled}
30
+ className={navBarButtonClass({ active, disabled })}
31
+ >
32
+ {children}
33
+ </button>
34
+ );
35
+ }
36
+
37
+ export function navBarButtonClass({
38
+ active = false,
39
+ disabled = false,
40
+ }: {
41
+ active?: boolean;
42
+ disabled?: boolean;
43
+ } = {}) {
44
+ return `w-8 h-8 rounded border-none flex items-center justify-center shrink-0 transition-[background,color] duration-100 ease-out ${
45
+ active ? "bg-white/15" : "bg-transparent"
46
+ } ${disabled ? "text-white/20" : "text-white/80"}`;
47
+ }
@@ -0,0 +1,3 @@
1
+ export function NavBarDivider() {
2
+ return <div className="w-px h-5 bg-white/12 mx-0.5 shrink-0" />;
3
+ }