@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
@@ -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
+ }
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';
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Monaco language-service worker wiring.
3
+ *
4
+ * Monaco offloads its heavy language services — css / html / json / typescript
5
+ * IntelliSense — plus a base editor service (word-based completions, link
6
+ * detection, diffing) to web workers. Those worker bundles must be produced by
7
+ * the HOST application's bundler: the mechanisms for it (Vite's `?worker`
8
+ * import suffix, `new Worker(new URL(...))`, webpack loaders) are all
9
+ * bundler-specific and cannot live inside this tsup-built library.
10
+ *
11
+ * So the division of labor is: the host supplies the five worker constructors
12
+ * (one line each with Vite's `?worker`), and this helper owns the
13
+ * `label → worker` mapping — the part that's fiddly and easy to get wrong.
14
+ *
15
+ * Call once, before the first editor mounts (typically in the app entry):
16
+ *
17
+ * ```ts
18
+ * import { configureMonacoWorkers } from '@bendyline/squisq-editor-react';
19
+ * import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
20
+ * import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
21
+ * import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
22
+ * import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
23
+ * import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
24
+ *
25
+ * configureMonacoWorkers({
26
+ * editor: EditorWorker, json: JsonWorker, css: CssWorker,
27
+ * html: HtmlWorker, ts: TsWorker,
28
+ * });
29
+ * ```
30
+ *
31
+ * This is purely additive: without it, highlighting, editing, and custom
32
+ * completion providers (e.g. the `{[template]}` typeahead) still work — they
33
+ * run on the main thread. Only the language-service IntelliSense is dormant
34
+ * until the workers are wired.
35
+ */
36
+
37
+ /** Zero-arg worker constructor, as produced by Vite's `?worker` import. */
38
+ export type MonacoWorkerConstructor = new () => Worker;
39
+
40
+ export interface MonacoWorkerConstructors {
41
+ /** Base editor worker (word completions, links, diff). Required. */
42
+ editor: MonacoWorkerConstructor;
43
+ /** JSON language service. */
44
+ json?: MonacoWorkerConstructor;
45
+ /** CSS/SCSS/LESS language service. */
46
+ css?: MonacoWorkerConstructor;
47
+ /** HTML/Handlebars/Razor language service. */
48
+ html?: MonacoWorkerConstructor;
49
+ /** TypeScript service — also handles JavaScript. */
50
+ ts?: MonacoWorkerConstructor;
51
+ }
52
+
53
+ type MonacoEnvironmentHost = typeof globalThis & {
54
+ MonacoEnvironment?: { getWorker(workerId: string, label: string): Worker };
55
+ };
56
+
57
+ /**
58
+ * Install `globalThis.MonacoEnvironment.getWorker` so Monaco routes each
59
+ * language to the matching worker, falling back to the base editor worker for
60
+ * any label without a dedicated service (which is every plain language — its
61
+ * grammar-based highlighting needs no worker).
62
+ */
63
+ export function configureMonacoWorkers(workers: MonacoWorkerConstructors): void {
64
+ const host = globalThis as MonacoEnvironmentHost;
65
+ host.MonacoEnvironment = {
66
+ getWorker(_workerId: string, label: string): Worker {
67
+ switch (label) {
68
+ case 'json':
69
+ if (workers.json) return new workers.json();
70
+ break;
71
+ case 'css':
72
+ case 'scss':
73
+ case 'less':
74
+ if (workers.css) return new workers.css();
75
+ break;
76
+ case 'html':
77
+ case 'handlebars':
78
+ case 'razor':
79
+ if (workers.html) return new workers.html();
80
+ break;
81
+ case 'typescript':
82
+ case 'javascript':
83
+ if (workers.ts) return new workers.ts();
84
+ break;
85
+ }
86
+ return new workers.editor();
87
+ },
88
+ };
89
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Source-preserving section moves for the document outline.
3
+ *
4
+ * Markdown headings form a flat AST, so a section extends from its heading
5
+ * through the next heading of equal or shallower depth. Reordering only direct
6
+ * siblings keeps heading levels and hierarchy intact; promote/demote remains a
7
+ * separate outline operation.
8
+ */
9
+
10
+ import { parseMarkdown } from '@bendyline/squisq/markdown';
11
+
12
+ export type OutlineDropPlacement = 'before' | 'after';
13
+
14
+ interface SourceHeading {
15
+ line: number;
16
+ depth: number;
17
+ parentLine: number | null;
18
+ start: number;
19
+ end: number;
20
+ }
21
+
22
+ interface SectionSlot extends SourceHeading {
23
+ content: string;
24
+ gap: string;
25
+ }
26
+
27
+ /**
28
+ * Move one heading-rooted section before or after a sibling section.
29
+ *
30
+ * The current source is parsed inside the helper so fenced-code lookalikes and
31
+ * stale/non-heading line numbers are rejected. The moved section includes its
32
+ * body and every descendant heading. Returns `null` for an invalid or no-op
33
+ * move.
34
+ */
35
+ export function moveHeadingSectionInSource(
36
+ source: string,
37
+ fromHeadingLine: number,
38
+ targetHeadingLine: number,
39
+ placement: OutlineDropPlacement,
40
+ ): string | null {
41
+ if (fromHeadingLine === targetHeadingLine || (placement !== 'before' && placement !== 'after')) {
42
+ return null;
43
+ }
44
+
45
+ const headings = readSourceHeadings(source);
46
+ if (!headings) return null;
47
+
48
+ const from = headings.find((heading) => heading.line === fromHeadingLine);
49
+ const target = headings.find((heading) => heading.line === targetHeadingLine);
50
+ if (!from || !target) return null;
51
+
52
+ // A reorder must not silently promote, demote, or re-parent a section.
53
+ if (from.depth !== target.depth || from.parentLine !== target.parentLine) return null;
54
+
55
+ const siblings = headings.filter((heading) => heading.parentLine === from.parentLine);
56
+ const fromIndex = siblings.findIndex((heading) => heading.line === fromHeadingLine);
57
+ const targetIndex = siblings.findIndex((heading) => heading.line === targetHeadingLine);
58
+ if (fromIndex < 0 || targetIndex < 0) return null;
59
+
60
+ // Direct sibling ranges must be contiguous. Aborting here is safer than
61
+ // rewriting source if a parser ever reports an unexpected position shape.
62
+ for (let i = 0; i < siblings.length - 1; i++) {
63
+ if (siblings[i].end !== siblings[i + 1].start) return null;
64
+ }
65
+
66
+ const slots: SectionSlot[] = siblings.map((heading) => {
67
+ const raw = source.slice(heading.start, heading.end);
68
+ const { content, gap } = splitTrailingLineGap(raw);
69
+ return { ...heading, content, gap };
70
+ });
71
+
72
+ const reordered = [...slots];
73
+ const [moved] = reordered.splice(fromIndex, 1);
74
+ const targetAfterRemoval = reordered.findIndex((heading) => heading.line === targetHeadingLine);
75
+ if (!moved || targetAfterRemoval < 0) return null;
76
+
77
+ const insertionIndex = targetAfterRemoval + (placement === 'after' ? 1 : 0);
78
+ reordered.splice(insertionIndex, 0, moved);
79
+
80
+ if (reordered.every((heading, index) => heading.line === slots[index].line)) return null;
81
+
82
+ // Whitespace belongs to positions, not to the section being moved. Keeping
83
+ // the original slot gaps preserves blank-line style, CRLF, and the exact
84
+ // final-newline state even when the former EOF section moves earlier.
85
+ const replacement = reordered
86
+ .map((heading, index) => heading.content + slots[index].gap)
87
+ .join('');
88
+ const rangeStart = slots[0].start;
89
+ const rangeEnd = slots[slots.length - 1].end;
90
+ const next = source.slice(0, rangeStart) + replacement + source.slice(rangeEnd);
91
+
92
+ // The move should never change the number of authored headings. This also
93
+ // catches any unforeseen boundary issue before the editor source is updated.
94
+ const nextHeadings = readSourceHeadings(next);
95
+ if (!nextHeadings || nextHeadings.length !== headings.length) return null;
96
+
97
+ return next;
98
+ }
99
+
100
+ /** Parse source headings and derive their parent + complete-section range. */
101
+ function readSourceHeadings(source: string): SourceHeading[] | null {
102
+ try {
103
+ const document = parseMarkdown(source);
104
+ const lineStarts = sourceLineStarts(source);
105
+ const headings: SourceHeading[] = [];
106
+ const stack: SourceHeading[] = [];
107
+
108
+ for (const node of document.children) {
109
+ if (node.type !== 'heading' || !node.position) continue;
110
+
111
+ const line = node.position.start.line;
112
+ const start = lineStarts[line - 1];
113
+ if (start == null) return null;
114
+
115
+ while (stack.length > 0 && stack[stack.length - 1].depth >= node.depth) {
116
+ stack.pop();
117
+ }
118
+
119
+ const heading: SourceHeading = {
120
+ line,
121
+ depth: node.depth,
122
+ parentLine: stack[stack.length - 1]?.line ?? null,
123
+ start,
124
+ end: source.length,
125
+ };
126
+ headings.push(heading);
127
+ stack.push(heading);
128
+ }
129
+
130
+ for (let i = 0; i < headings.length; i++) {
131
+ const heading = headings[i];
132
+ for (let j = i + 1; j < headings.length; j++) {
133
+ if (headings[j].depth <= heading.depth) {
134
+ heading.end = headings[j].start;
135
+ break;
136
+ }
137
+ }
138
+ }
139
+
140
+ return headings;
141
+ } catch (err: unknown) {
142
+ // Source mutations are best-effort UI operations. A parse failure leaves
143
+ // the current markdown untouched; EditorContext owns user-facing errors.
144
+ void err;
145
+ return null;
146
+ }
147
+ }
148
+
149
+ /** 0-based offsets of every physical line, preserving mixed EOL sequences. */
150
+ function sourceLineStarts(source: string): number[] {
151
+ const starts = [0];
152
+ const eol = /\r\n|\r|\n/g;
153
+ let match: RegExpExecArray | null;
154
+ while ((match = eol.exec(source)) != null) {
155
+ starts.push(match.index + match[0].length);
156
+ }
157
+ return starts;
158
+ }
159
+
160
+ /**
161
+ * Split the line-ending separator from a section without stealing spaces
162
+ * before the first newline (which may be an authored Markdown hard break).
163
+ */
164
+ function splitTrailingLineGap(value: string): { content: string; gap: string } {
165
+ const match = value.match(/(?:\r\n|\r|\n)(?:[ \t]*(?:\r\n|\r|\n))*[ \t]*$/);
166
+ if (!match || match.index == null) return { content: value, gap: '' };
167
+ return {
168
+ content: value.slice(0, match.index),
169
+ gap: value.slice(match.index),
170
+ };
171
+ }