@inkandswitch/patchwork-pkg-base 0.0.100 → 0.0.102
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/module-settings-url +1 -1
- package/package.json +1 -1
- package/packages/account/dist/index.css +32 -32
- package/packages/account/dist/index.css.map +2 -2
- package/packages/account-history/dist/assets/HistoryRecorder-_3PrVW6n.js +1 -1
- package/packages/account-history/dist/assets/HistoryViewer-BmG0rAfK.js +1 -1
- package/packages/account-history/dist/assets/NotebookViewer-DZ_ZQU5x.js +37 -37
- package/packages/add-doc-to-sidebar-button/styles.css +2 -2
- package/packages/chat/dist/{ChatRoot-BYmq4SXi.js → ChatRoot-1d-658Rr.js} +2 -2
- package/packages/chat/dist/{context-tool-Bob-FZ_T.js → context-tool-iP64Oz6H.js} +2 -2
- package/packages/chat/dist/{index-wkaHcVCz.js → index-Dm_2jZcu.js} +3 -3
- package/packages/chat/dist/index.js +2 -2
- package/packages/chat/dist/{tool-DCOdh_fx.js → tool-BmNd5mrj.js} +1 -1
- package/packages/codemirror-base/dist/{chunk-QRT4ZVQ4.js → chunk-A2WBDY3J.js} +5 -5
- package/packages/codemirror-base/dist/{chunk-QRT4ZVQ4.js.map → chunk-A2WBDY3J.js.map} +1 -1
- package/packages/codemirror-base/dist/lib/codemirror.js +1 -1
- package/packages/codemirror-base/dist/tool.js +3 -3
- package/packages/codemirror-base/dist/tool.js.map +1 -1
- package/packages/codemirror-markdown/dist/extensions/embed.js +1 -1
- package/packages/codemirror-markdown/dist/extensions/embed.js.map +1 -1
- package/packages/codemirror-markdown/dist/extensions/links.js +1 -1
- package/packages/codemirror-markdown/dist/extensions/links.js.map +1 -1
- package/packages/codemirror-markdown/dist/extensions/markdown.js +6 -6
- package/packages/codemirror-markdown/dist/extensions/markdown.js.map +1 -1
- package/packages/commands/dist/main.js +1 -1
- package/packages/commands/dist/main.js.map +1 -1
- package/packages/comments-view/dist/assets/main-wSSvAVfi.js +1 -1
- package/packages/doc-openwith/doc-openwith.js +5 -5
- package/packages/doc-presence/doc-presence.js +1 -1
- package/packages/drafts/dist/assets/main-CG9HOCai.js +31 -31
- package/packages/embed/dist/assets/tool-D58NGTZ7.js +1 -1
- package/packages/file/dist/index.js +2 -2
- package/packages/file/dist/{new-file-tool-DGKllnzw.js → new-file-tool-BolBDUrL.js} +27 -27
- package/packages/file/dist/new-file-tool-BolBDUrL.js.map +1 -0
- package/packages/file/dist/{tool-UKWDK93W.js → tool-Bt8I8T2-.js} +4 -4
- package/packages/file/dist/tool-Bt8I8T2-.js.map +1 -0
- package/packages/file/dist/{typescript-language-server-BgBx0vSF.js → typescript-language-server-Db4173Y5.js} +2 -2
- package/packages/file/dist/{typescript-language-server-BgBx0vSF.js.map → typescript-language-server-Db4173Y5.js.map} +1 -1
- package/packages/folder/dist/index.js +1 -1
- package/packages/folder/dist/{tool-WAWTPB4A.js → tool-5E3YPGES.js} +2 -2
- package/packages/folder/dist/{tool-WAWTPB4A.js.map → tool-5E3YPGES.js.map} +1 -1
- package/packages/history-view/dist/assets/{HistoryTimeline-COYNGORP.js → HistoryTimeline-Bo-BTf2A.js} +6 -6
- package/packages/history-view/dist/assets/{HistoryTimeline-COYNGORP.js.map → HistoryTimeline-Bo-BTf2A.js.map} +1 -1
- package/packages/history-view/dist/assets/{index-C1gYt7m5.js → index-B5Afrk9Y.js} +2 -2
- package/packages/history-view/dist/assets/{index-C1gYt7m5.js.map → index-B5Afrk9Y.js.map} +1 -1
- package/packages/history-view/dist/index.js +1 -1
- package/packages/llm-host/dist/{LlmConfigTray-DdZHZT4F.js → LlmConfigTray-CjW04jeT.js} +108 -108
- package/packages/llm-host/dist/index.js +1 -1
- package/packages/packages/dist/index.js +46 -46
- package/packages/patternwitch/tool.js +21 -0
- package/packages/raw/raw-editor.js +22 -22
- package/packages/settings/dist/assets/FrameConfigurator-DVRBzIR-.js +1 -1
- package/packages/sideboard/dist/index.css +1 -1
- package/packages/sideboard/dist/index.css.map +2 -2
- package/packages/sideboard/dist/index.js +1 -1
- package/packages/sync-indicator/dist/assets/{SyncIndicator-B2ag1OoZ.js → SyncIndicator-oVMcVRXg.js} +3 -3
- package/packages/sync-indicator/dist/assets/{SyncIndicator-B2ag1OoZ.js.map → SyncIndicator-oVMcVRXg.js.map} +1 -1
- package/packages/sync-indicator/dist/index.js +1 -1
- package/packages/tasks/dist/assets/index-BbuRtBN3.js +1 -1
- package/packages/theming/dist/{active-theme-BMSIQXBV.js → active-theme-CC3CS64F.js} +2 -2
- package/packages/theming/dist/{chunk-Z3JB7ERH.js → chunk-DH4YUYFJ.js} +2 -2
- package/packages/theming/dist/chunk-VJZ4NPK4.js +136 -0
- package/packages/theming/dist/chunk-VJZ4NPK4.js.map +7 -0
- package/packages/theming/dist/main.js +4 -4
- package/packages/theming/dist/{theme-7OG2K56L.css → theme-HE7ECLIW.css} +212 -58
- package/packages/theming/dist/{theme-editor-datatype-TMYH7U2S.js → theme-editor-datatype-TJ6LYT2U.js} +2 -2
- package/packages/theming/dist/theme-editor.js +35 -25
- package/packages/theming/dist/theme-editor.js.map +2 -2
- package/packages/theming/dist/{theme-titlebar-YIFJINPJ.js → theme-titlebar-BO2Y2GRN.js} +2 -2
- package/packages/theming/dist/{theme-tray-7J247NRY.js → theme-tray-V6BEXQWR.js} +15 -15
- package/packages/theming/dist/theme-tray-V6BEXQWR.js.map +7 -0
- package/packages/theming/dist/tool.js +9 -9
- package/packages/theming/dist/tool.js.map +1 -1
- package/packages/threepane/dist/IsolatedDocumentArea-IZWZ74TV.js +2 -0
- package/packages/threepane/dist/{PatchworkFrame-GEEBBGCS.js → PatchworkFrame-UNWWAISP.js} +2 -2
- package/packages/threepane/dist/{chunk-YC5AHP5G.js → chunk-MSZ7JVNB.js} +31 -31
- package/packages/threepane/dist/{chunk-YC5AHP5G.js.map → chunk-MSZ7JVNB.js.map} +3 -3
- package/packages/threepane/dist/index.js +1 -1
- package/plugin-registry-url +1 -1
- package/packages/file/dist/new-file-tool-DGKllnzw.js.map +0 -1
- package/packages/file/dist/tool-UKWDK93W.js.map +0 -1
- package/packages/theming/dist/chunk-GIPQLUDU.js +0 -86
- package/packages/theming/dist/chunk-GIPQLUDU.js.map +0 -7
- package/packages/theming/dist/theme-tray-7J247NRY.js.map +0 -7
- package/packages/threepane/dist/IsolatedDocumentArea-G3V7KYHJ.js +0 -2
- /package/packages/theming/dist/{active-theme-BMSIQXBV.js.map → active-theme-CC3CS64F.js.map} +0 -0
- /package/packages/theming/dist/{chunk-Z3JB7ERH.js.map → chunk-DH4YUYFJ.js.map} +0 -0
- /package/packages/theming/dist/{theme-editor-datatype-TMYH7U2S.js.map → theme-editor-datatype-TJ6LYT2U.js.map} +0 -0
- /package/packages/theming/dist/{theme-titlebar-YIFJINPJ.js.map → theme-titlebar-BO2Y2GRN.js.map} +0 -0
- /package/packages/threepane/dist/{IsolatedDocumentArea-G3V7KYHJ.js.map → IsolatedDocumentArea-IZWZ74TV.js.map} +0 -0
- /package/packages/threepane/dist/{PatchworkFrame-GEEBBGCS.js.map → PatchworkFrame-UNWWAISP.js.map} +0 -0
|
@@ -495,7 +495,7 @@ var DeletionMarker = class extends WidgetType {
|
|
|
495
495
|
box.style.whiteSpace = "nowrap";
|
|
496
496
|
box.style.backgroundColor = this.isActive ? "rgb(239 68 68 / 20%)" : "rgb(239 68 68 / 10%)";
|
|
497
497
|
box.style.borderRadius = "3px";
|
|
498
|
-
box.style.cursor = "default";
|
|
498
|
+
box.style.cursor = "var(--studio-cursor-default, default)";
|
|
499
499
|
box.innerText = "\u232B";
|
|
500
500
|
const hoverText = document.createElement("div");
|
|
501
501
|
hoverText.style.position = "absolute";
|
|
@@ -506,7 +506,7 @@ var DeletionMarker = class extends WidgetType {
|
|
|
506
506
|
hoverText.style.backgroundColor = "rgb(254 242 242)";
|
|
507
507
|
hoverText.style.fontSize = "15px";
|
|
508
508
|
hoverText.style.color = "rgb(17 24 39)";
|
|
509
|
-
hoverText.style.border = "1px solid rgb(185 28 28)";
|
|
509
|
+
hoverText.style.border = "var(--studio-border-width, 1px) var(--studio-border-style, solid) rgb(185 28 28)";
|
|
510
510
|
hoverText.style.boxShadow = "0px 0px 6px rgba(0, 0, 0, 0.1)";
|
|
511
511
|
hoverText.style.borderRadius = "3px";
|
|
512
512
|
hoverText.style.visibility = "hidden";
|
|
@@ -517,7 +517,7 @@ var DeletionMarker = class extends WidgetType {
|
|
|
517
517
|
box.style.backgroundColor = this.isActive ? "rgb(248 113 113 / 20%)" : "rgb(248 113 113 / 10%)";
|
|
518
518
|
hoverText.style.backgroundColor = "rgb(69 10 10)";
|
|
519
519
|
hoverText.style.color = "rgb(254 226 226)";
|
|
520
|
-
hoverText.style.border = "1px solid rgb(153 27 27)";
|
|
520
|
+
hoverText.style.border = "var(--studio-border-width, 1px) var(--studio-border-style, solid) rgb(153 27 27)";
|
|
521
521
|
}
|
|
522
522
|
box.appendChild(hoverText);
|
|
523
523
|
box.onmouseover = function() {
|
|
@@ -867,7 +867,7 @@ var presenceTheme = EditorView8.baseTheme({
|
|
|
867
867
|
pointerEvents: "none",
|
|
868
868
|
userSelect: "none",
|
|
869
869
|
opacity: "1",
|
|
870
|
-
transition: "opacity 200ms ease-in-out"
|
|
870
|
+
transition: "opacity max(1ms, calc(200ms * var(--studio-motion-scale, 1) * var(--studio-motion-preference, 1))) ease-in-out"
|
|
871
871
|
},
|
|
872
872
|
".cm-presence-caret--idle .cm-presence-caret-label": {
|
|
873
873
|
opacity: "0"
|
|
@@ -939,4 +939,4 @@ function CodeMirror(props) {
|
|
|
939
939
|
export {
|
|
940
940
|
CodeMirror
|
|
941
941
|
};
|
|
942
|
-
//# sourceMappingURL=chunk-
|
|
942
|
+
//# sourceMappingURL=chunk-A2WBDY3J.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/lib/codemirror.tsx", "../src/lib/extensions/decorations.ts", "../src/lib/extensions/readOnly.ts", "../src/lib/extensions/automerge.ts", "../src/vendor/automerge-codemirror/plugin.ts", "../src/vendor/automerge-codemirror/codeMirrorToAm.ts", "../src/vendor/automerge-codemirror/amToCodemirror.ts", "../src/vendor/automerge-codemirror/history.ts", "../src/vendor/automerge-codemirror/readOnly.ts", "../src/vendor/automerge-codemirror/index.ts", "../src/lib/extensions/diff.ts", "../src/lib/extensions/scrollHighlightIntoView.ts", "../src/lib/extensions/presence.ts"],
|
|
4
|
-
"sourcesContent": ["import { onCleanup, createEffect } from \"solid-js\";\nimport { EditorView } from \"@codemirror/view\";\nimport { EditorState, Compartment } from \"@codemirror/state\";\nimport { createAutomergeExtension, createReadOnlyExtension, createDecorationsExtension, createDiffExtension, createScrollHighlightIntoViewExtension, createPresenceExtension } from \"./extensions\";\nconst lookup = (doc, path) => {\n let current = doc;\n for (const key of path) {\n current = current[key];\n if (current === void 0) {\n return void 0;\n }\n }\n return current;\n};\nexport function CodeMirror(props) {\n const initialDoc = () => props.handle && lookup(props.handle.doc(), props.path) || \"\";\n const [automergeExtension, createEffectReconfigureAutomerge] = createAutomergeExtension(() => props.handle, () => props.path, initialDoc);\n const [readOnlyExtension, createEffectReconfigureReadOnly] = createReadOnlyExtension(() => !!props.readOnly);\n const [decorationsExtension, createEffectReconfigureDecorations] = createDecorationsExtension(() => props.decorations?.());\n const [diffExtension, createEffectReconfigureDiff] = createDiffExtension(() => props.handle, () => props.path, () => props.baseline?.() ?? null);\n const [scrollHighlightIntoViewExtension, createEffectScrollHighlightIntoView] = createScrollHighlightIntoViewExtension(() => props.scrollTarget?.() ?? null);\n const [presenceExtension, createEffectReconfigurePresence] = createPresenceExtension(() => props.handle, () => props.path, () => props.readOnly || props.handle.isReadOnly() ? null : props.contactUrl?.() ?? null, props.repo);\n const userExtensionsCompartment = new Compartment();\n const selectionExtension = EditorView.updateListener.of(update => {\n if (!props.onChangeSelection) return;\n if (update.selectionSet) {\n const sel = update.state.selection.main;\n props.onChangeSelection(sel.from, sel.to);\n }\n });\n const extensions = [selectionExtension, decorationsExtension,\n // automergeExtension must come before diffExtension so diff stays in sync with edits.\n automergeExtension, diffExtension, scrollHighlightIntoViewExtension, presenceExtension, userExtensionsCompartment.of(props.extensions || []), readOnlyExtension].filter(Boolean);\n const state = EditorState.create({\n doc: initialDoc(),\n extensions\n });\n const view = new EditorView({\n state\n });\n props.withView?.(view);\n createEffectReconfigureAutomerge(view);\n createEffectReconfigureReadOnly(view);\n createEffectReconfigureDecorations?.(view);\n createEffectReconfigureDiff(view);\n createEffectScrollHighlightIntoView(view);\n createEffectReconfigurePresence(view);\n createEffect(() => {\n view.dispatch({\n effects: userExtensionsCompartment.reconfigure(props.extensions || [])\n });\n });\n onCleanup(() => view.destroy());\n return view.dom;\n}", "import { createEffect } from \"solid-js\";\n\n/** CodeMirror */\nimport { Decoration, EditorView, type DecorationSet } from \"@codemirror/view\";\nimport { StateEffect, StateField } from \"@codemirror/state\";\n\n/**\n * Create a CodeMirror extension for managing decorations.\n * @param decorations An accessor function that returns the decorations (for reactivity).\n * @returns A tuple containing the extension and a function to create an effect for updating the\n decorations.\n */\nexport function createDecorationsExtension(decorations: () => DecorationSet) {\n const setDecorations = StateEffect.define<DecorationSet>();\n const decorationsField = StateField.define<DecorationSet>({\n create() {\n return Decoration.none;\n },\n update(value, tr) {\n for (const e of tr.effects) {\n if (e.is(setDecorations)) return e.value;\n }\n if (tr.docChanged) return value.map(tr.changes);\n return value;\n },\n provide: (f) => EditorView.decorations.from(f),\n });\n\n const createDecorationsEffect = (view: EditorView) =>\n createEffect(() => {\n decorations() &&\n view.dispatch({\n effects: setDecorations.of(decorations!()),\n });\n });\n\n return [decorationsField, createDecorationsEffect] as const;\n}\n", "import { createEffect } from \"solid-js\";\n\n/** CodeMirror */\nimport { EditorView } from \"@codemirror/view\";\nimport { Compartment, EditorState } from \"@codemirror/state\";\n\n/**\n * Force the editor read-only regardless of the handle's state.\n *\n * This is only the override: the handle's own read-only state (e.g. a\n * heads-pinned view, including flips when its backing is swapped in place)\n * is tracked by the vendored `automergeReadOnly`, installed via\n * `createAutomergeExtension`.\n *\n * @param force Forces read-only.\n * @returns A tuple containing the extension and a function to create the\n * effect that reconfigures the extension when `force` changes.\n */\nexport function createReadOnlyExtension(force: () => boolean) {\n const compartment = new Compartment();\n\n const extensions = () =>\n force()\n ? [EditorState.readOnly.of(true), EditorView.editable.of(false)]\n : [];\n\n const createReconfigureEffect = (view: EditorView) =>\n createEffect(() => {\n view.dispatch({\n effects: compartment.reconfigure(extensions()),\n });\n });\n\n return [compartment.of(extensions()), createReconfigureEffect] as const;\n}\n", "import { createEffect } from \"solid-js\";\n\n/** CodeMirror */\nimport { EditorView } from \"@codemirror/view\";\nimport { Compartment, Transaction } from \"@codemirror/state\";\n\n/** Automerge */\nimport type { Prop as AutomergeProp } from \"@automerge/automerge/slim\";\nimport type { DocHandle } from \"@automerge/automerge-repo/slim\";\nimport { automergePlugin } from \"../../vendor/automerge-codemirror/index.js\";\n\n/**\n * Returns a [Compartment extension, reconfig effect] tuple to sync a CodeMirror editor\n * with an Automerge handle+path. When reactive `handle` or `path` change, swaps the\n * plugin via Compartment and resets the doc from `initialDoc()` (kept off undo stack).\n */\nexport function createAutomergeExtension<T>(\n handle: () => DocHandle<T>,\n path: () => AutomergeProp[],\n initialDoc: () => string\n) {\n const compartment = new Compartment();\n\n const extension = () =>\n handle() && path()\n ? automergePlugin({\n handle: handle() as any, // typescript is confused by different version of doc handle\n path: path(),\n })\n : [];\n\n // Reconfigures the plugin and resets the doc to initialDoc() on reactive changes; this is synthetic remote state, so it bypasses the undo stack.\n const createReconfigureEffect = (view: EditorView) =>\n createEffect(() => {\n // handle()/path()/initialDoc() are tracked; changes rerun the rebuild.\n view.dispatch({\n effects: compartment.reconfigure(extension()),\n changes: {\n from: 0,\n to: view.state.doc.length,\n insert: initialDoc(),\n },\n annotations: [\n Transaction.addToHistory.of(false),\n Transaction.remote.of(true),\n ],\n });\n });\n\n return [compartment.of(extension()), createReconfigureEffect] as const;\n}\n", "import * as A from \"@automerge/automerge\"\nimport { EditorView, ViewPlugin, ViewUpdate } from \"@codemirror/view\"\nimport { Transaction, Annotation } from \"@codemirror/state\"\nimport { applyCmTransactionsToAmHandle } from \"./codeMirrorToAm.js\"\nimport { applyAmPatchesToCm } from \"./amToCodemirror.js\"\nimport type { DocHandle } from \"./DocHandle.js\"\n\nexport const reconcileAnnotationType = Annotation.define<unknown>()\n\nexport const isReconcileTx = (tr: Transaction): boolean =>\n !!tr.annotation(reconcileAnnotationType)\n\ntype AutomergeSyncPluginConfig = {\n handle: DocHandle<unknown>\n path: A.Prop[]\n}\n\nexport const automergeSyncPlugin = ({\n handle,\n path,\n}: AutomergeSyncPluginConfig) => {\n if (!handle.isReady()) {\n throw new Error(\n \"ensure the handle is ready before initializing the automergeSyncPlugin\"\n )\n }\n\n return ViewPlugin.fromClass(\n class {\n view: EditorView\n reconciledHeads = A.getHeads(handle.doc())\n isProcessingCmTransaction = false\n\n constructor(view: EditorView) {\n this.view = view\n\n this.onChange = this.onChange.bind(this)\n handle.on(\"change\", this.onChange)\n }\n\n update(update: ViewUpdate) {\n // start processing codemirror transaction\n // changes that are created through the transaction are ignored in the change listener on the handle\n this.isProcessingCmTransaction = true\n\n const newHeads = applyCmTransactionsToAmHandle(\n handle,\n path,\n update.transactions as Transaction[]\n )\n\n if (newHeads) {\n this.reconciledHeads = newHeads\n }\n\n // finish processing transaction\n this.isProcessingCmTransaction = false\n }\n\n // Changes with a replaced scope (the handle was re-pointed at a\n // different backing document) need no special-casing here. A.diff\n // ignores heads that aren't in the doc's history, treating that side\n // as the empty document: when the backings share history (heads\n // pinning, clones) it yields minimal patches that preserve the\n // selection, and when they share nothing it degenerates into\n // full-document patches that rebuild the editor wholesale. Either\n // way the patches are kept off the undo stack by applyAmPatchesToCm.\n // The history and read-only extensions are the ones that consume the\n // `scopeReplaced` flag.\n onChange = () => {\n // ignore changes that where triggered while processing a codemirror transaction\n if (this.isProcessingCmTransaction) {\n return\n }\n\n const currentHeads = A.getHeads(handle.doc())\n if (A.equals(currentHeads, this.reconciledHeads)) {\n return\n }\n\n // get the diff between the reconciled heads and the new heads\n // and apply that to the codemirror doc\n const patches = A.diff(handle.doc(), this.reconciledHeads, currentHeads)\n applyAmPatchesToCm(this.view, path, patches)\n this.reconciledHeads = currentHeads\n }\n\n destroy() {\n handle.off(\"change\", this.onChange)\n }\n }\n )\n}\n", "import * as A from \"@automerge/automerge\"\nimport { Text, Transaction } from \"@codemirror/state\"\nimport { isReconcileTx } from \"./plugin.js\"\nimport { type DocHandle } from \"./DocHandle.js\"\n\nexport const applyCmTransactionsToAmHandle = (\n handle: DocHandle<unknown>,\n path: A.Prop[],\n transactions: Transaction[]\n): A.Heads | undefined => {\n const transactionsWithChanges = transactions.filter(\n tr => !isReconcileTx(tr) && !tr.changes.empty\n )\n\n if (transactionsWithChanges.length === 0) {\n return\n }\n\n // A read-only handle (e.g. pinned to historical heads) rejects writes:\n // never forward editor changes to it. The automergeReadOnly extension\n // keeps the editor from producing such changes in the first place.\n if (handle.isReadOnly?.()) {\n return\n }\n\n handle.change((doc: A.Doc<unknown>) => {\n transactionsWithChanges.forEach(tr => {\n tr.changes.iterChanges(\n (\n fromA: number,\n toA: number,\n fromB: number,\n _toB: number,\n inserted: Text\n ) => {\n // We are cloning the path as `am.splice` calls `.unshift` on it, modifying it in place,\n // causing the path to be broken on subsequent changes\n A.splice(doc, path.slice(), fromB, toA - fromA, inserted.toString())\n }\n )\n })\n })\n\n return A.getHeads(handle.doc())\n}\n", "import type {\n DelPatch,\n InsertPatch,\n Patch,\n Prop,\n PutPatch,\n SpliceTextPatch,\n} from \"@automerge/automerge\"\nimport {\n ChangeSet,\n type ChangeSpec,\n EditorState,\n Transaction,\n} from \"@codemirror/state\"\nimport { EditorView } from \"@codemirror/view\"\nimport { reconcileAnnotationType } from \"./plugin.js\"\n\nexport const applyAmPatchesToCm = (\n view: EditorView,\n target: Prop[],\n patches: Patch[]\n) => {\n let selection = view.state.selection\n\n for (const patch of patches) {\n const changeSpec = handlePatch(patch, target, view.state)\n if (changeSpec != null) {\n const changeSet = ChangeSet.of(changeSpec, view.state.doc.length, \"\\n\")\n selection = selection.map(changeSet, 1)\n // Remote changes from automerge are excluded from local undo (addToHistory: false).\n view.dispatch({\n changes: changeSet,\n annotations: [\n reconcileAnnotationType.of({}),\n Transaction.addToHistory.of(false),\n Transaction.remote.of(true),\n ],\n })\n }\n }\n view.dispatch({\n selection,\n annotations: reconcileAnnotationType.of({}),\n })\n}\n\nfunction handlePatch(\n patch: Patch,\n target: Prop[],\n state: EditorState\n): ChangeSpec | null {\n if (patch.action === \"insert\") {\n return handleInsert(target, patch)\n } else if (patch.action === \"splice\") {\n return handleSplice(target, patch)\n } else if (patch.action === \"del\") {\n return handleDel(target, patch)\n } else if (patch.action === \"put\") {\n return handlePut(target, patch, state)\n } else {\n return null\n }\n}\n\nfunction handleInsert(target: Prop[], patch: InsertPatch): Array<ChangeSpec> {\n const index = charPath(target, patch.path)\n if (index == null) {\n return []\n }\n const text = patch.values.map(v => (v ? v.toString() : \"\")).join(\"\")\n return [{ from: index, to: index, insert: text }]\n}\n\nfunction handleSplice(\n target: Prop[],\n patch: SpliceTextPatch\n): Array<ChangeSpec> {\n const index = charPath(target, patch.path)\n if (index == null) {\n return []\n }\n return [{ from: index, insert: patch.value }]\n}\n\nfunction handleDel(target: Prop[], patch: DelPatch): Array<ChangeSpec> {\n const index = charPath(target, patch.path)\n if (index == null) {\n return []\n }\n const length = patch.length || 1\n return [{ from: index, to: index + length }]\n}\n\nfunction handlePut(\n target: Prop[],\n patch: PutPatch,\n state: EditorState\n): Array<ChangeSpec> {\n const index = charPath(target, [...patch.path, 0])\n if (index == null) {\n return []\n }\n const length = state.doc.length\n if (typeof patch.value !== \"string\") {\n return [] // TODO(dmaretskyi): How to handle non string values?\n }\n return [{ from: 0, to: length, insert: patch.value }]\n}\n\n// If the path of the patch is of the form [path, <index>] then we know this is\n// a path to a character within the sequence given by path\nfunction charPath(textPath: Prop[], candidatePath: Prop[]): number | null {\n if (candidatePath.length !== textPath.length + 1) return null\n for (let i = 0; i < textPath.length; i++) {\n if (textPath[i] !== candidatePath[i]) return null\n }\n const index = candidatePath[candidatePath.length - 1]\n if (typeof index === \"number\") return index\n return null\n}\n", "import { history, historyField, historyKeymap } from \"@codemirror/commands\"\nimport { Compartment, type Extension } from \"@codemirror/state\"\nimport { EditorView, ViewPlugin, keymap } from \"@codemirror/view\"\nimport type { DocHandle, DocHandleChangePayload } from \"./DocHandle.js\"\n\ntype AutomergeHistoryConfig = {\n handle: DocHandle<unknown>\n}\n\n/**\n * Undo/redo history that understands handles whose backing document can be\n * swapped in place: the stack is reset whenever the handle emits a `change`\n * with `scopeReplaced: true`, because the old entries describe a different\n * timeline and undo must only revert what the user has done since.\n *\n * Don't add another `history()` to the editor: the underlying history state\n * field is a module-level singleton in `@codemirror/commands`, so a second copy\n * elsewhere in the configuration keeps the field alive across the reset and defeats it.\n * If the user configures history on their own we detect this and warned about it at mount.\n */\nexport const automergeHistory = ({\n handle,\n}: AutomergeHistoryConfig): Extension => {\n const compartment = new Compartment()\n const historyExtension = () => [history(), keymap.of(historyKeymap)]\n\n const resetPlugin = ViewPlugin.fromClass(\n class {\n warned = false\n destroyed = false\n readonly view: EditorView\n\n constructor(view: EditorView) {\n this.view = view\n this.onChange = this.onChange.bind(this)\n handle.on(\"change\", this.onChange)\n // Probe once at mount, when the history is empty and cycling is\n // harmless: a duplicate history() warns immediately instead of on\n // the first swap. Deferred because dispatching isn't allowed while\n // the view is still initializing.\n queueMicrotask(() => {\n if (!this.destroyed) this.reset()\n })\n }\n\n onChange(payload?: DocHandleChangePayload) {\n if (payload?.scopeReplaced) this.reset()\n }\n\n // CodeMirror has no clear-history API; the sanctioned reset is cycling\n // the extension out of and back into the configuration, which drops\n // the history field's state and re-creates it empty.\n reset() {\n this.view.dispatch({ effects: compartment.reconfigure([]) })\n // With our copy out of the configuration the field must be gone; if\n // it survives, another history() is keeping it alive and the reset\n // is silently defeated.\n if (\n !this.warned &&\n this.view.state.field(historyField, false) !== undefined\n ) {\n this.warned = true\n console.warn(\n \"[automerge-codemirror] another history() extension is present\" +\n \" in this editor's configuration, so the undo history cannot\" +\n \" be reset when the handle's backing is swapped. Remove the\" +\n \" other copy \u2014 note that basicSetup includes one.\"\n )\n }\n this.view.dispatch({\n effects: compartment.reconfigure(historyExtension()),\n })\n }\n\n destroy() {\n this.destroyed = true\n handle.off(\"change\", this.onChange)\n }\n }\n )\n\n return [compartment.of(historyExtension()), resetPlugin]\n}\n", "import { Compartment, EditorState, type Extension } from \"@codemirror/state\"\nimport { EditorView, ViewPlugin } from \"@codemirror/view\"\nimport type { DocHandle, DocHandleChangePayload } from \"./DocHandle.js\"\n\ntype AutomergeReadOnlyConfig = {\n handle: DocHandle<unknown>\n}\n\n/**\n * Makes the editor read-only while the handle rejects writes\n * (`handle.isReadOnly()`, e.g. pinned to historical heads). This is a\n * correctness requirement of syncing, not a preference: an editable editor\n * bound to a read-only handle produces edits the write-back must drop.\n *\n * Tracked live: the flag can flip in place when the handle's backing is\n * swapped (a `change` event with `scopeReplaced: true`). Handles without\n * `isReadOnly` are treated as always writable.\n */\nexport const automergeReadOnly = ({\n handle,\n}: AutomergeReadOnlyConfig): Extension => {\n const compartment = new Compartment()\n const isReadOnly = () => !!handle.isReadOnly?.()\n const extensions = () =>\n isReadOnly()\n ? [EditorState.readOnly.of(true), EditorView.editable.of(false)]\n : []\n\n const trackPlugin = ViewPlugin.fromClass(\n class {\n wasPrevReadOnly = isReadOnly()\n destroyed = false\n readonly view: EditorView\n\n constructor(view: EditorView) {\n this.view = view\n this.onChange = this.onChange.bind(this)\n handle.on(\"change\", this.onChange)\n // The flag may have flipped between extension creation and mount;\n // deferred because dispatching isn't allowed while the view is\n // still initializing.\n queueMicrotask(() => {\n if (!this.destroyed) this.sync()\n })\n }\n\n onChange(payload?: DocHandleChangePayload) {\n if (payload?.scopeReplaced) this.sync()\n }\n\n sync() {\n const current = isReadOnly()\n if (current === this.wasPrevReadOnly) return\n this.wasPrevReadOnly = current\n this.view.dispatch({ effects: compartment.reconfigure(extensions()) })\n }\n\n destroy() {\n this.destroyed = true\n handle.off(\"change\", this.onChange)\n }\n }\n )\n\n return [compartment.of(extensions()), trackPlugin]\n}\n", "import type * as A from \"@automerge/automerge\"\nimport type { Extension } from \"@codemirror/state\"\nimport { automergeSyncPlugin } from \"./plugin.js\"\nimport { automergeHistory } from \"./history.js\"\nimport { automergeReadOnly } from \"./readOnly.js\"\nimport type { DocHandle } from \"./DocHandle.js\"\n\ntype AutomergePluginConfig = {\n handle: DocHandle<unknown>\n path: A.Prop[]\n}\n\n/**\n * The recommended entry point: two-way sync plus the pieces entangled with\n * it \u2014 undo history that resets when the handle's backing is swapped, and\n * read-only tracking for handles that reject writes.\n *\n * Don't add another `history()` to the editor (`basicSetup` includes one):\n * the bundled history must own the only copy for its reset to work. Each\n * piece is also exported individually for \u00E0-la-carte use.\n */\nexport const automergePlugin = ({\n handle,\n path,\n}: AutomergePluginConfig): Extension => [\n automergeSyncPlugin({ handle, path }),\n automergeHistory({ handle }),\n automergeReadOnly({ handle }),\n]\n\nexport { automergeSyncPlugin } from \"./plugin.js\"\nexport { automergeHistory } from \"./history.js\"\nexport { automergeReadOnly } from \"./readOnly.js\"\nexport type { DocHandle, DocHandleChangePayload } from \"./DocHandle.js\"\n", "import { createEffect } from \"solid-js\";\n\n/** CodeMirror */\nimport {\n Decoration,\n EditorView,\n ViewPlugin,\n WidgetType,\n type DecorationSet,\n type ViewUpdate,\n} from \"@codemirror/view\";\nimport { RangeSet, StateEffect, type Range } from \"@codemirror/state\";\n\n/** Automerge */\nimport { next as Automerge } from \"@automerge/automerge/slim\";\nimport type { Prop as AutomergeProp } from \"@automerge/automerge/slim\";\nimport {\n decodeHeads,\n type Doc,\n type DocHandle,\n type UrlHeads,\n} from \"@automerge/automerge-repo/slim\";\n\n// Carries the diff baseline (fork-point heads) from the provider subscription\n// into the editor. Dispatched only when the baseline changes (i.e. on fork),\n// which is rare -- per-edit recomputation is driven by `docChanged`, not this.\nconst setBaseline = StateEffect.define<UrlHeads | null>();\n\n/**\n * CodeMirror extension that renders a diff of the synced `path` against the\n * `baseline` heads. It recomputes its decorations on every doc change -- the\n * automerge sync plugin reflects both local and remote changes as editor\n * transactions -- so no external \"tick\" is needed. The baseline is pushed in\n * via a Solid effect because the editor can't observe a fork on its own.\n *\n * @returns the extension and an effect factory that feeds baseline changes in.\n */\nexport function createDiffExtension(\n handle: () => DocHandle<unknown>,\n path: () => AutomergeProp[],\n baseline: () => UrlHeads | null\n) {\n const plugin = ViewPlugin.fromClass(\n class {\n baseline: UrlHeads | null = null;\n decorations: DecorationSet = Decoration.none;\n\n update(update: ViewUpdate) {\n let baselineChanged = false;\n for (const tr of update.transactions) {\n for (const effect of tr.effects) {\n if (effect.is(setBaseline)) {\n this.baseline = effect.value;\n baselineChanged = true;\n }\n }\n }\n if (update.docChanged || baselineChanged) {\n this.decorations = this.build();\n }\n }\n\n build(): DecorationSet {\n const heads = this.baseline;\n const doc = handle()?.doc();\n if (!heads || !doc) return Decoration.none;\n return RangeSet.of(\n buildDiffDecorations(\n diffContent(doc, heads, path()),\n prefersDarkMode()\n ),\n true // sort ranges\n );\n }\n },\n { decorations: (v) => v.decorations }\n );\n\n const createReconfigureEffect = (view: EditorView) =>\n createEffect(() => {\n view.dispatch({ effects: setBaseline.of(baseline()) });\n });\n\n return [plugin, createReconfigureEffect] as const;\n}\n\ntype ContentDiff =\n | { type: \"added\"; from: number; to: number }\n | { type: \"deleted\"; at: number; text: string };\n\n// Diff the text at `path` between the baseline heads and the live doc using\n// Automerge patches directly (no annotations layer). A string field only ever\n// produces `splice` (insert) and `del` (delete) patches on `[...path, idx]`,\n// and their positions are in live-doc coordinates -- so `added` ranges map\n// straight to CodeMirror offsets. For `del` the removed text isn't in the\n// patch, so we recover it from the baseline view, tracking a running offset to\n// convert the live position back to a baseline position.\nfunction diffContent(\n doc: Doc<unknown>,\n baselineHeads: UrlHeads,\n path: AutomergeProp[]\n): ContentDiff[] {\n const before = decodeHeads(baselineHeads);\n const beforeContent = lookupString(Automerge.view(doc, before), path);\n const patches = Automerge.diff(doc, before, Automerge.getHeads(doc));\n\n const out: ContentDiff[] = [];\n let offset = 0;\n for (const patch of patches) {\n if (!isIndexedChildOf(patch.path, path)) continue;\n const pos = patch.path[path.length] as number;\n if (patch.action === \"splice\") {\n const len = (patch.value as string).length;\n out.push({ type: \"added\", from: pos, to: pos + len });\n offset -= len;\n } else if (patch.action === \"del\") {\n const len = patch.length ?? 1;\n const original = pos + offset;\n out.push({\n type: \"deleted\",\n at: pos,\n text: beforeContent.substring(original, original + len),\n });\n offset += len;\n }\n }\n return out;\n}\n\nfunction buildDiffDecorations(\n diffs: ContentDiff[],\n dark: boolean\n): Range<Decoration>[] {\n const out: Range<Decoration>[] = [];\n for (const diff of diffs) {\n if (diff.type === \"deleted\") {\n out.push(\n Decoration.widget({\n widget: new DeletionMarker(diff.text, false),\n side: 1,\n }).range(diff.at)\n );\n continue;\n }\n // Skip zero-length added ranges.\n if (diff.from === diff.to) continue;\n out.push(\n Decoration.mark({\n attributes: { style: addedDiffStyle(dark) },\n }).range(diff.from, diff.to)\n );\n }\n return out;\n}\n\n// True when `patchPath` addresses a direct indexed child of `path`, e.g.\n// `[\"content\", 5]` is an indexed child of `[\"content\"]`.\nfunction isIndexedChildOf(\n patchPath: readonly AutomergeProp[],\n path: AutomergeProp[]\n): boolean {\n if (patchPath.length !== path.length + 1) return false;\n for (let i = 0; i < path.length; i++) {\n if (patchPath[i] !== path[i]) return false;\n }\n return typeof patchPath[path.length] === \"number\";\n}\n\nfunction lookupString(doc: unknown, path: AutomergeProp[]): string {\n let current: unknown = doc;\n for (const key of path) {\n if (current == null) return \"\";\n current = (current as Record<AutomergeProp, unknown>)[key];\n }\n return typeof current === \"string\" ? current : \"\";\n}\n\nfunction addedDiffStyle(dark: boolean): string {\n return `\n border-bottom: 2px solid ${dark ? \"#4ade80\" : \"#22c55e\"};\n background-color: ${dark ? \"#14532d\" : \"#dcfce7\"};\n `;\n}\n\nfunction prefersDarkMode(): boolean {\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n}\n\nclass DeletionMarker extends WidgetType {\n deletedText: string;\n isActive: boolean;\n\n constructor(deletedText: string, isActive: boolean) {\n super();\n this.deletedText = deletedText;\n this.isActive = isActive;\n }\n\n toDOM(): HTMLElement {\n const box = document.createElement(\"div\");\n // Pin the badge to its own metrics so a tall line-height (e.g. on heading\n // lines) can't stretch it into a vertical rectangle. `position: relative`\n // anchors the hover tooltip to the badge so it can left-align to it.\n box.style.display = \"inline-block\";\n box.style.position = \"relative\";\n box.style.lineHeight = \"1\";\n box.style.verticalAlign = \"middle\";\n box.style.boxSizing = \"border-box\";\n box.style.padding = \"2px 3px\";\n box.style.color = \"rgb(239 68 68)\"; // red-500\n box.style.margin = \"0 4px\";\n box.style.fontSize = \"1.2em\";\n box.style.whiteSpace = \"nowrap\";\n box.style.backgroundColor = this.isActive\n ? \"rgb(239 68 68 / 20%)\" // red-500 with opacity\n : \"rgb(239 68 68 / 10%)\";\n box.style.borderRadius = \"3px\";\n box.style.cursor = \"default\";\n box.innerText = \"\\u232b\";\n\n const hoverText = document.createElement(\"div\");\n hoverText.style.position = \"absolute\";\n hoverText.style.top = \"100%\";\n hoverText.style.left = \"0\";\n hoverText.style.zIndex = \"1\";\n hoverText.style.padding = \"5px\";\n hoverText.style.backgroundColor = \"rgb(254 242 242)\"; // red-50\n hoverText.style.fontSize = \"15px\";\n hoverText.style.color = \"rgb(17 24 39)\"; // gray-900\n hoverText.style.border = \"1px solid rgb(185 28 28)\"; // red-700\n hoverText.style.boxShadow = \"0px 0px 6px rgba(0, 0, 0, 0.1)\";\n hoverText.style.borderRadius = \"3px\";\n hoverText.style.visibility = \"hidden\";\n hoverText.innerText = this.deletedText;\n\n const isDarkMode =\n document.documentElement.classList.contains(\"dark\") ||\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n if (isDarkMode) {\n box.style.color = \"rgb(248 113 113)\"; // red-400 for dark mode\n box.style.backgroundColor = this.isActive\n ? \"rgb(248 113 113 / 20%)\"\n : \"rgb(248 113 113 / 10%)\";\n hoverText.style.backgroundColor = \"rgb(69 10 10)\"; // red-950\n hoverText.style.color = \"rgb(254 226 226)\"; // red-100\n hoverText.style.border = \"1px solid rgb(153 27 27)\"; // red-800\n }\n\n box.appendChild(hoverText);\n\n box.onmouseover = function () {\n hoverText.style.visibility = \"visible\";\n };\n box.onmouseout = function () {\n hoverText.style.visibility = \"hidden\";\n };\n\n return box;\n }\n\n eq(other: DeletionMarker) {\n return (\n other.deletedText === this.deletedText && other.isActive === this.isActive\n );\n }\n\n ignoreEvent() {\n return true;\n }\n}\n", "import { createEffect } from \"solid-js\";\n\n/** CodeMirror */\nimport { EditorView } from \"@codemirror/view\";\nimport { EditorSelection, type Extension } from \"@codemirror/state\";\n\n/**\n * Create a CodeMirror extension that scrolls the highlighted range into view\n * when it changes.\n *\n * Uses the \"nearest\" scroll strategy, so a highlight that's already on screen\n * is left untouched and an off-screen one is scrolled just into frame -- i.e.\n * \"scroll the highlight into view only if it isn't already there\". Handy for\n * following focus/selection driven by other views (e.g. picking a comment).\n *\n * @param target An accessor returning the [from, to] range to reveal, or null.\n * @returns A tuple of the extension and a function to create the scroll effect.\n */\nexport function createScrollHighlightIntoViewExtension(\n target: () => readonly [number, number] | null\n) {\n // Scrolling is a fire-and-forget transaction effect with no editor state to\n // hold, so there's nothing to add to the editor. The empty extension just\n // keeps the [extension, effectFactory] shape shared with the siblings.\n const extension: Extension = [];\n\n const createScrollHighlightIntoViewEffect = (view: EditorView) =>\n createEffect(() => {\n const range = target();\n if (!range) return;\n // Clamp to the live doc: a target resolved against a slightly different\n // doc revision could otherwise point past the end and throw.\n const docLength = view.state.doc.length;\n const from = Math.min(range[0], docLength);\n const to = Math.min(range[1], docLength);\n view.dispatch({\n effects: EditorView.scrollIntoView(EditorSelection.range(from, to), {\n x: \"nearest\",\n y: \"nearest\",\n }),\n });\n });\n\n return [extension, createScrollHighlightIntoViewEffect] as const;\n}\n", "import { createEffect } from \"solid-js\";\n\n/** CodeMirror */\nimport {\n Decoration,\n EditorView,\n RectangleMarker,\n ViewPlugin,\n layer,\n type DecorationSet,\n type ViewUpdate,\n} from \"@codemirror/view\";\nimport {\n Compartment,\n EditorSelection,\n StateEffect,\n StateField,\n Transaction,\n type Extension,\n type Range,\n} from \"@codemirror/state\";\n\n/** Automerge */\nimport {\n next as A,\n type Prop as AutomergeProp,\n} from \"@automerge/automerge/slim\";\nimport {\n Presence,\n type AutomergeUrl,\n type DocHandle,\n type Repo,\n} from \"@automerge/automerge-repo/slim\";\n\n// The payload broadcast to peers: just the selection, as a pair of automerge\n// cursors rather than plain offsets, so every receiver resolves it against\n// its *own* view of the document and it stays glued to the text through\n// concurrent edits. Identity rides on Presence's own `userId` field (the\n// sender's contact url); receivers use it to look up the peer's contact doc\n// and read the color chosen there.\ntype PresenceSelection = {\n start: A.Cursor;\n end: A.Cursor;\n headAtStart: boolean;\n // Sender's Date.now(); used only as a change marker (not for clock comparison)\n lastMoved: number;\n};\ntype PresenceChannels = { selection: PresenceSelection | null };\n\n// A remote peer's selection resolved to positions in the current document.\n// `peerId` is unique per session, so two tabs of the same contact each get\n// their own caret.\ntype RemoteSelection = {\n peerId: string;\n contactUrl: AutomergeUrl | null;\n color: string;\n from: number;\n to: number;\n head: number;\n // Changes exactly when the peer acts (moves, types, refocuses) -- the\n // caret marker uses it to restart its nametag fade-out countdown.\n lastMoved: number;\n};\n\n/**\n * Create a CodeMirror extension for live remote cursors, reconfigurable via a\n * Compartment (same shape as the sync/diff/readOnly extensions).\n *\n * `contactUrl` is our identity: it travels as the Presence userId, and peers\n * use it to mount our \"contact-cursor\" token and read our chosen color. It\n * returning `null` renders no presence -- that's the state before the\n * identity has loaded, and permanently so for read-only views. When the\n * accessor changes, the compartment swaps the plugin wholesale: the outgoing\n * instance stops `Presence` (sending a goodbye to peers) and the new one\n * starts fresh with the new identity.\n *\n * `repo` is used to load each peer's contact doc, where their chosen\n * presence color lives.\n */\nexport function createPresenceExtension(\n handle: () => DocHandle<unknown>,\n path: () => AutomergeProp[],\n contactUrl: () => AutomergeUrl | null,\n repo: Repo\n) {\n const compartment = new Compartment();\n\n const extension = () => {\n const url = contactUrl();\n return url ? presence(handle(), path(), url, repo) : [];\n };\n\n const createReconfigureEffect = (view: EditorView) =>\n createEffect(() => {\n view.dispatch({ effects: compartment.reconfigure(extension()) });\n });\n\n return [compartment.of(extension()), createReconfigureEffect] as const;\n}\n\n/**\n * Live remote cursors for collaborative text, handling presence transport,\n * broadcasting/resolving selections, rendering colored ranges and contact-cursor carets.\n * Peers are keyed by session (peerId); idle carets hide their nametag after a timeout.\n * Lifetime matches the editor: reconfiguring or closing stops presence.\n */\nfunction presence(\n handle: DocHandle<unknown>,\n path: AutomergeProp[],\n contactUrl: AutomergeUrl,\n repo: Repo\n): Extension {\n const setRemoteSelections = StateEffect.define<RemoteSelection[]>();\n\n const remoteSelectionField = StateField.define<RemoteSelection[]>({\n create() {\n return [];\n },\n update(value, tr) {\n // Between re-resolutions, keep positions glued through edits.\n if (tr.docChanged) {\n value = value.map((sel) => ({\n ...sel,\n from: tr.changes.mapPos(sel.from),\n to: tr.changes.mapPos(sel.to),\n head: tr.changes.mapPos(sel.head),\n }));\n }\n for (const e of tr.effects) {\n if (e.is(setRemoteSelections)) value = e.value;\n }\n return value;\n },\n // Selection tints are plain mark decorations: they carry no stateful\n // DOM, so redrawing them is invisible.\n provide: (f) => EditorView.decorations.from(f, buildSelectionTints),\n });\n\n // Carets and their nametags are rendered in a layer (not as widget decorations)\n // to prevent remounts and visible flicker. This ensures the <patchwork-view>\n // element stays mounted and updated in place, even during edits.\n const caretLayer = layer({\n above: true,\n class: \"cm-presence-layer\",\n update: (update) =>\n update.docChanged ||\n update.transactions.some((tr) =>\n tr.effects.some((e) => e.is(setRemoteSelections))\n ),\n markers(view) {\n // Stable order, so the layer's pairwise DOM reuse matches each marker\n // with the same peer's existing element across redraws.\n const selections = [...view.state.field(remoteSelectionField)].sort(\n (a, b) => (a.peerId < b.peerId ? -1 : a.peerId > b.peerId ? 1 : 0)\n );\n const markers: PresenceCaretMarker[] = [];\n for (const sel of selections) {\n // Reuse drawSelection's measurement for a caret rectangle at head.\n const [rect] = RectangleMarker.forRange(\n view,\n \"cm-presence-caret\",\n EditorSelection.cursor(sel.head)\n );\n if (!rect) continue; // outside the rendered viewport\n markers.push(\n new PresenceCaretMarker(sel, rect.left, rect.top, rect.height)\n );\n }\n return markers;\n },\n });\n\n const plugin = ViewPlugin.fromClass(\n class {\n private view: EditorView;\n private presence: Presence<PresenceChannels>;\n private destroyed = false;\n private broadcastQueued = false;\n // Peers' contact docs, followed live so a color picked in the account\n // picker recolors that peer's caret and tint immediately. Keyed by\n // contact url; the stored function stops the subscription.\n private contactWatchers = new Map<AutomergeUrl, () => void>();\n private contactColors = new Map<AutomergeUrl, string>();\n\n constructor(view: EditorView) {\n this.view = view;\n this.presence = new Presence<PresenceChannels>({\n handle,\n userId: contactUrl,\n });\n this.presence.start({ initialState: { selection: null } });\n this.presence.on(\"update\", this.refresh);\n this.presence.on(\"snapshot\", this.refresh);\n this.presence.on(\"goodbye\", this.refresh);\n this.presence.on(\"pruned\", this.refresh);\n // Closing the page sends a goodbye, which removes us from peers\n // immediately instead of after the stale-peer timeout.\n window.addEventListener(\"pagehide\", this.stopPresence);\n }\n\n update(update: ViewUpdate) {\n // Typing is considered cursor movement; local edits may not set selection explicitly, so we also check userEvent annotations\n const typed =\n update.docChanged &&\n update.transactions.some(\n (tr) => tr.annotation(Transaction.userEvent) !== undefined\n );\n if (update.focusChanged && !update.view.hasFocus) {\n // Hide our caret on peers while blurred; the peer record itself\n // stays alive via heartbeats.\n if (this.presence.running) {\n this.presence.broadcast(\"selection\", null);\n }\n } else if (\n update.selectionSet ||\n typed ||\n (update.focusChanged && update.view.hasFocus)\n ) {\n this.scheduleBroadcast();\n }\n // Local and remote edits alike arrive as transactions (the sync\n // plugin turns automerge patches into transactions), so this\n // re-resolves peers' cursors against the updated doc. Deferred:\n // dispatching is not allowed from inside update().\n if (update.docChanged) {\n queueMicrotask(this.refresh);\n }\n }\n\n destroy() {\n this.destroyed = true;\n this.stopPresence();\n window.removeEventListener(\"pagehide\", this.stopPresence);\n for (const stop of this.contactWatchers.values()) stop();\n this.contactWatchers.clear();\n }\n\n // A peer's color is whatever their account says (contact doc `color`),\n // never derived locally. The doc is fetched once and followed; until it\n // loads (or if the contact never picked a color) the caret stays on the\n // neutral fallback.\n private trackContactColor(url: AutomergeUrl) {\n if (this.contactWatchers.has(url)) return;\n this.contactWatchers.set(url, () => {});\n repo\n .find<ContactDoc>(url)\n .then((contactHandle) => {\n if (this.destroyed) return;\n const read = () => {\n const color = contactHandle.doc()?.color;\n if (!color || this.contactColors.get(url) === color) return;\n this.contactColors.set(url, color);\n this.refresh();\n };\n contactHandle.on(\"change\", read);\n this.contactWatchers.set(url, () =>\n contactHandle.off(\"change\", read)\n );\n read();\n })\n .catch(() => {\n // Contact doc unavailable: the peer stays on the fallback color.\n });\n }\n\n private stopPresence = () => {\n if (this.presence.running) this.presence.stop();\n };\n\n // rAF-throttled (matching tldraw4) so drag-selections don't flood the\n // channel. The selection is read fresh at flush time.\n private scheduleBroadcast() {\n if (this.broadcastQueued) return;\n this.broadcastQueued = true;\n requestAnimationFrame(() => {\n this.broadcastQueued = false;\n if (this.destroyed || !this.presence.running) return;\n // Blurred in the meantime: the null broadcast already went out.\n if (!this.view.hasFocus) return;\n const doc = handle.doc();\n const sel = this.view.state.selection.main;\n this.presence.broadcast(\"selection\", {\n start: A.getCursor(doc, path, sel.from),\n end: A.getCursor(doc, path, sel.to),\n headAtStart: sel.to > sel.from && sel.head === sel.from,\n lastMoved: Date.now(),\n });\n });\n }\n\n // Re-resolve every peer's cursors against the current doc. A cursor can\n // briefly fail to resolve when a presence message races ahead of doc\n // sync -- the peer is skipped and picked up again on the next change.\n private refresh = () => {\n if (this.destroyed) return;\n const doc = handle.doc();\n const resolved: RemoteSelection[] = [];\n for (const peer of this.presence.getPeerStates().peers) {\n const peerId = String(peer.peerId);\n const selection = peer.value?.selection ?? null;\n if (!selection) continue;\n try {\n const from = A.getCursorPosition(doc, path, selection.start);\n const to = A.getCursorPosition(doc, path, selection.end);\n const peerContactUrl =\n (peer.userId as AutomergeUrl | undefined) ?? null;\n if (peerContactUrl) this.trackContactColor(peerContactUrl);\n resolved.push({\n peerId,\n contactUrl: peerContactUrl,\n // The color the peer chose in their account -- the same contact\n // doc the contact-cursor token renders from, so the tint and\n // the token can't drift apart.\n color:\n (peerContactUrl && this.contactColors.get(peerContactUrl)) ||\n FALLBACK_PRESENCE_COLOR,\n from,\n to,\n head: selection.headAtStart ? from : to,\n lastMoved: selection.lastMoved,\n });\n } catch {\n continue;\n }\n }\n this.view.dispatch({ effects: setRemoteSelections.of(resolved) });\n };\n }\n );\n\n return [remoteSelectionField, caretLayer, plugin, presenceTheme];\n}\n\nfunction buildSelectionTints(selections: RemoteSelection[]): DecorationSet {\n const ranges: Range<Decoration>[] = [];\n for (const sel of selections) {\n if (sel.from < sel.to) {\n ranges.push(\n Decoration.mark({\n class: \"cm-presence-range\",\n attributes: {\n style: `background-color: color-mix(in srgb, ${sel.color} 25%, transparent);`,\n },\n }).range(sel.from, sel.to)\n );\n }\n }\n return Decoration.set(ranges, true);\n}\n\n// A peer's nametag fades out once its selection has been still for this\n// long -- the caret itself stays visible. Moving the cursor or typing\n// brings it back.\nconst LABEL_TIMEOUT_MS = 2_000;\n\n// A layer marker for one peer's caret. Markers are throwaway value objects\n// (a fresh set arrives with every measure pass); the peer's DOM element is\n// what persists, via `update` mutating it in place. That persistence is the\n// point of the layer approach: the contact token inside must never remount.\nclass PresenceCaretMarker {\n private readonly sel: RemoteSelection;\n private readonly left: number;\n private readonly top: number;\n private readonly height: number;\n\n constructor(sel: RemoteSelection, left: number, top: number, height: number) {\n this.sel = sel;\n this.left = left;\n this.top = top;\n this.height = height;\n }\n\n eq(other: PresenceCaretMarker): boolean {\n return (\n other.sel.peerId === this.sel.peerId &&\n other.sel.contactUrl === this.sel.contactUrl &&\n other.sel.color === this.sel.color &&\n other.sel.lastMoved === this.sel.lastMoved &&\n other.left === this.left &&\n other.top === this.top &&\n other.height === this.height\n );\n }\n\n draw(): HTMLElement {\n const caret = document.createElement(\"div\");\n caret.className = \"cm-presence-caret\";\n caret.dataset.peerId = this.sel.peerId;\n if (this.sel.contactUrl) {\n const label = document.createElement(\"span\");\n label.className = \"cm-presence-caret-label\";\n const token = document.createElement(\"patchwork-view\");\n token.setAttribute(\"doc-url\", this.sel.contactUrl);\n token.setAttribute(\"tool-id\", \"contact-cursor\");\n token.style.display = \"block\";\n label.appendChild(token);\n caret.appendChild(label);\n }\n this.adjust(caret);\n restartIdleCountdown(caret);\n return caret;\n }\n\n // Reposition the peer's existing element instead of redrawing it, keeping\n // the mounted contact token alive (a remount re-renders asynchronously and\n // flickers -- see the layer comment in `presence`).\n update(dom: HTMLElement, prev: PresenceCaretMarker): boolean {\n // A different peer means a different contact token, which genuinely has\n // to remount. Only happens when peers join or leave.\n if (\n prev.sel.peerId !== this.sel.peerId ||\n prev.sel.contactUrl !== this.sel.contactUrl\n ) {\n return false;\n }\n this.adjust(dom);\n // Fresh activity restarts the nametag fade-out. Position shifts caused\n // by other people's edits deliberately don't.\n if (prev.sel.lastMoved !== this.sel.lastMoved) restartIdleCountdown(dom);\n return true;\n }\n\n private adjust(dom: HTMLElement) {\n dom.style.left = `${this.left}px`;\n dom.style.top = `${this.top}px`;\n dom.style.height = `${this.height}px`;\n dom.style.setProperty(\"--cm-presence-color\", this.sel.color);\n }\n}\n\n// Keyed on the DOM element rather than the marker, because marker instances\n// are replaced on every measure pass while the element lives on.\nconst idleTimers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();\n\nfunction restartIdleCountdown(caret: HTMLElement) {\n clearTimeout(idleTimers.get(caret));\n caret.classList.remove(\"cm-presence-caret--idle\");\n idleTimers.set(\n caret,\n setTimeout(\n () => caret.classList.add(\"cm-presence-caret--idle\"),\n LABEL_TIMEOUT_MS\n )\n );\n}\n\nconst presenceTheme = EditorView.baseTheme({\n // Positioned absolutely by the layer (document-relative); -1px centers\n // the 2px bar on the caret position.\n \".cm-presence-caret\": {\n borderLeft: \"2px solid var(--cm-presence-color)\",\n marginLeft: \"-1px\",\n pointerEvents: \"none\",\n },\n \".cm-presence-caret-label\": {\n position: \"absolute\",\n top: \"-1.4em\",\n left: \"-2px\",\n zIndex: \"150\",\n whiteSpace: \"nowrap\",\n pointerEvents: \"none\",\n userSelect: \"none\",\n opacity: \"1\",\n transition: \"opacity 200ms ease-in-out\",\n },\n \".cm-presence-caret--idle .cm-presence-caret-label\": {\n opacity: \"0\",\n },\n});\n\n// Shown until a peer's contact doc has loaded, and for contacts that never\n// picked a color. Matches the contact-cursor token's CSS fallback\n// (`var(--contact-cursor-color, #888)` in the account package).\nconst FALLBACK_PRESENCE_COLOR = \"#888\";\n\n// The subset of the account package's ContactDoc this extension reads.\ntype ContactDoc = { color?: string };\n"],
|
|
4
|
+
"sourcesContent": ["import { onCleanup, createEffect } from \"solid-js\";\nimport { EditorView } from \"@codemirror/view\";\nimport { EditorState, Compartment } from \"@codemirror/state\";\nimport { createAutomergeExtension, createReadOnlyExtension, createDecorationsExtension, createDiffExtension, createScrollHighlightIntoViewExtension, createPresenceExtension } from \"./extensions\";\nconst lookup = (doc, path) => {\n let current = doc;\n for (const key of path) {\n current = current[key];\n if (current === void 0) {\n return void 0;\n }\n }\n return current;\n};\nexport function CodeMirror(props) {\n const initialDoc = () => props.handle && lookup(props.handle.doc(), props.path) || \"\";\n const [automergeExtension, createEffectReconfigureAutomerge] = createAutomergeExtension(() => props.handle, () => props.path, initialDoc);\n const [readOnlyExtension, createEffectReconfigureReadOnly] = createReadOnlyExtension(() => !!props.readOnly);\n const [decorationsExtension, createEffectReconfigureDecorations] = createDecorationsExtension(() => props.decorations?.());\n const [diffExtension, createEffectReconfigureDiff] = createDiffExtension(() => props.handle, () => props.path, () => props.baseline?.() ?? null);\n const [scrollHighlightIntoViewExtension, createEffectScrollHighlightIntoView] = createScrollHighlightIntoViewExtension(() => props.scrollTarget?.() ?? null);\n const [presenceExtension, createEffectReconfigurePresence] = createPresenceExtension(() => props.handle, () => props.path, () => props.readOnly || props.handle.isReadOnly() ? null : props.contactUrl?.() ?? null, props.repo);\n const userExtensionsCompartment = new Compartment();\n const selectionExtension = EditorView.updateListener.of(update => {\n if (!props.onChangeSelection) return;\n if (update.selectionSet) {\n const sel = update.state.selection.main;\n props.onChangeSelection(sel.from, sel.to);\n }\n });\n const extensions = [selectionExtension, decorationsExtension,\n // automergeExtension must come before diffExtension so diff stays in sync with edits.\n automergeExtension, diffExtension, scrollHighlightIntoViewExtension, presenceExtension, userExtensionsCompartment.of(props.extensions || []), readOnlyExtension].filter(Boolean);\n const state = EditorState.create({\n doc: initialDoc(),\n extensions\n });\n const view = new EditorView({\n state\n });\n props.withView?.(view);\n createEffectReconfigureAutomerge(view);\n createEffectReconfigureReadOnly(view);\n createEffectReconfigureDecorations?.(view);\n createEffectReconfigureDiff(view);\n createEffectScrollHighlightIntoView(view);\n createEffectReconfigurePresence(view);\n createEffect(() => {\n view.dispatch({\n effects: userExtensionsCompartment.reconfigure(props.extensions || [])\n });\n });\n onCleanup(() => view.destroy());\n return view.dom;\n}", "import { createEffect } from \"solid-js\";\n\n/** CodeMirror */\nimport { Decoration, EditorView, type DecorationSet } from \"@codemirror/view\";\nimport { StateEffect, StateField } from \"@codemirror/state\";\n\n/**\n * Create a CodeMirror extension for managing decorations.\n * @param decorations An accessor function that returns the decorations (for reactivity).\n * @returns A tuple containing the extension and a function to create an effect for updating the\n decorations.\n */\nexport function createDecorationsExtension(decorations: () => DecorationSet) {\n const setDecorations = StateEffect.define<DecorationSet>();\n const decorationsField = StateField.define<DecorationSet>({\n create() {\n return Decoration.none;\n },\n update(value, tr) {\n for (const e of tr.effects) {\n if (e.is(setDecorations)) return e.value;\n }\n if (tr.docChanged) return value.map(tr.changes);\n return value;\n },\n provide: (f) => EditorView.decorations.from(f),\n });\n\n const createDecorationsEffect = (view: EditorView) =>\n createEffect(() => {\n decorations() &&\n view.dispatch({\n effects: setDecorations.of(decorations!()),\n });\n });\n\n return [decorationsField, createDecorationsEffect] as const;\n}\n", "import { createEffect } from \"solid-js\";\n\n/** CodeMirror */\nimport { EditorView } from \"@codemirror/view\";\nimport { Compartment, EditorState } from \"@codemirror/state\";\n\n/**\n * Force the editor read-only regardless of the handle's state.\n *\n * This is only the override: the handle's own read-only state (e.g. a\n * heads-pinned view, including flips when its backing is swapped in place)\n * is tracked by the vendored `automergeReadOnly`, installed via\n * `createAutomergeExtension`.\n *\n * @param force Forces read-only.\n * @returns A tuple containing the extension and a function to create the\n * effect that reconfigures the extension when `force` changes.\n */\nexport function createReadOnlyExtension(force: () => boolean) {\n const compartment = new Compartment();\n\n const extensions = () =>\n force()\n ? [EditorState.readOnly.of(true), EditorView.editable.of(false)]\n : [];\n\n const createReconfigureEffect = (view: EditorView) =>\n createEffect(() => {\n view.dispatch({\n effects: compartment.reconfigure(extensions()),\n });\n });\n\n return [compartment.of(extensions()), createReconfigureEffect] as const;\n}\n", "import { createEffect } from \"solid-js\";\n\n/** CodeMirror */\nimport { EditorView } from \"@codemirror/view\";\nimport { Compartment, Transaction } from \"@codemirror/state\";\n\n/** Automerge */\nimport type { Prop as AutomergeProp } from \"@automerge/automerge/slim\";\nimport type { DocHandle } from \"@automerge/automerge-repo/slim\";\nimport { automergePlugin } from \"../../vendor/automerge-codemirror/index.js\";\n\n/**\n * Returns a [Compartment extension, reconfig effect] tuple to sync a CodeMirror editor\n * with an Automerge handle+path. When reactive `handle` or `path` change, swaps the\n * plugin via Compartment and resets the doc from `initialDoc()` (kept off undo stack).\n */\nexport function createAutomergeExtension<T>(\n handle: () => DocHandle<T>,\n path: () => AutomergeProp[],\n initialDoc: () => string\n) {\n const compartment = new Compartment();\n\n const extension = () =>\n handle() && path()\n ? automergePlugin({\n handle: handle() as any, // typescript is confused by different version of doc handle\n path: path(),\n })\n : [];\n\n // Reconfigures the plugin and resets the doc to initialDoc() on reactive changes; this is synthetic remote state, so it bypasses the undo stack.\n const createReconfigureEffect = (view: EditorView) =>\n createEffect(() => {\n // handle()/path()/initialDoc() are tracked; changes rerun the rebuild.\n view.dispatch({\n effects: compartment.reconfigure(extension()),\n changes: {\n from: 0,\n to: view.state.doc.length,\n insert: initialDoc(),\n },\n annotations: [\n Transaction.addToHistory.of(false),\n Transaction.remote.of(true),\n ],\n });\n });\n\n return [compartment.of(extension()), createReconfigureEffect] as const;\n}\n", "import * as A from \"@automerge/automerge\"\nimport { EditorView, ViewPlugin, ViewUpdate } from \"@codemirror/view\"\nimport { Transaction, Annotation } from \"@codemirror/state\"\nimport { applyCmTransactionsToAmHandle } from \"./codeMirrorToAm.js\"\nimport { applyAmPatchesToCm } from \"./amToCodemirror.js\"\nimport type { DocHandle } from \"./DocHandle.js\"\n\nexport const reconcileAnnotationType = Annotation.define<unknown>()\n\nexport const isReconcileTx = (tr: Transaction): boolean =>\n !!tr.annotation(reconcileAnnotationType)\n\ntype AutomergeSyncPluginConfig = {\n handle: DocHandle<unknown>\n path: A.Prop[]\n}\n\nexport const automergeSyncPlugin = ({\n handle,\n path,\n}: AutomergeSyncPluginConfig) => {\n if (!handle.isReady()) {\n throw new Error(\n \"ensure the handle is ready before initializing the automergeSyncPlugin\"\n )\n }\n\n return ViewPlugin.fromClass(\n class {\n view: EditorView\n reconciledHeads = A.getHeads(handle.doc())\n isProcessingCmTransaction = false\n\n constructor(view: EditorView) {\n this.view = view\n\n this.onChange = this.onChange.bind(this)\n handle.on(\"change\", this.onChange)\n }\n\n update(update: ViewUpdate) {\n // start processing codemirror transaction\n // changes that are created through the transaction are ignored in the change listener on the handle\n this.isProcessingCmTransaction = true\n\n const newHeads = applyCmTransactionsToAmHandle(\n handle,\n path,\n update.transactions as Transaction[]\n )\n\n if (newHeads) {\n this.reconciledHeads = newHeads\n }\n\n // finish processing transaction\n this.isProcessingCmTransaction = false\n }\n\n // Changes with a replaced scope (the handle was re-pointed at a\n // different backing document) need no special-casing here. A.diff\n // ignores heads that aren't in the doc's history, treating that side\n // as the empty document: when the backings share history (heads\n // pinning, clones) it yields minimal patches that preserve the\n // selection, and when they share nothing it degenerates into\n // full-document patches that rebuild the editor wholesale. Either\n // way the patches are kept off the undo stack by applyAmPatchesToCm.\n // The history and read-only extensions are the ones that consume the\n // `scopeReplaced` flag.\n onChange = () => {\n // ignore changes that where triggered while processing a codemirror transaction\n if (this.isProcessingCmTransaction) {\n return\n }\n\n const currentHeads = A.getHeads(handle.doc())\n if (A.equals(currentHeads, this.reconciledHeads)) {\n return\n }\n\n // get the diff between the reconciled heads and the new heads\n // and apply that to the codemirror doc\n const patches = A.diff(handle.doc(), this.reconciledHeads, currentHeads)\n applyAmPatchesToCm(this.view, path, patches)\n this.reconciledHeads = currentHeads\n }\n\n destroy() {\n handle.off(\"change\", this.onChange)\n }\n }\n )\n}\n", "import * as A from \"@automerge/automerge\"\nimport { Text, Transaction } from \"@codemirror/state\"\nimport { isReconcileTx } from \"./plugin.js\"\nimport { type DocHandle } from \"./DocHandle.js\"\n\nexport const applyCmTransactionsToAmHandle = (\n handle: DocHandle<unknown>,\n path: A.Prop[],\n transactions: Transaction[]\n): A.Heads | undefined => {\n const transactionsWithChanges = transactions.filter(\n tr => !isReconcileTx(tr) && !tr.changes.empty\n )\n\n if (transactionsWithChanges.length === 0) {\n return\n }\n\n // A read-only handle (e.g. pinned to historical heads) rejects writes:\n // never forward editor changes to it. The automergeReadOnly extension\n // keeps the editor from producing such changes in the first place.\n if (handle.isReadOnly?.()) {\n return\n }\n\n handle.change((doc: A.Doc<unknown>) => {\n transactionsWithChanges.forEach(tr => {\n tr.changes.iterChanges(\n (\n fromA: number,\n toA: number,\n fromB: number,\n _toB: number,\n inserted: Text\n ) => {\n // We are cloning the path as `am.splice` calls `.unshift` on it, modifying it in place,\n // causing the path to be broken on subsequent changes\n A.splice(doc, path.slice(), fromB, toA - fromA, inserted.toString())\n }\n )\n })\n })\n\n return A.getHeads(handle.doc())\n}\n", "import type {\n DelPatch,\n InsertPatch,\n Patch,\n Prop,\n PutPatch,\n SpliceTextPatch,\n} from \"@automerge/automerge\"\nimport {\n ChangeSet,\n type ChangeSpec,\n EditorState,\n Transaction,\n} from \"@codemirror/state\"\nimport { EditorView } from \"@codemirror/view\"\nimport { reconcileAnnotationType } from \"./plugin.js\"\n\nexport const applyAmPatchesToCm = (\n view: EditorView,\n target: Prop[],\n patches: Patch[]\n) => {\n let selection = view.state.selection\n\n for (const patch of patches) {\n const changeSpec = handlePatch(patch, target, view.state)\n if (changeSpec != null) {\n const changeSet = ChangeSet.of(changeSpec, view.state.doc.length, \"\\n\")\n selection = selection.map(changeSet, 1)\n // Remote changes from automerge are excluded from local undo (addToHistory: false).\n view.dispatch({\n changes: changeSet,\n annotations: [\n reconcileAnnotationType.of({}),\n Transaction.addToHistory.of(false),\n Transaction.remote.of(true),\n ],\n })\n }\n }\n view.dispatch({\n selection,\n annotations: reconcileAnnotationType.of({}),\n })\n}\n\nfunction handlePatch(\n patch: Patch,\n target: Prop[],\n state: EditorState\n): ChangeSpec | null {\n if (patch.action === \"insert\") {\n return handleInsert(target, patch)\n } else if (patch.action === \"splice\") {\n return handleSplice(target, patch)\n } else if (patch.action === \"del\") {\n return handleDel(target, patch)\n } else if (patch.action === \"put\") {\n return handlePut(target, patch, state)\n } else {\n return null\n }\n}\n\nfunction handleInsert(target: Prop[], patch: InsertPatch): Array<ChangeSpec> {\n const index = charPath(target, patch.path)\n if (index == null) {\n return []\n }\n const text = patch.values.map(v => (v ? v.toString() : \"\")).join(\"\")\n return [{ from: index, to: index, insert: text }]\n}\n\nfunction handleSplice(\n target: Prop[],\n patch: SpliceTextPatch\n): Array<ChangeSpec> {\n const index = charPath(target, patch.path)\n if (index == null) {\n return []\n }\n return [{ from: index, insert: patch.value }]\n}\n\nfunction handleDel(target: Prop[], patch: DelPatch): Array<ChangeSpec> {\n const index = charPath(target, patch.path)\n if (index == null) {\n return []\n }\n const length = patch.length || 1\n return [{ from: index, to: index + length }]\n}\n\nfunction handlePut(\n target: Prop[],\n patch: PutPatch,\n state: EditorState\n): Array<ChangeSpec> {\n const index = charPath(target, [...patch.path, 0])\n if (index == null) {\n return []\n }\n const length = state.doc.length\n if (typeof patch.value !== \"string\") {\n return [] // TODO(dmaretskyi): How to handle non string values?\n }\n return [{ from: 0, to: length, insert: patch.value }]\n}\n\n// If the path of the patch is of the form [path, <index>] then we know this is\n// a path to a character within the sequence given by path\nfunction charPath(textPath: Prop[], candidatePath: Prop[]): number | null {\n if (candidatePath.length !== textPath.length + 1) return null\n for (let i = 0; i < textPath.length; i++) {\n if (textPath[i] !== candidatePath[i]) return null\n }\n const index = candidatePath[candidatePath.length - 1]\n if (typeof index === \"number\") return index\n return null\n}\n", "import { history, historyField, historyKeymap } from \"@codemirror/commands\"\nimport { Compartment, type Extension } from \"@codemirror/state\"\nimport { EditorView, ViewPlugin, keymap } from \"@codemirror/view\"\nimport type { DocHandle, DocHandleChangePayload } from \"./DocHandle.js\"\n\ntype AutomergeHistoryConfig = {\n handle: DocHandle<unknown>\n}\n\n/**\n * Undo/redo history that understands handles whose backing document can be\n * swapped in place: the stack is reset whenever the handle emits a `change`\n * with `scopeReplaced: true`, because the old entries describe a different\n * timeline and undo must only revert what the user has done since.\n *\n * Don't add another `history()` to the editor: the underlying history state\n * field is a module-level singleton in `@codemirror/commands`, so a second copy\n * elsewhere in the configuration keeps the field alive across the reset and defeats it.\n * If the user configures history on their own we detect this and warned about it at mount.\n */\nexport const automergeHistory = ({\n handle,\n}: AutomergeHistoryConfig): Extension => {\n const compartment = new Compartment()\n const historyExtension = () => [history(), keymap.of(historyKeymap)]\n\n const resetPlugin = ViewPlugin.fromClass(\n class {\n warned = false\n destroyed = false\n readonly view: EditorView\n\n constructor(view: EditorView) {\n this.view = view\n this.onChange = this.onChange.bind(this)\n handle.on(\"change\", this.onChange)\n // Probe once at mount, when the history is empty and cycling is\n // harmless: a duplicate history() warns immediately instead of on\n // the first swap. Deferred because dispatching isn't allowed while\n // the view is still initializing.\n queueMicrotask(() => {\n if (!this.destroyed) this.reset()\n })\n }\n\n onChange(payload?: DocHandleChangePayload) {\n if (payload?.scopeReplaced) this.reset()\n }\n\n // CodeMirror has no clear-history API; the sanctioned reset is cycling\n // the extension out of and back into the configuration, which drops\n // the history field's state and re-creates it empty.\n reset() {\n this.view.dispatch({ effects: compartment.reconfigure([]) })\n // With our copy out of the configuration the field must be gone; if\n // it survives, another history() is keeping it alive and the reset\n // is silently defeated.\n if (\n !this.warned &&\n this.view.state.field(historyField, false) !== undefined\n ) {\n this.warned = true\n console.warn(\n \"[automerge-codemirror] another history() extension is present\" +\n \" in this editor's configuration, so the undo history cannot\" +\n \" be reset when the handle's backing is swapped. Remove the\" +\n \" other copy \u2014 note that basicSetup includes one.\"\n )\n }\n this.view.dispatch({\n effects: compartment.reconfigure(historyExtension()),\n })\n }\n\n destroy() {\n this.destroyed = true\n handle.off(\"change\", this.onChange)\n }\n }\n )\n\n return [compartment.of(historyExtension()), resetPlugin]\n}\n", "import { Compartment, EditorState, type Extension } from \"@codemirror/state\"\nimport { EditorView, ViewPlugin } from \"@codemirror/view\"\nimport type { DocHandle, DocHandleChangePayload } from \"./DocHandle.js\"\n\ntype AutomergeReadOnlyConfig = {\n handle: DocHandle<unknown>\n}\n\n/**\n * Makes the editor read-only while the handle rejects writes\n * (`handle.isReadOnly()`, e.g. pinned to historical heads). This is a\n * correctness requirement of syncing, not a preference: an editable editor\n * bound to a read-only handle produces edits the write-back must drop.\n *\n * Tracked live: the flag can flip in place when the handle's backing is\n * swapped (a `change` event with `scopeReplaced: true`). Handles without\n * `isReadOnly` are treated as always writable.\n */\nexport const automergeReadOnly = ({\n handle,\n}: AutomergeReadOnlyConfig): Extension => {\n const compartment = new Compartment()\n const isReadOnly = () => !!handle.isReadOnly?.()\n const extensions = () =>\n isReadOnly()\n ? [EditorState.readOnly.of(true), EditorView.editable.of(false)]\n : []\n\n const trackPlugin = ViewPlugin.fromClass(\n class {\n wasPrevReadOnly = isReadOnly()\n destroyed = false\n readonly view: EditorView\n\n constructor(view: EditorView) {\n this.view = view\n this.onChange = this.onChange.bind(this)\n handle.on(\"change\", this.onChange)\n // The flag may have flipped between extension creation and mount;\n // deferred because dispatching isn't allowed while the view is\n // still initializing.\n queueMicrotask(() => {\n if (!this.destroyed) this.sync()\n })\n }\n\n onChange(payload?: DocHandleChangePayload) {\n if (payload?.scopeReplaced) this.sync()\n }\n\n sync() {\n const current = isReadOnly()\n if (current === this.wasPrevReadOnly) return\n this.wasPrevReadOnly = current\n this.view.dispatch({ effects: compartment.reconfigure(extensions()) })\n }\n\n destroy() {\n this.destroyed = true\n handle.off(\"change\", this.onChange)\n }\n }\n )\n\n return [compartment.of(extensions()), trackPlugin]\n}\n", "import type * as A from \"@automerge/automerge\"\nimport type { Extension } from \"@codemirror/state\"\nimport { automergeSyncPlugin } from \"./plugin.js\"\nimport { automergeHistory } from \"./history.js\"\nimport { automergeReadOnly } from \"./readOnly.js\"\nimport type { DocHandle } from \"./DocHandle.js\"\n\ntype AutomergePluginConfig = {\n handle: DocHandle<unknown>\n path: A.Prop[]\n}\n\n/**\n * The recommended entry point: two-way sync plus the pieces entangled with\n * it \u2014 undo history that resets when the handle's backing is swapped, and\n * read-only tracking for handles that reject writes.\n *\n * Don't add another `history()` to the editor (`basicSetup` includes one):\n * the bundled history must own the only copy for its reset to work. Each\n * piece is also exported individually for \u00E0-la-carte use.\n */\nexport const automergePlugin = ({\n handle,\n path,\n}: AutomergePluginConfig): Extension => [\n automergeSyncPlugin({ handle, path }),\n automergeHistory({ handle }),\n automergeReadOnly({ handle }),\n]\n\nexport { automergeSyncPlugin } from \"./plugin.js\"\nexport { automergeHistory } from \"./history.js\"\nexport { automergeReadOnly } from \"./readOnly.js\"\nexport type { DocHandle, DocHandleChangePayload } from \"./DocHandle.js\"\n", "import { createEffect } from \"solid-js\";\n\n/** CodeMirror */\nimport {\n Decoration,\n EditorView,\n ViewPlugin,\n WidgetType,\n type DecorationSet,\n type ViewUpdate,\n} from \"@codemirror/view\";\nimport { RangeSet, StateEffect, type Range } from \"@codemirror/state\";\n\n/** Automerge */\nimport { next as Automerge } from \"@automerge/automerge/slim\";\nimport type { Prop as AutomergeProp } from \"@automerge/automerge/slim\";\nimport {\n decodeHeads,\n type Doc,\n type DocHandle,\n type UrlHeads,\n} from \"@automerge/automerge-repo/slim\";\n\n// Carries the diff baseline (fork-point heads) from the provider subscription\n// into the editor. Dispatched only when the baseline changes (i.e. on fork),\n// which is rare -- per-edit recomputation is driven by `docChanged`, not this.\nconst setBaseline = StateEffect.define<UrlHeads | null>();\n\n/**\n * CodeMirror extension that renders a diff of the synced `path` against the\n * `baseline` heads. It recomputes its decorations on every doc change -- the\n * automerge sync plugin reflects both local and remote changes as editor\n * transactions -- so no external \"tick\" is needed. The baseline is pushed in\n * via a Solid effect because the editor can't observe a fork on its own.\n *\n * @returns the extension and an effect factory that feeds baseline changes in.\n */\nexport function createDiffExtension(\n handle: () => DocHandle<unknown>,\n path: () => AutomergeProp[],\n baseline: () => UrlHeads | null\n) {\n const plugin = ViewPlugin.fromClass(\n class {\n baseline: UrlHeads | null = null;\n decorations: DecorationSet = Decoration.none;\n\n update(update: ViewUpdate) {\n let baselineChanged = false;\n for (const tr of update.transactions) {\n for (const effect of tr.effects) {\n if (effect.is(setBaseline)) {\n this.baseline = effect.value;\n baselineChanged = true;\n }\n }\n }\n if (update.docChanged || baselineChanged) {\n this.decorations = this.build();\n }\n }\n\n build(): DecorationSet {\n const heads = this.baseline;\n const doc = handle()?.doc();\n if (!heads || !doc) return Decoration.none;\n return RangeSet.of(\n buildDiffDecorations(\n diffContent(doc, heads, path()),\n prefersDarkMode()\n ),\n true // sort ranges\n );\n }\n },\n { decorations: (v) => v.decorations }\n );\n\n const createReconfigureEffect = (view: EditorView) =>\n createEffect(() => {\n view.dispatch({ effects: setBaseline.of(baseline()) });\n });\n\n return [plugin, createReconfigureEffect] as const;\n}\n\ntype ContentDiff =\n | { type: \"added\"; from: number; to: number }\n | { type: \"deleted\"; at: number; text: string };\n\n// Diff the text at `path` between the baseline heads and the live doc using\n// Automerge patches directly (no annotations layer). A string field only ever\n// produces `splice` (insert) and `del` (delete) patches on `[...path, idx]`,\n// and their positions are in live-doc coordinates -- so `added` ranges map\n// straight to CodeMirror offsets. For `del` the removed text isn't in the\n// patch, so we recover it from the baseline view, tracking a running offset to\n// convert the live position back to a baseline position.\nfunction diffContent(\n doc: Doc<unknown>,\n baselineHeads: UrlHeads,\n path: AutomergeProp[]\n): ContentDiff[] {\n const before = decodeHeads(baselineHeads);\n const beforeContent = lookupString(Automerge.view(doc, before), path);\n const patches = Automerge.diff(doc, before, Automerge.getHeads(doc));\n\n const out: ContentDiff[] = [];\n let offset = 0;\n for (const patch of patches) {\n if (!isIndexedChildOf(patch.path, path)) continue;\n const pos = patch.path[path.length] as number;\n if (patch.action === \"splice\") {\n const len = (patch.value as string).length;\n out.push({ type: \"added\", from: pos, to: pos + len });\n offset -= len;\n } else if (patch.action === \"del\") {\n const len = patch.length ?? 1;\n const original = pos + offset;\n out.push({\n type: \"deleted\",\n at: pos,\n text: beforeContent.substring(original, original + len),\n });\n offset += len;\n }\n }\n return out;\n}\n\nfunction buildDiffDecorations(\n diffs: ContentDiff[],\n dark: boolean\n): Range<Decoration>[] {\n const out: Range<Decoration>[] = [];\n for (const diff of diffs) {\n if (diff.type === \"deleted\") {\n out.push(\n Decoration.widget({\n widget: new DeletionMarker(diff.text, false),\n side: 1,\n }).range(diff.at)\n );\n continue;\n }\n // Skip zero-length added ranges.\n if (diff.from === diff.to) continue;\n out.push(\n Decoration.mark({\n attributes: { style: addedDiffStyle(dark) },\n }).range(diff.from, diff.to)\n );\n }\n return out;\n}\n\n// True when `patchPath` addresses a direct indexed child of `path`, e.g.\n// `[\"content\", 5]` is an indexed child of `[\"content\"]`.\nfunction isIndexedChildOf(\n patchPath: readonly AutomergeProp[],\n path: AutomergeProp[]\n): boolean {\n if (patchPath.length !== path.length + 1) return false;\n for (let i = 0; i < path.length; i++) {\n if (patchPath[i] !== path[i]) return false;\n }\n return typeof patchPath[path.length] === \"number\";\n}\n\nfunction lookupString(doc: unknown, path: AutomergeProp[]): string {\n let current: unknown = doc;\n for (const key of path) {\n if (current == null) return \"\";\n current = (current as Record<AutomergeProp, unknown>)[key];\n }\n return typeof current === \"string\" ? current : \"\";\n}\n\nfunction addedDiffStyle(dark: boolean): string {\n return `\n border-bottom: 2px solid ${dark ? \"#4ade80\" : \"#22c55e\"};\n background-color: ${dark ? \"#14532d\" : \"#dcfce7\"};\n `;\n}\n\nfunction prefersDarkMode(): boolean {\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n}\n\nclass DeletionMarker extends WidgetType {\n deletedText: string;\n isActive: boolean;\n\n constructor(deletedText: string, isActive: boolean) {\n super();\n this.deletedText = deletedText;\n this.isActive = isActive;\n }\n\n toDOM(): HTMLElement {\n const box = document.createElement(\"div\");\n // Pin the badge to its own metrics so a tall line-height (e.g. on heading\n // lines) can't stretch it into a vertical rectangle. `position: relative`\n // anchors the hover tooltip to the badge so it can left-align to it.\n box.style.display = \"inline-block\";\n box.style.position = \"relative\";\n box.style.lineHeight = \"1\";\n box.style.verticalAlign = \"middle\";\n box.style.boxSizing = \"border-box\";\n box.style.padding = \"2px 3px\";\n box.style.color = \"rgb(239 68 68)\"; // red-500\n box.style.margin = \"0 4px\";\n box.style.fontSize = \"1.2em\";\n box.style.whiteSpace = \"nowrap\";\n box.style.backgroundColor = this.isActive\n ? \"rgb(239 68 68 / 20%)\" // red-500 with opacity\n : \"rgb(239 68 68 / 10%)\";\n box.style.borderRadius = \"3px\";\n box.style.cursor = \"var(--studio-cursor-default, default)\";\n box.innerText = \"\\u232b\";\n\n const hoverText = document.createElement(\"div\");\n hoverText.style.position = \"absolute\";\n hoverText.style.top = \"100%\";\n hoverText.style.left = \"0\";\n hoverText.style.zIndex = \"1\";\n hoverText.style.padding = \"5px\";\n hoverText.style.backgroundColor = \"rgb(254 242 242)\"; // red-50\n hoverText.style.fontSize = \"15px\";\n hoverText.style.color = \"rgb(17 24 39)\"; // gray-900\n hoverText.style.border = \"var(--studio-border-width, 1px) var(--studio-border-style, solid) rgb(185 28 28)\"; // red-700\n hoverText.style.boxShadow = \"0px 0px 6px rgba(0, 0, 0, 0.1)\";\n hoverText.style.borderRadius = \"3px\";\n hoverText.style.visibility = \"hidden\";\n hoverText.innerText = this.deletedText;\n\n const isDarkMode =\n document.documentElement.classList.contains(\"dark\") ||\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n if (isDarkMode) {\n box.style.color = \"rgb(248 113 113)\"; // red-400 for dark mode\n box.style.backgroundColor = this.isActive\n ? \"rgb(248 113 113 / 20%)\"\n : \"rgb(248 113 113 / 10%)\";\n hoverText.style.backgroundColor = \"rgb(69 10 10)\"; // red-950\n hoverText.style.color = \"rgb(254 226 226)\"; // red-100\n hoverText.style.border = \"var(--studio-border-width, 1px) var(--studio-border-style, solid) rgb(153 27 27)\"; // red-800\n }\n\n box.appendChild(hoverText);\n\n box.onmouseover = function () {\n hoverText.style.visibility = \"visible\";\n };\n box.onmouseout = function () {\n hoverText.style.visibility = \"hidden\";\n };\n\n return box;\n }\n\n eq(other: DeletionMarker) {\n return (\n other.deletedText === this.deletedText && other.isActive === this.isActive\n );\n }\n\n ignoreEvent() {\n return true;\n }\n}\n", "import { createEffect } from \"solid-js\";\n\n/** CodeMirror */\nimport { EditorView } from \"@codemirror/view\";\nimport { EditorSelection, type Extension } from \"@codemirror/state\";\n\n/**\n * Create a CodeMirror extension that scrolls the highlighted range into view\n * when it changes.\n *\n * Uses the \"nearest\" scroll strategy, so a highlight that's already on screen\n * is left untouched and an off-screen one is scrolled just into frame -- i.e.\n * \"scroll the highlight into view only if it isn't already there\". Handy for\n * following focus/selection driven by other views (e.g. picking a comment).\n *\n * @param target An accessor returning the [from, to] range to reveal, or null.\n * @returns A tuple of the extension and a function to create the scroll effect.\n */\nexport function createScrollHighlightIntoViewExtension(\n target: () => readonly [number, number] | null\n) {\n // Scrolling is a fire-and-forget transaction effect with no editor state to\n // hold, so there's nothing to add to the editor. The empty extension just\n // keeps the [extension, effectFactory] shape shared with the siblings.\n const extension: Extension = [];\n\n const createScrollHighlightIntoViewEffect = (view: EditorView) =>\n createEffect(() => {\n const range = target();\n if (!range) return;\n // Clamp to the live doc: a target resolved against a slightly different\n // doc revision could otherwise point past the end and throw.\n const docLength = view.state.doc.length;\n const from = Math.min(range[0], docLength);\n const to = Math.min(range[1], docLength);\n view.dispatch({\n effects: EditorView.scrollIntoView(EditorSelection.range(from, to), {\n x: \"nearest\",\n y: \"nearest\",\n }),\n });\n });\n\n return [extension, createScrollHighlightIntoViewEffect] as const;\n}\n", "import { createEffect } from \"solid-js\";\n\n/** CodeMirror */\nimport {\n Decoration,\n EditorView,\n RectangleMarker,\n ViewPlugin,\n layer,\n type DecorationSet,\n type ViewUpdate,\n} from \"@codemirror/view\";\nimport {\n Compartment,\n EditorSelection,\n StateEffect,\n StateField,\n Transaction,\n type Extension,\n type Range,\n} from \"@codemirror/state\";\n\n/** Automerge */\nimport {\n next as A,\n type Prop as AutomergeProp,\n} from \"@automerge/automerge/slim\";\nimport {\n Presence,\n type AutomergeUrl,\n type DocHandle,\n type Repo,\n} from \"@automerge/automerge-repo/slim\";\n\n// The payload broadcast to peers: just the selection, as a pair of automerge\n// cursors rather than plain offsets, so every receiver resolves it against\n// its *own* view of the document and it stays glued to the text through\n// concurrent edits. Identity rides on Presence's own `userId` field (the\n// sender's contact url); receivers use it to look up the peer's contact doc\n// and read the color chosen there.\ntype PresenceSelection = {\n start: A.Cursor;\n end: A.Cursor;\n headAtStart: boolean;\n // Sender's Date.now(); used only as a change marker (not for clock comparison)\n lastMoved: number;\n};\ntype PresenceChannels = { selection: PresenceSelection | null };\n\n// A remote peer's selection resolved to positions in the current document.\n// `peerId` is unique per session, so two tabs of the same contact each get\n// their own caret.\ntype RemoteSelection = {\n peerId: string;\n contactUrl: AutomergeUrl | null;\n color: string;\n from: number;\n to: number;\n head: number;\n // Changes exactly when the peer acts (moves, types, refocuses) -- the\n // caret marker uses it to restart its nametag fade-out countdown.\n lastMoved: number;\n};\n\n/**\n * Create a CodeMirror extension for live remote cursors, reconfigurable via a\n * Compartment (same shape as the sync/diff/readOnly extensions).\n *\n * `contactUrl` is our identity: it travels as the Presence userId, and peers\n * use it to mount our \"contact-cursor\" token and read our chosen color. It\n * returning `null` renders no presence -- that's the state before the\n * identity has loaded, and permanently so for read-only views. When the\n * accessor changes, the compartment swaps the plugin wholesale: the outgoing\n * instance stops `Presence` (sending a goodbye to peers) and the new one\n * starts fresh with the new identity.\n *\n * `repo` is used to load each peer's contact doc, where their chosen\n * presence color lives.\n */\nexport function createPresenceExtension(\n handle: () => DocHandle<unknown>,\n path: () => AutomergeProp[],\n contactUrl: () => AutomergeUrl | null,\n repo: Repo\n) {\n const compartment = new Compartment();\n\n const extension = () => {\n const url = contactUrl();\n return url ? presence(handle(), path(), url, repo) : [];\n };\n\n const createReconfigureEffect = (view: EditorView) =>\n createEffect(() => {\n view.dispatch({ effects: compartment.reconfigure(extension()) });\n });\n\n return [compartment.of(extension()), createReconfigureEffect] as const;\n}\n\n/**\n * Live remote cursors for collaborative text, handling presence transport,\n * broadcasting/resolving selections, rendering colored ranges and contact-cursor carets.\n * Peers are keyed by session (peerId); idle carets hide their nametag after a timeout.\n * Lifetime matches the editor: reconfiguring or closing stops presence.\n */\nfunction presence(\n handle: DocHandle<unknown>,\n path: AutomergeProp[],\n contactUrl: AutomergeUrl,\n repo: Repo\n): Extension {\n const setRemoteSelections = StateEffect.define<RemoteSelection[]>();\n\n const remoteSelectionField = StateField.define<RemoteSelection[]>({\n create() {\n return [];\n },\n update(value, tr) {\n // Between re-resolutions, keep positions glued through edits.\n if (tr.docChanged) {\n value = value.map((sel) => ({\n ...sel,\n from: tr.changes.mapPos(sel.from),\n to: tr.changes.mapPos(sel.to),\n head: tr.changes.mapPos(sel.head),\n }));\n }\n for (const e of tr.effects) {\n if (e.is(setRemoteSelections)) value = e.value;\n }\n return value;\n },\n // Selection tints are plain mark decorations: they carry no stateful\n // DOM, so redrawing them is invisible.\n provide: (f) => EditorView.decorations.from(f, buildSelectionTints),\n });\n\n // Carets and their nametags are rendered in a layer (not as widget decorations)\n // to prevent remounts and visible flicker. This ensures the <patchwork-view>\n // element stays mounted and updated in place, even during edits.\n const caretLayer = layer({\n above: true,\n class: \"cm-presence-layer\",\n update: (update) =>\n update.docChanged ||\n update.transactions.some((tr) =>\n tr.effects.some((e) => e.is(setRemoteSelections))\n ),\n markers(view) {\n // Stable order, so the layer's pairwise DOM reuse matches each marker\n // with the same peer's existing element across redraws.\n const selections = [...view.state.field(remoteSelectionField)].sort(\n (a, b) => (a.peerId < b.peerId ? -1 : a.peerId > b.peerId ? 1 : 0)\n );\n const markers: PresenceCaretMarker[] = [];\n for (const sel of selections) {\n // Reuse drawSelection's measurement for a caret rectangle at head.\n const [rect] = RectangleMarker.forRange(\n view,\n \"cm-presence-caret\",\n EditorSelection.cursor(sel.head)\n );\n if (!rect) continue; // outside the rendered viewport\n markers.push(\n new PresenceCaretMarker(sel, rect.left, rect.top, rect.height)\n );\n }\n return markers;\n },\n });\n\n const plugin = ViewPlugin.fromClass(\n class {\n private view: EditorView;\n private presence: Presence<PresenceChannels>;\n private destroyed = false;\n private broadcastQueued = false;\n // Peers' contact docs, followed live so a color picked in the account\n // picker recolors that peer's caret and tint immediately. Keyed by\n // contact url; the stored function stops the subscription.\n private contactWatchers = new Map<AutomergeUrl, () => void>();\n private contactColors = new Map<AutomergeUrl, string>();\n\n constructor(view: EditorView) {\n this.view = view;\n this.presence = new Presence<PresenceChannels>({\n handle,\n userId: contactUrl,\n });\n this.presence.start({ initialState: { selection: null } });\n this.presence.on(\"update\", this.refresh);\n this.presence.on(\"snapshot\", this.refresh);\n this.presence.on(\"goodbye\", this.refresh);\n this.presence.on(\"pruned\", this.refresh);\n // Closing the page sends a goodbye, which removes us from peers\n // immediately instead of after the stale-peer timeout.\n window.addEventListener(\"pagehide\", this.stopPresence);\n }\n\n update(update: ViewUpdate) {\n // Typing is considered cursor movement; local edits may not set selection explicitly, so we also check userEvent annotations\n const typed =\n update.docChanged &&\n update.transactions.some(\n (tr) => tr.annotation(Transaction.userEvent) !== undefined\n );\n if (update.focusChanged && !update.view.hasFocus) {\n // Hide our caret on peers while blurred; the peer record itself\n // stays alive via heartbeats.\n if (this.presence.running) {\n this.presence.broadcast(\"selection\", null);\n }\n } else if (\n update.selectionSet ||\n typed ||\n (update.focusChanged && update.view.hasFocus)\n ) {\n this.scheduleBroadcast();\n }\n // Local and remote edits alike arrive as transactions (the sync\n // plugin turns automerge patches into transactions), so this\n // re-resolves peers' cursors against the updated doc. Deferred:\n // dispatching is not allowed from inside update().\n if (update.docChanged) {\n queueMicrotask(this.refresh);\n }\n }\n\n destroy() {\n this.destroyed = true;\n this.stopPresence();\n window.removeEventListener(\"pagehide\", this.stopPresence);\n for (const stop of this.contactWatchers.values()) stop();\n this.contactWatchers.clear();\n }\n\n // A peer's color is whatever their account says (contact doc `color`),\n // never derived locally. The doc is fetched once and followed; until it\n // loads (or if the contact never picked a color) the caret stays on the\n // neutral fallback.\n private trackContactColor(url: AutomergeUrl) {\n if (this.contactWatchers.has(url)) return;\n this.contactWatchers.set(url, () => {});\n repo\n .find<ContactDoc>(url)\n .then((contactHandle) => {\n if (this.destroyed) return;\n const read = () => {\n const color = contactHandle.doc()?.color;\n if (!color || this.contactColors.get(url) === color) return;\n this.contactColors.set(url, color);\n this.refresh();\n };\n contactHandle.on(\"change\", read);\n this.contactWatchers.set(url, () =>\n contactHandle.off(\"change\", read)\n );\n read();\n })\n .catch(() => {\n // Contact doc unavailable: the peer stays on the fallback color.\n });\n }\n\n private stopPresence = () => {\n if (this.presence.running) this.presence.stop();\n };\n\n // rAF-throttled (matching tldraw4) so drag-selections don't flood the\n // channel. The selection is read fresh at flush time.\n private scheduleBroadcast() {\n if (this.broadcastQueued) return;\n this.broadcastQueued = true;\n requestAnimationFrame(() => {\n this.broadcastQueued = false;\n if (this.destroyed || !this.presence.running) return;\n // Blurred in the meantime: the null broadcast already went out.\n if (!this.view.hasFocus) return;\n const doc = handle.doc();\n const sel = this.view.state.selection.main;\n this.presence.broadcast(\"selection\", {\n start: A.getCursor(doc, path, sel.from),\n end: A.getCursor(doc, path, sel.to),\n headAtStart: sel.to > sel.from && sel.head === sel.from,\n lastMoved: Date.now(),\n });\n });\n }\n\n // Re-resolve every peer's cursors against the current doc. A cursor can\n // briefly fail to resolve when a presence message races ahead of doc\n // sync -- the peer is skipped and picked up again on the next change.\n private refresh = () => {\n if (this.destroyed) return;\n const doc = handle.doc();\n const resolved: RemoteSelection[] = [];\n for (const peer of this.presence.getPeerStates().peers) {\n const peerId = String(peer.peerId);\n const selection = peer.value?.selection ?? null;\n if (!selection) continue;\n try {\n const from = A.getCursorPosition(doc, path, selection.start);\n const to = A.getCursorPosition(doc, path, selection.end);\n const peerContactUrl =\n (peer.userId as AutomergeUrl | undefined) ?? null;\n if (peerContactUrl) this.trackContactColor(peerContactUrl);\n resolved.push({\n peerId,\n contactUrl: peerContactUrl,\n // The color the peer chose in their account -- the same contact\n // doc the contact-cursor token renders from, so the tint and\n // the token can't drift apart.\n color:\n (peerContactUrl && this.contactColors.get(peerContactUrl)) ||\n FALLBACK_PRESENCE_COLOR,\n from,\n to,\n head: selection.headAtStart ? from : to,\n lastMoved: selection.lastMoved,\n });\n } catch {\n continue;\n }\n }\n this.view.dispatch({ effects: setRemoteSelections.of(resolved) });\n };\n }\n );\n\n return [remoteSelectionField, caretLayer, plugin, presenceTheme];\n}\n\nfunction buildSelectionTints(selections: RemoteSelection[]): DecorationSet {\n const ranges: Range<Decoration>[] = [];\n for (const sel of selections) {\n if (sel.from < sel.to) {\n ranges.push(\n Decoration.mark({\n class: \"cm-presence-range\",\n attributes: {\n style: `background-color: color-mix(in srgb, ${sel.color} 25%, transparent);`,\n },\n }).range(sel.from, sel.to)\n );\n }\n }\n return Decoration.set(ranges, true);\n}\n\n// A peer's nametag fades out once its selection has been still for this\n// long -- the caret itself stays visible. Moving the cursor or typing\n// brings it back.\nconst LABEL_TIMEOUT_MS = 2_000;\n\n// A layer marker for one peer's caret. Markers are throwaway value objects\n// (a fresh set arrives with every measure pass); the peer's DOM element is\n// what persists, via `update` mutating it in place. That persistence is the\n// point of the layer approach: the contact token inside must never remount.\nclass PresenceCaretMarker {\n private readonly sel: RemoteSelection;\n private readonly left: number;\n private readonly top: number;\n private readonly height: number;\n\n constructor(sel: RemoteSelection, left: number, top: number, height: number) {\n this.sel = sel;\n this.left = left;\n this.top = top;\n this.height = height;\n }\n\n eq(other: PresenceCaretMarker): boolean {\n return (\n other.sel.peerId === this.sel.peerId &&\n other.sel.contactUrl === this.sel.contactUrl &&\n other.sel.color === this.sel.color &&\n other.sel.lastMoved === this.sel.lastMoved &&\n other.left === this.left &&\n other.top === this.top &&\n other.height === this.height\n );\n }\n\n draw(): HTMLElement {\n const caret = document.createElement(\"div\");\n caret.className = \"cm-presence-caret\";\n caret.dataset.peerId = this.sel.peerId;\n if (this.sel.contactUrl) {\n const label = document.createElement(\"span\");\n label.className = \"cm-presence-caret-label\";\n const token = document.createElement(\"patchwork-view\");\n token.setAttribute(\"doc-url\", this.sel.contactUrl);\n token.setAttribute(\"tool-id\", \"contact-cursor\");\n token.style.display = \"block\";\n label.appendChild(token);\n caret.appendChild(label);\n }\n this.adjust(caret);\n restartIdleCountdown(caret);\n return caret;\n }\n\n // Reposition the peer's existing element instead of redrawing it, keeping\n // the mounted contact token alive (a remount re-renders asynchronously and\n // flickers -- see the layer comment in `presence`).\n update(dom: HTMLElement, prev: PresenceCaretMarker): boolean {\n // A different peer means a different contact token, which genuinely has\n // to remount. Only happens when peers join or leave.\n if (\n prev.sel.peerId !== this.sel.peerId ||\n prev.sel.contactUrl !== this.sel.contactUrl\n ) {\n return false;\n }\n this.adjust(dom);\n // Fresh activity restarts the nametag fade-out. Position shifts caused\n // by other people's edits deliberately don't.\n if (prev.sel.lastMoved !== this.sel.lastMoved) restartIdleCountdown(dom);\n return true;\n }\n\n private adjust(dom: HTMLElement) {\n dom.style.left = `${this.left}px`;\n dom.style.top = `${this.top}px`;\n dom.style.height = `${this.height}px`;\n dom.style.setProperty(\"--cm-presence-color\", this.sel.color);\n }\n}\n\n// Keyed on the DOM element rather than the marker, because marker instances\n// are replaced on every measure pass while the element lives on.\nconst idleTimers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();\n\nfunction restartIdleCountdown(caret: HTMLElement) {\n clearTimeout(idleTimers.get(caret));\n caret.classList.remove(\"cm-presence-caret--idle\");\n idleTimers.set(\n caret,\n setTimeout(\n () => caret.classList.add(\"cm-presence-caret--idle\"),\n LABEL_TIMEOUT_MS\n )\n );\n}\n\nconst presenceTheme = EditorView.baseTheme({\n // Positioned absolutely by the layer (document-relative); -1px centers\n // the 2px bar on the caret position.\n \".cm-presence-caret\": {\n borderLeft: \"2px solid var(--cm-presence-color)\",\n marginLeft: \"-1px\",\n pointerEvents: \"none\",\n },\n \".cm-presence-caret-label\": {\n position: \"absolute\",\n top: \"-1.4em\",\n left: \"-2px\",\n zIndex: \"150\",\n whiteSpace: \"nowrap\",\n pointerEvents: \"none\",\n userSelect: \"none\",\n opacity: \"1\",\n transition: \"opacity max(1ms, calc(200ms * var(--studio-motion-scale, 1) * var(--studio-motion-preference, 1))) ease-in-out\",\n },\n \".cm-presence-caret--idle .cm-presence-caret-label\": {\n opacity: \"0\",\n },\n});\n\n// Shown until a peer's contact doc has loaded, and for contacts that never\n// picked a color. Matches the contact-cursor token's CSS fallback\n// (`var(--contact-cursor-color, #888)` in the account package).\nconst FALLBACK_PRESENCE_COLOR = \"#888\";\n\n// The subset of the account package's ContactDoc this extension reads.\ntype ContactDoc = { color?: string };\n"],
|
|
5
5
|
"mappings": ";AAAA,SAAS,WAAW,gBAAAA,qBAAoB;AACxC,SAAS,cAAAC,mBAAkB;AAC3B,SAAS,eAAAC,cAAa,eAAAC,oBAAmB;;;ACFzC,SAAS,oBAAoB;AAG7B,SAAS,YAAY,kBAAsC;AAC3D,SAAS,aAAa,kBAAkB;AAQjC,SAAS,2BAA2B,aAAkC;AAC3E,QAAM,iBAAiB,YAAY,OAAsB;AACzD,QAAM,mBAAmB,WAAW,OAAsB;AAAA,IACxD,SAAS;AACP,aAAO,WAAW;AAAA,IACpB;AAAA,IACA,OAAO,OAAO,IAAI;AAChB,iBAAW,KAAK,GAAG,SAAS;AAC1B,YAAI,EAAE,GAAG,cAAc,EAAG,QAAO,EAAE;AAAA,MACrC;AACA,UAAI,GAAG,WAAY,QAAO,MAAM,IAAI,GAAG,OAAO;AAC9C,aAAO;AAAA,IACT;AAAA,IACA,SAAS,CAAC,MAAM,WAAW,YAAY,KAAK,CAAC;AAAA,EAC/C,CAAC;AAED,QAAM,0BAA0B,CAAC,SAC/B,aAAa,MAAM;AACjB,gBAAY,KACV,KAAK,SAAS;AAAA,MACZ,SAAS,eAAe,GAAG,YAAa,CAAC;AAAA,IAC3C,CAAC;AAAA,EACL,CAAC;AAEH,SAAO,CAAC,kBAAkB,uBAAuB;AACnD;;;ACrCA,SAAS,gBAAAC,qBAAoB;AAG7B,SAAS,cAAAC,mBAAkB;AAC3B,SAAS,aAAa,mBAAmB;AAclC,SAAS,wBAAwB,OAAsB;AAC5D,QAAM,cAAc,IAAI,YAAY;AAEpC,QAAM,aAAa,MACjB,MAAM,IACF,CAAC,YAAY,SAAS,GAAG,IAAI,GAAGA,YAAW,SAAS,GAAG,KAAK,CAAC,IAC7D,CAAC;AAEP,QAAM,0BAA0B,CAAC,SAC/BD,cAAa,MAAM;AACjB,SAAK,SAAS;AAAA,MACZ,SAAS,YAAY,YAAY,WAAW,CAAC;AAAA,IAC/C,CAAC;AAAA,EACH,CAAC;AAEH,SAAO,CAAC,YAAY,GAAG,WAAW,CAAC,GAAG,uBAAuB;AAC/D;;;AClCA,SAAS,gBAAAE,qBAAoB;AAI7B,SAAS,eAAAC,cAAa,eAAAC,oBAAmB;;;ACJzC,YAAYC,QAAO;AACnB,SAAqB,kBAA8B;AACnD,SAAsB,kBAAkB;;;ACFxC,YAAY,OAAO;AAKZ,IAAM,gCAAgC,CAC3C,QACA,MACA,iBACwB;AACxB,QAAM,0BAA0B,aAAa;AAAA,IAC3C,QAAM,CAAC,cAAc,EAAE,KAAK,CAAC,GAAG,QAAQ;AAAA,EAC1C;AAEA,MAAI,wBAAwB,WAAW,GAAG;AACxC;AAAA,EACF;AAKA,MAAI,OAAO,aAAa,GAAG;AACzB;AAAA,EACF;AAEA,SAAO,OAAO,CAAC,QAAwB;AACrC,4BAAwB,QAAQ,QAAM;AACpC,SAAG,QAAQ;AAAA,QACT,CACE,OACA,KACA,OACA,MACA,aACG;AAGH,UAAE,SAAO,KAAK,KAAK,MAAM,GAAG,OAAO,MAAM,OAAO,SAAS,SAAS,CAAC;AAAA,QACrE;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAED,SAAS,WAAS,OAAO,IAAI,CAAC;AAChC;;;ACpCA;AAAA,EACE;AAAA,EAGA;AAAA,OACK;AAIA,IAAM,qBAAqB,CAChC,MACA,QACA,YACG;AACH,MAAI,YAAY,KAAK,MAAM;AAE3B,aAAW,SAAS,SAAS;AAC3B,UAAM,aAAa,YAAY,OAAO,QAAQ,KAAK,KAAK;AACxD,QAAI,cAAc,MAAM;AACtB,YAAM,YAAY,UAAU,GAAG,YAAY,KAAK,MAAM,IAAI,QAAQ,IAAI;AACtE,kBAAY,UAAU,IAAI,WAAW,CAAC;AAEtC,WAAK,SAAS;AAAA,QACZ,SAAS;AAAA,QACT,aAAa;AAAA,UACX,wBAAwB,GAAG,CAAC,CAAC;AAAA,UAC7B,YAAY,aAAa,GAAG,KAAK;AAAA,UACjC,YAAY,OAAO,GAAG,IAAI;AAAA,QAC5B;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACA,OAAK,SAAS;AAAA,IACZ;AAAA,IACA,aAAa,wBAAwB,GAAG,CAAC,CAAC;AAAA,EAC5C,CAAC;AACH;AAEA,SAAS,YACP,OACA,QACA,OACmB;AACnB,MAAI,MAAM,WAAW,UAAU;AAC7B,WAAO,aAAa,QAAQ,KAAK;AAAA,EACnC,WAAW,MAAM,WAAW,UAAU;AACpC,WAAO,aAAa,QAAQ,KAAK;AAAA,EACnC,WAAW,MAAM,WAAW,OAAO;AACjC,WAAO,UAAU,QAAQ,KAAK;AAAA,EAChC,WAAW,MAAM,WAAW,OAAO;AACjC,WAAO,UAAU,QAAQ,OAAO,KAAK;AAAA,EACvC,OAAO;AACL,WAAO;AAAA,EACT;AACF;AAEA,SAAS,aAAa,QAAgB,OAAuC;AAC3E,QAAM,QAAQ,SAAS,QAAQ,MAAM,IAAI;AACzC,MAAI,SAAS,MAAM;AACjB,WAAO,CAAC;AAAA,EACV;AACA,QAAM,OAAO,MAAM,OAAO,IAAI,OAAM,IAAI,EAAE,SAAS,IAAI,EAAG,EAAE,KAAK,EAAE;AACnE,SAAO,CAAC,EAAE,MAAM,OAAO,IAAI,OAAO,QAAQ,KAAK,CAAC;AAClD;AAEA,SAAS,aACP,QACA,OACmB;AACnB,QAAM,QAAQ,SAAS,QAAQ,MAAM,IAAI;AACzC,MAAI,SAAS,MAAM;AACjB,WAAO,CAAC;AAAA,EACV;AACA,SAAO,CAAC,EAAE,MAAM,OAAO,QAAQ,MAAM,MAAM,CAAC;AAC9C;AAEA,SAAS,UAAU,QAAgB,OAAoC;AACrE,QAAM,QAAQ,SAAS,QAAQ,MAAM,IAAI;AACzC,MAAI,SAAS,MAAM;AACjB,WAAO,CAAC;AAAA,EACV;AACA,QAAM,SAAS,MAAM,UAAU;AAC/B,SAAO,CAAC,EAAE,MAAM,OAAO,IAAI,QAAQ,OAAO,CAAC;AAC7C;AAEA,SAAS,UACP,QACA,OACA,OACmB;AACnB,QAAM,QAAQ,SAAS,QAAQ,CAAC,GAAG,MAAM,MAAM,CAAC,CAAC;AACjD,MAAI,SAAS,MAAM;AACjB,WAAO,CAAC;AAAA,EACV;AACA,QAAM,SAAS,MAAM,IAAI;AACzB,MAAI,OAAO,MAAM,UAAU,UAAU;AACnC,WAAO,CAAC;AAAA,EACV;AACA,SAAO,CAAC,EAAE,MAAM,GAAG,IAAI,QAAQ,QAAQ,MAAM,MAAM,CAAC;AACtD;AAIA,SAAS,SAAS,UAAkB,eAAsC;AACxE,MAAI,cAAc,WAAW,SAAS,SAAS,EAAG,QAAO;AACzD,WAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACxC,QAAI,SAAS,CAAC,MAAM,cAAc,CAAC,EAAG,QAAO;AAAA,EAC/C;AACA,QAAM,QAAQ,cAAc,cAAc,SAAS,CAAC;AACpD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,SAAO;AACT;;;AFhHO,IAAM,0BAA0B,WAAW,OAAgB;AAE3D,IAAM,gBAAgB,CAAC,OAC5B,CAAC,CAAC,GAAG,WAAW,uBAAuB;AAOlC,IAAM,sBAAsB,CAAC;AAAA,EAClC;AAAA,EACA;AACF,MAAiC;AAC/B,MAAI,CAAC,OAAO,QAAQ,GAAG;AACrB,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,SAAO,WAAW;AAAA,IAChB,MAAM;AAAA,MACJ;AAAA,MACA,kBAAoB,YAAS,OAAO,IAAI,CAAC;AAAA,MACzC,4BAA4B;AAAA,MAE5B,YAAY,MAAkB;AAC5B,aAAK,OAAO;AAEZ,aAAK,WAAW,KAAK,SAAS,KAAK,IAAI;AACvC,eAAO,GAAG,UAAU,KAAK,QAAQ;AAAA,MACnC;AAAA,MAEA,OAAO,QAAoB;AAGzB,aAAK,4BAA4B;AAEjC,cAAM,WAAW;AAAA,UACf;AAAA,UACA;AAAA,UACA,OAAO;AAAA,QACT;AAEA,YAAI,UAAU;AACZ,eAAK,kBAAkB;AAAA,QACzB;AAGA,aAAK,4BAA4B;AAAA,MACnC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAYA,WAAW,MAAM;AAEf,YAAI,KAAK,2BAA2B;AAClC;AAAA,QACF;AAEA,cAAM,eAAiB,YAAS,OAAO,IAAI,CAAC;AAC5C,YAAM,UAAO,cAAc,KAAK,eAAe,GAAG;AAChD;AAAA,QACF;AAIA,cAAM,UAAY,QAAK,OAAO,IAAI,GAAG,KAAK,iBAAiB,YAAY;AACvE,2BAAmB,KAAK,MAAM,MAAM,OAAO;AAC3C,aAAK,kBAAkB;AAAA,MACzB;AAAA,MAEA,UAAU;AACR,eAAO,IAAI,UAAU,KAAK,QAAQ;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AACF;;;AG5FA,SAAS,SAAS,cAAc,qBAAqB;AACrD,SAAS,eAAAC,oBAAmC;AAC5C,SAAqB,cAAAC,aAAY,cAAc;AAkBxC,IAAM,mBAAmB,CAAC;AAAA,EAC/B;AACF,MAAyC;AACvC,QAAM,cAAc,IAAID,aAAY;AACpC,QAAM,mBAAmB,MAAM,CAAC,QAAQ,GAAG,OAAO,GAAG,aAAa,CAAC;AAEnE,QAAM,cAAcC,YAAW;AAAA,IAC7B,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,YAAY;AAAA,MACH;AAAA,MAET,YAAY,MAAkB;AAC5B,aAAK,OAAO;AACZ,aAAK,WAAW,KAAK,SAAS,KAAK,IAAI;AACvC,eAAO,GAAG,UAAU,KAAK,QAAQ;AAKjC,uBAAe,MAAM;AACnB,cAAI,CAAC,KAAK,UAAW,MAAK,MAAM;AAAA,QAClC,CAAC;AAAA,MACH;AAAA,MAEA,SAAS,SAAkC;AACzC,YAAI,SAAS,cAAe,MAAK,MAAM;AAAA,MACzC;AAAA;AAAA;AAAA;AAAA,MAKA,QAAQ;AACN,aAAK,KAAK,SAAS,EAAE,SAAS,YAAY,YAAY,CAAC,CAAC,EAAE,CAAC;AAI3D,YACE,CAAC,KAAK,UACN,KAAK,KAAK,MAAM,MAAM,cAAc,KAAK,MAAM,QAC/C;AACA,eAAK,SAAS;AACd,kBAAQ;AAAA,YACN;AAAA,UAIF;AAAA,QACF;AACA,aAAK,KAAK,SAAS;AAAA,UACjB,SAAS,YAAY,YAAY,iBAAiB,CAAC;AAAA,QACrD,CAAC;AAAA,MACH;AAAA,MAEA,UAAU;AACR,aAAK,YAAY;AACjB,eAAO,IAAI,UAAU,KAAK,QAAQ;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AAEA,SAAO,CAAC,YAAY,GAAG,iBAAiB,CAAC,GAAG,WAAW;AACzD;;;AClFA,SAAS,eAAAC,cAAa,eAAAC,oBAAmC;AACzD,SAAS,cAAAC,aAAY,cAAAC,mBAAkB;AAiBhC,IAAM,oBAAoB,CAAC;AAAA,EAChC;AACF,MAA0C;AACxC,QAAM,cAAc,IAAIH,aAAY;AACpC,QAAM,aAAa,MAAM,CAAC,CAAC,OAAO,aAAa;AAC/C,QAAM,aAAa,MACjB,WAAW,IACP,CAACC,aAAY,SAAS,GAAG,IAAI,GAAGC,YAAW,SAAS,GAAG,KAAK,CAAC,IAC7D,CAAC;AAEP,QAAM,cAAcC,YAAW;AAAA,IAC7B,MAAM;AAAA,MACJ,kBAAkB,WAAW;AAAA,MAC7B,YAAY;AAAA,MACH;AAAA,MAET,YAAY,MAAkB;AAC5B,aAAK,OAAO;AACZ,aAAK,WAAW,KAAK,SAAS,KAAK,IAAI;AACvC,eAAO,GAAG,UAAU,KAAK,QAAQ;AAIjC,uBAAe,MAAM;AACnB,cAAI,CAAC,KAAK,UAAW,MAAK,KAAK;AAAA,QACjC,CAAC;AAAA,MACH;AAAA,MAEA,SAAS,SAAkC;AACzC,YAAI,SAAS,cAAe,MAAK,KAAK;AAAA,MACxC;AAAA,MAEA,OAAO;AACL,cAAM,UAAU,WAAW;AAC3B,YAAI,YAAY,KAAK,gBAAiB;AACtC,aAAK,kBAAkB;AACvB,aAAK,KAAK,SAAS,EAAE,SAAS,YAAY,YAAY,WAAW,CAAC,EAAE,CAAC;AAAA,MACvE;AAAA,MAEA,UAAU;AACR,aAAK,YAAY;AACjB,eAAO,IAAI,UAAU,KAAK,QAAQ;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AAEA,SAAO,CAAC,YAAY,GAAG,WAAW,CAAC,GAAG,WAAW;AACnD;;;AC5CO,IAAM,kBAAkB,CAAC;AAAA,EAC9B;AAAA,EACA;AACF,MAAwC;AAAA,EACtC,oBAAoB,EAAE,QAAQ,KAAK,CAAC;AAAA,EACpC,iBAAiB,EAAE,OAAO,CAAC;AAAA,EAC3B,kBAAkB,EAAE,OAAO,CAAC;AAC9B;;;ANZO,SAAS,yBACd,QACA,MACA,YACA;AACA,QAAM,cAAc,IAAIC,aAAY;AAEpC,QAAM,YAAY,MAChB,OAAO,KAAK,KAAK,IACb,gBAAgB;AAAA,IACd,QAAQ,OAAO;AAAA;AAAA,IACf,MAAM,KAAK;AAAA,EACb,CAAC,IACD,CAAC;AAGP,QAAM,0BAA0B,CAAC,SAC/BC,cAAa,MAAM;AAEjB,SAAK,SAAS;AAAA,MACZ,SAAS,YAAY,YAAY,UAAU,CAAC;AAAA,MAC5C,SAAS;AAAA,QACP,MAAM;AAAA,QACN,IAAI,KAAK,MAAM,IAAI;AAAA,QACnB,QAAQ,WAAW;AAAA,MACrB;AAAA,MACA,aAAa;AAAA,QACXC,aAAY,aAAa,GAAG,KAAK;AAAA,QACjCA,aAAY,OAAO,GAAG,IAAI;AAAA,MAC5B;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAEH,SAAO,CAAC,YAAY,GAAG,UAAU,CAAC,GAAG,uBAAuB;AAC9D;;;AOlDA,SAAS,gBAAAC,qBAAoB;AAG7B;AAAA,EACE,cAAAC;AAAA,EAEA,cAAAC;AAAA,EACA;AAAA,OAGK;AACP,SAAS,UAAU,eAAAC,oBAA+B;AAGlD,SAAS,QAAQ,iBAAiB;AAElC;AAAA,EACE;AAAA,OAIK;AAKP,IAAM,cAAcA,aAAY,OAAwB;AAWjD,SAAS,oBACd,QACA,MACA,UACA;AACA,QAAM,SAASD,YAAW;AAAA,IACxB,MAAM;AAAA,MACJ,WAA4B;AAAA,MAC5B,cAA6BD,YAAW;AAAA,MAExC,OAAO,QAAoB;AACzB,YAAI,kBAAkB;AACtB,mBAAW,MAAM,OAAO,cAAc;AACpC,qBAAW,UAAU,GAAG,SAAS;AAC/B,gBAAI,OAAO,GAAG,WAAW,GAAG;AAC1B,mBAAK,WAAW,OAAO;AACvB,gCAAkB;AAAA,YACpB;AAAA,UACF;AAAA,QACF;AACA,YAAI,OAAO,cAAc,iBAAiB;AACxC,eAAK,cAAc,KAAK,MAAM;AAAA,QAChC;AAAA,MACF;AAAA,MAEA,QAAuB;AACrB,cAAM,QAAQ,KAAK;AACnB,cAAM,MAAM,OAAO,GAAG,IAAI;AAC1B,YAAI,CAAC,SAAS,CAAC,IAAK,QAAOA,YAAW;AACtC,eAAO,SAAS;AAAA,UACd;AAAA,YACE,YAAY,KAAK,OAAO,KAAK,CAAC;AAAA,YAC9B,gBAAgB;AAAA,UAClB;AAAA,UACA;AAAA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,EAAE,aAAa,CAAC,MAAM,EAAE,YAAY;AAAA,EACtC;AAEA,QAAM,0BAA0B,CAAC,SAC/BD,cAAa,MAAM;AACjB,SAAK,SAAS,EAAE,SAAS,YAAY,GAAG,SAAS,CAAC,EAAE,CAAC;AAAA,EACvD,CAAC;AAEH,SAAO,CAAC,QAAQ,uBAAuB;AACzC;AAaA,SAAS,YACP,KACA,eACA,MACe;AACf,QAAM,SAAS,YAAY,aAAa;AACxC,QAAM,gBAAgB,aAAa,UAAU,KAAK,KAAK,MAAM,GAAG,IAAI;AACpE,QAAM,UAAU,UAAU,KAAK,KAAK,QAAQ,UAAU,SAAS,GAAG,CAAC;AAEnE,QAAM,MAAqB,CAAC;AAC5B,MAAI,SAAS;AACb,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,iBAAiB,MAAM,MAAM,IAAI,EAAG;AACzC,UAAM,MAAM,MAAM,KAAK,KAAK,MAAM;AAClC,QAAI,MAAM,WAAW,UAAU;AAC7B,YAAM,MAAO,MAAM,MAAiB;AACpC,UAAI,KAAK,EAAE,MAAM,SAAS,MAAM,KAAK,IAAI,MAAM,IAAI,CAAC;AACpD,gBAAU;AAAA,IACZ,WAAW,MAAM,WAAW,OAAO;AACjC,YAAM,MAAM,MAAM,UAAU;AAC5B,YAAM,WAAW,MAAM;AACvB,UAAI,KAAK;AAAA,QACP,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,MAAM,cAAc,UAAU,UAAU,WAAW,GAAG;AAAA,MACxD,CAAC;AACD,gBAAU;AAAA,IACZ;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,qBACP,OACA,MACqB;AACrB,QAAM,MAA2B,CAAC;AAClC,aAAWI,SAAQ,OAAO;AACxB,QAAIA,MAAK,SAAS,WAAW;AAC3B,UAAI;AAAA,QACFH,YAAW,OAAO;AAAA,UAChB,QAAQ,IAAI,eAAeG,MAAK,MAAM,KAAK;AAAA,UAC3C,MAAM;AAAA,QACR,CAAC,EAAE,MAAMA,MAAK,EAAE;AAAA,MAClB;AACA;AAAA,IACF;AAEA,QAAIA,MAAK,SAASA,MAAK,GAAI;AAC3B,QAAI;AAAA,MACFH,YAAW,KAAK;AAAA,QACd,YAAY,EAAE,OAAO,eAAe,IAAI,EAAE;AAAA,MAC5C,CAAC,EAAE,MAAMG,MAAK,MAAMA,MAAK,EAAE;AAAA,IAC7B;AAAA,EACF;AACA,SAAO;AACT;AAIA,SAAS,iBACP,WACA,MACS;AACT,MAAI,UAAU,WAAW,KAAK,SAAS,EAAG,QAAO;AACjD,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,QAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAG,QAAO;AAAA,EACvC;AACA,SAAO,OAAO,UAAU,KAAK,MAAM,MAAM;AAC3C;AAEA,SAAS,aAAa,KAAc,MAA+B;AACjE,MAAI,UAAmB;AACvB,aAAW,OAAO,MAAM;AACtB,QAAI,WAAW,KAAM,QAAO;AAC5B,cAAW,QAA2C,GAAG;AAAA,EAC3D;AACA,SAAO,OAAO,YAAY,WAAW,UAAU;AACjD;AAEA,SAAS,eAAe,MAAuB;AAC7C,SAAO;AAAA,+BACsB,OAAO,YAAY,SAAS;AAAA,wBACnC,OAAO,YAAY,SAAS;AAAA;AAEpD;AAEA,SAAS,kBAA2B;AAClC,SAAO,OAAO,WAAW,8BAA8B,EAAE;AAC3D;AAEA,IAAM,iBAAN,cAA6B,WAAW;AAAA,EACtC;AAAA,EACA;AAAA,EAEA,YAAY,aAAqB,UAAmB;AAClD,UAAM;AACN,SAAK,cAAc;AACnB,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,QAAqB;AACnB,UAAM,MAAM,SAAS,cAAc,KAAK;AAIxC,QAAI,MAAM,UAAU;AACpB,QAAI,MAAM,WAAW;AACrB,QAAI,MAAM,aAAa;AACvB,QAAI,MAAM,gBAAgB;AAC1B,QAAI,MAAM,YAAY;AACtB,QAAI,MAAM,UAAU;AACpB,QAAI,MAAM,QAAQ;AAClB,QAAI,MAAM,SAAS;AACnB,QAAI,MAAM,WAAW;AACrB,QAAI,MAAM,aAAa;AACvB,QAAI,MAAM,kBAAkB,KAAK,WAC7B,yBACA;AACJ,QAAI,MAAM,eAAe;AACzB,QAAI,MAAM,SAAS;AACnB,QAAI,YAAY;AAEhB,UAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,cAAU,MAAM,WAAW;AAC3B,cAAU,MAAM,MAAM;AACtB,cAAU,MAAM,OAAO;AACvB,cAAU,MAAM,SAAS;AACzB,cAAU,MAAM,UAAU;AAC1B,cAAU,MAAM,kBAAkB;AAClC,cAAU,MAAM,WAAW;AAC3B,cAAU,MAAM,QAAQ;AACxB,cAAU,MAAM,SAAS;AACzB,cAAU,MAAM,YAAY;AAC5B,cAAU,MAAM,eAAe;AAC/B,cAAU,MAAM,aAAa;AAC7B,cAAU,YAAY,KAAK;AAE3B,UAAM,aACJ,SAAS,gBAAgB,UAAU,SAAS,MAAM,KAClD,OAAO,WAAW,8BAA8B,EAAE;AAEpD,QAAI,YAAY;AACd,UAAI,MAAM,QAAQ;AAClB,UAAI,MAAM,kBAAkB,KAAK,WAC7B,2BACA;AACJ,gBAAU,MAAM,kBAAkB;AAClC,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,SAAS;AAAA,IAC3B;AAEA,QAAI,YAAY,SAAS;AAEzB,QAAI,cAAc,WAAY;AAC5B,gBAAU,MAAM,aAAa;AAAA,IAC/B;AACA,QAAI,aAAa,WAAY;AAC3B,gBAAU,MAAM,aAAa;AAAA,IAC/B;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,GAAG,OAAuB;AACxB,WACE,MAAM,gBAAgB,KAAK,eAAe,MAAM,aAAa,KAAK;AAAA,EAEtE;AAAA,EAEA,cAAc;AACZ,WAAO;AAAA,EACT;AACF;;;AC9QA,SAAS,gBAAAC,qBAAoB;AAG7B,SAAS,cAAAC,mBAAkB;AAC3B,SAAS,uBAAuC;AAczC,SAAS,uCACd,QACA;AAIA,QAAM,YAAuB,CAAC;AAE9B,QAAM,sCAAsC,CAAC,SAC3CD,cAAa,MAAM;AACjB,UAAM,QAAQ,OAAO;AACrB,QAAI,CAAC,MAAO;AAGZ,UAAM,YAAY,KAAK,MAAM,IAAI;AACjC,UAAM,OAAO,KAAK,IAAI,MAAM,CAAC,GAAG,SAAS;AACzC,UAAM,KAAK,KAAK,IAAI,MAAM,CAAC,GAAG,SAAS;AACvC,SAAK,SAAS;AAAA,MACZ,SAASC,YAAW,eAAe,gBAAgB,MAAM,MAAM,EAAE,GAAG;AAAA,QAClE,GAAG;AAAA,QACH,GAAG;AAAA,MACL,CAAC;AAAA,IACH,CAAC;AAAA,EACH,CAAC;AAEH,SAAO,CAAC,WAAW,mCAAmC;AACxD;;;AC5CA,SAAS,gBAAAC,qBAAoB;AAG7B;AAAA,EACE,cAAAC;AAAA,EACA,cAAAC;AAAA,EACA;AAAA,EACA,cAAAC;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE,eAAAC;AAAA,EACA,mBAAAC;AAAA,EACA,eAAAC;AAAA,EACA,cAAAC;AAAA,EACA,eAAAC;AAAA,OAGK;AAGP;AAAA,EACE,QAAQC;AAAA,OAEH;AACP;AAAA,EACE;AAAA,OAIK;AA+CA,SAAS,wBACd,QACA,MACA,YACA,MACA;AACA,QAAM,cAAc,IAAIL,aAAY;AAEpC,QAAM,YAAY,MAAM;AACtB,UAAM,MAAM,WAAW;AACvB,WAAO,MAAM,SAAS,OAAO,GAAG,KAAK,GAAG,KAAK,IAAI,IAAI,CAAC;AAAA,EACxD;AAEA,QAAM,0BAA0B,CAAC,SAC/BJ,cAAa,MAAM;AACjB,SAAK,SAAS,EAAE,SAAS,YAAY,YAAY,UAAU,CAAC,EAAE,CAAC;AAAA,EACjE,CAAC;AAEH,SAAO,CAAC,YAAY,GAAG,UAAU,CAAC,GAAG,uBAAuB;AAC9D;AAQA,SAAS,SACP,QACA,MACA,YACA,MACW;AACX,QAAM,sBAAsBM,aAAY,OAA0B;AAElE,QAAM,uBAAuBC,YAAW,OAA0B;AAAA,IAChE,SAAS;AACP,aAAO,CAAC;AAAA,IACV;AAAA,IACA,OAAO,OAAO,IAAI;AAEhB,UAAI,GAAG,YAAY;AACjB,gBAAQ,MAAM,IAAI,CAAC,SAAS;AAAA,UAC1B,GAAG;AAAA,UACH,MAAM,GAAG,QAAQ,OAAO,IAAI,IAAI;AAAA,UAChC,IAAI,GAAG,QAAQ,OAAO,IAAI,EAAE;AAAA,UAC5B,MAAM,GAAG,QAAQ,OAAO,IAAI,IAAI;AAAA,QAClC,EAAE;AAAA,MACJ;AACA,iBAAW,KAAK,GAAG,SAAS;AAC1B,YAAI,EAAE,GAAG,mBAAmB,EAAG,SAAQ,EAAE;AAAA,MAC3C;AACA,aAAO;AAAA,IACT;AAAA;AAAA;AAAA,IAGA,SAAS,CAAC,MAAML,YAAW,YAAY,KAAK,GAAG,mBAAmB;AAAA,EACpE,CAAC;AAKD,QAAM,aAAa,MAAM;AAAA,IACvB,OAAO;AAAA,IACP,OAAO;AAAA,IACP,QAAQ,CAAC,WACP,OAAO,cACP,OAAO,aAAa;AAAA,MAAK,CAAC,OACxB,GAAG,QAAQ,KAAK,CAAC,MAAM,EAAE,GAAG,mBAAmB,CAAC;AAAA,IAClD;AAAA,IACF,QAAQ,MAAM;AAGZ,YAAM,aAAa,CAAC,GAAG,KAAK,MAAM,MAAM,oBAAoB,CAAC,EAAE;AAAA,QAC7D,CAAC,GAAG,MAAO,EAAE,SAAS,EAAE,SAAS,KAAK,EAAE,SAAS,EAAE,SAAS,IAAI;AAAA,MAClE;AACA,YAAM,UAAiC,CAAC;AACxC,iBAAW,OAAO,YAAY;AAE5B,cAAM,CAAC,IAAI,IAAI,gBAAgB;AAAA,UAC7B;AAAA,UACA;AAAA,UACAG,iBAAgB,OAAO,IAAI,IAAI;AAAA,QACjC;AACA,YAAI,CAAC,KAAM;AACX,gBAAQ;AAAA,UACN,IAAI,oBAAoB,KAAK,KAAK,MAAM,KAAK,KAAK,KAAK,MAAM;AAAA,QAC/D;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AAED,QAAM,SAASF,YAAW;AAAA,IACxB,MAAM;AAAA,MACI;AAAA,MACA;AAAA,MACA,YAAY;AAAA,MACZ,kBAAkB;AAAA;AAAA;AAAA;AAAA,MAIlB,kBAAkB,oBAAI,IAA8B;AAAA,MACpD,gBAAgB,oBAAI,IAA0B;AAAA,MAEtD,YAAY,MAAkB;AAC5B,aAAK,OAAO;AACZ,aAAK,WAAW,IAAI,SAA2B;AAAA,UAC7C;AAAA,UACA,QAAQ;AAAA,QACV,CAAC;AACD,aAAK,SAAS,MAAM,EAAE,cAAc,EAAE,WAAW,KAAK,EAAE,CAAC;AACzD,aAAK,SAAS,GAAG,UAAU,KAAK,OAAO;AACvC,aAAK,SAAS,GAAG,YAAY,KAAK,OAAO;AACzC,aAAK,SAAS,GAAG,WAAW,KAAK,OAAO;AACxC,aAAK,SAAS,GAAG,UAAU,KAAK,OAAO;AAGvC,eAAO,iBAAiB,YAAY,KAAK,YAAY;AAAA,MACvD;AAAA,MAEA,OAAO,QAAoB;AAEzB,cAAM,QACJ,OAAO,cACP,OAAO,aAAa;AAAA,UAClB,CAAC,OAAO,GAAG,WAAWK,aAAY,SAAS,MAAM;AAAA,QACnD;AACF,YAAI,OAAO,gBAAgB,CAAC,OAAO,KAAK,UAAU;AAGhD,cAAI,KAAK,SAAS,SAAS;AACzB,iBAAK,SAAS,UAAU,aAAa,IAAI;AAAA,UAC3C;AAAA,QACF,WACE,OAAO,gBACP,SACC,OAAO,gBAAgB,OAAO,KAAK,UACpC;AACA,eAAK,kBAAkB;AAAA,QACzB;AAKA,YAAI,OAAO,YAAY;AACrB,yBAAe,KAAK,OAAO;AAAA,QAC7B;AAAA,MACF;AAAA,MAEA,UAAU;AACR,aAAK,YAAY;AACjB,aAAK,aAAa;AAClB,eAAO,oBAAoB,YAAY,KAAK,YAAY;AACxD,mBAAW,QAAQ,KAAK,gBAAgB,OAAO,EAAG,MAAK;AACvD,aAAK,gBAAgB,MAAM;AAAA,MAC7B;AAAA;AAAA;AAAA;AAAA;AAAA,MAMQ,kBAAkB,KAAmB;AAC3C,YAAI,KAAK,gBAAgB,IAAI,GAAG,EAAG;AACnC,aAAK,gBAAgB,IAAI,KAAK,MAAM;AAAA,QAAC,CAAC;AACtC,aACG,KAAiB,GAAG,EACpB,KAAK,CAAC,kBAAkB;AACvB,cAAI,KAAK,UAAW;AACpB,gBAAM,OAAO,MAAM;AACjB,kBAAM,QAAQ,cAAc,IAAI,GAAG;AACnC,gBAAI,CAAC,SAAS,KAAK,cAAc,IAAI,GAAG,MAAM,MAAO;AACrD,iBAAK,cAAc,IAAI,KAAK,KAAK;AACjC,iBAAK,QAAQ;AAAA,UACf;AACA,wBAAc,GAAG,UAAU,IAAI;AAC/B,eAAK,gBAAgB;AAAA,YAAI;AAAA,YAAK,MAC5B,cAAc,IAAI,UAAU,IAAI;AAAA,UAClC;AACA,eAAK;AAAA,QACP,CAAC,EACA,MAAM,MAAM;AAAA,QAEb,CAAC;AAAA,MACL;AAAA,MAEQ,eAAe,MAAM;AAC3B,YAAI,KAAK,SAAS,QAAS,MAAK,SAAS,KAAK;AAAA,MAChD;AAAA;AAAA;AAAA,MAIQ,oBAAoB;AAC1B,YAAI,KAAK,gBAAiB;AAC1B,aAAK,kBAAkB;AACvB,8BAAsB,MAAM;AAC1B,eAAK,kBAAkB;AACvB,cAAI,KAAK,aAAa,CAAC,KAAK,SAAS,QAAS;AAE9C,cAAI,CAAC,KAAK,KAAK,SAAU;AACzB,gBAAM,MAAM,OAAO,IAAI;AACvB,gBAAM,MAAM,KAAK,KAAK,MAAM,UAAU;AACtC,eAAK,SAAS,UAAU,aAAa;AAAA,YACnC,OAAOC,GAAE,UAAU,KAAK,MAAM,IAAI,IAAI;AAAA,YACtC,KAAKA,GAAE,UAAU,KAAK,MAAM,IAAI,EAAE;AAAA,YAClC,aAAa,IAAI,KAAK,IAAI,QAAQ,IAAI,SAAS,IAAI;AAAA,YACnD,WAAW,KAAK,IAAI;AAAA,UACtB,CAAC;AAAA,QACH,CAAC;AAAA,MACH;AAAA;AAAA;AAAA;AAAA,MAKQ,UAAU,MAAM;AACtB,YAAI,KAAK,UAAW;AACpB,cAAM,MAAM,OAAO,IAAI;AACvB,cAAM,WAA8B,CAAC;AACrC,mBAAW,QAAQ,KAAK,SAAS,cAAc,EAAE,OAAO;AACtD,gBAAM,SAAS,OAAO,KAAK,MAAM;AACjC,gBAAM,YAAY,KAAK,OAAO,aAAa;AAC3C,cAAI,CAAC,UAAW;AAChB,cAAI;AACF,kBAAM,OAAOA,GAAE,kBAAkB,KAAK,MAAM,UAAU,KAAK;AAC3D,kBAAM,KAAKA,GAAE,kBAAkB,KAAK,MAAM,UAAU,GAAG;AACvD,kBAAM,iBACH,KAAK,UAAuC;AAC/C,gBAAI,eAAgB,MAAK,kBAAkB,cAAc;AACzD,qBAAS,KAAK;AAAA,cACZ;AAAA,cACA,YAAY;AAAA;AAAA;AAAA;AAAA,cAIZ,OACG,kBAAkB,KAAK,cAAc,IAAI,cAAc,KACxD;AAAA,cACF;AAAA,cACA;AAAA,cACA,MAAM,UAAU,cAAc,OAAO;AAAA,cACrC,WAAW,UAAU;AAAA,YACvB,CAAC;AAAA,UACH,QAAQ;AACN;AAAA,UACF;AAAA,QACF;AACA,aAAK,KAAK,SAAS,EAAE,SAAS,oBAAoB,GAAG,QAAQ,EAAE,CAAC;AAAA,MAClE;AAAA,IACF;AAAA,EACF;AAEA,SAAO,CAAC,sBAAsB,YAAY,QAAQ,aAAa;AACjE;AAEA,SAAS,oBAAoB,YAA8C;AACzE,QAAM,SAA8B,CAAC;AACrC,aAAW,OAAO,YAAY;AAC5B,QAAI,IAAI,OAAO,IAAI,IAAI;AACrB,aAAO;AAAA,QACLR,YAAW,KAAK;AAAA,UACd,OAAO;AAAA,UACP,YAAY;AAAA,YACV,OAAO,wCAAwC,IAAI,KAAK;AAAA,UAC1D;AAAA,QACF,CAAC,EAAE,MAAM,IAAI,MAAM,IAAI,EAAE;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AACA,SAAOA,YAAW,IAAI,QAAQ,IAAI;AACpC;AAKA,IAAM,mBAAmB;AAMzB,IAAM,sBAAN,MAA0B;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,KAAsB,MAAc,KAAa,QAAgB;AAC3E,SAAK,MAAM;AACX,SAAK,OAAO;AACZ,SAAK,MAAM;AACX,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,GAAG,OAAqC;AACtC,WACE,MAAM,IAAI,WAAW,KAAK,IAAI,UAC9B,MAAM,IAAI,eAAe,KAAK,IAAI,cAClC,MAAM,IAAI,UAAU,KAAK,IAAI,SAC7B,MAAM,IAAI,cAAc,KAAK,IAAI,aACjC,MAAM,SAAS,KAAK,QACpB,MAAM,QAAQ,KAAK,OACnB,MAAM,WAAW,KAAK;AAAA,EAE1B;AAAA,EAEA,OAAoB;AAClB,UAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,UAAM,YAAY;AAClB,UAAM,QAAQ,SAAS,KAAK,IAAI;AAChC,QAAI,KAAK,IAAI,YAAY;AACvB,YAAM,QAAQ,SAAS,cAAc,MAAM;AAC3C,YAAM,YAAY;AAClB,YAAM,QAAQ,SAAS,cAAc,gBAAgB;AACrD,YAAM,aAAa,WAAW,KAAK,IAAI,UAAU;AACjD,YAAM,aAAa,WAAW,gBAAgB;AAC9C,YAAM,MAAM,UAAU;AACtB,YAAM,YAAY,KAAK;AACvB,YAAM,YAAY,KAAK;AAAA,IACzB;AACA,SAAK,OAAO,KAAK;AACjB,yBAAqB,KAAK;AAC1B,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,KAAkB,MAAoC;AAG3D,QACE,KAAK,IAAI,WAAW,KAAK,IAAI,UAC7B,KAAK,IAAI,eAAe,KAAK,IAAI,YACjC;AACA,aAAO;AAAA,IACT;AACA,SAAK,OAAO,GAAG;AAGf,QAAI,KAAK,IAAI,cAAc,KAAK,IAAI,UAAW,sBAAqB,GAAG;AACvE,WAAO;AAAA,EACT;AAAA,EAEQ,OAAO,KAAkB;AAC/B,QAAI,MAAM,OAAO,GAAG,KAAK,IAAI;AAC7B,QAAI,MAAM,MAAM,GAAG,KAAK,GAAG;AAC3B,QAAI,MAAM,SAAS,GAAG,KAAK,MAAM;AACjC,QAAI,MAAM,YAAY,uBAAuB,KAAK,IAAI,KAAK;AAAA,EAC7D;AACF;AAIA,IAAM,aAAa,oBAAI,QAAoD;AAE3E,SAAS,qBAAqB,OAAoB;AAChD,eAAa,WAAW,IAAI,KAAK,CAAC;AAClC,QAAM,UAAU,OAAO,yBAAyB;AAChD,aAAW;AAAA,IACT;AAAA,IACA;AAAA,MACE,MAAM,MAAM,UAAU,IAAI,yBAAyB;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAM,gBAAgBC,YAAW,UAAU;AAAA;AAAA;AAAA,EAGzC,sBAAsB;AAAA,IACpB,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,eAAe;AAAA,EACjB;AAAA,EACA,4BAA4B;AAAA,IAC1B,UAAU;AAAA,IACV,KAAK;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,YAAY;AAAA,EACd;AAAA,EACA,qDAAqD;AAAA,IACnD,SAAS;AAAA,EACX;AACF,CAAC;AAKD,IAAM,0BAA0B;;;AZrdhC,IAAM,SAAS,CAAC,KAAK,SAAS;AAC5B,MAAI,UAAU;AACd,aAAW,OAAO,MAAM;AACtB,cAAU,QAAQ,GAAG;AACrB,QAAI,YAAY,QAAQ;AACtB,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AACO,SAAS,WAAW,OAAO;AAChC,QAAM,aAAa,MAAM,MAAM,UAAU,OAAO,MAAM,OAAO,IAAI,GAAG,MAAM,IAAI,KAAK;AACnF,QAAM,CAAC,oBAAoB,gCAAgC,IAAI,yBAAyB,MAAM,MAAM,QAAQ,MAAM,MAAM,MAAM,UAAU;AACxI,QAAM,CAAC,mBAAmB,+BAA+B,IAAI,wBAAwB,MAAM,CAAC,CAAC,MAAM,QAAQ;AAC3G,QAAM,CAAC,sBAAsB,kCAAkC,IAAI,2BAA2B,MAAM,MAAM,cAAc,CAAC;AACzH,QAAM,CAAC,eAAe,2BAA2B,IAAI,oBAAoB,MAAM,MAAM,QAAQ,MAAM,MAAM,MAAM,MAAM,MAAM,WAAW,KAAK,IAAI;AAC/I,QAAM,CAAC,kCAAkC,mCAAmC,IAAI,uCAAuC,MAAM,MAAM,eAAe,KAAK,IAAI;AAC3J,QAAM,CAAC,mBAAmB,+BAA+B,IAAI,wBAAwB,MAAM,MAAM,QAAQ,MAAM,MAAM,MAAM,MAAM,MAAM,YAAY,MAAM,OAAO,WAAW,IAAI,OAAO,MAAM,aAAa,KAAK,MAAM,MAAM,IAAI;AAC9N,QAAM,4BAA4B,IAAIQ,aAAY;AAClD,QAAM,qBAAqBC,YAAW,eAAe,GAAG,YAAU;AAChE,QAAI,CAAC,MAAM,kBAAmB;AAC9B,QAAI,OAAO,cAAc;AACvB,YAAM,MAAM,OAAO,MAAM,UAAU;AACnC,YAAM,kBAAkB,IAAI,MAAM,IAAI,EAAE;AAAA,IAC1C;AAAA,EACF,CAAC;AACD,QAAM,aAAa;AAAA,IAAC;AAAA,IAAoB;AAAA;AAAA,IAExC;AAAA,IAAoB;AAAA,IAAe;AAAA,IAAkC;AAAA,IAAmB,0BAA0B,GAAG,MAAM,cAAc,CAAC,CAAC;AAAA,IAAG;AAAA,EAAiB,EAAE,OAAO,OAAO;AAC/K,QAAM,QAAQC,aAAY,OAAO;AAAA,IAC/B,KAAK,WAAW;AAAA,IAChB;AAAA,EACF,CAAC;AACD,QAAM,OAAO,IAAID,YAAW;AAAA,IAC1B;AAAA,EACF,CAAC;AACD,QAAM,WAAW,IAAI;AACrB,mCAAiC,IAAI;AACrC,kCAAgC,IAAI;AACpC,uCAAqC,IAAI;AACzC,8BAA4B,IAAI;AAChC,sCAAoC,IAAI;AACxC,kCAAgC,IAAI;AACpC,EAAAE,cAAa,MAAM;AACjB,SAAK,SAAS;AAAA,MACZ,SAAS,0BAA0B,YAAY,MAAM,cAAc,CAAC,CAAC;AAAA,IACvE,CAAC;AAAA,EACH,CAAC;AACD,YAAU,MAAM,KAAK,QAAQ,CAAC;AAC9B,SAAO,KAAK;AACd;",
|
|
6
6
|
"names": ["createEffect", "EditorView", "EditorState", "Compartment", "createEffect", "EditorView", "createEffect", "Compartment", "Transaction", "A", "Compartment", "ViewPlugin", "Compartment", "EditorState", "EditorView", "ViewPlugin", "Compartment", "createEffect", "Transaction", "createEffect", "Decoration", "ViewPlugin", "StateEffect", "diff", "createEffect", "EditorView", "createEffect", "Decoration", "EditorView", "ViewPlugin", "Compartment", "EditorSelection", "StateEffect", "StateField", "Transaction", "A", "Compartment", "EditorView", "EditorState", "createEffect"]
|
|
7
7
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CodeMirror
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-A2WBDY3J.js";
|
|
4
4
|
|
|
5
5
|
// src/tool.tsx
|
|
6
6
|
import { template as _$template } from "solid-js/web";
|
|
@@ -254,10 +254,10 @@ var commentUITheme = EditorView.baseTheme({
|
|
|
254
254
|
border: "none",
|
|
255
255
|
borderRadius: "6px",
|
|
256
256
|
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.25)",
|
|
257
|
-
cursor: "pointer",
|
|
257
|
+
cursor: "var(--studio-cursor-pointer, pointer)",
|
|
258
258
|
userSelect: "none",
|
|
259
259
|
whiteSpace: "nowrap",
|
|
260
|
-
transition: "background-color 0.15s"
|
|
260
|
+
transition: "background-color max(1ms, calc(0.15s * var(--studio-motion-scale, 1) * var(--studio-motion-preference, 1)))"
|
|
261
261
|
},
|
|
262
262
|
".cm-comment-button:hover": {
|
|
263
263
|
background: "var(--cm-comment-button-bg-hover)"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/tool.tsx", "../src/lib/extensions/commentUI.ts", "../src/lib/extensions/comments.ts", "../node_modules/.pnpm/@inkandswitch+patchwork-providers-solid@0.2.8_@automerge+automerge-repo@2.6.0-subductio_6905546828d892b9e331a5daae96ed66/node_modules/@inkandswitch/patchwork-providers-solid/dist/index.js"],
|
|
4
|
-
"sourcesContent": ["import { template as _$template } from \"solid-js/web\";\nimport { insert as _$insert } from \"solid-js/web\";\nimport { createComponent as _$createComponent } from \"solid-js/web\";\nvar _tmpl$ = /*#__PURE__*/_$template(`<div style=\"width:100%;height:100%;overflow:hidden;position:relative;background:var(--studio-fill, white)\"><div style=display:flex;height:100%><div style=position:relative;flex:1;height:100%>`);\nimport { CodeMirror } from \"./lib/codemirror.tsx\";\nimport { render } from \"solid-js/web\";\nimport { RangeSet } from \"@codemirror/state\";\nimport { Decoration } from \"@codemirror/view\";\nimport { commentUI } from \"./lib/extensions/commentUI.ts\";\nimport { cursor } from \"@automerge/automerge-repo/slim\";\nimport { getRegistry } from \"@inkandswitch/patchwork-plugins\";\nimport { subscribeDoc, subscribe } from \"@inkandswitch/patchwork-providers-solid\";\nimport { createMemo, createResource, Show } from \"solid-js\";\nimport { createCommentForRange } from \"./lib/extensions/comments.ts\";\nconst PATH = [\"content\"];\nexport function CodeMirrorEditor(props) {\n const baseline = subscribe(props.element, {\n type: \"draft:baseline\",\n url: props.handle.url\n }, {\n heads: null\n });\n const commentEntries = subscribe(props.element, {\n type: \"patchwork:comments\"\n }, []);\n const [focusDoc, focusHandle] = subscribeDoc(props.element, {\n type: \"patchwork:focus\"\n });\n const contactUrl = subscribe(props.element, {\n type: \"patchwork:contact\"\n });\n const [commentTargets] = createResource(commentEntries, entries => getDedupedCommentTargets(entries, props.handle.url, props.repo), {\n initialValue: []\n });\n const focusRefUrls = createMemo(() => {\n const doc = focusDoc();\n return [...Object.keys(doc?.selection ?? {}), ...Object.keys(doc?.highlight ?? {})];\n });\n const [emphasisTargets] = createResource(focusRefUrls, async urls => resolveSubDocUrlsOfDoc(urls, props.handle.url, props.repo), {\n initialValue: []\n });\n const scrollTarget = createMemo(() => {\n let from = Infinity;\n let to = -Infinity;\n for (const ref of emphasisTargets()) {\n const positions = ref.rangePositions();\n if (!positions) continue;\n const [start, end] = positions;\n from = Math.min(from, start);\n to = Math.max(to, end);\n }\n return from <= to ? [from, to] : null;\n });\n let lastEmittedUrl;\n const onChangeSelection = (from, to) => {\n const handle = focusHandle();\n if (!handle) return;\n const nextUrl = props.handle.sub(...PATH, cursor(from, to)).url;\n if (nextUrl === lastEmittedUrl) return;\n handle.change(doc => {\n doc.selection = {\n [nextUrl]: true\n };\n });\n lastEmittedUrl = nextUrl;\n };\n const decorations = () => {\n const targetRefs = commentTargets();\n const emphasisRefs = emphasisTargets();\n return RangeSet.of(buildCommentDecorations(targetRefs, emphasisRefs), true\n // sort ranges\n );\n };\n const createThreadForRange = (from, to) => {\n const url = contactUrl();\n if (!url) {\n console.warn(\"Cannot create comment: no contactUrl available\");\n return null;\n }\n return createCommentForRange(props.handle, PATH, from, to, url);\n };\n const threadAtPos = pos => {\n const threadByTarget = /* @__PURE__ */new Map();\n for (const entry of commentEntries()) {\n threadByTarget.set(entry.targetUrl, entry.threadUrl);\n }\n for (const handle of commentTargets()) {\n const positions = handle.rangePositions();\n if (!positions) continue;\n const [start, end] = positions;\n if (start === end || pos < start || pos > end) continue;\n const threadUrl = threadByTarget.get(handle.url);\n if (threadUrl) return {\n threadUrl,\n from: start\n };\n }\n return null;\n };\n const findThreadByUrl = threadUrl => {\n const threads = props.handle.doc()?.[\"@comments\"]?.threads ?? [];\n return threads.find(thread => props.handle.sub(\"@comments\", \"threads\", {\n id: thread.id\n }).url === threadUrl);\n };\n const watchThreadForClose = (threadUrl, close) => {\n const contact = contactUrl();\n const hasOwnDraft = thread => thread.comments.some(c => c.contactUrl === contact && (c.draftContent !== void 0 || c.content === void 0));\n const seed = findThreadByUrl(threadUrl);\n let sawDraft = seed ? hasOwnDraft(seed) : false;\n const onChange = () => {\n const thread = findThreadByUrl(threadUrl);\n if (!thread) return close();\n if (hasOwnDraft(thread)) sawDraft = true;else if (sawDraft) close();\n };\n props.handle.on(\"change\", onChange);\n return () => props.handle.off(\"change\", onChange);\n };\n const onClosePopover = threadUrl => {\n const contact = contactUrl();\n const isEmptyOwnDraft = c => c.contactUrl === contact && c.content === void 0 && (c.draftContent === void 0 || c.draftContent.trim() === \"\");\n const thread = findThreadByUrl(threadUrl);\n if (!thread || !thread.comments.some(isEmptyOwnDraft)) return;\n const threadId = thread.id;\n queueMicrotask(() => {\n props.handle.change(doc => {\n const list = doc[\"@comments\"]?.threads;\n if (!list) return;\n const index = list.findIndex(t => t.id === threadId);\n if (index === -1) return;\n const target = list[index];\n for (let i = target.comments.length - 1; i >= 0; i--) {\n if (isEmptyOwnDraft(target.comments[i])) target.comments.splice(i, 1);\n }\n if (target.comments.length === 0) list.splice(index, 1);\n });\n });\n };\n const [datatypeExtensions] = createResource(() => loadCodeMirrorExtensionsForDoc(props.handle));\n const extensions = () => [commentUI({\n createThreadForRange,\n threadAtPos,\n watchThreadForClose,\n onClose: onClosePopover\n }), ...(datatypeExtensions() ?? [])];\n return (() => {\n var _el$ = _tmpl$(),\n _el$2 = _el$.firstChild,\n _el$3 = _el$2.firstChild;\n _$insert(_el$3, _$createComponent(Show, {\n get when() {\n return datatypeExtensions.state === \"ready\";\n },\n get children() {\n return _$createComponent(CodeMirror, {\n get handle() {\n return props.handle;\n },\n get repo() {\n return props.repo;\n },\n path: PATH,\n decorations: decorations,\n baseline: () => baseline()?.heads ?? null,\n get extensions() {\n return extensions();\n },\n onChangeSelection: onChangeSelection,\n scrollTarget: scrollTarget,\n contactUrl: () => contactUrl() ?? null\n });\n }\n }));\n return _el$;\n })();\n}\nexport const mount = (handle, element) => render(() => _$createComponent(CodeMirrorEditor, {\n handle: handle,\n get repo() {\n return element.repo;\n },\n element: element\n}), element);\nasync function getDedupedCommentTargets(comments, docUrl, repo) {\n if (!comments) return [];\n const overlappingRefs = /* @__PURE__ */new Set();\n for (const comment of comments) {\n if (comment.targetUrl.startsWith(docUrl)) {\n const target = await repo.find(comment.targetUrl);\n overlappingRefs.add(target);\n }\n }\n return Array.from(overlappingRefs);\n}\nasync function resolveSubDocUrlsOfDoc(urls, docUrl, repo) {\n const refs = /* @__PURE__ */new Set();\n for (const url of urls) {\n if (url.startsWith(docUrl)) {\n refs.add(await repo.find(url));\n }\n }\n return Array.from(refs);\n}\nfunction buildCommentDecorations(targetRefs, emphasisRefs) {\n const out = [];\n for (const ref of targetRefs) {\n const positions = ref.rangePositions();\n if (!positions) continue;\n const [start, end] = positions;\n if (start === end) continue;\n const isEmphasised = emphasisRefs.some(s => s.overlaps(ref));\n out.push(Decoration.mark({\n attributes: {\n style: commentTargetStyle(isEmphasised)\n }\n }).range(start, end));\n }\n return out;\n}\nfunction commentTargetStyle(isEmphasised) {\n const paper = isEmphasised ? \"56%\" : \"82%\";\n return `\n border-bottom: 2px solid var(--text-editor-secondary-text);\n background-color: color-mix(in oklch, var(--studio-secondary), var(--text-editor-fill) ${paper});\n `;\n}\nasync function loadCodeMirrorExtensionsForDoc(handle) {\n const docType = handle.doc()?.[\"@patchwork\"]?.type;\n const registry = getRegistry(\"codemirror:extension\");\n const loaded = await registry.loadAll(registry.filter(ext => {\n return ext.supportedDatatypes === \"*\" || Array.isArray(ext.supportedDatatypes) && ext.supportedDatatypes.includes(docType);\n }));\n return loaded.flatMap(ext => {\n const impl = ext.module;\n return Array.isArray(impl) ? impl : [impl];\n });\n}", "import {\n Extension,\n StateEffect,\n StateField,\n type EditorState,\n} from \"@codemirror/state\";\nimport {\n EditorView,\n showTooltip,\n type Tooltip,\n type TooltipView,\n} from \"@codemirror/view\";\nimport type { AutomergeUrl } from \"@automerge/automerge-repo/slim\";\nimport { COMMENT_THREAD_TYPE } from \"./comments.ts\";\n\n// The data hooks the editor UI needs from its host (the CodeMirror tool). The\n// extension owns all the DOM/positioning/lifecycle; the host owns everything\n// that needs the repo, the current contact, or the comment index.\nexport type CommentUIOptions = {\n // Create a thread targeting [from, to] and return its url, or null if it\n // couldn't be created (e.g. no current contact). Called when the floating\n // \"Comment\" button is clicked.\n createThreadForRange: (from: number, to: number) => AutomergeUrl | null;\n // Map a document position to the thread whose commented range covers it (and\n // that range's start), or null. Called when the user clicks commented text so\n // we can pop that thread open under the range's start.\n threadAtPos: (\n pos: number\n ) => { threadUrl: AutomergeUrl; from: number } | null;\n // Watch a thread and invoke `close` once its pending draft is submitted (or\n // cancelled, or the thread is removed). Returns an unsubscribe. Lets the\n // popover dismiss itself the moment the comment is saved.\n watchThreadForClose: (threadUrl: AutomergeUrl, close: () => void) => () => void;\n // Called whenever a popover closes, for any reason. Lets the host discard an\n // abandoned, never-filled-in draft so dismissing the popover doesn't litter\n // the document with empty comments.\n onClose: (threadUrl: AutomergeUrl) => void;\n};\n\n// Which thread the popover is showing, and the document position it hangs off.\ntype PopoverState = { threadUrl: AutomergeUrl; anchor: number };\n\nconst openPopover = StateEffect.define<PopoverState>();\nconst closePopover = StateEffect.define<null>();\n// Hide the floating \"Comment\" button until the next selection change (Escape).\nconst dismissButton = StateEffect.define<null>();\n\n// Mutable UI state shared across the extension's pieces (not editor state\n// because it isn't versioned/undoable): the thread we most recently dismissed,\n// so a click that stays inside it doesn't immediately pop it back open.\ntype CommentUIState = { suppressed: AutomergeUrl | null };\n\nconst POPOVER_CLASS = \"cm-comment-popover\";\n\n// A speech-bubble icon + \"Comment\" label, shown floating above the selection.\nconst buildCommentButton = (\n from: number,\n to: number,\n head: number,\n view: EditorView,\n options: CommentUIOptions\n): HTMLElement => {\n const button = document.createElement(\"button\");\n button.className = \"cm-comment-button\";\n button.setAttribute(\"aria-label\", \"Comment\");\n\n button.innerHTML = `\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z\"\n fill=\"currentColor\" stroke=\"none\"/>\n </svg>\n <span>Comment</span>\n `;\n\n // Without this, the browser focuses the button on mousedown, the editor\n // blurs, the selection collapses, our tooltip source returns null, and\n // CodeMirror tears down the button before `click` fires \u2014 so the first\n // press silently does nothing and the user has to click twice. Swallowing\n // mousedown keeps the editor focused and the selection (and button) alive\n // long enough for the click to land.\n button.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n });\n\n button.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n const threadUrl = options.createThreadForRange(from, to);\n if (!threadUrl) return;\n // Open the popover right where the button was (the selection head).\n view.dispatch({ effects: openPopover.of({ threadUrl, anchor: head }) });\n });\n\n return button;\n};\n\n// The popover body: the shared comment-thread tool, rendered inline via the\n// `patchwork-view` custom element. It's mounted inside `view.dom`, so the\n// element's provider events (repo, contact, \u2026) still bubble up to the host.\nconst buildThreadPopover = (\n view: EditorView,\n state: PopoverState,\n options: CommentUIOptions,\n ui: CommentUIState\n): TooltipView => {\n const dom = document.createElement(\"div\");\n dom.className = POPOVER_CLASS;\n\n const threadView = document.createElement(\"patchwork-view\");\n threadView.setAttribute(\"doc-url\", state.threadUrl);\n threadView.setAttribute(\"tool-id\", COMMENT_THREAD_TYPE);\n dom.append(threadView);\n\n const close = () => view.dispatch({ effects: closePopover.of(null) });\n\n // Dismiss on a click anywhere outside the popover, or on Escape. Both are\n // armed on a deferred tick so the very interaction that opened the popover\n // (the button click / the click on commented text) doesn't also close it.\n const onOutsideMouseDown = (event: MouseEvent) => {\n if (!dom.contains(event.target as Node)) close();\n };\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") close();\n };\n const doc = view.win.document;\n const armTimer = view.win.setTimeout(() => {\n doc.addEventListener(\"mousedown\", onOutsideMouseDown, true);\n doc.addEventListener(\"keydown\", onKeyDown, true);\n }, 0);\n\n // Close as soon as the draft is submitted (or the thread goes away).\n const stopWatch = options.watchThreadForClose(state.threadUrl, close);\n\n return {\n dom,\n destroy() {\n view.win.clearTimeout(armTimer);\n doc.removeEventListener(\"mousedown\", onOutsideMouseDown, true);\n doc.removeEventListener(\"keydown\", onKeyDown, true);\n stopWatch();\n // Remember this thread as dismissed so clicking within its range again\n // doesn't immediately reopen it (cleared once a click lands elsewhere).\n ui.suppressed = state.threadUrl;\n options.onClose(state.threadUrl);\n },\n };\n};\n\nconst buttonTooltip = (\n state: EditorState,\n options: CommentUIOptions\n): Tooltip | null => {\n const sel = state.selection.main;\n // A read-only editor can't take the comment write, so don't offer the button.\n if (sel.empty || state.readOnly) return null;\n\n // Anchor the button at the drag tail (the selection head) rather than the\n // start, so dragging left-to-right lands it on the right and right-to-left\n // on the left.\n return {\n pos: sel.head,\n above: true,\n strictSide: false,\n arrow: false,\n create: (view) => ({\n dom: buildCommentButton(sel.from, sel.to, sel.head, view, options),\n }),\n };\n};\n\nconst popoverTooltip = (\n state: PopoverState,\n options: CommentUIOptions,\n ui: CommentUIState\n): Tooltip => ({\n pos: state.anchor,\n above: false, // comes out underneath the anchor\n strictSide: false,\n arrow: false,\n create: (view) => buildThreadPopover(view, state, options, ui),\n});\n\ntype FieldValue = {\n popover: PopoverState | null;\n // The floating button is hidden while this is true \u2014 after Escape, or after a\n // comment is added \u2014 until a fresh selection re-arms it.\n buttonDismissed: boolean;\n tooltip: Tooltip | null;\n};\n\n// A single field driving the one tooltip slot: the thread popover when one is\n// open, otherwise the selection's \"Comment\" button.\nconst commentUIField = (options: CommentUIOptions, ui: CommentUIState) =>\n StateField.define<FieldValue>({\n create: (state) => ({\n popover: null,\n buttonDismissed: false,\n tooltip: buttonTooltip(state, options),\n }),\n update(value, tr) {\n let popover = value.popover;\n let buttonDismissed = value.buttonDismissed;\n let popoverChanged = false;\n\n // Read-only can flip mid-session: entering it closes any open popover \u2014\n // its reply box writes to a doc that rejects writes \u2014 and the flip\n // itself must recompute the tooltip so the button hides/re-arms.\n const readOnlyChanged = tr.startState.readOnly !== tr.state.readOnly;\n if (readOnlyChanged && tr.state.readOnly && popover) {\n popover = null;\n popoverChanged = true;\n }\n\n // Keep the popover pinned to its text as the document shifts under it.\n if (popover && tr.docChanged) {\n popover = { ...popover, anchor: tr.changes.mapPos(popover.anchor) };\n popoverChanged = true;\n }\n for (const effect of tr.effects) {\n if (effect.is(openPopover)) {\n popover = effect.value;\n popoverChanged = true;\n } else if (effect.is(closePopover)) {\n popover = null;\n popoverChanged = true;\n // A comment was just added (or dismissed): don't flash the button\n // back up over the still-selected text.\n buttonDismissed = true;\n } else if (effect.is(dismissButton)) {\n buttonDismissed = true;\n }\n }\n // Any fresh selection re-arms the button.\n if (tr.selection) buttonDismissed = false;\n\n if (popover) {\n // Reuse the exact same tooltip object unless the popover state itself\n // changed. Otherwise an unrelated transaction (a stray selection\n // change, an extension reconfigure, \u2026) would hand CodeMirror a fresh\n // Tooltip and it would tear down and remount the thread view mid-edit.\n if (!popoverChanged) {\n return buttonDismissed === value.buttonDismissed\n ? value\n : { ...value, buttonDismissed };\n }\n return {\n popover,\n buttonDismissed,\n tooltip: popoverTooltip(popover, options, ui),\n };\n }\n\n // No popover: show the selection button, unless it's been dismissed.\n // Recompute only when something relevant changed.\n if (\n !popoverChanged &&\n !tr.docChanged &&\n !tr.selection &&\n !readOnlyChanged &&\n buttonDismissed === value.buttonDismissed\n ) {\n return value;\n }\n return {\n popover: null,\n buttonDismissed,\n tooltip: buttonDismissed ? null : buttonTooltip(tr.state, options),\n };\n },\n provide: (field) => showTooltip.from(field, (value) => value.tooltip),\n });\n\n// Editor-level interactions: click commented text to (re)open its thread, and\n// Escape to dismiss the floating button.\nconst commentInteractions = (\n options: CommentUIOptions,\n ui: CommentUIState,\n field: StateField<FieldValue>\n): Extension =>\n EditorView.domEventHandlers({\n mousedown(event, view) {\n if (\n event.button !== 0 ||\n event.metaKey ||\n event.ctrlKey ||\n event.altKey ||\n event.shiftKey\n ) {\n return false;\n }\n // Clicks inside an open popover are the thread tool's own business.\n if ((event.target as HTMLElement | null)?.closest(\".\" + POPOVER_CLASS)) {\n return false;\n }\n // Read-only: don't open threads either \u2014 the popover's reply box would\n // write into a doc that rejects writes.\n if (view.state.readOnly) return false;\n const pos = view.posAtCoords({ x: event.clientX, y: event.clientY });\n if (pos == null) return false;\n const hit = options.threadAtPos(pos);\n if (!hit) {\n ui.suppressed = null; // clicked away from any comment; re-arm reopening\n return false;\n }\n // Don't reopen the thread we just dismissed while the click stays inside\n // it \u2014 the user has to click elsewhere (or another comment) first.\n if (hit.threadUrl === ui.suppressed) return false;\n ui.suppressed = null;\n view.dispatch({\n effects: openPopover.of({ threadUrl: hit.threadUrl, anchor: hit.from }),\n });\n return false; // let the click also move the cursor as usual\n },\n keydown(event, view) {\n if (event.key !== \"Escape\") return false;\n const state = view.state.field(field, false);\n // While a popover is open its own Escape handler wins (it also works when\n // the popover's textarea holds focus); here we only hide the button.\n if (!state || state.popover || !state.tooltip) return false;\n view.dispatch({ effects: dismissButton.of(null) });\n return true;\n },\n });\n\n// Global colours are lifted into local --cm-comment-* tokens in a\n// :root/:host/[theme] block so they re-evaluate when the theme swaps. This\n// derivation can't live in the baseTheme below \u2014 CodeMirror prefixes every\n// non-\"&\" selector with the editor's scope class, so a `:root` key would\n// become `.cm... :root` and never match. It's injected as a plain stylesheet\n// instead, and the button reads only the derived tokens. The button is editor\n// UI, so it derives from --editor-* (not --studio-*, which is studio chrome).\nconst COMMENT_BUTTON_VARS_ID = \"cm-comment-button-vars\";\nconst ensureCommentButtonVars = (): void => {\n if (\n typeof document === \"undefined\" ||\n document.getElementById(COMMENT_BUTTON_VARS_ID)\n ) {\n return;\n }\n const style = document.createElement(\"style\");\n style.id = COMMENT_BUTTON_VARS_ID;\n style.textContent = `\n :root, :host, [theme] {\n /* the button sits on the editor surface: bg is the plain editor fill,\n text is the editor ink, and hover lifts a gentle one step up the fill\n ramp. see --text-editor-* in the theme. */\n --cm-comment-button-fg: var(--text-editor-line, #0a2b23);\n --cm-comment-button-bg: var(--text-editor-fill, #35f7ca);\n --cm-comment-button-bg-hover: var(--text-editor-fill-offset-10, #2ce0b6);\n }\n `;\n document.head.append(style);\n};\n\nconst commentUITheme = EditorView.baseTheme({\n \".cm-tooltip:has(.cm-comment-button)\": {\n border: \"none\",\n background: \"transparent\",\n boxShadow: \"none\",\n },\n \".cm-comment-button\": {\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: \"5px\",\n padding: \"4px 9px\",\n font: \"inherit\",\n fontSize: \"12px\",\n fontWeight: \"500\",\n lineHeight: \"1\",\n color: \"var(--cm-comment-button-fg)\",\n background: \"var(--cm-comment-button-bg)\",\n border: \"none\",\n borderRadius: \"6px\",\n boxShadow: \"0 2px 8px rgba(0, 0, 0, 0.25)\",\n cursor: \"pointer\",\n userSelect: \"none\",\n whiteSpace: \"nowrap\",\n transition: \"background-color 0.15s\",\n },\n \".cm-comment-button:hover\": {\n background: \"var(--cm-comment-button-bg-hover)\",\n },\n \".cm-comment-button svg\": {\n display: \"block\",\n },\n // The thread tool carries its own card (background, border, padding), so the\n // tooltip wrapper is just a transparent, softly-lifted host for it.\n \".cm-tooltip:has(.cm-comment-popover)\": {\n border: \"none\",\n background: \"transparent\",\n boxShadow: \"none\",\n },\n \".cm-comment-popover\": {\n width: \"min(360px, 92vw)\",\n marginTop: \"6px\",\n fontFamily: \"var(--editor-family)\",\n filter: \"drop-shadow(0 4px 14px rgba(0, 0, 0, 0.25))\",\n },\n // The popover supplies its own frame (the tooltip wrapper's shadow), so the\n // thread card inside drops its own border and shadow to avoid doubling up.\n \".cm-comment-popover .comments-thread-card\": {\n border: \"0\",\n boxShadow: \"none\",\n },\n // The draft textarea is width:100% with its own padding + border; without\n // border-box it spills out of the card's right edge (the card's own reset is\n // scoped to the sidebar panel, which the popover isn't inside).\n \".cm-comment-popover .comment-draft-editor\": {\n boxSizing: \"border-box\",\n maxWidth: \"100%\",\n minWidth: \"0\",\n width: \"100%\",\n },\n \".cm-comment-popover .comment-draft-editor .cm-content\": {\n minHeight: \"0\",\n minWidth: \"0\",\n margin: \"0\",\n padding: \"8px\",\n },\n \".cm-comment-popover .comment-draft-editor .cm-line\": {\n margin: \"0\",\n padding: \"0\",\n },\n \".cm-comment-popover .comment-draft-editor .cm-scroller\": {\n minWidth: \"0\",\n },\n});\n\n// The comment UI: the floating \"Comment\" button on a selection, and the thread\n// popover it (and clicking commented text) opens.\nexport const commentUI = (options: CommentUIOptions): Extension => {\n ensureCommentButtonVars();\n const ui: CommentUIState = { suppressed: null };\n const field = commentUIField(options, ui);\n return [field, commentInteractions(options, ui, field), commentUITheme];\n};\n", "import {\n cursor,\n type AutomergeUrl,\n type DocHandle,\n} from \"@automerge/automerge-repo/slim\";\n\nexport type DocWithComments = {\n \"@comments\"?: {\n threads: CommentThread[];\n };\n};\n\n/**\n * The `@patchwork.type` stamped on a comment thread so a datatype (and thus a\n * relevant tool) can be resolved when the thread's subdocument is opened on its\n * own. Mirrors `COMMENT_THREAD_TYPE` in the comments-view module.\n */\nexport const COMMENT_THREAD_TYPE = \"comment-thread\";\n\nexport type CommentThread = {\n id: string;\n refs: AutomergeUrl[];\n isResolved: boolean;\n comments: Comment[];\n \"@patchwork\"?: { type: string; title?: string };\n};\n\nexport type Comment = {\n id: string;\n content?: string;\n draftContent?: string;\n contactUrl: AutomergeUrl;\n timestamp: number;\n};\n\n// Creates a thread (with one empty, unfilled comment) targeting [from, to] and\n// returns the thread's url so the caller can open it \u2014 e.g. in a popover \u2014 for\n// the author to fill in. An abandoned, never-filled comment should be cleaned\n// up by the caller (see the comment UI's `onClose`).\nexport function createCommentForRange(\n handle: DocHandle<unknown>,\n path: readonly string[],\n from: number,\n to: number,\n contactUrl: AutomergeUrl\n): AutomergeUrl {\n const targetUrl = handle.sub(...path, cursor(from, to)).url;\n const threadId = crypto.randomUUID();\n const commentId = crypto.randomUUID();\n\n handle.change((doc: DocWithComments) => {\n doc[\"@comments\"] ??= { threads: [] };\n doc[\"@comments\"].threads.push({\n id: threadId,\n refs: [targetUrl],\n isResolved: false,\n comments: [\n {\n id: commentId,\n contactUrl,\n timestamp: Date.now(),\n },\n ],\n \"@patchwork\": { type: COMMENT_THREAD_TYPE },\n });\n });\n\n return handle.sub(\"@comments\", \"threads\", { id: threadId }).url;\n}\n", "import { createEffect, createSignal, onCleanup, onMount, } from \"solid-js\";\nimport { createStore, reconcile } from \"solid-js/store\";\nimport * as Providers from \"@inkandswitch/patchwork-providers\";\nfunction resolveElement(source) {\n return typeof source === \"function\" ? source() : source;\n}\n/**\n * Generic reactive request. Resolves the first value a provider emits for\n * `selector` (via the one-shot `request` helper) and returns an accessor that\n * reads `undefined` until then. `T` is the response type.\n */\nexport function request(element, selector) {\n const [value, setValue] = createSignal(undefined);\n onMount(() => {\n const target = resolveElement(element);\n if (!target)\n return;\n Providers.request(target, selector).then((v) => {\n if (v == null)\n return;\n setValue(() => v);\n });\n });\n return value;\n}\nexport function subscribe(element, selector, initialValue) {\n const [value, setValue] = createSignal(initialValue);\n onMount(() => {\n const target = resolveElement(element);\n if (!target)\n return;\n const unsubscribe = Providers.subscribe(target, selector, setValue);\n onCleanup(unsubscribe);\n });\n return value;\n}\n/**\n * Store-backed subscription. Use this when a consumer wants Solid's\n * fine-grained nested reactivity for incoming JSON object or array snapshots.\n * Requires an initial object/array so the store has a stable root.\n *\n * `reconcile` preserves stable object/array identity where possible, so this\n * helper is not a good fit when the top-level value is used as an identity key\n * for resources or memos.\n */\nexport function subscribeReconciled(element, selector, initialValue) {\n const [store, setStore] = createStore(initialValue);\n onMount(() => {\n const target = resolveElement(element);\n if (!target)\n return;\n const unsubscribe = Providers.subscribe(target, selector, (v) => {\n setStore(reconcile(v));\n });\n onCleanup(unsubscribe);\n });\n return store;\n}\n/**\n * Handle-specialized subscription. Use when the answering provider emits an\n * `AutomergeUrl`. The handle is recovered locally from the global repo\n * (`window.repo`), so it stays fully live \u2014 reads project reactively and writes go\n * straight back to the same repo. Returns `[doc, handle]` matching the shape\n * of `solid-automerge`'s `useDocument`; both read `undefined` until the first\n * url arrives. `T` is the doc shape inside the handle.\n */\nexport function subscribeDoc(element, selector) {\n const [handle, setHandle] = createSignal(undefined);\n onMount(() => {\n const target = resolveElement(element);\n if (!target)\n return;\n let canceled = false;\n const unsubscribe = Providers.subscribe(target, selector, (url) => {\n if (!url)\n return;\n const repo = \"repo\" in window ? window.repo : undefined;\n if (!repo)\n return;\n void Promise.resolve(repo.find(url)).then((h) => {\n if (canceled)\n return;\n setHandle(() => h);\n });\n });\n onCleanup(() => {\n canceled = true;\n unsubscribe();\n });\n });\n // Mirror the doc into a store by `reconcile`-ing against the *materialized*\n // snapshot (`handle.doc()`) on every change, rather than replaying the\n // change's incremental patches (as `solid-automerge`'s `createDocumentProjection`\n // / `autoproduce` does).\n //\n // A whole-value write such as `doc.list = [...]` is lowered by Automerge to\n // `putObjectFromHydrate`, whose patch stream is a `put` that materializes the\n // new container *plus* per-element `insert` patches. Replaying that delta with\n // `applyPatches` double-applies the contents \u2014 the `put` seeds `[a, b]`, then\n // the inserts splice `a, b` in again, yielding `[a, b, a, b]` \u2014 even though the\n // document itself is correct. The materialized snapshot is always\n // authoritative, so reconciling against it is robust to any valid change,\n // whole-array reassignment included.\n const [store, setStore] = createStore({});\n const [ready, setReady] = createSignal(false);\n createEffect(() => {\n const h = handle();\n if (!h) {\n setReady(false);\n return;\n }\n const sync = () => {\n setStore(reconcile((h.doc() ?? {})));\n setReady(true);\n };\n sync();\n h.on(\"change\", sync);\n onCleanup(() => h.off(\"change\", sync));\n });\n const doc = () => (ready() ? store : undefined);\n return [doc, handle];\n}\n"],
|
|
4
|
+
"sourcesContent": ["import { template as _$template } from \"solid-js/web\";\nimport { insert as _$insert } from \"solid-js/web\";\nimport { createComponent as _$createComponent } from \"solid-js/web\";\nvar _tmpl$ = /*#__PURE__*/_$template(`<div style=\"width:100%;height:100%;overflow:hidden;position:relative;background:var(--studio-fill, white)\"><div style=display:flex;height:100%><div style=position:relative;flex:1;height:100%>`);\nimport { CodeMirror } from \"./lib/codemirror.tsx\";\nimport { render } from \"solid-js/web\";\nimport { RangeSet } from \"@codemirror/state\";\nimport { Decoration } from \"@codemirror/view\";\nimport { commentUI } from \"./lib/extensions/commentUI.ts\";\nimport { cursor } from \"@automerge/automerge-repo/slim\";\nimport { getRegistry } from \"@inkandswitch/patchwork-plugins\";\nimport { subscribeDoc, subscribe } from \"@inkandswitch/patchwork-providers-solid\";\nimport { createMemo, createResource, Show } from \"solid-js\";\nimport { createCommentForRange } from \"./lib/extensions/comments.ts\";\nconst PATH = [\"content\"];\nexport function CodeMirrorEditor(props) {\n const baseline = subscribe(props.element, {\n type: \"draft:baseline\",\n url: props.handle.url\n }, {\n heads: null\n });\n const commentEntries = subscribe(props.element, {\n type: \"patchwork:comments\"\n }, []);\n const [focusDoc, focusHandle] = subscribeDoc(props.element, {\n type: \"patchwork:focus\"\n });\n const contactUrl = subscribe(props.element, {\n type: \"patchwork:contact\"\n });\n const [commentTargets] = createResource(commentEntries, entries => getDedupedCommentTargets(entries, props.handle.url, props.repo), {\n initialValue: []\n });\n const focusRefUrls = createMemo(() => {\n const doc = focusDoc();\n return [...Object.keys(doc?.selection ?? {}), ...Object.keys(doc?.highlight ?? {})];\n });\n const [emphasisTargets] = createResource(focusRefUrls, async urls => resolveSubDocUrlsOfDoc(urls, props.handle.url, props.repo), {\n initialValue: []\n });\n const scrollTarget = createMemo(() => {\n let from = Infinity;\n let to = -Infinity;\n for (const ref of emphasisTargets()) {\n const positions = ref.rangePositions();\n if (!positions) continue;\n const [start, end] = positions;\n from = Math.min(from, start);\n to = Math.max(to, end);\n }\n return from <= to ? [from, to] : null;\n });\n let lastEmittedUrl;\n const onChangeSelection = (from, to) => {\n const handle = focusHandle();\n if (!handle) return;\n const nextUrl = props.handle.sub(...PATH, cursor(from, to)).url;\n if (nextUrl === lastEmittedUrl) return;\n handle.change(doc => {\n doc.selection = {\n [nextUrl]: true\n };\n });\n lastEmittedUrl = nextUrl;\n };\n const decorations = () => {\n const targetRefs = commentTargets();\n const emphasisRefs = emphasisTargets();\n return RangeSet.of(buildCommentDecorations(targetRefs, emphasisRefs), true\n // sort ranges\n );\n };\n const createThreadForRange = (from, to) => {\n const url = contactUrl();\n if (!url) {\n console.warn(\"Cannot create comment: no contactUrl available\");\n return null;\n }\n return createCommentForRange(props.handle, PATH, from, to, url);\n };\n const threadAtPos = pos => {\n const threadByTarget = /* @__PURE__ */new Map();\n for (const entry of commentEntries()) {\n threadByTarget.set(entry.targetUrl, entry.threadUrl);\n }\n for (const handle of commentTargets()) {\n const positions = handle.rangePositions();\n if (!positions) continue;\n const [start, end] = positions;\n if (start === end || pos < start || pos > end) continue;\n const threadUrl = threadByTarget.get(handle.url);\n if (threadUrl) return {\n threadUrl,\n from: start\n };\n }\n return null;\n };\n const findThreadByUrl = threadUrl => {\n const threads = props.handle.doc()?.[\"@comments\"]?.threads ?? [];\n return threads.find(thread => props.handle.sub(\"@comments\", \"threads\", {\n id: thread.id\n }).url === threadUrl);\n };\n const watchThreadForClose = (threadUrl, close) => {\n const contact = contactUrl();\n const hasOwnDraft = thread => thread.comments.some(c => c.contactUrl === contact && (c.draftContent !== void 0 || c.content === void 0));\n const seed = findThreadByUrl(threadUrl);\n let sawDraft = seed ? hasOwnDraft(seed) : false;\n const onChange = () => {\n const thread = findThreadByUrl(threadUrl);\n if (!thread) return close();\n if (hasOwnDraft(thread)) sawDraft = true;else if (sawDraft) close();\n };\n props.handle.on(\"change\", onChange);\n return () => props.handle.off(\"change\", onChange);\n };\n const onClosePopover = threadUrl => {\n const contact = contactUrl();\n const isEmptyOwnDraft = c => c.contactUrl === contact && c.content === void 0 && (c.draftContent === void 0 || c.draftContent.trim() === \"\");\n const thread = findThreadByUrl(threadUrl);\n if (!thread || !thread.comments.some(isEmptyOwnDraft)) return;\n const threadId = thread.id;\n queueMicrotask(() => {\n props.handle.change(doc => {\n const list = doc[\"@comments\"]?.threads;\n if (!list) return;\n const index = list.findIndex(t => t.id === threadId);\n if (index === -1) return;\n const target = list[index];\n for (let i = target.comments.length - 1; i >= 0; i--) {\n if (isEmptyOwnDraft(target.comments[i])) target.comments.splice(i, 1);\n }\n if (target.comments.length === 0) list.splice(index, 1);\n });\n });\n };\n const [datatypeExtensions] = createResource(() => loadCodeMirrorExtensionsForDoc(props.handle));\n const extensions = () => [commentUI({\n createThreadForRange,\n threadAtPos,\n watchThreadForClose,\n onClose: onClosePopover\n }), ...(datatypeExtensions() ?? [])];\n return (() => {\n var _el$ = _tmpl$(),\n _el$2 = _el$.firstChild,\n _el$3 = _el$2.firstChild;\n _$insert(_el$3, _$createComponent(Show, {\n get when() {\n return datatypeExtensions.state === \"ready\";\n },\n get children() {\n return _$createComponent(CodeMirror, {\n get handle() {\n return props.handle;\n },\n get repo() {\n return props.repo;\n },\n path: PATH,\n decorations: decorations,\n baseline: () => baseline()?.heads ?? null,\n get extensions() {\n return extensions();\n },\n onChangeSelection: onChangeSelection,\n scrollTarget: scrollTarget,\n contactUrl: () => contactUrl() ?? null\n });\n }\n }));\n return _el$;\n })();\n}\nexport const mount = (handle, element) => render(() => _$createComponent(CodeMirrorEditor, {\n handle: handle,\n get repo() {\n return element.repo;\n },\n element: element\n}), element);\nasync function getDedupedCommentTargets(comments, docUrl, repo) {\n if (!comments) return [];\n const overlappingRefs = /* @__PURE__ */new Set();\n for (const comment of comments) {\n if (comment.targetUrl.startsWith(docUrl)) {\n const target = await repo.find(comment.targetUrl);\n overlappingRefs.add(target);\n }\n }\n return Array.from(overlappingRefs);\n}\nasync function resolveSubDocUrlsOfDoc(urls, docUrl, repo) {\n const refs = /* @__PURE__ */new Set();\n for (const url of urls) {\n if (url.startsWith(docUrl)) {\n refs.add(await repo.find(url));\n }\n }\n return Array.from(refs);\n}\nfunction buildCommentDecorations(targetRefs, emphasisRefs) {\n const out = [];\n for (const ref of targetRefs) {\n const positions = ref.rangePositions();\n if (!positions) continue;\n const [start, end] = positions;\n if (start === end) continue;\n const isEmphasised = emphasisRefs.some(s => s.overlaps(ref));\n out.push(Decoration.mark({\n attributes: {\n style: commentTargetStyle(isEmphasised)\n }\n }).range(start, end));\n }\n return out;\n}\nfunction commentTargetStyle(isEmphasised) {\n const paper = isEmphasised ? \"56%\" : \"82%\";\n return `\n border-bottom: 2px solid var(--text-editor-secondary-text);\n background-color: color-mix(in oklch, var(--studio-secondary), var(--text-editor-fill) ${paper});\n `;\n}\nasync function loadCodeMirrorExtensionsForDoc(handle) {\n const docType = handle.doc()?.[\"@patchwork\"]?.type;\n const registry = getRegistry(\"codemirror:extension\");\n const loaded = await registry.loadAll(registry.filter(ext => {\n return ext.supportedDatatypes === \"*\" || Array.isArray(ext.supportedDatatypes) && ext.supportedDatatypes.includes(docType);\n }));\n return loaded.flatMap(ext => {\n const impl = ext.module;\n return Array.isArray(impl) ? impl : [impl];\n });\n}", "import {\n Extension,\n StateEffect,\n StateField,\n type EditorState,\n} from \"@codemirror/state\";\nimport {\n EditorView,\n showTooltip,\n type Tooltip,\n type TooltipView,\n} from \"@codemirror/view\";\nimport type { AutomergeUrl } from \"@automerge/automerge-repo/slim\";\nimport { COMMENT_THREAD_TYPE } from \"./comments.ts\";\n\n// The data hooks the editor UI needs from its host (the CodeMirror tool). The\n// extension owns all the DOM/positioning/lifecycle; the host owns everything\n// that needs the repo, the current contact, or the comment index.\nexport type CommentUIOptions = {\n // Create a thread targeting [from, to] and return its url, or null if it\n // couldn't be created (e.g. no current contact). Called when the floating\n // \"Comment\" button is clicked.\n createThreadForRange: (from: number, to: number) => AutomergeUrl | null;\n // Map a document position to the thread whose commented range covers it (and\n // that range's start), or null. Called when the user clicks commented text so\n // we can pop that thread open under the range's start.\n threadAtPos: (\n pos: number\n ) => { threadUrl: AutomergeUrl; from: number } | null;\n // Watch a thread and invoke `close` once its pending draft is submitted (or\n // cancelled, or the thread is removed). Returns an unsubscribe. Lets the\n // popover dismiss itself the moment the comment is saved.\n watchThreadForClose: (threadUrl: AutomergeUrl, close: () => void) => () => void;\n // Called whenever a popover closes, for any reason. Lets the host discard an\n // abandoned, never-filled-in draft so dismissing the popover doesn't litter\n // the document with empty comments.\n onClose: (threadUrl: AutomergeUrl) => void;\n};\n\n// Which thread the popover is showing, and the document position it hangs off.\ntype PopoverState = { threadUrl: AutomergeUrl; anchor: number };\n\nconst openPopover = StateEffect.define<PopoverState>();\nconst closePopover = StateEffect.define<null>();\n// Hide the floating \"Comment\" button until the next selection change (Escape).\nconst dismissButton = StateEffect.define<null>();\n\n// Mutable UI state shared across the extension's pieces (not editor state\n// because it isn't versioned/undoable): the thread we most recently dismissed,\n// so a click that stays inside it doesn't immediately pop it back open.\ntype CommentUIState = { suppressed: AutomergeUrl | null };\n\nconst POPOVER_CLASS = \"cm-comment-popover\";\n\n// A speech-bubble icon + \"Comment\" label, shown floating above the selection.\nconst buildCommentButton = (\n from: number,\n to: number,\n head: number,\n view: EditorView,\n options: CommentUIOptions\n): HTMLElement => {\n const button = document.createElement(\"button\");\n button.className = \"cm-comment-button\";\n button.setAttribute(\"aria-label\", \"Comment\");\n\n button.innerHTML = `\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z\"\n fill=\"currentColor\" stroke=\"none\"/>\n </svg>\n <span>Comment</span>\n `;\n\n // Without this, the browser focuses the button on mousedown, the editor\n // blurs, the selection collapses, our tooltip source returns null, and\n // CodeMirror tears down the button before `click` fires \u2014 so the first\n // press silently does nothing and the user has to click twice. Swallowing\n // mousedown keeps the editor focused and the selection (and button) alive\n // long enough for the click to land.\n button.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n });\n\n button.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n const threadUrl = options.createThreadForRange(from, to);\n if (!threadUrl) return;\n // Open the popover right where the button was (the selection head).\n view.dispatch({ effects: openPopover.of({ threadUrl, anchor: head }) });\n });\n\n return button;\n};\n\n// The popover body: the shared comment-thread tool, rendered inline via the\n// `patchwork-view` custom element. It's mounted inside `view.dom`, so the\n// element's provider events (repo, contact, \u2026) still bubble up to the host.\nconst buildThreadPopover = (\n view: EditorView,\n state: PopoverState,\n options: CommentUIOptions,\n ui: CommentUIState\n): TooltipView => {\n const dom = document.createElement(\"div\");\n dom.className = POPOVER_CLASS;\n\n const threadView = document.createElement(\"patchwork-view\");\n threadView.setAttribute(\"doc-url\", state.threadUrl);\n threadView.setAttribute(\"tool-id\", COMMENT_THREAD_TYPE);\n dom.append(threadView);\n\n const close = () => view.dispatch({ effects: closePopover.of(null) });\n\n // Dismiss on a click anywhere outside the popover, or on Escape. Both are\n // armed on a deferred tick so the very interaction that opened the popover\n // (the button click / the click on commented text) doesn't also close it.\n const onOutsideMouseDown = (event: MouseEvent) => {\n if (!dom.contains(event.target as Node)) close();\n };\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") close();\n };\n const doc = view.win.document;\n const armTimer = view.win.setTimeout(() => {\n doc.addEventListener(\"mousedown\", onOutsideMouseDown, true);\n doc.addEventListener(\"keydown\", onKeyDown, true);\n }, 0);\n\n // Close as soon as the draft is submitted (or the thread goes away).\n const stopWatch = options.watchThreadForClose(state.threadUrl, close);\n\n return {\n dom,\n destroy() {\n view.win.clearTimeout(armTimer);\n doc.removeEventListener(\"mousedown\", onOutsideMouseDown, true);\n doc.removeEventListener(\"keydown\", onKeyDown, true);\n stopWatch();\n // Remember this thread as dismissed so clicking within its range again\n // doesn't immediately reopen it (cleared once a click lands elsewhere).\n ui.suppressed = state.threadUrl;\n options.onClose(state.threadUrl);\n },\n };\n};\n\nconst buttonTooltip = (\n state: EditorState,\n options: CommentUIOptions\n): Tooltip | null => {\n const sel = state.selection.main;\n // A read-only editor can't take the comment write, so don't offer the button.\n if (sel.empty || state.readOnly) return null;\n\n // Anchor the button at the drag tail (the selection head) rather than the\n // start, so dragging left-to-right lands it on the right and right-to-left\n // on the left.\n return {\n pos: sel.head,\n above: true,\n strictSide: false,\n arrow: false,\n create: (view) => ({\n dom: buildCommentButton(sel.from, sel.to, sel.head, view, options),\n }),\n };\n};\n\nconst popoverTooltip = (\n state: PopoverState,\n options: CommentUIOptions,\n ui: CommentUIState\n): Tooltip => ({\n pos: state.anchor,\n above: false, // comes out underneath the anchor\n strictSide: false,\n arrow: false,\n create: (view) => buildThreadPopover(view, state, options, ui),\n});\n\ntype FieldValue = {\n popover: PopoverState | null;\n // The floating button is hidden while this is true \u2014 after Escape, or after a\n // comment is added \u2014 until a fresh selection re-arms it.\n buttonDismissed: boolean;\n tooltip: Tooltip | null;\n};\n\n// A single field driving the one tooltip slot: the thread popover when one is\n// open, otherwise the selection's \"Comment\" button.\nconst commentUIField = (options: CommentUIOptions, ui: CommentUIState) =>\n StateField.define<FieldValue>({\n create: (state) => ({\n popover: null,\n buttonDismissed: false,\n tooltip: buttonTooltip(state, options),\n }),\n update(value, tr) {\n let popover = value.popover;\n let buttonDismissed = value.buttonDismissed;\n let popoverChanged = false;\n\n // Read-only can flip mid-session: entering it closes any open popover \u2014\n // its reply box writes to a doc that rejects writes \u2014 and the flip\n // itself must recompute the tooltip so the button hides/re-arms.\n const readOnlyChanged = tr.startState.readOnly !== tr.state.readOnly;\n if (readOnlyChanged && tr.state.readOnly && popover) {\n popover = null;\n popoverChanged = true;\n }\n\n // Keep the popover pinned to its text as the document shifts under it.\n if (popover && tr.docChanged) {\n popover = { ...popover, anchor: tr.changes.mapPos(popover.anchor) };\n popoverChanged = true;\n }\n for (const effect of tr.effects) {\n if (effect.is(openPopover)) {\n popover = effect.value;\n popoverChanged = true;\n } else if (effect.is(closePopover)) {\n popover = null;\n popoverChanged = true;\n // A comment was just added (or dismissed): don't flash the button\n // back up over the still-selected text.\n buttonDismissed = true;\n } else if (effect.is(dismissButton)) {\n buttonDismissed = true;\n }\n }\n // Any fresh selection re-arms the button.\n if (tr.selection) buttonDismissed = false;\n\n if (popover) {\n // Reuse the exact same tooltip object unless the popover state itself\n // changed. Otherwise an unrelated transaction (a stray selection\n // change, an extension reconfigure, \u2026) would hand CodeMirror a fresh\n // Tooltip and it would tear down and remount the thread view mid-edit.\n if (!popoverChanged) {\n return buttonDismissed === value.buttonDismissed\n ? value\n : { ...value, buttonDismissed };\n }\n return {\n popover,\n buttonDismissed,\n tooltip: popoverTooltip(popover, options, ui),\n };\n }\n\n // No popover: show the selection button, unless it's been dismissed.\n // Recompute only when something relevant changed.\n if (\n !popoverChanged &&\n !tr.docChanged &&\n !tr.selection &&\n !readOnlyChanged &&\n buttonDismissed === value.buttonDismissed\n ) {\n return value;\n }\n return {\n popover: null,\n buttonDismissed,\n tooltip: buttonDismissed ? null : buttonTooltip(tr.state, options),\n };\n },\n provide: (field) => showTooltip.from(field, (value) => value.tooltip),\n });\n\n// Editor-level interactions: click commented text to (re)open its thread, and\n// Escape to dismiss the floating button.\nconst commentInteractions = (\n options: CommentUIOptions,\n ui: CommentUIState,\n field: StateField<FieldValue>\n): Extension =>\n EditorView.domEventHandlers({\n mousedown(event, view) {\n if (\n event.button !== 0 ||\n event.metaKey ||\n event.ctrlKey ||\n event.altKey ||\n event.shiftKey\n ) {\n return false;\n }\n // Clicks inside an open popover are the thread tool's own business.\n if ((event.target as HTMLElement | null)?.closest(\".\" + POPOVER_CLASS)) {\n return false;\n }\n // Read-only: don't open threads either \u2014 the popover's reply box would\n // write into a doc that rejects writes.\n if (view.state.readOnly) return false;\n const pos = view.posAtCoords({ x: event.clientX, y: event.clientY });\n if (pos == null) return false;\n const hit = options.threadAtPos(pos);\n if (!hit) {\n ui.suppressed = null; // clicked away from any comment; re-arm reopening\n return false;\n }\n // Don't reopen the thread we just dismissed while the click stays inside\n // it \u2014 the user has to click elsewhere (or another comment) first.\n if (hit.threadUrl === ui.suppressed) return false;\n ui.suppressed = null;\n view.dispatch({\n effects: openPopover.of({ threadUrl: hit.threadUrl, anchor: hit.from }),\n });\n return false; // let the click also move the cursor as usual\n },\n keydown(event, view) {\n if (event.key !== \"Escape\") return false;\n const state = view.state.field(field, false);\n // While a popover is open its own Escape handler wins (it also works when\n // the popover's textarea holds focus); here we only hide the button.\n if (!state || state.popover || !state.tooltip) return false;\n view.dispatch({ effects: dismissButton.of(null) });\n return true;\n },\n });\n\n// Global colours are lifted into local --cm-comment-* tokens in a\n// :root/:host/[theme] block so they re-evaluate when the theme swaps. This\n// derivation can't live in the baseTheme below \u2014 CodeMirror prefixes every\n// non-\"&\" selector with the editor's scope class, so a `:root` key would\n// become `.cm... :root` and never match. It's injected as a plain stylesheet\n// instead, and the button reads only the derived tokens. The button is editor\n// UI, so it derives from --editor-* (not --studio-*, which is studio chrome).\nconst COMMENT_BUTTON_VARS_ID = \"cm-comment-button-vars\";\nconst ensureCommentButtonVars = (): void => {\n if (\n typeof document === \"undefined\" ||\n document.getElementById(COMMENT_BUTTON_VARS_ID)\n ) {\n return;\n }\n const style = document.createElement(\"style\");\n style.id = COMMENT_BUTTON_VARS_ID;\n style.textContent = `\n :root, :host, [theme] {\n /* the button sits on the editor surface: bg is the plain editor fill,\n text is the editor ink, and hover lifts a gentle one step up the fill\n ramp. see --text-editor-* in the theme. */\n --cm-comment-button-fg: var(--text-editor-line, #0a2b23);\n --cm-comment-button-bg: var(--text-editor-fill, #35f7ca);\n --cm-comment-button-bg-hover: var(--text-editor-fill-offset-10, #2ce0b6);\n }\n `;\n document.head.append(style);\n};\n\nconst commentUITheme = EditorView.baseTheme({\n \".cm-tooltip:has(.cm-comment-button)\": {\n border: \"none\",\n background: \"transparent\",\n boxShadow: \"none\",\n },\n \".cm-comment-button\": {\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: \"5px\",\n padding: \"4px 9px\",\n font: \"inherit\",\n fontSize: \"12px\",\n fontWeight: \"500\",\n lineHeight: \"1\",\n color: \"var(--cm-comment-button-fg)\",\n background: \"var(--cm-comment-button-bg)\",\n border: \"none\",\n borderRadius: \"6px\",\n boxShadow: \"0 2px 8px rgba(0, 0, 0, 0.25)\",\n cursor: \"var(--studio-cursor-pointer, pointer)\",\n userSelect: \"none\",\n whiteSpace: \"nowrap\",\n transition: \"background-color max(1ms, calc(0.15s * var(--studio-motion-scale, 1) * var(--studio-motion-preference, 1)))\",\n },\n \".cm-comment-button:hover\": {\n background: \"var(--cm-comment-button-bg-hover)\",\n },\n \".cm-comment-button svg\": {\n display: \"block\",\n },\n // The thread tool carries its own card (background, border, padding), so the\n // tooltip wrapper is just a transparent, softly-lifted host for it.\n \".cm-tooltip:has(.cm-comment-popover)\": {\n border: \"none\",\n background: \"transparent\",\n boxShadow: \"none\",\n },\n \".cm-comment-popover\": {\n width: \"min(360px, 92vw)\",\n marginTop: \"6px\",\n fontFamily: \"var(--editor-family)\",\n filter: \"drop-shadow(0 4px 14px rgba(0, 0, 0, 0.25))\",\n },\n // The popover supplies its own frame (the tooltip wrapper's shadow), so the\n // thread card inside drops its own border and shadow to avoid doubling up.\n \".cm-comment-popover .comments-thread-card\": {\n border: \"0\",\n boxShadow: \"none\",\n },\n // The draft textarea is width:100% with its own padding + border; without\n // border-box it spills out of the card's right edge (the card's own reset is\n // scoped to the sidebar panel, which the popover isn't inside).\n \".cm-comment-popover .comment-draft-editor\": {\n boxSizing: \"border-box\",\n maxWidth: \"100%\",\n minWidth: \"0\",\n width: \"100%\",\n },\n \".cm-comment-popover .comment-draft-editor .cm-content\": {\n minHeight: \"0\",\n minWidth: \"0\",\n margin: \"0\",\n padding: \"8px\",\n },\n \".cm-comment-popover .comment-draft-editor .cm-line\": {\n margin: \"0\",\n padding: \"0\",\n },\n \".cm-comment-popover .comment-draft-editor .cm-scroller\": {\n minWidth: \"0\",\n },\n});\n\n// The comment UI: the floating \"Comment\" button on a selection, and the thread\n// popover it (and clicking commented text) opens.\nexport const commentUI = (options: CommentUIOptions): Extension => {\n ensureCommentButtonVars();\n const ui: CommentUIState = { suppressed: null };\n const field = commentUIField(options, ui);\n return [field, commentInteractions(options, ui, field), commentUITheme];\n};\n", "import {\n cursor,\n type AutomergeUrl,\n type DocHandle,\n} from \"@automerge/automerge-repo/slim\";\n\nexport type DocWithComments = {\n \"@comments\"?: {\n threads: CommentThread[];\n };\n};\n\n/**\n * The `@patchwork.type` stamped on a comment thread so a datatype (and thus a\n * relevant tool) can be resolved when the thread's subdocument is opened on its\n * own. Mirrors `COMMENT_THREAD_TYPE` in the comments-view module.\n */\nexport const COMMENT_THREAD_TYPE = \"comment-thread\";\n\nexport type CommentThread = {\n id: string;\n refs: AutomergeUrl[];\n isResolved: boolean;\n comments: Comment[];\n \"@patchwork\"?: { type: string; title?: string };\n};\n\nexport type Comment = {\n id: string;\n content?: string;\n draftContent?: string;\n contactUrl: AutomergeUrl;\n timestamp: number;\n};\n\n// Creates a thread (with one empty, unfilled comment) targeting [from, to] and\n// returns the thread's url so the caller can open it \u2014 e.g. in a popover \u2014 for\n// the author to fill in. An abandoned, never-filled comment should be cleaned\n// up by the caller (see the comment UI's `onClose`).\nexport function createCommentForRange(\n handle: DocHandle<unknown>,\n path: readonly string[],\n from: number,\n to: number,\n contactUrl: AutomergeUrl\n): AutomergeUrl {\n const targetUrl = handle.sub(...path, cursor(from, to)).url;\n const threadId = crypto.randomUUID();\n const commentId = crypto.randomUUID();\n\n handle.change((doc: DocWithComments) => {\n doc[\"@comments\"] ??= { threads: [] };\n doc[\"@comments\"].threads.push({\n id: threadId,\n refs: [targetUrl],\n isResolved: false,\n comments: [\n {\n id: commentId,\n contactUrl,\n timestamp: Date.now(),\n },\n ],\n \"@patchwork\": { type: COMMENT_THREAD_TYPE },\n });\n });\n\n return handle.sub(\"@comments\", \"threads\", { id: threadId }).url;\n}\n", "import { createEffect, createSignal, onCleanup, onMount, } from \"solid-js\";\nimport { createStore, reconcile } from \"solid-js/store\";\nimport * as Providers from \"@inkandswitch/patchwork-providers\";\nfunction resolveElement(source) {\n return typeof source === \"function\" ? source() : source;\n}\n/**\n * Generic reactive request. Resolves the first value a provider emits for\n * `selector` (via the one-shot `request` helper) and returns an accessor that\n * reads `undefined` until then. `T` is the response type.\n */\nexport function request(element, selector) {\n const [value, setValue] = createSignal(undefined);\n onMount(() => {\n const target = resolveElement(element);\n if (!target)\n return;\n Providers.request(target, selector).then((v) => {\n if (v == null)\n return;\n setValue(() => v);\n });\n });\n return value;\n}\nexport function subscribe(element, selector, initialValue) {\n const [value, setValue] = createSignal(initialValue);\n onMount(() => {\n const target = resolveElement(element);\n if (!target)\n return;\n const unsubscribe = Providers.subscribe(target, selector, setValue);\n onCleanup(unsubscribe);\n });\n return value;\n}\n/**\n * Store-backed subscription. Use this when a consumer wants Solid's\n * fine-grained nested reactivity for incoming JSON object or array snapshots.\n * Requires an initial object/array so the store has a stable root.\n *\n * `reconcile` preserves stable object/array identity where possible, so this\n * helper is not a good fit when the top-level value is used as an identity key\n * for resources or memos.\n */\nexport function subscribeReconciled(element, selector, initialValue) {\n const [store, setStore] = createStore(initialValue);\n onMount(() => {\n const target = resolveElement(element);\n if (!target)\n return;\n const unsubscribe = Providers.subscribe(target, selector, (v) => {\n setStore(reconcile(v));\n });\n onCleanup(unsubscribe);\n });\n return store;\n}\n/**\n * Handle-specialized subscription. Use when the answering provider emits an\n * `AutomergeUrl`. The handle is recovered locally from the global repo\n * (`window.repo`), so it stays fully live \u2014 reads project reactively and writes go\n * straight back to the same repo. Returns `[doc, handle]` matching the shape\n * of `solid-automerge`'s `useDocument`; both read `undefined` until the first\n * url arrives. `T` is the doc shape inside the handle.\n */\nexport function subscribeDoc(element, selector) {\n const [handle, setHandle] = createSignal(undefined);\n onMount(() => {\n const target = resolveElement(element);\n if (!target)\n return;\n let canceled = false;\n const unsubscribe = Providers.subscribe(target, selector, (url) => {\n if (!url)\n return;\n const repo = \"repo\" in window ? window.repo : undefined;\n if (!repo)\n return;\n void Promise.resolve(repo.find(url)).then((h) => {\n if (canceled)\n return;\n setHandle(() => h);\n });\n });\n onCleanup(() => {\n canceled = true;\n unsubscribe();\n });\n });\n // Mirror the doc into a store by `reconcile`-ing against the *materialized*\n // snapshot (`handle.doc()`) on every change, rather than replaying the\n // change's incremental patches (as `solid-automerge`'s `createDocumentProjection`\n // / `autoproduce` does).\n //\n // A whole-value write such as `doc.list = [...]` is lowered by Automerge to\n // `putObjectFromHydrate`, whose patch stream is a `put` that materializes the\n // new container *plus* per-element `insert` patches. Replaying that delta with\n // `applyPatches` double-applies the contents \u2014 the `put` seeds `[a, b]`, then\n // the inserts splice `a, b` in again, yielding `[a, b, a, b]` \u2014 even though the\n // document itself is correct. The materialized snapshot is always\n // authoritative, so reconciling against it is robust to any valid change,\n // whole-array reassignment included.\n const [store, setStore] = createStore({});\n const [ready, setReady] = createSignal(false);\n createEffect(() => {\n const h = handle();\n if (!h) {\n setReady(false);\n return;\n }\n const sync = () => {\n setStore(reconcile((h.doc() ?? {})));\n setReady(true);\n };\n sync();\n h.on(\"change\", sync);\n onCleanup(() => h.off(\"change\", sync));\n });\n const doc = () => (ready() ? store : undefined);\n return [doc, handle];\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["cursor", "subscribe", "subscribe", "cursor"]
|
|
7
7
|
}
|
|
@@ -16,7 +16,7 @@ import { EditorView } from "@codemirror/view";
|
|
|
16
16
|
var embedTheme = EditorView.baseTheme({
|
|
17
17
|
".cm-embed": {
|
|
18
18
|
display: "block",
|
|
19
|
-
border: "1px solid",
|
|
19
|
+
border: "var(--studio-border-width, 1px) var(--studio-border-style, solid)",
|
|
20
20
|
borderRadius: "4px",
|
|
21
21
|
overflow: "hidden"
|
|
22
22
|
},
|