@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.
- package/AGENTS.md +25 -0
- package/DEVELOPMENT.md +522 -0
- package/LICENSE +21 -0
- package/Readme.md +49 -0
- package/SPEC.md +88 -0
- package/docs/components.md +63 -0
- package/docs/configuration.md +91 -0
- package/docs/getting-started.md +116 -0
- package/docs/kit-authoring.md +207 -0
- package/docs/kits.md +387 -0
- package/docs/local-development.md +95 -0
- package/docs/mermaid.md +198 -0
- package/docs/mobile.md +108 -0
- package/docs/navigation.md +93 -0
- package/docs/next-steps.md +377 -0
- package/docs/pdf-export.md +91 -0
- package/docs/presenter-mode.md +104 -0
- package/docs/slides.md +130 -0
- package/docs/slidev-migration.md +42 -0
- package/docs/steps-and-reveals.md +171 -0
- package/package.json +134 -0
- package/skills/SPEC.md +21 -0
- package/skills/honeydeck/SKILL.md +65 -0
- package/skills/presentation-writing/SKILL.md +75 -0
- package/skills/slidev-migration/SKILL.md +153 -0
- package/src/SPEC.md +89 -0
- package/src/assets.d.ts +30 -0
- package/src/cli/SPEC.md +230 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/banner.ts +9 -0
- package/src/cli/bin.js +5 -0
- package/src/cli/build.ts +229 -0
- package/src/cli/deck-path.ts +32 -0
- package/src/cli/dev.ts +263 -0
- package/src/cli/index.ts +126 -0
- package/src/cli/init.ts +369 -0
- package/src/cli/pdf.ts +923 -0
- package/src/cli/skill.ts +75 -0
- package/src/cli/templates/SPEC.md +70 -0
- package/src/cli/templates/deck-mdx.ts +15 -0
- package/src/cli/templates/package-json.ts +36 -0
- package/src/cli/templates/sparkle-button.ts +15 -0
- package/src/cli/templates/starter/components/SparkleButton.tsx +84 -0
- package/src/cli/templates/starter/deck.mdx +153 -0
- package/src/cli/templates/starter/styles.css +14 -0
- package/src/cli/templates/styles-css.ts +14 -0
- package/src/defaults.ts +1 -0
- package/src/layouts/ColorModeImage.tsx +55 -0
- package/src/layouts/SPEC.md +393 -0
- package/src/layouts/SlideFrame.tsx +48 -0
- package/src/layouts/bee/Blank.tsx +12 -0
- package/src/layouts/bee/Cover.tsx +70 -0
- package/src/layouts/bee/Default.tsx +42 -0
- package/src/layouts/bee/Image/Image.tsx +151 -0
- package/src/layouts/bee/Image/placeholder-dark.webp +0 -0
- package/src/layouts/bee/Image/placeholder-vertical-dark.webp +0 -0
- package/src/layouts/bee/Image/placeholder-vertical.webp +0 -0
- package/src/layouts/bee/Image/placeholder.webp +0 -0
- package/src/layouts/bee/ImageLeft.tsx +27 -0
- package/src/layouts/bee/ImageRight.tsx +27 -0
- package/src/layouts/bee/ImageSide.tsx +107 -0
- package/src/layouts/bee/Section.tsx +40 -0
- package/src/layouts/bee/TwoCol.tsx +108 -0
- package/src/layouts/bee/index.ts +40 -0
- package/src/layouts/clean/Blank.tsx +12 -0
- package/src/layouts/clean/Cover.tsx +58 -0
- package/src/layouts/clean/Default.tsx +33 -0
- package/src/layouts/clean/Image/Image.tsx +103 -0
- package/src/layouts/clean/ImageLeft.tsx +27 -0
- package/src/layouts/clean/ImageRight.tsx +27 -0
- package/src/layouts/clean/ImageSide.tsx +113 -0
- package/src/layouts/clean/Section.tsx +35 -0
- package/src/layouts/clean/TwoCol.tsx +63 -0
- package/src/layouts/clean/index.ts +40 -0
- package/src/layouts/index.ts +60 -0
- package/src/layouts/placeholders.ts +9 -0
- package/src/layouts/utils.ts +13 -0
- package/src/remark/SPEC.md +49 -0
- package/src/remark/h1-extract.ts +124 -0
- package/src/remark/index.ts +4 -0
- package/src/remark/shiki-code-blocks.ts +325 -0
- package/src/remark/step-numbering.ts +412 -0
- package/src/runtime/Deck.tsx +533 -0
- package/src/runtime/SPEC.md +256 -0
- package/src/runtime/SlideCanvas.tsx +95 -0
- package/src/runtime/TimelineContext.tsx +122 -0
- package/src/runtime/app-shell/index.html +31 -0
- package/src/runtime/app-shell/main.tsx +42 -0
- package/src/runtime/aspectRatio.ts +34 -0
- package/src/runtime/colorMode.ts +23 -0
- package/src/runtime/components/BrowserFrame.tsx +233 -0
- package/src/runtime/components/Button.tsx +57 -0
- package/src/runtime/components/CodeBlock.tsx +210 -0
- package/src/runtime/components/ColorModeCycleButton.tsx +59 -0
- package/src/runtime/components/ErrorBoundary.tsx +125 -0
- package/src/runtime/components/Keyboard.tsx +87 -0
- package/src/runtime/components/ListStyle.tsx +203 -0
- package/src/runtime/components/NavBar.tsx +223 -0
- package/src/runtime/components/NavBarButton.tsx +47 -0
- package/src/runtime/components/NavBarDivider.tsx +3 -0
- package/src/runtime/components/Notes.tsx +171 -0
- package/src/runtime/components/Reveal.tsx +82 -0
- package/src/runtime/components/RevealGroup.tsx +193 -0
- package/src/runtime/components/SPEC.md +263 -0
- package/src/runtime/components/SlideNumberBadge.tsx +11 -0
- package/src/runtime/components/TimelineSteps.tsx +115 -0
- package/src/runtime/components/index.ts +55 -0
- package/src/runtime/index.ts +42 -0
- package/src/runtime/inputOwnership.ts +68 -0
- package/src/runtime/keyboardTarget.ts +7 -0
- package/src/runtime/lastSlideRoute.ts +56 -0
- package/src/runtime/navigation.ts +211 -0
- package/src/runtime/router.ts +157 -0
- package/src/runtime/slideData.ts +137 -0
- package/src/runtime/sync.ts +267 -0
- package/src/runtime/types.ts +182 -0
- package/src/runtime/useKeyboardNav.ts +138 -0
- package/src/runtime/useSwipeNav.ts +257 -0
- package/src/runtime/views/DocsView.tsx +74 -0
- package/src/runtime/views/OverviewView.tsx +386 -0
- package/src/runtime/views/PresenterNotesPanel.tsx +76 -0
- package/src/runtime/views/PresenterView.tsx +340 -0
- package/src/runtime/views/SPEC.md +152 -0
- package/src/runtime/views/docs/ComponentsTab.tsx +178 -0
- package/src/runtime/views/docs/DocsHeader.tsx +101 -0
- package/src/runtime/views/docs/Intro.tsx +20 -0
- package/src/runtime/views/docs/LayoutsTab.tsx +324 -0
- package/src/runtime/views/docs/ThemeTab.tsx +110 -0
- package/src/runtime/views/index.ts +7 -0
- package/src/runtime/views/overviewGrid.ts +106 -0
- package/src/runtime/views/presenterPreview.ts +27 -0
- package/src/runtime/virtual-modules.d.ts +98 -0
- package/src/theme/SPEC.md +179 -0
- package/src/theme/base.css +623 -0
- package/src/theme/bee.css +35 -0
- package/src/theme/clean.css +38 -0
- package/src/vite-plugin/SPEC.md +114 -0
- package/src/vite-plugin/component-doc-crawler.ts +350 -0
- package/src/vite-plugin/deck-loader.ts +148 -0
- package/src/vite-plugin/index.ts +373 -0
- package/src/vite-plugin/layout-demo-crawler.ts +802 -0
- package/src/vite-plugin/splitter.ts +353 -0
- package/src/vite-plugin/token-manifest.ts +163 -0
- 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
|
+
}
|