diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c21c2c4 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Production/public domain. Must resolve to the VPS running frps. +DOMAIN=cents.homemade.net.cn + +# Public VPS / frps address. +FRPC_SERVER_HOST=119.28.12.24 +FRPC_SERVER_PORT=7000 + +# Production target. For host-debug mode, keep host.docker.internal. +APP_HOST=host.docker.internal +APP_PORT=6064 +APP_PROTOCOL=http + +# Use false only after the tunnel is known to work. +TEST_MODE=false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bbd338a --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +legacy-cent-app/ +node_modules/ +dist/ +.env +.env.* +!.env.example +*.log +.DS_Store + diff --git a/README.md b/README.md index e69de29..a7b9457 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,31 @@ +# Cents + +家庭共享账本 App。 + +目标是做一个精简、可靠、可离线使用的手机应用,支持 Android 和 iOS。用户在无网络时也能记账,网络恢复后自动同步到家庭共享账本。 + +当前阶段:产品边界和技术设计。 + +- [产品边界](docs/产品边界.md) +- [技术方案](docs/技术方案.md) +- [数据与同步](docs/数据与同步.md) +- [测试策略](docs/测试策略.md) +- [内网穿透调试](docs/内网穿透调试.md) +- [HTML 原型图](docs/原型图/原型说明.md) +- [评审问题](docs/评审问题.md) + +## 本地开发 + +```bash +npm install +npm run dev +``` + +前端默认运行在 `http://localhost:5173/`。 + +常用检查: + +```bash +npm run typecheck +npm run build +``` diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..6cf23aa --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,15 @@ + + + + + + + + Cents + + +
+ + + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..dfeaab2 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,25 @@ +{ + "name": "@cents/web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "vue-tsc --noEmit && vite build", + "typecheck": "vue-tsc --noEmit" + }, + "dependencies": { + "@cents/domain": "0.1.0", + "@lucide/vue": "^1.25.0", + "@vitejs/plugin-vue": "^6.0.0", + "dexie": "^4.0.11", + "pinia": "^3.0.3", + "vite": "^7.0.5", + "vue": "^3.5.17", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "typescript": "^5.8.3", + "vue-tsc": "^3.0.1" + } +} diff --git a/apps/web/public/icon.svg b/apps/web/public/icon.svg new file mode 100644 index 0000000..585881e --- /dev/null +++ b/apps/web/public/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/web/public/manifest.webmanifest b/apps/web/public/manifest.webmanifest new file mode 100644 index 0000000..b73dbf8 --- /dev/null +++ b/apps/web/public/manifest.webmanifest @@ -0,0 +1,18 @@ +{ + "name": "Cents", + "short_name": "Cents", + "description": "家庭共享账本", + "start_url": "/", + "display": "standalone", + "background_color": "#f7f4ee", + "theme_color": "#f7f4ee", + "icons": [ + { + "src": "/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} + diff --git a/apps/web/public/sw.js b/apps/web/public/sw.js new file mode 100644 index 0000000..0f3ac0e --- /dev/null +++ b/apps/web/public/sw.js @@ -0,0 +1,25 @@ +const CACHE_NAME = "cents-shell-v2"; +const APP_SHELL = ["/", "/manifest.webmanifest", "/icon.svg"]; + +self.addEventListener("install", (event) => { + event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL))); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))), + ); +}); + +self.addEventListener("fetch", (event) => { + if (event.request.method !== "GET") return; + + event.respondWith( + caches.match(event.request).then((cached) => { + if (cached) return cached; + return fetch(event.request).catch(() => caches.match("/")); + }), + ); +}); diff --git a/apps/web/src/App.vue b/apps/web/src/App.vue new file mode 100644 index 0000000..4be8965 --- /dev/null +++ b/apps/web/src/App.vue @@ -0,0 +1,4 @@ + + diff --git a/apps/web/src/components/QuickEntryDrawer.vue b/apps/web/src/components/QuickEntryDrawer.vue new file mode 100644 index 0000000..acf0890 --- /dev/null +++ b/apps/web/src/components/QuickEntryDrawer.vue @@ -0,0 +1,506 @@ + + + diff --git a/apps/web/src/components/QuickEntryHost.vue b/apps/web/src/components/QuickEntryHost.vue new file mode 100644 index 0000000..4d5544e --- /dev/null +++ b/apps/web/src/components/QuickEntryHost.vue @@ -0,0 +1,76 @@ + + + diff --git a/apps/web/src/data/categories.ts b/apps/web/src/data/categories.ts new file mode 100644 index 0000000..7535772 --- /dev/null +++ b/apps/web/src/data/categories.ts @@ -0,0 +1,115 @@ +import type { EntryType } from "@cents/domain"; +import { + Banknote, + Bike, + BriefcaseBusiness, + BusFront, + CarTaxiFront, + Coffee, + CookingPot, + Gift, + GraduationCap, + HeartPulse, + House, + Plane, + ShoppingBag, + ShoppingBasket, + Smartphone, + Soup, + Ticket, + Utensils, + WalletCards, + type LucideIcon, +} from "@lucide/vue"; + +export type Category = { + id: string; + label: string; + color: string; + tint: string; + icon: LucideIcon; + children?: Category[]; +}; + +export type EntryTypeOption = { + id: EntryType; + label: string; + color: string; + tint: string; + icon: LucideIcon; +}; + +export const entryTypes: EntryTypeOption[] = [ + { id: "expense", label: "支出", color: "#ef5b3f", tint: "#fff0ec", icon: WalletCards }, + { id: "income", label: "收入", color: "#16966f", tint: "#e9faf4", icon: Banknote }, +]; + +export const categories: Record = { + expense: [ + { + id: "food", + label: "餐饮", + color: "#f06a34", + tint: "#fff0e8", + icon: Utensils, + children: [ + { id: "takeout", label: "外卖", color: "#f06a34", tint: "#fff0e8", icon: Soup }, + { id: "restaurant", label: "堂食", color: "#e9572e", tint: "#ffede7", icon: CookingPot }, + { id: "coffee", label: "咖啡", color: "#9b6a48", tint: "#f8eee6", icon: Coffee }, + ], + }, + { + id: "transport", + label: "交通", + color: "#3478e5", + tint: "#edf4ff", + icon: BusFront, + children: [ + { id: "taxi", label: "打车", color: "#3478e5", tint: "#edf4ff", icon: CarTaxiFront }, + { id: "public-transit", label: "公交地铁", color: "#2563c7", tint: "#edf4ff", icon: Ticket }, + { id: "cycling", label: "骑行", color: "#168c87", tint: "#e8f8f6", icon: Bike }, + ], + }, + { + id: "daily", + label: "日用", + color: "#1a9b62", + tint: "#eaf9f1", + icon: ShoppingBasket, + children: [ + { id: "groceries", label: "杂货", color: "#1a9b62", tint: "#eaf9f1", icon: ShoppingBasket }, + { id: "digital", label: "数码", color: "#52606f", tint: "#eff2f5", icon: Smartphone }, + { id: "home", label: "家居", color: "#2b8a74", tint: "#e8f7f2", icon: House }, + ], + }, + { id: "shopping", label: "购物", color: "#d84486", tint: "#fff0f7", icon: ShoppingBag }, + { id: "health", label: "医疗", color: "#df3f5f", tint: "#fff0f2", icon: HeartPulse }, + { id: "travel", label: "旅行", color: "#7559d9", tint: "#f2efff", icon: Plane }, + ], + income: [ + { id: "salary", label: "工资", color: "#15956d", tint: "#e8faf3", icon: BriefcaseBusiness }, + { id: "bonus", label: "奖金", color: "#b57a0e", tint: "#fff7dd", icon: Gift }, + { id: "part-time", label: "兼职", color: "#3577c9", tint: "#edf5ff", icon: GraduationCap }, + { id: "other-income", label: "其他", color: "#8559cf", tint: "#f4efff", icon: Banknote }, + ], +}; + +export function findCategory(categoryId: string): Category | undefined { + for (const group of Object.values(categories)) { + for (const category of group) { + if (category.id === categoryId) return category; + const child = category.children?.find((item) => item.id === categoryId); + if (child) return child; + } + } + return undefined; +} + +export function findCategoryPath(type: EntryType, categoryId: string): Category[] { + for (const category of categories[type]) { + if (category.id === categoryId) return [category]; + const child = category.children?.find((item) => item.id === categoryId); + if (child) return [category, child]; + } + return []; +} diff --git a/apps/web/src/data/db.ts b/apps/web/src/data/db.ts new file mode 100644 index 0000000..5fa1e95 --- /dev/null +++ b/apps/web/src/data/db.ts @@ -0,0 +1,22 @@ +import Dexie, { type EntityTable } from "dexie"; +import type { LedgerEntry, SyncOperation } from "@cents/domain"; +import type { LedgerRecord, UserPreference } from "./ledgers"; + +export const db = new Dexie("cents") as Dexie & { + entries: EntityTable; + syncOperations: EntityTable; + ledgers: EntityTable; + userPreferences: EntityTable; +}; + +db.version(1).stores({ + entries: "id, ledgerId, occurredAt, updatedAt, deletedAt", + syncOperations: "id, ledgerId, createdAt, syncedAt", +}); + +db.version(2).stores({ + entries: "id, ledgerId, occurredAt, updatedAt, deletedAt", + syncOperations: "id, ledgerId, createdAt, syncedAt", + ledgers: "id, updatedAt, archivedAt", + userPreferences: "id, userId, key, updatedAt", +}); diff --git a/apps/web/src/data/entries.ts b/apps/web/src/data/entries.ts new file mode 100644 index 0000000..d995102 --- /dev/null +++ b/apps/web/src/data/entries.ts @@ -0,0 +1,57 @@ +import type { CurrencyCode, EntryType, LedgerEntry } from "@cents/domain"; +import { createId } from "./ids"; + +type EntryInput = { + ledgerId: string; + type: EntryType; + amount: number; + currency: CurrencyCode; + categoryId: string; + note: string; + occurredAt: string; +}; + +const baseCurrency: CurrencyCode = "CNY"; + +export function createEntry(input: EntryInput): LedgerEntry { + const now = new Date().toISOString(); + + return { + id: createId(), + ledgerId: input.ledgerId, + type: input.type, + amount: input.amount, + currency: input.currency, + baseCurrency, + baseAmount: input.amount, + exchangeRate: "1", + exchangeRateSource: "manual", + categoryId: input.categoryId, + note: input.note, + occurredAt: input.occurredAt, + createdBy: "local-user", + updatedBy: "local-user", + createdAt: now, + updatedAt: now, + deletedAt: null, + version: 1, + }; +} + +export function createDemoEntries(): LedgerEntry[] { + const now = new Date(); + const at = (daysAgo: number, hours: number, minutes: number) => { + const date = new Date(now); + date.setDate(date.getDate() - daysAgo); + date.setHours(hours, minutes, 0, 0); + return date.toISOString(); + }; + + return [ + createEntry({ ledgerId: "local-ledger", type: "expense", amount: 5800, currency: "CNY", categoryId: "takeout", note: "午餐", occurredAt: at(0, 12, 28) }), + createEntry({ ledgerId: "local-ledger", type: "expense", amount: 3150, currency: "CNY", categoryId: "taxi", note: "打车", occurredAt: at(0, 9, 16) }), + createEntry({ ledgerId: "local-ledger", type: "income", amount: 824000, currency: "CNY", categoryId: "salary", note: "工资", occurredAt: at(1, 18, 2) }), + createEntry({ ledgerId: "local-ledger", type: "expense", amount: 23680, currency: "CNY", categoryId: "groceries", note: "超市采购", occurredAt: at(1, 16, 45) }), + createEntry({ ledgerId: "local-ledger", type: "expense", amount: 2600, currency: "CNY", categoryId: "coffee", note: "咖啡", occurredAt: at(2, 14, 10) }), + ]; +} diff --git a/apps/web/src/data/ids.ts b/apps/web/src/data/ids.ts new file mode 100644 index 0000000..948d13a --- /dev/null +++ b/apps/web/src/data/ids.ts @@ -0,0 +1,9 @@ +export function createId() { + if (typeof crypto.randomUUID === "function") return crypto.randomUUID(); + + const bytes = crypto.getRandomValues(new Uint8Array(16)); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")); + return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`; +} diff --git a/apps/web/src/data/ledgers.ts b/apps/web/src/data/ledgers.ts new file mode 100644 index 0000000..59dbb6b --- /dev/null +++ b/apps/web/src/data/ledgers.ts @@ -0,0 +1,46 @@ +import type { CurrencyCode } from "@cents/domain"; + +export type LedgerRecord = { + id: string; + name: string; + color: string; + defaultCurrency: CurrencyCode; + createdAt: string; + updatedAt: string; + archivedAt: string | null; +}; + +export type UserPreference = { + id: string; + userId: string; + key: "currentLedgerId"; + value: string; + updatedAt: string; +}; + +export const localUserId = "local-user"; +export const currentLedgerPreferenceId = `${localUserId}:currentLedgerId`; + +export function createDefaultLedgers(): LedgerRecord[] { + const now = new Date().toISOString(); + return [ + { + id: "local-ledger", + name: "家庭日常", + color: "#087f72", + defaultCurrency: "CNY", + createdAt: now, + updatedAt: now, + archivedAt: null, + }, + { + id: "travel-ledger", + name: "旅行账本", + color: "#3478e5", + defaultCurrency: "CNY", + createdAt: now, + updatedAt: now, + archivedAt: null, + }, + ]; +} diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts new file mode 100644 index 0000000..a28cafe --- /dev/null +++ b/apps/web/src/main.ts @@ -0,0 +1,21 @@ +import { createPinia } from "pinia"; +import { createApp } from "vue"; +import App from "./App.vue"; +import { router } from "./router"; +import "./styles.css"; + +const app = createApp(App); + +app.use(createPinia()); +app.use(router); +app.mount("#app"); + +if (import.meta.env.PROD && "serviceWorker" in navigator) { + window.addEventListener("load", () => { + navigator.serviceWorker.register("/sw.js"); + }); +} else if ("serviceWorker" in navigator) { + navigator.serviceWorker.getRegistrations().then((registrations) => { + registrations.forEach((registration) => registration.unregister()); + }); +} diff --git a/apps/web/src/router.ts b/apps/web/src/router.ts new file mode 100644 index 0000000..83a69dc --- /dev/null +++ b/apps/web/src/router.ts @@ -0,0 +1,43 @@ +import { createRouter, createWebHistory } from "vue-router"; +import LedgerView from "./views/LedgerView.vue"; + +export const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: "/", + name: "ledger", + component: LedgerView, + }, + { + path: "/prototype/entry-detail", + name: "entry-detail-prototype", + component: () => import("./views/EntryDetailPrototypeView.vue"), + }, + { + path: "/entries/:id", + name: "entry-detail", + component: () => import("./views/EntryDetailView.vue"), + }, + { + path: "/stats", + name: "stats", + component: () => import("./views/StatsView.vue"), + }, + { + path: "/ledgers", + name: "ledgers", + component: () => import("./views/LedgersView.vue"), + }, + { + path: "/ledger/settings", + name: "ledger-settings", + component: () => import("./views/LedgerSettingsView.vue"), + }, + { + path: "/me", + name: "me", + component: () => import("./views/MeView.vue"), + }, + ], +}); diff --git a/apps/web/src/stores/entries.ts b/apps/web/src/stores/entries.ts new file mode 100644 index 0000000..a59f722 --- /dev/null +++ b/apps/web/src/stores/entries.ts @@ -0,0 +1,100 @@ +import { defineStore } from "pinia"; +import type { LedgerEntry, SyncOperation } from "@cents/domain"; +import { db } from "../data/db"; +import { createDemoEntries } from "../data/entries"; +import { createId } from "../data/ids"; + +export const useEntryStore = defineStore("entries", { + state: () => ({ + entries: [] as LedgerEntry[], + }), + actions: { + async loadEntries() { + if (import.meta.env.DEV && (await db.entries.count()) === 0) { + await db.entries.bulkAdd(createDemoEntries()); + } + this.entries = (await db.entries.orderBy("occurredAt").reverse().toArray()).filter( + (entry) => entry.deletedAt === null, + ); + }, + async addEntry(entry: LedgerEntry) { + const operation: SyncOperation = { + id: createId(), + ledgerId: entry.ledgerId, + entity: "entry", + entityId: entry.id, + action: "create", + payload: entry, + createdAt: new Date().toISOString(), + syncedAt: null, + }; + + await db.transaction("rw", db.entries, db.syncOperations, async () => { + await db.entries.put(entry); + await db.syncOperations.put(operation); + }); + + await this.loadEntries(); + }, + async updateEntry(entry: LedgerEntry) { + const existing = await db.entries.get(entry.id); + if (!existing || existing.deletedAt) throw new Error("Entry not found"); + + const updated: LedgerEntry = { + ...existing, + ...entry, + updatedAt: new Date().toISOString(), + updatedBy: "local-user", + version: existing.version + 1, + }; + const operation: SyncOperation = { + id: createId(), + ledgerId: updated.ledgerId, + entity: "entry", + entityId: updated.id, + action: "update", + payload: updated, + createdAt: updated.updatedAt, + syncedAt: null, + }; + + await db.transaction("rw", db.entries, db.syncOperations, async () => { + await db.entries.put(updated); + await db.syncOperations.put(operation); + }); + + await this.loadEntries(); + return updated; + }, + async deleteEntry(entryId: string) { + const existing = await db.entries.get(entryId); + if (!existing || existing.deletedAt) return; + + const deletedAt = new Date().toISOString(); + const deleted: LedgerEntry = { + ...existing, + deletedAt, + updatedAt: deletedAt, + updatedBy: "local-user", + version: existing.version + 1, + }; + const operation: SyncOperation = { + id: createId(), + ledgerId: deleted.ledgerId, + entity: "entry", + entityId: deleted.id, + action: "delete", + payload: deleted, + createdAt: deletedAt, + syncedAt: null, + }; + + await db.transaction("rw", db.entries, db.syncOperations, async () => { + await db.entries.put(deleted); + await db.syncOperations.put(operation); + }); + + await this.loadEntries(); + }, + }, +}); diff --git a/apps/web/src/stores/ledgers.ts b/apps/web/src/stores/ledgers.ts new file mode 100644 index 0000000..fb2e43d --- /dev/null +++ b/apps/web/src/stores/ledgers.ts @@ -0,0 +1,79 @@ +import { defineStore } from "pinia"; +import { db } from "../data/db"; +import { + createDefaultLedgers, + currentLedgerPreferenceId, + localUserId, + type LedgerRecord, + type UserPreference, +} from "../data/ledgers"; + +let ledgerLoadPromise: Promise | null = null; + +export const useLedgerStore = defineStore("ledgers", { + state: () => ({ + ledgers: [] as LedgerRecord[], + currentLedgerId: "", + loaded: false, + }), + getters: { + currentLedger(state) { + return state.ledgers.find((ledger) => ledger.id === state.currentLedgerId) ?? null; + }, + }, + actions: { + async loadLedgers() { + if (this.loaded) return; + if (ledgerLoadPromise) return ledgerLoadPromise; + ledgerLoadPromise = (async () => { + if ((await db.ledgers.count()) === 0) await db.ledgers.bulkAdd(createDefaultLedgers()); + this.ledgers = (await db.ledgers.orderBy("updatedAt").reverse().toArray()).filter( + (ledger) => ledger.archivedAt === null, + ); + + const preference = await db.userPreferences.get(currentLedgerPreferenceId); + const preferredLedger = this.ledgers.find((ledger) => ledger.id === preference?.value); + const fallbackLedger = this.ledgers.find((ledger) => ledger.id === "local-ledger") ?? this.ledgers[0]; + this.currentLedgerId = preferredLedger?.id ?? fallbackLedger?.id ?? ""; + if (this.currentLedgerId !== preference?.value) await this.persistCurrentLedger(); + this.loaded = true; + })(); + try { + await ledgerLoadPromise; + } finally { + ledgerLoadPromise = null; + } + }, + async setCurrentLedger(ledgerId: string) { + const ledger = this.ledgers.find((item) => item.id === ledgerId && item.archivedAt === null); + if (!ledger) throw new Error("Ledger not found"); + this.currentLedgerId = ledger.id; + await this.persistCurrentLedger(); + }, + async persistCurrentLedger() { + if (!this.currentLedgerId) return; + const preference: UserPreference = { + id: currentLedgerPreferenceId, + userId: localUserId, + key: "currentLedgerId", + value: this.currentLedgerId, + updatedAt: new Date().toISOString(), + }; + await db.userPreferences.put(preference); + }, + async updateLedger(input: Pick) { + const existing = await db.ledgers.get(input.id); + if (!existing) throw new Error("Ledger not found"); + const updated: LedgerRecord = { + ...existing, + ...input, + name: input.name.trim(), + updatedAt: new Date().toISOString(), + }; + await db.ledgers.put(updated); + this.loaded = false; + await this.loadLedgers(); + return updated; + }, + }, +}); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css new file mode 100644 index 0000000..2db8581 --- /dev/null +++ b/apps/web/src/styles.css @@ -0,0 +1,1363 @@ +:root { + color: #17202a; + background: #e8f0ee; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-synthesis: none; + line-height: 1.5; + text-rendering: optimizeLegibility; +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + min-width: 320px; + width: 100%; + height: 100%; + min-height: 100%; + margin: 0; + overflow: hidden; +} + +body { + position: fixed; + inset: 0; + min-height: 0; + overscroll-behavior: none; +} + +button, +input, +select, +textarea { + font: inherit; +} + +button { + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +button:focus-visible, +input:focus-visible { + outline: 3px solid rgba(36, 119, 209, 0.28); + outline-offset: 2px; +} + +.app-shell { + position: relative; + width: min(100%, 480px); + height: 100vh; + height: 100dvh; + min-height: 0; + margin: 0 auto; + overflow: hidden; + overscroll-behavior: none; + background: #f7faf9; + box-shadow: 0 0 40px rgba(25, 53, 48, 0.14); +} + +.ledger-content, +.stats-scroll, +.ledgers-scroll, +.me-scroll, +.entry-detail-scroll, +.settings-form { + overscroll-behavior-y: contain; +} + +.ledger-header { + position: relative; + z-index: 1; + min-height: 210px; + padding: max(18px, env(safe-area-inset-top)) 18px 20px; + background: #087f72; + color: #ffffff; +} + +.ledger-header::after { + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 7px; + background: linear-gradient(90deg, #ffc857 0 32%, #f46b45 32% 66%, #37a8d4 66%); + content: ""; +} + +.top-actions { + display: grid; + grid-template-columns: 56px 1fr 40px; + align-items: center; + gap: 10px; +} + +.participant-avatars { + width: 56px; + height: 40px; + display: flex; + align-items: center; + border: 0; + padding: 0 2px; + background: transparent; +} + +.participant-avatar { + width: 28px; + height: 28px; + display: grid; + flex: 0 0 auto; + place-items: center; + border: 2px solid #087f72; + border-radius: 50%; + color: #ffffff; + font-size: 10px; + font-weight: 760; + box-shadow: 0 3px 8px rgba(15, 52, 46, 0.18); +} + +.participant-avatar + .participant-avatar { + margin-left: -9px; +} + +.participant-avatar.owner, +.share-member-avatar.owner { + background: #f05d3f; +} + +.participant-avatar.member-wang, +.share-member-avatar.member-wang { + background: #3478e5; +} + +.participant-avatar.member-li, +.share-member-avatar.member-li { + background: #7559d9; +} + +.icon-button { + width: 40px; + height: 40px; + display: inline-grid; + flex: 0 0 auto; + place-items: center; + border: 0; + border-radius: 8px; + background: transparent; + color: inherit; +} + +.header-icon { + background: rgba(255, 255, 255, 0.14); +} + +.ledger-more-layer { + position: absolute; + inset: 0; + z-index: 8; +} + +.ledger-more-scrim { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + border: 0; + background: transparent; +} + +.ledger-more-menu { + position: absolute; + top: 52px; + right: 14px; + width: 142px; + overflow: hidden; + border: 1px solid rgba(218, 232, 228, 0.92); + border-radius: 8px; + background: #ffffff; + color: #26342f; + box-shadow: 0 12px 28px rgba(20, 53, 46, 0.24); +} + +.ledger-more-menu button { + width: 100%; + height: 48px; + display: flex; + align-items: center; + gap: 10px; + border: 0; + border-bottom: 1px solid #e7efed; + padding: 0 15px; + background: #ffffff; + color: #31504a; + font-size: 14px; + text-align: left; +} + +.ledger-more-menu button:last-child { + border-bottom: 0; +} + +.ledger-more-menu button:active { + background: #edf7f4; +} + +.ledger-switcher, +.month-switcher { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + border: 0; + background: transparent; + color: inherit; +} + +.ledger-switcher { + min-width: 0; + font-size: 19px; + font-weight: 780; +} + +.month-switcher { + margin-top: 22px; + padding: 0; + color: rgba(255, 255, 255, 0.82); + font-size: 13px; +} + +.monthly-summary { + display: grid; + grid-template-columns: 1.35fr 1fr 1fr; + align-items: end; + gap: 12px; + margin-top: 9px; +} + +.summary-main, +.summary-stat { + min-width: 0; +} + +.monthly-summary span, +.monthly-summary strong { + display: block; + white-space: nowrap; +} + +.monthly-summary span { + margin-bottom: 3px; + color: rgba(255, 255, 255, 0.72); + font-size: 12px; +} + +.summary-main strong { + overflow: hidden; + font-size: 23px; + line-height: 1.25; + text-overflow: ellipsis; +} + +.summary-stat { + border-left: 1px solid rgba(255, 255, 255, 0.22); + padding-left: 12px; +} + +.summary-stat strong { + overflow: hidden; + font-size: 15px; + text-overflow: ellipsis; +} + +.income-stat strong { + color: #d9ffe7; +} + +.expense-stat strong { + color: #ffe6dc; +} + +.ledger-content { + height: calc(100vh - 210px); + height: calc(100dvh - 210px); + min-height: 0; + overflow-y: auto; + padding: 0 16px 112px; + background: #f7faf9; +} + +.list-toolbar { + min-height: 70px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #dce8e4; +} + +.list-toolbar > .list-toolbar-copy { + min-width: 0; + display: grid; + gap: 3px; +} + +.list-toolbar-heading { + display: flex; + align-items: baseline; + gap: 8px; +} + +.list-toolbar strong { + font-size: 15px; +} + +.list-toolbar span { + color: #75817e; + font-size: 12px; +} + +.list-toolbar-totals { + min-width: 0; + display: flex; + gap: 10px; + font-variant-numeric: tabular-nums; +} + +.list-toolbar-totals span { + overflow: hidden; + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.list-toolbar-totals .income, +.day-summary .income { + color: #0d8b67; +} + +.list-toolbar-totals .expense, +.day-summary .expense { + color: #d84c36; +} + +.list-action { + width: 36px; + height: 36px; + color: #41605a; +} + +.day-group { + padding-top: 14px; +} + +.day-group-heading { + min-height: 23px; + display: flex; + align-items: start; + justify-content: space-between; + gap: 10px; +} + +.day-group h2 { + margin: 0 0 5px; + color: #64726f; + font-size: 12px; + font-weight: 680; +} + +.day-summary { + min-width: 0; + display: flex; + justify-content: flex-end; + gap: 10px; + font-size: 11px; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.entry-row { + display: grid; + grid-template-columns: 42px minmax(0, 1fr) auto; + align-items: center; + gap: 11px; + min-height: 66px; + border-bottom: 1px solid #e2ebe8; + color: inherit; + text-decoration: none; +} + +.entry-category-icon { + width: 38px; + height: 38px; + display: grid; + place-items: center; + border-radius: 8px; +} + +.entry-copy, +.entry-value { + min-width: 0; +} + +.entry-copy strong, +.entry-copy span, +.entry-value strong, +.entry-value span { + display: block; +} + +.entry-copy strong { + overflow: hidden; + font-size: 15px; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +.entry-subline { + min-width: 0; + display: flex; + align-items: center; + gap: 7px; + overflow: hidden; +} + +.entry-copy .entry-recorder { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + gap: 3px; + color: #3478e5; + font-weight: 650; +} + +.entry-copy .entry-category-label { + min-width: 0; + overflow: hidden; + color: #7b8683; + text-overflow: ellipsis; + white-space: nowrap; +} + +.entry-copy .entry-category-label::before { + margin-right: 7px; + color: #c4cecb; + content: "·"; +} + +.entry-copy span, +.entry-value span { + margin-top: 2px; + color: #7b8683; + font-size: 12px; +} + +.entry-value { + text-align: right; +} + +.entry-value strong { + font-size: 15px; + font-variant-numeric: tabular-nums; +} + +.entry-value .expense { + color: #d84c36; +} + +.entry-value .income { + color: #0d8b67; +} + +.empty-state { + min-height: 260px; + display: grid; + place-items: center; + align-content: center; + gap: 6px; + color: #7b8683; +} + +.empty-state strong { + color: #374541; +} + +.empty-state span { + font-size: 13px; +} + +.load-more-sentinel { + height: 52px; + display: flex; + align-items: center; + justify-content: center; + gap: 7px; + color: #71807c; + font-size: 12px; +} + +.load-more-sentinel svg { + animation: load-more-spin 900ms linear infinite; +} + +@keyframes load-more-spin { + to { transform: rotate(360deg); } +} + +.bottom-nav { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 4; + height: calc(68px + env(safe-area-inset-bottom)); + display: grid; + grid-template-columns: repeat(5, 1fr); + align-items: start; + border-top: 1px solid #d8e5e1; + padding: 7px 8px env(safe-area-inset-bottom); + background: rgba(255, 255, 255, 0.96); + backdrop-filter: blur(14px); +} + +.bottom-nav a { + min-width: 0; + height: 50px; + display: grid; + place-items: center; + align-content: center; + gap: 2px; + border: 0; + background: transparent; + color: #84908d; + text-decoration: none; +} + +.bottom-nav a.active { + color: #087f72; +} + +.bottom-nav span { + font-size: 10px; +} + +.add-entry-button { + position: absolute; + right: 50%; + bottom: calc(31px + env(safe-area-inset-bottom)); + z-index: 5; + width: 56px; + height: 56px; + display: grid; + place-items: center; + transform: translate(50%, 50%); + border: 5px solid #ffffff; + border-radius: 50%; + background: #f05d3f; + color: #ffffff; + box-shadow: 0 8px 18px rgba(202, 66, 40, 0.28); +} + +.drawer-scrim { + position: absolute; + inset: 0; + z-index: 10; + background: rgba(18, 35, 32, 0.36); + backdrop-filter: blur(1px); +} + +.drawer-scrim.category-mode { + background: rgba(18, 35, 32, 0.42); + backdrop-filter: blur(7px); + -webkit-backdrop-filter: blur(7px); +} + +.quick-drawer { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 11; + border-radius: 16px 16px 0 0; + padding-bottom: env(safe-area-inset-bottom); + background: #ffffff; + box-shadow: 0 -16px 36px rgba(22, 45, 40, 0.22); +} + +.drawer-handle { + width: 38px; + height: 4px; + margin: 8px auto 0; + border-radius: 2px; + background: #b9cac5; +} + +.drawer-header { + min-height: 76px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 7px 14px 5px 18px; +} + +.amount-block { + min-width: 0; + display: grid; + gap: 3px; +} + +.quick-ledger-target { + position: relative; + width: max-content; + max-width: 190px; + min-height: 23px; + display: inline-flex; + align-items: center; + gap: 4px; + overflow: hidden; + border-radius: 6px; + padding: 2px 5px; + background: #eaf6f3; + color: #087f72; + font-size: 11px; + font-weight: 680; +} + +.quick-ledger-target span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.quick-ledger-target select { + position: absolute; + inset: 0; + width: 100%; + opacity: 0; + cursor: pointer; +} + +.amount-result { + min-width: 0; + display: flex; + align-items: baseline; + gap: 5px; +} + +.amount-prefix { + color: #70807c; + font-size: 20px; + font-weight: 650; +} + +.amount-value { + overflow: hidden; + color: #172421; + font-size: clamp(34px, 10vw, 44px); + font-variant-numeric: tabular-nums; + line-height: 1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.calculation-line { + max-width: min(72vw, 330px); + overflow: hidden; + color: #75817e; + font-size: 12px; + font-variant-numeric: tabular-nums; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.entry-meta-row { + height: 56px; + display: grid; + grid-template-columns: minmax(90px, 1fr) auto; + align-items: center; + gap: 8px; + border-top: 1px solid #e4ece9; + padding: 3px 16px 5px; +} + +.header-date-button { + flex: 0 0 auto; + min-width: max-content; + min-height: 34px; + display: inline-flex; + align-items: center; + gap: 5px; + border: 1px solid #f3cf78; + border-radius: 8px; + padding: 0 9px; + background: #fff8df; + color: #8a620d; + font-size: 12px; + font-weight: 680; + white-space: nowrap; +} + +.native-date-input { + position: absolute; + right: 14px; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +} + +.note-field { + min-width: 0; + display: block; +} + +.note-field input { + width: 100%; + height: 32px; + border: 0; + border-bottom: 1px solid #dce8e4; + border-radius: 0; + padding: 0 3px; + background: transparent; + color: #27332f; + font-size: 13px; +} + +.note-field input::placeholder { + color: #9aa6a3; +} + +.note-field.editing input { + border-bottom-color: #78bdb2; +} + +.keypad { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-rows: repeat(4, 58px); + border-top: 1px solid #dbe7e3; + isolation: isolate; +} + +.key { + min-width: 0; + height: 100%; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + border: 0; + border-right: 1px solid #e2ebe8; + border-bottom: 1px solid #e2ebe8; + background: #ffffff; + color: #26332f; +} + +.key:nth-last-child(-n + 4) { + border-bottom: 0; +} + +.number-key { + font-size: 22px; + font-weight: 560; +} + +.operator-key, +.utility-key { + background: #f1f6f4; + color: #3f5c55; + font-size: 23px; +} + +.operator-key.active { + background: #fff3cd; + color: #9b6700; +} + +.category-selector-anchor { + position: relative; + z-index: 3; + min-width: 0; +} + +.meta-category-button { + position: relative; + z-index: 3; + max-width: min(190px, 48vw); + min-height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--category-color) 30%, white); + border-radius: 8px; + padding: 0 9px; + background: color-mix(in srgb, var(--category-color) 11%, white); + color: var(--category-color); + font-size: 12px; + font-weight: 750; +} + +.category-selector-anchor.selector-open { + z-index: 7; +} + +.meta-category-button.active { + z-index: 7; + width: 72px; + min-width: 72px; + height: 72px; + min-height: 72px; + flex-direction: column; + gap: 0; + border: 1px solid color-mix(in srgb, var(--category-color) 35%, white); + border-radius: 10px; + padding: 0; + background: color-mix(in srgb, var(--category-color) 13%, white); + box-shadow: 0 10px 24px rgba(29, 57, 50, 0.22); + isolation: isolate; +} + +.meta-category-button.active > * { + position: relative; + z-index: 2; +} + +.meta-category-button.active span { + font-size: 13px; + line-height: 1.1; +} + +.meta-category-button span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.save-key { + grid-column: 4; + grid-row: 3 / span 2; + border-right: 0; + background: #f05d3f; + color: #ffffff; +} + +.category-focus-scrim { + position: absolute; + inset: 0; + z-index: 4; + background: rgba(19, 38, 34, 0.24); + backdrop-filter: blur(7px); + -webkit-backdrop-filter: blur(7px); +} + +.category-coordinate { + position: absolute; + top: 0; + right: 0; + z-index: 6; + width: 72px; + height: 72px; + overflow: visible; + pointer-events: none; +} + +.selector-path { + position: absolute; + top: -2px; + right: 100%; + width: min(392px, calc(100vw - 88px)); + height: 76px; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + overflow: visible; + border: 0; + padding: 2px 6px; + background: transparent; + pointer-events: auto; +} + +.selector-path > button { + width: 72px; + min-width: 72px; + height: 72px; + display: inline-flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0; + overflow: hidden; + border: 2px solid color-mix(in srgb, var(--path-color) 72%, #263b36); + border-radius: 50%; + padding: 0; + background: var(--path-color); + color: #ffffff; + font-weight: 740; + white-space: nowrap; + box-shadow: 0 9px 22px rgba(29, 57, 50, 0.2); +} + +.selector-path > button:active, +.selector-path > button:focus-visible { + background: color-mix(in srgb, var(--path-color) 82%, #213832); + transform: translateY(1px); +} + +.selector-path > button svg { + flex: 0 0 auto; +} + +.selector-path small { + max-width: 62px; + overflow: hidden; + font-size: 13px; + line-height: 1.1; + text-overflow: ellipsis; +} + +.selector-path > svg { + flex: 0 0 auto; + color: #8ca09b; +} + +.candidate-above-axis, +.candidate-below-axis { + position: absolute; + right: -24px; + width: 120px; + background: transparent; + pointer-events: auto; +} + +.candidate-above-axis { + bottom: 60px; + overflow: hidden; + mask-image: linear-gradient( + to bottom, + transparent 0, + #000 30px, + #000 calc(100% - 34px), + transparent calc(100% - 16px), + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to bottom, + transparent 0, + #000 30px, + #000 calc(100% - 34px), + transparent calc(100% - 16px), + transparent 100% + ); +} + +.candidate-below-axis { + top: 70px; + height: 208px; + overflow: hidden; + mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 30px), transparent 100%); + -webkit-mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 30px), transparent 100%); +} + +.candidate-axis-viewport { + width: 100%; + height: 100%; + overflow: hidden; + touch-action: none; + user-select: none; + cursor: grab; +} + +.candidate-axis-viewport:active { + cursor: grabbing; +} + +.candidate-axis-list { + width: 120px; + min-height: max-content; + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px 24px 240px; +} + +.coordinate-option { + width: 72px; + min-width: 72px; + height: 72px; + min-height: 72px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0; + border: 2px solid color-mix(in srgb, var(--option-color) 50%, white); + border-radius: 50%; + padding: 0; + background: color-mix(in srgb, var(--option-tint) 74%, white); + color: var(--option-color); + font-size: 13px; + line-height: 1.05; + font-weight: 730; + white-space: nowrap; + box-shadow: 0 9px 22px rgba(29, 57, 50, 0.2); +} + +.coordinate-option.selected { + border-color: color-mix(in srgb, var(--option-color) 75%, #263b36); + background: var(--option-color); + color: #ffffff; + box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.78), 0 11px 25px rgba(29, 57, 50, 0.25); +} + +.coordinate-option.selected .coordinate-icon { + color: #ffffff; +} + +.coordinate-icon { + width: 28px; + height: 28px; + flex: 0 0 auto; + display: grid; + place-items: center; + color: var(--option-color); +} + +.coordinate-option > span:last-child { + max-width: 62px; + overflow: hidden; + text-overflow: ellipsis; +} + +.share-sheet-layer { + position: absolute; + inset: 0; + z-index: 16; +} + +.share-sheet-scrim { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + border: 0; + background: rgba(18, 35, 32, 0.4); + backdrop-filter: blur(3px); +} + +.ledger-share-sheet { + position: absolute; + right: 0; + bottom: 0; + left: 0; + border-radius: 14px 14px 0 0; + padding: 0 16px calc(18px + env(safe-area-inset-bottom)); + background: #ffffff; + color: #22302c; + box-shadow: 0 -16px 36px rgba(22, 45, 40, 0.22); +} + +.share-sheet-handle { + width: 38px; + height: 4px; + margin: 8px auto 3px; + border-radius: 2px; + background: #b9cac5; +} + +.ledger-share-sheet > header { + min-height: 58px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.ledger-share-sheet > header > div { + display: grid; + gap: 1px; +} + +.ledger-share-sheet > header strong { + font-size: 16px; +} + +.ledger-share-sheet > header span { + color: #7a8783; + font-size: 11px; +} + +.ledger-share-sheet > header button { + width: 38px; + height: 38px; + display: grid; + place-items: center; + border: 0; + border-radius: 8px; + background: #f0f5f3; + color: #536762; +} + +.share-member-list { + border-top: 1px solid #e1ebe8; + border-bottom: 1px solid #e1ebe8; +} + +.share-member-row { + min-height: 54px; + display: grid; + grid-template-columns: 36px minmax(0, 1fr); + align-items: center; + gap: 10px; + border-bottom: 1px solid #edf2f0; +} + +.share-member-row:last-child { + border-bottom: 0; +} + +.share-member-avatar { + width: 34px; + height: 34px; + display: grid; + place-items: center; + border-radius: 50%; + color: #ffffff; + font-size: 12px; + font-weight: 760; +} + +.share-member-row > span:last-child { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; +} + +.share-member-row strong { + font-size: 14px; +} + +.share-member-row small { + color: #7b8884; + font-size: 11px; +} + +.share-actions { + display: grid; + grid-template-columns: 1fr 1.35fr; + gap: 10px; + margin-top: 14px; +} + +.share-actions button { + height: 44px; + display: flex; + align-items: center; + justify-content: center; + gap: 7px; + border-radius: 8px; + font-size: 14px; + font-weight: 700; +} + +.copy-invite-action { + border: 1px solid #cbdad6; + background: #ffffff; + color: #48605a; +} + +.system-share-action { + border: 0; + background: #087f72; + color: #ffffff; +} + +.share-feedback { + min-height: 18px; + margin: 8px 0 0; + color: #7b8884; + text-align: center; + font-size: 11px; +} + +.participant-sheet { + padding-bottom: calc(24px + env(safe-area-inset-bottom)); +} + +.compact-share-sheet { + padding-bottom: calc(16px + env(safe-area-inset-bottom)); +} + + +.saved-toast { + position: absolute; + right: 50%; + bottom: 92px; + z-index: 20; + min-height: 38px; + display: inline-flex; + align-items: center; + gap: 7px; + transform: translateX(50%); + border-radius: 8px; + padding: 0 14px; + background: #173c35; + color: #ffffff; + box-shadow: 0 10px 24px rgba(21, 50, 44, 0.2); + font-size: 13px; + white-space: nowrap; +} + +.drawer-enter-active, +.drawer-leave-active { + transition: opacity 180ms ease; +} + +.drawer-enter-active .quick-drawer, +.drawer-leave-active .quick-drawer { + transition: transform 220ms ease; +} + +.drawer-enter-from, +.drawer-leave-to { + opacity: 0; +} + +.drawer-enter-from .quick-drawer, +.drawer-leave-to .quick-drawer { + transform: translateY(100%); +} + +.coordinate-selector-enter-active, +.coordinate-selector-leave-active { + transform-origin: 100% 0; + transition: opacity 130ms ease, transform 170ms ease; +} + +.coordinate-selector-enter-from, +.coordinate-selector-leave-to { + opacity: 0; + transform: scale(0.94); +} + +.toast-enter-active, +.toast-leave-active { + transition: opacity 160ms ease, transform 160ms ease; +} + +.toast-enter-from, +.toast-leave-to { + opacity: 0; + transform: translate(50%, 8px); +} + +.share-sheet-enter-active, +.share-sheet-leave-active { + transition: opacity 180ms ease; +} + +.share-sheet-enter-active .ledger-share-sheet, +.share-sheet-leave-active .ledger-share-sheet { + transition: transform 220ms ease; +} + +.share-sheet-enter-from, +.share-sheet-leave-to { + opacity: 0; +} + +.share-sheet-enter-from .ledger-share-sheet, +.share-sheet-leave-to .ledger-share-sheet { + transform: translateY(100%); +} + +.more-menu-enter-active, +.more-menu-leave-active { + transition: opacity 120ms ease; +} + +.more-menu-enter-from, +.more-menu-leave-to { + opacity: 0; +} + +@media (max-width: 360px) { + .ledger-header { + padding-right: 14px; + padding-left: 14px; + } + + .monthly-summary { + gap: 8px; + } + + .summary-main strong { + font-size: 20px; + } + + .summary-stat { + padding-left: 8px; + } + + .entry-meta-row { + grid-template-columns: minmax(76px, 1fr) auto; + gap: 6px; + padding-right: 12px; + padding-left: 12px; + } + + .meta-category-button { + max-width: 48vw; + } + + .header-date-button { + padding: 0 7px; + } + + .ledger-content { + padding-right: 12px; + padding-left: 12px; + } + + .day-summary { + gap: 7px; + font-size: 10px; + } + + .list-toolbar-totals { + gap: 7px; + } +} + +@media (min-width: 700px) { + body { + padding: 16px 0; + } + + .app-shell { + height: calc(100vh - 32px); + min-height: 620px; + border-radius: 18px; + } + + .ledger-content { + height: calc(100% - 210px); + min-height: 0; + } + + .bottom-nav, + .quick-drawer, + .drawer-scrim { + position: absolute; + } +} diff --git a/apps/web/src/views/EntryDetailPrototypeView.vue b/apps/web/src/views/EntryDetailPrototypeView.vue new file mode 100644 index 0000000..709824c --- /dev/null +++ b/apps/web/src/views/EntryDetailPrototypeView.vue @@ -0,0 +1,921 @@ + + + + + diff --git a/apps/web/src/views/EntryDetailView.vue b/apps/web/src/views/EntryDetailView.vue new file mode 100644 index 0000000..c367955 --- /dev/null +++ b/apps/web/src/views/EntryDetailView.vue @@ -0,0 +1,853 @@ + + + + + diff --git a/apps/web/src/views/LedgerSettingsView.vue b/apps/web/src/views/LedgerSettingsView.vue new file mode 100644 index 0000000..c65c662 --- /dev/null +++ b/apps/web/src/views/LedgerSettingsView.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/apps/web/src/views/LedgerView.vue b/apps/web/src/views/LedgerView.vue new file mode 100644 index 0000000..e2cd836 --- /dev/null +++ b/apps/web/src/views/LedgerView.vue @@ -0,0 +1,379 @@ + + + diff --git a/apps/web/src/views/LedgersView.vue b/apps/web/src/views/LedgersView.vue new file mode 100644 index 0000000..2b4bf1c --- /dev/null +++ b/apps/web/src/views/LedgersView.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/apps/web/src/views/MeView.vue b/apps/web/src/views/MeView.vue new file mode 100644 index 0000000..dcd81ff --- /dev/null +++ b/apps/web/src/views/MeView.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/apps/web/src/views/StatsView.vue b/apps/web/src/views/StatsView.vue new file mode 100644 index 0000000..7b49d3d --- /dev/null +++ b/apps/web/src/views/StatsView.vue @@ -0,0 +1,211 @@ + + + + + diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..de37ae9 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "preserve", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"] +} + diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..d6586b2 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,9 @@ +import vue from "@vitejs/plugin-vue"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [vue()], + server: { + allowedHosts: ["dev.cents.homemade.net.cn"], + }, +}); diff --git a/compose.dev.yaml b/compose.dev.yaml new file mode 100644 index 0000000..b26d136 --- /dev/null +++ b/compose.dev.yaml @@ -0,0 +1,34 @@ +services: + frpc: + image: homemade/auto-cert/proxy:latest + restart: unless-stopped + environment: + DOMAIN: dev.cents.homemade.net.cn + APP_HOST: host.docker.internal + APP_PORT: "6064" + APP_PROTOCOL: http + FRPC_SERVER_HOST: 119.28.12.24 + FRPC_SERVER_PORT: "7000" + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - frp_cert_volume:/etc/letsencrypt + networks: + - frp_dev + + cert: + image: homemade/auto-cert/cert:latest + restart: "no" + command: --domain dev.cents.homemade.net.cn --test-mode false + volumes: + - frp_cert_volume:/etc/letsencrypt + networks: + - frp_dev + +volumes: + frp_cert_volume: + +networks: + frp_dev: + driver: bridge + diff --git a/compose.frp-prod.yml b/compose.frp-prod.yml new file mode 100644 index 0000000..3328749 --- /dev/null +++ b/compose.frp-prod.yml @@ -0,0 +1,34 @@ +services: + frpc: + image: homemade/auto-cert/proxy:latest + restart: unless-stopped + environment: + DOMAIN: ${DOMAIN} + APP_HOST: ${APP_HOST} + APP_PORT: ${APP_PORT} + APP_PROTOCOL: ${APP_PROTOCOL} + FRPC_SERVER_HOST: ${FRPC_SERVER_HOST} + FRPC_SERVER_PORT: ${FRPC_SERVER_PORT} + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - frp_cert_volume:/etc/letsencrypt + networks: + - frp_prod + + cert: + image: homemade/auto-cert/cert:latest + restart: "no" + command: --domain ${DOMAIN} --test-mode ${TEST_MODE} + volumes: + - frp_cert_volume:/etc/letsencrypt + networks: + - frp_prod + +volumes: + frp_cert_volume: + +networks: + frp_prod: + driver: bridge + diff --git a/deploy/frp/frps.example.toml b/deploy/frp/frps.example.toml new file mode 100644 index 0000000..4d24540 --- /dev/null +++ b/deploy/frp/frps.example.toml @@ -0,0 +1,9 @@ +# Copy this to the public VPS and adapt it for frps. +bindPort = 7000 +vhostHTTPPort = 80 +vhostHTTPSPort = 443 + +# If your frps requires authentication, configure it on both frps and frpc. +# auth.method = "token" +# auth.token = "replace-with-a-secret-token" + diff --git a/docs/二期-报销与退款.md b/docs/二期-报销与退款.md new file mode 100644 index 0000000..73acce2 --- /dev/null +++ b/docs/二期-报销与退款.md @@ -0,0 +1,228 @@ +# 二期:报销与退款 + +## 背景 + +家庭账本里有一类收入并不代表真实新增收入,而是对过去支出的抵消,例如: + +- 公司报销餐费、交通费、差旅费。 +- 商家退款、退货退款、平台补贴返还。 +- AA 或代付后,其他成员转回款项。 + +首版只记录普通收入和支出。二期需要支持“收入关联原支出”,让流水、统计和净支出更接近真实消费。 + +## 目标 + +- 报销和退款在录入时仍作为收入类型保存。 +- 输入金额后,系统可以推荐匹配的历史支出。 +- 用户可以把一笔报销或退款关联到一笔或多笔支出。 +- 流水中能看出某笔收入是报销或退款,以及它关联了哪些支出。 +- 月度统计能区分总支出、报销/退款抵扣、净支出。 + +## 非目标 + +- 不做企业报销流程审批。 +- 不做发票、附件、小票 OCR。 +- 不做复杂应收应付和债务结算。 +- 不自动修改原支出金额。 +- 不要求系统自动判断 100% 正确,必须允许用户手动选择或取消关联。 + +## 概念定义 + +### 普通收入 + +真实增加可支配金额的收入,例如工资、奖金、利息。 + +### 报销 + +对已发生支出的补偿。通常来自公司、组织或共同生活成员。 + +示例:用户先记一笔 `交通 120 元`,之后收到公司报销 `120 元`。 + +### 退款 + +商家、平台或交易对方退回的金额。通常对应购物、服务、押金等历史支出。 + +示例:用户先记一笔 `购物 299 元`,之后退货收到 `299 元`。 + +### 关联支出 + +被报销或退款收入抵消的原始支出记录。 + +## 录入流程 + +### 快速记账入口 + +收入类型下增加收入子类型: + +- 普通收入 +- 报销 +- 退款 + +用户选择 `报销` 或 `退款` 后: + +1. 输入收入金额。 +2. 系统根据金额、时间、分类、备注、付款人推荐历史支出。 +3. 用户选择一笔或多笔支出。 +4. 用户确认保存。 +5. 新收入记录保存,并建立与原支出的关联。 + +### 推荐匹配 + +默认推荐范围: + +- 同一账本内。 +- 未删除支出。 +- 发生时间早于或等于当前报销/退款时间。 +- 最近 180 天内优先。 +- 金额相同或接近优先。 + +推荐排序建议: + +1. 金额完全相同。 +2. 金额差额较小。 +3. 日期更近。 +4. 分类更相关。 +5. 备注文本有相似词。 +6. 同一付款人。 + +### 手动关联 + +用户必须能: + +- 搜索历史支出。 +- 改选推荐结果。 +- 选择多笔支出。 +- 不关联任何支出,先保存为未关联报销/退款。 +- 保存后再补充或修改关联。 + +## 金额规则 + +一笔报销或退款可以关联: + +- 一笔支出。 +- 多笔支出。 +- 一笔支出的一部分金额。 + +关联金额需要单独记录,不能只靠收入金额和支出金额推导。 + +示例: + +- 支出 `餐饮 100 元`。 +- 报销 `80 元`。 +- 关联金额为 `80 元`,原支出仍显示 `100 元`。 + +多笔关联示例: + +- 支出 A `交通 60 元`。 +- 支出 B `餐饮 40 元`。 +- 报销 `100 元`。 +- 报销记录同时关联 A 和 B。 + +## 统计规则 + +二期统计至少区分: + +- 总收入:包含普通收入、报销、退款。 +- 普通收入:只包含真实收入。 +- 总支出:原始支出合计,不被报销/退款改写。 +- 报销/退款抵扣:已关联到支出的报销和退款金额。 +- 净支出:总支出减去报销/退款抵扣。 + +首选展示方式: + +- 流水仍显示原始收入和支出,保持账目真实发生。 +- 汇总页突出 `净支出`。 +- 报销和退款作为收入展示,但使用不同标签,避免被误认为工资等普通收入。 + +## 流水展示 + +报销/退款收入行应显示: + +- 金额为正数。 +- 类型标签:报销或退款。 +- 关联状态:已关联、部分关联、未关联。 +- 关联的原支出摘要。 + +原支出行应显示: + +- 原始支出金额不变。 +- 若已有报销/退款抵扣,显示抵扣金额。 +- 可进入详情查看关联收入。 + +状态建议: + +- `未关联`:没有任何关联支出。 +- `部分抵扣`:关联金额小于原支出金额。 +- `已抵扣`:关联金额大于或等于原支出金额。 + +## 数据模型建议 + +### entry 扩展 + +在 `entry` 上增加收入子类型字段: + +- `income_kind`:`regular`、`reimbursement`、`refund`。 + +约束: + +- `type = income` 时可填写 `income_kind`。 +- `type = expense` 时 `income_kind` 为空。 +- 兼容旧数据时,收入默认视为 `regular`。 + +### entry_link + +新增关联表: + +- `id`:客户端生成 UUID。 +- `ledger_id` +- `source_entry_id`:报销/退款收入。 +- `target_entry_id`:被关联支出。 +- `link_type`:`reimbursement` 或 `refund`。 +- `amount`:本次关联金额,整数分。 +- `created_by` +- `created_at` +- `updated_at` +- `deleted_at` +- `version` + +约束: + +- `source_entry_id` 必须指向收入。 +- `target_entry_id` 必须指向支出。 +- 两条账目必须属于同一账本。 +- 关联金额必须大于 0。 +- 同一收入关联多笔支出的金额合计不应超过收入金额,除非后续明确支持超额报销。 + +## 同步与离线 + +报销/退款关联也必须离线可用: + +- 新增、修改、删除关联写入本地 IndexedDB。 +- 每次关联变更生成同步操作。 +- 服务端按 `operation_id` 幂等处理。 +- 删除账目时不硬删除关联,使用软删除。 + +冲突策略二期先保持简单: + +- 同一条关联被多设备修改时,最后写入胜出。 +- 如果原支出或收入已删除,关联保留软删除或标记失效。 +- 汇总统计只计算未删除且有效的关联。 + +## 原型需求 + +需要补充这些原型: + +- 快速记账收入模式下的 `普通收入 / 报销 / 退款` 切换。 +- 输入金额后的候选支出匹配列表。 +- 手动搜索并选择历史支出。 +- 一笔收入关联多笔支出的选择态。 +- 流水中报销/退款和原支出的关联展示。 +- 账目详情中的关联管理。 + +## 待决策问题 + +- 报销和退款是否共用同一套分类,还是独立于普通收入分类。 +- 未关联的报销/退款是否计入净支出抵扣。 +- 关联金额是否允许超过原支出剩余未抵扣金额。 +- AA 或代付回款是否归入报销,还是后续独立成“代付结算”。 +- 统计页默认展示总支出还是净支出。 diff --git a/docs/产品边界.md b/docs/产品边界.md new file mode 100644 index 0000000..0082950 --- /dev/null +++ b/docs/产品边界.md @@ -0,0 +1,70 @@ +# 产品边界 + +## 定位 + +家庭账本。重点是快速记账、账本权限、离线可用、恢复网络后同步。 + +## 用户 + +- 家庭、伴侣、合租、小型共同开支群体。 +- 默认 2-8 人。 +- 不面向大规模公开推广。 + +## 首版必须有 + +### 账号与访问 + +- 不开放自助注册。 +- 后台分配用户名密码。 +- 设备 ID 审批作为备选方案。 +- 用户只能访问自己有权限的账本。 + +### 账本 + +- 创建账本。 +- 查看账本流水。 +- 账本可以独享。 +- 账本可以邀请成员共享。 +- 账本成员默认共享该账本的全量数据。 +- 首版权限只区分 `owner` 和 `member`。 + +### 邀请 + +- 账本 owner 可以邀请成员。 +- 邀请可以通过链接或邀请码完成。 +- 邀请需要过期时间。 +- 被邀请用户加入后才能看到该账本。 + +### 记账 + +- 记录收入。 +- 记录支出。 +- 字段:金额、分类、日期、备注、付款人。 +- 只记录流水,不做分摊和结算。 + +### 离线 + +- 无网络时可以新增、编辑、删除账目。 +- 网络恢复后自动同步。 +- 多设备同步有权限访问的账本。 + +### 汇总 + +- 按月查看流水。 +- 按月查看分类汇总。 + +## 首版暂缓 + +- 外币记账。 +- 汇率换算。 +- 预算。 +- 资产账户余额。 +- 转账。 +- 周期记账。 +- 附件和图片小票。 +- OCR、语音记账、AI 分类。 +- 复杂成员权限。 +- 导入导出。 +- 专门的桌面大屏布局。 +- 分摊和结算。 +- 报销、退款和收入关联原支出,见 [二期:报销与退款](二期-报销与退款.md)。 diff --git a/docs/内网穿透调试.md b/docs/内网穿透调试.md new file mode 100644 index 0000000..7070c73 --- /dev/null +++ b/docs/内网穿透调试.md @@ -0,0 +1,87 @@ +# 内网穿透调试 + +## 目标 + +先用调试模式验证 PWA 可以通过公网域名访问。 + +模式: + +- VPS 运行 `frps`。 +- 本机运行 Vite 开发服务。 +- 本机运行 `frpc` 和 `cert` sidecar。 +- `cert` 使用 Let's Encrypt 正式证书。 +- `frpc` 通过 HTTPS 转发到本机 `6064`。 + +## 前置条件 + +- 有一个域名,例如 `cents.example.com`。 +- 域名 A/AAAA 记录指向 VPS。 +- VPS 开放 `80`、`443` 和 `7000`。 +- VPS 的 `frps` 配置了 HTTP/HTTPS vhost 端口。 + +参考配置: + +- `deploy/frp/frps.example.toml` + +## 本机配置 + +调试模式不读取 `.env`,配置直接写在 `compose.dev.yaml`: + +- `DOMAIN=dev.cents.homemade.net.cn` +- `FRPC_SERVER_HOST=119.28.12.24` +- `FRPC_SERVER_PORT=7000` +- `APP_HOST=host.docker.internal` +- `APP_PORT=6064` +- `APP_PROTOCOL=http` +- `TEST_MODE=false` + +## 启动 + +先启动前端: + +```bash +npm run dev --workspace @cents/web -- --port 6064 +``` + +再启动内网穿透: + +```bash +docker compose -f compose.dev.yaml up -d +``` + +查看日志: + +```bash +docker compose -f compose.dev.yaml logs -f frpc cert +``` + +## 验证 + +```bash +curl -I http://$DOMAIN/.well-known/ +curl -vkI https://$DOMAIN +``` + +预期: + +- `cert` 能完成 HTTP-01 校验。 +- `/etc/letsencrypt/live/dev.cents.homemade.net.cn/` 中生成正式证书。 +- 浏览器可以通过 `https://$DOMAIN` 访问本地 PWA。 + +## 生产模式 + +`.env` 留给生产模式使用: + +```bash +cp .env.example .env +docker compose -f compose.frp-prod.yml up -d +``` + +不要提交真实 `.env`。 + +## 常见问题 + +- 证书申请失败:检查域名 DNS、VPS 80 端口、frps vhost HTTP 配置。 +- HTTPS 访问失败:检查 frps 443 端口和 frpc 日志。 +- 后端连不上:确认本机 Vite 服务正在监听 `6064`。 +- frps 有 token:需要同步配置 frps 和 frpc,不要把真实 token 提交到仓库。 diff --git a/docs/原型图/分类轮盘步骤.html b/docs/原型图/分类轮盘步骤.html new file mode 100644 index 0000000..4128b78 --- /dev/null +++ b/docs/原型图/分类轮盘步骤.html @@ -0,0 +1,175 @@ + + + + + + 分类轮盘步骤示意 + + + +
+
+

分类轮盘步骤示意

+

快速记账在账本流水页底部抽屉内完成。轮盘是临时选择态,不能把所有层级同时放在一个画面里。

+
+ +
+
+
+ 1 + 抽屉默认态 +
+
+ +
+ 58.00 +
+
+
+ 支出 + 餐饮 + 外卖 +
+ +
+
+ +
+
+ 789支出 + 456+ + 123- + .0 +
+
+
+ +
+
+ 2 + 点击支出按钮 +
+
+
+ 58.00 +
+
+
+ 支出 +
+ +
+
+
+ + + +
+
+
+ 789 + 456+ + 123- + .0 +
+
+
+ +
+
+ 3 + 选择支出后 +
+
+
+ 58.00 +
+
+
+ 支出 +
+ +
+
+
+ + + + + +
+
+
+ 789 + 456+ + 123- + .0 +
+
+
+ +
+
+ 4 + 进入子分类 +
+
+
+ 58.00 +
+
+
+ 支出 + 餐饮 +
+ +
+
+
+ + + + +
+
+
+ 789 + 456+ + 123- + .0 +
+
+
+ +
+
+ 5 + 确认当前层级 +
+
+
+ 58.00 +
+
+
+ 支出 + 餐饮 + 外卖 +
+ +
+
+ +
+
+ 789外卖 + 456+ + 123- + .0 +
+
+
+
+
+ + diff --git a/docs/原型图/原型样式.css b/docs/原型图/原型样式.css new file mode 100644 index 0000000..1d88f42 --- /dev/null +++ b/docs/原型图/原型样式.css @@ -0,0 +1,835 @@ +:root { + color: #101827; + background: + radial-gradient(circle at 12% 0%, rgba(255, 122, 89, 0.28), transparent 30%), + radial-gradient(circle at 92% 8%, rgba(45, 212, 191, 0.24), transparent 32%), + linear-gradient(135deg, #fff3e8 0%, #ecfeff 48%, #fff1f8 100%); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + line-height: 1.5; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; +} + +button, +input { + font: inherit; +} + +.stage { + min-height: 100vh; + display: grid; + place-items: start center; + padding: 24px; +} + +.phone { + width: min(100%, 390px); + height: 780px; + min-height: 780px; + overflow: hidden; + border: 1px solid rgba(20, 83, 45, 0.12); + border-radius: 28px; + background: #ffffff; + box-shadow: 0 24px 58px rgba(15, 23, 42, 0.14); +} + +.appbar { + background: linear-gradient(135deg, #00a676 0%, #00b4d8 52%, #ff7a59 100%); + color: #fff; + padding: 18px 18px 14px; +} + +.toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.title { + margin: 0; + font-size: 20px; + font-weight: 750; + letter-spacing: 0; +} + +.icon-button { + width: 36px; + height: 36px; + display: grid; + place-items: center; + border: 0; + border-radius: 18px; + background: rgba(255, 255, 255, 0.22); + color: inherit; + font-size: 26px; + line-height: 1; +} + +.toolbar-spacer { + width: 36px; + height: 36px; +} + +.icon { + width: 18px; + height: 18px; + display: block; +} + +.deprecated-note { + margin: 24px 14px; + border: 1px solid #bdeee0; + border-radius: 8px; + padding: 16px; + background: #ffffff; +} + +.deprecated-note h2 { + margin: 0 0 8px; + font-size: 18px; +} + +.deprecated-note p { + margin: 0 0 12px; + color: #526072; +} + +.deprecated-note a { + color: #007a78; + font-weight: 800; +} + +.stats { + display: grid; + grid-template-columns: 1fr 1.2fr 1fr; + gap: 12px; + margin-top: 20px; + text-align: center; +} + +.stat strong { + display: block; + font-size: 20px; +} + +.stat span { + color: rgba(255, 255, 255, 0.76); + font-size: 12px; +} + +.content { + padding: 14px 14px 2px; +} + +.phone-relative .content { + padding-bottom: 250px; +} + +.section-title { + margin: 14px 2px 8px; + color: #0f5f68; + font-size: 13px; + font-weight: 650; +} + +.ledger-card { + border: 1px solid #d8f1e7; + border-radius: 8px; + padding: 14px; + background: #ffffff; +} + +.ledger-card + .ledger-card { + margin-top: 10px; +} + +.ledger-head, +.row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.ledger-name { + margin: 0; + font-size: 19px; + line-height: 1.2; +} + +.badge { + display: inline-flex; + align-items: center; + min-height: 24px; + padding: 0 9px; + border-radius: 12px; + background: #d7fbef; + color: #007a5a; + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} + +.muted { + color: #526072; + font-size: 13px; +} + +.card-actions { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + margin-top: 14px; + border-top: 1px solid #dff5ee; + padding-top: 12px; +} + +.action { + min-height: 36px; + border: 1px solid #ccefe5; + border-radius: 8px; + background: #fff; + color: #152033; +} + +.day { + margin: 14px -14px 0; + padding: 7px 16px; + background: #e9fbf7; + color: #0f5f68; + font-size: 12px; + font-weight: 700; +} + +.entry { + display: grid; + grid-template-columns: 40px 1fr auto; + gap: 10px; + align-items: center; + min-height: 62px; + border-bottom: 1px solid #e0f4ed; +} + +.entry-icon { + width: 34px; + height: 34px; + display: grid; + place-items: center; + border-radius: 17px; + background: #00a676; + color: #fff; + font-size: 13px; + font-weight: 750; +} + +.entry-main strong, +.entry-amount strong { + display: block; +} + +.entry-main span, +.entry-amount span { + color: #526072; + font-size: 12px; +} + +.entry-amount { + text-align: right; +} + +.expense { + color: #d94836; +} + +.income { + color: #008f5f; +} + +.fab { + position: absolute; + right: 24px; + bottom: 24px; + width: 56px; + height: 56px; + border: 0; + border-radius: 28px; + background: #00a676; + color: #fff; + font-size: 28px; + box-shadow: 0 8px 24px rgba(0, 166, 118, 0.28); +} + +.phone-relative { + position: relative; +} + +.drawer-scrim { + position: absolute; + inset: 0; + z-index: 2; + background: linear-gradient(to bottom, rgba(15, 23, 42, 0.02), rgba(15, 23, 42, 0.2)); + pointer-events: none; +} + +.entry-drawer { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 3; + min-height: 292px; + overflow: hidden; + border-top: 1px solid #bdeee0; + border-radius: 18px 18px 0 0; + background: #ffffff; + box-shadow: 0 -18px 36px rgba(15, 23, 42, 0.16); +} + +.drawer-handle { + width: 42px; + height: 5px; + margin: 9px auto 0; + border-radius: 999px; + background: #8ee8d4; +} + +.drawer-amount-line { + padding-top: 12px; + padding-bottom: 8px; +} + +.radial-step { + position: relative; + width: 176px; + height: 118px; + margin-left: auto; +} + +.radial-center, +.radial-node { + position: absolute; + z-index: 1; + border: 1px solid transparent; + border-radius: 999px; + display: grid; + place-items: center; + box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08); + font-weight: 800; + white-space: nowrap; +} + +.radial-center { + right: 0; + bottom: 0; + width: 54px; + height: 54px; + border: 0; + background: #00a676; + color: #fff; + font-size: 20px; +} + +.radial-node { + min-width: 48px; + height: 34px; + padding: 0 10px; + background: #ffffff; + color: #101827; + font-size: 12px; +} + +.radial-node.active { + border-color: #ffb703; + background: #fff0bf; + color: #9b4d00; +} + +.type-expense { + right: 68px; + bottom: 8px; +} + +.type-income { + right: 34px; + bottom: 70px; +} + +.category-food { + right: 88px; + bottom: 4px; + border-color: #ffbf87; +} + +.category-transit { + right: 104px; + bottom: 48px; + border-color: #93c5fd; +} + +.category-daily { + right: 54px; + bottom: 82px; + border-color: #86efac; +} + +.category-shop { + right: 0; + bottom: 96px; + border-color: #f9a8d4; +} + +.sub-takeout { + right: 86px; + bottom: 8px; +} + +.sub-restaurant { + right: 92px; + bottom: 54px; +} + +.sub-coffee { + right: 40px; + bottom: 88px; +} + +.selection-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 0 14px 10px; +} + +.selected-path { + display: flex; + align-items: center; + min-width: 0; + flex-wrap: wrap; + gap: 5px; + padding: 0; + color: #0f5f68; + font-size: 12px; + font-weight: 750; +} + +.selected-path span + span::before, +.selected-path span + strong::before { + content: ">"; + margin-right: 5px; + color: #00a676; +} + +.selected-path span, +.selected-path strong { + min-height: 24px; + border-radius: 12px; + padding: 3px 8px; + background: #e6fbf8; +} + +.selected-path strong { + background: #fff0bf; + color: #9b4d00; +} + +.drawer-note-line { + padding-bottom: 10px; +} + +.flow-stage { + width: min(100%, 1180px); + margin: 0 auto; + padding: 28px 20px 48px; +} + +.flow-intro { + margin-bottom: 20px; +} + +.flow-intro h1 { + margin: 0 0 6px; + font-size: 26px; + line-height: 1.15; +} + +.flow-intro p { + margin: 0; + color: #526072; +} + +.flow-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(252px, 1fr)); + gap: 18px; + align-items: start; +} + +.flow-phone { + width: 100%; + height: 470px; + min-height: 470px; + border-radius: 20px; +} + +.mini-appbar { + min-height: 58px; + padding: 12px 14px; + display: flex; + align-items: center; + gap: 10px; + background: linear-gradient(135deg, #00a676 0%, #00b4d8 100%); + color: #fff; +} + +.mini-appbar span { + width: 28px; + height: 28px; + border-radius: 14px; + display: grid; + place-items: center; + background: rgba(255, 255, 255, 0.22); + font-weight: 850; +} + +.drawer-demo { + position: relative; + min-height: 412px; + background: #ffffff; +} + +.floating-choice { + position: absolute; + right: 7px; + bottom: 150px; + z-index: 4; + padding: 10px; + border: 1px solid rgba(189, 238, 224, 0.86); + border-radius: 16px; + background: rgba(255, 255, 255, 0.94); + box-shadow: 0 14px 34px rgba(15, 23, 42, 0.16); +} + +.mini-keypad { + display: grid; + grid-template-columns: repeat(4, 1fr); + margin-top: 8px; + border-top: 1px solid #bdeee0; +} + +.mini-keypad span, +.mini-keypad strong { + min-height: 38px; + border-right: 1px solid #d7f5ec; + border-bottom: 1px solid #d7f5ec; + display: grid; + place-items: center; + font-size: 14px; +} + +.mini-keypad span:nth-child(4n) { + border-right: 0; + background: #e6fbf8; + color: #007a78; +} + +.mini-keypad strong { + border-right: 0; + background: #00a676; + color: #fff; +} + +.mini-keypad .category-cell { + background: #fff0bf; + color: #9b4d00; + font-weight: 800; +} + +.mini-keypad .confirm-cell { + background: #00a676; + color: #fff; +} + +.active-picker { + margin-top: 170px; +} + +.bottom-panel { + margin-top: 18px; + border-top: 1px solid #d7f5ec; + background: #ffffff; +} + +.amount-line { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 16px; +} + +.amount-line strong { + font-size: 34px; + line-height: 1; +} + +.amount-meta { + display: grid; + justify-items: end; + gap: 7px; + min-width: max-content; +} + +.toggle { + min-width: 58px; + min-height: 34px; + border: 0; + border-radius: 17px; + background: #ff5a47; + color: #fff; + font-weight: 750; +} + +.date-pill { + min-height: 28px; + border: 1px solid #ffc56d; + border-radius: 14px; + padding: 0 10px; + background: #fff2cc; + color: #a45100; + font-size: 12px; + font-weight: 750; +} + +.chips { + display: flex; + gap: 8px; + overflow-x: auto; + padding: 0 14px 12px; + scrollbar-width: none; +} + +.chips::-webkit-scrollbar { + display: none; +} + +.chip { + min-width: max-content; + min-height: 34px; + border: 1px solid #bdeee0; + border-radius: 17px; + padding: 0 12px; + display: inline-flex; + align-items: center; + gap: 6px; + background: #ffffff; + color: #115e59; +} + +.chip.active { + border-color: #ffb703; + background: #fff0bf; + color: #9b4d00; + font-weight: 750; +} + +.category-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 8px; + padding-bottom: 12px; +} + +.category { + min-width: 0; + min-height: 78px; + border: 1px solid transparent; + border-radius: 8px; + padding: 8px 4px 7px; + display: grid; + justify-items: center; + align-content: center; + gap: 6px; + background: #fff; + color: #101827; + box-shadow: 0 8px 18px rgba(15, 23, 42, 0.06); +} + +.category span:last-child { + font-size: 12px; + font-weight: 700; + line-height: 1.1; +} + +.category-mark { + width: 38px; + height: 38px; + border-radius: 19px; + display: grid; + place-items: center; + color: #fff; + font-size: 17px; + font-weight: 850; +} + +.category.food { + border-color: #ffbf87; + background: #fff1e4; +} + +.category.food .category-mark { + background: #ff6b35; +} + +.category.daily { + border-color: #86efac; + background: #ecfdf3; +} + +.category.daily .category-mark { + background: #16a34a; +} + +.category.transit { + border-color: #93c5fd; + background: #eff6ff; +} + +.category.transit .category-mark { + background: #2563eb; +} + +.category.shopping { + border-color: #f9a8d4; + background: #fdf2f8; +} + +.category.shopping .category-mark { + background: #e84393; +} + +.category.income { + border-color: #bef264; + background: #f7fee7; +} + +.category.income .category-mark { + background: #65a30d; +} + +.category.active { + outline: 2px solid #00a676; + outline-offset: 0; +} + +.note-line { + padding: 0 14px 12px; +} + +.note-line input { + min-height: 42px; + width: 100%; + border: 1px solid #bdeee0; + border-radius: 8px; + padding: 0 12px; + background: #ffffff; +} + +.chip-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: currentColor; + opacity: 0.75; +} + +.keypad { + display: grid; + grid-template-columns: repeat(4, 1fr); + border-top: 1px solid #bdeee0; +} + +.key { + min-height: 58px; + border: 0; + border-right: 1px solid #d7f5ec; + border-bottom: 1px solid #d7f5ec; + background: #ffffff; + color: #101827; + font-size: 20px; + font-weight: 650; +} + +.key:nth-child(4n) { + border-right: 0; + background: #e6fbf8; + color: #007a78; +} + +.key:active { + background: #c9f7ef; +} + +.commit-key { + background: #00a676 !important; + color: #fff !important; +} + +.utility-key { + font-size: 16px; +} + +.category-key { + font-size: 16px; + background: #fff0bf !important; + color: #9b4d00 !important; +} + +.invite-box { + border: 1px dashed #86dfca; + border-radius: 8px; + padding: 14px; + background: #ffffff; +} + +.member { + display: grid; + grid-template-columns: 38px 1fr auto; + gap: 10px; + align-items: center; + min-height: 58px; + border-bottom: 1px solid #d7f5ec; +} + +.avatar { + width: 36px; + height: 36px; + display: grid; + place-items: center; + border-radius: 18px; + background: #d7fbef; + color: #007a5a; + font-weight: 800; +} + +@media (max-width: 430px) { + .stage { + padding: 0; + } + + .phone { + height: 100vh; + min-height: 100vh; + border: 0; + border-radius: 0; + box-shadow: none; + } +} diff --git a/docs/原型图/原型说明.md b/docs/原型图/原型说明.md new file mode 100644 index 0000000..09adeca --- /dev/null +++ b/docs/原型图/原型说明.md @@ -0,0 +1,56 @@ +# 原型说明 + +这些原型用于评审信息架构和移动端交互,不是最终视觉稿。 + +## 页面 + +- [账本列表](账本列表.html) +- [账本流水](账本流水.html) +- 条目明细:开发环境访问 `/prototype/entry-detail`,用于评审查看、编辑与常规分类选择流程。 +- [分类轮盘步骤](分类轮盘步骤.html) +- [成员邀请](成员邀请.html) + +## 快速记账 + +快速记账不再作为单独页面,而是账本流水页内的底部抽屉。 + +当前交互方向: + +- 点击账本流水页右下角记账入口,打开底部抽屉。 +- 抽屉内输入金额、备注、日期时间,并使用数字键盘确认。 +- 金额与日期时间位于抽屉顶部;关闭按钮取消,点击抽屉外部关闭。 +- 点击日期时间后直接打开系统日期时间选择器,不再展开额外输入行或二次确认按钮。 +- 信息行仅保留备注和分类按钮,完整分类路径合并显示为 `支出>餐饮>外卖`,初始显示 `支出`。 +- 加减键用于连续计算;发生运算时金额下方显示完整算式,金额区实时显示结果,退格同时更新算式和结果。 +- 点击键盘右上角的收支/分类按钮后,分类选择器覆盖原数字键盘区域,其他界面进入模糊的非交互状态。 +- 分类键原位变为圆形“确定”坐标原点,已选路径使用圆形按钮向左延伸;横轴节点可点击返回对应层。 +- 纵轴同时使用正负区域:原点上方最多放置两个候选,其余候选从原点下方排列并可纵向滚动。 +- 坐标节点均使用带轻阴影的圆形“图标 + 小文字”按钮;横纵轴容器保持透明,不绘制面板底色。 +- 坐标按钮为 `72px`,轴容器预留阴影安全区;纵轴上下端使用渐隐遮罩提示仍可继续滚动。 +- “确定”上下是同一分类序列的同步滚动视口,拖动任意一端时另一端同步位移,分类可连续经过原点上下区域。 +- Y 轴使用受边界约束的自定义拖动,不启用浏览器原生弹性滚动;触达首尾后继续拖动不会产生过度位移或回弹虚影。 +- 松手后根据拖动速度提供短距离减速惯性,惯性同样受首尾边界约束,触边立即停止。 +- 圆形按钮仅使用柔和投影表达悬浮层级,不使用拟物厚度或透视高光;靠近原点的阴影使用渐隐收尾,避免硬裁切。 +- 横轴减少上下内边距;圆形节点使用更清晰的 `2px` 分类色边框区分按钮边界。 +- 横轴已选路径使用分类色实底与白色前景,和未选择的浅色圆形候选形成明确区分。 +- “确定”使用方形按钮并建立独立前景层,Y 轴按钮与原点保留安全间距,滚动时不会被原点矩形覆盖。 +- 选择器关闭时保留当前层级和选择状态,再次打开时从上一次状态继续,不重置到收支类型层。 +- 横轴每一级均可点击并返回对应层重新选择;例如点击 `支出` 重新选择支出或收入,点击 `餐饮` 重新选择一级分类。 +- 横轴固定占满抽屉可用宽度,纵轴固定覆盖完整键盘高度;候选数量只影响滚动,不改变坐标轴尺寸。 +- 数字键盘不再放置分类键,右列为加、减和占据下面两格的整笔确认键。 +- 点击选择器外部确认当前层级;选择到最后一级分类时自动确认并收起选择器。 +- 备注为可选项,不自动获得焦点;用户手动点击备注后隐藏数字键盘,抽屉随内容收缩并交由系统键盘输入。 +- [分类选择步骤](分类轮盘步骤.html) 用分步骤画面表达交互状态(文件名暂时保留旧称)。 +- 收入后续可复用同一套二维选择器,二期再承载 `普通收入 / 报销 / 退款`。 + +[快速记账](快速记账.html) 仅保留为旧版独立页参考,不再作为主原型入口。 + +## 参考来源 + +参考了 `legacy-cent-app` 中的旧版方向: + +- 账本卡片列表。 +- 账本内顶部收入、结余、支出三栏。 +- 按日期分组的流水。 +- 底部快捷记账面板。 +- 数字键盘式金额输入。 diff --git a/docs/原型图/快速记账.html b/docs/原型图/快速记账.html new file mode 100644 index 0000000..b92f7e4 --- /dev/null +++ b/docs/原型图/快速记账.html @@ -0,0 +1,28 @@ + + + + + + 快速记账原型已合并 + + + +
+
+
+
+ +

快速记账

+ +
+
+ +
+

已并入账本流水页

+

快速记账不再作为独立页面。请在账本流水页查看底部抽屉和分类轮盘交互。

+ 打开账本流水原型 +
+
+
+ + diff --git a/docs/原型图/成员邀请.html b/docs/原型图/成员邀请.html new file mode 100644 index 0000000..774935c --- /dev/null +++ b/docs/原型图/成员邀请.html @@ -0,0 +1,62 @@ + + + + + + 成员邀请原型 + + + +
+
+
+
+ +

成员

+ +
+
+ +
+

家庭日常

+
+
+
+ 邀请成员加入账本 +
链接 24 小时后过期
+
+ +
+
+ +

当前成员

+
+
+
自己
owner · 可邀请和移除成员
+ 拥有者 +
+
+
+
小王
member · 可查看和记账
+ 成员 +
+
+
+
小李
member · 最近同步 10:42
+ 成员 +
+ +

权限说明

+
+
首版只做账本级权限。同一账本内成员共享全量流水,不做单条账目权限。
+
+
+
+
+ + + diff --git a/docs/原型图/账本列表.html b/docs/原型图/账本列表.html new file mode 100644 index 0000000..422e0cc --- /dev/null +++ b/docs/原型图/账本列表.html @@ -0,0 +1,76 @@ + + + + + + 账本列表原型 + + + +
+
+
+
+ +

账本

+ +
+
+ +
+

我的账本

+ +
+
+
+

家庭日常

+ 3 名成员 · 今天已同步 +
+ 共享 +
+
+ + + +
+
+ +
+
+
+

旅行备用

+ 仅自己 · 12 条流水 +
+ 独享 +
+
+ + + +
+
+ +
+
+
+

装修记录

+ 2 名成员 · 有 2 条待同步 +
+ 共享 +
+
+ + + +
+
+
+
+
+ + + diff --git a/docs/原型图/账本流水.html b/docs/原型图/账本流水.html new file mode 100644 index 0000000..e425590 --- /dev/null +++ b/docs/原型图/账本流水.html @@ -0,0 +1,84 @@ + + + + + + 账本流水原型 + + + +
+
+
+
+ +

家庭日常

+ +
+
+
8,240收入
+
5,986结余
+
2,254支出
+
+
+ +
+
今天 · 7 月 18 日
+
+
+
午餐餐饮 · 小王
+
-58.0012:28
+
+
+
+
打车交通 · 自己
+
-31.5009:16
+
+ +
昨天 · 7 月 17 日
+
+
+
工资收入 · 自己
+
+8,240.0018:02
+
+
+
+
超市采购日用 · 小王
+
-236.8016:45
+
+
+ + +
+ +
+ 58.00 +
+ +
+
+ 支出 + 餐饮 + 外卖 +
+ +
+ +
+ +
+
+ + + + +
+
+
+
+ + diff --git a/docs/技术方案.md b/docs/技术方案.md new file mode 100644 index 0000000..873824f --- /dev/null +++ b/docs/技术方案.md @@ -0,0 +1,82 @@ +# 技术方案 + +## 客户端 + +默认选 Web-first PWA。 + +技术栈: + +- Vue 3 + TypeScript。 +- Vite。 +- PWA manifest + Service Worker。 +- Vue Router。 +- Pinia。 +- Dexie + IndexedDB。 + +选择理由: + +- 不需要应用商店安装。 +- 更新由服务端发布,用户无需手动升级 App。 +- 同一套代码覆盖 iOS、Android 和桌面浏览器。 +- 账本主要是表单、列表、汇总和同步,Web 足够。 + +本地存储: + +- IndexedDB 是客户端主数据源。 +- 所有账目先写本地,再进入同步队列。 +- Service Worker 缓存应用壳,使无网络时仍能打开。 +- localStorage 只存轻量配置。 + +## 后端 + +默认选 Node.js + TypeScript + Fastify + PostgreSQL。 + +职责: + +- 账号登录。 +- 设备审批。 +- 账本、成员和邀请管理。 +- 按账本权限过滤数据访问。 +- 同步 push/pull。 +- 服务端变更日志。 + +## 测试 + +首期只做 Playwright 端到端测试。 + +覆盖重点: + +- 手机视口下的核心操作。 +- 账本权限和邀请。 +- 离线记账。 +- 恢复网络后的同步。 + +暂不引入单元测试和组件测试。 + +## 备选 + +### React Native + +仅在这些情况再考虑: + +- 必须应用商店分发。 +- 必须强后台同步。 +- 必须调用大量原生能力。 +- PWA 在目标设备上体验不可接受。 + +### Supabase / Firebase + +可以加快早期开发,但离线同步和冲突语义仍要自己把控。当前不作为默认方案。 + +## 工程结构 + +```text +apps/ + web/ # PWA Web App +services/ + api/ # Fastify API +packages/ + domain/ # 共享类型、同步操作定义 + db/ # 数据库 schema 和迁移 +docs/ +``` diff --git a/docs/数据与同步.md b/docs/数据与同步.md new file mode 100644 index 0000000..da62bbb --- /dev/null +++ b/docs/数据与同步.md @@ -0,0 +1,102 @@ +# 数据与同步 + +## 核心实体 + +- `user`:用户。 +- `device`:设备。 +- `ledger`:账本。 +- `ledger_member`:账本成员。 +- `ledger_invitation`:账本邀请。 +- `category`:分类。 +- `entry`:账目。 +- `sync_operation`:同步操作。 + +## 账本字段 + +- `id` +- `name` +- `visibility`:`private` 或 `shared`。 +- `owner_id` +- `created_at` +- `updated_at` +- `deleted_at` + +## 成员字段 + +- `ledger_id` +- `user_id` +- `role`:`owner` 或 `member`。 +- `joined_at` +- `removed_at` + +## 邀请字段 + +- `id` +- `ledger_id` +- `token` +- `created_by` +- `expires_at` +- `accepted_by` +- `accepted_at` +- `revoked_at` + +## 权限规则 + +- 用户只能同步自己有成员关系的账本。 +- 独享账本只有 owner 一名成员。 +- 共享账本可以有多名成员。 +- `owner` 可以邀请和移除成员。 +- `member` 可以查看和记账。 +- 首版不做单条账目权限。 + +## 账目字段 + +- `id`:客户端生成 UUID。 +- `ledger_id` +- `type`:`expense` 或 `income`。 +- `amount`:金额,整数分。 +- `category_id` +- `occurred_at` +- `note` +- `created_by` +- `updated_by` +- `created_at` +- `updated_at` +- `deleted_at` +- `version` + +## 同步原则 + +- 客户端先写 IndexedDB,界面立即成功。 +- 每次本地变更生成一个 `operation_id`。 +- 同步 API 必须幂等。 +- 服务端保存账本变更日志。 +- 客户端用游标拉取增量。 +- 同步范围必须按账本成员权限过滤。 + +## 同步流程 + +1. 用户新增、编辑或删除账目。 +2. 客户端写 IndexedDB。 +3. 客户端写本地同步队列。 +4. 网络可用时推送队列。 +5. 服务端按 `operation_id` 去重并应用。 +6. 客户端拉取远端增量。 +7. 客户端合并到 IndexedDB。 + +## 冲突策略 + +- 新增记录基本无冲突,ID 由客户端生成。 +- 删除使用软删除。 +- 同一条账目被多人离线修改时,首版最后写入胜出。 +- 不做字段级合并。 +- 冲突历史和人工选择版本后置。 + +冲突示例: + +- 手机 A 和手机 B 都同步过同一笔账:`午餐 100 元`。 +- 两台手机同时离线。 +- 手机 A 改成 `120 CNY`。 +- 手机 B 改成 `110 CNY` 或修改分类。 +- 两台手机恢复网络后上传。 +- 服务端必须决定最终版本。 diff --git a/docs/测试策略.md b/docs/测试策略.md new file mode 100644 index 0000000..bf81c3f --- /dev/null +++ b/docs/测试策略.md @@ -0,0 +1,56 @@ +# 测试策略 + +## 首期选择 + +首期只使用 Playwright 做端到端测试,不写单元测试。 + +## 原则 + +- 测试真实用户路径,不测试内部实现。 +- 优先覆盖高风险流程。 +- 使用手机视口作为主要测试环境。 +- 测试数据可重复创建和清理。 +- 不为早期快速变化的组件写细碎测试。 + +## 首期覆盖 + +### 记账 + +- 新增一笔收入。 +- 新增一笔支出。 +- 编辑账目。 +- 删除账目。 +- 刷新页面后账目仍存在。 + +### 账本权限 + +- 用户只能看到自己有权限的账本。 +- 独享账本不对其他用户可见。 +- owner 可以生成邀请。 +- 被邀请成员加入后可以看到账本。 + +### 离线 + +- 离线时可以新增账目。 +- 离线刷新后应用仍能打开。 +- 恢复网络后触发同步。 + +### 同步 + +- 同一操作重复提交不会重复记账。 +- 多设备拉取后能看到同一账本流水。 +- 同一账目离线冲突时最后写入胜出。 + +## 暂不覆盖 + +- 单元测试。 +- 组件测试。 +- 外币和汇率。 +- 大规模性能测试。 +- 视觉回归测试。 + +## 推荐命令 + +```bash +npm run test:e2e +``` diff --git a/docs/评审问题.md b/docs/评审问题.md new file mode 100644 index 0000000..66e0d40 --- /dev/null +++ b/docs/评审问题.md @@ -0,0 +1,31 @@ +# 评审问题 + +## 必须确认 + +1. 首版登录方式:后台分配用户名密码,还是设备 ID 审批? +2. 是否确认首期不做外币? +3. 是否确认只记录流水,不做分摊? +4. 离线冲突时,最后写入胜出是否可接受? +5. 删除账目是否需要恢复入口? +6. 邀请用链接、邀请码,还是两者都支持? +7. 成员是否允许记账,还是只能查看? +8. 首版是否需要导出 CSV? +9. 是否需要自部署? +10. 数据安全和开发速度冲突时,是否明确优先数据安全? + +## 当前默认答案 + +- 不开放注册。 +- 先采用后台分配用户名密码。 +- 设备 ID 审批作为备选。 +- 首期只做人民币,不做外币。 +- 账本可以独享,也可以邀请成员共享。 +- 用户只能访问自己有权限的账本。 +- 同一账本内成员共享全量账目。 +- 首版权限只有 `owner` 和 `member`。 +- 只记录收入和支出流水。 +- 冲突最后写入胜出。 +- 软删除,但不做恢复 UI。 +- 暂不导出。 +- 部署到普通云服务。 +- 数据安全优先于功能数量。 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8c3f52d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2259 @@ +{ + "name": "cents", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cents", + "version": "0.1.0", + "workspaces": [ + "apps/*", + "services/*", + "packages/*" + ], + "engines": { + "node": ">=22" + } + }, + "apps/web": { + "name": "@cents/web", + "version": "0.1.0", + "dependencies": { + "@cents/domain": "0.1.0", + "@lucide/vue": "^1.25.0", + "@vitejs/plugin-vue": "^6.0.0", + "dexie": "^4.0.11", + "pinia": "^3.0.3", + "vite": "^7.0.5", + "vue": "^3.5.17", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "typescript": "^5.8.3", + "vue-tsc": "^3.0.1" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cents/api": { + "resolved": "services/api", + "link": true + }, + "node_modules/@cents/domain": { + "resolved": "packages/domain", + "link": true + }, + "node_modules/@cents/web": { + "resolved": "apps/web", + "link": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^7.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@lucide/vue": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.25.0.tgz", + "integrity": "sha512-hkEetV+v48ScIn3uwqwWQ66sI8foeP2q6OMI09GzLFH4SfvBlfe3JHYlMBdBCqFC7WRlhFsndyDn/awRKRc2OQ==", + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", + "license": "MIT", + "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" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", + "license": "MIT", + "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.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.10.tgz", + "integrity": "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.10" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.10.tgz", + "integrity": "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.10", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.10.tgz", + "integrity": "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.7.tgz", + "integrity": "sha512-LzmkKinXAMMoh8Jfi/jMUSDUjuPdv8mynH5WJGKfXyZtDw3hQ6GBaoI6Bcnl/Xqlu32q/0Z6i/trp4VXykzyLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.1", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz", + "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz", + "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", + "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz", + "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", + "license": "MIT" + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz", + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dexie": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.4.tgz", + "integrity": "sha512-jIwsYI8Os2hgnqc6O49YwFDKGc5v5QjGx0wPVp543ip1F53VFAKMLthV2pQosQcVTv3eAskTWYspOx195PM0FQ==", + "license": "Apache-2.0" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/fast-uri": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.0.tgz", + "integrity": "sha512-ZodJ2cRiLVWGi9IgPb3mbgSqM4CD3LexCHkuv0FfBXHJI1ADfucTD06m6clO2Cy5RZYsw/SiCVl/dyrFI/SYWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.10.0.tgz", + "integrity": "sha512-A9L0ziuWGQHgEEVgF3davQ9vbD93IuX+lo2IsxapQmu5b/Y/ynn9m9K5JHt9dvyJXOFc5iN0Zk5GHEOqnzhWjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^7.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-my-way": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", + "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "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.2" + } + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", + "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", + "license": "MIT", + "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" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/vue-tsc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.7.tgz", + "integrity": "sha512-+C+rgD49wAQ5bUTl2sp5a8Bzg4YoldMNXM+g7CFe604MYcQ8PrZPMQhIjJSzKXtPBCa+C5ayMipqjbA7splekQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.7" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "packages/domain": { + "name": "@cents/domain", + "version": "0.1.0", + "devDependencies": { + "typescript": "^5.8.3" + } + }, + "services/api": { + "name": "@cents/api", + "version": "0.1.0", + "dependencies": { + "@cents/domain": "0.1.0", + "fastify": "^5.4.0" + }, + "devDependencies": { + "@types/node": "^22.13.14", + "tsx": "^4.20.3", + "typescript": "^5.8.3" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..51147d6 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "cents", + "version": "0.1.0", + "private": true, + "type": "module", + "workspaces": [ + "apps/*", + "services/*", + "packages/*" + ], + "scripts": { + "dev": "npm run dev --workspace @cents/web", + "dev:api": "npm run dev --workspace @cents/api", + "build": "npm run build --workspaces --if-present", + "typecheck": "npm run typecheck --workspaces --if-present", + "lint": "npm run lint --workspaces --if-present" + }, + "engines": { + "node": ">=22" + } +} + diff --git a/packages/domain/package.json b/packages/domain/package.json new file mode 100644 index 0000000..f254e92 --- /dev/null +++ b/packages/domain/package.json @@ -0,0 +1,16 @@ +{ + "name": "@cents/domain", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.8.3" + } +} + diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts new file mode 100644 index 0000000..cc0b78a --- /dev/null +++ b/packages/domain/src/index.ts @@ -0,0 +1,40 @@ +export type EntryType = "expense" | "income"; + +export type CurrencyCode = "CNY" | "USD" | "EUR" | "JPY" | "HKD"; + +export type ExchangeRateSource = "manual" | "system"; + +export type LedgerEntry = { + id: string; + ledgerId: string; + type: EntryType; + amount: number; + currency: CurrencyCode; + baseCurrency: CurrencyCode; + baseAmount: number; + exchangeRate: string; + exchangeRateSource: ExchangeRateSource; + categoryId: string; + note: string; + occurredAt: string; + createdBy: string; + updatedBy: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + version: number; +}; + +export type SyncOperationAction = "create" | "update" | "delete"; + +export type SyncOperation = { + id: string; + ledgerId: string; + entity: "entry"; + entityId: string; + action: SyncOperationAction; + payload: LedgerEntry; + createdAt: string; + syncedAt: string | null; +}; + diff --git a/packages/domain/tsconfig.json b/packages/domain/tsconfig.json new file mode 100644 index 0000000..c53c487 --- /dev/null +++ b/packages/domain/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022"] + }, + "include": ["src/**/*.ts"] +} + diff --git a/services/api/package.json b/services/api/package.json new file mode 100644 index 0000000..f413b4f --- /dev/null +++ b/services/api/package.json @@ -0,0 +1,20 @@ +{ + "name": "@cents/api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsc --noEmit", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@cents/domain": "0.1.0", + "fastify": "^5.4.0" + }, + "devDependencies": { + "@types/node": "^22.13.14", + "tsx": "^4.20.3", + "typescript": "^5.8.3" + } +} diff --git a/services/api/src/server.ts b/services/api/src/server.ts new file mode 100644 index 0000000..12d291d --- /dev/null +++ b/services/api/src/server.ts @@ -0,0 +1,25 @@ +import Fastify from "fastify"; + +const server = Fastify({ + logger: true, +}); + +server.get("/health", async () => ({ + ok: true, + service: "cents-api", +})); + +server.post("/sync/push", async () => ({ + accepted: true, + operations: [], +})); + +server.get("/sync/pull", async () => ({ + cursor: null, + operations: [], +})); + +const port = Number(process.env.PORT ?? 3000); +const host = process.env.HOST ?? "0.0.0.0"; + +await server.listen({ port, host }); diff --git a/services/api/tsconfig.json b/services/api/tsconfig.json new file mode 100644 index 0000000..ee71dd6 --- /dev/null +++ b/services/api/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022"], + "types": ["node"], + "moduleResolution": "NodeNext", + "module": "NodeNext" + }, + "include": ["src/**/*.ts"] +} + diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..34a3138 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true + } +} +