@textcortex/slidewise 1.7.0 → 1.9.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 +6303 -5377
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/SlidewiseEditor.tsx +10 -0
- package/src/SlidewiseFileEditor.tsx +8 -0
- package/src/components/editor/Canvas.tsx +15 -8
- package/src/components/editor/ElementView.tsx +97 -9
- package/src/components/editor/SlideView.tsx +6 -1
- package/src/compound/CanvasContext.tsx +176 -0
- package/src/compound/SlidewiseRoot.tsx +34 -11
- package/src/compound/index.ts +8 -0
- package/src/index.ts +5 -0
- package/src/lib/pptx/pptxToDeck.ts +1622 -126
- package/src/lib/types.ts +52 -0
package/package.json
CHANGED
package/src/SlidewiseEditor.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import type { SlidewiseIcons } from "./compound/IconContext";
|
|
18
18
|
import type { SlidewiseLabels } from "./compound/LabelsContext";
|
|
19
19
|
import type { SlidewiseSurfaces } from "./compound/SurfacesContext";
|
|
20
|
+
import type { SlidewiseCanvasConfig } from "./compound/CanvasContext";
|
|
20
21
|
import type { Transition } from "framer-motion";
|
|
21
22
|
import type { Deck } from "@/lib/types";
|
|
22
23
|
import "./SlidewiseEditor.css";
|
|
@@ -96,6 +97,13 @@ export interface SlidewiseEditorProps {
|
|
|
96
97
|
* list of keys.
|
|
97
98
|
*/
|
|
98
99
|
surfaces?: SlidewiseSurfaces;
|
|
100
|
+
/**
|
|
101
|
+
* Canvas/viewport configuration: padding, initial zoom, slide shadow +
|
|
102
|
+
* radius, and host-driven slide-background overrides. Use this when the
|
|
103
|
+
* default "slide fills the workspace" presentation isn't right for your
|
|
104
|
+
* host chrome.
|
|
105
|
+
*/
|
|
106
|
+
canvas?: SlidewiseCanvasConfig;
|
|
99
107
|
/** Extra class names appended to the editor root. */
|
|
100
108
|
className?: string;
|
|
101
109
|
/** Inline style applied to the editor root. */
|
|
@@ -151,6 +159,7 @@ export const SlidewiseEditor = forwardRef<
|
|
|
151
159
|
icons,
|
|
152
160
|
labels,
|
|
153
161
|
surfaces,
|
|
162
|
+
canvas,
|
|
154
163
|
className,
|
|
155
164
|
style,
|
|
156
165
|
},
|
|
@@ -178,6 +187,7 @@ export const SlidewiseEditor = forwardRef<
|
|
|
178
187
|
icons,
|
|
179
188
|
labels,
|
|
180
189
|
surfaces,
|
|
190
|
+
canvas,
|
|
181
191
|
className,
|
|
182
192
|
style,
|
|
183
193
|
};
|
|
@@ -12,6 +12,7 @@ import type { Deck } from "@/lib/types";
|
|
|
12
12
|
import type { SlidewiseIcons } from "./compound/IconContext";
|
|
13
13
|
import type { SlidewiseLabels } from "./compound/LabelsContext";
|
|
14
14
|
import type { SlidewiseSurfaces } from "./compound/SurfacesContext";
|
|
15
|
+
import type { SlidewiseCanvasConfig } from "./compound/CanvasContext";
|
|
15
16
|
import { DEFAULT_LABELS } from "./compound/LabelsContext";
|
|
16
17
|
import type { Transition } from "framer-motion";
|
|
17
18
|
import type { HistoryState, SelectionSnapshot } from "./compound/SlidewiseRoot";
|
|
@@ -106,6 +107,11 @@ export interface SlidewiseFileEditorProps {
|
|
|
106
107
|
* `--slidewise-bg-*` CSS variables.
|
|
107
108
|
*/
|
|
108
109
|
surfaces?: SlidewiseSurfaces;
|
|
110
|
+
/**
|
|
111
|
+
* Canvas/viewport configuration: padding, initial zoom, slide shadow +
|
|
112
|
+
* radius, and host-driven slide-background overrides.
|
|
113
|
+
*/
|
|
114
|
+
canvas?: SlidewiseCanvasConfig;
|
|
109
115
|
/**
|
|
110
116
|
* Reduced-motion behavior. `"system"` (default) respects the OS
|
|
111
117
|
* preference; `true` forces motion off; `false` forces it on.
|
|
@@ -217,6 +223,7 @@ export const SlidewiseFileEditor = forwardRef<
|
|
|
217
223
|
icons,
|
|
218
224
|
labels,
|
|
219
225
|
surfaces,
|
|
226
|
+
canvas,
|
|
220
227
|
reduceMotion,
|
|
221
228
|
transition,
|
|
222
229
|
className,
|
|
@@ -394,6 +401,7 @@ export const SlidewiseFileEditor = forwardRef<
|
|
|
394
401
|
icons={icons}
|
|
395
402
|
labels={labels}
|
|
396
403
|
surfaces={surfaces}
|
|
404
|
+
canvas={canvas}
|
|
397
405
|
reduceMotion={reduceMotion}
|
|
398
406
|
transition={transition}
|
|
399
407
|
onChange={(next) => {
|
|
@@ -5,6 +5,10 @@ import { SLIDE_W, SLIDE_H, type SlideElement, type ElementDraft } from "@/lib/ty
|
|
|
5
5
|
import { ElementView } from "./ElementView";
|
|
6
6
|
import { SelectionFrame } from "./SelectionFrame";
|
|
7
7
|
import { FloatingToolbar } from "./FloatingToolbar";
|
|
8
|
+
import {
|
|
9
|
+
useCanvasConfig,
|
|
10
|
+
resolveSlideBackground,
|
|
11
|
+
} from "@/compound/CanvasContext";
|
|
8
12
|
|
|
9
13
|
export function Canvas() {
|
|
10
14
|
const store = useEditorStore();
|
|
@@ -22,6 +26,7 @@ export function Canvas() {
|
|
|
22
26
|
const deleteElement = useEditor((s) => s.deleteElement);
|
|
23
27
|
const pushHistory = useEditor((s) => s.pushHistory);
|
|
24
28
|
|
|
29
|
+
const canvasConfig = useCanvasConfig();
|
|
25
30
|
const [editingId, setEditingId] = useState<string | null>(null);
|
|
26
31
|
const wrapRef = useRef<HTMLDivElement>(null);
|
|
27
32
|
const [autoScale, setAutoScale] = useState(0.6);
|
|
@@ -30,10 +35,12 @@ export function Canvas() {
|
|
|
30
35
|
if (fitMode !== "fit" || !wrapRef.current) return;
|
|
31
36
|
const recompute = () => {
|
|
32
37
|
const r = wrapRef.current!.getBoundingClientRect();
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
// Host-controllable padding via `canvas.padding` on <Slidewise.Root>.
|
|
39
|
+
// Default mirrors what the editor needed before the prop existed —
|
|
40
|
+
// ~32px horizontal breathing room and enough vertical headroom for
|
|
41
|
+
// the floating toolbar (~56) and the bottom toolbar (~76).
|
|
42
|
+
const padX = canvasConfig.padding.x * 2;
|
|
43
|
+
const padY = canvasConfig.padding.y * 2;
|
|
37
44
|
const fit = Math.min(
|
|
38
45
|
(r.width - padX) / SLIDE_W,
|
|
39
46
|
(r.height - padY) / SLIDE_H
|
|
@@ -44,7 +51,7 @@ export function Canvas() {
|
|
|
44
51
|
const ro = new ResizeObserver(recompute);
|
|
45
52
|
ro.observe(wrapRef.current);
|
|
46
53
|
return () => ro.disconnect();
|
|
47
|
-
}, [fitMode]);
|
|
54
|
+
}, [fitMode, canvasConfig.padding.x, canvasConfig.padding.y]);
|
|
48
55
|
|
|
49
56
|
const scale = fitMode === "fit" ? autoScale : zoom;
|
|
50
57
|
|
|
@@ -245,9 +252,9 @@ export function Canvas() {
|
|
|
245
252
|
width: SLIDE_W * scale,
|
|
246
253
|
height: SLIDE_H * scale,
|
|
247
254
|
transform: "translate(-50%, -50%)",
|
|
248
|
-
background: slide
|
|
249
|
-
borderRadius:
|
|
250
|
-
boxShadow:
|
|
255
|
+
background: resolveSlideBackground(canvasConfig, slide),
|
|
256
|
+
borderRadius: canvasConfig.slideRadius,
|
|
257
|
+
boxShadow: canvasConfig.slideShadow,
|
|
251
258
|
}}
|
|
252
259
|
>
|
|
253
260
|
<div
|
|
@@ -65,12 +65,17 @@ function TextView({
|
|
|
65
65
|
: el.vAlign === "middle"
|
|
66
66
|
? "center"
|
|
67
67
|
: "flex-end",
|
|
68
|
+
background: el.background,
|
|
69
|
+
padding: el.padding
|
|
70
|
+
? `${el.padding.t}px ${el.padding.r}px ${el.padding.b}px ${el.padding.l}px`
|
|
71
|
+
: undefined,
|
|
72
|
+
boxSizing: el.padding ? "border-box" : undefined,
|
|
68
73
|
cursor: editing ? "text" : "inherit",
|
|
69
74
|
};
|
|
70
75
|
const inner: React.CSSProperties = {
|
|
71
76
|
width: "100%",
|
|
72
77
|
color: el.color,
|
|
73
|
-
fontFamily: el.fontFamily,
|
|
78
|
+
fontFamily: withGenericFallback(el.fontFamily),
|
|
74
79
|
fontSize: el.fontSize,
|
|
75
80
|
fontWeight: el.fontWeight,
|
|
76
81
|
fontStyle: el.italic ? "italic" : "normal",
|
|
@@ -85,11 +90,40 @@ function TextView({
|
|
|
85
90
|
outline: "none",
|
|
86
91
|
};
|
|
87
92
|
|
|
93
|
+
const backingPath = el.backingPath;
|
|
94
|
+
const positionedOuter: React.CSSProperties = backingPath
|
|
95
|
+
? { ...outer, position: "relative" }
|
|
96
|
+
: outer;
|
|
97
|
+
const innerStacked: React.CSSProperties = backingPath
|
|
98
|
+
? { ...inner, position: "relative", zIndex: 1 }
|
|
99
|
+
: inner;
|
|
100
|
+
const backingSvg = backingPath ? (
|
|
101
|
+
<svg
|
|
102
|
+
viewBox={`0 0 ${backingPath.viewW} ${backingPath.viewH}`}
|
|
103
|
+
preserveAspectRatio="none"
|
|
104
|
+
style={{
|
|
105
|
+
position: "absolute",
|
|
106
|
+
inset: 0,
|
|
107
|
+
width: "100%",
|
|
108
|
+
height: "100%",
|
|
109
|
+
pointerEvents: "none",
|
|
110
|
+
zIndex: 0,
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<path
|
|
114
|
+
d={backingPath.d}
|
|
115
|
+
fill={backingPath.fill}
|
|
116
|
+
fillRule={backingPath.fillRule ?? "nonzero"}
|
|
117
|
+
/>
|
|
118
|
+
</svg>
|
|
119
|
+
) : null;
|
|
120
|
+
|
|
88
121
|
if (editing) {
|
|
89
122
|
return (
|
|
90
|
-
<div style={
|
|
123
|
+
<div style={positionedOuter}>
|
|
124
|
+
{backingSvg}
|
|
91
125
|
<EditableText
|
|
92
|
-
style={
|
|
126
|
+
style={innerStacked}
|
|
93
127
|
initialText={el.text}
|
|
94
128
|
initialRuns={el.runs}
|
|
95
129
|
onCommit={(t, r) => onCommit?.(t, r)}
|
|
@@ -100,8 +134,9 @@ function TextView({
|
|
|
100
134
|
|
|
101
135
|
if (el.runs && el.runs.length) {
|
|
102
136
|
return (
|
|
103
|
-
<div style={
|
|
104
|
-
|
|
137
|
+
<div style={positionedOuter}>
|
|
138
|
+
{backingSvg}
|
|
139
|
+
<div style={innerStacked}>
|
|
105
140
|
{el.runs.map((r, i) => (
|
|
106
141
|
<span key={i} style={runCssStyle(r)}>
|
|
107
142
|
{r.text}
|
|
@@ -113,15 +148,40 @@ function TextView({
|
|
|
113
148
|
}
|
|
114
149
|
|
|
115
150
|
return (
|
|
116
|
-
<div style={
|
|
117
|
-
|
|
151
|
+
<div style={positionedOuter}>
|
|
152
|
+
{backingSvg}
|
|
153
|
+
<div style={innerStacked}>{el.text}</div>
|
|
118
154
|
</div>
|
|
119
155
|
);
|
|
120
156
|
}
|
|
121
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Append a `sans-serif` generic so brand families imported from PPTX
|
|
160
|
+
* (e.g. "EON Office Head") degrade gracefully when the typeface isn't
|
|
161
|
+
* installed locally — without the generic the browser silently picks
|
|
162
|
+
* its default serif. Already-qualified stacks (containing a comma) and
|
|
163
|
+
* plain generics ("serif"/"monospace") pass through untouched.
|
|
164
|
+
*/
|
|
165
|
+
function withGenericFallback(family: string | undefined): string | undefined {
|
|
166
|
+
if (!family) return family;
|
|
167
|
+
if (family.includes(",")) return family;
|
|
168
|
+
const lower = family.trim().toLowerCase();
|
|
169
|
+
if (
|
|
170
|
+
lower === "serif" ||
|
|
171
|
+
lower === "sans-serif" ||
|
|
172
|
+
lower === "monospace" ||
|
|
173
|
+
lower === "cursive" ||
|
|
174
|
+
lower === "fantasy" ||
|
|
175
|
+
lower === "system-ui"
|
|
176
|
+
) {
|
|
177
|
+
return family;
|
|
178
|
+
}
|
|
179
|
+
return `${family}, sans-serif`;
|
|
180
|
+
}
|
|
181
|
+
|
|
122
182
|
function runCssStyle(r: TextRun): React.CSSProperties {
|
|
123
183
|
const s: React.CSSProperties = {};
|
|
124
|
-
if (r.fontFamily) s.fontFamily = r.fontFamily;
|
|
184
|
+
if (r.fontFamily) s.fontFamily = withGenericFallback(r.fontFamily);
|
|
125
185
|
if (r.fontSize) s.fontSize = r.fontSize;
|
|
126
186
|
if (r.fontWeight) s.fontWeight = r.fontWeight;
|
|
127
187
|
if (r.color) s.color = r.color;
|
|
@@ -315,6 +375,27 @@ function sameStyle(a: TextRun, b: TextRun): boolean {
|
|
|
315
375
|
function ShapeView({ el }: { el: ShapeElement }) {
|
|
316
376
|
const stroke = el.stroke ?? "transparent";
|
|
317
377
|
const sw = el.strokeWidth ?? 0;
|
|
378
|
+
// Custom vector path (PPTX <a:custGeom>) takes precedence over the preset
|
|
379
|
+
// kind — the path coordinates already encode the actual silhouette.
|
|
380
|
+
if (el.path) {
|
|
381
|
+
return (
|
|
382
|
+
<svg
|
|
383
|
+
viewBox={`0 0 ${el.path.viewW} ${el.path.viewH}`}
|
|
384
|
+
preserveAspectRatio="none"
|
|
385
|
+
width="100%"
|
|
386
|
+
height="100%"
|
|
387
|
+
>
|
|
388
|
+
<path
|
|
389
|
+
d={el.path.d}
|
|
390
|
+
fill={el.fill}
|
|
391
|
+
fillRule={el.path.fillRule ?? "nonzero"}
|
|
392
|
+
stroke={stroke}
|
|
393
|
+
strokeWidth={sw || undefined}
|
|
394
|
+
vectorEffect="non-scaling-stroke"
|
|
395
|
+
/>
|
|
396
|
+
</svg>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
318
399
|
if (el.shape === "rect" || el.shape === "rounded") {
|
|
319
400
|
return (
|
|
320
401
|
<div
|
|
@@ -474,7 +555,9 @@ function LineView({ el }: { el: LineElement }) {
|
|
|
474
555
|
function TableView({ el }: { el: TableElement }) {
|
|
475
556
|
const cols = el.rows[0]?.length ?? 1;
|
|
476
557
|
// PPTX-faithful: contiguous cells, no inter-cell gap, no rounded corners.
|
|
477
|
-
//
|
|
558
|
+
// Cells share their dividers via inset box-shadows so we draw a single
|
|
559
|
+
// grid line between adjacent cells instead of doubling-up borders.
|
|
560
|
+
const stroke = el.borderColor ?? "rgba(0, 0, 0, 0.12)";
|
|
478
561
|
return (
|
|
479
562
|
<div
|
|
480
563
|
style={{
|
|
@@ -485,6 +568,7 @@ function TableView({ el }: { el: TableElement }) {
|
|
|
485
568
|
height: "100%",
|
|
486
569
|
gap: 0,
|
|
487
570
|
background: "transparent",
|
|
571
|
+
boxShadow: `inset 0 0 0 1px ${stroke}`,
|
|
488
572
|
}}
|
|
489
573
|
>
|
|
490
574
|
{el.rows.flatMap((row, ri) =>
|
|
@@ -504,6 +588,10 @@ function TableView({ el }: { el: TableElement }) {
|
|
|
504
588
|
minHeight: 0,
|
|
505
589
|
overflow: "hidden",
|
|
506
590
|
wordBreak: "break-word",
|
|
591
|
+
borderRight:
|
|
592
|
+
ci < cols - 1 ? `1px solid ${stroke}` : undefined,
|
|
593
|
+
borderBottom:
|
|
594
|
+
ri < el.rows.length - 1 ? `1px solid ${stroke}` : undefined,
|
|
507
595
|
}}
|
|
508
596
|
>
|
|
509
597
|
{cell}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Slide } from "@/lib/types";
|
|
2
2
|
import { SLIDE_W, SLIDE_H } from "@/lib/types";
|
|
3
3
|
import { ElementView } from "./ElementView";
|
|
4
|
+
import {
|
|
5
|
+
useCanvasConfig,
|
|
6
|
+
resolveSlideBackground,
|
|
7
|
+
} from "@/compound/CanvasContext";
|
|
4
8
|
|
|
5
9
|
export function SlideView({
|
|
6
10
|
slide,
|
|
@@ -9,12 +13,13 @@ export function SlideView({
|
|
|
9
13
|
slide: Slide;
|
|
10
14
|
scale?: number;
|
|
11
15
|
}) {
|
|
16
|
+
const canvasConfig = useCanvasConfig();
|
|
12
17
|
return (
|
|
13
18
|
<div
|
|
14
19
|
style={{
|
|
15
20
|
width: SLIDE_W * scale,
|
|
16
21
|
height: SLIDE_H * scale,
|
|
17
|
-
background: slide
|
|
22
|
+
background: resolveSlideBackground(canvasConfig, slide),
|
|
18
23
|
position: "relative",
|
|
19
24
|
overflow: "hidden",
|
|
20
25
|
borderRadius: 12 * scale,
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, type ReactNode } from "react";
|
|
2
|
+
import type { Slide } from "@/lib/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Canvas/viewport configuration. Lets hosts tame the slide presentation so a
|
|
6
|
+
* slide with a bold background fill doesn't paint the entire workspace —
|
|
7
|
+
* which is what `<Slidewise.Root>` did by default before this prop existed.
|
|
8
|
+
*
|
|
9
|
+
* Pass any subset:
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <Slidewise.Root
|
|
13
|
+
* canvas={{
|
|
14
|
+
* padding: { x: 48, y: 32 },
|
|
15
|
+
* defaultZoom: 0.7,
|
|
16
|
+
* slideRadius: 12,
|
|
17
|
+
* slideShadow:
|
|
18
|
+
* "0 1px 2px rgba(0,0,0,0.25), 0 24px 60px rgba(0,0,0,0.45)",
|
|
19
|
+
* resolveSlideBackground: (slide) =>
|
|
20
|
+
* hostThemeOverridesSlideBg ? "#ffffff" : undefined,
|
|
21
|
+
* }}
|
|
22
|
+
* >
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Outside the slide rectangle hosts still own the canvas-frame backdrop via
|
|
26
|
+
* the `--slidewise-bg-canvas-from` / `--slidewise-bg-canvas-to` CSS tokens
|
|
27
|
+
* or the `surfaces.canvasFrom` / `surfaces.canvasTo` prop entries.
|
|
28
|
+
*/
|
|
29
|
+
export interface SlidewiseCanvasConfig {
|
|
30
|
+
/**
|
|
31
|
+
* Padding between the slide and the canvas frame, in pixels. Used both
|
|
32
|
+
* for the auto-fit calculation and as actual whitespace around the
|
|
33
|
+
* slide. Pass a number for uniform padding, or an object for per-axis
|
|
34
|
+
* control. Default: roughly the room the floating toolbars need —
|
|
35
|
+
* 32px horizontal, ~148px vertical (top bar + bottom toolbar).
|
|
36
|
+
*/
|
|
37
|
+
padding?: number | { x?: number; y?: number };
|
|
38
|
+
/**
|
|
39
|
+
* How the slide scales inside the canvas:
|
|
40
|
+
* - `"fit"` (default) auto-shrinks the slide to fit while preserving aspect
|
|
41
|
+
* - `"fill"` reserved for a future fill behavior; currently equivalent to fit
|
|
42
|
+
* - `"manual"` uses `defaultZoom` (or whatever the user has set via
|
|
43
|
+
* `api.setZoom` / pinch-zoom) verbatim
|
|
44
|
+
*
|
|
45
|
+
* Applied via `store.setFitMode()` once on mount. Subsequent user
|
|
46
|
+
* interactions (zoom in, fit toggle) override this.
|
|
47
|
+
*/
|
|
48
|
+
fitMode?: "fit" | "fill" | "manual";
|
|
49
|
+
/**
|
|
50
|
+
* Initial zoom level when `fitMode === "manual"` (or as a starting point
|
|
51
|
+
* when the user switches off auto-fit). 1 = 100%. Clamped to [0.1, 4].
|
|
52
|
+
*/
|
|
53
|
+
defaultZoom?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Box-shadow applied to the slide paper. Any valid CSS `box-shadow`
|
|
56
|
+
* value. Defaults to `var(--slide-shadow)`.
|
|
57
|
+
*/
|
|
58
|
+
slideShadow?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Border-radius applied to the slide paper. Number = pixels; string =
|
|
61
|
+
* any valid CSS length. Defaults to `8`.
|
|
62
|
+
*/
|
|
63
|
+
slideRadius?: number | string;
|
|
64
|
+
/**
|
|
65
|
+
* Hard-override the slide's background paint regardless of what the
|
|
66
|
+
* deck's `slide.background` says. Useful when the host wants every
|
|
67
|
+
* slide to render as a neutral surface (`#ffffff`, host-tinted, etc.)
|
|
68
|
+
* for a viewer experience where the deck's baked fills would clash
|
|
69
|
+
* with the chrome.
|
|
70
|
+
*
|
|
71
|
+
* If both `forceSlideBackground` and `resolveSlideBackground` are
|
|
72
|
+
* passed, the force value wins.
|
|
73
|
+
*/
|
|
74
|
+
forceSlideBackground?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Per-slide background resolver. Receives the current slide; return a
|
|
77
|
+
* CSS value to override, or `undefined` to fall through to the slide's
|
|
78
|
+
* own `background` property. Useful for "respect host theme but only
|
|
79
|
+
* for slides that don't explicitly set a fill" patterns.
|
|
80
|
+
*/
|
|
81
|
+
resolveSlideBackground?: (slide: Slide) => string | undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fully-resolved canvas config, with internal defaults filled in. Consumers
|
|
86
|
+
* (the Canvas component, host-rendered surfaces) read this shape via
|
|
87
|
+
* `useCanvasConfig()` and never have to check for undefined.
|
|
88
|
+
*/
|
|
89
|
+
export interface ResolvedCanvasConfig {
|
|
90
|
+
padding: { x: number; y: number };
|
|
91
|
+
fitMode: "fit" | "fill" | "manual" | null;
|
|
92
|
+
defaultZoom: number | null;
|
|
93
|
+
slideShadow: string;
|
|
94
|
+
slideRadius: number | string;
|
|
95
|
+
forceSlideBackground: string | null;
|
|
96
|
+
resolveSlideBackground:
|
|
97
|
+
| ((slide: Slide) => string | undefined)
|
|
98
|
+
| null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const DEFAULT_CANVAS_CONFIG: ResolvedCanvasConfig = {
|
|
102
|
+
// Matches the previous hardcoded padX=32 / padY=56+76+16 in Canvas.tsx —
|
|
103
|
+
// enough vertical room for the top bar + the floating bottom toolbar.
|
|
104
|
+
padding: { x: 32, y: 148 },
|
|
105
|
+
fitMode: null,
|
|
106
|
+
defaultZoom: null,
|
|
107
|
+
slideShadow: "var(--slide-shadow)",
|
|
108
|
+
slideRadius: 8,
|
|
109
|
+
forceSlideBackground: null,
|
|
110
|
+
resolveSlideBackground: null,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function mergeCanvasConfig(
|
|
114
|
+
config: SlidewiseCanvasConfig | undefined
|
|
115
|
+
): ResolvedCanvasConfig {
|
|
116
|
+
if (!config) return DEFAULT_CANVAS_CONFIG;
|
|
117
|
+
let padding = DEFAULT_CANVAS_CONFIG.padding;
|
|
118
|
+
if (typeof config.padding === "number") {
|
|
119
|
+
padding = { x: config.padding, y: config.padding };
|
|
120
|
+
} else if (config.padding && typeof config.padding === "object") {
|
|
121
|
+
padding = {
|
|
122
|
+
x: config.padding.x ?? DEFAULT_CANVAS_CONFIG.padding.x,
|
|
123
|
+
y: config.padding.y ?? DEFAULT_CANVAS_CONFIG.padding.y,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
padding,
|
|
128
|
+
fitMode: config.fitMode ?? null,
|
|
129
|
+
defaultZoom: config.defaultZoom ?? null,
|
|
130
|
+
slideShadow: config.slideShadow ?? DEFAULT_CANVAS_CONFIG.slideShadow,
|
|
131
|
+
slideRadius: config.slideRadius ?? DEFAULT_CANVAS_CONFIG.slideRadius,
|
|
132
|
+
forceSlideBackground: config.forceSlideBackground ?? null,
|
|
133
|
+
resolveSlideBackground: config.resolveSlideBackground ?? null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const CanvasContext = createContext<ResolvedCanvasConfig>(DEFAULT_CANVAS_CONFIG);
|
|
138
|
+
|
|
139
|
+
export function CanvasConfigProvider({
|
|
140
|
+
config,
|
|
141
|
+
children,
|
|
142
|
+
}: {
|
|
143
|
+
config: SlidewiseCanvasConfig | undefined;
|
|
144
|
+
children: ReactNode;
|
|
145
|
+
}) {
|
|
146
|
+
const value = useMemo(() => mergeCanvasConfig(config), [config]);
|
|
147
|
+
return (
|
|
148
|
+
<CanvasContext.Provider value={value}>{children}</CanvasContext.Provider>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Read the resolved canvas configuration. Always returns a complete object
|
|
154
|
+
* with defaults filled in.
|
|
155
|
+
*/
|
|
156
|
+
export function useCanvasConfig(): ResolvedCanvasConfig {
|
|
157
|
+
return useContext(CanvasContext);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Convenience helper used by the internal Canvas component (and exported
|
|
162
|
+
* so host-rendered slide previews can reuse the same resolution rules).
|
|
163
|
+
* Returns whatever the canvas config says, falling back to the slide's
|
|
164
|
+
* own `background` property.
|
|
165
|
+
*/
|
|
166
|
+
export function resolveSlideBackground(
|
|
167
|
+
config: ResolvedCanvasConfig,
|
|
168
|
+
slide: Slide
|
|
169
|
+
): string {
|
|
170
|
+
if (config.forceSlideBackground) return config.forceSlideBackground;
|
|
171
|
+
if (config.resolveSlideBackground) {
|
|
172
|
+
const resolved = config.resolveSlideBackground(slide);
|
|
173
|
+
if (resolved !== undefined) return resolved;
|
|
174
|
+
}
|
|
175
|
+
return slide.background;
|
|
176
|
+
}
|
|
@@ -29,6 +29,10 @@ import {
|
|
|
29
29
|
surfacesToCssVars,
|
|
30
30
|
type SlidewiseSurfaces,
|
|
31
31
|
} from "./SurfacesContext";
|
|
32
|
+
import {
|
|
33
|
+
CanvasConfigProvider,
|
|
34
|
+
type SlidewiseCanvasConfig,
|
|
35
|
+
} from "./CanvasContext";
|
|
32
36
|
|
|
33
37
|
export interface SlidewiseRootProps {
|
|
34
38
|
/**
|
|
@@ -118,6 +122,14 @@ export interface SlidewiseRootProps {
|
|
|
118
122
|
* ```
|
|
119
123
|
*/
|
|
120
124
|
surfaces?: SlidewiseSurfaces;
|
|
125
|
+
/**
|
|
126
|
+
* Canvas/viewport configuration. Set `padding`, `slideRadius`,
|
|
127
|
+
* `slideShadow`, and an initial `defaultZoom` to keep the slide
|
|
128
|
+
* presented as a centered card rather than letting a bold deck fill
|
|
129
|
+
* paint the entire workspace. `forceSlideBackground` /
|
|
130
|
+
* `resolveSlideBackground` let hosts override per-slide fills.
|
|
131
|
+
*/
|
|
132
|
+
canvas?: SlidewiseCanvasConfig;
|
|
121
133
|
/** Extra class names appended to the root. */
|
|
122
134
|
className?: string;
|
|
123
135
|
/** Inline style applied to the root. */
|
|
@@ -241,6 +253,7 @@ function RootInner({
|
|
|
241
253
|
icons,
|
|
242
254
|
labels,
|
|
243
255
|
surfaces,
|
|
256
|
+
canvas,
|
|
244
257
|
className,
|
|
245
258
|
style,
|
|
246
259
|
children,
|
|
@@ -305,6 +318,14 @@ function RootInner({
|
|
|
305
318
|
store.getState().selectSlide(initialSlideId);
|
|
306
319
|
}
|
|
307
320
|
}
|
|
321
|
+
// Apply canvas defaults once on mount. setZoom auto-flips fitMode to
|
|
322
|
+
// "manual" so we set fitMode last to honor an explicit canvas.fitMode.
|
|
323
|
+
if (canvas?.defaultZoom !== undefined) {
|
|
324
|
+
store.getState().setZoom(canvas.defaultZoom);
|
|
325
|
+
}
|
|
326
|
+
if (canvas?.fitMode) {
|
|
327
|
+
store.getState().setFitMode(canvas.fitMode);
|
|
328
|
+
}
|
|
308
329
|
// run once on mount
|
|
309
330
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
310
331
|
}, []);
|
|
@@ -515,17 +536,19 @@ function RootInner({
|
|
|
515
536
|
<IconProvider icons={icons ?? {}}>
|
|
516
537
|
<LabelsProvider labels={labels}>
|
|
517
538
|
<SurfacesProvider surfaces={surfaces}>
|
|
518
|
-
<
|
|
519
|
-
<
|
|
520
|
-
<
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
539
|
+
<CanvasConfigProvider config={canvas}>
|
|
540
|
+
<DirtyProvider dirty={dirty}>
|
|
541
|
+
<HostProvider callbacks={{ onSave: wrappedSave, onExport }}>
|
|
542
|
+
<RootShell
|
|
543
|
+
fontFamily={fontFamily}
|
|
544
|
+
className={combinedClassName}
|
|
545
|
+
style={mergedStyle}
|
|
546
|
+
>
|
|
547
|
+
{children}
|
|
548
|
+
</RootShell>
|
|
549
|
+
</HostProvider>
|
|
550
|
+
</DirtyProvider>
|
|
551
|
+
</CanvasConfigProvider>
|
|
529
552
|
</SurfacesProvider>
|
|
530
553
|
</LabelsProvider>
|
|
531
554
|
</IconProvider>
|
package/src/compound/index.ts
CHANGED
|
@@ -99,6 +99,14 @@ export {
|
|
|
99
99
|
surfacesToCssVars,
|
|
100
100
|
type SlidewiseSurfaces,
|
|
101
101
|
} from "./SurfacesContext";
|
|
102
|
+
export {
|
|
103
|
+
CanvasConfigProvider,
|
|
104
|
+
useCanvasConfig,
|
|
105
|
+
resolveSlideBackground,
|
|
106
|
+
DEFAULT_CANVAS_CONFIG,
|
|
107
|
+
type SlidewiseCanvasConfig,
|
|
108
|
+
type ResolvedCanvasConfig,
|
|
109
|
+
} from "./CanvasContext";
|
|
102
110
|
|
|
103
111
|
/**
|
|
104
112
|
* Store hooks. Use these from host components anywhere under
|
package/src/index.ts
CHANGED
|
@@ -48,6 +48,9 @@ export {
|
|
|
48
48
|
useDirty,
|
|
49
49
|
useLabels,
|
|
50
50
|
useSurfaces,
|
|
51
|
+
useCanvasConfig,
|
|
52
|
+
resolveSlideBackground,
|
|
53
|
+
DEFAULT_CANVAS_CONFIG,
|
|
51
54
|
useEditor,
|
|
52
55
|
useEditorStore,
|
|
53
56
|
useSlides,
|
|
@@ -67,7 +70,9 @@ export {
|
|
|
67
70
|
type SlidewiseIcons,
|
|
68
71
|
type SlidewiseLabels,
|
|
69
72
|
type SlidewiseSurfaces,
|
|
73
|
+
type SlidewiseCanvasConfig,
|
|
70
74
|
type ResolvedLabels,
|
|
75
|
+
type ResolvedCanvasConfig,
|
|
71
76
|
useSlideRailItem,
|
|
72
77
|
type RegionProps,
|
|
73
78
|
type TopBarProps,
|