@textcortex/slidewise 1.0.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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/dist/__vite-browser-external-DYxpcVy9.js +5 -0
  4. package/dist/__vite-browser-external-DYxpcVy9.js.map +1 -0
  5. package/dist/file.svg +1 -0
  6. package/dist/globe.svg +1 -0
  7. package/dist/index.mjs +16697 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/slidewise.css +1 -0
  10. package/dist/types/SlidewiseEditor.d.ts +47 -0
  11. package/dist/types/SlidewiseFileEditor.d.ts +54 -0
  12. package/dist/types/components/editor/BottomToolbar.d.ts +1 -0
  13. package/dist/types/components/editor/Canvas.d.ts +1 -0
  14. package/dist/types/components/editor/Editor.d.ts +8 -0
  15. package/dist/types/components/editor/ElementView.d.ts +6 -0
  16. package/dist/types/components/editor/FloatingToolbar.d.ts +6 -0
  17. package/dist/types/components/editor/GridView.d.ts +1 -0
  18. package/dist/types/components/editor/PlayMode.d.ts +1 -0
  19. package/dist/types/components/editor/SelectionFrame.d.ts +8 -0
  20. package/dist/types/components/editor/SlideRail.d.ts +1 -0
  21. package/dist/types/components/editor/SlideView.d.ts +5 -0
  22. package/dist/types/components/editor/TopBar.d.ts +7 -0
  23. package/dist/types/index.d.ts +7 -0
  24. package/dist/types/lib/StoreProvider.d.ts +8 -0
  25. package/dist/types/lib/fonts.d.ts +9 -0
  26. package/dist/types/lib/pptx/deckToPptx.d.ts +9 -0
  27. package/dist/types/lib/pptx/index.d.ts +3 -0
  28. package/dist/types/lib/pptx/pptxToDeck.d.ts +18 -0
  29. package/dist/types/lib/pptx/types.d.ts +15 -0
  30. package/dist/types/lib/pptx/units.d.ts +25 -0
  31. package/dist/types/lib/schema/migrate.d.ts +25 -0
  32. package/dist/types/lib/seed.d.ts +2 -0
  33. package/dist/types/lib/store.d.ts +55 -0
  34. package/dist/types/lib/types.d.ts +141 -0
  35. package/dist/window.svg +1 -0
  36. package/package.json +86 -0
  37. package/src/App.tsx +261 -0
  38. package/src/SlidewiseEditor.css +146 -0
  39. package/src/SlidewiseEditor.tsx +214 -0
  40. package/src/SlidewiseFileEditor.tsx +242 -0
  41. package/src/components/editor/BottomToolbar.tsx +216 -0
  42. package/src/components/editor/Canvas.tsx +467 -0
  43. package/src/components/editor/Editor.tsx +53 -0
  44. package/src/components/editor/ElementView.tsx +588 -0
  45. package/src/components/editor/FloatingToolbar.tsx +729 -0
  46. package/src/components/editor/GridView.tsx +232 -0
  47. package/src/components/editor/PlayMode.tsx +260 -0
  48. package/src/components/editor/SelectionFrame.tsx +241 -0
  49. package/src/components/editor/SlideRail.tsx +285 -0
  50. package/src/components/editor/SlideView.tsx +55 -0
  51. package/src/components/editor/TopBar.tsx +240 -0
  52. package/src/fonts.css +2 -0
  53. package/src/index.css +13 -0
  54. package/src/index.ts +36 -0
  55. package/src/lib/StoreProvider.tsx +43 -0
  56. package/src/lib/__tests__/css-scope.test.ts +133 -0
  57. package/src/lib/fonts.ts +104 -0
  58. package/src/lib/pptx/__tests__/roundtrip.test.ts +240 -0
  59. package/src/lib/pptx/deckToPptx.ts +300 -0
  60. package/src/lib/pptx/index.ts +3 -0
  61. package/src/lib/pptx/pptxToDeck.ts +1515 -0
  62. package/src/lib/pptx/types.ts +17 -0
  63. package/src/lib/pptx/units.ts +32 -0
  64. package/src/lib/schema/__tests__/migrate.test.ts +70 -0
  65. package/src/lib/schema/migrate.ts +102 -0
  66. package/src/lib/seed.ts +777 -0
  67. package/src/lib/store.ts +384 -0
  68. package/src/lib/types.ts +185 -0
  69. package/src/main.tsx +10 -0
  70. package/src/vite-env.d.ts +3 -0
@@ -0,0 +1,232 @@
1
+ import { useEffect } from "react";
2
+ import { Plus, X } from "lucide-react";
3
+ import { useEditor } from "@/lib/StoreProvider";
4
+ import { SlideView } from "./SlideView";
5
+ import { SLIDE_W } from "@/lib/types";
6
+
7
+ const COL_GAP = 28;
8
+ const ROW_GAP = 36;
9
+ const COLS = 4;
10
+ const PAD_X = 64;
11
+
12
+ export function GridView() {
13
+ const slides = useEditor((s) => s.deck.slides);
14
+ const currentId = useEditor((s) => s.currentSlideId);
15
+ const title = useEditor((s) => s.deck.title);
16
+ const selectSlide = useEditor((s) => s.selectSlide);
17
+ const addSlide = useEditor((s) => s.addSlide);
18
+ const setView = useEditor((s) => s.setView);
19
+
20
+ useEffect(() => {
21
+ const onKey = (e: KeyboardEvent) => {
22
+ if (e.key === "Escape") setView("editor");
23
+ };
24
+ window.addEventListener("keydown", onKey);
25
+ return () => window.removeEventListener("keydown", onKey);
26
+ }, [setView]);
27
+
28
+ const thumbW =
29
+ typeof window !== "undefined"
30
+ ? Math.min(
31
+ 340,
32
+ (window.innerWidth - PAD_X * 2 - COL_GAP * (COLS - 1)) / COLS
33
+ )
34
+ : 320;
35
+ const scale = thumbW / SLIDE_W;
36
+
37
+ return (
38
+ <div
39
+ style={{
40
+ position: "fixed",
41
+ inset: 0,
42
+ background: "var(--grid-overlay-bg)",
43
+ backdropFilter: "blur(18px)",
44
+ WebkitBackdropFilter: "blur(18px)",
45
+ zIndex: 80,
46
+ display: "flex",
47
+ flexDirection: "column",
48
+ fontFamily: "Inter, system-ui, sans-serif",
49
+ }}
50
+ >
51
+ <div
52
+ style={{
53
+ height: 56,
54
+ display: "flex",
55
+ alignItems: "center",
56
+ justifyContent: "space-between",
57
+ padding: "0 28px",
58
+ borderBottom: "1px solid var(--border)",
59
+ background: "var(--toolbar-bg)",
60
+ backdropFilter: "blur(20px)",
61
+ WebkitBackdropFilter: "blur(20px)",
62
+ }}
63
+ >
64
+ <div
65
+ style={{
66
+ display: "flex",
67
+ alignItems: "center",
68
+ gap: 14,
69
+ color: "var(--ink)",
70
+ }}
71
+ >
72
+ <span style={{ fontSize: 14, fontWeight: 600 }}>
73
+ {title || "Untitled deck"}
74
+ </span>
75
+ <span
76
+ style={{
77
+ fontSize: 12,
78
+ color: "var(--ink-muted)",
79
+ padding: "2px 8px",
80
+ borderRadius: 999,
81
+ background: "var(--input-bg)",
82
+ }}
83
+ >
84
+ {slides.length} slide{slides.length === 1 ? "" : "s"}
85
+ </span>
86
+ </div>
87
+ <button
88
+ onClick={() => setView("editor")}
89
+ title="Close overview (Esc)"
90
+ aria-label="Close slide overview"
91
+ style={{
92
+ width: 32,
93
+ height: 32,
94
+ borderRadius: 8,
95
+ border: "1px solid var(--border)",
96
+ background: "transparent",
97
+ color: "var(--ink)",
98
+ cursor: "pointer",
99
+ display: "flex",
100
+ alignItems: "center",
101
+ justifyContent: "center",
102
+ }}
103
+ onMouseEnter={(e) =>
104
+ (e.currentTarget.style.background = "var(--hover)")
105
+ }
106
+ onMouseLeave={(e) =>
107
+ (e.currentTarget.style.background = "transparent")
108
+ }
109
+ >
110
+ <X size={16} />
111
+ </button>
112
+ </div>
113
+
114
+ <div
115
+ style={{
116
+ flex: 1,
117
+ overflowY: "auto",
118
+ padding: `40px ${PAD_X}px 80px`,
119
+ }}
120
+ >
121
+ <div
122
+ style={{
123
+ display: "grid",
124
+ gridTemplateColumns: `repeat(${COLS}, ${thumbW}px)`,
125
+ columnGap: COL_GAP,
126
+ rowGap: ROW_GAP,
127
+ justifyContent: "center",
128
+ }}
129
+ >
130
+ {slides.map((s, i) => {
131
+ const isCurrent = s.id === currentId;
132
+ return (
133
+ <button
134
+ key={s.id}
135
+ aria-label={`Open slide ${i + 1}`}
136
+ aria-current={isCurrent ? "true" : undefined}
137
+ onClick={() => {
138
+ selectSlide(s.id);
139
+ setView("editor");
140
+ }}
141
+ style={{
142
+ position: "relative",
143
+ padding: 0,
144
+ border: "none",
145
+ background: "transparent",
146
+ cursor: "pointer",
147
+ textAlign: "left",
148
+ }}
149
+ >
150
+ <div
151
+ style={{
152
+ position: "absolute",
153
+ left: -10,
154
+ top: -10,
155
+ zIndex: 2,
156
+ width: 28,
157
+ height: 28,
158
+ borderRadius: 8,
159
+ background: isCurrent ? "var(--accent)" : "var(--ink)",
160
+ color: "#fff",
161
+ display: "flex",
162
+ alignItems: "center",
163
+ justifyContent: "center",
164
+ fontSize: 12,
165
+ fontWeight: 700,
166
+ boxShadow: "var(--thumb-shadow)",
167
+ }}
168
+ >
169
+ {String(i + 1).padStart(2, "0")}
170
+ </div>
171
+ <div
172
+ style={{
173
+ borderRadius: 14,
174
+ overflow: "hidden",
175
+ border: isCurrent
176
+ ? "3px solid var(--accent)"
177
+ : "3px solid transparent",
178
+ boxShadow: "var(--slide-shadow)",
179
+ transition: "transform 140ms, border-color 120ms",
180
+ }}
181
+ onMouseEnter={(e) => {
182
+ e.currentTarget.style.transform = "translateY(-2px)";
183
+ }}
184
+ onMouseLeave={(e) => {
185
+ e.currentTarget.style.transform = "translateY(0)";
186
+ }}
187
+ >
188
+ <SlideView slide={s} scale={scale} />
189
+ </div>
190
+ </button>
191
+ );
192
+ })}
193
+
194
+ <button
195
+ onClick={() => addSlide()}
196
+ aria-label="Add new slide"
197
+ style={{
198
+ width: thumbW,
199
+ aspectRatio: `${SLIDE_W} / 1080`,
200
+ borderRadius: 14,
201
+ border: "2px dashed var(--border-dashed)",
202
+ background: "transparent",
203
+ color: "var(--ink-muted)",
204
+ cursor: "pointer",
205
+ display: "flex",
206
+ flexDirection: "column",
207
+ alignItems: "center",
208
+ justifyContent: "center",
209
+ gap: 8,
210
+ fontSize: 13,
211
+ fontWeight: 500,
212
+ transition: "border-color 120ms, color 120ms, background 120ms",
213
+ }}
214
+ onMouseEnter={(e) => {
215
+ e.currentTarget.style.borderColor = "var(--accent)";
216
+ e.currentTarget.style.color = "var(--accent)";
217
+ e.currentTarget.style.background = "var(--accent-soft)";
218
+ }}
219
+ onMouseLeave={(e) => {
220
+ e.currentTarget.style.borderColor = "var(--border-dashed)";
221
+ e.currentTarget.style.color = "var(--ink-muted)";
222
+ e.currentTarget.style.background = "transparent";
223
+ }}
224
+ >
225
+ <Plus size={20} />
226
+ New Slide
227
+ </button>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ );
232
+ }
@@ -0,0 +1,260 @@
1
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import { ChevronLeft, ChevronRight, X } from "lucide-react";
4
+ import { useEditor } from "@/lib/StoreProvider";
5
+ import { SLIDE_W, SLIDE_H, type SlideElement, type EnterAnim } from "@/lib/types";
6
+ import { ElementView } from "./ElementView";
7
+
8
+ export function PlayMode() {
9
+ const slides = useEditor((s) => s.deck.slides);
10
+ const stop = useEditor((s) => s.stop);
11
+ const [index, setIndex] = useState(0);
12
+ const wrapRef = useRef<HTMLDivElement>(null);
13
+ const [scale, setScale] = useState(1);
14
+
15
+ useLayoutEffect(() => {
16
+ const compute = () => {
17
+ if (!wrapRef.current) return;
18
+ const r = wrapRef.current.getBoundingClientRect();
19
+ const fit = Math.min(r.width / SLIDE_W, r.height / SLIDE_H);
20
+ setScale(fit);
21
+ };
22
+ compute();
23
+ window.addEventListener("resize", compute);
24
+ return () => window.removeEventListener("resize", compute);
25
+ }, []);
26
+
27
+ useEffect(() => {
28
+ function onKey(e: KeyboardEvent) {
29
+ if (e.key === "Escape") stop();
30
+ if (e.key === "ArrowRight" || e.key === " " || e.key === "PageDown")
31
+ setIndex((i) => Math.min(slides.length - 1, i + 1));
32
+ if (e.key === "ArrowLeft" || e.key === "PageUp")
33
+ setIndex((i) => Math.max(0, i - 1));
34
+ }
35
+ window.addEventListener("keydown", onKey);
36
+ return () => window.removeEventListener("keydown", onKey);
37
+ }, [slides.length, stop]);
38
+
39
+ const slide = slides[index];
40
+
41
+ return (
42
+ <div
43
+ style={{
44
+ position: "fixed",
45
+ inset: 0,
46
+ background: "#000",
47
+ zIndex: 100,
48
+ display: "flex",
49
+ alignItems: "center",
50
+ justifyContent: "center",
51
+ }}
52
+ ref={wrapRef}
53
+ >
54
+ <button
55
+ onClick={stop}
56
+ title="Exit (Esc)"
57
+ aria-label="Exit play mode"
58
+ style={{
59
+ position: "absolute",
60
+ top: 16,
61
+ right: 16,
62
+ width: 40,
63
+ height: 40,
64
+ borderRadius: 999,
65
+ background: "rgba(255,255,255,0.10)",
66
+ border: "1px solid rgba(255,255,255,0.18)",
67
+ color: "#fff",
68
+ cursor: "pointer",
69
+ display: "flex",
70
+ alignItems: "center",
71
+ justifyContent: "center",
72
+ backdropFilter: "blur(12px)",
73
+ zIndex: 5,
74
+ }}
75
+ >
76
+ <X size={18} />
77
+ </button>
78
+
79
+ <button
80
+ onClick={() => setIndex((i) => Math.max(0, i - 1))}
81
+ disabled={index === 0}
82
+ aria-label="Previous slide"
83
+ title="Previous slide"
84
+ style={{
85
+ position: "absolute",
86
+ left: 16,
87
+ top: "50%",
88
+ transform: "translateY(-50%)",
89
+ width: 44,
90
+ height: 44,
91
+ borderRadius: 999,
92
+ background: "rgba(255,255,255,0.10)",
93
+ border: "1px solid rgba(255,255,255,0.18)",
94
+ color: "#fff",
95
+ cursor: index === 0 ? "default" : "pointer",
96
+ opacity: index === 0 ? 0.3 : 1,
97
+ display: "flex",
98
+ alignItems: "center",
99
+ justifyContent: "center",
100
+ backdropFilter: "blur(12px)",
101
+ zIndex: 5,
102
+ }}
103
+ >
104
+ <ChevronLeft size={20} />
105
+ </button>
106
+
107
+ <button
108
+ onClick={() => setIndex((i) => Math.min(slides.length - 1, i + 1))}
109
+ disabled={index === slides.length - 1}
110
+ aria-label="Next slide"
111
+ title="Next slide"
112
+ style={{
113
+ position: "absolute",
114
+ right: 16,
115
+ top: "50%",
116
+ transform: "translateY(-50%)",
117
+ width: 44,
118
+ height: 44,
119
+ borderRadius: 999,
120
+ background: "rgba(255,255,255,0.10)",
121
+ border: "1px solid rgba(255,255,255,0.18)",
122
+ color: "#fff",
123
+ cursor: index === slides.length - 1 ? "default" : "pointer",
124
+ opacity: index === slides.length - 1 ? 0.3 : 1,
125
+ display: "flex",
126
+ alignItems: "center",
127
+ justifyContent: "center",
128
+ backdropFilter: "blur(12px)",
129
+ zIndex: 5,
130
+ }}
131
+ >
132
+ <ChevronRight size={20} />
133
+ </button>
134
+
135
+ <div
136
+ role="status"
137
+ aria-live="polite"
138
+ aria-label={`Slide ${index + 1} of ${slides.length}`}
139
+ style={{
140
+ position: "absolute",
141
+ bottom: 18,
142
+ left: "50%",
143
+ transform: "translateX(-50%)",
144
+ padding: "6px 12px",
145
+ borderRadius: 999,
146
+ background: "rgba(255,255,255,0.08)",
147
+ border: "1px solid rgba(255,255,255,0.14)",
148
+ color: "#fff",
149
+ fontFamily: "Inter, system-ui, sans-serif",
150
+ fontSize: 12,
151
+ backdropFilter: "blur(12px)",
152
+ }}
153
+ >
154
+ {index + 1} / {slides.length}
155
+ </div>
156
+
157
+ <AnimatePresence mode="wait">
158
+ <motion.div
159
+ key={slide.id}
160
+ initial={{ opacity: 0 }}
161
+ animate={{ opacity: 1 }}
162
+ exit={{ opacity: 0 }}
163
+ transition={{ duration: 0.35 }}
164
+ style={{
165
+ width: SLIDE_W * scale,
166
+ height: SLIDE_H * scale,
167
+ background: slide.background,
168
+ position: "relative",
169
+ overflow: "hidden",
170
+ borderRadius: 4,
171
+ boxShadow: "0 30px 80px rgba(0,0,0,0.5)",
172
+ }}
173
+ >
174
+ <div
175
+ style={{
176
+ width: SLIDE_W,
177
+ height: SLIDE_H,
178
+ transform: `scale(${scale})`,
179
+ transformOrigin: "top left",
180
+ position: "absolute",
181
+ inset: 0,
182
+ }}
183
+ >
184
+ {[...slide.elements]
185
+ .sort((a, b) => a.z - b.z)
186
+ .map((el) => (
187
+ <AnimatedElement key={el.id} el={el} />
188
+ ))}
189
+ </div>
190
+ </motion.div>
191
+ </AnimatePresence>
192
+ </div>
193
+ );
194
+ }
195
+
196
+ function variantsFor(anim: EnterAnim) {
197
+ switch (anim) {
198
+ case "fade":
199
+ return { initial: { opacity: 0 }, animate: { opacity: 1 } };
200
+ case "slide-up":
201
+ return {
202
+ initial: { opacity: 0, y: 40 },
203
+ animate: { opacity: 1, y: 0 },
204
+ };
205
+ case "slide-down":
206
+ return {
207
+ initial: { opacity: 0, y: -40 },
208
+ animate: { opacity: 1, y: 0 },
209
+ };
210
+ case "slide-left":
211
+ return {
212
+ initial: { opacity: 0, x: 40 },
213
+ animate: { opacity: 1, x: 0 },
214
+ };
215
+ case "slide-right":
216
+ return {
217
+ initial: { opacity: 0, x: -40 },
218
+ animate: { opacity: 1, x: 0 },
219
+ };
220
+ case "scale":
221
+ return {
222
+ initial: { opacity: 0, scale: 0.92 },
223
+ animate: { opacity: 1, scale: 1 },
224
+ };
225
+ case "draw":
226
+ return {
227
+ initial: { opacity: 0, scaleX: 0 },
228
+ animate: { opacity: 1, scaleX: 1 },
229
+ };
230
+ case "none":
231
+ default:
232
+ return { initial: { opacity: 1 }, animate: { opacity: 1 } };
233
+ }
234
+ }
235
+
236
+ function AnimatedElement({ el }: { el: SlideElement }) {
237
+ const v = variantsFor(el.enter ?? "fade");
238
+ return (
239
+ <motion.div
240
+ initial={v.initial}
241
+ animate={v.animate}
242
+ transition={{
243
+ duration: 0.55,
244
+ delay: el.delay ?? 0,
245
+ ease: [0.22, 1, 0.36, 1],
246
+ }}
247
+ style={{
248
+ position: "absolute",
249
+ left: el.x,
250
+ top: el.y,
251
+ width: el.w,
252
+ height: el.h,
253
+ transform: `rotate(${el.rotation}deg)`,
254
+ transformOrigin: el.enter === "draw" ? "left center" : "center",
255
+ }}
256
+ >
257
+ <ElementView el={el} />
258
+ </motion.div>
259
+ );
260
+ }