From d0f37e840d07b08564c7756bc333c2b455abd305 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 8 Jan 2026 23:04:09 +0100 Subject: [PATCH 1/8] Add tag display mode toggle for full hierarchical view Adds an eye icon button in the tag editor that toggles between: - Auto mode: current behavior, shortens tags when >2 rows - Full mode: shows complete tag names, one per row, max 50vh with scroll Features: - Middle truncation for long tags that adapts to editor width - Keyboard shortcut: Ctrl+. - Setting persists via collection config Co-Authored-By: Claude Opus 4.5 --- ftl/core/editing.ftl | 1 + qt/aqt/editor.py | 5 + ts/editor/NoteEditor.svelte | 19 ++- ts/lib/tag-editor/TagDisplayModeButton.svelte | 57 ++++++++ ts/lib/tag-editor/TagEditMode.svelte | 4 + ts/lib/tag-editor/TagEditor.svelte | 91 +++++++++++-- ts/lib/tag-editor/TagWithTooltip.svelte | 124 +++++++++++++++++- .../tag-options-button/TagAddButton.svelte | 8 +- 8 files changed, 291 insertions(+), 18 deletions(-) create mode 100644 ts/lib/tag-editor/TagDisplayModeButton.svelte diff --git a/ftl/core/editing.ftl b/ftl/core/editing.ftl index 3aacb974673..28bef77dc81 100644 --- a/ftl/core/editing.ftl +++ b/ftl/core/editing.ftl @@ -69,6 +69,7 @@ editing-warning-cloze-deletions-will-not-work = Warning, cloze deletions will no editing-mathjax-preview = MathJax Preview editing-shrink-images = Shrink Images editing-close-html-tags = Auto-close HTML tags +editing-tag-display-toggle = Toggle full tag display editing-from-clipboard = From Clipboard editing-alignment = Alignment editing-equations = Equations diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index e86f5fe96c0..661c6a55f68 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -501,6 +501,10 @@ def onBridgeCmd(self, cmd: str) -> Any: collapsed = collapsed_string == "true" self.setTagsCollapsed(collapsed) + elif cmd.startswith("setTagDisplayFull"): + (type, value) = cmd.split(":", 1) + self.mw.col.set_config("tagDisplayFull", value == "true") + elif cmd.startswith("editorState"): (_, new_state_id, old_state_id) = cmd.split(":", 2) self.signal_state_change( @@ -607,6 +611,7 @@ def oncallback(arg: Any) -> None: setMathjaxEnabled({json.dumps(self.mw.col.get_config("renderMathjax", True))}); setShrinkImages({json.dumps(self.mw.col.get_config("shrinkEditorImages", True))}); setCloseHTMLTags({json.dumps(self.mw.col.get_config("closeHTMLTags", True))}); + setTagDisplayFull({json.dumps(self.mw.col.get_config("tagDisplayFull", False))}); triggerChanges(); """ diff --git a/ts/editor/NoteEditor.svelte b/ts/editor/NoteEditor.svelte index 17ced575bc3..989dc20f8e5 100644 --- a/ts/editor/NoteEditor.svelte +++ b/ts/editor/NoteEditor.svelte @@ -222,6 +222,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html $tagsCollapsed = collapsed; } + const tagDisplayFull = writable(false); + export function setTagDisplayFull(full: boolean): void { + $tagDisplayFull = full; + } + + function toggleTagDisplayMode(): void { + $tagDisplayFull = !$tagDisplayFull; + bridgeCommand(`setTagDisplayFull:${$tagDisplayFull}`); + } + function updateTagsCollapsed(collapsed: boolean) { $tagsCollapsed = collapsed; bridgeCommand(`setTagsCollapsed:${$tagsCollapsed}`); @@ -596,6 +606,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html setMathjaxEnabled, setShrinkImages, setCloseHTMLTags, + setTagDisplayFull, triggerChanges, setIsImageOcclusion, setupMaskEditor, @@ -802,6 +813,7 @@ the AddCards dialog) should be implemented in the user of this component. updateTagsCollapsed(false); }} /> + 0 ? tagAmount : ""} ${tr.editingTags()}`} - + {/if} diff --git a/ts/lib/tag-editor/TagDisplayModeButton.svelte b/ts/lib/tag-editor/TagDisplayModeButton.svelte new file mode 100644 index 00000000000..a6592d44450 --- /dev/null +++ b/ts/lib/tag-editor/TagDisplayModeButton.svelte @@ -0,0 +1,57 @@ + + + + +
+ + + +
+ + diff --git a/ts/lib/tag-editor/TagEditMode.svelte b/ts/lib/tag-editor/TagEditMode.svelte index 190e926f8b0..f6732f70a3d 100644 --- a/ts/lib/tag-editor/TagEditMode.svelte +++ b/ts/lib/tag-editor/TagEditMode.svelte @@ -17,6 +17,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let selected: boolean; export let active: boolean; export let shorten: boolean; + export let truncateMiddle: boolean = false; + export let editorWidth: number = 0; export let flash: () => void; @@ -34,6 +36,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {selected} {active} {shorten} + {truncateMiddle} + {editorWidth} {flash} on:tagrange on:tagselect diff --git a/ts/lib/tag-editor/TagEditor.svelte b/ts/lib/tag-editor/TagEditor.svelte index eb033ef7a76..71bf4cb2db8 100644 --- a/ts/lib/tag-editor/TagEditor.svelte +++ b/ts/lib/tag-editor/TagEditor.svelte @@ -6,13 +6,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { completeTag } from "@generated/backend"; import { tagActionsShortcutsKey } from "@tslib/context-keys"; import { isArrowDown, isArrowUp } from "@tslib/keys"; - import { createEventDispatcher, setContext, tick } from "svelte"; + import { createEventDispatcher, onDestroy, setContext, tick } from "svelte"; import type { Writable } from "svelte/store"; import { writable } from "svelte/store"; import Shortcut from "$lib/components/Shortcut.svelte"; import { execCommand } from "$lib/domlib"; + import TagDisplayModeButton from "./TagDisplayModeButton.svelte"; import { TagOptionsButton } from "./tag-options-button"; import TagEditMode from "./TagEditMode.svelte"; import TagInput from "./TagInput.svelte"; @@ -28,6 +29,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let tags: Writable; export let keyCombination: string = "Control+Shift+T"; + export let displayFull: boolean = false; const selectAllShortcut = "Control+A"; const copyShortcut = "Control+C"; @@ -388,8 +390,40 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // typically correct for rows < 7 $: assumedRows = Math.floor(height / badgeHeight); - $: shortenTags = shortenTags || assumedRows > 2; + $: shortenTags = displayFull ? false : shortenTags || assumedRows > 2; $: anyTagsSelected = tagTypes.some((tag) => tag.selected); + + // Track editor width for tag truncation + let editorElement: HTMLDivElement; + let editorWidth: number = 0; + let resizeObserver: ResizeObserver | null = null; + let resizeTimeout: ReturnType | null = null; + + function updateEditorWidth(): void { + if (editorElement) { + editorWidth = editorElement.clientWidth; + } + } + + function debouncedUpdateWidth(): void { + if (resizeTimeout) { + clearTimeout(resizeTimeout); + } + resizeTimeout = setTimeout(updateEditorWidth, 32); + } + + $: if (editorElement && !resizeObserver) { + resizeObserver = new ResizeObserver(debouncedUpdateWidth); + resizeObserver.observe(editorElement); + updateEditorWidth(); + } + + onDestroy(() => { + resizeObserver?.disconnect(); + if (resizeTimeout) { + clearTimeout(resizeTimeout); + } + }); {#if anyTagsSelected} @@ -398,17 +432,27 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {/if} -
- +
+
+ + + +
{#each tagTypes as tag, index (tag.id)}
@@ -418,6 +462,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html tooltip={tag.name} active={index === active} shorten={shortenTags} + truncateMiddle={displayFull} + {editorWidth} bind:flash={tag.flash} bind:selected={tag.selected} on:tagedit={() => { @@ -516,6 +562,25 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html outline-offset: -1px; outline: 2px solid var(--border-focus); } + + &.display-full { + max-height: 50vh; + overflow-y: auto; + + .tag-header { + flex: 0 0 auto; + } + + .tag-relative { + flex: 1 1 100%; + } + } + } + + .tag-header { + display: flex; + align-items: center; + margin-right: 0.75rem; } .tag-relative { diff --git a/ts/lib/tag-editor/TagWithTooltip.svelte b/ts/lib/tag-editor/TagWithTooltip.svelte index 3c648554573..31e6ea18ca9 100644 --- a/ts/lib/tag-editor/TagWithTooltip.svelte +++ b/ts/lib/tag-editor/TagWithTooltip.svelte @@ -21,9 +21,112 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let selected: boolean; export let active: boolean; export let shorten: boolean; + export let truncateMiddle: boolean = false; + export let editorWidth: number = 0; export let flash: () => void; + let displayName = name; + let needsTooltip = false; + let tagButton: HTMLButtonElement | null = null; + let cachedFont: string | undefined; + let cachedEllipsisWidth: number | undefined; + + const ELLIPSIS = "…"; + const PADDING_PX = 60; // Account for padding, delete badge, etc. + + // Canvas for text measurement - no DOM thrashing + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d")!; + + function getTextWidth(text: string): number { + return ctx.measureText(text).width; + } + + function fitText(): void { + if (!truncateMiddle || !tagButton || editorWidth <= 0) { + displayName = name; + needsTooltip = false; + return; + } + + // Keep font correct (can change via theme/class/parent) + const font = getComputedStyle(tagButton).font; + if (cachedFont !== font) { + cachedFont = font; + cachedEllipsisWidth = undefined; + } + ctx.font = cachedFont; + + const maxW = editorWidth - PADDING_PX; + if (maxW <= 0) { + displayName = ELLIPSIS; + needsTooltip = true; + return; + } + + const fullWidth = getTextWidth(name); + if (fullWidth <= maxW) { + displayName = name; + needsTooltip = false; + return; + } + + const ellipsisWidth = + cachedEllipsisWidth ?? (cachedEllipsisWidth = getTextWidth(ELLIPSIS)); + + if (ellipsisWidth > maxW) { + displayName = ELLIPSIS; + needsTooltip = true; + return; + } + + const build = (k: number): string => { + const kk = Math.max(0, Math.min(k, name.length)); + const head = Math.ceil(kk / 2); + const tail = kk - head; + return ( + name.slice(0, head) + + ELLIPSIS + + (tail > 0 ? name.slice(name.length - tail) : "") + ); + }; + + // Find maximum k such that build(k) fits in maxW + let lo = 0; + let hi = name.length; + let bestK = 0; + + while (lo <= hi) { + const mid = (lo + hi) >> 1; + const candidate = build(mid); + if (getTextWidth(candidate) <= maxW) { + bestK = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + + displayName = build(bestK); + needsTooltip = true; + } + + function onTagMount(event: CustomEvent<{ button: HTMLButtonElement }>): void { + tagButton = event.detail.button; + cachedFont = undefined; // Reset to get fresh font on mount + cachedEllipsisWidth = undefined; + fitText(); + } + + // Re-fit when name, truncateMiddle, or editorWidth changes + $: { + name; + truncateMiddle; + editorWidth; + fitText(); + } + const dispatch = createEventDispatcher(); let control = false; @@ -94,6 +197,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + {:else if needsTooltip} + + { + createTooltip(event.detail.button); + onTagMount(event); + }} + > + {displayName} + + + {:else} - {name} + {displayName} {/if} diff --git a/ts/lib/tag-editor/tag-options-button/TagAddButton.svelte b/ts/lib/tag-editor/tag-options-button/TagAddButton.svelte index fda953b4482..f5a3d0014f4 100644 --- a/ts/lib/tag-editor/tag-options-button/TagAddButton.svelte +++ b/ts/lib/tag-editor/tag-options-button/TagAddButton.svelte @@ -38,9 +38,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - - - + {#if $$slots.default} + + + + {/if}
dispatch("tagappend")} /> From 6a28e641d37a4379247df45d27e8930321ec8e50 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 8 Jan 2026 23:18:17 +0100 Subject: [PATCH 2/8] Update icons and tooltip text --- ftl/core/editing.ftl | 3 ++- ts/lib/components/icons.ts | 6 ++++++ ts/lib/tag-editor/TagDisplayModeButton.svelte | 16 ++++++++++------ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/ftl/core/editing.ftl b/ftl/core/editing.ftl index 28bef77dc81..307d1661211 100644 --- a/ftl/core/editing.ftl +++ b/ftl/core/editing.ftl @@ -69,7 +69,8 @@ editing-warning-cloze-deletions-will-not-work = Warning, cloze deletions will no editing-mathjax-preview = MathJax Preview editing-shrink-images = Shrink Images editing-close-html-tags = Auto-close HTML tags -editing-tag-display-toggle = Toggle full tag display +editing-tags-show-full = Show full tags +editing-tags-show-compact = Show compact tags editing-from-clipboard = From Clipboard editing-alignment = Alignment editing-equations = Equations diff --git a/ts/lib/components/icons.ts b/ts/lib/components/icons.ts index ab07cbf171d..46d582e8cf2 100644 --- a/ts/lib/components/icons.ts +++ b/ts/lib/components/icons.ts @@ -141,8 +141,12 @@ import Update_ from "@mdi/svg/svg/update.svg?component"; import update_ from "@mdi/svg/svg/update.svg?url"; import VectorPolygonVariant_ from "@mdi/svg/svg/vector-polygon-variant.svg?component"; import vectorPolygonVariant_ from "@mdi/svg/svg/vector-polygon-variant.svg?url"; +import ViewCompact_ from "@mdi/svg/svg/view-compact.svg?component"; +import viewCompact_ from "@mdi/svg/svg/view-compact.svg?url"; import ViewDashboard_ from "@mdi/svg/svg/view-dashboard.svg?component"; import viewDashboard_ from "@mdi/svg/svg/view-dashboard.svg?url"; +import ViewList_ from "@mdi/svg/svg/view-list.svg?component"; +import viewList_ from "@mdi/svg/svg/view-list.svg?url"; import Revert_ from "bootstrap-icons/icons/arrow-counterclockwise.svg?component"; import revert_ from "bootstrap-icons/icons/arrow-counterclockwise.svg?url"; import ArrowLeft_ from "bootstrap-icons/icons/arrow-left.svg?component"; @@ -283,5 +287,7 @@ export const mdiUndo = { url: undo_, component: Undo_ }; export const mdiUnfoldMoreHorizontal = { url: unfoldMoreHorizontal_, component: UnfoldMoreHorizontal_ }; export const mdiUngroup = { url: ungroup_, component: Ungroup_ }; export const mdiVectorPolygonVariant = { url: vectorPolygonVariant_, component: VectorPolygonVariant_ }; +export const mdiViewCompact = { url: viewCompact_, component: ViewCompact_ }; +export const mdiViewList = { url: viewList_, component: ViewList_ }; export const incrementClozeIcon = { url: incrementCloze_, component: IncrementCloze_ }; export const mdiEarth = { url: earth_, component: Earth_ }; diff --git a/ts/lib/tag-editor/TagDisplayModeButton.svelte b/ts/lib/tag-editor/TagDisplayModeButton.svelte index a6592d44450..0542172ce2e 100644 --- a/ts/lib/tag-editor/TagDisplayModeButton.svelte +++ b/ts/lib/tag-editor/TagDisplayModeButton.svelte @@ -9,7 +9,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import Icon from "$lib/components/Icon.svelte"; import IconConstrain from "$lib/components/IconConstrain.svelte"; - import { mdiEye } from "$lib/components/icons"; + import { mdiViewCompact, mdiViewList } from "$lib/components/icons"; export let full: boolean = false; export let keyCombination: string = "Control+."; @@ -20,20 +20,25 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html dispatch("displaymodechange"); } - $: title = `${tr.editingTagDisplayToggle()} (${getPlatformString(keyCombination)})`; + $: title = full + ? `${tr.editingTagsShowCompact()} (${getPlatformString(keyCombination)})` + : `${tr.editingTagsShowFull()} (${getPlatformString(keyCombination)})`;
- + {#if full} + + {:else} + + {/if}
@@ -49,8 +54,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html opacity: 0.4; } - &:hover :global(svg), - &.active :global(svg) { + &:hover :global(svg) { opacity: 1; } } From b86af26ca6b7889573a857de0254e9cd290996a4 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Thu, 8 Jan 2026 23:50:19 +0100 Subject: [PATCH 3/8] Fix full mode tag editing and layout stability - Add mousedown preventDefault on toggle button to prevent input blur - Add end-0 to TagInput for proper width in full mode - Add consistent min-height for tag rows and spacer using font-size calc - Hide spacer when editing to prevent layout shift Co-Authored-By: Claude Opus 4.5 --- ts/lib/tag-editor/TagDisplayModeButton.svelte | 1 + ts/lib/tag-editor/TagEditor.svelte | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/ts/lib/tag-editor/TagDisplayModeButton.svelte b/ts/lib/tag-editor/TagDisplayModeButton.svelte index 0542172ce2e..67a49457c55 100644 --- a/ts/lib/tag-editor/TagDisplayModeButton.svelte +++ b/ts/lib/tag-editor/TagDisplayModeButton.svelte @@ -31,6 +31,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {title} role="button" tabindex="-1" + on:mousedown|preventDefault on:click={toggle} > diff --git a/ts/lib/tag-editor/TagEditor.svelte b/ts/lib/tag-editor/TagEditor.svelte index 71bf4cb2db8..75102c04e3d 100644 --- a/ts/lib/tag-editor/TagEditor.svelte +++ b/ts/lib/tag-editor/TagEditor.svelte @@ -493,7 +493,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html > Date: Fri, 9 Jan 2026 00:11:02 +0100 Subject: [PATCH 4/8] Refactor tag display mode: extract shortcut, improve naming - Extract tagDisplayModeShortcut constant to single source of truth - Pass shortcut to TagDisplayModeButton for tooltip display - Rename PADDING_PX to RIGHT_PADDING_PX with detailed comment - Add overflow-x: hidden to prevent horizontal scrollbar in full mode Co-Authored-By: Claude Opus 4.5 --- ts/editor/NoteEditor.svelte | 8 +++++++- ts/lib/tag-editor/TagEditor.svelte | 9 ++++++++- ts/lib/tag-editor/TagWithTooltip.svelte | 5 +++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/ts/editor/NoteEditor.svelte b/ts/editor/NoteEditor.svelte index 989dc20f8e5..a86e3fc67c4 100644 --- a/ts/editor/NoteEditor.svelte +++ b/ts/editor/NoteEditor.svelte @@ -223,6 +223,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } const tagDisplayFull = writable(false); + const tagDisplayModeShortcut = "Control+."; + export function setTagDisplayFull(full: boolean): void { $tagDisplayFull = full; } @@ -813,7 +815,10 @@ the AddCards dialog) should be implemented in the user of this component. updateTagsCollapsed(false); }} /> - + diff --git a/ts/lib/tag-editor/TagEditor.svelte b/ts/lib/tag-editor/TagEditor.svelte index 75102c04e3d..3fbc0325264 100644 --- a/ts/lib/tag-editor/TagEditor.svelte +++ b/ts/lib/tag-editor/TagEditor.svelte @@ -30,6 +30,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let tags: Writable; export let keyCombination: string = "Control+Shift+T"; export let displayFull: boolean = false; + export let displayModeShortcut: string = "Control+."; const selectAllShortcut = "Control+A"; const copyShortcut = "Control+C"; @@ -451,7 +452,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --icon-align="baseline" /> - +
{#each tagTypes as tag, index (tag.id)} @@ -565,6 +571,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html &.display-full { max-height: 50vh; + overflow-x: hidden; overflow-y: auto; .tag-header { diff --git a/ts/lib/tag-editor/TagWithTooltip.svelte b/ts/lib/tag-editor/TagWithTooltip.svelte index 31e6ea18ca9..f2912032d88 100644 --- a/ts/lib/tag-editor/TagWithTooltip.svelte +++ b/ts/lib/tag-editor/TagWithTooltip.svelte @@ -33,7 +33,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let cachedEllipsisWidth: number | undefined; const ELLIPSIS = "…"; - const PADDING_PX = 60; // Account for padding, delete badge, etc. + // Space to the right of tag text: delete badge (~20px) + tag padding pe-1 (4px) + border (1px) + safety margin + const RIGHT_PADDING_PX = 51; // Canvas for text measurement - no DOM thrashing const canvas = document.createElement("canvas"); @@ -58,7 +59,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } ctx.font = cachedFont; - const maxW = editorWidth - PADDING_PX; + const maxW = editorWidth - RIGHT_PADDING_PX; if (maxW <= 0) { displayName = ELLIPSIS; needsTooltip = true; From 9bbabd98dd1dc7b341eb9c1560b692ab2e31e5e1 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Fri, 9 Jan 2026 00:54:08 +0100 Subject: [PATCH 5/8] Fix tag spacer layout stability in full display mode - Track new tag by ID (newTagId) instead of boolean flag for robustness - Spacer only collapses when adding new tag at end, not when editing existing - Auto-delete empty tags when navigating away (blur handler handles deletion) - Prevent creating duplicate empty tags when pressing Enter on empty field - Simplify logic: set newTagId in appendTagAndFocusAt, clear in on:tagedit Co-Authored-By: Claude Opus 4.5 --- ts/lib/tag-editor/TagEditor.svelte | 37 +++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/ts/lib/tag-editor/TagEditor.svelte b/ts/lib/tag-editor/TagEditor.svelte index 3fbc0325264..8938abd6b22 100644 --- a/ts/lib/tag-editor/TagEditor.svelte +++ b/ts/lib/tag-editor/TagEditor.svelte @@ -66,6 +66,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let activeAfterBlur: number | null = null; let activeName = ""; let activeInput: HTMLInputElement; + let newTagId: string | null = null; // ID of newly created empty tag let autocomplete: any; let autocompleteDisabled: boolean = false; @@ -142,9 +143,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } function appendTagAndFocusAt(index: number, name: string): void { - tagTypes.splice(index + 1, 0, attachId(name)); + const newTag = attachId(name); + tagTypes.splice(index + 1, 0, newTag); tagTypes = tagTypes; setActiveAfterBlur(index + 1); + // Track new empty tags for spacer collapse logic + if (name.length === 0) { + newTagId = newTag.id; + } } function isActiveNameUniqueAt(index: number): boolean { @@ -164,6 +170,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html const current = activeName.slice(0, start); const splitOff = activeName.slice(end); + // Don't create empty tags - if both parts are empty, just blur (blur handler deletes empty tags) + if (current.length === 0 && splitOff.length === 0) { + active = null; + activeInput.blur(); + return; + } + activeName = current; // await tag to update its name, so it can normalize correctly await tick(); @@ -211,6 +224,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html return; } + // If current tag is empty, move to previous (blur handler deletes empty tags) + if (activeName.length === 0) { + activeAfterBlur = index - 1; + active = null; + activeInput.blur(); + return; + } + const deleted = deleteTagAt(index - 1); activeName = deleted.name + activeName; active!--; @@ -246,7 +267,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html return; } - activeAfterBlur = index + 1; + // Blur handler deletes empty tags, so next tag shifts to current index + activeAfterBlur = activeName.length === 0 ? index : index + 1; active = null; activeInput.blur(); @@ -393,6 +415,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html $: assumedRows = Math.floor(height / badgeHeight); $: shortenTags = displayFull ? false : shortenTags || assumedRows > 2; $: anyTagsSelected = tagTypes.some((tag) => tag.selected); + // Spacer should collapse only when adding a new tag at the end + $: isAddingTagAtEnd = + active !== null && + newTagId !== null && + tagTypes[active]?.id === newTagId && + active === tagTypes.length - 1; // Track editor width for tag truncation let editorElement: HTMLDivElement; @@ -436,6 +464,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{ active = index; + newTagId = null; // Clear when editing existing tag deselect(); }} on:tagselect={() => select(index)} @@ -589,7 +619,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html margin-top: 0; } - .hide-tag ~ :global(.tag-spacer) { + // Only collapse spacer when adding a new tag, not when editing existing + &.adding-tag-at-end .hide-tag ~ :global(.tag-spacer) { min-height: 0; height: 0; overflow: hidden; From cf58da1221e6ef8ff3b1034687595929b31e49a7 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Fri, 9 Jan 2026 01:46:02 +0100 Subject: [PATCH 6/8] Fix shortenTags recomputation after mode switch When switching from full mode back to auto mode, tags were incorrectly shortened because the layout measurement happened before the browser completed CSS layout changes. Now we wait for the browser to finish layout (using double requestAnimationFrame) before measuring row count. Co-Authored-By: Claude Opus 4.5 --- ts/lib/tag-editor/TagEditor.svelte | 45 ++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/ts/lib/tag-editor/TagEditor.svelte b/ts/lib/tag-editor/TagEditor.svelte index 8938abd6b22..207b4ff8485 100644 --- a/ts/lib/tag-editor/TagEditor.svelte +++ b/ts/lib/tag-editor/TagEditor.svelte @@ -413,7 +413,48 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // typically correct for rows < 7 $: assumedRows = Math.floor(height / badgeHeight); - $: shortenTags = displayFull ? false : shortenTags || assumedRows > 2; + let shortenTags = false; + let prevDisplayFull = displayFull; + let recomputingShorten = false; + let shortenSeq = 0; + + const afterPaint = () => + new Promise((resolve) => + requestAnimationFrame(() => requestAnimationFrame(() => resolve())), + ); + + async function recomputeShortenForAuto(): Promise { + const seq = ++shortenSeq; + recomputingShorten = true; + shortenTags = false; + + await tick(); + await afterPaint(); + + if (seq !== shortenSeq || displayFull || !editorElement || !badgeHeight) { + recomputingShorten = false; + return; + } + + shortenTags = Math.floor(editorElement.offsetHeight / badgeHeight) > 2; + recomputingShorten = false; + } + + $: if (displayFull !== prevDisplayFull) { + const leftFull = prevDisplayFull && !displayFull; + prevDisplayFull = displayFull; + + if (displayFull) { + shortenSeq++; // cancel in-flight recompute + shortenTags = false; + } else if (leftFull) { + void recomputeShortenForAuto(); + } + } + + $: if (!displayFull && !recomputingShorten && !shortenTags && assumedRows > 2) { + shortenTags = true; + } $: anyTagsSelected = tagTypes.some((tag) => tag.selected); // Spacer should collapse only when adding a new tag at the end $: isAddingTagAtEnd = @@ -496,7 +537,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html name={index === active ? activeName : tag.name} tooltip={tag.name} active={index === active} - shorten={shortenTags} + shorten={!displayFull && shortenTags} truncateMiddle={displayFull} {editorWidth} bind:flash={tag.flash} From f10ccf9eb96ff4f0483448c4e97b4f7f7f2ae36e Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Sat, 10 Jan 2026 13:34:59 +0100 Subject: [PATCH 7/8] Touch CONTRIBUTORS to register email in git log --- CONTRIBUTORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index a874a313da3..65180ca0c6b 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -217,7 +217,7 @@ Mike Hardy Danika_Dakika Mumtaz Hajjo Alrifai Thomas Graves -Jakub Fidler +Jakub Fidler Valerie Enfys Julien Chol ikkz From 348eddc35609ccc18627204eabbfc5c99cf90064 Mon Sep 17 00:00:00 2001 From: RisingOrange Date: Tue, 13 Jan 2026 12:48:26 +0100 Subject: [PATCH 8/8] Small refactor, add comment --- ts/lib/tag-editor/TagEditor.svelte | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ts/lib/tag-editor/TagEditor.svelte b/ts/lib/tag-editor/TagEditor.svelte index 207b4ff8485..15a7977d50a 100644 --- a/ts/lib/tag-editor/TagEditor.svelte +++ b/ts/lib/tag-editor/TagEditor.svelte @@ -416,7 +416,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let shortenTags = false; let prevDisplayFull = displayFull; let recomputingShorten = false; - let shortenSeq = 0; + let shortenSeq = 0; // incremented to cancel in-flight recomputes const afterPaint = () => new Promise((resolve) => @@ -441,13 +441,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } $: if (displayFull !== prevDisplayFull) { - const leftFull = prevDisplayFull && !displayFull; prevDisplayFull = displayFull; if (displayFull) { shortenSeq++; // cancel in-flight recompute shortenTags = false; - } else if (leftFull) { + } else { void recomputeShortenForAuto(); } }