@textcortex/slidewise 1.5.0 → 1.7.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.
@@ -0,0 +1,67 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import { useEditor } from "@/lib/StoreProvider";
3
+ import type { Slide } from "@/lib/types";
4
+ import { Item } from "./Item";
5
+ import { Thumbnail } from "./Thumbnail";
6
+ import { Number as SlideNumber } from "./Number";
7
+
8
+ /**
9
+ * Iterates `deck.slides` and renders each one. Pass a render-prop child
10
+ * for full control over what each row contains; omit the child to get
11
+ * the default `<Item><Thumbnail /><Number /></Item>` layout.
12
+ *
13
+ * ```tsx
14
+ * // Default
15
+ * <Slidewise.SlideRail.List />
16
+ *
17
+ * // Custom — add a per-row context menu
18
+ * <Slidewise.SlideRail.List>
19
+ * {(slide) => (
20
+ * <Slidewise.SlideRail.Item slide={slide}>
21
+ * <Slidewise.SlideRail.Thumbnail />
22
+ * <Slidewise.SlideRail.Number />
23
+ * <MyContextMenu slide={slide} />
24
+ * </Slidewise.SlideRail.Item>
25
+ * )}
26
+ * </Slidewise.SlideRail.List>
27
+ * ```
28
+ */
29
+ export interface SlideRailListProps {
30
+ className?: string;
31
+ style?: CSSProperties;
32
+ /**
33
+ * Optional render-prop. Receives each slide + its zero-based index;
34
+ * return the row content. When omitted, the default row layout
35
+ * is used.
36
+ */
37
+ children?: (slide: Slide, index: number) => ReactNode;
38
+ }
39
+
40
+ export function List({ className, style, children }: SlideRailListProps) {
41
+ const slides = useEditor((s) => s.deck.slides);
42
+
43
+ return (
44
+ <div
45
+ className={className}
46
+ style={{
47
+ flex: 1,
48
+ overflowY: "auto",
49
+ padding: "12px 0",
50
+ ...style,
51
+ }}
52
+ >
53
+ {slides.map((slide, index) =>
54
+ children ? (
55
+ // Render-prop branch: host returns whatever they want, typically
56
+ // wrapping their JSX in a SlideRail.Item to wire selection.
57
+ <div key={slide.id}>{children(slide, index)}</div>
58
+ ) : (
59
+ <Item key={slide.id} slide={slide}>
60
+ <Thumbnail />
61
+ <SlideNumber />
62
+ </Item>
63
+ )
64
+ )}
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,57 @@
1
+ import type { CSSProperties } from "react";
2
+ import { useSlideRailItem } from "./ItemContext";
3
+
4
+ /**
5
+ * Renders the slide's "01" / "02" badge in the top-left corner of the rail
6
+ * item. Reads the current slide's index from `<SlideRail.Item>`'s context.
7
+ *
8
+ * Render after `<SlideRail.Thumbnail>` so it stacks on top (or position
9
+ * it manually via `style`).
10
+ */
11
+ export interface SlideRailNumberProps {
12
+ className?: string;
13
+ style?: CSSProperties;
14
+ /**
15
+ * Format the displayed number. Default is zero-padded to width 2
16
+ * (`01`, `02`, …, `10`).
17
+ */
18
+ format?: (index: number) => string;
19
+ }
20
+
21
+ const defaultFormat = (i: number) => String(i + 1).padStart(2, "0");
22
+
23
+ export function Number({
24
+ className,
25
+ style,
26
+ format = defaultFormat,
27
+ }: SlideRailNumberProps = {}) {
28
+ const { index, isCurrent } = useSlideRailItem();
29
+
30
+ return (
31
+ <span
32
+ className={className}
33
+ aria-hidden="true"
34
+ style={{
35
+ position: "absolute",
36
+ left: 20,
37
+ top: 6,
38
+ zIndex: 2,
39
+ width: 22,
40
+ height: 22,
41
+ borderRadius: 5,
42
+ background: isCurrent ? "var(--ink)" : "#6B7280",
43
+ color: isCurrent ? "var(--app-bg)" : "#fff",
44
+ display: "flex",
45
+ alignItems: "center",
46
+ justifyContent: "center",
47
+ fontSize: 11,
48
+ fontWeight: 700,
49
+ fontFamily: "Inter, system-ui, sans-serif",
50
+ pointerEvents: "none",
51
+ ...style,
52
+ }}
53
+ >
54
+ {format(index)}
55
+ </span>
56
+ );
57
+ }
@@ -0,0 +1,54 @@
1
+ import type { CSSProperties, PropsWithChildren } from "react";
2
+
3
+ export const SLIDERAIL_DEFAULT_WIDTH = 168;
4
+
5
+ /**
6
+ * Container for the slide rail subparts. Owns the rail's width, surface
7
+ * color, and divider. Hosts replace this when they want a different rail
8
+ * geometry (wider, narrower, full-bleed, etc.) — otherwise just render
9
+ * subparts inside it.
10
+ *
11
+ * ```tsx
12
+ * <Slidewise.SlideRail.Root>
13
+ * <Slidewise.SlideRail.Header />
14
+ * <Slidewise.SlideRail.List />
15
+ * <Slidewise.SlideRail.AddButton />
16
+ * </Slidewise.SlideRail.Root>
17
+ * ```
18
+ */
19
+ export interface SlideRailRootProps {
20
+ className?: string;
21
+ style?: CSSProperties;
22
+ /** Rail width in pixels. Defaults to 168. */
23
+ width?: number | string;
24
+ }
25
+
26
+ export function Root({
27
+ className,
28
+ style,
29
+ width = SLIDERAIL_DEFAULT_WIDTH,
30
+ children,
31
+ }: PropsWithChildren<SlideRailRootProps>) {
32
+ return (
33
+ <div
34
+ className={
35
+ className ? `slidewise-rail ${className}` : "slidewise-rail"
36
+ }
37
+ style={{
38
+ width,
39
+ flexShrink: 0,
40
+ background: "var(--slidewise-bg-rail, var(--rail-bg))",
41
+ borderRight: "1px solid var(--border)",
42
+ boxShadow: "var(--rail-shadow)",
43
+ display: "flex",
44
+ flexDirection: "column",
45
+ fontFamily: "Inter, system-ui, sans-serif",
46
+ overflow: "hidden",
47
+ zIndex: 5,
48
+ ...style,
49
+ }}
50
+ >
51
+ {children}
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,58 @@
1
+ import type { CSSProperties } from "react";
2
+ import { SlideView } from "@/components/editor/SlideView";
3
+ import { SLIDE_W } from "@/lib/types";
4
+ import { useSlideRailItem } from "./ItemContext";
5
+
6
+ const DEFAULT_THUMB_W = 132;
7
+
8
+ /**
9
+ * Renders the slide preview inside the rail item. Reads the current slide
10
+ * from `<SlideRail.Item>`'s context. Honors the focused/non-focused state
11
+ * via an accent border.
12
+ *
13
+ * ```tsx
14
+ * <Slidewise.SlideRail.Item slide={slide}>
15
+ * <Slidewise.SlideRail.Thumbnail />
16
+ * </Slidewise.SlideRail.Item>
17
+ * ```
18
+ */
19
+ export interface SlideRailThumbnailProps {
20
+ className?: string;
21
+ style?: CSSProperties;
22
+ /** Pixel width for the rendered thumbnail. Defaults to 132. */
23
+ width?: number;
24
+ }
25
+
26
+ export function Thumbnail({
27
+ className,
28
+ style,
29
+ width = DEFAULT_THUMB_W,
30
+ }: SlideRailThumbnailProps = {}) {
31
+ const { slide, isCurrent } = useSlideRailItem();
32
+ const scale = width / SLIDE_W;
33
+
34
+ return (
35
+ <div style={{ position: "relative" }}>
36
+ <div
37
+ className={className}
38
+ style={{
39
+ display: "block",
40
+ width,
41
+ border: isCurrent
42
+ ? "2px solid var(--slidewise-accent, var(--accent))"
43
+ : "2px solid transparent",
44
+ borderRadius: 8,
45
+ padding: 0,
46
+ background: "transparent",
47
+ overflow: "hidden",
48
+ transition: "border-color 120ms",
49
+ ...style,
50
+ }}
51
+ >
52
+ <div style={{ pointerEvents: "none" }}>
53
+ <SlideView slide={slide} scale={scale} />
54
+ </div>
55
+ </div>
56
+ </div>
57
+ );
58
+ }
@@ -0,0 +1,74 @@
1
+ import type { CSSProperties } from "react";
2
+ import { Root, type SlideRailRootProps } from "./Root";
3
+ import { Header, type SlideRailHeaderProps } from "./Header";
4
+ import { List, type SlideRailListProps } from "./List";
5
+ import { Item, type SlideRailItemProps } from "./Item";
6
+ import { Thumbnail, type SlideRailThumbnailProps } from "./Thumbnail";
7
+ import { Number, type SlideRailNumberProps } from "./Number";
8
+ import { AddButton, type SlideRailAddButtonProps } from "./AddButton";
9
+
10
+ export interface SlideRailProps {
11
+ className?: string;
12
+ style?: CSSProperties;
13
+ /** Rail width; forwarded to `<SlideRail.Root>`. */
14
+ width?: number | string;
15
+ /** Omit the header. */
16
+ hideHeader?: boolean;
17
+ /** Omit the "New Slide" button. */
18
+ hideAddButton?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Default SlideRail arrangement. Equivalent to:
23
+ *
24
+ * ```tsx
25
+ * <Slidewise.SlideRail.Root>
26
+ * <Slidewise.SlideRail.Header />
27
+ * <Slidewise.SlideRail.List />
28
+ * <Slidewise.SlideRail.AddButton />
29
+ * </Slidewise.SlideRail.Root>
30
+ * ```
31
+ *
32
+ * For full control (per-row context menus, custom thumbnail layout,
33
+ * etc.), drop down to the subparts directly.
34
+ */
35
+ function DefaultSlideRail({
36
+ className,
37
+ style,
38
+ width,
39
+ hideHeader,
40
+ hideAddButton,
41
+ }: SlideRailProps = {}) {
42
+ return (
43
+ <Root className={className} style={style} width={width}>
44
+ {!hideHeader && <Header />}
45
+ <List />
46
+ {!hideAddButton && <AddButton />}
47
+ </Root>
48
+ );
49
+ }
50
+
51
+ /**
52
+ * `<Slidewise.SlideRail />` is both the default arrangement and a
53
+ * namespace of subparts. Mirrors `<Slidewise.TopBar>`.
54
+ */
55
+ export const SlideRail = Object.assign(DefaultSlideRail, {
56
+ Root,
57
+ Header,
58
+ List,
59
+ Item,
60
+ Thumbnail,
61
+ Number,
62
+ AddButton,
63
+ });
64
+
65
+ export { useSlideRailItem, type SlideRailItemContextValue } from "./ItemContext";
66
+ export type {
67
+ SlideRailRootProps,
68
+ SlideRailHeaderProps,
69
+ SlideRailListProps,
70
+ SlideRailItemProps,
71
+ SlideRailThumbnailProps,
72
+ SlideRailNumberProps,
73
+ SlideRailAddButtonProps,
74
+ };
package/src/index.ts CHANGED
@@ -68,9 +68,19 @@ export {
68
68
  type SlidewiseLabels,
69
69
  type SlidewiseSurfaces,
70
70
  type ResolvedLabels,
71
+ useSlideRailItem,
71
72
  type RegionProps,
72
73
  type TopBarProps,
73
74
  type TopBarSlotId,
75
+ type SlideRailProps,
76
+ type SlideRailRootProps,
77
+ type SlideRailHeaderProps,
78
+ type SlideRailListProps,
79
+ type SlideRailItemProps,
80
+ type SlideRailThumbnailProps,
81
+ type SlideRailNumberProps,
82
+ type SlideRailAddButtonProps,
83
+ type SlideRailItemContextValue,
74
84
  } from "./compound";
75
85
 
76
86
  export { parsePptx, serializeDeck } from "./lib/pptx";
@@ -1,285 +0,0 @@
1
- import { Plus, LayoutGrid, Trash2 } from "lucide-react";
2
- import { useState } from "react";
3
- import { useEditor } from "@/lib/StoreProvider";
4
- import { SlideView } from "./SlideView";
5
- import { SLIDE_W, type Slide } from "@/lib/types";
6
-
7
- const RAIL_W = 168;
8
- const THUMB_W = 132;
9
- const THUMB_SCALE = THUMB_W / SLIDE_W;
10
-
11
- export function SlideRail() {
12
- const slides = useEditor((s) => s.deck.slides);
13
- const currentId = useEditor((s) => s.currentSlideId);
14
- const selectSlide = useEditor((s) => s.selectSlide);
15
- const addSlide = useEditor((s) => s.addSlide);
16
- const deleteSlide = useEditor((s) => s.deleteSlide);
17
- const setView = useEditor((s) => s.setView);
18
-
19
- return (
20
- <div
21
- style={{
22
- width: RAIL_W,
23
- flexShrink: 0,
24
- background: "var(--rail-bg)",
25
- borderRight: "1px solid var(--border)",
26
- boxShadow: "var(--rail-shadow)",
27
- display: "flex",
28
- flexDirection: "column",
29
- fontFamily: "Inter, system-ui, sans-serif",
30
- overflow: "hidden",
31
- zIndex: 5,
32
- }}
33
- >
34
- <div
35
- style={{
36
- height: 36,
37
- display: "flex",
38
- alignItems: "center",
39
- justifyContent: "space-between",
40
- padding: "0 12px",
41
- fontSize: 12,
42
- color: "var(--ink-muted)",
43
- borderBottom: "1px solid var(--border)",
44
- }}
45
- >
46
- <button
47
- title="Slide overview"
48
- aria-label="Open slide overview"
49
- onClick={() => setView("grid")}
50
- style={{
51
- width: 28,
52
- height: 28,
53
- border: "none",
54
- borderRadius: 6,
55
- background: "transparent",
56
- display: "flex",
57
- alignItems: "center",
58
- justifyContent: "center",
59
- color: "var(--ink)",
60
- cursor: "pointer",
61
- }}
62
- onMouseEnter={(e) =>
63
- (e.currentTarget.style.background = "var(--hover-strong)")
64
- }
65
- onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
66
- >
67
- <LayoutGrid size={14} />
68
- </button>
69
- <span>
70
- {slides.findIndex((s) => s.id === currentId) + 1} / {slides.length}
71
- </span>
72
- </div>
73
-
74
- <div
75
- style={{
76
- flex: 1,
77
- overflowY: "auto",
78
- padding: "12px 0",
79
- }}
80
- >
81
- {slides.map((s, i) => (
82
- <ThumbRow
83
- key={s.id}
84
- index={i}
85
- isCurrent={s.id === currentId}
86
- slide={s}
87
- onSelect={() => selectSlide(s.id)}
88
- onInsertAfter={() => addSlide(s.id)}
89
- onDelete={() => deleteSlide(s.id)}
90
- isLast={i === slides.length - 1}
91
- />
92
- ))}
93
- </div>
94
-
95
- <button
96
- onClick={() => addSlide()}
97
- style={{
98
- height: 44,
99
- margin: 12,
100
- display: "flex",
101
- alignItems: "center",
102
- justifyContent: "center",
103
- gap: 6,
104
- background: "var(--app-bg)",
105
- border: "1px dashed var(--border-dashed)",
106
- borderRadius: 10,
107
- color: "var(--ink)",
108
- fontSize: 13,
109
- fontWeight: 500,
110
- cursor: "pointer",
111
- transition:
112
- "background 120ms, border-color 120ms, color 120ms",
113
- }}
114
- onMouseEnter={(e) => {
115
- e.currentTarget.style.borderColor = "var(--accent)";
116
- e.currentTarget.style.color = "var(--accent)";
117
- }}
118
- onMouseLeave={(e) => {
119
- e.currentTarget.style.borderColor = "var(--border-dashed)";
120
- e.currentTarget.style.color = "var(--ink)";
121
- }}
122
- >
123
- <Plus size={14} />
124
- New Slide
125
- </button>
126
- </div>
127
- );
128
- }
129
-
130
- function ThumbRow({
131
- index,
132
- isCurrent,
133
- slide,
134
- onSelect,
135
- onInsertAfter,
136
- onDelete,
137
- isLast,
138
- }: {
139
- index: number;
140
- isCurrent: boolean;
141
- slide: Slide;
142
- onSelect: () => void;
143
- onInsertAfter: () => void;
144
- onDelete: () => void;
145
- isLast: boolean;
146
- }) {
147
- const [hover, setHover] = useState(false);
148
- return (
149
- <div
150
- onMouseEnter={() => setHover(true)}
151
- onMouseLeave={() => setHover(false)}
152
- style={{
153
- position: "relative",
154
- padding: "0 12px",
155
- marginBottom: 14,
156
- }}
157
- >
158
- <div style={{ position: "relative" }}>
159
- <span
160
- aria-hidden="true"
161
- style={{
162
- position: "absolute",
163
- left: 8,
164
- top: 6,
165
- zIndex: 2,
166
- width: 22,
167
- height: 22,
168
- borderRadius: 5,
169
- background: isCurrent ? "var(--ink)" : "#6B7280",
170
- color: isCurrent ? "var(--app-bg)" : "#fff",
171
- display: "flex",
172
- alignItems: "center",
173
- justifyContent: "center",
174
- fontSize: 11,
175
- fontWeight: 700,
176
- fontFamily: "Inter, system-ui, sans-serif",
177
- pointerEvents: "none",
178
- }}
179
- >
180
- {String(index + 1).padStart(2, "0")}
181
- </span>
182
- <button
183
- aria-label={`Open slide ${index + 1}`}
184
- aria-current={isCurrent ? "true" : undefined}
185
- onClick={onSelect}
186
- style={{
187
- display: "block",
188
- width: THUMB_W,
189
- border: isCurrent
190
- ? "2px solid var(--accent)"
191
- : "2px solid transparent",
192
- borderRadius: 8,
193
- padding: 0,
194
- background: "transparent",
195
- cursor: "pointer",
196
- overflow: "hidden",
197
- transition: "border-color 120ms",
198
- }}
199
- >
200
- <div style={{ pointerEvents: "none" }}>
201
- <SlideView slide={slide} scale={THUMB_SCALE} />
202
- </div>
203
- </button>
204
-
205
- {hover && (
206
- <button
207
- onClick={(e) => {
208
- e.stopPropagation();
209
- onDelete();
210
- }}
211
- title="Delete slide"
212
- aria-label={`Delete slide ${index + 1}`}
213
- style={{
214
- position: "absolute",
215
- right: 6,
216
- top: 6,
217
- width: 22,
218
- height: 22,
219
- borderRadius: 6,
220
- background: "var(--toolbar-bg)",
221
- backdropFilter: "blur(10px)",
222
- WebkitBackdropFilter: "blur(10px)",
223
- border: "1px solid var(--border-strong)",
224
- cursor: "pointer",
225
- display: "flex",
226
- alignItems: "center",
227
- justifyContent: "center",
228
- color: "var(--ink)",
229
- }}
230
- >
231
- <Trash2 size={11} />
232
- </button>
233
- )}
234
- </div>
235
-
236
- {!isLast && <InsertGap onInsert={onInsertAfter} />}
237
- </div>
238
- );
239
- }
240
-
241
- function InsertGap({ onInsert }: { onInsert: () => void }) {
242
- const [hover, setHover] = useState(false);
243
- return (
244
- <div
245
- onMouseEnter={() => setHover(true)}
246
- onMouseLeave={() => setHover(false)}
247
- style={{
248
- position: "absolute",
249
- left: 0,
250
- right: 0,
251
- bottom: -14,
252
- height: 28,
253
- display: "flex",
254
- alignItems: "center",
255
- justifyContent: "center",
256
- zIndex: 5,
257
- }}
258
- >
259
- <button
260
- onClick={onInsert}
261
- aria-label="Insert slide here"
262
- title="Insert slide"
263
- style={{
264
- width: hover ? 30 : 22,
265
- height: hover ? 30 : 22,
266
- borderRadius: 999,
267
- background: "var(--gap-icon-bg)",
268
- border: "1px solid var(--border-strong)",
269
- cursor: "pointer",
270
- display: "flex",
271
- alignItems: "center",
272
- justifyContent: "center",
273
- color: "var(--ink)",
274
- opacity: hover ? 1 : 0,
275
- transform: hover ? "scale(1)" : "scale(0.85)",
276
- transition:
277
- "opacity 140ms, transform 140ms, width 140ms, height 140ms",
278
- boxShadow: hover ? "var(--toolbar-shadow)" : "var(--thumb-shadow)",
279
- }}
280
- >
281
- <Plus size={hover ? 16 : 12} />
282
- </button>
283
- </div>
284
- );
285
- }