@graview/skills 0.0.0-stage → 0.1.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.
- package/LICENSE +96 -0
- package/README.md +46 -2
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +54 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -3
- package/skills/graview-agent-seat/SKILL.md +159 -0
- package/skills/graview-brand/SKILL.md +137 -0
- package/skills/graview-desk/SKILL.md +84 -0
- package/skills/graview-embed/SKILL.md +86 -0
- package/skills/graview-invariant/SKILL.md +112 -0
- package/skills/graview-lens/SKILL.md +187 -0
- package/skills/graview-new-app/SKILL.md +195 -0
- package/skills/graview-node-kind/SKILL.md +159 -0
- package/skills/graview-pages/SKILL.md +206 -0
- package/skills/graview-permissions/SKILL.md +145 -0
- package/skills/graview-port-app/SKILL.md +95 -0
- package/skills/graview-seed/SKILL.md +103 -0
- package/skills/graview-ship/SKILL.md +154 -0
- package/skills/graview-studio/SKILL.md +85 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graview-new-app
|
|
3
|
+
description: Start a product on Graview in its own repository — the shape of the declaration, the shell that comes for free, and the CI that keeps it honest afterwards.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Start a product on Graview
|
|
7
|
+
|
|
8
|
+
Graview ships as eleven packages. A product built on it lives in **its own
|
|
9
|
+
repository** and depends on them the way any other consumer does. This is the
|
|
10
|
+
setup that gets you from nothing to something that can tell you when you have
|
|
11
|
+
broken it.
|
|
12
|
+
|
|
13
|
+
## Start with the scaffolder
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
# from a framework checkout (the packages are not published yet):
|
|
17
|
+
pnpm install && pnpm build
|
|
18
|
+
pnpm graview create ../my-app --link . --name "My App" --kind thing
|
|
19
|
+
# once published, from anywhere:
|
|
20
|
+
npm create graview@latest my-app # or: pnpm create graview my-app
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Put the product BESIDE the framework, never inside its git tree; `--link`
|
|
24
|
+
names it relative to where you run the command. `--workspace` writes the
|
|
25
|
+
layout every real product ends up with (a root, the app under `app/`, the
|
|
26
|
+
harnesses beside it); `--merge` starts in a repository that already has a
|
|
27
|
+
README, naming collisions rather than writing over them.
|
|
28
|
+
|
|
29
|
+
It writes exactly the shape below — one kind with `creates`, `connects`,
|
|
30
|
+
`writes` and a `lifecycle`, one rule with its repair, the shell, the routed
|
|
31
|
+
face, a headless test, a CI workflow — installs it, and installs these skills
|
|
32
|
+
into it. Run its `verify`, then replace the first kind with the product's own.
|
|
33
|
+
The rest of this skill is what each part is for.
|
|
34
|
+
|
|
35
|
+
## The shape
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
src/
|
|
39
|
+
domain/ # No React in here. This is what `graview check` reads.
|
|
40
|
+
schema.ts # defineNode × n, createSchema (z comes from @graview/core)
|
|
41
|
+
mutations.ts # defineMutation × n — every change is a named, typed act
|
|
42
|
+
invariants.ts # defineInvariant × n — rules that name their repairs
|
|
43
|
+
policy.ts # who may do what, if anyone
|
|
44
|
+
brand.ts # name, mark, typeface, palette
|
|
45
|
+
app.ts # defineApp({ ... }) — one object, the whole surface
|
|
46
|
+
ui/
|
|
47
|
+
views.tsx # registerDefaultViews, then your own where you care
|
|
48
|
+
app.tsx # the provider, the scene, the workbench
|
|
49
|
+
main.tsx
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The domain/ui split keeps the declaration inspectable by a build, a CLI and
|
|
53
|
+
an agent: `graview check` reads `defineApp`, and a React import there would
|
|
54
|
+
drag a UI package into the checker.
|
|
55
|
+
|
|
56
|
+
## Do this
|
|
57
|
+
|
|
58
|
+
1. **Install.** The scaffold already did. Reach for `@graview/render/gpu`
|
|
59
|
+
only if you want the experimental capture path; the DOM path is what ships.
|
|
60
|
+
|
|
61
|
+
2. **Declare one kind, one mutation, one rule.** Not the whole domain. The
|
|
62
|
+
loop you want on day one is: declare → `graview check` → look → declare
|
|
63
|
+
more.
|
|
64
|
+
|
|
65
|
+
3. **Open it empty before you believe in it.** `graview describe
|
|
66
|
+
./dist/domain/app.js` reads out what a blank installation meets; `<Begin>`
|
|
67
|
+
is that as a surface, wired into the scaffolded home. See `graview-seed`.
|
|
68
|
+
|
|
69
|
+
4. **Declare the seams that make the interface smart.** These are one-line
|
|
70
|
+
declarations on mutations and kinds, and every derived surface reads them:
|
|
71
|
+
- `creates: ["<kind>"]` on every mutation that adds a kind — the chain
|
|
72
|
+
`Begin` and `graview describe` read, and the empty card's own way in.
|
|
73
|
+
- `connects: [...]` / `severs: [...]` naming the edge kinds a mutation
|
|
74
|
+
makes or breaks — what makes drawn LINES selectable, offers the act from
|
|
75
|
+
either end, and hides a severing act with nothing to sever.
|
|
76
|
+
- `lifecycle: { field, retired }` on kinds whose members expire — counts
|
|
77
|
+
say "+N past".
|
|
78
|
+
- `subject: { kinds, arg }` on every mutation that acts on a thing.
|
|
79
|
+
|
|
80
|
+
5. **Take the shell.** `Inspector`, `Standing`, `ActivityRail`, `ChatPanel`,
|
|
81
|
+
`QuickRelations`, `RelationKey`, `BackOut`, `Trail`, `OverviewButton` and
|
|
82
|
+
`Wordmark` from `@graview/primitives` are the parts of an interface that
|
|
83
|
+
are not about your domain — including a chat seat that answers from the
|
|
84
|
+
graph with no API key. A shell is about eighty lines; if yours is longer,
|
|
85
|
+
you are probably rebuilding something derived.
|
|
86
|
+
|
|
87
|
+
Search comes with `Shell`: its `FindBox` answers `/` or ⌘K from
|
|
88
|
+
anywhere, lights what the words find in whatever picture is open and
|
|
89
|
+
dims the rest, and `#q=` makes a search a stop Back returns to. A shell
|
|
90
|
+
of your own puts `<FindBox />` in its bar; nothing is declared per kind.
|
|
91
|
+
|
|
92
|
+
The routed face is one branch in `main.tsx`: when the path starts with
|
|
93
|
+
`/pages`, render `<PagesApp basename="/pages" context={{ store, brand,
|
|
94
|
+
views: views(), settings }} />` from `@graview/pages`. It lands on a
|
|
95
|
+
gallery of the app's pictures — every kind drawn as a card until you title
|
|
96
|
+
a lens, then the lens by its name — with a list, a record and a form per
|
|
97
|
+
kind, the problems and the map, at phone widths. Hand it the same `views`
|
|
98
|
+
the scene draws from, or it has no pictures to land on.
|
|
99
|
+
|
|
100
|
+
6. **Register default views first, override later.** `registerDefaultViews`
|
|
101
|
+
means a new kind renders sensibly at all three fidelities before you write
|
|
102
|
+
anything. Write a custom view for a kind when the generic one is genuinely
|
|
103
|
+
wrong, not on principle.
|
|
104
|
+
|
|
105
|
+
7. **Add the check to your build.** `"check": "graview check
|
|
106
|
+
./dist/domain/app.js"`, and a `verify` that runs typecheck, test, build
|
|
107
|
+
and check in that order. The scaffold writes both.
|
|
108
|
+
|
|
109
|
+
## What to make yours next
|
|
110
|
+
|
|
111
|
+
The scaffold is deliberately the framework's own face. A product replaces it
|
|
112
|
+
in this order, and each step has a skill and a worked chapter in
|
|
113
|
+
`apps/seedbed` behind it:
|
|
114
|
+
|
|
115
|
+
1. **The words.** Every edge gets `description` and `inverse` — how it reads
|
|
116
|
+
from each end — or `graview check` says `edge-without-inverse` and the
|
|
117
|
+
far end is captioned with the edge kind's name. Every mutation gets a
|
|
118
|
+
`title` and `description`; they are the button and the tool schema.
|
|
119
|
+
(`graview-node-kind`.)
|
|
120
|
+
2. **A lens with a name.** A lens registered over a group with a `title` is
|
|
121
|
+
a PLACE — in the bar, on an embed's strip, one press from anywhere. Start
|
|
122
|
+
from the three that ship; write your own when the domain has a picture of
|
|
123
|
+
itself, the way the garden has a map. (`graview-lens`, chapters 10-13.)
|
|
124
|
+
3. **The pages.** One page in the product's words first, then, when the
|
|
125
|
+
product needs to look like a product, every surface: the shell, the home,
|
|
126
|
+
the lists, the records, the problems — over the same store, acts, rules
|
|
127
|
+
and permissions. (`graview-pages`, chapters 9 and 13.)
|
|
128
|
+
4. **A seat and a policy.** Who may do what, declared once; the strip, the
|
|
129
|
+
pages and the agent's tools all narrow from it. (`graview-permissions`,
|
|
130
|
+
`graview-agent-seat`, chapters 5 and 7.)
|
|
131
|
+
5. **A brand, and shipping.** The name, the mark, the typefaces, the palette
|
|
132
|
+
the checker holds to AA; a version and a migration so a stored graph is
|
|
133
|
+
carried forward. (`graview-brand`, `graview-ship`, chapters 8 and 12.)
|
|
134
|
+
|
|
135
|
+
Work with an agent beside the declaration: describe a kind, let it draft the
|
|
136
|
+
edges, acts and rule, run `pnpm verify`, look, declare more.
|
|
137
|
+
|
|
138
|
+
## The CI a product on Graview needs
|
|
139
|
+
|
|
140
|
+
Copy the shape from the framework's own `.github/workflows/ci.yml`, minus the
|
|
141
|
+
packaging steps you do not need. What earns its place:
|
|
142
|
+
|
|
143
|
+
- **`tsc`** — an edge to an undeclared kind is a typecheck failure, so this
|
|
144
|
+
catches a whole class before anything runs.
|
|
145
|
+
- **`graview check`** — everything `tsc` cannot see: a repair naming a mutation
|
|
146
|
+
nobody registered, a lens role bound to a missing field, a role that may do
|
|
147
|
+
nothing, a palette pair below AA. **Fail the build on errors.** Warnings are
|
|
148
|
+
a judgement call; errors are not.
|
|
149
|
+
- **Headless tests** — the domain tier has no DOM in it. Test that your rules
|
|
150
|
+
fire on graphs that break them and that their repairs resolve them; that is
|
|
151
|
+
the test that catches a real regression.
|
|
152
|
+
- **An accessibility run, in BOTH schemes.** Copy
|
|
153
|
+
`apps/todo/scripts/run-a11y.mjs`: the real accessibility tree through CDP,
|
|
154
|
+
plus axe-core. A light palette that clears AA in the dark is what it catches.
|
|
155
|
+
|
|
156
|
+
Worth stealing later: a browser harness that walks your own acceptance
|
|
157
|
+
criteria and writes a JSON verdict (the framework has several; each names
|
|
158
|
+
the claim that stopped being true), and a persistence adapter test.
|
|
159
|
+
|
|
160
|
+
## Supported browsers
|
|
161
|
+
|
|
162
|
+
Build for the DOM path: Chromium, WebKit and Firefox, all three verified by
|
|
163
|
+
the framework (`pnpm engines`). The floor is `document.adoptedStyleSheets`
|
|
164
|
+
(Safari 16.4+, Firefox 101+, Chromium 99+). The altitude morph rides
|
|
165
|
+
`@property` and degrades to a clean cut where that is missing — write no
|
|
166
|
+
fallback. The chat's local-model rung needs WebGPU or Chrome's Prompt API;
|
|
167
|
+
without either the graph still answers and the header says why. The GPU
|
|
168
|
+
capture path is Chromium-only, experimental and opt-in (`attachRenderer`
|
|
169
|
+
from `@graview/render/gpu`; there is no URL switch) — never a requirement.
|
|
170
|
+
|
|
171
|
+
## Then find out whether it worked
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
pnpm verify # typecheck, tests, build, graview check
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Report the real output. On a new app the useful early findings are
|
|
178
|
+
`mutation-untitled`, `mutation-undescribed`, `edge-without-inverse` and
|
|
179
|
+
`required-invariant-unregistered` — things that look fine until somebody
|
|
180
|
+
reads the interface or an agent reads a tool schema.
|
|
181
|
+
|
|
182
|
+
## What the check cannot see
|
|
183
|
+
|
|
184
|
+
- Whether your kinds are the right kinds. The test for each: does anything
|
|
185
|
+
point AT it, and does it have a life of its own? A colour is a field. A
|
|
186
|
+
fixture is a kind.
|
|
187
|
+
- Whether your mutations are the acts a person would name. They are the labels
|
|
188
|
+
in the strip and the instructions in an agent's tool schema, so an opaque one
|
|
189
|
+
costs twice.
|
|
190
|
+
- Whether the interface is any good. Run it. Run it at the size it will have: seed a
|
|
191
|
+
real catalogue, not a dozen rows. The scene draws what a person can read — a
|
|
192
|
+
relation too long for its band is grouped by its best arrangement or closes
|
|
193
|
+
on "+N more", and a thumbnail draws its 12 most relevant members — so a
|
|
194
|
+
thousand records are a picture, not a smear. What that hides is yours to
|
|
195
|
+
judge: whether the groups are the ones a person would ask for.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graview-node-kind
|
|
3
|
+
description: Add a node kind to a Graview app — fields, edges, label, plural and field roles — and verify it with graview check rather than claiming it worked.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add a node kind
|
|
7
|
+
|
|
8
|
+
A kind is the unit of everything here. Declaring one gets you, without another
|
|
9
|
+
line: a place in the layout, a card at three fidelities, an aggregate view, an
|
|
10
|
+
accessibility label, a hue, a slot in the kinds plane, and inclusion in every
|
|
11
|
+
agent tool that walks the graph.
|
|
12
|
+
|
|
13
|
+
## Do this
|
|
14
|
+
|
|
15
|
+
1. **Find the schema.** It is the file calling `createSchema`. Usually
|
|
16
|
+
`src/domain/schema.ts`.
|
|
17
|
+
|
|
18
|
+
2. **Declare the kind.** Zod is the single source of runtime validation,
|
|
19
|
+
TypeScript types and JSON Schema — there is no second place to describe a
|
|
20
|
+
field.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
export const fixture = defineNode("fixture", {
|
|
24
|
+
fields: z.object({
|
|
25
|
+
// A NAME, and bounded. Every derived surface draws `label` — on a
|
|
26
|
+
// card, in a list, on a plan — and `z.string().min(1)` permits a
|
|
27
|
+
// paragraph. That is fine while a person is typing it; a model asked
|
|
28
|
+
// to survey a garden wrote "Pea-gravel corner with river-rock
|
|
29
|
+
// border, log seats and a fire bowl", which is a true sentence and a
|
|
30
|
+
// terrible name, and it was never told otherwise because the prompt
|
|
31
|
+
// is generated from this file. `graview check` notes
|
|
32
|
+
// `label-unbounded` on any kind a declared provider may create.
|
|
33
|
+
label: z.string().min(1).max(60),
|
|
34
|
+
kickOff: z.string(),
|
|
35
|
+
opponent: z.string(),
|
|
36
|
+
}),
|
|
37
|
+
plural: "Fixtures",
|
|
38
|
+
description: "A match, and the team you intend to put out for it.",
|
|
39
|
+
edges: {
|
|
40
|
+
// One edge, two readings: the fixture's page says who is in the
|
|
41
|
+
// team; the player's page says which fixtures they are picked for.
|
|
42
|
+
"picked-for": {
|
|
43
|
+
to: ["player"],
|
|
44
|
+
description: "who is in the team",
|
|
45
|
+
inverse: "the fixtures they are picked for",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
// Lets a lens ask for "the start time" without knowing your field names.
|
|
49
|
+
fieldRoles: { start: "kickOff" },
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
3. **Add it to `createSchema([...])`.** An edge pointing at a kind that is not
|
|
54
|
+
in that array is a *typecheck* failure, not a runtime one — `createSchema`'s
|
|
55
|
+
`ValidateEdgeTargets` sees it.
|
|
56
|
+
|
|
57
|
+
4. **Say what a node of it is called.** `label` defaults to a `label` field and
|
|
58
|
+
then to the id. An id in the interface is a bug you shipped, not a
|
|
59
|
+
placeholder. And say what ONE of the kind is called where its id does not:
|
|
60
|
+
`noun: "staff member"` on a kind called `staff`, or every sentence about
|
|
61
|
+
one reads "Change the staff".
|
|
62
|
+
|
|
63
|
+
An id is still a name a caller may choose: every act that declares
|
|
64
|
+
`creates: ["fixture"]` takes an optional `id` argument the framework adds,
|
|
65
|
+
so a seed being synced or an agent that will refer to the node next call
|
|
66
|
+
gets exactly the id it asked for, or a refusal by name. Never hardcode a
|
|
67
|
+
bootstrap id — `"today"`, `"u-nick"` — inside a mutation body or an
|
|
68
|
+
invariant; bind a role, a flag or an edge instead, so a store seeded
|
|
69
|
+
differently still works. And every kind gets `remove-<kind>` derived,
|
|
70
|
+
permitted through the acts that create it.
|
|
71
|
+
|
|
72
|
+
5. **Give it verbs.** A kind with no mutation naming it as a subject renders
|
|
73
|
+
fine and can have nothing done to it, and the actions strip will say so out
|
|
74
|
+
loud. If that is not what you meant, see `graview-invariant` for the rule
|
|
75
|
+
shape and add at least one mutation whose `subject.kinds` includes it.
|
|
76
|
+
|
|
77
|
+
6. **Let the fields be changed, or say why not.** A field you could set at
|
|
78
|
+
creation, you can change: every settable field no mutation writes is
|
|
79
|
+
covered by a derived act per kind (`edit-<kind>`, "Change the drill"),
|
|
80
|
+
editable where it is shown and logged like any other. Two declarations
|
|
81
|
+
shape it. `writes: ["done"]` on a mutation says which fields it sets when
|
|
82
|
+
its arguments do not (`finish()` writing `done`) — and a mutation whose
|
|
83
|
+
argument merely shares a field's name should say `writes: []`. `fixed:
|
|
84
|
+
{ text: "the client's words, as sent" }` on the kind says a field never
|
|
85
|
+
changes, and why; the sentence is the documentation. The checker warns
|
|
86
|
+
`field-without-writer` when a field is still out of everyone's reach,
|
|
87
|
+
and refuses `writes-unknown-field`, `fixed-unknown-field` and a
|
|
88
|
+
`fixed-but-written` contradiction.
|
|
89
|
+
|
|
90
|
+
## Worked examples
|
|
91
|
+
|
|
92
|
+
- `apps/todo/src/domain/schema.ts` — four kinds, including a `rule` kind whose
|
|
93
|
+
`spec` makes the rules a domain enforces into data, and `fieldRoles` binding
|
|
94
|
+
the timeline lens to a task's own field names
|
|
95
|
+
- `apps/seedbed/src/domain/schema.ts` — a kind with a declared `lifecycle`, so
|
|
96
|
+
the past is a horizon rather than a delete
|
|
97
|
+
|
|
98
|
+
## The declarations that keep paying
|
|
99
|
+
|
|
100
|
+
- **`creates`** on the mutation that adds this kind (`creates: ["fixture"]`):
|
|
101
|
+
the empty kind card then offers "Add a fixture" by derivation — the blank
|
|
102
|
+
graph onboards itself, FROM THE ROOT OF THE CHAIN. An act that also takes
|
|
103
|
+
a `nodeRef` has no candidates on an empty graph, so it is withheld (a
|
|
104
|
+
picker with nothing in it is worse than no button) and the district says
|
|
105
|
+
what it is waiting for instead: *"Place a feature" cannot begin until there
|
|
106
|
+
is a zone.* Expect exactly one way in on a blank installation, and check
|
|
107
|
+
that it is the one you meant — this only ever shows up on the graph nobody
|
|
108
|
+
tests against.
|
|
109
|
+
- **`fromTheOtherEnd`** on the act that makes or breaks the edge. An act
|
|
110
|
+
declaring `connects` or `severs` is offered from BOTH ends of the tie, and
|
|
111
|
+
`title` is written from the subject's side: "Name a caretaker", offered on
|
|
112
|
+
the gardener, reads as naming hers. Say how it reads standing there
|
|
113
|
+
(`fromTheOtherEnd: "Take on a plot"`) — `graview check` warns
|
|
114
|
+
`act-without-far-end-reading` and names the end it has no words for.
|
|
115
|
+
- **`lifecycle`** when members expire — `{ field: "status", retired:
|
|
116
|
+
["played"] }` or `{ field: "until", retired: "date" }`. Every count then
|
|
117
|
+
aggregates over the horizon ("4, +12 past") instead of drowning, and
|
|
118
|
+
`graview check` refuses a lifecycle reading a missing field.
|
|
119
|
+
- **A figure**, which is line art of the THING at the city's own three-quarter
|
|
120
|
+
angle — a person, a plot of ground, a gutter — drawn wherever the kind is
|
|
121
|
+
drawn. Nine ship; any domain that is not an abstract tracker runs out of
|
|
122
|
+
them at once, so draw the rest:
|
|
123
|
+
`graview figure ./dist/domain/app.js --kind gutter --from "a gutter along a
|
|
124
|
+
roof edge"` prints the brief (the rules, the angle, a shipped figure as the
|
|
125
|
+
style), and `--judge <file>` reads the answer back, holds it to the rules
|
|
126
|
+
`graview check` holds a figure to, and prints the line to paste. Then look
|
|
127
|
+
at it at twenty pixels, which is the size a chip gives it.
|
|
128
|
+
- **A declared hue** in the brand (`accents: { fixture: 210 }`) if this kind
|
|
129
|
+
should wear a chosen colour rather than a stable hash — every chip dot,
|
|
130
|
+
district roof and the focus tag follow.
|
|
131
|
+
|
|
132
|
+
## Then find out whether it worked
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
pnpm build && npx graview check ./dist/domain/app.js
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Report **the actual output**, including warnings. What it catches here:
|
|
139
|
+
|
|
140
|
+
- `edge-target-undeclared` — an edge to a kind nobody declared
|
|
141
|
+
- `edge-without-inverse` — a relation with words for one of its two readings
|
|
142
|
+
- `edge-name-shared` — one edge name declared on two kinds in two sets of
|
|
143
|
+
words (`by` on a song and on an album); every surface treats a name as ONE
|
|
144
|
+
relation, so name each its own
|
|
145
|
+
- `act-without-far-end-reading` — an act offered on an end it has no words for
|
|
146
|
+
- `field-role-missing-field` — a role pointing at a field that is not there
|
|
147
|
+
- `required-invariant-unregistered` — `requiresInvariant` naming no rule
|
|
148
|
+
- `mutation-untitled` / `mutation-undescribed` — a verb nobody can read
|
|
149
|
+
|
|
150
|
+
## What the check cannot see
|
|
151
|
+
|
|
152
|
+
Say so rather than implying otherwise:
|
|
153
|
+
|
|
154
|
+
- Whether the kind is a *kind* at all, or should have been a field on an
|
|
155
|
+
existing one. The test: does anything point AT it, and does it have a life of
|
|
156
|
+
its own? A colour is a field. A fixture is a kind.
|
|
157
|
+
- Whether the default views read well. Run the app and look.
|
|
158
|
+
- Whether the plural reads naturally in a sentence — "3 Fixtures" is fine,
|
|
159
|
+
"3 Person" is not.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graview-pages
|
|
3
|
+
description: Give a Graview app the routed face it wants — from the derived pages, to one page in the app's own words, to a product design that replaces every surface.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# The pages face
|
|
7
|
+
|
|
8
|
+
A Graview app has two faces over one store. The scene is the picture. The
|
|
9
|
+
pages face is the same declaration routed as an ordinary web application:
|
|
10
|
+
a home, a list per kind, a record per node, forms for every act, and a
|
|
11
|
+
problems page — derived, then replaceable one surface at a time, all the way
|
|
12
|
+
to a product design of its own. The ladder ends where the framework's own
|
|
13
|
+
example does: `apps/seedbed`, chapters nine and thirteen.
|
|
14
|
+
|
|
15
|
+
## What comes for free
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
// main.tsx — one branch: the path decides the face
|
|
19
|
+
if (location.pathname.startsWith("/pages")) {
|
|
20
|
+
root.render(<PagesApp basename="/pages" context={{ store, brand, principal, sceneHref: "/" }} />);
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`PagesApp` renders with no registry at all:
|
|
25
|
+
|
|
26
|
+
- `/` — a GALLERY. The standing as the headline ("2 gardeners, 3 plots and
|
|
27
|
+
1 planting.", or "Nothing here yet." and which act begins it), then every
|
|
28
|
+
picture as a large live card, two across at a desk, one on a phone, then
|
|
29
|
+
the kinds as a row of counts, a line to the map, and Recently. A kind
|
|
30
|
+
with no titled lens gets a contact sheet of its members, so a new app
|
|
31
|
+
lands on a gallery; titling a lens replaces that card with the
|
|
32
|
+
lens by its name. An empty picture names the act that fills it.
|
|
33
|
+
- `/<plural>` — a list per kind, marking trouble, with the creating acts
|
|
34
|
+
beneath it.
|
|
35
|
+
- `/<plural>/<id>` — a record: its facts, its relations captioned in the
|
|
36
|
+
declaration's words, what can be done, what has happened.
|
|
37
|
+
- `/problems` — every broken rule with its repairs.
|
|
38
|
+
|
|
39
|
+
The shell is one row — pictures (home), kinds, Map, Problems — scrolling
|
|
40
|
+
sideways on a phone. Shell and gallery are 1160px wide; read pages, 760.
|
|
41
|
+
|
|
42
|
+
**Hand it `views`** (the scene's registry, plus `settings`/`presence`) —
|
|
43
|
+
`graview create` does — and the face puts the scene's provider
|
|
44
|
+
under its routes, which buys three things at once:
|
|
45
|
+
|
|
46
|
+
- `/places`, `/places/<as>` — every named lens as a page (fullscreen, over
|
|
47
|
+
the kind's members, siblings one press away, the beginning acts beneath)
|
|
48
|
+
and the gallery again. A kind's page lists its pictures; a pick in a lens
|
|
49
|
+
travels to the record.
|
|
50
|
+
- `/map` — `kindMap(store)`: every declared relation in its words with
|
|
51
|
+
its live count, also a section on the home page. A kind's list says what
|
|
52
|
+
it relates to and arranges itself in the shared words (`?sort=due:desc`,
|
|
53
|
+
`?filter=done:false`, `?group=due:month`, `?q=tape`) a lens carries in
|
|
54
|
+
its fragment, so an arrangement is a link; `?by=`, `?<edge>=<id>`,
|
|
55
|
+
`?with=` and `?past=1` still land. A record links back.
|
|
56
|
+
- `/search?q=` — the Find box's matcher: hits by kind, each with why; a
|
|
57
|
+
nav box narrows a list, else lands here. Find and the way back ("Take
|
|
58
|
+
back “…”", ⌘Z) are on every face: a shell that places `<PageFind>` or
|
|
59
|
+
`<PageUndo>` says where, one that does not gets them drawn around it,
|
|
60
|
+
and `surface("shell", Shell, { without: ["find"] })` goes without.
|
|
61
|
+
- **The assistant**, on every route: one control opens the scene's own
|
|
62
|
+
`Companion` in a drawer, and the ROUTE is what "this" means. Grounded
|
|
63
|
+
questions before anybody types; proposals apply through the same runtime,
|
|
64
|
+
attributed and undoable, withheld ones struck through. Open questions are
|
|
65
|
+
listed on `/problems`; the rung that answers is chosen in the footer.
|
|
66
|
+
|
|
67
|
+
Everything a page shows is a derivation the scene also uses: `recordFacts`,
|
|
68
|
+
`deriveAffordances`, `store.permits`. **A page never decides what an act is
|
|
69
|
+
or who may take it** — it strikes through what the seat may not, and says why.
|
|
70
|
+
|
|
71
|
+
**At a phone's width this face is the answer.** The scene still holds there —
|
|
72
|
+
districts stay legible, panels scroll — but
|
|
73
|
+
a 134px card in a 390px viewport is a city through a letterbox. `Shell`
|
|
74
|
+
carries `pagesHref`.
|
|
75
|
+
|
|
76
|
+
## Rung one: a page in the app's own words
|
|
77
|
+
|
|
78
|
+
`createPageRegistry(schema)` replaces pages per kind and surfaces per app:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { createPageRegistry, DerivedForm, kindFacts, PageMain, pageStyles, recordFacts, spatialHref, useStoreTick } from "@graview/pages";
|
|
82
|
+
|
|
83
|
+
function PlotPage({ context }: { context: PageContext<S> }) {
|
|
84
|
+
const { store, principal } = context;
|
|
85
|
+
useStoreTick(store); // re-render on every op
|
|
86
|
+
const id = decodeURIComponent(useParams()["id"] ?? "");
|
|
87
|
+
const facts = recordFacts(store, id, { principal }); // the same derivations
|
|
88
|
+
return (
|
|
89
|
+
<PageMain context={context}>
|
|
90
|
+
<h1 style={pageStyles.h1}>{facts.label}</h1>
|
|
91
|
+
<a href={spatialHref(id)}>See it in the scene ↗</a>
|
|
92
|
+
<DerivedForm store={store} mutation={sow} prefilled={{ plotId: id }} />
|
|
93
|
+
</PageMain>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const pages = createPageRegistry<S, PageComponent<S>>(schema)
|
|
98
|
+
.register("plot", "record", PlotPage)
|
|
99
|
+
.route("/survey", SurveyDesk); // about nothing in the schema
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`.route(path, Component)` gives a page that is NOT about a kind an address —
|
|
103
|
+
onboarding, settings, import — matched before `/:plural`. **Every `to` is
|
|
104
|
+
basename-relative** (`to="/survey"`, never `to="/pages/survey"`), and so is
|
|
105
|
+
`initialPath`, so a test renders the hrefs a browser will.
|
|
106
|
+
|
|
107
|
+
Rules for a page at this rung:
|
|
108
|
+
|
|
109
|
+
1. **`PageMain`, not `<main>`** — inside an embed the host owns the one
|
|
110
|
+
`main`, and a hand-written one is a duplicate landmark.
|
|
111
|
+
2. **`useStoreTick(store)`** at the top of every page that reads the graph,
|
|
112
|
+
or it goes stale after the first act.
|
|
113
|
+
3. **`pageStyles`** for the parts you did not design, so one custom page
|
|
114
|
+
still reads as the same face as the derived ones.
|
|
115
|
+
4. **Prefill, never wire.** `DerivedForm` with `prefilled` is how a record
|
|
116
|
+
offers an act about itself; the form asks for the rest.
|
|
117
|
+
5. **Read permission before drawing an act.** `store.permits({ name, args },
|
|
118
|
+
principal)`, struck through with `verdict.refusal.message`. A form that
|
|
119
|
+
refuses on submit is the bug this prevents.
|
|
120
|
+
6. **Link to a PICTURE, not only to a node.** `spatialHref(id)` opens the
|
|
121
|
+
scene on one thing; `placeHref(as)` — `/#view=the-grounds`, the title
|
|
122
|
+
through `placeSlug` — opens it on one named place, group in focus, not on
|
|
123
|
+
a default view that says "press The grounds".
|
|
124
|
+
7. **Take the act LIST from the derivation, not from the mutations.**
|
|
125
|
+
`recordFacts(store, id, { principal }).actions` is the same `AffordanceSet`
|
|
126
|
+
the scene's strip reads: `affordances` are the acts that can actually act
|
|
127
|
+
here, each with its `args` already decided and its `open` questions left,
|
|
128
|
+
and `withheld` are the ones this seat may not take, with the reason.
|
|
129
|
+
Filtering `store.allMutations()` is not equivalent: it offers acts with
|
|
130
|
+
nothing to act on. A **list** page asks it of a kind instead —
|
|
131
|
+
`kindFacts(store, kind, { principal }).actions`, the acts that can BEGIN it.
|
|
132
|
+
|
|
133
|
+
## Rung two: a product design
|
|
134
|
+
|
|
135
|
+
The registry has three surfaces and two page types, and replacing all of them
|
|
136
|
+
is a product:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
createPageRegistry<S, PageComponent<S>>(schema)
|
|
140
|
+
.surface("shell", Shell) // the frame around every route: nav, masthead, standing
|
|
141
|
+
.surface("home", Home)
|
|
142
|
+
.surface("problems", Problems)
|
|
143
|
+
.register("plot", "list", Plots).register("plot", "record", PlotRecord)
|
|
144
|
+
.register("gardener", "list", Gardeners).register("gardener", "record", GardenerRecord)
|
|
145
|
+
// ... every kind
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The shell surface receives `{ context, children }`. Two worked examples. `apps/todo/src/ui/design.tsx` is the FINISHED one, and
|
|
149
|
+
what makes it finished is not the type — it is that everything a person
|
|
150
|
+
tries there works: sort, filter and group come from `ArrangeBar` and live
|
|
151
|
+
in `useSearchParams` in the shared words (a list you arranged is a link); a record edits where it is
|
|
152
|
+
shown, heading included, through `editableFields(store, id)`; an act's form
|
|
153
|
+
opens where the act is, prefilled, with the store's own refusal said at the
|
|
154
|
+
press; the problems page is an inbox, its repairs ordered by
|
|
155
|
+
`rankedRepairs`; and every empty state offers the way out of itself.
|
|
156
|
+
`apps/seedbed/src/ui/design.tsx` is the other end — an almanac whose own
|
|
157
|
+
drawing has two homes, the page and the scene's lens reading one model.
|
|
158
|
+
|
|
159
|
+
What every design must keep doing:
|
|
160
|
+
|
|
161
|
+
- **Read the graph through one model.** `readGarden(store)` turns nodes,
|
|
162
|
+
edges and violations into the design's words; every page reads it and none
|
|
163
|
+
reaches for `store.graph`. The scene's lens reads the same model.
|
|
164
|
+
- **Style through the theme's tokens** (`--graview-ground`, `-panel`, `-ink`,
|
|
165
|
+
`-edge`, `-warn`, `-accent`, `-font-display`, `-font-body`), tinted with
|
|
166
|
+
`color-mix` for the design's own paper. Never a colour that works in one
|
|
167
|
+
scheme only.
|
|
168
|
+
- **Keep landmarks and targets honest.** One `main` — a `section` when
|
|
169
|
+
`context.embedded`, and UNNAMED, or a page holding two embeds has two
|
|
170
|
+
regions with one name. Controls at least 24px, AA contrast.
|
|
171
|
+
Every size in `rem`: at 200% text a flex or grid item's automatic minimum
|
|
172
|
+
is its CONTENT's, so one un-wrappable row pushes the whole column off the
|
|
173
|
+
screen — `minmax(0, 1fr)` and `min-width: 0` on the column, `flex-wrap` on
|
|
174
|
+
the row. Measure rather than trust: `-ink-faint` fails AA on an 11px
|
|
175
|
+
label, and the accent fails on the warning ground. `node
|
|
176
|
+
scripts/verify-pages.mjs` runs axe over every route, both widths, both
|
|
177
|
+
schemes.
|
|
178
|
+
- **Withhold, do not hide.** Rule 7 again, at every surface: struck through
|
|
179
|
+
with the policy's own sentence rather than dropped.
|
|
180
|
+
|
|
181
|
+
## On somebody else's page
|
|
182
|
+
|
|
183
|
+
The embed mounts the app — this face included (`face: "pages"`, `path`) —
|
|
184
|
+
into one element of any page. That is its own skill: `graview-embed`.
|
|
185
|
+
|
|
186
|
+
## Then find out whether it worked
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
pnpm build && npx graview check ./dist/domain/app.js # the declaration is still whole
|
|
190
|
+
pnpm test # render every page you registered
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Render each registered page with `PagesApp` and `initialPath` in a test —
|
|
194
|
+
`apps/seedbed/tests/integration/chapters.test.ts` does. Then open it: a
|
|
195
|
+
design that passes its tests and reads like an admin panel has replaced
|
|
196
|
+
nothing.
|
|
197
|
+
|
|
198
|
+
## What the check cannot see
|
|
199
|
+
|
|
200
|
+
- Whether the design's words are the domain's. The derived pages use the
|
|
201
|
+
declaration's `description` and `inverse`; a design that writes its own
|
|
202
|
+
sentences must keep them true as the declaration changes.
|
|
203
|
+
- Whether a page still offers everything the seat may do. Listing acts by
|
|
204
|
+
NAME misses the one declared after it was written; scanning the mutations
|
|
205
|
+
offers acts that cannot act. `facts.actions` is neither — but only a
|
|
206
|
+
person can see whether the page gives them room.
|