@tailor-platform/app-shell 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,355 @@
1
+ # Design System
2
+
3
+ Authority for **visual-only** decisions — tokens, theme imports, breakpoints intent, `**astw:`** rules, and custom-component conformance. For **React component APIs** (imports, props, JSX composition), pair this file with `**components.md`\*\*; that split avoids duplicating tables and lengthy examples across both docs.
4
+
5
+ `@tailor-platform/app-shell` (ERP scaffolds target **≥0.36**; bump your app’s pinned version deliberately) ships an opinionated design system via CSS variables and a `theme.css` import. Use it whether you are consuming AppShell components (most cases) or building a custom component to fill a gap.
6
+
7
+ **The tokens are the rails.** Consistency across customers, apps, and AI runs comes from the token system, not from rules written in prose. A hand-typed `#fff` or `padding: 13px` is not a "small deviation" — it is the mechanism by which consistency dies. Every visual value you reach for must resolve to a token in this file. If a token is missing, add one; never inline.
8
+
9
+ ## 1. Setup
10
+
11
+ Authoritative app wiring also lives in `**project-setup.md`**; **scaffold `index.css`\*\* currently does:
12
+
13
+ ```css
14
+ @import "tailwindcss";
15
+ @import "tw-animate-css";
16
+
17
+ @import "@tailor-platform/app-shell/styles";
18
+ @import "@tailor-platform/app-shell/theme.css";
19
+ ```
20
+
21
+ Adjust if your App Shell version documents a different barrel filename, but keep this split:
22
+
23
+ - `**theme.css**` — design tokens as CSS variables on `:root`.
24
+ - `**styles**` (package export) — bundled component styles AppShell ships for primitives.
25
+ - `**tailwindcss**` — utilities; token-backed classes (`bg-surface-1`, `text-fg-muted`) resolve through the theme.
26
+
27
+ Older docs referred to `app-shell.css`; prefer the `**styles**` import the template uses.
28
+
29
+ Tailwind v4 stays CSS-first; minimal `vite` / PostCSS wiring is in `**project-setup.md**`.
30
+
31
+ ## 2. Theming via CSS variables
32
+
33
+ AppShell controls its theme through CSS variables. Override them in `:root` (global) or a scoped selector (per-section, per-tenant, dark mode) to customize. Any token defined in `theme.css` can be overridden after the import.
34
+
35
+ ```css
36
+ :root {
37
+ --color-primary: #3b82f6;
38
+ --color-background: #ffffff;
39
+ }
40
+
41
+ [data-theme="dark"] {
42
+ --color-background: #0a0a0a;
43
+ --color-foreground: #fafafa;
44
+ }
45
+ ```
46
+
47
+ Override at the highest scope where the change applies. Do not duplicate token values across files — change them at the source.
48
+
49
+ **Dark mode** is supported via `[data-theme="dark"]` on the root element. AppShell primitives respect it automatically. Custom components inherit dark-mode behaviour for free as long as they reference tokens (`bg-surface-1`, `text-fg-default`) and never inline literal colors.
50
+
51
+ ## 3. Component styling with data attributes
52
+
53
+ AppShell's UI components support data-attribute-based styling, following the [Base UI data attributes](https://base-ui.com/react/handbook/styling#data-attributes) convention. Components expose `data-`\* attributes that reflect their internal state, enabling CSS-only style control without JavaScript:
54
+
55
+ ```css
56
+ /* Style a component based on its state */
57
+ .SwitchThumb[data-checked] {
58
+ background-color: green;
59
+ }
60
+
61
+ .MenuItem[data-highlighted] {
62
+ background-color: var(--color-primary);
63
+ color: white;
64
+ }
65
+ ```
66
+
67
+ This works with Tailwind as well — **use theme tokens**, not raw Tailwind grays:
68
+
69
+ ```tsx
70
+ <Switch.Thumb className="bg-surface-3 data-[checked]:bg-primary" />
71
+ ```
72
+
73
+ Check each component's API reference (`components.md`) for the data attributes it exposes. Custom components must follow the same convention (see Section 6).
74
+
75
+ ## 4. Tokens
76
+
77
+ All values below are exposed by `theme.css`. **Use the token, never hand-type the value.** A hex literal or magic px in a PR is a review failure.
78
+
79
+ ### Color
80
+
81
+ Four families. Pick by **intent**, not by visual taste.
82
+
83
+ | Family | Token | Use | Tailwind |
84
+ | ---------- | ------------------------ | ----------------------------------- | ----------------------------- |
85
+ | Surface | `--color-surface-1` | page background | `bg-surface-1` |
86
+ | Surface | `--color-surface-2` | card on page | `bg-surface-2` |
87
+ | Surface | `--color-surface-3` | nested card on card | `bg-surface-3` |
88
+ | Foreground | `--color-fg-default` | primary text | `text-fg-default` |
89
+ | Foreground | `--color-fg-muted` | secondary text, descriptions | `text-fg-muted` |
90
+ | Foreground | `--color-fg-subtle` | tertiary text, captions, timestamps | `text-fg-subtle` |
91
+ | Brand | `--color-primary` | primary buttons, links, focus rings | `bg-primary` / `text-primary` |
92
+ | Brand | `--color-primary-hover` | brand hover state | `hover:bg-primary-hover` |
93
+ | Brand | `--color-primary-active` | brand pressed state | `active:bg-primary-active` |
94
+ | Status | `--color-danger` | destructive actions, errors | `bg-danger` / `text-danger` |
95
+ | Status | `--color-warning` | non-blocking caution | `bg-warning` / `text-warning` |
96
+ | Status | `--color-success` | confirmations, completed states | `bg-success` / `text-success` |
97
+ | Status | `--color-info` | neutral callouts | `bg-info` / `text-info` |
98
+
99
+ ```tsx
100
+ // Good — Button exposes a destructive variant; prefer variants over bolting tokens on className when available
101
+ <div className="bg-surface-1 text-fg-default">…</div>
102
+ <Button variant="destructive">Delete</Button>
103
+
104
+ // Bad — raw colors bypass the theme
105
+ <div style={{ background: "#fff", color: "#111" }}>…</div>
106
+ ```
107
+
108
+ Never reach for raw color names. If a status doesn't fit, that's a content problem, not a token problem.
109
+
110
+ ### Spacing
111
+
112
+ Linear scale on a 4px base. Padding, margin, and gap all come from this scale. Tailwind's `p-4`, `gap-2`, `mt-8` resolve to the same tokens.
113
+
114
+ | Token | Value | Common use |
115
+ | ------------ | ----- | ------------------------- |
116
+ | `--space-0` | 0 | reset |
117
+ | `--space-1` | 4px | tight icon-text gap |
118
+ | `--space-2` | 8px | inline gap, small padding |
119
+ | `--space-3` | 12px | row gap, button padding |
120
+ | `--space-4` | 16px | card padding, default gap |
121
+ | `--space-6` | 24px | section gap |
122
+ | `--space-8` | 32px | major section gap |
123
+ | `--space-12` | 48px | page section break |
124
+ | `--space-16` | 64px | hero spacing |
125
+
126
+ ```tsx
127
+ // Good — scale step
128
+ <div className="flex gap-3 p-4">…</div>
129
+
130
+ // Bad — magic value
131
+ <div className="p-[13px]">…</div>
132
+ ```
133
+
134
+ Hand-typing `padding: 13px` is a smell. Round to the nearest scale step; if nothing fits, the layout is wrong, not the scale.
135
+
136
+ ### Typography
137
+
138
+ Named roles. Each token bundles font-size + line-height + weight. Pick by **role**, not by size. Don't set `font-size` and `line-height` independently.
139
+
140
+ | Token | Tailwind | Use |
141
+ | --------- | -------------- | ---------------------------- |
142
+ | `display` | `text-display` | hero / marketing surfaces |
143
+ | `h1` | `text-h1` | page title |
144
+ | `h2` | `text-h2` | section heading |
145
+ | `h3` | `text-h3` | subsection / card title |
146
+ | `h4` | `text-h4` | nested heading |
147
+ | `body-lg` | `text-body-lg` | emphasised body |
148
+ | `body` | `text-body` | default body copy |
149
+ | `body-sm` | `text-body-sm` | dense rows, secondary copy |
150
+ | `caption` | `text-caption` | timestamps, labels, metadata |
151
+ | `mono` | `text-mono` | IDs, code, numbers in tables |
152
+
153
+ ```tsx
154
+ <h2 className="text-h2">Section</h2>
155
+ <p className="text-body text-fg-muted">Description copy</p>
156
+ <span className="text-caption text-fg-subtle">Updated 2h ago</span>
157
+ ```
158
+
159
+ ### Radius
160
+
161
+ Pick by component role. A card is always `md`, regardless of its size on screen.
162
+
163
+ | Token | Use |
164
+ | --------------- | ------------------- |
165
+ | `--radius-sm` | inputs, small chips |
166
+ | `--radius-md` | cards, buttons |
167
+ | `--radius-lg` | modals, sheets |
168
+ | `--radius-xl` | large surfaces |
169
+ | `--radius-full` | pills, avatars |
170
+
171
+ ### Elevation
172
+
173
+ Higher elevation reads as "more transient" — match the component's lifetime. Never hand-craft a `box-shadow`.
174
+
175
+ | Token | Use |
176
+ | --------------- | ------------------------ |
177
+ | `--elevation-0` | flat surface |
178
+ | `--elevation-1` | persistent panel, card |
179
+ | `--elevation-2` | sticky bar, hovered card |
180
+ | `--elevation-3` | popover, menu |
181
+ | `--elevation-4` | modal, dialog, sheet |
182
+
183
+ ### Motion
184
+
185
+ Duration paired with easing. Match motion to the change's lifetime — short events get short durations.
186
+
187
+ | Token | ~Duration | Use |
188
+ | ----------------- | --------- | ------------------------------ |
189
+ | `--motion-fast` | 120ms | hover, focus, button press |
190
+ | `--motion-base` | 200ms | state changes (toggle, select) |
191
+ | `--motion-slow` | 320ms | entrance, dialog open |
192
+ | `--motion-slower` | 500ms | full-page transitions |
193
+ | `--ease-out` | — | entrances, reveals |
194
+ | `--ease-in-out` | — | symmetric state changes |
195
+
196
+ ```css
197
+ .menu-item {
198
+ transition: background-color var(--motion-fast) var(--ease-out);
199
+ }
200
+
201
+ @media (prefers-reduced-motion: reduce) {
202
+ .menu-item {
203
+ transition: none;
204
+ }
205
+ }
206
+ ```
207
+
208
+ Always wrap motion in `@media (prefers-reduced-motion: reduce)` and collapse to instant or near-instant transitions. AppShell components handle this internally; custom components must do the same.
209
+
210
+ ### Z-index
211
+
212
+ Never invent a z value. If you need a new layer, add a token; never `z-index: 9999`. Popups and overlays share `50` intentionally — sequencing comes from DOM order, not z escalation.
213
+
214
+ | Token | Value | Use |
215
+ | ------------------ | ----- | ----------------------------- |
216
+ | `--z-sidebar` | 10 | persistent sidebar |
217
+ | `--z-sidebar-rail` | 10 | sidebar collapsed rail |
218
+ | `--z-popup` | 50 | menu, tooltip, popover |
219
+ | `--z-overlay` | 50 | modal, sheet, dialog backdrop |
220
+
221
+ ### Icon sizes
222
+
223
+ Pair icon size with the surrounding text scale. Pass via the `size` prop, not raw width/height.
224
+
225
+ | Token | Pairs with text |
226
+ | ----------- | --------------------- |
227
+ | `--icon-sm` | `body-sm`, `caption` |
228
+ | `--icon-md` | `body`, `body-lg` |
229
+ | `--icon-lg` | `h3`, `h4` |
230
+ | `--icon-xl` | `h1`, `h2`, `display` |
231
+
232
+ ```tsx
233
+ <Icon name="check" size="md" />
234
+ ```
235
+
236
+ ### Breakpoints
237
+
238
+ | Token | Width |
239
+ | ----- | ------ |
240
+ | `sm` | 640px |
241
+ | `md` | 768px |
242
+ | `lg` | 1024px |
243
+ | `xl` | 1280px |
244
+ | `2xl` | 1536px |
245
+
246
+ **ERP target is `xl`/`2xl` desktop.** Pages should be designed for those widths first; smaller breakpoints exist for graceful degradation, not parity. Don't waste effort on mobile-first composition unless a screen explicitly calls for it. A list page that collapses gracefully at `md` is fine; a list page redesigned for `sm` is over-investment.
247
+
248
+ Two-column **behavior** (right rail stacks under `**lg`**): respect AppShell defaults — do not force side-by-side grids on narrow viewports. `**Layout`column width table** numbers live in`**components.md` → Layout**; reuse them instead of guessing rem values here.
249
+
250
+ ## 5. The `astw:` prefix
251
+
252
+ AppShell exposes **layout / sizing / overflow** escapes on some components via props like `containerClassName`, `contentClassName`, `className` on roots. Prefix those utilities with `**astw:`\*\* so they apply to the wrapper AppShell controls.
253
+
254
+ **Do not duplicate full component trees here.** Typical patterns (full `**DataTable`** composition, `**Sheet`+ footer**,`**Table.Root` + card insets**) live in `**components.md`\*\* with JSX you can copy.
255
+
256
+ Minimal illustrations — same rules apply to other `*ClassName` hooks:
257
+
258
+ ```tsx
259
+ <Table.Root containerClassName="astw:px-6 astw:max-h-96 astw:overflow-y-auto" />
260
+ <Sheet contentClassName="astw:w-[480px] astw:flex astw:flex-col astw:gap-4" />
261
+ ```
262
+
263
+ Rules:
264
+
265
+ - `**astw:**` only on AppShell `*ClassName` / root `className` hooks each component exposes. Use **plain** Tailwind (`flex`, `gap-4`, `bg-surface-1`, …) on **your** markup.
266
+ - Stick to **layout** utilities (`flex`, `grid`, `max-h-*`, `min-h-0`, `overflow-*`, widths). Avoid painting over internal AppShell padding or colors via `astw:` — prefer an upstream prop or composition change.
267
+ - Steps like `**astw:p-4`\*\* still resolve through tokens — never arbitrary `astw:p-[13px]`.
268
+
269
+ ## 6. When AppShell doesn't have a component you need
270
+
271
+ Most ERP screens compose entirely from AppShell primitives. When you hit a gap, work through this decision tree before building anything:
272
+
273
+ ### Decision tree
274
+
275
+ 1. **Can you compose existing AppShell primitives?** A "card with metric and trend arrow" is `Card` + `Stat` + `Icon`, not a new component. Compose first.
276
+ 2. **If composition won't work, is the behavior one-off?** Build it locally under `src/components/<name>/` and flag it for the `build-component` skill, which promotes useful customs into AppShell upstream.
277
+ 3. **If it's already proven reusable across 2+ apps**, skip local entirely — use the `build-component` skill to add it to AppShell directly.
278
+
279
+ ### Conformance rules (non-negotiable for any custom component)
280
+
281
+ - **Tokens only.** No hex literals, no magic px values, no hand-rolled shadows. Every visual property maps to a token from Section 4.
282
+ - **Base UI data-attribute pattern for state.** Expose `data-*` attributes that reflect internal state; never style off React props alone. A custom toggle exposes `data-checked`; a custom step indicator exposes `data-active`, `data-completed`, etc.
283
+ - **Compose AppShell primitives inside.** If the custom needs a button, use `Button` — not raw `<button>`. Same for `Input`, `Badge`, `Icon`. The custom's job is composition, not reinvention.
284
+ - **Document with a `README.md`** in the component folder listing: purpose, props, tokens used, and a brief justification for why this can't be composed from existing AppShell primitives.
285
+ - **Match accessibility behavior** of the closest AppShell equivalent — focus management, ARIA roles/attributes, keyboard interactions. Reach for Base UI primitives if the behavior is non-trivial.
286
+
287
+ Example skeleton for a local custom component:
288
+
289
+ ```tsx
290
+ // src/components/StepIndicator/index.tsx
291
+ import { Icon } from "@tailor-platform/app-shell";
292
+
293
+ type Props = { steps: string[]; current: number };
294
+
295
+ export function StepIndicator({ steps, current }: Props) {
296
+ return (
297
+ <ol className="flex gap-2">
298
+ {steps.map((label, i) => (
299
+ <li
300
+ key={label}
301
+ data-active={i === current ? "" : undefined}
302
+ data-completed={i < current ? "" : undefined}
303
+ className="
304
+ flex items-center gap-2 px-3 py-2 rounded-md
305
+ bg-surface-2 text-fg-muted text-body-sm
306
+ data-[active]:bg-primary data-[active]:text-fg-default
307
+ data-[completed]:text-success
308
+ "
309
+ >
310
+ {i < current && <Icon name="check" size="sm" />}
311
+ {label}
312
+ </li>
313
+ ))}
314
+ </ol>
315
+ );
316
+ }
317
+ ```
318
+
319
+ Notice: tokens for every value, `data-*` attributes for state, no hex literals, AppShell `Icon` composed inside.
320
+
321
+ ### Promotion path
322
+
323
+ When a custom component proves reusable across 2+ apps, promote it upstream into AppShell via the `build-component` skill (a separate skill at `~/.claude/skills/build-component/`). The skill handles API design, exports, test coverage, and the upstream PR. Local custom components should be considered staging — not a permanent home.
324
+
325
+ ## 7. Quick reference
326
+
327
+ ### Where to look
328
+
329
+ | Concern | File |
330
+ | --------------------------------------------------- | ---------------------------------- |
331
+ | Component imports, props, JSX composition | **`components.md`** |
332
+ | Page / screen layout patterns | **`patterns/<type>/<slug>.md`** |
333
+ | Design tokens, `astw:` rules, custom UI conformance | **this file** (`design-system.md`) |
334
+
335
+ **`components.md`** holds long JSX compositions (e.g. **`DataTable`**, **card + `Table`**). **Do not copy those trees into this file** — link back here only for tokens and `astw:` policy.
336
+
337
+ `patterns/` is organised by page type: `patterns/list/`, `patterns/detail/`, `patterns/form/`, `patterns/interaction/`. Pick the slug matching the screen you're building.
338
+
339
+ ### Semantic decisions
340
+
341
+ | Intent | Pick |
342
+ | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
343
+ | Destructive action (delete, void) | Prefer **`Button`** `variant="destructive"`; use danger tokens on custom surfaces; confirm in dialog at elevation `4` |
344
+ | Non-blocking caution | `bg-warning` |
345
+ | Confirmation / completed state | `bg-success` |
346
+ | Neutral callout | `bg-info` |
347
+ | Persistent panel (sidebar, header) | elevation `1` |
348
+ | Hovered / sticky surface | elevation `2` |
349
+ | Popover / menu / tooltip | elevation `3`, `--motion-fast` |
350
+ | Modal / sheet / dialog | elevation `4`, `--motion-slow` entrance |
351
+ | Hover / focus transition | `--motion-fast` |
352
+ | State change (toggle, select) | `--motion-base` |
353
+ | Two-column detail at <1024 | right column collapses below main — do not override |
354
+ | Inline ID, code, table number | `text-mono` |
355
+ | Timestamp, label, subtle metadata | `text-caption text-fg-subtle` |
@@ -0,0 +1,205 @@
1
+ # GraphQL Conventions
2
+
3
+ Type-safe GraphQL via `gql.tada`. The frontend never hand-writes types — generated bindings come from `pnpm generate` (step 2 of the workflow) reading the deployed backend schema.
4
+
5
+ ## Setup
6
+
7
+ Every GraphQL-aware module imports from the local `@/graphql` barrel:
8
+
9
+ ```ts
10
+ import { graphql, type FragmentOf, readFragment } from "@/graphql";
11
+ ```
12
+
13
+ `@/graphql` is generated. Don't import `gql` from `urql` or `gql.tada` directly — go through the barrel so types stay coherent.
14
+
15
+ The urql client is wired in `App.tsx` (see [project-setup.md](project-setup.md)). Pages don't construct their own client.
16
+
17
+ ## Built-in operations vs custom resolvers
18
+
19
+ The Tailor Platform auto-generates per-entity queries when the resolver doc opts in via `gqlOperations: "query"`:
20
+
21
+ - `<entity>(id: ID!)` — get one
22
+ - `<entities>` — list, returned as a connection (`edges { node { ... } }`, plus `pageInfo`)
23
+
24
+ Mutations are always custom — they come from resolver docs (Tier 3) and live in `backend/src/resolvers/`. The generated types appear in `@/graphql` after `pnpm generate`.
25
+
26
+ ## Fragment collocation
27
+
28
+ **Each component owns its fragment.** The parent page imports the fragment and composes it into the page query. This is the single most important GraphQL convention in this skill.
29
+
30
+ ```tsx
31
+ // components/order-summary.tsx
32
+ import { graphql, type FragmentOf, readFragment } from "@/graphql";
33
+
34
+ export const OrderSummaryFragment = graphql(`
35
+ fragment OrderSummary on Order {
36
+ id
37
+ number
38
+ status
39
+ total
40
+ }
41
+ `);
42
+
43
+ export const OrderSummary = ({ order }: { order: FragmentOf<typeof OrderSummaryFragment> }) => {
44
+ const data = readFragment(OrderSummaryFragment, order);
45
+ return (
46
+ <div>
47
+ {data.number} — {data.status}
48
+ </div>
49
+ );
50
+ };
51
+ ```
52
+
53
+ ```tsx
54
+ // pages/orders/[id]/page.tsx
55
+ import { graphql } from "@/graphql";
56
+ import { OrderSummary, OrderSummaryFragment } from "@/components/order-summary";
57
+
58
+ const OrderQuery = graphql(
59
+ `
60
+ query Order($id: ID!) {
61
+ order(id: $id) {
62
+ ...OrderSummary
63
+ }
64
+ }
65
+ `,
66
+ [OrderSummaryFragment],
67
+ );
68
+ ```
69
+
70
+ Rules:
71
+
72
+ - Each component file exports `<Name>Fragment` alongside the component.
73
+ - The component's prop type is `FragmentOf<typeof <Name>Fragment>`. Always call `readFragment(<Name>Fragment, prop)` once to read it.
74
+ - The parent's query lists the fragment in the second `graphql(...)` argument (the dependency array). Forgetting the dep array works at runtime but loses type narrowing.
75
+ - Don't pass raw fields between components — pass the fragment value. This keeps types and queries in lockstep.
76
+
77
+ ## Connection pattern (lists)
78
+
79
+ Built-in `<entities>` queries return a connection. Use `edges { node { ... } }`:
80
+
81
+ ```tsx
82
+ const OrdersQuery = graphql(
83
+ `
84
+ query Orders($first: Int, $after: String) {
85
+ orders(first: $first, after: $after) {
86
+ edges {
87
+ node {
88
+ ...OrderSummary
89
+ }
90
+ }
91
+ pageInfo {
92
+ hasNextPage
93
+ endCursor
94
+ }
95
+ }
96
+ }
97
+ `,
98
+ [OrderSummaryFragment],
99
+ );
100
+
101
+ const [{ data }] = useQuery({ query: OrdersQuery, variables: { first: 25 } });
102
+ const orders = data?.orders.edges.map((e) => e.node) ?? [];
103
+ ```
104
+
105
+ For `pnpm gql-tada:check` to pass, always destructure with optional chaining and provide a fallback (`?? []`).
106
+
107
+ ### Wiring to `DataTable` (sort, filters, pagination)
108
+
109
+ For **standard list pages**, do not hand-roll only `first` / `after`. Compose AppShell **`DataTable`** + **`useCollectionVariables`** + **`useDataTable`**: derive query variables from `variables.pagination`, `variables.query`, `variables.order`, map `edges` / `pageInfo` / `total` into `useDataTable`’s `data`. See **`patterns/list/dense-scan.md`** and **`components.md` → DataTable**.
110
+
111
+ When `@tailor-platform/app-shell-sdk-plugin` codegen is enabled, pass generated **`tableMetadata`** into **`useCollectionVariables`** so `variables.query` / `variables.order` are typed against your Tailor GraphQL inputs (upstream **data-table.md** Typed query variables).
112
+
113
+ ## Mutations and cache invalidation
114
+
115
+ Mutations from custom resolvers appear in `@/graphql` after `pnpm generate`:
116
+
117
+ ```tsx
118
+ import { useMutation } from "urql";
119
+ import { graphql } from "@/graphql";
120
+ import { useToast } from "@tailor-platform/app-shell";
121
+
122
+ const CreateOrderMutation = graphql(`
123
+ mutation CreateOrder($input: CreateOrderInput!) {
124
+ createOrder(input: $input) {
125
+ id
126
+ number
127
+ }
128
+ }
129
+ `);
130
+
131
+ function CreateOrderButton() {
132
+ const toast = useToast();
133
+ const [{ fetching }, createOrder] = useMutation(CreateOrderMutation);
134
+
135
+ const handleSubmit = async (input: CreateOrderInput) => {
136
+ const result = await createOrder({ input });
137
+ if (result.error) {
138
+ toast.error(`Failed to create order: ${result.error.message}`);
139
+ return;
140
+ }
141
+ toast.success(`Order ${result.data!.createOrder.number} created`);
142
+ };
143
+ // ...
144
+ }
145
+ ```
146
+
147
+ ### `additionalTypenames`
148
+
149
+ When a mutation creates/updates/deletes an entity that another query already has cached, urql won't know to refetch unless you pass `additionalTypenames`. Use it on **both** the query and the mutation when they share a typename:
150
+
151
+ ```tsx
152
+ // Query side — declares it cares about the Order typename
153
+ const [{ data }] = useQuery({
154
+ query: OrdersQuery,
155
+ variables: { first: 25 },
156
+ context: { additionalTypenames: ["Order"] },
157
+ });
158
+
159
+ // Mutation side — declares it produces Order writes
160
+ const [, createOrder] = useMutation(CreateOrderMutation);
161
+ await createOrder(
162
+ { input },
163
+ { additionalTypenames: ["Order"] }, // triggers refetch of any query subscribed to 'Order'
164
+ );
165
+ ```
166
+
167
+ Skip `additionalTypenames` only when the mutation's return type already includes the affected entity _and_ every cached query for it touched the same `id` — urql will then refresh by id. When in doubt, add it.
168
+
169
+ ### Optimistic updates
170
+
171
+ Use sparingly — only for actions that:
172
+
173
+ 1. Are reversible (rollback is cheap on error)
174
+ 2. The user expects to feel instant (toggling a flag, reordering a list)
175
+
176
+ Prefer plain pending-state UI (button spinner) for everything else. See `interaction/toast.md` and `interaction/confirm.md` for feedback patterns.
177
+
178
+ ## Page conventions
179
+
180
+ Every page component:
181
+
182
+ 1. Is the **default export** of `page.tsx` (or `index.tsx` depending on routing convention — match siblings in `src/pages/`).
183
+ 2. Sets `appShellPageProps` with at least `meta.title`:
184
+
185
+ ```tsx
186
+ export default function OrdersListPage() {
187
+ /* ... */
188
+ }
189
+ OrdersListPage.appShellPageProps = {
190
+ meta: { title: "Orders" },
191
+ };
192
+ ```
193
+
194
+ 3. Uses `appShellPageProps.guards` for permission gates and `appShellPageProps.loader` for route loaders. See [project-setup.md](project-setup.md).
195
+
196
+ ## Quick reference
197
+
198
+ | Concern | Answer |
199
+ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
200
+ | Where do generated types come from? | `@/graphql` barrel, populated by `pnpm generate` |
201
+ | How do I add a field to a card? | Add it to the component's fragment, not the page query |
202
+ | Why isn't my list refreshing after a mutation? | Add `additionalTypenames: ['<Type>']` on both query and mutation |
203
+ | Connection vs array? | Always connection (`edges { node }`) for built-in `<entities>` queries; check the schema for custom resolvers |
204
+ | Pagination / sort / toolbar filters together? | `useCollectionVariables` + `DataTable`; see § **Wiring to `DataTable`** above |
205
+ | Where's the urql client configured? | `App.tsx` — see `project-setup.md` |