Stacked on #372 — branched from it, so merge that first or this diff will look like it contains both.
store/mail.ts was 1,463 lines, the largest module in the app. It is now a directory, so @/store/mail resolves to index.ts and none of the 36 modules that import useMail changes a line.
File
Lines
Holds
mail/props.ts
72
MAILBOX_PROPS, LIST_PROPS, FULL_PROPS, BODY_PROPS
mail/types.ts
125
ListQuery, ListState, MailState, DEFAULT_SORT
mail/mailboxes.ts
28
mailboxIcon, ROLE_ORDER
mail/index.ts
1,266
the store, and everything bound to it
Everything exported before is still exported from index.ts. No behavior change, no call-site change.
What this does not do
index.ts is still 1,266 lines, because 947 of them are a single create<MailState>((set, get) => ({ … })). This PR removes 13% of the file, not 60%.
Cutting up the rest means Zustand slices — splitting the state object itself and recombining it. That is a change to how the store is built, not to where its text lives, in the part of the app every screen leans on. It deserves its own PR and its own argument rather than a quiet ride along with a file move. Say the word and I'll do it as a follow-up.
Three boundaries that look wrong until you check
Each of these is somewhere the obvious split fails:
listKey sits among the type declarations but is a function the store calls, not a type. Moving it to types.ts compiles and then fails at the call site.
ensureFolderPath, folderRefs, followFolders read like folder helpers and look like they belong beside mailboxIcon — but they close over useMail. Moving them makes mailboxes.ts import index.ts, which imports mailboxes.ts.
The sieve import is dynamic — await import("./sieve"), not a static from clause — so a path rewrite driven by from "…" silently misses it. It needed ../sieve after the move.
Translations: adds none. No user-visible strings.
Testing:npm run typecheck exit 0 across both workspaces, npm test exit 0 (135 web test files / 1,292 tests, 228 server tests), npm run build exit 0. Run locally, not left to CI.
**Stacked on #372** — branched from it, so merge that first or this diff will look like it contains both.
`store/mail.ts` was 1,463 lines, the largest module in the app. It is now a directory, so `@/store/mail` resolves to `index.ts` and **none of the 36 modules that import `useMail` changes a line.**
| File | Lines | Holds |
| --- | --- | --- |
| `mail/props.ts` | 72 | `MAILBOX_PROPS`, `LIST_PROPS`, `FULL_PROPS`, `BODY_PROPS` |
| `mail/types.ts` | 125 | `ListQuery`, `ListState`, `MailState`, `DEFAULT_SORT` |
| `mail/mailboxes.ts` | 28 | `mailboxIcon`, `ROLE_ORDER` |
| `mail/index.ts` | 1,266 | the store, and everything bound to it |
Everything exported before is still exported from `index.ts`. No behavior change, no call-site change.
## What this does not do
**`index.ts` is still 1,266 lines**, because 947 of them are a single `create<MailState>((set, get) => ({ … }))`. This PR removes 13% of the file, not 60%.
Cutting up the rest means Zustand slices — splitting the state object itself and recombining it. That is a change to *how the store is built*, not to where its text lives, in the part of the app every screen leans on. It deserves its own PR and its own argument rather than a quiet ride along with a file move. Say the word and I'll do it as a follow-up.
## Three boundaries that look wrong until you check
Each of these is somewhere the obvious split fails:
- **`listKey`** sits among the type declarations but is a function the store calls, not a type. Moving it to `types.ts` compiles and then fails at the call site.
- **`ensureFolderPath`, `folderRefs`, `followFolders`** read like folder helpers and look like they belong beside `mailboxIcon` — but they close over `useMail`. Moving them makes `mailboxes.ts` import `index.ts`, which imports `mailboxes.ts`.
- **The sieve import is dynamic** — `await import("./sieve")`, not a static `from` clause — so a path rewrite driven by `from "…"` silently misses it. It needed `../sieve` after the move.
**Translations:** adds none. No user-visible strings.
**Testing:** `npm run typecheck` exit 0 across both workspaces, `npm test` exit 0 (135 web test files / 1,292 tests, 228 server tests), `npm run build` exit 0. Run locally, not left to CI.
**Merged** 2026-09-15 as coffey-labs/ihasmail@5cc31037c178
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Stacked on #372 — branched from it, so merge that first or this diff will look like it contains both.
store/mail.tswas 1,463 lines, the largest module in the app. It is now a directory, so@/store/mailresolves toindex.tsand none of the 36 modules that importuseMailchanges a line.mail/props.tsMAILBOX_PROPS,LIST_PROPS,FULL_PROPS,BODY_PROPSmail/types.tsListQuery,ListState,MailState,DEFAULT_SORTmail/mailboxes.tsmailboxIcon,ROLE_ORDERmail/index.tsEverything exported before is still exported from
index.ts. No behavior change, no call-site change.What this does not do
index.tsis still 1,266 lines, because 947 of them are a singlecreate<MailState>((set, get) => ({ … })). This PR removes 13% of the file, not 60%.Cutting up the rest means Zustand slices — splitting the state object itself and recombining it. That is a change to how the store is built, not to where its text lives, in the part of the app every screen leans on. It deserves its own PR and its own argument rather than a quiet ride along with a file move. Say the word and I'll do it as a follow-up.
Three boundaries that look wrong until you check
Each of these is somewhere the obvious split fails:
listKeysits among the type declarations but is a function the store calls, not a type. Moving it totypes.tscompiles and then fails at the call site.ensureFolderPath,folderRefs,followFoldersread like folder helpers and look like they belong besidemailboxIcon— but they close overuseMail. Moving them makesmailboxes.tsimportindex.ts, which importsmailboxes.ts.await import("./sieve"), not a staticfromclause — so a path rewrite driven byfrom "…"silently misses it. It needed../sieveafter the move.Translations: adds none. No user-visible strings.
Testing:
npm run typecheckexit 0 across both workspaces,npm testexit 0 (135 web test files / 1,292 tests, 228 server tests),npm run buildexit 0. Run locally, not left to CI.Merged 2026-09-15 as coffey-labs/ihasmail@5cc31037c1
Rebuilt from: git history, session transcript.