@inkandswitch/patchwork-pkg-base 0.0.133 → 0.0.135

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.
@@ -1 +1 @@
1
- automerge:2SHMj6yUG5A3KFx2DaAFfk5DnP1G
1
+ automerge:o3ynRYfyqxhBt5GS6ZEpm26Ppm8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkandswitch/patchwork-pkg-base",
3
- "version": "0.0.133",
3
+ "version": "0.0.135",
4
4
  "type": "module",
5
5
  "description": "Base packages for the Patchwork system",
6
6
  "repository": {
@@ -17,7 +17,8 @@ import {
17
17
  } from "@codemirror/state";
18
18
  import {
19
19
  EditorView,
20
- showTooltip
20
+ showTooltip,
21
+ tooltips
21
22
  } from "@codemirror/view";
22
23
 
23
24
  // src/lib/extensions/comments.ts
@@ -351,8 +352,23 @@ var commentUI = (options) => {
351
352
  ensureCommentButtonVars();
352
353
  const ui = { suppressed: null };
353
354
  const field = commentUIField(options, ui);
354
- return [field, commentInteractions(options, ui, field), commentUITheme];
355
+ return [
356
+ field,
357
+ commentInteractions(options, ui, field),
358
+ commentUITheme,
359
+ tooltips({ tooltipSpace: visibleEditorBox })
360
+ ];
355
361
  };
362
+ function visibleEditorBox(view) {
363
+ const box = view.dom.getBoundingClientRect();
364
+ const win = view.dom.ownerDocument.defaultView ?? window;
365
+ return {
366
+ top: Math.max(0, box.top),
367
+ left: Math.max(0, box.left),
368
+ bottom: Math.min(win.innerHeight, box.bottom),
369
+ right: Math.min(win.innerWidth, box.right)
370
+ };
371
+ }
356
372
 
357
373
  // src/tool.tsx
358
374
  import { cursor as cursor2 } from "@automerge/automerge-repo/slim";
@@ -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_64b21ee2f70813c59f45ca0bcbd2a52c/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 // read when clicked, so no need to rebuild the extension on change\n threadsShownElsewhere: () => Object.keys(focusDoc()?.commentViews ?? {}).length > 0,\n openThreadsElsewhere: () => {\n const handle = focusHandle();\n if (!handle || !Object.keys(focusDoc()?.commentViewOpeners ?? {}).length) return false;\n handle.change(doc => {\n doc.openCommentView = (doc.openCommentView ?? 0) + 1;\n });\n return true;\n }\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 // True while some other view (a comments panel) is showing threads on\n // screen. Then clicking commented text only moves the caret \u2014 that view\n // follows the selection to the thread \u2014 and a new comment is drafted\n // there, not in an inline popover. Without it, or while it's false, the\n // editor shows threads inline.\n threadsShownElsewhere?: () => boolean;\n // With nothing showing threads, ask something that can (a frame with a\n // comments sidebar) to bring a comments view up; true if anything was\n // asked. The view, once on screen, picks the thread up itself.\n openThreadsElsewhere?: () => boolean;\n};\n\n// Threads go to a comments view if one's showing, or one can be opened.\nconst leaveToElsewhere = (options: CommentUIOptions) =>\n !!options.threadsShownElsewhere?.() || !!options.openThreadsElsewhere?.();\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// The main mouse button went down (true) or up (false) in the editor. The\n// button waits for the mouse to come up: it doesn't chase a selection that's\n// still being dragged out.\nconst setPointerDown = StateEffect.define<boolean>();\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.\n// It reads the selection when it is pressed rather than being built for one:\n// the same button stays up as the selection under it is extended.\nconst buildCommentButton = (\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 sel = view.state.selection.main;\n if (sel.empty) return;\n const threadUrl = options.createThreadForRange(sel.from, sel.to);\n if (!threadUrl) return;\n // A visible comments panel picks the new thread up (it's on the\n // selection) and opens its draft; just put the button away.\n if (leaveToElsewhere(options)) {\n view.dispatch({ effects: dismissButton.of(null) });\n return;\n }\n // Open the popover right where the button was (the selection head).\n view.dispatch({ effects: openPopover.of({ threadUrl, anchor: sel.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\n// `create` has to be the same function from one selection to the next (see\n// `commentUIField`): CodeMirror keeps a tooltip's DOM only while its `create`\n// is unchanged, and otherwise rebuilds the button, replaying its entrance, on\n// every keystroke that moves the selection.\nconst buttonTooltip = (\n state: EditorState,\n options: CommentUIOptions,\n create: Tooltip[\"create\"]\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,\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 // A selection is being dragged out with the mouse.\n pointerDown: 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 const createButton: Tooltip[\"create\"] = (view) => ({\n dom: buildCommentButton(view, options),\n });\n return StateField.define<FieldValue>({\n create: (state) => ({\n popover: null,\n buttonDismissed: false,\n pointerDown: false,\n tooltip: buttonTooltip(state, options, createButton),\n }),\n update(value, tr) {\n let popover = value.popover;\n let buttonDismissed = value.buttonDismissed;\n let pointerDown = value.pointerDown;\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 } else if (effect.is(setPointerDown)) {\n pointerDown = effect.value;\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 pointerDown === value.pointerDown\n ? value\n : { ...value, buttonDismissed, pointerDown };\n }\n return {\n popover,\n buttonDismissed,\n pointerDown,\n // Following its text as the document shifts is a move, not a new\n // popover: keep the tooltip's `create`, so CodeMirror repositions the\n // thread view it has rather than closing it (which discards an\n // unwritten draft, see `onClose`) and mounting another.\n tooltip:\n value.tooltip && value.popover?.threadUrl === popover.threadUrl\n ? { ...value.tooltip, pos: popover.anchor }\n : 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 pointerDown === value.pointerDown\n ) {\n return value;\n }\n return {\n popover: null,\n buttonDismissed,\n pointerDown,\n tooltip:\n buttonDismissed || pointerDown\n ? null\n : buttonTooltip(tr.state, options, createButton),\n };\n },\n provide: (field) => showTooltip.from(field, (value) => value.tooltip),\n });\n};\n\n// Note the main button going down, and (wherever it's let go \u2014 often outside\n// the editor, at the end of a drag) coming back up.\nexport const trackPointer = (view: EditorView) => {\n view.dispatch({ effects: setPointerDown.of(true) });\n const up = () => {\n view.win.removeEventListener(\"mouseup\", up, true);\n view.win.removeEventListener(\"blur\", up);\n // the editor may have gone while the button was down\n if (view.dom.isConnected)\n view.dispatch({ effects: setPointerDown.of(false) });\n };\n view.win.addEventListener(\"mouseup\", up, true);\n view.win.addEventListener(\"blur\", up);\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 (event.button === 0) trackPointer(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 // A comments view (showing, or opened now) follows the caret to this\n // thread itself.\n if (leaveToElsewhere(options)) 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 // fades in once the selection's made (it waits for the mouse to come up)\n animation:\n \"cm-comment-button-in max(1ms, calc(0.16s * var(--studio-motion-scale, 1) * var(--studio-motion-preference, 1))) ease-out both\",\n },\n \"@keyframes cm-comment-button-in\": {\n from: { opacity: \"0\", transform: \"translateY(2px)\" },\n to: { opacity: \"1\", transform: \"none\" },\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
- "mappings": "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",
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 // read when clicked, so no need to rebuild the extension on change\n threadsShownElsewhere: () => Object.keys(focusDoc()?.commentViews ?? {}).length > 0,\n openThreadsElsewhere: () => {\n const handle = focusHandle();\n if (!handle || !Object.keys(focusDoc()?.commentViewOpeners ?? {}).length) return false;\n handle.change(doc => {\n doc.openCommentView = (doc.openCommentView ?? 0) + 1;\n });\n return true;\n }\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 tooltips,\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 // True while some other view (a comments panel) is showing threads on\n // screen. Then clicking commented text only moves the caret \u2014 that view\n // follows the selection to the thread \u2014 and a new comment is drafted\n // there, not in an inline popover. Without it, or while it's false, the\n // editor shows threads inline.\n threadsShownElsewhere?: () => boolean;\n // With nothing showing threads, ask something that can (a frame with a\n // comments sidebar) to bring a comments view up; true if anything was\n // asked. The view, once on screen, picks the thread up itself.\n openThreadsElsewhere?: () => boolean;\n};\n\n// Threads go to a comments view if one's showing, or one can be opened.\nconst leaveToElsewhere = (options: CommentUIOptions) =>\n !!options.threadsShownElsewhere?.() || !!options.openThreadsElsewhere?.();\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// The main mouse button went down (true) or up (false) in the editor. The\n// button waits for the mouse to come up: it doesn't chase a selection that's\n// still being dragged out.\nconst setPointerDown = StateEffect.define<boolean>();\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.\n// It reads the selection when it is pressed rather than being built for one:\n// the same button stays up as the selection under it is extended.\nconst buildCommentButton = (\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 sel = view.state.selection.main;\n if (sel.empty) return;\n const threadUrl = options.createThreadForRange(sel.from, sel.to);\n if (!threadUrl) return;\n // A visible comments panel picks the new thread up (it's on the\n // selection) and opens its draft; just put the button away.\n if (leaveToElsewhere(options)) {\n view.dispatch({ effects: dismissButton.of(null) });\n return;\n }\n // Open the popover right where the button was (the selection head).\n view.dispatch({ effects: openPopover.of({ threadUrl, anchor: sel.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\n// `create` has to be the same function from one selection to the next (see\n// `commentUIField`): CodeMirror keeps a tooltip's DOM only while its `create`\n// is unchanged, and otherwise rebuilds the button, replaying its entrance, on\n// every keystroke that moves the selection.\nconst buttonTooltip = (\n state: EditorState,\n options: CommentUIOptions,\n create: Tooltip[\"create\"]\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,\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 // A selection is being dragged out with the mouse.\n pointerDown: 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 const createButton: Tooltip[\"create\"] = (view) => ({\n dom: buildCommentButton(view, options),\n });\n return StateField.define<FieldValue>({\n create: (state) => ({\n popover: null,\n buttonDismissed: false,\n pointerDown: false,\n tooltip: buttonTooltip(state, options, createButton),\n }),\n update(value, tr) {\n let popover = value.popover;\n let buttonDismissed = value.buttonDismissed;\n let pointerDown = value.pointerDown;\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 } else if (effect.is(setPointerDown)) {\n pointerDown = effect.value;\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 pointerDown === value.pointerDown\n ? value\n : { ...value, buttonDismissed, pointerDown };\n }\n return {\n popover,\n buttonDismissed,\n pointerDown,\n // Following its text as the document shifts is a move, not a new\n // popover: keep the tooltip's `create`, so CodeMirror repositions the\n // thread view it has rather than closing it (which discards an\n // unwritten draft, see `onClose`) and mounting another.\n tooltip:\n value.tooltip && value.popover?.threadUrl === popover.threadUrl\n ? { ...value.tooltip, pos: popover.anchor }\n : 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 pointerDown === value.pointerDown\n ) {\n return value;\n }\n return {\n popover: null,\n buttonDismissed,\n pointerDown,\n tooltip:\n buttonDismissed || pointerDown\n ? null\n : buttonTooltip(tr.state, options, createButton),\n };\n },\n provide: (field) => showTooltip.from(field, (value) => value.tooltip),\n });\n};\n\n// Note the main button going down, and (wherever it's let go \u2014 often outside\n// the editor, at the end of a drag) coming back up.\nexport const trackPointer = (view: EditorView) => {\n view.dispatch({ effects: setPointerDown.of(true) });\n const up = () => {\n view.win.removeEventListener(\"mouseup\", up, true);\n view.win.removeEventListener(\"blur\", up);\n // the editor may have gone while the button was down\n if (view.dom.isConnected)\n view.dispatch({ effects: setPointerDown.of(false) });\n };\n view.win.addEventListener(\"mouseup\", up, true);\n view.win.addEventListener(\"blur\", up);\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 (event.button === 0) trackPointer(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 // A comments view (showing, or opened now) follows the caret to this\n // thread itself.\n if (leaveToElsewhere(options)) 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 // fades in once the selection's made (it waits for the mouse to come up)\n animation:\n \"cm-comment-button-in max(1ms, calc(0.16s * var(--studio-motion-scale, 1) * var(--studio-motion-preference, 1))) ease-out both\",\n },\n \"@keyframes cm-comment-button-in\": {\n from: { opacity: \"0\", transform: \"translateY(2px)\" },\n to: { opacity: \"1\", transform: \"none\" },\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 [\n field,\n commentInteractions(options, ui, field),\n commentUITheme,\n tooltips({ tooltipSpace: visibleEditorBox }),\n ];\n};\n\n// The room a tooltip has: the editor's own visible box, not the whole\n// window. Measured against the window, a button over a selection on the top\n// visible line \"fits\" above it \u2014 under whatever chrome sits above the editor\n// (a frame's title bar) \u2014 so it never flips below. Against this, it does.\nfunction visibleEditorBox(view: EditorView) {\n const box = view.dom.getBoundingClientRect();\n const win = view.dom.ownerDocument.defaultView ?? window;\n return {\n top: Math.max(0, box.top),\n left: Math.max(0, box.left),\n bottom: Math.min(win.innerHeight, box.bottom),\n right: Math.min(win.innerWidth, box.right),\n };\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
+ "mappings": "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",
6
6
  "names": ["cursor", "subscribe", "subscribe", "cursor"]
7
7
  }
@@ -23,12 +23,26 @@ var linkTheme = EditorView.baseTheme({
23
23
  fontFamily: "var(--markdown-body-family, var(--text-editor-family-sans))",
24
24
  color: "var(--syntax-link, #0066cc)",
25
25
  backgroundColor: "var(--syntax-fill-link)",
26
- fontStyle: "var(--syntax-style-link)",
27
- fontWeight: "var(--syntax-weight-link)",
26
+ // weight and style come from where the link sits — a heading, bold or
27
+ // italic text — so they're inherited, not set (see the rules below)
28
28
  "&:hover": {
29
29
  color: "inherit"
30
30
  }
31
31
  },
32
+ // in a heading, the heading's own font (its line class sets it)
33
+ ".cm-md-h1 .cm-link, .cm-md-h2 .cm-link, .cm-md-h3 .cm-link, .cm-md-h4 .cm-link, .cm-md-h5 .cm-link, .cm-md-h6 .cm-link": {
34
+ fontFamily: "inherit"
35
+ },
36
+ // the inline styling the link sits in (links.ts: inheritedStyle)
37
+ ".cm-link.cm-md-strong": {
38
+ fontWeight: "var(--syntax-weight-strong, bold)"
39
+ },
40
+ ".cm-link.cm-md-em": {
41
+ fontStyle: "var(--syntax-style-emphasis, italic)"
42
+ },
43
+ ".cm-link.cm-md-strike": {
44
+ textDecoration: "underline line-through"
45
+ },
32
46
  ".cm-md-link-hover, .cm-md-link-hover *": {
33
47
  textDecoration: "underline",
34
48
  cursor: "var(--studio-cursor-pointer, pointer)"
@@ -53,19 +67,22 @@ function openUrl(url, from) {
53
67
  var LinkWidget = class extends WidgetType {
54
68
  url;
55
69
  text;
56
- constructor(url, text) {
70
+ /** the bold/italic/strikethrough the link sits in (see `inheritedStyle`) */
71
+ style;
72
+ constructor(url, text, style = "") {
57
73
  super();
58
74
  this.url = url;
59
75
  this.text = text;
76
+ this.style = style;
60
77
  }
61
78
  eq(other) {
62
- return other.url === this.url && other.text === this.text;
79
+ return other.url === this.url && other.text === this.text && other.style === this.style;
63
80
  }
64
81
  toDOM() {
65
82
  const link = document.createElement("a");
66
83
  link.href = this.url;
67
84
  link.textContent = this.text;
68
- link.className = "cm-link";
85
+ link.className = "cm-link" + (this.style ? " " + this.style : "");
69
86
  link.title = this.url;
70
87
  link.addEventListener("click", (e) => e.preventDefault());
71
88
  return link;
@@ -74,6 +91,15 @@ var LinkWidget = class extends WidgetType {
74
91
  return false;
75
92
  }
76
93
  };
94
+ function inheritedStyle(node) {
95
+ const classes = /* @__PURE__ */ new Set();
96
+ for (let n = node.parent; n; n = n.parent) {
97
+ if (n.name === "StrongEmphasis") classes.add("cm-md-strong");
98
+ else if (n.name === "Emphasis") classes.add("cm-md-em");
99
+ else if (n.name === "Strikethrough") classes.add("cm-md-strike");
100
+ }
101
+ return [...classes].join(" ");
102
+ }
77
103
  function getLinks(view) {
78
104
  const widgets = [];
79
105
  const { state } = view;
@@ -91,7 +117,9 @@ function getLinks(view) {
91
117
  const text = state.doc.sliceString(url.from, url.to);
92
118
  const href = /^[a-z][a-z0-9+.-]*:/i.test(text) || !text.includes("@") ? text : `mailto:${text}`;
93
119
  widgets.push(
94
- Decoration.replace({ widget: new LinkWidget(href, text) }).range(
120
+ Decoration.replace({
121
+ widget: new LinkWidget(href, text, inheritedStyle(node.node))
122
+ }).range(
95
123
  node.from,
96
124
  node.to
97
125
  )
@@ -110,7 +138,7 @@ function getLinks(view) {
110
138
  if (match) {
111
139
  const [, text, url] = match;
112
140
  const deco = Decoration.replace({
113
- widget: new LinkWidget(url, text)
141
+ widget: new LinkWidget(url, text, inheritedStyle(node.node))
114
142
  });
115
143
  widgets.push(deco.range(linkFrom, linkTo));
116
144
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/extensions/links.ts", "../../src/themes/links.ts"],
4
- "sourcesContent": ["import {\n EditorView,\n Decoration,\n WidgetType,\n ViewPlugin,\n ViewUpdate,\n type DecorationSet,\n} from \"@codemirror/view\";\nimport {\n type EditorState,\n Range,\n StateEffect,\n StateField,\n} from \"@codemirror/state\";\nimport { syntaxTree } from \"@codemirror/language\";\nimport { isValidAutomergeUrl } from \"@automerge/automerge-repo/slim\";\nimport { linkTheme } from \"../themes/links.ts\";\n\n/**\n * Open a link from the editor. Automerge urls open inside Patchwork rather\n * than as a browser URL (automerge: isn't navigable); bubbles/composed so the\n * event escapes the editor and any shadow root up to whoever drives selection.\n */\nexport function openUrl(url: string, from: Element) {\n if (isValidAutomergeUrl(url)) {\n from.dispatchEvent(\n new CustomEvent(\"patchwork:open-document\", {\n detail: { url },\n bubbles: true,\n composed: true,\n }),\n );\n } else {\n // GFM autolinks `www.example.com` with no scheme.\n const href = /^[a-z][a-z0-9+.-]*:/i.test(url) ? url : `https://${url}`;\n window.open(href, \"_blank\", \"noopener,noreferrer\");\n }\n}\n\n/**\n * Widget to render a clickable link in the editor\n */\nclass LinkWidget extends WidgetType {\n readonly url: string;\n readonly text: string;\n\n constructor(url: string, text: string) {\n super();\n this.url = url;\n this.text = text;\n }\n\n eq(other: LinkWidget) {\n return other.url === this.url && other.text === this.text;\n }\n\n toDOM() {\n const link = document.createElement(\"a\");\n link.href = this.url;\n link.textContent = this.text;\n link.className = \"cm-link\";\n link.title = this.url;\n\n // Clicks work as on any other link in the editor: a plain click puts\n // the caret there (which shows the markup), and Mod-click opens it \u2014 see\n // modClick below. The browser mustn't follow the href itself.\n link.addEventListener(\"click\", (e) => e.preventDefault());\n\n return link;\n }\n\n ignoreEvent() {\n // hand every event to the editor, so it places the caret and modClick\n // sees the Mod-click.\n return false;\n }\n}\n\nfunction getLinks(view: EditorView) {\n const widgets: Range<Decoration>[] = [];\n const { state } = view;\n const selection = state.selection.main;\n\n for (let { from, to } of view.visibleRanges) {\n syntaxTree(state).iterate({\n from,\n to,\n enter: (node) => {\n // `<https://\u2026>`, `<fax:\u2026>`, `<a@b.c>`: the url, without its brackets\n if (node.name === \"Autolink\") {\n const url = node.node.getChild(\"URL\");\n const inside =\n selection.from >= node.from && selection.from <= node.to;\n const spans = selection.from < node.from && selection.to > node.to;\n if (url && !inside && !spans) {\n const text = state.doc.sliceString(url.from, url.to);\n const href =\n /^[a-z][a-z0-9+.-]*:/i.test(text) || !text.includes(\"@\")\n ? text\n : `mailto:${text}`;\n widgets.push(\n Decoration.replace({ widget: new LinkWidget(href, text) }).range(\n node.from,\n node.to\n )\n );\n }\n return;\n }\n\n // Match markdown link syntax\n if (node.name === \"Link\") {\n const linkFrom = node.from;\n const linkTo = node.to;\n\n // Check if cursor is inside this link\n const cursorInLink =\n selection.from >= linkFrom && selection.from <= linkTo;\n // Check if the link is inside the selection\n const selectionSpansLink =\n selection.from < linkFrom && selection.to > linkTo;\n\n // Replace if cursor is outside the link and there's no selection across the link\n if (!cursorInLink && !selectionSpansLink) {\n const linkText = state.doc.sliceString(linkFrom, linkTo);\n const match = linkText.match(/\\[([^\\]]+)\\]\\(([^)]+)\\)/);\n\n if (match) {\n const [, text, url] = match;\n\n // Replace the entire [text](url) with just the link widget\n const deco = Decoration.replace({\n widget: new LinkWidget(url, text),\n });\n\n widgets.push(deco.range(linkFrom, linkTo));\n }\n }\n }\n },\n });\n }\n\n return Decoration.set(widgets);\n}\n\nconst linkPlugin = ViewPlugin.fromClass(\n class {\n decorations: DecorationSet;\n\n constructor(view: EditorView) {\n this.decorations = getLinks(view);\n }\n\n update(update: ViewUpdate) {\n // Recompute when doc changes, selection moves, or viewport changes\n if (\n update.docChanged ||\n update.selectionSet ||\n update.viewportChanged ||\n syntaxTree(update.startState) !== syntaxTree(update.state)\n ) {\n this.decorations = getLinks(update.view);\n }\n }\n },\n {\n decorations: (v) => v.decorations,\n }\n);\n\ntype SyntaxNode = ReturnType<ReturnType<typeof syntaxTree>[\"resolveInner\"]>;\n\n/**\n * The link at `pos`, if any: an inline `[text](url)`, an autolink `<url>`, a\n * GFM bare url, or the url in a `[ref]: url` definition. `from`/`to` cover\n * the whole link as written. Reference-style `[text][ref]` links have no url\n * of their own and are skipped.\n */\nexport function linkAt(\n state: EditorState,\n pos: number\n): { from: number; to: number; url: string } | null {\n const slice = (n: SyntaxNode) => state.doc.sliceString(n.from, n.to);\n for (const side of [1, -1] as const) {\n for (\n let node: SyntaxNode | null = syntaxTree(state).resolveInner(pos, side);\n node;\n node = node.parent\n ) {\n const inLink = node.parent?.name === \"Link\" || node.parent?.name === \"Autolink\";\n if (node.name === \"URL\" && !inLink)\n return { from: node.from, to: node.to, url: slice(node) };\n if (node.name === \"Link\" || node.name === \"Autolink\") {\n const url = node.getChild(\"URL\");\n return url ? { from: node.from, to: node.to, url: slice(url) } : null;\n }\n }\n }\n return null;\n}\n\nexport const urlAt = (state: EditorState, pos: number) =>\n linkAt(state, pos)?.url ?? null;\n\n/**\n * The link actually under the pointer. `posAndSideAtCoords` snaps to the\n * nearest position, so without the distance check the blank space past the\n * end of a line would count as hovering a link that ends the line.\n */\nfunction linkUnderPointer(\n view: EditorView,\n x: number,\n y: number,\n target?: EventTarget | null\n) {\n // over a rendered link: its whole width is the link, not just near an edge.\n const widget =\n target instanceof Element ? target.closest(\".cm-link\") : null;\n if (widget && view.contentDOM.contains(widget))\n return linkAt(view.state, view.posAtDOM(widget));\n const hit = view.posAndSideAtCoords({ x, y });\n if (!hit) return null;\n const at = view.coordsAtPos(hit.pos, hit.assoc);\n if (\n !at ||\n y < at.top ||\n y > at.bottom ||\n Math.abs(x - at.left) > view.defaultCharacterWidth * 1.5\n )\n return null;\n return linkAt(view.state, hit.pos);\n}\n\nconst isMac =\n typeof navigator !== \"undefined\" &&\n /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);\nconst modHeld = (e: KeyboardEvent | MouseEvent) =>\n isMac ? e.metaKey : e.ctrlKey;\n\n/** The one link to show as clickable: under the pointer, with Mod held. */\nconst setHover = StateEffect.define<{ from: number; to: number } | null>();\nconst hoverMark = Decoration.mark({ class: \"cm-md-link-hover\" });\n\nconst hoverField = StateField.define<DecorationSet>({\n create: () => Decoration.none,\n update(deco, tr) {\n deco = deco.map(tr.changes);\n for (const e of tr.effects) {\n if (e.is(setHover)) {\n deco = e.value\n ? Decoration.set(hoverMark.range(e.value.from, e.value.to))\n : Decoration.none;\n }\n }\n return deco;\n },\n provide: (f) => EditorView.decorations.from(f),\n});\n\n/**\n * Cmd-click (Ctrl off a Mac) opens the link under the pointer, even while the\n * cursor is inside it and it's showing as raw markdown. While the key is held\n * the hovered link gets `cm-md-link-hover`, so the theme can underline it.\n */\nconst modClick = ViewPlugin.fromClass(\n class {\n pointer: { x: number; y: number; target: EventTarget | null } | null = null;\n mod = false;\n\n constructor(readonly view: EditorView) {}\n\n refresh() {\n const { view } = this;\n const link =\n this.mod && this.pointer\n ? linkUnderPointer(view, this.pointer.x, this.pointer.y, this.pointer.target)\n : null;\n let current: { from: number; to: number } | null = null;\n view.state.field(hoverField).between(0, view.state.doc.length, (from, to) => {\n current = { from, to };\n });\n const same = (a: typeof current, b: typeof current) =>\n a?.from === b?.from && a?.to === b?.to;\n const next = link && { from: link.from, to: link.to };\n if (!same(current, next)) view.dispatch({ effects: setHover.of(next) });\n }\n },\n {\n eventHandlers: {\n mousedown(event, view) {\n if (event.button !== 0 || !modHeld(event)) return false;\n const link = linkUnderPointer(\n view,\n event.clientX,\n event.clientY,\n event.target\n );\n if (!link) return false;\n event.preventDefault();\n openUrl(link.url, view.dom);\n return true;\n },\n mousemove(event) {\n this.pointer = { x: event.clientX, y: event.clientY, target: event.target };\n this.mod = modHeld(event);\n this.refresh();\n },\n mouseleave() {\n this.pointer = null;\n this.refresh();\n },\n keydown(event) {\n this.mod = modHeld(event);\n this.refresh();\n },\n keyup(event) {\n this.mod = modHeld(event);\n this.refresh();\n },\n blur() {\n this.mod = false;\n this.refresh();\n },\n },\n }\n);\n\nexport function markdownLinks() {\n return [linkPlugin, hoverField, modClick, linkTheme];\n}\n", "import { EditorView } from \"@codemirror/view\";\n\nexport const linkTheme = EditorView.baseTheme({\n \".cm-link\": {\n textDecoration: \"underline\",\n // a plain click edits; it's a pointer only while Mod is held (below).\n // the widget replaces the link's text, so the theme's `t.link` highlight\n // (theming/src/codemirror-theme.ts) never reaches it: read the same tokens.\n fontFamily: \"var(--markdown-body-family, var(--text-editor-family-sans))\",\n color: \"var(--syntax-link, #0066cc)\",\n backgroundColor: \"var(--syntax-fill-link)\",\n fontStyle: \"var(--syntax-style-link)\",\n fontWeight: \"var(--syntax-weight-link)\",\n \"&:hover\": {\n color: \"inherit\",\n },\n },\n \".cm-md-link-hover, .cm-md-link-hover *\": {\n textDecoration: \"underline\",\n cursor: \"var(--studio-cursor-pointer, pointer)\",\n },\n});\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n EditorView,\n Decoration,\n WidgetType,\n ViewPlugin,\n ViewUpdate,\n type DecorationSet,\n} from \"@codemirror/view\";\nimport {\n type EditorState,\n Range,\n StateEffect,\n StateField,\n} from \"@codemirror/state\";\nimport { syntaxTree } from \"@codemirror/language\";\nimport { isValidAutomergeUrl } from \"@automerge/automerge-repo/slim\";\nimport { linkTheme } from \"../themes/links.ts\";\n\n/**\n * Open a link from the editor. Automerge urls open inside Patchwork rather\n * than as a browser URL (automerge: isn't navigable); bubbles/composed so the\n * event escapes the editor and any shadow root up to whoever drives selection.\n */\nexport function openUrl(url: string, from: Element) {\n if (isValidAutomergeUrl(url)) {\n from.dispatchEvent(\n new CustomEvent(\"patchwork:open-document\", {\n detail: { url },\n bubbles: true,\n composed: true,\n }),\n );\n } else {\n // GFM autolinks `www.example.com` with no scheme.\n const href = /^[a-z][a-z0-9+.-]*:/i.test(url) ? url : `https://${url}`;\n window.open(href, \"_blank\", \"noopener,noreferrer\");\n }\n}\n\n/**\n * Widget to render a clickable link in the editor\n */\nclass LinkWidget extends WidgetType {\n readonly url: string;\n readonly text: string;\n /** the bold/italic/strikethrough the link sits in (see `inheritedStyle`) */\n readonly style: string;\n\n constructor(url: string, text: string, style = \"\") {\n super();\n this.url = url;\n this.text = text;\n this.style = style;\n }\n\n eq(other: LinkWidget) {\n return (\n other.url === this.url &&\n other.text === this.text &&\n other.style === this.style\n );\n }\n\n toDOM() {\n const link = document.createElement(\"a\");\n link.href = this.url;\n link.textContent = this.text;\n link.className = \"cm-link\" + (this.style ? \" \" + this.style : \"\");\n link.title = this.url;\n\n // Clicks work as on any other link in the editor: a plain click puts\n // the caret there (which shows the markup), and Mod-click opens it \u2014 see\n // modClick below. The browser mustn't follow the href itself.\n link.addEventListener(\"click\", (e) => e.preventDefault());\n\n return link;\n }\n\n ignoreEvent() {\n // hand every event to the editor, so it places the caret and modClick\n // sees the Mod-click.\n return false;\n }\n}\n\ntype SyntaxNode = ReturnType<ReturnType<typeof syntaxTree>[\"resolveInner\"]>;\n\n/**\n * Classes for the inline styling around a link. The widget replaces text the\n * highlighter splits into several spans (the brackets, the text, the url), so\n * no single bold or italic span covers it to wrap it \u2014 without these a link\n * inside `**\u2026**` or `*\u2026*` would come out plain. (A heading's size and weight\n * reach it through the heading line's class instead; see margin.ts.)\n */\nfunction inheritedStyle(node: SyntaxNode) {\n const classes = new Set<string>();\n for (let n = node.parent; n; n = n.parent) {\n if (n.name === \"StrongEmphasis\") classes.add(\"cm-md-strong\");\n else if (n.name === \"Emphasis\") classes.add(\"cm-md-em\");\n else if (n.name === \"Strikethrough\") classes.add(\"cm-md-strike\");\n }\n return [...classes].join(\" \");\n}\n\nfunction getLinks(view: EditorView) {\n const widgets: Range<Decoration>[] = [];\n const { state } = view;\n const selection = state.selection.main;\n\n for (let { from, to } of view.visibleRanges) {\n syntaxTree(state).iterate({\n from,\n to,\n enter: (node) => {\n // `<https://\u2026>`, `<fax:\u2026>`, `<a@b.c>`: the url, without its brackets\n if (node.name === \"Autolink\") {\n const url = node.node.getChild(\"URL\");\n const inside =\n selection.from >= node.from && selection.from <= node.to;\n const spans = selection.from < node.from && selection.to > node.to;\n if (url && !inside && !spans) {\n const text = state.doc.sliceString(url.from, url.to);\n const href =\n /^[a-z][a-z0-9+.-]*:/i.test(text) || !text.includes(\"@\")\n ? text\n : `mailto:${text}`;\n widgets.push(\n Decoration.replace({\n widget: new LinkWidget(href, text, inheritedStyle(node.node)),\n }).range(\n node.from,\n node.to\n )\n );\n }\n return;\n }\n\n // Match markdown link syntax\n if (node.name === \"Link\") {\n const linkFrom = node.from;\n const linkTo = node.to;\n\n // Check if cursor is inside this link\n const cursorInLink =\n selection.from >= linkFrom && selection.from <= linkTo;\n // Check if the link is inside the selection\n const selectionSpansLink =\n selection.from < linkFrom && selection.to > linkTo;\n\n // Replace if cursor is outside the link and there's no selection across the link\n if (!cursorInLink && !selectionSpansLink) {\n const linkText = state.doc.sliceString(linkFrom, linkTo);\n const match = linkText.match(/\\[([^\\]]+)\\]\\(([^)]+)\\)/);\n\n if (match) {\n const [, text, url] = match;\n\n // Replace the entire [text](url) with just the link widget\n const deco = Decoration.replace({\n widget: new LinkWidget(url, text, inheritedStyle(node.node)),\n });\n\n widgets.push(deco.range(linkFrom, linkTo));\n }\n }\n }\n },\n });\n }\n\n return Decoration.set(widgets);\n}\n\nconst linkPlugin = ViewPlugin.fromClass(\n class {\n decorations: DecorationSet;\n\n constructor(view: EditorView) {\n this.decorations = getLinks(view);\n }\n\n update(update: ViewUpdate) {\n // Recompute when doc changes, selection moves, or viewport changes\n if (\n update.docChanged ||\n update.selectionSet ||\n update.viewportChanged ||\n syntaxTree(update.startState) !== syntaxTree(update.state)\n ) {\n this.decorations = getLinks(update.view);\n }\n }\n },\n {\n decorations: (v) => v.decorations,\n }\n);\n\n\n/**\n * The link at `pos`, if any: an inline `[text](url)`, an autolink `<url>`, a\n * GFM bare url, or the url in a `[ref]: url` definition. `from`/`to` cover\n * the whole link as written. Reference-style `[text][ref]` links have no url\n * of their own and are skipped.\n */\nexport function linkAt(\n state: EditorState,\n pos: number\n): { from: number; to: number; url: string } | null {\n const slice = (n: SyntaxNode) => state.doc.sliceString(n.from, n.to);\n for (const side of [1, -1] as const) {\n for (\n let node: SyntaxNode | null = syntaxTree(state).resolveInner(pos, side);\n node;\n node = node.parent\n ) {\n const inLink = node.parent?.name === \"Link\" || node.parent?.name === \"Autolink\";\n if (node.name === \"URL\" && !inLink)\n return { from: node.from, to: node.to, url: slice(node) };\n if (node.name === \"Link\" || node.name === \"Autolink\") {\n const url = node.getChild(\"URL\");\n return url ? { from: node.from, to: node.to, url: slice(url) } : null;\n }\n }\n }\n return null;\n}\n\nexport const urlAt = (state: EditorState, pos: number) =>\n linkAt(state, pos)?.url ?? null;\n\n/**\n * The link actually under the pointer. `posAndSideAtCoords` snaps to the\n * nearest position, so without the distance check the blank space past the\n * end of a line would count as hovering a link that ends the line.\n */\nfunction linkUnderPointer(\n view: EditorView,\n x: number,\n y: number,\n target?: EventTarget | null\n) {\n // over a rendered link: its whole width is the link, not just near an edge.\n const widget =\n target instanceof Element ? target.closest(\".cm-link\") : null;\n if (widget && view.contentDOM.contains(widget))\n return linkAt(view.state, view.posAtDOM(widget));\n const hit = view.posAndSideAtCoords({ x, y });\n if (!hit) return null;\n const at = view.coordsAtPos(hit.pos, hit.assoc);\n if (\n !at ||\n y < at.top ||\n y > at.bottom ||\n Math.abs(x - at.left) > view.defaultCharacterWidth * 1.5\n )\n return null;\n return linkAt(view.state, hit.pos);\n}\n\nconst isMac =\n typeof navigator !== \"undefined\" &&\n /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);\nconst modHeld = (e: KeyboardEvent | MouseEvent) =>\n isMac ? e.metaKey : e.ctrlKey;\n\n/** The one link to show as clickable: under the pointer, with Mod held. */\nconst setHover = StateEffect.define<{ from: number; to: number } | null>();\nconst hoverMark = Decoration.mark({ class: \"cm-md-link-hover\" });\n\nconst hoverField = StateField.define<DecorationSet>({\n create: () => Decoration.none,\n update(deco, tr) {\n deco = deco.map(tr.changes);\n for (const e of tr.effects) {\n if (e.is(setHover)) {\n deco = e.value\n ? Decoration.set(hoverMark.range(e.value.from, e.value.to))\n : Decoration.none;\n }\n }\n return deco;\n },\n provide: (f) => EditorView.decorations.from(f),\n});\n\n/**\n * Cmd-click (Ctrl off a Mac) opens the link under the pointer, even while the\n * cursor is inside it and it's showing as raw markdown. While the key is held\n * the hovered link gets `cm-md-link-hover`, so the theme can underline it.\n */\nconst modClick = ViewPlugin.fromClass(\n class {\n pointer: { x: number; y: number; target: EventTarget | null } | null = null;\n mod = false;\n\n constructor(readonly view: EditorView) {}\n\n refresh() {\n const { view } = this;\n const link =\n this.mod && this.pointer\n ? linkUnderPointer(view, this.pointer.x, this.pointer.y, this.pointer.target)\n : null;\n let current: { from: number; to: number } | null = null;\n view.state.field(hoverField).between(0, view.state.doc.length, (from, to) => {\n current = { from, to };\n });\n const same = (a: typeof current, b: typeof current) =>\n a?.from === b?.from && a?.to === b?.to;\n const next = link && { from: link.from, to: link.to };\n if (!same(current, next)) view.dispatch({ effects: setHover.of(next) });\n }\n },\n {\n eventHandlers: {\n mousedown(event, view) {\n if (event.button !== 0 || !modHeld(event)) return false;\n const link = linkUnderPointer(\n view,\n event.clientX,\n event.clientY,\n event.target\n );\n if (!link) return false;\n event.preventDefault();\n openUrl(link.url, view.dom);\n return true;\n },\n mousemove(event) {\n this.pointer = { x: event.clientX, y: event.clientY, target: event.target };\n this.mod = modHeld(event);\n this.refresh();\n },\n mouseleave() {\n this.pointer = null;\n this.refresh();\n },\n keydown(event) {\n this.mod = modHeld(event);\n this.refresh();\n },\n keyup(event) {\n this.mod = modHeld(event);\n this.refresh();\n },\n blur() {\n this.mod = false;\n this.refresh();\n },\n },\n }\n);\n\nexport function markdownLinks() {\n return [linkPlugin, hoverField, modClick, linkTheme];\n}\n", "import { EditorView } from \"@codemirror/view\";\n\nexport const linkTheme = EditorView.baseTheme({\n \".cm-link\": {\n textDecoration: \"underline\",\n // a plain click edits; it's a pointer only while Mod is held (below).\n // the widget replaces the link's text, so the theme's `t.link` highlight\n // (theming/src/codemirror-theme.ts) never reaches it: read the same tokens.\n fontFamily: \"var(--markdown-body-family, var(--text-editor-family-sans))\",\n color: \"var(--syntax-link, #0066cc)\",\n backgroundColor: \"var(--syntax-fill-link)\",\n // weight and style come from where the link sits \u2014 a heading, bold or\n // italic text \u2014 so they're inherited, not set (see the rules below)\n \"&:hover\": {\n color: \"inherit\",\n },\n },\n // in a heading, the heading's own font (its line class sets it)\n \".cm-md-h1 .cm-link, .cm-md-h2 .cm-link, .cm-md-h3 .cm-link, .cm-md-h4 .cm-link, .cm-md-h5 .cm-link, .cm-md-h6 .cm-link\":\n {\n fontFamily: \"inherit\",\n },\n // the inline styling the link sits in (links.ts: inheritedStyle)\n \".cm-link.cm-md-strong\": {\n fontWeight: \"var(--syntax-weight-strong, bold)\",\n },\n \".cm-link.cm-md-em\": {\n fontStyle: \"var(--syntax-style-emphasis, italic)\",\n },\n \".cm-link.cm-md-strike\": {\n textDecoration: \"underline line-through\",\n },\n \".cm-md-link-hover, .cm-md-link-hover *\": {\n textDecoration: \"underline\",\n cursor: \"var(--studio-cursor-pointer, pointer)\",\n },\n});\n"],
5
+ "mappings": "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",
6
6
  "names": ["EditorView", "EditorView"]
7
7
  }
@@ -10,7 +10,7 @@
10
10
  console.error("vite-plugin-css-injected-by-js", e);
11
11
  }
12
12
  })();
13
- import { use, insert, createComponent, setAttribute, getOwner, effect, template, delegateEvents, className, memo, render } from "solid-js/web";
13
+ import { insert, createComponent, use, setAttribute, getOwner, effect, template, delegateEvents, className, memo, render } from "solid-js/web";
14
14
  import { onCleanup, createMemo, createContext, useContext, createResource, createEffect, createSignal, onMount, createComputed, on, Show, For } from "solid-js";
15
15
  import { createStore, reconcile, produce } from "solid-js/store";
16
16
  import * as Providers from "@inkandswitch/patchwork-providers";
@@ -1152,27 +1152,18 @@ function CommentsView(props) {
1152
1152
  const [focusDoc, focusHandle] = subscribeDoc(props.element, {
1153
1153
  type: "patchwork:focus"
1154
1154
  });
1155
- let panel;
1156
1155
  const viewId = `comments-view-${Math.random().toString(36).slice(2)}`;
1157
- const [onScreen, setOnScreen] = createSignal(false);
1158
- onMount(() => {
1159
- const observer = new IntersectionObserver(([entry]) => setOnScreen(!!entry && entry.isIntersecting && entry.intersectionRect.width > 0));
1160
- observer.observe(panel);
1161
- onCleanup(() => observer.disconnect());
1162
- });
1163
- const listed = (shown) => {
1156
+ createEffect(() => {
1164
1157
  const handle = focusHandle();
1165
1158
  if (!handle) return;
1166
- const already = !!handle.doc()?.commentViews?.[viewId];
1167
- if (already === shown) return;
1168
1159
  handle.change((doc) => {
1169
1160
  if (!doc.commentViews) doc.commentViews = {};
1170
- if (shown) doc.commentViews[viewId] = true;
1171
- else delete doc.commentViews[viewId];
1161
+ doc.commentViews[viewId] = true;
1172
1162
  });
1173
- };
1174
- createEffect(() => listed(onScreen()));
1175
- onCleanup(() => listed(false));
1163
+ onCleanup(() => handle.change((doc) => {
1164
+ if (doc.commentViews) delete doc.commentViews[viewId];
1165
+ }));
1166
+ });
1176
1167
  const [, contactHandle] = subscribeDoc(props.element, {
1177
1168
  type: "patchwork:contact"
1178
1169
  });
@@ -1245,21 +1236,18 @@ function CommentsView(props) {
1245
1236
  });
1246
1237
  });
1247
1238
  let pickedHere;
1248
- createEffect(on([primaryThreadUrl, onScreen], ([url, shown], previous) => {
1249
- if (!url || !shown) return;
1250
- const [wasUrl, wasShown] = previous ?? [void 0, false];
1251
- if (url === wasUrl && wasShown) return;
1252
- if (url === pickedHere) {
1239
+ const [flashDue, setFlashDue] = createSignal();
1240
+ createEffect(on(primaryThreadUrl, (url) => {
1241
+ if (url && url === pickedHere) {
1253
1242
  pickedHere = void 0;
1254
1243
  return;
1255
1244
  }
1256
- let tries = 30;
1257
- const flash = () => {
1258
- const card = panel.querySelector(`.comments-thread[data-thread-url="${CSS.escape(url)}"]`);
1259
- if (!card) {
1260
- if (--tries > 0) frame = requestAnimationFrame(flash);
1261
- return;
1262
- }
1245
+ setFlashDue(url);
1246
+ }));
1247
+ const flashIfDue = (card, threadUrl) => {
1248
+ createEffect(() => {
1249
+ if (flashDue() !== threadUrl) return;
1250
+ setFlashDue(void 0);
1263
1251
  card.scrollIntoView({
1264
1252
  block: "nearest",
1265
1253
  behavior: "smooth"
@@ -1267,13 +1255,8 @@ function CommentsView(props) {
1267
1255
  card.removeAttribute("data-flash");
1268
1256
  void card.offsetWidth;
1269
1257
  card.setAttribute("data-flash", "");
1270
- };
1271
- let frame = 0;
1272
- flash();
1273
- onCleanup(() => cancelAnimationFrame(frame));
1274
- }, {
1275
- defer: true
1276
- }));
1258
+ });
1259
+ };
1277
1260
  const onSelectThread = (threadUrl, targetUrls) => {
1278
1261
  const handle = focusHandle();
1279
1262
  if (!handle) return;
@@ -1313,8 +1296,6 @@ function CommentsView(props) {
1313
1296
  return (() => {
1314
1297
  var _el$ = _tmpl$2$1(), _el$2 = _el$.firstChild;
1315
1298
  _el$2.firstChild;
1316
- var _ref$ = panel;
1317
- typeof _ref$ === "function" ? use(_ref$, _el$) : panel = _el$;
1318
1299
  insert(_el$2, createComponent(Show, {
1319
1300
  get when() {
1320
1301
  return canAddComment();
@@ -1350,6 +1331,7 @@ function CommentsView(props) {
1350
1331
  _el$6.addEventListener("animationend", (e) => {
1351
1332
  if (e.animationName === "comments-thread-flash") e.currentTarget.removeAttribute("data-flash");
1352
1333
  });
1334
+ use((card) => flashIfDue(card, threadUrl), _el$6);
1353
1335
  setAttribute(_el$6, "data-thread-url", threadUrl);
1354
1336
  _el$7.docUrl = threadUrl;
1355
1337
  _el$7._$owner = getOwner();
@@ -2988,4 +2970,4 @@ export {
2988
2970
  renderCommentThread,
2989
2971
  renderCommentsView
2990
2972
  };
2991
- //# sourceMappingURL=main-DnOX-fXU.js.map
2973
+ //# sourceMappingURL=main-DcxRp9fz.js.map