@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.
Files changed (45) hide show
  1. package/dist/EditorContext.d.ts +6 -2
  2. package/dist/EditorContext.d.ts.map +1 -1
  3. package/dist/EditorContext.js +3 -1
  4. package/dist/EditorContext.js.map +1 -1
  5. package/dist/EditorShell.d.ts +11 -1
  6. package/dist/EditorShell.d.ts.map +1 -1
  7. package/dist/EditorShell.js +9 -7
  8. package/dist/EditorShell.js.map +1 -1
  9. package/dist/ImageNodeView.d.ts +15 -0
  10. package/dist/ImageNodeView.d.ts.map +1 -0
  11. package/dist/ImageNodeView.js +52 -0
  12. package/dist/ImageNodeView.js.map +1 -0
  13. package/dist/PreviewControls.d.ts +41 -0
  14. package/dist/PreviewControls.d.ts.map +1 -0
  15. package/dist/PreviewControls.js +201 -0
  16. package/dist/PreviewControls.js.map +1 -0
  17. package/dist/PreviewPanel.d.ts +7 -7
  18. package/dist/PreviewPanel.d.ts.map +1 -1
  19. package/dist/PreviewPanel.js +183 -199
  20. package/dist/PreviewPanel.js.map +1 -1
  21. package/dist/Toolbar.d.ts +8 -1
  22. package/dist/Toolbar.d.ts.map +1 -1
  23. package/dist/Toolbar.js +4 -12
  24. package/dist/Toolbar.js.map +1 -1
  25. package/dist/WysiwygEditor.d.ts.map +1 -1
  26. package/dist/WysiwygEditor.js +3 -1
  27. package/dist/WysiwygEditor.js.map +1 -1
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/tiptapBridge.d.ts.map +1 -1
  33. package/dist/tiptapBridge.js +4 -5
  34. package/dist/tiptapBridge.js.map +1 -1
  35. package/package.json +5 -4
  36. package/src/EditorContext.tsx +8 -1
  37. package/src/EditorShell.tsx +71 -32
  38. package/src/ImageNodeView.tsx +70 -0
  39. package/src/PreviewControls.tsx +340 -0
  40. package/src/PreviewPanel.tsx +216 -287
  41. package/src/Toolbar.tsx +23 -6
  42. package/src/WysiwygEditor.tsx +3 -1
  43. package/src/index.ts +6 -0
  44. package/src/styles/editor.css +31 -8
  45. 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
+ }