Add a README glossary and intended scale; correct the TOKENS comment #184

Merged
lilleman merged 7 commits from item-26 into main 2026-10-31 08:17:39 +01:00
7 changed files with 69 additions and 21 deletions
+1 -1
View File
@@ -370,7 +370,7 @@ docker compose -f compose.yml up --build -d # production
stack up and a *minimal* plugin live. Nothing comes before Quick start. Keep its commands stack up and a *minimal* plugin live. Nothing comes before Quick start. Keep its commands
copy-pasteable; deeper detail lives in its own section, linked. copy-pasteable; deeper detail lives in its own section, linked.
2. **Returning developer (rest).** A **Contents** ToC right after Quick start, then sections ordered 2. **Returning developer (rest).** A **Contents** ToC right after Quick start, then sections ordered
by **what an adopter reaches for first**, not by architectural layering: Goals → Audience → Overview → Users, groups by **what an adopter reaches for first**, not by architectural layering: Goals → Audience → Overview → Glossary → Users, groups
& permissions → Building plugins → menu/blocks/interactivity → Configuration → Auth → Email → & permissions → Building plugins → menu/blocks/interactivity → Configuration → Auth → Email →
Architecture → Testing → Production → Observability → JWT-rotation runbook → Project-layout file Architecture → Testing → Production → Observability → JWT-rotation runbook → Project-layout file
map → Extending. Place a new section by how early an adopter needs it. **Users, groups & map → Extending. Place a new section by how early an adopter needs it. **Users, groups &
+6
View File
@@ -6,6 +6,12 @@ at boot. Entries start at 0.3.0.
## Unreleased ## Unreleased
### The README defines its terms and states its intended scale
A new Glossary defines host, plugin, manifest, chrome, nav fragment, permission name, system plugin
and denylist. Plugin dependencies also states the scale Plainpages is sized for: a handful of plugins
per deployment.
### The mobile nav stays reachable on engines without `:has()` ### The mobile nav stays reachable on engines without `:has()`
At 860px and narrower, an engine without `:has()` (Chromium below 105, such as webOS TV 22 and 23, At 860px and narrower, an engine without `:has()` (Chromium below 105, such as webOS TV 22 and 23,
+22
View File
@@ -73,6 +73,7 @@ From here, render real pages against the app shell and fetch upstream data — s
- [Goals](#goals) - [Goals](#goals)
- [Audience](#audience) - [Audience](#audience)
- [Overview](#overview) - [Overview](#overview)
- [Glossary](#glossary)
- [Users, groups & permissions](#users-groups--permissions) - [Users, groups & permissions](#users-groups--permissions)
- [naming a permission](#naming-a-permission) - [naming a permission](#naming-a-permission)
- [a worked example](#a-worked-example) - [a worked example](#a-worked-example)
@@ -210,6 +211,24 @@ feature removes the need for JS (theme switching, popovers, disclosure) we use i
avoid is shipping a client-side runtime, not using the platform. Markup is semantic and accessible avoid is shipping a client-side runtime, not using the platform. Markup is semantic and accessible
(see [AGENTS.md](AGENTS.md)). (see [AGENTS.md](AGENTS.md)).
## Glossary
- **Host** — the `web` container: the Node process that discovers plugins, serves every page and
owns no state of its own. Everything outside it (a plugin's upstream, Ory) is reached over the network.
- **Plugin** — a folder under `plugins/` that adds domain features.
- **Manifest** — the object a plugin's `plugin.ts` exports as its default: its routes, nav,
permissions and hooks. The host checks it at boot and refuses a bad one ([The manifest](#the-manifest)).
- **Chrome** — everything on a page that isn't the page's own content: brand, menu, user, theme and
CSRF token. A handler gets it as `ctx.chrome` ([RequestContext](#requestcontext)).
- **Nav fragment** — the `nav` entries one plugin contributes. The host merges every plugin's
fragment into the one global menu and filters it per user ([The menu system](#the-menu-system)).
- **Permission name** — a `<resource>:<action>` string such as `scheduling:write`, granted in Keto and
carried in the session JWT's `permissions` claim ([Naming a permission](#naming-a-permission)).
- **System plugin** — a plugin that administers Plainpages' own identity stack, through the
privileged `ctx.system` ([System capabilities](#system-capabilities-the-ctxsystem-surface)).
- **Denylist** — the optional in-memory list, per instance, of subjects revoked just now, so a
deactivation or a user's own permission change lands before the JWT expires ([Instant revoke](#instant-revoke-the-optional-denylist)).
## Users, groups & permissions ## Users, groups & permissions
Authorization here is two hops: a **user** — directly, or through a **group** — is granted a Authorization here is two hops: a **user** — directly, or through a **group** — is granted a
@@ -793,6 +812,9 @@ included, since the plugin folder *is* the repo. A baked image needs no extra st
above every plugin, and a plugin resolves it from there — nothing to declare, just import it. A above every plugin, and a plugin resolves it from there — nothing to declare, just import it. A
copy inside your plugin's own `node_modules` would shadow it with a *second* instance of the host's copy inside your plugin's own `node_modules` would shadow it with a *second* instance of the host's
contract, turning a sign-in redirect into a 500, so discovery refuses one there at boot. contract, turning a sign-in redirect into a 500, so discovery refuses one there at boot.
- **Plainpages is sized for a handful of plugins per deployment, about ten.** Each plugin carries
its own dependency tree, so every one adds to disk use and to the memory of the `web` process;
hundreds of plugins is not a goal.
- **The host never upgrades or dedupes your dependencies.** Two plugins depending on the same package - **The host never upgrades or dedupes your dependencies.** Two plugins depending on the same package
each get their own copy at their own version, so neither can break the other by upgrading — and each get their own copy at their own version, so neither can break the other by upgrading — and
keeping yours current, and audited, is yours to own. Renovate here watches every manifest in this keeping yours current, and audited, is yours to own. Renovate here watches every manifest in this
+21
View File
@@ -62,3 +62,24 @@ Inherited architect, decided by:
- Self-sufficiency, Overall: `public/css/styles.css:5` `Architecture` header: the false `light-dark()` bullet is gone, so the header no longer contradicts the TOKENS comment - Self-sufficiency, Overall: `public/css/styles.css:5` `Architecture` header: the false `light-dark()` bullet is gone, so the header no longer contradicts the TOKENS comment
- Locality, Self-sufficiency, Overall: `public/css/styles.css:687` `@supports selector(:has(a))` in `@media (max-width: 860px)`: the guard and its comment put the dependency of the open state on `:has()` where an editor would change it - Locality, Self-sufficiency, Overall: `public/css/styles.css:687` `@supports selector(:has(a))` in `@media (max-width: 860px)`: the guard and its comment put the dependency of the open state on `:has()` where an editor would change it
## 2026-10-31T01:19:49Z, PR #184 at af157c9, against bb8e6bc
Ruling: better
| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall |
|---|---|---|---|---|---|
| Mid A | better | same | same | better | better |
| Inherited architect | better | better | same | better | better |
Mid A, decided by:
- Self-sufficiency, Navigation, Overall: `public/css/styles.css:12` `TOKENS` header comment: says all three theme rules need `:has()`, names the engines without it and points to the `@supports not selector(:has(a))` block, where the old text called the swaps "rock-solid"
- Navigation, Self-sufficiency, Overall: `README.md:214` `## Glossary`: defines host, plugin, manifest, chrome, nav fragment, permission name, system plugin and denylist and links each to its home section
- Self-sufficiency: `README.md:815` `Plugin dependencies`: the scale bullet states the plugin count the per-plugin dependency design assumes
Inherited architect, decided by:
- Self-sufficiency, Locality, Overall: `public/css/styles.css:12` `1. TOKENS` comment: an editor of the theme mapping learns of the distant `:has()` fallback from the comment, where the old text said the swap could not fail
- Navigation, Self-sufficiency, Overall: `README.md:214` `## Glossary`: a cold reader reaches "chrome" or "denylist" and their home sections in one hop
- Self-sufficiency: `README.md:815` `### Plugin dependencies`: the scale bullet gives the plugin count the dependency design assumes
+12
View File
@@ -47,3 +47,15 @@ Read most (opens and searches):
- `examples/plugins/scheduling/views/overview.ejs` 1 - `examples/plugins/scheduling/views/overview.ejs` 1
- `examples/plugins/scheduling/views/shifts.ejs` 1 - `examples/plugins/scheduling/views/shifts.ejs` 1
- `src/http/app.test.ts` 1 - `src/http/app.test.ts` 1
## Reading cost, chunk 91d11dbe63f9, finished 2026-10-31T01:13:44Z
69010 new input tokens before the first commit.
Read most (opens and searches):
- `todo.md` 7
- `README.md` 4
- `public/css/styles.css` 4
- `CHANGELOG.md` 1
- `src/ui/stylesheet-floor.test.ts` 1
+7 -4
View File
@@ -9,14 +9,17 @@
/* ---------- 1. TOKENS ---------------------------------------- /* ---------- 1. TOKENS ----------------------------------------
Two raw palettes (--l-* light, --d-* dark) are defined once. Two raw palettes (--l-* light, --d-* dark) are defined once.
A small mapping (--bg: var(--l-bg) ...) picks the active set. A small mapping (--bg: var(--l-bg) ...) picks the active set.
Theme resolution (no JS): Theme resolution (no JS); all three rules below need :has():
• default + @media(dark) → follows the OS (Auto) • default + @media(dark) → follows the OS (Auto)
• html:has(#theme-light) → force light • html:has(#theme-light) → force light
• html:has(#theme-dark) → force dark • html:has(#theme-dark) → force dark
Engines without :has() (Chromium below 105, Firefox below 121,
Safari below 15.4) ignore all three and stay light; see the @supports not
selector(:has(a)) block further down.
We avoid light-dark()/color-scheme for app colors because their We avoid light-dark()/color-scheme for app colors because their
resolution is unreliable across engines; :has() variable swaps resolution is unreliable across engines. color-scheme is still
are rock-solid. color-scheme is still set as a hint for native set as a hint for native widgets (date picker, scrollbars,
widgets (date picker, scrollbars, selects). selects).
------------------------------------------------------------- */ ------------------------------------------------------------- */
:root { :root {
/* Hex, never oklch: below Chromium 111 `var(--accent)` is invalid at computed-value time and the /* Hex, never oklch: below Chromium 111 `var(--accent)` is invalid at computed-value time and the
-16
View File
@@ -12,13 +12,11 @@
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|---|---|---|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|---|---|
| 26 | 0.4.21 | | | **Add a domain glossary and the project's intended horizon to the README.** | 1 | 2 | 1 | 2 | 3 | 9 by 3, code health by 2 | 11.9 |
| 8 | 0.4.21 | | | **Run the visual E2E specs at viewports from a large desktop down to a small phone.** | 2 | 1 | 1 | 2 | 4 | code health by 3 | 9.0 | | 8 | 0.4.21 | | | **Run the visual E2E specs at viewports from a large desktop down to a small phone.** | 2 | 1 | 1 | 2 | 4 | code health by 3 | 9.0 |
| 9 | 0.4.21 | | | **Typecheck `e2e-tests/` in the gate.** | 1 | 1 | 1 | 1 | 3 | code health by 2 | 9.0 | | 9 | 0.4.21 | | | **Typecheck `e2e-tests/` in the gate.** | 1 | 1 | 1 | 1 | 3 | code health by 2 | 9.0 |
| 24 | 0.4.21 | | | **Retire `src/ui/shell-context.ts` and render the dashboard from `ctx.chrome`.** | 2 | 1 | 2 | 3 | 4 | code health by 3 | 9.0 | | 24 | 0.4.21 | | | **Retire `src/ui/shell-context.ts` and render the dashboard from `ctx.chrome`.** | 2 | 1 | 2 | 3 | 4 | code health by 3 | 9.0 |
| 17 | 0.4.21 | | | **Remove the icons no caller uses from `ICON_NAMES` and the sprite.** | 1 | 1 | 1 | 1 | 1 | 8 by 1, code health by 1 | 8.8 | | 17 | 0.4.21 | | | **Remove the icons no caller uses from `ICON_NAMES` and the sprite.** | 1 | 1 | 1 | 1 | 1 | 8 by 1, code health by 1 | 8.8 |
| 21 | 0.4.21 | | | **Fail the gate on a function that exceeds a size or complexity limit.** | 3 | 1 | 1 | 3 | 5 | code health by 4 | 7.2 | | 21 | 0.4.21 | | | **Fail the gate on a function that exceeds a size or complexity limit.** | 3 | 1 | 1 | 3 | 5 | code health by 4 | 7.2 |
| 38 | 0.4.21 | | | **Correct the TOKENS comment at the top of `public/css/styles.css` so it no longer claims `:has()` swaps are "rock-solid" or that the theme follows the OS on every engine.** | 1 | 2 | 1 | 2 | 1 | code health by 1 | 7.2 |
| 32 | 0.4.21 | | | **Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded.** | 3 | 1 | 1 | 3 | 4 | code health by 2 | 6.0 | | 32 | 0.4.21 | | | **Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded.** | 3 | 1 | 1 | 3 | 4 | code health by 2 | 6.0 |
| 20 | 0.4.21 | | | **Redirect an anonymous hit on a session-needing flow page to sign-in, instead of a 500.** | 2 | 2 | 2 | 4 | 3 | 2 by 4 | 4.2 | | 20 | 0.4.21 | | | **Redirect an anonymous hit on a session-needing flow page to sign-in, instead of a 500.** | 2 | 2 | 2 | 4 | 3 | 2 by 4 | 4.2 |
| 4 | 0.4.21 | | | **Refuse a permission save whose baseline no longer matches Keto, so concurrent edits are not silently lost.** | 2 | 2 | 2 | 4 | 4 | 2 by 4 | 2.8 | | 4 | 0.4.21 | | | **Refuse a permission save whose baseline no longer matches Keto, so concurrent edits are not silently lost.** | 2 | 2 | 2 | 4 | 4 | 2 by 4 | 2.8 |
@@ -39,12 +37,6 @@
## Details ## Details
### 26. Add a domain glossary and the project's intended horizon to the README.
Glossary: host, manifest, chrome, nav fragment, permission token, system plugin, denylist. Also the
expected plugin count, since per-plugin dependency isolation prices N dependency trees on disk and
in RSS.
### 24. Retire `src/ui/shell-context.ts` and render the dashboard from `ctx.chrome`. ### 24. Retire `src/ui/shell-context.ts` and render the dashboard from `ctx.chrome`.
`ShellModel`/`buildShellContext` has one consumer, the dashboard, and duplicates `PageChrome` on `ShellModel`/`buildShellContext` has one consumer, the dashboard, and duplicates `PageChrome` on
@@ -59,14 +51,6 @@ AGENTS.md → "`ICON_NAMES` is a host-owned registry" already makes this routine
Only `tsc --strict` runs today. A new tool is a new dependency, so justify it. Only `tsc --strict` runs today. A new tool is a new dependency, so justify it.
### 38. Correct the TOKENS comment at the top of `public/css/styles.css` so it no longer claims `:has()` swaps are "rock-solid" or that the theme follows the OS on every engine.
Section 1's TOKENS comment says the theme follows the OS by default and that `:has()` variable swaps
are "rock-solid". On an engine without `:has()` (Chromium below 105, Firefox below 121) every theme
rule is inert, the OS-dark rule included, so those engines stay light, as the
`@supports not selector(:has(a))` block further down says. A reader trusting the comment expects dark
mode there.
### 32. Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded. ### 32. Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded.
Nothing checks README → "Two browser floors", and no E2E engine is old enough to show a break. Nothing checks README → "Two browser floors", and no E2E engine is old enough to show a break.