From 6f3c126fda32ea29c5445c8e918f97a1026e53e1 Mon Sep 17 00:00:00 2001 From: Phil LaFayette Date: Fri, 26 Jun 2026 15:37:11 -0500 Subject: [PATCH] Fix @mention styling inside bold/italic + 3 audit loose ends (#55 follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRIMARY: @mentions inside inline emphasis lost their mention styling. MessageBubble.parseBody emitted bold/italic/strike tokens whose inner `value` was raw text never run through the mention/link splitter, so an @mention in **bold** or *italic* rendered with emphasis styling but no mention pill/color. Extract the mention+link classification into a shared `splitTextToSegments` helper, run it over emphasis inner values, and render the resulting children INSIDE the emphasis wrapper via a recursive `renderSeg` snippet — mentions/links now inherit emphasis styling AND get their own chip/link styling. Code spans stay literal (never linkified). `hasSelfMention` now recurses into emphasis children for the border accent. SECONDARY (audit follow-ups): - ChannelModal isPrivate wired end-to-end: App -> store.createChannel(id, topic, isPrivate). The create API takes no visibility arg, so createChannel sets the local row + retained meta to private AND persists via the admin path (setVisibility -> comms_conversation_update), mirroring ChannelAdminPanel. #handleMeta now reads visibility from the broadcast. - ArtifactDetailHeader Compare dropdown closes on outside mousedown via a guard, mirroring ArtifactPanel's primary-dropdown pattern. - safeStorage DRY: extracted the byte-identical copies from mqtt-store and resizable-panel into a shared web/src/lib/safe-storage.js. Tests: +11 (bold/italic/strike/plain mention chips + code-not-linkified; createChannel private/public/queued; compare-dropdown outside-click). 1314 -> 1325 tests, all green; build green. --- web/src/App.svelte | 2 +- .../components/ArtifactDetailHeader.svelte | 21 ++ web/src/components/MessageBubble.svelte | 184 +++++++++++------- web/src/lib/mqtt-store.svelte.js | 79 +++++--- web/src/lib/resizable-panel.svelte.js | 30 +-- web/src/lib/safe-storage.js | 41 ++++ ...act-compare-dropdown-outside-click.spec.js | 102 ++++++++++ web/tests/channel-modal-wires.spec.js | 7 +- .../message-bubble-mention-emphasis.spec.js | 162 +++++++++++++++ web/tests/mqtt-store-admin-actions.spec.js | 59 ++++++ 10 files changed, 556 insertions(+), 131 deletions(-) create mode 100644 web/src/lib/safe-storage.js create mode 100644 web/tests/artifact-compare-dropdown-outside-click.spec.js create mode 100644 web/tests/message-bubble-mention-emphasis.spec.js diff --git a/web/src/App.svelte b/web/src/App.svelte index 88f99ab..cb83b6d 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -1114,7 +1114,7 @@ {#if showChannelModal} showChannelModal = false} - onCreate={(id, topic) => { store.createChannel(id, topic); showChannelModal = false; }} + onCreate={(id, topic, isPrivate) => { store.createChannel(id, topic, isPrivate); showChannelModal = false; }} /> {/if} diff --git a/web/src/components/ArtifactDetailHeader.svelte b/web/src/components/ArtifactDetailHeader.svelte index 4ae76ab..1b7515f 100644 --- a/web/src/components/ArtifactDetailHeader.svelte +++ b/web/src/components/ArtifactDetailHeader.svelte @@ -144,6 +144,25 @@ showCompareDropdown = !showCompareDropdown; } + /** + * Close the Compare dropdown on an outside click. The Compare dropdown's + * open state (`showCompareDropdown`) is owned locally in this component + * (unlike the primary dropdown, whose state lives in ArtifactPanel and is + * closed by that file's window guard), so the outside-click listener + * belongs here too. Mirrors ArtifactPanel.handleVersionDropdownOutsideClick: + * the trigger + listbox both live inside + * `[data-testid="compare-version-selector"]`, so a mousedown anywhere + * outside that subtree closes the dropdown. + * @param {MouseEvent} e + */ + function handleCompareDropdownOutsideClick(e) { + if (!showCompareDropdown) return; + const node = e.target; + const el = node instanceof Element ? node : (node?.parentElement ?? null); + if (el && el.closest('[data-testid="compare-version-selector"]')) return; + showCompareDropdown = false; + } + // Diff mode is disabled when there is only a single version to compare. let diffDisabled = $derived(!hasMultipleVersions); @@ -325,6 +344,8 @@ ); + +