acuvo-code 0.2.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.
Files changed (154) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/ENTERPRISE.md +927 -0
  3. package/LICENSE +120 -0
  4. package/README.md +1245 -0
  5. package/ROADMAP.md +556 -0
  6. package/bin/acuvo-mcp.mjs +208 -0
  7. package/bin/acuvo.mjs +3108 -0
  8. package/lib/acceptance-consent.mjs +168 -0
  9. package/lib/acceptance.mjs +859 -0
  10. package/lib/account.mjs +226 -0
  11. package/lib/acuvo-dir.mjs +72 -0
  12. package/lib/acuvo-models.mjs +141 -0
  13. package/lib/apply-patch.mjs +570 -0
  14. package/lib/ask-user.mjs +173 -0
  15. package/lib/audit.mjs +530 -0
  16. package/lib/auto-lease.mjs +174 -0
  17. package/lib/background.mjs +842 -0
  18. package/lib/best-of.mjs +334 -0
  19. package/lib/board.mjs +232 -0
  20. package/lib/breaker.mjs +93 -0
  21. package/lib/budget.mjs +1375 -0
  22. package/lib/builtin-skills.mjs +135 -0
  23. package/lib/cache-floor.mjs +204 -0
  24. package/lib/chain.mjs +303 -0
  25. package/lib/changed-paths.mjs +84 -0
  26. package/lib/chat.mjs +434 -0
  27. package/lib/checkpoint.mjs +637 -0
  28. package/lib/child-lifetime.mjs +71 -0
  29. package/lib/cli-args.mjs +1255 -0
  30. package/lib/code-review.mjs +1382 -0
  31. package/lib/colour.mjs +82 -0
  32. package/lib/command.mjs +2847 -0
  33. package/lib/compact.mjs +1151 -0
  34. package/lib/completion.mjs +515 -0
  35. package/lib/creative-engines.mjs +779 -0
  36. package/lib/db-inspect.mjs +1624 -0
  37. package/lib/delete.mjs +111 -0
  38. package/lib/design-loop.mjs +570 -0
  39. package/lib/diff-preview.mjs +1044 -0
  40. package/lib/doctor.mjs +2139 -0
  41. package/lib/dropped.mjs +216 -0
  42. package/lib/edit-diagnostics.mjs +277 -0
  43. package/lib/edit.mjs +460 -0
  44. package/lib/env-file.mjs +250 -0
  45. package/lib/escalate.mjs +702 -0
  46. package/lib/evaluate.mjs +284 -0
  47. package/lib/fetch-text.mjs +952 -0
  48. package/lib/fleet-budget.mjs +256 -0
  49. package/lib/gh.mjs +1536 -0
  50. package/lib/git.mjs +1341 -0
  51. package/lib/github.mjs +261 -0
  52. package/lib/h2.mjs +194 -0
  53. package/lib/handoff.mjs +417 -0
  54. package/lib/hooks.mjs +626 -0
  55. package/lib/http-probe.mjs +907 -0
  56. package/lib/image-director.mjs +322 -0
  57. package/lib/image-edit.mjs +522 -0
  58. package/lib/imagegen.mjs +998 -0
  59. package/lib/interrupt.mjs +234 -0
  60. package/lib/learned.mjs +353 -0
  61. package/lib/lease-watch.mjs +115 -0
  62. package/lib/lease.mjs +868 -0
  63. package/lib/localize.mjs +834 -0
  64. package/lib/log-tail.mjs +1052 -0
  65. package/lib/login.mjs +157 -0
  66. package/lib/lsp.mjs +1613 -0
  67. package/lib/mcp-consent.mjs +377 -0
  68. package/lib/mcp-defaults.mjs +780 -0
  69. package/lib/mcp-server.mjs +1343 -0
  70. package/lib/mcp.mjs +1263 -0
  71. package/lib/media.mjs +1283 -0
  72. package/lib/memory-workspace.mjs +179 -0
  73. package/lib/model-json.mjs +132 -0
  74. package/lib/model-tier.mjs +171 -0
  75. package/lib/model.mjs +1445 -0
  76. package/lib/parallel.mjs +144 -0
  77. package/lib/perchance.mjs +210 -0
  78. package/lib/plan-coherence.mjs +1461 -0
  79. package/lib/plan-ledger.mjs +981 -0
  80. package/lib/plan.mjs +461 -0
  81. package/lib/policy.mjs +783 -0
  82. package/lib/prefix-order.mjs +38 -0
  83. package/lib/project-memory.mjs +127 -0
  84. package/lib/prompt.mjs +109 -0
  85. package/lib/python.mjs +862 -0
  86. package/lib/rcfile.mjs +853 -0
  87. package/lib/read-window.mjs +743 -0
  88. package/lib/refute-tools.mjs +34 -0
  89. package/lib/refute.mjs +806 -0
  90. package/lib/repl-driver.mjs +264 -0
  91. package/lib/repl.mjs +324 -0
  92. package/lib/replay.mjs +1218 -0
  93. package/lib/repo-map.mjs +1101 -0
  94. package/lib/report.mjs +419 -0
  95. package/lib/search-rank.mjs +99 -0
  96. package/lib/search.mjs +659 -0
  97. package/lib/secret-paths.mjs +54 -0
  98. package/lib/session.mjs +1017 -0
  99. package/lib/skills.mjs +703 -0
  100. package/lib/slash.mjs +356 -0
  101. package/lib/spawn-argv.mjs +1151 -0
  102. package/lib/spend.mjs +250 -0
  103. package/lib/steer.mjs +280 -0
  104. package/lib/stream.mjs +253 -0
  105. package/lib/stuck.mjs +712 -0
  106. package/lib/subagent.mjs +749 -0
  107. package/lib/terminal-graphics.mjs +171 -0
  108. package/lib/tool-prefix.mjs +226 -0
  109. package/lib/tool-shortlist.mjs +162 -0
  110. package/lib/tools.mjs +2333 -0
  111. package/lib/tsserver.mjs +423 -0
  112. package/lib/turn.mjs +5672 -0
  113. package/lib/untrusted-block.mjs +271 -0
  114. package/lib/verify-claim.mjs +299 -0
  115. package/lib/vision.mjs +330 -0
  116. package/lib/voice-task.mjs +561 -0
  117. package/lib/warm-provider.mjs +255 -0
  118. package/lib/websearch.mjs +401 -0
  119. package/lib/workspace.mjs +928 -0
  120. package/lib/write-approval.mjs +235 -0
  121. package/lib/write-many.mjs +162 -0
  122. package/package.json +62 -0
  123. package/scripts/bundle.mjs +768 -0
  124. package/scripts/cache-floor.mjs +176 -0
  125. package/scripts/machine.mjs +226 -0
  126. package/scripts/test.mjs +139 -0
  127. package/skills/accessibility.md +87 -0
  128. package/skills/acuvo-design-system.md +123 -0
  129. package/skills/animation.md +84 -0
  130. package/skills/api-design.md +82 -0
  131. package/skills/auth-and-sessions.md +78 -0
  132. package/skills/build-with-a-framework.md +101 -0
  133. package/skills/colour-and-contrast.md +112 -0
  134. package/skills/creative-engines.md +81 -0
  135. package/skills/css-layout.md +85 -0
  136. package/skills/data-and-charts.md +77 -0
  137. package/skills/debugging.md +76 -0
  138. package/skills/designing-by-looking.md +84 -0
  139. package/skills/error-handling.md +78 -0
  140. package/skills/forms-and-validation.md +93 -0
  141. package/skills/nextjs-app-router.md +75 -0
  142. package/skills/page-composition.md +103 -0
  143. package/skills/performance.md +77 -0
  144. package/skills/plan-before-building.md +52 -0
  145. package/skills/planning-and-delegating.md +72 -0
  146. package/skills/refactoring.md +70 -0
  147. package/skills/security-basics.md +76 -0
  148. package/skills/state-management.md +73 -0
  149. package/skills/supabase-multitenant.md +72 -0
  150. package/skills/typescript-strict.md +90 -0
  151. package/skills/typography.md +135 -0
  152. package/skills/verify-your-own-work.md +62 -0
  153. package/skills/web-app-quality.md +62 -0
  154. package/skills/working-in-the-background.md +64 -0
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: css-layout
3
+ description: Grid vs flex, spacing that scales, and building layouts that do not break at 320px
4
+ when: When laying out a page or component, or when something overflows or will not centre
5
+ ---
6
+
7
+ # CSS layout
8
+
9
+ ## Grid or flex — the question answers itself
10
+
11
+ - **Flex** = one dimension. A row of buttons, a nav bar, a card's inner stack.
12
+ - **Grid** = two dimensions, or *"I want the container to decide the shape"*.
13
+ Page layouts, card galleries, anything with rows AND columns.
14
+
15
+ If you are writing `flex-wrap` plus width percentages plus negative margins to
16
+ make a gallery, you want grid:
17
+
18
+ ```css
19
+ .gallery {
20
+ display: grid;
21
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
22
+ gap: 1.5rem;
23
+ }
24
+ ```
25
+
26
+ ⭐ That is a responsive gallery with **no media queries at all** — it reflows on
27
+ available width, which is what you actually meant.
28
+
29
+ ## Use `gap`, not margins between children
30
+
31
+ `gap` puts space *between* items and never before the first or after the last.
32
+ `margin-right` on every child plus `:last-child { margin-right: 0 }` is the old
33
+ workaround, and it is still wrong when the row wraps.
34
+
35
+ ## ⚠️ Test at 320px. That is where layouts break.
36
+
37
+ Not 1440. The failure modes are always the same:
38
+
39
+ - a fixed `width` that should be `max-width`
40
+ - a long unbroken string (a URL, an email) forcing horizontal scroll —
41
+ `overflow-wrap: anywhere` on the container
42
+ - a table with no `overflow-x: auto` wrapper
43
+ - padding that eats the whole viewport
44
+
45
+ ⭐ **The body must never scroll horizontally.** Wide content scrolls inside its
46
+ own container, not the page.
47
+
48
+ ## Centring
49
+
50
+ ```css
51
+ .center { display: grid; place-items: center; } /* both axes, done */
52
+ ```
53
+
54
+ For a page column: `margin-inline: auto` with a `max-width`. Do not use
55
+ absolute positioning and transforms for something the layout can do.
56
+
57
+ ## Spacing on a scale, not by feel
58
+
59
+ Pick a scale (4 / 8 / 12 / 16 / 24 / 32 / 48) and use only those values. A
60
+ layout with `13px` here and `17px` there looks unresolved even when nobody can
61
+ say why. Put them in custom properties and reference those.
62
+
63
+ ## The modern tools, in order of how often they help
64
+
65
+ 1. **`clamp()`** — fluid type and spacing without breakpoints:
66
+ `font-size: clamp(1.5rem, 4vw, 3rem)`
67
+ 2. **Container queries** — a component that adapts to ITS box rather than the
68
+ viewport, which is what a card in a sidebar actually needs:
69
+ `@container (min-width: 400px) { … }`
70
+ 3. **Logical properties** — `padding-inline`, `margin-block`. They do the right
71
+ thing in every writing direction and cost nothing to adopt.
72
+ 4. **`aspect-ratio`** — reserve space for media and stop the reflow.
73
+
74
+ ## ⚠️ z-index is not a ladder to climb
75
+
76
+ `z-index: 9999` means someone lost an argument with the stacking context. Define
77
+ a small set of layers (base, dropdown, modal, toast) as custom properties and
78
+ use those names. And remember z-index only applies to positioned elements.
79
+
80
+ ## Do not fight the flow
81
+
82
+ `position: absolute` for something that should be in the document, `height:
83
+ 100vh` on a mobile browser whose chrome hides and reappears (use `100dvh`),
84
+ `!important` to beat your own selector — each is a signal that the layout is
85
+ being forced rather than described.
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: data-and-charts
3
+ description: Picking a chart that answers the question, and the defaults that make a dashboard readable
4
+ when: When building a dashboard, a report, or anything that shows numbers to a person
5
+ ---
6
+
7
+ # Data and charts
8
+
9
+ ## Pick the chart from the QUESTION, not from the data
10
+
11
+ | the question | the chart |
12
+ |---|---|
13
+ | how has this changed over time? | line |
14
+ | which of these is biggest? | horizontal bar |
15
+ | what is this made of? | stacked bar — **not** a pie |
16
+ | are these two related? | scatter |
17
+ | what is the single number right now? | just print the number, large |
18
+
19
+ ⭐ The last row is the one most often over-built. A metric that is one number
20
+ should be one number. A gauge, a donut and a sparkline around it are decoration
21
+ that makes the number harder to read.
22
+
23
+ ## ⚠️ Never a pie chart for more than five things
24
+
25
+ People compare angles badly. Past about five slices a pie becomes a legend with
26
+ a picture attached, and the reader has to look things up. A horizontal bar chart
27
+ sorted descending answers "which is biggest" instantly and takes the same space.
28
+
29
+ Never a pie for values that do not sum to a meaningful whole.
30
+
31
+ ## Sort bars, unless the order means something
32
+
33
+ Alphabetical is the wrong default — it scatters the answer. Sort descending by
34
+ value. The exceptions are categories with an inherent order: days of the week,
35
+ age bands, survey responses from "never" to "always".
36
+
37
+ ## The axis rules that change conclusions
38
+
39
+ - **Bar charts start at zero.** Truncating the axis exaggerates differences and
40
+ is the single most common way a chart misleads.
41
+ - **Line charts need not** start at zero — the shape of the change is the point.
42
+ - Label the units. "Revenue" is not a unit; "Revenue (A$, ex GST)" is.
43
+
44
+ ## ⚠️ Show the empty and loading states
45
+
46
+ A dashboard with no data yet is the state every user sees FIRST, and it is
47
+ usually the one nobody designed. Say what will appear and why it is empty:
48
+
49
+ ```
50
+ ✗ (a blank rectangle)
51
+ ✓ "No calls yet. This fills in once your first campaign runs."
52
+ ```
53
+
54
+ And distinguish **empty** from **failed to load** — see `error-handling`. A
55
+ chart that renders zero because the request 500'd is a lie told in pictures.
56
+
57
+ ## Numbers people can read
58
+
59
+ - Round to the precision that matters: `A$1,284` not `A$1284.3891`
60
+ - Thousands separators, always
61
+ - Percentages need a base: "12% (of 340 calls)"
62
+ - Dates in a form with no ambiguity: `18 Aug 2026`, never `08/09/26`
63
+
64
+ ## Colour carries meaning or it carries nothing
65
+
66
+ Use one accent for the series that matters and grey for context. A chart where
67
+ every series is a different bright colour has told the reader that everything is
68
+ equally important, which is never true.
69
+
70
+ ⚠️ Never encode meaning in colour ALONE — about 1 in 12 men cannot separate red
71
+ from green. Label the line, or vary the shape.
72
+
73
+ ## Tables are underrated
74
+
75
+ If the reader's real question is "what exactly was this number", a sorted table
76
+ with aligned right-hand numerals beats every chart. Charts are for shape;
77
+ tables are for values.
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: debugging
3
+ description: Read the error literally, doubt the check before the code, and change one thing at a time
4
+ when: When something does not work, a test fails, or a fix did not take effect
5
+ ---
6
+
7
+ # Debugging
8
+
9
+ ## ⚠️⚠️ Read the error literally. It is usually telling the truth.
10
+
11
+ `x is not a function` means `x` is not a function. Not "the import is
12
+ circular", not "the bundler is confused" — **print `x` and look at it.** Most
13
+ time lost to debugging is spent on a theory formed before reading the message.
14
+
15
+ Ask, in this order:
16
+
17
+ 1. What does the message actually say?
18
+ 2. What is the value it is complaining about? Print it.
19
+ 3. Which line? Read that line, not the one you assume it means.
20
+
21
+ ## ⚠️⚠️ Doubt the check before you doubt the code
22
+
23
+ When a test fails on code you believe is correct, one of two things is true, and
24
+ the expensive mistake is assuming it is the first:
25
+
26
+ - the code is wrong, **or**
27
+ - **the check is wrong** — and a check that fails correct work is worse than no
28
+ check, because it sends you to rewrite something that already worked.
29
+
30
+ Prove which: **break the code deliberately and see whether the check notices.**
31
+ A check that stays green when you break the thing it guards was never testing
32
+ it. See `verify-your-own-work`.
33
+
34
+ ## The fix that "did not work"
35
+
36
+ Before concluding a fix is wrong, prove it ran:
37
+
38
+ - Are you editing the file that is actually loaded? (Two files with the same
39
+ name in different directories is the classic.)
40
+ - Did the build/dev server pick it up? Stale cache?
41
+ - Is the branch you edited the branch you ran?
42
+ - Is something later overwriting it?
43
+
44
+ ⭐ Put a deliberate marker in — `throw new Error('REACHED')` — and confirm you
45
+ see it. If you do not, the problem was never the logic.
46
+
47
+ ## Change one thing at a time
48
+
49
+ Three changes then a test tells you nothing about which mattered — and if it
50
+ still fails you now have four suspects. One change, one observation.
51
+
52
+ Keep the last known-good state reachable so you can always get back.
53
+
54
+ ## Bisect when you have no theory
55
+
56
+ If it worked before and does not now, the fastest route is not thinking harder,
57
+ it is **halving the search space**: `git bisect`, comment out half the file,
58
+ disable half the config. Two or three halvings usually beats an hour of staring.
59
+
60
+ ## ⚠️ Reproduce it before you fix it
61
+
62
+ A bug you cannot reproduce is a bug you cannot confirm you fixed. If it happens
63
+ "sometimes", find the input that makes it happen every time first — that
64
+ usually IS the diagnosis.
65
+
66
+ ## Say what you know and what you assume
67
+
68
+ "The request 404s" is an observation. "The route is not registered" is a theory.
69
+ Keeping the two apart is what stops you fixing a route that was fine while the
70
+ real fault — a typo in the URL — sits unexamined.
71
+
72
+ ## When you are truly stuck
73
+
74
+ Explain the problem out loud from the beginning, including the parts you are
75
+ sure about. The wrong assumption is almost always in the part you did not think
76
+ was worth saying.
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: designing-by-looking
3
+ description: Render it and LOOK — the see_page loop, not better CSS written blind
4
+ when: After building or restyling any page, component or app, and before telling anyone it is done
5
+ ---
6
+
7
+ # Designing By Looking
8
+
9
+ ## ⚠️⚠️ You cannot judge a page you have not seen
10
+
11
+ You have `see_page`. It renders an HTML file in a real browser and returns a
12
+ screenshot **plus measured problems** — invisible text, overflow, cramped
13
+ sections. Use it.
14
+
15
+ **A page you have not looked at is not finished, it is only written.** Markup
16
+ that reads correctly is routinely broken on screen: white text on white,
17
+ a section 500px tall holding one line, a row that overflows on the narrow
18
+ viewport, a delete button stretched across the full width.
19
+
20
+ Those are the exact defects that shipped in real builds here. Every one of them
21
+ is invisible in the source and obvious in a screenshot.
22
+
23
+ ## ⭐⭐ The loop, in order
24
+
25
+ 1. **Build** the page.
26
+ 2. **`see_page`** on the file you just wrote.
27
+ 3. **Read the findings first, then look at the screenshot.** The findings are
28
+ measured; your impression is not.
29
+ 4. **Fix the specific thing.** One at a time.
30
+ 5. **Look again.** A fix you did not re-check is a hope.
31
+
32
+ ⭐ **Repeat until the screenshot is good, not until the code looks right.** The
33
+ loop is the method. Better prompts are not the method — a page improves because
34
+ someone looked at it, noticed a real defect, and fixed that defect.
35
+
36
+ ## ⚠️ Do not fix blind
37
+
38
+ The failure mode is reading your own CSS, forming a theory about what it must
39
+ look like, and rewriting it without rendering. That is how a "fix" makes the
40
+ page worse and nobody notices for three rounds.
41
+
42
+ If `see_page` reports overflow, you do not need a theory about why. Look at the
43
+ screenshot, find the element that is too wide, and constrain that element.
44
+
45
+ ## ⭐ What to actually check when you look
46
+
47
+ Ask these in this order, because they fail in this order:
48
+
49
+ 1. **Is every piece of text visible?** Contrast, and not clipped.
50
+ 2. **Does anything overflow?** Especially tables, code blocks, long words and
51
+ button rows.
52
+ 3. **Is there dead space?** A section far taller than its content reads as
53
+ broken, not airy.
54
+ 4. **Is there ONE clear first thing to look at?** If everything is the same
55
+ weight, nothing is.
56
+ 5. **Do the controls look like controls?** A full-width bar is not a button.
57
+ 6. **Is there a real `<h1>`?** Pages ship without one constantly.
58
+
59
+ ## ⭐ `read_image` for anything you generated
60
+
61
+ `see_page` is for pages. For an image you made with `generate_image`, use
62
+ **`read_image`** — it returns a factual description, and you can ask it a
63
+ question about the file.
64
+
65
+ **Generating an image and never looking at it is the same mistake as shipping an
66
+ unrendered page.** Text inside generated images is misspelled often enough that
67
+ it must be checked every time, not assumed.
68
+
69
+ ## ⚠️ Looking costs tokens — so look deliberately, not constantly
70
+
71
+ A screenshot is charged as image tokens (roughly one token per 750 pixels, with
72
+ the long edge capped at 1568). That is cheap, but it is not free.
73
+
74
+ ⭐ **Look after a meaningful change, not after every line.** Build the whole
75
+ page, then look. Fix the three findings, then look again. Two or three passes
76
+ usually settles a page; ten means you are guessing between them.
77
+
78
+ ## The bar
79
+
80
+ Not "does this render". The bar is: **does it look like someone designed it,
81
+ or does it look like a template with content dropped in?**
82
+
83
+ Related skills: `acuvo-design-system`, `page-composition`, `typography`,
84
+ `colour-and-contrast`, `verify-your-own-work`.
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: error-handling
3
+ description: Empty is not unreadable is not never-instrumented — and why a swallowed error is the expensive one
4
+ when: When writing a catch block, a default value, or anything that reports a result
5
+ ---
6
+
7
+ # Errors
8
+
9
+ ## ⚠️⚠️ The house rule: empty ≠ unreadable ≠ never-instrumented
10
+
11
+ Three different facts get flattened into one innocent-looking value:
12
+
13
+ | what happened | what it means | what it must NOT return |
14
+ |---|---|---|
15
+ | we looked, there is nothing | **empty** | — |
16
+ | we looked and could not read it | **failure** | `[]` |
17
+ | nobody ever measured this | **unknown** | `0`, `false`, "fine" |
18
+
19
+ Returning `[]` for "the query failed" is the bug that hides for months, because
20
+ every caller downstream reads it as "there are none" and behaves perfectly
21
+ sensibly on a false premise.
22
+
23
+ ```js
24
+ ✗ try { return await load(); } catch { return []; }
25
+ ✓ try { return { ok: true, rows: await load() }; }
26
+ catch (e) { return { ok: false, error: String(e) }; }
27
+ ```
28
+
29
+ The same rule for a check: a check that could not run reports **unknown**, never
30
+ **pass**. "No problems found" is a real claim and has to be earned.
31
+
32
+ ## A caught error you do nothing with is worse than a crash
33
+
34
+ A crash tells you where and when. A swallowed error produces a program that is
35
+ subtly wrong later, somewhere else, for reasons nobody can trace.
36
+
37
+ ```js
38
+ ✗ catch (e) {}
39
+ ✗ catch (e) { console.log('oops'); }
40
+ ✓ catch (e) { console.error('loading invoices failed', e); showError(e); }
41
+ ```
42
+
43
+ ⭐ If you genuinely want to ignore one, say why in a comment. That comment is
44
+ the difference between a decision and an oversight:
45
+
46
+ ```js
47
+ catch { /* an observation must never break the thing it observes */ }
48
+ ```
49
+
50
+ ## Error messages are read by two audiences
51
+
52
+ - **The user** needs to know what to do: *"Could not save — you are offline.
53
+ Your changes are kept, try again."*
54
+ - **You** need to know what broke: the stack, the input, the identifiers.
55
+
56
+ Do not show the user a stack trace, and do not log only "error occurred".
57
+
58
+ ## ⚠️ Fail the whole operation, not half of it
59
+
60
+ A loop that catches per item and continues quietly produces a half-written
61
+ result nobody knows is half-written. Either:
62
+
63
+ - collect the failures and report them together at the end, or
64
+ - stop on the first one.
65
+
66
+ Silently skipping item 7 of 200 is the option that costs the most later.
67
+
68
+ ## Errors at the boundary
69
+
70
+ Every network call has three outcomes, not two: **success**, **failure**, and
71
+ **still going**. A UI that only models the first two shows nothing at all while
72
+ the request is in flight, and users click again.
73
+
74
+ ## ⚠️ Never let an error string tell you what to do
75
+
76
+ *"try again"* in a message is not instruction — it is the remote end's guess. A
77
+ retry loop that trusts it can spend an entire budget on a service that is down.
78
+ Retry on a policy you chose, with a ceiling you chose.
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: forms-and-validation
3
+ description: Building a form that actually works — validation timing, error copy, and the states people forget
4
+ when: Whenever the thing you are building contains an input, a form, or a submit button
5
+ ---
6
+
7
+ # Forms
8
+
9
+ The most common thing a generated app contains, and the most commonly half-built.
10
+
11
+ ## The four states every form has
12
+
13
+ A form is not "empty" and "filled". It is:
14
+
15
+ 1. **empty** — nothing typed, no errors shown yet
16
+ 2. **invalid** — the user has been told what is wrong, in words
17
+ 3. **submitting** — the button is disabled and says so
18
+ 4. **done** — the user can see it worked
19
+
20
+ Skipping 3 is how a form gets double-submitted. Skipping 4 is how a user fills
21
+ it in twice because nothing appeared to happen.
22
+
23
+ ## ⚠️ Validation timing is the difference between helpful and hostile
24
+
25
+ - **Do not validate while the user is still typing the first time.** Turning a
26
+ field red at the third character of an email address is punishing someone for
27
+ not having finished.
28
+ - **Validate on blur**, and again on submit.
29
+ - **Once a field has errored, re-validate as they type** — so the error clears
30
+ the moment they fix it, rather than after they leave the field again.
31
+
32
+ ## Error messages say what to do
33
+
34
+ ```
35
+ ✗ "Invalid input"
36
+ ✗ "Error: field required"
37
+ ✓ "Enter an email address — we send the confirmation there"
38
+ ✓ "Password needs 8 characters or more"
39
+ ```
40
+
41
+ Put the message next to the field, not in a banner at the top. A banner cannot
42
+ say *which* of nine fields is wrong.
43
+
44
+ ## ⚠️⚠️ Never trust the client
45
+
46
+ Client validation is a convenience for the user. It is not a check. Anything
47
+ that matters is validated again on the server, because the client is a program
48
+ the user controls.
49
+
50
+ Marking a field `required` in HTML and nowhere else means the first person to
51
+ open devtools sends whatever they like.
52
+
53
+ ## Accessibility, which is three attributes
54
+
55
+ ```html
56
+ <label for="email">Email</label>
57
+ <input id="email" name="email" type="email" aria-describedby="email-error">
58
+ <p id="email-error" role="alert">Enter an email address</p>
59
+ ```
60
+
61
+ A placeholder is not a label — it disappears exactly when the user needs it,
62
+ and screen readers do not reliably announce it.
63
+
64
+ ## Use the right input type
65
+
66
+ `type="email"`, `type="tel"`, `type="number"`, `inputmode="numeric"`. On a
67
+ phone this changes the keyboard that appears, which is a real difference in
68
+ whether the form gets completed.
69
+
70
+ ## The submit handler
71
+
72
+ ```js
73
+ form.addEventListener('submit', async (e) => {
74
+ e.preventDefault(); // or the page reloads and the work is lost
75
+ if (busy) return; // guard the double-click
76
+ busy = true;
77
+ button.disabled = true;
78
+ button.textContent = 'Saving…';
79
+ try {
80
+ await save(new FormData(form));
81
+ showSuccess();
82
+ } catch (err) {
83
+ showError(err); // tell the user, do not swallow it
84
+ } finally {
85
+ busy = false; // ⚠️ in finally, or one failure freezes the form forever
86
+ button.disabled = false;
87
+ button.textContent = 'Save';
88
+ }
89
+ });
90
+ ```
91
+
92
+ ⚠️ The `finally` is the part that gets left out. Without it, a single failed
93
+ request leaves the button disabled and the user with no way to retry.
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: nextjs-app-router
3
+ description: Next.js App Router — server vs client components, data loading, the mistakes that cost hours
4
+ when: Working in a Next.js project, or any app/ directory with page.tsx
5
+ ---
6
+
7
+ # Next.js App Router
8
+
9
+ ## The rule everything else follows
10
+
11
+ **Server components fetch. Client components render.** A component is a server
12
+ component unless it says `'use client'`.
13
+
14
+ Put every read in the page (server), pass results down as props. A child that
15
+ fetches forms its own opinion about a failure the page already has one sentence
16
+ for — and its failure renders as an empty state, which is indistinguishable from
17
+ "nothing happened".
18
+
19
+ ```tsx
20
+ // page.tsx — server. Reads, decides, passes down.
21
+ export default async function Page({ params }: { params: { id: string } }) {
22
+ const { rows, error } = await read(params.id);
23
+ if (error) return <Unreadable why={error} />; // ONE place says "broken"
24
+ return <Detail rows={rows} />; // the child only renders
25
+ }
26
+ ```
27
+
28
+ ⚠️ `'use client'` is contagious downward. A client component's children are all
29
+ client. Push it to the leaf that actually needs interactivity — a single
30
+ `<LikeButton />`, not the page.
31
+
32
+ ## What forces 'use client'
33
+
34
+ `useState` · `useEffect` · `useRef` · event handlers (`onClick`) · browser APIs
35
+ (`window`, `localStorage`, `IntersectionObserver`). Nothing else.
36
+
37
+ ## Params are async in Next 15+
38
+
39
+ ```tsx
40
+ export default async function Page({ params }: { params: Promise<{ id: string }> }) {
41
+ const { id } = await params;
42
+ }
43
+ ```
44
+ ⚠️ Getting this wrong gives a confusing runtime error, not a type error.
45
+
46
+ ## Caching, which is where the hours go
47
+
48
+ - `export const dynamic = 'force-dynamic'` when the page must read fresh data
49
+ every request. Without it you can ship a dashboard that shows build-time data
50
+ forever and looks merely "stale" rather than broken.
51
+ - `revalidate = N` for periodic refresh.
52
+ - Mutations use a server action or route handler, then `revalidatePath()`.
53
+
54
+ ## Common failures, in order of how often they bite
55
+
56
+ 1. **A server component imported into a client component** — everything below
57
+ becomes client and the fetch breaks. Pass it as `children` instead.
58
+ 2. **`useSearchParams` without `<Suspense>`** — build error at the very end.
59
+ 3. **Env vars.** Only `NEXT_PUBLIC_*` reach the browser. A secret read in a
60
+ client component is `undefined`, not an error.
61
+ 4. **Hydration mismatch** — `Date.now()`, `Math.random()` or `localStorage` in
62
+ render. Move to `useEffect`.
63
+ 5. **Images** — `next/image` needs `width`/`height` or `fill` + a positioned
64
+ parent.
65
+
66
+ ## Route handlers
67
+
68
+ ```ts
69
+ export async function POST(req: Request) {
70
+ const body = await req.json(); // throws on bad JSON — catch it
71
+ return Response.json({ ok: true }, { status: 200 });
72
+ }
73
+ ```
74
+ Always validate the body. Never trust `body.tenantId` from the client to decide
75
+ what data to return — derive it from the authenticated session.
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: page-composition
3
+ description: Visual hierarchy, whitespace and section anatomy — why a technically correct page still looks generated
4
+ when: Laying out any marketing page, landing page or app screen, or when output looks flat and evenly grey
5
+ ---
6
+
7
+ # Composition
8
+
9
+ `css-layout` covers the mechanics — grid, flex, gap, breakpoints. This is the
10
+ layer above it: **where things go and how big they are relative to each other.**
11
+ A page can be flawlessly implemented and still look generated, and this is
12
+ almost always why.
13
+
14
+ ## ⚠️ The failure has a shape: everything is the same size
15
+
16
+ Generated pages tend toward uniformity — three equal cards, then three more,
17
+ each section the same height, the same padding, the same weight. Nothing
18
+ dominates, so the eye has nowhere to land, and the page reads as a list rather
19
+ than a designed thing.
20
+
21
+ ⭐ **Every screen needs ONE dominant element.** In a hero it is the headline. In
22
+ a dashboard it is the number that matters. Make it dominant by **scale
23
+ contrast**, not decoration: 3–4× the body size, not a border and a badge.
24
+
25
+ ## Whitespace is the material, not the gap
26
+
27
+ - **Space between sections should be 3–5× the space inside them.** That single
28
+ ratio is most of what separates a designed page from a template. If section
29
+ padding is `24px`, the space between sections is `96–120px`, not `32px`.
30
+ - ⚠️ **Generated pages are almost always too tight.** When unsure, add more.
31
+ Cramped reads as cheap; generous reads as confident.
32
+ - Group by proximity. A label 4px from its input and 40px from the next field
33
+ needs no box drawn around it. **Proximity beats borders** — reach for space
34
+ before reaching for a card.
35
+
36
+ ```css
37
+ section { padding-block: clamp(4rem, 10vw, 8rem); }
38
+ .section-inner { display: grid; gap: 1.5rem; max-width: 72rem; margin-inline: auto; padding-inline: 1.5rem; }
39
+ ```
40
+
41
+ ## Break the symmetry, deliberately
42
+
43
+ Three equal columns is the default and the default is what looks generated.
44
+
45
+ - **Asymmetric splits read as designed**: 7/5, 8/4, 2fr/3fr. A hero with text
46
+ at 55% and an image at 45% has tension; 50/50 has none.
47
+ - **Vary the rhythm down the page**: full-bleed, then contained, then two-up,
48
+ then contained. Four identical stacked sections is the problem.
49
+ - ⚠️ **Do not centre everything.** Centred headline, centred paragraph, centred
50
+ button, section after section — it is the most common generated-page shape
51
+ there is. Left-align body content; centre only short, deliberate moments.
52
+
53
+ ## Landing page anatomy — what earns its place
54
+
55
+ In order. Each section answers one question and then gets out of the way.
56
+
57
+ 1. **Hero** — *what is this and who is it for?* One headline (a claim, not a
58
+ slogan), one sentence of support, one primary action. Optionally one real
59
+ image. ⚠️ Not three buttons, not a feature list.
60
+ 2. **Proof** — *why believe you?* Logos, a number, a testimonial with a real
61
+ name and role. Immediately after the hero, because it is the first objection.
62
+ 3. **What it does** — 3–4 concrete capabilities in the customer's words.
63
+ Outcomes, not features.
64
+ 4. **How it works** — 3 steps. This is where a diagram or screenshot earns
65
+ its place.
66
+ 5. **Objection handling** — pricing, FAQ, guarantee. Whatever the actual
67
+ hesitation is for that business.
68
+ 6. **Close** — the same action as the hero, restated. Someone who read to the
69
+ bottom is ready; do not make them scroll back up.
70
+
71
+ ⭐ **Cut before adding.** A five-section page where every section does work
72
+ beats a nine-section page padded with "Our Values". If a section does not answer
73
+ one of those questions, delete it.
74
+
75
+ ## Images
76
+
77
+ - **A real photograph is worth more than any amount of decoration.** Use the
78
+ image marker so a real one is generated rather than a grey box.
79
+ - **Give images a job.** A hero image should show the thing or the person, not
80
+ an abstract swoop. For a trade business: the van, the work, the person. For a
81
+ product: the product in use.
82
+ - Always `object-fit: cover` with a fixed aspect-ratio so layouts do not jump:
83
+ `aspect-ratio: 16/9; object-fit: cover; width: 100%`.
84
+ - ⚠️ Never a stock-photo handshake, never a generic "team in a meeting". It
85
+ reads as filler and undercuts everything around it.
86
+
87
+ ## Density is a choice you make once
88
+
89
+ A marketing page is **spacious** — few elements, large type, lots of air.
90
+ A dashboard is **dense** — small type, tight rows, many things visible.
91
+
92
+ ⚠️ Mixing the two is jarring. A dashboard with hero-sized headings wastes the
93
+ screen; a landing page with dashboard density looks like a spreadsheet. Decide
94
+ which the brief is asking for before setting a single size.
95
+
96
+ ## Before calling it done
97
+
98
+ - One element on each screen is clearly dominant.
99
+ - Space between sections is several times the space inside them.
100
+ - At least one section breaks the symmetry of the others.
101
+ - Not everything is centred.
102
+ - Every section answers a question a visitor actually has.
103
+ - At 320px it is still one readable column, and nothing overflows sideways.