Imported from kai-orion/to-do (
AGENTS.md). Install upstream withnpx skills add kai-orion/to-do. Copyright stays with the author.
AGENTS.md — to-do (Google Tasks clone SPA)
SPA. Vue 3.5 + Pinia 4 + Vue Router 5 + TypeScript ~6.0 + Vite 8 + TailwindCSS v4 (@tailwindcss/vite) + @material/web 2.5 + @sandlada/mcu-helper / @sandlada/material-design-css. Target: Chrome 150+, MDN Baseline 2026+, ESNext. No legacy fallbacks/polyfills.
Commands
npm run dev— local dev server.npm run type-check(vue-tsc --noEmit) — run before finishing any code change; repo has no lint/test runner.npm run build— outputs to./docswithbase: '/to-do'(GitHub Pages artifact). Do not changeoutDir/base.npm run preview— serve thedocs/build.
No test, lint, or format scripts exist. fake-indexeddb is a dep reserved for future IndexedDB work/tests.
Architecture
- Entry:
index.html→src/main.ts(singleimport '@material/web/all'+ Pinia +globalRouter) →src/App.vue(themecssTextviauseAdoptedStyleSheet+RouterView, starts/stops themedia-queryobserver) →src/pages/viasrc/layouts/ProductLayout.vue. - Router
src/router/index.ts: only/and/settings. Must staycreateWebHashHistory— static hosting underdocs/has no SPA fallback. - Stores (
src/stores/, Pinia setup-styleref+ explicitsave()):todo-list,todo-tabs,material-theme,media-query,navigation. No cross-store imports except components composing them. - Theme:
src/App.vueinjectstheme.cssText(built withcreateTheme/toCSSfrom@sandlada/mcu-helper,specVersion: '2025') viauseAdoptedStyleSheet, and toggles thedarkattribute on<html>.material-theme.tsreads both flat and legacy{ configuration: {...} }persisted shapes — keep that compat. - Breakpoints:
media-querystore wrapscreateBreakpointObserverfrom@sandlada/breakpoint(MEDIA_WIDTH_BREAKPOINTS, keyscompact/medium/expanded/large/extra-large,dimension: 'width',start(el)/stop()lifecycle fromApp.vue) and syncscurrentWidth/currentBreakpointplus the breakpoint class ontodocument.body.rxjsis a direct dep (observer peer dep).ProductLayout/TaskSidebarLayout/NavigationDrawerLayouttreatcompactas modal drawer. Never hardcode600px/1200pxelsewhere; read the store (breakpoint attributes on<html>). - Components are all
.vueSFCs (incl.header/,navigation-drawer/— formerly.tsx+.module.css, converted to SFCs with scoped<style>). - Design target:
prototype/holds the goal-state UI references (currentlyall-task.demo.png, the Google Tasks multi-list board to replicate). It is the source of truth for task-list layout — consult it before building or changing task UI; the currentsrc/implementation may not match it yet. src/pages/index.vueis the top-level orchestrator for the board: it owns the shared filter state (activeView,visibleLists,isListsCollapsed,tabs) as the single source for the sidebar + board Layouts.TaskBoardLayoutowns all board-local state (sort/menus/composers/drag/dialogs/masonry) plus todo stores;TaskSidebarLayoutowns drawer chrome + counts via stores. Shared filter mutations travelLayout --emits--> page --props--> Layout. Extracted board modules must run independently and talk toindex.vueonly viaprops/emits. Do not let leaf modules reach into stores directly.
Components (通用組件 vs Layout,統稱 components)
- Terminology:
components/andlayouts/are both calledcomponents— do not scatter attention by using the folder split as the responsibility boundary. The boundary is the naming + data-access rule below, not the folder. - Naming: plain components are PascalCase
{Name}.vue(e.g.TaskSidebar.vue,TaskItem.vue); layouts are{Name}Layout.vue(e.g.TaskBoardLayout.vue,ProductLayout.vue).src/layouts/Product.vuewas legacy — already renamed toProductLayout.vue. - Plain
components({name}.vue) are generic/pure:- 絕對禁止調用 Pinia (
use*Store),也禁止調用useRouter/useRoute. - 禁止在純組件內使用
<RouterLink>— 導航狀態經props進入(如activeUrl),導航意圖經emits交給 Layout(如navigate→router.push)。 - 禁止調用非 component 自身分發的
inject/provide. Only exception: a component mayinjectwhat it (or its own sub-component family)provides — e.g.navigation-drawermayprovide,navigation-drawer-tabmayinject, butndt只能inject由nd分發的provide. - Data in only via
props(e.g.tabs,counts,visible), actions out only viaemits. Example:components/nd只能接受tabs等props,不可自行useStore.
- 絕對禁止調用 Pinia (
layouts({name}Layout.vue) are component + business-logic integration:- 可以使用
useRouter也可以調用useStore. Responsible for fetching business data and dispatching data + logic down tocomponent/*viaprops/emits. - Example:
ndLayout負責獲取業務數據並將業務數據和業務邏輯分發到component/nd,通過emits和props通信. - Current layouts:
ProductLayout(shell + header/nav slots),NavigationDrawerLayout(global nav + router),TaskSidebarLayout(drawer chrome + counts),TaskBoardLayout(board state + todo stores + dialogs/masonry).
- 可以使用
- Migration debt (do not extend):
src/components/create-todo/CreateTodoFab.vueandsrc/components/create-collection/CreateCollectionTab.vuecalled Pinia directly and were unreferenced orphans — deleted (their dialogs are covered byTaskDialogs+useBoardDialogs).material-theme-provider.tsxno longer exists (logic lives inApp.vue+useAdoptedStyleSheet). - 不要過度組件化,請善用 Vue 的
template功能: keep tiny markup (chip,menu-item,circle-check, composer rows,<template v-for>fragments, slots) as inline<template>in the parent instead of spinning up one-file-per-element components. Split only at independently-runnable module boundaries (header / composer / task row incl. subtasks via<template>/ list card / dialogs).
Composables (src/composables/use*.ts)
- Scattered page logic goes into composables, not more components. Max ~5 cohesive hooks per page (current board:
useBoardDialogs,useBoardDrag,useComposers,useMasonry— owned byTaskBoardLayout; the olduseBoardViewwas deleted after its filter state moved to the page and its board-local state moved into the Layout). - Two kinds, same rule as components: pure hooks (no
use*Store, e.g.useComposers,useMasonrywith breakpoint passed as param) are usable anywhere; store-touching hooks (e.g.useBoardDialogs,useBoardDrag) are business logic — only call them from a page /*Layout.vue, never from a pure{name}.vue. - Hooks never import each other. Cross-cutting needs (e.g.
closePop,ensureVisible, busy guard) arrive as explicit params — the page composes them. - Data-mutating operations belong in the Pinia store as methods (
findOneTodoByUuid,findManyStepsByParent/updateManyStepsByParent,insertOneTodoFromFields/updateOneTodo, step ops,updateOneTodoCollection,updateOneTab,removeManyTodosByList, …); hooks and pages only dispatch. Keep cross-store imports out of stores. - Each hook returns an explicit object and stays independently runnable/testable; the page keeps identical
props/emitsnames toward children so templates don't churn.
Naming
- 接口一律
I開頭 (e.g.ITodo,IUser). - Composable / 構造器的參數類型命名為
I{Name}Args,且一定是interface;調用時參數名一律args,即funcName(args: IXxxArgs)(e.g.IUserSendOneMessageUseCaseArgs). - 數據操作動詞只用
find/insert/remove/update.個數與範圍用One/Many;條件用By(e.g.findOneUserById).update通常已隱含按身份定位,不加By(e.g.updateOneUser).查全部用無參的findMany(). - Boolean 類型的數據 / 屬性優先
is/has/exists開頭 (e.g.isVisible,isSupported).
Verification — Playwright MCP first
- 優先采用 Playwright MCP (
playwright_browser_*tools) for any UI / layout / visual work. Do not rely on code reading ornpm run previeweyeballing alone. - Standard loop:
npm run dev→playwright_browser_navigateto the dev URL →playwright_browser_snapshotfor structure →playwright_browser_take_screenshotfor pixel compare againstprototype/*.png→ iterate →npm run type-check. - Use
playwright_browser_click/playwright_browser_type/playwright_browser_press_key/playwright_browser_wait_forto exercise task CRUD, drawer open/close, breakpoints, and dark mode instead of guessing behavior. - Use
playwright_browser_console_messagesandplaywright_browser_network_requestswhen debugging blank pages or missing assets. - Pixel-level replication means: compare spacing, radius, colors (M3 tokens), typography, and iconography against
prototype/side-by-side at the same viewport (default desktop 1600px wide unless testingcompact); fix discrepancies before finishing.
Conventions & gotchas
@material/webtags (md-*):vite.config.tsmarks them as custom elements in thevue()plugin options — keep it when editing config. (No JSX remains:@vitejs/plugin-vue-jsxwas removed; do not reintroduce.tsx/.jsx.)- Styling: tokens/utilities come from
@sandlada/material-design-cssimports insrc/styles/tailwind.css(e.g.bg-surface,bg-surface-container,ease-emphasized-decelerate). Scoped<style>blocks must start with@reference "@styles/tailwind.css";for@applyto work (seeProductLayout.vue). - Imports: use path alias for all
src/-internal imports — never..//./.vite.config.ts(resolve.alias) andtsconfig.json(paths) define the same set:@/*→src/*, plus@components/*,@composables/*,@layouts/*,@pages/*,@router/*,@stores/*,@styles/*,@utils/*. Keep both configs in sync when adding/removing top-levelsrc/folders. - TS:
verbatimModuleSyntax: true— useimport typefor type-only imports.strict,target/module ES2022. - IDs: use the
uuidpackage (v14, already a dependency).src/utils/uuid.ts:10(makeUuid, hand-rolled RFC4122) is legacy — don't extend it; migrate call sites (currentlystores/todo-list.ts:90) touuidwhen touching that code. - Persistence: simple config (theme) →
localStoragewith existingSymbol(__...)key strings. Complex/stateful data (todos, collections/tabs) → IndexedDB (migration pending; currentlocalStorageintodo-list/todo-tabsis the tech debt to replace). Settings "Clear" wipeslocalStorage(src/pages/settings.vue) — extend it to IndexedDB once migration lands.