@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.
- package/README.md +12 -3
- package/dist/index.d.ts +1304 -208
- package/dist/index.js +22838 -14494
- package/dist/index.js.map +1 -1
- package/dist/monaco.d.ts +1 -0
- package/dist/monaco.js +4 -0
- package/dist/monaco.js.map +1 -0
- package/dist/styles/index.css +2384 -408
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +48 -17
- package/src/DocumentSettingsDialog.tsx +13 -21
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +403 -270
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +276 -40
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +466 -93
- package/src/PreviewPanel.tsx +313 -77
- package/src/RawEditor.tsx +13 -2
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +34 -10
- package/src/TemplateContentPreview.tsx +56 -0
- package/src/TemplatePicker.tsx +355 -134
- package/src/ThemeCustomizerPanel.tsx +30 -15
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +719 -238
- package/src/TransitionPicker.tsx +44 -5
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/ViewSwitcher.tsx +4 -4
- package/src/WysiwygEditor.tsx +167 -92
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
- package/src/__tests__/blockTagActivity.test.ts +183 -0
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
- package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +22 -3
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +29 -1
- package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
- package/src/__tests__/editorScrollSync.test.ts +65 -0
- package/src/__tests__/editorShellProps.test.tsx +390 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +53 -15
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +38 -1
- package/src/__tests__/jsonEditor.test.tsx +56 -1
- package/src/__tests__/layersPanel.test.tsx +75 -5
- package/src/__tests__/majorApiTypes.test.ts +26 -0
- package/src/__tests__/mediaBinDrop.test.tsx +90 -0
- package/src/__tests__/mediaReferences.test.ts +82 -0
- package/src/__tests__/narrationSave.test.ts +148 -0
- package/src/__tests__/outlinePanel.test.tsx +93 -3
- package/src/__tests__/outlineSource.test.ts +217 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
- package/src/__tests__/presentationMode.test.tsx +290 -0
- package/src/__tests__/previewControls.test.tsx +312 -6
- package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
- package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
- package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
- package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/statusBar.test.tsx +27 -0
- package/src/__tests__/teleprompter.test.tsx +190 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +49 -2
- package/src/__tests__/templateContentPreview.test.ts +101 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +70 -0
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/transformStyleId.test.ts +13 -0
- package/src/__tests__/useImageEditor.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +75 -0
- package/src/__tests__/viewMenuPanel.test.tsx +130 -0
- package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
- package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +224 -0
- package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +227 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +148 -0
- package/src/asciiDiagram/asciiDiagramData.ts +116 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +219 -0
- package/src/asciiDiagram/asciiPaste.ts +19 -0
- package/src/blockTagActivity.ts +233 -0
- package/src/buildDocumentPreviewMarkdown.ts +168 -0
- package/src/buildPreviewDoc.ts +19 -12
- package/src/customTemplates/CustomLayoutManager.tsx +3 -2
- package/src/customTemplates/CustomTemplateContext.tsx +6 -0
- package/src/customTemplates/TemplateDesigner.tsx +7 -4
- package/src/customTemplates/__tests__/library.test.ts +8 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customThemes/CustomThemeContext.tsx +6 -0
- package/src/customThemes/CustomThemeDialog.tsx +35 -5
- package/src/customThemes/ImportThemeSection.tsx +178 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
- package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
- package/src/customThemes/customThemeLibrary.ts +7 -4
- package/src/customThemes/index.ts +7 -1
- package/src/customThemes/themeDraft.ts +23 -7
- package/src/diagram/DiagramCanvas.tsx +9 -4
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/headingTransition.ts +85 -25
- package/src/imageEditor/CanvasSurface.tsx +29 -23
- package/src/imageEditor/LayersPanel.tsx +78 -3
- package/src/imageEditor/Toolbar.tsx +5 -1
- package/src/imageEditor/icons.tsx +4 -0
- package/src/imageEditor/image-editor.css +75 -0
- package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
- package/src/imageEditor/useImageEditor.ts +71 -14
- package/src/index.ts +193 -20
- package/src/jsonEditor/JsonEditorContext.tsx +12 -6
- package/src/jsonEditor/RenderNode.tsx +24 -3
- package/src/jsonEditor/editors.tsx +86 -19
- package/src/mediaEntries.ts +12 -0
- package/src/mediaReferences.ts +299 -0
- package/src/monaco.ts +35 -0
- package/src/monacoWorkers.ts +89 -0
- package/src/outlineSource.ts +171 -0
- package/src/presentation/PresentationMode.tsx +596 -0
- package/src/rawEditorIsolation.ts +18 -0
- package/src/recorder/hooks/useMediaRecorder.ts +97 -53
- package/src/resolveBlockVisual.ts +10 -4
- package/src/scene/Scene.tsx +101 -25
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +11 -4
- package/src/scene/SceneSelection.tsx +19 -15
- package/src/scene/SceneSideToolbar.tsx +89 -0
- package/src/scene/SceneViewport.tsx +7 -3
- package/src/scene/ShapePalette.tsx +17 -2
- package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
- package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
- package/src/scene/__tests__/sceneIsolation.test.tsx +93 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/commands/SceneCommand.ts +4 -1
- package/src/scene/index.ts +1 -6
- package/src/scene/layers/DiagramEdges.tsx +24 -9
- package/src/scene/layers/edgeGeometry.ts +31 -4
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +145 -2
- package/src/scene/text/SceneTextOverlay.tsx +5 -5
- package/src/scene/text/sceneTextChannel.ts +26 -20
- package/src/scene/text/sceneTextConfig.ts +4 -0
- package/src/scene/tools/ConnectTool.ts +126 -28
- package/src/scene/tools/DrawingConnectTool.ts +86 -16
- package/src/scene/tools/SceneTool.ts +10 -0
- package/src/scene/tools/SelectTool.ts +34 -18
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +499 -0
- package/src/styles/editor.css +1566 -219
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +139 -0
- package/src/teleprompter/TeleprompterControls.tsx +218 -0
- package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
- package/src/teleprompter/TeleprompterSurface.tsx +267 -0
- package/src/teleprompter/TeleprompterView.tsx +338 -0
- package/src/teleprompter/canvasRenderer.ts +161 -0
- package/src/teleprompter/floatingWindow.ts +352 -0
- package/src/teleprompter/index.ts +61 -0
- package/src/teleprompter/pcmWorklet.ts +62 -0
- package/src/teleprompter/recording/insertPreamble.ts +80 -0
- package/src/teleprompter/recording/narrationSave.ts +134 -0
- package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
- package/src/teleprompter/scrollModel.ts +85 -0
- package/src/teleprompter/teleprompterTheme.ts +303 -0
- package/src/teleprompter/types.ts +38 -0
- package/src/teleprompter/useFloatingWindow.ts +74 -0
- package/src/teleprompter/useMicAnalysis.ts +211 -0
- package/src/teleprompter/useTeleprompter.ts +421 -0
- package/src/templateContentPreviewResolver.ts +360 -0
- package/src/timeline/TimelineEditorWidget.tsx +702 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +355 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +327 -0
- package/src/timeline/__tests__/timelineOps.test.ts +222 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +367 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +277 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +84 -49
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +240 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +125 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +163 -0
- package/src/treeview/treeOps.ts +172 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +94 -0
- package/src/treeview/treeViewData.ts +41 -0
- package/src/useMonacoLoader.ts +11 -34
- package/src/wysiwygImageUpload.ts +113 -0
- package/src/diagram/DiagramExtension.ts +0 -209
- package/src/diagram/DiagramWidget.tsx +0 -270
- 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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
216
|
+
export function CheckboxEditor(props: EditorProps) {
|
|
217
|
+
const { value, schema, pointer, disabled } = props;
|
|
193
218
|
const { setAtPath } = useJsonEditor();
|
|
194
|
-
const
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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={
|
|
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(
|
|
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
|
-
<
|
|
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(/</g, '<')
|
|
295
|
+
.replace(/>/g, '>')
|
|
296
|
+
.replace(/"/g, '"')
|
|
297
|
+
.replace(/'|'/g, "'")
|
|
298
|
+
.replace(/&/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';
|