@bendyline/squisq-editor-react 1.1.0 → 1.1.1
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/EditorContext.d.ts +6 -2
- package/dist/EditorContext.d.ts.map +1 -1
- package/dist/EditorContext.js +3 -1
- package/dist/EditorContext.js.map +1 -1
- package/dist/EditorShell.d.ts +11 -1
- package/dist/EditorShell.d.ts.map +1 -1
- package/dist/EditorShell.js +9 -7
- package/dist/EditorShell.js.map +1 -1
- package/dist/ImageNodeView.d.ts +15 -0
- package/dist/ImageNodeView.d.ts.map +1 -0
- package/dist/ImageNodeView.js +52 -0
- package/dist/ImageNodeView.js.map +1 -0
- package/dist/PreviewControls.d.ts +41 -0
- package/dist/PreviewControls.d.ts.map +1 -0
- package/dist/PreviewControls.js +201 -0
- package/dist/PreviewControls.js.map +1 -0
- package/dist/PreviewPanel.d.ts +7 -7
- package/dist/PreviewPanel.d.ts.map +1 -1
- package/dist/PreviewPanel.js +183 -199
- package/dist/PreviewPanel.js.map +1 -1
- package/dist/Toolbar.d.ts +8 -1
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar.js +4 -12
- package/dist/Toolbar.js.map +1 -1
- package/dist/WysiwygEditor.d.ts.map +1 -1
- package/dist/WysiwygEditor.js +3 -1
- package/dist/WysiwygEditor.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tiptapBridge.d.ts.map +1 -1
- package/dist/tiptapBridge.js +4 -5
- package/dist/tiptapBridge.js.map +1 -1
- package/package.json +5 -4
- package/src/EditorContext.tsx +8 -1
- package/src/EditorShell.tsx +71 -32
- package/src/ImageNodeView.tsx +70 -0
- package/src/PreviewControls.tsx +340 -0
- package/src/PreviewPanel.tsx +216 -287
- package/src/Toolbar.tsx +23 -6
- package/src/WysiwygEditor.tsx +3 -1
- package/src/index.ts +6 -0
- package/src/styles/editor.css +31 -8
- package/src/tiptapBridge.ts +5 -6
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/* eslint-disable react-refresh/only-export-components */
|
|
2
|
+
/**
|
|
3
|
+
* PreviewControls
|
|
4
|
+
*
|
|
5
|
+
* Shared context and inline toolbar component for preview settings
|
|
6
|
+
* (viewport format, display mode, theme, transform, caption style).
|
|
7
|
+
*
|
|
8
|
+
* The context is provided by EditorShell and consumed by both:
|
|
9
|
+
* - PreviewControls (toolbar dropdowns, rendered in the main toolbar)
|
|
10
|
+
* - PreviewPanel (the actual player, which reads the selected values)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createContext, useContext, useState, useMemo, useEffect } from 'react';
|
|
14
|
+
import type { ReactNode } from 'react';
|
|
15
|
+
import type { DisplayMode, CaptionStyle } from '@bendyline/squisq-react';
|
|
16
|
+
import type { ViewportPreset, ViewportConfig } from '@bendyline/squisq/schemas';
|
|
17
|
+
import { VIEWPORT_PRESETS, getThemeSummaries, resolveTheme } from '@bendyline/squisq/schemas';
|
|
18
|
+
import type { Theme } from '@bendyline/squisq/schemas';
|
|
19
|
+
import { getTransformStyleSummaries } from '@bendyline/squisq/transform';
|
|
20
|
+
import type { Doc } from '@bendyline/squisq/schemas';
|
|
21
|
+
|
|
22
|
+
// ── Context ──────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export interface PreviewSettings {
|
|
25
|
+
activePreset: ViewportPreset;
|
|
26
|
+
setSelectedPreset: (preset: ViewportPreset | null) => void;
|
|
27
|
+
activeViewport: ViewportConfig;
|
|
28
|
+
activeDisplayMode: DisplayMode;
|
|
29
|
+
setSelectedDisplayMode: (mode: DisplayMode | null) => void;
|
|
30
|
+
activeThemeId: string;
|
|
31
|
+
setSelectedThemeId: (id: string | null) => void;
|
|
32
|
+
activeTheme: Theme;
|
|
33
|
+
activeTransformStyle: string;
|
|
34
|
+
setSelectedTransformStyle: (id: string | null) => void;
|
|
35
|
+
activeCaptionStyle: CaptionStyle;
|
|
36
|
+
setSelectedCaptionStyle: (style: CaptionStyle | null) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const PreviewSettingsContext = createContext<PreviewSettings | null>(null);
|
|
40
|
+
|
|
41
|
+
export function usePreviewSettings(): PreviewSettings {
|
|
42
|
+
const ctx = useContext(PreviewSettingsContext);
|
|
43
|
+
if (!ctx) throw new Error('usePreviewSettings must be used within PreviewSettingsProvider');
|
|
44
|
+
return ctx;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Frontmatter resolvers ────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
function resolveRenderAs(value: unknown): ViewportPreset | null {
|
|
50
|
+
if (typeof value !== 'string') return null;
|
|
51
|
+
const v = value.trim().toLowerCase();
|
|
52
|
+
const mapping: Record<string, ViewportPreset> = {
|
|
53
|
+
landscape: 'landscape',
|
|
54
|
+
'16:9': 'landscape',
|
|
55
|
+
widescreen: 'landscape',
|
|
56
|
+
portrait: 'portrait',
|
|
57
|
+
'9:16': 'portrait',
|
|
58
|
+
vertical: 'portrait',
|
|
59
|
+
stories: 'portrait',
|
|
60
|
+
square: 'square',
|
|
61
|
+
'1:1': 'square',
|
|
62
|
+
standard: 'standard',
|
|
63
|
+
'4:3': 'standard',
|
|
64
|
+
};
|
|
65
|
+
return mapping[v] ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveDisplayMode(value: unknown): DisplayMode | null {
|
|
69
|
+
if (typeof value !== 'string') return null;
|
|
70
|
+
const v = value.trim().toLowerCase();
|
|
71
|
+
if (v === 'video' || v === 'slideshow' || v === 'linear') return v;
|
|
72
|
+
if (v === 'slides' || v === 'presentation' || v === 'deck') return 'slideshow';
|
|
73
|
+
if (v === 'document' || v === 'scroll' || v === 'page') return 'linear';
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const VALID_THEME_IDS = new Set(getThemeSummaries().map((s) => s.id));
|
|
78
|
+
|
|
79
|
+
function resolveFrontmatterTheme(value: unknown): string | null {
|
|
80
|
+
if (typeof value !== 'string') return null;
|
|
81
|
+
const v = value.trim().toLowerCase();
|
|
82
|
+
if (VALID_THEME_IDS.has(v)) return v;
|
|
83
|
+
const normalized = v.replace(/\s+/g, '-');
|
|
84
|
+
if (VALID_THEME_IDS.has(normalized)) return normalized;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const VALID_TRANSFORM_IDS = new Set(getTransformStyleSummaries().map((s) => s.id));
|
|
89
|
+
|
|
90
|
+
function resolveFrontmatterTransform(value: unknown): string | null {
|
|
91
|
+
if (typeof value !== 'string') return null;
|
|
92
|
+
const v = value.trim().toLowerCase();
|
|
93
|
+
if (VALID_TRANSFORM_IDS.has(v)) return v;
|
|
94
|
+
const normalized = v.replace(/\s+/g, '-');
|
|
95
|
+
if (VALID_TRANSFORM_IDS.has(normalized)) return normalized;
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resolveFrontmatterCaptionStyle(value: unknown): CaptionStyle | null {
|
|
100
|
+
if (typeof value !== 'string') return null;
|
|
101
|
+
const v = value.trim().toLowerCase();
|
|
102
|
+
if (v === 'standard' || v === 'social') return v;
|
|
103
|
+
if (v === 'instagram' || v === 'tiktok' || v === 'reels') return 'social';
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Provider ─────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
export interface PreviewSettingsProviderProps {
|
|
110
|
+
doc: Doc | null;
|
|
111
|
+
children: ReactNode;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function PreviewSettingsProvider({ doc, children }: PreviewSettingsProviderProps) {
|
|
115
|
+
const frontmatter = doc?.frontmatter;
|
|
116
|
+
|
|
117
|
+
// Viewport
|
|
118
|
+
const fmPreset = useMemo(
|
|
119
|
+
() => resolveRenderAs(frontmatter?.['document-render-as']),
|
|
120
|
+
[frontmatter],
|
|
121
|
+
);
|
|
122
|
+
const [selectedPreset, setSelectedPreset] = useState<ViewportPreset | null>(null);
|
|
123
|
+
useEffect(() => setSelectedPreset(null), [fmPreset]);
|
|
124
|
+
const activePreset = selectedPreset ?? fmPreset ?? 'landscape';
|
|
125
|
+
const activeViewport = VIEWPORT_PRESETS[activePreset];
|
|
126
|
+
|
|
127
|
+
// Display mode
|
|
128
|
+
const fmMode = useMemo(() => resolveDisplayMode(frontmatter?.['display-mode']), [frontmatter]);
|
|
129
|
+
const [selectedDisplayMode, setSelectedDisplayMode] = useState<DisplayMode | null>(null);
|
|
130
|
+
useEffect(() => setSelectedDisplayMode(null), [fmMode]);
|
|
131
|
+
const activeDisplayMode = selectedDisplayMode ?? fmMode ?? 'video';
|
|
132
|
+
|
|
133
|
+
// Theme
|
|
134
|
+
const fmTheme = useMemo(() => resolveFrontmatterTheme(frontmatter?.['theme']), [frontmatter]);
|
|
135
|
+
const [selectedThemeId, setSelectedThemeId] = useState<string | null>(null);
|
|
136
|
+
useEffect(() => setSelectedThemeId(null), [fmTheme]);
|
|
137
|
+
const activeThemeId = selectedThemeId ?? fmTheme ?? 'standard';
|
|
138
|
+
const activeTheme = useMemo(() => resolveTheme(activeThemeId), [activeThemeId]);
|
|
139
|
+
|
|
140
|
+
// Transform
|
|
141
|
+
const fmTransform = useMemo(
|
|
142
|
+
() => resolveFrontmatterTransform(frontmatter?.['transform-style']),
|
|
143
|
+
[frontmatter],
|
|
144
|
+
);
|
|
145
|
+
const [selectedTransformStyle, setSelectedTransformStyle] = useState<string | null>(null);
|
|
146
|
+
useEffect(() => setSelectedTransformStyle(null), [fmTransform]);
|
|
147
|
+
const activeTransformStyle = selectedTransformStyle ?? fmTransform ?? '';
|
|
148
|
+
|
|
149
|
+
// Caption style
|
|
150
|
+
const fmCaption = useMemo(
|
|
151
|
+
() => resolveFrontmatterCaptionStyle(frontmatter?.['caption-style']),
|
|
152
|
+
[frontmatter],
|
|
153
|
+
);
|
|
154
|
+
const [selectedCaptionStyle, setSelectedCaptionStyle] = useState<CaptionStyle | null>(null);
|
|
155
|
+
useEffect(() => setSelectedCaptionStyle(null), [fmCaption]);
|
|
156
|
+
const activeCaptionStyle = selectedCaptionStyle ?? fmCaption ?? 'standard';
|
|
157
|
+
|
|
158
|
+
const value = useMemo<PreviewSettings>(
|
|
159
|
+
() => ({
|
|
160
|
+
activePreset,
|
|
161
|
+
setSelectedPreset,
|
|
162
|
+
activeViewport,
|
|
163
|
+
activeDisplayMode,
|
|
164
|
+
setSelectedDisplayMode,
|
|
165
|
+
activeThemeId,
|
|
166
|
+
setSelectedThemeId,
|
|
167
|
+
activeTheme,
|
|
168
|
+
activeTransformStyle,
|
|
169
|
+
setSelectedTransformStyle,
|
|
170
|
+
activeCaptionStyle,
|
|
171
|
+
setSelectedCaptionStyle,
|
|
172
|
+
}),
|
|
173
|
+
[
|
|
174
|
+
activePreset,
|
|
175
|
+
activeViewport,
|
|
176
|
+
activeDisplayMode,
|
|
177
|
+
activeThemeId,
|
|
178
|
+
activeTheme,
|
|
179
|
+
activeTransformStyle,
|
|
180
|
+
activeCaptionStyle,
|
|
181
|
+
],
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<PreviewSettingsContext.Provider value={value}>{children}</PreviewSettingsContext.Provider>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Dropdown options ─────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
const VIEWPORT_OPTIONS: { key: ViewportPreset; label: string }[] = [
|
|
192
|
+
{ key: 'landscape', label: '16:9' },
|
|
193
|
+
{ key: 'portrait', label: '9:16' },
|
|
194
|
+
{ key: 'square', label: '1:1' },
|
|
195
|
+
{ key: 'standard', label: '4:3' },
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
const DISPLAY_MODE_OPTIONS: { key: DisplayMode; label: string }[] = [
|
|
199
|
+
{ key: 'video', label: 'Video' },
|
|
200
|
+
{ key: 'slideshow', label: 'Slideshow' },
|
|
201
|
+
{ key: 'linear', label: 'Document' },
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const THEME_OPTIONS = getThemeSummaries().map((s) => ({ key: s.id, label: s.name }));
|
|
205
|
+
|
|
206
|
+
const TRANSFORM_STYLE_OPTIONS = [
|
|
207
|
+
{ key: '', label: 'None' },
|
|
208
|
+
...getTransformStyleSummaries().map((s) => ({ key: s.id, label: s.name })),
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
const CAPTION_STYLE_OPTIONS: { key: CaptionStyle; label: string }[] = [
|
|
212
|
+
{ key: 'standard', label: 'Standard' },
|
|
213
|
+
{ key: 'social', label: 'Social' },
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
// ── Shared styles ────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
const labelStyle: React.CSSProperties = {
|
|
219
|
+
color: 'var(--squisq-text-muted, #6b7280)',
|
|
220
|
+
fontSize: '12px',
|
|
221
|
+
whiteSpace: 'nowrap',
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const selectStyle: React.CSSProperties = {
|
|
225
|
+
padding: '2px 6px',
|
|
226
|
+
borderRadius: '4px',
|
|
227
|
+
border: '1px solid var(--squisq-border, #d1d5db)',
|
|
228
|
+
background: 'var(--squisq-input-bg, #fff)',
|
|
229
|
+
color: 'var(--squisq-text, #1f2937)',
|
|
230
|
+
fontSize: '12px',
|
|
231
|
+
cursor: 'pointer',
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// ── Toolbar Controls Component ───────────────────────────────────
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Inline preview controls rendered in the main toolbar row.
|
|
238
|
+
* Reads from PreviewSettingsContext.
|
|
239
|
+
*/
|
|
240
|
+
export function PreviewToolbarControls() {
|
|
241
|
+
const s = usePreviewSettings();
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<div
|
|
245
|
+
style={{
|
|
246
|
+
display: 'flex',
|
|
247
|
+
alignItems: 'center',
|
|
248
|
+
gap: '6px',
|
|
249
|
+
flexWrap: 'wrap',
|
|
250
|
+
padding: '2px 0',
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<label style={labelStyle}>Format:</label>
|
|
254
|
+
<select
|
|
255
|
+
value={s.activePreset}
|
|
256
|
+
onChange={(e) => s.setSelectedPreset(e.target.value as ViewportPreset)}
|
|
257
|
+
style={selectStyle}
|
|
258
|
+
>
|
|
259
|
+
{VIEWPORT_OPTIONS.map((o) => (
|
|
260
|
+
<option key={o.key} value={o.key}>
|
|
261
|
+
{o.label}
|
|
262
|
+
</option>
|
|
263
|
+
))}
|
|
264
|
+
</select>
|
|
265
|
+
|
|
266
|
+
<Divider />
|
|
267
|
+
|
|
268
|
+
<label style={labelStyle}>Mode:</label>
|
|
269
|
+
<select
|
|
270
|
+
value={s.activeDisplayMode}
|
|
271
|
+
onChange={(e) => s.setSelectedDisplayMode(e.target.value as DisplayMode)}
|
|
272
|
+
style={selectStyle}
|
|
273
|
+
>
|
|
274
|
+
{DISPLAY_MODE_OPTIONS.map((o) => (
|
|
275
|
+
<option key={o.key} value={o.key}>
|
|
276
|
+
{o.label}
|
|
277
|
+
</option>
|
|
278
|
+
))}
|
|
279
|
+
</select>
|
|
280
|
+
|
|
281
|
+
<Divider />
|
|
282
|
+
|
|
283
|
+
<label style={labelStyle}>Theme:</label>
|
|
284
|
+
<select
|
|
285
|
+
value={s.activeThemeId}
|
|
286
|
+
onChange={(e) => s.setSelectedThemeId(e.target.value)}
|
|
287
|
+
style={selectStyle}
|
|
288
|
+
>
|
|
289
|
+
{THEME_OPTIONS.map((o) => (
|
|
290
|
+
<option key={o.key} value={o.key}>
|
|
291
|
+
{o.label}
|
|
292
|
+
</option>
|
|
293
|
+
))}
|
|
294
|
+
</select>
|
|
295
|
+
|
|
296
|
+
<Divider />
|
|
297
|
+
|
|
298
|
+
<label style={labelStyle}>Transform:</label>
|
|
299
|
+
<select
|
|
300
|
+
value={s.activeTransformStyle}
|
|
301
|
+
onChange={(e) => s.setSelectedTransformStyle(e.target.value)}
|
|
302
|
+
style={selectStyle}
|
|
303
|
+
>
|
|
304
|
+
{TRANSFORM_STYLE_OPTIONS.map((o) => (
|
|
305
|
+
<option key={o.key} value={o.key}>
|
|
306
|
+
{o.label}
|
|
307
|
+
</option>
|
|
308
|
+
))}
|
|
309
|
+
</select>
|
|
310
|
+
|
|
311
|
+
<Divider />
|
|
312
|
+
|
|
313
|
+
<label style={labelStyle}>Captions:</label>
|
|
314
|
+
<select
|
|
315
|
+
value={s.activeCaptionStyle}
|
|
316
|
+
onChange={(e) => s.setSelectedCaptionStyle(e.target.value as CaptionStyle)}
|
|
317
|
+
style={selectStyle}
|
|
318
|
+
>
|
|
319
|
+
{CAPTION_STYLE_OPTIONS.map((o) => (
|
|
320
|
+
<option key={o.key} value={o.key}>
|
|
321
|
+
{o.label}
|
|
322
|
+
</option>
|
|
323
|
+
))}
|
|
324
|
+
</select>
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function Divider() {
|
|
330
|
+
return (
|
|
331
|
+
<span
|
|
332
|
+
style={{
|
|
333
|
+
width: '1px',
|
|
334
|
+
height: '16px',
|
|
335
|
+
background: 'var(--squisq-border, #d1d5db)',
|
|
336
|
+
margin: '0 2px',
|
|
337
|
+
}}
|
|
338
|
+
/>
|
|
339
|
+
);
|
|
340
|
+
}
|