From d3c89d2c093dd22918eba5089c6e20d63a50e78d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=B2=A9=E5=B2=A9?= Date: Thu, 23 Jul 2026 17:47:38 +0800 Subject: [PATCH] =?UTF-8?q?rebuild:=20=E7=AC=AC=E4=BA=94=E7=89=88=E9=87=8D?= =?UTF-8?q?=E6=9E=84=20-=20=E6=B7=BB=E5=8A=A0pinia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- REFACTORING_GUIDE.md | 1874 ++++++++++++++++++++++++++++++ package.json | 2 + pnpm-lock.yaml | 1946 ++++++++++++++++++++++++++++++++ src/composables/useEventBus.ts | 28 + src/main.ts | 2 + src/stores/apps.ts | 55 + src/stores/fs.ts | 315 ++++++ src/stores/index.ts | 11 + src/stores/notify.ts | 64 ++ src/stores/plugins/persist.ts | 37 + src/stores/settings.ts | 118 ++ src/stores/ui.ts | 89 ++ src/stores/wm.ts | 135 +++ src/system/index.ts | 19 +- src/types/app.ts | 29 + src/types/fs.ts | 17 + src/types/notify.ts | 11 + src/types/settings.ts | 30 + src/types/ui.ts | 10 + src/types/window.ts | 36 + 20 files changed, 4811 insertions(+), 17 deletions(-) create mode 100644 REFACTORING_GUIDE.md create mode 100644 pnpm-lock.yaml create mode 100644 src/composables/useEventBus.ts create mode 100644 src/stores/apps.ts create mode 100644 src/stores/fs.ts create mode 100644 src/stores/index.ts create mode 100644 src/stores/notify.ts create mode 100644 src/stores/plugins/persist.ts create mode 100644 src/stores/settings.ts create mode 100644 src/stores/ui.ts create mode 100644 src/stores/wm.ts create mode 100644 src/types/app.ts create mode 100644 src/types/fs.ts create mode 100644 src/types/notify.ts create mode 100644 src/types/settings.ts create mode 100644 src/types/ui.ts create mode 100644 src/types/window.ts diff --git a/REFACTORING_GUIDE.md b/REFACTORING_GUIDE.md new file mode 100644 index 0000000..76fcdb8 --- /dev/null +++ b/REFACTORING_GUIDE.md @@ -0,0 +1,1874 @@ +# macOS-web 纯 Vue 重构手册 + +> **目标**:去除原生 JS 思路和实现,去掉 `window` 全局调用,将项目改造为纯 Vue 3 项目。 +> **范围**:全量重构,不考虑成本。 + +--- + +## 目录 + +1. [现状诊断](#1-现状诊断) +2. [目标架构](#2-目标架构) +3. [前端架构设计](#3-前端架构设计) +4. [重构总路线](#4-重构总路线) +5. [阶段一:基础设施层](#5-阶段一基础设施层) +6. [阶段二:系统 UI 组件化](#6-阶段二系统-ui-组件化) +7. [阶段三:窗口管理器现代化](#7-阶段三窗口管理器现代化) +8. [阶段四:应用层优化](#8-阶段四应用层优化) +9. [阶段五:清理与收尾](#9-阶段五清理与收尾) +10. [附录:新旧对照表](#10-附录新旧对照表) + +--- + +## 1. 现状诊断 + +### 1.1 双渲染系统并存(核心问题) + +| 层 | 当前渲染方式 | 问题 | +|---|---|---| +| **系统 UI 层**(菜单栏/Dock/对话框/菜单/通知/Spotlight/控制中心/锁屏/屏保/启动界面) | `el()` 命令式 DOM 构建 + `$()` 直接 DOM 查询 | 非 Vue,绕过虚拟 DOM | +| **窗口 chrome 层**(traffic lights/标题栏/resize handles) | `el()` 命令式 DOM 构建 | 非 Vue | +| **应用层**(Finder/Calculator/Safari 等 33 个应用) | Vue 3 SFC + `h()`/`render()` 手动挂载 | 部分合理 | +| **容器层**(DesktopArea/MenuBarArea 等) | Vue 模板(仅作为 DOM 挂载锚点) | 合理 | + +### 1.2 原生 JS 使用分布 + +| 模块 | 原生 API | 严重度 | +|---|---|---| +| `useSys.ts` (~800 行) | `$`, `$$`, `el`, `addEventListener`, `matchMedia`, `MutationObserver`, `requestAnimationFrame`, `setInterval`, `Image()`, `document.activeElement`, `getBoundingClientRect` | 🔴 最重度 | +| `useWM.ts` (~400 行) | `el()`, `addEventListener('pointermove/up')`, `document.getElementById`, `document.body.classList`, `setPointerCapture`, `getBoundingClientRect` | 🔴 重度 | +| `useUI.ts` (~200 行) | `document.createElement/addEventListener/body.append/remove` + `window.innerWidth/innerHeight` | 🔴 全部原生 | +| `useNotify.ts` (~200 行) | `$`, `$$`, `el`, `setTimeout`, `querySelector` | 🔴 重度 | +| `useSpotlight.ts` (~150 行) | `$`, `$$`, `el`, `addEventListener`, `navigator.clipboard`, `scrollIntoView` | 🔴 重度 | +| `useApps.ts` (~120 行) | `document.activeElement`, `document.execCommand`, `navigator.clipboard` | 🟡 中度 | +| `system/index.ts` (~160 行) | `$`, `$$`, `el`, `document.addEventListener`, 14 个 `window` 全局暴露 | 🔴 重度 | +| `utils/index.ts` (~100 行) | `document.querySelector/querySelectorAll/createElement` | 🔴 基础依赖 | + +### 1.3 关键反模式 + +```mermaid +graph TD + A["window 全局暴露
14 个全局变量"] --> B["隐式依赖,模块封装破坏"] + C["懒引用 lazy ref
let _Sys: any = null"] --> D["循环依赖 workaround
类型不安全"] + E["el() 命令式 DOM
微型虚拟 DOM 替代品"] --> F["绕过 Vue 响应式
6+ 模块重度依赖"] + G["$('') 直接 DOM 查询"] --> H["绕过 Vue ref 系统"] + I["bus.on/emit"] --> J["类型不安全
非 Vue 标准模式"] + K["33 个副作用 import"] --> L["无懒加载
全量打包"] +``` + +### 1.4 核心循环依赖 + +``` +useSys ←→ useWM ←→ useApps ←→ useFS +``` +通过 `setSysRef()` / `setSysForNotify()` / `setAppsRef()` 等 lazy setter 打破。 + +--- + +## 2. 目标架构 + +### 2.1 总览 + +```mermaid +graph TD + subgraph "Vue 应用入口" + main.ts --> App.vue + end + + subgraph "状态管理 Pinia" + settingsStore["settingsStore
(原 useSettings + useStore)"] + fsStore["fsStore
(原 useFS)"] + appStore["appStore
(原 useApps 注册表部分)"] + wmStore["wmStore
(原 useWM 状态部分)"] + notifyStore["notifyStore
(原 useNotify 数据部分)"] + sysStore["sysStore
(原 useSys 设置/电源部分)"] + end + + subgraph "系统 UI 组件(纯 Vue SFC)" + MenuBar["<MenuBar />"] + Dock["<Dock />"] + DesktopIcons["<DesktopIcons />"] + Spotlight["<Spotlight />"] + NotificationCenter["<NotificationCenter />"] + ControlCenter["<ControlCenter />"] + LockScreen["<LockScreen />"] + Screensaver["<Screensaver />"] + BootScreen["<BootScreen />"] + ContextMenu["<ContextMenu />"] + DialogLayer["<DialogLayer />"] + end + + subgraph "窗口系统" + WindowLayer["<WindowLayer />"] + WindowFrame["<WindowFrame />
(chrome: 标题栏/traffic lights/resize)"] + end + + subgraph "应用层(懒加载)" + AppLoader["<AppLoader />
defineAsyncComponent"] + Finder["Finder.vue"] + Safari["Safari.vue"] + TxtEdit["...33 个应用"] + end + + subgraph "组合式函数(纯逻辑,无 DOM)" + useKeyboard["useKeyboard(全局快捷键)"] + useIdleWatch["useIdleWatch(屏保计时)"] + useTheme["useTheme(暗色模式/壁纸)"] + useVolume["useVolume(音量控制)"] + useEventBus["useEventBus(mitt 替代 bus)"] + end + + App.vue --> MenuBar + App.vue --> Dock + App.vue --> DesktopIcons + App.vue --> WindowLayer + App.vue --> Spotlight + App.vue --> NotificationCenter + App.vue --> ControlCenter + App.vue --> LockScreen + App.vue --> Screensaver + App.vue --> BootScreen + App.vue --> ContextMenu + App.vue --> DialogLayer + + WindowLayer --> WindowFrame + WindowLayer --> AppLoader +``` + +### 2.2 核心原则 + +1. **一切 UI 皆 Vue 组件**:不再使用 `el()` 创建任何 DOM 元素 +2. **状态归 Pinia**:跨模块共享状态走 Pinia store,不挂 `window` +3. **通信走 provide/inject + mitt**:父子用 provide/inject,跨层级用 mitt(类型安全的事件总线) +4. **无直接 DOM 查询**:用 Vue `ref` / `template ref` 替代 `$()` / `$$()` +5. **应用懒加载**:`defineAsyncComponent` + 动态 import +6. **仅保留必要的原生 API**:`pointer events` 拖拽、`localStorage`、`navigator.clipboard` 等不可避免的部分,封装在专用 composable 中 + +--- + +## 3. 前端架构设计 + +### 3.1 组件层级树 + +```mermaid +graph TD + subgraph "App.vue(根组件)" + direction TB + A[App.vue] + end + + subgraph "Layer 1:背景层 z-0" + B1["<OverlayBrightness />"] + B2["<OverlayNightShift />"] + B3["<DesktopArea />"] + end + + subgraph "Layer 2:内容层 z-1~99" + C1["<WindowLayer />"] + end + + subgraph "Layer 3:系统 UI 层 z-100~499" + D1["<MenuBar />"] + D2["<Dock />"] + end + + subgraph "Layer 4:浮层 z-500~899" + E1["<Spotlight />"] + E2["<ControlCenter />"] + E3["<NotificationCenter />"] + E4["<ContextMenu />"] + E5["<DialogLayer />"] + E6["<BannerContainer />"] + end + + subgraph "Layer 5:全屏覆盖层 z-900+" + F1["<LockScreen />"] + F2["<Screensaver />"] + F3["<BootScreen />"] + F4["<PowerOff />"] + end + + A --> B1 & B2 & B3 + A --> C1 + A --> D1 & D2 + A --> E1 & E2 & E3 & E4 & E5 & E6 + A --> F1 & F2 & F3 & F4 + + C1 --> C1A["<WindowFrame /> ×N"] + C1A --> C1A1["<TrafficLights />"] + C1A --> C1A2["<AppLoader />"] + C1A2 --> C1A2A["应用 SFC
(Finder/Safari/...)"] +``` + +**z-index 分层规范**: + +| 层 | z-index 范围 | 组件 | 说明 | +|---|---|---|---| +| 背景层 | 0 | OverlayBrightness, OverlayNightShift, DesktopArea | 静态背景,无交互 | +| 内容层 | 1–99 | WindowLayer → WindowFrame ×N | 窗口动态分配 z-index | +| 系统 UI 层 | 100–499 | MenuBar(100), Dock(200) | 始终在窗口之上 | +| 浮层 | 500–899 | Spotlight(500), ControlCenter(510), NotificationCenter(510), ContextMenu(600), DialogLayer(700), BannerContainer(800) | 临时弹出内容 | +| 全屏覆盖层 | 900+ | LockScreen(900), Screensaver(950), BootScreen(999), PowerOff(998) | 独占式全屏状态 | + +### 3.2 数据流架构 + +```mermaid +flowchart LR + subgraph "数据源" + LS[localStorage] + UA[用户操作] + SYS[系统事件
resize/keyboard/idle] + end + + subgraph "Pinia Stores(单一数据源)" + direction TB + SS["settingsStore
设置/外观/音量/壁纸"] + FS["fsStore
虚拟文件系统"] + WS["wmStore
窗口状态"] + AS["appStore
应用注册表"] + NS["notifyStore
通知数据"] + US["uiStore
菜单/对话框状态"] + end + + subgraph "Composables(纯逻辑)" + UK[useKeyboard] + UI[useIdleWatch] + UT[useTheme] + UD[useDrag / useResize] + UC[useClipboard] + end + + subgraph "Vue 组件(视图层)" + direction TB + MB["<MenuBar />"] + DK["<Dock />"] + SP["<Spotlight />"] + CC["<ControlCenter />"] + NC["<NotificationCenter />"] + WF["<WindowFrame />"] + APPS["33 个应用组件"] + end + + subgraph "跨组件通信" + MITT["mitt 事件总线
(wm:focus, fs:changed,
volume:changed, apps:ready)"] + PI["provide / inject
(窗口级上下文)"] + end + + LS -->|"watch 持久化"| SS + LS --> FS + LS --> NS + + UA -->|"@click / v-model"| SS + UA --> WS + UA --> AS + UA --> US + + SYS --> UK + SYS --> UI + SYS --> UT + + SS -->|"reactive state"| MB + SS --> DK + SS --> CC + SS --> NC + SS --> WF + SS --> APPS + + WS -->|"reactive state"| WF + WS --> DK + + FS -->|"reactive state"| SP + FS --> APPS + + NS -->|"reactive state"| NC + NS --> DK + + AS -->|"registry lookup"| SP + AS --> WF + AS --> DK + + US -->|"reactive state"| MB + US --> SP + + UK -->|"快捷键路由"| SS + UK --> WS + UK --> AS + + UI -->|"空闲超时"| SS + + UT -->|"外观变更"| SS + + MITT -->|"事件订阅"| WS + MITT --> FS + MITT --> DK + MITT --> APPS + + PI -->|"窗口上下文"| APPS +``` + +**核心数据流原则**: + +1. **单向数据流**:`用户操作 → Store 更新 → 组件响应式重渲染` +2. **Store 是唯一真相源**:所有共享状态必须位于 Pinia store 中 +3. **组件不直接修改其他组件的状态**:通过 store action 或事件总线 +4. **事件总线仅用于通知,不用于状态传递**:`emitter.emit('fs:changed')` 是"文件变了,你们自己去看",而非"文件变了,这是新数据" + +### 3.3 路由与懒加载策略 + +本项目是**桌面模拟器**,不使用 vue-router(无需 URL 路由)。应用加载使用以下策略: + +```typescript +// src/apps/loaders.ts +import { defineAsyncComponent, type AsyncComponentLoader } from 'vue' + +/** + * 应用懒加载映射表 + * + * 策略: + * - 核心应用(Finder)→ 立即加载(Eager) + * - 系统应用(前 7 个)→ 空闲时预加载(Idle Preload) + * - 其他应用 → 按需加载(Lazy) + */ + +// 加载策略枚举 +enum LoadStrategy { + Eager, // 立即加载 + IdlePreload, // requestIdleCallback 预加载 + Lazy, // 首次打开时加载 +} + +export const appLoaders: Record = { + // === 核心应用(Eager:启动即加载) === + finder: { + loader: () => import('./finder/Finder.vue'), + strategy: LoadStrategy.Eager, + }, + + // === 系统应用(IdlePreload:空闲时后台加载) === + settings: { + loader: () => import('./settings/Settings.vue'), + strategy: LoadStrategy.IdlePreload, + }, + safari: { + loader: () => import('./safari/Safari.vue'), + strategy: LoadStrategy.IdlePreload, + }, + mail: { + loader: () => import('./mail/Mail.vue'), + strategy: LoadStrategy.IdlePreload, + }, + messages: { + loader: () => import('./messages/Messages.vue'), + strategy: LoadStrategy.IdlePreload, + }, + music: { + loader: () => import('./music/Music.vue'), + strategy: LoadStrategy.IdlePreload, + }, + photos: { + loader: () => import('./photos/Photos.vue'), + strategy: LoadStrategy.IdlePreload, + }, + notes: { + loader: () => import('./notes/Notes.vue'), + strategy: LoadStrategy.IdlePreload, + }, + + // === 其他应用(Lazy:按需加载) === + calculator: { + loader: () => import('./calculator/Calculator.vue'), + strategy: LoadStrategy.Lazy, + }, + calendar: { + loader: () => import('./calendar/Calendar.vue'), + strategy: LoadStrategy.Lazy, + }, + // ... 其余 24 个应用 +} + +/** + * 在 appStore.init() 之后调用,利用浏览器空闲时间预加载常用应用 + */ +export function initPreload() { + const idleLoaders = Object.values(appLoaders) + .filter(a => a.strategy === LoadStrategy.IdlePreload) + + idleLoaders.forEach(({ loader }) => { + requestIdleCallback(() => { + // 触发异步加载但不等待结果 + ;(loader as () => Promise)() + }, { timeout: 3000 }) + }) +} +``` + +**加载状态处理**: + +```vue + + + + +``` + +### 3.4 状态管理设计 + +#### 3.4.1 Store 职责矩阵 + +| Store | 拥有的状态 | 不拥有的状态 | 持久化 | +|---|---|---|---| +| `settingsStore` | 用户设置、外观、音量、壁纸、登录项 | 窗口状态、通知数据 | ✅ localStorage | +| `fsStore` | 虚拟文件系统树、当前目录 | 应用注册信息 | ✅ localStorage | +| `wmStore` | 窗口列表、activeWin、z-index、cascade | 窗口 DOM 元素 | ❌ 仅会话 | +| `appStore` | 应用注册表、打开队列 | 应用内部状态 | ❌ 仅会话 | +| `notifyStore` | 通知列表、已读状态 | 通知 DOM、横幅定时器 | ✅ localStorage | +| `uiStore` | 菜单/对话框显隐、位置、回调 | 菜单/对话框 DOM | ❌ 仅会话 | + +#### 3.4.2 Store 间依赖规则 + +``` +settingsStore ← 无依赖(叶子 store) +fsStore ← 无依赖 +notifyStore ← 依赖 settingsStore(权限检查) +appStore ← 依赖 wmStore(open 时创建窗口) +wmStore ← 依赖 settingsStore(可用区域计算)、appStore(窗口应用信息) +uiStore ← 无依赖 +``` + +规则:**禁止 Store 间循环依赖**。如果 A 需要 B 的数据而 B 也需要 A 的数据,应提取到第三个 Store C 或使用事件总线解耦。 + +#### 3.4.3 Pinia 插件:持久化 + +```typescript +// src/stores/plugins/persist.ts +import type { PiniaPluginContext } from 'pinia' + +const PERSISTED_STORES = ['settings', 'fs', 'notify'] + +export function persistPlugin({ store }: PiniaPluginContext) { + if (!PERSISTED_STORES.includes(store.$id)) return + + // 初始化:从 localStorage 恢复 + const saved = localStorage.getItem(`macos-web:${store.$id}`) + if (saved) { + try { store.$patch(JSON.parse(saved)) } catch { /* ignore */ } + } + + // 自动持久化 + store.$subscribe((_, state) => { + localStorage.setItem(`macos-web:${store.$id}`, JSON.stringify(state)) + }, { detached: true }) +} +``` + +### 3.5 事件通信设计 + +#### 3.5.1 通信方式选型 + +| 场景 | 方式 | 示例 | +|---|---|---| +| 父子组件通信 | `props` + `emits` | `` | +| 祖先→后代(跨层级) | `provide` / `inject` | 窗口级上下文(win 对象、clipboard 等) | +| 兄弟/任意组件(通知类) | `mitt` 事件总线 | `emitter.emit('fs:changed', { paths })` | +| 全局状态共享 | Pinia store | `settingsStore.settings.darkMode` | +| 浏览器级事件 | `composable` 封装 | `useKeyboard()` 监听 keydown | + +#### 3.5.2 mitt 事件契约 + +```typescript +// src/composables/useEventBus.ts +import mitt from 'mitt' + +// ⚠️ 所有事件必须在此声明类型,保证端到端类型安全 +type Events = { + // 窗口事件 + 'wm:focus': { winId: string; appId: string } + 'wm:changed': void + 'wm:closed': { winId: string } + + // 文件系统事件 + 'fs:changed': { paths: string[] } + 'trash:changed': void + + // 系统事件 + 'apps:ready': void + 'volume:changed': number + 'unlocked': void + 'locked': void + + // 应用间事件 + 'spotlight:open': void + 'spotlight:close': void +} + +export const emitter = mitt() +``` + +#### 3.5.3 provide / inject 设计 + +```typescript +// 窗口级上下文 —— 由 WindowFrame 提供,所有应用组件注入 + +// src/composables/useWindowContext.ts +import { provide, inject, type InjectionKey, type Ref } from 'vue' + +interface WindowContext { + winId: string + appId: string + clipboard: Ref // 应用内剪贴板 + isActive: Ref // 窗口是否聚焦 + closeWindow: () => void + setTitle: (title: string) => void +} + +export const WIN_CTX_KEY: InjectionKey = Symbol('winCtx') + +export function provideWindowContext(ctx: WindowContext) { + provide(WIN_CTX_KEY, ctx) +} + +export function useWindowContext(): WindowContext { + const ctx = inject(WIN_CTX_KEY) + if (!ctx) throw new Error('useWindowContext() must be used inside a WindowFrame') + return ctx +} +``` + +### 3.6 组件分类与职责 + +```mermaid +quadrantChart + title 组件分类矩阵 + x-axis "有状态" --> "无状态" + y-axis "有 DOM" --> "无 DOM" + quadrant-1 "容器组件" + quadrant-2 "展示组件" + quadrant-3 "逻辑 composable" + quadrant-4 "纯工具函数" + "WindowFrame": [0.2, 0.85] + "MenuBar": [0.25, 0.9] + "Dock": [0.3, 0.9] + "Spotlight": [0.25, 0.85] + "ControlCenter": [0.2, 0.85] + "Finder": [0.35, 0.8] + "TrafficLights": [0.85, 0.55] + "DockIcon": [0.75, 0.6] + "Banner": [0.9, 0.5] + "CCToggle": [0.85, 0.55] + "useDrag": [0.95, 0.15] + "useKeyboard": [0.9, 0.1] + "useTheme": [0.85, 0.1] + "useClipboard": [0.95, 0.05] + "emitter": [0.98, 0.02] + "stdMenus": [0.98, 0.02] +``` + +| 分类 | 特征 | 示例 | 规则 | +|---|---|---|---| +| **容器组件** | 连接 Store,管理子组件状态,有 DOM 输出 | `WindowFrame`, `MenuBar`, `Dock`, `Spotlight` | 可访问 Pinia store,可 emit 事件 | +| **展示组件** | 纯 props 输入 + emits 输出,无 Store 依赖 | `TrafficLights`, `DockIcon`, `Banner`, `CCToggle` | 禁止直接访问 Pinia store | +| **逻辑 composable** | 封装有状态逻辑,无 DOM 输出 | `useDrag`, `useKeyboard`, `useTheme`, `useClipboard` | 可访问 Pinia store,不输出 DOM | +| **纯工具函数** | 纯函数,无副作用,无状态 | `stdMenus`, `fmtDateCN`, `clamp` | 禁止访问任何 store 或 DOM | + +### 3.7 目录结构设计 + +``` +src/ +├── main.ts # 入口:createApp + Pinia + 全局样式 +├── App.vue # 根布局组件 +│ +├── stores/ # Pinia 状态管理 +│ ├── index.ts # createPinia + 插件注册 +│ ├── plugins/ +│ │ └── persist.ts # 持久化插件 +│ ├── settings.ts # useSettingsStore +│ ├── fs.ts # useFSStore +│ ├── wm.ts # useWMStore +│ ├── apps.ts # useAppStore +│ ├── notify.ts # useNotifyStore +│ └── ui.ts # useUIStore +│ +├── composables/ # 纯逻辑组合式函数(无 DOM) +│ ├── useEventBus.ts # mitt 实例 + 类型定义 +│ ├── useWindowContext.ts # provide/inject 窗口上下文 +│ ├── useKeyboard.ts # 全局快捷键 +│ ├── useIdleWatch.ts # 空闲计时 & 屏保触发 +│ ├── useTheme.ts # 暗色模式 & 壁纸 +│ ├── useVolume.ts # 音量 & 媒体注册 +│ ├── useDrag.ts # 窗口拖拽(pointer events 封装) +│ ├── useResize.ts # 窗口缩放(pointer events 封装) +│ ├── useClipboard.ts # 剪贴板操作封装 +│ ├── useClickOutside.ts # 外部点击检测 +│ ├── useStdMenus.ts # 标准菜单模板(纯函数) +│ └── useDockMag.ts # Dock 放大效果 +│ +├── components/ # Vue 组件 +│ ├── system/ # 系统 UI 组件 +│ │ ├── MenuBar.vue # 菜单栏(容器) +│ │ ├── MenuBarItem.vue # 菜单栏单项(展示) +│ │ ├── Dock.vue # 程序坞(容器) +│ │ ├── DockIcon.vue # 程序坞图标(展示) +│ │ ├── DesktopIcons.vue # 桌面图标区(容器) +│ │ ├── DesktopIcon.vue # 单个桌面图标(展示) +│ │ ├── Spotlight.vue # 聚焦搜索(容器) +│ │ ├── SpotlightItem.vue # 搜索结果项(展示) +│ │ ├── ControlCenter.vue # 控制中心(容器) +│ │ ├── CCToggle.vue # 控制中心开关(展示) +│ │ ├── CCSlider.vue # 控制中心滑块(展示) +│ │ ├── NotificationCenter.vue # 通知中心(容器) +│ │ ├── BannerContainer.vue # 横幅容器(容器) +│ │ ├── Banner.vue # 单个横幅(展示) +│ │ ├── LockScreen.vue # 锁屏界面(容器) +│ │ ├── Screensaver.vue # 屏保(容器) +│ │ ├── BootScreen.vue # 启动动画(展示) +│ │ └── PowerOff.vue # 关机提示(展示) +│ ├── wm/ # 窗口管理组件 +│ │ ├── WindowLayer.vue # 窗口层容器(容器) +│ │ ├── WindowFrame.vue # 窗口框架(容器) +│ │ ├── TrafficLights.vue # 红绿灯按钮(展示) +│ │ └── AppLoader.vue # 应用加载器(容器) +│ └── ui/ # 通用 UI 组件 +│ ├── ContextMenu.vue # 右键/下拉菜单(容器) +│ ├── ContextMenuItem.vue # 菜单项(展示) +│ ├── DialogLayer.vue # 对话框层(容器) +│ ├── AlertDialog.vue # 警告框(展示) +│ ├── ConfirmDialog.vue # 确认框(展示) +│ └── PromptDialog.vue # 输入框(展示) +│ +├── apps/ # 应用层 +│ ├── loaders.ts # 懒加载映射 + 预加载策略 +│ ├── finder/ +│ │ ├── index.ts # AppDefinition(纯数据) +│ │ └── Finder.vue # Finder SFC +│ ├── safari/ +│ │ ├── index.ts +│ │ └── Safari.vue +│ └── ...(其余 31 个应用,结构相同) +│ +├── styles/ # 样式(不变) +│ ├── base.css +│ ├── desktop.css +│ ├── window.css +│ ├── apps.css +│ └── apps2.css +│ +├── types/ # 全局类型定义 +│ ├── app.ts # AppDefinition, MenuItem +│ ├── window.ts # WinState, Rect +│ ├── fs.ts # FSNode, FSEntry +│ └── settings.ts # Settings, Wallpaper +│ +└── utils/ # 纯工具函数(无 DOM) + ├── format.ts # fmtDateCN, fmtTime, fmtBytes + ├── math.ts # clamp + ├── misc.ts # uid, debounce, esc + └── index.ts # 统一导出 +``` + +### 3.8 关键技术决策 + +| 决策点 | 选择 | 理由 | +|---|---|---| +| 路由方案 | **不使用 vue-router** | 桌面模拟器无 URL 路由需求,应用切换由 `wmStore` 窗口管理 + `AppLoader` 动态组件实现 | +| 状态管理 | **Pinia**(非 Vuex) | Vue 3 官方推荐,完整 TS 支持,模块化设计 | +| 跨组件通信 | **mitt + provide/inject** | mitt 用于全局通知,provide/inject 用于窗口级上下文,避免过度使用全局事件 | +| CSS 方案 | **纯 CSS(不变)** | 项目样式已成熟,无需引入 Tailwind/UnoCSS,减少重构范围 | +| 构建工具 | **Vite(不变)** | 当前已使用 Vite,保留配置 | +| 测试框架 | **Vitest(不变)** | 当前已使用 Vitest | +| 窗口拖拽/缩放 | **原生 Pointer Events + composable 封装** | 这是性能关键路径,Vue 事件系统不适合高频 pointermove | +| 持久化 | **Pinia watch + localStorage** | 简单可靠,无需引入额外的持久化库 | +| 应用加载 | **defineAsyncComponent + 分级策略** | Eager/IdlePreload/Lazy 三级策略兼顾首屏速度与体验 | +| 类型安全 | **全局禁止 `any`**(eslint rule) | 强制所有接口、store、事件使用精确类型 | + +### 3.9 性能策略 + +```mermaid +flowchart TB + subgraph "首屏加载" + A1["仅加载 Eager 应用(Finder)"] + A2["系统 UI 组件同步渲染"] + A3["requestIdleCallback 预加载 7 个常用应用"] + end + + subgraph "窗口操作" + B1["打开应用 → 检查是否已加载"] + B2["未加载 → Suspense + loading 占位"] + B3["已加载 → 立即显示 <component :is=>"] + end + + subgraph "关闭优化" + C1["关闭窗口 → 组件 v-if 卸载"] + C2["已加载的异步 chunk 保留在浏览器缓存"] + C3["再次打开 → 不重新下载 JS"] + end + + subgraph "内存管理" + D1["窗口关闭 → clearInterval/timeout"] + D2["onUnmounted → 移除事件监听"] + D3["Pinia store 按需持久化,不存储 DOM 引用"] + end + + A1 --> A2 --> A3 + B1 --> B2 + B1 --> B3 + C1 --> C2 --> C3 +``` + +--- + +## 4. 重构总路线 + +``` +Phase 1: 基础设施层 Phase 2: 系统 UI 组件化 Phase 3: WM 现代化 +───────────────────── ──────────────────────── ──────────────── +Pinia stores MenuBar → SFC WindowFrame → SFC +mitt 事件总线 Dock → SFC TrafficLights → SFC +消除 window 全局暴露 DesktopIcons → SFC 拖拽/缩放 composable +消除 lazy ref Spotlight → SFC resize handles → SFC + NotificationCenter → SFC + ControlCenter → SFC Phase 5: 清理 + LockScreen → SFC ─────────── + Screensaver → SFC 删除 utils/index.ts + BootScreen → SFC 删除 system/index.ts + ContextMenu → SFC 删除 el() 函数 + Dialog/Alert/Confirm → SFC 删除 $/$$ + 全局类型检查 + + Phase 4: 应用层优化 + ───────────────────── + defineAsyncComponent + 移除 (window as any).Sys + 移除 h()/render() 手动挂载 + stdMenus 纯函数化 +``` + +--- + +## 5. 阶段一:基础设施层 + +### 4.1 建立 Pinia Stores + +#### 4.1.1 `settingsStore` — 替代 useSettings + useStore + +```typescript +// src/stores/settings.ts +import { defineStore } from 'pinia' +import { reactive, watch } from 'vue' + +export const WALLPAPERS = [ /* 原数据 */ ] +export const DEFAULT_SETTINGS = { /* 原数据 */ } + +export const useSettingsStore = defineStore('settings', () => { + const settings = reactive(structuredClone(DEFAULT_SETTINGS)) + + // 持久化 watch(替代 store.set/load) + watch(settings, (v) => { + localStorage.setItem('macos-web:settings', JSON.stringify(v)) + }, { deep: true }) + + // 计算属性 + const isDark = computed(() => { /* 原 Sys.isDark() */ }) + const wallpaperSrc = computed(() => { /* 原 Sys.wallpaperSrc() */ }) + + // 方法 + function applyAppearance() { /* 操作 document.body.classList */ } + function applyWallpaper() { /* ... */ } + + return { settings, isDark, wallpaperSrc, applyAppearance, applyWallpaper } +}) +``` + +**改动要点**: +- `store.get/set` → Pinia `$state` + watch 持久化 +- `Sys.settings.xxx` → `settingsStore.settings.xxx` +- `Sys.isDark()` → `settingsStore.isDark` (computed) +- `Sys.save()` → 自动 watch 持久化 + +#### 4.1.2 `fsStore` — 替代 useFS + +```typescript +// src/stores/fs.ts +export const useFSStore = defineStore('fs', () => { + const root = ref(null) + const HOME = '/Users/guest' + const TRASH = computed(() => HOME + '/.Trash') + + function init() { /* 原 fs.init() */ } + function list(path: string) { /* ... */ } + function node(path: string) { /* ... */ } + function mkdir(path: string) { /* ... */ } + function rename(path: string, newName: string) { /* ... */ } + function trash(path: string) { /* ... */ } + function emptyTrash() { /* ... */ } + + // 用 mitt 替代 bus.emit('fs:changed') + function notifyChange(paths: string[]) { + emitter.emit('fs:changed', { paths }) + } + + return { root, HOME, TRASH, init, list, node, mkdir, rename, trash, emptyTrash } +}) +``` + +**改动要点**: +- `fs.node(path)` → `fsStore.node(path)`,响应式数据 +- `bus.emit('fs:changed')` → `emitter.emit('fs:changed')`(mitt) +- 文件系统树用 `ref` / `reactive` 保持响应式 + +#### 4.1.3 `wmStore` — 替代 useWM 的状态部分 + +```typescript +// src/stores/wm.ts +export const useWMStore = defineStore('wm', () => { + const windows = reactive([]) + const activeWin = ref(null) + const zTop = ref(100) + const cascadeCount = ref(0) + + function addWindow(win: WinState) { windows.push(win) } + function removeWindow(id: string) { /* splice */ } + function focus(win: WinState) { /* 更新 z-index + activeWin */ } + + return { windows, activeWin, zTop, cascadeCount, addWindow, removeWindow, focus } +}) +``` + +**改动要点**: +- WM `windows`/`activeWin`/`zTop` → Pinia store +- 窗口 DOM 创建逻辑分离到 `` 组件 + +#### 4.1.4 `appStore` — 替代 useApps 注册表 + +```typescript +// src/stores/apps.ts +export const useAppStore = defineStore('apps', () => { + const registry = reactive>({}) + + function register(def: AppDefinition) { registry[def.id] = def } + function get(id: string) { return registry[id] } + + // open 方法:不再手动调用 wm.openWindow + h() + render() + // 而是设置一个"待打开"信号,由 组件消费 + const pendingOpen = ref<{ id: string; args?: any } | null>(null) + + function open(id: string, args?: any) { + pendingOpen.value = { id, args } + } + + return { registry, register, get, pendingOpen, open } +}) +``` + +#### 4.1.5 `notifyStore` — 替代 useNotify 数据部分 + +```typescript +// src/stores/notify.ts +export const useNotifyStore = defineStore('notify', () => { + const notifications = ref([]) + + function send(n: Notification) { /* push + 持久化 */ } + function markRead(id: string) { /* ... */ } + function remove(id: string) { /* ... */ } + function clearAll() { /* ... */ } + function badgeCount(appId: string) { /* computed */ } + + return { notifications, send, markRead, remove, clearAll, badgeCount } +}) +``` + +### 4.2 引入 mitt 替代 useBus + +```bash +pnpm add mitt +``` + +```typescript +// src/composables/useEventBus.ts +import mitt from 'mitt' + +type Events = { + 'wm:focus': WinState + 'wm:changed': void + 'fs:changed': { paths: string[] } + 'apps:ready': void + 'volume:changed': number + 'trash:changed': void + // ... 所有事件类型 +} + +export const emitter = mitt() +``` + +**替代映射**: +| 原 bus 用法 | 新 mitt 用法 | +|---|---| +| `bus.on('wm:focus', fn)` | `emitter.on('wm:focus', fn)` | +| `bus.emit('wm:focus', win)` | `emitter.emit('wm:focus', win)` | +| `bus.off('wm:focus', fn)` | `emitter.off('wm:focus', fn)` | + +### 4.3 消除 `window` 全局暴露 + +**删除 `system/index.ts` 中的所有 `(window as any)` 赋值**。 + +替代方案: +- 模块间引用 → Pinia store / provide-inject +- App 组件引用 → 通过 `useAppStore().get(id)` 获取 +- 工具函数引用 → 直接 import + +### 4.4 消除 lazy ref + +**删除所有 `let _Xxx: any = null` + `setXxxRef()` 模式**。 + +原懒引用的用途和替代方案: +| 原用途 | 替代 | +|---|---| +| `setSysRef(wm)` — WM 需要 Sys.settings | WM 直接 import `useSettingsStore` | +| `setSysForNotify(sys)` — Notify 需要 Sys.settings | Notify 直接 import `useSettingsStore` | +| `setSysForSpotlight(sys)` — Spotlight 需要 Sys | Spotlight 直接 import 对应 store | +| `setAppsRef(apps)` — FS 需要 Apps 注册表 | FS 直接 import `useAppStore` | +| `setAppStoreRef(a)` — Sys 需要 AppStore | 延迟访问 `useAppStore().get('appstore')` | + +解决循环依赖的根本方法:**拆分模块**。例如 `useSys.ts` 应该拆分为: +- `stores/settings.ts` — 设置管理 +- `composables/useKeyboard.ts` — 全局快捷键 +- `composables/useIdleWatch.ts` — 空闲计时/屏保 +- `composables/useTheme.ts` — 外观/壁纸应用 +- `` / `` / `` 等 Vue 组件 + +--- + +## 6. 阶段二:系统 UI 组件化 + +### 5.1 `` — 替代 Sys.buildMenubar() + +**原实现**:`useSys.ts` 中 `buildMenubar()` 用 `el()` 构建整个菜单栏 DOM,操作 `#menubar-left` / `#menubar-right`。 + +**目标实现**: + +```vue + + + + +``` + +**改动要点**: +- `el()` 构建 DOM → Vue 模板循环 +- `(this as any)._mbClock` → `ref` + `setInterval` +- `addEventListener('click')` → `@click` +- 菜单栏左侧应用菜单 → 根据 `wmStore.activeWin` 计算 + +### 5.2 `` — 替代 Sys.renderDock() + +```vue + + +``` + +**改动要点**: +- `dock.innerHTML = ''` + `el()` 循环构建 → `v-for` 组件 +- `ic.addEventListener('click')` → `@click` emit +- `ic.addEventListener('contextmenu')` → `@contextmenu` emit +- Dock 放大动画用 CSS + 少量 pointer 事件(封装 composable) +- `updateDockDots()` → 基于 `wmStore.windows` 的 computed + +### 5.3 `` — 替代 Sys.renderDesktopIcons() + +```vue + + +``` + +**改动要点**: +- 每个桌面图标是一个独立的 `` SFC +- 拖拽移动用 pointer events(保留原生,封装在 composable 中) +- 双击/右键 → emit 到父组件处理 + +### 5.4 `` / `` — 替代 useUI + +这是最关键的变革之一:**将动态创建的弹出菜单和对话框改为 Vue 组件 + Teleport**。 + +```vue + + +``` + +**全局菜单/对话框管理**: + +```typescript +// src/stores/ui.ts +export const useUIStore = defineStore('ui', () => { + // 弹出菜单 + const menuVisible = ref(false) + const menuItems = ref([]) + const menuPosition = ref({ x: 0, y: 0 }) + const menuOnClose = ref<(() => void) | null>(null) + + function showMenu(items: MenuItem[], x: number, y: number, onClose?: () => void) { + closeMenu() + menuItems.value = items + menuPosition.value = { x, y } + menuVisible.value = true + menuOnClose.value = onClose ?? null + } + function closeMenu() { + menuOnClose.value?.() + menuVisible.value = false + } + + // 对话框 + const dialogVisible = ref(false) + const dialogConfig = ref({ /* title, msg, buttons, ... */ }) + let dialogResolve: ((v: any) => void) | null = null + + function showDialog(config: DialogConfig): Promise { + return new Promise(resolve => { + dialogConfig.value = config + dialogVisible.value = true + dialogResolve = resolve + }) + } + + return { menuVisible, menuItems, menuPosition, showMenu, closeMenu, + dialogVisible, dialogConfig, showDialog, /* ... */ } +}) +``` + +**改动要点**: +- `ui.menu(items, x, y)` → `uiStore.showMenu(items, x, y)` +- `ui.dialog({...})` → `uiStore.showDialog({...})`,返回 Promise 不变 +- `ui.alert/confirm/prompt` → 基于 `showDialog` 的语法糖 +- 菜单嵌套(submenu)用递归组件 +- 全局 `pointerdown` 关闭菜单 → 在 `` 组件内部用 `onClickOutside` + +### 5.5 `` — 替代 useSpotlight + +```vue + + +``` + +**改动要点**: +- HTML 结构在模板中定义(当前已在 `SpotlightArea.vue` 中有静态模板,但内容仍由 JS 动态填充) +- `input.oninput` → `v-model` + `watch` +- `input.onkeydown` → `@keydown` +- `render()` 中的 `el()` 构建 → computed `results` + `v-for` +- `scrollIntoView` → `nextTick` + Vue 方式滚动 + +### 5.6 `` / `` — 替代 useNotify 渲染 + +```vue + + +``` + +**改动要点**: +- `el()` 创建横幅 DOM → `` SFC +- `setTimeout` 自动消失 → `watch` + `setTimeout`,配合 `` +- 通知中心 `nc.innerHTML = ''` 重建 → computed 列表 + `v-for` + +### 5.7 `` / `` / `` / `` + +这些容器组件当前已有 Vue 模板(如 `LockScreenArea.vue`),但内部内容(锁屏按钮、屏保幻灯片等)全由 `el()` 动态构建。需改为: + +- `` 模板中包含完整的 HTML 结构,用 `v-if`/`v-show` 控制显示 +- `` 中的时钟/幻灯片 → 用计算属性 + `v-if` 选择类型 +- `` 中的进度条动画 → CSS transition + `v-show` + +### 5.8 `` — 替代 Sys.renderControlCenter() + +```vue + + +``` + +**改动要点**: +- `ccToggle()` 函数生成 DOM → `` SFC,用 `v-model` 双向绑定 +- `cc.innerHTML = ''` 重建 → 响应式数据驱动 +- 外点击关闭 → `onClickOutside` 指令/composable + +--- + +## 7. 阶段三:窗口管理器现代化 + +### 6.1 `` — 替代 useWM 的窗口 chrome 构建 + +**原实现**:`openWindow()` 中用 `el()` 创建 traffic lights、标题栏、body、resize handles,全部命令式。 + +**目标实现**: + +```vue + + + + +``` + +### 6.2 `` — 独立组件 + +```vue + + +``` + +### 6.3 `` — 容器组件 + +```vue + + +``` + +### 6.4 拖拽/缩放 composable + +拖拽和缩放必须用原生 pointer events(这是性能关键路径,不可用 Vue 事件替代),但封装为纯 composable: + +```typescript +// src/composables/useDrag.ts +export function useDrag(elRef: Ref, win: WinState) { + function onDragStart(e: PointerEvent) { + if (win.state === 'fullscreen') return + const el = elRef.value! + el.setPointerCapture(e.pointerId) + + const startX = e.clientX + const startY = e.clientY + const origX = win.rect.x + const origY = win.rect.y + + function onMove(e: PointerEvent) { + win.rect.x = origX + e.clientX - startX + win.rect.y = origY + e.clientY - startY + } + + function onUp() { + el.removeEventListener('pointermove', onMove) + el.removeEventListener('pointerup', onUp) + } + + el.addEventListener('pointermove', onMove) + el.addEventListener('pointerup', onUp) + } + + return { onDragStart } +} +``` + +```typescript +// src/composables/useResize.ts +export function useResize(elRef: Ref, win: WinState) { + function onResizeStart(e: PointerEvent, dir: string) { + // 类似 useDrag,但修改 width/height/x/y + } + return { onResizeStart } +} +``` + +### 6.5 窗口关闭流程现代化 + +原 `close()` 方法的 Promise + confirm 流程保持不变,但 `closePromise` 管理移到 `wmStore`: + +```typescript +// stores/wm.ts +async function close(winId: string): Promise { + const win = windows.find(w => w.id === winId) + if (!win) return 'alreadyClosed' + + // 如果有确认回调 + if (win.confirmClose) { + return new Promise(resolve => { + win.confirmClose!( + () => { doClose(win); resolve('closed') }, + () => resolve('cancelled'), + ) + }) + } + + doClose(win) + return 'closed' +} +``` + +--- + +## 8. 阶段四:应用层优化 + +### 7.1 `` — 替代手动 `h()` + `render()` + +**原实现**:每个 app 的 `index.ts` 中: +```typescript +render(win) { + const vnode = h(XxxComponent, { win }) + render(vnode, win.body) +} +``` + +**目标实现**:用 `defineAsyncComponent` + `` 动态加载: + +```vue + + + + +``` + +应用懒加载映射表: + +```typescript +// src/apps/loaders.ts +import { defineAsyncComponent } from 'vue' + +export const appLoaders: Record = { + finder: defineAsyncComponent(() => import('./finder/Finder.vue')), + safari: defineAsyncComponent(() => import('./safari/Safari.vue')), + calculator: defineAsyncComponent(() => import('./calculator/Calculator.vue')), + settings: defineAsyncComponent(() => import('./settings/Settings.vue')), + // ... 33 个应用 +} +``` + +### 7.2 应用注册简化 + +**原 `index.ts`**: +```typescript +export const FinderApp = { id:'finder', ..., render(win) { ... } } +Apps.register(FinderApp) +``` + +**改为纯数据定义**: + +```typescript +// src/apps/finder/index.ts +import type { AppDefinition } from '@/stores/apps' + +export const finderDef: AppDefinition = { + id: 'finder', + name: '访达', + icon: '/assets/icons/finder.png', + w: 800, h: 550, + minW: 480, minH: 300, + singleton: true, + menus(win) { return stdMenus('finder', { /* ... */ }) }, + // 不再需要 render 方法! +} +``` + +```typescript +// src/stores/apps.ts +import { finderDef } from '@/apps/finder' + +// 在 store 初始化时注册 +function init() { + register(finderDef) + register(safariDef) + // ... 33 个应用 +} +``` + +**关键变化**:每个 app 不再包含 `render(win)` 方法,而是由 `` 根据 `appId` 查找对应的异步组件来渲染。 + +### 7.3 移除应用中的 `(window as any)` + +当前多个应用访问 `(window as any).Sys` / `(window as any).Notify` 等: + +| 文件 | 全局引用 | 替代方案 | +|---|---|---| +| `finder/Finder.vue` | `(window as any).__finderClipboard` | Pinia store 或 provide/inject | +| `safari/Safari.vue` | `window.open(url, '_blank')` | 允许(无替代方案) | +| `calculator/Calculator.vue` | `props.win.el?.addEventListener` | 用 `@keydown` 在模板中处理 | +| 多个应用 | `bus.on('fs:changed')` | `emitter.on('fs:changed')` | +| 多个应用 | `ui.alert/confirm/prompt` | `uiStore.showDialog()` | +| 多个应用 | `wm.setTitle(props.win, ...)` | `wmStore.setTitle(winId, title)` | +| 多个应用 | `props.win.appState = st` | 通过 `win.data` 或 provide | + +### 7.4 `stdMenus()` 纯函数化 + +**原实现**使用 `document.activeElement` / `document.execCommand`: + +```typescript +// 原:直接访问 document +const editBase = () => { + const a = document.activeElement as any + const isText = a && (a.tagName === 'INPUT' || a.tagName === 'TEXTAREA' || a.isContentEditable) + // ... +} +``` + +**改进**:接受 `isTextFocused: boolean` 参数,由调用方传入: + +```typescript +// src/composables/useStdMenus.ts +export function stdMenus(appDef: AppDef, options: StdMenuOptions = {}) { + return function (win: WinState | null) { + return [ + { + label: '编辑', + items: () => [ + { label: '剪切', key: '⌘X', action: () => clipboard.cut() }, + { label: '拷贝', key: '⌘C', action: () => clipboard.copy() }, + { label: '粘贴', key: '⌘V', action: () => clipboard.paste() }, + // ... + ] + }, + // ... + ] + } +} +``` + +剪贴板操作封装为独立的 `useClipboard` composable(内部使用 `navigator.clipboard` API)。 + +--- + +## 9. 阶段五:清理与收尾 + +### 8.1 删除文件 + +``` +删除: + src/utils/index.ts — el/$/$$ 函数,完全由 Vue 替代 + src/system/index.ts — 启动序列逻辑分散到各初始化的 store/composable + src/composables/useBus.ts — 由 mitt 替代 + src/composables/useStore.ts — 由 Pinia 持久化插件替代 + src/composables/useUI.ts — 由 / + uiStore 替代 + src/composables/useSys.ts — 拆分为多个 store + composable + Vue 组件 +``` + +### 8.2 重构后的目录结构 + +``` +src/ +├── main.ts +├── App.vue +├── stores/ # Pinia stores(新增) +│ ├── settings.ts # 设置管理 +│ ├── fs.ts # 虚拟文件系统 +│ ├── wm.ts # 窗口管理器状态 +│ ├── apps.ts # 应用注册表 +│ ├── notify.ts # 通知数据 +│ └── ui.ts # 菜单/对话框状态 +├── composables/ # 纯逻辑 composable(无 DOM) +│ ├── useKeyboard.ts # 全局快捷键 +│ ├── useIdleWatch.ts # 空闲计时/屏保 +│ ├── useTheme.ts # 外观/壁纸 +│ ├── useVolume.ts # 音量/媒体控制 +│ ├── useDrag.ts # 窗口拖拽(pointer events 封装) +│ ├── useResize.ts # 窗口缩放(pointer events 封装) +│ ├── useClipboard.ts # 剪贴板操作封装 +│ ├── useStdMenus.ts # 标准菜单模板(纯函数) +│ ├── useEventBus.ts # mitt 事件总线实例 +│ └── useClickOutside.ts # 外点击检测 +├── components/ +│ ├── system/ # 系统 UI 组件 +│ │ ├── MenuBar.vue +│ │ ├── MenuBarItem.vue +│ │ ├── Dock.vue +│ │ ├── DockIcon.vue +│ │ ├── DesktopIcons.vue +│ │ ├── DesktopIcon.vue +│ │ ├── Spotlight.vue +│ │ ├── SpotlightItem.vue +│ │ ├── ControlCenter.vue +│ │ ├── CCToggle.vue +│ │ ├── CCSlider.vue +│ │ ├── NotificationCenter.vue +│ │ ├── Banner.vue +│ │ ├── BannerContainer.vue +│ │ ├── LockScreen.vue +│ │ ├── Screensaver.vue +│ │ ├── BootScreen.vue +│ │ └── PowerOff.vue +│ ├── wm/ # 窗口管理组件 +│ │ ├── WindowLayer.vue +│ │ ├── WindowFrame.vue +│ │ ├── TrafficLights.vue +│ │ └── AppLoader.vue +│ └── ui/ # 通用 UI 组件 +│ ├── ContextMenu.vue +│ ├── ContextMenuItem.vue +│ ├── DialogLayer.vue +│ ├── AlertDialog.vue +│ ├── ConfirmDialog.vue +│ └── PromptDialog.vue +├── apps/ # 应用层(基本不变,去掉 render 方法) +│ ├── loaders.ts # 懒加载映射表(新增) +│ ├── finder/ +│ │ ├── index.ts # 仅数据定义(AppDefinition) +│ │ └── Finder.vue +│ ├── calculator/ +│ │ ├── index.ts +│ │ └── Calculator.vue +│ └── ...(33 个应用) +├── styles/ # 不变 +└── types/ # 类型定义(从各模块提取) + ├── app.ts + ├── window.ts + ├── fs.ts + └── settings.ts +``` + +### 8.3 启动流程重构 + +**原**:`system/index.ts` → `initSystem()` → 手动串联 `fs.init → Sys.init → Notify.init → Sys.renderDock → initDesktop → boot` + +**新**:在 `App.vue` 的 `onMounted` 中: + +```typescript +// App.vue +onMounted(async () => { + // 1. 初始化 stores + const fsStore = useFSStore() + await fsStore.init() + + const settingsStore = useSettingsStore() + settingsStore.init() + + const appStore = useAppStore() + appStore.init() // 注册所有 33 个应用 + + const notifyStore = useNotifyStore() + notifyStore.init() + + // 2. 初始化全局 composable + useKeyboard() + useIdleWatch() + useTheme() + + // 3. 触发启动动画 + const sysStore = useSysStore() + sysStore.boot() + + // 4. 通知应用就绪 + emitter.emit('apps:ready') +}) +``` + +### 8.4 测试适配 + +测试文件(`tests/cases/` 下 47 个)需要相应更新: +- `(window as any).Sys` / `(window as any).WM` → 通过 Pinia store 操作 +- `(window as any).FS` → `useFSStore()` +- `(window as any).Notify` → `useNotifyStore()` +- `(window as any).Apps` → `useAppStore()` +- `bus.on/emit` → `emitter.on/emit` + +建议在 `tests/setup.ts` 中创建一个测试辅助工具,提供简化的 store 访问。 + +--- + +## 10. 附录:新旧对照表 + +### 9.1 文件映射 + +| 原文件 | 重构后 | 说明 | +|---|---|---| +| `src/utils/index.ts` | **删除** | `el()`/`$`/`$$` 由 Vue 模板/ref 替代;格式化函数移入 `src/utils/format.ts` | +| `src/system/index.ts` | **删除** | 启动逻辑分散到 store 初始化和 App.vue | +| `src/composables/useBus.ts` | **删除** | 由 `src/composables/useEventBus.ts` (mitt) 替代 | +| `src/composables/useStore.ts` | **删除** | 由 Pinia 持久化 watch 替代 | +| `src/composables/useUI.ts` | `src/stores/ui.ts` + `src/components/ui/*.vue` | 拆为状态 store + Vue 组件 | +| `src/composables/useSys.ts` | 拆分为 7 个模块 | 见下一节 | +| `src/composables/useWM.ts` | `src/stores/wm.ts` + `src/components/wm/*.vue` + `src/composables/useDrag.ts` + `src/composables/useResize.ts` | 状态/UI/拖拽分离 | +| `src/composables/useNotify.ts` | `src/stores/notify.ts` + `src/components/system/Banner.vue` + `src/components/system/NotificationCenter.vue` | 数据与视图分离 | +| `src/composables/useSpotlight.ts` | `src/stores/spotlight.ts` + `src/components/system/Spotlight.vue` | 数据与视图分离 | +| `src/composables/useApps.ts` | `src/stores/apps.ts` + `src/composables/useStdMenus.ts` | 注册表 + 菜单模板分离 | +| `src/composables/useFS.ts` | `src/stores/fs.ts` | 直接迁移到 Pinia | +| `src/composables/useSettings.ts` | `src/stores/settings.ts` | 合并 useStore 到 Pinia | + +### 9.2 useSys.ts 拆分 + +| 原负责内容 | 目标位置 | +|---|---| +| `settings` / `save` / `set` / `applyAll` / `isDark` / `wallpaperSrc` / `applyAppearance` / `applyWallpaper` / `applyBrightness` / `applyNightShift` / `applyVolume` / `registerMedia` | `src/stores/settings.ts` | +| `boot` / `showLock` / `closeOverlays` / `powerOff` / `restart` / `logout` / `sleep` / `forceQuitDialog` | `src/stores/sys.ts` | +| `globalKeys` | `src/composables/useKeyboard.ts` | +| `initIdleWatch` / `resetIdle` / `showScreensaver` / `hideScreensaver` | `src/composables/useIdleWatch.ts` | +| `buildMenubar` / `wifiSvg` / `tickClock` / `openAppleMenu` / `setActiveApp` | `src/components/system/MenuBar.vue` | +| `dockPinned` / `dockItems` / `renderDock` / `effDockSize` / `layoutDock` / `dockPeek` / `dockHide` / `dockResetMag` / `initDockMag` / `updateDockDots` / `dockClick` / `dockIconMenu` / `dockBgMenu` | `src/components/system/Dock.vue` + `src/composables/useDockMag.ts` | +| `toggleControlCenter` / `ccToggle` / `renderControlCenter` | `src/components/system/ControlCenter.vue` | +| `toggleNotificationCenter` | `src/components/system/MenuBar.vue`(时钟点击) | +| `resetAll` | `src/stores/settings.ts` | + +### 9.3 API 对照 + +| 原 API | 新 API | +|---|---| +| `Sys.settings.xxx` | `settingsStore.settings.xxx` | +| `Sys.isDark()` | `settingsStore.isDark` (computed) | +| `Sys.set('key', val)` | `settingsStore.settings.key = val` | +| `Sys.renderDesktopIcons()` | `emitter.emit('fs:changed')` → 自动更新 | +| `fs.node(path)` | `fsStore.node(path)` | +| `fs.list(path)` | `fsStore.list(path)` | +| `bus.emit('fs:changed', data)` | `emitter.emit('fs:changed', data)` | +| `ui.menu(items, x, y)` | `uiStore.showMenu(items, x, y)` | +| `ui.dialog({...})` | `uiStore.showDialog({...})` | +| `ui.alert(title, msg)` | `uiStore.alert(title, msg)` | +| `ui.confirm(title, msg)` | `uiStore.confirm(title, msg)` | +| `wm.openWindow(opts)` | `wmStore.openWindow(opts)` | +| `wm.close(win)` | `wmStore.close(winId)` | +| `wm.focus(win)` | `wmStore.focus(winId)` | +| `wm.activeWin.value` | `wmStore.activeWin` | +| `Apps.open('finder', args)` | `appStore.open('finder', args)` | +| `Apps.get('finder')` | `appStore.get('finder')` | +| `Apps.register(def)` | `appStore.register(def)` | +| `Notify.send({...})` | `notifyStore.send({...})` | +| `Notify.badgeCount('mail')` | `notifyStore.badgeCount('mail')` | +| `Spotlight.toggle()` | `spotlightStore.toggle()` | + +### 9.4 保留的原生 API + +以下原生 API 是必须保留的(封装在专用 composable 中): + +| API | 用途 | 封装位置 | +|---|---|---| +| `PointerEvent` (pointerdown/move/up/capture) | 窗口拖拽、缩放、桌面图标拖拽 | `useDrag.ts` / `useResize.ts` | +| `localStorage` | 持久化(由 Pinia watch 自动处理) | 各 store 内部 | +| `matchMedia('prefers-color-scheme: dark')` | 系统暗色模式检测 | `useTheme.ts` | +| `navigator.clipboard` | 剪贴板操作 | `useClipboard.ts` | +| `window.open(url, '_blank')` | Safari 外链打开 | `useSafari.ts` | +| `requestAnimationFrame` | 启动动画 | `` 组件内 | +| `Image()` | 壁纸预加载 | `useTheme.ts` | + +--- + +## 11. 执行建议 + +### 10.1 执行顺序 + +1. **安装依赖**:`pnpm add pinia mitt` +2. **阶段一**:创建所有 Pinia stores + mitt 事件总线,确保与原功能等价(此时还依赖 `el()` 等旧 API) +3. **阶段二**:逐个将系统 UI 组件化,每完成一个就用新组件替换旧的 `el()` 渲染,保持可运行 +4. **阶段三**:窗口管理器重构 +5. **阶段四**:应用层优化 + lazy load +6. **阶段五**:清理删除旧文件,更新测试 + +### 10.2 关键原则 + +- **每步保持可运行**:不要一次性改完所有东西再测试;每个组件替换后立即验证 +- **先测试后重构**:在重构前确保现有 47 个测试用例全部通过,重构后逐批更新测试 +- **类型安全优先**:所有新增模块用完整 TypeScript 类型定义,消除 `any` +- **CSS 不变**:现有 `styles/*.css` 无需修改,CSS 类名保持不变 + +### 10.3 预估工作量 + +| 阶段 | 描述 | 预估 | +|---|---|---| +| 阶段一 | Pinia stores + mitt | 1-2 天 | +| 阶段二 | 系统 UI 组件化(MenuBar/Dock/Spotlight/CC/通知/锁屏/屏保/启动/对话框/右键菜单 — 共 10+ 个组件) | 3-5 天 | +| 阶段三 | WM 重构(WindowFrame/TrafficLights/AppLoader + drag/resize composable) | 1-2 天 | +| 阶段四 | 33 个应用适配(去 render 方法、去 window 引用、lazy load) | 2-3 天 | +| 阶段五 | 清理 + 测试更新 | 1-2 天 | +| **总计** | | **8-14 天** | diff --git a/package.json b/package.json index 0a38c39..60d0e59 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "test:watch": "vitest" }, "dependencies": { + "mitt": "^3.0.1", + "pinia": "^4.0.2", "vue": "^3.5.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..b4d8c04 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1946 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + mitt: + specifier: ^3.0.1 + version: 3.0.1 + pinia: + specifier: ^4.0.2 + version: 4.0.2(@vue/devtools-api@8.1.5)(typescript@5.6.3)(vue@3.5.40(typescript@5.6.3)) + vue: + specifier: ^3.5.0 + version: 3.5.40(typescript@5.6.3) + devDependencies: + '@types/node': + specifier: ^26.1.1 + version: 26.1.1 + '@vitejs/plugin-vue': + specifier: ^5.1.0 + version: 5.2.4(vite@5.4.21(@types/node@26.1.1))(vue@3.5.40(typescript@5.6.3)) + '@vue/test-utils': + specifier: ^2.4.0 + version: 2.4.11(@vue/compiler-dom@3.5.40)(@vue/server-renderer@3.5.40)(vue@3.5.40(typescript@5.6.3)) + jsdom: + specifier: ^29.1.1 + version: 29.1.1 + typescript: + specifier: ~5.6.0 + version: 5.6.3 + vite: + specifier: ^5.4.0 + version: 5.4.21(@types/node@26.1.1) + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@26.1.1)(jsdom@29.1.1) + vue-tsc: + specifier: ^2.1.0 + version: 2.2.12(typescript@5.6.3) + +packages: + + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.3.0': + resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.10': + resolution: {integrity: sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.7': + resolution: {integrity: sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@26.1.1': + resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + '@volar/language-core@2.4.15': + resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} + + '@volar/source-map@2.4.15': + resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==} + + '@volar/typescript@2.4.15': + resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} + + '@vue/compiler-core@3.5.40': + resolution: {integrity: sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==} + + '@vue/compiler-dom@3.5.40': + resolution: {integrity: sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==} + + '@vue/compiler-sfc@3.5.40': + resolution: {integrity: sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==} + + '@vue/compiler-ssr@3.5.40': + resolution: {integrity: sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/devtools-api@8.1.5': + resolution: {integrity: sha512-YJipMVAKe5wT5CWf5kTYCaNV7NMNjFVxJkIkJaJ4W/nCxEBzlZzrOsYKeCymdCrFZmBS/+wTWFoUs3Jf/Q6XSQ==} + + '@vue/devtools-kit@8.1.5': + resolution: {integrity: sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA==} + + '@vue/devtools-shared@8.1.5': + resolution: {integrity: sha512-mhT4zcPFhF+Xk1O4BfhhrbXzpmfqY03fS6xGpcllbQG7lDjhQf8pQHcTIhqQIYx1hfwtHmk/6jM96ele0UxPqQ==} + + '@vue/language-core@2.2.12': + resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.40': + resolution: {integrity: sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==} + + '@vue/runtime-core@3.5.40': + resolution: {integrity: sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==} + + '@vue/runtime-dom@3.5.40': + resolution: {integrity: sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==} + + '@vue/server-renderer@3.5.40': + resolution: {integrity: sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==} + + '@vue/shared@3.5.40': + resolution: {integrity: sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==} + + '@vue/test-utils@2.4.11': + resolution: {integrity: sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==} + peerDependencies: + '@vue/compiler-dom': 3.x + '@vue/server-renderer': 3.x + vue: 3.x + peerDependenciesMeta: + '@vue/server-renderer': + optional: true + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + alien-signals@1.0.13: + resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + brace-expansion@2.1.2: + resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.8: + resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} + + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + nostics@1.2.0: + resolution: {integrity: sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + pinia@4.0.2: + resolution: {integrity: sha512-yKVVA7bSj5oRZFp/Ab9wLlmyb5gPUYEiIm4ryiWTe/xe7PtkRdMVOp1X1ggvq0c6Uj7Q0Du1HnV2mtAwM0Ks1g==} + peerDependencies: + '@vue/devtools-api': ^8.1.5 + typescript: '>=5.6.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + postcss@8.5.22: + resolution: {integrity: sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==} + engines: {node: ^10 || ^12 || >=14} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tldts-core@7.4.9: + resolution: {integrity: sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==} + + tldts@7.4.9: + resolution: {integrity: sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==} + hasBin: true + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-component-type-helpers@3.3.8: + resolution: {integrity: sha512-troqCMmQodQDqUqn63NQaFi+CDSclSe7sc8VEBFqf5GFLqmGR2Ph3P2WEC7qwpRVyEWsTi/aAr4vyOe/B1hU3g==} + + vue-tsc@2.2.12: + resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.40: + resolution: {integrity: sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + +snapshots: + + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.1.0': {} + + '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.7(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@exodus/bytes@1.15.1': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@one-ini/wasm@0.1.1': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@types/estree@1.0.9': {} + + '@types/node@26.1.1': + dependencies: + undici-types: 8.3.0 + + '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@26.1.1))(vue@3.5.40(typescript@5.6.3))': + dependencies: + vite: 5.4.21(@types/node@26.1.1) + vue: 3.5.40(typescript@5.6.3) + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@26.1.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@26.1.1) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + '@volar/language-core@2.4.15': + dependencies: + '@volar/source-map': 2.4.15 + + '@volar/source-map@2.4.15': {} + + '@volar/typescript@2.4.15': + dependencies: + '@volar/language-core': 2.4.15 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.40': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.40 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.40': + dependencies: + '@vue/compiler-core': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/compiler-sfc@3.5.40': + dependencies: + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.40 + '@vue/compiler-dom': 3.5.40 + '@vue/compiler-ssr': 3.5.40 + '@vue/shared': 3.5.40 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.22 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.40': + dependencies: + '@vue/compiler-dom': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/devtools-api@8.1.5': + dependencies: + '@vue/devtools-kit': 8.1.5 + + '@vue/devtools-kit@8.1.5': + dependencies: + '@vue/devtools-shared': 8.1.5 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@8.1.5': {} + + '@vue/language-core@2.2.12(typescript@5.6.3)': + dependencies: + '@volar/language-core': 2.4.15 + '@vue/compiler-dom': 3.5.40 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.40 + alien-signals: 1.0.13 + minimatch: 9.0.9 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.6.3 + + '@vue/reactivity@3.5.40': + dependencies: + '@vue/shared': 3.5.40 + + '@vue/runtime-core@3.5.40': + dependencies: + '@vue/reactivity': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/runtime-dom@3.5.40': + dependencies: + '@vue/reactivity': 3.5.40 + '@vue/runtime-core': 3.5.40 + '@vue/shared': 3.5.40 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.40': + dependencies: + '@vue/compiler-ssr': 3.5.40 + '@vue/runtime-dom': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/shared@3.5.40': {} + + '@vue/test-utils@2.4.11(@vue/compiler-dom@3.5.40)(@vue/server-renderer@3.5.40)(vue@3.5.40(typescript@5.6.3))': + dependencies: + '@vue/compiler-dom': 3.5.40 + js-beautify: 1.15.4 + vue: 3.5.40(typescript@5.6.3) + vue-component-type-helpers: 3.3.8 + optionalDependencies: + '@vue/server-renderer': 3.5.40 + + abbrev@2.0.0: {} + + alien-signals@1.0.13: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + assertion-error@2.0.1: {} + + balanced-match@1.0.2: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + birpc@2.9.0: {} + + brace-expansion@2.1.2: + dependencies: + balanced-match: 1.0.2 + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@10.0.1: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + csstype@3.2.3: {} + + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + de-indent@1.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-eql@5.0.2: {} + + eastasianwidth@0.2.0: {} + + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.9 + semver: 7.8.5 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@7.0.1: {} + + entities@8.0.0: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + he@1.2.0: {} + + hookable@5.5.3: {} + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + + ini@1.3.8: {} + + is-fullwidth-code-point@3.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.4.5 + js-cookie: 3.0.8 + nopt: 7.2.1 + + js-cookie@3.0.8: {} + + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.7(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 7.28.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@11.5.2: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mdn-data@2.27.1: {} + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.2 + + minipass@7.1.3: {} + + mitt@3.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.16: {} + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + + nostics@1.2.0: {} + + package-json-from-dist@1.0.1: {} + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + path-browserify@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pathe@1.1.2: {} + + pathval@2.0.1: {} + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + pinia@4.0.2(@vue/devtools-api@8.1.5)(typescript@5.6.3)(vue@3.5.40(typescript@5.6.3)): + dependencies: + '@vue/devtools-api': 8.1.5 + nostics: 1.2.0 + vue: 3.5.40(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + + postcss@8.5.22: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proto-list@1.2.4: {} + + punycode@2.3.1: {} + + require-from-string@2.0.2: {} + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + symbol-tree@3.2.4: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tldts-core@7.4.9: {} + + tldts@7.4.9: + dependencies: + tldts-core: 7.4.9 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.9 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + typescript@5.6.3: {} + + undici-types@8.3.0: {} + + undici@7.28.0: {} + + vite-node@2.1.9(@types/node@26.1.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@26.1.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@26.1.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.22 + rollup: 4.62.2 + optionalDependencies: + '@types/node': 26.1.1 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@26.1.1)(jsdom@29.1.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@26.1.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@26.1.1) + vite-node: 2.1.9(@types/node@26.1.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.1.1 + jsdom: 29.1.1 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vscode-uri@3.1.0: {} + + vue-component-type-helpers@3.3.8: {} + + vue-tsc@2.2.12(typescript@5.6.3): + dependencies: + '@volar/typescript': 2.4.15 + '@vue/language-core': 2.2.12(typescript@5.6.3) + typescript: 5.6.3 + + vue@3.5.40(typescript@5.6.3): + dependencies: + '@vue/compiler-dom': 3.5.40 + '@vue/compiler-sfc': 3.5.40 + '@vue/runtime-dom': 3.5.40 + '@vue/server-renderer': 3.5.40 + '@vue/shared': 3.5.40 + optionalDependencies: + typescript: 5.6.3 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} diff --git a/src/composables/useEventBus.ts b/src/composables/useEventBus.ts new file mode 100644 index 0000000..374af2b --- /dev/null +++ b/src/composables/useEventBus.ts @@ -0,0 +1,28 @@ +/** + * useEventBus — mitt 类型安全事件总线 + * 替代原 useBus.ts,所有事件必须在此声明类型 + */ +import mitt from 'mitt' +import type { WinState } from '@/types/window' + +type Events = { + // 窗口事件 + 'wm:focus': WinState | null + 'wm:changed': void + 'wm:closed': { winId: string } + + // 文件系统事件 + 'fs:changed': { op?: string; paths: string[]; dirty?: boolean } + + // 系统事件 + 'apps:ready': void + 'volume:changed': number + 'trash:changed': void + 'unlocked': void + 'locked': void + + // 通知事件 + 'notify:badges-updated': void +} + +export const emitter = mitt() diff --git a/src/main.ts b/src/main.ts index 2b39912..1a7af6f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,6 @@ import { createApp } from 'vue' import App from './App.vue' +import { initPinia } from './stores' // Import all CSS import './styles/base.css' @@ -9,4 +10,5 @@ import './styles/apps.css' import './styles/apps2.css' const app = createApp(App) +app.use(initPinia()) app.mount('#app') diff --git a/src/stores/apps.ts b/src/stores/apps.ts new file mode 100644 index 0000000..fbbe48b --- /dev/null +++ b/src/stores/apps.ts @@ -0,0 +1,55 @@ +/** + * appStore — 应用注册表 + * 替代 useApps.ts 中的注册表部分 + */ +import { defineStore } from 'pinia' +import { ref, reactive } from 'vue' +import { useFSStore } from './fs' +import type { AppDefinition } from '@/types/app' + +export const useAppStore = defineStore('apps', () => { + const registry = reactive>({}) + + // 待打开的应用(由 AppLoader 消费) + const pendingOpen = ref<{ id: string; args?: any } | null>(null) + + function register(def: AppDefinition) { + def.w = def.w || 720 + def.h = def.h || 480 + def.minW = def.minW || 320 + def.minH = def.minH || 240 + def.about = def.about || `${def.name} — macOS 网页版内置应用` + registry[def.id] = def + } + + function get(id: string): AppDefinition | undefined { + return registry[id] + } + + function open(id: string, args?: any) { + pendingOpen.value = { id, args } + } + + function openPath(path: string) { + const fsStore = useFSStore() + const n = fsStore.node(path) + if (!n) { + // ui.alert 稍后接入 + console.warn('[apps] 找不到项目:', path) + return + } + if (n.t === 'd') return open('finder', { path }) + if (n.t === 'a') return open(n.app!) + const ext = (path.split('.').pop() || '').toLowerCase() + if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'pdf'].includes(ext)) + return open('preview', { path }) + return open('textedit', { path }) + } + + function quit(id: string) { + // 由外部 wm 处理实际关闭 + pendingOpen.value = { id, args: { quit: true } } + } + + return { registry, pendingOpen, register, get, open, openPath, quit } +}) diff --git a/src/stores/fs.ts b/src/stores/fs.ts new file mode 100644 index 0000000..78b0638 --- /dev/null +++ b/src/stores/fs.ts @@ -0,0 +1,315 @@ +/** + * fsStore — 虚拟文件系统 + * 替代 useFS.ts,数据用 Pinia 管理 + */ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { emitter } from '@/composables/useEventBus' +import type { FSNode, FSEntry } from '@/types/fs' + +// ---- 文件扩展名 → MIME 映射 ---- +function mimeOf(name: string): string { + const ext = name.split('.').pop()?.toLowerCase() || '' + const map: Record = { + txt: 'text/plain', md: 'text/markdown', html: 'text/html', css: 'text/css', js: 'text/javascript', + json: 'application/json', xml: 'text/xml', svg: 'image/svg+xml', csv: 'text/csv', + } + return map[ext] || 'text/plain' +} + +// ---- 文件扩展名 → 图标映射 ---- +export function iconFor(path: string): string { + const segs = path.split('/') + const name = segs[segs.length - 1] || '' + const ext = name.split('.').pop()?.toLowerCase() || '' + + const extIcons: Record = { + txt: '/assets/icons/file-text.svg', md: '/assets/icons/file-text.svg', + html: '/assets/icons/file-code.svg', css: '/assets/icons/file-code.svg', + js: '/assets/icons/file-code.svg', ts: '/assets/icons/file-code.svg', + json: '/assets/icons/file-code.svg', xml: '/assets/icons/file-code.svg', + png: '/assets/icons/file-image.svg', jpg: '/assets/icons/file-image.svg', + jpeg: '/assets/icons/file-image.svg', gif: '/assets/icons/file-image.svg', + webp: '/assets/icons/file-image.svg', svg: '/assets/icons/file-image.svg', + pdf: '/assets/icons/file-pdf.svg', + mp3: '/assets/icons/file-audio.svg', wav: '/assets/icons/file-audio.svg', ogg: '/assets/icons/file-audio.svg', + mp4: '/assets/icons/file-video.svg', mov: '/assets/icons/file-video.svg', + zip: '/assets/icons/file-archive.svg', tar: '/assets/icons/file-archive.svg', gz: '/assets/icons/file-archive.svg', + app: '/assets/icons/app-default.svg', + } + if (extIcons[ext]) return extIcons[ext] + return '/assets/icons/file-generic.svg' +} + +export const useFSStore = defineStore('fs', () => { + const HOME = '/Users/guest' + const root = ref(null) + + // ---- 懒引用(由外部注入,打破循环依赖) ---- + let _Apps: any = null + let _AppStoreApp: any = null + + function setAppsRef(apps: any, appStore: any) { + _Apps = apps + _AppStoreApp = appStore + } + + function setAppStoreRef(a: any) { _AppStoreApp = a } + + const TRASH = computed(() => HOME + '/.Trash') + + // ---- 路径工具 ---- + function normalize(p: string): string { + if (!p) return '/' + const parts: string[] = [] + for (const seg of String(p).split('/')) { + if (!seg || seg === '.') continue + if (seg === '..') parts.pop() + else parts.push(seg) + } + return '/' + parts.join('/') + } + + function join(...segs: string[]) { return normalize(segs.join('/')) } + function baseName(p: string) { p = normalize(p); return p === '/' ? '/' : p.slice(p.lastIndexOf('/') + 1) } + function dirName(p: string) { p = normalize(p); const i = p.lastIndexOf('/'); return i <= 0 ? '/' : p.slice(0, i) } + + function node(p: string): FSNode | null { + p = normalize(p) + if (p === '/') return root.value + let cur: FSNode | null = root.value + for (const seg of p.slice(1).split('/')) { + if (!cur || cur.t !== 'd' || !cur.c![seg]) return null + cur = cur.c![seg] + } + return cur + } + + function exists(p: string) { return !!node(p) } + function isDir(p: string) { return node(p)?.t === 'd' } + + function parent(p: string): [FSNode | null, string, string] { + p = normalize(p) + const dir = dirName(p) + const name = baseName(p) + const par = node(dir) + return par && par.t === 'd' ? [par, name, dir] : [null, name, dir] + } + + function assertDir(p: string): FSNode { + const n = node(p) + if (!n || n.t !== 'd') throw new Error('不是文件夹: ' + p) + return n + } + + // ---- 持久化 ---- + function loadFromStorage(): FSNode | null { + try { + const raw = localStorage.getItem('macos-web:fs') + if (!raw) return null + const v = JSON.parse(raw) + return v && v.t === 'd' ? v : null + } catch { return null } + } + + function save() { + try { localStorage.setItem('macos-web:fs', JSON.stringify(root.value)) } + catch (e) { console.warn('[fs] 保存失败:', e) } + } + + // ---- 初始化 & 种子数据 ---- + function seed() { + const dirs = ['Desktop', 'Documents', 'Downloads', 'Pictures', 'Music', 'Applications', '.Trash'] + dirs.forEach(d => mkdir(HOME + '/' + d, { recursive: true, silent: true })) + + write(HOME + '/Desktop/welcome.txt', [ + '欢迎使用 macOS 网页版!', '', + '这是一套在浏览器中运行的桌面模拟器。', '你可以:', + '· 双击打开「Sample Folder」和各个应用', '· 在访达、终端、文本编辑之间管理同一套虚拟文件', + '· 通过 Apple 菜单锁定、重启或关机', '· 在系统设置中更换壁纸、切换深色模式', '', + '所有数据都保存在浏览器本地,刷新后依然存在。' + ].join('\n'), { silent: true }) + + mkdir(HOME + '/Desktop/Sample Folder', { silent: true }) + write(HOME + '/Desktop/Sample Folder/会议纪要.txt', '周会纪要\n\n1. 桌面端体验优化\n2. 虚拟文件系统联调\n3. 下周发布预览版', { silent: true }) + write(HOME + '/Desktop/Sample Folder/待办.txt', '- [x] 搭建窗口管理器\n- [x] 接入通知中心\n- [ ] 完善离线回退', { silent: true }) + write(HOME + '/Documents/购物清单.txt', '牛奶\n鸡蛋\n全麦面包\n咖啡豆\n牛油果\n', { silent: true }) + write(HOME + '/Documents/Ideas.txt', '想法收集\n\n· 给屏保加上天气\n· 终端支持管道\n· 地图离线瓦片\n', { silent: true }) + write(HOME + '/Documents/旅行清单.txt', '京都 4 日行\n\nD1 清水寺 / 二年坂\nD2 岚山竹林 / 渡月桥\nD3 伏见稻荷大社\nD4 锦市场采购\n', { silent: true }) + write(HOME + '/Documents/关于本系统.txt', 'macOS 网页版 v1.0\n\n纯 HTML/CSS/JavaScript 实现,无需构建。\n数据存储于 localStorage,离线可用。', { silent: true }) + write(HOME + '/Downloads/说明.txt', '此目录用于存放下载的文件。', { silent: true }) + write(HOME + '/Pictures/壁纸说明.txt', '系统内置多张壁纸,可在「系统设置 › 墙纸」中切换。', { silent: true }) + write(HOME + '/Music/曲目说明.txt', '音乐 App 已内置 6 首 Kevin MacLeod (CC-BY) 曲目。', { silent: true }) + save() + } + + function init() { + root.value = loadFromStorage() + if (!root.value || root.value.t !== 'd') { + root.value = { t: 'd', c: {}, mtime: Date.now() } + seed() + } + emitter.on('apps:ready', () => syncApps()) + } + + // ---- 应用同步 ---- + function syncApps() { + if (!_Apps) return + const dir = node(HOME + '/Applications') + if (!dir) return + const installed = (id: string) => _AppStoreApp && _AppStoreApp.isInstalled(id) + const want = new Set() + for (const app of Object.values(_Apps.registry) as any[]) { + if ((app as any).storeApp && !installed((app as any).id)) continue + want.add((app as any).id) + const name = (app as any).name + '.app' + if (!dir.c![name]) dir.c![name] = { t: 'a', app: (app as any).id, mtime: Date.now() } + } + let dirty = false + for (const [name, n] of Object.entries(dir.c!)) { + if ((n as FSNode).t === 'a' && (n as FSNode).app && !want.has((n as FSNode).app!)) { + const reg = _Apps.registry[(n as FSNode).app!] + if (reg && reg.storeApp) { delete dir.c![name]; dirty = true } + } + } + save() + emitter.emit('fs:changed', { op: 'sync', paths: [HOME + '/Applications'], dirty }) + } + + // ---- CRUD 操作 ---- + function list(p: string, opts: { showHidden?: boolean } = {}): FSEntry[] { + const n = assertDir(p) + return Object.entries(n.c!) + .filter(([name]) => opts.showHidden || !name.startsWith('.')) + .map(([name, nd]) => ({ name, path: join(p, name), node: nd })) + .sort((a, b) => a.name.localeCompare(b.name, 'zh-Hans-CN')) + } + + function read(p: string): string { + const n = node(p) + if (!n) throw new Error('文件不存在: ' + p) + if (n.t !== 'f') throw new Error('不是文本文件: ' + p) + return n.data ?? '' + } + + function write(p: string, data: string, opts: { mime?: string; silent?: boolean } = {}) { + const [par, name] = parent(p) + if (!par) throw new Error('父目录不存在: ' + p) + const now = Date.now() + if (par.c![name] && par.c![name].t === 'd') throw new Error('同名文件夹已存在: ' + name) + par.c![name] = { t: 'f', data: String(data ?? ''), mime: opts.mime || mimeOf(name), mtime: now } + save() + if (!opts.silent) emitter.emit('fs:changed', { op: 'write', paths: [normalize(p), dirName(p)] }) + } + + function mkdir(p: string, opts: { recursive?: boolean; silent?: boolean } = {}) { + p = normalize(p) + if (exists(p)) { if (!opts.recursive) throw new Error('已存在: ' + p); return } + if (opts.recursive) { + let cur = root.value! + let curPath = '' + for (const seg of p.slice(1).split('/')) { + curPath += '/' + seg + if (!cur.c![seg]) cur.c![seg] = { t: 'd', c: {}, mtime: Date.now() } + if (cur.c![seg].t !== 'd') throw new Error('路径冲突: ' + curPath) + cur = cur.c![seg] + } + save() + if (!opts.silent) emitter.emit('fs:changed', { op: 'mkdir', paths: [p, dirName(p)] }) + return + } + const [par, name] = parent(p) + if (!par) throw new Error('父目录不存在: ' + p) + par.c![name] = { t: 'd', c: {}, mtime: Date.now() } + save() + if (!opts.silent) emitter.emit('fs:changed', { op: 'mkdir', paths: [p, dirName(p)] }) + } + + function renameItem(p: string, newName: string): string { + p = normalize(p); newName = String(newName || '').trim() + if (!newName || newName.includes('/')) throw new Error('名称无效') + const [par, name, dir] = parent(p) + if (!par || !par.c![name]) throw new Error('不存在: ' + p) + if (name === newName) return p + if (par.c![newName]) throw new Error('已存在同名项目: ' + newName) + par.c![newName] = par.c![name]; delete par.c![name] + par.c![newName].mtime = Date.now() + const np = join(dir, newName) + save(); emitter.emit('fs:changed', { op: 'rename', paths: [p, np, dir] }) + return np + } + + function uniqueName(dir: string, base: string): string { + const n = assertDir(dir) + if (!n.c![base]) return base + const dot = base.lastIndexOf('.') + const stem = dot > 0 ? base.slice(0, dot) : base + const ext = dot > 0 ? base.slice(dot) : '' + for (let i = 2; ; i++) { const cand = `${stem} ${i}${ext}`; if (!n.c![cand]) return cand } + } + + function copy(src: string, dstDir: string): string { + src = normalize(src); dstDir = normalize(dstDir) + const sn = node(src); if (!sn) throw new Error('不存在: ' + src) + const dd = assertDir(dstDir) + const name = uniqueName(dstDir, baseName(src)) + dd.c![name] = structuredClone(sn); dd.c![name].mtime = Date.now() + delete (dd.c![name] as any).origPath + save(); emitter.emit('fs:changed', { op: 'copy', paths: [src, join(dstDir, name), dstDir] }) + return join(dstDir, name) + } + + function move(src: string, dstDir: string): string { + src = normalize(src); dstDir = normalize(dstDir) + if (src === dstDir) throw new Error('不能移动到自身') + if (dstDir === src || dstDir.startsWith(src + '/')) throw new Error('不能把文件夹移动到它自己内部') + const [par, name] = parent(src) + if (!par || !par.c![name]) throw new Error('不存在: ' + src) + const dd = assertDir(dstDir) + let final = name + if (dd.c![final]) final = uniqueName(dstDir, name) + dd.c![final] = par.c![name]; delete par.c![name] + dd.c![final].mtime = Date.now() + const np = join(dstDir, final) + save(); emitter.emit('fs:changed', { op: 'move', paths: [src, np, dirName(src), dstDir] }) + return np + } + + function remove(p: string) { + p = normalize(p) + if (p === '/' || p === TRASH.value) throw new Error('不能删除该项目') + const [par, name, dir] = parent(p) + if (!par || !par.c![name]) throw new Error('不存在: ' + p) + delete par.c![name] + save(); emitter.emit('fs:changed', { op: 'remove', paths: [p, dir] }) + } + + function trash(p: string): string { + p = normalize(p) + if (p.startsWith(TRASH.value + '/')) { remove(p); return p } + const n = node(p); if (!n) throw new Error('不存在: ' + p) + ;(n as any).origPath = p + const np = move(p, TRASH.value) + save() + emitter.emit('trash:changed') + return np + } + + function emptyTrash() { + const t = node(TRASH.value) + if (t && t.c) Object.keys(t.c).forEach(k => delete t.c![k]) + save() + emitter.emit('trash:changed') + emitter.emit('fs:changed', { op: 'trash:empty', paths: [TRASH.value] }) + } + + return { + HOME, TRASH, root, + setAppsRef, setAppStoreRef, + init, seed, save, syncApps, + normalize, join, baseName, dirName, + node, exists, isDir, assertDir, + list, read, write, mkdir, renameItem, uniqueName, + copy, move, remove, trash, emptyTrash, + } +}) diff --git a/src/stores/index.ts b/src/stores/index.ts new file mode 100644 index 0000000..5a4b9fb --- /dev/null +++ b/src/stores/index.ts @@ -0,0 +1,11 @@ +/** + * Pinia 实例创建 & 插件注册 + */ +import { createPinia } from 'pinia' +import { persistPlugin } from './plugins/persist' + +export function initPinia() { + const pinia = createPinia() + pinia.use(persistPlugin) + return pinia +} diff --git a/src/stores/notify.ts b/src/stores/notify.ts new file mode 100644 index 0000000..4539464 --- /dev/null +++ b/src/stores/notify.ts @@ -0,0 +1,64 @@ +/** + * notifyStore — 通知数据管理 + * 替代 useNotify.ts 中的数据部分(通知列表/持久化/徽标计数) + */ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { Notification } from '@/types/notify' + +export const useNotifyStore = defineStore('notify', () => { + const notifications = ref([]) + + // ---- 持久化 ---- + function loadFromStorage(): Notification[] { + try { + const raw = localStorage.getItem('macos-web:notify') + return raw ? JSON.parse(raw) : [] + } catch { return [] } + } + + function save() { + try { + localStorage.setItem('macos-web:notify', JSON.stringify(notifications.value.slice(-60))) + } catch { /* ignore */ } + } + + function init() { + notifications.value = loadFromStorage() + } + + // ---- 操作 ---- + function send(n: Notification) { + notifications.value.push(n) + save() + } + + function markRead(id: string) { + const n = notifications.value.find(x => x.id === id) + if (n && !n.read) { n.read = true; save() } + } + + function remove(id: string) { + notifications.value = notifications.value.filter(x => x.id !== id) + save() + } + + function clearAll() { + notifications.value = [] + save() + } + + function badgeCount(appId: string): number { + return notifications.value.filter(n => n.appId === appId && !n.read).length + } + + function unreadCount(): number { + return notifications.value.filter(n => !n.read).length + } + + return { + notifications, init, save, + send, markRead, remove, clearAll, + badgeCount, unreadCount, + } +}) diff --git a/src/stores/plugins/persist.ts b/src/stores/plugins/persist.ts new file mode 100644 index 0000000..502d4fc --- /dev/null +++ b/src/stores/plugins/persist.ts @@ -0,0 +1,37 @@ +/** + * Pinia persist 插件 + * 自动将指定 store 的状态持久化到 localStorage + */ +import type { PiniaPluginContext } from 'pinia' + +const PREFIX = 'macos-web:' + +/** 需要持久化的 store id 列表 */ +const PERSISTED = ['settings', 'fs', 'notify'] + +export function persistPlugin({ store }: PiniaPluginContext) { + if (!PERSISTED.includes(store.$id)) return + + // 1. 初始化:从 localStorage 恢复 + try { + const raw = localStorage.getItem(PREFIX + store.$id) + if (raw) { + const parsed = JSON.parse(raw) + if (parsed) store.$patch(parsed) + } + } catch { /* ignore */ } + + // 2. 自动保存 + store.$subscribe((_, state) => { + try { + localStorage.setItem(PREFIX + store.$id, JSON.stringify(state)) + } catch (e) { console.warn(`[persist] ${store.$id} 保存失败:`, e) } + }, { detached: true, deep: true }) +} + +/** 清除所有持久化数据 */ +export function clearAllPersisted() { + Object.keys(localStorage) + .filter(k => k.startsWith(PREFIX)) + .forEach(k => localStorage.removeItem(k)) +} diff --git a/src/stores/settings.ts b/src/stores/settings.ts new file mode 100644 index 0000000..fcc4a62 --- /dev/null +++ b/src/stores/settings.ts @@ -0,0 +1,118 @@ +/** + * settingsStore — 系统设置管理 + * 替代 useSettings.ts + useStore.ts 的设置部分 + */ +import { defineStore } from 'pinia' +import { reactive, computed } from 'vue' +import type { SystemSettings, Wallpaper } from '@/types/settings' + +export const WALLPAPERS: Wallpaper[] = [ + { id: 'monterey', name: 'Monterey 抽象', src: '/assets/wallpapers/monterey.jpg' }, + { id: 'sonoma', name: 'Sonoma 落日', src: '/assets/wallpapers/sonoma.jpg' }, + { id: 'ventura', name: 'Ventura 霞光', src: '/assets/wallpapers/ventura.jpg' }, + { id: 'bigsur', name: 'Big Sur 海岸', src: '/assets/wallpapers/big-sur-day.jpg', dark: '/assets/wallpapers/big-sur-night.jpg' }, + { id: 'sequoia', name: 'Sequoia 山谷', src: '/assets/wallpapers/sequoia.jpg' }, + { id: 'galaxy', name: '银河', src: '/assets/wallpapers/galaxy.jpg' }, +] + +export const DEFAULT_SETTINGS: SystemSettings = { + appearance: 'light', wallpaper: 'monterey', accent: '#0a84ff', + reduceTransparency: false, reduceMotion: false, increaseContrast: false, + dockSize: 54, dockMagnify: true, dockMagnifyLevel: 1.6, dockPosition: 'bottom', dockAutohide: false, + brightness: 1, nightShift: false, nightShiftStrength: 0.4, + volume: 0.6, muted: false, + wifi: true, bluetooth: true, airdrop: false, focus: false, vpn: false, + userName: '客人用户', avatar: '/assets/icons/avatar.svg', + passwordEnabled: false, password: '', + screensaverType: 'off', screensaverDelay: 5, + h24: false, language: 'zh-Hans', region: '中国', firstDayMonday: true, + searchEngine: 'bing', computerName: 'MacBook Pro', + siriApps: true, siriFiles: true, siriSettings: true, + loginItems: {}, notifAllow: {}, + notificationsEnabled: true, + timezone: 'local', + wifiNetwork: '家庭网络 5G', + btDevices: {}, + kbRepeat: 7, kbDelay: 4, mouseSpeed: 5, + finderView: 'icon', finderSort: 'name', +} + +export const useSettingsStore = defineStore('settings', () => { + // ---- 状态 ---- + const settings = reactive(structuredClone(DEFAULT_SETTINGS)) + + // ---- 计算属性 ---- + const isDark = computed(() => { + const a = settings.appearance + if (a === 'dark') return true + if (a === 'auto') return matchMedia('(prefers-color-scheme: dark)').matches + return false + }) + + const wallpaperSrc = computed(() => { + const w = WALLPAPERS.find(wp => wp.id === settings.wallpaper) || WALLPAPERS[0] + return (isDark.value && w.dark) ? w.dark : w.src + }) + + // ---- 初始化 ---- + function init(saved?: Partial) { + if (saved) Object.assign(settings, structuredClone(DEFAULT_SETTINGS), saved) + } + + // ---- 方法 ---- + function set(key: K, value: SystemSettings[K]) { + settings[key] = value + } + + function applyAppearance() { + const b = document.body + b.classList.toggle('dark', isDark.value) + b.classList.toggle('reduce-transparency', !!settings.reduceTransparency) + b.classList.toggle('reduce-motion', !!settings.reduceMotion) + b.classList.toggle('increase-contrast', !!settings.increaseContrast) + applyWallpaper() + } + + function applyWallpaper() { + const src = wallpaperSrc.value + const desk = document.getElementById('desktop') + const lock = document.getElementById('lockscreen-bg') + if (desk) desk.style.backgroundImage = `url("${src}")` + if (lock) lock.style.backgroundImage = `url("${src}")` + // 壁纸预加载错误回退 + const probe = new Image() + probe.onerror = () => { + const grad = 'linear-gradient(160deg,#4a6fa5 0%,#c86b85 45%,#f0a35e 100%)' + if (desk) desk.style.backgroundImage = grad + if (lock) lock.style.backgroundImage = grad + } + probe.src = src + } + + function applyBrightness() { + const el = document.getElementById('overlay-brightness') + if (el) el.style.opacity = String((1 - settings.brightness) * 0.55) + } + + function applyNightShift() { + const el = document.getElementById('overlay-nightshift') + if (el) el.style.opacity = settings.nightShift ? String(settings.nightShiftStrength * 0.32) : '0' + } + + function applyAccent() { + document.documentElement.style.setProperty('--accent', settings.accent) + } + + function applyAll() { + applyAppearance() + applyBrightness() + applyNightShift() + applyAccent() + } + + return { + settings, isDark, wallpaperSrc, + init, set, + applyAppearance, applyWallpaper, applyBrightness, applyNightShift, applyAccent, applyAll, + } +}) diff --git a/src/stores/ui.ts b/src/stores/ui.ts new file mode 100644 index 0000000..f328e8b --- /dev/null +++ b/src/stores/ui.ts @@ -0,0 +1,89 @@ +/** + * uiStore — 菜单/对话框状态管理 + * 替代 useUI.ts 中的状态管理部分 + */ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { MenuItem } from '@/types/app' +import type { DialogConfig } from '@/types/ui' + +export const useUIStore = defineStore('ui', () => { + // ---- 弹出菜单 ---- + const menuVisible = ref(false) + const menuItems = ref([]) + const menuPosition = ref({ x: 0, y: 0 }) + const menuSub = ref(false) + let menuOnClose: (() => void) | null = null + + function showMenu(items: MenuItem[], x: number, y: number, opts: { sub?: boolean; onClose?: () => void } = {}) { + hideMenu() + menuItems.value = items + menuPosition.value = { x, y } + menuSub.value = !!opts.sub + menuVisible.value = true + menuOnClose = opts.onClose ?? null + } + + function hideMenu() { + menuOnClose?.() + menuOnClose = null + menuVisible.value = false + } + + function showContextMenu(items: MenuItem[], e: MouseEvent) { + e.preventDefault(); e.stopPropagation() + showMenu(items, e.clientX, e.clientY) + } + + // ---- 对话框 ---- + const dialogVisible = ref(false) + const dialogConfig = ref({ title: '', msg: '' }) + let dialogResolve: ((v: string | boolean) => void) | null = null + + function showDialog(config: DialogConfig): Promise { + return new Promise(resolve => { + dialogConfig.value = config + dialogVisible.value = true + dialogResolve = resolve + }) + } + + function resolveDialog(value: string | boolean) { + dialogVisible.value = false + dialogResolve?.(value) + dialogResolve = null + } + + async function alert(title: string, msg: string, icon?: string): Promise { + return showDialog({ icon, title, msg, buttons: ['好'] }).then(() => true) + } + + async function confirm( + title: string, msg: string, + opts: { ok?: string; danger?: boolean; icon?: string } = {} + ): Promise { + const result = await showDialog({ + icon: opts.icon, title, msg, + ok: opts.ok || '确定', danger: opts.danger, + }) + return result === 'ok' || result === true + } + + async function prompt(title: string, msg: string, defaultValue?: string): Promise { + const result = await showDialog({ + title, msg: msg + (defaultValue ? `\n\n默认值:${defaultValue}` : ''), + ok: '确定', + }) + return typeof result === 'string' ? result : null + } + + return { + // menu + menuVisible, menuItems, menuPosition, menuSub, + showMenu, hideMenu, showContextMenu, + // dialog + dialogVisible, dialogConfig, + showDialog, resolveDialog, + alert, confirm, prompt, + } +}) diff --git a/src/stores/wm.ts b/src/stores/wm.ts new file mode 100644 index 0000000..cfaec27 --- /dev/null +++ b/src/stores/wm.ts @@ -0,0 +1,135 @@ +/** + * wmStore — 窗口管理器状态 + * 替代 useWM.ts 中的状态管理部分(windows / activeWin / zTop / cascade) + * 注意:窗口 DOM 创建/拖拽/缩放逻辑保留在 useWM.ts 中 + */ +import { defineStore } from 'pinia' +import { ref, reactive } from 'vue' +import { emitter } from '@/composables/useEventBus' +import { useSettingsStore } from './settings' +import type { Rect, WinState, WinStateType } from '@/types/window' + +export const useWMStore = defineStore('wm', () => { + const windows: WinState[] = reactive([]) + const activeWin = ref(null) + const zTop = ref(100) + const cascadeCount = ref(0) + + // ---- 辅助 ---- + function usableRect(): Rect { + const s = useSettingsStore().settings + const r: Rect = { x: 0, y: 30, w: window.innerWidth, h: window.innerHeight - 30 } + if (!s.dockAutohide) { + const dockH = (s.dockSize || 48) + 26 + if (s.dockPosition === 'bottom') r.h -= dockH + else if (s.dockPosition === 'left') { r.x += dockH; r.w -= dockH } + else r.w -= dockH + } + return r + } + + function windowsForApp(appId: string): WinState[] { + return windows.filter(w => w.appId === appId) + } + + // ---- 窗口操作 ---- + function addWindow(win: WinState) { + windows.push(win) + } + + function removeWindow(winId: string) { + const idx = windows.findIndex(w => w.id === winId) + if (idx >= 0) windows.splice(idx, 1) + } + + function focus(winId: string) { + const win = windows.find(w => w.id === winId) + if (!win) return + if (win.state === 'minimized') restore(winId) + if (activeWin.value?.id === winId) { + if (win.el) win.el.style.zIndex = String(++zTop.value) + return + } + if (activeWin.value) activeWin.value.el?.classList.add('inactive') + activeWin.value = win + win.el?.classList.remove('inactive') + if (win.el) win.el.style.zIndex = String(++zTop.value) + emitter.emit('wm:focus', win) + emitter.emit('wm:changed') + } + + function minimize(winId: string) { + const win = windows.find(w => w.id === winId) + if (!win || win.state === 'fullscreen') return + ;(win as any).stateBeforeMin = win.state + win.state = 'minimized' + win.el?.classList.add('minimized') + if (activeWin.value?.id === winId) { + const rest = windows.filter(w => w.state !== 'minimized') + activeWin.value = null + if (rest.length) focus(rest[rest.length - 1].id) + else emitter.emit('wm:focus', null) + } + emitter.emit('wm:changed') + } + + function restore(winId: string) { + const win = windows.find(w => w.id === winId) + if (!win) return + win.state = ((win.stateBeforeMin && win.stateBeforeMin !== 'minimized') ? win.stateBeforeMin : 'normal') as WinStateType + win.el?.classList.remove('minimized') + focus(winId) + emitter.emit('wm:changed') + } + + function applyRect(win: WinState) { + const r = win.rect + if (win.el) Object.assign(win.el.style, { + left: r.x + 'px', top: r.y + 'px', + width: r.w + 'px', height: r.h + 'px', + }) + } + + function toggleZoom(winId: string) { + const win = windows.find(w => w.id === winId) + if (!win || win.state === 'fullscreen') return + if (win.state === 'minimized') restore(winId) + if (win.state === 'zoomed') { + win.rect = { ...win.prevRect! } + win.state = 'normal' + } else { + win.prevRect = { ...win.rect } + const u = usableRect() + win.rect = { x: u.x + 4, y: u.y + 4, w: u.w - 8, h: u.h - 8 } + win.state = 'zoomed' + } + applyRect(win) + } + + function setTitle(winId: string, title: string) { + const win = windows.find(w => w.id === winId) + if (!win) return + win.title = title + if (win.titleEl) { + const t = win.titleEl.querySelector('.t') + if (t) t.textContent = title + } + } + + function clampAll() { + const u = usableRect() + for (const w of windows) { + if (w.state === 'fullscreen' || w.state === 'minimized') continue + w.rect.x = Math.max(u.x, Math.min(w.rect.x, u.x + u.w - 80)) + w.rect.y = Math.max(u.y, Math.min(w.rect.y, u.y + u.h - 40)) + applyRect(w) + } + } + + return { + windows, activeWin, zTop, cascadeCount, + usableRect, windowsForApp, + addWindow, removeWindow, focus, minimize, restore, + applyRect, toggleZoom, setTitle, clampAll, + } +}) diff --git a/src/system/index.ts b/src/system/index.ts index bad4508..1887215 100644 --- a/src/system/index.ts +++ b/src/system/index.ts @@ -161,20 +161,5 @@ export function initSystem() { Sys.boot() } -// Expose globals for compatibility -;(window as any).Sys = Sys -;(window as any).Notify = Notify -;(window as any).Spotlight = Spotlight -;(window as any).WM = wm -;(window as any).UI = ui -;(window as any).FS = fs -;(window as any).Apps = Apps -;(window as any).Bus = bus -;(window as any).Store = store -;(window as any).WALLPAPERS = WALLPAPERS -;(window as any).$ = $ -;(window as any).$$ = $$ -;(window as any).el = el -;(window as any).esc = esc -;(window as any).clamp = clamp -;(window as any).iconImg = iconImg +// 各模块通过 store/composable 访问,不再挂载到 window 全局 +// 如需在其他位置使用,请通过 import { useXxxStore } from '@/stores/xxx' 访问 diff --git a/src/types/app.ts b/src/types/app.ts new file mode 100644 index 0000000..2bf1560 --- /dev/null +++ b/src/types/app.ts @@ -0,0 +1,29 @@ +// ============ 应用 ============ + +export interface MenuItem { + label?: string + key?: string + checked?: boolean + disabled?: boolean + submenu?: MenuItem[] + action?: () => void + sep?: boolean + icon?: string +} + +export interface AppDefinition { + id: string + name: string + icon: string + w?: number + h?: number + minW?: number + minH?: number + singleton?: boolean + about?: string + noResize?: boolean + storeApp?: boolean + onArgs?: (args: any, win: any) => void + menus?: (win: any) => MenuItem[] + render?: (win: any, args?: any) => void +} diff --git a/src/types/fs.ts b/src/types/fs.ts new file mode 100644 index 0000000..bb0b0f5 --- /dev/null +++ b/src/types/fs.ts @@ -0,0 +1,17 @@ +// ============ 虚拟文件系统 ============ + +export interface FSNode { + t: 'd' | 'f' | 'a' + c?: Record + data?: string + mtime: number + mime?: string + app?: string + origPath?: string +} + +export interface FSEntry { + name: string + path: string + node: FSNode +} diff --git a/src/types/notify.ts b/src/types/notify.ts new file mode 100644 index 0000000..f3f5e81 --- /dev/null +++ b/src/types/notify.ts @@ -0,0 +1,11 @@ +// ============ 通知 ============ + +export interface Notification { + id: string + appId: string + title: string + body: string + ts: number + read: boolean + icon: string +} diff --git a/src/types/settings.ts b/src/types/settings.ts new file mode 100644 index 0000000..c6007c4 --- /dev/null +++ b/src/types/settings.ts @@ -0,0 +1,30 @@ +// ============ 系统设置 ============ + +export interface Wallpaper { + id: string + name: string + src: string + dark?: string +} + +export interface SystemSettings { + appearance: string; wallpaper: string; accent: string + reduceTransparency: boolean; reduceMotion: boolean; increaseContrast: boolean + dockSize: number; dockMagnify: boolean; dockMagnifyLevel: number; dockPosition: string; dockAutohide: boolean + brightness: number; nightShift: boolean; nightShiftStrength: number + volume: number; muted: boolean + wifi: boolean; bluetooth: boolean; airdrop: boolean; focus: boolean; vpn: boolean + userName: string; avatar: string + passwordEnabled: boolean; password: string + screensaverType: string; screensaverDelay: number + h24: boolean; language: string; region: string; firstDayMonday: boolean + searchEngine: string; computerName: string + siriApps: boolean; siriFiles: boolean; siriSettings: boolean + loginItems: Record; notifAllow: Record + notificationsEnabled: boolean + timezone: string + wifiNetwork: string + btDevices: Record + kbRepeat: number; kbDelay: number; mouseSpeed: number + finderView: string; finderSort: string +} diff --git a/src/types/ui.ts b/src/types/ui.ts new file mode 100644 index 0000000..c953241 --- /dev/null +++ b/src/types/ui.ts @@ -0,0 +1,10 @@ +// ============ 对话框 ============ + +export interface DialogConfig { + icon?: string + title: string + msg: string + buttons?: string[] + ok?: string + danger?: boolean +} diff --git a/src/types/window.ts b/src/types/window.ts new file mode 100644 index 0000000..59e6ea6 --- /dev/null +++ b/src/types/window.ts @@ -0,0 +1,36 @@ +// ============ 窗口状态 ============ + +export interface Rect { + x: number + y: number + w: number + h: number +} + +export type WinStateType = 'normal' | 'minimized' | 'zoomed' | 'fullscreen' + +export interface WinState { + id: string + appId: string + app: any + title: string + icon: string + rect: Rect + prevRect: Rect | null + minW: number + minH: number + state: WinStateType + onClose: (() => void) | null + data: any + noResize: boolean + el: HTMLElement | null + body: HTMLElement | null + titleEl: HTMLElement | null + timers: ReturnType[] + appState?: any + confirmClose?: (done: () => void, cancel: () => void) => void + closePromise?: Promise | null + _closed?: boolean + stateBeforeMin?: string + stateBeforeFs?: string +}