@bendyline/squisq-editor-react 1.6.2 → 2.0.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 (218) hide show
  1. package/README.md +12 -3
  2. package/dist/index.d.ts +1275 -183
  3. package/dist/index.js +20345 -12777
  4. package/dist/index.js.map +1 -1
  5. package/dist/monaco.d.ts +1 -0
  6. package/dist/monaco.js +4 -0
  7. package/dist/monaco.js.map +1 -0
  8. package/dist/styles/index.css +1607 -241
  9. package/package.json +9 -5
  10. package/src/BlockPropertiesPopover.tsx +30 -15
  11. package/src/DocumentSettingsDialog.tsx +39 -33
  12. package/src/EditorContext.tsx +199 -42
  13. package/src/EditorShell.tsx +349 -261
  14. package/src/ImageEditor.tsx +22 -1
  15. package/src/InlinePreviewGutter.tsx +16 -8
  16. package/src/MediaBin.tsx +107 -14
  17. package/src/OutlinePanel.tsx +202 -7
  18. package/src/PlainHtmlPreview.tsx +40 -1
  19. package/src/PreviewControls.tsx +362 -89
  20. package/src/PreviewPanel.tsx +312 -78
  21. package/src/RawEditor.tsx +13 -2
  22. package/src/RecorderEntry.tsx +2 -0
  23. package/src/StatusBar.tsx +5 -2
  24. package/src/TemplateAnnotation.ts +12 -3
  25. package/src/TemplatePicker.tsx +75 -21
  26. package/src/ThemeCustomizerPanel.tsx +8 -0
  27. package/src/ThemePicker.tsx +10 -5
  28. package/src/TimelineBlockPreview.tsx +1 -1
  29. package/src/Toolbar.tsx +357 -51
  30. package/src/TransitionPicker.tsx +36 -4
  31. package/src/ViewMenuPanel.tsx +12 -14
  32. package/src/WysiwygEditor.tsx +124 -91
  33. package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
  34. package/src/__tests__/blockTagActivity.test.ts +183 -0
  35. package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
  36. package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
  37. package/src/__tests__/buildPreviewDocTransition.test.ts +21 -1
  38. package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
  39. package/src/__tests__/codeContextSectionView.test.tsx +8 -6
  40. package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
  41. package/src/__tests__/documentSettingsDialog.test.tsx +51 -1
  42. package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
  43. package/src/__tests__/editorScrollSync.test.ts +65 -0
  44. package/src/__tests__/editorShellProps.test.tsx +123 -1
  45. package/src/__tests__/emojiPicker.test.tsx +2 -42
  46. package/src/__tests__/headingTransition.test.ts +0 -12
  47. package/src/__tests__/imageEditAffordance.test.tsx +5 -1
  48. package/src/__tests__/imageEditModalContract.test.tsx +119 -0
  49. package/src/__tests__/imageEditorShell.test.tsx +15 -1
  50. package/src/__tests__/jsonEditor.test.tsx +56 -1
  51. package/src/__tests__/layersPanel.test.tsx +75 -5
  52. package/src/__tests__/majorApiTypes.test.ts +26 -0
  53. package/src/__tests__/mediaAttachmentFlow.test.ts +2 -2
  54. package/src/__tests__/mediaBinDrop.test.tsx +90 -0
  55. package/src/__tests__/narrationSave.test.ts +148 -0
  56. package/src/__tests__/outlinePanel.test.tsx +93 -3
  57. package/src/__tests__/outlineSource.test.ts +217 -0
  58. package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
  59. package/src/__tests__/presentationMode.test.tsx +290 -0
  60. package/src/__tests__/previewControls.test.tsx +382 -5
  61. package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
  62. package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
  63. package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
  64. package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
  65. package/src/__tests__/recorderTheme.test.tsx +42 -0
  66. package/src/__tests__/resolveBlockVisual.test.ts +43 -0
  67. package/src/__tests__/selectionConversions.test.ts +80 -0
  68. package/src/__tests__/statusBar.test.tsx +27 -0
  69. package/src/__tests__/teleprompter.test.tsx +190 -0
  70. package/src/__tests__/templateAnnotationRoundTrip.test.ts +26 -0
  71. package/src/__tests__/templatePickerPortal.test.tsx +62 -0
  72. package/src/__tests__/tiptapBridge.test.ts +71 -7
  73. package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
  74. package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
  75. package/src/__tests__/toolbarSelectionConversion.test.tsx +164 -0
  76. package/src/__tests__/transformStyleId.test.ts +13 -0
  77. package/src/__tests__/useImageEditor.test.tsx +67 -0
  78. package/src/__tests__/useMediaRecorder.test.ts +75 -0
  79. package/src/__tests__/viewMenuPanel.test.tsx +130 -0
  80. package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
  81. package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
  82. package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
  83. package/src/asciiDiagram/AsciiDiagramWidget.tsx +254 -0
  84. package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
  85. package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
  86. package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
  87. package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +284 -0
  88. package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
  89. package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
  90. package/src/asciiDiagram/asciiDiagramCommands.ts +174 -0
  91. package/src/asciiDiagram/asciiDiagramData.ts +149 -0
  92. package/src/asciiDiagram/asciiDiagramOps.ts +238 -0
  93. package/src/asciiDiagram/asciiPaste.ts +19 -0
  94. package/src/blockTagActivity.ts +233 -0
  95. package/src/buildDocumentPreviewMarkdown.ts +168 -0
  96. package/src/buildPreviewDoc.ts +19 -12
  97. package/src/codeContext/types.ts +1 -1
  98. package/src/customTemplates/CustomLayoutManager.tsx +3 -2
  99. package/src/customTemplates/CustomTemplateContext.tsx +6 -0
  100. package/src/customTemplates/TemplateDesigner.tsx +7 -4
  101. package/src/customTemplates/__tests__/library.test.ts +8 -0
  102. package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +13 -0
  103. package/src/customTemplates/designer.css +158 -99
  104. package/src/customTemplates/library.ts +16 -3
  105. package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
  106. package/src/customThemes/CustomThemeContext.tsx +6 -0
  107. package/src/customThemes/CustomThemeDialog.tsx +35 -5
  108. package/src/customThemes/ImportThemeSection.tsx +178 -0
  109. package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
  110. package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
  111. package/src/customThemes/customThemeLibrary.ts +7 -4
  112. package/src/customThemes/index.ts +7 -1
  113. package/src/customThemes/themeDraft.ts +23 -7
  114. package/src/diagram/DiagramCanvas.tsx +25 -6
  115. package/src/diagram/types.ts +35 -0
  116. package/src/editorScrollSync.ts +73 -0
  117. package/src/emojiData.ts +3 -23
  118. package/src/frontmatterSettings.ts +23 -0
  119. package/src/headingTransition.ts +0 -15
  120. package/src/imageEditor/CanvasSurface.tsx +29 -23
  121. package/src/imageEditor/LayersPanel.tsx +78 -3
  122. package/src/imageEditor/Toolbar.tsx +5 -1
  123. package/src/imageEditor/icons.tsx +4 -0
  124. package/src/imageEditor/image-editor.css +75 -0
  125. package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
  126. package/src/imageEditor/useImageEditor.ts +71 -14
  127. package/src/index.ts +198 -20
  128. package/src/jsonEditor/JsonEditorContext.tsx +12 -6
  129. package/src/jsonEditor/RenderNode.tsx +24 -3
  130. package/src/jsonEditor/editors.tsx +86 -19
  131. package/src/mediaEntries.ts +12 -0
  132. package/src/monaco.ts +35 -0
  133. package/src/monacoWorkers.ts +89 -0
  134. package/src/outlineSource.ts +171 -0
  135. package/src/presentation/PresentationMode.tsx +596 -0
  136. package/src/rawEditorIsolation.ts +18 -0
  137. package/src/recorder/RecorderButton.tsx +9 -1
  138. package/src/recorder/RecorderModal.tsx +84 -41
  139. package/src/recorder/RecorderPanel.tsx +9 -1
  140. package/src/recorder/hooks/useMediaRecorder.ts +97 -53
  141. package/src/resolveBlockVisual.ts +10 -4
  142. package/src/scene/Scene.tsx +48 -18
  143. package/src/scene/SceneBlockExtension.ts +17 -10
  144. package/src/scene/SceneBlockWidget.tsx +5 -1
  145. package/src/scene/SceneViewport.tsx +7 -3
  146. package/src/scene/ShapePalette.tsx +17 -2
  147. package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
  148. package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
  149. package/src/scene/__tests__/sceneIsolation.test.tsx +119 -0
  150. package/src/scene/adapters/DiagramAdapter.ts +12 -101
  151. package/src/scene/adapters/DrawingAdapter.ts +6 -1
  152. package/src/scene/adapters/LayoutAdapter.ts +3 -0
  153. package/src/scene/commands/SceneCommand.ts +17 -3
  154. package/src/scene/index.ts +1 -6
  155. package/src/scene/layers/DiagramEdges.tsx +21 -7
  156. package/src/scene/layers/edgeGeometry.ts +9 -1
  157. package/src/scene/layers/nodeCard.tsx +27 -8
  158. package/src/scene/scene.css +5 -2
  159. package/src/scene/text/SceneTextOverlay.tsx +5 -5
  160. package/src/scene/text/sceneTextChannel.ts +26 -20
  161. package/src/scene/text/sceneTextConfig.ts +4 -0
  162. package/src/scene/tools/ConnectTool.ts +13 -4
  163. package/src/scene/tools/SceneTool.ts +8 -0
  164. package/src/scene/tools/SelectTool.ts +39 -23
  165. package/src/scene/tools/ShapeTool.ts +2 -3
  166. package/src/selectionConversions.ts +155 -0
  167. package/src/styles/ascii-diagram.css +79 -0
  168. package/src/styles/ascii-timeline.css +596 -0
  169. package/src/styles/editor.css +736 -120
  170. package/src/styles/index.css +3 -0
  171. package/src/styles/tree-view.css +189 -0
  172. package/src/teleprompter/TeleprompterControls.tsx +218 -0
  173. package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
  174. package/src/teleprompter/TeleprompterSurface.tsx +267 -0
  175. package/src/teleprompter/TeleprompterView.tsx +338 -0
  176. package/src/teleprompter/canvasRenderer.ts +161 -0
  177. package/src/teleprompter/floatingWindow.ts +352 -0
  178. package/src/teleprompter/index.ts +61 -0
  179. package/src/teleprompter/pcmWorklet.ts +62 -0
  180. package/src/teleprompter/recording/insertPreamble.ts +80 -0
  181. package/src/teleprompter/recording/narrationSave.ts +134 -0
  182. package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
  183. package/src/teleprompter/scrollModel.ts +85 -0
  184. package/src/teleprompter/teleprompterTheme.ts +303 -0
  185. package/src/teleprompter/types.ts +38 -0
  186. package/src/teleprompter/useFloatingWindow.ts +74 -0
  187. package/src/teleprompter/useMicAnalysis.ts +211 -0
  188. package/src/teleprompter/useTeleprompter.ts +421 -0
  189. package/src/templateContentPreviewResolver.ts +12 -5
  190. package/src/timeline/TimelineEditorWidget.tsx +861 -0
  191. package/src/timeline/TimelineViewExtension.ts +252 -0
  192. package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +414 -0
  193. package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
  194. package/src/timeline/__tests__/timelineCommands.test.ts +359 -0
  195. package/src/timeline/__tests__/timelineOps.test.ts +277 -0
  196. package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
  197. package/src/timeline/timelineCommands.ts +421 -0
  198. package/src/timeline/timelineData.ts +54 -0
  199. package/src/timeline/timelineOps.ts +381 -0
  200. package/src/timeline/timelinePaste.ts +8 -0
  201. package/src/tiptapBridge.ts +47 -21
  202. package/src/transformStyleId.ts +17 -0
  203. package/src/treeview/TreeOutlineWidget.tsx +390 -0
  204. package/src/treeview/TreeViewExtension.ts +250 -0
  205. package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
  206. package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
  207. package/src/treeview/__tests__/treeOps.test.ts +177 -0
  208. package/src/treeview/__tests__/treePaste.test.ts +40 -0
  209. package/src/treeview/__tests__/treeViewCommands.test.ts +179 -0
  210. package/src/treeview/treeOps.ts +231 -0
  211. package/src/treeview/treePaste.ts +13 -0
  212. package/src/treeview/treeViewCommands.ts +99 -0
  213. package/src/treeview/treeViewData.ts +41 -0
  214. package/src/useMonacoLoader.ts +11 -34
  215. package/src/wysiwygImageUpload.ts +113 -0
  216. package/src/diagram/DiagramExtension.ts +0 -209
  217. package/src/diagram/DiagramWidget.tsx +0 -272
  218. package/src/diagram/useDiagramData.ts +0 -126
@@ -6,7 +6,7 @@
6
6
  * and a one-sentence description so authors can quickly find the right layout.
7
7
  */
8
8
 
9
- import { useEffect, useMemo, useRef, useState } from 'react';
9
+ import { useEffect, useId, useMemo, useRef, useState } from 'react';
10
10
  import { createPortal } from 'react-dom';
11
11
  import type { CustomTemplateDefinition } from '@bendyline/squisq/schemas';
12
12
  import { TEMPLATE_METADATA, resolveTemplateName } from '@bendyline/squisq/doc';
@@ -36,14 +36,20 @@ interface TemplateEntry {
36
36
  icon: JSX.Element;
37
37
  }
38
38
 
39
- /**
40
- * DOM id of the portaled template gallery. Exported so hosts that embed the
41
- * picker inside their own popovers (e.g. the toolbar overflow menu) can treat
42
- * clicks inside the gallery as "inside" in their outside-click handling.
43
- */
44
- export const TEMPLATE_GALLERY_PORTAL_ID = 'squisq-template-gallery-portal';
39
+ /** Matches every gallery when multiple editor instances are mounted. */
40
+ export const TEMPLATE_GALLERY_PORTAL_SELECTOR = '[data-squisq-template-gallery-portal]';
45
41
 
46
42
  const TEMPLATE_GALLERY_DIALOG_ID = 'squisq-template-gallery-dialog';
43
+ const TEMPLATE_GALLERY_PORTAL_ID_PREFIX = 'squisq-template-gallery-portal';
44
+
45
+ /**
46
+ * Give each portaled gallery a stable, collision-free id. Hosts should match
47
+ * galleries through {@link TEMPLATE_GALLERY_PORTAL_SELECTOR}; a process-wide
48
+ * singleton id cannot identify the right editor when several are mounted.
49
+ */
50
+ function useTemplateGalleryPortalId(): string {
51
+ return `${TEMPLATE_GALLERY_PORTAL_ID_PREFIX}-${useId().replace(/:/g, '')}`;
52
+ }
47
53
 
48
54
  const W = 56;
49
55
  const H = 40;
@@ -419,28 +425,62 @@ export const TEMPLATE_ENTRIES: TemplateEntry[] = [
419
425
  <rect x={4} y={6} width={16} height={10} rx={2} fill={FA} opacity={0.85} />
420
426
  <rect x={36} y={6} width={16} height={10} rx={2} fill={F2} opacity={0.8} />
421
427
  <rect x={20} y={24} width={16} height={10} rx={2} fill={F2} opacity={0.8} />
428
+ <path d="M 20 11 L 36 11" stroke={FA} strokeWidth={1.5} fill="none" opacity={0.7} />
429
+ <path d="M 36 11 L 32 8.5 L 32 13.5 Z" fill={FA} opacity={0.7} />
422
430
  <path
423
- d="M 20 11 L 36 11"
431
+ d="M 12 16 C 12 22, 24 22, 24 24"
424
432
  stroke={FA}
425
433
  strokeWidth={1.5}
426
434
  fill="none"
427
435
  opacity={0.7}
428
- markerEnd="url(#tp-diagram-arrow)"
429
436
  />
430
437
  <path
431
- d="M 12 16 C 12 22, 24 22, 24 24"
438
+ d="M 44 16 C 44 22, 32 22, 32 24"
432
439
  stroke={FA}
433
440
  strokeWidth={1.5}
434
441
  fill="none"
435
442
  opacity={0.7}
436
443
  />
444
+ </TemplateIcon>
445
+ ),
446
+ },
447
+ {
448
+ name: 'tree',
449
+ label: 'Tree',
450
+ description:
451
+ 'Renders an ASCII file-tree / outline fence as a filesystem-style treeview — edit as an outline.',
452
+ icon: (
453
+ <TemplateIcon>
454
+ <rect x={6} y={5} width={16} height={7} rx={1.5} fill={FA} opacity={0.85} />
437
455
  <path
438
- d="M 44 16 C 44 22, 32 22, 32 24"
439
- stroke={FA}
456
+ d="M 12 12 L 12 34 M 12 20 L 22 20 M 12 34 L 22 34"
457
+ stroke={F1}
440
458
  strokeWidth={1.5}
441
459
  fill="none"
442
460
  opacity={0.7}
443
461
  />
462
+ <rect x={24} y={16} width={14} height={7} rx={1.5} fill={F2} opacity={0.8} />
463
+ <rect x={24} y={30} width={14} height={7} rx={1.5} fill={F2} opacity={0.8} />
464
+ <rect x={26} y={2} width={14} height={7} rx={1.5} fill={F2} opacity={0.6} />
465
+ </TemplateIcon>
466
+ ),
467
+ },
468
+ {
469
+ name: 'timeline',
470
+ label: 'Timeline',
471
+ description:
472
+ 'Plots events across one or more horizontal tracks with callouts and optional branching links.',
473
+ icon: (
474
+ <TemplateIcon>
475
+ <path d="M 5 21 L 51 21" stroke={F1} strokeWidth={1.8} fill="none" />
476
+ <path d="M 47 18 L 52 21 L 47 24" stroke={F1} strokeWidth={1.5} fill="none" />
477
+ <circle cx={13} cy={21} r={3} fill={FA} />
478
+ <circle cx={29} cy={21} r={3} fill={F2} />
479
+ <circle cx={44} cy={21} r={3} fill={FA} />
480
+ <path d="M 13 18 L 13 10 M 29 24 L 29 32 M 44 18 L 44 8" stroke={F1} strokeWidth={1} />
481
+ <rect x={7} y={5} width={12} height={3} rx={1} fill={F2} opacity={0.8} />
482
+ <rect x={23} y={33} width={12} height={3} rx={1} fill={F2} opacity={0.8} />
483
+ <rect x={38} y={3} width={12} height={3} rx={1} fill={F2} opacity={0.8} />
444
484
  </TemplateIcon>
445
485
  ),
446
486
  },
@@ -566,6 +606,8 @@ export function TemplatePicker({
566
606
  const [dialogStyle, setDialogStyle] = useState<React.CSSProperties>({});
567
607
  const containerRef = useRef<HTMLDivElement>(null);
568
608
  const triggerRef = useRef<HTMLButtonElement>(null);
609
+ const dialogRef = useRef<HTMLDivElement>(null);
610
+ const dialogId = `${TEMPLATE_GALLERY_DIALOG_ID}-${useId().replace(/:/g, '')}`;
569
611
 
570
612
  const updateDialogBounds = () => {
571
613
  if (!triggerRef.current) return;
@@ -583,7 +625,7 @@ export function TemplatePicker({
583
625
  const handler = (e: MouseEvent) => {
584
626
  const target = e.target as Node;
585
627
  const inTrigger = triggerRef.current?.contains(target);
586
- const inDialog = document.getElementById(TEMPLATE_GALLERY_DIALOG_ID)?.contains(target);
628
+ const inDialog = dialogRef.current?.contains(target);
587
629
  if (!inTrigger && !inDialog) {
588
630
  setOpen(false);
589
631
  }
@@ -646,6 +688,8 @@ export function TemplatePicker({
646
688
  const gallery = open
647
689
  ? createPortal(
648
690
  <TemplateGalleryDialog
691
+ dialogRef={dialogRef}
692
+ dialogId={dialogId}
649
693
  title={dialogTitle}
650
694
  colorScheme={colorScheme}
651
695
  style={dialogStyle}
@@ -673,6 +717,7 @@ export function TemplatePicker({
673
717
  onClick={handleOpen}
674
718
  aria-haspopup="dialog"
675
719
  aria-expanded={open}
720
+ aria-controls={open ? dialogId : undefined}
676
721
  title="Choose block type"
677
722
  >
678
723
  <span className="squisq-template-picker-trigger-label">Block:</span>
@@ -707,12 +752,16 @@ export function TemplatePicker({
707
752
  // ── Reusable gallery body ──────────────────────────────────────────
708
753
 
709
754
  function TemplateGalleryDialog({
755
+ dialogRef,
756
+ dialogId,
710
757
  children,
711
758
  title,
712
759
  colorScheme,
713
760
  style,
714
761
  onClose,
715
762
  }: {
763
+ dialogRef: React.RefObject<HTMLDivElement>;
764
+ dialogId: string;
716
765
  children: React.ReactNode;
717
766
  title: string;
718
767
  colorScheme: 'light' | 'dark';
@@ -721,7 +770,8 @@ function TemplateGalleryDialog({
721
770
  }) {
722
771
  return (
723
772
  <div
724
- id={TEMPLATE_GALLERY_DIALOG_ID}
773
+ ref={dialogRef}
774
+ id={dialogId}
725
775
  className="squisq-template-gallery-dialog"
726
776
  data-theme={colorScheme}
727
777
  style={style}
@@ -833,6 +883,7 @@ function TemplateGalleryBody({
833
883
  onOpenDesigner?: () => void;
834
884
  previewSource?: TemplatePreviewSource;
835
885
  }) {
886
+ const galleryId = useTemplateGalleryPortalId();
836
887
  const [query, setQuery] = useState('');
837
888
  const searchRef = useRef<HTMLInputElement>(null);
838
889
  // Pull merged doc + library templates from the surrounding context.
@@ -894,7 +945,8 @@ function TemplateGalleryBody({
894
945
 
895
946
  return (
896
947
  <div
897
- id={TEMPLATE_GALLERY_PORTAL_ID}
948
+ id={galleryId}
949
+ data-squisq-template-gallery-portal=""
898
950
  className={`squisq-template-gallery${grouped ? ' squisq-template-gallery--segmented' : ''}`}
899
951
  role="listbox"
900
952
  aria-label="Block types"
@@ -1154,10 +1206,13 @@ export function TemplateBadgePopover({
1154
1206
  previewSource,
1155
1207
  }: TemplateBadgePopoverProps) {
1156
1208
  const [style, setStyle] = useState<React.CSSProperties>(() => computeDialogStyle(anchorRect));
1209
+ const dialogRef = useRef<HTMLDivElement>(null);
1210
+ const dialogId = `${TEMPLATE_GALLERY_DIALOG_ID}-${useId().replace(/:/g, '')}`;
1157
1211
 
1158
1212
  // Reposition once after mount so the dialog covers the current editor shell.
1159
1213
  useEffect(() => {
1160
- requestAnimationFrame(() => setStyle(computeDialogStyle(anchorRect)));
1214
+ const frame = requestAnimationFrame(() => setStyle(computeDialogStyle(anchorRect)));
1215
+ return () => cancelAnimationFrame(frame);
1161
1216
  }, [anchorRect]);
1162
1217
 
1163
1218
  // Outside click + Escape close
@@ -1167,7 +1222,7 @@ export function TemplateBadgePopover({
1167
1222
  };
1168
1223
  const onMouse = (e: MouseEvent) => {
1169
1224
  const target = e.target as Node;
1170
- const inDialog = document.getElementById(TEMPLATE_GALLERY_DIALOG_ID)?.contains(target);
1225
+ const inDialog = dialogRef.current?.contains(target);
1171
1226
  if (!inDialog) onClose();
1172
1227
  };
1173
1228
  // Defer the mousedown listener by one frame so the click that opened
@@ -1192,6 +1247,8 @@ export function TemplateBadgePopover({
1192
1247
 
1193
1248
  return createPortal(
1194
1249
  <TemplateGalleryDialog
1250
+ dialogRef={dialogRef}
1251
+ dialogId={dialogId}
1195
1252
  title={dialogTitle}
1196
1253
  colorScheme={colorScheme}
1197
1254
  style={style}
@@ -1211,7 +1268,7 @@ export function TemplateBadgePopover({
1211
1268
  }
1212
1269
 
1213
1270
  function templateDialogTitle(previewSource?: TemplatePreviewSource): string {
1214
- return `Block type for ${templateDialogBlockTitle(previewSource)}`;
1271
+ return `Block Type for ${templateDialogBlockTitle(previewSource)}`;
1215
1272
  }
1216
1273
 
1217
1274
  function templateDialogBlockTitle(previewSource?: TemplatePreviewSource): string {
@@ -1253,8 +1310,5 @@ function findEditorShellRect(anchor: Element | DOMRect): DOMRect {
1253
1310
  }
1254
1311
  }
1255
1312
 
1256
- const fallback = document.querySelector('.squisq-editor-shell');
1257
- if (fallback) return fallback.getBoundingClientRect();
1258
-
1259
1313
  return new DOMRect(0, 0, window.innerWidth, window.innerHeight);
1260
1314
  }
@@ -40,6 +40,7 @@ import {
40
40
  type ImageTreatmentPreset,
41
41
  } from './customThemes/themeDraft';
42
42
  import { Section, SeedColorRow, FontPicker, PresetRow } from './customThemes/themeControls';
43
+ import { ImportThemeSection } from './customThemes/ImportThemeSection';
43
44
 
44
45
  // ── Component ───────────────────────────────────────────────────────
45
46
 
@@ -188,6 +189,13 @@ export function ThemeCustomizerPanel({
188
189
  />
189
190
  </Section>
190
191
 
192
+ <Section
193
+ title="Import from file"
194
+ hint="Pull colors and fonts from a Word, PowerPoint, or Excel file."
195
+ >
196
+ <ImportThemeSection onImported={(patch) => updateDraft(patch)} />
197
+ </Section>
198
+
191
199
  <Section title="Colors" hint="Pick seed colors. The rest is derived.">
192
200
  <SeedColorRow
193
201
  label="Primary"
@@ -9,7 +9,7 @@
9
9
  * preview-on-hover style listing rather than a wall of names.
10
10
  */
11
11
 
12
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
12
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
13
13
  import { createPortal } from 'react-dom';
14
14
  import {
15
15
  getThemeSummaries,
@@ -95,6 +95,7 @@ function ThemeNameChip({
95
95
  return (
96
96
  <span
97
97
  className={`squisq-theme-picker-name-chip${className ? ` ${className}` : ''}`}
98
+ data-theme-id={theme.id}
98
99
  style={{
99
100
  background: theme.colors.background,
100
101
  color: theme.colors.text,
@@ -102,7 +103,7 @@ function ThemeNameChip({
102
103
  borderColor: theme.colors.backgroundLight,
103
104
  }}
104
105
  >
105
- {label}
106
+ <span className="squisq-theme-picker-name-chip-label">{label}</span>
106
107
  </span>
107
108
  );
108
109
  }
@@ -133,6 +134,8 @@ export function ThemePicker({
133
134
  const [open, setOpen] = useState(false);
134
135
  const [popoverStyle, setPopoverStyle] = useState<React.CSSProperties>({});
135
136
  const triggerRef = useRef<HTMLButtonElement>(null);
137
+ const popoverRef = useRef<HTMLDivElement>(null);
138
+ const popoverId = `squisq-theme-picker-popover-${useId().replace(/:/g, '')}`;
136
139
 
137
140
  // Custom entries are computed per-render (built-ins stay static at module
138
141
  // load). A custom theme is already a full Theme, so it becomes an entry
@@ -189,7 +192,7 @@ export function ThemePicker({
189
192
  // trigger as-is. On the first open it doesn't exist yet — we'll
190
193
  // re-run after mount via the requestAnimationFrame in the open
191
194
  // effect, so estimate generously for paint #1.
192
- const popoverEl = document.getElementById('squisq-theme-picker-popover');
195
+ const popoverEl = popoverRef.current;
193
196
  const measured = popoverEl?.scrollHeight ?? 520;
194
197
  const spaceBelow = vh - rect.bottom - margin - gap;
195
198
  const spaceAbove = rect.top - margin - gap;
@@ -229,7 +232,7 @@ export function ThemePicker({
229
232
  const onDown = (e: MouseEvent) => {
230
233
  const target = e.target as Node;
231
234
  if (triggerRef.current?.contains(target)) return;
232
- const popoverEl = document.getElementById('squisq-theme-picker-popover');
235
+ const popoverEl = popoverRef.current;
233
236
  if (popoverEl?.contains(target)) return;
234
237
  setOpen(false);
235
238
  };
@@ -258,7 +261,8 @@ export function ThemePicker({
258
261
  const popover = open
259
262
  ? createPortal(
260
263
  <div
261
- id="squisq-theme-picker-popover"
264
+ ref={popoverRef}
265
+ id={popoverId}
262
266
  className="squisq-theme-picker-popover"
263
267
  role="listbox"
264
268
  aria-label={ariaLabel}
@@ -392,6 +396,7 @@ export function ThemePicker({
392
396
  onClick={handleToggle}
393
397
  aria-haspopup="listbox"
394
398
  aria-expanded={open}
399
+ aria-controls={open ? popoverId : undefined}
395
400
  aria-label={ariaLabel}
396
401
  >
397
402
  {previewEntry && (
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Renders a tiny slideshow-style thumbnail of a single block, used to fill each
5
5
  * bar on the timeline. Reuses the same rendering path as the inline preview
6
- * gutter and the slideshow: `buildPreviewDoc` → `getLayers` → `BlockRenderer`.
6
+ * gutter and the slideshow: `buildPreviewDoc` → `materializeBlockLayers` → `BlockRenderer`.
7
7
  */
8
8
 
9
9
  import { memo } from 'react';