Floating or sidebar quick navigation with per-site groups, icons, JS script execution, and editable items.
// ==UserScript==
// @name UTags Shortcuts
// @name:zh-CN UTags 快捷导航
// @namespace https://github.com/utags
// @homepageURL https://github.com/utags/userscripts#readme
// @supportURL https://github.com/utags/userscripts/issues
// @version 0.1.7
// @description Floating or sidebar quick navigation with per-site groups, icons, JS script execution, and editable items.
// @description:zh-CN 悬浮或侧边栏快速导航,支持按站点分组、图标、执行JS脚本与可编辑导航项。
// @icon data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20viewBox%3D%220%200%2064%2064%22%20fill%3D%22none%22%3E%3Crect%20x%3D%228%22%20y%3D%228%22%20width%3D%2248%22%20height%3D%2248%22%20rx%3D%2212%22%20stroke%3D%22%231f2937%22%20stroke-width%3D%224%22/%3E%3Cpath%20d%3D%22M22%2032h20M22%2042h16M22%2022h12%22%20stroke%3D%22%231f2937%22%20stroke-width%3D%226%22%20stroke-linecap%3D%22round%22/%3E%3C/svg%3E
// @author Pipecraft
// @license MIT
// @match *://*/*
// @connect cdn.jsdelivr.net
// @connect fastly.jsdelivr.net
// @connect unpkg.com
// @connect wsrv.nl
// @noframes
// @run-at document-start
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM.getValue
// @grant GM_getValue
// @grant GM.setValue
// @grant GM_setValue
// @grant GM.addValueChangeListener
// @grant GM_addValueChangeListener
// @grant GM.xmlHttpRequest
// @grant GM_xmlhttpRequest
// ==/UserScript==
//
;(() => {
'use strict'
var __defProp = Object.defineProperty
var __defProps = Object.defineProperties
var __getOwnPropDescs = Object.getOwnPropertyDescriptors
var __getOwnPropSymbols = Object.getOwnPropertySymbols
var __hasOwnProp = Object.prototype.hasOwnProperty
var __propIsEnum = Object.prototype.propertyIsEnumerable
var __defNormalProp = (obj, key, value) =>
key in obj
? __defProp(obj, key, {
enumerable: true,
configurable: true,
writable: true,
value,
})
: (obj[key] = value)
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop])
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop])
}
return a
}
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b))
function getFaviconUrl(href, size = 64) {
try {
const domain = new URL(href, location.origin).origin
const url =
'https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url='
.concat(domain, '&size=')
.concat(size)
const wrapUrl = 'https://wsrv.nl/?w='
.concat(size, '&h=')
.concat(size, '&url=')
.concat(encodeURIComponent(url), '&default=')
.concat(defaultFavicons[size])
return wrapUrl
} catch (error) {
console.error('Error generating favicon URL:', error)
return decodeURIComponent(defaultFavicons[size])
}
}
function getWrappedIconUrl(href, size = 64) {
try {
const url = new URL(href, location.origin).toString()
if (url.startsWith('https://wsrv.nl/')) {
return url
}
const wrapUrl = 'https://wsrv.nl/?w='
.concat(size, '&h=')
.concat(size, '&url=')
.concat(encodeURIComponent(url), '&default=')
.concat(defaultFavicons[size])
return wrapUrl
} catch (error) {
console.error('Error generating favicon URL:', error)
return decodeURIComponent(defaultFavicons[size])
}
}
var defaultFavicon16 = encodeURIComponent(
'https://wsrv.nl/?w=16&h=16&url=th.bing.com/th?id=ODLS.A2450BEC-5595-40BA-9F13-D9EC6AB74B9F'
)
var defaultFavicon32 = encodeURIComponent(
'https://wsrv.nl/?w=32&h=32&url=th.bing.com/th?id=ODLS.A2450BEC-5595-40BA-9F13-D9EC6AB74B9F'
)
var defaultFavicon64 = encodeURIComponent(
'https://wsrv.nl/?w=64&h=64&url=th.bing.com/th?id=ODLS.A2450BEC-5595-40BA-9F13-D9EC6AB74B9F'
)
var defaultFavicons = {
16: defaultFavicon16,
32: defaultFavicon32,
64: defaultFavicon64,
}
function registerMenu(caption, onClick) {
if (typeof GM_registerMenuCommand === 'function') {
return GM_registerMenuCommand(caption, onClick)
}
return 0
}
function unregisterMenu(menuId) {
if (typeof GM_unregisterMenuCommand === 'function') {
GM_unregisterMenuCommand(menuId)
}
}
async function getValue(key, defaultValue) {
if (typeof GM !== 'undefined' && typeof GM.getValue === 'function') {
return GM.getValue(key, defaultValue)
}
if (typeof GM_getValue === 'function') {
return GM_getValue(key, defaultValue)
}
return defaultValue
}
async function setValue(key, value) {
if (typeof GM !== 'undefined' && typeof GM.setValue === 'function') {
await GM.setValue(key, value)
return
}
if (typeof GM_setValue === 'function') {
GM_setValue(key, value)
}
}
async function addValueChangeListener(key, callback) {
if (
typeof GM !== 'undefined' &&
typeof GM.addValueChangeListener === 'function'
) {
return GM.addValueChangeListener(key, callback)
}
if (typeof GM_addValueChangeListener === 'function') {
return GM_addValueChangeListener(key, callback)
}
return 0
}
function xmlHttpRequest(options) {
try {
if (
typeof GM !== 'undefined' &&
typeof GM.xmlHttpRequest === 'function'
) {
GM.xmlHttpRequest(options)
return
}
} catch (e) {}
try {
if (typeof GM_xmlhttpRequest === 'function') {
GM_xmlhttpRequest(options)
}
} catch (e) {}
}
function clearChildren(el) {
try {
el.textContent = ''
} catch (e) {
try {
while (el.firstChild) el.firstChild.remove()
} catch (e2) {}
}
}
function querySelectorAllDeep(root, selector) {
const result = []
const visited = /* @__PURE__ */ new Set()
const visit = (node) => {
if (!node || visited.has(node)) return
visited.add(node)
const anyNode = node
try {
if (typeof anyNode.querySelectorAll === 'function') {
const found = Array.from(anyNode.querySelectorAll(selector))
for (const el of found) if (el instanceof Element) result.push(el)
}
} catch (e) {}
try {
const children = Array.from(anyNode.childNodes || [])
for (const child of children) visit(child)
} catch (e) {}
try {
const shadow = anyNode.shadowRoot
if (shadow) visit(shadow)
} catch (e) {}
}
visit(root)
return Array.from(new Set(result))
}
var iconCache = /* @__PURE__ */ new Map()
function renderIcon(s) {
const span = document.createElement('span')
span.className = 'icon'
let t = String(s || '').trim()
if (!t) t = 'lucide:link'
if (t.startsWith('lucide:')) {
const k = t.split(':')[1]
injectLucideIcon(span, k)
return span
}
if (t.startsWith('url:')) {
const url = t.slice(4)
injectImageAsData(span, getWrappedIconUrl(url))
return span
}
if (t.startsWith('svg:')) {
try {
const svg = t.slice(4)
const url =
'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg)
const img = document.createElement('img')
img.width = 16
img.height = 16
img.style.objectFit = 'contain'
img.src = url
clearChildren(span)
span.append(img)
} catch (e) {}
return span
}
span.textContent = t
return span
}
function setIcon(el, icon, title) {
try {
clearChildren(el)
el.append(renderIcon(icon))
if (title !== void 0) el.title = title
} catch (e) {}
}
var lastSuccessfulCdnIndex = 0
var cdnBases = [
'https://cdn.jsdelivr.net/npm',
'https://fastly.jsdelivr.net/npm',
'https://unpkg.com',
]
function injectLucideIcon(container, name) {
try {
const cached = iconCache.get(name)
if (cached) {
const img = document.createElement('img')
img.width = 16
img.height = 16
img.style.objectFit = 'contain'
img.className = 'lucide-icon'
img.src = cached
clearChildren(container)
container.append(img)
return
}
} catch (e) {}
const orderedCdnIndices = [
lastSuccessfulCdnIndex,
...[0, 1, 2].filter((i) => i !== lastSuccessfulCdnIndex),
]
const tryFetch = (attempt) => {
if (attempt >= orderedCdnIndices.length) {
return
}
const cdnIndex = orderedCdnIndices[attempt]
const cdnBase = cdnBases[cdnIndex]
const url = ''
.concat(cdnBase, '/lucide-static@latest/icons/')
.concat(name, '.svg')
try {
xmlHttpRequest({
method: 'GET',
url,
onload(res) {
try {
const svg = String(res.responseText || '')
if (res.status >= 200 && res.status < 300 && svg) {
const dataUrl =
'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg)
iconCache.set(name, dataUrl)
const img = document.createElement('img')
img.width = 16
img.height = 16
img.style.objectFit = 'contain'
img.className = 'lucide-icon'
img.src = dataUrl
clearChildren(container)
container.append(img)
lastSuccessfulCdnIndex = cdnIndex
} else {
tryFetch(attempt + 1)
}
} catch (e) {
tryFetch(attempt + 1)
}
},
onerror() {
tryFetch(attempt + 1)
},
})
} catch (e) {
tryFetch(attempt + 1)
}
}
tryFetch(0)
}
function injectImageAsData(container, url) {
try {
xmlHttpRequest({
method: 'GET',
url,
responseType: 'blob',
onload(res) {
try {
const blob = res.response
if (!blob) return
const reader = new FileReader()
reader.addEventListener('load', () => {
const img = document.createElement('img')
img.width = 16
img.height = 16
img.style.objectFit = 'contain'
img.src = String(reader.result || '')
clearChildren(container)
container.append(img)
})
reader.readAsDataURL(blob)
} catch (e) {}
},
})
} catch (e) {}
}
function uid() {
return Math.random().toString(36).slice(2, 10)
}
function resolveUrlTemplate(s) {
const re = /{([^}]+)}/g
return String(s || '').replaceAll(re, (_, body) => {
var _a
const parts = String(body || '')
.split('||')
.map((x) => x.trim())
.filter(Boolean)
const resolvers = {
hostname() {
var _a2
return (
((_a2 = globalThis.location) == null ? void 0 : _a2.hostname) || ''
)
},
hostname_without_www() {
var _a2
const h =
((_a2 = globalThis.location) == null ? void 0 : _a2.hostname) || ''
return h.startsWith('www.') ? h.slice(4) : h
},
query() {
var _a2
try {
const href =
((_a2 = globalThis.location) == null ? void 0 : _a2.href) || ''
const u = new URL(href)
return (
u.searchParams.get('query') ||
u.searchParams.get('q') ||
u.searchParams.get('kw') ||
u.searchParams.get('wd') ||
u.searchParams.get('keyword') ||
u.searchParams.get('p') ||
u.searchParams.get('s') ||
u.searchParams.get('term') ||
''
)
} catch (e) {}
return ''
},
kw() {
var _a2
try {
const href =
((_a2 = globalThis.location) == null ? void 0 : _a2.href) || ''
const u = new URL(href)
return u.searchParams.get('kw') || ''
} catch (e) {}
return ''
},
wd() {
var _a2
try {
const href =
((_a2 = globalThis.location) == null ? void 0 : _a2.href) || ''
const u = new URL(href)
return u.searchParams.get('wd') || ''
} catch (e) {}
return ''
},
keyword() {
var _a2
try {
const href =
((_a2 = globalThis.location) == null ? void 0 : _a2.href) || ''
const u = new URL(href)
return u.searchParams.get('keyword') || ''
} catch (e) {}
return ''
},
p() {
var _a2
try {
const href =
((_a2 = globalThis.location) == null ? void 0 : _a2.href) || ''
const u = new URL(href)
return u.searchParams.get('p') || ''
} catch (e) {}
return ''
},
s() {
var _a2
try {
const href =
((_a2 = globalThis.location) == null ? void 0 : _a2.href) || ''
const u = new URL(href)
return u.searchParams.get('s') || ''
} catch (e) {}
return ''
},
term() {
var _a2
try {
const href =
((_a2 = globalThis.location) == null ? void 0 : _a2.href) || ''
const u = new URL(href)
return u.searchParams.get('term') || ''
} catch (e) {}
return ''
},
selected() {
var _a2, _b
try {
return (
((_b =
((_a2 = globalThis.getSelection) == null
? void 0
: _a2.call(globalThis)) || void 0) == null
? void 0
: _b.toString()) || ''
)
} catch (e) {}
return ''
},
}
for (const p of parts) {
const v = String(
((_a = resolvers[p]) == null ? void 0 : _a.call(resolvers)) || ''
).trim()
if (v) return v
}
return ''
})
}
var style_default =
'/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */@layer properties;@layer theme, base, components, utilities;@layer theme{:host,:root{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-300:oklch(80.8% 0.114 19.571);--color-red-400:oklch(70.4% 0.191 22.216);--color-red-500:oklch(63.7% 0.237 25.331);--color-red-600:oklch(57.7% 0.245 27.325);--color-blue-300:oklch(80.9% 0.105 251.813);--color-blue-400:oklch(70.7% 0.165 254.624);--color-blue-500:oklch(62.3% 0.214 259.815);--color-blue-600:oklch(54.6% 0.245 262.881);--color-blue-700:oklch(48.8% 0.243 264.376);--color-gray-50:oklch(98.5% 0.002 247.839);--color-gray-100:oklch(96.7% 0.003 264.542);--color-gray-200:oklch(92.8% 0.006 264.531);--color-gray-300:oklch(87.2% 0.01 258.338);--color-gray-400:oklch(70.7% 0.022 261.325);--color-gray-500:oklch(55.1% 0.027 264.364);--color-gray-600:oklch(44.6% 0.03 256.802);--color-gray-700:oklch(37.3% 0.034 259.733);--color-gray-800:oklch(27.8% 0.033 256.848);--color-gray-900:oklch(21% 0.034 264.665);--color-black:#000;--color-white:#fff;--spacing:0.25rem;--text-xs:0.75rem;--text-xs--line-height:1.33333;--text-sm:0.875rem;--text-sm--line-height:1.42857;--text-lg:1.125rem;--text-lg--line-height:1.55556;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wider:0.05em;--leading-snug:1.375;--radius-md:0.375rem;--radius-lg:0.5rem;--radius-xl:0.75rem;--radius-2xl:1rem;--default-transition-duration:150ms;--default-transition-timing-function:cubic-bezier(0.4,0,0.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,::backdrop,::file-selector-button,:after,:before{border:0 solid;box-sizing:border-box;margin:0;padding:0}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-size:1em;font-variation-settings:var(--default-mono-font-variation-settings,normal)}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}menu,ol,ul{list-style:none}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}::file-selector-button,button,input,optgroup,select,textarea{background-color:transparent;border-radius:0;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:currentcolor;@supports (color:color-mix(in lab,red,red)){color:color-mix(in oklab,currentcolor 50%,transparent)}}::placeholder{color:currentcolor;@supports (color:color-mix(in lab,red,red)){color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}::file-selector-button,button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer utilities{.visible{visibility:visible}.fixed{position:fixed}.container{width:100%;@media (width >= 40rem){max-width:40rem}@media (width >= 48rem){max-width:48rem}@media (width >= 64rem){max-width:64rem}@media (width >= 80rem){max-width:80rem}@media (width >= 96rem){max-width:96rem}}.mt-4{margin-top:calc(var(--spacing)*4)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.w-full{width:100%}.flex-1{flex:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-2{gap:calc(var(--spacing)*2)}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-red-600{color:var(--color-red-600)}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,rgba(0,0,0,.1)),0 1px 2px -1px var(--tw-shadow-color,rgba(0,0,0,.1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}:host{all:initial}div{line-height:normal}.ushortcuts{color:var(--color-gray-900);font-family:var(--font-sans);font-size:13px;position:fixed;z-index:2147483647}.ushortcuts.dark{color:var(--color-gray-100)}.panel{background-color:var(--color-white);border-color:var(--color-gray-200);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;display:flex;flex-direction:column;gap:calc(var(--spacing)*3);max-height:100vh;max-width:360px;overflow-y:auto;padding:calc(var(--spacing)*3);--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,rgba(0,0,0,.1)),0 8px 10px -6px var(--tw-shadow-color,rgba(0,0,0,.1))}.panel,.ushortcuts.dark .panel{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ushortcuts.dark .panel{background-color:var(--color-gray-900);border-color:var(--color-gray-700);--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,rgba(0,0,0,.25))}.panel.sidebar-right{border-bottom-width:0;border-right-width:0;border-top-width:0;box-shadow:unset;width:360px}.panel.sidebar-left{border-bottom-width:0;border-left-width:0;border-top-width:0;box-shadow:unset;width:360px}@keyframes ushortcuts-slide-in-left{0%{opacity:0;transform:translateX(-12px)}to{opacity:1;transform:translateX(0)}}@keyframes ushortcuts-slide-in-right{0%{opacity:0;transform:translateX(12px)}to{opacity:1;transform:translateX(0)}}@keyframes ushortcuts-slide-in-top{0%{opacity:0;transform:translateY(0)}to{opacity:1;transform:translateY(0)}}@keyframes ushortcuts-slide-in-bottom{0%{opacity:0;transform:translateY(0)}to{opacity:1;transform:translateY(0)}}@keyframes ushortcuts-slide-out-left{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(-12px)}}@keyframes ushortcuts-slide-out-right{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(12px)}}@keyframes ushortcuts-slide-out-top{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(0)}}@keyframes ushortcuts-slide-out-bottom{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(0)}}.anim-in-left{animation:ushortcuts-slide-in-left .2s ease-out}.anim-in-right{animation:ushortcuts-slide-in-right .2s ease-out}.anim-in-top{animation:ushortcuts-slide-in-top .2s ease-out}.anim-in-bottom{animation:ushortcuts-slide-in-bottom .2s ease-out}.anim-out-left{animation:ushortcuts-slide-out-left .18s ease-in forwards}.anim-out-right{animation:ushortcuts-slide-out-right .18s ease-in forwards}.anim-out-top{animation:ushortcuts-slide-out-top .18s ease-in forwards}.anim-out-bottom{animation:ushortcuts-slide-out-bottom .18s ease-in forwards}.header{gap:calc(var(--spacing)*2);justify-content:space-between}.header,.header-actions{align-items:center;display:flex}.header-actions{gap:calc(var(--spacing)*1.5)}.header-actions .icon-btn{opacity:0;transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));--tw-duration:150ms;transition-duration:.15s}.header-actions .icon-btn.toggle,.section .header:hover .header-actions .icon-btn:not(.toggle){opacity:100%}.section .header{margin-bottom:calc(var(--spacing)*0)}.icon-btn{align-items:center;border-radius:var(--radius-md);color:var(--color-gray-600);display:flex;height:calc(var(--spacing)*6);justify-content:center;padding:calc(var(--spacing)*0);transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));width:calc(var(--spacing)*6);--tw-duration:150ms;transition-duration:.15s;&:hover{@media (hover:hover){background-color:var(--color-gray-100)}}&:hover{@media (hover:hover){color:var(--color-gray-900)}}}.modal.dark .icon-btn,.ushortcuts.dark .icon-btn{color:var(--color-gray-300);&:hover{@media (hover:hover){background-color:var(--color-gray-800)}}&:hover{@media (hover:hover){color:var(--color-white)}}}.modal.dark .icon img.lucide-icon,.ushortcuts.dark .icon img.lucide-icon{filter:invert(1) brightness(1.15) saturate(1.1)}.icon-btn.active{background-color:var(--color-gray-200);color:var(--color-gray-900);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);--tw-ring-color:var(--color-gray-300)}.icon-btn.active,.modal.dark .icon-btn.active,.ushortcuts.dark .icon-btn.active{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.modal.dark .icon-btn.active,.ushortcuts.dark .icon-btn.active{background-color:var(--color-gray-700);color:var(--color-white);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);--tw-ring-color:var(--color-gray-600)}.text-danger{color:var(--color-red-500)}.text-danger:hover{color:var(--color-red-600)}.modal.dark .text-danger,.ushortcuts.dark .text-danger{color:var(--color-red-400)}.modal.dark .text-danger:hover,.ushortcuts.dark .text-danger:hover{color:var(--color-red-300)}.title{align-items:center;display:flex;gap:calc(var(--spacing)*1.5);--tw-font-weight:var(--font-weight-semibold);color:var(--color-gray-800);font-weight:var(--font-weight-semibold)}.ushortcuts.dark .title{color:var(--color-gray-100)}.btn{align-items:center;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--color-white);border-color:var(--color-gray-300);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;cursor:pointer;display:inline-flex;gap:calc(var(--spacing)*1.5);justify-content:center;padding-block:calc(var(--spacing)*1.5);padding-inline:calc(var(--spacing)*2.5);--tw-font-weight:var(--font-weight-medium);color:var(--color-gray-800);font-weight:var(--font-weight-medium);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,rgba(0,0,0,.1)),0 1px 2px -1px var(--tw-shadow-color,rgba(0,0,0,.1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));--tw-duration:150ms;transition-duration:.15s;&:hover{@media (hover:hover){background-color:var(--color-gray-100)}}&:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-gray-300);--tw-outline-style:none;outline-style:none}&:active{scale:.99}}.ushortcuts.dark .btn{background-color:var(--color-gray-800);border-color:var(--color-gray-700);color:var(--color-gray-200);&:hover{@media (hover:hover){background-color:var(--color-gray-700)}}&:focus{--tw-ring-color:var(--color-gray-700)}}.btn-primary{background-color:var(--color-blue-600);border-color:var(--color-blue-600);color:var(--color-white);--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,rgba(0,0,0,.1)),0 2px 4px -2px var(--tw-shadow-color,rgba(0,0,0,.1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);&:hover{@media (hover:hover){border-color:var(--color-blue-700)}}&:hover{@media (hover:hover){background-color:var(--color-blue-700)}}}.modal.dark .btn-primary,.ushortcuts.dark .btn-primary{background-color:var(--color-blue-500);border-color:var(--color-blue-500);color:var(--color-white);&:hover{@media (hover:hover){border-color:var(--color-blue-600)}}&:hover{@media (hover:hover){background-color:var(--color-blue-600)}}}.btn-secondary{background-color:var(--color-gray-100);border-color:var(--color-gray-300);color:var(--color-gray-800);&:hover{@media (hover:hover){background-color:var(--color-gray-200)}}}.modal.dark .btn-secondary,.ushortcuts.dark .btn-secondary{background-color:var(--color-gray-800);border-color:var(--color-gray-700);color:var(--color-gray-200);&:hover{@media (hover:hover){background-color:var(--color-gray-700)}}}.items{display:grid;gap:calc(var(--spacing)*1);grid-template-columns:repeat(var(--cols,1),minmax(0,1fr))}.items input[type=checkbox]{flex:none;height:14px;width:14px}.item{align-items:center;border-radius:var(--radius-md);color:var(--color-gray-900);display:inline-flex;gap:calc(var(--spacing)*1.5);min-width:calc(var(--spacing)*0);overflow:hidden;padding-block:calc(var(--spacing)*1.5);padding-inline:calc(var(--spacing)*2);text-decoration-line:none;text-overflow:ellipsis;transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));white-space:nowrap;--tw-duration:150ms;transition-duration:.15s;width:100%}.item:hover{background-color:var(--color-gray-100)}.ushortcuts.dark .item:hover{background-color:var(--color-gray-800)}.ushortcuts.dark .item{background-color:var(--color-gray-800);border-color:var(--color-gray-700);color:var(--color-gray-100);&:hover{@media (hover:hover){background-color:var(--color-gray-700)}}}.icon{align-items:center;display:inline-flex;flex:none;height:calc(var(--spacing)*4);justify-content:center;overflow:hidden;width:calc(var(--spacing)*4);--tw-leading:1;line-height:1;white-space:nowrap}.collapsed-tab{background-color:var(--color-gray-700);border-radius:0;height:60px;opacity:40%;position:fixed;width:3px;z-index:2147483647}.ushortcuts.dark .collapsed-tab{background-color:var(--color-gray-400);opacity:40%}.collapsed-tab:hover{opacity:80%}.modal-mask{align-items:center;background-color:color-mix(in srgb,#000 40%,transparent);display:flex;inset:calc(var(--spacing)*0);justify-content:center;position:fixed;z-index:2147483647;@supports (color:color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.modal{color:var(--color-gray-900);font-family:var(--font-sans);font-size:13px}.modal h2:not(.section-title){font-size:16px;margin:calc(var(--spacing)*0);margin-bottom:calc(var(--spacing)*2.5)}.row{display:flex;flex-wrap:wrap;gap:calc(var(--spacing)*2);margin-block:calc(var(--spacing)*1.5)}.modal .row{align-items:center}.modal .actions{justify-content:flex-end}.modal .check{align-items:center;display:inline-flex;gap:calc(var(--spacing)*2);height:32px;width:unset!important}.modal .check input[type=checkbox]{height:14px;width:14px}.segmented{align-items:center;background-color:var(--color-gray-100);border-color:var(--color-gray-200);border-radius:calc(infinity*1px);border-style:var(--tw-border-style);border-width:1px;display:inline-flex;gap:calc(var(--spacing)*1);padding-block:calc(var(--spacing)*.5);padding-inline:calc(var(--spacing)*1);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,rgba(0,0,0,.1)),0 1px 2px -1px var(--tw-shadow-color,rgba(0,0,0,.1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.modal .segmented{margin-bottom:calc(var(--spacing)*3)}.ushortcuts.dark .segmented{background-color:var(--color-gray-800);border-color:var(--color-gray-700)}.seg-item{align-items:center;border-radius:calc(infinity*1px);cursor:pointer;display:inline-flex;-webkit-user-select:none;-moz-user-select:none;user-select:none}.seg-radio{border-width:0;clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.seg-text{border-radius:calc(infinity*1px);color:var(--color-gray-700);padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*2);text-align:center;width:100%}.ushortcuts.dark .seg-text{color:var(--color-gray-300)}.seg-item .seg-radio:checked+.seg-text{background-color:var(--color-white);color:var(--color-gray-900);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);--tw-ring-color:var(--color-gray-300)}.seg-item .seg-radio:checked+.seg-text,.ushortcuts.dark .seg-item .seg-radio:checked+.seg-text{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ushortcuts.dark .seg-item .seg-radio:checked+.seg-text{background-color:var(--color-gray-700);color:var(--color-gray-100);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);--tw-ring-color:var(--color-gray-600)}.seg-item .seg-radio:focus+.seg-text{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500)}.field-help{background-color:var(--color-gray-100);border-radius:var(--radius-md);display:block;flex-basis:100%;font-size:12px;margin-left:130px;padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*2);width:100%;--tw-leading:var(--leading-snug);color:var(--color-gray-700);line-height:var(--leading-snug)}.modal.dark .field-help,.ushortcuts.dark .field-help{background-color:var(--color-gray-800);color:var(--color-gray-300)}.field-help-title{align-items:center;display:flex;gap:calc(var(--spacing)*1);margin-bottom:calc(var(--spacing)*1);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.field-help a{color:var(--color-blue-600);text-decoration-line:underline}.modal.dark .field-help a,.ushortcuts.dark .field-help a{color:var(--color-blue-400);text-decoration-line:underline}input,select,textarea{border-color:var(--color-gray-300);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;flex:1;font-size:13px;padding-block:calc(var(--spacing)*1.5);padding-inline:calc(var(--spacing)*2)}textarea{min-height:80px}.grid{display:grid;gap:calc(var(--spacing)*2);grid-template-columns:repeat(2,minmax(0,1fr))}.group-list{display:flex;flex-wrap:wrap;gap:calc(var(--spacing)*1.5);margin-top:calc(var(--spacing)*1.5)}.group-pill{border-color:var(--color-gray-200);border-radius:calc(infinity*1px);border-style:var(--tw-border-style);border-width:1px;cursor:pointer;padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*2);transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));--tw-duration:150ms;transition-duration:.15s;&:hover{@media (hover:hover){background-color:var(--color-gray-100)}}}.group-pill.active{background-color:var(--color-gray-900);border-color:var(--color-gray-900);color:var(--color-white)}.modal.dark .group-pill,.ushortcuts.dark .group-pill{border-color:var(--color-gray-700);color:var(--color-gray-200);&:hover{@media (hover:hover){background-color:var(--color-gray-700)}}}.modal.dark .group-pill.active,.ushortcuts.dark .group-pill.active{background-color:var(--color-gray-100);border-color:var(--color-gray-100);color:var(--color-gray-900)}.mini{border-radius:var(--radius-md);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));padding-block:calc(var(--spacing)*.5);padding-inline:calc(var(--spacing)*1.5)}.btn:disabled{cursor:not-allowed;opacity:50%}.divider{background-color:var(--color-gray-200);height:1px}.modal.dark .divider,.ushortcuts.dark .divider{background-color:var(--color-gray-700)}.section-title{background-color:var(--color-gray-100);border-radius:var(--radius-md);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));margin-bottom:calc(var(--spacing)*1);margin-top:calc(var(--spacing)*3);padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*2);--tw-tracking:var(--tracking-wider);color:var(--color-gray-600);letter-spacing:var(--tracking-wider);text-transform:uppercase}.modal.dark .section-title,.ushortcuts.dark .section-title{background-color:var(--color-gray-800);color:var(--color-gray-300)}.row label.mini{align-items:center;display:inline-flex;gap:calc(var(--spacing)*2)}.modal{background-color:var(--color-white);border-radius:var(--radius-2xl);max-width:92vw;padding:calc(var(--spacing)*3);width:720px;--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,rgba(0,0,0,.25));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.modal.dark,.ushortcuts.dark .modal{background-color:var(--color-gray-900);color:var(--color-gray-100)}.modal.dark input,.modal.dark select,.modal.dark textarea,.ushortcuts.dark .modal input,.ushortcuts.dark .modal select,.ushortcuts.dark .modal textarea{background-color:var(--color-gray-800);border-color:var(--color-gray-700);color:var(--color-gray-100)}.ushortcuts.dark .modal input::-moz-placeholder,.ushortcuts.dark .modal textarea::-moz-placeholder{color:#9ca3af}.ushortcuts.dark .modal input::placeholder,.ushortcuts.dark .modal textarea::placeholder{color:#9ca3af}.modal.dark input::-moz-placeholder,.modal.dark textarea::-moz-placeholder{color:#9ca3af}.modal.dark input::placeholder,.modal.dark textarea::placeholder{color:#9ca3af}.modal.dark .row label{color:var(--color-gray-400)}.modal.dark .segmented{background-color:var(--color-gray-800);border-color:var(--color-gray-700)}.modal.dark .seg-item .seg-radio:checked+.seg-text{background-color:var(--color-gray-700);color:var(--color-gray-100);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-gray-600)}.modal.dark .seg-text{color:var(--color-gray-300)}.editor{border-radius:var(--radius-2xl);max-height:72vh;overflow-y:auto;padding:calc(var(--spacing)*4)}.editor .grid,.editor .row{gap:calc(var(--spacing)*2)}.editor .row{align-items:center}.editor .row label{color:var(--color-gray-500);width:120px}.ushortcuts.dark .editor .row label{color:var(--color-gray-400)}.editor input,.editor select,.editor textarea{background-color:var(--color-white);border-color:var(--color-gray-300);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;padding-block:calc(var(--spacing)*1.5);padding-inline:calc(var(--spacing)*2);&:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-gray-300);--tw-outline-style:none;outline-style:none}}.ushortcuts.dark .editor input,.ushortcuts.dark .editor select,.ushortcuts.dark .editor textarea{background-color:var(--color-gray-800);border-color:var(--color-gray-700);&:focus{--tw-ring-color:var(--color-gray-700)}}input:disabled,select:disabled,textarea:disabled{background-color:var(--color-gray-100);cursor:not-allowed;opacity:60%}.dark input:disabled,.dark select:disabled,.dark textarea:disabled{background-color:var(--color-gray-700);cursor:not-allowed;opacity:60%}.editor .item-row{align-items:center;background-color:var(--color-gray-50);border-radius:var(--radius-md);display:grid;gap:8px;grid-template-columns:1.2fr 1.1fr .9fr 2fr 1fr .9fr 1.3fr auto auto;padding-block:calc(var(--spacing)*1.5);padding-inline:calc(var(--spacing)*2)}.editor .item-row:hover{background-color:var(--color-gray-100)}.modal.dark .item-row,.ushortcuts.dark .editor .item-row{background-color:var(--color-gray-800)}.modal.dark .item-row:hover,.ushortcuts.dark .editor .item-row:hover{background-color:var(--color-gray-700)}.editor .btn{border-radius:var(--radius-md);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*2)}.row label{color:var(--color-gray-500);width:120px}.ushortcuts.dark .row label{color:var(--color-gray-400)}.panel-actions,.panel-actions-left{align-items:center;display:flex;gap:calc(var(--spacing)*1.5)}.theme-switch{align-items:center;background-color:var(--color-gray-100);border-color:var(--color-gray-200);border-radius:calc(infinity*1px);border-style:var(--tw-border-style);border-width:1px;display:inline-flex;gap:calc(var(--spacing)*1);padding-block:2px;padding-inline:calc(var(--spacing)*1);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,rgba(0,0,0,.1)),0 1px 2px -1px var(--tw-shadow-color,rgba(0,0,0,.1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.modal.dark .theme-switch,.ushortcuts.dark .theme-switch{background-color:var(--color-gray-800)}.theme-btn{align-items:center;border-radius:calc(infinity*1px);color:var(--color-gray-600);display:flex;height:calc(var(--spacing)*6);justify-content:center;padding:calc(var(--spacing)*0);transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));width:calc(var(--spacing)*6);--tw-duration:150ms;transition-duration:.15s;&:hover{@media (hover:hover){background-color:var(--color-gray-200)}}&:hover{@media (hover:hover){color:var(--color-gray-900)}}}.modal.dark .theme-btn,.ushortcuts.dark .theme-btn{color:var(--color-gray-300);&:hover{@media (hover:hover){background-color:var(--color-gray-700)}}&:hover{@media (hover:hover){color:var(--color-white)}}}.theme-btn.active{background-color:var(--color-white);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);--tw-ring-color:var(--color-gray-300)}.modal.dark .theme-btn.active,.theme-btn.active,.ushortcuts.dark .theme-btn.active{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.modal.dark .theme-btn.active,.ushortcuts.dark .theme-btn.active{background-color:var(--color-gray-700);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);--tw-ring-color:var(--color-gray-600)}.collapse-btn{align-items:center;border-radius:var(--radius-md);color:var(--color-gray-600);display:flex;height:calc(var(--spacing)*6);justify-content:center;padding:calc(var(--spacing)*0);transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));width:calc(var(--spacing)*6);--tw-duration:150ms;transition-duration:.15s;&:hover{@media (hover:hover){background-color:var(--color-gray-200)}}&:hover{@media (hover:hover){color:var(--color-gray-900)}}}.ushortcuts.dark .collapse-btn{color:var(--color-gray-300);&:hover{@media (hover:hover){background-color:var(--color-gray-700)}}&:hover{@media (hover:hover){color:var(--color-white)}}}.item+.icon-btn{justify-self:flex-end}.items{align-items:center;margin-top:calc(var(--spacing)*1.5)}.item-wrap{align-items:center;display:flex;gap:8px;justify-content:space-between}.item-wrap .item{flex:1}.item-wrap .icon-btn{opacity:0;transition:opacity .15s ease-in-out}.item-wrap:hover .icon-btn{opacity:1}.item-wrap:focus-within .icon-btn{opacity:1}.quick-add-menu{background-color:var(--color-white);border-color:var(--color-gray-200);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;font-family:var(--font-sans);font-size:13px;min-width:160px;padding:calc(var(--spacing)*1.5);position:fixed;z-index:2147483647;--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,rgba(0,0,0,.1)),0 8px 10px -6px var(--tw-shadow-color,rgba(0,0,0,.1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ushortcuts.dark .quick-add-menu,.ushortcuts.dark~.quick-add-menu{background-color:var(--color-gray-900);border-color:var(--color-gray-700);color:var(--color-gray-100);--tw-shadow-color:color-mix(in srgb,#000 40%,transparent);@supports (color:color-mix(in lab,red,red)){--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-black) 40%,transparent) var(--tw-shadow-alpha),transparent)}}.quick-add-item{align-items:center;border-radius:var(--radius-md);color:var(--color-gray-900);display:flex;gap:calc(var(--spacing)*1.5);padding-block:calc(var(--spacing)*1.5);padding-inline:calc(var(--spacing)*2);text-align:left;transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));width:100%;--tw-duration:150ms;transition-duration:.15s;&:hover{@media (hover:hover){background-color:var(--color-gray-100)}}}.ushortcuts.dark .quick-add-menu .quick-add-item,.ushortcuts.dark~.quick-add-menu .quick-add-item{color:var(--color-gray-100);&:hover{@media (hover:hover){background-color:var(--color-gray-800)}}}.ushortcuts.dark .quick-add-menu .icon img.lucide-icon,.ushortcuts.dark~.quick-add-menu .icon img.lucide-icon{filter:invert(1) brightness(1.15) saturate(1.1)}.picker-highlight{cursor:pointer!important;outline:2px dashed #ef4444!important;outline-offset:2px!important}.picker-tip{background:#fff;border:1px solid #e5e7eb;border-radius:8px;box-shadow:0 10px 20px rgba(0,0,0,.1);color:#111827;font:13px/1.4 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,Apple Color Emoji,Segoe UI Emoji;padding:6px 10px;position:fixed;right:12px;top:12px;z-index:2147483647}.ushortcuts.dark .picker-tip,.ushortcuts.dark~.picker-tip{background:#111827;border-color:#374151;color:#f9fafb}.panel.all-mode{height:100vh;max-width:100vw;overflow:hidden;width:100vw}.panel-scroll{height:calc(100% - 36px);overflow-x:auto;width:100%}.panel.all-mode .header{background-color:#fff;position:sticky;top:0;z-index:2147483647}.ushortcuts.dark .panel.all-mode .header{background-color:#111827}.panel-columns{-moz-column-gap:12px;column-gap:12px;-moz-column-width:360px;column-width:360px;height:100%}.divider,.section{-moz-column-break-inside:avoid;break-inside:avoid}.check{align-items:center;display:inline-flex;gap:calc(var(--spacing)*2);height:32px}.check input[type=checkbox]{height:14px;width:14px}.item-wrap,.section{transition:opacity .15s ease}@keyframes ushortcuts-fade-in{0%{opacity:.01}to{opacity:1}}.item-wrap.fade-in,.section.fade-in{animation:ushortcuts-fade-in .15s ease both}.section.is-hidden .header{opacity:60%}.section.is-hidden{background-color:var(--color-gray-50);border-radius:var(--radius-lg);outline-color:var(--color-gray-300);outline-style:var(--tw-outline-style);outline-width:1px;--tw-outline-style:dashed;outline-style:dashed}.ushortcuts.dark .section.is-hidden{background-color:var(--color-gray-800);outline-color:var(--color-gray-600)}.item-wrap.is-hidden .item{opacity:60%}.item-wrap.is-hidden{border-radius:var(--radius-md);outline-color:var(--color-gray-300);outline-style:var(--tw-outline-style);outline-width:1px;--tw-outline-style:dashed;outline-style:dashed}.ushortcuts.dark .item-wrap.is-hidden{outline-color:var(--color-gray-600)}.empty-msg{color:var(--color-gray-500);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*2)}.ushortcuts.dark .empty-msg{color:var(--color-gray-400)}.segmented label.seg-item{min-width:50px;width:unset}.panel-split{border-color:var(--color-gray-200);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;display:flex;height:500px;max-height:70vh;overflow:hidden}.modal.dark .panel-split,.ushortcuts.dark .panel-split{border-color:var(--color-gray-700)}.panel-sidebar{background-color:var(--color-gray-50);border-color:var(--color-gray-200);border-right-style:var(--tw-border-style);border-right-width:1px;display:flex;flex:none;flex-direction:column;overflow-y:auto;width:160px}.modal.dark .panel-sidebar,.ushortcuts.dark .panel-sidebar{background-color:color-mix(in srgb,oklch(27.8% .033 256.848) 50%,transparent);border-color:var(--color-gray-700);@supports (color:color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-gray-800) 50%,transparent)}}.sidebar-item{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-100);cursor:pointer;padding-block:calc(var(--spacing)*2);padding-inline:calc(var(--spacing)*3);text-align:left;transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));&:hover{@media (hover:hover){background-color:var(--color-gray-100)}}}.modal.dark .sidebar-item,.ushortcuts.dark .sidebar-item{border-color:color-mix(in srgb,oklch(37.3% .034 259.733) 50%,transparent);@supports (color:color-mix(in lab,red,red)){border-color:color-mix(in oklab,var(--color-gray-700) 50%,transparent)}&:hover{@media (hover:hover){background-color:var(--color-gray-700)}}}.sidebar-item.active{background-color:var(--color-white);border-left:4px var(--tw-border-style) var(--color-blue-500);border-right-color:transparent;--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,rgba(0,0,0,.1)),0 1px 2px -1px var(--tw-shadow-color,rgba(0,0,0,.1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.modal.dark .sidebar-item.active,.ushortcuts.dark .sidebar-item.active{background-color:var(--color-gray-800);border-left-color:var(--color-blue-400)}.sidebar-item-name{display:block;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));overflow:hidden;text-overflow:ellipsis;white-space:nowrap;--tw-font-weight:var(--font-weight-medium);color:var(--color-gray-900);font-weight:var(--font-weight-medium)}.modal.dark .sidebar-item-name,.ushortcuts.dark .sidebar-item-name{color:var(--color-gray-100)}.sidebar-item-desc{display:block;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));margin-top:calc(var(--spacing)*.5);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;--tw-font-weight:var(--font-weight-normal);color:var(--color-gray-400);font-weight:var(--font-weight-normal)}.modal.dark .sidebar-item-desc,.ushortcuts.dark .sidebar-item-desc{color:var(--color-gray-500)}.sidebar-actions{background-color:var(--color-gray-50);border-color:var(--color-gray-200);border-top-style:var(--tw-border-style);border-top-width:1px;bottom:calc(var(--spacing)*0);display:flex;flex-direction:column;gap:calc(var(--spacing)*2);margin-top:auto;padding:calc(var(--spacing)*2);position:sticky}.modal.dark .sidebar-actions,.ushortcuts.dark .sidebar-actions{background-color:var(--color-gray-800);border-color:var(--color-gray-700)}.panel-content{background-color:var(--color-white);display:flex;flex:1;flex-direction:column;min-width:calc(var(--spacing)*0)}.modal.dark .panel-content,.ushortcuts.dark .panel-content{background-color:var(--color-gray-900)}.content-header{align-items:center;background-color:var(--color-white);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);display:flex;justify-content:space-between;padding-block:calc(var(--spacing)*3);padding-inline:calc(var(--spacing)*4);position:sticky;top:calc(var(--spacing)*0);z-index:10}.modal.dark .content-header,.ushortcuts.dark .content-header{background-color:var(--color-gray-900);border-color:var(--color-gray-700)}.content-title{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-bold);color:var(--color-gray-900);font-weight:var(--font-weight-bold)}.modal.dark .content-title,.ushortcuts.dark .content-title{color:var(--color-gray-100)}.content-tabs{background-color:color-mix(in srgb,oklch(98.5% .002 247.839) 50%,transparent);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);display:flex;gap:calc(var(--spacing)*4);@supports (color:color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-gray-50) 50%,transparent)}padding-inline:calc(var(--spacing)*4)}.modal.dark .content-tabs,.ushortcuts.dark .content-tabs{background-color:color-mix(in srgb,oklch(27.8% .033 256.848) 30%,transparent);border-color:var(--color-gray-700);@supports (color:color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-gray-800) 30%,transparent)}}.tab-btn{border-bottom:2px var(--tw-border-style);border-color:transparent;cursor:pointer;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));padding-block:calc(var(--spacing)*2);--tw-font-weight:var(--font-weight-medium);color:var(--color-gray-500);font-weight:var(--font-weight-medium);&:hover{@media (hover:hover){color:var(--color-gray-700)}}}.modal.dark .tab-btn,.ushortcuts.dark .tab-btn{color:var(--color-gray-400);&:hover{@media (hover:hover){color:var(--color-gray-200)}}}.tab-btn.active{border-color:var(--color-blue-500);color:var(--color-blue-600)}.modal.dark .tab-btn.active,.ushortcuts.dark .tab-btn.active{border-color:var(--color-blue-400);color:var(--color-blue-400)}.tab-pane{flex:1;overflow-y:auto;padding:calc(var(--spacing)*4)}.shortcut-list{display:flex;flex-direction:column;gap:calc(var(--spacing)*2)}.shortcut-item{align-items:center;background-color:var(--color-white);border-color:var(--color-gray-200);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;display:flex;gap:calc(var(--spacing)*3);padding:calc(var(--spacing)*2.5);transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));&:hover{@media (hover:hover){border-color:var(--color-blue-300)}}&:hover{@media (hover:hover){--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,rgba(0,0,0,.1)),0 1px 2px -1px var(--tw-shadow-color,rgba(0,0,0,.1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}}.modal.dark .shortcut-item,.ushortcuts.dark .shortcut-item{background-color:var(--color-gray-800);border-color:var(--color-gray-700);&:hover{@media (hover:hover){border-color:var(--color-gray-600)}}}.shortcut-item.is-hidden{--tw-border-style:dashed;background-color:var(--color-gray-50);border-style:dashed;opacity:60%}.modal.dark .shortcut-item.is-hidden,.ushortcuts.dark .shortcut-item.is-hidden{background-color:color-mix(in srgb,oklch(27.8% .033 256.848) 50%,transparent);@supports (color:color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-gray-800) 50%,transparent)}}.shortcut-icon{align-items:center;background-color:var(--color-gray-50);border-radius:var(--radius-md);color:var(--color-gray-500);display:flex;flex:none;height:calc(var(--spacing)*8);justify-content:center;overflow:hidden;width:calc(var(--spacing)*8)}.modal.dark .shortcut-icon,.ushortcuts.dark .shortcut-icon{background-color:var(--color-gray-700);color:var(--color-gray-400)}.shortcut-info{flex:1;min-width:calc(var(--spacing)*0)}.shortcut-name{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));overflow:hidden;text-overflow:ellipsis;white-space:nowrap;--tw-font-weight:var(--font-weight-medium);color:var(--color-gray-900);font-weight:var(--font-weight-medium)}.modal.dark .shortcut-name,.ushortcuts.dark .shortcut-name{color:var(--color-gray-100)}.shortcut-meta{align-items:center;color:var(--color-gray-400);display:flex;font-size:var(--text-xs);gap:calc(var(--spacing)*2);line-height:var(--tw-leading,var(--text-xs--line-height));margin-top:calc(var(--spacing)*.5)}.modal.dark .shortcut-meta,.ushortcuts.dark .shortcut-meta{color:var(--color-gray-500)}.shortcut-actions{align-items:center;display:flex;gap:calc(var(--spacing)*1);opacity:0;transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.group:hover .shortcut-actions{opacity:1}.shortcut-actions .icon-btn{height:calc(var(--spacing)*7);width:calc(var(--spacing)*7)}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-duration{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-leading{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@layer properties{*,::backdrop,:after,:before{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-border-style:solid;--tw-duration:initial;--tw-font-weight:initial;--tw-leading:initial;--tw-tracking:initial;--tw-outline-style:solid}}'
function ensurePickerStylesIn(r) {
var _a
const has =
(_a = r.querySelector) == null
? void 0
: _a.call(r, '#ushortcuts-picker-styles')
if (has) return
const st = document.createElement('style')
st.id = 'ushortcuts-picker-styles'
st.textContent =
'.ushortcuts-picker-highlight{outline:2px dashed #ef4444!important;outline-offset:0!important;box-shadow:0 0 0 2px rgba(239,68,68,.35) inset!important;cursor:pointer!important;}.ushortcuts-picker-tip{position:fixed;top:12px;right:12px;z-index:2147483647;background:#fff;color:#111827;border:1px solid #e5e7eb;border-radius:8px;padding:6px 10px;box-shadow:0 10px 20px rgba(0,0,0,0.1);font:13px/1.4 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji";}'
if (r instanceof Document) {
r.head.append(st)
} else {
r.append(st)
}
}
function addCurrentPageLinkToGroup(root, cfg, helpers, groupId, openMode) {
const grp = (cfg.groups || []).find((g) => g.id === groupId)
if (!grp) return
let nm = '\u5F53\u524D\u7F51\u9875'
let href = location.href
try {
nm = document.title || nm
} catch (e) {}
try {
href = location.href
} catch (e) {}
if (hasDuplicateInGroup(grp, 'url', String(href || '/'))) {
const ok = globalThis.confirm(
'\u8BE5\u5206\u7EC4\u5185\u5DF2\u5B58\u5728\u76F8\u540C\u7684 URL\uFF0C\u662F\u5426\u7EE7\u7EED\u6DFB\u52A0\uFF1F'
)
if (!ok) return
}
const it = {
id: uid(),
name: String(nm || href),
icon: void 0,
type: 'url',
data: String(href || '/'),
openIn: openMode,
}
grp.items.push(it)
try {
helpers.saveConfig(cfg)
} catch (e) {}
try {
helpers.rerender(root, cfg)
} catch (e) {}
}
function pickLinkFromPageAndAdd(root, cfg, helpers, groupId, openMode, opts) {
const grp = (cfg.groups || []).find((g) => g.id === groupId)
if (!grp) return
pickLinkFromPage(root, {
beforeStart: opts == null ? void 0 : opts.beforeStart,
afterFinish: opts == null ? void 0 : opts.afterFinish,
onPicked(nm, href) {
if (hasDuplicateInGroup(grp, 'url', String(href || '/'))) {
const ok = globalThis.confirm(
'\u8BE5\u5206\u7EC4\u5185\u5DF2\u5B58\u5728\u76F8\u540C\u7684 URL\uFF0C\u662F\u5426\u7EE7\u7EED\u6DFB\u52A0\uFF1F'
)
if (!ok) return
}
const it = {
id: uid(),
name: nm,
icon: void 0,
type: 'url',
data: href,
openIn: openMode,
}
grp.items.push(it)
try {
helpers.saveConfig(cfg)
} catch (e) {}
try {
helpers.rerender(root, cfg)
} catch (e) {}
},
})
}
function pickLinkFromPage(root, opts) {
ensurePickerStylesIn(document)
if (opts.beforeStart) {
try {
opts.beforeStart()
} catch (e) {}
}
const tip = document.createElement('div')
tip.className = 'ushortcuts-picker-tip'
tip.textContent =
'\u70B9\u51FB\u7EA2\u6846\u94FE\u63A5\u6DFB\u52A0\uFF0CESC \u53D6\u6D88'
document.body.append(tip)
const anchors = querySelectorAllDeep(document, 'a[href]').filter((el) => {
const href = (el.getAttribute('href') || '').trim()
if (!href || href === '#') return false
let u
try {
u = new URL(href, location.href)
} catch (e) {
return false
}
return u.protocol === 'http:' || u.protocol === 'https:'
})
const panelEl = root.querySelector('.ushortcuts')
const prevPanelDisplay =
panelEl instanceof HTMLElement ? panelEl.style.display || '' : ''
if (panelEl instanceof HTMLElement) panelEl.style.display = 'none'
const cleanup = () => {
for (const a of anchors) a.classList.remove('ushortcuts-picker-highlight')
try {
tip.remove()
} catch (e) {}
if (panelEl instanceof HTMLElement)
panelEl.style.display = prevPanelDisplay
try {
const ov = document.querySelector('#ushortcuts-picker-overlay')
ov == null ? void 0 : ov.remove()
} catch (e) {}
if (opts.afterFinish) {
try {
opts.afterFinish()
} catch (e) {}
}
}
const onEsc = (ev) => {
if (ev.key === 'Escape') {
document.removeEventListener('keydown', onEsc, true)
cleanup()
}
}
document.addEventListener('keydown', onEsc, true)
for (const a of anchors) {
const rn = a.getRootNode()
if (rn instanceof Document || rn instanceof ShadowRoot)
ensurePickerStylesIn(rn)
a.classList.add('ushortcuts-picker-highlight')
}
const overlay = document.createElement('div')
overlay.id = 'ushortcuts-picker-overlay'
overlay.style.position = 'fixed'
overlay.style.inset = '0'
overlay.style.zIndex = '2147483647'
overlay.style.background = 'transparent'
overlay.style.cursor = 'crosshair'
const onOverlayClick = (ev) => {
var _a
ev.preventDefault()
ev.stopPropagation()
;(_a = ev.stopImmediatePropagation) == null ? void 0 : _a.call(ev)
let picked
try {
const x = ev.clientX
const y = ev.clientY
const seen = /* @__PURE__ */ new Set()
const search = (r) => {
var _a2
const arr = r.elementsFromPoint(x, y)
for (const el of arr) {
if (el === overlay) continue
if (seen.has(el)) continue
seen.add(el)
const a =
(_a2 = el.closest) == null ? void 0 : _a2.call(el, 'a[href]')
if (a instanceof HTMLAnchorElement) return a
const sr = el.shadowRoot
if (sr) {
const inner = search(sr)
if (inner) return inner
}
}
return void 0
}
picked = search(document)
if (picked) {
const href = picked.href
const text = (picked.textContent || '').trim() || href
try {
opts.onPicked(text, href)
} catch (e) {}
}
} catch (e) {}
if (picked) {
document.removeEventListener('keydown', onEsc, true)
cleanup()
}
}
overlay.addEventListener('click', onOverlayClick, true)
document.body.append(overlay)
}
function hasDuplicateInGroup(grp, type, data, excludeId) {
const d = String(data || '').trim()
return (grp.items || []).some((x) => {
if (!x || x.type !== type) return false
const xd = String(x.data || '').trim()
if (excludeId && x.id === excludeId) return false
return xd === d
})
}
function createModalFrame(options) {
const { root, title, onClose } = options
const previousFocus = root.activeElement || document.activeElement
for (const n of Array.from(root.querySelectorAll('.modal-mask'))) n.remove()
const mask = document.createElement('div')
mask.className = 'modal-mask'
try {
mask.style.zIndex = '2147483647'
} catch (e) {}
mask.addEventListener('keydown', (e) => {
e.stopPropagation()
})
const modal = document.createElement('div')
modal.className = 'modal'
modal.style.overscrollBehavior = 'contain'
modal.tabIndex = -1
try {
const panel = root.querySelector('.ushortcuts')
const isDarkPanel =
panel == null ? void 0 : panel.classList.contains('dark')
const prefersDark = (() => {
var _a, _b
try {
return (_b =
(_a = globalThis.matchMedia) == null
? void 0
: _a.call(globalThis, '(prefers-color-scheme: dark)')) == null
? void 0
: _b.matches
} catch (e) {
return false
}
})()
if (isDarkPanel || prefersDark) modal.classList.add('dark')
} catch (e) {}
const h2 = document.createElement('h2')
h2.textContent = title
modal.append(h2)
const body = document.createElement('div')
modal.append(body)
const actions = document.createElement('div')
actions.className = 'row actions'
modal.append(actions)
mask.append(modal)
root.append(mask)
const preventBackgroundScroll = (e) => {
const path = e.composedPath()
if (!path.includes(modal)) {
e.preventDefault()
}
}
document.addEventListener('wheel', preventBackgroundScroll, {
passive: false,
})
document.addEventListener('touchmove', preventBackgroundScroll, {
passive: false,
})
const close = () => {
try {
mask.remove()
} catch (e) {}
try {
document.removeEventListener('keydown', onKey, true)
document.removeEventListener('wheel', preventBackgroundScroll)
document.removeEventListener('touchmove', preventBackgroundScroll)
} catch (e) {}
if (onClose) onClose()
try {
if (previousFocus && 'focus' in previousFocus) {
previousFocus.focus()
}
} catch (e) {}
}
const onKey = (e) => {
const visible = root.contains(mask) && modal.style.display !== 'none'
if (!visible) return
if (e.key === 'Escape') {
e.preventDefault()
close()
return
}
if (!e.composedPath().includes(root)) {
e.preventDefault()
e.stopPropagation()
return
}
if (e.key === 'Tab') {
const focusables = Array.from(
modal.querySelectorAll(
'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'
)
).filter((el) => !el.hasAttribute('disabled'))
if (focusables.length === 0) return
const first = focusables[0]
const last = focusables[focusables.length - 1]
const current = root.activeElement
if (e.shiftKey) {
if (current === first || !modal.contains(current)) {
e.preventDefault()
last.focus()
}
} else if (current === last || !modal.contains(current)) {
e.preventDefault()
first.focus()
}
}
}
document.addEventListener('keydown', onKey, true)
requestAnimationFrame(() => {
const focusables = modal.querySelectorAll(
'input, button, [tabindex]:not([tabindex="-1"])'
)
if (focusables.length > 0) {
focusables[0].focus()
}
})
return {
mask,
modal,
body,
actions,
close,
}
}
function createSegmentedRadios(initial, values, onChange, opts) {
var _a, _b
const wrap = document.createElement('div')
wrap.className = 'segmented'
const name =
((opts == null ? void 0 : opts.namePrefix) || 'ushortcuts-seg-') + uid()
const labels = (_a = opts == null ? void 0 : opts.labels) != null ? _a : {}
for (const m of values) {
const label = document.createElement('label')
label.className = 'seg-item'
const input = document.createElement('input')
input.type = 'radio'
input.name = name
input.value = m
input.className = 'seg-radio'
input.checked = initial === m
input.addEventListener('change', () => {
if (input.checked) onChange(m)
})
const text = document.createElement('span')
text.className = 'seg-text'
text.textContent = (_b = labels[m]) != null ? _b : String(m)
label.append(input)
label.append(text)
wrap.append(label)
}
return wrap
}
function createOpenModeRadios(initial, onChange, opts) {
var _a
const labels =
(_a = opts == null ? void 0 : opts.labels) != null
? _a
: {
'same-tab': '\u5F53\u524D\u9875',
'new-tab': '\u65B0\u6807\u7B7E\u9875',
}
return createSegmentedRadios(initial, ['same-tab', 'new-tab'], onChange, {
labels,
namePrefix: 'ushortcuts-open-',
})
}
function debounce(fn, delay) {
let timer
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
function detectIconKind(v, kinds) {
const s = String(v || '').trim()
if (kinds.includes('favicon') && s.startsWith('favicon')) return 'favicon'
if (s.startsWith('url:')) return 'url'
if (s.includes(':')) return 'icon'
if (s) return 'emoji'
return 'icon'
}
function createIconInput(initialValue, kinds, opts) {
var _a
const wrap = document.createElement('div')
wrap.style.flex = '1'
const inputContainer = document.createElement('div')
inputContainer.style.display = 'flex'
inputContainer.style.alignItems = 'center'
inputContainer.style.gap = '0.5em'
const preview = document.createElement('span')
preview.style.display = 'inline-flex'
preview.style.alignItems = 'center'
preview.style.justifyContent = 'center'
preview.style.width = '1.5em'
preview.style.height = '1.em'
const input = document.createElement('input')
try {
input.style.width = '100%'
} catch (e) {}
inputContainer.append(preview)
inputContainer.append(input)
const help = document.createElement('div')
help.className = 'field-help'
try {
help.style.marginLeft = '0'
help.style.marginTop = '0.8em'
} catch (e) {}
let kind = detectIconKind(initialValue, kinds)
const radios = createSegmentedRadios(
kind,
kinds,
(v) => {
kind = v
syncPlaceholder()
input.value = ''
if (typeof (opts == null ? void 0 : opts.onKindChange) === 'function')
opts.onKindChange(kind)
updatePreview()
syncHelp()
},
{
labels: (_a = opts == null ? void 0 : opts.labels) != null ? _a : {},
namePrefix: opts == null ? void 0 : opts.namePrefix,
}
)
function syncPlaceholder() {
var _a2, _b, _c, _d, _e
const p =
(_a2 = opts == null ? void 0 : opts.placeholders) != null ? _a2 : {}
input.placeholder =
kind === 'icon'
? (_b = p.icon) != null
? _b
: 'home | search | folder | file | ...'
: kind === 'favicon'
? (_c = p.favicon) != null
? _c
: '16 | 32 | 64'
: kind === 'url'
? (_d = p.url) != null
? _d
: 'https://...'
: (_e = p.emoji) != null
? _e
: '\u{1F525} | \u{1F353} | \u{1F3BE} | ...'
}
{
const raw = String(initialValue || '')
let shown = raw
switch (kind) {
case 'icon': {
shown = raw.includes(':') ? raw.split(':').pop() || '' : raw
break
}
case 'favicon': {
if (raw.startsWith('favicon')) {
const param = raw.split(':')[1]
shown = param || ''
}
break
}
case 'url': {
shown = raw.startsWith('url:') ? raw.slice(4) : raw
break
}
case 'emoji': {
shown = raw
break
}
}
input.value = shown
}
const debouncedUpdatePreview = debounce(updatePreview, 500)
input.addEventListener('change', () => {
debouncedUpdatePreview()
if (typeof (opts == null ? void 0 : opts.onValueChange) === 'function') {
opts.onValueChange(input.value)
}
})
input.addEventListener('input', () => {
debouncedUpdatePreview()
})
syncPlaceholder()
updatePreview()
syncHelp()
const br = document.createElement('div')
br.style.flexBasis = '100%'
wrap.append(radios)
wrap.append(br)
wrap.append(inputContainer)
wrap.append(help)
function updatePreview() {
const finalValue = getFinalValue()
clearChildren(preview)
if (finalValue && !finalValue.startsWith('favicon')) {
setIcon(preview, finalValue)
}
}
function getFinalValue() {
const raw = input.value.trim()
if (!raw && kind !== 'favicon') return void 0
switch (kind) {
case 'icon': {
return raw.includes(':') ? raw : 'lucide:' + raw
}
case 'favicon': {
const sizeNum = Number.parseInt(raw, 10)
const s =
sizeNum === 16 ? 16 : sizeNum === 32 ? 32 : sizeNum === 64 ? 64 : 64
return 'favicon' + (raw ? ':' + String(s) : '')
}
case 'url': {
return raw.startsWith('url:') ? raw : 'url:' + raw
}
case 'emoji': {
return raw
}
}
}
function syncHelp() {
clearChildren(help)
switch (kind) {
case 'icon': {
const line = document.createElement('div')
line.append('\u67E5\u627E\u56FE\u6807\uFF1A ')
const a = document.createElement('a')
a.href = 'https://lucide.dev/icons/'
a.target = '_blank'
a.rel = 'noopener noreferrer'
a.textContent = 'https://lucide.dev/icons/'
line.append(a)
help.append(line)
break
}
case 'favicon': {
const line = document.createElement('div')
line.textContent = '\u65E0\u9884\u89C8\u6548\u679C'
help.append(line)
break
}
case 'url': {
const line = document.createElement('div')
line.textContent = '\u8BF7\u8F93\u5165\u56FE\u7247 URL'
help.append(line)
break
}
case 'emoji': {
const line = document.createElement('div')
line.textContent = '\u8BF7\u8F93\u5165\u4E00\u4E2A emoji'
help.append(line)
break
}
}
}
return {
el: wrap,
input,
radios,
getKind: () => kind,
setKind(k) {
kind = k
syncPlaceholder()
},
getRaw: () => input.value,
getFinal: getFinalValue,
}
}
function renderLinkForm(container, data, options) {
const grid = document.createElement('div')
grid.className = 'grid'
try {
grid.style.gridTemplateColumns = '1fr'
} catch (e) {}
const notifyChange = () => {
if (options.onChange) options.onChange()
}
if (options.groups && options.groups.length > 0) {
const grpRow = document.createElement('div')
grpRow.className = 'row'
const grpLabel = document.createElement('label')
grpLabel.textContent = '\u5206\u7EC4'
const grpSel = document.createElement('select')
for (const g of options.groups) {
const o = document.createElement('option')
o.value = g.id
o.textContent = g.name
if (g.id === data.groupId) o.selected = true
grpSel.append(o)
}
grpSel.addEventListener('change', () => {
data.groupId = grpSel.value
notifyChange()
})
if (options.disableGroupSelector) {
grpSel.disabled = true
}
grpRow.append(grpLabel)
grpRow.append(grpSel)
grid.append(grpRow)
}
const nameRow = document.createElement('div')
nameRow.className = 'row'
const nameLabel = document.createElement('label')
nameLabel.textContent = '\u540D\u79F0'
const nameInput = document.createElement('input')
nameInput.value = data.name || ''
nameInput.addEventListener('input', () => {
data.name = nameInput.value
notifyChange()
})
nameRow.append(nameLabel)
nameRow.append(nameInput)
grid.append(nameRow)
const iconRow = document.createElement('div')
iconRow.className = 'row'
const iconLabel = document.createElement('label')
iconLabel.textContent = '\u56FE\u6807'
const updateIconData = () => {
if (iconComp) {
data.icon = iconComp.getFinal()
notifyChange()
}
}
const iconComp = createIconInput(
data.icon || '',
['icon', 'favicon', 'url', 'emoji'],
{
labels: {
icon: '\u56FE\u6807',
favicon: 'Favicon',
url: 'URL',
emoji: 'Emoji',
},
namePrefix: 'ushortcuts-item-icon-kind-' + (data.id || 'new'),
onValueChange: updateIconData,
onKindChange: updateIconData,
}
)
iconRow.append(iconLabel)
iconRow.append(iconComp.el)
grid.append(iconRow)
const typeRow = document.createElement('div')
typeRow.className = 'row'
const typeLabel = document.createElement('label')
typeLabel.textContent = '\u7C7B\u578B'
const typeRadios = createSegmentedRadios(
data.type,
['url', 'js'],
(v) => {
data.type = v
syncTypeUi()
notifyChange()
},
{
labels: { url: 'URL', js: 'JS' },
namePrefix: 'ushortcuts-item-type-' + (data.id || 'new'),
}
)
typeRow.append(typeLabel)
typeRow.append(typeRadios)
grid.append(typeRow)
const urlRow = document.createElement('div')
urlRow.className = 'row'
const urlLabel = document.createElement('label')
urlLabel.textContent = 'URL'
const urlInput = document.createElement('input')
urlInput.placeholder = 'https://...'
urlInput.value = data.type === 'url' ? data.data || '/' : '/'
urlInput.addEventListener('input', () => {
if (data.type === 'url') {
data.data = urlInput.value
notifyChange()
}
})
urlRow.append(urlLabel)
urlRow.append(urlInput)
grid.append(urlRow)
const urlHelpRow = document.createElement('div')
urlHelpRow.className = 'row'
const urlHelp = document.createElement('div')
urlHelp.className = 'field-help'
urlHelp.innerHTML =
'\n <div class="field-help-title">\u{1F517} URL \u53D8\u91CF\u4E0E\u793A\u4F8B</div>\n <div>\u53D8\u91CF\uFF1A{hostname}\u3001{hostname_without_www}\u3001{query}\u3001{selected}</div>\n <div>\u793A\u4F8B\uFF1Ahttp://example.com/search?query={selected||query}</div>\n <div>\u66F4\u591A\u4F7F\u7528\u8BF4\u660E\u53C2\u8003 <a href="https://github.com/utags/userscripts" target="_blank" rel="noopener noreferrer">https://github.com/utags/userscripts</a></div>\n '
urlHelpRow.append(urlHelp)
grid.append(urlHelpRow)
const jsRow = document.createElement('div')
jsRow.className = 'row'
const jsLabel = document.createElement('label')
jsLabel.textContent = 'JS'
const jsInput = document.createElement('textarea')
jsInput.placeholder =
'console.log("hello")\n// \u6216\u8005\u7C98\u8D34\u811A\u672C\u5185\u5BB9'
jsInput.value = data.type === 'js' ? data.data || '' : ''
jsInput.addEventListener('input', () => {
if (data.type === 'js') {
data.data = jsInput.value
notifyChange()
}
})
jsRow.append(jsLabel)
jsRow.append(jsInput)
grid.append(jsRow)
const jsHelpRow = document.createElement('div')
jsHelpRow.className = 'row'
const jsHelp = document.createElement('div')
jsHelp.className = 'field-help'
jsHelp.innerHTML =
'\n <div class="field-help-title">\u{1F9E9} JS \u8FD4\u56DE\u4E0E\u793A\u4F8B</div>\n <div>JS\uFF1A\u8FD4\u56DE\u5B57\u7B26\u4E32\u6216 {url, mode} \u5BFC\u822A</div>\n <div>\u793A\u4F8B\uFF1Areturn "http://example.com/search?query={selected||query}"</div>\n <div>\u793A\u4F8B\uFF1Areturn { url: "http://example.com/?q={query}", mode: "new-tab" }</div>\n <div>\u66F4\u591A\u4F7F\u7528\u8BF4\u660E\u53C2\u8003 <a href="https://github.com/utags/userscripts" target="_blank" rel="noopener noreferrer">https://github.com/utags/userscripts</a></div>\n '
jsHelpRow.append(jsHelp)
grid.append(jsHelpRow)
const openRow = document.createElement('div')
openRow.className = 'row'
const openLabel = document.createElement('label')
openLabel.textContent = '\u6253\u5F00\u65B9\u5F0F'
const openRadios = createOpenModeRadios(data.openIn, (m) => {
data.openIn = m
notifyChange()
})
openRow.append(openLabel)
openRow.append(openRadios)
grid.append(openRow)
const visibleRow = document.createElement('div')
visibleRow.className = 'row'
const visibleLabel = document.createElement('label')
visibleLabel.textContent = '\u663E\u793A\u72B6\u6001'
const stateRadios = createSegmentedRadios(
data.hidden ? 'hidden' : 'visible',
['visible', 'hidden'],
(v) => {
data.hidden = v === 'hidden'
notifyChange()
},
{
labels: { visible: '\u663E\u793A', hidden: '\u9690\u85CF' },
namePrefix: 'ushortcuts-item-state-' + (data.id || 'new'),
}
)
visibleRow.append(visibleLabel)
visibleRow.append(stateRadios)
grid.append(visibleRow)
const quickRow = document.createElement('div')
quickRow.className = 'row'
const addCurrentBtn = document.createElement('button')
addCurrentBtn.className = 'btn btn-secondary'
addCurrentBtn.textContent = '\u6DFB\u52A0\u5F53\u524D\u7F51\u9875'
const pickLinksBtn = document.createElement('button')
pickLinksBtn.className = 'btn btn-secondary'
pickLinksBtn.textContent =
'\u4ECE\u5F53\u524D\u7F51\u9875\u91C7\u96C6\u94FE\u63A5'
quickRow.append(addCurrentBtn)
quickRow.append(pickLinksBtn)
grid.append(quickRow)
addCurrentBtn.addEventListener('click', () => {
try {
nameInput.value = document.title || '\u5F53\u524D\u7F51\u9875'
data.name = nameInput.value
const currentUrl = location.href
if (data.type === 'url') {
urlInput.value = currentUrl
data.data = currentUrl
} else {
const urlRadio = typeRadios.querySelector('input[value="url"]')
if (urlRadio) {
urlRadio.checked = true
urlRadio.dispatchEvent(new Event('change'))
}
urlInput.value = currentUrl
data.data = currentUrl
}
notifyChange()
} catch (e) {}
})
pickLinksBtn.addEventListener('click', () => {
try {
pickLinkFromPage(options.root, {
beforeStart() {
if (options.onPickStart) options.onPickStart()
},
afterFinish() {
if (options.onPickEnd) options.onPickEnd()
},
onPicked(nm, href) {
nameInput.value = nm
data.name = nm
const urlRadio = typeRadios.querySelector('input[value="url"]')
if (urlRadio) {
urlRadio.checked = true
urlRadio.dispatchEvent(new Event('change'))
}
urlInput.value = href
data.data = href
notifyChange()
},
})
} catch (e) {}
})
function syncTypeUi() {
if (data.type === 'url') {
urlRow.style.display = ''
jsRow.style.display = 'none'
quickRow.style.display = ''
urlHelpRow.style.display = ''
jsHelpRow.style.display = 'none'
} else {
urlRow.style.display = 'none'
jsRow.style.display = ''
quickRow.style.display = 'none'
urlHelpRow.style.display = 'none'
jsHelpRow.style.display = ''
}
}
syncTypeUi()
container.append(grid)
}
function openAddLinkModal(root, cfg, helpers) {
const { modal, body, actions, close, mask } = createModalFrame({
root,
title: helpers.existingItem
? '\u7F16\u8F91\u94FE\u63A5'
: '\u6DFB\u52A0\u94FE\u63A5',
})
modal.classList.add('editor')
const firstGroup = (cfg.groups && cfg.groups[0]) || void 0
const defaultGroup =
helpers.defaultGroupId || (firstGroup && firstGroup.id) || ''
const currentGroupId = helpers.existingItem
? helpers.defaultGroupId || defaultGroup
: defaultGroup
const formData = helpers.existingItem
? {
id: helpers.existingItem.id,
groupId: currentGroupId,
name: helpers.existingItem.name || '\u65B0\u9879',
icon: helpers.existingItem.icon,
type: helpers.existingItem.type || 'url',
data:
helpers.existingItem.data ||
(helpers.existingItem.type === 'js' ? '' : '/'),
openIn:
helpers.existingItem.openIn || helpers.defaultOpen || 'same-tab',
hidden: helpers.existingItem.hidden,
}
: {
id: uid(),
groupId: defaultGroup,
name: '\u65B0\u9879',
type: 'url',
data: '/',
openIn: helpers.defaultOpen || 'same-tab',
}
const formContainer = document.createElement('div')
renderLinkForm(formContainer, formData, {
root,
groups: cfg.groups || [],
disableGroupSelector: Boolean(helpers.existingItem),
onChange() {},
onPickStart() {
modal.style.display = 'none'
mask.remove()
},
onPickEnd() {
modal.style.display = ''
root.append(mask)
},
})
body.append(formContainer)
const saveBtn = document.createElement('button')
saveBtn.className = 'btn btn-primary'
saveBtn.textContent = helpers.existingItem ? '\u786E\u8BA4' : '\u6DFB\u52A0'
const cancelBtn = document.createElement('button')
cancelBtn.className = 'btn btn-secondary'
cancelBtn.textContent = '\u53D6\u6D88'
const deleteBtn = document.createElement('button')
deleteBtn.className = 'btn btn-secondary'
deleteBtn.textContent = '\u5220\u9664'
saveBtn.addEventListener('click', () => {
var _a
const gid = formData.groupId
const grp = (cfg.groups || []).find((g) => g.id === gid)
if (!grp) return
const hasDup = hasDuplicateInGroup(
grp,
formData.type,
formData.data,
(_a = helpers.existingItem) == null ? void 0 : _a.id
)
if (hasDup) {
const msg =
formData.type === 'url'
? helpers.existingItem
? '\u8BE5\u5206\u7EC4\u5185\u5DF2\u5B58\u5728\u76F8\u540C\u7684 URL\uFF0C\u662F\u5426\u7EE7\u7EED\u4FDD\u5B58\uFF1F'
: '\u8BE5\u5206\u7EC4\u5185\u5DF2\u5B58\u5728\u76F8\u540C\u7684 URL\uFF0C\u662F\u5426\u7EE7\u7EED\u6DFB\u52A0\uFF1F'
: helpers.existingItem
? '\u8BE5\u5206\u7EC4\u5185\u5DF2\u5B58\u5728\u76F8\u540C\u7684 JS\uFF0C\u662F\u5426\u7EE7\u7EED\u4FDD\u5B58\uFF1F'
: '\u8BE5\u5206\u7EC4\u5185\u5DF2\u5B58\u5728\u76F8\u540C\u7684 JS\uFF0C\u662F\u5426\u7EE7\u7EED\u6DFB\u52A0\uFF1F'
const ok = globalThis.confirm(msg)
if (!ok) return
}
if (helpers.existingItem) {
const it = helpers.existingItem
it.name = formData.name
it.icon = formData.icon
it.type = formData.type
it.data = formData.data
it.openIn = formData.openIn
it.hidden = formData.hidden
} else {
const it = {
id: formData.id || uid(),
name: formData.name,
icon: formData.icon,
type: formData.type,
data: formData.data,
openIn: formData.openIn,
hidden: formData.hidden,
}
grp.items.push(it)
}
try {
helpers.saveConfig(cfg)
} catch (e) {}
try {
helpers.rerender(root, cfg)
} catch (e) {}
close()
})
deleteBtn.addEventListener('click', () => {
if (!helpers.existingItem) return
const ok = globalThis.confirm(
'\u662F\u5426\u5220\u9664\u6B64\u94FE\u63A5\uFF1F'
)
if (!ok) return
const gid = formData.groupId
const grp = (cfg.groups || []).find((g) => g.id === gid)
if (!grp) return
const idx = grp.items.findIndex(
(x) => x && x.id === helpers.existingItem.id
)
if (idx !== -1) {
try {
grp.items.splice(idx, 1)
} catch (e) {}
try {
helpers.saveConfig(cfg)
} catch (e) {}
try {
helpers.rerender(root, cfg)
} catch (e) {}
close()
}
})
cancelBtn.addEventListener('click', close)
actions.append(saveBtn)
actions.append(cancelBtn)
if (helpers.existingItem) {
actions.append(deleteBtn)
}
}
function renderGroupForm(container, data, options) {
var _a
const grid = document.createElement('div')
grid.className = 'grid'
try {
grid.style.gridTemplateColumns = '1fr'
} catch (e) {}
const notifyChange = () => {
if (options.onChange) options.onChange()
}
const nameRow = document.createElement('div')
nameRow.className = 'row'
const nameLabel = document.createElement('label')
nameLabel.textContent = '\u7EC4\u540D'
const nameInput = document.createElement('input')
nameInput.value = data.name || ''
nameInput.addEventListener('input', () => {
data.name = nameInput.value
if (!displayToggle.checked) {
displayInput.value = nameInput.value
}
notifyChange()
})
nameRow.append(nameLabel)
nameRow.append(nameInput)
const displayRow = document.createElement('div')
displayRow.className = 'row'
const displayLabel = document.createElement('label')
displayLabel.textContent = '\u663E\u793A\u7EC4\u540D'
const displayInput = document.createElement('input')
const displayCtrl = document.createElement('label')
displayCtrl.className = 'check'
const displayToggle = document.createElement('input')
displayToggle.type = 'checkbox'
const displayText = document.createElement('span')
displayText.textContent = '\u81EA\u5B9A\u4E49'
displayCtrl.append(displayToggle)
displayCtrl.append(displayText)
const hasCustomDisplay =
typeof data.displayName === 'string' && data.displayName !== data.name
displayToggle.checked = Boolean(hasCustomDisplay)
displayInput.value = hasCustomDisplay
? data.displayName || ''
: data.name || nameInput.value
displayInput.disabled = !displayToggle.checked
const updateDisplay = () => {
if (displayToggle.checked) {
data.displayName = displayInput.value
displayInput.disabled = false
} else {
delete data.displayName
displayInput.value = nameInput.value
displayInput.disabled = true
}
notifyChange()
}
displayInput.addEventListener('input', updateDisplay)
displayToggle.addEventListener('change', updateDisplay)
displayRow.append(displayLabel)
displayRow.append(displayInput)
displayRow.append(displayCtrl)
const iconRow = document.createElement('div')
iconRow.className = 'row'
const iconLabel = document.createElement('label')
iconLabel.textContent = '\u56FE\u6807'
const iconComp = createIconInput(
data.icon || 'lucide:folder',
['icon', 'url', 'emoji'],
{
labels: { icon: '\u56FE\u6807', url: 'URL', emoji: 'Emoji' },
namePrefix: 'ushortcuts-group-icon-kind-' + (data.id || Math.random()),
onValueChange() {
data.icon = iconComp.getFinal()
notifyChange()
},
onKindChange() {
data.icon = iconComp.getFinal()
notifyChange()
},
}
)
iconRow.append(iconLabel)
iconRow.append(iconComp.el)
const ruleRow = document.createElement('div')
ruleRow.className = 'row'
const ruleLabel = document.createElement('label')
ruleLabel.textContent = 'URL \u89C4\u5219'
const ta = document.createElement('textarea')
const host = location.hostname || ''
const defaultMatch = ['*://' + host + '/*']
ta.value = (
data.match && data.match.length > 0 ? data.match : defaultMatch
).join('\n')
const updateMatch = () => {
data.match = ta.value
.split(/\n+/)
.map((v) => v.trim())
.filter(Boolean)
notifyChange()
}
ta.addEventListener('change', updateMatch)
ruleRow.append(ruleLabel)
ruleRow.append(ta)
function escRe(s) {
let out = ''
const specials = '\\^$.*+?()[]{}|'
for (const ch of s) out += specials.includes(ch) ? '\\' + ch : ch
return out
}
function regexHostAll(h) {
const hh = escRe(h)
return '/.+://'.concat(hh, '/.*$/')
}
function regexHostDir(h, d) {
const hh = escRe(h)
const dd = escRe(d)
return '/.+://'.concat(hh).concat(dd, '.*$/')
}
function regexHostPath(h, p) {
const hh = escRe(h)
const pp = escRe(p)
return '/.+://'.concat(hh).concat(pp, '$/')
}
const tplRow = document.createElement('div')
tplRow.className = 'row'
const tplLabel = document.createElement('label')
tplLabel.textContent = '\u89C4\u5219\u6A21\u677F'
const tplSel = document.createElement('select')
const pathname = location.pathname || '/'
const dir = pathname.endsWith('/')
? pathname
: pathname.replace(/[^/]+$/, '')
const opts = [
{
v: '*://'.concat(host, '/*'),
t: '\u5F53\u524D\u57DF\u540D\u6240\u6709\u9875\u9762',
},
{
v: '*://'.concat(host).concat(dir, '*'),
t: '\u5F53\u524D\u8DEF\u5F84\u524D\u7F00',
},
{
v: '*://'.concat(host).concat(pathname),
t: '\u5F53\u524D\u5B8C\u6574\u8DEF\u5F84',
},
{ v: '*', t: '\u4EFB\u610F\u57DF\u540D\u6240\u6709\u9875\u9762' },
{
v: regexHostAll(host),
t: '\u6B63\u5219\uFF1A\u5F53\u524D\u57DF\u540D\u6240\u6709\u9875\u9762',
},
{
v: regexHostDir(host, dir),
t: '\u6B63\u5219\uFF1A\u5F53\u524D\u8DEF\u5F84\u524D\u7F00',
},
{
v: regexHostPath(host, pathname),
t: '\u6B63\u5219\uFF1A\u5F53\u524D\u5B8C\u6574\u8DEF\u5F84',
},
]
for (const it of opts) {
const o = document.createElement('option')
o.value = it.v
o.textContent = it.t
tplSel.append(o)
}
tplSel.addEventListener('change', () => {
ta.value = tplSel.value
updateMatch()
})
tplRow.append(tplLabel)
tplRow.append(tplSel)
const openRow = document.createElement('div')
openRow.className = 'row'
const openLabel = document.createElement('label')
openLabel.textContent = '\u9ED8\u8BA4\u6253\u5F00\u65B9\u5F0F'
const openRadios = createOpenModeRadios(
data.defaultOpen || 'same-tab',
(m) => {
data.defaultOpen = m
notifyChange()
}
)
openRow.append(openLabel)
openRow.append(openRadios)
const colsRow = document.createElement('div')
colsRow.className = 'row'
const colsLabel = document.createElement('label')
colsLabel.textContent = '\u6BCF\u884C\u663E\u793A\u4E2A\u6570'
let colVal = String((_a = data.itemsPerRow) != null ? _a : 1)
const colsRadios = createSegmentedRadios(
colVal,
['1', '2', '3', '4', '5', '6'],
(v) => {
colVal = v
data.itemsPerRow = Number.parseInt(v, 10)
notifyChange()
},
{ namePrefix: 'ushortcuts-cols-' + (data.id || Math.random()) }
)
colsRow.append(colsLabel)
colsRow.append(colsRadios)
const stateRow = document.createElement('div')
stateRow.className = 'row'
const stateLabel = document.createElement('label')
stateLabel.textContent = '\u5206\u7EC4\u663E\u793A\u72B6\u6001'
let groupState = data.hidden ? 'hidden' : 'visible'
const stateRadios = createSegmentedRadios(
groupState,
['visible', 'hidden'],
(v) => {
groupState = v
data.hidden = v === 'hidden'
notifyChange()
},
{
labels: { visible: '\u663E\u793A', hidden: '\u9690\u85CF' },
namePrefix: 'ushortcuts-state-' + (data.id || Math.random()),
}
)
stateRow.append(stateLabel)
stateRow.append(stateRadios)
grid.append(nameRow)
grid.append(displayRow)
grid.append(iconRow)
grid.append(tplRow)
grid.append(ruleRow)
grid.append(openRow)
grid.append(colsRow)
grid.append(stateRow)
container.append(grid)
return {
nameInput,
}
}
function openAddGroupModal(root, cfg, helpers) {
var _a, _b, _c, _d, _e, _f, _g
const { modal, body, actions, close } = createModalFrame({
root,
title: helpers.existingGroup
? '\u7F16\u8F91\u5206\u7EC4'
: '\u6DFB\u52A0\u5206\u7EC4',
})
modal.classList.add('editor')
const initialData = {
name:
((_a = helpers.existingGroup) == null ? void 0 : _a.name) ||
'\u65B0\u5206\u7EC4',
displayName:
(_b = helpers.existingGroup) == null ? void 0 : _b.displayName,
icon:
((_c = helpers.existingGroup) == null ? void 0 : _c.icon) ||
'lucide:folder',
match: ((_d = helpers.existingGroup) == null ? void 0 : _d.match) ||
helpers.defaultMatch || ['*://' + (location.hostname || '') + '/*'],
defaultOpen:
((_e = helpers.existingGroup) == null ? void 0 : _e.defaultOpen) ||
helpers.defaultOpen ||
'same-tab',
itemsPerRow:
((_f = helpers.existingGroup) == null ? void 0 : _f.itemsPerRow) || 1,
hidden: (_g = helpers.existingGroup) == null ? void 0 : _g.hidden,
}
renderGroupForm(body, initialData, {
onChange() {},
})
const saveBtn = document.createElement('button')
saveBtn.className = 'btn btn-primary'
saveBtn.textContent = helpers.existingGroup
? '\u786E\u8BA4'
: '\u6DFB\u52A0'
const cancelBtn = document.createElement('button')
cancelBtn.className = 'btn btn-secondary'
cancelBtn.textContent = '\u53D6\u6D88'
saveBtn.addEventListener('click', () => {
const res = initialData
if (!res.name) {
return
}
if (helpers.existingGroup) {
const g = helpers.existingGroup
Object.assign(g, res)
if (!res.displayName) delete g.displayName
} else {
const g = __spreadValues(
{
id: uid(),
items: [],
},
res
)
if (!res.displayName) delete g.displayName
cfg.groups.push(g)
}
try {
helpers.saveConfig(cfg)
} catch (e) {}
try {
helpers.rerender(root, cfg)
} catch (e) {}
close()
})
cancelBtn.addEventListener('click', close)
actions.append(saveBtn)
actions.append(cancelBtn)
}
function showDropdownMenu(root, anchor, items, rightSide) {
for (const n of Array.from(root.querySelectorAll('.quick-add-menu')))
n.remove()
const menu = document.createElement('div')
menu.className = 'quick-add-menu'
menu.setAttribute('role', 'menu')
for (const it of items) {
const btn = document.createElement('button')
btn.className = 'quick-add-item'
btn.setAttribute('role', 'menuitem')
btn.setAttribute('tabindex', '0')
btn.dataset.icon = it.icon
btn.textContent = it.label
btn.addEventListener('click', (e) => {
e.stopPropagation()
try {
it.onClick(e)
} finally {
for (const n of Array.from(root.querySelectorAll('.quick-add-menu')))
n.remove()
}
})
menu.append(btn)
}
const r = anchor.getBoundingClientRect()
menu.style.position = 'fixed'
const top = Math.round(r.bottom + 6)
if (rightSide) {
const right = Math.round(window.innerWidth - r.right)
menu.style.top = ''.concat(top, 'px')
menu.style.right = ''.concat(right, 'px')
} else {
const left = Math.round(r.left)
menu.style.top = ''.concat(top, 'px')
menu.style.left = ''.concat(left, 'px')
}
root.append(menu)
setTimeout(() => {
const onOutside = () => {
for (const n of Array.from(root.querySelectorAll('.quick-add-menu')))
n.remove()
}
root.addEventListener('click', onOutside, { once: true })
document.addEventListener('click', onOutside, { once: true })
document.addEventListener(
'keydown',
(ev) => {
if (ev.key === 'Escape') onOutside()
},
{ once: true }
)
}, 0)
}
function createGroupManagerPanel(root, cfg, helpers) {
const wrap = document.createElement('div')
wrap.className = 'panel-split'
const sidebar = document.createElement('div')
sidebar.className = 'panel-sidebar'
const sidebarList = document.createElement('div')
sidebarList.className = 'flex-1'
const sidebarActions = document.createElement('div')
sidebarActions.className = 'sidebar-actions'
const content = document.createElement('div')
content.className = 'panel-content'
const contentHeader = document.createElement('div')
contentHeader.className = 'content-header'
const contentTabs = document.createElement('div')
contentTabs.className = 'content-tabs'
const contentBody = document.createElement('div')
contentBody.className = 'tab-pane'
content.append(contentHeader)
content.append(contentTabs)
content.append(contentBody)
let activeGroup = (cfg.groups || [])[0]
let activeTab = 'shortcuts'
let isSettingsDirty = false
let pendingGroupData
let activeLinkItem
let isLinkDirty = false
let editingLinkOriginalId
const savePendingSettings = () => {
if (pendingGroupData && activeGroup) {
Object.assign(activeGroup, pendingGroupData)
if (!activeGroup.displayName) delete activeGroup.displayName
if (!activeGroup.icon) delete activeGroup.icon
if (activeGroup.hidden === false) delete activeGroup.hidden
helpers.saveConfig(cfg)
helpers.rerender(root, cfg)
rebuildContentHeader()
rebuildSidebar()
}
isSettingsDirty = false
pendingGroupData = void 0
}
const checkUnsavedChanges = (callback) => {
if (isSettingsDirty) {
if (
globalThis.confirm(
'\u5F53\u524D\u5206\u7EC4\u8BBE\u7F6E\u6709\u672A\u4FDD\u5B58\u7684\u4FEE\u6539\uFF0C\u662F\u5426\u4FDD\u5B58\uFF1F\n(\u786E\u5B9A\uFF1A\u4FDD\u5B58\u5E76\u7EE7\u7EED\uFF1B\u53D6\u6D88\uFF1A\u653E\u5F03\u4FEE\u6539\u5E76\u7EE7\u7EED)'
)
) {
savePendingSettings()
} else {
isSettingsDirty = false
pendingGroupData = void 0
}
}
if (isLinkDirty) {
if (
!globalThis.confirm(
'\u5F53\u524D\u94FE\u63A5\u7F16\u8F91\u6709\u672A\u4FDD\u5B58\u7684\u4FEE\u6539\uFF0C\u786E\u5B9A\u653E\u5F03\u4FEE\u6539\u5417\uFF1F'
)
) {
return
}
activeLinkItem = void 0
isLinkDirty = false
editingLinkOriginalId = void 0
}
callback()
}
const handleGroupClick = (g) => {
checkUnsavedChanges(() => {
activeGroup = g
activeLinkItem = void 0
isLinkDirty = false
editingLinkOriginalId = void 0
rebuildSidebar()
rebuildContent()
})
}
function rebuildSidebar() {
clearChildren(sidebarList)
for (const g of cfg.groups || []) {
const item = document.createElement('div')
item.className =
'sidebar-item' + (g.id === activeGroup.id ? ' active' : '')
item.addEventListener('click', () => {
handleGroupClick(g)
})
const name = document.createElement('span')
name.className = 'sidebar-item-name'
name.textContent = g.name
item.append(name)
if (g.displayName) {
const desc = document.createElement('span')
desc.className = 'sidebar-item-desc'
desc.textContent = g.displayName
item.append(desc)
}
sidebarList.append(item)
}
}
const addGroupBtn = document.createElement('button')
addGroupBtn.className = 'btn btn-secondary w-full justify-center'
addGroupBtn.textContent = '\u6DFB\u52A0\u5206\u7EC4'
addGroupBtn.addEventListener('click', () => {
checkUnsavedChanges(() => {
const ng = {
id: uid(),
name: '\u65B0\u5206\u7EC4',
icon: 'lucide:folder',
match: ['*://' + (location.hostname || '') + '/*'],
items: [],
defaultOpen: helpers.sitePref.defaultOpen,
}
cfg.groups.push(ng)
activeGroup = ng
activeTab = 'settings'
helpers.saveConfig(cfg)
rebuildSidebar()
rebuildContent()
helpers.rerender(root, cfg)
})
})
sidebarActions.append(addGroupBtn)
const delEmptyGroupsBtn = document.createElement('button')
delEmptyGroupsBtn.className =
'btn btn-secondary w-full justify-center text-xs'
delEmptyGroupsBtn.textContent = '\u6E05\u7406\u7A7A\u5206\u7EC4'
delEmptyGroupsBtn.addEventListener('click', () => {
checkUnsavedChanges(() => {
const empties = (cfg.groups || []).filter(
(g) => (g.items || []).length === 0
)
const n = empties.length
if (n === 0) {
globalThis.alert('\u6CA1\u6709\u53D1\u73B0\u7A7A\u5206\u7EC4')
return
}
const ok = globalThis.confirm(
'\u786E\u8BA4\u5220\u9664 ' +
String(n) +
' \u4E2A\u7A7A\u5206\u7EC4\uFF1F'
)
if (!ok) return
const kept = (cfg.groups || []).filter(
(g) => (g.items || []).length > 0
)
if (kept.length === 0) {
const ng = {
id: uid(),
name: '\u65B0\u5206\u7EC4',
icon: 'lucide:folder',
match: ['*://' + (location.hostname || '') + '/*'],
items: [],
defaultOpen: helpers.sitePref.defaultOpen,
}
kept.push(ng)
}
cfg.groups = kept
activeGroup = cfg.groups[0]
helpers.saveConfig(cfg)
rebuildSidebar()
rebuildContent()
helpers.rerender(root, cfg)
})
})
sidebarActions.append(delEmptyGroupsBtn)
sidebar.append(sidebarList)
sidebar.append(sidebarActions)
function rebuildContentHeader() {
clearChildren(contentHeader)
const title = document.createElement('div')
title.className = 'content-title'
title.textContent = activeGroup.name
contentHeader.append(title)
const delBtn = document.createElement('button')
delBtn.className = 'btn btn-secondary mini text-red-600'
delBtn.textContent = '\u5220\u9664\u5206\u7EC4'
delBtn.addEventListener('click', () => {
if ((cfg.groups || []).length <= 1) return
if (
!globalThis.confirm(
'\u786E\u8BA4\u5220\u9664\u5206\u7EC4 "' +
String(activeGroup.name) +
'" \u53CA\u5176\u6240\u6709\u5185\u5BB9\uFF1F'
)
)
return
isSettingsDirty = false
pendingGroupData = void 0
activeLinkItem = void 0
isLinkDirty = false
cfg.groups = (cfg.groups || []).filter((g) => g.id !== activeGroup.id)
activeGroup = cfg.groups[0]
helpers.saveConfig(cfg)
rebuildSidebar()
rebuildContent()
helpers.rerender(root, cfg)
})
if ((cfg.groups || []).length <= 1) {
delBtn.disabled = true
delBtn.style.opacity = '0.5'
}
contentHeader.append(delBtn)
}
const handleTabClick = (k) => {
checkUnsavedChanges(() => {
activeTab = k
rebuildTabs()
rebuildTabContent()
})
}
function rebuildTabs() {
clearChildren(contentTabs)
const tabs = [
{ key: 'shortcuts', label: '\u5FEB\u6377\u5BFC\u822A (Shortcuts)' },
{ key: 'settings', label: '\u5206\u7EC4\u8BBE\u7F6E' },
]
for (const t of tabs) {
const btn = document.createElement('div')
btn.className = 'tab-btn' + (activeTab === t.key ? ' active' : '')
btn.textContent = t.label
btn.addEventListener('click', () => {
handleTabClick(t.key)
})
contentTabs.append(btn)
}
}
function rebuildTabContent() {
clearChildren(contentBody)
if (activeTab === 'settings') {
renderSettingsTab(contentBody)
} else {
renderShortcutsTab(contentBody)
}
}
function renderSettingsTab(container) {
const initData = __spreadValues({}, activeGroup)
pendingGroupData = initData
if (activeGroup.match) pendingGroupData.match = [...activeGroup.match]
isSettingsDirty = false
const cancelBtn = document.createElement('button')
const saveBtn = document.createElement('button')
const formWrap = document.createElement('div')
renderGroupForm(formWrap, pendingGroupData, {
onChange() {
isSettingsDirty = true
cancelBtn.disabled = false
saveBtn.disabled = false
},
})
const actions = document.createElement('div')
actions.className = 'row justify-end mt-4 gap-2'
cancelBtn.className = 'btn btn-secondary'
cancelBtn.textContent = '\u53D6\u6D88'
cancelBtn.disabled = true
cancelBtn.addEventListener('click', () => {
if (
isSettingsDirty &&
!globalThis.confirm(
'\u786E\u5B9A\u653E\u5F03\u672A\u4FDD\u5B58\u7684\u4FEE\u6539\u5417\uFF1F'
)
) {
return
}
isSettingsDirty = false
pendingGroupData = void 0
renderSettingsTab(container)
})
saveBtn.className = 'btn btn-primary'
saveBtn.textContent = '\u4FDD\u5B58\u8BBE\u7F6E'
saveBtn.disabled = true
saveBtn.addEventListener('click', () => {
savePendingSettings()
renderSettingsTab(container)
})
actions.append(cancelBtn)
actions.append(saveBtn)
clearChildren(container)
container.append(formWrap)
container.append(actions)
}
function renderShortcutsTab(container) {
if (activeLinkItem) {
renderLinkEditor(container)
} else {
renderLinkList(container)
}
}
function renderLinkEditor(container) {
if (!activeLinkItem) return
const formWrap = document.createElement('div')
const actions = document.createElement('div')
actions.className = 'row justify-end mt-4 gap-2'
const cancelBtn = document.createElement('button')
const saveBtn = document.createElement('button')
let tempMask
let tempModal
renderLinkForm(formWrap, activeLinkItem, {
root,
groups: cfg.groups || [],
disableGroupSelector: Boolean(editingLinkOriginalId),
onChange() {
isLinkDirty = true
saveBtn.disabled = false
},
onPickStart() {
const mask = root.querySelector('.modal-mask')
if (mask) {
tempMask = mask
tempModal = mask.querySelector('.modal')
if (tempModal) tempModal.style.display = 'none'
mask.remove()
}
},
onPickEnd() {
if (tempMask && tempModal) {
tempModal.style.display = ''
root.append(tempMask)
}
},
})
cancelBtn.className = 'btn btn-secondary'
cancelBtn.textContent = '\u53D6\u6D88'
cancelBtn.addEventListener('click', () => {
if (
isLinkDirty &&
!globalThis.confirm(
'\u786E\u5B9A\u653E\u5F03\u672A\u4FDD\u5B58\u7684\u4FEE\u6539\u5417\uFF1F'
)
) {
return
}
activeLinkItem = void 0
isLinkDirty = false
editingLinkOriginalId = void 0
renderShortcutsTab(container)
})
saveBtn.className = 'btn btn-primary'
saveBtn.textContent = editingLinkOriginalId
? '\u786E\u8BA4'
: '\u6DFB\u52A0'
saveBtn.disabled = !isLinkDirty
saveBtn.addEventListener('click', () => {
const gid = activeLinkItem.groupId
const grp = (cfg.groups || []).find((g) => g.id === gid)
if (!grp) return
const hasDup = hasDuplicateInGroup(
grp,
activeLinkItem.type,
activeLinkItem.data,
editingLinkOriginalId
)
if (hasDup) {
const msg =
activeLinkItem.type === 'url'
? editingLinkOriginalId
? '\u8BE5\u5206\u7EC4\u5185\u5DF2\u5B58\u5728\u76F8\u540C\u7684 URL\uFF0C\u662F\u5426\u7EE7\u7EED\u4FDD\u5B58\uFF1F'
: '\u8BE5\u5206\u7EC4\u5185\u5DF2\u5B58\u5728\u76F8\u540C\u7684 URL\uFF0C\u662F\u5426\u7EE7\u7EED\u6DFB\u52A0\uFF1F'
: editingLinkOriginalId
? '\u8BE5\u5206\u7EC4\u5185\u5DF2\u5B58\u5728\u76F8\u540C\u7684 JS\uFF0C\u662F\u5426\u7EE7\u7EED\u4FDD\u5B58\uFF1F'
: '\u8BE5\u5206\u7EC4\u5185\u5DF2\u5B58\u5728\u76F8\u540C\u7684 JS\uFF0C\u662F\u5426\u7EE7\u7EED\u6DFB\u52A0\uFF1F'
const ok = globalThis.confirm(msg)
if (!ok) return
}
if (editingLinkOriginalId) {
const it = grp.items.find((x) => x.id === editingLinkOriginalId)
if (it) {
it.name = activeLinkItem.name
it.icon = activeLinkItem.icon
it.type = activeLinkItem.type
it.data = activeLinkItem.data
it.openIn = activeLinkItem.openIn
it.hidden = activeLinkItem.hidden
}
} else {
const it = {
id: activeLinkItem.id || uid(),
name: activeLinkItem.name,
icon: activeLinkItem.icon,
type: activeLinkItem.type,
data: activeLinkItem.data,
openIn: activeLinkItem.openIn,
hidden: activeLinkItem.hidden,
}
grp.items.push(it)
}
try {
helpers.saveConfig(cfg)
} catch (e) {}
try {
helpers.rerender(root, cfg)
} catch (e) {}
activeLinkItem = void 0
isLinkDirty = false
editingLinkOriginalId = void 0
if (grp.id !== activeGroup.id) {
}
renderShortcutsTab(container)
})
actions.append(cancelBtn)
actions.append(saveBtn)
clearChildren(container)
container.append(formWrap)
container.append(actions)
}
function renderLinkList(container) {
clearChildren(container)
const list = document.createElement('div')
list.className = 'shortcut-list'
const grp = activeGroup
const addRow = document.createElement('div')
addRow.className = 'mb-3'
const addBtn = document.createElement('button')
addBtn.className = 'btn btn-primary w-full justify-center'
addBtn.textContent = '+ \u6DFB\u52A0\u5FEB\u6377\u5BFC\u822A'
addBtn.addEventListener('click', () => {
var _a
activeLinkItem = {
id: uid(),
groupId: activeGroup.id,
name: '\u65B0\u9879',
type: 'url',
data: '/',
openIn:
(_a = activeGroup.defaultOpen) != null
? _a
: helpers.sitePref.defaultOpen,
}
isLinkDirty = false
editingLinkOriginalId = void 0
renderShortcutsTab(container)
})
addRow.append(addBtn)
container.append(addRow)
for (const it of grp.items || []) {
const itemEl = document.createElement('div')
itemEl.className = 'shortcut-item group'
if (it.hidden) itemEl.classList.add('is-hidden')
const iconEl = document.createElement('div')
iconEl.className = 'shortcut-icon'
setIcon(iconEl, it.icon || 'lucide:link')
itemEl.append(iconEl)
const info = document.createElement('div')
info.className = 'shortcut-info'
const name = document.createElement('div')
name.className = 'shortcut-name'
name.textContent = it.name
info.append(name)
const meta = document.createElement('div')
meta.className = 'shortcut-meta'
meta.textContent =
(it.type === 'js' ? 'JS' : 'URL') +
' \u2022 ' +
String(it.openIn || '\u9ED8\u8BA4')
if (it.hidden) meta.textContent += ' \u2022 \u5DF2\u9690\u85CF'
info.append(meta)
itemEl.append(info)
const actions = document.createElement('div')
actions.className = 'shortcut-actions'
const editBtn = document.createElement('button')
editBtn.className = 'icon-btn'
setIcon(editBtn, 'lucide:edit-3', '\u7F16\u8F91')
editBtn.addEventListener('click', () => {
activeLinkItem = {
id: it.id,
groupId: activeGroup.id,
name: it.name,
icon: it.icon,
type: it.type,
data: it.data,
openIn: it.openIn,
hidden: it.hidden,
}
isLinkDirty = false
editingLinkOriginalId = it.id
renderShortcutsTab(container)
})
actions.append(editBtn)
const hideBtn = document.createElement('button')
hideBtn.className = 'icon-btn'
setIcon(
hideBtn,
it.hidden ? 'lucide:eye' : 'lucide:eye-off',
it.hidden ? '\u663E\u793A' : '\u9690\u85CF'
)
hideBtn.addEventListener('click', () => {
it.hidden = !it.hidden
helpers.saveConfig(cfg)
rebuildTabContent()
helpers.rerender(root, cfg)
})
actions.append(hideBtn)
const delBtn = document.createElement('button')
delBtn.className = 'icon-btn text-danger'
setIcon(delBtn, 'lucide:trash-2', '\u5220\u9664')
delBtn.addEventListener('click', () => {
if (
!globalThis.confirm(
'\u786E\u5B9A\u5220\u9664 "' + String(it.name) + '" \u5417\uFF1F'
)
)
return
grp.items = (grp.items || []).filter((x) => x.id !== it.id)
helpers.saveConfig(cfg)
rebuildTabContent()
helpers.rerender(root, cfg)
})
actions.append(delBtn)
itemEl.append(actions)
list.append(itemEl)
}
container.append(list)
}
function rebuildContent() {
rebuildContentHeader()
rebuildTabs()
rebuildTabContent()
}
rebuildSidebar()
rebuildContent()
wrap.append(sidebar)
wrap.append(content)
return { el: wrap, checkUnsavedChanges }
}
function openEditorModal(root, cfg, helpers) {
const { modal, body, actions, close } = createModalFrame({
root,
title: '\u5206\u7EC4\u7BA1\u7406',
})
modal.classList.add('editor')
const groupsPanel = createGroupManagerPanel(root, cfg, helpers)
body.append(groupsPanel.el)
const closeBtn = document.createElement('button')
closeBtn.className = 'btn btn-secondary'
closeBtn.textContent = '\u5173\u95ED'
closeBtn.addEventListener('click', () => {
groupsPanel.checkUnsavedChanges(() => {
close()
})
})
actions.append(closeBtn)
}
function deepMergeReplaceArrays(target, source) {
if (target === null || typeof target !== 'object') return source
if (source === null || typeof source !== 'object')
return source != null ? source : target
if (Array.isArray(target) && Array.isArray(source)) return source
const out = __spreadValues({}, target)
const src = source
const trg = target
for (const k of Object.keys(src)) {
const sv = src[k]
const tv = trg[k]
if (Array.isArray(sv)) out[k] = sv
else if (sv && typeof sv === 'object')
out[k] = deepMergeReplaceArrays(tv != null ? tv : {}, sv)
else out[k] = sv
}
return out
}
var normalizeToDefaultType = (val, dv) => {
const t = typeof dv
if (t === 'number') {
const n = Number(val)
return Number.isFinite(n) ? n : dv
}
if (t === 'object') {
return val && typeof val === 'object' ? val : dv
}
return typeof val === t ? val : dv
}
function setOrDelete(obj, key, value, defaultValue) {
const normalized = normalizeToDefaultType(value, defaultValue)
const isEqual = (a, b) => {
if (a === b) return true
if (a && b && typeof a === 'object' && typeof b === 'object') {
try {
return JSON.stringify(a) === JSON.stringify(b)
} catch (e) {}
}
return false
}
if (isEqual(normalized, defaultValue)) {
delete obj[key]
} else {
obj[key] = normalized
}
}
function c(tag, opts) {
const el = document.createElement(tag)
if (!opts) return el
if (opts.className) el.className = opts.className
if (opts.classes) for (const cls of opts.classes) el.classList.add(cls)
if (opts.dataset)
for (const k of Object.keys(opts.dataset)) el.dataset[k] = opts.dataset[k]
if (opts.attrs)
for (const k of Object.keys(opts.attrs)) el.setAttribute(k, opts.attrs[k])
if (opts.style)
for (const k of Object.keys(opts.style)) el.style[k] = opts.style[k]
if ('text' in opts) el.textContent = opts.text || ''
if (opts.type && 'type' in el) el.type = opts.type
if ('value' in opts && 'value' in el) el.value = opts.value || ''
if (opts.rows && 'rows' in el) el.rows = opts.rows
if (opts.placeholder && 'placeholder' in el)
el.placeholder = opts.placeholder
if (typeof opts.checked === 'boolean' && 'checked' in el)
el.checked = opts.checked
if (opts.children) {
for (const ch of opts.children) {
if (typeof ch === 'string') el.append(document.createTextNode(ch))
else el.append(ch)
}
}
return el
}
var style_default2 =
'/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */@layer properties;@layer theme, base, components, utilities;@layer theme{:host,:root{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(97.1% 0.013 17.38);--color-red-500:oklch(63.7% 0.237 25.331);--color-gray-50:oklch(98.5% 0.002 247.839);--color-gray-100:oklch(96.7% 0.003 264.542);--color-gray-300:oklch(87.2% 0.01 258.338);--color-gray-400:oklch(70.7% 0.022 261.325);--color-gray-500:oklch(55.1% 0.027 264.364);--color-gray-600:oklch(44.6% 0.03 256.802);--color-gray-700:oklch(37.3% 0.034 259.733);--color-gray-800:oklch(27.8% 0.033 256.848);--color-gray-900:oklch(21% 0.034 264.665);--color-white:#fff;--spacing:0.25rem;--font-weight-semibold:600;--font-weight-bold:700;--radius-md:0.375rem;--radius-xl:0.75rem;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,::backdrop,::file-selector-button,:after,:before{border:0 solid;box-sizing:border-box;margin:0;padding:0}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-size:1em;font-variation-settings:var(--default-mono-font-variation-settings,normal)}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}menu,ol,ul{list-style:none}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}::file-selector-button,button,input,optgroup,select,textarea{background-color:transparent;border-radius:0;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:currentcolor;@supports (color:color-mix(in lab,red,red)){color:color-mix(in oklab,currentcolor 50%,transparent)}}::placeholder{color:currentcolor;@supports (color:color-mix(in lab,red,red)){color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}::file-selector-button,button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer utilities{.container{width:100%;@media (width >= 40rem){max-width:40rem}@media (width >= 48rem){max-width:48rem}@media (width >= 64rem){max-width:64rem}@media (width >= 80rem){max-width:80rem}@media (width >= 96rem){max-width:96rem}}.grid{display:grid}}:host{all:initial}.user-settings{position:fixed;right:calc(var(--spacing)*3);top:calc(var(--spacing)*3);z-index:2147483647;--tw-ring-color:var(--user-color-ring,#111827)}.user-settings .panel{background-color:var(--color-gray-100);border-bottom-left-radius:var(--radius-xl);border-bottom-right-radius:var(--radius-xl);color:var(--color-gray-900);font-family:var(--font-sans);font-size:14px;max-height:90vh;overflow-y:auto;padding-inline:calc(var(--spacing)*4);padding-bottom:calc(var(--spacing)*4);padding-top:calc(var(--spacing)*0);width:420px;--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,rgba(0,0,0,.1)),0 8px 10px -6px var(--tw-shadow-color,rgba(0,0,0,.1));background:#f2f2f7;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);box-shadow:0 10px 39px 10px #3e424238!important;scrollbar-color:rgba(156,163,175,.25) transparent;scrollbar-width:thin}.user-settings .grid{display:flex;flex-direction:column;gap:calc(var(--spacing)*3)}.user-settings .row{align-items:center;display:flex;gap:calc(var(--spacing)*3);justify-content:space-between;padding-block:calc(var(--spacing)*3);padding-inline:calc(var(--spacing)*4)}.user-settings .group{background-color:var(--color-white);border-radius:var(--radius-xl);gap:calc(var(--spacing)*0);overflow:hidden}.user-settings .group .row{background-color:var(--color-white);border-radius:0;border-style:var(--tw-border-style);border-width:0;padding-block:calc(var(--spacing)*3);padding-inline:calc(var(--spacing)*4);position:relative}.user-settings .group .row:not(:last-child):after{background:#e5e7eb;bottom:0;content:"";height:1px;left:16px;position:absolute;right:0}.user-settings .header-row{align-items:center;border-radius:0;display:flex;justify-content:center;padding-inline:calc(var(--spacing)*0);padding-bottom:calc(var(--spacing)*3);padding-top:calc(var(--spacing)*0)}.user-settings .panel-stuck .header-row .panel-title{opacity:0;transform:translateY(-2px);transition:opacity .15s ease,transform .15s ease}.user-settings label{color:var(--color-gray-600)}.user-settings .label-wrap{display:flex;flex-direction:column;gap:calc(var(--spacing)*1);min-width:60px;text-align:left}.user-settings .btn{border-color:var(--color-gray-300);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;color:var(--color-gray-700);padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*3);white-space:nowrap;&:hover{@media (hover:hover){background-color:var(--color-gray-50)}}}.user-settings .btn-danger{border-color:var(--color-red-500);color:var(--color-red-500);&:hover{@media (hover:hover){background-color:var(--color-red-50)}}}.user-settings .btn-ghost{border-radius:var(--radius-md);color:var(--color-gray-500);padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*2);&:hover{@media (hover:hover){background-color:var(--color-gray-100)}}}.user-settings input[type=text]{border-color:transparent;border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;color:var(--color-gray-700);padding-block:calc(var(--spacing)*2);padding-inline:calc(var(--spacing)*3);text-align:right;width:180px;--tw-outline-style:none;outline-style:none}.user-settings input[type=text]:focus,.user-settings input[type=text]:hover{border-color:var(--color-gray-300)}.user-settings select{background-color:var(--color-white);border-color:transparent;border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;color:var(--color-gray-700);padding-block:calc(var(--spacing)*2);padding-inline:calc(var(--spacing)*3);text-align:right;width:180px;--tw-outline-style:none;outline-style:none}.user-settings select:focus,.user-settings select:hover{border-color:var(--color-gray-300)}.user-settings input[type=color]{border-color:var(--color-gray-300);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;height:calc(var(--spacing)*8);padding:calc(var(--spacing)*0);width:80px}.user-settings textarea{border-color:transparent;border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;color:var(--color-gray-700);padding-block:calc(var(--spacing)*2);padding-inline:calc(var(--spacing)*3);text-align:right;width:100%;--tw-outline-style:none;outline-style:none}.user-settings textarea:focus,.user-settings textarea:hover{border-color:var(--color-gray-300)}.user-settings .switch,.user-settings .toggle-wrap{align-items:center;display:flex;gap:calc(var(--spacing)*2)}.user-settings .toggle-checkbox{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#e5e5ea;border:1px solid #d1d1d6;border-radius:9999px;box-shadow:inset 0 1px 1px rgba(0,0,0,.1);cursor:pointer;display:inline-block;height:22px;position:relative;transition:background-color .2s ease,border-color .2s ease;width:42px}.user-settings .toggle-checkbox:before{background:#fff;border-radius:9999px;box-shadow:0 2px 4px rgba(0,0,0,.25);content:"";height:18px;left:2px;position:absolute;top:50%;transform:translateY(-50%);transition:transform .2s ease,background-color .2s ease,left .2s ease,right .2s ease;width:18px}.user-settings .toggle-checkbox:checked{background:var(--user-toggle-on-bg,#34c759);border-color:var(--user-toggle-on-bg,#34c759)}.user-settings .panel-title{font-size:20px;--tw-font-weight:var(--font-weight-bold);color:var(--color-gray-800);font-weight:var(--font-weight-bold)}.user-settings .outer-header{align-items:center;background-color:var(--color-gray-100);background:#f2f2f7;border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl);display:flex;font-family:var(--font-sans);height:calc(var(--spacing)*11);justify-content:center;position:relative}.user-settings .outer-header .outer-title{font-size:20px;opacity:0;transition:opacity .15s ease;--tw-font-weight:var(--font-weight-bold);color:var(--color-gray-800);font-weight:var(--font-weight-bold)}.user-settings .outer-header.stuck .outer-title{opacity:1}.user-settings .outer-header:after{background:#e5e7eb;bottom:0;content:"";height:1px;left:0;opacity:0;position:absolute;right:0;transition:opacity .15s ease}.user-settings .outer-header.stuck:after{opacity:1}.user-settings .group-title{font-size:13px;padding-inline:calc(var(--spacing)*1);--tw-font-weight:var(--font-weight-semibold);color:var(--color-gray-600);font-weight:var(--font-weight-semibold)}.user-settings .btn-ghost.icon{align-items:center;border-radius:calc(infinity*1px);color:var(--color-gray-500);cursor:pointer;display:flex;font-size:16px;height:calc(var(--spacing)*9);justify-content:center;transition:background-color .15s ease,color .15s ease;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:calc(var(--spacing)*9);&:hover{@media (hover:hover){background-color:var(--color-gray-100)}}&:hover{@media (hover:hover){color:var(--color-gray-700)}}}.user-settings .close-btn:hover{background-color:var(--color-gray-300);box-shadow:0 0 0 1px rgba(0,0,0,.05);color:var(--color-gray-900);font-size:19px;transform:translateY(-50%)}.user-settings .close-btn{position:absolute;right:12px;top:50%;transform:translateY(-50%);transition:transform .15s ease,background-color .15s ease,color .15s ease,font-size .15s ease}.user-settings .toggle-checkbox:checked:before{background:#fff;left:auto;right:2px;transform:translateY(-50%)}.user-settings .color-row{align-items:center;display:flex;gap:calc(var(--spacing)*1.5)}.user-settings .color-swatch{border-radius:var(--radius-md);cursor:pointer;height:calc(var(--spacing)*6);width:calc(var(--spacing)*6)}.user-settings .color-swatch.active{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-color:var(--user-color-ring,#111827)}.user-settings .seg{align-items:center;display:flex;flex-wrap:wrap;gap:calc(var(--spacing)*2)}.user-settings .seg-btn{border-color:var(--color-gray-300);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;color:var(--color-gray-700);cursor:pointer;padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*3);-webkit-user-select:none;-moz-user-select:none;user-select:none;&:hover{@media (hover:hover){background-color:var(--color-gray-50)}}}.user-settings .seg-btn.active{background:var(--user-active-bg,#111827);border-color:var(--user-active-bg,#111827);color:var(--user-active-fg,#fff)}.user-settings .value-wrap{align-items:flex-end;display:flex;flex-direction:column;gap:calc(var(--spacing)*1);text-align:right}.user-settings .tabs{align-items:center;display:flex;gap:calc(var(--spacing)*2);margin-bottom:calc(var(--spacing)*2)}.user-settings .tab-btn{border-color:var(--color-gray-300);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;color:var(--color-gray-700);cursor:pointer;padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*3);-webkit-user-select:none;-moz-user-select:none;user-select:none;&:hover{@media (hover:hover){background-color:var(--color-gray-50)}}}.user-settings .tab-btn.active{background:var(--user-active-bg,#111827);border-color:var(--user-active-bg,#111827);color:var(--user-active-fg,#fff)}.user-settings .field-help{color:var(--color-gray-400);font-size:11px}@media (prefers-color-scheme:dark){.user-settings .panel{background-color:var(--color-gray-800);border-bottom-left-radius:var(--radius-xl);border-bottom-right-radius:var(--radius-xl);box-shadow:0 10px 39px 10px #00000040!important;color:var(--color-gray-100)}.user-settings .row{background-color:transparent;border-style:var(--tw-border-style);border-width:0}.user-settings .header-row{background-color:var(--color-gray-800);border-color:var(--color-gray-700)}.user-settings .outer-header{background-color:var(--color-gray-800);border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl)}.user-settings .outer-header:after{background:#4b5563}.user-settings .footer a.issue-link{color:var(--color-gray-300);&:hover{@media (hover:hover){color:var(--color-gray-100)}}}.user-settings .footer .brand{color:var(--color-gray-400)}.user-settings label{color:var(--color-gray-300)}.user-settings .field-help{color:var(--color-gray-400)}.user-settings .group{background-color:var(--color-gray-700)}.user-settings .group .row:not(:last-child):after{background:#4b5563}}.user-settings .panel::-webkit-scrollbar{width:4px}.user-settings .panel::-webkit-scrollbar-track{background:transparent}.user-settings .panel::-webkit-scrollbar-thumb{background:rgba(156,163,175,.25);border-radius:9999px;opacity:.25}.user-settings .footer{align-items:center;color:var(--color-gray-500);display:flex;flex-direction:column;font-size:12px;gap:calc(var(--spacing)*1);padding-bottom:calc(var(--spacing)*3);padding-top:calc(var(--spacing)*6)}.user-settings .footer a.issue-link{color:var(--color-gray-600);cursor:pointer;text-decoration-line:underline;text-underline-offset:2px;-webkit-user-select:none;-moz-user-select:none;user-select:none;&:hover{@media (hover:hover){color:var(--color-gray-800)}}}.user-settings .footer .brand{color:var(--color-gray-500);cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;&:hover{@media (hover:hover){color:var(--color-gray-700)}}}.user-settings button{-webkit-user-select:none;-moz-user-select:none;user-select:none}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@layer properties{*,::backdrop,:after,:before{--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-border-style:solid;--tw-font-weight:initial}}'
function isObject(item) {
return Boolean(item) && typeof item === 'object'
}
var currentHost
function onKeyDown(e) {
if (e.key === 'Escape') {
closeSettingsPanel()
}
}
function closeSettingsPanel() {
try {
currentHost == null ? void 0 : currentHost.remove()
} catch (e) {}
try {
globalThis.removeEventListener('keydown', onKeyDown, true)
} catch (e) {}
currentHost = void 0
}
function createFieldRow(opts, content) {
const row = c('div', { className: 'row' })
const labWrap = c('div', { className: 'label-wrap' })
const lab = c('label', { text: opts.label })
labWrap.append(lab)
if (opts.help) {
labWrap.append(c('div', { className: 'field-help', text: opts.help }))
}
const val = c('div', { className: 'value-wrap' })
if (Array.isArray(content)) {
val.append(...content)
} else {
val.append(content)
}
row.append(labWrap)
row.append(val)
return row
}
function createToggleRow(opts) {
const seg = c('div', { className: 'toggle-wrap' })
const chk = c('input', {
type: 'checkbox',
className: 'toggle-checkbox',
dataset: { key: opts.key, isSitePref: opts.isSitePref ? '1' : '' },
})
seg.append(chk)
const row = createFieldRow(opts, seg)
return { row, chk }
}
function createInputRow(opts) {
const inp = c('input', {
type: 'text',
placeholder: opts.placeholder || '',
dataset: { key: opts.key, isSitePref: opts.isSitePref ? '1' : '' },
})
const row = createFieldRow(opts, inp)
return { row, inp }
}
function createTextareaRow(opts) {
const ta = c('textarea', {
rows: opts.rows || 4,
dataset: { key: opts.key, isSitePref: opts.isSitePref ? '1' : '' },
})
const row = createFieldRow(opts, ta)
return { row, ta }
}
function createRadioRow(opts) {
const seg = c('div', { className: 'seg' })
for (const o of opts.options) {
const b = c('button', {
className: 'seg-btn',
dataset: {
key: opts.key,
value: o.value,
isSitePref: opts.isSitePref ? '1' : '',
},
text: o.label,
})
seg.append(b)
}
const row = createFieldRow(opts, seg)
return { row, seg }
}
function createColorRow(opts) {
const seg = c('div', { className: 'color-row' })
for (const o of opts.options) {
const b = c('button', {
className: 'color-swatch',
dataset: {
key: opts.key,
value: o.value,
isSitePref: opts.isSitePref ? '1' : '',
},
style: { backgroundColor: o.value },
})
seg.append(b)
}
const row = createFieldRow(opts, seg)
return { row, seg }
}
function createSelectRow(opts) {
const sel = c('select', {
dataset: { key: opts.key, isSitePref: opts.isSitePref ? '1' : '' },
})
for (const o of opts.options) {
const opt = c('option', { value: o.value, text: o.label })
sel.append(opt)
}
const row = createFieldRow(opts, sel)
return { row, sel }
}
function createActionRow(opts) {
const act = c('div', { className: 'seg' })
for (const a of opts.actions) {
const b = c('button', {
className: 'btn action-btn'.concat(
a.kind === 'danger' ? ' btn-danger' : ''
),
dataset: { key: opts.key, action: a.id },
text: a.text,
})
act.append(b)
}
const row = createFieldRow(opts, act)
return { row }
}
function openSettingsPanel(schema, store2, options) {
const { host, root, existed } = ensureHostAndRoot(options)
currentHost = host
if (existed) return
let lastValues = { global: {}, site: {} }
const styleTag = c('style', {
text: style_default2.concat(
(options == null ? void 0 : options.styleText) || ''
),
})
root.append(styleTag)
const wrap = c('div', { className: 'user-settings' })
applyThemeStyles(wrap, options == null ? void 0 : options.theme)
const panel = c('div', { className: 'panel' })
const grid = c('div', { className: 'grid' })
const { row: headerRow } = buildHeader(schema.title)
grid.append(headerRow)
const fillers = {}
const addFiller = (key, fn) => {
if (!fillers[key]) fillers[key] = []
fillers[key].push(fn)
}
function appendAndFill(container, row, key, filler) {
container.append(row)
addFiller(key, filler)
}
function appendField(container, f) {
switch (f.type) {
case 'toggle': {
const { row, chk } = createToggleRow({
label: f.label,
key: f.key,
help: f.help,
isSitePref: f.isSitePref,
})
appendAndFill(container, row, f.key, () => {
fillToggleUI(chk, f.key)
})
break
}
case 'input': {
const { row, inp } = createInputRow({
label: f.label,
key: f.key,
placeholder: f.placeholder,
help: f.help,
isSitePref: f.isSitePref,
})
appendAndFill(container, row, f.key, () => {
fillInput(inp, f.key)
})
break
}
case 'textarea': {
const { row, ta } = createTextareaRow({
label: f.label,
key: f.key,
rows: f.rows,
help: f.help,
isSitePref: f.isSitePref,
})
appendAndFill(container, row, f.key, () => {
fillTextarea(ta, f.key)
})
break
}
case 'radio': {
const { row, seg } = createRadioRow({
label: f.label,
key: f.key,
options: f.options,
help: f.help,
isSitePref: f.isSitePref,
})
appendAndFill(container, row, f.key, () => {
fillRadioUI(seg, f.key)
})
break
}
case 'select': {
const { row, sel } = createSelectRow({
label: f.label,
key: f.key,
options: f.options,
help: f.help,
isSitePref: f.isSitePref,
})
appendAndFill(container, row, f.key, () => {
fillSelect(sel, f.key)
})
break
}
case 'colors': {
const { row, seg } = createColorRow({
label: f.label,
key: f.key,
options: f.options,
help: f.help,
isSitePref: f.isSitePref,
})
appendAndFill(container, row, f.key, () => {
fillColorUI(seg, f.key)
})
break
}
case 'action': {
const { row } = createActionRow({
label: f.label,
key: f.key,
actions: f.actions,
help: f.help,
})
container.append(row)
break
}
}
}
function sanitizeDatasetKey(rawKey) {
let out = ''
for (const ch of rawKey) {
const code = ch.codePointAt(0) || 0
out += code >= 65 && code <= 90 ? '-' + ch.toLowerCase() : ch
}
return out
}
function ensureHostAndRoot(options2) {
const keySan = sanitizeDatasetKey(
(options2 == null ? void 0 : options2.hostDatasetKey) || 'usrHost'
)
const sel = '[data-'
.concat(keySan, '="')
.concat(
(options2 == null ? void 0 : options2.hostDatasetValue) || 'settings',
'"]'
)
const existing = document.querySelector(sel)
let root2
let hostEl
if (existing instanceof HTMLDivElement && existing.shadowRoot) {
hostEl = existing
root2 = existing.shadowRoot
try {
document.documentElement.append(hostEl)
} catch (e) {}
return { host: hostEl, root: root2, existed: true }
}
const key =
(options2 == null ? void 0 : options2.hostDatasetKey) || 'userHost'
const val =
(options2 == null ? void 0 : options2.hostDatasetValue) || 'settings'
hostEl = c('div', { dataset: { [key]: val } })
root2 = hostEl.attachShadow({ mode: 'open' })
document.documentElement.append(hostEl)
return { host: hostEl, root: root2, existed: false }
}
function applyThemeStyles(wrap2, theme) {
if (!theme) return
const properties = []
if (theme.activeBg)
properties.push('--user-active-bg: '.concat(theme.activeBg, ';'))
if (theme.activeFg)
properties.push('--user-active-fg: '.concat(theme.activeFg, ';'))
if (theme.colorRing)
properties.push('--user-color-ring: '.concat(theme.colorRing, ';'))
if (theme.toggleOnBg)
properties.push('--user-toggle-on-bg: '.concat(theme.toggleOnBg, ';'))
const accent = theme.activeBg || theme.colorRing
if (accent) properties.push('--user-accent: '.concat(accent, ';'))
if (properties.length > 0) wrap2.style.cssText = properties.join(' ')
}
function buildHeader(title) {
const row = c('div', { className: 'row header-row' })
const titleEl = c('label', { className: 'panel-title', text: title })
row.append(titleEl)
return { row }
}
function renderSimplePanel(container, data) {
if (data.groups && Array.isArray(data.groups)) {
renderGroupsPanel(container, data.groups)
return
}
const fields = data.fields || []
const body = c('div', { className: 'grid group' })
container.append(body)
for (const f of fields) appendField(body, f)
}
function renderTabsPanel(container, tabs) {
var _a
const tabsWrap = c('div', { className: 'tabs' })
const panels = {}
let active = ((_a = tabs[0]) == null ? void 0 : _a.id) || ''
for (const t of tabs) {
const b = c('button', {
className: 'tab-btn',
dataset: { tabId: t.id },
text: t.title,
})
tabsWrap.append(b)
const p = c('div', { className: 'grid' })
panels[t.id] = p
if (t.id !== active) p.style.display = 'none'
if ('groups' in t && Array.isArray(t.groups)) {
renderGroupsPanel(p, t.groups)
} else if ('fields' in t && Array.isArray(t.fields)) {
p.className = 'grid group'
for (const f of t.fields) appendField(p, f)
}
}
container.append(tabsWrap)
for (const id of Object.keys(panels)) container.append(panels[id])
function updateTabsUI() {
for (const b of Array.from(tabsWrap.querySelectorAll('.tab-btn'))) {
const id = b.dataset.tabId || ''
if (id === active) b.classList.add('active')
else b.classList.remove('active')
}
for (const id of Object.keys(panels)) {
panels[id].style.display = id === active ? '' : 'none'
}
}
function onTabsClick(e) {
const t = e.target
const b = t.closest('.tab-btn')
if (b && b instanceof HTMLElement) {
active = b.dataset.tabId || ''
updateTabsUI()
}
}
tabsWrap.addEventListener('click', onTabsClick)
updateTabsUI()
}
function renderGroupsPanel(container, groups) {
for (const g of groups) {
const body = c('div', { className: 'grid group' })
if (g.title) {
const header = c('h2', { className: 'group-title', text: g.title })
container.append(header)
}
container.append(body)
for (const f of g.fields) appendField(body, f)
}
}
const refreshAll = async () => {
try {
const g = await store2.getAll(true)
const s = await store2.getAll(false)
lastValues = { global: g, site: s }
} catch (e) {}
for (const k of Object.keys(fillers)) {
for (const fn of fillers[k]) {
try {
fn()
} catch (e) {}
}
}
}
function wireStoreChange(store3, fillers2) {
var _a
try {
;(_a = store3.onChange) == null
? void 0
: _a.call(store3, (e) => {
if (e.key === '*' || !fillers2[e.key]) {
void refreshAll()
return
}
for (const fn of fillers2[e.key]) {
try {
fn()
} catch (e2) {}
}
})
} catch (e) {}
}
function getFieldValue(key, el) {
const isSitePref = Boolean(el.dataset.isSitePref)
const values = isSitePref ? lastValues.site : lastValues.global
return values[key]
}
function getFieldInfo(el) {
const key = el.dataset.key
if (!key) return null
const isSitePref = Boolean(el.dataset.isSitePref)
return { key, isSitePref }
}
function fillRadioUI(seg, key) {
try {
const btn = seg.querySelector('.seg-btn')
if (!btn) return
const v = getFieldValue(key, btn)
for (const b of Array.from(seg.querySelectorAll('.seg-btn'))) {
const val = b.dataset.value || ''
if (val === String(v)) b.classList.add('active')
else b.classList.remove('active')
}
} catch (e) {}
}
function fillColorUI(seg, key) {
try {
const btn = seg.querySelector('.color-swatch')
if (!btn) return
const v = getFieldValue(key, btn)
for (const b of Array.from(seg.querySelectorAll('.color-swatch'))) {
const val = b.dataset.value || ''
if (val.toLowerCase() === String(v || '').toLowerCase())
b.classList.add('active')
else b.classList.remove('active')
}
} catch (e) {}
}
function fillToggleUI(onBtn, key) {
try {
if (onBtn instanceof HTMLInputElement && onBtn.type === 'checkbox') {
const v = getFieldValue(key, onBtn)
onBtn.checked = Boolean(v)
}
} catch (e) {}
}
function fillInput(inp, key) {
try {
const v = getFieldValue(key, inp)
inp.value = String(v != null ? v : '')
} catch (e) {}
}
function fillTextarea(ta, key) {
try {
const v = getFieldValue(key, ta)
ta.value = String(v != null ? v : '')
} catch (e) {}
}
function fillSelect(sel, key) {
try {
const v = getFieldValue(key, sel)
for (const o of Array.from(sel.querySelectorAll('option'))) {
o.selected = o.value === String(v)
}
} catch (e) {}
}
async function handleSegButton(rb) {
const info = getFieldInfo(rb)
if (!info) return
const val = rb.dataset.value || ''
try {
await store2.set(info.key, val, !info.isSitePref)
} catch (e) {}
}
async function handleColorSwatch(cs) {
const info = getFieldInfo(cs)
if (!info) return
const val = cs.dataset.value || ''
try {
await store2.set(info.key, val, !info.isSitePref)
} catch (e) {}
}
function handleActionBtn(ab) {
var _a
const key = ab.dataset.key || ''
const actionId = ab.dataset.action || ''
try {
;(_a = options == null ? void 0 : options.onAction) == null
? void 0
: _a.call(options, { key, actionId, target: ab })
} catch (e) {}
}
function onPanelClick(e) {
const t = e.target
if (t === topCloseBtn) {
closeSettingsPanel()
return
}
const rb = t.closest('.seg-btn')
if (rb && rb instanceof HTMLElement) {
void handleSegButton(rb)
return
}
const cs = t.closest('.color-swatch')
if (cs && cs instanceof HTMLElement) {
void handleColorSwatch(cs)
return
}
const ab = t.closest('.action-btn')
if (ab && ab instanceof HTMLElement) handleActionBtn(ab)
}
function handleInputChange(inp) {
const info = getFieldInfo(inp)
if (!info) return
const isCheckbox = (inp.type || '').toLowerCase() === 'checkbox'
const v = isCheckbox ? Boolean(inp.checked) : inp.value
void store2.set(info.key, v, !info.isSitePref)
}
function handleTextareaChange(ta) {
const info = getFieldInfo(ta)
if (!info) return
void store2.set(info.key, ta.value, !info.isSitePref)
}
function handleSelectChange(sel) {
const info = getFieldInfo(sel)
if (!info) return
void store2.set(info.key, sel.value, !info.isSitePref)
}
function onPanelChange(e) {
const t = e.target
const inp = t.closest('input')
if (inp && inp instanceof HTMLInputElement) {
handleInputChange(inp)
return
}
const ta = t.closest('textarea')
if (ta && ta instanceof HTMLTextAreaElement) {
handleTextareaChange(ta)
return
}
const sel = t.closest('select')
if (sel && sel instanceof HTMLSelectElement) {
handleSelectChange(sel)
}
}
switch (schema.type) {
case 'simple': {
renderSimplePanel(grid, schema)
break
}
case 'tabs': {
renderTabsPanel(grid, schema.tabs)
break
}
}
panel.addEventListener('click', onPanelClick)
panel.addEventListener('change', onPanelChange)
const outerHeader = c('div', { className: 'outer-header' })
const outerTitle = c('label', {
className: 'outer-title',
text: schema.title,
})
const topCloseBtn = c('button', {
className: 'btn-ghost icon close-btn',
text: '\xD7',
attrs: { 'aria-label': '\u5173\u95ED' },
})
outerHeader.append(outerTitle)
outerHeader.append(topCloseBtn)
try {
outerHeader.addEventListener('click', (e) => {
const t = e.target
if (t === topCloseBtn) {
closeSettingsPanel()
}
})
} catch (e) {}
panel.append(grid)
const footer = c('div', { className: 'footer' })
const issueLink = c('a', {
className: 'issue-link',
text: 'Report an Issue\u2026',
attrs: {
href:
(options == null ? void 0 : options.issuesUrl) ||
'https://github.com/utags/userscripts/issues',
target: '_blank',
rel: 'noopener noreferrer',
},
})
const brand = c('a', {
className: 'brand',
text: 'Made with \u2764\uFE0F by Pipecraft',
attrs: {
href: 'https://www.pipecraft.net/',
target: '_blank',
rel: 'noopener noreferrer',
},
})
footer.append(issueLink)
footer.append(brand)
panel.append(footer)
const stickyThreshold = 22
let stickyTimer
const stickyDebounceMs = 80
function updateHeaderStickyCore() {
try {
const sc = panel.scrollTop || 0
const stuck = sc > stickyThreshold
if (stuck) {
panel.classList.add('panel-stuck')
outerHeader.classList.add('stuck')
} else {
panel.classList.remove('panel-stuck')
outerHeader.classList.remove('stuck')
}
} catch (e) {}
}
function updateHeaderSticky() {
try {
if (stickyTimer !== void 0) globalThis.clearTimeout(stickyTimer)
stickyTimer = globalThis.setTimeout(
updateHeaderStickyCore,
stickyDebounceMs
)
} catch (e) {}
}
try {
panel.addEventListener('scroll', updateHeaderSticky)
updateHeaderStickyCore()
} catch (e) {}
wrap.append(outerHeader)
wrap.append(panel)
root.append(wrap)
wireStoreChange(store2, fillers)
void refreshAll()
globalThis.addEventListener('keydown', onKeyDown, true)
}
function createSettingsStore(
storageKey,
defaults,
isSupportSitePref = false
) {
const rootKey = storageKey || 'settings'
let cache
let globalCache
let siteCache
let initPromise
const changeCbs = []
let listenerRegistered = false
const getHostname = () => {
var _a
return (
((_a = globalThis.location) == null ? void 0 : _a.hostname) || 'unknown'
)
}
function updateCache(obj) {
if (isSupportSitePref) {
const rootObj = isObject(obj) ? obj : {}
const globalData = rootObj.global
globalCache = __spreadValues({}, defaults)
if (isObject(globalData)) {
Object.assign(globalCache, globalData)
}
const hostname = getHostname()
const siteData = rootObj[hostname]
siteCache = __spreadValues({}, globalCache)
if (isObject(siteData)) {
Object.assign(siteCache, siteData)
}
cache = siteCache
} else {
cache = __spreadValues({}, defaults)
if (isObject(obj)) Object.assign(cache, obj)
}
}
function registerValueChangeListener() {
if (listenerRegistered) return
try {
void addValueChangeListener(rootKey, (n, ov, nv, remote) => {
try {
updateCache(nv)
for (const f of changeCbs) {
f({ key: '*', oldValue: ov, newValue: nv, remote })
}
} catch (e) {}
})
listenerRegistered = true
} catch (e) {}
}
registerValueChangeListener()
async function ensure() {
if (cache) return cache
if (initPromise) return initPromise
initPromise = (async () => {
let obj
try {
obj = await getValue(rootKey, void 0)
} catch (e) {}
updateCache(obj)
initPromise = void 0
return cache
})()
return initPromise
}
return {
async get(key, isGlobalPref) {
await ensure()
if (isSupportSitePref) {
if (isGlobalPref) return globalCache[key]
return siteCache[key]
}
return cache[key]
},
async getAll(isGlobalPref) {
await ensure()
if (isSupportSitePref) {
if (isGlobalPref) return __spreadValues({}, globalCache)
return __spreadValues({}, siteCache)
}
return __spreadValues({}, cache)
},
async set(...args) {
let obj
try {
obj = await getValue(rootKey, {})
} catch (e) {}
if (!isObject(obj)) obj = {}
let isGlobalPref = false
let key
let value
let values
if (typeof args[0] === 'string') {
key = args[0]
value = args[1]
isGlobalPref = Boolean(args[2])
} else {
values = args[0]
isGlobalPref = Boolean(args[1])
}
let target
let global
if (isSupportSitePref) {
const hostname = isGlobalPref ? 'global' : getHostname()
if (!isObject(obj[hostname])) obj[hostname] = {}
target = obj[hostname]
global = isObject(obj.global) ? obj.global : {}
} else {
target = obj
}
const isSitePref = isSupportSitePref && !isGlobalPref
const apply = (key2, value2) => {
if (isSitePref && key2 in global) {
const normalized = normalizeToDefaultType(value2, defaults[key2])
target[key2] = normalized
return
}
setOrDelete(target, key2, value2, defaults[key2])
}
if (key !== void 0) {
apply(key, value)
} else if (values) {
for (const k of Object.keys(values)) {
const v = values[k]
apply(k, v)
}
}
if (isSupportSitePref && target && Object.keys(target).length === 0) {
const hostname = isGlobalPref ? 'global' : getHostname()
delete obj[hostname]
}
updateCache(obj)
try {
await setValue(rootKey, obj)
} catch (e) {}
},
async reset(isGlobalPref) {
let obj
if (isSupportSitePref) {
try {
obj = await getValue(rootKey, {})
} catch (e) {}
if (!isObject(obj)) obj = {}
const hostname = isGlobalPref ? 'global' : getHostname()
delete obj[hostname]
} else {
obj = {}
}
updateCache(obj)
try {
await setValue(rootKey, obj)
} catch (e) {}
},
defaults() {
return __spreadValues({}, defaults)
},
onChange(cb) {
changeCbs.push(cb)
},
}
}
function importJson(options) {
const {
validate,
onSuccess,
confirmMessage = '\u5BFC\u5165\u4F1A\u4E0E\u73B0\u6709\u6570\u636E\u5408\u5E76\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F',
errorMessage = '\u5BFC\u5165\u7684\u6570\u636E\u683C\u5F0F\u4E0D\u6B63\u786E',
} = options
const ok = globalThis.confirm(confirmMessage)
if (!ok) return
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = 'application/json'
fileInput.style.display = 'none'
const onChange = async () => {
var _a
try {
const f = (_a = fileInput.files) == null ? void 0 : _a[0]
if (!f) return
const txt = await f.text()
let obj
try {
obj = JSON.parse(txt)
} catch (e) {
alert('\u65E0\u6CD5\u89E3\u6790 JSON \u6587\u4EF6')
return
}
if (validate && !validate(obj)) {
alert(errorMessage)
return
}
await onSuccess(obj)
alert('\u5BFC\u5165\u5B8C\u6210')
} catch (error) {
console.error(error)
alert('\u5BFC\u5165\u5931\u8D25')
} finally {
fileInput.removeEventListener('change', onChange)
fileInput.remove()
}
}
fileInput.addEventListener('change', onChange)
document.documentElement.append(fileInput)
fileInput.click()
}
var SETTINGS_KEY = 'settings'
var CONFIG_KEY = 'ushortcuts'
var POSITION_OPTIONS = [
'right-top',
'right-center',
'right-bottom',
'left-top',
'left-center',
'left-bottom',
'top-left',
'top-center',
'top-right',
'bottom-left',
'bottom-center',
'bottom-right',
]
var DEFAULTS = {
hotkey: 'Alt+Shift+K',
syncUrl: '',
position: 'right-top',
defaultOpen: 'same-tab',
theme: 'system',
pinned: false,
enabled: true,
layoutMode: 'floating',
sidebarSide: 'right',
edgeWidth: 3,
edgeHeight: 60,
edgeOpacity: 0.6,
edgeColorLight: '#1A73E8',
edgeColorDark: '#8AB4F8',
edgeHidden: false,
}
var COMMON_SETTINGS_FIELDS = [
{ type: 'toggle', key: 'enabled', label: '\u542F\u7528' },
{
type: 'input',
key: 'hotkey',
label: '\u5FEB\u6377\u952E',
placeholder: DEFAULTS.hotkey,
help: '\u6253\u5F00\u9762\u677F\u7684\u5FEB\u6377\u952E',
},
{
type: 'radio',
key: 'defaultOpen',
label: '\u9ED8\u8BA4\u6253\u5F00\u65B9\u5F0F',
options: [
{ value: 'same-tab', label: '\u540C\u6807\u7B7E' },
{ value: 'new-tab', label: '\u65B0\u6807\u7B7E' },
],
help: '\u9009\u62E9\u70B9\u51FB\u94FE\u63A5\u65F6\u7684\u9ED8\u8BA4\u6253\u5F00\u884C\u4E3A',
},
{
type: 'radio',
key: 'theme',
label: '\u4E3B\u9898',
options: [
{ value: 'system', label: '\u7CFB\u7EDF' },
{ value: 'light', label: '\u6D45\u8272' },
{ value: 'dark', label: '\u6DF1\u8272' },
],
help: '\u7AD9\u70B9\u7EA7\u4E3B\u9898\u504F\u597D',
},
]
var EDGE_SETTINGS_FIELDS = [
{
type: 'radio',
key: 'layoutMode',
label: '\u663E\u793A\u6A21\u5F0F',
options: [
{ value: 'floating', label: '\u60AC\u6D6E' },
{ value: 'sidebar', label: '\u4FA7\u8FB9\u680F' },
],
},
{ type: 'toggle', key: 'pinned', label: '\u56FA\u5B9A\u9762\u677F' },
{
type: 'radio',
key: 'sidebarSide',
label: '\u4FA7\u8FB9\u680F\u4F4D\u7F6E',
options: [
{ value: 'left', label: '\u5DE6\u4FA7' },
{ value: 'right', label: '\u53F3\u4FA7' },
],
},
{
type: 'select',
key: 'position',
label: '\u4F4D\u7F6E',
options: POSITION_OPTIONS.map((p) => ({ value: p, label: p })),
help: '\u63A7\u5236\u60AC\u505C\u7AD6\u7EBF\u63D0\u793A\u7684\u4F4D\u7F6E',
},
{
type: 'input',
key: 'edgeWidth',
label: '\u7AD6\u7EBF\u5BBD\u5EA6',
help: '\u5355\u4F4D\u50CF\u7D20\uFF0C\u5EFA\u8BAE 2-4',
},
{
type: 'input',
key: 'edgeHeight',
label: '\u7AD6\u7EBF\u9AD8\u5EA6',
help: '\u5355\u4F4D\u50CF\u7D20\uFF0C\u5EFA\u8BAE 40-80',
},
{
type: 'input',
key: 'edgeOpacity',
label: '\u4E0D\u900F\u660E\u5EA6',
help: '0-1 \u4E4B\u95F4\u7684\u5C0F\u6570',
},
{
type: 'colors',
key: 'edgeColorLight',
label: '\u6D45\u8272\u4E3B\u9898\u989C\u8272',
options: [
{ value: '#1A73E8' },
{ value: '#2563EB' },
{ value: '#3B82F6' },
{ value: '#10B981' },
{ value: '#F59E0B' },
{ value: '#EF4444' },
{ value: '#6B7280' },
],
help: '\u7528\u4E8E\u6D45\u8272\u4E3B\u9898\u7684\u7AD6\u7EBF\u989C\u8272',
},
{
type: 'colors',
key: 'edgeColorDark',
label: '\u6DF1\u8272\u4E3B\u9898\u989C\u8272',
options: [
{ value: '#8AB4F8' },
{ value: '#60A5FA' },
{ value: '#93C5FD' },
{ value: '#22C55E' },
{ value: '#F59E0B' },
{ value: '#EF4444' },
{ value: '#9CA3AF' },
],
help: '\u7528\u4E8E\u6DF1\u8272\u4E3B\u9898\u7684\u7AD6\u7EBF\u989C\u8272',
},
{ type: 'toggle', key: 'edgeHidden', label: '\u9690\u85CF\u7AD6\u7EBF' },
{
type: 'action',
key: 'edge-reset',
label: '\u7AD6\u7EBF\u8BBE\u7F6E',
actions: [{ id: 'edgeReset', text: '\u91CD\u7F6E\u9ED8\u8BA4' }],
help: '\u6062\u590D\u7AD6\u7EBF\u5BBD\u5EA6/\u9AD8\u5EA6/\u4E0D\u900F\u660E\u5EA6\u4E0E\u989C\u8272\u4E3A\u9ED8\u8BA4\u503C',
},
]
function createUshortcutsSettingsStore() {
return createSettingsStore(SETTINGS_KEY, DEFAULTS, true)
}
function openSettingsPanel2(store2) {
const schema = {
type: 'tabs',
title: '\u5FEB\u901F\u5BFC\u822A\u8BBE\u7F6E',
tabs: [
{
id: 'global',
title: '\u5168\u5C40\u8BBE\u7F6E',
groups: [
{
id: 'global-basic',
title: '',
fields: COMMON_SETTINGS_FIELDS,
},
{
id: 'global-edge',
title: '\u9762\u677F\u4E0E\u7AD6\u7EBF',
fields: EDGE_SETTINGS_FIELDS,
},
{
id: 'global-reset',
title: '',
fields: [
{
type: 'action',
key: 'global-reset',
label: '\u91CD\u7F6E',
actions: [
{
id: 'resetGlobal',
text: '\u91CD\u7F6E\u5168\u5C40\u8BBE\u7F6E',
},
],
help: '\u6062\u590D\u5168\u5C40\u8BBE\u7F6E\u4E3A\u9ED8\u8BA4\u503C',
},
],
},
],
},
{
id: 'site',
title: '\u7AD9\u70B9\u8BBE\u7F6E',
groups: [
{
id: 'site-basic',
title: '',
fields: COMMON_SETTINGS_FIELDS.map((f) =>
__spreadProps(__spreadValues({}, f), {
isSitePref: true,
})
),
},
{
id: 'site-edge',
title: '\u9762\u677F\u4E0E\u7AD6\u7EBF',
fields: EDGE_SETTINGS_FIELDS.map((f) =>
__spreadProps(__spreadValues({}, f), {
isSitePref: true,
})
),
},
{
id: 'site-reset',
title: '',
fields: [
{
type: 'action',
key: 'site-reset',
label: '\u91CD\u7F6E',
actions: [
{
id: 'resetSite',
text: '\u91CD\u7F6E\u7AD9\u70B9\u8BBE\u7F6E',
},
],
help: '\u6062\u590D\u5F53\u524D\u7AD9\u70B9\u8BBE\u7F6E\u4E3A\u9ED8\u8BA4\u503C',
},
],
},
],
},
{
id: 'actions',
title: '\u6570\u636E\u7BA1\u7406',
groups: [
{
id: 'data-group-manager',
title: '\u5206\u7EC4\u4E0E\u5BFC\u822A\u9879',
fields: [
{
type: 'action',
key: 'group-management',
label: '\u5206\u7EC4\u7BA1\u7406',
actions: [
{
id: 'openGroupManager',
text: '\u6253\u5F00\u5206\u7EC4\u7BA1\u7406',
},
],
help: '\u7BA1\u7406\u5BFC\u822A\u5206\u7EC4\u4E0E\u5BFC\u822A\u9879',
},
{
type: 'action',
key: 'export-import',
label: '\u6570\u636E\u5BFC\u51FA',
actions: [
{
id: 'exportShortcutsDataJson',
text: '\u5BFC\u51FA JSON \u6587\u4EF6',
},
],
help: '\u5BFC\u51FA\u6240\u6709\u914D\u7F6E\uFF08\u5305\u542B\u5404\u5206\u7EC4\u3001\u5BFC\u822A\u9879\u8BBE\u7F6E\uFF09',
},
{
type: 'action',
key: 'export-import',
label: '\u6570\u636E\u5BFC\u5165',
actions: [
{
id: 'importShortcutsDataJson',
text: '\u4ECE JSON \u6587\u4EF6\u5BFC\u5165',
},
],
help: '\u5BFC\u5165\u4E4B\u524D\u5BFC\u51FA\u7684\u6587\u4EF6',
},
{
type: 'action',
key: 'clear-data',
label: '\u6E05\u7A7A\u6240\u6709\u6570\u636E',
actions: [
{
id: 'clearShortcutsData',
text: '\u6267\u884C',
kind: 'danger',
},
],
},
],
},
{
id: 'data-settings',
title: '\u8BBE\u7F6E',
fields: [
{
type: 'action',
key: 'export-import',
label: '\u6570\u636E\u5BFC\u51FA',
actions: [
{
id: 'exportSettingsJson',
text: '\u5BFC\u51FA JSON \u6587\u4EF6',
},
],
help: '\u5BFC\u51FA\u6240\u6709\u8BBE\u7F6E',
},
{
type: 'action',
key: 'export-import',
label: '\u6570\u636E\u5BFC\u5165',
actions: [
{
id: 'importSettingsJson',
text: '\u4ECE JSON \u6587\u4EF6\u5BFC\u5165',
},
],
help: '\u5BFC\u5165\u4E4B\u524D\u5BFC\u51FA\u7684\u6587\u4EF6',
},
],
},
],
},
],
}
openSettingsPanel(schema, store2, {
hostDatasetKey: 'ushortcutsHost',
hostDatasetValue: 'ushortcuts-settings',
theme: {
activeBg: '#111827',
activeFg: '#ffffff',
colorRing: '#111827',
toggleOnBg: '#111827',
},
onAction({ actionId }) {
switch (actionId) {
case 'openGroupManager': {
;(async () => {
try {
const existing = document.querySelector(
'[data-ushortcuts-host="utags-shortcuts"]'
)
const root =
existing instanceof HTMLElement && existing.shadowRoot
? existing.shadowRoot
: (() => {
const host = document.createElement('div')
host.dataset.ushortcutsHost = 'utags-shortcuts'
const r = host.attachShadow({ mode: 'open' })
const style = document.createElement('style')
style.textContent = style_default
r.append(style)
document.documentElement.append(host)
return r
})()
let raw = {}
try {
const s = await getValue(CONFIG_KEY, '')
raw = s ? JSON.parse(String(s) || '{}') || {} : {}
} catch (e) {}
if (!Array.isArray(raw.groups) || raw.groups.length === 0) {
const g = {
id: uid(),
name: '\u9ED8\u8BA4\u7EC4',
icon: 'lucide:folder',
match: ['*'],
defaultOpen: 'same-tab',
items: [
{
id: uid(),
name: '\u9996\u9875',
icon: 'lucide:home',
type: 'url',
data: '/',
openIn: 'same-tab',
hidden: false,
},
],
collapsed: false,
itemsPerRow: 1,
hidden: false,
}
raw.groups = [g]
}
const sitePref = await store2.getAll()
openEditorModal(root, raw, {
async saveConfig(cfg) {
try {
await setValue(CONFIG_KEY, JSON.stringify(cfg))
} catch (e) {}
},
rerender() {},
sitePref,
updateThemeUI() {},
edgeDefaults: {
width: 3,
height: 60,
opacity: 0.6,
colorLight: '#1A73E8',
colorDark: '#8AB4F8',
},
tempOpenGetter() {
return false
},
})
try {
const modal = root.querySelector('.modal.editor')
const segs = Array.from(
modal.querySelectorAll('.segmented .seg-item')
)
for (const seg of segs) {
const textEl = seg.querySelector('.seg-text')
const inputEl = seg.querySelector('.seg-radio')
if (
textEl &&
textEl.textContent === '\u5206\u7EC4' &&
inputEl instanceof HTMLInputElement
) {
inputEl.click()
break
}
}
closeSettingsPanel()
} catch (e) {}
} catch (e) {}
})()
break
}
case 'exportShortcutsDataJson': {
;(async () => {
try {
const s = await getValue(CONFIG_KEY, '')
const raw = s ? JSON.parse(String(s) || '{}') || {} : {}
const date = /* @__PURE__ */ new Date()
const timestamp = ''
.concat(date.getFullYear())
.concat(String(date.getMonth() + 1).padStart(2, '0'))
.concat(String(date.getDate()).padStart(2, '0'), '_')
.concat(String(date.getHours()).padStart(2, '0'))
.concat(String(date.getMinutes()).padStart(2, '0'))
.concat(String(date.getSeconds()).padStart(2, '0'))
const blob = new Blob([JSON.stringify(raw, null, 2)], {
type: 'application/json',
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'utags-shortcuts-data-'.concat(timestamp, '.json')
a.click()
setTimeout(() => {
URL.revokeObjectURL(url)
}, 1e3)
} catch (e) {}
})()
break
}
case 'exportSettingsJson': {
;(async () => {
try {
const raw = await getValue(SETTINGS_KEY, {})
const date = /* @__PURE__ */ new Date()
const timestamp = ''
.concat(date.getFullYear())
.concat(String(date.getMonth() + 1).padStart(2, '0'))
.concat(String(date.getDate()).padStart(2, '0'), '_')
.concat(String(date.getHours()).padStart(2, '0'))
.concat(String(date.getMinutes()).padStart(2, '0'))
.concat(String(date.getSeconds()).padStart(2, '0'))
const blob = new Blob([JSON.stringify(raw, null, 2)], {
type: 'application/json',
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'utags-shortcuts-settings-'.concat(
timestamp,
'.json'
)
a.click()
setTimeout(() => {
URL.revokeObjectURL(url)
}, 1e3)
} catch (e) {}
})()
break
}
case 'importShortcutsDataJson': {
importJson({
validate: (data) => data && Array.isArray(data.groups),
errorMessage:
'\u65E0\u6548\u7684\u5BFC\u822A\u6570\u636E\u6587\u4EF6\uFF08\u7F3A\u5C11 groups \u5B57\u6BB5\uFF09',
async onSuccess(obj) {
const existing = await getValue(CONFIG_KEY, '')
const existingObj = existing
? JSON.parse(String(existing) || '{}') || {}
: {}
const merged = deepMergeReplaceArrays(existingObj, obj)
await setValue(CONFIG_KEY, JSON.stringify(merged))
},
})
break
}
case 'importSettingsJson': {
importJson({
validate: (data) =>
data && typeof data === 'object' && !Array.isArray(data),
errorMessage:
'\u65E0\u6548\u7684\u8BBE\u7F6E\u6587\u4EF6\uFF08\u683C\u5F0F\u5E94\u4E3A\u5BF9\u8C61\uFF09',
async onSuccess(obj) {
const existing = await getValue(SETTINGS_KEY, {})
const merged = __spreadValues(__spreadValues({}, existing), obj)
await setValue(SETTINGS_KEY, merged)
},
})
break
}
case 'clearShortcutsData': {
const ok = globalThis.confirm(
'\u662F\u5426\u771F\u7684\u8981\u6E05\u7A7A\u6570\u636E\uFF1F\u4E0D\u53EF\u9006\uFF0C\u5EFA\u8BAE\u5148\u5BFC\u51FA\u5907\u4EFD\u3002'
)
if (!ok) break
;(async () => {
try {
await setValue(CONFIG_KEY, JSON.stringify({}))
} catch (e) {}
})()
break
}
case 'resetGlobal': {
const ok = globalThis.confirm(
'\u786E\u8BA4\u8981\u91CD\u7F6E\u5168\u5C40\u8BBE\u7F6E\u5417\uFF1F\uFF08\u4E0D\u5F71\u54CD\u7AD9\u70B9\u8BBE\u7F6E\uFF09'
)
if (!ok) break
;(async () => {
try {
await store2.reset(true)
} catch (e) {}
})()
break
}
case 'resetSite': {
const ok = globalThis.confirm(
'\u786E\u8BA4\u8981\u91CD\u7F6E\u5F53\u524D\u7AD9\u70B9\u8BBE\u7F6E\u5417\uFF1F'
)
if (!ok) break
;(async () => {
try {
await store2.reset(false)
} catch (e) {}
})()
break
}
case 'edgeReset': {
;(async () => {
try {
await store2.set({
position: DEFAULTS.position,
edgeWidth: DEFAULTS.edgeWidth,
edgeHeight: DEFAULTS.edgeHeight,
edgeOpacity: DEFAULTS.edgeOpacity,
edgeColorLight: DEFAULTS.edgeColorLight,
edgeColorDark: DEFAULTS.edgeColorDark,
edgeHidden: DEFAULTS.edgeHidden,
})
} catch (e) {}
})()
break
}
default: {
break
}
}
},
})
}
var EDGE_DEFAULT_WIDTH = 3
var EDGE_DEFAULT_HEIGHT = 60
var EDGE_DEFAULT_OPACITY = 0.6
var EDGE_DEFAULT_COLOR_LIGHT = '#1A73E8'
var EDGE_DEFAULT_COLOR_DARK = '#8AB4F8'
var OPEN_DEFAULT = 'same-tab'
var THEME_DEFAULT = 'system'
var HOTKEY_DEFAULT = 'Alt+Shift+K'
var LAYOUT_DEFAULT = 'floating'
var SIDEBAR_SIDE_DEFAULT = 'right'
function ensureGlobalStyles() {
try {
const existed = document.head.querySelector(
'style[data-ushortcuts-style="sidebar"]'
)
if (existed) return
const style = document.createElement('style')
style.dataset.ushortcutsStyle = 'sidebar'
style.textContent =
'\nhtml[data-utags-shortcuts-sidebar="left-open"] body { width: calc(100% - 360px) !important; margin-left: 360px !important; margin-right: 0 !important; }\nhtml[data-utags-shortcuts-sidebar="right-open"] body { width: calc(100% - 360px) !important; margin-right: 360px !important; margin-left: 0 !important; }\n'
document.head.append(style)
} catch (e) {}
}
var store = createUshortcutsSettingsStore()
var settings = {}
var lastSaved = ''
var tempOpen = false
var tempClosed = false
var menuIds = []
var showAllGroups = false
var showHiddenGroups = false
var showHiddenItems = false
var editingGroups = /* @__PURE__ */ new Set()
var selectedItemsByGroup = /* @__PURE__ */ new Map()
function matchPattern(url, pattern) {
try {
const t = String(pattern || '')
if (t.startsWith('/') && t.lastIndexOf('/') > 0) {
const last = t.lastIndexOf('/')
const body = t.slice(1, last)
const flags = t.slice(last + 1)
const re2 = new RegExp(body, flags)
return re2.test(url)
}
const esc = t
.replaceAll(/[.+^${}()|[\]\\]/g, '\\$&')
.replaceAll('*', '.*')
const re = new RegExp('^' + esc + '$')
return re.test(url)
} catch (e) {
return false
}
}
function openItem(it, group, cfg, opts) {
const mode = it.openIn || group.defaultOpen || settings.defaultOpen
if (it.type === 'url') {
const url = new URL(
resolveUrlTemplate(String(it.data || '/')),
location.href
).href
const finalMode = (opts == null ? void 0 : opts.forceNewTab)
? 'new-tab'
: mode
if (finalMode === 'new-tab') {
window.open(url, '_blank', 'noopener')
} else {
location.assign(url)
}
return
}
try {
const onMsg = (ev) => {
const d = (ev && ev.data) || null
if (
d &&
typeof d.__ushortcuts_err__ === 'string' &&
d.__ushortcuts_err__
) {
try {
if (typeof globalThis.alert === 'function') {
globalThis.alert(
'\u811A\u672C\u6267\u884C\u51FA\u9519\uFF1A' +
String(d.__ushortcuts_err__)
)
} else {
console.error(
'\u811A\u672C\u6267\u884C\u51FA\u9519\uFF1A' +
String(d.__ushortcuts_err__)
)
}
} catch (e) {}
return
}
const raw =
d && typeof d.__ushortcuts_url__ === 'string'
? d.__ushortcuts_url__
: ''
if (!raw) return
try {
const url = new URL(
resolveUrlTemplate(String(raw).trim()),
location.href
).href
const overrideMode =
d && typeof d.__ushortcuts_mode__ === 'string'
? d.__ushortcuts_mode__
: void 0
const finalMode = (opts == null ? void 0 : opts.forceNewTab)
? 'new-tab'
: overrideMode || mode
if (finalMode === 'new-tab') window.open(url, '_blank', 'noopener')
else location.assign(url)
} catch (e) {}
}
window.addEventListener('message', onMsg, { once: true })
const s = document.createElement('script')
const codeSrc = JSON.stringify(String(it.data || ''))
s.textContent = '(async function(){try{var __code='.concat(
codeSrc,
";var __fn=new Function(__code);var __ret=__fn();if(__ret&&typeof __ret.then==='function'){__ret=await __ret;}var __url='';var __mode='';if(typeof __ret==='string'&&__ret.trim()){__url=__ret.trim();}else if(__ret&&typeof __ret==='object'){try{if(typeof __ret.error==='string'&&__ret.error){window.postMessage({__ushortcuts_err__:__ret.error},'*');return;}var __x=__ret.url||(__ret.href?String(__ret):'');if(typeof __x==='string'&&__x.trim()){__url=__x.trim();}var __m=__ret.mode; if(__m==='same-tab'||__m==='new-tab'){__mode=__m;} }catch{}}if(__url){window.postMessage({__ushortcuts_url__:__url,__ushortcuts_mode__:__mode},'*');}}catch(e){try{window.postMessage({__ushortcuts_err__:String(e&&(e.message||e))},'*');}catch{}}})()"
)
;(document.documentElement || document.body).append(s)
s.remove()
} catch (e) {}
}
async function loadConfig() {
try {
const v = await getValue(CONFIG_KEY, '')
if (v) {
const raw = JSON.parse(String(v) || '{}')
const host2 = location.hostname || ''
const ensureGroup = (gg) => ({
id: String((gg == null ? void 0 : gg.id) || uid()),
name: String((gg == null ? void 0 : gg.name) || '\u9ED8\u8BA4\u7EC4'),
icon: String((gg == null ? void 0 : gg.icon) || 'lucide:folder'),
match: Array.isArray(gg == null ? void 0 : gg.match)
? gg.match
: ['*'],
defaultOpen:
(gg == null ? void 0 : gg.defaultOpen) === 'new-tab'
? 'new-tab'
: 'same-tab',
items: Array.isArray(gg == null ? void 0 : gg.items) ? gg.items : [],
collapsed: Boolean(gg == null ? void 0 : gg.collapsed),
itemsPerRow: Number.isFinite(gg == null ? void 0 : gg.itemsPerRow)
? gg.itemsPerRow
: 1,
hidden: Boolean(gg == null ? void 0 : gg.hidden),
})
const groupsArr = Array.isArray(raw == null ? void 0 : raw.groups)
? raw.groups.map((x) => ensureGroup(x))
: []
if (groupsArr.length === 0) {
const g2 = ensureGroup({})
g2.items = [
{
id: uid(),
name: '\u9996\u9875',
icon: 'lucide:home',
type: 'url',
data: '/',
openIn: OPEN_DEFAULT,
hidden: false,
},
]
groupsArr.push(g2)
}
const cfg = {
groups: groupsArr,
}
return cfg
}
} catch (e) {}
const host = location.hostname || ''
const g = {
id: uid(),
name: '\u9ED8\u8BA4\u7EC4',
icon: 'lucide:folder',
match: ['*'],
defaultOpen: OPEN_DEFAULT,
items: [
{
id: uid(),
name: '\u9996\u9875',
icon: 'lucide:home',
type: 'url',
data: '/',
openIn: OPEN_DEFAULT,
hidden: false,
},
{
id: uid(),
name: '\u7AD9\u5185\u641C\u7D22',
icon: 'favicon',
type: 'url',
data: 'https://www.google.com/search?q=site:{hostname}%20{selected||query}',
openIn: 'new-tab',
hidden: false,
},
],
collapsed: false,
itemsPerRow: 1,
hidden: false,
}
return {
groups: [g],
}
}
async function saveConfig(cfg) {
try {
const s = JSON.stringify(cfg)
if (s === lastSaved) return
lastSaved = s
await setValue(CONFIG_KEY, s)
} catch (e) {}
}
function createRoot() {
console.log('createRoot')
const existing = document.querySelector(
'[data-ushortcuts-host="utags-shortcuts"]'
)
if (existing instanceof HTMLElement) {
const root2 = existing.shadowRoot
return { host: existing, root: root2 }
}
const host = document.createElement('div')
host.dataset.ushortcutsHost = 'utags-shortcuts'
const root = host.attachShadow({ mode: 'open' })
const style = document.createElement('style')
style.textContent = style_default
root.append(style)
document.documentElement.append(host)
return { host, root }
}
function place(el, cfg) {
el.style.position = 'fixed'
el.style.inset = 'auto'
if (settings.layoutMode === 'sidebar') {
el.style.top = '0'
el.style.bottom = '0'
el.style.left = 'auto'
el.style.right = 'auto'
el.style.transform = ''
if ((settings.sidebarSide || SIDEBAR_SIDE_DEFAULT) === 'left') {
el.style.left = '0'
} else {
el.style.right = '0'
}
return
}
const p = settings.position
switch (p) {
case 'left-top': {
el.style.top = '0'
el.style.left = '0'
break
}
case 'left-center': {
el.style.top = '50%'
el.style.left = '0'
el.style.transform = 'translateY(-50%)'
break
}
case 'left-bottom': {
el.style.bottom = '0'
el.style.left = '0'
break
}
case 'right-center': {
el.style.top = '50%'
el.style.right = '0'
el.style.transform = 'translateY(-50%)'
break
}
case 'right-bottom': {
el.style.bottom = '0'
el.style.right = '0'
break
}
case 'top-left': {
el.style.top = '0'
el.style.left = '0'
break
}
case 'top-center': {
el.style.top = '0'
el.style.left = '50%'
el.style.transform = 'translateX(-50%)'
break
}
case 'top-right': {
el.style.top = '0'
el.style.right = '0'
break
}
case 'bottom-left': {
el.style.bottom = '0'
el.style.left = '0'
break
}
case 'bottom-center': {
el.style.bottom = '0'
el.style.left = '50%'
el.style.transform = 'translateX(-50%)'
break
}
case 'bottom-right': {
el.style.bottom = '0'
el.style.right = '0'
break
}
default: {
el.style.top = '0'
el.style.right = '0'
break
}
}
}
function isHorizontalPos(pos) {
return pos.startsWith('top-') || pos.startsWith('bottom-')
}
function isRightSide(pos) {
return pos.startsWith('right-')
}
function isTopSide(pos) {
return pos.startsWith('top-')
}
function scorePattern(url, pattern) {
const neg = pattern.startsWith('!')
const pat = neg ? pattern.slice(1) : pattern
if (!matchPattern(url, pat)) return -1
if (pat.startsWith('/') && pat.lastIndexOf('/') > 0) {
const last = pat.lastIndexOf('/')
return pat.slice(1, last).length
}
return pat.replaceAll('*', '').length
}
function groupScore(url, g) {
let max = -1
for (const p of g.match) {
const neg = p.startsWith('!')
const pat = neg ? p.slice(1) : p
if (neg) {
if (matchPattern(url, pat)) return -1
continue
}
const s = scorePattern(url, p)
if (s > max) max = s
}
return max
}
function currentGroups(cfg) {
if (showAllGroups) {
return cfg.groups.filter((g) => showHiddenGroups || !g.hidden)
}
const url = location.href
return cfg.groups
.map((g) => ({ g, s: groupScore(url, g) }))
.filter((x) => x.s >= 0 && !x.g.hidden)
.sort((a, b) => b.s - a.s)
.map((x) => x.g)
}
function preserveScroll(panel, cb) {
const scroller = panel.querySelector('.panel-scroll') || panel
const sx = scroller.scrollLeft
const sy = scroller.scrollTop
cb()
const apply = () => {
try {
scroller.scrollLeft = sx
scroller.scrollTop = sy
} catch (e) {}
}
apply()
try {
requestAnimationFrame(apply)
} catch (e) {}
}
function isDarkTheme(cfg) {
const t = settings.theme || THEME_DEFAULT
if (t === 'dark') return true
if (t === 'light') return false
try {
return (
globalThis.window !== void 0 &&
Boolean(globalThis.matchMedia) &&
globalThis.matchMedia('(prefers-color-scheme: dark)').matches
)
} catch (e) {
return false
}
}
function parseHotkeySpec(spec) {
const s = String(spec || '').trim()
if (!s) return null
const parts = s.split('+').map((x) => x.trim().toLowerCase())
let key = ''
const need = { ctrl: false, meta: false, alt: false, shift: false }
for (const p of parts) {
switch (p) {
case 'ctrl':
case 'control': {
need.ctrl = true
break
}
case 'meta':
case 'cmd':
case 'command': {
need.meta = true
break
}
case 'alt':
case 'option': {
need.alt = true
break
}
case 'shift': {
need.shift = true
break
}
default: {
key = p
break
}
}
}
if (!key) return null
let code = ''
if (key.length === 1) code = 'Key' + key.toUpperCase()
else if (key === 'space') code = 'Space'
else code = key
return {
ctrl: need.ctrl,
meta: need.meta,
alt: need.alt,
shift: need.shift,
code,
}
}
function isEditableTarget(t) {
const el = t
if (!el) return false
const tag = el.tagName ? el.tagName.toLowerCase() : ''
if (tag === 'input' || tag === 'textarea' || tag === 'select') return true
const ce = el.isContentEditable
return Boolean(ce)
}
function registerHotkeys(root, cfg) {
document.addEventListener('keydown', (e) => {
if (e.defaultPrevented) return
if (isEditableTarget(e.target || void 0)) return
const spec = settings.hotkey || HOTKEY_DEFAULT
const p = parseHotkeySpec(spec)
if (!p) return
if (!(p.ctrl || p.meta || p.alt)) return
const hasCtrl = Boolean(e.ctrlKey)
const hasMeta = Boolean(e.metaKey)
const hasAlt = Boolean(e.altKey)
const hasShift = Boolean(e.shiftKey)
if (p.ctrl !== hasCtrl) return
if (p.meta !== hasMeta) return
if (p.alt !== hasAlt) return
if (p.shift !== hasShift) return
if (e.code !== p.code) return
e.preventDefault()
const visible = Boolean(root.querySelector('.ushortcuts .panel'))
if (visible) {
collapseWithAnim(root, cfg)
} else {
tempOpen = true
rerender(root, cfg)
}
})
}
function renderShortcutsItem(
root,
cfg,
g,
it,
section,
isEditing,
siteDefaultOpenConst,
defOpen
) {
var _a
const wrap = document.createElement('div')
wrap.className = 'item-wrap'
wrap.dataset.itemId = it.id
wrap.classList.add('fade-in')
if (it.hidden) wrap.classList.add('is-hidden')
const a = document.createElement('a')
a.className = 'item'
if (isEditing) {
a.href = '#'
a.addEventListener('click', (e) => {
e.preventDefault()
e.stopImmediatePropagation()
})
} else if (it.type === 'url') {
const url = new URL(
resolveUrlTemplate(String(it.data || '/')),
location.href
).href
a.href = url
a.addEventListener('click', (e) => {
e.preventDefault()
const forceNew = Boolean(e.ctrlKey || e.metaKey)
openItem(it, g, cfg, { forceNewTab: forceNew })
})
a.addEventListener('auxclick', (e) => {
if (e.button === 1) {
e.preventDefault()
openItem(it, g, cfg, { forceNewTab: true })
}
})
} else {
a.href = '#'
a.addEventListener('click', (e) => {
e.preventDefault()
const forceNew = Boolean(e.ctrlKey || e.metaKey)
openItem(it, g, cfg, { forceNewTab: forceNew })
})
a.addEventListener('auxclick', (e) => {
if (e.button === 1) {
e.preventDefault()
openItem(it, g, cfg, { forceNewTab: true })
}
})
}
{
const rawIcon = String(it.icon || '')
let iconStr = rawIcon
if (rawIcon.startsWith('favicon')) {
const param = rawIcon.split(':')[1]
const sizeNum = param ? Number.parseInt(param, 10) : 64
const size = sizeNum === 32 ? 32 : sizeNum === 64 ? 64 : 64
const targetUrl =
it.type === 'url'
? new URL(resolveUrlTemplate(String(it.data || '/')), location.href)
.href
: location.href
try {
iconStr = 'url:' + getFaviconUrl(targetUrl, size)
} catch (e) {}
}
setIcon(a, iconStr)
}
const t = document.createElement('span')
t.textContent = it.name
a.append(t)
if (isEditing) {
const set = selectedItemsByGroup.get(g.id) || /* @__PURE__ */ new Set()
selectedItemsByGroup.set(g.id, set)
const sel = document.createElement('input')
sel.type = 'checkbox'
sel.checked = set.has(it.id)
const updateDeleteBtnState = () => {
var _a2
const btn = section.querySelector(
'.header-actions .btn.mini:last-child'
)
if (btn instanceof HTMLButtonElement) {
const count =
((_a2 = selectedItemsByGroup.get(g.id)) == null
? void 0
: _a2.size) || 0
btn.disabled = !(count > 0)
}
}
sel.addEventListener('change', () => {
if (sel.checked) set.add(it.id)
else set.delete(it.id)
updateDeleteBtnState()
})
wrap.append(sel)
}
wrap.append(a)
if (isEditing) {
const editItemBtn = document.createElement('button')
editItemBtn.className = 'icon-btn'
setIcon(editItemBtn, 'lucide:edit-3', '\u7F16\u8F91\u8BE5\u5BFC\u822A')
const defaultOpenForItems =
(_a = g.defaultOpen) != null ? _a : siteDefaultOpenConst
editItemBtn.addEventListener('click', (e) => {
e.stopPropagation()
openAddLinkModal(root, cfg, {
saveConfig(c2) {
void saveConfig(c2)
},
rerender(r, c2) {
rerender(r, c2)
},
defaultOpen: defaultOpenForItems,
defaultGroupId: g.id,
existingItem: it,
})
})
const hideBtn = document.createElement('button')
hideBtn.className = 'icon-btn'
if (it.hidden) {
setIcon(hideBtn, 'lucide:eye', '\u663E\u793A\u8BE5\u5BFC\u822A')
} else {
setIcon(hideBtn, 'lucide:eye-off', '\u9690\u85CF\u8BE5\u5BFC\u822A')
}
hideBtn.addEventListener('click', (e) => {
e.stopPropagation()
it.hidden = !it.hidden
void saveConfig(cfg)
rerender(root, cfg)
})
wrap.append(editItemBtn)
wrap.append(hideBtn)
}
return wrap
}
function renderGroupSection(root, cfg, g, body) {
var _a
const isEditing = editingGroups.has(g.id)
const div = document.createElement('div')
div.className = 'divider'
body.append(div)
const section = document.createElement('div')
section.className = 'section'
section.dataset.gid = g.id
if (g.hidden) section.classList.add('is-hidden')
const header = document.createElement('div')
header.className = 'header'
const title = document.createElement('div')
title.className = 'title'
setIcon(title, g.icon || 'lucide:folder')
const nameSpan = document.createElement('span')
nameSpan.textContent = g.displayName || g.name
title.append(nameSpan)
header.append(title)
title.addEventListener('click', () => {
g.collapsed = !g.collapsed
void saveConfig(cfg)
const itemsDiv = section.querySelector('.items')
if (itemsDiv) itemsDiv.style.display = g.collapsed ? 'none' : ''
const btn = section.querySelector('.header .icon-btn.toggle')
if (btn instanceof HTMLElement)
setIcon(
btn,
g.collapsed ? 'lucide:chevron-right' : 'lucide:chevron-down',
g.collapsed ? '\u5C55\u5F00' : '\u6298\u53E0'
)
})
const actions = document.createElement('div')
actions.className = 'header-actions'
const siteDefaultOpenConst = settings.defaultOpen
const editMenuRightSide =
isRightSide(settings.position) || settings.position.endsWith('-right')
const groupMenuRightSide = editMenuRightSide
if (isEditing) {
const exitBtn = document.createElement('button')
exitBtn.className = 'btn mini'
exitBtn.textContent = '\u9000\u51FA\u7F16\u8F91'
exitBtn.addEventListener('click', () => {
editingGroups.delete(g.id)
selectedItemsByGroup.delete(g.id)
rerender(root, cfg)
})
const delBtn = document.createElement('button')
delBtn.className = 'btn mini'
delBtn.textContent = '\u5220\u9664'
{
const count =
((_a = selectedItemsByGroup.get(g.id)) == null ? void 0 : _a.size) ||
0
delBtn.disabled = !(count > 0)
}
delBtn.addEventListener('click', () => {
const set = selectedItemsByGroup.get(g.id)
if (!set || set.size === 0) return
const ok = globalThis.confirm(
'\u662F\u5426\u5220\u9664\u6240\u9009\u5BFC\u822A\u9879\uFF1F'
)
if (!ok) return
const ids = new Set(Array.from(set))
g.items = g.items.filter((x) => !ids.has(x.id))
selectedItemsByGroup.delete(g.id)
void saveConfig(cfg)
rerender(root, cfg)
})
actions.append(exitBtn)
actions.append(delBtn)
} else {
const addLinkBtn = document.createElement('button')
addLinkBtn.className = 'icon-btn'
setIcon(
addLinkBtn,
'lucide:plus',
'\u6DFB\u52A0\u94FE\u63A5\u5230\u6B64\u5206\u7EC4'
)
addLinkBtn.addEventListener('click', (e) => {
e.stopPropagation()
showDropdownMenu(
root,
addLinkBtn,
[
{
icon: 'lucide:keyboard',
label: '\u624B\u52A8\u8F93\u5165',
onClick() {
var _a2
openAddLinkModal(root, cfg, {
saveConfig(c2) {
void saveConfig(c2)
},
rerender(r, c2) {
rerender(r, c2)
},
defaultOpen:
(_a2 = g.defaultOpen) != null
? _a2
: settings.defaultOpen || OPEN_DEFAULT,
defaultGroupId: g.id,
})
},
},
{
icon: 'lucide:globe',
label: '\u6DFB\u52A0\u5F53\u524D\u7F51\u9875',
onClick() {
var _a2
addCurrentPageLinkToGroup(
root,
cfg,
{
saveConfig(c2) {
void saveConfig(c2)
},
rerender(r, c2) {
rerender(r, c2)
},
},
g.id,
(_a2 = g.defaultOpen) != null
? _a2
: settings.defaultOpen || OPEN_DEFAULT
)
},
},
{
icon: 'lucide:link',
label: '\u4ECE\u5F53\u524D\u7F51\u9875\u91C7\u96C6\u94FE\u63A5',
onClick() {
var _a2
pickLinkFromPageAndAdd(
root,
cfg,
{
saveConfig(c2) {
void saveConfig(c2)
},
rerender(r, c2) {
rerender(r, c2)
},
},
g.id,
(_a2 = g.defaultOpen) != null
? _a2
: settings.defaultOpen || OPEN_DEFAULT
)
},
},
],
groupMenuRightSide
)
})
const hideGroupBtn = document.createElement('button')
hideGroupBtn.className = 'icon-btn'
setIcon(
hideGroupBtn,
g.hidden ? 'lucide:eye' : 'lucide:eye-off',
g.hidden ? '\u663E\u793A\u5206\u7EC4' : '\u9690\u85CF\u5206\u7EC4'
)
hideGroupBtn.addEventListener('click', () => {
g.hidden = !g.hidden
void saveConfig(cfg)
rerender(root, cfg)
})
const editBtn = document.createElement('button')
editBtn.className = 'icon-btn'
setIcon(editBtn, 'lucide:edit-3', '\u7F16\u8F91')
editBtn.addEventListener('click', (ev) => {
ev.stopPropagation()
showDropdownMenu(
root,
editBtn,
[
{
icon: 'lucide:edit-3',
label: '\u7F16\u8F91\u5206\u7EC4',
onClick() {
openAddGroupModal(root, cfg, {
saveConfig(c2) {
void saveConfig(c2)
},
rerender(r, c2) {
rerender(r, c2)
},
defaultOpen: g.defaultOpen || siteDefaultOpenConst,
defaultMatch: g.match,
existingGroup: g,
})
},
},
{
icon: 'lucide:list',
label: '\u7F16\u8F91\u5BFC\u822A\u9879',
onClick() {
if (editingGroups.has(g.id)) editingGroups.delete(g.id)
else editingGroups.add(g.id)
rerender(root, cfg)
},
},
],
editMenuRightSide
)
})
const toggleBtn = document.createElement('button')
toggleBtn.className = 'icon-btn toggle'
setIcon(
toggleBtn,
g.collapsed ? 'lucide:chevron-right' : 'lucide:chevron-down',
g.collapsed ? '\u5C55\u5F00' : '\u6298\u53E0'
)
toggleBtn.addEventListener('click', () => {
g.collapsed = !g.collapsed
void saveConfig(cfg)
const itemsDiv = section.querySelector('.items')
if (itemsDiv) itemsDiv.style.display = g.collapsed ? 'none' : ''
setIcon(
toggleBtn,
g.collapsed ? 'lucide:chevron-right' : 'lucide:chevron-down',
g.collapsed ? '\u5C55\u5F00' : '\u6298\u53E0'
)
})
actions.append(addLinkBtn)
actions.append(editBtn)
actions.append(hideGroupBtn)
actions.append(toggleBtn)
}
header.append(actions)
section.append(header)
const items = document.createElement('div')
items.className = 'items'
items.style.setProperty(
'--cols',
String(isEditing ? 1 : g.itemsPerRow || 1)
)
items.style.display = g.collapsed ? 'none' : ''
let visibleCount = 0
const defOpen = settings.defaultOpen || OPEN_DEFAULT
for (const it of g.items) {
if (it.hidden && !showHiddenItems && !isEditing) continue
visibleCount++
const wrap = renderShortcutsItem(
root,
cfg,
g,
it,
section,
isEditing,
siteDefaultOpenConst,
defOpen
)
items.append(wrap)
}
items.style.setProperty(
'--cols',
String(
isEditing
? 1
: Math.max(1, Math.min(g.itemsPerRow || 1, visibleCount || 1))
)
)
if (visibleCount === 0) {
const msg = document.createElement('div')
msg.className = 'empty-msg'
msg.textContent =
g.items.length === 0
? '\u65E0\u9879\u76EE'
: '\u9879\u76EE\u5DF2\u88AB\u9690\u85CF'
items.append(msg)
}
section.append(items)
section.classList.add('fade-in')
body.append(section)
}
function renderPanelHeader(root, cfg, panel) {
const collapseRow = document.createElement('div')
collapseRow.className = 'header'
const leftActions = document.createElement('div')
leftActions.className = 'panel-actions-left'
const rightActions = document.createElement('div')
rightActions.className = 'panel-actions'
const closeBtn = document.createElement('button')
closeBtn.className = 'collapse-btn'
setIcon(closeBtn, 'lucide:x', '\u5173\u95ED')
closeBtn.addEventListener('click', () => {
collapseWithAnim(root, cfg)
})
const plusBtn = document.createElement('button')
plusBtn.className = 'icon-btn'
setIcon(plusBtn, 'lucide:plus', '\u6DFB\u52A0')
plusBtn.addEventListener('click', (ev) => {
ev.stopPropagation()
openQuickAddMenu(root, cfg, plusBtn)
})
const showAllBtn = document.createElement('button')
showAllBtn.className = 'icon-btn'
setIcon(showAllBtn, 'lucide:layout-dashboard', '\u663E\u793A\u5168\u90E8')
showAllBtn.classList.toggle('active', Boolean(showAllGroups))
showAllBtn.addEventListener('click', () => {
showAllGroups = !showAllGroups
showAllBtn.classList.toggle('active', Boolean(showAllGroups))
rerender(root, cfg)
})
const settingsBtn = document.createElement('button')
settingsBtn.className = 'icon-btn'
setIcon(settingsBtn, 'lucide:settings', '\u8BBE\u7F6E')
settingsBtn.addEventListener('click', () => {
openSettingsPanel2(store)
})
const pinBtn = document.createElement('button')
pinBtn.className = 'icon-btn'
setIcon(
pinBtn,
settings.pinned ? 'lucide:pin' : 'lucide:pin-off',
settings.pinned ? '\u53D6\u6D88\u56FA\u5B9A' : '\u56FA\u5B9A\u663E\u793A'
)
pinBtn.classList.toggle('active', Boolean(settings.pinned))
pinBtn.addEventListener('click', () => {
void store.set({ pinned: !settings.pinned })
})
rightActions.append(plusBtn)
rightActions.append(showAllBtn)
if (showAllGroups) {
const showHiddenGroupsLabel = document.createElement('label')
showHiddenGroupsLabel.className = 'check'
const showHiddenGroupsCb = document.createElement('input')
showHiddenGroupsCb.type = 'checkbox'
showHiddenGroupsCb.checked = Boolean(showHiddenGroups)
const showHiddenGroupsSpan = document.createElement('span')
showHiddenGroupsSpan.textContent =
'\u663E\u793A\u9690\u85CF\u7684\u5206\u7EC4'
showHiddenGroupsLabel.append(showHiddenGroupsCb)
showHiddenGroupsLabel.append(showHiddenGroupsSpan)
showHiddenGroupsCb.addEventListener('change', () => {
showHiddenGroups = Boolean(showHiddenGroupsCb.checked)
rerender(root, cfg)
})
const showHiddenItemsLabel = document.createElement('label')
showHiddenItemsLabel.className = 'check'
const showHiddenItemsCb = document.createElement('input')
showHiddenItemsCb.type = 'checkbox'
showHiddenItemsCb.checked = Boolean(showHiddenItems)
const showHiddenItemsSpan = document.createElement('span')
showHiddenItemsSpan.textContent =
'\u663E\u793A\u9690\u85CF\u7684\u5BFC\u822A'
showHiddenItemsLabel.append(showHiddenItemsCb)
showHiddenItemsLabel.append(showHiddenItemsSpan)
showHiddenItemsCb.addEventListener('change', () => {
showHiddenItems = Boolean(showHiddenItemsCb.checked)
rerender(root, cfg)
})
const expandAllBtn = document.createElement('button')
expandAllBtn.className = 'btn mini'
expandAllBtn.textContent = '\u5C55\u5F00\u6240\u6709\u5206\u7EC4'
expandAllBtn.addEventListener('click', () => {
preserveScroll(panel, () => {
for (const g of cfg.groups) g.collapsed = false
void saveConfig(cfg)
for (const sec of Array.from(panel.querySelectorAll('.section'))) {
const itemsDiv = sec.querySelector('.items')
if (itemsDiv) itemsDiv.style.display = ''
const gid = sec.dataset.gid
const grp = cfg.groups.find((x) => x.id === gid)
const btn = sec.querySelector('.header .icon-btn:nth-last-child(1)')
if (grp && btn) setIcon(btn, 'lucide:chevron-down', '\u6298\u53E0')
}
})
})
const collapseAllBtn = document.createElement('button')
collapseAllBtn.className = 'btn mini'
collapseAllBtn.textContent = '\u6298\u53E0\u6240\u6709\u5206\u7EC4'
collapseAllBtn.addEventListener('click', () => {
preserveScroll(panel, () => {
for (const g of cfg.groups) g.collapsed = true
void saveConfig(cfg)
for (const sec of Array.from(panel.querySelectorAll('.section'))) {
const itemsDiv = sec.querySelector('.items')
if (itemsDiv) itemsDiv.style.display = 'none'
const gid = sec.dataset.gid
const grp = cfg.groups.find((x) => x.id === gid)
const btn = sec.querySelector('.header .icon-btn:nth-last-child(1)')
if (grp && btn) setIcon(btn, 'lucide:chevron-right', '\u5C55\u5F00')
}
})
})
const manageGroupsBtn = document.createElement('button')
manageGroupsBtn.className = 'btn mini'
manageGroupsBtn.textContent = '\u7BA1\u7406\u5206\u7EC4'
manageGroupsBtn.addEventListener('click', () => {
openEditorModal(root, cfg, {
saveConfig(c2) {
void saveConfig(c2)
},
rerender(r, c2) {
rerender(r, c2)
},
sitePref: {
defaultOpen: settings.defaultOpen || OPEN_DEFAULT,
},
updateThemeUI,
edgeDefaults: {
width: EDGE_DEFAULT_WIDTH,
height: EDGE_DEFAULT_HEIGHT,
opacity: EDGE_DEFAULT_OPACITY,
colorLight: EDGE_DEFAULT_COLOR_LIGHT,
colorDark: EDGE_DEFAULT_COLOR_DARK,
},
tempOpenGetter: () => tempOpen,
})
})
rightActions.append(showHiddenGroupsLabel)
rightActions.append(showHiddenItemsLabel)
rightActions.append(expandAllBtn)
rightActions.append(collapseAllBtn)
rightActions.append(manageGroupsBtn)
}
rightActions.append(settingsBtn)
if ((settings.layoutMode || LAYOUT_DEFAULT) !== 'sidebar')
rightActions.append(pinBtn)
rightActions.append(closeBtn)
collapseRow.append(leftActions)
collapseRow.append(rightActions)
panel.append(collapseRow)
let body = panel
if (showAllGroups) {
panel.classList.add('all-mode')
const scroller = document.createElement('div')
scroller.className = 'panel-scroll'
const columns = document.createElement('div')
columns.className = 'panel-columns'
scroller.append(columns)
panel.append(scroller)
body = columns
} else {
panel.classList.remove('all-mode')
}
return body
}
function renderPanel(root, cfg, animIn) {
const wrapper = document.createElement('div')
wrapper.className = 'ushortcuts' + (isDarkTheme(cfg) ? ' dark' : '')
const panel = document.createElement('div')
panel.className = 'panel'
if (settings.layoutMode === 'sidebar') {
try {
panel.style.height = '100vh'
panel.style.borderRadius = '0'
} catch (e) {}
try {
const side =
(settings.sidebarSide || SIDEBAR_SIDE_DEFAULT) === 'left'
? 'sidebar-left'
: 'sidebar-right'
panel.classList.add('sidebar', side)
} catch (e) {}
}
const pos = settings.position
const isRight = isRightSide(pos)
const isHoriz = isHorizontalPos(pos)
const isTop = isTopSide(pos)
if (animIn)
panel.classList.add(
isHoriz
? isTop
? 'anim-in-top'
: 'anim-in-bottom'
: isRight
? 'anim-in-right'
: 'anim-in-left'
)
const body = renderPanelHeader(root, cfg, panel)
const groupsToShow = currentGroups(cfg)
for (const g of groupsToShow) renderGroupSection(root, cfg, g, body)
wrapper.append(panel)
wrapper.addEventListener('mouseenter', () => {
try {
if (collapseTimer) clearTimeout(collapseTimer)
} catch (e) {}
})
wrapper.addEventListener('mouseleave', () => {
const pinnedFlag =
(settings.layoutMode || LAYOUT_DEFAULT) === 'sidebar'
? true
: Boolean(settings.pinned)
if (!pinnedFlag && !suppressCollapse) scheduleAutoCollapse(root, cfg)
})
place(wrapper, cfg)
const mask = root.querySelector('.modal-mask')
if (mask) {
mask.before(wrapper)
} else {
root.append(wrapper)
}
}
function openQuickAddMenu(root, cfg, anchor) {
suppressCollapse = true
tempOpen = true
const rightSide =
isRightSide(settings.position) || settings.position.endsWith('-right')
showDropdownMenu(
root,
anchor,
[
{
icon: 'lucide:folder',
label: '\u6DFB\u52A0\u5206\u7EC4',
onClick() {
suppressCollapse = false
openAddGroupModal(root, cfg, {
saveConfig(c2) {
void saveConfig(c2)
},
rerender(r, c2) {
rerender(r, c2)
},
defaultOpen: settings.defaultOpen,
defaultMatch: ['*://' + (location.hostname || '') + '/*'],
})
},
},
{
icon: 'lucide:link',
label: '\u6DFB\u52A0\u94FE\u63A5',
onClick() {
var _a
suppressCollapse = false
const matched = currentGroups(cfg)
openAddLinkModal(root, cfg, {
saveConfig(c2) {
void saveConfig(c2)
},
rerender(r, c2) {
rerender(r, c2)
},
defaultOpen: settings.defaultOpen || OPEN_DEFAULT,
defaultGroupId:
(_a = matched[0] || cfg.groups[0]) == null ? void 0 : _a.id,
})
},
},
],
rightSide
)
}
var lastCollapsed = true
var suppressCollapse = false
function rerender(root, cfg) {
var _a, _b, _c
suppressCollapse = true
let sx = 0
let sy = 0
try {
const cur =
root.querySelector('.ushortcuts .panel-scroll') ||
root.querySelector('.ushortcuts .panel')
if (cur) {
sx = cur.scrollLeft
sy = cur.scrollTop
}
} catch (e) {}
for (const n of Array.from(
root.querySelectorAll('.ushortcuts,.collapsed-tab,.quick-add-menu')
))
n.remove()
if (settings.enabled === false) {
lastCollapsed = true
suppressCollapse = false
try {
delete document.documentElement.dataset.utagsShortcutsSidebar
} catch (e) {}
return
}
let isCollapsed = !tempOpen && (tempClosed || !settings.pinned)
if ((settings.layoutMode || LAYOUT_DEFAULT) === 'sidebar')
isCollapsed = !tempOpen && Boolean(tempClosed)
if (isCollapsed) {
const effectiveEdgeHidden =
(settings.layoutMode || LAYOUT_DEFAULT) === 'sidebar'
? true
: Boolean(settings.edgeHidden)
if (!effectiveEdgeHidden) {
const tab = document.createElement('div')
tab.className = 'collapsed-tab'
place(tab, cfg)
try {
const gw = (_a = settings.edgeWidth) != null ? _a : EDGE_DEFAULT_WIDTH
const gh =
(_b = settings.edgeHeight) != null ? _b : EDGE_DEFAULT_HEIGHT
const go =
(_c = settings.edgeOpacity) != null ? _c : EDGE_DEFAULT_OPACITY
const horiz = isHorizontalPos(settings.position)
const thickness = Math.max(1, Math.min(24, gw))
const length = Math.max(24, Math.min(320, gh))
tab.style.width = horiz
? ''.concat(length, 'px')
: ''.concat(thickness, 'px')
tab.style.height = horiz
? ''.concat(thickness, 'px')
: ''.concat(length, 'px')
tab.style.opacity = String(Math.max(0, Math.min(1, go)))
tab.style.backgroundColor = isDarkTheme(cfg)
? String(settings.edgeColorDark || EDGE_DEFAULT_COLOR_DARK)
: String(settings.edgeColorLight || EDGE_DEFAULT_COLOR_LIGHT)
} catch (e) {}
tab.addEventListener('mouseenter', () => {
tempOpen = true
rerender(root, cfg)
})
tab.addEventListener('mouseleave', () => {
const pinnedFlag =
(settings.layoutMode || LAYOUT_DEFAULT) === 'sidebar'
? true
: Boolean(settings.pinned)
if (!pinnedFlag && !suppressCollapse) scheduleAutoCollapse(root, cfg)
})
root.append(tab)
}
lastCollapsed = true
suppressCollapse = false
try {
delete document.documentElement.dataset.utagsShortcutsSidebar
} catch (e) {}
return
}
renderPanel(root, cfg, lastCollapsed)
updateSidebarClass()
try {
const cur =
root.querySelector('.ushortcuts .panel-scroll') ||
root.querySelector('.ushortcuts .panel')
if (cur) {
cur.scrollLeft = sx
cur.scrollTop = sy
try {
requestAnimationFrame(() => {
cur.scrollLeft = sx
cur.scrollTop = sy
})
} catch (e) {}
}
} catch (e) {}
lastCollapsed = false
suppressCollapse = false
}
function registerMenus(root, cfg) {
try {
for (const id of menuIds) {
try {
unregisterMenu(id)
} catch (e) {}
}
menuIds = []
const text = settings.enabled
? '\u{1F6AB} \u7981\u7528\u5F53\u524D\u7F51\u7AD9\u5FEB\u901F\u5BFC\u822A'
: '\u2705 \u542F\u7528\u5F53\u524D\u7F51\u7AD9\u5FEB\u901F\u5BFC\u822A'
menuIds.push(
registerMenu(
'\u{1F9ED} \u6253\u5F00\u5FEB\u901F\u5BFC\u822A\u9762\u677F',
() => {
if (settings.enabled === false) {
const ok = globalThis.confirm(
'\u5F53\u524D\u7F51\u7AD9\u5DF2\u7981\u7528\uFF0C\u662F\u5426\u542F\u7528\u5E76\u6253\u5F00\u9762\u677F\uFF1F'
)
if (ok) {
void store.set({ enabled: true })
tempOpen = true
}
return
}
tempOpen = true
rerender(root, cfg)
}
),
registerMenu(
'\u2699\uFE0F \u8BBE\u7F6E\u5FEB\u901F\u5BFC\u822A',
() => {
openSettingsPanel2(store)
}
),
registerMenu(text, () => {
void store.set({ enabled: !settings.enabled })
})
)
} catch (e) {}
}
function registerStorageListener(root, cfg) {
try {
void addValueChangeListener(CONFIG_KEY, (_name, _old, nv, remote) => {
try {
const obj = JSON.parse(nv)
if (obj && obj.groups) {
cfg.groups = obj.groups
rerender(root, cfg)
}
} catch (e) {}
})
} catch (e) {}
}
var collapseTimer
function scheduleAutoCollapse(root, cfg) {
if (collapseTimer) clearTimeout(collapseTimer)
collapseTimer = setTimeout(() => {
collapseWithAnim(root, cfg)
}, 500)
}
function collapseWithAnim(root, cfg) {
try {
const p = settings.position
const sel = root.querySelector('.ushortcuts .panel')
if (sel) {
if (isHorizontalPos(p)) {
const isTop = isTopSide(p)
sel.classList.add(isTop ? 'anim-out-top' : 'anim-out-bottom')
} else {
const right = isRightSide(p)
sel.classList.add(right ? 'anim-out-right' : 'anim-out-left')
}
sel.addEventListener(
'animationend',
() => {
tempClosed = true
tempOpen = false
rerender(root, cfg)
},
{ once: true }
)
return
}
} catch (e) {}
tempOpen = false
rerender(root, cfg)
}
function updateThemeUI(root, cfg) {
const wrapper = root.querySelector('.ushortcuts')
if (!wrapper) return
wrapper.classList.toggle('dark', isDarkTheme(cfg))
const curTheme = settings.theme || THEME_DEFAULT
const map = {
系统: 'system',
浅色: 'light',
深色: 'dark',
}
const btns = wrapper.querySelectorAll('.theme-btn')
for (const b of Array.from(btns)) {
const key = b.title
const val = map[key] || ''
b.classList.toggle('active', val === curTheme)
}
}
function registerUrlChangeListener(root, cfg) {
let last = location.href
function onChange() {
const now = location.href
if (now === last) return
last = now
rerender(root, cfg)
}
try {
const origPush = history.pushState.bind(history)
history.pushState = function (...args) {
const r = origPush(...args)
try {
onChange()
} catch (e) {}
return r
}
} catch (e) {}
try {
const origReplace = history.replaceState.bind(history)
history.replaceState = function (...args) {
const r = origReplace(...args)
try {
onChange()
} catch (e) {}
return r
}
} catch (e) {}
globalThis.addEventListener('popstate', () => {
onChange()
})
globalThis.addEventListener('hashchange', () => {
onChange()
})
}
function updateSidebarClass() {
try {
if (settings.enabled !== false && settings.layoutMode === 'sidebar') {
ensureGlobalStyles()
document.documentElement.dataset.utagsShortcutsSidebar =
(settings.sidebarSide || SIDEBAR_SIDE_DEFAULT) === 'left'
? 'left-open'
: 'right-open'
} else {
delete document.documentElement.dataset.utagsShortcutsSidebar
}
} catch (e) {}
}
function registerHostAutofix(_root, cfg) {
try {
const mo = new MutationObserver(() => {
const existing = document.querySelector(
'[data-ushortcuts-host="utags-shortcuts"]'
)
if (!(existing instanceof HTMLElement)) {
try {
const host = _root == null ? void 0 : _root.host
if (host) {
if (!document.documentElement.contains(host)) {
document.documentElement.append(host)
}
updateSidebarClass()
return
}
} catch (e) {}
const { root: newRoot } = createRoot()
rerender(newRoot, cfg)
}
})
mo.observe(document.documentElement || document.body, {
childList: true,
subtree: true,
})
} catch (e) {}
}
function main() {
try {
const de = document.documentElement
if (de && de.dataset && de.dataset.utagsShortcuts === '1') return
if (de && de.dataset) de.dataset.utagsShortcuts = '1'
} catch (e) {}
const { root } = createRoot()
void (async () => {
const cfg = await loadConfig()
settings = await store.getAll()
console.log('settings initial', settings)
const updateState = () => {
rerender(root, cfg)
registerMenus(root, cfg)
updateSidebarClass()
}
store.onChange(async () => {
settings = await store.getAll()
console.log('settings onChange', settings)
updateState()
})
ensureGlobalStyles()
registerHostAutofix(root, cfg)
registerHotkeys(root, cfg)
registerStorageListener(root, cfg)
registerUrlChangeListener(root, cfg)
try {
const mq = globalThis.matchMedia('(prefers-color-scheme: dark)')
mq.addEventListener('change', () => {
if ((settings.theme || 'system') === 'system') rerender(root, cfg)
})
} catch (e) {}
try {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') rerender(root, cfg)
})
} catch (e) {}
updateState()
})()
}
main()
})()