@bendyline/squisq-editor-react 1.1.1 → 1.2.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.
@@ -10,7 +10,7 @@
10
10
  * - PreviewPanel (the actual player, which reads the selected values)
11
11
  */
12
12
 
13
- import { createContext, useContext, useState, useMemo, useEffect } from 'react';
13
+ import { createContext, useContext, useState, useMemo, useEffect, useRef } from 'react';
14
14
  import type { ReactNode } from 'react';
15
15
  import type { DisplayMode, CaptionStyle } from '@bendyline/squisq-react';
16
16
  import type { ViewportPreset, ViewportConfig } from '@bendyline/squisq/schemas';
@@ -233,90 +233,132 @@ const selectStyle: React.CSSProperties = {
233
233
 
234
234
  // ── Toolbar Controls Component ───────────────────────────────────
235
235
 
236
+ /** Hook to track whether the viewport is narrow. */
237
+ function useIsNarrow(breakpoint = 600): boolean {
238
+ const [narrow, setNarrow] = useState(
239
+ () => typeof window !== 'undefined' && window.innerWidth <= breakpoint,
240
+ );
241
+ useEffect(() => {
242
+ const mq = window.matchMedia(`(max-width: ${breakpoint}px)`);
243
+ const handler = (e: MediaQueryListEvent) => setNarrow(e.matches);
244
+ mq.addEventListener('change', handler);
245
+ return () => mq.removeEventListener('change', handler);
246
+ }, [breakpoint]);
247
+ return narrow;
248
+ }
249
+
236
250
  /**
237
251
  * Inline preview controls rendered in the main toolbar row.
238
- * Reads from PreviewSettingsContext.
252
+ * On narrow viewports, collapses into a single settings button with a dropdown.
239
253
  */
240
254
  export function PreviewToolbarControls() {
241
255
  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
256
+ const isNarrow = useIsNarrow(768);
257
+ const [popoverOpen, setPopoverOpen] = useState(false);
258
+ const popoverRef = useRef<HTMLDivElement>(null);
259
+
260
+ // Close popover on outside click
261
+ useEffect(() => {
262
+ if (!popoverOpen) return;
263
+ const handler = (e: MouseEvent) => {
264
+ if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
265
+ setPopoverOpen(false);
266
+ }
267
+ };
268
+ document.addEventListener('mousedown', handler);
269
+ return () => document.removeEventListener('mousedown', handler);
270
+ }, [popoverOpen]);
271
+
272
+ const controls = (
273
+ <>
274
+ <PreviewSelect
275
+ label="Format"
255
276
  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
277
+ options={VIEWPORT_OPTIONS}
278
+ onChange={(v) => s.setSelectedPreset(v as ViewportPreset)}
279
+ compact={isNarrow}
280
+ />
281
+ <PreviewSelect
282
+ label="Mode"
270
283
  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
284
+ options={DISPLAY_MODE_OPTIONS}
285
+ onChange={(v) => s.setSelectedDisplayMode(v as DisplayMode)}
286
+ compact={isNarrow}
287
+ />
288
+ <PreviewSelect
289
+ label="Theme"
285
290
  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
291
+ options={THEME_OPTIONS}
292
+ onChange={(v) => s.setSelectedThemeId(v)}
293
+ compact={isNarrow}
294
+ />
295
+ <PreviewSelect
296
+ label="Transform"
300
297
  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>
298
+ options={TRANSFORM_STYLE_OPTIONS}
299
+ onChange={(v) => s.setSelectedTransformStyle(v)}
300
+ compact={isNarrow}
301
+ />
302
+ <PreviewSelect
303
+ label="Captions"
304
+ value={s.activeCaptionStyle}
305
+ options={CAPTION_STYLE_OPTIONS}
306
+ onChange={(v) => s.setSelectedCaptionStyle(v as CaptionStyle)}
307
+ compact={isNarrow}
308
+ />
309
+ </>
310
+ );
310
311
 
311
- <Divider />
312
+ if (isNarrow) {
313
+ return (
314
+ <div className="squisq-preview-controls-compact" ref={popoverRef}>
315
+ <button
316
+ className="squisq-toolbar-button"
317
+ onClick={() => setPopoverOpen((v) => !v)}
318
+ aria-label="Preview settings"
319
+ title="Preview settings"
320
+ aria-expanded={popoverOpen}
321
+ >
322
+ <svg
323
+ width="16"
324
+ height="16"
325
+ viewBox="0 0 16 16"
326
+ fill="none"
327
+ stroke="currentColor"
328
+ strokeWidth="1.5"
329
+ strokeLinecap="round"
330
+ strokeLinejoin="round"
331
+ >
332
+ <circle cx="8" cy="8" r="2.5" />
333
+ <path d="M13.5 8a5.5 5.5 0 01-.4 1.8l1.2 1.2-1.6 1.6-1.2-1.2A5.5 5.5 0 018 13.5a5.5 5.5 0 01-3.5-1.3L3.3 13.4 1.7 11.8l1.2-1.2A5.5 5.5 0 012.5 8c0-.6.1-1.2.4-1.8L1.7 5 3.3 3.4l1.2 1.2A5.5 5.5 0 018 2.5c1.3 0 2.5.5 3.5 1.3l1.2-1.2 1.6 1.6-1.2 1.2c.3.6.4 1.2.4 1.6z" />
334
+ </svg>
335
+ </button>
336
+ {popoverOpen && <div className="squisq-preview-controls-popover">{controls}</div>}
337
+ </div>
338
+ );
339
+ }
340
+
341
+ return <div className="squisq-preview-controls-inline">{controls}</div>;
342
+ }
312
343
 
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) => (
344
+ function PreviewSelect({
345
+ label,
346
+ value,
347
+ options,
348
+ onChange,
349
+ compact,
350
+ }: {
351
+ label: string;
352
+ value: string;
353
+ options: { key: string; label: string }[];
354
+ onChange: (value: string) => void;
355
+ compact?: boolean;
356
+ }) {
357
+ return (
358
+ <div className={`squisq-preview-control${compact ? ' squisq-preview-control--compact' : ''}`}>
359
+ <label style={labelStyle}>{label}:</label>
360
+ <select value={value} onChange={(e) => onChange(e.target.value)} style={selectStyle}>
361
+ {options.map((o) => (
320
362
  <option key={o.key} value={o.key}>
321
363
  {o.label}
322
364
  </option>
@@ -325,16 +367,3 @@ export function PreviewToolbarControls() {
325
367
  </div>
326
368
  );
327
369
  }
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
- }
package/src/RawEditor.tsx CHANGED
@@ -12,7 +12,16 @@ import * as monaco from 'monaco-editor';
12
12
  import { useEditorContext } from './EditorContext';
13
13
  import { getAvailableTemplates } from '@bendyline/squisq/doc';
14
14
 
15
- // Use locally installed monaco-editor instead of CDN
15
+ // Use locally installed monaco-editor instead of CDN.
16
+ //
17
+ // NOTE: By default this imports the full monaco-editor with all 80+ languages
18
+ // and workers (~9MB). Consumers can dramatically reduce bundle size by aliasing
19
+ // 'monaco-editor' to a slim entry in their bundler config. For example with Vite:
20
+ //
21
+ // resolve: { alias: [{ find: /^monaco-editor$/, replacement: './monaco-slim.ts' }] }
22
+ //
23
+ // Where monaco-slim.ts re-exports 'monaco-editor/esm/vs/editor/editor.api' plus
24
+ // only the language contributions needed (e.g. markdown, javascript, etc.).
16
25
  loader.config({ monaco });
17
26
 
18
27
  export interface RawEditorProps {