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(null)
const newTabPosEl = ref(null)
const pinTabsEl = ref(null)
+const anchorTabsEl = ref(null)
const tabsTreeEl = ref(null)
const tabsColorEl = ref(null)
const tabsPreviewEl = ref(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 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(null)
const favImgEl = ref(null)
const favSvgUseEl = ref(null)
const flashFxEl = ref(null)
+const favHovered = ref(false)
const tabColor = computed(() => {
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 " 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(null)
const favImgEl = ref(null)
const favSvgUseEl = ref(null)
const flashFxEl = ref(null)
-const favHovered = ref(false)
const tabColor = computed(() => {
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 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
" 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(null)
const newTabPosEl = ref(null)
const pinTabsEl = ref(null)
-const anchorTabsEl = ref(null)
const tabsTreeEl = ref(null)
const tabsColorEl = ref(null)
const tabsPreviewEl = ref(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 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 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 ", 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 @@
svg://src/assets/sync.svg#icon_sync
svg://src/assets/private-window.svg#icon_priv_win
svg://src/assets/undo.svg#icon_undo
+ svg://src/assets/home.svg#icon_home