@pikku/skills 0.12.30 → 0.12.33
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/README.md +1 -1
- package/dist/skills.gen.js +2 -2
- package/package.json +10 -6
- package/skills/pikku-addon/SKILL.md +5 -0
- package/skills/pikku-build/SKILL.md +4 -3
- package/skills/pikku-build/references/app.md +28 -8
- package/skills/pikku-build/references/design.md +218 -0
- package/skills/pikku-build/references/ship.md +79 -0
- package/skills/pikku-build/references/theming.md +122 -0
- package/skills/pikku-knowledge/SKILL.md +14 -0
- package/skills/pikku-services/references/services.md +7 -1
- package/CHANGELOG.md +0 -1368
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/skills",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.33",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
7
|
+
"directory": "packages/skills"
|
|
8
|
+
},
|
|
4
9
|
"description": "The Pikku agent skills — the instruction set coding agents read to build, wire and deploy Pikku projects",
|
|
5
10
|
"author": "yasser.fadl@gmail.com",
|
|
6
11
|
"license": "MIT",
|
|
@@ -10,13 +15,13 @@
|
|
|
10
15
|
"type": "module",
|
|
11
16
|
"scripts": {
|
|
12
17
|
"embed": "node scripts/embed.mjs",
|
|
13
|
-
"tsc": "
|
|
14
|
-
"build": "
|
|
18
|
+
"tsc": "bun run embed && tsc",
|
|
19
|
+
"build": "bun run embed && tsc -b",
|
|
15
20
|
"ncu": "npx npm-check-updates",
|
|
16
21
|
"test": "bash run-tests.sh",
|
|
17
22
|
"test:watch": "bash run-tests.sh --watch",
|
|
18
23
|
"test:coverage": "bash run-tests.sh --coverage",
|
|
19
|
-
"prepublishOnly": "
|
|
24
|
+
"prepublishOnly": "bun run build"
|
|
20
25
|
},
|
|
21
26
|
"exports": {
|
|
22
27
|
".": "./dist/index.js"
|
|
@@ -27,10 +32,9 @@
|
|
|
27
32
|
],
|
|
28
33
|
"devDependencies": {
|
|
29
34
|
"@types/node": "^24.13.3",
|
|
30
|
-
"typescript": "^6.0.3",
|
|
31
35
|
"yaml": "^2.9.0"
|
|
32
36
|
},
|
|
33
37
|
"engines": {
|
|
34
38
|
"node": ">=24"
|
|
35
39
|
}
|
|
36
|
-
}
|
|
40
|
+
}
|
|
@@ -324,6 +324,11 @@ wireAddon({ name: 'todos', package: '@my-org/addon-todos' })
|
|
|
324
324
|
|
|
325
325
|
After registration, run `yarn pikku all` to generate types for the addon's functions.
|
|
326
326
|
|
|
327
|
+
Give each addon its own wiring file. A deployment unit imports a `wireAddon`
|
|
328
|
+
file only while at least one addon that file wires survives the unit's filter,
|
|
329
|
+
so wiring two addons from one file means a unit needing either one registers
|
|
330
|
+
both and bundles both packages' dependencies.
|
|
331
|
+
|
|
327
332
|
If the addon ships tables, `pikku db generate` then writes one migration per
|
|
328
333
|
addon — named after the package, carrying the addon's own SQL — after Better
|
|
329
334
|
Auth's and the runtime's, so an addon table may reference `user` or a runtime
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -332,6 +338,10 @@ What a milestone is:
|
|
|
332
338
|
persona. If you cannot write the gherkin, you cannot build it yet — that is a
|
|
333
339
|
`questions/` note, not a milestone.
|
|
334
340
|
|
|
341
|
+
If §1's screen mock was made and approved, the milestones are read off it: every
|
|
342
|
+
screen on that page belongs to some milestone, and a screen no milestone builds
|
|
343
|
+
is a hole in this plan. Say which milestone covers which screen.
|
|
344
|
+
|
|
335
345
|
How to order them:
|
|
336
346
|
|
|
337
347
|
1. **The spine first.** The one object everything else hangs off, and the screen
|
|
@@ -415,16 +425,26 @@ over, and an uncovered function is a half-milestone whether or not the note says
|
|
|
415
425
|
5. **UI.** Pages in `<app>/src/pages/`, one route file each in `<app>/src/routes/`,
|
|
416
426
|
calling functions through the generated `usePikkuQuery` / `usePikkuMutation`
|
|
417
427
|
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
|
-
|
|
428
|
+
file. Compose the kit from `@/components/<Name>` rather than hand-rolling
|
|
429
|
+
controls — and **add to that kit**: the component that draws the thing this
|
|
430
|
+
product is actually about, and the furniture you would otherwise copy-paste
|
|
431
|
+
into eight pages. The kit is where you start, not where you stop; an app whose
|
|
432
|
+
every screen is `Card` + `Stack` + `Text` composed the inventory rather than a
|
|
433
|
+
design. Register the screen in `useNavItems()` — that one file feeds both the
|
|
434
|
+
desktop sidebar and the phone navigation.
|
|
421
435
|
**Read `references/design.md` before you write the first screen.** You commit
|
|
422
436
|
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
|
-
|
|
437
|
+
layouts, because the design is yours to make. What you build here is then
|
|
438
|
+
judged at step 7, at the end of this milestone rather than once at §8, where
|
|
439
|
+
the only affordable fix is a repaint of eight screens.
|
|
440
|
+
6. **Scenario** (§7).
|
|
441
|
+
7. **Look at it.** Screenshot every screen this milestone touched, at both
|
|
442
|
+
widths, with the seed in place, and look at the images. This is a gate, the
|
|
443
|
+
same as the scenario: a milestone whose screens nobody has seen is not built,
|
|
444
|
+
it is unproven at the one layer scenarios cannot reach. `references/design.md`
|
|
445
|
+
carries how to take the shot when no browser tool is wired up, and what to
|
|
446
|
+
look for. Then `status: built`, and say in the note what you looked at and
|
|
447
|
+
what it made you change.
|
|
428
448
|
|
|
429
449
|
Rules that are not optional:
|
|
430
450
|
|
|
@@ -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.
|
|
@@ -23,6 +23,85 @@ how a worker deploy fails at runtime instead of at build.
|
|
|
23
23
|
Always run `plan` before `apply`, and read it. It names what will be created,
|
|
24
24
|
updated and deleted — the deletions are the reason to look.
|
|
25
25
|
|
|
26
|
+
### How many workers you get
|
|
27
|
+
|
|
28
|
+
By default a unit holds every function that builds the same set of singleton
|
|
29
|
+
services. One unit per function is available too, but on a large app it means a
|
|
30
|
+
hundred workers whose bundles are mostly the same framework code repeated, and a
|
|
31
|
+
build and an upload for each. `deploy.grouping` in `pikku.config.json` sets the
|
|
32
|
+
shape:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
"deploy": {
|
|
36
|
+
"grouping": {
|
|
37
|
+
"strategy": "single",
|
|
38
|
+
"rules": [
|
|
39
|
+
{ "unit": "console", "addon": "console" },
|
|
40
|
+
{ "unit": "pdf", "tags": ["pdf"] }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`strategy` is the fallback for a function no rule matches:
|
|
47
|
+
|
|
48
|
+
| strategy | fallback |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `services` | one unit per distinct set of singleton services (the default) |
|
|
51
|
+
| `function` | one unit each |
|
|
52
|
+
| `single` | one shared unit |
|
|
53
|
+
|
|
54
|
+
Rules are ordered, first match wins, and match on `tags`, an `addon` namespace
|
|
55
|
+
or `routes` globs. A rule always beats the strategy, so under `function` a rule
|
|
56
|
+
merges and under `single` or `services` it carves out.
|
|
57
|
+
|
|
58
|
+
`services` is the default because it is the middle ground: `single` is too
|
|
59
|
+
coarse to reason about and `function` pays a cold start and a deploy step per
|
|
60
|
+
function. Functions are keyed
|
|
61
|
+
on the singleton services their bodies destructure, minus the ones every unit
|
|
62
|
+
builds anyway (`config`, `logger`, `variables`, `schema`, `secrets`, and the
|
|
63
|
+
per-request `rpc`/`mcp`/`channel`/`userSession`). Units are named for that set —
|
|
64
|
+
`svc-todo-store`, `svc-event-hub-todo-store`, `svc-base` for functions needing
|
|
65
|
+
nothing else — and each records it as `servicesKey` in the manifest. On the
|
|
66
|
+
`templates/functions` app it turns 44 units into 10.
|
|
67
|
+
|
|
68
|
+
The deploy target is part of the key, so a `server` unit is named `-server` and
|
|
69
|
+
can never merge with its serverless twin. That is what keeps `services` from
|
|
70
|
+
tripping the mixed-target refusal below: two functions can carry identical
|
|
71
|
+
services and still run in different places, because a function may name
|
|
72
|
+
`deploy: 'server'` itself without any service crossing it.
|
|
73
|
+
|
|
74
|
+
Read the shape before trusting it. The partition depends entirely on how varied
|
|
75
|
+
the app's service use is: an app where nearly every function reaches the same
|
|
76
|
+
database collapses to roughly `single` with a few carve-outs. Run `pikku deploy
|
|
77
|
+
plan` and look at the unit list; set `"strategy": "function"` if you want a unit
|
|
78
|
+
per function back.
|
|
79
|
+
|
|
80
|
+
Changing the strategy renames units, and a unit name is what a queue consumer, a
|
|
81
|
+
scheduled task and a `dependsOn` point at. The manifest rewrites all three, but
|
|
82
|
+
anything holding a unit name outside the manifest does not follow. Units dropped
|
|
83
|
+
from the manifest are not deleted by OSS `deploy()` either (pikkujs/pikku#543) —
|
|
84
|
+
fabric sweeps its dispatch namespace after each deploy, plain pikku leaves the
|
|
85
|
+
old workers in place.
|
|
86
|
+
|
|
87
|
+
`tags` matches the tags a function inherits from its wirings — `wireHTTP({ ...,
|
|
88
|
+
tags: ['pdf'] })` — as well as any on the function itself, which is where almost
|
|
89
|
+
every project writes them.
|
|
90
|
+
|
|
91
|
+
Two things that bite:
|
|
92
|
+
|
|
93
|
+
- **Grouping cannot change a deploy target.** Put a `serverlessIncompatible`
|
|
94
|
+
function in a group with serverless ones and the build fails naming both
|
|
95
|
+
sides. Give it its own rule — that refusal is the design, not a bug. To find
|
|
96
|
+
the culprit, read `deployment-manifest.json`: a unit carries `targetForcedBy`
|
|
97
|
+
naming the services that crossed it to `server`, and `groupedBy` naming the
|
|
98
|
+
rule that made it, so you can see which rule to carve the function out of.
|
|
99
|
+
- **Grouping widens secret scope.** Every function in a unit reads every secret
|
|
100
|
+
that unit is granted, so treat a merge as a security decision too.
|
|
101
|
+
|
|
102
|
+
Group by something that means something — a domain, a secret scope, a deploy
|
|
103
|
+
cadence. There is deliberately no automatic packing by bundle size.
|
|
104
|
+
|
|
26
105
|
The frontends build independently (`bun run build` at the root builds every
|
|
27
106
|
workspace). Serve each behind its own hostname, and put the API behind `/api` on
|
|
28
107
|
**all of them**, mirroring the Vite proxy from the multi-app reference: `/api/auth/*` keeps its
|
|
@@ -53,6 +53,128 @@ Then **write the direction into `knowledge/decisions/design/`** — the words th
|
|
|
53
53
|
user gave you, what you chose, and what it rules out. The JSON records what the
|
|
54
54
|
theme is; only the note records why.
|
|
55
55
|
|
|
56
|
+
## The colours the theme has no field for
|
|
57
|
+
|
|
58
|
+
`brand` is the product's accent. It is not the only colour a screen needs, and
|
|
59
|
+
the missing ones are why "don't hardcode colours per component" gets broken by
|
|
60
|
+
the same agent that wrote it down.
|
|
61
|
+
|
|
62
|
+
A screen has to say *covered* and *still open*, *fine* and *needs attention* —
|
|
63
|
+
and those are not the accent. Using the accent for them is worse than a stray
|
|
64
|
+
hex: the brand colour stops meaning "this product" and starts meaning "good", so
|
|
65
|
+
it means nothing. But there is no `brand.covered` field, so the value lands
|
|
66
|
+
inline as `#3f7d5c`, once per component, slightly different each time.
|
|
67
|
+
|
|
68
|
+
Give them a home. A small stylesheet of custom properties, imported once beside
|
|
69
|
+
the Mantine styles, is enough:
|
|
70
|
+
|
|
71
|
+
```css
|
|
72
|
+
:root:root:root {
|
|
73
|
+
--app-covered: #3f7d5c; --app-covered-bg: #e6f1ea;
|
|
74
|
+
--app-open: #a8701a; --app-open-bg: #fbeedb;
|
|
75
|
+
--app-sunk: #fdf7f4; /* a recessed surface, for forms and asides */
|
|
76
|
+
--app-hairline: #ecdfd9; /* NOT var(--mantine-color-gray-2) — see below */
|
|
77
|
+
}
|
|
78
|
+
:root:root[data-mantine-color-scheme='dark'] {
|
|
79
|
+
--app-covered: #7fc09a; --app-covered-bg: #1e2f26;
|
|
80
|
+
--app-open: #e0ab5c; --app-open-bg: #33271a;
|
|
81
|
+
--app-sunk: #241b18;
|
|
82
|
+
--app-hairline: #392b26;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Those values are one app's warm direction, not a palette to copy — derive your
|
|
87
|
+
own from yours.
|
|
88
|
+
|
|
89
|
+
## Choose the neutrals; do not inherit them
|
|
90
|
+
|
|
91
|
+
The two details in that snippet that look like typos are the two things most
|
|
92
|
+
likely to make your app look like every other app.
|
|
93
|
+
|
|
94
|
+
**The hairline is a literal, not `var(--mantine-color-gray-2)`.** Mantine's grey
|
|
95
|
+
ramp is blue-biased — `#f8f9fa`, `#dee2e6`, `#868e96` are all cool — and it is
|
|
96
|
+
what draws card borders, dividers, the shell's edges and every disabled control.
|
|
97
|
+
The theme JSON has `brand` and `structure` and **no neutral field at all**, so
|
|
98
|
+
unless you choose otherwise, every app built from this skill runs its accent on
|
|
99
|
+
somebody else's greys.
|
|
100
|
+
|
|
101
|
+
If the accent is not itself blue, that mismatch lands on every screen at once:
|
|
102
|
+
warm content ruled off in cold lines, off everywhere and wrong nowhere in
|
|
103
|
+
particular, which is the hardest kind of wrong to find. It survives a careful
|
|
104
|
+
critique because no single screen is broken.
|
|
105
|
+
|
|
106
|
+
So bias the whole ramp toward the accent — not just the tokens with obvious
|
|
107
|
+
names. Keep Mantine's lightness steps so contrast behaviour and every component
|
|
108
|
+
that picks a step by number are unchanged; move only the hue:
|
|
109
|
+
|
|
110
|
+
```css
|
|
111
|
+
/* The ramps: hue only. Mantine picks a step by number in either scheme, so
|
|
112
|
+
these are scheme-independent and belong in the unscoped block. */
|
|
113
|
+
:root:root:root {
|
|
114
|
+
--mantine-color-gray-0: #faf7f5; --mantine-color-gray-5: #b8a49d;
|
|
115
|
+
--mantine-color-gray-1: #f5efec; --mantine-color-gray-6: #93807a;
|
|
116
|
+
--mantine-color-gray-2: #efe6e2; --mantine-color-gray-7: #574a45;
|
|
117
|
+
--mantine-color-gray-3: #e6dad5; --mantine-color-gray-8: #3d332f;
|
|
118
|
+
--mantine-color-gray-4: #d8c8c2; --mantine-color-gray-9: #2a1f1b;
|
|
119
|
+
--mantine-color-dark-0: #f3e9e4; --mantine-color-dark-5: #4a3a34;
|
|
120
|
+
--mantine-color-dark-1: #cdbdb6; --mantine-color-dark-6: #392b26;
|
|
121
|
+
--mantine-color-dark-2: #a8938c; --mantine-color-dark-7: #241b18;
|
|
122
|
+
--mantine-color-dark-3: #7a655e; --mantine-color-dark-8: #1b1512;
|
|
123
|
+
--mantine-color-dark-4: #5c4a44; --mantine-color-dark-9: #120d0b;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Ground, text and border are a different colour in each scheme, so each one
|
|
127
|
+
is set in the scheme it belongs to. A scheme-dependent token left in the
|
|
128
|
+
block above is the classic unreadable-in-dark bug. */
|
|
129
|
+
:root:root:root[data-mantine-color-scheme='light'] {
|
|
130
|
+
--mantine-color-body: #fdfaf8;
|
|
131
|
+
--mantine-color-text: #2a1f1b;
|
|
132
|
+
--mantine-color-default-border: #ecdfd9;
|
|
133
|
+
--mantine-color-dimmed: #7a625c; /* keep AA: ~5.4:1 on the body above */
|
|
134
|
+
--mantine-color-placeholder: #826a64; /* ~4.8:1 */
|
|
135
|
+
}
|
|
136
|
+
:root:root:root[data-mantine-color-scheme='dark'] {
|
|
137
|
+
--mantine-color-body: #1b1512;
|
|
138
|
+
--mantine-color-text: #f3e9e4;
|
|
139
|
+
--mantine-color-default-border: #392b26;
|
|
140
|
+
--mantine-color-dimmed: #a8938c; /* ~5.6:1 on the body above */
|
|
141
|
+
--mantine-color-placeholder: #8c7a74; /* ~4.7:1 */
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Check the four text tokens against your own grounds rather than copying these —
|
|
146
|
+
`dimmed` is the most-used text colour in the app and the easiest to drop below
|
|
147
|
+
4.5:1 while making it prettier, and it has to clear the bar on *both* grounds.
|
|
148
|
+
|
|
149
|
+
**The selector is tripled on purpose.** Mantine's `cssVariablesResolver` injects
|
|
150
|
+
its own `:root` block into `<head>` at runtime, which lands *after* your
|
|
151
|
+
stylesheet and wins on source order at equal specificity. A plain `:root` here is
|
|
152
|
+
silently reverted: the file reads correct, the app renders Mantine's defaults,
|
|
153
|
+
and nothing errors. Repeating the pseudo-class raises specificity without adding
|
|
154
|
+
an element to the selector.
|
|
155
|
+
|
|
156
|
+
This one costs a whole pass if you meet it without knowing: you diagnose the
|
|
157
|
+
colours correctly, write the right values, reload, and see no change — so you
|
|
158
|
+
assume the diagnosis was wrong. **The only thing that catches it is looking at a
|
|
159
|
+
screenshot and disbelieving the CSS.**
|
|
160
|
+
|
|
161
|
+
Name them for what they *mean* in this product, never for the colour — `covered`,
|
|
162
|
+
not `green`. The name is the whole value: it survives a change of palette, and it
|
|
163
|
+
is the thing that makes the second use agree with the first. Define both colour
|
|
164
|
+
schemes at once; a token defined only in light is the classic unreadable-in-dark
|
|
165
|
+
bug, and Mantine will happily render it.
|
|
166
|
+
|
|
167
|
+
The same file is where a couple of other things belong that the theme JSON has no
|
|
168
|
+
field for and every screen otherwise re-invents: the hairline that separates rows
|
|
169
|
+
in a list, the recessed surface a form sits on so it does not carry the same
|
|
170
|
+
weight as the content it adds to, and the one animation the product is allowed
|
|
171
|
+
(behind `prefers-reduced-motion`). Two or three rules, not a framework.
|
|
172
|
+
|
|
173
|
+
If the app ships template screens you did not write — the error and not-found
|
|
174
|
+
pages usually — read them before you call the palette done. They arrive with the
|
|
175
|
+
scaffold's colours hardcoded, and a stock blue accent on an app whose direction
|
|
176
|
+
says warm is the single loudest contradiction in the build.
|
|
177
|
+
|
|
56
178
|
**Set the theme once, don't hardcode colours per component.** A screen full of
|
|
57
179
|
inline `color="blue"` and one-off hex values is why apps look templated. Change
|
|
58
180
|
the theme, not the components — and keep it theme-aware for light and dark.
|
|
@@ -284,6 +284,20 @@ pikku knowledge plan defer <milestone> <item> -r "<why>"
|
|
|
284
284
|
|
|
285
285
|
`progress` reconciles the plan against pikku's generated meta — set membership, never anyone's status — and exits non-zero while the first pass is short, or while anything already built contradicts the plan. Unbuilt work in a later pass is reported, not blocked; a function that shipped wide open against a planned permission rule blocks from any pass, because that is a hole rather than a backlog. Writing a plan is its own seat: read `pikku-architect`. Building against one is `pikku-build`.
|
|
286
286
|
|
|
287
|
+
### A finished milestone is a tombstone
|
|
288
|
+
|
|
289
|
+
**Once a milestone reaches `built`, its note and its plan are closed. Do not edit either.** Not to correct the wording, not to fold in what the build actually turned out to need, not to add the item everyone agrees should have been there. A finished milestone is the record of what was agreed and what was measured against it, and a record that can be revised afterwards measures nothing.
|
|
290
|
+
|
|
291
|
+
This is the rule the shape of the thing already implies. `progress` reconciles a plan against generated meta and fails when what shipped contradicts it — a check with no force at all if the losing side of the contradiction may simply be rewritten. `attempts:` brakes a note nothing can satisfy, and refunds that budget when the note's content really changes; a `built` note that keeps changing is that brake removed. Both only work while the plan stays still.
|
|
292
|
+
|
|
293
|
+
So when a `built` milestone turns out to be wrong or incomplete, **the answer is always a new note, never an edit to the old one**:
|
|
294
|
+
|
|
295
|
+
- It needed more than it said → a new milestone, which may name the old one.
|
|
296
|
+
- It was built differently than planned → that is what `progress` is for. Reconcile forward, or record a decision saying why the plan was not the right shape.
|
|
297
|
+
- It was simply wrong → a decision note that supersedes it. The wrong milestone stays where it is; a base whose history is edited cannot answer *why* anything is the way it is, which is most of what a base is for.
|
|
298
|
+
|
|
299
|
+
The exception, and it is narrow: bookkeeping the loop owns. `statusAt:` and `attempts:` are written by whatever moved the note, at any status, and are bookkeeping rather than content. Nothing else about a `built` note moves again.
|
|
300
|
+
|
|
287
301
|
## Profiles built on this one
|
|
288
302
|
|
|
289
303
|
OKF permits frontmatter fields a reader does not know, and the parser ignores them rather than failing. That is the extension point: a tool layered on Pikku can add its own sections and fields on top of everything above without forking the format.
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# Pikku Services (Dependency Injection)
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
## Before You Start
|
|
5
4
|
|
|
6
5
|
```bash
|
|
@@ -195,6 +194,13 @@ const createSingletonServices = pikkuServices(async (config) => {
|
|
|
195
194
|
})
|
|
196
195
|
```
|
|
197
196
|
|
|
197
|
+
A deployment split regenerates this manifest **per unit**, so the same
|
|
198
|
+
`services.ts` sees a different manifest in each bundle and each unit builds only
|
|
199
|
+
what its own functions, middleware and permissions reach. A `services.ts` that
|
|
200
|
+
constructs unconditionally gets none of that: every unit pays for every service,
|
|
201
|
+
and the split buys nothing but duplicated bytes. Branching on the manifest is
|
|
202
|
+
what makes the split real.
|
|
203
|
+
|
|
198
204
|
### Audit Wire Service
|
|
199
205
|
|
|
200
206
|
`createInvocationAudit` + `createAuditedKysely` add per-request audit buffering that flushes on request close (no-op if `audit` is unconfigured). For the full pattern, no-op behavior, custom-event usage, and Fabric notes, read `references/audit-wire-service.md`.
|