From 39b544c4e34ea002d05105a4f9c50475cc058d23 Mon Sep 17 00:00:00 2001 From: Victor239 <12621257+Victor239@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:26:12 +0100 Subject: [PATCH 01/10] feat: add anchor tabs feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces "anchor tabs" — a tab can be anchored to a URL+title, and when the user navigates away a visual reset indicator appears between the favicon and title. Hovering the favicon reveals a "Back to " affordance (normal weight prefix, bold title) and clicking it returns the tab to its anchor URL. Unanchoring via the context menu or keybind clears all anchor state. Key behaviours: - Right-click menu: Anchor / Unanchor (below Pin), using the undo icon - Keybinding: Toggle anchor tab (no default shortcut, in Tabs section) - Settings → Tabs → Anchor tabs: configurable Reset indicator (default \) with note that it accepts text and emojis - Anchor data (URL + title) persisted in tabsDataCache and restored on extension reload - For anchored-away tabs the full-tab hover highlight is suppressed; only the favicon area is highlighted and clickable --- src/_locales/dict.browser.json | 11 +++++ src/_locales/dict.common.ts | 6 +++ src/_locales/dict.setup-page.ts | 9 ++++ src/defaults/menu.ts | 1 + src/defaults/settings.ts | 3 ++ src/manifest.json | 3 ++ src/page.setup/components/keybindings.vue | 1 + src/page.setup/components/settings.tabs.vue | 15 ++++++ src/services/keybindings.fg.ts | 7 +++ src/services/menu.fg.options.tabs.ts | 11 +++++ src/services/setup-page.fg.ts | 1 + src/services/tabs.fg.handlers.ts | 5 ++ src/services/tabs.fg.ts | 52 +++++++++++++++++++++ src/sidebar/components/tab.vue | 37 ++++++++++++++- src/styles/sidebar/tab.styl | 43 +++++++++++++++++ src/types/settings.ts | 3 ++ src/types/tabs.ts | 6 +++ 17 files changed, 212 insertions(+), 2 deletions(-) diff --git a/src/_locales/dict.browser.json b/src/_locales/dict.browser.json index a7e8f65b2..67b8da01d 100644 --- a/src/_locales/dict.browser.json +++ b/src/_locales/dict.browser.json @@ -423,6 +423,17 @@ "zh_CN": "编辑标题", "zh_TW": "編輯標題" }, + "KbToggleAnchorTab": { + "en": "Toggle anchor tab", + "de": "Anker-Tab umschalten", + "fr": "Basculer l'onglet ancré", + "hu": "Horgonylap váltása", + "pl": "Przełącz zakotwiczoną kartę", + "ru": "Переключить закреплённую вкладку", + "zh_CN": "切换锚定标签页", + "zh_TW": "切換錨定分頁", + "ja": "アンカータブを切り替え" + }, "KbOpenNewTabInCurrPanel": { "en": "Open a new tab in the active panel", "de": "Öffne neuen Tab im aktiven Panel", diff --git a/src/_locales/dict.common.ts b/src/_locales/dict.common.ts index f61e5b34c..cc90851c9 100644 --- a/src/_locales/dict.common.ts +++ b/src/_locales/dict.common.ts @@ -1933,6 +1933,12 @@ export const commonTranslations: Translations = { zh_TW: '取消釘選分頁', ja: 'ピン留め解除', }, + 'menu.tab.anchor': { + en: 'Anchor', + }, + 'menu.tab.unanchor': { + en: 'Unanchor', + }, 'menu.tab.duplicate': { en: 'Duplicate', de: 'Duplizieren', diff --git a/src/_locales/dict.setup-page.ts b/src/_locales/dict.setup-page.ts index 4b5d924f8..d45c64fec 100644 --- a/src/_locales/dict.setup-page.ts +++ b/src/_locales/dict.setup-page.ts @@ -707,6 +707,15 @@ Liste de “sous-chaines” ou “/RegExp/” avec une entrée par ligne: zh_TW: '已釘選分頁', ja: 'ピン留めされたタブ', }, + 'settings.nav_settings_anchor_tabs': { + en: 'Anchor tabs', + }, + 'settings.anchor_tabs_reset_indicator': { + en: 'Reset indicator', + }, + 'settings.anchor_tabs_reset_indicator_note': { + en: 'Accepts text and emojis', + }, 'settings.nav_settings_tabs_tree': { en: 'Tabs tree', de: 'Tab-Baum', diff --git a/src/defaults/menu.ts b/src/defaults/menu.ts index a7aa7915e..324165b38 100644 --- a/src/defaults/menu.ts +++ b/src/defaults/menu.ts @@ -35,6 +35,7 @@ export const TABS_MENU: MenuConf = [ }, 'separator-2', 'pin', + 'anchor', 'duplicate', 'discard', 'copyTabsUrls', diff --git a/src/defaults/settings.ts b/src/defaults/settings.ts index c808f69d0..2e5f566bf 100644 --- a/src/defaults/settings.ts +++ b/src/defaults/settings.ts @@ -133,6 +133,9 @@ export const DEFAULT_SETTINGS: SettingsState = { pinnedNoUnloadExplicit: true, pinnedForcedDiscard: false, + // Anchor tabs + anchorTabsResetIndicator: '\\', + // Tabs tree tabsTree: true, tabsTreeLimit: 'none', diff --git a/src/manifest.json b/src/manifest.json index 7fa06d49b..931c88272 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -415,6 +415,9 @@ "edit_title": { "description": "__MSG_KbEditTitle__" }, + "toggle_anchor_tab": { + "description": "__MSG_KbToggleAnchorTab__" + }, "sort_tabs_by_title_asc": { "description": "__MSG_KbSortTabsByTitleAsc__" }, diff --git a/src/page.setup/components/keybindings.vue b/src/page.setup/components/keybindings.vue index 327abc019..b46381650 100644 --- a/src/page.setup/components/keybindings.vue +++ b/src/page.setup/components/keybindings.vue @@ -65,6 +65,7 @@ KeybindingField(:keybinding="Keybindings.reactive.byName.group_tabs_act") KeybindingField(:keybinding="Keybindings.reactive.byName.flatten_tabs") KeybindingField(:keybinding="Keybindings.reactive.byName.edit_title") + KeybindingField(:keybinding="Keybindings.reactive.byName.toggle_anchor_tab") section h2 {{translate('settings.kb_tabs_open')}} diff --git a/src/page.setup/components/settings.tabs.vue b/src/page.setup/components/settings.tabs.vue index 768cb4eb6..94a87c75e 100644 --- a/src/page.setup/components/settings.tabs.vue +++ b/src/page.setup/components/settings.tabs.vue @@ -310,6 +310,18 @@ section(ref="el") :default="DEFAULT_SETTINGS.pinnedNoUnloadExplicit" @update:value="Settings.saveDebounced(150)") + .wrapper(ref="anchorTabsEl") + .sub-title: .text {{translate('settings.nav_settings_anchor_tabs')}} + TextField.-inline.-no-separator( + label="settings.anchor_tabs_reset_indicator" + or="\\" + :note="translate('settings.anchor_tabs_reset_indicator_note')" + v-model:value="Settings.state.anchorTabsResetIndicator" + dbg="anchorTabsResetIndicator" + :default="DEFAULT_SETTINGS.anchorTabsResetIndicator" + :line="true" + @update:value="Settings.saveDebounced(500)") + .wrapper(ref="tabsTreeEl") .sub-title: .text {{translate('settings.nav_settings_tabs_tree')}} ToggleField.-no-separator( @@ -665,10 +677,12 @@ import CountField from '../../components/count-field.vue' import ToggleField from '../../components/toggle-field.vue' import SelectField from '../../components/select-field.vue' import NumField from '../../components/num-field.vue' +import TextField from 'src/components/text-field.vue' const el = ref<HTMLElement | null>(null) const newTabPosEl = ref<HTMLElement | null>(null) const pinTabsEl = ref<HTMLElement | null>(null) +const anchorTabsEl = ref<HTMLElement | null>(null) const tabsTreeEl = ref<HTMLElement | null>(null) const tabsColorEl = ref<HTMLElement | null>(null) const tabsPreviewEl = ref<HTMLElement | null>(null) @@ -793,6 +807,7 @@ onMounted(() => { SetupPage.registerEl('settings_tabs', el.value) SetupPage.registerEl('settings_new_tab_position', newTabPosEl.value) SetupPage.registerEl('settings_pinned_tabs', pinTabsEl.value) + SetupPage.registerEl('settings_anchor_tabs', anchorTabsEl.value) SetupPage.registerEl('settings_tabs_tree', tabsTreeEl.value) SetupPage.registerEl('settings_tabs_colorization', tabsColorEl.value) SetupPage.registerEl('settings_tabs_preview', tabsPreviewEl.value) diff --git a/src/services/keybindings.fg.ts b/src/services/keybindings.fg.ts index 68926dbc1..dcffbafd3 100644 --- a/src/services/keybindings.fg.ts +++ b/src/services/keybindings.fg.ts @@ -266,6 +266,7 @@ function onCmd(name: string): void { else if (name === 'group_tabs_act') onKeyGroupTabs(true) else if (name === 'flatten_tabs') onKeyFlattenTabs() else if (name === 'edit_title') onKeyEditTitle() + else if (name === 'toggle_anchor_tab') onKeyToggleAnchorTab() else if (name === 'sel_child_tabs') onKeySelChildTabs() else if (name === 'sort_tabs_by_title_asc') onKeySortTabs(SortBy.Title, 1) else if (name === 'sort_tabs_by_title_des') onKeySortTabs(SortBy.Title, -1) @@ -1530,6 +1531,12 @@ function onKeyPinTabs() { else Tabs.pinTabs(ids) } +function onKeyToggleAnchorTab() { + const ids = Selection.isTabs() ? Selection.ids() : [Tabs.activeId] + if (!ids.length) return + Tabs.toggleAnchorTabs(ids) +} + function onKeyHidePanel() { let panelId = Selection.getFirst() if (!Sidebar.panelsById[panelId]) panelId = Sidebar.activePanelId diff --git a/src/services/menu.fg.options.tabs.ts b/src/services/menu.fg.options.tabs.ts index fad4e7e14..22199f540 100644 --- a/src/services/menu.fg.options.tabs.ts +++ b/src/services/menu.fg.options.tabs.ts @@ -260,6 +260,17 @@ export const tabsMenuOptions: Record<string, () => MenuOption | MenuOption[] | u } }, + anchor: () => { + const selected = Selection.ids() + const firstTab = Tabs.byId[selected[0]] + if (!firstTab) return + return { + label: translate('menu.tab.' + (firstTab.anchorUrl ? 'unanchor' : 'anchor')), + icon: 'icon_undo', + onClick: firstTab.anchorUrl ? () => Tabs.unanchorTabs(selected) : () => Tabs.anchorTabs(selected), + } + }, + reload: () => { return { label: translate('menu.tab.reload'), diff --git a/src/services/setup-page.fg.ts b/src/services/setup-page.fg.ts index faf7f513f..3d0b58a4a 100644 --- a/src/services/setup-page.fg.ts +++ b/src/services/setup-page.fg.ts @@ -81,6 +81,7 @@ export let reactive: SetupPageReactiveState = { { active: false, name: 'settings_tabs', lvl: 1 }, { active: false, name: 'settings_new_tab_position', lvl: 2 }, { active: false, name: 'settings_pinned_tabs', lvl: 2 }, + { active: false, name: 'settings_anchor_tabs', lvl: 2 }, { active: false, name: 'settings_tabs_tree', lvl: 2 }, { active: false, name: 'settings_tabs_colorization', lvl: 2 }, { active: false, name: 'settings_tabs_preview', lvl: 2 }, diff --git a/src/services/tabs.fg.handlers.ts b/src/services/tabs.fg.handlers.ts index 5e51b33c2..fd58331a7 100644 --- a/src/services/tabs.fg.handlers.ts +++ b/src/services/tabs.fg.handlers.ts @@ -820,6 +820,11 @@ function onTabUpdated(tabId: ID, change: browser.tabs.ChangeInfo, nativeTab: Nat IPPC.onHashMsg(tab, change.url) } + // Handle anchor tab URL change + if (tab.anchorUrl) { + tab.reactive.anchoredAway = change.url !== tab.anchorUrl + } + // Reset favicon (to cached) if (tab.internal || !Utils.sameStart(change.url, tab.url, 16)) { change.favIconUrl = Favicons.getFavicon(change.url) diff --git a/src/services/tabs.fg.ts b/src/services/tabs.fg.ts index d3384ec86..5f9b13796 100644 --- a/src/services/tabs.fg.ts +++ b/src/services/tabs.fg.ts @@ -153,6 +153,8 @@ export function mutateNativeTabToSideberyTab(nativeTab: T.NativeTab): T.Tab { branchColor: null, color: null, isGroup: tab.isGroup, + anchored: false, + anchoredAway: false, } } @@ -504,6 +506,12 @@ function restoreTab( tab.reactive.folded = tab.folded = !!props.folded if (props.customTitle) tab.customTitle = props.customTitle if (props.customColor) tab.reactive.customColor = tab.customColor = props.customColor + if (props.anchorUrl) { + tab.anchorUrl = props.anchorUrl + tab.anchorTitle = props.anchorTitle + tab.reactive.anchored = true + tab.reactive.anchoredAway = tab.url !== props.anchorUrl + } } else { Logs.warn(`Tabs.restoreTab: no props for: "${tab.id} i${tab.index} url${tab.url}"`) } @@ -730,6 +738,8 @@ export function cacheTabsData(delay = 300): void { if (tab.cookieStoreId !== D.CONTAINER_ID) info.ctx = tab.cookieStoreId if (tab.customTitle) info.customTitle = tab.customTitle if (tab.customColor) info.customColor = tab.customColor + if (tab.anchorUrl) info.anchorUrl = tab.anchorUrl + if (tab.anchorTitle) info.anchorTitle = tab.anchorTitle data.push(info) } @@ -1240,6 +1250,48 @@ export function repinTabs(tabIds: ID[]): void { } } +/** + * Anchor tabs + */ +export function anchorTabs(tabIds: ID[]): void { + for (const tabId of tabIds) { + const tab = Tabs.byId[tabId] + if (!tab) continue + tab.anchorUrl = tab.url + tab.anchorTitle = tab.customTitle ?? tab.title + tab.reactive.anchored = true + tab.reactive.anchoredAway = false + } + Tabs.cacheTabsData() +} + +export function unanchorTabs(tabIds: ID[]): void { + for (const tabId of tabIds) { + const tab = Tabs.byId[tabId] + if (!tab) continue + tab.anchorUrl = undefined + tab.anchorTitle = undefined + tab.reactive.anchored = false + tab.reactive.anchoredAway = false + } + Tabs.cacheTabsData() +} + +export function toggleAnchorTabs(tabIds: ID[]): void { + const firstTab = Tabs.byId[tabIds[0]] + if (!firstTab) return + if (firstTab.anchorUrl) unanchorTabs(tabIds) + else anchorTabs(tabIds) +} + +export function returnToAnchor(tabId: ID): void { + const tab = Tabs.byId[tabId] + if (!tab?.anchorUrl) return + browser.tabs.update(tabId, { url: tab.anchorUrl }).catch(err => { + Logs.err('Tabs.returnToAnchor: Cannot navigate to anchor URL:', err) + }) +} + /** * Duplicate tabs */ diff --git a/src/sidebar/components/tab.vue b/src/sidebar/components/tab.vue index df616e5c3..abcb57817 100644 --- a/src/sidebar/components/tab.vue +++ b/src/sidebar/components/tab.vue @@ -20,6 +20,7 @@ :data-colorized="!!tabColor" :data-unread="tab.reactive.unread" :data-edit="tab.reactive.customTitleEdit" + :data-anchored-away="tab.reactive.anchoredAway" :title="tab.reactive.tooltip" :draggable="!tab.reactive.customTitleEdit" @dragstart="onDragStart" @@ -34,7 +35,12 @@ .color-layer(v-if="tabColor" :style="{ '--tab-color': tabColor }") .flash-fx(ref="flashFxEl") .unread-mark(v-if="tab.reactive.unread") - .fav(@dragstart.stop.prevent) + .fav( + @dragstart.stop.prevent + @mouseenter="onFavMouseEnter" + @mouseleave="onFavMouseLeave" + @mousedown.stop="onFavMouseDown" + @mouseup.stop="onFavMouseUp") img.fav-icon(ref="favImgEl" @error="onError" draggable="false") svg.fav-icon: use(ref="favSvgUseEl" href="#icon_ff") .exp( @@ -54,6 +60,8 @@ svg.audio-icon.-loud: use(href="#icon_loud_badge") svg.audio-icon.-mute: use(href="#icon_mute_badge") svg.audio-icon.-pause: use(href="#icon_pause_12") + .anchor-reset-indicator(v-if="tab.reactive.anchoredAway && !iconOnly && !favHovered") + | {{Settings.state.anchorTabsResetIndicator}} .t-box(v-if="!iconOnly") input.custom-title-input( v-if="tab.reactive.customTitleEdit" @@ -65,7 +73,10 @@ tabindex="-1" @blur="onCustomTitleBlur" @keydown="onCustomTitlteKD") - .title(ref="titleEl") {{tab.customTitle ?? tab.title}} + .title(ref="titleEl" v-show="!(tab.reactive.anchoredAway && favHovered)") {{tab.customTitle ?? tab.title}} + .title.-anchor-back(v-if="tab.reactive.anchoredAway && favHovered") + span.back-to-prefix Back to + span.back-to-title {{tab.anchorTitle}} .close( v-if="!iconOnly && Settings.state.tabRmBtn !== 'none'" draggable="true" @@ -107,6 +118,7 @@ const titleEl = ref<HTMLElement | null>(null) const favImgEl = ref<HTMLImageElement | null>(null) const favSvgUseEl = ref<SVGElement | null>(null) const flashFxEl = ref<HTMLElement | null>(null) +const favHovered = ref(false) const tabColor = computed<string>(() => { if (tab.reactive.customColor) return RGB_COLORS[tab.customColor as browser.ColorName] @@ -399,6 +411,27 @@ function longClickFeedback(e: MouseEvent) { return !noop } +function onFavMouseEnter(): void { + if (tab.reactive.anchoredAway) favHovered.value = true +} + +function onFavMouseLeave(): void { + favHovered.value = false +} + +function onFavMouseDown(e: MouseEvent): void { + if (tab.reactive.anchoredAway && e.button === 0) return + onMouseDown(e) +} + +function onFavMouseUp(e: MouseEvent): void { + if (tab.reactive.anchoredAway && e.button === 0) { + Tabs.returnToAnchor(tab.id) + return + } + onMouseUp(e) +} + function onMouseUp(e: MouseEvent): void { const sameTarget = Mouse.isTarget('tab', tab.id) const sameTargetType = Mouse.isTarget('tab') diff --git a/src/styles/sidebar/tab.styl b/src/styles/sidebar/tab.styl index d98f4810e..62e4422f2 100644 --- a/src/styles/sidebar/tab.styl +++ b/src/styles/sidebar/tab.styl @@ -682,6 +682,49 @@ background-color: var(--tabs-normal-fg) +// --- +// -- Anchor tabs +// - + +// Anchored-away: restrict hover highlight to favicon area only +.Tab[data-anchored-away="true"]:hover .body:before + background-color: initial + +.Tab[data-anchored-away="true"] .fav:hover + cursor: pointer + &:before + content: '' + position: absolute + inset: -4px + border-radius: 4px + background-color: var(--frame-el-overlay-hover-bg) + z-index: -1 + +// Reset indicator between favicon and title +.Tab .anchor-reset-indicator + flex-shrink: 0 + font-size: 11px + padding: 0 2px 0 4px + opacity: 0.6 + color: var(--tabs-normal-fg) + pointer-events: none + +// "Back to" title when hovering favicon of anchored-away tab +.Tab .title.-anchor-back + display: flex + align-items: center + gap: 0.25em + overflow: hidden + white-space: nowrap + text-overflow: ellipsis + .back-to-prefix + font-weight: normal + flex-shrink: 0 + .back-to-title + font-weight: bold + overflow: hidden + text-overflow: ellipsis + // --- // -- Keyframes // - diff --git a/src/types/settings.ts b/src/types/settings.ts index 514dbb100..437a80337 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -131,6 +131,9 @@ export interface SettingsState { pinnedNoUnloadExplicit: boolean pinnedForcedDiscard: boolean + // Anchor tabs + anchorTabsResetIndicator: string + // Tabs tree tabsTree: boolean groupOnOpen?: boolean // DEPR diff --git a/src/types/tabs.ts b/src/types/tabs.ts index 6835826cd..d5ed3f8a6 100644 --- a/src/types/tabs.ts +++ b/src/types/tabs.ts @@ -35,6 +35,8 @@ export interface Tab extends NativeTab { reopenInContainer?: string customTitle?: string customColor?: string + anchorUrl?: string + anchorTitle?: string moving?: boolean removing?: boolean flashAnimationTimeout?: number @@ -77,6 +79,8 @@ export interface ReactiveTabProps { branchColor: string | null customColor: string | null isGroup: boolean + anchored: boolean + anchoredAway: boolean } export interface BgTab extends NativeTab { @@ -111,6 +115,8 @@ export interface TabCache { uniqWinId?: ID /* only for the first tab of window */ customTitle?: string customColor?: string + anchorUrl?: string + anchorTitle?: string index?: number isMissedGroup?: boolean From f2851524d6e4ee4eaebf5b4853f515d951354cdc Mon Sep 17 00:00:00 2001 From: Victor239 <12621257+Victor239@users.noreply.github.com> Date: Tue, 31 Mar 2026 17:29:48 +0100 Subject: [PATCH 02/10] feat(anchor-tabs): add hover interaction for anchored-away tabs - Wrap .fav in a .fav-anchor container; its padding extends the hit-test area to match the permanent highlight box, replacing the previous per-element margins on .fav - Show a permanent highlight (clicked-level color) on .fav-anchor for anchored tabs via a ::before pseudo-element - On hover of an anchored-away tab's .fav-anchor: - Swap favicon to undo icon via CSS opacity (preserves renderFavicon() display value; v-show/display:none would wipe it) - Show "Back to <title>" text inside .fav-anchor in place of t-box - Suppress the permanent highlight and hide the title box (.t-box) - Show a tighter highlight around the favicon - Change cursor to pointer - Use opacity:0 (not display:none) on the reset indicator during hover so .fav-anchor's layout stays stable and :hover doesn't flicker - Remove JS favHovered ref and mouseenter/mouseleave handlers; all effects are pure CSS driven by .fav-anchor:hover - Add data-anchored attribute to .Tab for the permanent highlight selector - Add pointer-events:none to .body::before to stop it intercepting hover - Align "Back to" text left edge with normal tab title via margin-left --- src/sidebar/components/tab.vue | 53 ++++++++++++---------------- src/styles/sidebar/tab.styl | 64 ++++++++++++++++++++++++++++------ 2 files changed, 76 insertions(+), 41 deletions(-) diff --git a/src/sidebar/components/tab.vue b/src/sidebar/components/tab.vue index abcb57817..65638d4b1 100644 --- a/src/sidebar/components/tab.vue +++ b/src/sidebar/components/tab.vue @@ -20,6 +20,7 @@ :data-colorized="!!tabColor" :data-unread="tab.reactive.unread" :data-edit="tab.reactive.customTitleEdit" + :data-anchored="tab.reactive.anchored" :data-anchored-away="tab.reactive.anchoredAway" :title="tab.reactive.tooltip" :draggable="!tab.reactive.customTitleEdit" @@ -35,24 +36,28 @@ .color-layer(v-if="tabColor" :style="{ '--tab-color': tabColor }") .flash-fx(ref="flashFxEl") .unread-mark(v-if="tab.reactive.unread") - .fav( - @dragstart.stop.prevent - @mouseenter="onFavMouseEnter" - @mouseleave="onFavMouseLeave" + .fav-anchor( @mousedown.stop="onFavMouseDown" @mouseup.stop="onFavMouseUp") - img.fav-icon(ref="favImgEl" @error="onError" draggable="false") - svg.fav-icon: use(ref="favSvgUseEl" href="#icon_ff") - .exp( - v-if="tab.reactive.isParent" - @dblclick.prevent.stop - @mousedown.stop="onExpandMouseDown" - @mouseup="onExpandMouseUp") - svg.exp-icon: use(href="#icon_expand") - .badge - .progress-spinner(v-if="Settings.state.animations") - svg.progress-spinner(v-else): use(href="#icon_hourglass") - .child-count(v-if="tab.reactive.folded && tab.reactive.branchLen") {{tab.reactive.branchLen}} + .fav(@dragstart.stop.prevent) + img.fav-icon(ref="favImgEl" @error="onError" draggable="false") + svg.fav-icon: use(ref="favSvgUseEl" href="#icon_ff") + svg.fav-icon.-anchor-hover: use(href="#icon_undo") + .exp( + v-if="tab.reactive.isParent" + @dblclick.prevent.stop + @mousedown.stop="onExpandMouseDown" + @mouseup.stop="onExpandMouseUp") + svg.exp-icon: use(href="#icon_expand") + .badge + .progress-spinner(v-if="Settings.state.animations") + svg.progress-spinner(v-else): use(href="#icon_hourglass") + .child-count(v-if="tab.reactive.folded && tab.reactive.branchLen") {{tab.reactive.branchLen}} + .title.-anchor-back(v-if="tab.reactive.anchoredAway") + span.back-to-prefix Back to + span.back-to-title {{tab.anchorTitle}} + .anchor-reset-indicator(v-if="tab.reactive.anchoredAway && !iconOnly") + | {{Settings.state.anchorTabsResetIndicator}} .audio( v-if="tab.reactive.mediaAudible || tab.reactive.mediaMuted || tab.reactive.mediaPaused" @mousedown.stop.prevent="onAudioMouseDown($event, tab)" @@ -60,8 +65,6 @@ svg.audio-icon.-loud: use(href="#icon_loud_badge") svg.audio-icon.-mute: use(href="#icon_mute_badge") svg.audio-icon.-pause: use(href="#icon_pause_12") - .anchor-reset-indicator(v-if="tab.reactive.anchoredAway && !iconOnly && !favHovered") - | {{Settings.state.anchorTabsResetIndicator}} .t-box(v-if="!iconOnly") input.custom-title-input( v-if="tab.reactive.customTitleEdit" @@ -73,10 +76,7 @@ tabindex="-1" @blur="onCustomTitleBlur" @keydown="onCustomTitlteKD") - .title(ref="titleEl" v-show="!(tab.reactive.anchoredAway && favHovered)") {{tab.customTitle ?? tab.title}} - .title.-anchor-back(v-if="tab.reactive.anchoredAway && favHovered") - span.back-to-prefix Back to - span.back-to-title {{tab.anchorTitle}} + .title(ref="titleEl") {{tab.customTitle ?? tab.title}} .close( v-if="!iconOnly && Settings.state.tabRmBtn !== 'none'" draggable="true" @@ -118,7 +118,6 @@ const titleEl = ref<HTMLElement | null>(null) const favImgEl = ref<HTMLImageElement | null>(null) const favSvgUseEl = ref<SVGElement | null>(null) const flashFxEl = ref<HTMLElement | null>(null) -const favHovered = ref(false) const tabColor = computed<string>(() => { if (tab.reactive.customColor) return RGB_COLORS[tab.customColor as browser.ColorName] @@ -411,14 +410,6 @@ function longClickFeedback(e: MouseEvent) { return !noop } -function onFavMouseEnter(): void { - if (tab.reactive.anchoredAway) favHovered.value = true -} - -function onFavMouseLeave(): void { - favHovered.value = false -} - function onFavMouseDown(e: MouseEvent): void { if (tab.reactive.anchoredAway && e.button === 0) return onMouseDown(e) diff --git a/src/styles/sidebar/tab.styl b/src/styles/sidebar/tab.styl index 62e4422f2..b0bcf977e 100644 --- a/src/styles/sidebar/tab.styl +++ b/src/styles/sidebar/tab.styl @@ -167,6 +167,7 @@ height: 100% border-radius: var(--tabs-border-radius) z-index: 1 + pointer-events: none .Tab:hover .body:before background-color: var(--frame-el-overlay-hover-bg) @@ -314,6 +315,12 @@ // --- // -- Favicon // - +.Tab .fav-anchor + display: flex + align-items: center + flex-shrink: 0 + position: relative + .Tab .fav position: relative width: 16px @@ -323,8 +330,11 @@ z-index: 10 transform: translateZ(0) -.Tab[data-pin="false"] .fav - margin: 0 var(--tabs-inner-gap) 0 calc(var(--tabs-inner-gap) + 2px) +.Tab[data-pin="false"] .fav-anchor + // Padding extends the hit area to match the visual highlight box. + // Margins reduced by same amounts so flex layout spacing is unchanged. + padding: calc((var(--tabs-height) - 16px) / 2 - 2px) var(--tabs-inner-gap) + margin: 0 0 0 2px z-index: 20 .Tab .fav-icon @@ -337,6 +347,13 @@ transition: opacity var(--d-fast) .Tab img.fav-icon display: none +.Tab .fav-icon.-anchor-hover + opacity: 0 + pointer-events: none +.Tab[data-anchored-away="true"] .fav-anchor:hover .fav-icon:not(.-anchor-hover) + opacity: 0 +.Tab[data-anchored-away="true"] .fav-anchor:hover .fav-icon.-anchor-hover + opacity: 1 .Tab .fav svg opacity: .7 @@ -690,33 +707,60 @@ .Tab[data-anchored-away="true"]:hover .body:before background-color: initial -.Tab[data-anchored-away="true"] .fav:hover +// Permanent highlight covering favicon + reset indicator for all anchored tabs +.Tab[data-anchored="true"] .fav-anchor + &:before + content: '' + position: absolute + inset: 0 + border-radius: var(--tabs-border-radius) + background-color: var(--frame-el-overlay-clicked-bg) + z-index: -1 + +// Hover effects for anchored-away tabs — all driven by .fav-anchor:hover +.Tab[data-anchored-away="true"] .fav-anchor:hover cursor: pointer + flex-grow: 1 + // Hide permanent highlight background on hover &:before + background-color: transparent + // Extra highlight on favicon area + .fav:after content: '' position: absolute inset: -4px border-radius: 4px - background-color: var(--frame-el-overlay-hover-bg) + background-color: var(--frame-el-overlay-selected-bg) z-index: -1 + // Show "Back to" title + .title.-anchor-back + display: flex + // Fade out indicator (opacity keeps layout so hover area stays stable; no flicker) + .anchor-reset-indicator + opacity: 0 + pointer-events: none + +// Hide title box on hover (Back to title is shown inside .fav-anchor instead) +.Tab[data-anchored-away="true"] .fav-anchor:hover ~ .t-box + display: none // Reset indicator between favicon and title .Tab .anchor-reset-indicator flex-shrink: 0 - font-size: 11px + font: var(--tabs-font) padding: 0 2px 0 4px opacity: 0.6 color: var(--tabs-normal-fg) - pointer-events: none -// "Back to" title when hovering favicon of anchored-away tab -.Tab .title.-anchor-back - display: flex +// "Back to" title inside .fav-anchor — hidden by default, shown on hover +.Tab .fav-anchor .title.-anchor-back + display: none + flex-grow: 1 + margin-left: var(--tabs-inner-gap) align-items: center gap: 0.25em overflow: hidden white-space: nowrap - text-overflow: ellipsis .back-to-prefix font-weight: normal flex-shrink: 0 From a663d25bf8302cd7b7b1b5e03c46518817eb82a5 Mon Sep 17 00:00:00 2001 From: Victor239 <12621257+Victor239@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:00:21 +0100 Subject: [PATCH 03/10] i18n(anchor-tabs): add translations + guard undo icon render - Add de/fr/hu/pl/ru/zh_CN/zh_TW/ja translations for menu.tab.anchor, menu.tab.unanchor, settings.nav_settings_anchor_tabs, settings.anchor_tabs_reset_indicator, and settings.anchor_tabs_reset_indicator_note - Add v-if="tab.reactive.anchored" to svg.fav-icon.-anchor-hover so the undo icon SVG is only rendered for anchored tabs, not every tab --- src/_locales/dict.common.ts | 16 ++++++++++++++++ src/_locales/dict.setup-page.ts | 24 ++++++++++++++++++++++++ src/sidebar/components/tab.vue | 2 +- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/_locales/dict.common.ts b/src/_locales/dict.common.ts index cc90851c9..bbb9b9d40 100644 --- a/src/_locales/dict.common.ts +++ b/src/_locales/dict.common.ts @@ -1935,9 +1935,25 @@ export const commonTranslations: Translations = { }, 'menu.tab.anchor': { en: 'Anchor', + de: 'Ankern', + fr: 'Ancrer', + hu: 'Horgonyzás', + pl: 'Zakotwicz', + ru: 'Привязать', + zh_CN: '锚定', + zh_TW: '錨定', + ja: 'アンカー設定', }, 'menu.tab.unanchor': { en: 'Unanchor', + de: 'Anker entfernen', + fr: 'Désancrer', + hu: 'Horgony eltávolítása', + pl: 'Usuń zakotwiczenie', + ru: 'Снять привязку', + zh_CN: '取消锚定', + zh_TW: '取消錨定', + ja: 'アンカー解除', }, 'menu.tab.duplicate': { en: 'Duplicate', diff --git a/src/_locales/dict.setup-page.ts b/src/_locales/dict.setup-page.ts index d45c64fec..7a852e831 100644 --- a/src/_locales/dict.setup-page.ts +++ b/src/_locales/dict.setup-page.ts @@ -709,12 +709,36 @@ Liste de “sous-chaines” ou “/RegExp/” avec une entrée par ligne: }, 'settings.nav_settings_anchor_tabs': { en: 'Anchor tabs', + de: 'Anker-Tabs', + fr: 'Onglets ancrés', + hu: 'Horgonyzott lapok', + pl: 'Zakotwiczone karty', + ru: 'Привязанные вкладки', + zh_CN: '锚定标签页', + zh_TW: '錨定分頁', + ja: 'アンカータブ', }, 'settings.anchor_tabs_reset_indicator': { en: 'Reset indicator', + de: 'Zurücksetzen-Indikator', + fr: 'Indicateur de réinitialisation', + hu: 'Visszaállítási jelző', + pl: 'Wskaźnik resetowania', + ru: 'Индикатор сброса', + zh_CN: '重置指示符', + zh_TW: '重置指示符', + ja: 'リセットインジケーター', }, 'settings.anchor_tabs_reset_indicator_note': { en: 'Accepts text and emojis', + de: 'Akzeptiert Text und Emojis', + fr: 'Accepte du texte et des emojis', + hu: 'Elfogad szöveget és emojikat', + pl: 'Akceptuje tekst i emoji', + ru: 'Принимает текст и эмодзи', + zh_CN: '支持文字和表情符号', + zh_TW: '支援文字和表情符號', + ja: 'テキストと絵文字を使用できます', }, 'settings.nav_settings_tabs_tree': { en: 'Tabs tree', diff --git a/src/sidebar/components/tab.vue b/src/sidebar/components/tab.vue index 65638d4b1..ae79a7bd9 100644 --- a/src/sidebar/components/tab.vue +++ b/src/sidebar/components/tab.vue @@ -42,7 +42,7 @@ .fav(@dragstart.stop.prevent) img.fav-icon(ref="favImgEl" @error="onError" draggable="false") svg.fav-icon: use(ref="favSvgUseEl" href="#icon_ff") - svg.fav-icon.-anchor-hover: use(href="#icon_undo") + svg.fav-icon.-anchor-hover(v-if="tab.reactive.anchored"): use(href="#icon_undo") .exp( v-if="tab.reactive.isParent" @dblclick.prevent.stop From 83cfdd4b3769c9ac91cfaadef79da2f181b0cf63 Mon Sep 17 00:00:00 2001 From: Victor239 <12621257+Victor239@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:07:14 +0100 Subject: [PATCH 04/10] fix(anchor-tabs): resolve lint and type errors - Fix prettier formatting: break long ternary in menu.fg.options.tabs.ts - Add anchored/anchoredAway to MTab reactive mock (ReactiveTabProps) - Add anchorUrl/anchorTitle fields to MTab class and TabSessionData interface so the restoreTab union type (TabSessionData | TabCache) resolves correctly --- src/defaults/mocks.tabs.fg.ts | 5 ++++- src/services/menu.fg.options.tabs.ts | 4 +++- src/types/tabs.ts | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/defaults/mocks.tabs.fg.ts b/src/defaults/mocks.tabs.fg.ts index fdcd4c31f..2e752fe3b 100644 --- a/src/defaults/mocks.tabs.fg.ts +++ b/src/defaults/mocks.tabs.fg.ts @@ -34,6 +34,8 @@ export class MTab implements Tab { reopenInContainer?: string | undefined customTitle?: string | undefined customColor?: string | undefined + anchorUrl?: string | undefined + anchorTitle?: string | undefined moving?: boolean | undefined previewImg?: string | undefined removing?: boolean | undefined @@ -64,7 +66,8 @@ export class MTab implements Tab { branchColor: null, customColor: null, isGroup: false, - preview: false, + anchored: false, + anchoredAway: false, } sessionData?: TabSessionData | undefined titleEl?: HTMLElement | undefined diff --git a/src/services/menu.fg.options.tabs.ts b/src/services/menu.fg.options.tabs.ts index 22199f540..234511378 100644 --- a/src/services/menu.fg.options.tabs.ts +++ b/src/services/menu.fg.options.tabs.ts @@ -267,7 +267,9 @@ export const tabsMenuOptions: Record<string, () => MenuOption | MenuOption[] | u return { label: translate('menu.tab.' + (firstTab.anchorUrl ? 'unanchor' : 'anchor')), icon: 'icon_undo', - onClick: firstTab.anchorUrl ? () => Tabs.unanchorTabs(selected) : () => Tabs.anchorTabs(selected), + onClick: firstTab.anchorUrl + ? () => Tabs.unanchorTabs(selected) + : () => Tabs.anchorTabs(selected), } }, diff --git a/src/types/tabs.ts b/src/types/tabs.ts index d5ed3f8a6..76eda3efb 100644 --- a/src/types/tabs.ts +++ b/src/types/tabs.ts @@ -129,6 +129,8 @@ export interface TabSessionData { folded: boolean customTitle?: string customColor?: string + anchorUrl?: string + anchorTitle?: string } export interface ActiveTabsHistory { From 6cc692c77e70e97fc5d47992d6040f3d64bf93d1 Mon Sep 17 00:00:00 2001 From: Victor239 <12621257+Victor239@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:57:25 +0100 Subject: [PATCH 05/10] i18n(anchor-tabs): rename Anchor/Unanchor to Bind URL/Unbind URL Update English labels across locale files to use "Bind URL" / "Unbind URL" instead of "Anchor" / "Unanchor" for clearer user-facing terminology. --- src/_locales/dict.browser.json | 2 +- src/_locales/dict.common.ts | 4 ++-- src/_locales/dict.setup-page.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_locales/dict.browser.json b/src/_locales/dict.browser.json index 67b8da01d..abd5de2dc 100644 --- a/src/_locales/dict.browser.json +++ b/src/_locales/dict.browser.json @@ -424,7 +424,7 @@ "zh_TW": "編輯標題" }, "KbToggleAnchorTab": { - "en": "Toggle anchor tab", + "en": "Toggle bind URL tab", "de": "Anker-Tab umschalten", "fr": "Basculer l'onglet ancré", "hu": "Horgonylap váltása", diff --git a/src/_locales/dict.common.ts b/src/_locales/dict.common.ts index bbb9b9d40..03c52f639 100644 --- a/src/_locales/dict.common.ts +++ b/src/_locales/dict.common.ts @@ -1934,7 +1934,7 @@ export const commonTranslations: Translations = { ja: 'ピン留め解除', }, 'menu.tab.anchor': { - en: 'Anchor', + en: 'Bind URL', de: 'Ankern', fr: 'Ancrer', hu: 'Horgonyzás', @@ -1945,7 +1945,7 @@ export const commonTranslations: Translations = { ja: 'アンカー設定', }, 'menu.tab.unanchor': { - en: 'Unanchor', + en: 'Unbind URL', de: 'Anker entfernen', fr: 'Désancrer', hu: 'Horgony eltávolítása', diff --git a/src/_locales/dict.setup-page.ts b/src/_locales/dict.setup-page.ts index 7a852e831..822449c60 100644 --- a/src/_locales/dict.setup-page.ts +++ b/src/_locales/dict.setup-page.ts @@ -708,7 +708,7 @@ Liste de “sous-chaines” ou “/RegExp/” avec une entrée par ligne: ja: 'ピン留めされたタブ', }, 'settings.nav_settings_anchor_tabs': { - en: 'Anchor tabs', + en: 'Bind URL tabs', de: 'Anker-Tabs', fr: 'Onglets ancrés', hu: 'Horgonyzott lapok', From 51149574bc21d40020a442bcfa36bcad35c8ec9e Mon Sep 17 00:00:00 2001 From: VictorVow <12621257+VictorVow@users.noreply.github.com> Date: Sat, 23 May 2026 13:28:49 +0100 Subject: [PATCH 06/10] refactor(anchor-tabs): rework interaction per PR review Address maintainer feedback on PR #2384 (mbnuqw/sidebery) by replacing the clickable in-tab anchor UI with a passive indicator plus configurable gestures, menu entries and keybindings. - Remove the clickable .anchor-badge, favicon hover-to-undo swap, "Back to <title>" affordance and reset-indicator text; the favicon click now only activates the tab (no new interactive zones). - Add a passive bottom-left corner marker (.bind-mark) driven by data-anchored/data-anchored-away, with a distinct colour when navigated away. Works for normal and pinned tabs. - Expose bind/unbind/return as configurable actions (toggle_bind_url, return_to_bound) in the double/long/middle-click action lists and wire them up in tab.vue. - Add a "Return to bound URL" context-menu entry (shown only when navigated away) and a return_to_bound_url keybinding; rename keybinding command toggle_anchor_tab -> toggle_bind_url and menu id anchor -> bind_url. - Retire the now-redundant anchorTabsResetIndicator setting and its settings field, nav entry and locale strings. --- src/_locales/dict.browser.json | 31 ++++-- src/_locales/dict.common.ts | 11 +++ src/_locales/dict.setup-page.ts | 55 +++++------ src/defaults/menu.ts | 3 +- src/defaults/settings.ts | 13 +-- src/manifest.json | 7 +- src/page.setup/components/keybindings.vue | 3 +- src/page.setup/components/settings.tabs.vue | 14 --- src/services/keybindings.fg.ts | 11 ++- src/services/menu.fg.options.tabs.ts | 13 ++- src/services/setup-page.fg.ts | 1 - src/sidebar/components/tab.vue | 67 ++++++------- src/styles/sidebar/tab.styl | 104 +++++--------------- src/types/settings.ts | 3 - 14 files changed, 142 insertions(+), 194 deletions(-) diff --git a/src/_locales/dict.browser.json b/src/_locales/dict.browser.json index abd5de2dc..b34feee91 100644 --- a/src/_locales/dict.browser.json +++ b/src/_locales/dict.browser.json @@ -423,16 +423,27 @@ "zh_CN": "编辑标题", "zh_TW": "編輯標題" }, - "KbToggleAnchorTab": { - "en": "Toggle bind URL tab", - "de": "Anker-Tab umschalten", - "fr": "Basculer l'onglet ancré", - "hu": "Horgonylap váltása", - "pl": "Przełącz zakotwiczoną kartę", - "ru": "Переключить закреплённую вкладку", - "zh_CN": "切换锚定标签页", - "zh_TW": "切換錨定分頁", - "ja": "アンカータブを切り替え" + "KbToggleBindUrl": { + "en": "Bind/unbind tab URL", + "de": "Tab-URL binden/lösen", + "fr": "Lier/délier l'URL de l'onglet", + "hu": "Lap URL kötése/feloldása", + "pl": "Powiąż/odwiąż URL karty", + "ru": "Привязать/отвязать URL вкладки", + "zh_CN": "绑定/解绑标签页网址", + "zh_TW": "綁定/解綁分頁網址", + "ja": "タブURLをバインド/解除" + }, + "KbReturnToBoundUrl": { + "en": "Return to bound URL", + "de": "Zur gebundenen URL zurück", + "fr": "Revenir à l'URL liée", + "hu": "Vissza a kötött URL-hez", + "pl": "Wróć do powiązanego URL", + "ru": "Вернуться к привязанному URL", + "zh_CN": "返回绑定的网址", + "zh_TW": "返回綁定的網址", + "ja": "バインドされたURLに戻る" }, "KbOpenNewTabInCurrPanel": { "en": "Open a new tab in the active panel", diff --git a/src/_locales/dict.common.ts b/src/_locales/dict.common.ts index 03c52f639..4e8c3c538 100644 --- a/src/_locales/dict.common.ts +++ b/src/_locales/dict.common.ts @@ -1955,6 +1955,17 @@ export const commonTranslations: Translations = { zh_TW: '取消錨定', ja: 'アンカー解除', }, + 'menu.tab.return_to_bound': { + en: 'Return to bound URL', + de: 'Zur gebundenen URL zurück', + fr: 'Revenir à l’URL liée', + hu: 'Vissza a kötött URL-hez', + pl: 'Wróć do powiązanego URL', + ru: 'Вернуться к привязанному URL', + zh_CN: '返回绑定的网址', + zh_TW: '返回綁定的網址', + ja: 'バインドされたURLに戻る', + }, 'menu.tab.duplicate': { en: 'Duplicate', de: 'Duplizieren', diff --git a/src/_locales/dict.setup-page.ts b/src/_locales/dict.setup-page.ts index 822449c60..2052f5574 100644 --- a/src/_locales/dict.setup-page.ts +++ b/src/_locales/dict.setup-page.ts @@ -707,39 +707,6 @@ Liste de “sous-chaines” ou “/RegExp/” avec une entrée par ligne: zh_TW: '已釘選分頁', ja: 'ピン留めされたタブ', }, - 'settings.nav_settings_anchor_tabs': { - en: 'Bind URL tabs', - de: 'Anker-Tabs', - fr: 'Onglets ancrés', - hu: 'Horgonyzott lapok', - pl: 'Zakotwiczone karty', - ru: 'Привязанные вкладки', - zh_CN: '锚定标签页', - zh_TW: '錨定分頁', - ja: 'アンカータブ', - }, - 'settings.anchor_tabs_reset_indicator': { - en: 'Reset indicator', - de: 'Zurücksetzen-Indikator', - fr: 'Indicateur de réinitialisation', - hu: 'Visszaállítási jelző', - pl: 'Wskaźnik resetowania', - ru: 'Индикатор сброса', - zh_CN: '重置指示符', - zh_TW: '重置指示符', - ja: 'リセットインジケーター', - }, - 'settings.anchor_tabs_reset_indicator_note': { - en: 'Accepts text and emojis', - de: 'Akzeptiert Text und Emojis', - fr: 'Accepte du texte et des emojis', - hu: 'Elfogad szöveget és emojikat', - pl: 'Akceptuje tekst i emoji', - ru: 'Принимает текст и эмодзи', - zh_CN: '支持文字和表情符号', - zh_TW: '支援文字和表情符號', - ja: 'テキストと絵文字を使用できます', - }, 'settings.nav_settings_tabs_tree': { en: 'Tabs tree', de: 'Tab-Baum', @@ -5655,6 +5622,28 @@ Przykłady: "*", "ctrl+$", "ctrl+alt+g"`, zh_TW: '編輯標題', ja: 'タイトルを編集', }, + 'settings.tab_action_toggle_bind_url': { + en: 'bind/unbind URL', + de: 'URL binden/lösen', + fr: 'lier/délier l’URL', + hu: 'URL kötése/feloldása', + pl: 'powiąż/odwiąż URL', + ru: 'привязать/отвязать URL', + zh_CN: '绑定/解绑网址', + zh_TW: '綁定/解綁網址', + ja: 'URLをバインド/解除', + }, + 'settings.tab_action_return_to_bound': { + en: 'return to bound URL', + de: 'zur gebundenen URL zurück', + fr: 'revenir à l’URL liée', + hu: 'vissza a kötött URL-hez', + pl: 'wróć do powiązanego URL', + ru: 'вернуться к привязанному URL', + zh_CN: '返回绑定的网址', + zh_TW: '返回綁定的網址', + ja: 'バインドされたURLに戻る', + }, 'settings.tab_action_none': { en: 'none', de: 'Nichts', diff --git a/src/defaults/menu.ts b/src/defaults/menu.ts index 324165b38..8d5e4c72b 100644 --- a/src/defaults/menu.ts +++ b/src/defaults/menu.ts @@ -35,7 +35,8 @@ export const TABS_MENU: MenuConf = [ }, 'separator-2', 'pin', - 'anchor', + 'bind_url', + 'return_to_bound', 'duplicate', 'discard', 'copyTabsUrls', diff --git a/src/defaults/settings.ts b/src/defaults/settings.ts index 2e5f566bf..850d54f7b 100644 --- a/src/defaults/settings.ts +++ b/src/defaults/settings.ts @@ -133,9 +133,6 @@ export const DEFAULT_SETTINGS: SettingsState = { pinnedNoUnloadExplicit: true, pinnedForcedDiscard: false, - // Anchor tabs - anchorTabsResetIndicator: '\\', - // Tabs tree tabsTree: true, tabsTreeLimit: 'none', @@ -313,13 +310,13 @@ export const SETTINGS_OPTIONS = { scrollThroughTabs: ['panel', 'global', 'psp', 'psg', 'none'], discardFoldedDelayUnit: ['sec', 'min'], tabDoubleClick: ['reload', 'duplicate', 'dup_child', 'pin', 'mute', 'clear_cookies', 'exp', - 'new_after', 'new_child', 'close', 'edit_title', 'none'], + 'new_after', 'new_child', 'close', 'edit_title', 'toggle_bind_url', 'return_to_bound', 'none'], tabLongLeftClick: ['reload', 'discard', 'duplicate', 'dup_child', 'pin', 'mute', 'clear_cookies', 'new_after', - 'new_child', 'edit_title', 'none'], + 'new_child', 'edit_title', 'toggle_bind_url', 'return_to_bound', 'none'], tabLongRightClick: ['reload', 'discard', 'duplicate', 'dup_child', 'pin', 'mute', 'clear_cookies', 'new_after', - 'new_child', 'edit_title', 'none'], - tabMiddleClick: ['close', 'discard', 'discard_or_close', 'duplicate', 'dup_child', 'none'], - tabPinnedMiddleClick: ['close', 'discard', 'discard_or_close', 'duplicate', 'unpin', 'none'], + 'new_child', 'edit_title', 'toggle_bind_url', 'return_to_bound', 'none'], + tabMiddleClick: ['close', 'discard', 'discard_or_close', 'duplicate', 'dup_child', 'toggle_bind_url', 'return_to_bound', 'none'], + tabPinnedMiddleClick: ['close', 'discard', 'discard_or_close', 'duplicate', 'unpin', 'toggle_bind_url', 'return_to_bound', 'none'], tabMiddleClickModifier: ['discard', 'discard_or_close', 'duplicate', 'dup_child', 'edit_title', 'none'], tabCloseMiddleClick: ['close', 'discard', 'discard_or_close'], tabsPanelLeftClickAction: ['prev', 'expand', 'parent', 'tab', 'none'], diff --git a/src/manifest.json b/src/manifest.json index 931c88272..fb44ad89c 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -415,8 +415,11 @@ "edit_title": { "description": "__MSG_KbEditTitle__" }, - "toggle_anchor_tab": { - "description": "__MSG_KbToggleAnchorTab__" + "toggle_bind_url": { + "description": "__MSG_KbToggleBindUrl__" + }, + "return_to_bound_url": { + "description": "__MSG_KbReturnToBoundUrl__" }, "sort_tabs_by_title_asc": { "description": "__MSG_KbSortTabsByTitleAsc__" diff --git a/src/page.setup/components/keybindings.vue b/src/page.setup/components/keybindings.vue index b46381650..7e3a653a3 100644 --- a/src/page.setup/components/keybindings.vue +++ b/src/page.setup/components/keybindings.vue @@ -65,7 +65,8 @@ KeybindingField(:keybinding="Keybindings.reactive.byName.group_tabs_act") KeybindingField(:keybinding="Keybindings.reactive.byName.flatten_tabs") KeybindingField(:keybinding="Keybindings.reactive.byName.edit_title") - KeybindingField(:keybinding="Keybindings.reactive.byName.toggle_anchor_tab") + KeybindingField(:keybinding="Keybindings.reactive.byName.toggle_bind_url") + KeybindingField(:keybinding="Keybindings.reactive.byName.return_to_bound_url") section h2 {{translate('settings.kb_tabs_open')}} diff --git a/src/page.setup/components/settings.tabs.vue b/src/page.setup/components/settings.tabs.vue index 94a87c75e..427dea9ba 100644 --- a/src/page.setup/components/settings.tabs.vue +++ b/src/page.setup/components/settings.tabs.vue @@ -310,18 +310,6 @@ section(ref="el") :default="DEFAULT_SETTINGS.pinnedNoUnloadExplicit" @update:value="Settings.saveDebounced(150)") - .wrapper(ref="anchorTabsEl") - .sub-title: .text {{translate('settings.nav_settings_anchor_tabs')}} - TextField.-inline.-no-separator( - label="settings.anchor_tabs_reset_indicator" - or="\\" - :note="translate('settings.anchor_tabs_reset_indicator_note')" - v-model:value="Settings.state.anchorTabsResetIndicator" - dbg="anchorTabsResetIndicator" - :default="DEFAULT_SETTINGS.anchorTabsResetIndicator" - :line="true" - @update:value="Settings.saveDebounced(500)") - .wrapper(ref="tabsTreeEl") .sub-title: .text {{translate('settings.nav_settings_tabs_tree')}} ToggleField.-no-separator( @@ -682,7 +670,6 @@ import TextField from 'src/components/text-field.vue' const el = ref<HTMLElement | null>(null) const newTabPosEl = ref<HTMLElement | null>(null) const pinTabsEl = ref<HTMLElement | null>(null) -const anchorTabsEl = ref<HTMLElement | null>(null) const tabsTreeEl = ref<HTMLElement | null>(null) const tabsColorEl = ref<HTMLElement | null>(null) const tabsPreviewEl = ref<HTMLElement | null>(null) @@ -807,7 +794,6 @@ onMounted(() => { SetupPage.registerEl('settings_tabs', el.value) SetupPage.registerEl('settings_new_tab_position', newTabPosEl.value) SetupPage.registerEl('settings_pinned_tabs', pinTabsEl.value) - SetupPage.registerEl('settings_anchor_tabs', anchorTabsEl.value) SetupPage.registerEl('settings_tabs_tree', tabsTreeEl.value) SetupPage.registerEl('settings_tabs_colorization', tabsColorEl.value) SetupPage.registerEl('settings_tabs_preview', tabsPreviewEl.value) diff --git a/src/services/keybindings.fg.ts b/src/services/keybindings.fg.ts index dcffbafd3..f6d02b39f 100644 --- a/src/services/keybindings.fg.ts +++ b/src/services/keybindings.fg.ts @@ -266,7 +266,8 @@ function onCmd(name: string): void { else if (name === 'group_tabs_act') onKeyGroupTabs(true) else if (name === 'flatten_tabs') onKeyFlattenTabs() else if (name === 'edit_title') onKeyEditTitle() - else if (name === 'toggle_anchor_tab') onKeyToggleAnchorTab() + else if (name === 'toggle_bind_url') onKeyToggleBindUrl() + else if (name === 'return_to_bound_url') onKeyReturnToBoundUrl() else if (name === 'sel_child_tabs') onKeySelChildTabs() else if (name === 'sort_tabs_by_title_asc') onKeySortTabs(SortBy.Title, 1) else if (name === 'sort_tabs_by_title_des') onKeySortTabs(SortBy.Title, -1) @@ -1531,12 +1532,18 @@ function onKeyPinTabs() { else Tabs.pinTabs(ids) } -function onKeyToggleAnchorTab() { +function onKeyToggleBindUrl() { const ids = Selection.isTabs() ? Selection.ids() : [Tabs.activeId] if (!ids.length) return Tabs.toggleAnchorTabs(ids) } +function onKeyReturnToBoundUrl() { + const tabId = Selection.isTabs() ? Selection.getFirst() : Tabs.activeId + if (tabId === NOID) return + Tabs.returnToAnchor(tabId) +} + function onKeyHidePanel() { let panelId = Selection.getFirst() if (!Sidebar.panelsById[panelId]) panelId = Sidebar.activePanelId diff --git a/src/services/menu.fg.options.tabs.ts b/src/services/menu.fg.options.tabs.ts index 234511378..c1f00c38e 100644 --- a/src/services/menu.fg.options.tabs.ts +++ b/src/services/menu.fg.options.tabs.ts @@ -260,7 +260,7 @@ export const tabsMenuOptions: Record<string, () => MenuOption | MenuOption[] | u } }, - anchor: () => { + bind_url: () => { const selected = Selection.ids() const firstTab = Tabs.byId[selected[0]] if (!firstTab) return @@ -273,6 +273,17 @@ export const tabsMenuOptions: Record<string, () => MenuOption | MenuOption[] | u } }, + return_to_bound: () => { + const selected = Selection.ids() + const firstTab = Tabs.byId[selected[0]] + if (!firstTab?.anchorUrl || firstTab.url === firstTab.anchorUrl) return + return { + label: translate('menu.tab.return_to_bound'), + icon: 'icon_undo', + onClick: () => Tabs.returnToAnchor(firstTab.id), + } + }, + reload: () => { return { label: translate('menu.tab.reload'), diff --git a/src/services/setup-page.fg.ts b/src/services/setup-page.fg.ts index 3d0b58a4a..faf7f513f 100644 --- a/src/services/setup-page.fg.ts +++ b/src/services/setup-page.fg.ts @@ -81,7 +81,6 @@ export let reactive: SetupPageReactiveState = { { active: false, name: 'settings_tabs', lvl: 1 }, { active: false, name: 'settings_new_tab_position', lvl: 2 }, { active: false, name: 'settings_pinned_tabs', lvl: 2 }, - { active: false, name: 'settings_anchor_tabs', lvl: 2 }, { active: false, name: 'settings_tabs_tree', lvl: 2 }, { active: false, name: 'settings_tabs_colorization', lvl: 2 }, { active: false, name: 'settings_tabs_preview', lvl: 2 }, diff --git a/src/sidebar/components/tab.vue b/src/sidebar/components/tab.vue index ae79a7bd9..b8403263e 100644 --- a/src/sidebar/components/tab.vue +++ b/src/sidebar/components/tab.vue @@ -36,28 +36,20 @@ .color-layer(v-if="tabColor" :style="{ '--tab-color': tabColor }") .flash-fx(ref="flashFxEl") .unread-mark(v-if="tab.reactive.unread") - .fav-anchor( - @mousedown.stop="onFavMouseDown" - @mouseup.stop="onFavMouseUp") - .fav(@dragstart.stop.prevent) - img.fav-icon(ref="favImgEl" @error="onError" draggable="false") - svg.fav-icon: use(ref="favSvgUseEl" href="#icon_ff") - svg.fav-icon.-anchor-hover(v-if="tab.reactive.anchored"): use(href="#icon_undo") - .exp( - v-if="tab.reactive.isParent" - @dblclick.prevent.stop - @mousedown.stop="onExpandMouseDown" - @mouseup.stop="onExpandMouseUp") - svg.exp-icon: use(href="#icon_expand") - .badge - .progress-spinner(v-if="Settings.state.animations") - svg.progress-spinner(v-else): use(href="#icon_hourglass") - .child-count(v-if="tab.reactive.folded && tab.reactive.branchLen") {{tab.reactive.branchLen}} - .title.-anchor-back(v-if="tab.reactive.anchoredAway") - span.back-to-prefix Back to - span.back-to-title {{tab.anchorTitle}} - .anchor-reset-indicator(v-if="tab.reactive.anchoredAway && !iconOnly") - | {{Settings.state.anchorTabsResetIndicator}} + .fav(@dragstart.stop.prevent) + img.fav-icon(ref="favImgEl" @error="onError" draggable="false") + svg.fav-icon: use(ref="favSvgUseEl" href="#icon_ff") + .exp( + v-if="tab.reactive.isParent" + @dblclick.prevent.stop + @mousedown.stop="onExpandMouseDown" + @mouseup="onExpandMouseUp") + svg.exp-icon: use(href="#icon_expand") + .badge + .progress-spinner(v-if="Settings.state.animations") + svg.progress-spinner(v-else): use(href="#icon_hourglass") + .child-count(v-if="tab.reactive.folded && tab.reactive.branchLen") {{tab.reactive.branchLen}} + .bind-mark(v-if="tab.reactive.anchored") .audio( v-if="tab.reactive.mediaAudible || tab.reactive.mediaMuted || tab.reactive.mediaPaused" @mousedown.stop.prevent="onAudioMouseDown($event, tab)" @@ -351,6 +343,12 @@ function onMouseDown(e: MouseEvent): void { } else if (Settings.state.tabPinnedMiddleClick === 'unpin') { Tabs.unpinTabs([tab.id]) return + } else if (Settings.state.tabPinnedMiddleClick === 'toggle_bind_url') { + Tabs.toggleAnchorTabs(selectedTabs) + return + } else if (Settings.state.tabPinnedMiddleClick === 'return_to_bound') { + Tabs.returnToAnchor(tab.id) + return } } @@ -368,6 +366,10 @@ function onMouseDown(e: MouseEvent): void { Tabs.duplicateTabs([tab.id]) } else if (Settings.state.tabMiddleClick === 'dup_child') { Tabs.duplicateTabs([tab.id], true) + } else if (Settings.state.tabMiddleClick === 'toggle_bind_url') { + Tabs.toggleAnchorTabs(selectedTabs) + } else if (Settings.state.tabMiddleClick === 'return_to_bound') { + Tabs.returnToAnchor(tab.id) } } } @@ -403,26 +405,17 @@ function longClickFeedback(e: MouseEvent) { else if (action === 'new_after') Tabs.createTabAfter(tab.id) else if (action === 'new_child' && !tab.pinned) Tabs.createChildTab(tab.id) else if (action === 'edit_title' && !tab.pinned) Tabs.editTabTitle([tab.id]) - else noop = true + else if (action === 'toggle_bind_url') Tabs.toggleAnchorTabs([tab.id]) + else if (action === 'return_to_bound') { + if (tab.anchorUrl && tab.url !== tab.anchorUrl) Tabs.returnToAnchor(tab.id) + else noop = true + } else noop = true if (!noop) Tabs.triggerFlashAnimation(tab) return !noop } -function onFavMouseDown(e: MouseEvent): void { - if (tab.reactive.anchoredAway && e.button === 0) return - onMouseDown(e) -} - -function onFavMouseUp(e: MouseEvent): void { - if (tab.reactive.anchoredAway && e.button === 0) { - Tabs.returnToAnchor(tab.id) - return - } - onMouseUp(e) -} - function onMouseUp(e: MouseEvent): void { const sameTarget = Mouse.isTarget('tab', tab.id) const sameTargetType = Mouse.isTarget('tab') @@ -535,6 +528,8 @@ function onDoubleClick(): void { if (shouldBeConvertedToGroup()) convertToGroup() else Tabs.removeTabs([tab.id]) } else if (dc === 'edit_title') Tabs.editTabTitle([tab.id]) + else if (dc === 'toggle_bind_url') Tabs.toggleAnchorTabs([tab.id]) + else if (dc === 'return_to_bound') Tabs.returnToAnchor(tab.id) } function onDragStart(e: DragEvent): void { diff --git a/src/styles/sidebar/tab.styl b/src/styles/sidebar/tab.styl index b0bcf977e..b11fe9bfa 100644 --- a/src/styles/sidebar/tab.styl +++ b/src/styles/sidebar/tab.styl @@ -315,12 +315,6 @@ // --- // -- Favicon // - -.Tab .fav-anchor - display: flex - align-items: center - flex-shrink: 0 - position: relative - .Tab .fav position: relative width: 16px @@ -330,11 +324,8 @@ z-index: 10 transform: translateZ(0) -.Tab[data-pin="false"] .fav-anchor - // Padding extends the hit area to match the visual highlight box. - // Margins reduced by same amounts so flex layout spacing is unchanged. - padding: calc((var(--tabs-height) - 16px) / 2 - 2px) var(--tabs-inner-gap) - margin: 0 0 0 2px +.Tab[data-pin="false"] .fav + margin: 0 var(--tabs-inner-gap) 0 calc(var(--tabs-inner-gap) + 2px) z-index: 20 .Tab .fav-icon @@ -347,13 +338,6 @@ transition: opacity var(--d-fast) .Tab img.fav-icon display: none -.Tab .fav-icon.-anchor-hover - opacity: 0 - pointer-events: none -.Tab[data-anchored-away="true"] .fav-anchor:hover .fav-icon:not(.-anchor-hover) - opacity: 0 -.Tab[data-anchored-away="true"] .fav-anchor:hover .fav-icon.-anchor-hover - opacity: 1 .Tab .fav svg opacity: .7 @@ -700,74 +684,30 @@ // --- -// -- Anchor tabs +// -- Bind URL tabs // - -// Anchored-away: restrict hover highlight to favicon area only -.Tab[data-anchored-away="true"]:hover .body:before - background-color: initial - -// Permanent highlight covering favicon + reset indicator for all anchored tabs -.Tab[data-anchored="true"] .fav-anchor - &:before - content: '' - position: absolute - inset: 0 - border-radius: var(--tabs-border-radius) - background-color: var(--frame-el-overlay-clicked-bg) - z-index: -1 - -// Hover effects for anchored-away tabs — all driven by .fav-anchor:hover -.Tab[data-anchored-away="true"] .fav-anchor:hover - cursor: pointer - flex-grow: 1 - // Hide permanent highlight background on hover - &:before - background-color: transparent - // Extra highlight on favicon area - .fav:after - content: '' - position: absolute - inset: -4px - border-radius: 4px - background-color: var(--frame-el-overlay-selected-bg) - z-index: -1 - // Show "Back to" title - .title.-anchor-back - display: flex - // Fade out indicator (opacity keeps layout so hover area stays stable; no flicker) - .anchor-reset-indicator - opacity: 0 - pointer-events: none - -// Hide title box on hover (Back to title is shown inside .fav-anchor instead) -.Tab[data-anchored-away="true"] .fav-anchor:hover ~ .t-box - display: none +// Passive marker at the bottom-left corner of the favicon. Non-interactive: +// bind/unbind/return are triggered via mouse actions, keybinding or context menu. +.Tab .bind-mark + position: absolute + left: -2px + bottom: -2px + width: 6px + height: 6px + border-radius: 50% + background-color: var(--tabs-normal-fg) + box-shadow: 0 0 0 1.5px var(--frame-bg) + pointer-events: none + z-index: 12 + animation: appearing-animation var(--d-norm) -// Reset indicator between favicon and title -.Tab .anchor-reset-indicator - flex-shrink: 0 - font: var(--tabs-font) - padding: 0 2px 0 4px - opacity: 0.6 - color: var(--tabs-normal-fg) +.Tab[data-active="true"] .bind-mark + background-color: var(--tabs-activated-fg) -// "Back to" title inside .fav-anchor — hidden by default, shown on hover -.Tab .fav-anchor .title.-anchor-back - display: none - flex-grow: 1 - margin-left: var(--tabs-inner-gap) - align-items: center - gap: 0.25em - overflow: hidden - white-space: nowrap - .back-to-prefix - font-weight: normal - flex-shrink: 0 - .back-to-title - font-weight: bold - overflow: hidden - text-overflow: ellipsis +// Navigated away from the bound URL — highlight the marker +.Tab[data-anchored-away="true"] .bind-mark + background-color: var(--accent) // --- // -- Keyframes diff --git a/src/types/settings.ts b/src/types/settings.ts index 437a80337..514dbb100 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -131,9 +131,6 @@ export interface SettingsState { pinnedNoUnloadExplicit: boolean pinnedForcedDiscard: boolean - // Anchor tabs - anchorTabsResetIndicator: string - // Tabs tree tabsTree: boolean groupOnOpen?: boolean // DEPR From 1b732b009b6b6865352fa53fabc38ea5a35dde1e Mon Sep 17 00:00:00 2001 From: VictorVow <12621257+VictorVow@users.noreply.github.com> Date: Sat, 23 May 2026 13:51:57 +0100 Subject: [PATCH 07/10] feat(anchor-tabs): icon-based bind marker and restored "Back to" hover Refine the bound-URL tab affordances based on follow-up feedback: - Replace the plain corner dot with a state-aware icon at the favicon's bottom-left: the "home" icon while the tab is on its bound URL and the "undo" icon once it has navigated away. The marker inherits the favicon foreground colour in both states (no accent tint). - Restore the hover interaction for anchored-away tabs: hovering the favicon area reveals "Back to <title>", shows a pointer cursor and a highlight, and clicking returns to the bound URL. - Use the "home" icon for the "Unbind URL" context-menu entry (the bound state) while "Bind URL" keeps the "undo" icon. - Inject home.svg#icon_home into both sidebar and setup pages so the icon is available in the live tab marker, the context menu and the menu editor. --- src/page.setup/setup.html | 1 + src/services/menu.fg.options.tabs.ts | 2 +- src/sidebar/components/tab.vue | 48 +++++++++++----- src/sidebar/sidebar.html | 1 + src/styles/sidebar/tab.styl | 82 +++++++++++++++++++++++----- 5 files changed, 104 insertions(+), 30 deletions(-) diff --git a/src/page.setup/setup.html b/src/page.setup/setup.html index 89e32a581..1881aeac2 100644 --- a/src/page.setup/setup.html +++ b/src/page.setup/setup.html @@ -94,6 +94,7 @@ <inject>svg://src/assets/sync.svg#icon_sync</inject> <inject>svg://src/assets/private-window.svg#icon_priv_win</inject> <inject>svg://src/assets/undo.svg#icon_undo</inject> + <inject>svg://src/assets/home.svg#icon_home</inject> </div> </body> </html> diff --git a/src/services/menu.fg.options.tabs.ts b/src/services/menu.fg.options.tabs.ts index c1f00c38e..c5704104a 100644 --- a/src/services/menu.fg.options.tabs.ts +++ b/src/services/menu.fg.options.tabs.ts @@ -266,7 +266,7 @@ export const tabsMenuOptions: Record<string, () => MenuOption | MenuOption[] | u if (!firstTab) return return { label: translate('menu.tab.' + (firstTab.anchorUrl ? 'unanchor' : 'anchor')), - icon: 'icon_undo', + icon: firstTab.anchorUrl ? 'icon_home' : 'icon_undo', onClick: firstTab.anchorUrl ? () => Tabs.unanchorTabs(selected) : () => Tabs.anchorTabs(selected), diff --git a/src/sidebar/components/tab.vue b/src/sidebar/components/tab.vue index b8403263e..9b2f7289b 100644 --- a/src/sidebar/components/tab.vue +++ b/src/sidebar/components/tab.vue @@ -36,20 +36,27 @@ .color-layer(v-if="tabColor" :style="{ '--tab-color': tabColor }") .flash-fx(ref="flashFxEl") .unread-mark(v-if="tab.reactive.unread") - .fav(@dragstart.stop.prevent) - img.fav-icon(ref="favImgEl" @error="onError" draggable="false") - svg.fav-icon: use(ref="favSvgUseEl" href="#icon_ff") - .exp( - v-if="tab.reactive.isParent" - @dblclick.prevent.stop - @mousedown.stop="onExpandMouseDown" - @mouseup="onExpandMouseUp") - svg.exp-icon: use(href="#icon_expand") - .badge - .progress-spinner(v-if="Settings.state.animations") - svg.progress-spinner(v-else): use(href="#icon_hourglass") - .child-count(v-if="tab.reactive.folded && tab.reactive.branchLen") {{tab.reactive.branchLen}} - .bind-mark(v-if="tab.reactive.anchored") + .fav-anchor( + @mousedown.stop="onFavMouseDown" + @mouseup.stop="onFavMouseUp") + .fav(@dragstart.stop.prevent) + img.fav-icon(ref="favImgEl" @error="onError" draggable="false") + svg.fav-icon: use(ref="favSvgUseEl" href="#icon_ff") + .exp( + v-if="tab.reactive.isParent" + @dblclick.prevent.stop + @mousedown.stop="onExpandMouseDown" + @mouseup="onExpandMouseUp") + svg.exp-icon: use(href="#icon_expand") + .badge + .progress-spinner(v-if="Settings.state.animations") + svg.progress-spinner(v-else): use(href="#icon_hourglass") + .child-count(v-if="tab.reactive.folded && tab.reactive.branchLen") {{tab.reactive.branchLen}} + .bind-mark(v-if="tab.reactive.anchored") + svg.bind-mark-icon: use(:href="tab.reactive.anchoredAway ? '#icon_undo' : '#icon_home'") + .title.-anchor-back(v-if="tab.reactive.anchoredAway && !iconOnly") + span.back-to-prefix Back to + span.back-to-title {{tab.anchorTitle}} .audio( v-if="tab.reactive.mediaAudible || tab.reactive.mediaMuted || tab.reactive.mediaPaused" @mousedown.stop.prevent="onAudioMouseDown($event, tab)" @@ -416,6 +423,19 @@ function longClickFeedback(e: MouseEvent) { return !noop } +function onFavMouseDown(e: MouseEvent): void { + if (tab.reactive.anchoredAway && e.button === 0) return + onMouseDown(e) +} + +function onFavMouseUp(e: MouseEvent): void { + if (tab.reactive.anchoredAway && e.button === 0) { + Tabs.returnToAnchor(tab.id) + return + } + onMouseUp(e) +} + function onMouseUp(e: MouseEvent): void { const sameTarget = Mouse.isTarget('tab', tab.id) const sameTargetType = Mouse.isTarget('tab') diff --git a/src/sidebar/sidebar.html b/src/sidebar/sidebar.html index 47a44064a..27347edcf 100644 --- a/src/sidebar/sidebar.html +++ b/src/sidebar/sidebar.html @@ -35,6 +35,7 @@ <inject>svg://src/assets/firefox.svg#icon_ff</inject> <inject>svg://src/assets/firefox-min.svg#icon_ffm</inject> <inject>svg://src/assets/undo.svg#icon_undo</inject> + <inject>svg://src/assets/home.svg#icon_home</inject> <inject>svg://src/assets/reload.svg#icon_reload</inject> <inject>svg://src/assets/star.svg#icon_star</inject> <inject>svg://src/assets/star-add.svg#icon_star_add</inject> diff --git a/src/styles/sidebar/tab.styl b/src/styles/sidebar/tab.styl index b11fe9bfa..e87330e27 100644 --- a/src/styles/sidebar/tab.styl +++ b/src/styles/sidebar/tab.styl @@ -315,6 +315,12 @@ // --- // -- Favicon // - +.Tab .fav-anchor + display: flex + align-items: center + flex-shrink: 0 + position: relative + .Tab .fav position: relative width: 16px @@ -324,8 +330,11 @@ z-index: 10 transform: translateZ(0) -.Tab[data-pin="false"] .fav - margin: 0 var(--tabs-inner-gap) 0 calc(var(--tabs-inner-gap) + 2px) +.Tab[data-pin="false"] .fav-anchor + // Padding extends the hit area to match the visual highlight box. + // Margins reduced by same amounts so flex layout spacing is unchanged. + padding: calc((var(--tabs-height) - 16px) / 2 - 2px) var(--tabs-inner-gap) + margin: 0 0 0 2px z-index: 20 .Tab .fav-icon @@ -687,27 +696,70 @@ // -- Bind URL tabs // - -// Passive marker at the bottom-left corner of the favicon. Non-interactive: -// bind/unbind/return are triggered via mouse actions, keybinding or context menu. +// Marker at the bottom-left corner of the favicon. Shows the "home" icon when +// the tab is on its bound URL and the "undo" icon once it has navigated away. .Tab .bind-mark position: absolute - left: -2px - bottom: -2px - width: 6px - height: 6px + left: -3px + bottom: -3px + width: 11px + height: 11px + display: flex + align-items: center + justify-content: center border-radius: 50% - background-color: var(--tabs-normal-fg) - box-shadow: 0 0 0 1.5px var(--frame-bg) + background-color: var(--frame-bg) pointer-events: none z-index: 12 animation: appearing-animation var(--d-norm) -.Tab[data-active="true"] .bind-mark - background-color: var(--tabs-activated-fg) +.Tab .bind-mark-icon + width: 9px + height: 9px + fill: var(--tabs-normal-fg) + +.Tab[data-active="true"] .bind-mark-icon + fill: var(--tabs-activated-fg) + +// Anchored-away: restrict hover highlight to the favicon area only +.Tab[data-anchored-away="true"]:hover .body:before + background-color: initial -// Navigated away from the bound URL — highlight the marker -.Tab[data-anchored-away="true"] .bind-mark - background-color: var(--accent) +// Hover over an anchored-away tab reveals the "Back to <title>" affordance and +// a pointer cursor; clicking the favicon area returns to the bound URL. +.Tab[data-anchored-away="true"] .fav-anchor:hover + cursor: pointer + flex-grow: 1 + .fav:after + content: '' + position: absolute + inset: -4px + border-radius: 4px + background-color: var(--frame-el-overlay-selected-bg) + z-index: -1 + .title.-anchor-back + display: flex + +// Hide the normal title while the "Back to" affordance is shown +.Tab[data-anchored-away="true"] .fav-anchor:hover ~ .t-box + display: none + +// "Back to" title inside .fav-anchor — hidden by default, shown on hover +.Tab .fav-anchor .title.-anchor-back + display: none + flex-grow: 1 + margin-left: var(--tabs-inner-gap) + align-items: center + gap: 0.25em + overflow: hidden + white-space: nowrap + .back-to-prefix + font-weight: normal + flex-shrink: 0 + .back-to-title + font-weight: bold + overflow: hidden + text-overflow: ellipsis // --- // -- Keyframes From 14e3d703f103c789eabefc12e7d50fa51f65aead Mon Sep 17 00:00:00 2001 From: VictorVow <12621257+VictorVow@users.noreply.github.com> Date: Sat, 23 May 2026 13:57:05 +0100 Subject: [PATCH 08/10] feat(anchor-tabs): remove "Back to <title>" hover interaction Drop the favicon hover affordance for anchored-away tabs. Hovering no longer reveals "Back to <title>" and clicking the favicon no longer returns to the bound URL. - Remove the .fav-anchor wrapper (back to plain .fav), the .title.-anchor-back element and the onFavMouseDown/onFavMouseUp handlers in tab.vue. - Revert the .fav layout rule and delete the hover styles (.fav-anchor:hover, .title.-anchor-back and the body-highlight override) in tab.styl. The passive corner marker (home icon when bound, undo icon when navigated away) remains. Returning to the bound URL is still available via the context menu, the return_to_bound_url keybinding and the configurable mouse actions. --- src/sidebar/components/tab.vue | 49 ++++++++++--------------------- src/styles/sidebar/tab.styl | 53 ++-------------------------------- 2 files changed, 17 insertions(+), 85 deletions(-) diff --git a/src/sidebar/components/tab.vue b/src/sidebar/components/tab.vue index 9b2f7289b..d1c7930f1 100644 --- a/src/sidebar/components/tab.vue +++ b/src/sidebar/components/tab.vue @@ -36,27 +36,21 @@ .color-layer(v-if="tabColor" :style="{ '--tab-color': tabColor }") .flash-fx(ref="flashFxEl") .unread-mark(v-if="tab.reactive.unread") - .fav-anchor( - @mousedown.stop="onFavMouseDown" - @mouseup.stop="onFavMouseUp") - .fav(@dragstart.stop.prevent) - img.fav-icon(ref="favImgEl" @error="onError" draggable="false") - svg.fav-icon: use(ref="favSvgUseEl" href="#icon_ff") - .exp( - v-if="tab.reactive.isParent" - @dblclick.prevent.stop - @mousedown.stop="onExpandMouseDown" - @mouseup="onExpandMouseUp") - svg.exp-icon: use(href="#icon_expand") - .badge - .progress-spinner(v-if="Settings.state.animations") - svg.progress-spinner(v-else): use(href="#icon_hourglass") - .child-count(v-if="tab.reactive.folded && tab.reactive.branchLen") {{tab.reactive.branchLen}} - .bind-mark(v-if="tab.reactive.anchored") - svg.bind-mark-icon: use(:href="tab.reactive.anchoredAway ? '#icon_undo' : '#icon_home'") - .title.-anchor-back(v-if="tab.reactive.anchoredAway && !iconOnly") - span.back-to-prefix Back to - span.back-to-title {{tab.anchorTitle}} + .fav(@dragstart.stop.prevent) + img.fav-icon(ref="favImgEl" @error="onError" draggable="false") + svg.fav-icon: use(ref="favSvgUseEl" href="#icon_ff") + .exp( + v-if="tab.reactive.isParent" + @dblclick.prevent.stop + @mousedown.stop="onExpandMouseDown" + @mouseup="onExpandMouseUp") + svg.exp-icon: use(href="#icon_expand") + .badge + .progress-spinner(v-if="Settings.state.animations") + svg.progress-spinner(v-else): use(href="#icon_hourglass") + .child-count(v-if="tab.reactive.folded && tab.reactive.branchLen") {{tab.reactive.branchLen}} + .bind-mark(v-if="tab.reactive.anchored") + svg.bind-mark-icon: use(:href="tab.reactive.anchoredAway ? '#icon_undo' : '#icon_home'") .audio( v-if="tab.reactive.mediaAudible || tab.reactive.mediaMuted || tab.reactive.mediaPaused" @mousedown.stop.prevent="onAudioMouseDown($event, tab)" @@ -423,19 +417,6 @@ function longClickFeedback(e: MouseEvent) { return !noop } -function onFavMouseDown(e: MouseEvent): void { - if (tab.reactive.anchoredAway && e.button === 0) return - onMouseDown(e) -} - -function onFavMouseUp(e: MouseEvent): void { - if (tab.reactive.anchoredAway && e.button === 0) { - Tabs.returnToAnchor(tab.id) - return - } - onMouseUp(e) -} - function onMouseUp(e: MouseEvent): void { const sameTarget = Mouse.isTarget('tab', tab.id) const sameTargetType = Mouse.isTarget('tab') diff --git a/src/styles/sidebar/tab.styl b/src/styles/sidebar/tab.styl index e87330e27..c23a95397 100644 --- a/src/styles/sidebar/tab.styl +++ b/src/styles/sidebar/tab.styl @@ -315,12 +315,6 @@ // --- // -- Favicon // - -.Tab .fav-anchor - display: flex - align-items: center - flex-shrink: 0 - position: relative - .Tab .fav position: relative width: 16px @@ -330,11 +324,8 @@ z-index: 10 transform: translateZ(0) -.Tab[data-pin="false"] .fav-anchor - // Padding extends the hit area to match the visual highlight box. - // Margins reduced by same amounts so flex layout spacing is unchanged. - padding: calc((var(--tabs-height) - 16px) / 2 - 2px) var(--tabs-inner-gap) - margin: 0 0 0 2px +.Tab[data-pin="false"] .fav + margin: 0 var(--tabs-inner-gap) 0 calc(var(--tabs-inner-gap) + 2px) z-index: 20 .Tab .fav-icon @@ -721,46 +712,6 @@ .Tab[data-active="true"] .bind-mark-icon fill: var(--tabs-activated-fg) -// Anchored-away: restrict hover highlight to the favicon area only -.Tab[data-anchored-away="true"]:hover .body:before - background-color: initial - -// Hover over an anchored-away tab reveals the "Back to <title>" affordance and -// a pointer cursor; clicking the favicon area returns to the bound URL. -.Tab[data-anchored-away="true"] .fav-anchor:hover - cursor: pointer - flex-grow: 1 - .fav:after - content: '' - position: absolute - inset: -4px - border-radius: 4px - background-color: var(--frame-el-overlay-selected-bg) - z-index: -1 - .title.-anchor-back - display: flex - -// Hide the normal title while the "Back to" affordance is shown -.Tab[data-anchored-away="true"] .fav-anchor:hover ~ .t-box - display: none - -// "Back to" title inside .fav-anchor — hidden by default, shown on hover -.Tab .fav-anchor .title.-anchor-back - display: none - flex-grow: 1 - margin-left: var(--tabs-inner-gap) - align-items: center - gap: 0.25em - overflow: hidden - white-space: nowrap - .back-to-prefix - font-weight: normal - flex-shrink: 0 - .back-to-title - font-weight: bold - overflow: hidden - text-overflow: ellipsis - // --- // -- Keyframes // - From ba1acbad87655ed21d0385bf19556e8a1ac90fe6 Mon Sep 17 00:00:00 2001 From: VictorVow <12621257+VictorVow@users.noreply.github.com> Date: Sat, 23 May 2026 14:01:19 +0100 Subject: [PATCH 09/10] feat(anchor-tabs): use home icon for bind marker and menu entry - Always use the "home" icon for the corner marker; distinguish the navigated-away state with the accent colour instead of swapping to the "undo" icon. - Use the "home" icon for the Bind URL context-menu entry as well (it previously showed "undo" in the unbound state). --- src/services/menu.fg.options.tabs.ts | 2 +- src/sidebar/components/tab.vue | 2 +- src/styles/sidebar/tab.styl | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/services/menu.fg.options.tabs.ts b/src/services/menu.fg.options.tabs.ts index c5704104a..ff97eb1d4 100644 --- a/src/services/menu.fg.options.tabs.ts +++ b/src/services/menu.fg.options.tabs.ts @@ -266,7 +266,7 @@ export const tabsMenuOptions: Record<string, () => MenuOption | MenuOption[] | u if (!firstTab) return return { label: translate('menu.tab.' + (firstTab.anchorUrl ? 'unanchor' : 'anchor')), - icon: firstTab.anchorUrl ? 'icon_home' : 'icon_undo', + icon: 'icon_home', onClick: firstTab.anchorUrl ? () => Tabs.unanchorTabs(selected) : () => Tabs.anchorTabs(selected), diff --git a/src/sidebar/components/tab.vue b/src/sidebar/components/tab.vue index d1c7930f1..d37b85db1 100644 --- a/src/sidebar/components/tab.vue +++ b/src/sidebar/components/tab.vue @@ -50,7 +50,7 @@ svg.progress-spinner(v-else): use(href="#icon_hourglass") .child-count(v-if="tab.reactive.folded && tab.reactive.branchLen") {{tab.reactive.branchLen}} .bind-mark(v-if="tab.reactive.anchored") - svg.bind-mark-icon: use(:href="tab.reactive.anchoredAway ? '#icon_undo' : '#icon_home'") + svg.bind-mark-icon: use(href="#icon_home") .audio( v-if="tab.reactive.mediaAudible || tab.reactive.mediaMuted || tab.reactive.mediaPaused" @mousedown.stop.prevent="onAudioMouseDown($event, tab)" diff --git a/src/styles/sidebar/tab.styl b/src/styles/sidebar/tab.styl index c23a95397..fea7c02d7 100644 --- a/src/styles/sidebar/tab.styl +++ b/src/styles/sidebar/tab.styl @@ -712,6 +712,10 @@ .Tab[data-active="true"] .bind-mark-icon fill: var(--tabs-activated-fg) +// Navigated away from the bound URL — highlight the marker with the accent +.Tab[data-anchored-away="true"] .bind-mark-icon + fill: var(--accent) + // --- // -- Keyframes // - From 31c9dbff36cf91c15b3fa9741ff3d5a4cde47a9e Mon Sep 17 00:00:00 2001 From: VictorVow <12621257+VictorVow@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:03:36 +0100 Subject: [PATCH 10/10] refactor(anchor-tabs): use bind terminology internally, drop AI translations Address PR review feedback (mbnuqw/sidebery#2384): internal code still used "anchor" while the UI was renamed to "Bind/Unbind URL", mixing terminologies. Rename all feature-internal identifiers to the bind/bound terminology: - anchorUrl/anchorTitle -> boundUrl/boundTitle - anchored/anchoredAway -> bound/boundAway (incl. data-* attributes) - anchorTabs/unanchorTabs/toggleAnchorTabs/returnToAnchor -> bindTabs/unbindTabs/toggleBindTabs/returnToBound - locale keys menu.tab.anchor/unanchor -> menu.tab.bind_url/unbind_url Drop the AI-generated, non-English translations for the feature's new strings (menu entries, settings tab-action labels, keybinding command descriptions), keeping only the English source. The machine translations were unreliable (e.g. the Japanese back-translated to "anchor settings" rather than "bind this tab") and still carried the old terminology; missing locales fall back to English. --- src/_locales/dict.browser.json | 20 ++--------- src/_locales/dict.common.ts | 30 ++-------------- src/_locales/dict.setup-page.ts | 16 --------- src/defaults/mocks.tabs.fg.ts | 8 ++--- src/services/keybindings.fg.ts | 4 +-- src/services/menu.fg.options.tabs.ts | 10 +++--- src/services/tabs.fg.handlers.ts | 6 ++-- src/services/tabs.fg.ts | 54 ++++++++++++++-------------- src/sidebar/components/tab.vue | 22 ++++++------ src/styles/sidebar/tab.styl | 2 +- src/types/tabs.ts | 16 ++++----- 11 files changed, 65 insertions(+), 123 deletions(-) diff --git a/src/_locales/dict.browser.json b/src/_locales/dict.browser.json index b34feee91..941b803d7 100644 --- a/src/_locales/dict.browser.json +++ b/src/_locales/dict.browser.json @@ -424,26 +424,10 @@ "zh_TW": "編輯標題" }, "KbToggleBindUrl": { - "en": "Bind/unbind tab URL", - "de": "Tab-URL binden/lösen", - "fr": "Lier/délier l'URL de l'onglet", - "hu": "Lap URL kötése/feloldása", - "pl": "Powiąż/odwiąż URL karty", - "ru": "Привязать/отвязать URL вкладки", - "zh_CN": "绑定/解绑标签页网址", - "zh_TW": "綁定/解綁分頁網址", - "ja": "タブURLをバインド/解除" + "en": "Bind/unbind tab URL" }, "KbReturnToBoundUrl": { - "en": "Return to bound URL", - "de": "Zur gebundenen URL zurück", - "fr": "Revenir à l'URL liée", - "hu": "Vissza a kötött URL-hez", - "pl": "Wróć do powiązanego URL", - "ru": "Вернуться к привязанному URL", - "zh_CN": "返回绑定的网址", - "zh_TW": "返回綁定的網址", - "ja": "バインドされたURLに戻る" + "en": "Return to bound URL" }, "KbOpenNewTabInCurrPanel": { "en": "Open a new tab in the active panel", diff --git a/src/_locales/dict.common.ts b/src/_locales/dict.common.ts index 4e8c3c538..752ccc3b3 100644 --- a/src/_locales/dict.common.ts +++ b/src/_locales/dict.common.ts @@ -1933,38 +1933,14 @@ export const commonTranslations: Translations = { zh_TW: '取消釘選分頁', ja: 'ピン留め解除', }, - 'menu.tab.anchor': { + 'menu.tab.bind_url': { en: 'Bind URL', - de: 'Ankern', - fr: 'Ancrer', - hu: 'Horgonyzás', - pl: 'Zakotwicz', - ru: 'Привязать', - zh_CN: '锚定', - zh_TW: '錨定', - ja: 'アンカー設定', - }, - 'menu.tab.unanchor': { + }, + 'menu.tab.unbind_url': { en: 'Unbind URL', - de: 'Anker entfernen', - fr: 'Désancrer', - hu: 'Horgony eltávolítása', - pl: 'Usuń zakotwiczenie', - ru: 'Снять привязку', - zh_CN: '取消锚定', - zh_TW: '取消錨定', - ja: 'アンカー解除', }, 'menu.tab.return_to_bound': { en: 'Return to bound URL', - de: 'Zur gebundenen URL zurück', - fr: 'Revenir à l’URL liée', - hu: 'Vissza a kötött URL-hez', - pl: 'Wróć do powiązanego URL', - ru: 'Вернуться к привязанному URL', - zh_CN: '返回绑定的网址', - zh_TW: '返回綁定的網址', - ja: 'バインドされたURLに戻る', }, 'menu.tab.duplicate': { en: 'Duplicate', diff --git a/src/_locales/dict.setup-page.ts b/src/_locales/dict.setup-page.ts index 2052f5574..c6615a5f4 100644 --- a/src/_locales/dict.setup-page.ts +++ b/src/_locales/dict.setup-page.ts @@ -5624,25 +5624,9 @@ Przykłady: "*", "ctrl+$", "ctrl+alt+g"`, }, 'settings.tab_action_toggle_bind_url': { en: 'bind/unbind URL', - de: 'URL binden/lösen', - fr: 'lier/délier l’URL', - hu: 'URL kötése/feloldása', - pl: 'powiąż/odwiąż URL', - ru: 'привязать/отвязать URL', - zh_CN: '绑定/解绑网址', - zh_TW: '綁定/解綁網址', - ja: 'URLをバインド/解除', }, 'settings.tab_action_return_to_bound': { en: 'return to bound URL', - de: 'zur gebundenen URL zurück', - fr: 'revenir à l’URL liée', - hu: 'vissza a kötött URL-hez', - pl: 'wróć do powiązanego URL', - ru: 'вернуться к привязанному URL', - zh_CN: '返回绑定的网址', - zh_TW: '返回綁定的網址', - ja: 'バインドされたURLに戻る', }, 'settings.tab_action_none': { en: 'none', diff --git a/src/defaults/mocks.tabs.fg.ts b/src/defaults/mocks.tabs.fg.ts index 2e752fe3b..246a41752 100644 --- a/src/defaults/mocks.tabs.fg.ts +++ b/src/defaults/mocks.tabs.fg.ts @@ -34,8 +34,8 @@ export class MTab implements Tab { reopenInContainer?: string | undefined customTitle?: string | undefined customColor?: string | undefined - anchorUrl?: string | undefined - anchorTitle?: string | undefined + boundUrl?: string | undefined + boundTitle?: string | undefined moving?: boolean | undefined previewImg?: string | undefined removing?: boolean | undefined @@ -66,8 +66,8 @@ export class MTab implements Tab { branchColor: null, customColor: null, isGroup: false, - anchored: false, - anchoredAway: false, + bound: false, + boundAway: false, } sessionData?: TabSessionData | undefined titleEl?: HTMLElement | undefined diff --git a/src/services/keybindings.fg.ts b/src/services/keybindings.fg.ts index f6d02b39f..bc4a1ce43 100644 --- a/src/services/keybindings.fg.ts +++ b/src/services/keybindings.fg.ts @@ -1535,13 +1535,13 @@ function onKeyPinTabs() { function onKeyToggleBindUrl() { const ids = Selection.isTabs() ? Selection.ids() : [Tabs.activeId] if (!ids.length) return - Tabs.toggleAnchorTabs(ids) + Tabs.toggleBindTabs(ids) } function onKeyReturnToBoundUrl() { const tabId = Selection.isTabs() ? Selection.getFirst() : Tabs.activeId if (tabId === NOID) return - Tabs.returnToAnchor(tabId) + Tabs.returnToBound(tabId) } function onKeyHidePanel() { diff --git a/src/services/menu.fg.options.tabs.ts b/src/services/menu.fg.options.tabs.ts index ff97eb1d4..32da8bb68 100644 --- a/src/services/menu.fg.options.tabs.ts +++ b/src/services/menu.fg.options.tabs.ts @@ -265,22 +265,20 @@ export const tabsMenuOptions: Record<string, () => MenuOption | MenuOption[] | u const firstTab = Tabs.byId[selected[0]] if (!firstTab) return return { - label: translate('menu.tab.' + (firstTab.anchorUrl ? 'unanchor' : 'anchor')), + label: translate('menu.tab.' + (firstTab.boundUrl ? 'unbind_url' : 'bind_url')), icon: 'icon_home', - onClick: firstTab.anchorUrl - ? () => Tabs.unanchorTabs(selected) - : () => Tabs.anchorTabs(selected), + onClick: firstTab.boundUrl ? () => Tabs.unbindTabs(selected) : () => Tabs.bindTabs(selected), } }, return_to_bound: () => { const selected = Selection.ids() const firstTab = Tabs.byId[selected[0]] - if (!firstTab?.anchorUrl || firstTab.url === firstTab.anchorUrl) return + if (!firstTab?.boundUrl || firstTab.url === firstTab.boundUrl) return return { label: translate('menu.tab.return_to_bound'), icon: 'icon_undo', - onClick: () => Tabs.returnToAnchor(firstTab.id), + onClick: () => Tabs.returnToBound(firstTab.id), } }, diff --git a/src/services/tabs.fg.handlers.ts b/src/services/tabs.fg.handlers.ts index fd58331a7..5091beb66 100644 --- a/src/services/tabs.fg.handlers.ts +++ b/src/services/tabs.fg.handlers.ts @@ -820,9 +820,9 @@ function onTabUpdated(tabId: ID, change: browser.tabs.ChangeInfo, nativeTab: Nat IPPC.onHashMsg(tab, change.url) } - // Handle anchor tab URL change - if (tab.anchorUrl) { - tab.reactive.anchoredAway = change.url !== tab.anchorUrl + // Handle bound tab URL change + if (tab.boundUrl) { + tab.reactive.boundAway = change.url !== tab.boundUrl } // Reset favicon (to cached) diff --git a/src/services/tabs.fg.ts b/src/services/tabs.fg.ts index 5f9b13796..ca674a5bc 100644 --- a/src/services/tabs.fg.ts +++ b/src/services/tabs.fg.ts @@ -153,8 +153,8 @@ export function mutateNativeTabToSideberyTab(nativeTab: T.NativeTab): T.Tab { branchColor: null, color: null, isGroup: tab.isGroup, - anchored: false, - anchoredAway: false, + bound: false, + boundAway: false, } } @@ -506,11 +506,11 @@ function restoreTab( tab.reactive.folded = tab.folded = !!props.folded if (props.customTitle) tab.customTitle = props.customTitle if (props.customColor) tab.reactive.customColor = tab.customColor = props.customColor - if (props.anchorUrl) { - tab.anchorUrl = props.anchorUrl - tab.anchorTitle = props.anchorTitle - tab.reactive.anchored = true - tab.reactive.anchoredAway = tab.url !== props.anchorUrl + if (props.boundUrl) { + tab.boundUrl = props.boundUrl + tab.boundTitle = props.boundTitle + tab.reactive.bound = true + tab.reactive.boundAway = tab.url !== props.boundUrl } } else { Logs.warn(`Tabs.restoreTab: no props for: "${tab.id} i${tab.index} url${tab.url}"`) @@ -738,8 +738,8 @@ export function cacheTabsData(delay = 300): void { if (tab.cookieStoreId !== D.CONTAINER_ID) info.ctx = tab.cookieStoreId if (tab.customTitle) info.customTitle = tab.customTitle if (tab.customColor) info.customColor = tab.customColor - if (tab.anchorUrl) info.anchorUrl = tab.anchorUrl - if (tab.anchorTitle) info.anchorTitle = tab.anchorTitle + if (tab.boundUrl) info.boundUrl = tab.boundUrl + if (tab.boundTitle) info.boundTitle = tab.boundTitle data.push(info) } @@ -1251,44 +1251,44 @@ export function repinTabs(tabIds: ID[]): void { } /** - * Anchor tabs + * Bind tabs to their current URL */ -export function anchorTabs(tabIds: ID[]): void { +export function bindTabs(tabIds: ID[]): void { for (const tabId of tabIds) { const tab = Tabs.byId[tabId] if (!tab) continue - tab.anchorUrl = tab.url - tab.anchorTitle = tab.customTitle ?? tab.title - tab.reactive.anchored = true - tab.reactive.anchoredAway = false + tab.boundUrl = tab.url + tab.boundTitle = tab.customTitle ?? tab.title + tab.reactive.bound = true + tab.reactive.boundAway = false } Tabs.cacheTabsData() } -export function unanchorTabs(tabIds: ID[]): void { +export function unbindTabs(tabIds: ID[]): void { for (const tabId of tabIds) { const tab = Tabs.byId[tabId] if (!tab) continue - tab.anchorUrl = undefined - tab.anchorTitle = undefined - tab.reactive.anchored = false - tab.reactive.anchoredAway = false + tab.boundUrl = undefined + tab.boundTitle = undefined + tab.reactive.bound = false + tab.reactive.boundAway = false } Tabs.cacheTabsData() } -export function toggleAnchorTabs(tabIds: ID[]): void { +export function toggleBindTabs(tabIds: ID[]): void { const firstTab = Tabs.byId[tabIds[0]] if (!firstTab) return - if (firstTab.anchorUrl) unanchorTabs(tabIds) - else anchorTabs(tabIds) + if (firstTab.boundUrl) unbindTabs(tabIds) + else bindTabs(tabIds) } -export function returnToAnchor(tabId: ID): void { +export function returnToBound(tabId: ID): void { const tab = Tabs.byId[tabId] - if (!tab?.anchorUrl) return - browser.tabs.update(tabId, { url: tab.anchorUrl }).catch(err => { - Logs.err('Tabs.returnToAnchor: Cannot navigate to anchor URL:', err) + if (!tab?.boundUrl) return + browser.tabs.update(tabId, { url: tab.boundUrl }).catch(err => { + Logs.err('Tabs.returnToBound: Cannot navigate to bound URL:', err) }) } diff --git a/src/sidebar/components/tab.vue b/src/sidebar/components/tab.vue index d37b85db1..3d46528a0 100644 --- a/src/sidebar/components/tab.vue +++ b/src/sidebar/components/tab.vue @@ -20,8 +20,8 @@ :data-colorized="!!tabColor" :data-unread="tab.reactive.unread" :data-edit="tab.reactive.customTitleEdit" - :data-anchored="tab.reactive.anchored" - :data-anchored-away="tab.reactive.anchoredAway" + :data-bound="tab.reactive.bound" + :data-bound-away="tab.reactive.boundAway" :title="tab.reactive.tooltip" :draggable="!tab.reactive.customTitleEdit" @dragstart="onDragStart" @@ -49,7 +49,7 @@ .progress-spinner(v-if="Settings.state.animations") svg.progress-spinner(v-else): use(href="#icon_hourglass") .child-count(v-if="tab.reactive.folded && tab.reactive.branchLen") {{tab.reactive.branchLen}} - .bind-mark(v-if="tab.reactive.anchored") + .bind-mark(v-if="tab.reactive.bound") svg.bind-mark-icon: use(href="#icon_home") .audio( v-if="tab.reactive.mediaAudible || tab.reactive.mediaMuted || tab.reactive.mediaPaused" @@ -345,10 +345,10 @@ function onMouseDown(e: MouseEvent): void { Tabs.unpinTabs([tab.id]) return } else if (Settings.state.tabPinnedMiddleClick === 'toggle_bind_url') { - Tabs.toggleAnchorTabs(selectedTabs) + Tabs.toggleBindTabs(selectedTabs) return } else if (Settings.state.tabPinnedMiddleClick === 'return_to_bound') { - Tabs.returnToAnchor(tab.id) + Tabs.returnToBound(tab.id) return } } @@ -368,9 +368,9 @@ function onMouseDown(e: MouseEvent): void { } else if (Settings.state.tabMiddleClick === 'dup_child') { Tabs.duplicateTabs([tab.id], true) } else if (Settings.state.tabMiddleClick === 'toggle_bind_url') { - Tabs.toggleAnchorTabs(selectedTabs) + Tabs.toggleBindTabs(selectedTabs) } else if (Settings.state.tabMiddleClick === 'return_to_bound') { - Tabs.returnToAnchor(tab.id) + Tabs.returnToBound(tab.id) } } } @@ -406,9 +406,9 @@ function longClickFeedback(e: MouseEvent) { else if (action === 'new_after') Tabs.createTabAfter(tab.id) else if (action === 'new_child' && !tab.pinned) Tabs.createChildTab(tab.id) else if (action === 'edit_title' && !tab.pinned) Tabs.editTabTitle([tab.id]) - else if (action === 'toggle_bind_url') Tabs.toggleAnchorTabs([tab.id]) + else if (action === 'toggle_bind_url') Tabs.toggleBindTabs([tab.id]) else if (action === 'return_to_bound') { - if (tab.anchorUrl && tab.url !== tab.anchorUrl) Tabs.returnToAnchor(tab.id) + if (tab.boundUrl && tab.url !== tab.boundUrl) Tabs.returnToBound(tab.id) else noop = true } else noop = true @@ -529,8 +529,8 @@ function onDoubleClick(): void { if (shouldBeConvertedToGroup()) convertToGroup() else Tabs.removeTabs([tab.id]) } else if (dc === 'edit_title') Tabs.editTabTitle([tab.id]) - else if (dc === 'toggle_bind_url') Tabs.toggleAnchorTabs([tab.id]) - else if (dc === 'return_to_bound') Tabs.returnToAnchor(tab.id) + else if (dc === 'toggle_bind_url') Tabs.toggleBindTabs([tab.id]) + else if (dc === 'return_to_bound') Tabs.returnToBound(tab.id) } function onDragStart(e: DragEvent): void { diff --git a/src/styles/sidebar/tab.styl b/src/styles/sidebar/tab.styl index fea7c02d7..749bc04db 100644 --- a/src/styles/sidebar/tab.styl +++ b/src/styles/sidebar/tab.styl @@ -713,7 +713,7 @@ fill: var(--tabs-activated-fg) // Navigated away from the bound URL — highlight the marker with the accent -.Tab[data-anchored-away="true"] .bind-mark-icon +.Tab[data-bound-away="true"] .bind-mark-icon fill: var(--accent) // --- diff --git a/src/types/tabs.ts b/src/types/tabs.ts index 76eda3efb..88a2108be 100644 --- a/src/types/tabs.ts +++ b/src/types/tabs.ts @@ -35,8 +35,8 @@ export interface Tab extends NativeTab { reopenInContainer?: string customTitle?: string customColor?: string - anchorUrl?: string - anchorTitle?: string + boundUrl?: string + boundTitle?: string moving?: boolean removing?: boolean flashAnimationTimeout?: number @@ -79,8 +79,8 @@ export interface ReactiveTabProps { branchColor: string | null customColor: string | null isGroup: boolean - anchored: boolean - anchoredAway: boolean + bound: boolean + boundAway: boolean } export interface BgTab extends NativeTab { @@ -115,8 +115,8 @@ export interface TabCache { uniqWinId?: ID /* only for the first tab of window */ customTitle?: string customColor?: string - anchorUrl?: string - anchorTitle?: string + boundUrl?: string + boundTitle?: string index?: number isMissedGroup?: boolean @@ -129,8 +129,8 @@ export interface TabSessionData { folded: boolean customTitle?: string customColor?: string - anchorUrl?: string - anchorTitle?: string + boundUrl?: string + boundTitle?: string } export interface ActiveTabsHistory {