@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
|
@@ -193,6 +193,73 @@
|
|
|
193
193
|
.squisq-image-editor-layers {
|
|
194
194
|
border-bottom: 1px solid var(--squisq-image-editor-panel-border);
|
|
195
195
|
}
|
|
196
|
+
.squisq-image-editor-layers-header {
|
|
197
|
+
display: flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
justify-content: space-between;
|
|
200
|
+
min-height: 32px;
|
|
201
|
+
}
|
|
202
|
+
.squisq-image-editor-layer-add {
|
|
203
|
+
position: relative;
|
|
204
|
+
display: inline-flex;
|
|
205
|
+
}
|
|
206
|
+
.squisq-image-editor-layer-add-button {
|
|
207
|
+
display: inline-flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
justify-content: center;
|
|
210
|
+
width: 22px;
|
|
211
|
+
height: 22px;
|
|
212
|
+
padding: 0;
|
|
213
|
+
color: inherit;
|
|
214
|
+
background: transparent;
|
|
215
|
+
border: 1px solid transparent;
|
|
216
|
+
border-radius: 3px;
|
|
217
|
+
cursor: pointer;
|
|
218
|
+
font: inherit;
|
|
219
|
+
}
|
|
220
|
+
.squisq-image-editor-layer-add-button:hover,
|
|
221
|
+
.squisq-image-editor-layer-add-button[aria-expanded='true'] {
|
|
222
|
+
background: var(--squisq-image-editor-control-bg);
|
|
223
|
+
border-color: var(--squisq-image-editor-control-border);
|
|
224
|
+
}
|
|
225
|
+
.squisq-image-editor-layer-add-menu {
|
|
226
|
+
position: absolute;
|
|
227
|
+
top: calc(100% + 4px);
|
|
228
|
+
right: 0;
|
|
229
|
+
z-index: 20;
|
|
230
|
+
min-width: 150px;
|
|
231
|
+
padding: 4px;
|
|
232
|
+
background: var(--squisq-image-editor-panel-bg);
|
|
233
|
+
border: 1px solid var(--squisq-image-editor-panel-border);
|
|
234
|
+
border-radius: 5px;
|
|
235
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
|
236
|
+
text-transform: none;
|
|
237
|
+
letter-spacing: normal;
|
|
238
|
+
}
|
|
239
|
+
.squisq-image-editor-layer-add-menu-item {
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
gap: 8px;
|
|
243
|
+
width: 100%;
|
|
244
|
+
padding: 6px 8px;
|
|
245
|
+
color: inherit;
|
|
246
|
+
background: transparent;
|
|
247
|
+
border: 0;
|
|
248
|
+
border-radius: 3px;
|
|
249
|
+
cursor: pointer;
|
|
250
|
+
font: inherit;
|
|
251
|
+
font-size: 12px;
|
|
252
|
+
text-align: left;
|
|
253
|
+
}
|
|
254
|
+
.squisq-image-editor-layer-add-menu-item:hover,
|
|
255
|
+
.squisq-image-editor-layer-add-menu-item:focus-visible {
|
|
256
|
+
background: var(--squisq-image-editor-control-bg);
|
|
257
|
+
outline: none;
|
|
258
|
+
}
|
|
259
|
+
.squisq-image-editor-layer-add-menu-item svg {
|
|
260
|
+
flex: 0 0 auto;
|
|
261
|
+
width: 14px;
|
|
262
|
+
}
|
|
196
263
|
.squisq-image-editor-layer-list {
|
|
197
264
|
list-style: none;
|
|
198
265
|
margin: 0;
|
|
@@ -239,6 +306,7 @@
|
|
|
239
306
|
cursor: not-allowed;
|
|
240
307
|
}
|
|
241
308
|
.squisq-image-editor-layer-name {
|
|
309
|
+
min-width: 0;
|
|
242
310
|
flex: 1 1 auto;
|
|
243
311
|
text-align: left;
|
|
244
312
|
background: transparent;
|
|
@@ -254,7 +322,14 @@
|
|
|
254
322
|
text-overflow: ellipsis;
|
|
255
323
|
white-space: nowrap;
|
|
256
324
|
}
|
|
325
|
+
.squisq-image-editor-layer-name-text {
|
|
326
|
+
min-width: 0;
|
|
327
|
+
overflow: hidden;
|
|
328
|
+
text-overflow: ellipsis;
|
|
329
|
+
white-space: nowrap;
|
|
330
|
+
}
|
|
257
331
|
.squisq-image-editor-layer-kind {
|
|
332
|
+
flex: 0 0 auto;
|
|
258
333
|
margin-left: auto;
|
|
259
334
|
font-size: 10px;
|
|
260
335
|
color: var(--squisq-image-editor-text-muted);
|
|
@@ -35,7 +35,7 @@ export function SelectionHandles({ box, onHandlePointerDown }: Props) {
|
|
|
35
35
|
];
|
|
36
36
|
|
|
37
37
|
return (
|
|
38
|
-
<g pointerEvents="none">
|
|
38
|
+
<g className="squisq-image-editor-selection-handles" pointerEvents="none">
|
|
39
39
|
{/*
|
|
40
40
|
* Outline: rendered as two stacked rectangles so the selection
|
|
41
41
|
* stays visible over both light and dark imagery. The white
|
|
@@ -110,6 +110,7 @@ export function useImageEditor(options: UseImageEditorOptions): UseImageEditorRe
|
|
|
110
110
|
const seeded = await seedFromSource(container, initialSrc);
|
|
111
111
|
if (cancelled) return;
|
|
112
112
|
await writeImageEditDoc(container, seeded, stateFilename);
|
|
113
|
+
if (cancelled) return;
|
|
113
114
|
dispatch({ type: 'load', doc: seeded });
|
|
114
115
|
setReady(true);
|
|
115
116
|
// Capture an initial snapshot of the freshly-seeded state so the
|
|
@@ -131,27 +132,53 @@ export function useImageEditor(options: UseImageEditorOptions): UseImageEditorRe
|
|
|
131
132
|
// ── Debounced persistence of state.json ────────────────────────────────
|
|
132
133
|
const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
133
134
|
const docRef = useRef<ImageEditDoc | null>(null);
|
|
134
|
-
|
|
135
|
+
const dirtyRef = useRef(false);
|
|
136
|
+
const revisionRef = useRef(0);
|
|
137
|
+
const previousDocRef = useRef<ImageEditDoc | null>(null);
|
|
138
|
+
const nextDoc = state?.doc ?? null;
|
|
139
|
+
if (nextDoc !== previousDocRef.current) {
|
|
140
|
+
previousDocRef.current = nextDoc;
|
|
141
|
+
revisionRef.current += 1;
|
|
142
|
+
}
|
|
143
|
+
docRef.current = nextDoc;
|
|
144
|
+
dirtyRef.current = state?.dirty ?? false;
|
|
145
|
+
const writeQueueRef = useRef<Promise<void>>(Promise.resolve());
|
|
146
|
+
const persistTargetRef = useRef({ container, stateFilename });
|
|
147
|
+
persistTargetRef.current = { container, stateFilename };
|
|
148
|
+
|
|
149
|
+
const enqueueWrite = useCallback(
|
|
150
|
+
(doc: ImageEditDoc, revision: number, markClean: boolean): Promise<void> => {
|
|
151
|
+
const write = writeQueueRef.current
|
|
152
|
+
.catch(() => undefined)
|
|
153
|
+
.then(() => writeImageEditDoc(container, doc, stateFilename));
|
|
154
|
+
writeQueueRef.current = write.catch(() => undefined);
|
|
155
|
+
return write.then(() => {
|
|
156
|
+
if (markClean && revision === revisionRef.current && docRef.current === doc) {
|
|
157
|
+
dispatch({ type: 'mark-clean' });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
[container, stateFilename],
|
|
162
|
+
);
|
|
135
163
|
|
|
136
164
|
useEffect(() => {
|
|
137
165
|
if (!state?.dirty) return;
|
|
138
166
|
if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
|
|
139
167
|
persistTimerRef.current = setTimeout(() => {
|
|
140
|
-
const doc =
|
|
168
|
+
const doc = state.doc;
|
|
141
169
|
if (!doc) return;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
});
|
|
170
|
+
const revision = revisionRef.current;
|
|
171
|
+
enqueueWrite(doc, revision, true).catch((err: unknown) => {
|
|
172
|
+
console.warn(
|
|
173
|
+
'[squisq-editor] image-edit state persist failed:',
|
|
174
|
+
err instanceof Error ? err.message : err,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
150
177
|
}, persistDebounceMs);
|
|
151
178
|
return () => {
|
|
152
179
|
if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
|
|
153
180
|
};
|
|
154
|
-
}, [state?.dirty, state?.doc,
|
|
181
|
+
}, [state?.dirty, state?.doc, persistDebounceMs, enqueueWrite]);
|
|
155
182
|
|
|
156
183
|
const flush = useCallback(async () => {
|
|
157
184
|
const doc = docRef.current;
|
|
@@ -160,9 +187,29 @@ export function useImageEditor(options: UseImageEditorOptions): UseImageEditorRe
|
|
|
160
187
|
clearTimeout(persistTimerRef.current);
|
|
161
188
|
persistTimerRef.current = null;
|
|
162
189
|
}
|
|
163
|
-
await
|
|
164
|
-
|
|
165
|
-
|
|
190
|
+
await enqueueWrite(doc, revisionRef.current, true);
|
|
191
|
+
}, [enqueueWrite]);
|
|
192
|
+
|
|
193
|
+
// A quick modal close must not discard the final edit that is still inside
|
|
194
|
+
// the debounce window. Queue it behind any in-flight write on unmount.
|
|
195
|
+
useEffect(
|
|
196
|
+
() => () => {
|
|
197
|
+
if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
|
|
198
|
+
const doc = docRef.current;
|
|
199
|
+
if (!dirtyRef.current || !doc) return;
|
|
200
|
+
const target = persistTargetRef.current;
|
|
201
|
+
const write = writeQueueRef.current
|
|
202
|
+
.catch(() => undefined)
|
|
203
|
+
.then(() => writeImageEditDoc(target.container, doc, target.stateFilename));
|
|
204
|
+
writeQueueRef.current = write.catch((err: unknown) => {
|
|
205
|
+
console.warn(
|
|
206
|
+
'[squisq-editor] image-edit final persist failed:',
|
|
207
|
+
err instanceof Error ? err.message : err,
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
[],
|
|
212
|
+
);
|
|
166
213
|
|
|
167
214
|
// ── Versioning ─────────────────────────────────────────────────────────
|
|
168
215
|
const versioning = useMemo(
|
|
@@ -204,17 +251,26 @@ export function useImageEditor(options: UseImageEditorOptions): UseImageEditorRe
|
|
|
204
251
|
|
|
205
252
|
// ── Asset URL cache ────────────────────────────────────────────────────
|
|
206
253
|
const urlCacheRef = useRef<Map<string, string>>(new Map());
|
|
254
|
+
const urlCacheGenerationRef = useRef(0);
|
|
207
255
|
|
|
208
256
|
const resolveAssetUrl = useCallback(
|
|
209
257
|
async (path: string): Promise<string> => {
|
|
210
258
|
const cache = urlCacheRef.current;
|
|
259
|
+
const generation = urlCacheGenerationRef.current;
|
|
211
260
|
const cached = cache.get(path);
|
|
212
261
|
if (cached) return cached;
|
|
213
262
|
const data = await container.readFile(path);
|
|
263
|
+
if (generation !== urlCacheGenerationRef.current) {
|
|
264
|
+
throw new Error('useImageEditor: asset resolution cancelled');
|
|
265
|
+
}
|
|
214
266
|
if (!data) throw new Error(`useImageEditor: missing asset "${path}"`);
|
|
215
267
|
const list = await container.listFiles(path);
|
|
216
268
|
const mime = list.find((e) => e.path === path)?.mimeType ?? 'application/octet-stream';
|
|
217
269
|
const url = URL.createObjectURL(new Blob([data], { type: mime }));
|
|
270
|
+
if (generation !== urlCacheGenerationRef.current) {
|
|
271
|
+
URL.revokeObjectURL(url);
|
|
272
|
+
throw new Error('useImageEditor: asset resolution cancelled');
|
|
273
|
+
}
|
|
218
274
|
cache.set(path, url);
|
|
219
275
|
return url;
|
|
220
276
|
},
|
|
@@ -225,6 +281,7 @@ export function useImageEditor(options: UseImageEditorOptions): UseImageEditorRe
|
|
|
225
281
|
useEffect(() => {
|
|
226
282
|
const cache = urlCacheRef.current;
|
|
227
283
|
return () => {
|
|
284
|
+
urlCacheGenerationRef.current += 1;
|
|
228
285
|
for (const url of cache.values()) URL.revokeObjectURL(url);
|
|
229
286
|
cache.clear();
|
|
230
287
|
};
|
package/src/index.ts
CHANGED
|
@@ -45,6 +45,7 @@ export type {
|
|
|
45
45
|
DocumentLinkProvider,
|
|
46
46
|
ViewPreferences,
|
|
47
47
|
ThemeInheritance,
|
|
48
|
+
BlockTagVisibility,
|
|
48
49
|
LayoutMode,
|
|
49
50
|
} from './EditorContext.js';
|
|
50
51
|
|
|
@@ -99,11 +100,12 @@ export { DocumentSettingsDialog } from './DocumentSettingsDialog.js';
|
|
|
99
100
|
export type { DocumentSettingsDialogProps } from './DocumentSettingsDialog.js';
|
|
100
101
|
export { ThemePicker } from './ThemePicker.js';
|
|
101
102
|
export type { ThemePickerProps } from './ThemePicker.js';
|
|
102
|
-
export {
|
|
103
|
-
export type {
|
|
103
|
+
export { PICKER_CATEGORIES, ALL_PICKER_ENTRIES, searchPickerEntries } from './emojiData.js';
|
|
104
|
+
export type { PickerEntry, PickerCategory } from './emojiData.js';
|
|
104
105
|
export {
|
|
105
106
|
PreviewSettingsProvider,
|
|
106
107
|
PreviewToolbarControls,
|
|
108
|
+
PreviewModeMenu,
|
|
107
109
|
PreviewModeSwitch,
|
|
108
110
|
PreviewFormatSwitch,
|
|
109
111
|
usePreviewSettings,
|
|
@@ -141,10 +143,10 @@ export {
|
|
|
141
143
|
readHeadingLineTransition,
|
|
142
144
|
setHeadingLineTransition,
|
|
143
145
|
readBlockAttrsTransition,
|
|
144
|
-
|
|
146
|
+
setHeadingAttrsTransition,
|
|
145
147
|
EMPTY_TRANSITION,
|
|
146
148
|
} from './headingTransition.js';
|
|
147
|
-
export type { TransitionFields } from './headingTransition.js';
|
|
149
|
+
export type { HeadingTransitionAttrs, TransitionFields } from './headingTransition.js';
|
|
148
150
|
export {
|
|
149
151
|
readBlockAttrsParams,
|
|
150
152
|
readBlockAttrsValue,
|
|
@@ -189,12 +191,26 @@ export {
|
|
|
189
191
|
export { useMonacoLoader } from './useMonacoLoader.js';
|
|
190
192
|
export type { UseMonacoLoaderResult } from './useMonacoLoader.js';
|
|
191
193
|
|
|
194
|
+
// Monaco language-service worker wiring — the host supplies the `?worker`
|
|
195
|
+
// constructors, this owns the label→worker mapping. See monacoWorkers.ts.
|
|
196
|
+
export { configureMonacoWorkers } from './monacoWorkers.js';
|
|
197
|
+
export type { MonacoWorkerConstructor, MonacoWorkerConstructors } from './monacoWorkers.js';
|
|
198
|
+
|
|
192
199
|
// Custom themes — provider stack (doc frontmatter + browser-local library)
|
|
193
|
-
export {
|
|
200
|
+
export {
|
|
201
|
+
CustomThemeProvider,
|
|
202
|
+
useCustomThemes,
|
|
203
|
+
useDocCustomThemes,
|
|
204
|
+
ImportThemeSection,
|
|
205
|
+
draftPatchFromImportedTheme,
|
|
206
|
+
} from './customThemes/index.js';
|
|
194
207
|
export type {
|
|
195
208
|
CustomThemeContextValue,
|
|
196
209
|
CustomThemeProviderProps,
|
|
197
210
|
DocCustomThemes,
|
|
211
|
+
ImportThemeSectionProps,
|
|
212
|
+
ImportedThemeResult,
|
|
213
|
+
ThemeSaveExtras,
|
|
198
214
|
} from './customThemes/index.js';
|
|
199
215
|
|
|
200
216
|
// Custom templates — provider stack (doc frontmatter + browser-local library)
|
|
@@ -218,24 +234,128 @@ export { buildPreviewDoc } from './buildPreviewDoc.js';
|
|
|
218
234
|
// Tiptap extension: Heading with template annotation support
|
|
219
235
|
export { HeadingWithTemplate } from './TemplateAnnotation.js';
|
|
220
236
|
|
|
221
|
-
// Diagram editor —
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
237
|
+
// Diagram editor — ASCII fences are the authored diagram format. The
|
|
238
|
+
// AsciiDiagramExtension turns code fences containing box-and-line art
|
|
239
|
+
// into interactive canvases; edits re-render the art (the fence stays the
|
|
240
|
+
// source of truth). Legacy `{[diagram]}` heading sections still RENDER in
|
|
241
|
+
// preview/player via core, but heading-based canvas editing was removed
|
|
242
|
+
// (BREAKING: DiagramExtension / DiagramWidget / useDiagramData / the
|
|
243
|
+
// heading command helpers are gone).
|
|
225
244
|
export { DiagramCanvas } from './diagram/DiagramCanvas.js';
|
|
226
245
|
export type { DiagramCommand } from './diagram/DiagramCanvas.js';
|
|
227
|
-
export {
|
|
228
|
-
export {
|
|
229
|
-
|
|
246
|
+
export type { DiagramData, DiagramNode, DiagramEdge } from './diagram/types.js';
|
|
247
|
+
export {
|
|
248
|
+
AsciiDiagramExtension,
|
|
249
|
+
findAsciiDiagramBlockPos,
|
|
250
|
+
isAsciiSourceVisible,
|
|
251
|
+
toggleAsciiSource,
|
|
252
|
+
} from './asciiDiagram/AsciiDiagramExtension.js';
|
|
253
|
+
export type {
|
|
254
|
+
AsciiDiagramBlockEntry,
|
|
255
|
+
AsciiDiagramExtensionOptions,
|
|
256
|
+
AsciiDiagramPluginState,
|
|
257
|
+
} from './asciiDiagram/AsciiDiagramExtension.js';
|
|
258
|
+
export { AsciiDiagramWidget } from './asciiDiagram/AsciiDiagramWidget.js';
|
|
259
|
+
export { useAsciiDiagramData, asciiDiagramToCanvas } from './asciiDiagram/asciiDiagramData.js';
|
|
260
|
+
export type { AsciiDiagramView } from './asciiDiagram/asciiDiagramData.js';
|
|
261
|
+
export {
|
|
262
|
+
applyAsciiDiagramCommand,
|
|
263
|
+
applyRepairCommand,
|
|
264
|
+
replaceAsciiFenceText,
|
|
265
|
+
} from './asciiDiagram/asciiDiagramCommands.js';
|
|
266
|
+
// RepairableDiagramExtension mounts an inline "Repair as diagram" button on
|
|
267
|
+
// code fences holding BROKEN box art — art too misaligned for clean detection
|
|
268
|
+
// (so it renders as a faithful code block). One click reconstructs it into
|
|
269
|
+
// clean `diagram`-tagged art that the AsciiDiagramExtension turns into a
|
|
270
|
+
// canvas. Peer to the diagram/tree editors; claims the fences neither does.
|
|
271
|
+
export {
|
|
272
|
+
RepairableDiagramExtension,
|
|
273
|
+
findRepairableBlockPos,
|
|
274
|
+
isRepairableFence,
|
|
275
|
+
REPAIRABLE_KEY,
|
|
276
|
+
} from './asciiDiagram/RepairableDiagramExtension.js';
|
|
277
|
+
export type {
|
|
278
|
+
RepairableBlockEntry,
|
|
279
|
+
RepairableDiagramExtensionOptions,
|
|
280
|
+
RepairablePluginState,
|
|
281
|
+
} from './asciiDiagram/RepairableDiagramExtension.js';
|
|
282
|
+
export {
|
|
283
|
+
addEdgeOp,
|
|
284
|
+
addNodeOp,
|
|
285
|
+
moveNodeOp,
|
|
286
|
+
removeEdgeOp,
|
|
287
|
+
removeNodeOp,
|
|
288
|
+
renameNodeOp,
|
|
289
|
+
resizeNodeOp,
|
|
290
|
+
sanitizeAsciiLabel,
|
|
291
|
+
} from './asciiDiagram/asciiDiagramOps.js';
|
|
292
|
+
export { shouldPasteAsAsciiFence } from './asciiDiagram/asciiPaste.js';
|
|
293
|
+
|
|
294
|
+
// Treeview editor — ASCII file-tree / outline fences get an interactive
|
|
295
|
+
// outline widget; edits re-render the tree art (the fence stays the source
|
|
296
|
+
// of truth). Peer to the ASCII diagram editor; mutually exclusive with it
|
|
297
|
+
// (a fence with boxes is a diagram, not a tree).
|
|
298
|
+
export { TreeViewExtension, findTreeBlockPos } from './treeview/TreeViewExtension.js';
|
|
299
|
+
export type {
|
|
300
|
+
TreeBlockEntry,
|
|
301
|
+
TreeViewExtensionOptions,
|
|
302
|
+
TreeViewPluginState,
|
|
303
|
+
} from './treeview/TreeViewExtension.js';
|
|
304
|
+
export { TreeOutlineWidget } from './treeview/TreeOutlineWidget.js';
|
|
305
|
+
export { useTreeViewData } from './treeview/treeViewData.js';
|
|
306
|
+
export type { TreeViewData } from './treeview/treeViewData.js';
|
|
307
|
+
export { applyTreeCommand, replaceTreeFenceText } from './treeview/treeViewCommands.js';
|
|
308
|
+
export type { TreeCommand } from './treeview/treeViewCommands.js';
|
|
309
|
+
export {
|
|
310
|
+
addItemOp,
|
|
311
|
+
indentItemOp,
|
|
312
|
+
moveItemDownOp,
|
|
313
|
+
moveItemUpOp,
|
|
314
|
+
outdentItemOp,
|
|
315
|
+
removeItemOp,
|
|
316
|
+
renameItemOp,
|
|
317
|
+
sanitizeTreeLabel,
|
|
318
|
+
toggleDirOp,
|
|
319
|
+
} from './treeview/treeOps.js';
|
|
320
|
+
export { shouldPasteAsTreeFence } from './treeview/treePaste.js';
|
|
321
|
+
|
|
322
|
+
// Timeline editor — authored marker-rail fences become an accessible canvas
|
|
323
|
+
// with add-point affordances and a selected-point inspector. Semantic edits
|
|
324
|
+
// re-render the canonical `timeline` fence in one undoable transaction.
|
|
325
|
+
export {
|
|
326
|
+
TimelineViewExtension,
|
|
327
|
+
TIMELINE_VIEW_KEY,
|
|
328
|
+
findTimelineBlockPos,
|
|
329
|
+
getTimelineForNode,
|
|
330
|
+
parseTimelineForNode,
|
|
331
|
+
} from './timeline/TimelineViewExtension.js';
|
|
332
|
+
export type {
|
|
333
|
+
TimelineBlockEntry,
|
|
334
|
+
TimelineViewExtensionOptions,
|
|
335
|
+
TimelineViewPluginState,
|
|
336
|
+
} from './timeline/TimelineViewExtension.js';
|
|
337
|
+
export { TimelineEditorWidget } from './timeline/TimelineEditorWidget.js';
|
|
338
|
+
export type { TimelineEditorWidgetProps } from './timeline/TimelineEditorWidget.js';
|
|
339
|
+
export { useTimelineData } from './timeline/timelineData.js';
|
|
340
|
+
export type { TimelineViewData } from './timeline/timelineData.js';
|
|
230
341
|
export {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
342
|
+
applyTimelineCommand,
|
|
343
|
+
isTimelineSourceSafeForSemanticEdit,
|
|
344
|
+
} from './timeline/timelineCommands.js';
|
|
345
|
+
export type { TimelineCommand, TimelineCommandResult } from './timeline/timelineCommands.js';
|
|
346
|
+
export {
|
|
347
|
+
addTimelineEventOp,
|
|
348
|
+
nextTimelineEventId,
|
|
349
|
+
removeTimelineEventOp,
|
|
350
|
+
sanitizeTimelineText,
|
|
351
|
+
updateTimelineEventOp,
|
|
352
|
+
} from './timeline/timelineOps.js';
|
|
353
|
+
export type {
|
|
354
|
+
AddTimelineEventOptions,
|
|
355
|
+
AddTimelineEventResult,
|
|
356
|
+
TimelineEventPatch,
|
|
357
|
+
} from './timeline/timelineOps.js';
|
|
358
|
+
export { shouldPasteAsTimelineFence } from './timeline/timelinePaste.js';
|
|
239
359
|
|
|
240
360
|
// JSON Form — editable component
|
|
241
361
|
export { JsonEditor } from './jsonEditor/index.js';
|
|
@@ -276,6 +396,59 @@ export type { CaptureKind, ResolvedFormat } from './recorder/formats.js';
|
|
|
276
396
|
export { buildTimingJson, encodeTimingJson, timingPathFor } from './recorder/timingJson.js';
|
|
277
397
|
export type { TimingJson, RecordedBookmark } from './recorder/timingJson.js';
|
|
278
398
|
|
|
399
|
+
// Teleprompter — the Narrate display mode under the Use tab: voice-paced
|
|
400
|
+
// prompter surface, floating-window tiers, and the mic analysis pipeline.
|
|
401
|
+
export {
|
|
402
|
+
TeleprompterView,
|
|
403
|
+
TeleprompterSurface,
|
|
404
|
+
TeleprompterControls,
|
|
405
|
+
useTeleprompter,
|
|
406
|
+
useNarrationRecorder,
|
|
407
|
+
buildNarrationSavePlan,
|
|
408
|
+
executeNarrationSave,
|
|
409
|
+
insertNarrationPreamble,
|
|
410
|
+
narrationAnnotationLine,
|
|
411
|
+
cameraVideoLine,
|
|
412
|
+
vadConfigForSensitivity,
|
|
413
|
+
useMicAnalysis,
|
|
414
|
+
PCM_WORKLET_SOURCE,
|
|
415
|
+
PCM_WORKLET_NAME,
|
|
416
|
+
registerPcmWorklet,
|
|
417
|
+
createFloatingWindowManager,
|
|
418
|
+
detectFloatTiers,
|
|
419
|
+
useFloatingWindow,
|
|
420
|
+
measureTokenLines,
|
|
421
|
+
targetOffsetFor,
|
|
422
|
+
stepScroll,
|
|
423
|
+
EYE_LINE_FRACTION,
|
|
424
|
+
TELEPROMPTER_CSS,
|
|
425
|
+
ensureTeleprompterStyles,
|
|
426
|
+
prompterVarsFromTheme,
|
|
427
|
+
DEFAULT_TELEPROMPTER_PREFS,
|
|
428
|
+
} from './teleprompter/index.js';
|
|
429
|
+
export type {
|
|
430
|
+
TeleprompterViewProps,
|
|
431
|
+
TeleprompterSurfaceProps,
|
|
432
|
+
TeleprompterControlsProps,
|
|
433
|
+
TeleprompterController,
|
|
434
|
+
TeleprompterRecordingDeps,
|
|
435
|
+
NarrationRecorderController,
|
|
436
|
+
NarrationRecorderState,
|
|
437
|
+
NarrationTake,
|
|
438
|
+
NarrationSavePlan,
|
|
439
|
+
NarrationSaveResult,
|
|
440
|
+
MicAnalysisHandle,
|
|
441
|
+
MicAnalysisStatus,
|
|
442
|
+
FloatingWindowManager,
|
|
443
|
+
FloatOpenOptions,
|
|
444
|
+
CanvasSink,
|
|
445
|
+
FloatingWindowHandle,
|
|
446
|
+
TokenLineMap,
|
|
447
|
+
TeleprompterPrefs,
|
|
448
|
+
PrompterTransport,
|
|
449
|
+
FloatTier,
|
|
450
|
+
} from './teleprompter/index.js';
|
|
451
|
+
|
|
279
452
|
// Image editor — layered, sidecar-persisted raster authoring surface.
|
|
280
453
|
// Pairs with `ImageViewer` and the `<basename>_files/` sidecar convention.
|
|
281
454
|
export { ImageEditor } from './ImageEditor.js';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* to the host's `onChange`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { createContext, useContext, useMemo, type ReactNode } from 'react';
|
|
8
|
+
import { createContext, useContext, useEffect, useMemo, type ReactNode } from 'react';
|
|
9
9
|
import type {
|
|
10
10
|
JsonFormValidationError,
|
|
11
11
|
JsonFormValidator,
|
|
@@ -38,18 +38,24 @@ export interface JsonEditorProviderProps {
|
|
|
38
38
|
export function JsonEditorProvider(props: JsonEditorProviderProps) {
|
|
39
39
|
const { rootSchema, rootData, onRootChange, density, validate, onValidate, children } = props;
|
|
40
40
|
|
|
41
|
+
const validationErrors = useMemo(
|
|
42
|
+
() => (validate ? validate(rootData, rootSchema) : []),
|
|
43
|
+
[validate, rootData, rootSchema],
|
|
44
|
+
);
|
|
41
45
|
const errors = useMemo(() => {
|
|
42
|
-
if (!validate) return new Map<string, JsonFormValidationError[]>();
|
|
43
|
-
const list = validate(rootData, rootSchema);
|
|
44
|
-
if (onValidate) onValidate(list);
|
|
45
46
|
const grouped = new Map<string, JsonFormValidationError[]>();
|
|
46
|
-
for (const e of
|
|
47
|
+
for (const e of validationErrors) {
|
|
47
48
|
const arr = grouped.get(e.path) ?? [];
|
|
48
49
|
arr.push(e);
|
|
49
50
|
grouped.set(e.path, arr);
|
|
50
51
|
}
|
|
51
52
|
return grouped;
|
|
52
|
-
}, [
|
|
53
|
+
}, [validationErrors]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!onValidate) return;
|
|
57
|
+
onValidate(validationErrors);
|
|
58
|
+
}, [validationErrors, onValidate]);
|
|
53
59
|
|
|
54
60
|
const setAtPath = useMemo(
|
|
55
61
|
() => (pointer: string, value: unknown) => {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
resolveRef,
|
|
12
12
|
type SquisqAnnotatedSchema,
|
|
13
13
|
} from '@bendyline/squisq/jsonForm';
|
|
14
|
+
import { useId } from 'react';
|
|
14
15
|
import { useJsonEditor } from './JsonEditorContext';
|
|
15
16
|
import { EDITORS, isCompositeControl } from './editors';
|
|
16
17
|
|
|
@@ -23,6 +24,7 @@ export interface RenderNodeProps {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function RenderNode(props: RenderNodeProps): React.ReactElement | null {
|
|
27
|
+
const generatedId = useId().replace(/:/g, '');
|
|
26
28
|
const { rootSchema, rootData, errors } = useJsonEditor();
|
|
27
29
|
const resolved = resolveRef(props.schema, rootSchema) ?? props.schema;
|
|
28
30
|
|
|
@@ -35,6 +37,11 @@ export function RenderNode(props: RenderNodeProps): React.ReactElement | null {
|
|
|
35
37
|
const label = resolved.squisq?.label ?? resolved.title;
|
|
36
38
|
const help = resolved.squisq?.help ?? resolved.description;
|
|
37
39
|
const fieldErrors = errors.get(props.pointer) ?? [];
|
|
40
|
+
const controlId = `squisq-json-control-${generatedId}`;
|
|
41
|
+
const labelId = label ? `${controlId}-label` : undefined;
|
|
42
|
+
const helpId = help ? `${controlId}-help` : undefined;
|
|
43
|
+
const errorId = fieldErrors.length > 0 ? `${controlId}-error` : undefined;
|
|
44
|
+
const describedBy = [helpId, errorId].filter(Boolean).join(' ') || undefined;
|
|
38
45
|
|
|
39
46
|
const editorEl = (
|
|
40
47
|
<Editor
|
|
@@ -42,6 +49,10 @@ export function RenderNode(props: RenderNodeProps): React.ReactElement | null {
|
|
|
42
49
|
schema={resolved}
|
|
43
50
|
pointer={props.pointer}
|
|
44
51
|
disabled={disabled}
|
|
52
|
+
controlId={controlId}
|
|
53
|
+
labelledBy={labelId}
|
|
54
|
+
describedBy={describedBy}
|
|
55
|
+
invalid={fieldErrors.length > 0}
|
|
45
56
|
// GroupEditor honors this — other editors ignore the extra prop.
|
|
46
57
|
// @ts-expect-error optional renderer-specific prop
|
|
47
58
|
suppressTitle={props.suppressTopGroupTitle}
|
|
@@ -55,11 +66,21 @@ export function RenderNode(props: RenderNodeProps): React.ReactElement | null {
|
|
|
55
66
|
|
|
56
67
|
return (
|
|
57
68
|
<div className={`squisq-jf-field${disabled ? ' squisq-jf-field--disabled' : ''}`}>
|
|
58
|
-
{label ?
|
|
69
|
+
{label ? (
|
|
70
|
+
<label id={labelId} htmlFor={controlId} className="squisq-jf-field__label">
|
|
71
|
+
{label}
|
|
72
|
+
</label>
|
|
73
|
+
) : null}
|
|
59
74
|
{editorEl}
|
|
60
|
-
{help ?
|
|
75
|
+
{help ? (
|
|
76
|
+
<p id={helpId} className="squisq-jf-field__help">
|
|
77
|
+
{help}
|
|
78
|
+
</p>
|
|
79
|
+
) : null}
|
|
61
80
|
{fieldErrors.length > 0 ? (
|
|
62
|
-
<p className="squisq-jf-field__error">
|
|
81
|
+
<p id={errorId} className="squisq-jf-field__error" role="alert">
|
|
82
|
+
{fieldErrors[0].message}
|
|
83
|
+
</p>
|
|
63
84
|
) : null}
|
|
64
85
|
</div>
|
|
65
86
|
);
|