@pikku/skills 0.12.32 → 0.12.34
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.
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-architect/SKILL.md +23 -16
- package/skills/pikku-build/SKILL.md +21 -17
- package/skills/pikku-build/references/app.md +61 -30
- package/skills/pikku-build/references/design.md +218 -0
- package/skills/pikku-build/references/theming.md +122 -0
- package/skills/pikku-knowledge/SKILL.md +2 -2
package/package.json
CHANGED
|
@@ -3,9 +3,9 @@ name: pikku-architect
|
|
|
3
3
|
description: >-
|
|
4
4
|
Use to turn one settled milestone note into the technical plan the build is measured against —
|
|
5
5
|
the tables, functions, wires, roles, scopes, screens and scenarios it owes, split into passes and
|
|
6
|
-
written through `pikku knowledge plan set`.
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
written through `pikku knowledge plan set`. The plan is the denominator
|
|
7
|
+
`pikku knowledge plan progress` divides by, so it is written BEFORE any of the milestone's code
|
|
8
|
+
exists and never edited afterwards to match what got built. TRIGGER when: a milestone note is settled and the next step is
|
|
9
9
|
planning it, the user asks to plan or architect a milestone, `pikku knowledge plan progress` says
|
|
10
10
|
a milestone has no plan, or pikku-build's App mode reaches a milestone with nothing planned. DO
|
|
11
11
|
NOT TRIGGER when: the milestone notes themselves are still being written (use pikku-knowledge),
|
|
@@ -17,18 +17,19 @@ installGroups: [core]
|
|
|
17
17
|
# Plan one milestone
|
|
18
18
|
|
|
19
19
|
A milestone note says what the app must DO and how it must feel for the person using it. It
|
|
20
|
-
deliberately does not say how.
|
|
21
|
-
|
|
20
|
+
deliberately does not say how. This is where how gets decided, once, in writing, before any of
|
|
21
|
+
it is built.
|
|
22
22
|
|
|
23
|
-
**Why
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
**Why the plan comes first and stays fixed.** A builder who plans after seeing its own work can
|
|
24
|
+
build a fraction, plan only that fraction, and certify itself complete — `pikku knowledge plan
|
|
25
|
+
progress` then divides by a denominator chosen after the answer. The defence is the ORDER: the plan
|
|
26
|
+
is written against the note, in its own turn, before a single migration for it exists, and is never
|
|
27
|
+
edited afterwards. An item that will not land is deferred with its reason through `plan defer`,
|
|
28
|
+
not rewritten out of the plan. The same agent plans and then builds; nothing hands off.
|
|
28
29
|
|
|
29
|
-
**One milestone, one plan, then
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
**One milestone, one plan, then build it.** Do not plan the next milestone "while you are here" —
|
|
31
|
+
the notes after this one are still allowed to change, and a plan written against a note that later
|
|
32
|
+
moves is worse than no plan.
|
|
32
33
|
|
|
33
34
|
---
|
|
34
35
|
|
|
@@ -38,6 +39,11 @@ The plan reaches disk through `pikku knowledge plan set <milestone> <file>` and
|
|
|
38
39
|
validates first and names the field that is wrong if it refuses; a plan file written with an editor
|
|
39
40
|
is a plan nothing checked, and the place that discovers that is a finished build.
|
|
40
41
|
|
|
42
|
+
It is also written ONCE. `plan set` refuses a milestone that already has a plan: the plan is the
|
|
43
|
+
order the build is measured against, and an order that can be rewritten measures nothing. The one
|
|
44
|
+
way down from a written plan is `pikku knowledge plan defer <milestone> <item> --reason <why>`,
|
|
45
|
+
which records what was left out and why.
|
|
46
|
+
|
|
41
47
|
It is JSON rather than a note on purpose. Everything else under `knowledge/` is prose a human
|
|
42
48
|
reads; this one is consumed field-by-field, and a markdown parser is one more place a misspelt
|
|
43
49
|
heading silently passes. It cannot live INSIDE the milestone note either: that note is frozen once
|
|
@@ -260,6 +266,7 @@ cover it — so a role × resource cross product there costs the milestone nothi
|
|
|
260
266
|
|
|
261
267
|
## When you are done
|
|
262
268
|
|
|
263
|
-
The accepted `plan set` is the end of
|
|
264
|
-
plan with `plan show --for-build`,
|
|
265
|
-
`pikku knowledge plan progress` is clean. What you wrote is what
|
|
269
|
+
The accepted `plan set` is the end of planning. Go straight on to the build in `pikku-build` §6:
|
|
270
|
+
read the plan with `plan show --for-build`, build it, and close the milestone only when
|
|
271
|
+
`pikku knowledge plan progress` is clean. What you wrote is what you are measured against, so do
|
|
272
|
+
not touch it once the first migration is open.
|
|
@@ -19,13 +19,13 @@ installGroups: [core]
|
|
|
19
19
|
|
|
20
20
|
## Which mode
|
|
21
21
|
|
|
22
|
-
| The situation
|
|
23
|
-
|
|
|
24
|
-
| A template was just cloned or scaffolded, and the tree still looks like one
|
|
25
|
-
| A real product, meant to be picked up by someone else
|
|
26
|
-
| A spike, a throwaway demo, an idea nobody has committed to
|
|
27
|
-
| A showcase meant to exercise every Pikku surface
|
|
28
|
-
| A feature added to an app that already has its knowledge base and milestones | `references/feature.md`
|
|
22
|
+
| The situation | Read |
|
|
23
|
+
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
24
|
+
| A template was just cloned or scaffolded, and the tree still looks like one | `references/post-clone.md` first, then come back |
|
|
25
|
+
| A real product, meant to be picked up by someone else | `references/app.md` — the default |
|
|
26
|
+
| A spike, a throwaway demo, an idea nobody has committed to | `references/quick.md` |
|
|
27
|
+
| A showcase meant to exercise every Pikku surface | `references/platform.md`, which is a delta on top of `references/app.md` |
|
|
28
|
+
| A feature added to an app that already has its knowledge base and milestones | `references/feature.md` |
|
|
29
29
|
|
|
30
30
|
**App is the default.** A small or toy-sounding app does not make it Quick;
|
|
31
31
|
only an explicit signal of speed or throwaway-ness does. Platform is not "App
|
|
@@ -33,9 +33,10 @@ plus more effort" — it is App plus a deliberate surface checklist, so read the
|
|
|
33
33
|
base first and follow it in full rather than blending the two into one plan.
|
|
34
34
|
|
|
35
35
|
The supporting references belong to whichever mode sends you to them:
|
|
36
|
-
`references/multi-app.md` (a second frontend), `references/design.md` (
|
|
37
|
-
to a design direction and judging whether
|
|
38
|
-
the first screen is built, not after the
|
|
36
|
+
`references/multi-app.md` (a second frontend), `references/design.md` (offering
|
|
37
|
+
to mock the screens first, committing to a design direction, and judging whether
|
|
38
|
+
the screens realise it — read before the first screen is built, not after the
|
|
39
|
+
last), `references/theming.md`
|
|
39
40
|
(authoring the theme),
|
|
40
41
|
`references/ship.md` (deploying, and the Fabric-readiness contract).
|
|
41
42
|
|
|
@@ -52,9 +53,10 @@ while still planning. Those failures look alarming and are nothing but this.
|
|
|
52
53
|
|
|
53
54
|
## What holds in every mode
|
|
54
55
|
|
|
55
|
-
- **The branch and the diff are the contract.**
|
|
56
|
-
|
|
57
|
-
`
|
|
56
|
+
- **The branch and the diff are the contract.** A reviewer sees real, compiled,
|
|
57
|
+
working code: apply is a merge, reject is a `git branch -D`. The milestone's
|
|
58
|
+
plan is your own denominator, measured by `pikku knowledge plan progress` —
|
|
59
|
+
never something a reviewer is handed instead of the code.
|
|
58
60
|
- **Discover before editing.** `yarn pikku meta context --json` returns
|
|
59
61
|
functions, wires, middleware, permissions, workflows, `capabilities` and
|
|
60
62
|
`layout` in one call. Fall back to targeted `meta` commands only for a full
|
|
@@ -65,11 +67,13 @@ while still planning. Those failures look alarming and are nothing but this.
|
|
|
65
67
|
lives in `messages/*.json`.
|
|
66
68
|
- **`pikku all` is the gate.** Run it after touching functions, wirings or
|
|
67
69
|
schemas, and treat its criticals as real.
|
|
68
|
-
- **A milestone is planned
|
|
70
|
+
- **A milestone is planned before it is built, and the plan then stays fixed.**
|
|
69
71
|
The plan — tables, functions, wires, roles, scopes, screens, scenarios, in
|
|
70
|
-
passes — is written through `pikku knowledge plan set`
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
passes — is written through `pikku knowledge plan set` (how: `pikku-architect`)
|
|
73
|
+
in its own turn before any of that milestone's code exists, and
|
|
74
|
+
`pikku knowledge plan progress` measures the build against it from the
|
|
75
|
+
generated meta. You plan it and you build it; what you never do is edit the
|
|
76
|
+
plan afterwards to match what you built — that is grading yourself.
|
|
73
77
|
|
|
74
78
|
## What NOT to do
|
|
75
79
|
|
|
@@ -61,6 +61,12 @@ in one message. Then stop; do not interview the user.
|
|
|
61
61
|
Neutral (fine for an internal tool, but say so out loud); a direction in words;
|
|
62
62
|
a reference (brand guide, screenshots, a site whose register they want); or
|
|
63
63
|
their own design agent/prompt, whose output you take as the direction.
|
|
64
|
+
- **Do they want to see the screens before you build them?** Offer it here, in
|
|
65
|
+
this same round, as a question and not a gate: one HTML page mocking the main
|
|
66
|
+
screens, a few minutes, far cheaper to change than built screens. If they say
|
|
67
|
+
yes, `references/design.md` owns what to make and what it then binds — the
|
|
68
|
+
approved page becomes source of truth for the screens, and the theme is written
|
|
69
|
+
before it so what they approve is what ships. If they say no, build.
|
|
64
70
|
- **What language should the app speak, and what language does the team work
|
|
65
71
|
in?** Two answers, not one — see §1a, which is where they go. Ask only if the
|
|
66
72
|
request is not obviously English; a brief written in English about an English
|
|
@@ -75,11 +81,11 @@ A brief saying "the entire UI is German" is about **one** of these. Getting this
|
|
|
75
81
|
wrong has already shipped a project that can never add a second language, so
|
|
76
82
|
settle all three explicitly before you write code.
|
|
77
83
|
|
|
78
|
-
| Axis | What it covers
|
|
79
|
-
| --------------- |
|
|
80
|
-
| **Identifiers** | Function, component, type, variable and file names. Database tables and columns. Commit messages.
|
|
81
|
-
| **Meta** | `description` on functions and steps, `name`/`title` on features and scenarios, step `template`, role and persona descriptions
|
|
82
|
-
| **Product UI** | Every string the app shows a user
|
|
84
|
+
| Axis | What it covers | Where it goes |
|
|
85
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
|
|
86
|
+
| **Identifiers** | Function, component, type, variable and file names. Database tables and columns. Commit messages. | Nowhere — **always English**, no setting, not negotiable |
|
|
87
|
+
| **Meta** | `description` on functions and steps, `name`/`title` on features and scenarios, step `template`, role and persona descriptions | `metaLocale` in `pikku.config.json`, default `en` |
|
|
88
|
+
| **Product UI** | Every string the app shows a user | `messages/<locale>.json`, and `defaultLocale` for what a first-time visitor opens in |
|
|
83
89
|
|
|
84
90
|
**Identifiers are English.** The product's market does not change this and
|
|
85
91
|
neither does `metaLocale`. Identifiers are the surface the generated `#pikku/*`
|
|
@@ -212,7 +218,11 @@ defineSystemRole({
|
|
|
212
218
|
})
|
|
213
219
|
|
|
214
220
|
definePersonas({
|
|
215
|
-
visitor: {
|
|
221
|
+
visitor: {
|
|
222
|
+
name: 'Visitor',
|
|
223
|
+
jobTitle: 'Synthetic health-check user',
|
|
224
|
+
account: {},
|
|
225
|
+
},
|
|
216
226
|
amina: {
|
|
217
227
|
name: 'Amina',
|
|
218
228
|
jobTitle: 'Property owner',
|
|
@@ -223,7 +233,8 @@ definePersonas({
|
|
|
223
233
|
bilal: {
|
|
224
234
|
name: 'Bilal',
|
|
225
235
|
jobTitle: 'Property owner',
|
|
226
|
-
personality:
|
|
236
|
+
personality:
|
|
237
|
+
'A second owner — exists so "you see yours, not theirs" is testable',
|
|
227
238
|
roles: ['owner'],
|
|
228
239
|
account: {},
|
|
229
240
|
},
|
|
@@ -332,6 +343,10 @@ What a milestone is:
|
|
|
332
343
|
persona. If you cannot write the gherkin, you cannot build it yet — that is a
|
|
333
344
|
`questions/` note, not a milestone.
|
|
334
345
|
|
|
346
|
+
If §1's screen mock was made and approved, the milestones are read off it: every
|
|
347
|
+
screen on that page belongs to some milestone, and a screen no milestone builds
|
|
348
|
+
is a hole in this plan. Say which milestone covers which screen.
|
|
349
|
+
|
|
335
350
|
How to order them:
|
|
336
351
|
|
|
337
352
|
1. **The spine first.** The one object everything else hangs off, and the screen
|
|
@@ -359,10 +374,10 @@ scenarios, split into passes. It is JSON, it lives beside the note, and
|
|
|
359
374
|
`pikku knowledge plan progress` measures the finished build against it.
|
|
360
375
|
|
|
361
376
|
**Read `pikku-architect` and follow it.** The plan is the denominator the
|
|
362
|
-
completion check divides by, so a builder who
|
|
363
|
-
fraction, plan only that fraction, and certify itself complete.
|
|
364
|
-
|
|
365
|
-
|
|
377
|
+
completion check divides by, so a builder who plans after seeing their own work
|
|
378
|
+
can build a fraction, plan only that fraction, and certify itself complete. The
|
|
379
|
+
defence is the ORDER, and it only holds if you keep it: the plan is written
|
|
380
|
+
against the note in its own turn,
|
|
366
381
|
before any of the code it measures exists, and is never edited afterwards to
|
|
367
382
|
match what you ended up building. An item that will not land is deferred with
|
|
368
383
|
its reason — `plan defer` — not quietly rewritten. Write it before you open a
|
|
@@ -386,8 +401,8 @@ no plan, and everything after the current milestone is still allowed to move.
|
|
|
386
401
|
|
|
387
402
|
**Per milestone** — plan it (§5a), set its note to `status: dispatched`, do the
|
|
388
403
|
six steps, close it out (§6a), set it to `built`. Do not start the next one
|
|
389
|
-
until §6a passes, §7 is green for this one
|
|
390
|
-
|
|
404
|
+
until §6a passes, §7 is green for this one _and §7a shows its functions
|
|
405
|
+
covered_. A stack of half-milestones cannot be reviewed and cannot be handed
|
|
391
406
|
over, and an uncovered function is a half-milestone whether or not the note says
|
|
392
407
|
`built`.
|
|
393
408
|
|
|
@@ -406,7 +421,7 @@ over, and an uncovered function is a half-milestone whether or not the note says
|
|
|
406
421
|
Do this generously and do it now: an empty app demos badly and critiques
|
|
407
422
|
badly, and you cannot judge a screen's hierarchy, overflow, or truncation
|
|
408
423
|
against zero rows. Seed rows each persona sees differently — with an ownership
|
|
409
|
-
rule that means seeding rows for the
|
|
424
|
+
rule that means seeding rows for the _second_ owner too.
|
|
410
425
|
3. **Functions.** One `pikkuFunc` per `*.function.ts`. Mark it `expose: true` and
|
|
411
426
|
Pikku generates the typed RPC client and the React Query hooks the UI calls;
|
|
412
427
|
you do NOT write an HTTP route for it. Add `wireHTTP` only for a real REST
|
|
@@ -415,16 +430,28 @@ over, and an uncovered function is a half-milestone whether or not the note says
|
|
|
415
430
|
5. **UI.** Pages in `<app>/src/pages/`, one route file each in `<app>/src/routes/`,
|
|
416
431
|
calling functions through the generated `usePikkuQuery` / `usePikkuMutation`
|
|
417
432
|
hooks from `@project/functions-sdk/pikku/api.gen`. One component per `.tsx`
|
|
418
|
-
file. Compose the kit from `@/components/<Name>` rather than hand-rolling
|
|
419
|
-
|
|
420
|
-
|
|
433
|
+
file. Compose the kit from `@/components/<Name>` rather than hand-rolling
|
|
434
|
+
controls — and **add to that kit**: the component that draws the thing this
|
|
435
|
+
product is actually about, and the furniture you would otherwise copy-paste
|
|
436
|
+
into eight pages. The kit is where you start, not where you stop; an app whose
|
|
437
|
+
every screen is `Card` + `Stack` + `Text` composed the inventory rather than a
|
|
438
|
+
design. Register the screen in `useNavItems()` — that one file feeds both the
|
|
439
|
+
desktop sidebar and the phone navigation.
|
|
421
440
|
**Read `references/design.md` before you write the first screen.** You commit
|
|
422
441
|
to a design direction there and are then accountable to it — it hands you no
|
|
423
|
-
layouts, because the design is yours to make.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
442
|
+
layouts, because the design is yours to make. What you build here is then
|
|
443
|
+
judged at step 7, at the end of this milestone rather than once at §8, where
|
|
444
|
+
the only affordable fix is a repaint of eight screens.
|
|
445
|
+
6. **Scenario** (§7).
|
|
446
|
+
7. **Look at it.** Screenshot every screen this milestone touched, at both
|
|
447
|
+
widths, with the seed in place, and look at the images. This is a gate, the
|
|
448
|
+
same as the scenario: a milestone whose screens nobody has seen is not built,
|
|
449
|
+
it is unproven at the one layer scenarios cannot reach. `references/design.md`
|
|
450
|
+
carries how to take the shot when no browser tool is wired up, and what to
|
|
451
|
+
look for. Then close it against its plan (§6a) — `pikku knowledge plan
|
|
452
|
+
progress` has to exit zero before anything is `built` — and only then set
|
|
453
|
+
`status: built`, saying in the note what you looked at and what it made you
|
|
454
|
+
change.
|
|
428
455
|
|
|
429
456
|
Rules that are not optional:
|
|
430
457
|
|
|
@@ -444,8 +471,8 @@ Rules that are not optional:
|
|
|
444
471
|
```typescript
|
|
445
472
|
export const classifications = {
|
|
446
473
|
payment: {
|
|
447
|
-
paid_at:
|
|
448
|
-
metadata: { kind: 'json', tsType: 'PaymentMeta' },
|
|
474
|
+
paid_at: { kind: 'date' }, // -> Date, not an ISO string
|
|
475
|
+
metadata: { kind: 'json', tsType: 'PaymentMeta' }, // -> parsed object, not unknown
|
|
449
476
|
},
|
|
450
477
|
}
|
|
451
478
|
```
|
|
@@ -454,6 +481,7 @@ Rules that are not optional:
|
|
|
454
481
|
`JSON` column with no entry types as `unknown` (the CLI warns PKU481). Add the
|
|
455
482
|
annotation rather than casting around the generated type. Once the file carries
|
|
456
483
|
manual fields, `db migrate` stops overwriting it.
|
|
484
|
+
|
|
457
485
|
- A `z.date()` on a function's **input** arrives over RPC as an ISO string, not a
|
|
458
486
|
`Date`. Normalise before calling date methods on it (`new Date(value)`), or it
|
|
459
487
|
throws `.getTime is not a function` at runtime — schema validation accepts the
|
|
@@ -522,6 +550,7 @@ Three things it says, and what each one asks of you:
|
|
|
522
550
|
milestone is two milestones — say so to the user rather than deferring again.
|
|
523
551
|
What you may never do is drop the item silently: the plan is what the next
|
|
524
552
|
person reads to know what this milestone was for.
|
|
553
|
+
|
|
525
554
|
- **PROBLEMS** — something exists but does not do what was planned. A function
|
|
526
555
|
planned as restricted whose meta says `auth: false`; a `cascade` no migration
|
|
527
556
|
declares; a browser scenario that opens a page and asserts it is still on it.
|
|
@@ -530,8 +559,9 @@ Three things it says, and what each one asks of you:
|
|
|
530
559
|
visible, never blocking.
|
|
531
560
|
|
|
532
561
|
**Do not set the note to `built` while this exits non-zero**, and do not edit the
|
|
533
|
-
plan to match what you built —
|
|
534
|
-
rewriting
|
|
562
|
+
plan to match what you built — the plan was written before the code on purpose,
|
|
563
|
+
and rewriting your own denominator afterwards is exactly what that order exists
|
|
564
|
+
to stop.
|
|
535
565
|
|
|
536
566
|
## 7. Prove it — scenarios
|
|
537
567
|
|
|
@@ -546,26 +576,27 @@ import { pikkuScenario } from '#pikku/scenarios'
|
|
|
546
576
|
|
|
547
577
|
export const tenantReportsAFaultScenario = pikkuScenario<void, { id: string }>({
|
|
548
578
|
title: 'A tenant reports a fault and the owner sees it',
|
|
549
|
-
description:
|
|
579
|
+
description:
|
|
580
|
+
'The report lands on the owning landlord’s queue, and nobody else’s',
|
|
550
581
|
tags: ['scenario', 'maintenance'],
|
|
551
582
|
func: async (_services, _data, { scenario, actors }) => {
|
|
552
583
|
const report = await scenario.do(
|
|
553
584
|
'reports a broken boiler',
|
|
554
585
|
'createMaintenanceReport',
|
|
555
586
|
{ summary: 'No hot water' },
|
|
556
|
-
{ actor: actors.chidi }
|
|
587
|
+
{ actor: actors.chidi }
|
|
557
588
|
)
|
|
558
589
|
await scenario.then(
|
|
559
590
|
'appears on the owner’s queue',
|
|
560
591
|
'reportShowsOnQueue',
|
|
561
592
|
{ id: report.id },
|
|
562
|
-
{ actor: actors.amina }
|
|
593
|
+
{ actor: actors.amina }
|
|
563
594
|
)
|
|
564
595
|
await scenario.then(
|
|
565
596
|
'is invisible to the other owner',
|
|
566
597
|
'reportIsNotVisible',
|
|
567
598
|
{ id: report.id },
|
|
568
|
-
{ actor: actors.bilal }
|
|
599
|
+
{ actor: actors.bilal }
|
|
569
600
|
)
|
|
570
601
|
return { id: report.id }
|
|
571
602
|
},
|
|
@@ -30,6 +30,117 @@ and so is the commitment.
|
|
|
30
30
|
Be ambitious with it. A direction that could describe any SaaS app has not been
|
|
31
31
|
chosen — it has been defaulted to in words instead of in components.
|
|
32
32
|
|
|
33
|
+
### The looks you will default to
|
|
34
|
+
|
|
35
|
+
Being ambitious is easier against a list of specific things to not be. Generated
|
|
36
|
+
interfaces cluster hard, and these are the attractors — not because any is ugly,
|
|
37
|
+
but because arriving at one *by default* means no choice was made:
|
|
38
|
+
|
|
39
|
+
- **Stock Mantine.** The strongest pull, and the hardest to see: a component
|
|
40
|
+
library's untouched defaults do not look broken, they look finished. Blue
|
|
41
|
+
accent, `#dee2e6` borders, `md` radius on everything, `Card` + `Stack` + `Text`
|
|
42
|
+
down every page. An app can be entirely this and never trip a critique, because
|
|
43
|
+
nothing on any screen is *wrong*.
|
|
44
|
+
- Warm cream ground with a serif display face and a terracotta accent.
|
|
45
|
+
- Near-black with one acid-green or vermilion pop.
|
|
46
|
+
- A purple-to-blue gradient header on white.
|
|
47
|
+
- Inter, or Space Grotesk, as the "safe" typeface.
|
|
48
|
+
- Emoji as section markers; everything centre-aligned; one radius and one shadow
|
|
49
|
+
stamped on every block, which flattens hierarchy instead of creating it.
|
|
50
|
+
|
|
51
|
+
If the user asks for one of these, build it — their words win. What is not
|
|
52
|
+
allowed is landing on one because it was nearest to hand.
|
|
53
|
+
|
|
54
|
+
## Offer to draw the screens before you build them
|
|
55
|
+
|
|
56
|
+
Before the first milestone, **ask** whether they want to see the screens first.
|
|
57
|
+
One question, in §1's round, not a gate of its own:
|
|
58
|
+
|
|
59
|
+
> Want me to mock the main screens as a page you can look at before I build
|
|
60
|
+
> anything? It takes a few minutes and it is much cheaper to change a picture
|
|
61
|
+
> than a built screen.
|
|
62
|
+
|
|
63
|
+
If they decline, build; the direction in words is enough to be accountable to.
|
|
64
|
+
If they accept, this is the cheapest decision in the project — a picture of eight
|
|
65
|
+
screens costs a fraction of eight built screens, and it is the only point where
|
|
66
|
+
"that is not what I meant" is free.
|
|
67
|
+
|
|
68
|
+
**Author the theme first, then draw the mock from it.** This order is the whole
|
|
69
|
+
point. A beautiful page in hand-rolled CSS sets a bar Mantine then misses, and
|
|
70
|
+
what the user approved is not what ships — they signed off on a picture and
|
|
71
|
+
received an approximation of it. So write `themes/<name>.json` first
|
|
72
|
+
(`references/theming.md`), and let the mock take its every value from that file:
|
|
73
|
+
the palette, `structure.radius`, the spacing scale, the fonts, the component
|
|
74
|
+
`defaultProps`. Approving the mock then approves the theme, and the built screens
|
|
75
|
+
inherit it rather than chase it.
|
|
76
|
+
|
|
77
|
+
**The mock has two halves, and only one of them is Mantine's.** This is the same
|
|
78
|
+
split the built screen lives under, applied a step earlier so the two agree by
|
|
79
|
+
construction. The PAGE — the shell, the regions, the columns, the rhythm, the
|
|
80
|
+
material behind the content, what overlaps what — is plain HTML and your own
|
|
81
|
+
CSS, arranged however the layout decision demands; that half is free, and it is
|
|
82
|
+
where the design actually happens. The COMPONENTS — anything a person would
|
|
83
|
+
point at and call a control, and that the app will adopt as itself: buttons,
|
|
84
|
+
inputs, selects, tables, badges, menus, modals — are drawn as *Mantine's*, at the
|
|
85
|
+
metrics Mantine actually uses: its control heights, its input shapes, its table
|
|
86
|
+
and menu behaviour. The test is the one the build will apply too: is this thing
|
|
87
|
+
the SHAPE OF THE PAGE, or a COMPONENT someone would point at?
|
|
88
|
+
|
|
89
|
+
Getting that second half wrong is what makes a mock a lie. A control the mock
|
|
90
|
+
invents is a promise the app cannot keep, and a beautiful hand-rolled input sets
|
|
91
|
+
a bar the real one misses on screen one. If the mock wants something Mantine
|
|
92
|
+
does not do, that is a real finding, and finding it here is the point: change the
|
|
93
|
+
theme so it does, or change the mock, and say which.
|
|
94
|
+
|
|
95
|
+
**What to make.** One self-contained HTML page holding every screen the app
|
|
96
|
+
needs — not a prototype, not a click-through. Static markup, real content from
|
|
97
|
+
their domain (never lorem), the empty and error states beside the happy path,
|
|
98
|
+
laid out so the whole app is legible by scrolling. Its CSS is custom properties
|
|
99
|
+
on `:root` carrying the theme JSON's values, so a change to either is a change
|
|
100
|
+
to one number in both. Mantine itself will not load here — it is a React library
|
|
101
|
+
and a page like this has no bundler, and on hosts that sandbox the page (a Claude
|
|
102
|
+
Artifact) external stylesheets are blocked outright — so do not try; the mock
|
|
103
|
+
reproduces the theme's values by hand, which is why they have to be written down
|
|
104
|
+
first. Whatever your host offers for showing a page is how you show it: an
|
|
105
|
+
Artifact, a file they open, a preview server. The page is the deliverable; how it
|
|
106
|
+
gets in front of them is not this file's business.
|
|
107
|
+
|
|
108
|
+
Write it to `knowledge/decisions/design/screens.html` and treat it as **source of
|
|
109
|
+
truth for the screens** once they approve it. That has consequences worth
|
|
110
|
+
stating:
|
|
111
|
+
|
|
112
|
+
- The milestones are read off it. A screen in the mock that no milestone builds
|
|
113
|
+
is a gap in the plan, not a spare drawing.
|
|
114
|
+
- A screen the build turns out to need that the mock does not have means the
|
|
115
|
+
mock was wrong. Update it, and say you did. Do not let the app and the mock
|
|
116
|
+
drift and then quietly prefer the app.
|
|
117
|
+
- The knowledge graph still owns the domain — objects, roles, rules. The mock
|
|
118
|
+
owns what the screens look like. When they disagree about a *fact*, knowledge
|
|
119
|
+
wins; when they disagree about a *layout*, the mock wins.
|
|
120
|
+
|
|
121
|
+
**Building it is then a transcription, not a translation.** Because the theme
|
|
122
|
+
already exists and the mock was drawn from it, the screen is Mantine components
|
|
123
|
+
arranged the way the mock arranges them — the look arrives with the theme. Two
|
|
124
|
+
rules keep it that way:
|
|
125
|
+
|
|
126
|
+
- **Layout is yours to write; components are Mantine's.** The page shape — the
|
|
127
|
+
regions, the columns, the rhythm, what sits beside what — is ordinary markup
|
|
128
|
+
and your own CSS. Anything a person would point at and call a control comes
|
|
129
|
+
from Mantine: buttons, inputs, selects, tables, badges, menus. Those carry
|
|
130
|
+
focus rings, keyboard behaviour and i18n, and hand-rolling one throws all of
|
|
131
|
+
it away.
|
|
132
|
+
- **A gap goes back to the theme, never into a component override.** If a screen
|
|
133
|
+
does not match the mock, the fix is a value in `themes/<name>.json`. A stack of
|
|
134
|
+
one-off `className`s and `!important` fighting Mantine's defaults looks like
|
|
135
|
+
progress on screen one and is unmaintainable by screen five, and the screens
|
|
136
|
+
drift apart because nothing central holds them together.
|
|
137
|
+
|
|
138
|
+
**Checking the built screen against the mock** is a structural comparison, not a
|
|
139
|
+
pixel one: the same regions in the same order, the same hierarchy, the same
|
|
140
|
+
states present, the same tokens used. Do not chase pixel equality — Mantine's
|
|
141
|
+
components have their own metrics and the mock was drawn without them. A built
|
|
142
|
+
screen that reads as the same screen has passed.
|
|
143
|
+
|
|
33
144
|
## One screen, designed properly, before the rest exist
|
|
34
145
|
|
|
35
146
|
Design the first real screen as if it were the only one, and take it further than
|
|
@@ -48,6 +159,31 @@ the thing that made it is always yes. Use evidence.
|
|
|
48
159
|
- **Screenshot every screen and look at the image**, at ~390px and at ~1440px.
|
|
49
160
|
Judging your own UI from source is guessing, and the failures that matter —
|
|
50
161
|
proportion, hierarchy, a wall of identical boxes — are invisible in JSX.
|
|
162
|
+
**Sort out how you will take that screenshot before you need it**, because an
|
|
163
|
+
instruction with no working mechanism behind it is one that gets skipped, and
|
|
164
|
+
this is the one that gets skipped. If a browser-driving tool is wired up, use
|
|
165
|
+
it. If it is not — or it fails to connect, which happens — the fallback is
|
|
166
|
+
short enough to write once and keep:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
CHROME=$(command -v google-chrome || command -v chromium || \
|
|
170
|
+
command -v chromium-browser || \
|
|
171
|
+
echo "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
|
|
172
|
+
"$CHROME" --headless=new --no-sandbox \
|
|
173
|
+
--remote-debugging-port=9333 --user-data-dir=/tmp/gc-shots &
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Resolve the binary rather than hardcoding the macOS path: the same gate has to
|
|
177
|
+
work on a Linux box and in CI, and a fallback that only starts on one host is
|
|
178
|
+
a gate that gets skipped everywhere else. If the host already exposes a CDP
|
|
179
|
+
endpoint, point the script at that instead and start nothing.
|
|
180
|
+
|
|
181
|
+
Then drive it over CDP from a script: `Page.navigate`,
|
|
182
|
+
`Emulation.setDeviceMetricsOverride` for the two widths,
|
|
183
|
+
`Page.captureScreenshot`, write the PNG, and open it. Sign in the way a person
|
|
184
|
+
does — click the dev actor switcher on the login page — rather than reaching
|
|
185
|
+
for the secret the client uses; the UI path is shorter and it also proves the
|
|
186
|
+
login screen works. Keep the script; you will run it at every milestone.
|
|
51
187
|
- **Run `impeccable`** (`npx impeccable install`, Node 22.18+) and feed it the
|
|
52
188
|
screenshots. It is external, it does not flatter, and it scores execution
|
|
53
189
|
against interaction heuristics. But it audits how well you executed the design
|
|
@@ -69,6 +205,77 @@ questions are fixed:
|
|
|
69
205
|
had?
|
|
70
206
|
6. Would you show it to the user without apologising for it?
|
|
71
207
|
|
|
208
|
+
## The kit is a floor, not a ceiling
|
|
209
|
+
|
|
210
|
+
The scaffold hands you a component kit, and the build instructions tell you to
|
|
211
|
+
compose from it rather than hand-roll. Both are right, and together they have a
|
|
212
|
+
failure mode worth naming: an app whose every screen is `Card` + `Stack` + `Text`
|
|
213
|
+
because those were the pieces in the box. That is not a composed design, it is an
|
|
214
|
+
inventory, and it produces the wall of identical boxes further down this file.
|
|
215
|
+
|
|
216
|
+
**Composing from the kit means using its primitives, not being limited to its
|
|
217
|
+
list.** A product has objects of its own, and the ones that carry its meaning are
|
|
218
|
+
the ones no generic kit ships:
|
|
219
|
+
|
|
220
|
+
- the thing the product is *about*, rendered as itself — a funding meter, a
|
|
221
|
+
streak, a seat map, a run's status over time. If a decision note says the
|
|
222
|
+
progress toward a goal is the primary object, then a component that draws that
|
|
223
|
+
progress has to exist, or the note is describing an app you did not build.
|
|
224
|
+
- the repeated furniture that is currently copy-pasted — the page header, the
|
|
225
|
+
section label, the empty state, the recessed panel a form sits in. Eight inline
|
|
226
|
+
copies of a heading block will not agree with each other; they will disagree by
|
|
227
|
+
a few pixels each, and the screens will read as unrelated for reasons nobody
|
|
228
|
+
can point at.
|
|
229
|
+
|
|
230
|
+
Both kinds are ordinary components built out of kit primitives and theme values.
|
|
231
|
+
Adding them is not hand-rolling, and they are the difference between an app that
|
|
232
|
+
uses a design system and an app that looks like one.
|
|
233
|
+
|
|
234
|
+
The tell that you skipped this: your `components/` directory maps one-to-one onto
|
|
235
|
+
your data model and contains nothing that names a *quality* of the product.
|
|
236
|
+
|
|
237
|
+
## Design is a gate on the milestone, not a phase at the end
|
|
238
|
+
|
|
239
|
+
The loop that actually runs is plan, build, prove, close. Design advice that
|
|
240
|
+
lives outside that loop does not run — it gets read, agreed with, and skipped,
|
|
241
|
+
because nothing blocks on it. Milestones close on green scenarios, and scenarios
|
|
242
|
+
say nothing about how anything looks.
|
|
243
|
+
|
|
244
|
+
So put it in the loop. **A milestone is not built until its screens have been
|
|
245
|
+
looked at**, in the same sense that it is not built until its scenario passes:
|
|
246
|
+
|
|
247
|
+
- Screenshot every screen the milestone touched, at both widths, with the seed
|
|
248
|
+
in place.
|
|
249
|
+
- Look at the images. Not the JSX.
|
|
250
|
+
- Fix what they show, in this milestone, while it is one screen and not eight.
|
|
251
|
+
- Say in the milestone note what you looked at and what you changed.
|
|
252
|
+
|
|
253
|
+
A milestone closed without that is closed on a claim, not on evidence. The cost
|
|
254
|
+
of being honest about it now is minutes; the cost at §8 is a repaint of the whole
|
|
255
|
+
app, and by then the wrong register has been inherited by every screen so the
|
|
256
|
+
repaint is a rewrite.
|
|
257
|
+
|
|
258
|
+
### The seed is part of the gate
|
|
259
|
+
|
|
260
|
+
A screenshot is only evidence if the screen has something on it. Before the gate
|
|
261
|
+
runs, the dev seed must populate what each screen *is for* — not one row, and not
|
|
262
|
+
an empty state.
|
|
263
|
+
|
|
264
|
+
This is a real and quiet failure: a list app whose seed has no list, a countdown
|
|
265
|
+
whose seed has no dates, judged for weeks against its own empty state while the
|
|
266
|
+
screen it was built for was never once looked at. The empty state is worth
|
|
267
|
+
designing and is not what the milestone is about.
|
|
268
|
+
|
|
269
|
+
Seed enough to be judged against: several rows, not three identical ones, and a
|
|
270
|
+
deliberate spread of the cases the screen has to hold — a long title that wraps,
|
|
271
|
+
a missing optional field, a picture and no picture, one item in each state the
|
|
272
|
+
screen can show. Anything derived from *today* — a countdown, "3 days ago", an
|
|
273
|
+
expiry — is seeded as an interval from `now`, never as a fixed date: fixed dates
|
|
274
|
+
are correct on the afternoon they are written and meaningless a month later.
|
|
275
|
+
|
|
276
|
+
Data left over from a scenario run is not a seed. If the screens are full of
|
|
277
|
+
`Filter coffee grinder mttqdvsx`, you are designing against test debris.
|
|
278
|
+
|
|
72
279
|
## Facts, not taste
|
|
73
280
|
|
|
74
281
|
These are not design opinions and are not open to a different answer.
|
|
@@ -125,4 +332,15 @@ first one.
|
|
|
125
332
|
- Every row carries the same buttons, and the buttons outweigh the content.
|
|
126
333
|
- The palette's meaningful colours are also used decoratively, so they have
|
|
127
334
|
stopped meaning anything.
|
|
335
|
+
- A decision note describes something the screens do not do — the note says
|
|
336
|
+
progress is the primary object and no screen draws progress, or it says warm
|
|
337
|
+
and not clinical and the error page is still template blue.
|
|
338
|
+
- The theme JSON is rich and the screens are bare. Tokens are the cheapest half
|
|
339
|
+
of design and the easiest to mistake for the whole of it: a considered palette
|
|
340
|
+
and a display font applied to a default layout is a well-dressed default.
|
|
341
|
+
- Every border, divider and disabled control is a cool blue-grey while the
|
|
342
|
+
accent is not — the surest sign the neutrals were inherited rather than
|
|
343
|
+
chosen. See `references/theming.md`.
|
|
128
344
|
- It looks like the last app you built.
|
|
345
|
+
- It looks like Mantine. Not *built with* Mantine, which it is and should be —
|
|
346
|
+
but indistinguishable from a component gallery with the brand hue swapped in.
|