@textcortex/slidewise 1.2.0 → 1.4.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/dist/index.mjs +5130 -4789
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/SlidewiseEditor.tsx +26 -1
- package/src/SlidewiseFileEditor.tsx +91 -1
- package/src/compound/DirtyContext.tsx +30 -0
- package/src/compound/SlidewiseRoot.tsx +197 -17
- package/src/compound/hooks.ts +93 -0
- package/src/compound/index.ts +47 -4
- package/src/compound/parts.tsx +4 -15
- package/src/compound/topbar/Export.tsx +65 -0
- package/src/compound/topbar/Group.tsx +36 -0
- package/src/compound/topbar/Play.tsx +42 -0
- package/src/compound/topbar/Redo.tsx +50 -0
- package/src/compound/topbar/Root.tsx +51 -0
- package/src/compound/topbar/Save.tsx +80 -0
- package/src/compound/topbar/Spacer.tsx +27 -0
- package/src/compound/topbar/ThemeToggle.tsx +49 -0
- package/src/compound/topbar/Title.tsx +79 -0
- package/src/compound/topbar/Undo.tsx +62 -0
- package/src/compound/topbar/index.tsx +107 -0
- package/src/compound/topbar/styles.ts +81 -0
- package/src/index.ts +15 -0
- package/src/lib/__tests__/api-extensions.test.ts +74 -0
- package/src/lib/store.ts +20 -10
- package/src/components/editor/TopBar.tsx +0 -253
package/src/compound/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* thin wrapper rendering this same tree:
|
|
6
6
|
*
|
|
7
7
|
* ```tsx
|
|
8
|
-
* <Slidewise.Root deck={deck} onChange={
|
|
8
|
+
* <Slidewise.Root deck={deck} onChange={...} onSave={...}>
|
|
9
9
|
* <Slidewise.TopBar />
|
|
10
10
|
* <Slidewise.Body>
|
|
11
11
|
* <Slidewise.SlideRail />
|
|
@@ -17,10 +17,19 @@
|
|
|
17
17
|
* </Slidewise.Root>
|
|
18
18
|
* ```
|
|
19
19
|
*
|
|
20
|
-
*
|
|
20
|
+
* For full control over the top bar (host buttons mixed in, subparts
|
|
21
|
+
* reordered, individual buttons skinned), drop down to its subparts:
|
|
21
22
|
*
|
|
22
23
|
* ```tsx
|
|
23
|
-
*
|
|
24
|
+
* <Slidewise.TopBar.Root>
|
|
25
|
+
* <MyExitButton />
|
|
26
|
+
* <Slidewise.TopBar.Spacer />
|
|
27
|
+
* <Slidewise.TopBar.Group>
|
|
28
|
+
* <Slidewise.TopBar.Undo />
|
|
29
|
+
* <Slidewise.TopBar.Redo />
|
|
30
|
+
* </Slidewise.TopBar.Group>
|
|
31
|
+
* <Slidewise.TopBar.Save />
|
|
32
|
+
* </Slidewise.TopBar.Root>
|
|
24
33
|
* ```
|
|
25
34
|
*/
|
|
26
35
|
export {
|
|
@@ -28,9 +37,9 @@ export {
|
|
|
28
37
|
type SlidewiseRootProps,
|
|
29
38
|
type SlidewiseRootHandle,
|
|
30
39
|
type HistoryState,
|
|
40
|
+
type SelectionSnapshot,
|
|
31
41
|
} from "./SlidewiseRoot";
|
|
32
42
|
export {
|
|
33
|
-
TopBar,
|
|
34
43
|
SlideRail,
|
|
35
44
|
Canvas,
|
|
36
45
|
BottomToolbar,
|
|
@@ -39,6 +48,21 @@ export {
|
|
|
39
48
|
CanvasFrame,
|
|
40
49
|
type RegionProps,
|
|
41
50
|
} from "./parts";
|
|
51
|
+
|
|
52
|
+
export { TopBar, type TopBarProps, type TopBarSlotId } from "./topbar";
|
|
53
|
+
export type {
|
|
54
|
+
TopBarRootProps,
|
|
55
|
+
TopBarTitleProps,
|
|
56
|
+
TopBarUndoProps,
|
|
57
|
+
TopBarRedoProps,
|
|
58
|
+
TopBarSaveProps,
|
|
59
|
+
TopBarPlayProps,
|
|
60
|
+
TopBarThemeToggleProps,
|
|
61
|
+
TopBarExportProps,
|
|
62
|
+
TopBarSpacerProps,
|
|
63
|
+
TopBarGroupProps,
|
|
64
|
+
} from "./topbar";
|
|
65
|
+
|
|
42
66
|
export {
|
|
43
67
|
useHostCallbacks,
|
|
44
68
|
type SlidewiseHostCallbacks,
|
|
@@ -49,3 +73,22 @@ export {
|
|
|
49
73
|
type SlidewiseIcons,
|
|
50
74
|
} from "./IconContext";
|
|
51
75
|
export { ReadOnlyProvider, useReadOnly } from "./ReadOnlyContext";
|
|
76
|
+
export { DirtyProvider, useDirty } from "./DirtyContext";
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Store hooks. Use these from host components anywhere under
|
|
80
|
+
* `<Slidewise.Root>` to read or write editor state without prop drilling.
|
|
81
|
+
*/
|
|
82
|
+
export {
|
|
83
|
+
useEditor,
|
|
84
|
+
useEditorStore,
|
|
85
|
+
useSlides,
|
|
86
|
+
useActiveSlide,
|
|
87
|
+
useActiveSlideId,
|
|
88
|
+
useSelection,
|
|
89
|
+
useSelectedElements,
|
|
90
|
+
useTheme,
|
|
91
|
+
useZoom,
|
|
92
|
+
usePlaying,
|
|
93
|
+
useHistory,
|
|
94
|
+
} from "./hooks";
|
package/src/compound/parts.tsx
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
-
import { TopBar as TopBarInternal } from "@/components/editor/TopBar";
|
|
3
2
|
import { SlideRail as SlideRailInternal } from "@/components/editor/SlideRail";
|
|
4
3
|
import { Canvas as CanvasInternal } from "@/components/editor/Canvas";
|
|
5
4
|
import { BottomToolbar as BottomToolbarInternal } from "@/components/editor/BottomToolbar";
|
|
6
|
-
import { useHostCallbacks } from "./HostContext";
|
|
7
5
|
|
|
8
6
|
/**
|
|
9
7
|
* Region-level compound parts. Each consumes the editor store via context,
|
|
10
8
|
* so any part can be omitted, wrapped, or replaced. None of these accept
|
|
11
9
|
* deck/onChange/onSave props — those live on `<Slidewise.Root>`.
|
|
10
|
+
*
|
|
11
|
+
* Note: `<Slidewise.TopBar>` is defined separately in `./topbar/` because
|
|
12
|
+
* it itself decomposes into subparts (`TopBar.Root`, `TopBar.Title`,
|
|
13
|
+
* `TopBar.Undo`, etc.) and ships a `hide` prop for per-button removal.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
export interface RegionProps {
|
|
@@ -16,19 +18,6 @@ export interface RegionProps {
|
|
|
16
18
|
style?: CSSProperties;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
/**
|
|
20
|
-
* The default top bar (title input, undo/redo, save, play, theme toggle,
|
|
21
|
-
* export). Reads host callbacks from context, so the Save and Export
|
|
22
|
-
* buttons fire the host's `onSave` / `onExport` from `<Slidewise.Root>`.
|
|
23
|
-
*
|
|
24
|
-
* Omit it from the tree to hide the whole bar; or render your own toolbar
|
|
25
|
-
* alongside `<Slidewise.Canvas>` for full control.
|
|
26
|
-
*/
|
|
27
|
-
export function TopBar(_props: RegionProps = {}) {
|
|
28
|
-
const { onSave, onExport } = useHostCallbacks();
|
|
29
|
-
return <TopBarInternal onSave={onSave} onExport={onExport} />;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
21
|
/**
|
|
33
22
|
* Left-side slide thumbnail rail with add/duplicate/delete.
|
|
34
23
|
*/
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { Download } from "lucide-react";
|
|
3
|
+
import { useEditorStore } from "@/lib/StoreProvider";
|
|
4
|
+
import { useHostCallbacks } from "../HostContext";
|
|
5
|
+
import { useIcons } from "../IconContext";
|
|
6
|
+
import { primaryBtnStyle, primaryHoverHandlers } from "./styles";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Export button. Calls the host's `onExport` (from `<Slidewise.Root
|
|
10
|
+
* onExport>`) with the current deck. If no host callback is registered,
|
|
11
|
+
* falls back to downloading a `.slidewise.json` of the deck.
|
|
12
|
+
*
|
|
13
|
+
* Visually emphasized vs the chrome buttons — uses `--primary-bg` so hosts
|
|
14
|
+
* retheming the primary surface get a consistent affordance.
|
|
15
|
+
*/
|
|
16
|
+
export interface TopBarExportProps {
|
|
17
|
+
className?: string;
|
|
18
|
+
style?: CSSProperties;
|
|
19
|
+
ariaLabel?: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Export({
|
|
25
|
+
className,
|
|
26
|
+
style,
|
|
27
|
+
ariaLabel = "Export",
|
|
28
|
+
label = "Export",
|
|
29
|
+
children,
|
|
30
|
+
}: TopBarExportProps = {}) {
|
|
31
|
+
const store = useEditorStore();
|
|
32
|
+
const { onExport: onExportHost } = useHostCallbacks();
|
|
33
|
+
const icons = useIcons();
|
|
34
|
+
|
|
35
|
+
const onClick = () => {
|
|
36
|
+
const deck = store.getState().deck;
|
|
37
|
+
if (onExportHost) {
|
|
38
|
+
onExportHost(deck);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const blob = new Blob([JSON.stringify(deck, null, 2)], {
|
|
42
|
+
type: "application/json",
|
|
43
|
+
});
|
|
44
|
+
const url = URL.createObjectURL(blob);
|
|
45
|
+
const a = document.createElement("a");
|
|
46
|
+
a.href = url;
|
|
47
|
+
a.download = `${(deck.title || "deck").replace(/[^a-z0-9-_]+/gi, "-")}.slidewise.json`;
|
|
48
|
+
a.click();
|
|
49
|
+
URL.revokeObjectURL(url);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
className={className}
|
|
56
|
+
aria-label={ariaLabel}
|
|
57
|
+
onClick={onClick}
|
|
58
|
+
style={{ ...primaryBtnStyle(), ...style }}
|
|
59
|
+
{...primaryHoverHandlers()}
|
|
60
|
+
>
|
|
61
|
+
{children ?? icons.export ?? <Download size={14} />}
|
|
62
|
+
{label}
|
|
63
|
+
</button>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CSSProperties, PropsWithChildren } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Visual cluster wrapper for a related set of buttons (e.g. Undo + Redo).
|
|
5
|
+
* Just a flex row with a small inner gap — keeps clusters visually together
|
|
6
|
+
* separate from the bar's default 10px gap.
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <Slidewise.TopBar.Group>
|
|
10
|
+
* <Slidewise.TopBar.Undo />
|
|
11
|
+
* <Slidewise.TopBar.Redo />
|
|
12
|
+
* </Slidewise.TopBar.Group>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export interface TopBarGroupProps {
|
|
16
|
+
className?: string;
|
|
17
|
+
style?: CSSProperties;
|
|
18
|
+
/** Gap between children inside the group. Default 2px. */
|
|
19
|
+
gap?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Group({
|
|
23
|
+
className,
|
|
24
|
+
style,
|
|
25
|
+
gap = 2,
|
|
26
|
+
children,
|
|
27
|
+
}: PropsWithChildren<TopBarGroupProps>) {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={className}
|
|
31
|
+
style={{ display: "flex", alignItems: "center", gap, ...style }}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { Play as PlayIcon } from "lucide-react";
|
|
3
|
+
import { useEditor } from "@/lib/StoreProvider";
|
|
4
|
+
import { useIcons } from "../IconContext";
|
|
5
|
+
import { chromeBtnStyle, hoverHandlers } from "./styles";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Play button. Enters slideshow / play mode. Always rendered (read-only
|
|
9
|
+
* viewers should still be able to present).
|
|
10
|
+
*/
|
|
11
|
+
export interface TopBarPlayProps {
|
|
12
|
+
className?: string;
|
|
13
|
+
style?: CSSProperties;
|
|
14
|
+
ariaLabel?: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Play({
|
|
20
|
+
className,
|
|
21
|
+
style,
|
|
22
|
+
ariaLabel = "Play",
|
|
23
|
+
label = "Play",
|
|
24
|
+
children,
|
|
25
|
+
}: TopBarPlayProps = {}) {
|
|
26
|
+
const play = useEditor((s) => s.play);
|
|
27
|
+
const icons = useIcons();
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
className={className}
|
|
33
|
+
aria-label={ariaLabel}
|
|
34
|
+
onClick={play}
|
|
35
|
+
style={{ ...chromeBtnStyle(), ...style }}
|
|
36
|
+
{...hoverHandlers()}
|
|
37
|
+
>
|
|
38
|
+
{children ?? icons.play ?? <PlayIcon size={14} />}
|
|
39
|
+
{label}
|
|
40
|
+
</button>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { Redo2 } from "lucide-react";
|
|
3
|
+
import { useEditor } from "@/lib/StoreProvider";
|
|
4
|
+
import { useIcons } from "../IconContext";
|
|
5
|
+
import { useReadOnly } from "../ReadOnlyContext";
|
|
6
|
+
import { iconBtnStyle, hoverHandlers } from "./styles";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Redo button. Calls `store.redo()`. Hidden in read-only mode. Disables
|
|
10
|
+
* itself when the redo stack is empty.
|
|
11
|
+
*/
|
|
12
|
+
export interface TopBarRedoProps {
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: CSSProperties;
|
|
15
|
+
ariaLabel?: string;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Redo({
|
|
20
|
+
className,
|
|
21
|
+
style,
|
|
22
|
+
ariaLabel = "Redo",
|
|
23
|
+
children,
|
|
24
|
+
}: TopBarRedoProps = {}) {
|
|
25
|
+
const redo = useEditor((s) => s.redo);
|
|
26
|
+
const canRedo = useEditor((s) => s.future.length > 0);
|
|
27
|
+
const icons = useIcons();
|
|
28
|
+
const readOnly = useReadOnly();
|
|
29
|
+
if (readOnly) return null;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
className={className}
|
|
35
|
+
title={ariaLabel}
|
|
36
|
+
aria-label={ariaLabel}
|
|
37
|
+
disabled={!canRedo}
|
|
38
|
+
onClick={redo}
|
|
39
|
+
style={{
|
|
40
|
+
...iconBtnStyle(),
|
|
41
|
+
cursor: canRedo ? "pointer" : "default",
|
|
42
|
+
opacity: canRedo ? 1 : 0.4,
|
|
43
|
+
...style,
|
|
44
|
+
}}
|
|
45
|
+
{...hoverHandlers()}
|
|
46
|
+
>
|
|
47
|
+
{children ?? icons.redo ?? <Redo2 size={16} />}
|
|
48
|
+
</button>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { CSSProperties, PropsWithChildren } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Container for the TopBar subparts. Owns the bar's height, padding, theme
|
|
5
|
+
* background, and shadow. Hosts replace this when they want a different
|
|
6
|
+
* shell shape (taller bar, different alignment, etc.); otherwise just render
|
|
7
|
+
* subparts inside it.
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <Slidewise.TopBar.Root>
|
|
11
|
+
* <Slidewise.TopBar.Title />
|
|
12
|
+
* <Slidewise.TopBar.Spacer />
|
|
13
|
+
* <Slidewise.TopBar.Save />
|
|
14
|
+
* </Slidewise.TopBar.Root>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export interface TopBarRootProps {
|
|
18
|
+
className?: string;
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Root({
|
|
23
|
+
className,
|
|
24
|
+
style,
|
|
25
|
+
children,
|
|
26
|
+
}: PropsWithChildren<TopBarRootProps>) {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
className={
|
|
30
|
+
className ? `slidewise-topbar ${className}` : "slidewise-topbar"
|
|
31
|
+
}
|
|
32
|
+
style={{
|
|
33
|
+
height: 56,
|
|
34
|
+
display: "flex",
|
|
35
|
+
alignItems: "center",
|
|
36
|
+
padding: "0 14px",
|
|
37
|
+
gap: 10,
|
|
38
|
+
background: "var(--slidewise-bar-bg, var(--app-bg))",
|
|
39
|
+
borderBottom: "1px solid var(--border)",
|
|
40
|
+
boxShadow: "var(--topbar-shadow)",
|
|
41
|
+
fontFamily: "Inter, system-ui, sans-serif",
|
|
42
|
+
position: "relative",
|
|
43
|
+
zIndex: 10,
|
|
44
|
+
color: "var(--ink)",
|
|
45
|
+
...style,
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useState, type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import { Save as SaveIcon } from "lucide-react";
|
|
3
|
+
import { useEditorStore } from "@/lib/StoreProvider";
|
|
4
|
+
import { useHostCallbacks } from "../HostContext";
|
|
5
|
+
import { useIcons } from "../IconContext";
|
|
6
|
+
import { useReadOnly } from "../ReadOnlyContext";
|
|
7
|
+
import { chromeBtnStyle, hoverHandlers } from "./styles";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Save button. Calls the host's `onSave` (from `<Slidewise.Root onSave>`)
|
|
11
|
+
* with the current deck. If no host callback is registered, falls back to
|
|
12
|
+
* `localStorage.setItem("slidewise-deck", ...)` so the dev shell works
|
|
13
|
+
* without wiring.
|
|
14
|
+
*
|
|
15
|
+
* Cycles through idle → saving → saved labels around the click; hosts that
|
|
16
|
+
* want their own loading affordance render their own button using
|
|
17
|
+
* `useHostCallbacks().onSave` directly.
|
|
18
|
+
*/
|
|
19
|
+
export interface TopBarSaveProps {
|
|
20
|
+
className?: string;
|
|
21
|
+
style?: CSSProperties;
|
|
22
|
+
ariaLabel?: string;
|
|
23
|
+
labels?: { idle?: string; saving?: string; saved?: string };
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Save({
|
|
28
|
+
className,
|
|
29
|
+
style,
|
|
30
|
+
ariaLabel = "Save",
|
|
31
|
+
labels,
|
|
32
|
+
children,
|
|
33
|
+
}: TopBarSaveProps = {}) {
|
|
34
|
+
const store = useEditorStore();
|
|
35
|
+
const { onSave: onSaveHost } = useHostCallbacks();
|
|
36
|
+
const icons = useIcons();
|
|
37
|
+
const readOnly = useReadOnly();
|
|
38
|
+
const [phase, setPhase] = useState<"idle" | "saving" | "saved">("idle");
|
|
39
|
+
|
|
40
|
+
if (readOnly) return null;
|
|
41
|
+
|
|
42
|
+
const onClick = async () => {
|
|
43
|
+
setPhase("saving");
|
|
44
|
+
const deck = store.getState().deck;
|
|
45
|
+
try {
|
|
46
|
+
if (onSaveHost) {
|
|
47
|
+
await onSaveHost(deck);
|
|
48
|
+
} else {
|
|
49
|
+
try {
|
|
50
|
+
localStorage.setItem("slidewise-deck", JSON.stringify(deck));
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
setTimeout(() => setPhase("saved"), 320);
|
|
54
|
+
setTimeout(() => setPhase("idle"), 1600);
|
|
55
|
+
} catch {
|
|
56
|
+
setPhase("idle");
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const text =
|
|
61
|
+
phase === "saving"
|
|
62
|
+
? (labels?.saving ?? "Saving…")
|
|
63
|
+
: phase === "saved"
|
|
64
|
+
? (labels?.saved ?? "Saved")
|
|
65
|
+
: (labels?.idle ?? "Save");
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
className={className}
|
|
71
|
+
aria-label={ariaLabel}
|
|
72
|
+
onClick={onClick}
|
|
73
|
+
style={{ ...chromeBtnStyle(), ...style }}
|
|
74
|
+
{...hoverHandlers()}
|
|
75
|
+
>
|
|
76
|
+
{children ?? icons.save ?? <SaveIcon size={14} />}
|
|
77
|
+
{text}
|
|
78
|
+
</button>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pushes subsequent subparts to the far edge of `<TopBar.Root>`.
|
|
5
|
+
*
|
|
6
|
+
* ```tsx
|
|
7
|
+
* <Slidewise.TopBar.Root>
|
|
8
|
+
* <MyExitButton />
|
|
9
|
+
* <Slidewise.TopBar.Spacer />
|
|
10
|
+
* <Slidewise.TopBar.Save />
|
|
11
|
+
* </Slidewise.TopBar.Root>
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export interface TopBarSpacerProps {
|
|
15
|
+
className?: string;
|
|
16
|
+
style?: CSSProperties;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Spacer({ className, style }: TopBarSpacerProps = {}) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={className}
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
style={{ flex: 1, minWidth: 0, ...style }}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { Sun, Moon } from "lucide-react";
|
|
3
|
+
import { useEditor } from "@/lib/StoreProvider";
|
|
4
|
+
import { useIcons } from "../IconContext";
|
|
5
|
+
import { iconBtnStyle, hoverHandlers } from "./styles";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Light/dark theme toggle. Always rendered; theme is a viewer concern,
|
|
9
|
+
* not an editing one.
|
|
10
|
+
*/
|
|
11
|
+
export interface TopBarThemeToggleProps {
|
|
12
|
+
className?: string;
|
|
13
|
+
style?: CSSProperties;
|
|
14
|
+
labels?: { toggleToDark?: string; toggleToLight?: string };
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ThemeToggle({
|
|
19
|
+
className,
|
|
20
|
+
style,
|
|
21
|
+
labels,
|
|
22
|
+
children,
|
|
23
|
+
}: TopBarThemeToggleProps = {}) {
|
|
24
|
+
const theme = useEditor((s) => s.theme);
|
|
25
|
+
const toggleTheme = useEditor((s) => s.toggleTheme);
|
|
26
|
+
const icons = useIcons();
|
|
27
|
+
|
|
28
|
+
const label =
|
|
29
|
+
theme === "dark"
|
|
30
|
+
? (labels?.toggleToLight ?? "Light mode")
|
|
31
|
+
: (labels?.toggleToDark ?? "Dark mode");
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
className={className}
|
|
37
|
+
title={label}
|
|
38
|
+
aria-label={label}
|
|
39
|
+
onClick={toggleTheme}
|
|
40
|
+
style={{ ...iconBtnStyle(), ...style }}
|
|
41
|
+
{...hoverHandlers()}
|
|
42
|
+
>
|
|
43
|
+
{children ??
|
|
44
|
+
(theme === "dark"
|
|
45
|
+
? (icons.themeLight ?? <Sun size={16} />)
|
|
46
|
+
: (icons.themeDark ?? <Moon size={16} />))}
|
|
47
|
+
</button>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { Sparkles } from "lucide-react";
|
|
3
|
+
import { useEditor } from "@/lib/StoreProvider";
|
|
4
|
+
import { useIcons } from "../IconContext";
|
|
5
|
+
import { useReadOnly } from "../ReadOnlyContext";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Deck title input wrapped in the "Smart" pill. Reads + writes
|
|
9
|
+
* `deck.title` on the editor store. In read-only mode the input is locked.
|
|
10
|
+
*
|
|
11
|
+
* Hosts that want a different title affordance (a static label, a custom
|
|
12
|
+
* input style) drop this subpart and render their own — `useEditor` is
|
|
13
|
+
* exported so the host's component can read `s.deck.title` and call
|
|
14
|
+
* `s.setTitle(...)`.
|
|
15
|
+
*/
|
|
16
|
+
export interface TopBarTitleProps {
|
|
17
|
+
className?: string;
|
|
18
|
+
style?: CSSProperties;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Title({ className, style }: TopBarTitleProps = {}) {
|
|
22
|
+
const title = useEditor((s) => s.deck.title);
|
|
23
|
+
const setTitle = useEditor((s) => s.setTitle);
|
|
24
|
+
const icons = useIcons();
|
|
25
|
+
const readOnly = useReadOnly();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
className={
|
|
30
|
+
className
|
|
31
|
+
? `slidewise-topbar-title ${className}`
|
|
32
|
+
: "slidewise-topbar-title"
|
|
33
|
+
}
|
|
34
|
+
style={{
|
|
35
|
+
flex: 1,
|
|
36
|
+
display: "flex",
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
justifyContent: "center",
|
|
39
|
+
gap: 8,
|
|
40
|
+
minWidth: 0,
|
|
41
|
+
...style,
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<span
|
|
45
|
+
style={{
|
|
46
|
+
display: "inline-flex",
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
gap: 4,
|
|
49
|
+
padding: "3px 8px",
|
|
50
|
+
background: "var(--smart-grad)",
|
|
51
|
+
color: "var(--smart-fg)",
|
|
52
|
+
borderRadius: 999,
|
|
53
|
+
fontSize: 11,
|
|
54
|
+
fontWeight: 600,
|
|
55
|
+
letterSpacing: 0.2,
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{icons.smart ?? <Sparkles size={11} />}
|
|
59
|
+
Smart
|
|
60
|
+
</span>
|
|
61
|
+
<input
|
|
62
|
+
aria-label="Deck title"
|
|
63
|
+
value={title}
|
|
64
|
+
readOnly={readOnly}
|
|
65
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
66
|
+
style={{
|
|
67
|
+
background: "transparent",
|
|
68
|
+
border: "none",
|
|
69
|
+
fontSize: 14,
|
|
70
|
+
fontWeight: 500,
|
|
71
|
+
color: "var(--ink)",
|
|
72
|
+
textAlign: "center",
|
|
73
|
+
minWidth: 240,
|
|
74
|
+
maxWidth: 520,
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { Undo2 } from "lucide-react";
|
|
3
|
+
import { useEditor } from "@/lib/StoreProvider";
|
|
4
|
+
import { useIcons } from "../IconContext";
|
|
5
|
+
import { useReadOnly } from "../ReadOnlyContext";
|
|
6
|
+
import { iconBtnStyle, hoverHandlers } from "./styles";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Undo button. Calls `store.undo()`. Hidden in read-only mode. Disables
|
|
10
|
+
* itself when the undo stack is empty.
|
|
11
|
+
*
|
|
12
|
+
* Hosts replace this when they need a different shape (e.g. their own
|
|
13
|
+
* tooltip primitive or icon set with custom layout). The hook + store
|
|
14
|
+
* action are public, so a host replacement is trivially:
|
|
15
|
+
*
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const MyUndo = () => {
|
|
18
|
+
* const undo = useEditor(s => s.undo);
|
|
19
|
+
* const canUndo = useEditor(s => s.history.length > 0);
|
|
20
|
+
* return <MyIconButton disabled={!canUndo} onClick={undo}>↶</MyIconButton>;
|
|
21
|
+
* };
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export interface TopBarUndoProps {
|
|
25
|
+
className?: string;
|
|
26
|
+
style?: CSSProperties;
|
|
27
|
+
ariaLabel?: string;
|
|
28
|
+
children?: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function Undo({
|
|
32
|
+
className,
|
|
33
|
+
style,
|
|
34
|
+
ariaLabel = "Undo",
|
|
35
|
+
children,
|
|
36
|
+
}: TopBarUndoProps = {}) {
|
|
37
|
+
const undo = useEditor((s) => s.undo);
|
|
38
|
+
const canUndo = useEditor((s) => s.history.length > 0);
|
|
39
|
+
const icons = useIcons();
|
|
40
|
+
const readOnly = useReadOnly();
|
|
41
|
+
if (readOnly) return null;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
className={className}
|
|
47
|
+
title={ariaLabel}
|
|
48
|
+
aria-label={ariaLabel}
|
|
49
|
+
disabled={!canUndo}
|
|
50
|
+
onClick={undo}
|
|
51
|
+
style={{
|
|
52
|
+
...iconBtnStyle(),
|
|
53
|
+
cursor: canUndo ? "pointer" : "default",
|
|
54
|
+
opacity: canUndo ? 1 : 0.4,
|
|
55
|
+
...style,
|
|
56
|
+
}}
|
|
57
|
+
{...hoverHandlers()}
|
|
58
|
+
>
|
|
59
|
+
{children ?? icons.undo ?? <Undo2 size={16} />}
|
|
60
|
+
</button>
|
|
61
|
+
);
|
|
62
|
+
}
|