โ All Skills Knowledge History
Move all memo page logic into the Pinia store (Rule 15)
memorule-15pinia-storethin-pageuseFileSystemAccess-in-storeprogrammatic-file-inputrefactor
Request
- Every possible logic should live in /stores; the page is too heavy.
- Create a Mono Skills entry.
Summary
Move all memo page logic into the Pinia store (Rule 15)
The /memo page had grown to ~160 lines of logic in <script setup> (file I/O, four
import/export actions, fallback handlers, two hidden <input> refs, useMediaQuery, formatters).
Rule 15 โ pages render, stores think, composables provide reusable helpers โ says that belongs
in the store. After the refactor the page <script> is 22 lines (render-only) and
use-memo.ts owns everything.
What moved into src/stores/use-memo.ts
useFileSystemAccess({ dataType: 'Text', โฆ })anduseMediaQuery('(max-width: 640px)')โ yes, VueUse composables run fine inside a Pinia setup store (it's an effect scope), and both are SSR-safe (they guardwindow/document), which matters for the Nuxt host's prod SSR.- Actions:
exportMemo/importMemo(prefill the form) /exportAll/importBatch. - Private helpers that dedupe the FSA-vs-fallback branching:
readPickedText()(open) andsaveText(text,name,okMsg)(save), plusapplyImported(text). - Render glue so the template stays declarative:
yn,fmtDate,onSearch(e)(reads themono-inputCustomEvent),clearSearch(), and aninputAccentcomputed for the paper tint.
The nice simplification: programmatic file-input fallback
The old page kept two hidden <input type="file"> elements + refs + change handlers just for
non-Chromium import. In the store there's no template, so the fallback creates the input in JS:
function pickTextFile(): Promise<string | null> {
return new Promise((resolve) => {
const el = document.createElement('input')
el.type = 'file'; el.accept = '.txt,text/plain'
el.onchange = async () => { const f = el.files?.[0]; resolve(f ? await f.text() : null) }
el.click() // cancel โ no change event โ promise stays pending (harmless)
})
}
This removed both hidden inputs (and their refs/handlers) from the page entirely โ fewer moving
parts, and all import paths funnel through one readPickedText().
Layering kept (per the user's choice)
Pure data transforms (serializeMemo / serializeMemos / parseMemos / memoFileName /
backupFileName) stay in the use-memo-utils composable โ the store consumes it
(const utils = useMemoUtilsMyMemo()). So: page renders โ store thinks โ composable provides the
reusable helpers. Exactly Rule 15.
Page after refactor
Script = side-effect mono-helper/ui imports + static-data imports (memoCols, memoCategories,
memoCategoryAccent โ data, not logic) + definePage + const store = useMemoMyMemo() +
onMounted(() => store.load()). Every @mno-click / @mno-input calls a store method
(store.exportAll(), store.onSearch($event), โฆ); the modal sizing reads store.isSmallScreen;
the paper tint reads store.inputAccent.
Gotcha
VueUse auto-imports (useFileSystemAccess, useMediaQuery) and the useMemoUtilsMyMemo composable
all resolve inside the store file โ unplugin-auto-import scans src/stores too, same as it
already did for useHelperMonoVue().
Files
src/stores/use-memo.tsโ gained the FSA instance, the four file actions, the dedup helpers, and the render-glue (onSearch/fmtDate/yn/inputAccent/isSmallScreen).src/pages/memo/index.vueโ reduced to render-only; removed the hidden inputs and all functions.src/composables/use-memo-utils.tsโ unchanged (now consumed by the store).
Outcome
successvite module transform + page-leftover grep /memo, page, store, and use-memo-utils.ts all transform 200 Page <script> is 22 lines (render-only); no fsa/useFileSystemAccess/fmtDate/val/fileInput left in the page Store now exposes exportMemo/importMemo/exportAll/importBatch/onSearch/inputAccent/fmtDate/yn/isSmallScreen + the private FSA helpers
Decisions
All orchestration/state/file-I/O lives in the store (Rule 15)
Moved FSA, the four import/export actions, fallbacks, useMediaQuery, and formatters from the page into use-memo.ts; the page only renders and calls store methods.
Why: Rule 15 (pages render, stores think) and the user's standing rule 'every possible logic on /stores'. Keeps logic reusable and the page declarative.
Pure serialize/parse stay in the use-memo-utils composable
Did NOT inline serialize/parse into the store; the store consumes useMemoUtilsMyMemo().
Why: User chose to keep the helper layer. Rule 15 puts reusable pure helpers in a use-<name>-utils composable consumed by the store.
Programmatic <input> for the non-Chromium import fallback
Create the file input via document.createElement in the store instead of keeping hidden <input> elements + template refs in the page.
Why: The store has no template; building the input in JS removes two hidden inputs/refs/handlers from the page and funnels all imports through one readPickedText().
VueUse composables inside the Pinia store are fine
useFileSystemAccess / useMediaQuery are instantiated in the store setup.
Why: Pinia setup stores are an effect scope; both composables are SSR-safe (guard window/document), so they work in the Nuxt host (SSR) and the remote (SPA). Auto-import resolves them in src/stores too.
Files changed
| File | Operation | Note |
|---|---|---|
src/stores/use-memo.ts | modified | Absorbed page logic: useFileSystemAccess + useMediaQuery instances; exportMemo/importMemo/exportAll/importBatch; private readPickedText/saveText/pickTextFile (programmatic input fallback) + applyImported; render glue yn/fmtDate/onSearch/clearSearch/inputAccent. Consumes useMemoUtilsMyMemo. |
src/pages/memo/index.vue | modified | Reduced to render-only (~22-line script): static-data imports + store + onMounted(load). All handlers call store methods; removed the two hidden <input> fallback elements, fsa, formatters, and val. |
src/composables/use-memo-utils.ts | modified | Pure serialize/parse helpers โ now consumed by the store instead of the page (Rule 15 helper layer). |
Commands
pnpm dev
curl /memo + page + store + use-memo-utils.ts (all 200)
MONO_SKILLS=true MONO_SKILLS_GITHUB_TOKEN=[REDACTED] pnpm exec mono skills save --app my-memo --dir .mono/skills/pending/<id>Validation
Tests: not run Build: passed