@bendyline/squisq-editor-react 1.6.1 → 2.0.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.
Files changed (208) hide show
  1. package/README.md +12 -3
  2. package/dist/index.d.ts +1304 -208
  3. package/dist/index.js +22838 -14494
  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 +2384 -408
  9. package/package.json +9 -5
  10. package/src/BlockPropertiesPopover.tsx +48 -17
  11. package/src/DocumentSettingsDialog.tsx +13 -21
  12. package/src/EditorContext.tsx +199 -42
  13. package/src/EditorShell.tsx +403 -270
  14. package/src/ImageEditor.tsx +22 -1
  15. package/src/InlinePreviewGutter.tsx +16 -8
  16. package/src/MediaBin.tsx +276 -40
  17. package/src/OutlinePanel.tsx +202 -7
  18. package/src/PlainHtmlPreview.tsx +40 -1
  19. package/src/PreviewControls.tsx +466 -93
  20. package/src/PreviewPanel.tsx +313 -77
  21. package/src/RawEditor.tsx +13 -2
  22. package/src/StatusBar.tsx +5 -2
  23. package/src/TemplateAnnotation.ts +34 -10
  24. package/src/TemplateContentPreview.tsx +56 -0
  25. package/src/TemplatePicker.tsx +355 -134
  26. package/src/ThemeCustomizerPanel.tsx +30 -15
  27. package/src/ThemePicker.tsx +10 -5
  28. package/src/TimelineBlockPreview.tsx +1 -1
  29. package/src/Toolbar.tsx +719 -238
  30. package/src/TransitionPicker.tsx +44 -5
  31. package/src/ViewMenuPanel.tsx +12 -14
  32. package/src/ViewSwitcher.tsx +4 -4
  33. package/src/WysiwygEditor.tsx +167 -92
  34. package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
  35. package/src/__tests__/blockTagActivity.test.ts +183 -0
  36. package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
  37. package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
  38. package/src/__tests__/buildPreviewDocTransition.test.ts +22 -3
  39. package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
  40. package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
  41. package/src/__tests__/documentSettingsDialog.test.tsx +29 -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 +390 -1
  45. package/src/__tests__/emojiPicker.test.tsx +2 -42
  46. package/src/__tests__/headingTransition.test.ts +53 -15
  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 +38 -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__/mediaBinDrop.test.tsx +90 -0
  54. package/src/__tests__/mediaReferences.test.ts +82 -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 +312 -6
  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__/resolveBlockVisual.test.ts +43 -0
  66. package/src/__tests__/statusBar.test.tsx +27 -0
  67. package/src/__tests__/teleprompter.test.tsx +190 -0
  68. package/src/__tests__/templateAnnotationRoundTrip.test.ts +49 -2
  69. package/src/__tests__/templateContentPreview.test.ts +101 -0
  70. package/src/__tests__/templatePickerPortal.test.tsx +62 -0
  71. package/src/__tests__/tiptapBridge.test.ts +70 -0
  72. package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
  73. package/src/__tests__/transformStyleId.test.ts +13 -0
  74. package/src/__tests__/useImageEditor.test.tsx +67 -0
  75. package/src/__tests__/useMediaRecorder.test.ts +75 -0
  76. package/src/__tests__/viewMenuPanel.test.tsx +130 -0
  77. package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
  78. package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
  79. package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
  80. package/src/asciiDiagram/AsciiDiagramWidget.tsx +224 -0
  81. package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
  82. package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
  83. package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
  84. package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +227 -0
  85. package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
  86. package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
  87. package/src/asciiDiagram/asciiDiagramCommands.ts +148 -0
  88. package/src/asciiDiagram/asciiDiagramData.ts +116 -0
  89. package/src/asciiDiagram/asciiDiagramOps.ts +219 -0
  90. package/src/asciiDiagram/asciiPaste.ts +19 -0
  91. package/src/blockTagActivity.ts +233 -0
  92. package/src/buildDocumentPreviewMarkdown.ts +168 -0
  93. package/src/buildPreviewDoc.ts +19 -12
  94. package/src/customTemplates/CustomLayoutManager.tsx +3 -2
  95. package/src/customTemplates/CustomTemplateContext.tsx +6 -0
  96. package/src/customTemplates/TemplateDesigner.tsx +7 -4
  97. package/src/customTemplates/__tests__/library.test.ts +8 -0
  98. package/src/customTemplates/designer.css +158 -99
  99. package/src/customTemplates/library.ts +16 -3
  100. package/src/customThemes/CustomThemeContext.tsx +6 -0
  101. package/src/customThemes/CustomThemeDialog.tsx +35 -5
  102. package/src/customThemes/ImportThemeSection.tsx +178 -0
  103. package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
  104. package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
  105. package/src/customThemes/customThemeLibrary.ts +7 -4
  106. package/src/customThemes/index.ts +7 -1
  107. package/src/customThemes/themeDraft.ts +23 -7
  108. package/src/diagram/DiagramCanvas.tsx +9 -4
  109. package/src/diagram/types.ts +35 -0
  110. package/src/editorScrollSync.ts +73 -0
  111. package/src/emojiData.ts +3 -23
  112. package/src/headingTransition.ts +85 -25
  113. package/src/imageEditor/CanvasSurface.tsx +29 -23
  114. package/src/imageEditor/LayersPanel.tsx +78 -3
  115. package/src/imageEditor/Toolbar.tsx +5 -1
  116. package/src/imageEditor/icons.tsx +4 -0
  117. package/src/imageEditor/image-editor.css +75 -0
  118. package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
  119. package/src/imageEditor/useImageEditor.ts +71 -14
  120. package/src/index.ts +193 -20
  121. package/src/jsonEditor/JsonEditorContext.tsx +12 -6
  122. package/src/jsonEditor/RenderNode.tsx +24 -3
  123. package/src/jsonEditor/editors.tsx +86 -19
  124. package/src/mediaEntries.ts +12 -0
  125. package/src/mediaReferences.ts +299 -0
  126. package/src/monaco.ts +35 -0
  127. package/src/monacoWorkers.ts +89 -0
  128. package/src/outlineSource.ts +171 -0
  129. package/src/presentation/PresentationMode.tsx +596 -0
  130. package/src/rawEditorIsolation.ts +18 -0
  131. package/src/recorder/hooks/useMediaRecorder.ts +97 -53
  132. package/src/resolveBlockVisual.ts +10 -4
  133. package/src/scene/Scene.tsx +101 -25
  134. package/src/scene/SceneBlockExtension.ts +17 -10
  135. package/src/scene/SceneBlockWidget.tsx +11 -4
  136. package/src/scene/SceneSelection.tsx +19 -15
  137. package/src/scene/SceneSideToolbar.tsx +89 -0
  138. package/src/scene/SceneViewport.tsx +7 -3
  139. package/src/scene/ShapePalette.tsx +17 -2
  140. package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
  141. package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
  142. package/src/scene/__tests__/sceneIsolation.test.tsx +93 -0
  143. package/src/scene/adapters/DiagramAdapter.ts +12 -101
  144. package/src/scene/commands/SceneCommand.ts +4 -1
  145. package/src/scene/index.ts +1 -6
  146. package/src/scene/layers/DiagramEdges.tsx +24 -9
  147. package/src/scene/layers/edgeGeometry.ts +31 -4
  148. package/src/scene/layers/nodeCard.tsx +27 -8
  149. package/src/scene/scene.css +145 -2
  150. package/src/scene/text/SceneTextOverlay.tsx +5 -5
  151. package/src/scene/text/sceneTextChannel.ts +26 -20
  152. package/src/scene/text/sceneTextConfig.ts +4 -0
  153. package/src/scene/tools/ConnectTool.ts +126 -28
  154. package/src/scene/tools/DrawingConnectTool.ts +86 -16
  155. package/src/scene/tools/SceneTool.ts +10 -0
  156. package/src/scene/tools/SelectTool.ts +34 -18
  157. package/src/scene/tools/ShapeTool.ts +2 -3
  158. package/src/styles/ascii-diagram.css +79 -0
  159. package/src/styles/ascii-timeline.css +499 -0
  160. package/src/styles/editor.css +1566 -219
  161. package/src/styles/index.css +3 -0
  162. package/src/styles/tree-view.css +139 -0
  163. package/src/teleprompter/TeleprompterControls.tsx +218 -0
  164. package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
  165. package/src/teleprompter/TeleprompterSurface.tsx +267 -0
  166. package/src/teleprompter/TeleprompterView.tsx +338 -0
  167. package/src/teleprompter/canvasRenderer.ts +161 -0
  168. package/src/teleprompter/floatingWindow.ts +352 -0
  169. package/src/teleprompter/index.ts +61 -0
  170. package/src/teleprompter/pcmWorklet.ts +62 -0
  171. package/src/teleprompter/recording/insertPreamble.ts +80 -0
  172. package/src/teleprompter/recording/narrationSave.ts +134 -0
  173. package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
  174. package/src/teleprompter/scrollModel.ts +85 -0
  175. package/src/teleprompter/teleprompterTheme.ts +303 -0
  176. package/src/teleprompter/types.ts +38 -0
  177. package/src/teleprompter/useFloatingWindow.ts +74 -0
  178. package/src/teleprompter/useMicAnalysis.ts +211 -0
  179. package/src/teleprompter/useTeleprompter.ts +421 -0
  180. package/src/templateContentPreviewResolver.ts +360 -0
  181. package/src/timeline/TimelineEditorWidget.tsx +702 -0
  182. package/src/timeline/TimelineViewExtension.ts +252 -0
  183. package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +355 -0
  184. package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
  185. package/src/timeline/__tests__/timelineCommands.test.ts +327 -0
  186. package/src/timeline/__tests__/timelineOps.test.ts +222 -0
  187. package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
  188. package/src/timeline/timelineCommands.ts +367 -0
  189. package/src/timeline/timelineData.ts +54 -0
  190. package/src/timeline/timelineOps.ts +277 -0
  191. package/src/timeline/timelinePaste.ts +8 -0
  192. package/src/tiptapBridge.ts +84 -49
  193. package/src/transformStyleId.ts +17 -0
  194. package/src/treeview/TreeOutlineWidget.tsx +240 -0
  195. package/src/treeview/TreeViewExtension.ts +250 -0
  196. package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
  197. package/src/treeview/__tests__/treeOps.test.ts +125 -0
  198. package/src/treeview/__tests__/treePaste.test.ts +40 -0
  199. package/src/treeview/__tests__/treeViewCommands.test.ts +163 -0
  200. package/src/treeview/treeOps.ts +172 -0
  201. package/src/treeview/treePaste.ts +13 -0
  202. package/src/treeview/treeViewCommands.ts +94 -0
  203. package/src/treeview/treeViewData.ts +41 -0
  204. package/src/useMonacoLoader.ts +11 -34
  205. package/src/wysiwygImageUpload.ts +113 -0
  206. package/src/diagram/DiagramExtension.ts +0 -209
  207. package/src/diagram/DiagramWidget.tsx +0 -270
  208. package/src/diagram/useDiagramData.ts +0 -126
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { useEffect, useId, useRef, useState, type ChangeEvent } from 'react';
10
10
  import {
11
+ appendPointer,
11
12
  arrayItemKind,
12
13
  type ControlKind,
13
14
  type SquisqAnnotatedSchema,
@@ -21,6 +22,19 @@ export interface EditorProps {
21
22
  schema: SquisqAnnotatedSchema;
22
23
  pointer: string;
23
24
  disabled: boolean;
25
+ controlId?: string;
26
+ labelledBy?: string;
27
+ describedBy?: string;
28
+ invalid?: boolean;
29
+ }
30
+
31
+ function accessibilityProps(props: EditorProps) {
32
+ return {
33
+ id: props.controlId,
34
+ 'aria-labelledby': props.labelledBy,
35
+ 'aria-describedby': props.describedBy,
36
+ 'aria-invalid': props.invalid || undefined,
37
+ };
24
38
  }
25
39
 
26
40
  // ── Helpers ───────────────────────────────────────────────────────
@@ -38,10 +52,12 @@ function asNumber(v: unknown): number | '' {
38
52
 
39
53
  // ── Primitive editors ────────────────────────────────────────────
40
54
 
41
- export function TextEditor({ value, schema, pointer, disabled }: EditorProps) {
55
+ export function TextEditor(props: EditorProps) {
56
+ const { value, schema, pointer, disabled } = props;
42
57
  const { setAtPath } = useJsonEditor();
43
58
  return (
44
59
  <input
60
+ {...accessibilityProps(props)}
45
61
  type="text"
46
62
  className="squisq-jf-input"
47
63
  value={asString(value)}
@@ -52,7 +68,8 @@ export function TextEditor({ value, schema, pointer, disabled }: EditorProps) {
52
68
  );
53
69
  }
54
70
 
55
- export function MultilineEditor({ value, schema, pointer, disabled }: EditorProps) {
71
+ export function MultilineEditor(props: EditorProps) {
72
+ const { value, schema, pointer, disabled } = props;
56
73
  const { setAtPath } = useJsonEditor();
57
74
  const ref = useRef<HTMLTextAreaElement>(null);
58
75
  // Auto-grow vertically.
@@ -64,6 +81,7 @@ export function MultilineEditor({ value, schema, pointer, disabled }: EditorProp
64
81
  }, [value]);
65
82
  return (
66
83
  <textarea
84
+ {...accessibilityProps(props)}
67
85
  ref={ref}
68
86
  className="squisq-jf-textarea"
69
87
  value={asString(value)}
@@ -86,7 +104,8 @@ export function RichTextEditor({ value, schema, pointer, disabled }: EditorProps
86
104
  );
87
105
  }
88
106
 
89
- export function NumberStepperEditor({ value, schema, pointer, disabled }: EditorProps) {
107
+ export function NumberStepperEditor(props: EditorProps) {
108
+ const { value, schema, pointer, disabled } = props;
90
109
  const { setAtPath } = useJsonEditor();
91
110
  const isInt = schema.type === 'integer';
92
111
  const step = schema.squisq?.step ?? schema.multipleOf ?? (isInt ? 1 : 0.1);
@@ -111,6 +130,7 @@ export function NumberStepperEditor({ value, schema, pointer, disabled }: Editor
111
130
 
112
131
  </button>
113
132
  <input
133
+ {...accessibilityProps(props)}
114
134
  className="squisq-jf-stepper__input"
115
135
  type="number"
116
136
  value={num === '' ? '' : num}
@@ -138,7 +158,8 @@ export function NumberStepperEditor({ value, schema, pointer, disabled }: Editor
138
158
  );
139
159
  }
140
160
 
141
- export function SliderEditor({ value, schema, pointer, disabled }: EditorProps) {
161
+ export function SliderEditor(props: EditorProps) {
162
+ const { value, schema, pointer, disabled } = props;
142
163
  const { setAtPath } = useJsonEditor();
143
164
  const min = (schema.minimum ?? schema.exclusiveMinimum ?? 0) as number;
144
165
  const max = (schema.maximum ?? schema.exclusiveMaximum ?? 100) as number;
@@ -148,6 +169,7 @@ export function SliderEditor({ value, schema, pointer, disabled }: EditorProps)
148
169
  return (
149
170
  <span className="squisq-jf-slider-row">
150
171
  <input
172
+ {...accessibilityProps(props)}
151
173
  type="range"
152
174
  className="squisq-jf-slider"
153
175
  min={min}
@@ -162,11 +184,13 @@ export function SliderEditor({ value, schema, pointer, disabled }: EditorProps)
162
184
  );
163
185
  }
164
186
 
165
- export function ToggleEditor({ value, schema, pointer, disabled }: EditorProps) {
187
+ export function ToggleEditor(props: EditorProps) {
188
+ const { value, schema, pointer, disabled } = props;
166
189
  const { setAtPath } = useJsonEditor();
167
190
  const on = Boolean(value);
168
191
  return (
169
192
  <button
193
+ {...accessibilityProps(props)}
170
194
  type="button"
171
195
  className={`squisq-jf-toggle${on ? ' squisq-jf-toggle--on' : ''}`}
172
196
  disabled={disabled}
@@ -189,12 +213,17 @@ export function ToggleEditor({ value, schema, pointer, disabled }: EditorProps)
189
213
  );
190
214
  }
191
215
 
192
- export function CheckboxEditor({ value, schema, pointer, disabled }: EditorProps) {
216
+ export function CheckboxEditor(props: EditorProps) {
217
+ const { value, schema, pointer, disabled } = props;
193
218
  const { setAtPath } = useJsonEditor();
194
- const id = useId();
219
+ const generatedId = useId();
220
+ const id = props.controlId ?? generatedId;
195
221
  return (
196
222
  <label htmlFor={id} className="squisq-jf-toggle">
197
223
  <input
224
+ aria-labelledby={props.labelledBy}
225
+ aria-describedby={props.describedBy}
226
+ aria-invalid={props.invalid || undefined}
198
227
  id={id}
199
228
  type="checkbox"
200
229
  checked={Boolean(value)}
@@ -214,11 +243,12 @@ function enumOptions(schema: SquisqAnnotatedSchema): { value: unknown; label: st
214
243
  }));
215
244
  }
216
245
 
217
- export function SegmentedEditor({ value, schema, pointer, disabled }: EditorProps) {
246
+ export function SegmentedEditor(props: EditorProps) {
247
+ const { value, schema, pointer, disabled } = props;
218
248
  const { setAtPath } = useJsonEditor();
219
249
  const options = enumOptions(schema);
220
250
  return (
221
- <span className="squisq-jf-segmented">
251
+ <span {...accessibilityProps(props)} className="squisq-jf-segmented" role="group">
222
252
  {options.map((opt, i) => {
223
253
  const active = value === opt.value;
224
254
  return (
@@ -228,6 +258,7 @@ export function SegmentedEditor({ value, schema, pointer, disabled }: EditorProp
228
258
  className={`squisq-jf-segmented__btn${active ? ' squisq-jf-segmented__btn--active' : ''}`}
229
259
  disabled={disabled}
230
260
  onClick={() => setAtPath(pointer, opt.value)}
261
+ aria-pressed={active}
231
262
  >
232
263
  {opt.label}
233
264
  </button>
@@ -237,12 +268,13 @@ export function SegmentedEditor({ value, schema, pointer, disabled }: EditorProp
237
268
  );
238
269
  }
239
270
 
240
- export function RadioEditor({ value, schema, pointer, disabled }: EditorProps) {
271
+ export function RadioEditor(props: EditorProps) {
272
+ const { value, schema, pointer, disabled } = props;
241
273
  const { setAtPath } = useJsonEditor();
242
274
  const options = enumOptions(schema);
243
275
  const name = useId();
244
276
  return (
245
- <div className="squisq-jf-radio">
277
+ <div {...accessibilityProps(props)} className="squisq-jf-radio" role="radiogroup">
246
278
  {options.map((opt, i) => {
247
279
  const active = value === opt.value;
248
280
  return (
@@ -262,11 +294,13 @@ export function RadioEditor({ value, schema, pointer, disabled }: EditorProps) {
262
294
  );
263
295
  }
264
296
 
265
- export function ComboboxEditor({ value, schema, pointer, disabled }: EditorProps) {
297
+ export function ComboboxEditor(props: EditorProps) {
298
+ const { value, schema, pointer, disabled } = props;
266
299
  const { setAtPath } = useJsonEditor();
267
300
  const options = enumOptions(schema);
268
301
  return (
269
302
  <select
303
+ {...accessibilityProps(props)}
270
304
  className="squisq-jf-select"
271
305
  value={asString(value)}
272
306
  disabled={disabled}
@@ -285,12 +319,14 @@ export function ComboboxEditor({ value, schema, pointer, disabled }: EditorProps
285
319
  );
286
320
  }
287
321
 
288
- export function ColorEditor({ value, pointer, disabled }: EditorProps) {
322
+ export function ColorEditor(props: EditorProps) {
323
+ const { value, pointer, disabled } = props;
289
324
  const { setAtPath } = useJsonEditor();
290
325
  const hex = typeof value === 'string' && /^#[0-9a-f]{6}$/i.test(value) ? value : '#000000';
291
326
  return (
292
327
  <span className="squisq-jf-color">
293
328
  <input
329
+ {...accessibilityProps(props)}
294
330
  type="color"
295
331
  className="squisq-jf-color__input"
296
332
  value={hex}
@@ -308,7 +344,8 @@ export function ColorEditor({ value, pointer, disabled }: EditorProps) {
308
344
  );
309
345
  }
310
346
 
311
- export function DateEditor({ value, schema, pointer, disabled }: EditorProps) {
347
+ export function DateEditor(props: EditorProps) {
348
+ const { value, schema, pointer, disabled } = props;
312
349
  const { setAtPath } = useJsonEditor();
313
350
  const fmt = schema.format;
314
351
  const inputType = fmt === 'time' ? 'time' : fmt === 'date' ? 'date' : 'datetime-local';
@@ -321,6 +358,7 @@ export function DateEditor({ value, schema, pointer, disabled }: EditorProps) {
321
358
  : asString(value);
322
359
  return (
323
360
  <input
361
+ {...accessibilityProps(props)}
324
362
  type={inputType}
325
363
  className="squisq-jf-input"
326
364
  value={display}
@@ -348,7 +386,7 @@ export function GroupEditor(props: EditorProps & { suppressTitle?: boolean }) {
348
386
  key={key}
349
387
  value={obj[key]}
350
388
  schema={propSchema}
351
- pointer={`${pointer}/${key}`}
389
+ pointer={appendPointer(pointer, key)}
352
390
  parentDisabled={disabled}
353
391
  />
354
392
  ))}
@@ -581,7 +619,9 @@ function defaultForSchema(schema: SquisqAnnotatedSchema): unknown {
581
619
  }
582
620
  }
583
621
 
584
- export function TabsEditor({ value, schema, pointer, disabled }: EditorProps) {
622
+ export function TabsEditor(props: EditorProps) {
623
+ const { value, schema, pointer, disabled } = props;
624
+ const tabsId = useId().replace(/:/g, '');
585
625
  const branches = (schema.oneOf ?? schema.anyOf ?? []) as readonly SquisqAnnotatedSchema[];
586
626
  const initial = pickMatchingBranch(branches, value);
587
627
  const [active, setActive] = useState(initial);
@@ -593,9 +633,14 @@ export function TabsEditor({ value, schema, pointer, disabled }: EditorProps) {
593
633
 
594
634
  const branch = branches[active];
595
635
  if (!branch) return null;
636
+ const activateTab = (index: number) => {
637
+ const next = (index + branches.length) % branches.length;
638
+ setActive(next);
639
+ requestAnimationFrame(() => document.getElementById(`${tabsId}-tab-${next}`)?.focus());
640
+ };
596
641
  return (
597
- <div>
598
- <div className="squisq-jf-tabs__strip">
642
+ <div {...accessibilityProps(props)}>
643
+ <div className="squisq-jf-tabs__strip" role="tablist">
599
644
  {branches.map((b, i) => (
600
645
  <button
601
646
  key={i}
@@ -603,12 +648,34 @@ export function TabsEditor({ value, schema, pointer, disabled }: EditorProps) {
603
648
  className={`squisq-jf-tabs__tab${i === active ? ' squisq-jf-tabs__tab--active' : ''}`}
604
649
  disabled={disabled}
605
650
  onClick={() => setActive(i)}
651
+ onKeyDown={(event) => {
652
+ if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
653
+ event.preventDefault();
654
+ activateTab(i + 1);
655
+ } else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
656
+ event.preventDefault();
657
+ activateTab(i - 1);
658
+ } else if (event.key === 'Home') {
659
+ event.preventDefault();
660
+ activateTab(0);
661
+ } else if (event.key === 'End') {
662
+ event.preventDefault();
663
+ activateTab(branches.length - 1);
664
+ }
665
+ }}
666
+ role="tab"
667
+ id={`${tabsId}-tab-${i}`}
668
+ aria-selected={i === active}
669
+ aria-controls={`${tabsId}-panel`}
670
+ tabIndex={i === active ? 0 : -1}
606
671
  >
607
672
  {b.squisq?.label ?? b.title ?? `Option ${i + 1}`}
608
673
  </button>
609
674
  ))}
610
675
  </div>
611
- <RenderNode value={value} schema={branch} pointer={pointer} parentDisabled={disabled} />
676
+ <div id={`${tabsId}-panel`} role="tabpanel" aria-labelledby={`${tabsId}-tab-${active}`}>
677
+ <RenderNode value={value} schema={branch} pointer={pointer} parentDisabled={disabled} />
678
+ </div>
612
679
  </div>
613
680
  );
614
681
  }
@@ -0,0 +1,12 @@
1
+ import type { MediaEntry } from '@bendyline/squisq/schemas';
2
+
3
+ /** Files managed by the container but intentionally omitted from the Files panel. */
4
+ export function isVisibleMediaEntry(entry: MediaEntry): boolean {
5
+ const basename = entry.name.slice(entry.name.lastIndexOf('/') + 1);
6
+ return basename.toLowerCase() !== '.gitignore';
7
+ }
8
+
9
+ /** Return the entries that should participate in the Files panel and its count. */
10
+ export function filterVisibleMediaEntries(entries: readonly MediaEntry[]): MediaEntry[] {
11
+ return entries.filter(isVisibleMediaEntry);
12
+ }
@@ -0,0 +1,299 @@
1
+ import { splitKeyValueToken, tokenizeAttrTokens } from '@bendyline/squisq/markdown';
2
+
3
+ interface MediaReferenceRange {
4
+ start: number;
5
+ end: number;
6
+ }
7
+
8
+ interface MarkdownReferenceMatch {
9
+ destination: string;
10
+ range: MediaReferenceRange;
11
+ }
12
+
13
+ const MEDIA_REFERENCE_PARAM_KEYS = new Set([
14
+ 'audio',
15
+ 'file',
16
+ 'href',
17
+ 'image',
18
+ 'imagesrc',
19
+ 'path',
20
+ 'poster',
21
+ 'src',
22
+ 'url',
23
+ 'video',
24
+ ]);
25
+
26
+ const HTML_MEDIA_ATTRS = ['src', 'href', 'poster'] as const;
27
+
28
+ /**
29
+ * Collect media references from the document source. Used by the Files panel
30
+ * to distinguish stored files that are actually referenced by the document
31
+ * from files that only exist in the bin.
32
+ */
33
+ export function collectMediaReferencesFromMarkdown(source: string): ReadonlySet<string> {
34
+ const refs = new Set<string>();
35
+ collectMarkdownInlineReferences(source, refs);
36
+ collectHtmlAttributeReferences(source, refs);
37
+ collectAnnotationReferences(source, refs);
38
+ return refs;
39
+ }
40
+
41
+ /**
42
+ * Remove markdown/html references to a media asset without reparsing and
43
+ * reserializing the whole document. This targets the ref shapes the editor
44
+ * emits for uploaded files while preserving unrelated author formatting.
45
+ */
46
+ export function removeMediaReferencesFromMarkdown(source: string, mediaPath: string): string {
47
+ if (!mediaPath) return source;
48
+ const withoutMarkdownRefs = removeMarkdownInlineReferences(source, mediaPath);
49
+ const withoutImageTags = removeHtmlTagsByAttribute(withoutMarkdownRefs, 'img', 'src', mediaPath);
50
+ return removeHtmlTagsByAttribute(withoutImageTags, 'a', 'href', mediaPath);
51
+ }
52
+
53
+ function removeMarkdownInlineReferences(source: string, mediaPath: string): string {
54
+ let result = '';
55
+ let cursor = 0;
56
+
57
+ while (cursor < source.length) {
58
+ const range = findNextMarkdownReference(source, cursor, mediaPath);
59
+ if (!range) break;
60
+ result += source.slice(cursor, range.start);
61
+ cursor = range.end;
62
+ }
63
+
64
+ return result + source.slice(cursor);
65
+ }
66
+
67
+ function findNextMarkdownReference(
68
+ source: string,
69
+ startIndex: number,
70
+ mediaPath: string,
71
+ ): MediaReferenceRange | null {
72
+ const match = findNextMarkdownReferenceMatch(source, startIndex, mediaPath);
73
+ return match?.range ?? null;
74
+ }
75
+
76
+ function collectMarkdownInlineReferences(source: string, refs: Set<string>): void {
77
+ let cursor = 0;
78
+ while (cursor < source.length) {
79
+ const match = findNextMarkdownReferenceMatch(source, cursor);
80
+ if (!match) return;
81
+ refs.add(match.destination);
82
+ cursor = Math.max(match.range.end, cursor + 1);
83
+ }
84
+ }
85
+
86
+ function findNextMarkdownReferenceMatch(
87
+ source: string,
88
+ startIndex: number,
89
+ mediaPath?: string,
90
+ ): MarkdownReferenceMatch | null {
91
+ let bracketIndex = source.indexOf('[', startIndex);
92
+
93
+ while (bracketIndex !== -1) {
94
+ const previous = source[bracketIndex - 1];
95
+ const isImage = previous === '!';
96
+ if (previous === '@') {
97
+ bracketIndex = source.indexOf('[', bracketIndex + 1);
98
+ continue;
99
+ }
100
+
101
+ const closeBracket = findClosingBracket(source, bracketIndex);
102
+ if (closeBracket === -1 || source[closeBracket + 1] !== '(') {
103
+ bracketIndex = source.indexOf('[', bracketIndex + 1);
104
+ continue;
105
+ }
106
+
107
+ const openParen = closeBracket + 1;
108
+ const closeParen = findClosingParen(source, openParen);
109
+ if (closeParen === -1) {
110
+ bracketIndex = source.indexOf('[', bracketIndex + 1);
111
+ continue;
112
+ }
113
+
114
+ const destination = readLinkDestination(source.slice(openParen + 1, closeParen));
115
+ if (destination && (mediaPath === undefined || destination === mediaPath)) {
116
+ const tokenStart = isImage ? bracketIndex - 1 : bracketIndex;
117
+ return {
118
+ destination,
119
+ range: expandStandaloneLine(source, tokenStart, closeParen + 1),
120
+ };
121
+ }
122
+
123
+ bracketIndex = source.indexOf('[', bracketIndex + 1);
124
+ }
125
+
126
+ return null;
127
+ }
128
+
129
+ function collectHtmlAttributeReferences(source: string, refs: Set<string>): void {
130
+ const tagPattern = /<[a-z][^>]*>/gi;
131
+ let match: RegExpExecArray | null;
132
+
133
+ while ((match = tagPattern.exec(source))) {
134
+ const tag = match[0];
135
+ for (const attrName of HTML_MEDIA_ATTRS) {
136
+ const value = readHtmlAttribute(tag, attrName);
137
+ if (value) refs.add(value);
138
+ }
139
+ }
140
+ }
141
+
142
+ function collectAnnotationReferences(source: string, refs: Set<string>): void {
143
+ const annotationPattern = /\{\[([^\]]*)\]\}/g;
144
+ let match: RegExpExecArray | null;
145
+
146
+ while ((match = annotationPattern.exec(source))) {
147
+ const inner = match[1];
148
+ if (!inner) continue;
149
+ const tokens = tokenizeAttrTokens(inner);
150
+ for (const token of tokens) {
151
+ const pair = splitKeyValueToken(token);
152
+ if (!pair) continue;
153
+ if (MEDIA_REFERENCE_PARAM_KEYS.has(pair.key.toLowerCase())) {
154
+ refs.add(pair.value);
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ function findClosingBracket(source: string, openIndex: number): number {
161
+ let depth = 0;
162
+ for (let i = openIndex; i < source.length; i++) {
163
+ if (source[i] === '\\') {
164
+ i += 1;
165
+ continue;
166
+ }
167
+ if (source[i] === '[') depth += 1;
168
+ if (source[i] === ']') {
169
+ depth -= 1;
170
+ if (depth === 0) return i;
171
+ }
172
+ }
173
+ return -1;
174
+ }
175
+
176
+ function findClosingParen(source: string, openIndex: number): number {
177
+ let depth = 0;
178
+ let inAngleDestination = false;
179
+
180
+ for (let i = openIndex; i < source.length; i++) {
181
+ const ch = source[i];
182
+ if (ch === '\\') {
183
+ i += 1;
184
+ continue;
185
+ }
186
+ if (inAngleDestination) {
187
+ if (ch === '>') inAngleDestination = false;
188
+ continue;
189
+ }
190
+ if (ch === '<') {
191
+ inAngleDestination = true;
192
+ continue;
193
+ }
194
+ if (ch === '(') {
195
+ depth += 1;
196
+ continue;
197
+ }
198
+ if (ch === ')') {
199
+ depth -= 1;
200
+ if (depth === 0) return i;
201
+ }
202
+ }
203
+
204
+ return -1;
205
+ }
206
+
207
+ function readLinkDestination(raw: string): string {
208
+ const content = raw.trimStart();
209
+ if (!content) return '';
210
+
211
+ if (content.startsWith('<')) {
212
+ const close = findUnescaped(content, '>', 1);
213
+ if (close === -1) return '';
214
+ return unescapeMarkdownUrl(content.slice(1, close));
215
+ }
216
+
217
+ let depth = 0;
218
+ for (let i = 0; i < content.length; i++) {
219
+ const ch = content[i];
220
+ if (ch === '\\') {
221
+ i += 1;
222
+ continue;
223
+ }
224
+ if (ch === '(') {
225
+ depth += 1;
226
+ continue;
227
+ }
228
+ if (ch === ')' && depth > 0) {
229
+ depth -= 1;
230
+ continue;
231
+ }
232
+ if (depth === 0 && /\s/.test(ch)) {
233
+ return unescapeMarkdownUrl(content.slice(0, i));
234
+ }
235
+ }
236
+
237
+ return unescapeMarkdownUrl(content);
238
+ }
239
+
240
+ function findUnescaped(source: string, needle: string, startIndex: number): number {
241
+ for (let i = startIndex; i < source.length; i++) {
242
+ if (source[i] === '\\') {
243
+ i += 1;
244
+ continue;
245
+ }
246
+ if (source[i] === needle) return i;
247
+ }
248
+ return -1;
249
+ }
250
+
251
+ function unescapeMarkdownUrl(value: string): string {
252
+ return value.replace(/\\([\\`*{}[\]()#+\-.!_>])/g, '$1');
253
+ }
254
+
255
+ function expandStandaloneLine(source: string, start: number, end: number): MediaReferenceRange {
256
+ const lineStart = source.lastIndexOf('\n', start - 1) + 1;
257
+ const nextNewline = source.indexOf('\n', end);
258
+ const lineEnd = nextNewline === -1 ? source.length : nextNewline;
259
+ const before = source.slice(lineStart, start);
260
+ const after = source.slice(end, lineEnd);
261
+
262
+ if (before.trim() === '' && after.trim() === '') {
263
+ return {
264
+ start: lineStart,
265
+ end: nextNewline === -1 ? lineEnd : lineEnd + 1,
266
+ };
267
+ }
268
+
269
+ return { start, end };
270
+ }
271
+
272
+ function removeHtmlTagsByAttribute(
273
+ source: string,
274
+ tagName: 'a' | 'img',
275
+ attrName: 'href' | 'src',
276
+ mediaPath: string,
277
+ ): string {
278
+ const tagPattern = tagName === 'img' ? /<img\b[^>]*>/gi : /<a\b[^>]*>[\s\S]*?<\/a>/gi;
279
+ return source.replace(tagPattern, (tag) => {
280
+ const value = readHtmlAttribute(tag, attrName);
281
+ return value === mediaPath ? '' : tag;
282
+ });
283
+ }
284
+
285
+ function readHtmlAttribute(tag: string, attrName: string): string | null {
286
+ const attrPattern = new RegExp(`\\b${attrName}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i');
287
+ const match = attrPattern.exec(tag);
288
+ if (!match) return null;
289
+ return unescapeHtmlAttribute(match[2] ?? match[3] ?? match[4] ?? '');
290
+ }
291
+
292
+ function unescapeHtmlAttribute(value: string): string {
293
+ return value
294
+ .replace(/&lt;/g, '<')
295
+ .replace(/&gt;/g, '>')
296
+ .replace(/&quot;/g, '"')
297
+ .replace(/&#39;|&apos;/g, "'")
298
+ .replace(/&amp;/g, '&');
299
+ }
package/src/monaco.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Canonical Monaco entry for squisq — and for apps downstream of it.
3
+ *
4
+ * Anything that needs Monaco (squisq's own RawEditor via {@link useMonacoLoader},
5
+ * or a downstream app that wants its own editor) should get it from *here*
6
+ * rather than importing `monaco-editor` directly. That guarantees a single
7
+ * shared instance — one language / theme / completion-provider registry — and
8
+ * gives squisq one place to decide which slice of Monaco ships. Downstream
9
+ * hosts therefore need no bundler aliasing or hand-rolled "slim" Monaco entry
10
+ * (getting that recipe wrong silently breaks features like the suggest widget).
11
+ *
12
+ * This is the FULL editor. `editor.main.js` pulls in:
13
+ * - every editor-feature contribution — the suggest-widget controller
14
+ * (completion popups), hover, folding, find, parameter hints, bracket
15
+ * matching, …
16
+ * - TM grammars for ~70 languages (syntax highlighting in fenced code), and
17
+ * - the rich css / html / json / typescript language *services*.
18
+ *
19
+ * `editor.main.js` has no sibling `.d.ts` (only `editor.api.d.ts` is
20
+ * published), so we take the types from `editor.api`: the side-effect import
21
+ * registers all the contributions, and the re-export exposes the — now
22
+ * augmented — API surface. Both reference the same underlying singleton.
23
+ *
24
+ * NOTE: the language *services* (css/html/json/typescript) need Monaco web
25
+ * workers wired via `globalThis.MonacoEnvironment`. Without that host-side
26
+ * setup their in-editor IntelliSense stays dormant — but highlighting,
27
+ * editing, and custom completion providers (e.g. the `{[template]}` typeahead)
28
+ * all run on the main thread and work with no worker configuration.
29
+ */
30
+
31
+ // Side-effect: register the full editor + all language contributions.
32
+ import 'monaco-editor/esm/vs/editor/editor.main.js';
33
+
34
+ // Typed API surface (the only barrel Monaco publishes declarations for).
35
+ export * from 'monaco-editor/esm/vs/editor/editor.api';