diff --git a/examples/plugin-git-ui/src/node/plugin.ts b/examples/plugin-git-ui/src/node/plugin.ts index 057799042..a116de629 100644 --- a/examples/plugin-git-ui/src/node/plugin.ts +++ b/examples/plugin-git-ui/src/node/plugin.ts @@ -131,7 +131,7 @@ export function GitUIPlugin(): PluginWithDevTools { title: 'Git', icon: 'ph:git-branch-duotone', category: 'app', - ui, + view: ui.view, badge: total > 0 ? String(total) : undefined, }) diff --git a/packages/core/assets/vite-devtools-kit-dark.svg b/packages/core/assets/vite-devtools-kit-dark.svg new file mode 100644 index 000000000..065a2795b --- /dev/null +++ b/packages/core/assets/vite-devtools-kit-dark.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/core/assets/vite-devtools-kit-light.svg b/packages/core/assets/vite-devtools-kit-light.svg new file mode 100644 index 000000000..f663cdd24 --- /dev/null +++ b/packages/core/assets/vite-devtools-kit-light.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/core/assets/vite-plus.svg b/packages/core/assets/vite-plus.svg new file mode 100644 index 000000000..433cfbfba --- /dev/null +++ b/packages/core/assets/vite-plus.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/core/package.json b/packages/core/package.json index c010d7afc..00f169fa6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,10 +23,6 @@ ".": "./dist/index.js", "./cli": "./dist/cli.js", "./cli-commands": "./dist/cli-commands.js", - "./client/inject": "./dist/client/inject.js", - "./client/inject-passive": "./dist/client/inject-passive.js", - "./client/inject-hidden": "./dist/client/inject-hidden.js", - "./client/webcomponents": "./dist/client/webcomponents.js", "./config": "./dist/config.js", "./dirs": "./dist/dirs.js", "./integration": "./dist/integration.js", @@ -43,11 +39,8 @@ "dist" ], "scripts": { - "build": "pnpm build:js && pnpm build:standalone", - "build:js": "tsdown --config-loader=unrun", - "build:standalone": "cd src/client/standalone && vite build", + "build": "tsdown --config-loader=unrun", "watch": "tsdown --watch --config-loader=unrun", - "dev:standalone": "cd src/client/standalone && vite dev", "prepack": "pnpm build", "cli": "DEBUG='vite:devtools:*' tsx src/node/cli.ts" }, @@ -74,16 +67,16 @@ }, "dependencies": { "@devframes/hub": "catalog:deps", + "@devframes/hub-ui": "catalog:deps", + "@devframes/json-render-ui": "catalog:deps", "@devframes/plugin-inspect": "catalog:deps", "@devframes/plugin-messages": "catalog:deps", "@devframes/plugin-terminals": "catalog:deps", "@vitejs/devtools-kit": "workspace:*", - "birpc": "catalog:deps", "cac": "catalog:deps", "devframe": "catalog:deps", "h3": "catalog:deps", "local-pkg": "catalog:deps", - "mlly": "catalog:deps", "nostics": "catalog:deps", "obug": "catalog:deps", "pathe": "catalog:deps", diff --git a/packages/core/scripts/check-client-dist.ts b/packages/core/scripts/check-client-dist.ts deleted file mode 100644 index 772b68483..000000000 --- a/packages/core/scripts/check-client-dist.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { readFile } from 'node:fs/promises' -import { dirname, relative, resolve } from 'node:path' -import { findDynamicImports, findExports, findStaticImports } from 'mlly' - -interface ForbiddenRule { - name: string - match: (specifier: string) => boolean -} - -const FORBIDDEN: ForbiddenRule[] = [ - { name: 'ws', match: id => id === 'ws' || id.startsWith('ws/') }, - { name: 'h3', match: id => id === 'h3' || id.startsWith('h3/') }, - { name: 'node:* builtin', match: id => id.startsWith('node:') }, - { name: 'devframe/rpc/transports/*', match: id => id.startsWith('devframe/rpc/transports/') }, - { name: 'devframe/node*', match: id => id === 'devframe/node' || id.startsWith('devframe/node/') }, -] - -interface ScannedSpecifiers { - static: string[] - dynamic: string[] -} - -interface Violation { - file: string - specifier: string - rule: string -} - -async function scanSpecifiers(file: string): Promise { - const code = await readFile(file, 'utf8') - const staticIds = new Set() - for (const i of findStaticImports(code)) - staticIds.add(i.specifier) - for (const e of findExports(code)) { - if (e.specifier) - staticIds.add(e.specifier) - } - const dynamicIds = new Set() - for (const d of findDynamicImports(code)) { - // Only consider plain string expressions; ignore variable/template imports. - const match = d.expression.match(/^\s*['"]([^'"]+)['"]\s*$/) - if (match?.[1]) - dynamicIds.add(match[1]) - } - return { static: [...staticIds], dynamic: [...dynamicIds] } -} - -export interface CheckClientDistOptions { - /** Absolute paths to the client entry chunks to walk from. */ - entries: string[] - /** Used to build relative paths in error messages. */ - cwd: string -} - -export async function checkClientDist(options: CheckClientDistOptions): Promise { - const { entries, cwd } = options - const visited = new Set() - const violations: Violation[] = [] - - async function visit(file: string): Promise { - if (visited.has(file)) - return - visited.add(file) - - let scanned: ScannedSpecifiers - try { - scanned = await scanSpecifiers(file) - } - catch (err) { - throw new Error(`[check-client-dist] Failed to read ${relative(cwd, file)}: ${(err as Error).message}`) - } - - // Static imports load eagerly when the file is evaluated — they're the leak - // vector this guard exists to catch. Flag any forbidden specifier. - for (const id of scanned.static) { - const hit = FORBIDDEN.find(r => r.match(id)) - if (hit) - violations.push({ file, specifier: id, rule: hit.name }) - } - - // Follow both static and dynamic relative imports to discover every chunk - // the browser can end up loading. Dynamic specifiers themselves aren't - // checked against FORBIDDEN — the chunk they target is, on visit. - for (const id of [...scanned.static, ...scanned.dynamic]) { - if (id.startsWith('./') || id.startsWith('../')) { - const next = resolve(dirname(file), id) - await visit(next) - } - } - } - - for (const entry of entries) - await visit(entry) - - if (violations.length > 0) { - const lines: string[] = ['[check-client-dist] Forbidden server-only imports found in client dist:', ''] - for (const v of violations) { - lines.push(` ${relative(cwd, v.file)}`) - lines.push(` imports ${JSON.stringify(v.specifier)} (matches forbidden rule: ${v.rule})`) - } - lines.push('') - lines.push(`Scanned ${visited.size} chunks reachable from ${entries.length} client entries.`) - lines.push('Client chunks must not statically import server-only modules — see packages/core/tsdown.config.ts.') - throw new Error(lines.join('\n')) - } - - console.log(`[check-client-dist] OK — scanned ${visited.size} chunks reachable from ${entries.length} client entries`) -} diff --git a/packages/core/src/client/inject-hidden/index.ts b/packages/core/src/client/inject-hidden/index.ts deleted file mode 100644 index 2adbcc6b2..000000000 --- a/packages/core/src/client/inject-hidden/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// - -import { startDevTools } from '../inject/runtime' - -// Hidden entry: keep the docks hidden and print a hint to reveal them with the -// activation shortcut — but never remember the reveal. Node injects this when -// the project's DevTools visibility resolves to `hidden`, so every load starts -// hidden and the shortcut opens the docks for that session only. -startDevTools('hidden') diff --git a/packages/core/src/client/inject-passive/index.ts b/packages/core/src/client/inject-passive/index.ts deleted file mode 100644 index a03f800d6..000000000 --- a/packages/core/src/client/inject-passive/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/// -/// - -import { startDevTools } from '../inject/runtime' - -// Passive entry: keep the docks hidden and print a hint to reveal them with the -// activation shortcut. Node injects this when the project's DevTools visibility -// resolves to `passive` (the default, until the developer opts in). -startDevTools('passive') diff --git a/packages/core/src/client/inject/index.ts b/packages/core/src/client/inject/index.ts deleted file mode 100644 index d26a30a19..000000000 --- a/packages/core/src/client/inject/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/// -/// - -import { startDevTools } from './runtime' - -// Normal entry: mount the floating docks immediately. Node injects this when -// the project's DevTools visibility resolves to `normal`. -startDevTools('normal') diff --git a/packages/core/src/client/inject/runtime.test.ts b/packages/core/src/client/inject/runtime.test.ts deleted file mode 100644 index d653aaf5e..000000000 --- a/packages/core/src/client/inject/runtime.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { startDevTools } from './runtime' - -const mocks = vi.hoisted(() => ({ - getDevToolsRpcClient: vi.fn( - (_options: { baseURL: string[] }) => new Promise(() => {}), - ), -})) - -vi.mock('@vitejs/devtools-kit/client', () => ({ - CLIENT_CONTEXT_KEY: '__VITE_DEVTOOLS_CLIENT_CONTEXT__', - getDevToolsRpcClient: mocks.getDevToolsRpcClient, -})) - -vi.mock('@vueuse/core', () => ({ - useLocalStorage: vi.fn(), -})) - -vi.mock('../webcomponents/state/context', () => ({ - createDocksContext: vi.fn(), -})) - -describe('injected DevTools runtime', () => { - beforeEach(() => { - const hostWindow = { - addEventListener: vi.fn(), - } as unknown as Window - Object.defineProperty(hostWindow, 'parent', { value: hostWindow }) - vi.stubGlobal('window', hostWindow) - - startDevTools('normal') - }) - - afterEach(() => { - vi.unstubAllGlobals() - mocks.getDevToolsRpcClient.mockClear() - }) - - it('preserves the host-relative mount path as the primary base', () => { - const options = mocks.getDevToolsRpcClient.mock.calls[0]![0] - - expect(options.baseURL).toHaveLength(2) - expect(options.baseURL[0]).toBe('/__devtools/') - }) - - it('adds the Vite module origin as the fallback base', () => { - const options = mocks.getDevToolsRpcClient.mock.calls[0]![0] - - expect(options.baseURL[1]).toBe( - new URL('/__devtools/', import.meta.url).href, - ) - }) -}) diff --git a/packages/core/src/client/inject/runtime.ts b/packages/core/src/client/inject/runtime.ts deleted file mode 100644 index 0a5d2c520..000000000 --- a/packages/core/src/client/inject/runtime.ts +++ /dev/null @@ -1,182 +0,0 @@ -/// -/// - -import type { DockPanelStorage } from '@vitejs/devtools-kit/client' -import { CLIENT_CONTEXT_KEY, getDevToolsRpcClient } from '@vitejs/devtools-kit/client' -import { DEVTOOLS_MOUNT_PATH } from '@vitejs/devtools-kit/constants' -import { useLocalStorage } from '@vueuse/core' -import { DEVTOOLS_HIDE_EVENT, DEVTOOLS_MODE_FILENAME } from '../../constants' -import { createDocksContext } from '../webcomponents/state/context' - -export type InjectMode = 'passive' | 'normal' | 'hidden' - -// Persistence endpoint the node middleware serves next to `__connection.json`. -// Node picks the client entry to inject from the persisted flag; this -// `POST { enabled }` is how the client writes that flag back when the developer -// activates the docks (passive mode) or hides them again. -const MODE_URL = `${DEVTOOLS_MOUNT_PATH}${DEVTOOLS_MODE_FILENAME}` - -const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform ?? '') - -// Activation combo mirrors Nuxt DevTools' Shift+Alt+D so muscle memory carries -// over. Displayed with platform-native glyphs. -const SHORTCUT_LABEL = isMac ? '⇧ ⌥ D' : 'Shift + Alt + D' - -let dockEl: HTMLElement | undefined -let shortcutListener: ((event: KeyboardEvent) => void) | undefined - -function matchesActivation(event: KeyboardEvent): boolean { - return event.altKey - && event.shiftKey - && !event.ctrlKey - && !event.metaKey - && (event.code === 'KeyD' || event.key === 'd' || event.key === 'D') -} - -async function persistNormalMode(enabled: boolean): Promise { - try { - await fetch(MODE_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ enabled }), - }) - } - catch { - // Persistence is best-effort — the overlay still toggles for this session. - } -} - -async function mountDock(): Promise { - if (dockEl) - return - - // Prefer the host page's origin for backwards compatibility. When the page - // is served by another backend, fall back to the Vite origin that loaded - // this module. - const rpc = await getDevToolsRpcClient({ - baseURL: [ - DEVTOOLS_MOUNT_PATH, - new URL(DEVTOOLS_MOUNT_PATH, import.meta.url).href, - ], - }) - - const state = useLocalStorage( - 'vite-devtools-dock-state', - { - mode: 'float', - width: 80, - height: 80, - top: 0, - left: 0, - position: 'left', - open: false, - inactiveTimeout: 3_000, - }, - { mergeDefaults: true }, - ) - - const context = await createDocksContext( - 'embedded', - rpc, - state, - ) - ;(globalThis as any)[CLIENT_CONTEXT_KEY] = context - - const { DockEmbedded } = import.meta.env.VITE_DEVTOOLS_LOCAL_DEV - ? await import('../webcomponents') - : await import('@vitejs/devtools/client/webcomponents') - - dockEl = new DockEmbedded({ context }) as unknown as HTMLElement - document.body.appendChild(dockEl) -} - -function unmountDock(): void { - dockEl?.remove() - dockEl = undefined -} - -function printPassiveHint(): void { - // eslint-disable-next-line no-console - console.log( - `%c Vite DevTools %c press %c${SHORTCUT_LABEL}%c to open`, - 'background:#646cff;color:#fff;padding:2px 6px;border-radius:4px;font-weight:bold', - 'color:inherit', - 'background:#2d2d2d;color:#fff;padding:1px 6px;border-radius:4px;font-family:monospace', - 'color:inherit', - ) -} - -function armPassive(persist: boolean): void { - printPassiveHint() - if (shortcutListener) - return - shortcutListener = (event) => { - if (!matchesActivation(event)) - return - event.preventDefault() - disarmPassive() - void activate(persist) - } - window.addEventListener('keydown', shortcutListener, true) -} - -function disarmPassive(): void { - if (!shortcutListener) - return - window.removeEventListener('keydown', shortcutListener, true) - shortcutListener = undefined -} - -// Reveal the docks. In passive mode this also remembers "normal mode" for the -// project so the next load injects the `inject` entry directly; in hidden mode -// (`persist` false) the reveal lasts only for this session. -async function activate(persist: boolean): Promise { - if (persist) - await persistNormalMode(true) - await mountDock() -} - -// Tear the docks down and re-arm the shortcut (the "Hide DevTools" command -// dispatches `DEVTOOLS_HIDE_EVENT` to reach us here). In passive mode this also -// clears the persisted flag so the project drops back to passive on next load. -async function deactivate(persist: boolean): Promise { - if (persist) - await persistNormalMode(false) - unmountDock() - armPassive(persist) -} - -/** - * Boot the injected overlay in the given mode: - * - * - `normal` mounts the docks immediately. - * - `passive` prints the activation hint and waits for the shortcut; revealing - * remembers "normal mode" for the project (next load starts shown). - * - `hidden` behaves like passive but never persists — the docks reveal on the - * shortcut for this session only, and every load starts hidden. - * - * In every mode the client can transition in-page (activate / the "Hide - * DevTools" command) without a reload. - */ -export function startDevTools(initialMode: InjectMode): void { - if (window.parent !== window) { - // eslint-disable-next-line no-console - console.log('[VITE DEVTOOLS] Skipping in iframe') - return - } - - // eslint-disable-next-line no-console - console.log('[VITE DEVTOOLS] Client injected') - - // Only passive mode remembers the reveal across loads; hidden is per-session. - const persist = initialMode === 'passive' - - window.addEventListener(DEVTOOLS_HIDE_EVENT, () => { - void deactivate(persist) - }) - - if (initialMode === 'normal') - void mountDock() - else - armPassive(persist) -} diff --git a/packages/core/src/client/standalone/App.vue b/packages/core/src/client/standalone/App.vue deleted file mode 100644 index 043d85e54..000000000 --- a/packages/core/src/client/standalone/App.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - diff --git a/packages/core/src/client/standalone/index.html b/packages/core/src/client/standalone/index.html deleted file mode 100644 index 8a74eac66..000000000 --- a/packages/core/src/client/standalone/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Vite DevTools - - - -
- - - diff --git a/packages/core/src/client/standalone/main.ts b/packages/core/src/client/standalone/main.ts deleted file mode 100644 index c274dffc3..000000000 --- a/packages/core/src/client/standalone/main.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createApp, h, Suspense } from 'vue' -import App from './App.vue' - -import '@unocss/reset/tailwind.css' -import './styles/main.css' -import '../webcomponents/style.css' -import 'uno.css' - -const app = createApp({ - render: () => h(Suspense, {}, { - default: () => h(App), - fallback: () => h('div', 'Loading...'), - }), -}) - -app.mount('#app') diff --git a/packages/core/src/client/standalone/styles/main.css b/packages/core/src/client/standalone/styles/main.css deleted file mode 100755 index 62167ab0f..000000000 --- a/packages/core/src/client/standalone/styles/main.css +++ /dev/null @@ -1,13 +0,0 @@ -html, -body, -#app { - height: 100%; - margin: 0; - padding: 0; -} - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } -} diff --git a/packages/core/src/client/standalone/uno.config.ts b/packages/core/src/client/standalone/uno.config.ts deleted file mode 100644 index c978fa56a..000000000 --- a/packages/core/src/client/standalone/uno.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { fileURLToPath } from 'node:url' -import { defineConfig } from 'unocss' -import config from '../webcomponents/uno.config' - -export default defineConfig({ - ...config, - content: { - filesystem: [ - fileURLToPath(new URL('../src/client/webcomponents/components/**/*.vue', import.meta.url)), - ], - }, -}) diff --git a/packages/core/src/client/standalone/vite.config.ts b/packages/core/src/client/standalone/vite.config.ts deleted file mode 100644 index 5752dc612..000000000 --- a/packages/core/src/client/standalone/vite.config.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { fileURLToPath } from 'node:url' -import { DEVTOOLS_MOUNT_PATH_NO_TRAILING_SLASH } from '@vitejs/devtools-kit/constants' -import Vue from '@vitejs/plugin-vue' -import UnoCSS from 'unocss/vite' -import { defineConfig } from 'vite' -import { createDevToolsContext } from '../../node/context' -import { createDevToolsMiddleware } from '../../node/server' - -export default defineConfig({ - build: { - outDir: fileURLToPath(new URL('../../../dist/client/standalone', import.meta.url)), - emptyOutDir: true, - }, - base: './', - plugins: [ - Vue(), - UnoCSS(), - { - name: 'setup', - async configureServer(viteDevServer) { - const context = await createDevToolsContext(viteDevServer.config, viteDevServer) - const host = viteDevServer.config.server.host === true - ? '0.0.0.0' - : viteDevServer.config.server.host || 'localhost' - const { middleware } = await createDevToolsMiddleware({ - cwd: viteDevServer.config.root, - websocket: { - host, - https: false, - }, - context, - }) - viteDevServer.middlewares.use(DEVTOOLS_MOUNT_PATH_NO_TRAILING_SLASH, middleware) - }, - }, - ], -}) diff --git a/packages/core/src/client/webcomponents/.generated/css.ts b/packages/core/src/client/webcomponents/.generated/css.ts deleted file mode 100644 index 0994cbef8..000000000 --- a/packages/core/src/client/webcomponents/.generated/css.ts +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable eslint-comments/no-unlimited-disable */ -/* eslint-disable */ -export default "*{box-sizing:border-box;border-style:solid;border-width:0;border-color:var(--un-default-border-color,#e5e7eb)}:before{box-sizing:border-box;border-style:solid;border-width:0;border-color:var(--un-default-border-color,#e5e7eb)}:after{box-sizing:border-box;border-style:solid;border-width:0;border-color:var(--un-default-border-color,#e5e7eb)}:before{--un-content:\"\"}:after{--un-content:\"\"}html{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}:host{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-feature-settings:normal;font-variation-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-feature-settings:inherit;font-variation-settings:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button{-webkit-appearance:button;background-color:transparent;background-image:none}[type=button]{-webkit-appearance:button;background-color:transparent;background-image:none}[type=reset]{-webkit-appearance:button;background-color:transparent;background-image:none}[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}dialog{padding:0}textarea{resize:vertical}input::placeholder{opacity:1;color:#9ca3af}textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--un-text-opacity:100%}:host{--app-scrollbar-size:6px;--app-scrollbar-radius:1px;--app-scrollbar-thumb:rgba(136,136,136,.267);--app-scrollbar-thumb-hover:rgba(136,136,136,.333)}::-webkit-scrollbar{width:var(--app-scrollbar-size)}::-webkit-scrollbar:horizontal{height:var(--app-scrollbar-size)}::-webkit-scrollbar-corner{background:0 0}::-webkit-scrollbar-thumb{background-color:var(--app-scrollbar-thumb);-webkit-border-radius:var(--app-scrollbar-radius);border-radius:var(--app-scrollbar-radius);transition:background .2s}::-webkit-scrollbar-thumb:hover{background-color:var(--app-scrollbar-thumb-hover)}::-webkit-scrollbar-track{-webkit-border-radius:var(--app-scrollbar-radius);border-radius:var(--app-scrollbar-radius);background:0 0}#vite-devtools-anchor{z-index:2147483644;box-sizing:border-box;transform-origin:50%;width:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-size:15px;position:fixed;transform:translate(-50%,-50%)rotate(0)}#vite-devtools-anchor #vite-devtools-dock-container{width:max-content;height:var(--vite-devtools-dock-height,40px);min-width:var(--vite-devtools-dock-min-width,100px);display:flex;position:absolute;top:0;left:0;transform:translate(-50%,-50%)}#vite-devtools-anchor.vite-devtools-vertical #vite-devtools-dock-container{transition-property:all;transition-duration:.5s;transition-timing-function:cubic-bezier(.4,0,.2,1);transform:translate(-50%,-50%)rotate(90deg)}#vite-devtools-anchor #vite-devtools-dock{touch-action:none;user-select:none;--vdt-backdrop-blur:blur(7px);height:100%;backdrop-filter:var(--vdt-backdrop-blur) var(--vdt-backdrop-brightness) var(--vdt-backdrop-contrast) var(--vdt-backdrop-grayscale) var(--vdt-backdrop-hue-rotate) var(--vdt-backdrop-invert) var(--vdt-backdrop-opacity) var(--vdt-backdrop-saturate) var(--vdt-backdrop-sepia);--vdt-text-opacity:1;color:rgba(51,51,51,var(--vdt-text-opacity));--vdt-shadow:var(--vdt-shadow-inset) 0 1px 3px 0 var(--vdt-shadow-color,rgba(0,0,0,.1)),var(--vdt-shadow-inset) 0 1px 2px -1px var(--vdt-shadow-color,rgba(0,0,0,.1));box-shadow:var(--vdt-ring-offset-shadow), var(--vdt-ring-shadow), var(--vdt-shadow);height:var(--vite-devtools-dock-height,40px);width:calc-size(max-content, size);background-color:#fff;-webkit-border-radius:9999px;border-radius:9999px;margin:auto;transition-property:all;transition-duration:.5s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dark #vite-devtools-anchor #vite-devtools-dock{--vdt-text-opacity:1;color:rgba(255,255,255,var(--vdt-text-opacity));background-color:rgba(5,5,5,.8)}#vite-devtools-anchor.vite-devtools-minimized #vite-devtools-dock{width:var(--vite-devtools-dock-minimized-size,22px);height:var(--vite-devtools-dock-minimized-size,22px);padding:2px 0}#vite-devtools-anchor.vite-devtools-minimized .vite-devtools-dock-bracket{opacity:.5;width:.375rem}#vite-devtools-anchor:hover #vite-devtools-glowing{opacity:.6}#vite-devtools-anchor #vite-devtools-glowing{pointer-events:none;z-index:-1;opacity:0;width:var(--vite-devtools-dock-glow-size,160px);height:var(--vite-devtools-dock-glow-size,160px);filter:blur(var(--vite-devtools-dock-glow-blur,60px));background-image:linear-gradient(45deg,#61d9ff,#6b84fd,#715ebd);-webkit-border-radius:9999px;border-radius:9999px;transition-property:all;transition-duration:1s;transition-timing-function:cubic-bezier(0,0,.2,1);position:absolute;top:0;left:0;transform:translate(-50%,-50%)}@media print{#vite-devtools-anchor{display:none}}.vite-devtools-resize-handle-horizontal{cursor:ns-resize;-webkit-border-radius:.375rem;border-radius:.375rem;height:10px;margin-top:-5px;margin-bottom:-5px;position:absolute;left:6px;right:6px}.vite-devtools-resize-handle-vertical{cursor:ew-resize;-webkit-border-radius:.375rem;border-radius:.375rem;width:10px;margin-left:-5px;margin-right:-5px;position:absolute;top:6px;bottom:0}.vite-devtools-resize-handle-corner{-webkit-border-radius:.375rem;border-radius:.375rem;width:14px;height:14px;margin:-6px;position:absolute}.vite-devtools-resize-handle{z-index:30}.vite-devtools-resize-handle:hover{background-color:rgba(156,163,175,.1)}@keyframes vite-devtools-shake{0%,to{transform:translate(0)}20%,60%{transform:translate(-5px)}40%,80%{transform:translate(5px)}}.vite-devtools-shake{animation:.4s cubic-bezier(.36,.07,.19,.97) vite-devtools-shake}@media (prefers-reduced-motion:reduce){.vite-devtools-shake{animation:none}}*{--vdt-rotate:0;--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-scale-x:1;--vdt-scale-y:1;--vdt-scale-z:1;--vdt-skew-x:0;--vdt-skew-y:0;--vdt-translate-x:0;--vdt-translate-y:0;--vdt-translate-z:0;--vdt-pan-x: ;--vdt-pan-y: ;--vdt-pinch-zoom: ;--vdt-scroll-snap-strictness:proximity;--vdt-ordinal: ;--vdt-slashed-zero: ;--vdt-numeric-figure: ;--vdt-numeric-spacing: ;--vdt-numeric-fraction: ;--vdt-border-spacing-x:0;--vdt-border-spacing-y:0;--vdt-ring-offset-shadow:0 0 transparent;--vdt-ring-shadow:0 0 transparent;--vdt-shadow-inset: ;--vdt-shadow:0 0 transparent;--vdt-ring-inset: ;--vdt-ring-offset-width:0px;--vdt-ring-offset-color:#fff;--vdt-ring-width:0px;--vdt-ring-color:rgba(147,197,253,.5);--vdt-blur: ;--vdt-brightness: ;--vdt-contrast: ;--vdt-drop-shadow: ;--vdt-grayscale: ;--vdt-hue-rotate: ;--vdt-invert: ;--vdt-saturate: ;--vdt-sepia: ;--vdt-backdrop-blur: ;--vdt-backdrop-brightness: ;--vdt-backdrop-contrast: ;--vdt-backdrop-grayscale: ;--vdt-backdrop-hue-rotate: ;--vdt-backdrop-invert: ;--vdt-backdrop-opacity: ;--vdt-backdrop-saturate: ;--vdt-backdrop-sepia: }:before{--vdt-rotate:0;--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-scale-x:1;--vdt-scale-y:1;--vdt-scale-z:1;--vdt-skew-x:0;--vdt-skew-y:0;--vdt-translate-x:0;--vdt-translate-y:0;--vdt-translate-z:0;--vdt-pan-x: ;--vdt-pan-y: ;--vdt-pinch-zoom: ;--vdt-scroll-snap-strictness:proximity;--vdt-ordinal: ;--vdt-slashed-zero: ;--vdt-numeric-figure: ;--vdt-numeric-spacing: ;--vdt-numeric-fraction: ;--vdt-border-spacing-x:0;--vdt-border-spacing-y:0;--vdt-ring-offset-shadow:0 0 transparent;--vdt-ring-shadow:0 0 transparent;--vdt-shadow-inset: ;--vdt-shadow:0 0 transparent;--vdt-ring-inset: ;--vdt-ring-offset-width:0px;--vdt-ring-offset-color:#fff;--vdt-ring-width:0px;--vdt-ring-color:rgba(147,197,253,.5);--vdt-blur: ;--vdt-brightness: ;--vdt-contrast: ;--vdt-drop-shadow: ;--vdt-grayscale: ;--vdt-hue-rotate: ;--vdt-invert: ;--vdt-saturate: ;--vdt-sepia: ;--vdt-backdrop-blur: ;--vdt-backdrop-brightness: ;--vdt-backdrop-contrast: ;--vdt-backdrop-grayscale: ;--vdt-backdrop-hue-rotate: ;--vdt-backdrop-invert: ;--vdt-backdrop-opacity: ;--vdt-backdrop-saturate: ;--vdt-backdrop-sepia: }:after{--vdt-rotate:0;--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-scale-x:1;--vdt-scale-y:1;--vdt-scale-z:1;--vdt-skew-x:0;--vdt-skew-y:0;--vdt-translate-x:0;--vdt-translate-y:0;--vdt-translate-z:0;--vdt-pan-x: ;--vdt-pan-y: ;--vdt-pinch-zoom: ;--vdt-scroll-snap-strictness:proximity;--vdt-ordinal: ;--vdt-slashed-zero: ;--vdt-numeric-figure: ;--vdt-numeric-spacing: ;--vdt-numeric-fraction: ;--vdt-border-spacing-x:0;--vdt-border-spacing-y:0;--vdt-ring-offset-shadow:0 0 transparent;--vdt-ring-shadow:0 0 transparent;--vdt-shadow-inset: ;--vdt-shadow:0 0 transparent;--vdt-ring-inset: ;--vdt-ring-offset-width:0px;--vdt-ring-offset-color:#fff;--vdt-ring-width:0px;--vdt-ring-color:rgba(147,197,253,.5);--vdt-blur: ;--vdt-brightness: ;--vdt-contrast: ;--vdt-drop-shadow: ;--vdt-grayscale: ;--vdt-hue-rotate: ;--vdt-invert: ;--vdt-saturate: ;--vdt-sepia: ;--vdt-backdrop-blur: ;--vdt-backdrop-brightness: ;--vdt-backdrop-contrast: ;--vdt-backdrop-grayscale: ;--vdt-backdrop-hue-rotate: ;--vdt-backdrop-invert: ;--vdt-backdrop-opacity: ;--vdt-backdrop-saturate: ;--vdt-backdrop-sepia: }::backdrop{--vdt-rotate:0;--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-scale-x:1;--vdt-scale-y:1;--vdt-scale-z:1;--vdt-skew-x:0;--vdt-skew-y:0;--vdt-translate-x:0;--vdt-translate-y:0;--vdt-translate-z:0;--vdt-pan-x: ;--vdt-pan-y: ;--vdt-pinch-zoom: ;--vdt-scroll-snap-strictness:proximity;--vdt-ordinal: ;--vdt-slashed-zero: ;--vdt-numeric-figure: ;--vdt-numeric-spacing: ;--vdt-numeric-fraction: ;--vdt-border-spacing-x:0;--vdt-border-spacing-y:0;--vdt-ring-offset-shadow:0 0 transparent;--vdt-ring-shadow:0 0 transparent;--vdt-shadow-inset: ;--vdt-shadow:0 0 transparent;--vdt-ring-inset: ;--vdt-ring-offset-width:0px;--vdt-ring-offset-color:#fff;--vdt-ring-width:0px;--vdt-ring-color:rgba(147,197,253,.5);--vdt-blur: ;--vdt-brightness: ;--vdt-contrast: ;--vdt-drop-shadow: ;--vdt-grayscale: ;--vdt-hue-rotate: ;--vdt-invert: ;--vdt-saturate: ;--vdt-sepia: ;--vdt-backdrop-blur: ;--vdt-backdrop-brightness: ;--vdt-backdrop-contrast: ;--vdt-backdrop-grayscale: ;--vdt-backdrop-hue-rotate: ;--vdt-backdrop-invert: ;--vdt-backdrop-opacity: ;--vdt-backdrop-saturate: ;--vdt-backdrop-sepia: }.i-fluent-emoji-flat-warning{background:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 32 32' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='none'%3E%3Cpath fill='%23FFB02E' d='m14.839 5.668l-12.66 21.93c-.51.89.13 2.01 1.16 2.01h25.32c1.03 0 1.67-1.11 1.16-2.01l-12.66-21.93c-.52-.89-1.8-.89-2.32 0'/%3E%3Cpath fill='%23000' d='M14.599 21.498a1.4 1.4 0 1 0 2.8-.01v-9.16c0-.77-.62-1.4-1.4-1.4c-.77 0-1.4.62-1.4 1.4zm2.8 3.98a1.4 1.4 0 1 1-2.8 0a1.4 1.4 0 0 1 2.8 0'/%3E%3C/g%3E%3C/svg%3E\") 0 0/100% 100% no-repeat;width:1em;height:1em}.i-ph-arrow-clockwise{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M240 56v48a8 8 0 0 1-8 8h-48a8 8 0 0 1 0-16h27.4l-26.59-24.36l-.25-.24a80 80 0 1 0-1.67 114.78a8 8 0 0 1 11 11.63A95.44 95.44 0 0 1 128 224h-1.32a96 96 0 1 1 69.07-164L224 85.8V56a8 8 0 1 1 16 0'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-arrow-clockwise-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88' opacity='.2'/%3E%3Cpath d='M240 56v48a8 8 0 0 1-8 8h-48a8 8 0 0 1 0-16h27.4l-26.59-24.36l-.25-.24a80 80 0 1 0-1.67 114.78a8 8 0 0 1 11 11.63A95.44 95.44 0 0 1 128 224h-1.32a96 96 0 1 1 69.07-164L224 85.8V56a8 8 0 1 1 16 0'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-arrow-counter-clockwise{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M224 128a96 96 0 0 1-94.71 96H128a95.38 95.38 0 0 1-65.9-26.2a8 8 0 0 1 11-11.63a80 80 0 1 0-1.67-114.78a3 3 0 0 1-.26.25L44.59 96H72a8 8 0 0 1 0 16H24a8 8 0 0 1-8-8V56a8 8 0 0 1 16 0v29.8L60.25 60A96 96 0 0 1 224 128'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-arrow-square-out-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M184 80v128a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8V80a8 8 0 0 1 8-8h128a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M224 104a8 8 0 0 1-16 0V59.32l-66.33 66.34a8 8 0 0 1-11.32-11.32L196.68 48H152a8 8 0 0 1 0-16h64a8 8 0 0 1 8 8Zm-40 24a8 8 0 0 0-8 8v72H48V80h72a8 8 0 0 0 0-16H48a16 16 0 0 0-16 16v128a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-72a8 8 0 0 0-8-8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-arrows-counter-clockwise-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88' opacity='.2'/%3E%3Cpath d='M88 104H40a8 8 0 0 1-8-8V48a8 8 0 0 1 16 0v28.69l14.63-14.63A95.43 95.43 0 0 1 130 33.94h.53a95.36 95.36 0 0 1 67.07 27.33a8 8 0 0 1-11.18 11.44a79.52 79.52 0 0 0-55.89-22.77h-.45a79.56 79.56 0 0 0-56.14 23.43L59.31 88H88a8 8 0 0 1 0 16m128 48h-48a8 8 0 0 0 0 16h28.69l-14.63 14.63a79.56 79.56 0 0 1-56.13 23.43h-.45a79.52 79.52 0 0 1-55.89-22.77a8 8 0 1 0-11.18 11.44a95.36 95.36 0 0 0 67.07 27.33h.52a95.43 95.43 0 0 0 67.36-28.12L208 179.31V208a8 8 0 0 0 16 0v-48a8 8 0 0 0-8-8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-arrows-out-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M208 48v160H48V48Z' opacity='.2'/%3E%3Cpath d='M216 48v48a8 8 0 0 1-16 0V67.31l-42.34 42.35a8 8 0 0 1-11.32-11.32L188.69 56H160a8 8 0 0 1 0-16h48a8 8 0 0 1 8 8M98.34 146.34L56 188.69V160a8 8 0 0 0-16 0v48a8 8 0 0 0 8 8h48a8 8 0 0 0 0-16H67.31l42.35-42.34a8 8 0 0 0-11.32-11.32M208 152a8 8 0 0 0-8 8v28.69l-42.34-42.35a8 8 0 0 0-11.32 11.32L188.69 200H160a8 8 0 0 0 0 16h48a8 8 0 0 0 8-8v-48a8 8 0 0 0-8-8M67.31 56H96a8 8 0 0 0 0-16H48a8 8 0 0 0-8 8v48a8 8 0 0 0 16 0V67.31l42.34 42.35a8 8 0 0 0 11.32-11.32Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-cards-three-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 104v96a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8v-96a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M208 88H48a16 16 0 0 0-16 16v96a16 16 0 0 0 16 16h160a16 16 0 0 0 16-16v-96a16 16 0 0 0-16-16m0 112H48v-96h160zM48 64a8 8 0 0 1 8-8h144a8 8 0 0 1 0 16H56a8 8 0 0 1-8-8m16-32a8 8 0 0 1 8-8h112a8 8 0 0 1 0 16H72a8 8 0 0 1-8-8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-caret-down{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m213.66 101.66l-80 80a8 8 0 0 1-11.32 0l-80-80a8 8 0 0 1 11.32-11.32L128 164.69l74.34-74.35a8 8 0 0 1 11.32 11.32'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-caret-left{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M165.66 202.34a8 8 0 0 1-11.32 11.32l-80-80a8 8 0 0 1 0-11.32l80-80a8 8 0 0 1 11.32 11.32L91.31 128Z'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-caret-right{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m181.66 133.66l-80 80a8 8 0 0 1-11.32-11.32L164.69 128L90.34 53.66a8 8 0 0 1 11.32-11.32l80 80a8 8 0 0 1 0 11.32'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-caret-up{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M213.66 165.66a8 8 0 0 1-11.32 0L128 91.31l-74.34 74.35a8 8 0 0 1-11.32-11.32l80-80a8 8 0 0 1 11.32 0l80 80a8 8 0 0 1 0 11.32'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-check-bold{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m232.49 80.49l-128 128a12 12 0 0 1-17 0l-56-56a12 12 0 1 1 17-17L96 183L215.51 63.51a12 12 0 0 1 17 17Z'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-circle-notch{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M232 128a104 104 0 0 1-208 0c0-41 23.81-78.36 60.66-95.27a8 8 0 0 1 6.68 14.54C60.15 61.59 40 93.27 40 128a88 88 0 0 0 176 0c0-34.73-20.15-66.41-51.34-80.73a8 8 0 0 1 6.68-14.54C208.19 49.64 232 87 232 128'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-dots-six-vertical{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M104 60a12 12 0 1 1-12-12a12 12 0 0 1 12 12m60 12a12 12 0 1 0-12-12a12 12 0 0 0 12 12m-72 44a12 12 0 1 0 12 12a12 12 0 0 0-12-12m72 0a12 12 0 1 0 12 12a12 12 0 0 0-12-12m-72 68a12 12 0 1 0 12 12a12 12 0 0 0-12-12m72 0a12 12 0 1 0 12 12a12 12 0 0 0-12-12'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-eye-slash{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M53.92 34.62a8 8 0 1 0-11.84 10.76l19.24 21.17C25 88.84 9.38 123.2 8.69 124.76a8 8 0 0 0 0 6.5c.35.79 8.82 19.57 27.65 38.4C61.43 194.74 93.12 208 128 208a127.1 127.1 0 0 0 52.07-10.83l22 24.21a8 8 0 1 0 11.84-10.76Zm47.33 75.84l41.67 45.85a32 32 0 0 1-41.67-45.85M128 192c-30.78 0-57.67-11.19-79.93-33.25A133.2 133.2 0 0 1 25 128c4.69-8.79 19.66-33.39 47.35-49.38l18 19.75a48 48 0 0 0 63.66 70l14.73 16.2A112 112 0 0 1 128 192m6-95.43a8 8 0 0 1 3-15.72a48.16 48.16 0 0 1 38.77 42.64a8 8 0 0 1-7.22 8.71a6 6 0 0 1-.75 0a8 8 0 0 1-8-7.26A32.09 32.09 0 0 0 134 96.57m113.28 34.69c-.42.94-10.55 23.37-33.36 43.8a8 8 0 1 1-10.67-11.92a132.8 132.8 0 0 0 27.8-35.14a133.2 133.2 0 0 0-23.12-30.77C185.67 75.19 158.78 64 128 64a118.4 118.4 0 0 0-19.36 1.57A8 8 0 1 1 106 49.79A134 134 0 0 1 128 48c34.88 0 66.57 13.26 91.66 38.35c18.83 18.83 27.3 37.62 27.65 38.41a8 8 0 0 1 0 6.5Z'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-eye-slash-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M128 56c-80 0-112 72-112 72s32 72 112 72s112-72 112-72s-32-72-112-72m0 112a40 40 0 1 1 40-40a40 40 0 0 1-40 40' opacity='.2'/%3E%3Cpath d='M53.92 34.62a8 8 0 1 0-11.84 10.76l19.24 21.17C25 88.84 9.38 123.2 8.69 124.76a8 8 0 0 0 0 6.5c.35.79 8.82 19.57 27.65 38.4C61.43 194.74 93.12 208 128 208a127.1 127.1 0 0 0 52.07-10.83l22 24.21a8 8 0 1 0 11.84-10.76Zm47.33 75.84l41.67 45.85a32 32 0 0 1-41.67-45.85M128 192c-30.78 0-57.67-11.19-79.93-33.25A133.2 133.2 0 0 1 25 128c4.69-8.79 19.66-33.39 47.35-49.38l18 19.75a48 48 0 0 0 63.66 70l14.73 16.2A112 112 0 0 1 128 192m6-95.43a8 8 0 0 1 3-15.72a48.16 48.16 0 0 1 38.77 42.64a8 8 0 0 1-7.22 8.71a6 6 0 0 1-.75 0a8 8 0 0 1-8-7.26A32.09 32.09 0 0 0 134 96.57m113.28 34.69c-.42.94-10.55 23.37-33.36 43.8a8 8 0 1 1-10.67-11.92a132.8 132.8 0 0 0 27.8-35.14a133.2 133.2 0 0 0-23.12-30.77C185.67 75.19 158.78 64 128 64a118.4 118.4 0 0 0-19.36 1.57A8 8 0 1 1 106 49.79A134 134 0 0 1 128 48c34.88 0 66.57 13.26 91.66 38.35c18.83 18.83 27.3 37.62 27.65 38.41a8 8 0 0 1 0 6.5Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-globe{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M128 24a104 104 0 1 0 104 104A104.12 104.12 0 0 0 128 24m88 104a87.6 87.6 0 0 1-3.33 24h-38.51a157.4 157.4 0 0 0 0-48h38.51a87.6 87.6 0 0 1 3.33 24m-114 40h52a115.1 115.1 0 0 1-26 45a115.3 115.3 0 0 1-26-45m-3.9-16a140.8 140.8 0 0 1 0-48h59.88a140.8 140.8 0 0 1 0 48ZM40 128a87.6 87.6 0 0 1 3.33-24h38.51a157.4 157.4 0 0 0 0 48H43.33A87.6 87.6 0 0 1 40 128m114-40h-52a115.1 115.1 0 0 1 26-45a115.3 115.3 0 0 1 26 45m52.33 0h-35.62a135.3 135.3 0 0 0-22.3-45.6A88.29 88.29 0 0 1 206.37 88Zm-98.74-45.6A135.3 135.3 0 0 0 85.29 88H49.63a88.29 88.29 0 0 1 57.96-45.6M49.63 168h35.66a135.3 135.3 0 0 0 22.3 45.6A88.29 88.29 0 0 1 49.63 168m98.78 45.6a135.3 135.3 0 0 0 22.3-45.6h35.66a88.29 88.29 0 0 1-57.96 45.6'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-keyboard-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M232 64v128a8 8 0 0 1-8 8H32a8 8 0 0 1-8-8V64a8 8 0 0 1 8-8h192a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M224 48H32a16 16 0 0 0-16 16v128a16 16 0 0 0 16 16h192a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16m0 144H32V64h192zm-16-64a8 8 0 0 1-8 8H56a8 8 0 0 1 0-16h144a8 8 0 0 1 8 8m0-32a8 8 0 0 1-8 8H56a8 8 0 0 1 0-16h144a8 8 0 0 1 8 8M72 160a8 8 0 0 1-8 8h-8a8 8 0 0 1 0-16h8a8 8 0 0 1 8 8m96 0a8 8 0 0 1-8 8H96a8 8 0 0 1 0-16h64a8 8 0 0 1 8 8m40 0a8 8 0 0 1-8 8h-8a8 8 0 0 1 0-16h8a8 8 0 0 1 8 8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-laptop-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 72v104H40V72a16 16 0 0 1 16-16h144a16 16 0 0 1 16 16' opacity='.2'/%3E%3Cpath d='M232 168h-8V72a24 24 0 0 0-24-24H56a24 24 0 0 0-24 24v96h-8a8 8 0 0 0-8 8v16a24 24 0 0 0 24 24h176a24 24 0 0 0 24-24v-16a8 8 0 0 0-8-8M48 72a8 8 0 0 1 8-8h144a8 8 0 0 1 8 8v96H48Zm176 120a8 8 0 0 1-8 8H40a8 8 0 0 1-8-8v-8h192ZM152 88a8 8 0 0 1-8 8h-32a8 8 0 0 1 0-16h32a8 8 0 0 1 8 8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-layout-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M104 104v104H40a8 8 0 0 1-8-8v-96Z' opacity='.2'/%3E%3Cpath d='M216 40H40a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16m0 16v40H40V56ZM40 112h56v88H40Zm176 88H112v-88h104z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-magnifying-glass-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M192 112a80 80 0 1 1-80-80a80 80 0 0 1 80 80' opacity='.2'/%3E%3Cpath d='m229.66 218.34l-50.06-50.06a88.21 88.21 0 1 0-11.32 11.31l50.06 50.07a8 8 0 0 0 11.32-11.32M40 112a72 72 0 1 1 72 72a72.08 72.08 0 0 1-72-72'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-moon-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M227.89 147.89A96 96 0 1 1 108.11 28.11a96.09 96.09 0 0 0 119.78 119.78' opacity='.2'/%3E%3Cpath d='M233.54 142.23a8 8 0 0 0-8-2a88.08 88.08 0 0 1-109.8-109.8a8 8 0 0 0-10-10a104.84 104.84 0 0 0-52.91 37A104 104 0 0 0 136 224a103.1 103.1 0 0 0 62.52-20.88a104.84 104.84 0 0 0 37-52.91a8 8 0 0 0-1.98-7.98m-44.64 48.11A88 88 0 0 1 65.66 67.11a89 89 0 0 1 31.4-26A106 106 0 0 0 96 56a104.11 104.11 0 0 0 104 104a106 106 0 0 0 14.92-1.06a89 89 0 0 1-26.02 31.4'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-paint-brush-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 32c0 32.81-31.64 67.43-58.64 91.05A84.4 84.4 0 0 0 133 90.64c23.57-27 58.19-58.64 91-58.64' opacity='.2'/%3E%3Cpath d='M232 32a8 8 0 0 0-8-8c-44.08 0-89.31 49.71-114.43 82.63A60 60 0 0 0 32 164c0 30.88-19.54 44.73-20.47 45.37A8 8 0 0 0 16 224h76a60 60 0 0 0 57.37-77.57C182.3 121.31 232 76.08 232 32M92 208H34.63C41.38 198.41 48 183.92 48 164a44 44 0 1 1 44 44m32.42-94.45q5.14-6.66 10.09-12.55A76.2 76.2 0 0 1 155 121.49q-5.9 4.94-12.55 10.09a60.5 60.5 0 0 0-18.03-18.03m42.7-2.68a92.6 92.6 0 0 0-22-22c31.78-34.53 55.75-45 69.9-47.91c-2.85 14.16-13.37 38.13-47.9 69.91'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-push-pin{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m235.32 81.37l-60.69-60.68a16 16 0 0 0-22.63 0l-53.63 53.8c-10.66-3.34-35-7.37-60.4 13.14a16 16 0 0 0-1.29 23.78L85 159.71l-42.66 42.63a8 8 0 0 0 11.32 11.32L96.29 171l48.29 48.29A16 16 0 0 0 155.9 224h1.13a15.93 15.93 0 0 0 11.64-6.33c19.64-26.1 17.75-47.32 13.19-60L235.33 104a16 16 0 0 0-.01-22.63M224 92.69l-57.27 57.46a8 8 0 0 0-1.49 9.22c9.46 18.93-1.8 38.59-9.34 48.62L48 100.08c12.08-9.74 23.64-12.31 32.48-12.31A40.1 40.1 0 0 1 96.81 91a8 8 0 0 0 9.25-1.51L163.32 32L224 92.68Z'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-push-pin-fill{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m235.33 104l-53.47 53.65c4.56 12.67 6.45 33.89-13.19 60A15.93 15.93 0 0 1 157 224h-1.13a16 16 0 0 1-11.32-4.69L96.29 171l-42.63 42.66a8 8 0 0 1-11.32-11.32L85 159.71l-48.3-48.3A16 16 0 0 1 38 87.63c25.42-20.51 49.75-16.48 60.4-13.14L152 20.7a16 16 0 0 1 22.63 0l60.69 60.68a16 16 0 0 1 .01 22.62'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-rocket-launch-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M184 120v61.65a8 8 0 0 1-2.34 5.65l-34.35 34.35a8 8 0 0 1-13.57-4.53L128 176Zm-48-48H74.35a8 8 0 0 0-5.65 2.34l-34.35 34.35a8 8 0 0 0 4.53 13.57L80 128ZM40 216c37.65 0 50.69-19.69 54.56-28.18l-26.38-26.38C59.69 165.31 40 178.35 40 216' opacity='.2'/%3E%3Cpath d='M223.85 47.12a16 16 0 0 0-15-15c-12.58-.75-44.73.4-71.41 27.07L132.69 64H74.36A15.9 15.9 0 0 0 63 68.68L28.7 103a16 16 0 0 0 9.07 27.16l38.47 5.37l44.21 44.21l5.37 38.49a15.94 15.94 0 0 0 10.78 12.92a16.1 16.1 0 0 0 5.1.83a15.9 15.9 0 0 0 11.3-4.68l34.32-34.3a15.9 15.9 0 0 0 4.68-11.36v-58.33l4.77-4.77c26.68-26.68 27.83-58.83 27.08-71.42M74.36 80h42.33l-39.53 39.52L40 114.34Zm74.41-9.45a76.65 76.65 0 0 1 59.11-22.47a76.46 76.46 0 0 1-22.42 59.16L128 164.68L91.32 128ZM176 181.64L141.67 216l-5.19-37.17L176 139.31Zm-74.16 9.5C97.34 201 82.29 224 40 224a8 8 0 0 1-8-8c0-42.29 23-57.34 32.86-61.85a8 8 0 0 1 6.64 14.56c-6.43 2.93-20.62 12.36-23.12 38.91c26.55-2.5 36-16.69 38.91-23.12a8 8 0 1 1 14.56 6.64Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-shield-check-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 56v56c0 96-88 120-88 120s-88-24-88-120V56a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M208 40H48a16 16 0 0 0-16 16v56c0 52.72 25.52 84.67 46.93 102.19c23.06 18.86 46 25.26 47 25.53a8 8 0 0 0 4.2 0c1-.27 23.91-6.67 47-25.53C198.48 196.67 224 164.72 224 112V56a16 16 0 0 0-16-16m0 72c0 37.07-13.66 67.16-40.6 89.42a129.3 129.3 0 0 1-39.4 22.2a128.3 128.3 0 0 1-38.92-21.81C61.82 179.51 48 149.3 48 112V56h160ZM82.34 141.66a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 11.32l-56 56a8 8 0 0 1-11.32 0Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-sign-out-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 56v144a16 16 0 0 1-16 16H48V40h160a16 16 0 0 1 16 16' opacity='.2'/%3E%3Cpath d='M120 216a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8V40a8 8 0 0 1 8-8h64a8 8 0 0 1 0 16H56v160h56a8 8 0 0 1 8 8m109.66-93.66l-40-40a8 8 0 0 0-11.32 11.32L204.69 120H112a8 8 0 0 0 0 16h92.69l-26.35 26.34a8 8 0 0 0 11.32 11.32l40-40a8 8 0 0 0 0-11.32'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-spinner-gap-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M136 32v32a8 8 0 0 1-16 0V32a8 8 0 0 1 16 0m88 88h-32a8 8 0 0 0 0 16h32a8 8 0 0 0 0-16m-45.09 47.6a8 8 0 0 0-11.31 11.31l22.62 22.63a8 8 0 0 0 11.32-11.32ZM128 184a8 8 0 0 0-8 8v32a8 8 0 0 0 16 0v-32a8 8 0 0 0-8-8m-50.91-16.4l-22.63 22.62a8 8 0 0 0 11.32 11.32l22.62-22.63a8 8 0 0 0-11.31-11.31M72 128a8 8 0 0 0-8-8H32a8 8 0 0 0 0 16h32a8 8 0 0 0 8-8m-6.22-73.54a8 8 0 0 0-11.32 11.32L77.09 88.4A8 8 0 0 0 88.4 77.09Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-square-half-bottom-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M208 128v72a8 8 0 0 1-8 8H56a8 8 0 0 1-8-8v-72Z' opacity='.2'/%3E%3Cpath d='M200 40H56a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h144a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16m0 16v64H56V56Zm0 144H56v-64h144z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-sun-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M184 128a56 56 0 1 1-56-56a56 56 0 0 1 56 56' opacity='.2'/%3E%3Cpath d='M120 40V16a8 8 0 0 1 16 0v24a8 8 0 0 1-16 0m72 88a64 64 0 1 1-64-64a64.07 64.07 0 0 1 64 64m-16 0a48 48 0 1 0-48 48a48.05 48.05 0 0 0 48-48M58.34 69.66a8 8 0 0 0 11.32-11.32l-16-16a8 8 0 0 0-11.32 11.32Zm0 116.68l-16 16a8 8 0 0 0 11.32 11.32l16-16a8 8 0 0 0-11.32-11.32M192 72a8 8 0 0 0 5.66-2.34l16-16a8 8 0 0 0-11.32-11.32l-16 16A8 8 0 0 0 192 72m5.66 114.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32-11.32ZM48 128a8 8 0 0 0-8-8H16a8 8 0 0 0 0 16h24a8 8 0 0 0 8-8m80 80a8 8 0 0 0-8 8v24a8 8 0 0 0 16 0v-24a8 8 0 0 0-8-8m112-88h-24a8 8 0 0 0 0 16h24a8 8 0 0 0 0-16'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-warning-duotone,.i-ph\\:warning-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M215.46 216H40.54c-12.62 0-20.54-13.21-14.41-23.91l87.46-151.87c6.3-11 22.52-11 28.82 0l87.46 151.87c6.13 10.7-1.79 23.91-14.41 23.91' opacity='.2'/%3E%3Cpath d='M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72m-13.87 15.71a8.5 8.5 0 0 1-7.48 4.2H40.55a8.5 8.5 0 0 1-7.48-4.2a7.59 7.59 0 0 1 0-7.72l87.45-151.87a8.75 8.75 0 0 1 15 0l87.45 151.87a7.59 7.59 0 0 1-.04 7.72M120 144v-40a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0m20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-wrench-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 96a64 64 0 0 1-94.94 56L73 217a24 24 0 0 1-34-34l65-56.06a64 64 0 0 1 80-90.29L144 80l5.66 26.34L176 112l43.35-40A63.8 63.8 0 0 1 224 96' opacity='.2'/%3E%3Cpath d='M226.76 69a8 8 0 0 0-12.84-2.88l-40.3 37.19l-17.23-3.7l-3.7-17.23l37.19-40.3A8 8 0 0 0 187 29.24A72 72 0 0 0 88 96a72.3 72.3 0 0 0 6 28.94L33.79 177c-.15.12-.29.26-.43.39a32 32 0 0 0 45.26 45.26c.13-.13.27-.28.39-.42L131.06 162A72 72 0 0 0 232 96a71.6 71.6 0 0 0-5.24-27M160 152a56.14 56.14 0 0 1-27.07-7a8 8 0 0 0-9.92 1.77l-55.9 64.74a16 16 0 0 1-22.62-22.62L109.18 133a8 8 0 0 0 1.77-9.93a56 56 0 0 1 58.36-82.31l-31.2 33.81a8 8 0 0 0-1.94 7.1l5.66 26.33a8 8 0 0 0 6.14 6.14l26.35 5.66a8 8 0 0 0 7.1-1.94l33.81-31.2A56.06 56.06 0 0 1 160 152'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph-x{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128L50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph\\:bug-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M208 128v16a80 80 0 0 1-160 0v-16Z' opacity='.2'/%3E%3Cpath d='M144 92a12 12 0 1 1 12 12a12 12 0 0 1-12-12m-44-12a12 12 0 1 0 12 12a12 12 0 0 0-12-12m116 64a87.8 87.8 0 0 1-3 23l22.24 9.72A8 8 0 0 1 232 192a7.9 7.9 0 0 1-3.2-.67L207.38 182a88 88 0 0 1-158.76 0l-21.42 9.33a7.9 7.9 0 0 1-3.2.67a8 8 0 0 1-3.2-15.33L43 167a87.8 87.8 0 0 1-3-23v-8H16a8 8 0 0 1 0-16h24v-8a87.8 87.8 0 0 1 3-23l-22.2-9.67a8 8 0 1 1 6.4-14.66L48.62 74a88 88 0 0 1 158.76 0l21.42-9.36a8 8 0 0 1 6.4 14.66L213 89.05a87.8 87.8 0 0 1 3 23v8h24a8 8 0 0 1 0 16h-24ZM56 120h144v-8a72 72 0 0 0-144 0Zm64 95.54V136H56v8a72.08 72.08 0 0 0 64 71.54M200 144v-8h-64v79.54A72.08 72.08 0 0 0 200 144'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph\\:check{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m229.66 77.66l-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69L218.34 66.34a8 8 0 0 1 11.32 11.32'/%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph\\:check-circle-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M173.66 98.34a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph\\:globe-simple-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M128 24a104 104 0 1 0 104 104A104.12 104.12 0 0 0 128 24m87.62 96h-39.83c-1.79-36.51-15.85-62.33-27.38-77.6a88.19 88.19 0 0 1 67.22 77.6ZM96.23 136h63.54c-2.31 41.61-22.23 67.11-31.77 77c-9.55-9.9-29.46-35.4-31.77-77m0-16c2.31-41.61 22.23-67.11 31.77-77c9.55 9.93 29.46 35.43 31.77 77Zm11.36-77.6C96.06 57.67 82 83.49 80.21 120H40.37a88.19 88.19 0 0 1 67.22-77.6M40.37 136h39.84c1.82 36.51 15.85 62.33 27.38 77.6A88.19 88.19 0 0 1 40.37 136m108 77.6c11.53-15.27 25.56-41.09 27.38-77.6h39.84a88.19 88.19 0 0 1-67.18 77.6Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph\\:hexagon-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 80.18v95.64a8 8 0 0 1-4.16 7l-88 48.18a8 8 0 0 1-7.68 0l-88-48.18a8 8 0 0 1-4.16-7V80.18a8 8 0 0 1 4.16-7l88-48.18a8 8 0 0 1 7.68 0l88 48.18a8 8 0 0 1 4.16 7' opacity='.2'/%3E%3Cpath d='m223.68 66.15l-88-48.15a15.88 15.88 0 0 0-15.36 0l-88 48.17a16 16 0 0 0-8.32 14v95.64a16 16 0 0 0 8.32 14l88 48.17a15.88 15.88 0 0 0 15.36 0l88-48.17a16 16 0 0 0 8.32-14V80.18a16 16 0 0 0-8.32-14.03M216 175.82L128 224l-88-48.18V80.18L128 32l88 48.17Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph\\:info-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M144 176a8 8 0 0 1-8 8a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 8 8m88-48A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88m-92-32a12 12 0 1 0-12-12a12 12 0 0 0 12 12'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-ph\\:x-circle-duotone{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M165.66 101.66L139.31 128l26.35 26.34a8 8 0 0 1-11.32 11.32L128 139.31l-26.34 26.35a8 8 0 0 1-11.32-11.32L116.69 128l-26.35-26.34a8 8 0 0 1 11.32-11.32L128 116.69l26.34-26.35a8 8 0 0 1 11.32 11.32M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-svg-spinners-3-dots-fade{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Ccircle cx='4' cy='12' r='3' fill='currentColor'%3E%3Canimate id='SVG7x14Dcom' fill='freeze' attributeName='opacity' begin='0;SVGqSjG0dUp.end-0.25s' dur='0.75s' values='1;.2'/%3E%3C/circle%3E%3Ccircle cx='12' cy='12' r='3' fill='currentColor' opacity='.4'%3E%3Canimate fill='freeze' attributeName='opacity' begin='SVG7x14Dcom.begin+0.15s' dur='0.75s' values='1;.2'/%3E%3C/circle%3E%3Ccircle cx='20' cy='12' r='3' fill='currentColor' opacity='.3'%3E%3Canimate id='SVGqSjG0dUp' fill='freeze' attributeName='opacity' begin='SVG7x14Dcom.begin+0.3s' dur='0.75s' values='1;.2'/%3E%3C/circle%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.i-svg-spinners-8-dots-rotate{--vdt-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg%3E%3Ccircle cx='3' cy='12' r='2' fill='currentColor'/%3E%3Ccircle cx='21' cy='12' r='2' fill='currentColor'/%3E%3Ccircle cx='12' cy='21' r='2' fill='currentColor'/%3E%3Ccircle cx='12' cy='3' r='2' fill='currentColor'/%3E%3Ccircle cx='5.64' cy='5.64' r='2' fill='currentColor'/%3E%3Ccircle cx='18.36' cy='18.36' r='2' fill='currentColor'/%3E%3Ccircle cx='5.64' cy='18.36' r='2' fill='currentColor'/%3E%3Ccircle cx='18.36' cy='5.64' r='2' fill='currentColor'/%3E%3CanimateTransform attributeName='transform' dur='1.5s' repeatCount='indefinite' type='rotate' values='0 12 12;360 12 12'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--vdt-icon) no-repeat;mask:var(--vdt-icon) no-repeat;color:inherit;background-color:currentColor;width:1em;height:1em;mask-size:100% 100%}.container{width:100%}.z-command-palette{z-index:2147483646}.z-floating-anchor{z-index:2147483644}.z-floating-tooltip{z-index:2147483645}.border-base{--vdt-border-opacity:.13;border-color:rgba(136,136,136,var(--vdt-border-opacity))}.hover\\:border-base:hover{--vdt-border-opacity:.13;border-color:rgba(136,136,136,var(--vdt-border-opacity))}.bg-active{--vdt-bg-opacity:.07;background-color:rgba(136,136,136,var(--vdt-bg-opacity))}.bg-base{--vdt-bg-opacity:1;background-color:rgba(255,255,255,var(--vdt-bg-opacity))}.dark .bg-base{--vdt-bg-opacity:1;background-color:rgba(17,17,17,var(--vdt-bg-opacity))}.bg-glass\\:80{--vdt-backdrop-blur:blur(7px);backdrop-filter:var(--vdt-backdrop-blur) var(--vdt-backdrop-brightness) var(--vdt-backdrop-contrast) var(--vdt-backdrop-grayscale) var(--vdt-backdrop-hue-rotate) var(--vdt-backdrop-invert) var(--vdt-backdrop-opacity) var(--vdt-backdrop-saturate) var(--vdt-backdrop-sepia);background-color:#fff}.dark .bg-glass\\:80{background-color:rgba(5,5,5,.8)}.bg-secondary{--vdt-bg-opacity:1;background-color:rgba(238,238,238,var(--vdt-bg-opacity))}.dark .bg-secondary{--vdt-bg-opacity:1;background-color:rgba(34,34,34,var(--vdt-bg-opacity))}.hover\\:bg-active:hover{--vdt-bg-opacity:.07;background-color:rgba(136,136,136,var(--vdt-bg-opacity))}.color-base{--vdt-text-opacity:1;color:rgba(38,38,38,var(--vdt-text-opacity))}.dark .color-base{--vdt-text-opacity:1;color:rgba(229,229,229,var(--vdt-text-opacity))}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.disabled\\:pointer-events-none:disabled{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{top:0;bottom:0;left:0;right:0}.bottom-0{bottom:0}.bottom-4{bottom:1rem}.bottom-4px{bottom:4px}.left--1{left:-.25rem}.left-0{left:0}.left-1\\/2{left:50%}.left-2{left:.5rem}.left-5px{left:5px}.right--1{right:-.25rem}.right--1px{right:-1px}.right-0{right:0}.right-0\\.5{right:.125rem}.right-2{right:.5rem}.right-4{right:1rem}.top--32px{top:-32px}.top-0\\.5{top:.125rem}.top-1{top:.25rem}.top-1\\/2{top:50%}.top-4px{top:4px}.z--1{z-index:-1}.z-2147483647{z-index:2147483647}.grid{display:grid}.cols-\\[max-content_1fr\\]{grid-template-columns:max-content 1fr}.m-auto,.ma{margin:auto}.m1{margin:.25rem}.mx--1{margin-left:-.25rem;margin-right:-.25rem}.mx--2{margin-left:-.5rem;margin-right:-.5rem}.mx-auto{margin-left:auto;margin-right:auto}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my0\\.5{margin-top:.125rem;margin-bottom:.125rem}.my1{margin-top:.25rem;margin-bottom:.25rem}.mb-1\\.5{margin-bottom:.375rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mr-1{margin-right:.25rem}.mt-0\\.5{margin-top:.125rem}.mt-1\\.5{margin-top:.375rem}.mt-6,.mt6{margin-top:1.5rem}.mt-auto{margin-top:auto}.mt2{margin-top:.5rem}.mt4{margin-top:1rem}.box-border{box-sizing:border-box}.inline{display:inline}.block{display:block}.contents{display:contents}.dark .dark-hidden,.hidden,.light .light-hidden{display:none}.h-0\\.5{height:.125rem}.h-1\\.5{height:.375rem}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-20px{height:20px}.h-24{height:6rem}.h-3{height:.75rem}.h-3\\.5{height:.875rem}.h-4{height:1rem}.h-4\\.5{height:1.125rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-\\[60vh\\]{max-height:60vh}.max-w-108{max-width:27rem}.max-w-200{max-width:50rem}.max-w-220px{max-width:220px}.max-w-64{max-width:16rem}.max-w-92{max-width:23rem}.max-w-96{max-width:24rem}.max-w-full{max-width:100%}.min-h-0{min-height:0}.min-h-16{min-height:4rem}.min-h-5{min-height:1.25rem}.min-w-0{min-width:0}.min-w-28{min-width:7rem}.min-w-36{min-width:9rem}.min-w-40{min-width:10rem}.min-w-44{min-width:11rem}.w-\\[40px\\]{width:40px}.w-1\\.5{width:.375rem}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-16{width:4rem}.w-2\\.5{width:.625rem}.w-20px{width:20px}.w-24{width:6rem}.w-2px{width:2px}.w-3{width:.75rem}.w-3\\.5{width:.875rem}.w-4{width:1rem}.w-4\\.5{width:1.125rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-60{width:15rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-8{width:2rem}.w-96{width:24rem}.w-fit{width:fit-content}.w-full{width:100%}.w-lg{width:32rem}.w-max{width:max-content}.w-px{width:1px}.w-screen{width:100vw}.flex{display:flex}.inline-flex{display:inline-flex}.flex-1{flex:1}.flex-auto{flex:auto}.flex-none{flex:none}.shrink-0{flex-shrink:0}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.-translate-y-2{--vdt-translate-y:-.5rem;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.translate-x--1\\/2{--vdt-translate-x:-50%;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.translate-x-1{--vdt-translate-x:.25rem;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.translate-x-4{--vdt-translate-x:1rem;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.translate-x-5{--vdt-translate-x:1.25rem;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.translate-y--1\\/2{--vdt-translate-y:-50%;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.translate-y-0{--vdt-translate-y:0;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.rotate--45{--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-rotate:-45deg;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.rotate-0{--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-rotate:0deg;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.rotate-180{--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-rotate:180deg;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.rotate-270{--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-rotate:270deg;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.rotate-90{--vdt-rotate-x:0;--vdt-rotate-y:0;--vdt-rotate-z:0;--vdt-rotate:90deg;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.scale-100{--vdt-scale-x:1;--vdt-scale-y:1;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.scale-120{--vdt-scale-x:1.2;--vdt-scale-y:1.2;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.scale-98{--vdt-scale-x:.98;--vdt-scale-y:.98;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.hover\\:scale-110:hover{--vdt-scale-x:1.1;--vdt-scale-y:1.1;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.scale-y--100{--vdt-scale-y:-1;transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}.transform{transform:translateX(var(--vdt-translate-x)) translateY(var(--vdt-translate-y)) translateZ(var(--vdt-translate-z)) rotate(var(--vdt-rotate)) rotateX(var(--vdt-rotate-x)) rotateY(var(--vdt-rotate-y)) rotateZ(var(--vdt-rotate-z)) skewX(var(--vdt-skew-x)) skewY(var(--vdt-skew-y)) scaleX(var(--vdt-scale-x)) scaleY(var(--vdt-scale-y)) scaleZ(var(--vdt-scale-z))}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.animate-spin{animation:1s linear infinite spin}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.cursor-not-allowed{cursor:not-allowed}.cursor-grab{cursor:grab}.select-none{user-select:none}.resize{resize:both}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0{gap:0}.gap-0\\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-x-2{column-gap:.5rem}.gap-y-0\\.5{row-gap:.125rem}.space-y-0\\.5>:not([hidden])~:not([hidden]){--vdt-space-y-reverse:0;margin-top:calc(.125rem * calc(1 - var(--vdt-space-y-reverse)));margin-bottom:calc(.125rem * var(--vdt-space-y-reverse))}.of-hidden,.overflow-hidden{overflow:hidden}.overflow-auto{overflow:auto}.of-x-hidden{overflow-x:hidden}.of-y-auto{overflow-y:auto}.of-y-hidden{overflow-y:hidden}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.whitespace-pre-wrap{white-space:pre-wrap}.ws-nowrap{white-space:nowrap}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-0{border-bottom-width:0}.border-b-1\\.5{border-bottom-width:1.5px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-r-1\\.5{border-right-width:1.5px}.border-t{border-top-width:1px}.border-t-0{border-top-width:0}.last\\:border-b-0:last-child{border-bottom-width:0}.border-amber\\/20{border-color:rgba(251,191,36,.2)}.border-current{border-color:currentColor}.border-primary-400\\/60{border-color:rgba(144,163,254,.6)}.border-primary\\/30{border-color:rgba(107,132,253,.3)}.border-red-500\\/70{border-color:rgba(239,68,68,.7)}.border-transparent{border-color:transparent}.dark .dark\\:border-primary-400\\/50{border-color:rgba(144,163,254,.5)}.focus-within\\:border-gray\\/15:focus-within{border-color:rgba(156,163,175,.15)}.hover\\:border-gray\\/10:hover{border-color:rgba(156,163,175,.1)}.focus\\:border-primary-500:focus{--vdt-border-opacity:1;border-color:rgba(107,132,253,var(--vdt-border-opacity))}.focus\\:border-primary\\/40:focus{border-color:rgba(107,132,253,.4)}.border-t-transparent{border-top-color:transparent}.rounded{-webkit-border-radius:.25rem;border-radius:.25rem}.rounded-full{-webkit-border-radius:9999px;border-radius:9999px}.rounded-lg{-webkit-border-radius:.5rem;border-radius:.5rem}.rounded-md{-webkit-border-radius:.375rem;border-radius:.375rem}.rounded-xl{-webkit-border-radius:.75rem;border-radius:.75rem}.rounded-r{-webkit-border-top-right-radius:.25rem;border-top-right-radius:.25rem;-webkit-border-bottom-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-t-md{-webkit-border-top-left-radius:.375rem;border-top-left-radius:.375rem;-webkit-border-top-right-radius:.375rem;border-top-right-radius:.375rem}.rounded-tr-md{-webkit-border-top-right-radius:.375rem;border-top-right-radius:.375rem}.bg-\\[\\#8881\\]{--vdt-bg-opacity:.07;background-color:rgba(136,136,136,var(--vdt-bg-opacity))}.bg-amber{--vdt-bg-opacity:1;background-color:rgba(251,191,36,var(--vdt-bg-opacity))}.bg-amber\\/10{background-color:rgba(251,191,36,.1)}.bg-black\\/30,.dark .dark\\:bg-black\\/30{background-color:rgba(0,0,0,.3)}.bg-blue{--vdt-bg-opacity:1;background-color:rgba(96,165,250,var(--vdt-bg-opacity))}.bg-blue\\/10{background-color:rgba(96,165,250,.1)}.bg-gray{--vdt-bg-opacity:1;background-color:rgba(156,163,175,var(--vdt-bg-opacity))}.bg-gray-6{--vdt-bg-opacity:1;background-color:rgba(75,85,99,var(--vdt-bg-opacity))}.bg-gray\\/10{background-color:rgba(156,163,175,.1)}.bg-gray\\/20{background-color:rgba(156,163,175,.2)}.bg-gray\\/3{background-color:rgba(156,163,175,.03)}.bg-gray\\/30{background-color:rgba(156,163,175,.3)}.bg-gray\\/5{background-color:rgba(156,163,175,.05)}.bg-green{--vdt-bg-opacity:1;background-color:rgba(74,222,128,var(--vdt-bg-opacity))}.bg-orange\\/10{background-color:rgba(251,146,60,.1)}.bg-primary{--vdt-bg-opacity:1;background-color:rgba(107,132,253,var(--vdt-bg-opacity))}.bg-primary-500\\/20,.bg-primary\\/20{background-color:rgba(107,132,253,.2)}.bg-primary-600{--vdt-bg-opacity:1;background-color:rgba(94,116,223,var(--vdt-bg-opacity))}.bg-primary\\/10{background-color:rgba(107,132,253,.1)}.bg-primary\\/15{background-color:rgba(107,132,253,.15)}.bg-red{--vdt-bg-opacity:1;background-color:rgba(248,113,113,var(--vdt-bg-opacity))}.bg-red-500\\/12{background-color:rgba(239,68,68,.12)}.bg-red\\/10{background-color:rgba(248,113,113,.1)}.bg-transparent{background-color:transparent}.bg-white{--vdt-bg-opacity:1;background-color:rgba(255,255,255,var(--vdt-bg-opacity))}.bg-white\\/50{background-color:rgba(255,255,255,.5)}.dark .dark\\:bg-black\\/45{background-color:rgba(0,0,0,.45)}.hover\\:bg-\\[\\#8881\\]:hover{--vdt-bg-opacity:.07;background-color:rgba(136,136,136,var(--vdt-bg-opacity))}.hover\\:bg-\\[\\#8882\\]:hover{--vdt-bg-opacity:.13;background-color:rgba(136,136,136,var(--vdt-bg-opacity))}.hover\\:bg-\\[\\#8883\\]:hover{--vdt-bg-opacity:.2;background-color:rgba(136,136,136,var(--vdt-bg-opacity))}.hover\\:bg-gray\\/10:hover{background-color:rgba(156,163,175,.1)}.hover\\:bg-gray\\/15:hover{background-color:rgba(156,163,175,.15)}.hover\\:bg-gray\\/20:hover{background-color:rgba(156,163,175,.2)}.hover\\:bg-gray\\/5:hover{background-color:rgba(156,163,175,.05)}.hover\\:bg-orange\\/20:hover{background-color:rgba(251,146,60,.2)}.hover\\:bg-primary-700:hover{--vdt-bg-opacity:1;background-color:rgba(77,95,182,var(--vdt-bg-opacity))}.hover\\:bg-primary\\/20:hover{background-color:rgba(107,132,253,.2)}.hover\\:bg-primary\\/25:hover{background-color:rgba(107,132,253,.25)}.hover\\:bg-red-500\\/20:hover{background-color:rgba(239,68,68,.2)}.hover\\:bg-red\\/20:hover{background-color:rgba(248,113,113,.2)}.dark .dark\\:fill-hex-fff,.dark .dark\\:fill-white{--vdt-fill-opacity:1;fill:rgba(255,255,255,var(--vdt-fill-opacity))}.fill-black{--vdt-fill-opacity:1;fill:rgba(0,0,0,var(--vdt-fill-opacity))}.fill-hex-08060D{--vdt-fill-opacity:1;fill:rgba(8,6,13,var(--vdt-fill-opacity))}.p-0\\.5{padding:.125rem}.p-1\\.5,.p1\\.5{padding:.375rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p1{padding:.25rem}.p2{padding:.5rem}.p6{padding:1.5rem}.p8{padding:2rem}.px,.px-4,.px4{padding-left:1rem;padding-right:1rem}.px-0\\.5{padding-left:.125rem;padding-right:.125rem}.px-1,.px1{padding-left:.25rem;padding-right:.25rem}.px-1\\.5{padding-left:.375rem;padding-right:.375rem}.px-2,.px2{padding-left:.5rem;padding-right:.5rem}.px-2\\.5{padding-left:.625rem;padding-right:.625rem}.px-3,.px3{padding-left:.75rem;padding-right:.75rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0{padding-top:0;padding-bottom:0}.py-0\\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1,.py1{padding-top:.25rem;padding-bottom:.25rem}.py-1\\.5,.py1\\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2,.py2{padding-top:.5rem;padding-bottom:.5rem}.py-2\\.5,.py2\\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb2\\.5{padding-bottom:.625rem}.pt-\\[20vh\\]{padding-top:20vh}.pt-6{padding-top:1.5rem}.pt2{padding-top:.5rem}.text-center{text-align:center}.text-left{text-align:left}.indent{text-indent:1.5rem}.text-balance{text-wrap:balance}.text-\\[10px\\]{font-size:10px}.text-\\[15px\\]{font-size:15px}.text-0\\.6em{font-size:.6em}.text-2\\.75{font-size:.6875rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-base{font-size:1rem;line-height:1.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.dark .dark\\:text-amber-200{--vdt-text-opacity:1;color:rgba(253,230,138,var(--vdt-text-opacity))}.dark .dark\\:text-green-200{--vdt-text-opacity:1;color:rgba(187,247,208,var(--vdt-text-opacity))}.dark .dark\\:text-primary-300{--vdt-text-opacity:1;color:rgba(181,194,254,var(--vdt-text-opacity))}.dark .dark\\:text-red-400,.text-red{--vdt-text-opacity:1;color:rgba(248,113,113,var(--vdt-text-opacity))}.text-amber{--vdt-text-opacity:1;color:rgba(251,191,36,var(--vdt-text-opacity))}.text-amber-800{--vdt-text-opacity:1;color:rgba(146,64,14,var(--vdt-text-opacity))}.text-blue{--vdt-text-opacity:1;color:rgba(96,165,250,var(--vdt-text-opacity))}.text-gray{--vdt-text-opacity:1;color:rgba(156,163,175,var(--vdt-text-opacity))}.text-green{--vdt-text-opacity:1;color:rgba(74,222,128,var(--vdt-text-opacity))}.text-green-800{--vdt-text-opacity:1;color:rgba(22,101,52,var(--vdt-text-opacity))}.text-orange{--vdt-text-opacity:1;color:rgba(251,146,60,var(--vdt-text-opacity))}.text-primary{--vdt-text-opacity:1;color:rgba(107,132,253,var(--vdt-text-opacity))}.text-primary-600{--vdt-text-opacity:1;color:rgba(94,116,223,var(--vdt-text-opacity))}.text-primary-700{--vdt-text-opacity:1;color:rgba(77,95,182,var(--vdt-text-opacity))}.text-red-500{--vdt-text-opacity:1;color:rgba(239,68,68,var(--vdt-text-opacity))}.text-red-600{--vdt-text-opacity:1;color:rgba(220,38,38,var(--vdt-text-opacity))}.text-white{--vdt-text-opacity:1;color:rgba(255,255,255,var(--vdt-text-opacity))}.font-bold{font-weight:700}.font-medium{font-weight:500}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.tabular-nums{--vdt-numeric-spacing:tabular-nums;font-variant-numeric:var(--vdt-ordinal) var(--vdt-slashed-zero) var(--vdt-numeric-figure) var(--vdt-numeric-spacing) var(--vdt-numeric-fraction)}.line-through{text-decoration-line:line-through}.tab{tab-size:4}.caret-primary-500{--vdt-caret-opacity:1;caret-color:rgba(107,132,253,var(--vdt-caret-opacity))}.op0,.opacity-0{opacity:0}.op100,.opacity-100{opacity:1}.group:hover .group-hover\\:opacity-100{opacity:1}.op25{opacity:.25}.op30{opacity:.3}.op40{opacity:.4}.op50{opacity:.5}.group:hover .group-hover\\:op50{opacity:.5}.op60{opacity:.6}.op60\\!{opacity:.6!important}.op70{opacity:.7}.op75{opacity:.75}.op80{opacity:.8}.hover\\:op100:hover{opacity:1}.hover\\:op60:hover{opacity:.6}.hover\\:op70:hover{opacity:.7}.hover\\:op80:hover{opacity:.8}.disabled\\:op40:disabled{opacity:.4}.disabled\\:op50:disabled{opacity:.5}.shadow{--vdt-shadow:var(--vdt-shadow-inset) 0 1px 3px 0 var(--vdt-shadow-color,rgba(0,0,0,.1)),var(--vdt-shadow-inset) 0 1px 2px -1px var(--vdt-shadow-color,rgba(0,0,0,.1));box-shadow:var(--vdt-ring-offset-shadow), var(--vdt-ring-shadow), var(--vdt-shadow)}.shadow-sm{--vdt-shadow:var(--vdt-shadow-inset) 0 1px 2px 0 var(--vdt-shadow-color,rgba(0,0,0,.05));box-shadow:var(--vdt-ring-offset-shadow), var(--vdt-ring-shadow), var(--vdt-shadow)}.shadow-xl{--vdt-shadow:var(--vdt-shadow-inset) 0 20px 25px -5px var(--vdt-shadow-color,rgba(0,0,0,.1)),var(--vdt-shadow-inset) 0 8px 10px -6px var(--vdt-shadow-color,rgba(0,0,0,.1));box-shadow:var(--vdt-ring-offset-shadow), var(--vdt-ring-shadow), var(--vdt-shadow)}.outline{outline-style:solid}.outline-none{outline-offset:2px;outline:2px solid transparent}.ring-1\\.5{--vdt-ring-width:1.5px;--vdt-ring-offset-shadow:var(--vdt-ring-inset) 0 0 0 var(--vdt-ring-offset-width) var(--vdt-ring-offset-color);--vdt-ring-shadow:var(--vdt-ring-inset) 0 0 0 calc(var(--vdt-ring-width) + var(--vdt-ring-offset-width)) var(--vdt-ring-color);box-shadow:var(--vdt-ring-offset-shadow), var(--vdt-ring-shadow), var(--vdt-shadow)}.focus\\:ring-1:focus{--vdt-ring-width:1px;--vdt-ring-offset-shadow:var(--vdt-ring-inset) 0 0 0 var(--vdt-ring-offset-width) var(--vdt-ring-offset-color);--vdt-ring-shadow:var(--vdt-ring-inset) 0 0 0 calc(var(--vdt-ring-width) + var(--vdt-ring-offset-width)) var(--vdt-ring-color);box-shadow:var(--vdt-ring-offset-shadow), var(--vdt-ring-shadow), var(--vdt-shadow)}.focus\\:ring-3:focus{--vdt-ring-width:3px;--vdt-ring-offset-shadow:var(--vdt-ring-inset) 0 0 0 var(--vdt-ring-offset-width) var(--vdt-ring-offset-color);--vdt-ring-shadow:var(--vdt-ring-inset) 0 0 0 calc(var(--vdt-ring-width) + var(--vdt-ring-offset-width)) var(--vdt-ring-color);box-shadow:var(--vdt-ring-offset-shadow), var(--vdt-ring-shadow), var(--vdt-shadow)}.focus-visible\\:ring-3:focus-visible{--vdt-ring-width:3px;--vdt-ring-offset-shadow:var(--vdt-ring-inset) 0 0 0 var(--vdt-ring-offset-width) var(--vdt-ring-offset-color);--vdt-ring-shadow:var(--vdt-ring-inset) 0 0 0 calc(var(--vdt-ring-width) + var(--vdt-ring-offset-width)) var(--vdt-ring-color);box-shadow:var(--vdt-ring-offset-shadow), var(--vdt-ring-shadow), var(--vdt-shadow)}.ring-purple\\/50{--vdt-ring-color:rgba(192,132,252,.5)}.focus\\:ring-primary-500\\/25:focus{--vdt-ring-color:rgba(107,132,253,.25)}.focus\\:ring-primary\\/30:focus{--vdt-ring-color:rgba(107,132,253,.3)}.focus-visible\\:ring-primary-500\\/30:focus-visible{--vdt-ring-color:rgba(107,132,253,.3)}.focus-visible\\:ring-primary-500\\/40:focus-visible{--vdt-ring-color:rgba(107,132,253,.4)}.focus-visible\\:ring-red-500\\/30:focus-visible{--vdt-ring-color:rgba(239,68,68,.3)}.backdrop-blur-0{--vdt-backdrop-blur:blur(0);backdrop-filter:var(--vdt-backdrop-blur) var(--vdt-backdrop-brightness) var(--vdt-backdrop-contrast) var(--vdt-backdrop-grayscale) var(--vdt-backdrop-hue-rotate) var(--vdt-backdrop-invert) var(--vdt-backdrop-opacity) var(--vdt-backdrop-saturate) var(--vdt-backdrop-sepia)}.backdrop-blur-1{--vdt-backdrop-blur:blur(1px);backdrop-filter:var(--vdt-backdrop-blur) var(--vdt-backdrop-brightness) var(--vdt-backdrop-contrast) var(--vdt-backdrop-grayscale) var(--vdt-backdrop-hue-rotate) var(--vdt-backdrop-invert) var(--vdt-backdrop-opacity) var(--vdt-backdrop-saturate) var(--vdt-backdrop-sepia)}.blur-2xl{--vdt-blur:blur(40px);filter:var(--vdt-blur) var(--vdt-brightness) var(--vdt-contrast) var(--vdt-drop-shadow) var(--vdt-grayscale) var(--vdt-hue-rotate) var(--vdt-invert) var(--vdt-saturate) var(--vdt-sepia)}.saturate-0{--vdt-saturate:saturate(0);filter:var(--vdt-blur) var(--vdt-brightness) var(--vdt-contrast) var(--vdt-drop-shadow) var(--vdt-grayscale) var(--vdt-hue-rotate) var(--vdt-invert) var(--vdt-saturate) var(--vdt-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-property:all;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-property:transform;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.delay-200{transition-delay:.2s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}@media (min-width:640px){.sm\\:gap-2\\.5{gap:.625rem}}" diff --git a/packages/core/src/client/webcomponents/components/DockEmbedded.ts b/packages/core/src/client/webcomponents/components/DockEmbedded.ts deleted file mode 100644 index f08fa9ed5..000000000 --- a/packages/core/src/client/webcomponents/components/DockEmbedded.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { DocksContext } from '@vitejs/devtools-kit/client' -import type { VueElementConstructor } from 'vue' -import { defineCustomElement } from 'vue' -import css from '../.generated/css' -import Component from './dock/DockEmbedded.vue' - -export const DockEmbedded = defineCustomElement( - Component, - { - shadowRoot: true, - styles: [css], - }, -) as VueElementConstructor<{ - context: DocksContext -}> - -customElements.define('vite-devtools-dock-embedded', DockEmbedded) diff --git a/packages/core/src/client/webcomponents/components/DockStandalone.ts b/packages/core/src/client/webcomponents/components/DockStandalone.ts deleted file mode 100644 index 8614ac1b3..000000000 --- a/packages/core/src/client/webcomponents/components/DockStandalone.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { DocksContext } from '@vitejs/devtools-kit/client' -import type { VueElementConstructor } from 'vue' -import { defineCustomElement } from 'vue' -import css from '../.generated/css' -import Component from './dock/DockStandalone.vue' - -export const DockStandalone = defineCustomElement( - Component, - { - shadowRoot: true, - styles: [css], - }, -) as VueElementConstructor<{ - context: DocksContext -}> - -if (!customElements.get('vite-devtools-dock-standalone')) - customElements.define('vite-devtools-dock-standalone', DockStandalone) diff --git a/packages/core/src/client/webcomponents/components/command-palette/CommandPalette.stories.ts b/packages/core/src/client/webcomponents/components/command-palette/CommandPalette.stories.ts deleted file mode 100644 index b67091448..000000000 --- a/packages/core/src/client/webcomponents/components/command-palette/CommandPalette.stories.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { defineComponent, h, onMounted } from 'vue' -import { groupedEntries } from '../../stories/fixtures' -import { mountWithContext } from '../../stories/story-helpers' -import CommandPalette from './CommandPalette.vue' - -const meta = { - title: 'Commands/Palette', - component: CommandPalette, - tags: ['autodocs'], - parameters: { - layout: 'fullscreen', - // Fixed, full-screen overlay — render in an iframe for the docs canvas. - docs: { - story: { inline: false, height: '480px' }, - description: { - component: 'The ⌘K command palette. Lists client + server commands (and dock navigation), supports fuzzy search and drill-down into grouped commands.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** - * The palette opened over the built-in and dock-navigation commands. - * - * `paletteOpen` is flipped in `onMounted` (not before mount): the palette's - * open transition keys off the `show` change, so a value that's already `true` - * at mount would leave it stuck at `opacity-0`. - */ -export const Open: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: groupedEntries }, - ctx => h(defineComponent({ - setup() { - onMounted(() => { - ctx.commands.paletteOpen = true - }) - return () => h(CommandPalette, { context: ctx }) - }, - })), - ), - }), -} diff --git a/packages/core/src/client/webcomponents/components/command-palette/CommandPalette.vue b/packages/core/src/client/webcomponents/components/command-palette/CommandPalette.vue deleted file mode 100644 index b8905b8aa..000000000 --- a/packages/core/src/client/webcomponents/components/command-palette/CommandPalette.vue +++ /dev/null @@ -1,338 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/command-palette/CommandPaletteItem.stories.ts b/packages/core/src/client/webcomponents/components/command-palette/CommandPaletteItem.stories.ts deleted file mode 100644 index 594d17663..000000000 --- a/packages/core/src/client/webcomponents/components/command-palette/CommandPaletteItem.stories.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import type { DevToolsCommandEntry } from '@vitejs/devtools-kit' -import CommandPaletteItem from './CommandPaletteItem.vue' - -const entry: DevToolsCommandEntry = { - id: 'devtools:open-settings', - source: 'client', - title: 'Open Settings', - icon: 'ph:gear-duotone', -} as DevToolsCommandEntry - -const meta = { - title: 'Commands/PaletteItem', - component: CommandPaletteItem, - tags: ['autodocs'], - decorators: [() => ({ template: '
' })], - args: { - entry, - showParentTitle: false, - selected: false, - loading: false, - keybindings: [], - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** A resting command row. */ -export const Default: Story = {} - -/** The highlighted (keyboard-focused) row. */ -export const Selected: Story = { args: { selected: true } } - -/** With a keybinding badge on the right. */ -export const WithKeybinding: Story = { - args: { keybindings: [{ key: 'Mod+,' }] }, -} - -/** A child command showing its parent group as a prefix. */ -export const WithParentTitle: Story = { - args: { parentTitle: 'Docks', showParentTitle: true }, -} - -/** Mid-execution: the row shows a spinner. */ -export const Loading: Story = { args: { loading: true, selected: true } } diff --git a/packages/core/src/client/webcomponents/components/command-palette/CommandPaletteItem.vue b/packages/core/src/client/webcomponents/components/command-palette/CommandPaletteItem.vue deleted file mode 100644 index 198bdd761..000000000 --- a/packages/core/src/client/webcomponents/components/command-palette/CommandPaletteItem.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/command-palette/KeybindingBadge.stories.ts b/packages/core/src/client/webcomponents/components/command-palette/KeybindingBadge.stories.ts deleted file mode 100644 index 9ff1c8e67..000000000 --- a/packages/core/src/client/webcomponents/components/command-palette/KeybindingBadge.stories.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { h } from 'vue' -import KeybindingBadge from './KeybindingBadge.vue' - -const meta = { - title: 'Commands/KeybindingBadge', - component: KeybindingBadge, - tags: ['autodocs'], - decorators: [() => ({ template: '
' })], - args: { keyString: 'Mod+K' }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** A single chord, formatted for the current platform (⌘ on macOS, Ctrl elsewhere). */ -export const Default: Story = {} - -/** A shortcut with a modifier and shift. */ -export const WithShift: Story = { args: { keyString: 'Mod+Shift+P' } } - -/** A plain key. */ -export const SingleKey: Story = { args: { keyString: 'Escape' } } - -/** Several badges together, as the palette lists them. */ -export const Gallery: Story = { - render: () => ({ - setup: () => () => h('div', { class: 'flex flex-col gap-2 items-start p6 font-sans' }, [ - h(KeybindingBadge, { keyString: 'Mod+K' }), - h(KeybindingBadge, { keyString: 'Mod+Shift+P' }), - h(KeybindingBadge, { keyString: 'Escape' }), - h(KeybindingBadge, { keyString: 'Alt+ArrowUp' }), - ]), - }), -} diff --git a/packages/core/src/client/webcomponents/components/command-palette/KeybindingBadge.vue b/packages/core/src/client/webcomponents/components/command-palette/KeybindingBadge.vue deleted file mode 100644 index aef6f4804..000000000 --- a/packages/core/src/client/webcomponents/components/command-palette/KeybindingBadge.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/display/Button.stories.ts b/packages/core/src/client/webcomponents/components/display/Button.stories.ts deleted file mode 100644 index 7f46fae7e..000000000 --- a/packages/core/src/client/webcomponents/components/display/Button.stories.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { h } from 'vue' -import Button from './Button.vue' - -const variants = ['primary', 'soft', 'secondary', 'ghost', 'danger'] as const -const sizes = ['sm', 'md', 'lg'] as const - -function row(children: any, label?: string) { - return h('div', { class: 'flex items-center gap-3 flex-wrap' }, [ - label ? h('div', { class: 'w-20 text-xs op-mute font-mono' }, label) : null, - children, - ]) -} - -const meta = { - title: 'Display/Button', - component: Button, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: 'The unified DevTools button, shared by the confirm modal, the auth OTP form, the launcher view and json-render. Variants (`primary`, `soft`, `secondary`, `ghost`, `danger`), sizes (`sm`/`md`/`lg`), plus `block`, `loading`, `disabled` and an optional leading `#icon` slot.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** Every variant at the default size. */ -export const Variants: Story = { - render: () => ({ - setup: () => () => h('div', { class: 'flex flex-col gap-3 p8 bg-base color-base font-sans' }, variants.map(variant => row( - h(Button, { variant }, { default: () => variant[0]!.toUpperCase() + variant.slice(1) }), - variant, - ))), - }), -} - -/** The three sizes (shown with the primary variant). */ -export const Sizes: Story = { - render: () => ({ - setup: () => () => h('div', { class: 'flex items-center gap-3 p8 bg-base color-base font-sans' }, sizes.map(size => h(Button, { variant: 'primary', size }, { default: () => size }))), - }), -} - -/** With a leading icon (via the `#icon` slot). */ -export const WithIcon: Story = { - render: () => ({ - setup: () => () => h('div', { class: 'flex items-center gap-3 p8 bg-base color-base font-sans' }, [ - h(Button, { variant: 'primary' }, { - icon: () => h('div', { class: 'i-ph-shield-check-duotone w-4.5 h-4.5' }), - default: () => 'Authorize', - }), - h(Button, { variant: 'danger' }, { - icon: () => h('div', { class: 'i-ph-trash-duotone w-4.5 h-4.5' }), - default: () => 'Delete', - }), - ]), - }), -} - -/** Loading (spinner + disabled) and disabled states. */ -export const States: Story = { - render: () => ({ - setup: () => () => h('div', { class: 'flex items-center gap-3 p8 bg-base color-base font-sans' }, [ - h(Button, { variant: 'primary', loading: true }, { default: () => 'Authorizing' }), - h(Button, { variant: 'primary', disabled: true }, { default: () => 'Disabled' }), - ]), - }), -} - -/** Full-width block button, as used in the auth form. */ -export const Block: Story = { - render: () => ({ - setup: () => () => h('div', { class: 'max-w-80 p8 bg-base color-base font-sans' }, h(Button, { variant: 'primary', size: 'lg', block: true }, { default: () => 'Authorize' })), - }), -} diff --git a/packages/core/src/client/webcomponents/components/display/Button.vue b/packages/core/src/client/webcomponents/components/display/Button.vue deleted file mode 100644 index 58aeee862..000000000 --- a/packages/core/src/client/webcomponents/components/display/Button.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/display/Confirm.stories.ts b/packages/core/src/client/webcomponents/components/display/Confirm.stories.ts deleted file mode 100644 index c5cec14cd..000000000 --- a/packages/core/src/client/webcomponents/components/display/Confirm.stories.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { h, onMounted } from 'vue' -import { useConfirm } from '../../state/confirm' -import Confirm from './Confirm.vue' - -const meta = { - title: 'Display/Confirm', - component: Confirm, - tags: ['autodocs'], - parameters: { - layout: 'fullscreen', - // Fixed, full-screen modal — render in an iframe for the docs canvas. - docs: { - story: { inline: false, height: '360px' }, - description: { - component: 'The confirmation dialog, driven by the `useConfirm()` template-promise. These stories trigger it on mount.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -function confirmStory(options: { title?: string, message: string, confirmText?: string, cancelText?: string }): Story { - return { - render: () => ({ - setup() { - const confirm = useConfirm() - onMounted(() => { - confirm(options) - }) - return () => h(Confirm) - }, - }), - } -} - -/** A titled destructive confirmation. */ -export const Default: Story = confirmStory({ - title: 'Remove workspace?', - message: 'This deletes the worktree and branch. This cannot be undone.', - confirmText: 'Remove', - cancelText: 'Cancel', -}) - -/** A message-only prompt (no title). */ -export const MessageOnly: Story = confirmStory({ - message: 'Reload the page to apply the new settings?', -}) diff --git a/packages/core/src/client/webcomponents/components/display/Confirm.vue b/packages/core/src/client/webcomponents/components/display/Confirm.vue deleted file mode 100644 index 23c3cbd7f..000000000 --- a/packages/core/src/client/webcomponents/components/display/Confirm.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/display/HashBadge.stories.ts b/packages/core/src/client/webcomponents/components/display/HashBadge.stories.ts deleted file mode 100644 index 005cd52e4..000000000 --- a/packages/core/src/client/webcomponents/components/display/HashBadge.stories.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { h } from 'vue' -import HashBadge from './HashBadge.vue' - -const meta = { - title: 'Display/HashBadge', - component: HashBadge, - tags: ['autodocs'], - decorators: [() => ({ template: '
' })], - args: { label: 'a11y' }, - parameters: { - docs: { - description: { - component: 'A category/label chip whose colour is derived deterministically from the label text.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -export const Default: Story = {} - -/** The colour is stable per string — the same label always gets the same hue. */ -export const Palette: Story = { - render: () => ({ - setup: () => () => h('div', { class: 'flex flex-wrap gap-1.5 max-w-100 p6 font-sans' }, ['a11y', 'lint', 'runtime', 'test', 'network', 'build', 'hmr', 'plugin', 'vite', 'rolldown'] - .map(label => h(HashBadge, { key: label, label }))), - }), -} diff --git a/packages/core/src/client/webcomponents/components/display/HashBadge.vue b/packages/core/src/client/webcomponents/components/display/HashBadge.vue deleted file mode 100644 index e1ca1b7ee..000000000 --- a/packages/core/src/client/webcomponents/components/display/HashBadge.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/display/OtpInput.stories.ts b/packages/core/src/client/webcomponents/components/display/OtpInput.stories.ts deleted file mode 100644 index fa72d2cc6..000000000 --- a/packages/core/src/client/webcomponents/components/display/OtpInput.stories.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { defineComponent, h, ref } from 'vue' -import OtpInput from './OtpInput.vue' - -/** A stateful harness so the controlled `v-model` reflects typing/paste. */ -function harness(props: Record = {}, initial = '') { - return defineComponent({ - setup() { - const code = ref(initial) - return () => h('div', { class: 'p10 bg-base color-base font-sans flex flex-col items-center gap-4' }, [ - h(OtpInput, { 'modelValue': code.value, 'onUpdate:modelValue': (v: string) => (code.value = v), 'autofocus': false, ...props }), - h('div', { class: 'text-sm op-fade font-mono' }, `value: "${code.value}"`), - ]) - }, - }) -} - -const meta = { - title: 'Display/OtpInput', - component: OtpInput, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: 'An accessible one-time-code input: per-digit boxes with auto-advance, backspace-to-previous, arrow-key navigation, full-code paste, numeric-only entry (`inputmode="numeric"`, `autocomplete="one-time-code"`), per-digit `aria-label`s, and an error state that paints the boxes red and shakes.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** Empty — type digits, they auto-advance; paste a full code to fill at once. */ -export const Default: Story = { - render: () => ({ setup: () => () => h(harness()) }), -} - -/** Partially filled. */ -export const Filled: Story = { - render: () => ({ setup: () => () => h(harness({}, '1234')) }), -} - -/** Error state — red boxes and a shake (see the AuthNotice for the full flow). */ -export const Invalid: Story = { - render: () => ({ setup: () => () => h(harness({ invalid: true }, '0042')) }), -} - -/** Disabled. */ -export const Disabled: Story = { - render: () => ({ setup: () => () => h(harness({ disabled: true }, '12')) }), -} - -/** A shorter, four-digit variant. */ -export const FourDigits: Story = { - render: () => ({ setup: () => () => h(harness({ length: 4 })) }), -} diff --git a/packages/core/src/client/webcomponents/components/display/OtpInput.vue b/packages/core/src/client/webcomponents/components/display/OtpInput.vue deleted file mode 100644 index e802b4199..000000000 --- a/packages/core/src/client/webcomponents/components/display/OtpInput.vue +++ /dev/null @@ -1,192 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/display/ToastOverlay.stories.ts b/packages/core/src/client/webcomponents/components/display/ToastOverlay.stories.ts deleted file mode 100644 index 08b291914..000000000 --- a/packages/core/src/client/webcomponents/components/display/ToastOverlay.stories.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { h, onMounted } from 'vue' -import { addToast } from '../../state/toasts' -import { message } from '../../stories/fixtures' -import ToastOverlay from './ToastOverlay.vue' - -const meta = { - title: 'Messages/ToastOverlay', - component: ToastOverlay, - tags: ['autodocs'], - parameters: { - layout: 'fullscreen', - // Fixed bottom-right overlay — render in an iframe for the docs canvas. - docs: { - story: { inline: false, height: '360px' }, - description: { - component: 'The toast stack shown bottom-right for `notify` messages. These stories push toasts on mount (with a long auto-dismiss so they stay visible).', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** A few stacked toasts across levels. */ -export const Stack: Story = { - render: () => ({ - setup() { - onMounted(() => { - addToast(message({ level: 'error', message: 'Build failed', description: '2 errors in 1 file', notify: true, autoDismiss: 10 ** 7 })) - addToast(message({ level: 'warn', message: 'Slow HMR update (820ms)', notify: true, autoDismiss: 10 ** 7 })) - addToast(message({ level: 'success', message: 'Server ready on :5173', notify: true, autoDismiss: 10 ** 7 })) - }) - return () => h(ToastOverlay) - }, - }), -} diff --git a/packages/core/src/client/webcomponents/components/display/ToastOverlay.vue b/packages/core/src/client/webcomponents/components/display/ToastOverlay.vue deleted file mode 100644 index 9151b1bff..000000000 --- a/packages/core/src/client/webcomponents/components/display/ToastOverlay.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/ColorSchemeRoot.vue b/packages/core/src/client/webcomponents/components/dock/ColorSchemeRoot.vue deleted file mode 100644 index 54d0e54a6..000000000 --- a/packages/core/src/client/webcomponents/components/dock/ColorSchemeRoot.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/Dock.stories.ts b/packages/core/src/client/webcomponents/components/dock/Dock.stories.ts deleted file mode 100644 index e7779be2f..000000000 --- a/packages/core/src/client/webcomponents/components/dock/Dock.stories.ts +++ /dev/null @@ -1,152 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import type { CreateMockContextOptions } from '../../stories/mock-context' -import type { DockLayout } from './dock-layout' -import { h } from 'vue' -import { categorizedEntries, groupedEntries, overflowEntries } from '../../stories/fixtures' -import { mountWithContext } from '../../stories/story-helpers' -import FloatingElements from '../floating/FloatingElements.vue' -import { DEFAULT_DOCK_LAYOUT } from './dock-layout' -import Dock from './Dock.vue' - -/** - * The story args ARE the dock layout: every field is wired to a Storybook - * control so the bar's dimensions, capacity, spacing and snapping can be tuned - * live from the Controls panel. Individual stories override specific fields to - * showcase a single tunable in isolation. - */ -type LayoutArgs = DockLayout - -const meta = { - title: 'Dock/Shell/Float Bar', - component: Dock, - tags: ['autodocs'], - args: { ...DEFAULT_DOCK_LAYOUT }, - argTypes: { - barHeight: { control: { type: 'range', min: 24, max: 80, step: 1 }, description: 'Height of the bar (px).' }, - barMinWidth: { control: { type: 'range', min: 60, max: 220, step: 1 }, description: 'Minimum bar width before content (px).' }, - minimizedSize: { control: { type: 'range', min: 14, max: 40, step: 1 }, description: 'Side length of the collapsed nub (px).' }, - glowSize: { control: { type: 'range', min: 60, max: 320, step: 1 }, description: 'Diameter of the ambient glow (px).' }, - glowBlur: { control: { type: 'range', min: 0, max: 120, step: 1 }, description: 'Blur radius of the glow (px).' }, - maxVisibleItems: { control: { type: 'range', min: 1, max: 12, step: 1 }, description: 'Inline item capacity before overflow.' }, - viewportMargin: { control: { type: 'range', min: 0, max: 48, step: 1 }, description: 'Gap from the viewport edge (px).' }, - panelOverlapFactor: { control: { type: 'range', min: 0, max: 1, step: 0.05 }, description: 'Dock↔panel overlap (fraction of bar thickness). Visible with the panel open — see the Embedded stories.' }, - edgeSnapPercent: { control: { type: 'range', min: 0, max: 20, step: 1 }, description: 'Snap-to-edge zone (viewport %).' }, - centerSnapPercent: { control: { type: 'range', min: 0, max: 20, step: 1 }, description: 'Snap-to-center zone (viewport %).' }, - edgeZoneHeight: { control: { type: 'range', min: 0, max: 200, step: 1 }, description: 'Top/bottom edge-detection zone height (px).' }, - }, - parameters: { - layout: 'fullscreen', - // The shell is `position: fixed` to the viewport, so it escapes the small - // inline docs preview block — render it in an iframe of a fixed height. - docs: { - story: { inline: false, height: '520px' }, - description: { - component: 'The floating dock bar (float mode). It anchors to an edge of the viewport and can be dragged around. Its padding, sizing, spacing, capacity and snapping are driven by the `DockLayout` constants (`dock-layout.ts`) — every field is exposed here as a live control. These stories pin `inactiveTimeout: -1` so the bar stays expanded.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** - * Build a float-bar story whose layout is driven by the live story args, so the - * Controls panel tunes the same `DockLayout` the runtime uses. - */ -function floatStory(options: CreateMockContextOptions): Story { - return { - render: (args: LayoutArgs) => ({ - setup: () => mountWithContext( - options, - ctx => [h(Dock, { context: ctx, layout: args }), h(FloatingElements)], - ), - }), - } -} - -/** Default: docked to the bottom-center of the viewport. */ -export const Bottom: Story = floatStory( - { entries: categorizedEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }, -) - -/** Docked to the top edge. */ -export const Top: Story = floatStory( - { entries: categorizedEntries, panel: { position: 'top', left: 50, top: 0, inactiveTimeout: -1 } }, -) - -/** Docked to the left edge — the bar rotates to vertical. */ -export const Left: Story = floatStory( - { entries: categorizedEntries, panel: { position: 'left', left: 0, top: 50, inactiveTimeout: -1 } }, -) - -/** Docked to the right edge. */ -export const Right: Story = floatStory( - { entries: categorizedEntries, panel: { position: 'right', left: 100, top: 50, inactiveTimeout: -1 } }, -) - -/** With collapsed groups on the bar. */ -export const WithGroups: Story = floatStory( - { entries: groupedEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }, -) - -/** Past capacity: an overflow button appears at the end of the bar. */ -export const WithOverflow: Story = floatStory( - { entries: overflowEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }, -) - -/** Collapsed to the minimized nub (`inactiveTimeout: 0`). */ -export const Minimized: Story = floatStory( - { entries: categorizedEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: 0 } }, -) - -/** Unauthorized: the bar shows the warning affordance instead of the tools. */ -export const Unauthorized: Story = { - ...floatStory({ entries: categorizedEntries, isTrusted: false, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }), -} - -// --- Layout tunables --------------------------------------------------------- -// Each of the following overrides a single `DockLayout` field to demonstrate -// how the bar responds. Adjust the Controls panel to combine them. - -/** `barHeight: 56` — a taller bar (also grows the panel-to-dock offset). */ -export const TallBar: Story = { - ...floatStory({ entries: categorizedEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }), - args: { barHeight: 56 }, -} - -/** `barHeight: 30`, `minimizedSize: 18` — a compact bar. */ -export const CompactBar: Story = { - ...floatStory({ entries: categorizedEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }), - args: { barHeight: 30, minimizedSize: 18 }, -} - -/** `viewportMargin: 28` — a roomy gap between the bar and the viewport edge. */ -export const RoomyViewportMargin: Story = { - ...floatStory({ entries: categorizedEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }), - args: { viewportMargin: 28 }, -} - -/** `maxVisibleItems: 3` — a lower capacity forces more entries into overflow. */ -export const LowerCapacity: Story = { - ...floatStory({ entries: overflowEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }), - args: { maxVisibleItems: 3 }, -} - -/** `maxVisibleItems: 9` — a higher capacity absorbs the overflow set inline. */ -export const HigherCapacity: Story = { - ...floatStory({ entries: overflowEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }), - args: { maxVisibleItems: 9 }, -} - -/** `maxVisibleItems: 8` — exactly one entry overflows, so it renders inline instead of behind the overflow button. */ -export const SingleOverflowItem: Story = { - ...floatStory({ entries: overflowEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }), - args: { maxVisibleItems: 8 }, -} - -/** `glowSize: 280`, `glowBlur: 90` — a larger, softer ambient glow. */ -export const LargeGlow: Story = { - ...floatStory({ entries: categorizedEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }), - args: { glowSize: 280, glowBlur: 90 }, -} diff --git a/packages/core/src/client/webcomponents/components/dock/Dock.vue b/packages/core/src/client/webcomponents/components/dock/Dock.vue deleted file mode 100644 index 5935b1747..000000000 --- a/packages/core/src/client/webcomponents/components/dock/Dock.vue +++ /dev/null @@ -1,329 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/DockContextMenu.ts b/packages/core/src/client/webcomponents/components/dock/DockContextMenu.ts deleted file mode 100644 index f85948ee2..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockContextMenu.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { DevToolsDockEntry } from '@vitejs/devtools-kit' -import type { DocksContext } from '@vitejs/devtools-kit/client' -import { h } from 'vue' -import { setDockContextMenu } from '../../state/floating-tooltip' -import { isDockPopupSupported, requestDockPopupOpen, useIsDockPopupOpen } from '../../state/popup' - -// @unocss-include - -interface DockMenuItem { - label: string - icon: string - action: () => void - visible: boolean -} - -function renderMenuItem(item: DockMenuItem) { - return h('button', { - class: 'flex items-center gap-2 px3 py1.5 rounded hover:bg-active transition text-left', - onClick: item.action, - }, [ - h('div', { class: `${item.icon} text-base op60` }), - h('span', item.label), - ]) -} - -function hideDock(context: DocksContext, entry: DevToolsDockEntry) { - const settingsStore = context.docks.settings - const id = entry.id - settingsStore.mutate((state) => { - if (!state.docksHidden.includes(id)) - state.docksHidden = [...state.docksHidden, id] - }) - if (context.docks.selected?.id === id) - context.docks.switchEntry(null) - setDockContextMenu(null) -} - -function refreshDock(context: DocksContext, entry: DevToolsDockEntry) { - const state = context.docks.getStateById(entry.id) - const iframe = state?.domElements.iframe - if (!iframe) { - setDockContextMenu(null) - return - } - const src = iframe.src - iframe.src = '' - iframe.src = src - setDockContextMenu(null) -} - -function canHide(context: DocksContext, entry: DevToolsDockEntry) { - if (entry.id === '~settings') - return false - return context.docks.entries.some(item => item.id === entry.id) -} - -function canRefresh(entry: DevToolsDockEntry) { - return entry.type === 'iframe' -} - -export function openDockContextMenu(options: { - context: DocksContext - entry: DevToolsDockEntry - el: HTMLElement - gap?: number -}) { - const { context, entry, el, gap = 6 } = options - const isEdgeMode = context.panel.store.mode === 'edge' - const items: DockMenuItem[] = [ - { - label: 'Hide', - icon: 'i-ph-eye-slash-duotone', - action: () => hideDock(context, entry), - visible: canHide(context, entry), - }, - { - label: 'Refresh', - icon: 'i-ph-arrow-clockwise-duotone', - action: () => refreshDock(context, entry), - visible: canRefresh(entry), - }, - { - label: isEdgeMode ? 'Float Mode' : 'Edge Mode', - icon: isEdgeMode ? 'i-ph-arrows-out-duotone' : 'i-ph-square-half-bottom-duotone', - action: () => { - if (isEdgeMode) { - // Reset float position defaults based on current edge position - const store = context.panel.store - switch (store.position) { - case 'bottom': - store.left = 50 - store.top = 100 - break - case 'top': - store.left = 50 - store.top = 0 - break - case 'left': - store.left = 0 - store.top = 50 - break - case 'right': - store.left = 100 - store.top = 50 - break - } - store.mode = 'float' - } - else { - context.panel.store.mode = 'edge' - } - setDockContextMenu(null) - }, - visible: context.clientType === 'embedded', - }, - { - label: 'Popup', - icon: 'i-ph-arrow-square-out-duotone', - action: () => { - setDockContextMenu(null) - requestDockPopupOpen(context) - }, - visible: isDockPopupSupported() && !useIsDockPopupOpen().value && context.clientType === 'embedded', - }, - ].filter(item => item.visible) - - if (items.length === 0) - return - - setDockContextMenu({ - el, - gap, - content: () => h('div', { class: 'flex flex-col text-sm min-w-36 mx--1' }, items.map(renderMenuItem)), - }) -} diff --git a/packages/core/src/client/webcomponents/components/dock/DockEdge.stories.ts b/packages/core/src/client/webcomponents/components/dock/DockEdge.stories.ts deleted file mode 100644 index 439ee4e69..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockEdge.stories.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { h } from 'vue' -import { categorizedEntries, groupedEntries } from '../../stories/fixtures' -import { mountWithContext } from '../../stories/story-helpers' -import FloatingElements from '../floating/FloatingElements.vue' -import DockEdge from './DockEdge.vue' - -/** A stand-in panel body (the real one mounts iframe/custom views). */ -function body(entry: any) { - return h('div', { class: 'w-full h-full p6 font-sans color-base of-auto' }, [ - h('div', { class: 'text-lg font-medium mb2' }, entry?.title ?? 'No selection'), - h('div', { class: 'op60 text-sm' }, `Panel content for "${entry?.id ?? '—'}"`), - ]) -} - -const meta = { - title: 'Dock/Shell/Edge Panel', - component: DockEdge, - tags: ['autodocs'], - parameters: { - layout: 'fullscreen', - docs: { - story: { inline: false, height: '520px' }, - description: { - component: 'The edge-docked shell (edge mode): a toolbar pinned to one viewport edge with a resizable panel. The `#view` slot is stubbed here in place of the live view renderer.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -function edgeStory(position: 'top' | 'right' | 'bottom' | 'left', open = true) { - return { - render: () => ({ - setup: () => mountWithContext( - { - entries: categorizedEntries, - selectedId: open ? 'overview' : null, - panel: { mode: 'edge', position, open, height: 40, width: 30 }, - }, - ctx => [ - h(DockEdge, { context: ctx }, { view: ({ entry }: any) => body(entry) }), - h(FloatingElements), - ], - ), - }), - } satisfies Story -} - -/** Bottom edge with the panel open. */ -export const Bottom: Story = edgeStory('bottom') - -/** Top edge. */ -export const Top: Story = edgeStory('top') - -/** Left edge — toolbar runs vertically. */ -export const Left: Story = edgeStory('left') - -/** Right edge. */ -export const Right: Story = edgeStory('right') - -/** Toolbar only — nothing selected, so the panel body is collapsed away. */ -export const ToolbarOnly: Story = edgeStory('bottom', false) - -/** Edge dock hosting a group — the group rail shows inside the panel. */ -export const WithGroup: Story = { - render: () => ({ - setup: () => mountWithContext( - { - entries: groupedEntries, - selectedId: 'nuxt:overview', - panel: { mode: 'edge', position: 'bottom', open: true, height: 45 }, - }, - ctx => [ - h(DockEdge, { context: ctx }, { view: ({ entry }: any) => body(entry) }), - h(FloatingElements), - ], - ), - }), -} diff --git a/packages/core/src/client/webcomponents/components/dock/DockEdge.vue b/packages/core/src/client/webcomponents/components/dock/DockEdge.vue deleted file mode 100644 index b676df964..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockEdge.vue +++ /dev/null @@ -1,292 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/DockEmbedded.stories.ts b/packages/core/src/client/webcomponents/components/dock/DockEmbedded.stories.ts deleted file mode 100644 index ebedf1116..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockEmbedded.stories.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import type { DevToolsDockEntry } from '@vitejs/devtools-kit' -import { h } from 'vue' -import { categorizedEntries } from '../../stories/fixtures' -import { mountWithContext } from '../../stories/story-helpers' -import DockEmbedded from './DockEmbedded.vue' - -// Entries whose iframes load `about:blank`, so the full shell (address bar + -// iframe pane) renders without reaching for a dev server. -const blankEntries: DevToolsDockEntry[] = [ - { id: 'overview', type: 'iframe', url: 'about:blank', title: 'Overview', icon: 'ph:gauge-duotone' }, - { id: 'inspect', type: 'iframe', url: 'about:blank', title: 'Inspect', icon: 'ph:stethoscope-duotone' }, - { id: 'assets', type: 'iframe', url: 'about:blank', title: 'Assets', icon: 'ph:images-duotone' }, -] as DevToolsDockEntry[] - -const meta = { - title: 'Dock/Shell/Embedded', - component: DockEmbedded, - tags: ['autodocs'], - parameters: { - layout: 'fullscreen', - docs: { - story: { inline: false, height: '540px' }, - description: { - component: 'The full embedded shell as injected into a host app: the dock bar plus panel (float or edge), floating overlays, command palette, toasts and confirm dialog — switched by `panel.store.mode`.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** Float mode, panel closed — just the resting dock bar. */ -export const FloatClosed: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: categorizedEntries, panel: { mode: 'float', position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }, - ctx => h(DockEmbedded, { context: ctx }), - ), - }), -} - -/** Float mode with the panel open over an `about:blank` iframe. */ -export const FloatOpen: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: blankEntries, selectedId: 'overview', panel: { mode: 'float', position: 'bottom', left: 50, top: 100, inactiveTimeout: -1, width: 60, height: 60 } }, - ctx => h(DockEmbedded, { context: ctx }), - ), - }), -} - -/** - * `panelOverlapFactor: 0.2` — the panel slides clear of the dock bar, leaving - * most of the bar hanging past the panel edge so the iframe underneath stays - * readable (compare with `FloatOpen`, which uses the default `0.5`). - */ -export const FloatReducedOverlap: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: blankEntries, selectedId: 'overview', panel: { mode: 'float', position: 'bottom', left: 50, top: 100, inactiveTimeout: -1, width: 60, height: 60 } }, - ctx => h(DockEmbedded, { context: ctx, layout: { panelOverlapFactor: 0.2 } }), - ), - }), -} - -/** Edge mode, docked to the bottom with the panel open. */ -export const Edge: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: blankEntries, selectedId: 'overview', panel: { mode: 'edge', position: 'bottom', open: true, height: 45 } }, - ctx => h(DockEmbedded, { context: ctx }), - ), - }), -} - -/** Unauthorized — the shell forces float mode and shows the warning. */ -export const Unauthorized: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: categorizedEntries, isTrusted: false, panel: { mode: 'edge', position: 'bottom', inactiveTimeout: -1 } }, - ctx => h(DockEmbedded, { context: ctx }), - ), - }), -} diff --git a/packages/core/src/client/webcomponents/components/dock/DockEmbedded.vue b/packages/core/src/client/webcomponents/components/dock/DockEmbedded.vue deleted file mode 100644 index 142773512..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockEmbedded.vue +++ /dev/null @@ -1,80 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/DockEntries.vue b/packages/core/src/client/webcomponents/components/dock/DockEntries.vue deleted file mode 100644 index 299d628e3..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockEntries.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/DockEntriesWithCategories.stories.ts b/packages/core/src/client/webcomponents/components/dock/DockEntriesWithCategories.stories.ts deleted file mode 100644 index 4b7c3117c..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockEntriesWithCategories.stories.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { h } from 'vue' -import { categorizedEntries, groupedEntries } from '../../stories/fixtures' -import { mountWithContext, stage } from '../../stories/story-helpers' -import DockEntriesWithCategories from './DockEntriesWithCategories.vue' - -/** Horizontal dock-bar pill wrapper. */ -function bar(children: any, vertical = false) { - return h('div', { - class: [ - 'flex items-center gap-0.5 p1.5 rounded-full bg-glass border border-base shadow color-base', - vertical ? 'flex-col' : 'flex-row', - ], - }, children) -} - -const meta = { - title: 'Dock/Bar/EntriesWithCategories', - component: DockEntriesWithCategories, - tags: ['autodocs'], -} satisfies Meta - -export default meta -type Story = StoryObj - -/** - * Entries spanning several categories. Categories are ordered by the built-in - * ranking and separated by a divider. - */ -export const Categories: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: categorizedEntries }, - ctx => stage(bar(h(DockEntriesWithCategories, { - context: ctx, - groups: ctx.docks.groupedEntries, - selected: ctx.docks.selected, - isVertical: false, - onSelect: (e: any) => ctx.docks.switchEntry(e?.id), - }))), - ), - }), -} - -/** - * A bar with collapsed groups — the group members fold behind their group - * button and only the group icon shows on the bar. - */ -export const WithGroups: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: groupedEntries }, - ctx => stage(bar(h(DockEntriesWithCategories, { - context: ctx, - groups: ctx.docks.groupedEntries, - selected: ctx.docks.selected, - isVertical: false, - onSelect: (e: any) => ctx.docks.switchEntry(e?.id), - }))), - ), - }), -} - -/** The same bar rotated for a left/right edge dock. */ -export const Vertical: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: categorizedEntries }, - ctx => stage(bar(h(DockEntriesWithCategories, { - context: ctx, - groups: ctx.docks.groupedEntries, - selected: ctx.docks.selected, - isVertical: true, - }), true)), - ), - }), -} diff --git a/packages/core/src/client/webcomponents/components/dock/DockEntriesWithCategories.vue b/packages/core/src/client/webcomponents/components/dock/DockEntriesWithCategories.vue deleted file mode 100644 index 38063cdaa..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockEntriesWithCategories.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/DockEntry.stories.ts b/packages/core/src/client/webcomponents/components/dock/DockEntry.stories.ts deleted file mode 100644 index 5eca60252..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockEntry.stories.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { h } from 'vue' -import { basicEntries, iframe } from '../../stories/fixtures' -import { mountWithContext, stage } from '../../stories/story-helpers' -import DockEntry from './DockEntry.vue' - -const sample = iframe('overview', 'Overview', 'ph:gauge-duotone') - -interface EntryArgs { - isSelected?: boolean - isDimmed?: boolean - isVertical?: boolean - isAction?: boolean - badge?: string - tooltip?: boolean -} - -/** A pill container that echoes the dock bar, so the button reads in context. */ -function pill(children: any) { - return h('div', { class: 'flex items-center gap-1 p1.5 rounded-full bg-glass border border-base shadow' }, children) -} - -const meta = { - title: 'Dock/Entry', - component: DockEntry, - tags: ['autodocs'], - args: { - isSelected: false, - isDimmed: false, - isVertical: false, - isAction: false, - badge: '', - tooltip: true, - }, - render: (args: EntryArgs) => ({ - setup: () => mountWithContext( - { entries: basicEntries }, - ctx => stage(pill(h(DockEntry, { - context: ctx, - dock: sample, - isSelected: args.isSelected, - isDimmed: args.isDimmed, - isVertical: args.isVertical, - isAction: args.isAction, - badge: args.badge || undefined, - tooltip: args.tooltip, - }))), - ), - }), -} satisfies Meta - -export default meta -type Story = StoryObj - -/** Idle button — the resting state on the dock bar. */ -export const Default: Story = {} - -/** The active entry: scaled up and tinted while its panel owns the screen. */ -export const Selected: Story = { args: { isSelected: true } } - -/** Dimmed and desaturated because a sibling entry is the active one. */ -export const Dimmed: Story = { args: { isDimmed: true } } - -/** Action entries (one-shot commands) get a circular, filled treatment. */ -export const Action: Story = { args: { isAction: true } } - -/** A count badge in the corner (e.g. pending items). */ -export const WithBadge: Story = { args: { badge: '3' } } - -/** Rotated for a left/right edge dock. */ -export const Vertical: Story = { args: { isVertical: true } } - -/** Every state laid out together for a quick visual diff. */ -export const States: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: basicEntries }, - ctx => stage(pill([ - h(DockEntry, { context: ctx, dock: sample }), - h(DockEntry, { context: ctx, dock: sample, isSelected: true }), - h(DockEntry, { context: ctx, dock: sample, isDimmed: true }), - h(DockEntry, { context: ctx, dock: sample, isAction: true }), - h(DockEntry, { context: ctx, dock: sample, badge: '9+' }), - ])), - ), - }), -} diff --git a/packages/core/src/client/webcomponents/components/dock/DockEntry.vue b/packages/core/src/client/webcomponents/components/dock/DockEntry.vue deleted file mode 100644 index a94574bd6..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockEntry.vue +++ /dev/null @@ -1,102 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/DockGroupButton.stories.ts b/packages/core/src/client/webcomponents/components/dock/DockGroupButton.stories.ts deleted file mode 100644 index 0598c083a..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockGroupButton.stories.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import type { DevToolsViewGroup } from '@vitejs/devtools-kit' -import { h } from 'vue' -import { groupedEntries } from '../../stories/fixtures' -import { mountWithContext, stage } from '../../stories/story-helpers' -import FloatingElements from '../floating/FloatingElements.vue' -import DockGroupButton from './DockGroupButton.vue' - -const nuxtGroup = groupedEntries.find(e => e.id === 'nuxt') as DevToolsViewGroup -const playgroundGroup = groupedEntries.find(e => e.id === 'playground') as DevToolsViewGroup - -function bar(children: any) { - return h('div', { class: 'flex items-center gap-0.5 p1.5 rounded-full bg-glass border border-base shadow color-base' }, children) -} - -const meta = { - title: 'Dock/Group/Button', - component: DockGroupButton, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: 'The dock-bar button representing a group. Click behaviour depends on the group: a group with `defaultChildId` opens that member directly, otherwise it reveals a popover of members. `FloatingElements` is mounted alongside so the popover renders.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** - * A popover-only group (no `defaultChildId`): clicking reveals the member - * popover. - */ -export const PopoverOnly: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: groupedEntries }, - ctx => stage([ - bar(h(DockGroupButton, { - context: ctx, - group: playgroundGroup, - isVertical: false, - selected: ctx.docks.selected, - onSelect: (e: any) => ctx.docks.switchEntry(e?.id), - })), - h(FloatingElements), - ]), - ), - }), -} - -/** - * A group with a `defaultChildId`: clicking opens that member straight away - * instead of showing the popover. - */ -export const WithDefaultChild: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: groupedEntries }, - ctx => stage([ - bar(h(DockGroupButton, { - context: ctx, - group: nuxtGroup, - isVertical: false, - selected: ctx.docks.selected, - onSelect: (e: any) => ctx.docks.switchEntry(e?.id), - })), - h(FloatingElements), - ]), - ), - }), -} - -/** Active state — a member of the group currently owns the panel. */ -export const Active: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: groupedEntries, selectedId: 'nuxt:pages' }, - ctx => stage([ - bar(h(DockGroupButton, { - context: ctx, - group: nuxtGroup, - isVertical: false, - selected: ctx.docks.selected, - onSelect: (e: any) => ctx.docks.switchEntry(e?.id), - })), - h(FloatingElements), - ]), - ), - }), -} diff --git a/packages/core/src/client/webcomponents/components/dock/DockGroupButton.vue b/packages/core/src/client/webcomponents/components/dock/DockGroupButton.vue deleted file mode 100644 index 27a9125b8..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockGroupButton.vue +++ /dev/null @@ -1,134 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/DockGroupPopover.stories.ts b/packages/core/src/client/webcomponents/components/dock/DockGroupPopover.stories.ts deleted file mode 100644 index ccab03bea..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockGroupPopover.stories.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import type { DevToolsViewGroup } from '@vitejs/devtools-kit' -import { DEFAULT_STATE_USER_SETTINGS } from '@vitejs/devtools-kit/constants' -import { h } from 'vue' -import { getGroupMembersGrouped } from '../../state/dock-settings' -import { group, groupedEntries, subcategorizedGroupEntries, toolsGroup } from '../../stories/fixtures' -import { mountWithContext, stage } from '../../stories/story-helpers' -import DockGroupPopover from './DockGroupPopover.vue' - -const settings = DEFAULT_STATE_USER_SETTINGS() -const nuxtGroup = groupedEntries.find(e => e.id === 'nuxt') as DevToolsViewGroup -// Members split by in-group sub-category (the shape the popover renders). -const nuxtMembers = getGroupMembersGrouped(groupedEntries, 'nuxt', settings) -const toolsMembers = getGroupMembersGrouped(subcategorizedGroupEntries, 'tools', settings) - -/** A framed surface that stands in for the floating popover container. */ -function popover(children: any) { - return h('div', { class: 'bg-glass color-base border border-base rounded-lg shadow p1 font-sans' }, children) -} - -const meta = { - title: 'Dock/Group/Popover', - component: DockGroupPopover, - tags: ['autodocs'], -} satisfies Meta - -export default meta -type Story = StoryObj - -/** The group's members listed under its heading; click to select one. */ -export const Default: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: groupedEntries }, - ctx => stage(popover(h(DockGroupPopover, { - context: ctx, - group: nuxtGroup, - members: nuxtMembers, - selectedId: ctx.docks.selectedId, - onSelect: (entry: any) => ctx.docks.switchEntry(entry.id), - }))), - ), - }), -} - -/** With one member already active — it gets the tinted, highlighted row. */ -export const WithSelection: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: groupedEntries, selectedId: 'nuxt:pages' }, - ctx => stage(popover(h(DockGroupPopover, { - context: ctx, - group: nuxtGroup, - members: nuxtMembers, - selectedId: ctx.docks.selectedId, - onSelect: (entry: any) => ctx.docks.switchEntry(entry.id), - }))), - ), - }), -} - -/** A member carrying a count badge. */ -export const WithBadge: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: groupedEntries }, - ctx => stage(popover(h(DockGroupPopover, { - context: ctx, - group: nuxtGroup, - members: nuxtMembers, - selectedId: 'nuxt:components', - onSelect: (entry: any) => ctx.docks.switchEntry(entry.id), - }))), - ), - }), -} - -/** Members split across in-group sub-categories, shown with section dividers. */ -export const WithSubcategories: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: subcategorizedGroupEntries }, - ctx => stage(popover(h(DockGroupPopover, { - context: ctx, - group: toolsGroup, - members: toolsMembers, - selectedId: ctx.docks.selectedId, - onSelect: (entry: any) => ctx.docks.switchEntry(entry.id), - }))), - ), - }), -} - -/** An empty group falls back to the "No tools yet" placeholder. */ -export const Empty: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: [group('empty', 'Empty', 'ph:folder-dashed-duotone')] }, - ctx => stage(popover(h(DockGroupPopover, { - context: ctx, - group: group('empty', 'Empty', 'ph:folder-dashed-duotone') as DevToolsViewGroup, - members: [], - selectedId: null, - }))), - ), - }), -} diff --git a/packages/core/src/client/webcomponents/components/dock/DockGroupPopover.vue b/packages/core/src/client/webcomponents/components/dock/DockGroupPopover.vue deleted file mode 100644 index 1d1a57112..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockGroupPopover.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/DockGroupSidebar.stories.ts b/packages/core/src/client/webcomponents/components/dock/DockGroupSidebar.stories.ts deleted file mode 100644 index 00c68263b..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockGroupSidebar.stories.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import type { DevToolsViewGroup } from '@vitejs/devtools-kit' -import { h } from 'vue' -import { groupedEntries, subcategorizedGroupEntries, toolsGroup } from '../../stories/fixtures' -import { mountWithContext, stage } from '../../stories/story-helpers' -import DockGroupSidebar from './DockGroupSidebar.vue' - -const nuxtGroup = groupedEntries.find(e => e.id === 'nuxt') as DevToolsViewGroup - -/** A bordered shell that mimics the panel the sidebar lives inside. */ -function shell(children: any, heightClass = 'h-80') { - return h('div', { class: `flex ${heightClass} bg-glass color-base border border-base rounded-lg shadow overflow-hidden font-sans` }, [ - children, - h('div', { class: 'flex-1 flex items-center justify-center op40 text-sm' }, 'panel body'), - ]) -} - -const meta = { - title: 'Dock/Group/Sidebar', - component: DockGroupSidebar, - tags: ['autodocs'], -} satisfies Meta - -export default meta -type Story = StoryObj - -/** - * The group rail shown down the side of a panel when the active entry belongs - * to a group — the group anchor on top, its members below. - */ -export const Default: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: groupedEntries, selectedId: 'nuxt:overview' }, - ctx => stage(shell(h(DockGroupSidebar, { - context: ctx, - group: nuxtGroup, - selectedId: ctx.docks.selectedId, - }))), - ), - }), -} - -/** A different member active — the highlight follows the selection. */ -export const WithSelection: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: groupedEntries, selectedId: 'nuxt:components' }, - ctx => stage(shell(h(DockGroupSidebar, { - context: ctx, - group: nuxtGroup, - selectedId: ctx.docks.selectedId, - }))), - ), - }), -} - -/** - * A short frame folds the members that don't fit into a bottom "show more" - * button (with a count badge). Here the active member (`tools:graph`) lands in - * the overflow, so the show-more button is highlighted to flag the hidden - * selection. - */ -export const Overflow: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: subcategorizedGroupEntries, selectedId: 'tools:graph' }, - ctx => stage(shell(h(DockGroupSidebar, { - context: ctx, - group: toolsGroup, - selectedId: ctx.docks.selectedId, - }), 'h-44')), - ), - }), -} diff --git a/packages/core/src/client/webcomponents/components/dock/DockGroupSidebar.vue b/packages/core/src/client/webcomponents/components/dock/DockGroupSidebar.vue deleted file mode 100644 index 26d4688ee..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockGroupSidebar.vue +++ /dev/null @@ -1,197 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/DockIcon.stories.ts b/packages/core/src/client/webcomponents/components/dock/DockIcon.stories.ts deleted file mode 100644 index e27901040..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockIcon.stories.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import DockIcon from './DockIcon.vue' - -const meta = { - title: 'Dock/Icon', - component: DockIcon, - tags: ['autodocs'], - // Render at a legible size — DockIcon fills its box. Use the template-based - // `` decorator (the stable Storybook Vue API). - decorators: [() => ({ template: '
' })], - argTypes: { - icon: { control: false }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** An Iconify collection reference (`collection:name`), fetched on demand. */ -export const Iconify: Story = { - args: { icon: 'ph:gauge-duotone' }, -} - -/** A logo-style Iconify icon, as a framework group would use. */ -export const Logo: Story = { - args: { icon: 'logos:nuxt-icon' }, -} - -/** - * An object icon with distinct light/dark variants — only the one matching the - * active theme is shown (toggle the theme toolbar to compare). - */ -export const LightDark: Story = { - args: { icon: { light: 'ph:sun-duotone', dark: 'ph:moon-duotone' } }, -} - -/** The bundled Vite+ core mark, referenced by the sentinel `builtin:` id. */ -export const BuiltinVitePlus: Story = { - args: { icon: 'builtin:vite-plus-core' }, -} - -/** A raw image URL (here an inline data URI) rendered via ``. */ -export const ImageUrl: Story = { - args: { - icon: `data:image/svg+xml,${ - encodeURIComponent('')}`, - }, -} diff --git a/packages/core/src/client/webcomponents/components/dock/DockIcon.vue b/packages/core/src/client/webcomponents/components/dock/DockIcon.vue deleted file mode 100644 index 93c63cb1e..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockIcon.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/DockOverflowButton.stories.ts b/packages/core/src/client/webcomponents/components/dock/DockOverflowButton.stories.ts deleted file mode 100644 index 698775e83..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockOverflowButton.stories.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { h } from 'vue' -import { docksSplitGroupsWithCapacity } from '../../state/dock-settings' -import { overflowEntries } from '../../stories/fixtures' -import { mountWithContext, stage } from '../../stories/story-helpers' -import FloatingElements from '../floating/FloatingElements.vue' -import DockOverflowButton from './DockOverflowButton.vue' - -function bar(children: any) { - return h('div', { class: 'flex items-center gap-0.5 p1.5 rounded-full bg-glass border border-base shadow color-base' }, children) -} - -const meta = { - title: 'Dock/Bar/OverflowButton', - component: DockOverflowButton, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: 'Shown when the bar exceeds its slot capacity. Carries a badge with the hidden count and reveals the remaining entries in a popover on click.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** Enough entries to overflow the 5-slot capacity; the remainder go behind the button. */ -export const Default: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: overflowEntries }, - (ctx) => { - const overflow = docksSplitGroupsWithCapacity(ctx.docks.groupedEntries, 5).overflow - return stage([ - bar(h(DockOverflowButton, { - context: ctx, - isVertical: false, - groups: overflow, - selected: ctx.docks.selected, - onSelect: (e: any) => ctx.docks.switchEntry(e?.id), - })), - h(FloatingElements), - ]) - }, - ), - }), -} diff --git a/packages/core/src/client/webcomponents/components/dock/DockOverflowButton.vue b/packages/core/src/client/webcomponents/components/dock/DockOverflowButton.vue deleted file mode 100644 index acd1ed052..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockOverflowButton.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/DockPanel.stories.ts b/packages/core/src/client/webcomponents/components/dock/DockPanel.stories.ts deleted file mode 100644 index 6e3b72605..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockPanel.stories.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { h } from 'vue' -import { categorizedEntries, groupedEntries } from '../../stories/fixtures' -import { mountWithContext } from '../../stories/story-helpers' -import DockPanel from './DockPanel.vue' - -const MARGINS = { left: 2, top: 2, right: 2, bottom: 2 } - -function body(entry: any) { - return h('div', { class: 'w-full h-full p6 font-sans color-base of-auto' }, [ - h('div', { class: 'text-lg font-medium mb2' }, entry?.title ?? 'No selection'), - h('div', { class: 'op60 text-sm' }, `Panel content for "${entry?.id ?? '—'}"`), - ]) -} - -const meta = { - title: 'Dock/Shell/Float Panel', - component: DockPanel, - tags: ['autodocs'], - parameters: { - layout: 'fullscreen', - docs: { - story: { inline: false, height: '540px' }, - description: { - component: 'The floating panel shown above the dock bar in float mode. The `#view` slot is stubbed here in place of the live view renderer.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** A plain entry selected, panel anchored to the bottom. */ -export const Default: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: categorizedEntries, selectedId: 'overview', panel: { position: 'bottom', width: 60, height: 60 } }, - ctx => h(DockPanel, { - context: ctx, - selected: ctx.docks.selected, - panelMargins: MARGINS, - }, { view: ({ entry }: any) => body(entry) }), - ), - }), -} - -/** A group member selected — the group rail appears down the left of the panel. */ -export const WithGroupSidebar: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: groupedEntries, selectedId: 'nuxt:overview', panel: { position: 'bottom', width: 60, height: 60 } }, - ctx => h(DockPanel, { - context: ctx, - selected: ctx.docks.selected, - panelMargins: MARGINS, - }, { view: ({ entry }: any) => body(entry) }), - ), - }), -} - -/** Anchored to the right edge. */ -export const RightAnchored: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: categorizedEntries, selectedId: 'overview', panel: { position: 'right', width: 40, height: 80 } }, - ctx => h(DockPanel, { - context: ctx, - selected: ctx.docks.selected, - panelMargins: MARGINS, - }, { view: ({ entry }: any) => body(entry) }), - ), - }), -} diff --git a/packages/core/src/client/webcomponents/components/dock/DockPanel.vue b/packages/core/src/client/webcomponents/components/dock/DockPanel.vue deleted file mode 100644 index b5a16d075..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockPanel.vue +++ /dev/null @@ -1,212 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/DockPanelResizer.vue b/packages/core/src/client/webcomponents/components/dock/DockPanelResizer.vue deleted file mode 100644 index 8c4475404..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockPanelResizer.vue +++ /dev/null @@ -1,144 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/DockStandalone.stories.ts b/packages/core/src/client/webcomponents/components/dock/DockStandalone.stories.ts deleted file mode 100644 index 3335c01c3..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockStandalone.stories.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import type { DevToolsDockEntry } from '@vitejs/devtools-kit' -import { h } from 'vue' -import { mountWithContext } from '../../stories/story-helpers' -import DockStandalone from './DockStandalone.vue' - -// Standalone renders the selected entry's view; use about:blank iframes so the -// shell renders without a dev server. -const blankEntries: DevToolsDockEntry[] = [ - { id: 'overview', type: 'iframe', url: 'about:blank', title: 'Overview', icon: 'ph:gauge-duotone', category: 'app' }, - { id: 'routes', type: 'iframe', url: 'about:blank', title: 'Routes', icon: 'ph:signpost-duotone', category: 'app' }, - { id: 'nuxt', type: 'group', title: 'Nuxt', icon: 'logos:nuxt-icon', category: 'framework', defaultChildId: 'nuxt:overview' }, - { id: 'nuxt:overview', type: 'iframe', url: 'about:blank', title: 'Overview', icon: 'ph:gauge-duotone', groupId: 'nuxt' }, - { id: 'nuxt:pages', type: 'iframe', url: 'about:blank', title: 'Pages', icon: 'ph:files-duotone', groupId: 'nuxt' }, -] as DevToolsDockEntry[] - -const meta = { - title: 'Dock/Shell/Standalone', - component: DockStandalone, - tags: ['autodocs'], - parameters: { - layout: 'fullscreen', - docs: { - story: { inline: false, height: '560px' }, - description: { - component: 'The standalone shell: a full-window sidebar + view layout (no floating dock), used when DevTools runs in its own page/window.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** Sidebar of tools with the first entry auto-selected. */ -export const Default: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: blankEntries, clientType: 'standalone' }, - ctx => h(DockStandalone, { context: ctx }), - ), - }), -} - -/** A group member selected — the group rail appears beside the view. */ -export const WithGroup: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: blankEntries, clientType: 'standalone', selectedId: 'nuxt:overview' }, - ctx => h(DockStandalone, { context: ctx }), - ), - }), -} - -/** Unauthorized — the standalone window shows the auth notice. */ -export const Unauthorized: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: blankEntries, clientType: 'standalone', isTrusted: false }, - ctx => h(DockStandalone, { context: ctx }), - ), - }), -} diff --git a/packages/core/src/client/webcomponents/components/dock/DockStandalone.vue b/packages/core/src/client/webcomponents/components/dock/DockStandalone.vue deleted file mode 100644 index 1b71657ab..000000000 --- a/packages/core/src/client/webcomponents/components/dock/DockStandalone.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/dock/dock-layout.ts b/packages/core/src/client/webcomponents/components/dock/dock-layout.ts deleted file mode 100644 index 3f32bb93e..000000000 --- a/packages/core/src/client/webcomponents/components/dock/dock-layout.ts +++ /dev/null @@ -1,235 +0,0 @@ -/** - * Layout tunables for the floating dock bar (`Dock.vue`). - * - * Every spatial magic number the float-mode shell relies on lives here — bar - * dimensions, viewport spacing, item capacity and drag-snapping behaviour — so - * the bar's look and feel can be adjusted from a single place instead of being - * scattered across the component's template, script and stylesheet. - * - * Pixel values are logical CSS pixels. Percent values are relative to the - * viewport (the panel store persists `left`/`top` as `0`–`100` percentages). - * The dimension fields ({@link DockLayout.barHeight} and friends) are projected - * to CSS custom properties by {@link dockLayoutCssVars} and consumed by - * `style.css`; the rest feed the positioning math in this module. - */ -export interface DockLayout { - /** Height of the floating dock bar, in px. */ - barHeight: number - /** Minimum width of the bar before its content grows it, in px. */ - barMinWidth: number - /** Side length of the collapsed (minimized) nub, in px. */ - minimizedSize: number - - /** Diameter of the ambient glow behind the bar, in px. */ - glowSize: number - /** Blur radius applied to the glow, in px. */ - glowBlur: number - - /** - * Maximum number of dock items shown inline on the bar. Any beyond this - * capacity are folded into the overflow button. - */ - maxVisibleItems: number - - /** - * Gap between the bar (and its panel) and the viewport edge, in px. Added on - * top of the device safe-area insets. - */ - viewportMargin: number - - /** - * How much of the dock bar's thickness overlaps the panel/iframe, as a - * fraction of that thickness. `0.5` floats the dock's inner half over the - * panel (its outer half hangs past the panel edge); lower it toward `0` to - * slide the panel clear of the bar so the content underneath stays readable, - * or raise it toward `1` to tuck the panel further behind the bar. - */ - panelOverlapFactor: number - - /** - * Snap-to-edge zone width, as a viewport percentage. While dragging, a - * position within this distance of an edge snaps flush to `0` / `100`. - */ - edgeSnapPercent: number - /** - * Snap-to-center zone half-width, as a viewport percentage. A position within - * this distance of the midpoint snaps to `50`. - */ - centerSnapPercent: number - /** - * Height of the top/bottom detection zones (in px) used when deciding which - * edge a dragged bar should dock to. A larger value widens the "top" and - * "bottom" wedges at the expense of "left"/"right". - */ - edgeZoneHeight: number -} - -/** Which viewport edge the floating bar is docked to. */ -export type DockEdge = 'top' | 'right' | 'bottom' | 'left' - -/** Per-side spacing, in px (viewport margins, safe-area insets, ...). */ -export interface DockMargins { - left: number - top: number - right: number - bottom: number -} - -/** The built-in dock layout. Override individual fields via {@link resolveDockLayout}. */ -export const DEFAULT_DOCK_LAYOUT: DockLayout = Object.freeze({ - barHeight: 34, - barMinWidth: 100, - minimizedSize: 22, - glowSize: 160, - glowBlur: 60, - maxVisibleItems: 5, - viewportMargin: 2, - panelOverlapFactor: -0.04, - edgeSnapPercent: 5, - centerSnapPercent: 2, - edgeZoneHeight: 70, -}) - -/** Merge partial overrides over {@link DEFAULT_DOCK_LAYOUT}. */ -export function resolveDockLayout(overrides?: Partial): DockLayout { - if (!overrides) - return DEFAULT_DOCK_LAYOUT - return { ...DEFAULT_DOCK_LAYOUT, ...overrides } -} - -/** - * Project a layout's dimension fields to the CSS custom properties consumed by - * `style.css`. Bind the result on the `#vite-devtools-anchor` root so the - * values cascade to the bar, minimized nub and glow. - */ -export function dockLayoutCssVars(layout: DockLayout): Record { - return { - '--vite-devtools-dock-height': `${layout.barHeight}px`, - '--vite-devtools-dock-min-width': `${layout.barMinWidth}px`, - '--vite-devtools-dock-minimized-size': `${layout.minimizedSize}px`, - '--vite-devtools-dock-glow-size': `${layout.glowSize}px`, - '--vite-devtools-dock-glow-blur': `${layout.glowBlur}px`, - } -} - -/** - * Add the viewport margin on top of the device safe-area insets, yielding the - * effective per-side spacing the bar and its panel keep from the viewport edge. - */ -export function resolveViewportMargins(safeArea: DockMargins, layout: DockLayout): DockMargins { - return { - left: safeArea.left + layout.viewportMargin, - top: safeArea.top + layout.viewportMargin, - right: safeArea.right + layout.viewportMargin, - bottom: safeArea.bottom + layout.viewportMargin, - } -} - -/** - * Snap a `0`–`100` viewport percentage toward the nearest edge (`0` / `100`) or - * the center (`50`) when it falls inside the configured snap zones. - */ -export function snapDockPercent(value: number, layout: DockLayout): number { - if (value < layout.edgeSnapPercent) - return 0 - if (value > 100 - layout.edgeSnapPercent) - return 100 - if (Math.abs(value - 50) < layout.centerSnapPercent) - return 50 - return value -} - -/** - * Decide which viewport edge a dragged point belongs to, by comparing the angle - * from the viewport center against the four corner angles (offset inward by - * {@link DockLayout.edgeZoneHeight}). - */ -export function resolveDockEdge(params: { - x: number - y: number - viewportWidth: number - viewportHeight: number - layout: DockLayout -}): DockEdge { - const { x, y, viewportWidth, viewportHeight, layout } = params - const centerX = viewportWidth / 2 - const centerY = viewportHeight / 2 - const zone = layout.edgeZoneHeight - - const deg = Math.atan2(y - centerY, x - centerX) - const tl = Math.atan2(0 - centerY + zone, 0 - centerX) - const tr = Math.atan2(0 - centerY + zone, viewportWidth - centerX) - const bl = Math.atan2(viewportHeight - zone - centerY, 0 - centerX) - const br = Math.atan2(viewportHeight - zone - centerY, viewportWidth - centerX) - - if (deg >= tl && deg <= tr) - return 'top' - if (deg >= tr && deg <= br) - return 'right' - if (deg >= br && deg <= bl) - return 'bottom' - return 'left' -} - -function clamp(value: number, min: number, max: number): number { - return Math.min(Math.max(value, min), max) -} - -/** - * Resolve the pixel anchor (the center point of the bar) for a given edge. - * - * The axis pinned to the edge is offset by half the bar's cross-size plus the - * viewport margin; the free axis is driven by the stored percentage and clamped - * so the bar stays fully inside the viewport margins. - */ -export function resolveDockAnchor(params: { - edge: DockEdge - leftPercent: number - topPercent: number - viewportWidth: number - viewportHeight: number - dockWidth: number - dockHeight: number - margins: DockMargins -}): { left: number, top: number } { - const { - edge, - leftPercent, - topPercent, - viewportWidth, - viewportHeight, - dockWidth, - dockHeight, - margins, - } = params - - const halfWidth = dockWidth / 2 - const halfHeight = dockHeight / 2 - - const left = leftPercent * viewportWidth / 100 - const top = topPercent * viewportHeight / 100 - - switch (edge) { - case 'top': - return { - left: clamp(left, halfWidth + margins.left, viewportWidth - halfWidth - margins.right), - top: margins.top + halfHeight, - } - case 'right': - return { - left: viewportWidth - margins.right - halfHeight, - top: clamp(top, halfWidth + margins.top, viewportHeight - halfWidth - margins.bottom), - } - case 'left': - return { - left: margins.left + halfHeight, - top: clamp(top, halfWidth + margins.top, viewportHeight - halfWidth - margins.bottom), - } - case 'bottom': - default: - return { - left: clamp(left, halfWidth + margins.left, viewportWidth - halfWidth - margins.right), - top: viewportHeight - margins.bottom - halfHeight, - } - } -} diff --git a/packages/core/src/client/webcomponents/components/floating/FloatingElements.vue b/packages/core/src/client/webcomponents/components/floating/FloatingElements.vue deleted file mode 100644 index a0c9d1b72..000000000 --- a/packages/core/src/client/webcomponents/components/floating/FloatingElements.vue +++ /dev/null @@ -1,28 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/floating/FloatingPopover.stories.ts b/packages/core/src/client/webcomponents/components/floating/FloatingPopover.stories.ts deleted file mode 100644 index e770fce19..000000000 --- a/packages/core/src/client/webcomponents/components/floating/FloatingPopover.stories.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { computed, defineComponent, h, onMounted, ref, shallowRef } from 'vue' -import FloatingPopover from './FloatingPopover' - -// @unocss-include - -/** - * A story harness: render an anchor element, then feed its DOM node to the - * popover on mount (the real component positions against a live element). - */ -function harness(content: string | (() => any), anchorLabel = 'Anchor') { - return defineComponent({ - setup() { - const anchor = ref(null) - const item = shallowRef(null) - onMounted(() => { - if (anchor.value) - item.value = { el: anchor.value, content } - }) - return () => h('div', { class: 'flex items-center justify-center p20 min-h-80 font-sans' }, [ - h('button', { - ref: (el: any) => (anchor.value = el), - class: 'px3 py1.5 rounded border border-base bg-glass color-base shadow', - }, anchorLabel), - h(FloatingPopover, { item: item.value, dismissOnClickOutside: false }), - ]) - }, - }) -} - -const meta = { - title: 'Dock/Floating/Popover', - component: FloatingPopover, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: 'The floating primitive behind tooltips, group popovers, the overflow panel and the edge-position dropdown. It anchors to a DOM element and auto-aligns based on the anchor\'s position in the viewport.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** String content — the tooltip form. */ -export const TextTooltip: Story = { - render: () => harness('Open the inspector'), -} - -/** VNode content — a small menu, as the group/overflow popovers render. */ -export const MenuContent: Story = { - render: () => harness(() => h('div', { class: 'flex flex-col gap-0.5 min-w-40' }, [ - h('div', { class: 'px2 pt1 pb1.5 op60 text-2.75 uppercase tracking-wide font-medium' }, 'Menu'), - ...['Overview', 'Pages', 'Components'].map(label => - h('button', { class: 'px2 py1.5 rounded text-sm text-left op80 hover:op100 hover:bg-active transition' }, label)), - ]), 'Reveal menu'), -} - -/** - * A real toggle button drives the popover (rather than the mount-time - * harness the other stories use), to exercise `ignore` — clicking the - * trigger again while open must close it once, not close-then-reopen — and - * `panelClass`, which replaces the default tooltip padding. - */ -export const ToggleTrigger: Story = { - render: () => defineComponent({ - setup() { - const triggerEl = ref(null) - const open = ref(false) - const item = computed(() => (open.value && triggerEl.value) - ? { el: triggerEl.value, content: () => h('div', { class: 'flex flex-col gap-0.5 min-w-40' }, [ - h('div', { class: 'px2 pt1 pb1.5 op60 text-2.75 uppercase tracking-wide font-medium' }, 'Menu'), - ...['Overview', 'Pages', 'Components'].map(label => - h('button', { class: 'px2 py1.5 rounded text-sm text-left op80 hover:op100 hover:bg-active transition' }, label)), - ]) } - : null) - return () => h('div', { class: 'flex items-center justify-center p20 min-h-80 font-sans' }, [ - h('button', { - ref: (el: any) => (triggerEl.value = el), - class: 'px3 py1.5 rounded border border-base bg-glass color-base shadow', - onClick: () => (open.value = !open.value), - }, 'Toggle menu'), - h(FloatingPopover, { - item: item.value, - panelClass: '!p0', - ignore: [triggerEl], - onDismiss: () => (open.value = false), - }), - ]) - }, - }), -} - -export const CornerAnchors: Story = { - render: () => defineComponent({ - setup() { - const corners = [ - { label: 'Top left', class: 'left-2 top-2' }, - { label: 'Top right', class: 'right-2 top-2' }, - { label: 'Bottom left', class: 'left-2 bottom-2' }, - { label: 'Bottom right', class: 'right-2 bottom-2' }, - ].map(corner => ({ - ...corner, - el: null as HTMLElement | null, - item: shallowRef(null), - })) - const menu = () => h('div', { class: 'flex flex-col gap-0.5 min-w-40' }, [ - h('div', { class: 'px2 pt1 pb1.5 op60 text-2.75 uppercase tracking-wide font-medium' }, 'Menu'), - ...['Overview', 'Pages', 'Components'].map(label => - h('button', { class: 'px2 py1.5 rounded text-sm text-left op80 hover:op100 hover:bg-active transition' }, label)), - ]) - onMounted(() => { - for (const corner of corners) { - if (corner.el) - corner.item.value = { el: corner.el, content: menu } - } - }) - return () => h('div', { class: 'min-h-100 font-sans' }, corners.flatMap(corner => [ - h('button', { - key: corner.label, - ref: (el: any) => (corner.el = el), - class: `fixed ${corner.class} px3 py1.5 rounded border border-base bg-glass color-base shadow`, - }, corner.label), - h(FloatingPopover, { item: corner.item.value, dismissOnClickOutside: false }), - ])) - }, - }), -} diff --git a/packages/core/src/client/webcomponents/components/floating/FloatingPopover.ts b/packages/core/src/client/webcomponents/components/floating/FloatingPopover.ts deleted file mode 100644 index 5c1b63a21..000000000 --- a/packages/core/src/client/webcomponents/components/floating/FloatingPopover.ts +++ /dev/null @@ -1,177 +0,0 @@ -import type { MaybeElementRef } from '@vueuse/core' -import type { PropType, VNode } from 'vue' -import type { FloatingPopoverProps } from '../../state/floating-tooltip' -import { onClickOutside, useDebounceFn, useEventListener } from '@vueuse/core' -import { defineComponent, h, nextTick, onMounted, onUpdated, reactive, ref, useTemplateRef, watch } from 'vue' -import { resolveFloatingPosition } from './floating-position' - -// @unocss-include - -const FloatingPopoverComponent = defineComponent({ - name: 'FloatingPopover', - props: { - item: { - type: Object as PropType, - required: false, - }, - dismissOnClickOutside: { - type: Boolean, - default: true, - }, - /** Appended to the panel's class list — lets a consumer replace the default tooltip padding (e.g. a listbox). */ - panelClass: { - type: [String, Array] as PropType, - required: false, - }, - /** Elements `dismissOnClickOutside` should not treat as "outside" — typically the trigger that toggles this popover. */ - ignore: { - type: Array as PropType, - required: false, - }, - }, - emits: ['dismiss'], - setup(props, { emit }) { - const panel = useTemplateRef('panel') - const el = ref(props.item?.el) - const renderCounter = ref(0) - - const panelSize = reactive({ width: 0, height: 0 }) - // Before the first measurement, `resolveFloatingPosition` centers the panel - // under the anchor via `transform: translateX(-50%)` (it doesn't know the - // panel's real width yet); once measured, it switches to an absolute `left` - // with no transform. Both resolve to the same visual position, but - // transitioning `left` and `transform` independently between them produces - // a visible sideways wobble — so `measured` only flips (re-enabling the - // transition) a tick after `panelSize` updates, letting that one - // size-correcting render apply instantly rather than animate. - const measured = ref(false) - - function measurePanel() { - if (!props.item || !panel.value) - return - const { width, height } = panel.value.getBoundingClientRect() - if (Math.abs(width - panelSize.width) > 0.5 || Math.abs(height - panelSize.height) > 0.5) { - panelSize.width = width - panelSize.height = height - } - nextTick(() => { - measured.value = true - }) - } - - onMounted(measurePanel) - onUpdated(measurePanel) - - useEventListener(window, 'resize', () => { - if (el.value) - renderCounter.value++ - }) - - // The panel is `position: fixed` against a rect measured at render time, so - // scrolling any ancestor (not just the window) needs to trigger a re-measure. - useEventListener(window, 'scroll', () => { - if (el.value) - renderCounter.value++ - }, { capture: true, passive: true }) - - const clearThrottled = useDebounceFn(() => { - if (props.item?.el == null) { - el.value = undefined - panelSize.width = 0 - panelSize.height = 0 - measured.value = false - } - }, 800) - - if (props.dismissOnClickOutside) { - onClickOutside(panel, () => { - emit('dismiss') - }, { ignore: props.ignore }) - } - - watch( - () => props.item, - (value) => { - if (value) { - if (el.value !== value.el) - el.value = value.el - else - renderCounter.value++ - } - else { - clearThrottled() - } - }, - ) - - let previousContent: VNode | undefined - let previousStyle: Record = {} - - return () => { - // Force re-render to update the position - // eslint-disable-next-line ts/no-unused-expressions - renderCounter.value - - if (!el.value) - return null - - const transitionClass = measured.value ? 'transition-all duration-300' : 'transition-opacity duration-300' - - // When dismissing (item is null), keep the last known position - // so the popover fades out in place instead of jumping - if (!props.item) { - return h( - 'div', - { - ref: 'panel', - class: [ - `fixed z-floating-tooltip text-xs ${transitionClass} w-max bg-glass:80 color-base border border-base rounded px2 p1`, - 'op0 pointer-events-none', - props.panelClass, - ], - style: previousStyle, - }, - previousContent, - ) - } - - const rect = el.value.getBoundingClientRect() - - const { style } = resolveFloatingPosition({ - rect, - viewportWidth: window.innerWidth, - viewportHeight: window.innerHeight, - panelWidth: panelSize.width, - panelHeight: panelSize.height, - gap: props.item.gap, - placement: props.item.placement, - }) - - previousStyle = style - - const content = ( - typeof props.item?.content === 'string' - ? h('span', props.item?.content) - : props.item?.content() - ) ?? previousContent - - previousContent = content - - return h( - 'div', - { - ref: 'panel', - class: [ - `fixed z-floating-tooltip text-xs ${transitionClass} w-max bg-glass:80 color-base border border-base rounded px2 p1`, - props.item ? 'op100' : 'op0 pointer-events-none', - props.panelClass, - ], - style, - }, - content, - ) - } - }, -}) - -export default FloatingPopoverComponent diff --git a/packages/core/src/client/webcomponents/components/floating/__tests__/floating-position.test.ts b/packages/core/src/client/webcomponents/components/floating/__tests__/floating-position.test.ts deleted file mode 100644 index 8b7e3ed2f..000000000 --- a/packages/core/src/client/webcomponents/components/floating/__tests__/floating-position.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { resolveFloatingPosition } from '../floating-position' - -const VW = 1280 -const VH = 720 - -interface RectInit { - left: number - top: number - width?: number - height?: number -} - -function resolve(rect: RectInit, extra: Partial[0]> = {}) { - return resolveFloatingPosition({ - rect: { width: 40, height: 40, ...rect }, - viewportWidth: VW, - viewportHeight: VH, - ...extra, - }) -} - -describe('alignment guess', () => { - it('defaults to bottom for a mid-viewport anchor', () => { - const { align, style } = resolve({ left: 600, top: 300 }) - expect(align).toBe('bottom') - expect(style.top).toBe('350px') - }) - - it('prefers right when the anchor hugs the left edge', () => { - expect(resolve({ left: 20, top: 300 }).align).toBe('right') - }) - - it('prefers left when the anchor hugs the right edge', () => { - expect(resolve({ left: 1230, top: 300 }).align).toBe('left') - }) - - it('prefers top when the anchor hugs the bottom edge', () => { - expect(resolve({ left: 600, top: 660 }).align).toBe('top') - }) - - it('respects an explicit placement', () => { - expect(resolve({ left: 600, top: 300 }, { placement: 'left' }).align).toBe('left') - }) -}) - -describe('cross-axis clamping', () => { - it('clamps a wide panel against the right viewport edge', () => { - const { align, style } = resolve({ left: 1100, top: 660 }, { panelWidth: 400, panelHeight: 100 }) - expect(align).toBe('top') - expect(style.left).toBe(`${VW - 400 - 8}px`) - expect(style.bottom).toBe('70px') - expect(style.transform).toBeUndefined() - }) - - it('clamps a wide panel against the left viewport edge', () => { - const { align, style } = resolve({ left: 110, top: 660 }, { panelWidth: 400, panelHeight: 100 }) - expect(align).toBe('top') - expect(style.left).toBe('8px') - }) - - it('clamps a tall side panel against the bottom viewport edge', () => { - const { align, style } = resolve({ left: 1230, top: 600 }, { panelWidth: 200, panelHeight: 240 }) - expect(align).toBe('left') - expect(style.top).toBe(`${VH - 240 - 8}px`) - expect(style.right).toBe('60px') - expect(style.transform).toBeUndefined() - }) - - it('clamps a tall side panel against the top viewport edge', () => { - const { align, style } = resolve({ left: 1230, top: 60 }, { panelWidth: 200, panelHeight: 240 }) - expect(align).toBe('left') - expect(style.top).toBe('8px') - }) -}) - -describe('side flipping', () => { - it('flips bottom → top when the panel cannot fit below', () => { - const { align, style } = resolve({ left: 600, top: 480 }, { panelWidth: 200, panelHeight: 250 }) - expect(align).toBe('top') - expect(style.bottom).toBe(`${VH - 480 + 10}px`) - }) - - it('keeps the guessed side when neither side fits', () => { - const { align } = resolve({ left: 600, top: 480 }, { panelWidth: 200, panelHeight: 800 }) - expect(align).toBe('bottom') - }) - - it('never flips an explicit placement', () => { - const { align } = resolve({ left: 600, top: 480 }, { placement: 'bottom', panelWidth: 200, panelHeight: 250 }) - expect(align).toBe('bottom') - }) -}) - -describe('first paint (panel size not yet measured)', () => { - it('falls back to CSS centering below the anchor', () => { - const { style } = resolve({ left: 600, top: 300 }) - expect(style.left).toBe('620px') - expect(style.transform).toBe('translateX(-50%)') - }) - - it('falls back to CSS centering beside the anchor', () => { - const { align, style } = resolve({ left: 20, top: 300 }) - expect(align).toBe('right') - expect(style.left).toBe('70px') - expect(style.top).toBe('320px') - expect(style.transform).toBe('translateY(-50%)') - }) -}) diff --git a/packages/core/src/client/webcomponents/components/floating/floating-position.ts b/packages/core/src/client/webcomponents/components/floating/floating-position.ts deleted file mode 100644 index a66dc7391..000000000 --- a/packages/core/src/client/webcomponents/components/floating/floating-position.ts +++ /dev/null @@ -1,102 +0,0 @@ -export const DETECT_MARGIN = 100 -export const DEFAULT_GAP = 10 -export const VIEWPORT_MARGIN = 8 - -export type FloatingAlign = 'top' | 'bottom' | 'left' | 'right' - -export interface FloatingAnchorRect { - left: number - top: number - width: number - height: number -} - -export interface ResolveFloatingPositionOptions { - rect: FloatingAnchorRect - viewportWidth: number - viewportHeight: number - panelWidth?: number - panelHeight?: number - gap?: number - placement?: FloatingAlign -} - -export interface ResolvedFloatingPosition { - align: FloatingAlign - style: Record -} - -function clamp(value: number, min: number, max: number): number { - return Math.min(Math.max(value, min), Math.max(max, min)) -} - -export function resolveFloatingPosition(options: ResolveFloatingPositionOptions): ResolvedFloatingPosition { - const { - rect, - viewportWidth: vw, - viewportHeight: vh, - panelWidth = 0, - panelHeight = 0, - gap = DEFAULT_GAP, - placement, - } = options - - const anchorRight = rect.left + rect.width - const anchorBottom = rect.top + rect.height - - let align: FloatingAlign = 'bottom' - if (placement) - align = placement - else if (rect.left < DETECT_MARGIN) - align = 'right' - else if (anchorRight > vw - DETECT_MARGIN) - align = 'left' - else if (rect.top < DETECT_MARGIN) - align = 'bottom' - else if (anchorBottom > vh - DETECT_MARGIN) - align = 'top' - - if (!placement && panelWidth && panelHeight) { - if (align === 'bottom' && anchorBottom + gap + panelHeight > vh - VIEWPORT_MARGIN && rect.top - gap - panelHeight >= VIEWPORT_MARGIN) - align = 'top' - else if (align === 'top' && rect.top - gap - panelHeight < VIEWPORT_MARGIN && anchorBottom + gap + panelHeight <= vh - VIEWPORT_MARGIN) - align = 'bottom' - else if (align === 'right' && anchorRight + gap + panelWidth > vw - VIEWPORT_MARGIN && rect.left - gap - panelWidth >= VIEWPORT_MARGIN) - align = 'left' - else if (align === 'left' && rect.left - gap - panelWidth < VIEWPORT_MARGIN && anchorRight + gap + panelWidth <= vw - VIEWPORT_MARGIN) - align = 'right' - } - - const style: Record = {} - - if (align === 'top' || align === 'bottom') { - if (align === 'bottom') - style.top = `${anchorBottom + gap}px` - else - style.bottom = `${vh - rect.top + gap}px` - - if (panelWidth) { - style.left = `${clamp(rect.left + rect.width / 2 - panelWidth / 2, VIEWPORT_MARGIN, vw - panelWidth - VIEWPORT_MARGIN)}px` - } - else { - style.left = `${rect.left + rect.width / 2}px` - style.transform = 'translateX(-50%)' - } - } - else { - if (align === 'right') - style.left = `${anchorRight + gap}px` - else - style.right = `${vw - rect.left + gap}px` - - if (panelHeight) { - style.top = `${clamp(rect.top + rect.height / 2 - panelHeight / 2, VIEWPORT_MARGIN, vh - panelHeight - VIEWPORT_MARGIN)}px` - } - else { - style.top = `${rect.top + rect.height / 2}px` - style.transform = 'translateY(-50%)' - } - } - - return { align, style } -} diff --git a/packages/core/src/client/webcomponents/components/icons/BracketLeft.vue b/packages/core/src/client/webcomponents/components/icons/BracketLeft.vue deleted file mode 100644 index 99a629777..000000000 --- a/packages/core/src/client/webcomponents/components/icons/BracketLeft.vue +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/packages/core/src/client/webcomponents/components/icons/BracketRight.vue b/packages/core/src/client/webcomponents/components/icons/BracketRight.vue deleted file mode 100644 index 9b65059b5..000000000 --- a/packages/core/src/client/webcomponents/components/icons/BracketRight.vue +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/packages/core/src/client/webcomponents/components/icons/Brand.stories.ts b/packages/core/src/client/webcomponents/components/icons/Brand.stories.ts deleted file mode 100644 index 56083b522..000000000 --- a/packages/core/src/client/webcomponents/components/icons/Brand.stories.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { h } from 'vue' -import ViteDevTools from './ViteDevTools.vue' -import VitePlus from './VitePlus.vue' -import VitePlusCore from './VitePlusCore.vue' - -const meta = { - title: 'Brand/Logos', - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: 'The Vite DevTools brand marks used across the dock and standalone shells.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** The Vite+ mark (the minimized dock nub / auth screen logo). */ -export const VitePlusMark: Story = { - name: 'VitePlus', - render: () => ({ setup: () => () => h('div', { class: 'w-24 h-24' }, h(VitePlus)) }), -} - -/** The compact Vite+ core glyph shown in the collapsed dock. */ -export const VitePlusCoreMark: Story = { - name: 'VitePlusCore', - render: () => ({ setup: () => () => h('div', { class: 'w-16 h-16' }, h(VitePlusCore)) }), -} - -/** The full Vite DevTools wordmark (recolors with the theme). */ -export const Wordmark: Story = { - name: 'ViteDevTools', - render: () => ({ setup: () => () => h('div', { class: 'w-80' }, h(ViteDevTools)) }), -} diff --git a/packages/core/src/client/webcomponents/components/icons/IconifyIcon.vue b/packages/core/src/client/webcomponents/components/icons/IconifyIcon.vue deleted file mode 100644 index 7a210ccae..000000000 --- a/packages/core/src/client/webcomponents/components/icons/IconifyIcon.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/icons/ViteDevTools.vue b/packages/core/src/client/webcomponents/components/icons/ViteDevTools.vue deleted file mode 100644 index 18ec6e279..000000000 --- a/packages/core/src/client/webcomponents/components/icons/ViteDevTools.vue +++ /dev/null @@ -1,143 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/icons/VitePlus.vue b/packages/core/src/client/webcomponents/components/icons/VitePlus.vue deleted file mode 100644 index e364c0bee..000000000 --- a/packages/core/src/client/webcomponents/components/icons/VitePlus.vue +++ /dev/null @@ -1,133 +0,0 @@ - diff --git a/packages/core/src/client/webcomponents/components/icons/VitePlusCore.vue b/packages/core/src/client/webcomponents/components/icons/VitePlusCore.vue deleted file mode 100644 index 04a19795e..000000000 --- a/packages/core/src/client/webcomponents/components/icons/VitePlusCore.vue +++ /dev/null @@ -1,137 +0,0 @@ - diff --git a/packages/core/src/client/webcomponents/components/message/MessageItem.stories.ts b/packages/core/src/client/webcomponents/components/message/MessageItem.stories.ts deleted file mode 100644 index 3504b4605..000000000 --- a/packages/core/src/client/webcomponents/components/message/MessageItem.stories.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { h } from 'vue' -import { message, sampleMessages } from '../../stories/fixtures' -import MessageItem from './MessageItem.vue' - -const meta = { - title: 'Messages/MessageItem', - component: MessageItem, - tags: ['autodocs'], - decorators: [() => ({ template: '
' })], - parameters: { - docs: { - description: { - component: 'A single log/message row: level accent, icon, title, description, relative time and category/label chips.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -export const Error: Story = { - render: () => ({ setup: () => () => h(MessageItem, { entry: sampleMessages[0]! }) }), -} - -export const Loading: Story = { - render: () => ({ setup: () => () => h(MessageItem, { entry: message({ level: 'warn', message: 'Running checks…', status: 'loading', category: 'a11y' }) }) }), -} - -/** The compact variant used inside toasts (no timestamp / chips). */ -export const Compact: Story = { - render: () => ({ setup: () => () => h(MessageItem, { entry: sampleMessages[2]!, compact: true }) }), -} - -/** Every level stacked together. */ -export const AllLevels: Story = { - render: () => ({ - setup: () => () => h('div', { class: 'flex flex-col gap-3' }, sampleMessages.map(entry => h(MessageItem, { key: entry.id, entry }))), - }), -} diff --git a/packages/core/src/client/webcomponents/components/message/MessageItem.vue b/packages/core/src/client/webcomponents/components/message/MessageItem.vue deleted file mode 100644 index 3af73b62f..000000000 --- a/packages/core/src/client/webcomponents/components/message/MessageItem.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/message/MessageItemConstants.ts b/packages/core/src/client/webcomponents/components/message/MessageItemConstants.ts deleted file mode 100644 index c769415c7..000000000 --- a/packages/core/src/client/webcomponents/components/message/MessageItemConstants.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { DevToolsMessageEntryFrom, DevToolsMessageLevel } from '@vitejs/devtools-kit' - -// @unocss-include - -export interface LevelStyle { - icon: string - color: string - bg: string - label: string -} - -export interface FromStyle { - icon: string - color: string - label: string -} - -export const levels: Record = { - info: { icon: 'i-ph:info-duotone', color: 'text-blue', bg: 'bg-blue', label: 'Info' }, - warn: { icon: 'i-ph:warning-duotone', color: 'text-amber', bg: 'bg-amber', label: 'Warning' }, - error: { icon: 'i-ph:x-circle-duotone', color: 'text-red', bg: 'bg-red', label: 'Error' }, - success: { icon: 'i-ph:check-circle-duotone', color: 'text-green', bg: 'bg-green', label: 'Success' }, - debug: { icon: 'i-ph:bug-duotone', color: 'text-gray', bg: 'bg-gray', label: 'Debug' }, -} - -export const fromEntries: Record = { - server: { icon: 'i-ph:hexagon-duotone', color: 'text-green-800 dark:text-green-200', label: 'Server' }, - browser: { icon: 'i-ph:globe-simple-duotone', color: 'text-amber-800 dark:text-amber-200', label: 'Browser' }, -} - -// Intentionally uses fixed saturation/lightness (unlike @vitejs/devtools-ui/utils/color which -// is dark-mode-aware via Vue reactivity). Webcomponents run in shadow DOM with media-based dark -// mode, so they can't access the isDark composable. -export function getHashColorFromString(name: string, opacity: number = 1): string { - let hash = 0 - for (let i = 0; i < name.length; i++) - hash = name.charCodeAt(i) + ((hash << 5) - hash) - const h = hash % 360 - return `hsla(${h}, 55%, 55%, ${opacity})` -} diff --git a/packages/core/src/client/webcomponents/components/views-builtin/SettingsAdvanced.vue b/packages/core/src/client/webcomponents/components/views-builtin/SettingsAdvanced.vue deleted file mode 100644 index 0ab084af2..000000000 --- a/packages/core/src/client/webcomponents/components/views-builtin/SettingsAdvanced.vue +++ /dev/null @@ -1,169 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/views-builtin/SettingsAppearance.vue b/packages/core/src/client/webcomponents/components/views-builtin/SettingsAppearance.vue deleted file mode 100644 index 651309bff..000000000 --- a/packages/core/src/client/webcomponents/components/views-builtin/SettingsAppearance.vue +++ /dev/null @@ -1,131 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/views-builtin/SettingsDocks.vue b/packages/core/src/client/webcomponents/components/views-builtin/SettingsDocks.vue deleted file mode 100644 index b83cf65fd..000000000 --- a/packages/core/src/client/webcomponents/components/views-builtin/SettingsDocks.vue +++ /dev/null @@ -1,557 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/views-builtin/SettingsShortcuts.vue b/packages/core/src/client/webcomponents/components/views-builtin/SettingsShortcuts.vue deleted file mode 100644 index 60760e919..000000000 --- a/packages/core/src/client/webcomponents/components/views-builtin/SettingsShortcuts.vue +++ /dev/null @@ -1,434 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinClientAuthNotice.stories.ts b/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinClientAuthNotice.stories.ts deleted file mode 100644 index dddb5b148..000000000 --- a/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinClientAuthNotice.stories.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { h } from 'vue' -import { mountWithContext } from '../../stories/story-helpers' -import ViewBuiltinClientAuthNotice from './ViewBuiltinClientAuthNotice.vue' - -const meta = { - title: 'Views/Builtin/AuthNotice', - component: ViewBuiltinClientAuthNotice, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: 'The authorization prompt shown when the client is not yet trusted — explains the risk and takes a one-time code.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -export const Default: Story = { - render: () => ({ - setup: () => mountWithContext( - { isTrusted: false }, - ctx => h('div', { class: 'h-140 bg-base color-base border border-base rounded-lg overflow-auto' }, h(ViewBuiltinClientAuthNotice, { context: ctx })), - ), - }), -} diff --git a/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinClientAuthNotice.vue b/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinClientAuthNotice.vue deleted file mode 100644 index b1083674f..000000000 --- a/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinClientAuthNotice.vue +++ /dev/null @@ -1,133 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinSettings.stories.ts b/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinSettings.stories.ts deleted file mode 100644 index a4a777fc2..000000000 --- a/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinSettings.stories.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import type { DevToolsViewBuiltin } from '@vitejs/devtools-kit' -import { h } from 'vue' -import { groupedEntries } from '../../stories/fixtures' -import { mountWithContext } from '../../stories/story-helpers' -import ViewBuiltinSettings from './ViewBuiltinSettings.vue' - -const entry: DevToolsViewBuiltin = { - type: '~builtin', - id: '~settings', - title: 'Settings', - icon: 'ph:gear-duotone', -} - -function stage(children: any) { - return h('div', { class: 'h-140 bg-base color-base border border-base rounded-lg overflow-hidden font-sans' }, children) -} - -const meta = { - title: 'Views/Builtin/Settings', - component: ViewBuiltinSettings, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: 'The built-in settings view: Appearance, Docks, Shortcuts and Advanced tabs. Click the tabs to switch.', - }, - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -/** Embedded client — the Appearance tab shows dock-mode options. */ -export const Embedded: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: groupedEntries, clientType: 'embedded' }, - ctx => stage(h(ViewBuiltinSettings, { context: ctx, entry })), - ), - }), -} - -/** Standalone client — dock-mode options are hidden. */ -export const Standalone: Story = { - render: () => ({ - setup: () => mountWithContext( - { entries: groupedEntries, clientType: 'standalone' }, - ctx => stage(h(ViewBuiltinSettings, { context: ctx, entry })), - ), - }), -} diff --git a/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinSettings.vue b/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinSettings.vue deleted file mode 100644 index 51d372103..000000000 --- a/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinSettings.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/views/ViewCustomRenderer.vue b/packages/core/src/client/webcomponents/components/views/ViewCustomRenderer.vue deleted file mode 100644 index d79c3133d..000000000 --- a/packages/core/src/client/webcomponents/components/views/ViewCustomRenderer.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - diff --git a/packages/core/src/client/webcomponents/components/views/ViewEntry.vue b/packages/core/src/client/webcomponents/components/views/ViewEntry.vue deleted file mode 100644 index 931a9de24..000000000 --- a/packages/core/src/client/webcomponents/components/views/ViewEntry.vue +++ /dev/null @@ -1,80 +0,0 @@ - - -