@panaversity/ksor 0.0.19 → 0.0.21
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/CHANGELOG.md +501 -0
- package/dist/cli.mjs +99 -19
- package/docs/authorization.md +197 -0
- package/docs/index.md +4 -0
- package/package.json +1 -1
- package/templates/scaffold/.agents/skills/format-checker/check.mjs +232 -9
- package/templates/scaffold/.claude/skills/format-checker/check.mjs +232 -9
- package/templates/scaffold/AGENTS.md +52 -4
- package/templates/scaffold/instance.md +28 -20
- package/templates/scaffold/knowledge/governance-ladder.md +36 -0
- package/templates/scaffold/knowledge/surfaces/for-agents.md +29 -0
- package/templates/scaffold/knowledge/surfaces/for-people.md +35 -0
- package/templates/scaffold/knowledge/surfaces/index.md +21 -0
- package/templates/scaffold/knowledge/what-is-a-ksor.md +39 -0
- package/templates/scaffold/pnpm-lock.yaml +1198 -228
- package/templates/scaffold/system/site/app/(home)/layout.tsx +6 -0
- package/templates/scaffold/system/site/app/(home)/page.tsx +65 -70
- package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +122 -14
- package/templates/scaffold/system/site/app/docs/layout.tsx +2 -21
- package/templates/scaffold/system/site/app/global.css +552 -9
- package/templates/scaffold/system/site/app/layout.tsx +23 -4
- package/templates/scaffold/system/site/app/llms-full.txt/route.ts +4 -2
- package/templates/scaffold/system/site/app/llms.txt/route.ts +11 -9
- package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +51 -0
- package/templates/scaffold/system/site/components/copy-markdown.tsx +70 -0
- package/templates/scaffold/system/site/components/governance.tsx +262 -0
- package/templates/scaffold/system/site/components/home-cover.tsx +137 -0
- package/templates/scaffold/system/site/components/record-index.tsx +120 -0
- package/templates/scaffold/system/site/components/record-shell.tsx +68 -0
- package/templates/scaffold/system/site/components/record-stack.tsx +131 -0
- package/templates/scaffold/system/site/components/record-toc.tsx +160 -0
- package/templates/scaffold/system/site/components/search-dialog.tsx +130 -0
- package/templates/scaffold/system/site/components/sidebar-status.tsx +35 -0
- package/templates/scaffold/system/site/components/ui/badge.tsx +46 -0
- package/templates/scaffold/system/site/components/ui/button.tsx +62 -0
- package/templates/scaffold/system/site/components/ui/separator.tsx +28 -0
- package/templates/scaffold/system/site/components.json +25 -0
- package/templates/scaffold/system/site/lib/governance.ts +432 -0
- package/templates/scaffold/system/site/lib/layout.shared.tsx +1 -1
- package/templates/scaffold/system/site/lib/shared.ts +38 -0
- package/templates/scaffold/system/site/lib/source.ts +221 -5
- package/templates/scaffold/system/site/lib/utils.ts +6 -0
- package/templates/scaffold/system/site/package.json +9 -3
- package/templates/scaffold/knowledge/example.md +0 -23
|
@@ -1,19 +1,247 @@
|
|
|
1
|
+
/* shadcn/ui is the component system (`components.json`, `pnpm dlx shadcn@latest
|
|
2
|
+
add <name>`), and Fumadocs reads its colours: the `shadcn` preset maps every
|
|
3
|
+
`--color-fd-*` token onto the shadcn variable of the same role, so one
|
|
4
|
+
palette dresses the shell and anything added from the registry. `neutral`
|
|
5
|
+
stood here before and painted both — the page and the sidebar it sits
|
|
6
|
+
against — hsl(0 0% 96%) and 94.7%, 1.6% apart, which is no separation at
|
|
7
|
+
all; shadcn's neutral gives the page white and the rail its own surface.
|
|
8
|
+
|
|
9
|
+
`shadcn init` also writes `@import "shadcn/tailwind.css"`, which is dropped
|
|
10
|
+
here: that import makes the shadcn CLI a runtime dependency of every
|
|
11
|
+
scaffolded project, and the CLI carries Babel, ts-morph, dotenvx and an MCP
|
|
12
|
+
SDK — 578 extra packages in the lockfile, measured, against 2 for the four
|
|
13
|
+
deps this file actually needs. `components.json` therefore names the classic
|
|
14
|
+
`new-york` style, whose components compile against plain Tailwind utilities
|
|
15
|
+
and the tokens below (verified by adding one). An adopter who wants the
|
|
16
|
+
preset styles adds the import and the dependency in their own repo. */
|
|
1
17
|
@import "tailwindcss";
|
|
2
|
-
@import "fumadocs-ui/css/
|
|
18
|
+
@import "fumadocs-ui/css/shadcn.css";
|
|
3
19
|
@import "fumadocs-ui/css/preset.css";
|
|
20
|
+
@import "tw-animate-css";
|
|
21
|
+
|
|
22
|
+
/* next-themes switches the theme by class, so `dark:` must follow the class
|
|
23
|
+
rather than the OS setting. Nothing here uses a `dark:` utility — the theme
|
|
24
|
+
is tokens — but a component added from the registry will. */
|
|
25
|
+
@custom-variant dark (&:is(.dark *));
|
|
26
|
+
|
|
27
|
+
/* ── Ink, paper, rule ────────────────────────────────────────────────────────
|
|
28
|
+
shadcn's neutral palette, retuned on two axes and otherwise left alone.
|
|
4
29
|
|
|
5
|
-
|
|
6
|
-
|
|
30
|
+
The ink carries a trace of blue (chroma .012 at hue 255) instead of being a
|
|
31
|
+
pure grey: it sits with the accent rather than beside it, and a record set in
|
|
32
|
+
cool ink reads as printed rather than as chrome. The rules are darker than
|
|
33
|
+
shadcn's default (0.9 against 0.922) because this design puts weight on
|
|
34
|
+
hairlines, and a rule you have to look for is not doing its job.
|
|
35
|
+
|
|
36
|
+
`--primary` and its foreground are the whole brand — re-brand by changing
|
|
37
|
+
that pair and every accented element follows, in the shell and in every
|
|
38
|
+
shadcn component alike, because Fumadocs reads `--primary` too. It is spent
|
|
39
|
+
ONLY on actions, links and the active state: a page where the accent is
|
|
40
|
+
everywhere has no accent. `--ksor-caution` (further down) is the only other
|
|
41
|
+
colour with a job, and its job is the opposite one. */
|
|
7
42
|
:root {
|
|
8
|
-
--
|
|
9
|
-
--
|
|
10
|
-
--
|
|
43
|
+
--background: oklch(1 0 0);
|
|
44
|
+
--foreground: oklch(0.17 0.012 255);
|
|
45
|
+
--card: oklch(0.985 0.002 255);
|
|
46
|
+
--card-foreground: oklch(0.17 0.012 255);
|
|
47
|
+
--popover: oklch(1 0 0);
|
|
48
|
+
--popover-foreground: oklch(0.17 0.012 255);
|
|
49
|
+
--primary: #1d4ed8;
|
|
50
|
+
--primary-foreground: #ffffff;
|
|
51
|
+
--secondary: oklch(0.97 0 0);
|
|
52
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
53
|
+
--muted: oklch(0.972 0.002 255);
|
|
54
|
+
--muted-foreground: oklch(0.53 0.012 255);
|
|
55
|
+
--accent: oklch(0.965 0.004 255);
|
|
56
|
+
--accent-foreground: oklch(0.17 0.012 255);
|
|
57
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
58
|
+
--border: oklch(0.9 0.004 255);
|
|
59
|
+
--input: oklch(0.9 0.004 255);
|
|
60
|
+
--ring: #1d4ed8;
|
|
61
|
+
--chart-1: oklch(0.87 0 0);
|
|
62
|
+
--chart-2: oklch(0.556 0 0);
|
|
63
|
+
--chart-3: oklch(0.439 0 0);
|
|
64
|
+
--chart-4: oklch(0.371 0 0);
|
|
65
|
+
--chart-5: oklch(0.269 0 0);
|
|
66
|
+
--radius: 0.5rem;
|
|
67
|
+
--sidebar: oklch(0.985 0.002 255);
|
|
68
|
+
--sidebar-foreground: oklch(0.17 0.012 255);
|
|
69
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
70
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
71
|
+
--sidebar-accent: oklch(0.965 0.004 255);
|
|
72
|
+
--sidebar-accent-foreground: oklch(0.17 0.012 255);
|
|
73
|
+
--sidebar-border: oklch(0.9 0.004 255);
|
|
74
|
+
--sidebar-ring: #1d4ed8;
|
|
75
|
+
|
|
76
|
+
/* The cover: the surface the front door's band is printed on. It follows the
|
|
77
|
+
theme like everything else — in the light it is pale stock, a shade of the
|
|
78
|
+
brand rather than a shade of grey (hue 258, between the text's 255 and the
|
|
79
|
+
accent's 264). It was ink in both themes for one afternoon, which made the
|
|
80
|
+
light theme a dark page with a white foot. */
|
|
81
|
+
--ksor-cover: oklch(0.965 0.013 258);
|
|
82
|
+
--ksor-cover-foreground: oklch(0.2 0.03 258);
|
|
83
|
+
--ksor-cover-muted: oklch(0.5 0.022 258);
|
|
84
|
+
--ksor-cover-rule: oklch(0.885 0.016 258);
|
|
85
|
+
--ksor-cover-hover: oklch(0.935 0.018 258);
|
|
86
|
+
--ksor-cover-panel: oklch(1 0 0);
|
|
87
|
+
--ksor-cover-panel-rule: oklch(0.9 0.014 258);
|
|
11
88
|
}
|
|
12
89
|
|
|
13
90
|
.dark {
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
91
|
+
--background: oklch(0.155 0.012 255);
|
|
92
|
+
--foreground: oklch(0.965 0.004 255);
|
|
93
|
+
--card: oklch(0.195 0.012 255);
|
|
94
|
+
--card-foreground: oklch(0.965 0.004 255);
|
|
95
|
+
--popover: oklch(0.195 0.012 255);
|
|
96
|
+
--popover-foreground: oklch(0.965 0.004 255);
|
|
97
|
+
--primary: #7fb0f9;
|
|
98
|
+
--primary-foreground: #081226;
|
|
99
|
+
--secondary: oklch(0.269 0 0);
|
|
100
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
101
|
+
--muted: oklch(0.24 0.012 255);
|
|
102
|
+
--muted-foreground: oklch(0.7 0.014 255);
|
|
103
|
+
--accent: oklch(0.26 0.014 255);
|
|
104
|
+
--accent-foreground: oklch(0.965 0.004 255);
|
|
105
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
106
|
+
--border: oklch(1 0 0 / 12%);
|
|
107
|
+
--input: oklch(1 0 0 / 16%);
|
|
108
|
+
--ring: #7fb0f9;
|
|
109
|
+
--chart-1: oklch(0.87 0 0);
|
|
110
|
+
--chart-2: oklch(0.556 0 0);
|
|
111
|
+
--chart-3: oklch(0.439 0 0);
|
|
112
|
+
--chart-4: oklch(0.371 0 0);
|
|
113
|
+
--chart-5: oklch(0.269 0 0);
|
|
114
|
+
--sidebar: oklch(0.185 0.012 255);
|
|
115
|
+
--sidebar-foreground: oklch(0.965 0.004 255);
|
|
116
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
117
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
118
|
+
--sidebar-accent: oklch(0.26 0.014 255);
|
|
119
|
+
--sidebar-accent-foreground: oklch(0.965 0.004 255);
|
|
120
|
+
--sidebar-border: oklch(1 0 0 / 12%);
|
|
121
|
+
--sidebar-ring: #7fb0f9;
|
|
122
|
+
|
|
123
|
+
/* In the dark the cover rises one step ABOVE the page (0.245 against the
|
|
124
|
+
background's 0.155) rather than inverting: a cover is the surface that
|
|
125
|
+
catches the light. Same object, same lighting, darker room. */
|
|
126
|
+
--ksor-cover: oklch(0.245 0.038 258);
|
|
127
|
+
--ksor-cover-foreground: oklch(0.97 0.006 258);
|
|
128
|
+
--ksor-cover-muted: oklch(0.72 0.022 258);
|
|
129
|
+
--ksor-cover-rule: oklch(0.34 0.03 258);
|
|
130
|
+
--ksor-cover-hover: oklch(0.3 0.035 258);
|
|
131
|
+
--ksor-cover-panel: oklch(0.285 0.036 258);
|
|
132
|
+
--ksor-cover-panel-rule: oklch(0.36 0.03 258);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* The token → utility mapping shadcn components compile against, as
|
|
136
|
+
`shadcn init` writes it MINUS its two font lines: those pointed at a
|
|
137
|
+
`--font-sans` that only exists when a next/font face is loaded in the
|
|
138
|
+
layout, and this scaffold deliberately loads none (see app/layout.tsx —
|
|
139
|
+
a Google face is fetched at BUILD time, which costs the offline build and
|
|
140
|
+
byte-identical rebuilds). Tailwind's own system stack stands. */
|
|
141
|
+
/* ── Three voices ────────────────────────────────────────────────────────────
|
|
142
|
+
The record speaks in a serif, the site speaks in a sans, and the machine
|
|
143
|
+
surface speaks in mono. Not decoration: each face marks WHO is talking, so a
|
|
144
|
+
reader can tell the record's own words (its title, its documents' titles)
|
|
145
|
+
from the site's furniture around them, and both from an address a machine
|
|
146
|
+
resolves.
|
|
147
|
+
|
|
148
|
+
System stacks only. A web font would be fetched at build time, which costs
|
|
149
|
+
the offline build and byte-identical rebuilds the scaffold pins — and the
|
|
150
|
+
browser smoke test asserts zero external requests. Georgia and its siblings
|
|
151
|
+
are on every machine this site is read on, and a serif that arrives instantly
|
|
152
|
+
beats a distinctive one that arrives late. */
|
|
153
|
+
@theme {
|
|
154
|
+
--font-display: ui-serif, Georgia, "Times New Roman", Times, serif;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@theme inline {
|
|
158
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
159
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
160
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
161
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
162
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
163
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
164
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
165
|
+
--color-sidebar: var(--sidebar);
|
|
166
|
+
--color-chart-5: var(--chart-5);
|
|
167
|
+
--color-chart-4: var(--chart-4);
|
|
168
|
+
--color-chart-3: var(--chart-3);
|
|
169
|
+
--color-chart-2: var(--chart-2);
|
|
170
|
+
--color-chart-1: var(--chart-1);
|
|
171
|
+
--color-ring: var(--ring);
|
|
172
|
+
--color-input: var(--input);
|
|
173
|
+
--color-border: var(--border);
|
|
174
|
+
--color-destructive: var(--destructive);
|
|
175
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
176
|
+
--color-accent: var(--accent);
|
|
177
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
178
|
+
--color-muted: var(--muted);
|
|
179
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
180
|
+
--color-secondary: var(--secondary);
|
|
181
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
182
|
+
--color-primary: var(--primary);
|
|
183
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
184
|
+
--color-popover: var(--popover);
|
|
185
|
+
--color-card-foreground: var(--card-foreground);
|
|
186
|
+
--color-card: var(--card);
|
|
187
|
+
--color-foreground: var(--foreground);
|
|
188
|
+
--color-background: var(--background);
|
|
189
|
+
--radius-sm: calc(var(--radius) * 0.6);
|
|
190
|
+
--radius-md: calc(var(--radius) * 0.8);
|
|
191
|
+
--radius-lg: var(--radius);
|
|
192
|
+
--radius-xl: calc(var(--radius) * 1.4);
|
|
193
|
+
--radius-2xl: calc(var(--radius) * 1.8);
|
|
194
|
+
--radius-3xl: calc(var(--radius) * 2.2);
|
|
195
|
+
--radius-4xl: calc(var(--radius) * 2.6);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@layer base {
|
|
199
|
+
* {
|
|
200
|
+
@apply border-border outline-ring/50;
|
|
201
|
+
}
|
|
202
|
+
body {
|
|
203
|
+
@apply bg-background text-foreground;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* ── The left rail ───────────────────────────────────────────────────────────
|
|
208
|
+
The docs grid is `offset | sidebar | main | toc | offset`, and the sidebar
|
|
209
|
+
panel spans BOTH the offset and the sidebar column: its surface runs to the
|
|
210
|
+
window edge while its contents are pinned to the 268px column (`items-end`,
|
|
211
|
+
children `w-(--fd-sidebar-width)`). Each offset is half of whatever the
|
|
212
|
+
viewport has over `--fd-layout-width`, which the shell defaults to 97rem —
|
|
213
|
+
so on a wide display the offset is real: measured at a 1728px viewport it
|
|
214
|
+
was 88px, and the first nav item began 103px from the window edge with
|
|
215
|
+
nothing but sidebar-coloured space to its left. That reads as a panel
|
|
216
|
+
somebody forgot to align rather than as the window's rail.
|
|
217
|
+
|
|
218
|
+
`100%` leaves the grid definition untouched and takes both offsets to zero,
|
|
219
|
+
so the rail is flush and only the record's own padding stands before the
|
|
220
|
+
first link. The main column absorbs exactly what the offsets held, so the
|
|
221
|
+
prose does not move (measured at 1728px: the article starts at x=548 either
|
|
222
|
+
way). Below 97rem nothing changes — the offsets were already zero. Percent
|
|
223
|
+
rather than `100vw` on purpose: `vw` includes the scrollbar the stable
|
|
224
|
+
gutter below reserves, which would overflow the row by its width. The home
|
|
225
|
+
layout is its own shell and does not use this grid at all. */
|
|
226
|
+
:root {
|
|
227
|
+
--fd-layout-width: 100%;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* ── The reading measure ─────────────────────────────────────────────────────
|
|
231
|
+
The shell caps the article at 900px, which at the body's 16px puts 78
|
|
232
|
+
characters on a line (measured) — half again the 45–75 that reading research
|
|
233
|
+
and every typographic manual settle on, and the reason the page read as a
|
|
234
|
+
slab. 46rem holds the text to 672px, about 66 characters, and the article
|
|
235
|
+
still centres itself in the main column.
|
|
236
|
+
|
|
237
|
+
It is also half of what stops the column moving: the other half is holding
|
|
238
|
+
the table-of-contents column on every page (app/docs/[[...slug]]/page.tsx).
|
|
239
|
+
Sidebar and rail are both 268px, so main is centred in the viewport and this
|
|
240
|
+
measure is centred in main — the same place on a document with headings and
|
|
241
|
+
one without. `data-full` is the shell's own escape hatch for a page that
|
|
242
|
+
asked for the whole width; it keeps it. */
|
|
243
|
+
#nd-page:not([data-full="true"]) {
|
|
244
|
+
max-width: 46rem;
|
|
17
245
|
}
|
|
18
246
|
|
|
19
247
|
html {
|
|
@@ -24,3 +252,318 @@ html > body[data-scroll-locked] {
|
|
|
24
252
|
margin-right: 0px !important;
|
|
25
253
|
--removed-body-scroll-bar-size: 0px !important;
|
|
26
254
|
}
|
|
255
|
+
|
|
256
|
+
/* The supersession notice's own colour. It used to wear --color-fd-primary,
|
|
257
|
+
which is also the link colour, the primary button and the focus ring — so
|
|
258
|
+
"stop trusting this document" was painted in exactly the blue that means
|
|
259
|
+
"go here". A caution needs a role of its own; this is the only place it is
|
|
260
|
+
used, and the word "Superseded" still carries the meaning without it
|
|
261
|
+
(GOV.UK's rule: never rely on colour alone). */
|
|
262
|
+
/* The caution colour is a TOKEN, declared as a light/dark pair on the root
|
|
263
|
+
rather than inside the one class that first used it. Every surface that has
|
|
264
|
+
to say "the record withdrew this" now spends the same colour — the banner
|
|
265
|
+
over a superseded document, and the chip that marks it in the sidebar, the
|
|
266
|
+
listings, the front door and search. Declared in both themes deliberately:
|
|
267
|
+
a colour written only on `:root` leaks into dark, because `:root` and
|
|
268
|
+
`.dark` have equal specificity and whichever comes later wins. */
|
|
269
|
+
:root {
|
|
270
|
+
--ksor-caution: #9c3f24;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.dark {
|
|
274
|
+
--ksor-caution: #e0906e;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.ksor-caution {
|
|
278
|
+
border-color: color-mix(in oklab, var(--ksor-caution) 32%, transparent);
|
|
279
|
+
border-left-color: var(--ksor-caution);
|
|
280
|
+
background-color: color-mix(in oklab, var(--ksor-caution) 8%, transparent);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.dark .ksor-caution {
|
|
284
|
+
background-color: color-mix(in oklab, var(--ksor-caution) 12%, transparent);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* A withdrawn document's status chip, wherever one renders. A `draft` chip and
|
|
288
|
+
a `superseded` chip were pixel-identical — the same hairline border and the
|
|
289
|
+
same muted text — so the two statuses that mean the most different things
|
|
290
|
+
looked the same at the moment a reader picks between two documents. The
|
|
291
|
+
colour is additive, never the whole signal: the word "superseded" is beside
|
|
292
|
+
it in every one of these places (GOV.UK's rule — never rely on colour alone).
|
|
293
|
+
Unlayered, so it beats the Tailwind utility that sets the chip's border and
|
|
294
|
+
text colour; the banner class above has always worked this way. */
|
|
295
|
+
.ksor-withdrawn {
|
|
296
|
+
border-color: color-mix(in oklab, var(--ksor-caution) 45%, transparent);
|
|
297
|
+
color: var(--ksor-caution);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* A search result whose document carries a caveat status. Appended in CSS so
|
|
301
|
+
the row keeps the shell's own markdown rendering of the search highlights —
|
|
302
|
+
passing children to the item replaces that, and the <mark> tags leak into
|
|
303
|
+
the dialog as literal text. */
|
|
304
|
+
[data-ksor-status]::after {
|
|
305
|
+
content: attr(data-ksor-status);
|
|
306
|
+
margin-inline-start: auto;
|
|
307
|
+
align-self: center;
|
|
308
|
+
flex-shrink: 0;
|
|
309
|
+
border: 1px solid var(--color-fd-border);
|
|
310
|
+
border-radius: 0.25rem;
|
|
311
|
+
padding: 0.05rem 0.35rem;
|
|
312
|
+
font-size: 0.65rem;
|
|
313
|
+
font-weight: 500;
|
|
314
|
+
color: var(--color-fd-muted-foreground);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* …and in search too, where a reader is choosing between results. The chip is
|
|
318
|
+
drawn by `content`, so the tone has to be set on the same pseudo-element. */
|
|
319
|
+
[data-ksor-status="superseded"]::after {
|
|
320
|
+
border-color: color-mix(in oklab, var(--ksor-caution) 45%, transparent);
|
|
321
|
+
color: var(--ksor-caution);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* ── The document's type ramp ────────────────────────────────────────────────
|
|
325
|
+
The shell sizes the title at 1.75em — 28px against 16px body, with normal
|
|
326
|
+
tracking. Measured against the description at 18px, that is four shades of
|
|
327
|
+
grey text rather than a document with structure: the title barely leads and
|
|
328
|
+
the description competes with it. The title takes the lead it should have,
|
|
329
|
+
the description steps back under it, and the body is left alone at 16/28,
|
|
330
|
+
which is right for long-form reading. Scoped to the shell's own page id, so
|
|
331
|
+
nothing depends on which order two arbitrary Tailwind sizes land in. */
|
|
332
|
+
#nd-page > h1 {
|
|
333
|
+
font-family: var(--font-display);
|
|
334
|
+
font-size: 2.375rem;
|
|
335
|
+
font-weight: 600;
|
|
336
|
+
line-height: 1.1;
|
|
337
|
+
/* Serifs need less negative tracking than the sans did at this size — the
|
|
338
|
+
-0.021em that suited the old face closes the counters on this one. */
|
|
339
|
+
letter-spacing: -0.012em;
|
|
340
|
+
text-wrap: balance;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#nd-page > p:first-of-type {
|
|
344
|
+
font-size: 1.0625rem;
|
|
345
|
+
line-height: 1.6;
|
|
346
|
+
text-wrap: pretty;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/* The section headings inside a document. The shell styles the title and
|
|
350
|
+
leaves the body's headings to its prose defaults — 30 / 24 / 20 / 16px for
|
|
351
|
+
h1-h4 in the SANS body face, and nothing at all for h5-h6, which the prose
|
|
352
|
+
plugin never defines and preflight resets to inherit (read out of the built
|
|
353
|
+
stylesheet, 2026-08-22). Two things were wrong with that.
|
|
354
|
+
|
|
355
|
+
Voice: a section heading is the RECORD speaking, exactly like the title
|
|
356
|
+
above it, so it belongs in the display serif. Left in sans, a single
|
|
357
|
+
document spoke in two voices and the change of voice meant nothing —
|
|
358
|
+
which is the one thing the three faces on this site exist to carry.
|
|
359
|
+
|
|
360
|
+
Scale: h4 came out at 16px/600 — the body size — and h5/h6 at 16px/400 with
|
|
361
|
+
NO margins, which is a paragraph. The ramp is 38 / 32 / 28 / 22 / 18 / 16 —
|
|
362
|
+
the page title, then h1 down to h6 — against a 16px body. The size step runs
|
|
363
|
+
out at h5, so below it the serif, the weight and the air above it are what
|
|
364
|
+
make a heading.
|
|
365
|
+
|
|
366
|
+
A body `# heading` should not exist — the frontmatter title IS the page's
|
|
367
|
+
h1 (scaffolded AGENTS.md) — but nothing enforces it and this repo's own
|
|
368
|
+
fixture corpus opens one (workbench/example-corpus/knowledge/about.md:9).
|
|
369
|
+
`#nd-page > h1` is a CHILD combinator, so it reached the page title only,
|
|
370
|
+
and a body h1 fell through both rules to 30px sans weight 800: the loudest
|
|
371
|
+
thing on the page, in the site's voice, for the record's own words.
|
|
372
|
+
|
|
373
|
+
Scoped INSIDE `.prose`, which is the container Fumadocs wraps the document's
|
|
374
|
+
own markdown in — the record's words, and nothing else. Scoped to the page
|
|
375
|
+
instead, this rule reached the site's own headings too and blew up
|
|
376
|
+
`Provenance`'s deliberately small mono "Sources" label into a 28px serif
|
|
377
|
+
heading (caught by measuring the rendered page, 2026-08-22): an id beats the
|
|
378
|
+
Tailwind utilities a component sets on itself, so the component lost an
|
|
379
|
+
argument it did not know it was having.
|
|
380
|
+
|
|
381
|
+
Specificity, not luck: `#nd-page .prose` (1,1,0) beats the prose rule's
|
|
382
|
+
single class (0,1,0), because that selector wraps its element in `:where()`,
|
|
383
|
+
which contributes nothing. */
|
|
384
|
+
#nd-page .prose :is(h1, h2, h3, h4, h5, h6) {
|
|
385
|
+
font-family: var(--font-display);
|
|
386
|
+
font-weight: 600;
|
|
387
|
+
letter-spacing: -0.008em;
|
|
388
|
+
text-wrap: balance;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* Space above a heading, not below it: a heading belongs to the text it
|
|
392
|
+
introduces. The prose defaults set both in `em`, so raising the sizes above
|
|
393
|
+
would have scaled the gaps with them and left h2 floating on 56px of air. */
|
|
394
|
+
#nd-page .prose h1 {
|
|
395
|
+
font-size: 2rem;
|
|
396
|
+
line-height: 1.2;
|
|
397
|
+
margin-top: 3rem;
|
|
398
|
+
margin-bottom: 1rem;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
#nd-page .prose h2 {
|
|
402
|
+
font-size: 1.75rem;
|
|
403
|
+
line-height: 1.25;
|
|
404
|
+
margin-top: 2.75rem;
|
|
405
|
+
margin-bottom: 0.875rem;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
#nd-page .prose h3 {
|
|
409
|
+
font-size: 1.375rem;
|
|
410
|
+
line-height: 1.35;
|
|
411
|
+
margin-top: 2rem;
|
|
412
|
+
margin-bottom: 0.625rem;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/* The floor of the ramp. The size step is spent here, so weight and air do
|
|
416
|
+
the work: at 16px/400 with no margins at all, a fifth-level heading was
|
|
417
|
+
byte-identical to the paragraph under it and glued to the one above. */
|
|
418
|
+
#nd-page .prose :is(h5, h6) {
|
|
419
|
+
font-size: 1rem;
|
|
420
|
+
line-height: 1.5;
|
|
421
|
+
letter-spacing: 0;
|
|
422
|
+
margin-top: 1.5rem;
|
|
423
|
+
margin-bottom: 0.375rem;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#nd-page .prose h4 {
|
|
427
|
+
font-size: 1.125rem;
|
|
428
|
+
line-height: 1.45;
|
|
429
|
+
/* No negative tracking this close to the body size — it only closes the
|
|
430
|
+
counters without buying any apparent tightness. */
|
|
431
|
+
letter-spacing: 0;
|
|
432
|
+
margin-top: 1.75rem;
|
|
433
|
+
margin-bottom: 0.5rem;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* ── The document's furniture ────────────────────────────────────────────────
|
|
437
|
+
Everything under the headings: links, emphasis, quotes, tables, code. The
|
|
438
|
+
shell leaves nearly all of it at one weight of one ink, which is why a page
|
|
439
|
+
of real documentation read as a wall of black (owner, 2026-08-22). Measured
|
|
440
|
+
before touching anything: a link inside a paragraph rendered lab(4.41) at
|
|
441
|
+
weight 500 — the SAME colour and weight as the `<strong>` beside it, told
|
|
442
|
+
apart only by an underline. A reader cannot see what is clickable.
|
|
443
|
+
|
|
444
|
+
Every rule below is scoped to `.prose`, the container the record's own
|
|
445
|
+
markdown renders into, so the site's own furniture keeps its styling. */
|
|
446
|
+
|
|
447
|
+
/* A link is what the accent is FOR. Only in running text: a heading carries an
|
|
448
|
+
anchor link around its own text, and colouring those turns every heading
|
|
449
|
+
blue — which is the same mistake in the other direction. */
|
|
450
|
+
#nd-page .prose :is(p, li, td, th, blockquote) a {
|
|
451
|
+
color: var(--color-fd-primary);
|
|
452
|
+
text-decoration-color: color-mix(in oklab, var(--color-fd-primary) 30%, transparent);
|
|
453
|
+
text-underline-offset: 3px;
|
|
454
|
+
transition: text-decoration-color 150ms;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
#nd-page .prose :is(p, li, td, th, blockquote) a:hover {
|
|
458
|
+
text-decoration-color: var(--color-fd-primary);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* Emphasis is weight, not colour. The prose default sets 500 against a 400
|
|
462
|
+
body, which at 16px is not a difference a reader notices. */
|
|
463
|
+
#nd-page .prose strong {
|
|
464
|
+
font-weight: 650;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/* A table is a register: its head is a column label, so it speaks in the mono
|
|
468
|
+
voice every other label on this site uses, and the rows are separated by
|
|
469
|
+
hairlines rather than by nothing. The prose default left `tbody tr` with a
|
|
470
|
+
0px bottom border — the cells simply floated. */
|
|
471
|
+
#nd-page .prose thead th {
|
|
472
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
473
|
+
font-size: 0.6875rem;
|
|
474
|
+
font-weight: 500;
|
|
475
|
+
letter-spacing: 0.14em;
|
|
476
|
+
text-transform: uppercase;
|
|
477
|
+
color: var(--color-fd-muted-foreground);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
#nd-page .prose tbody tr {
|
|
481
|
+
border-bottom: 1px solid var(--color-fd-border);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
#nd-page .prose tbody tr:last-child {
|
|
485
|
+
border-bottom: 0;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/* A quotation is an aside, so it steps back rather than shouting in full ink.
|
|
489
|
+
Italic at 16px in a system sans is a default, not a choice — and it is the
|
|
490
|
+
least readable setting of the three things a blockquote could be. */
|
|
491
|
+
#nd-page .prose blockquote {
|
|
492
|
+
font-style: normal;
|
|
493
|
+
color: var(--color-fd-muted-foreground);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/* Inline code is the record's machine vocabulary — a frontmatter key, a path,
|
|
497
|
+
a command — and it was set in the same ink as the prose around it, so the
|
|
498
|
+
grey chip carried the whole distinction (owner, 2026-08-22).
|
|
499
|
+
|
|
500
|
+
It takes a colour of its own rather than one already spoken for. The accent
|
|
501
|
+
belongs to links and actions, and inline code is neither — tinting it blue
|
|
502
|
+
would make every key look clickable, which is the defect one rule up in
|
|
503
|
+
reverse. `--ksor-caution` is the other coloured thing here and it means "the
|
|
504
|
+
record withdrew this". So: a third token, a deep teal, far enough from the
|
|
505
|
+
accent's hue to be told apart at 13px and cool enough to sit in a palette
|
|
506
|
+
built on blue ink. A pair, like every colour on this site. */
|
|
507
|
+
:root {
|
|
508
|
+
--ksor-code: #0f6f6a;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.dark {
|
|
512
|
+
--ksor-code: #6fd8c8;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
#nd-page .prose :not(pre) > code {
|
|
516
|
+
color: var(--ksor-code);
|
|
517
|
+
background: color-mix(in oklab, var(--ksor-code) 7%, var(--color-fd-muted));
|
|
518
|
+
border-color: color-mix(in oklab, var(--ksor-code) 22%, transparent);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/* A code block has to look like a block. Fumadocs paints its surface with
|
|
522
|
+
`bg-fd-card`, and `--card` in this palette is all but the page colour — the
|
|
523
|
+
measured difference was under 2% lightness, so a fenced block read as loose
|
|
524
|
+
monospace text with a faint outline. `--muted` is the token that means
|
|
525
|
+
"a surface ON the page", and it is a pair, so this holds in both themes. */
|
|
526
|
+
#nd-page .prose figure.shiki {
|
|
527
|
+
background: var(--color-fd-muted);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/* Re-assert the shell's own reset, which the ramp above broke. The prose
|
|
531
|
+
plugin zeroes the first block's top margin, but writes that rule as
|
|
532
|
+
`.prose :where(> :first-child)`, and `:where()` contributes nothing to
|
|
533
|
+
specificity — so every heading rule above beat it, and a document whose body
|
|
534
|
+
OPENED with a heading started 44px lower than one that opened with a
|
|
535
|
+
sentence (measured, 2026-08-22). The top of a document must not move with
|
|
536
|
+
the author's choice of first block. This selector is (1,2,0) against the
|
|
537
|
+
heading rules' (1,1,1), so it takes the argument back. */
|
|
538
|
+
#nd-page .prose > :first-child {
|
|
539
|
+
margin-top: 0;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/* ── The page pager ──────────────────────────────────────────────────────────
|
|
543
|
+
Fumadocs lays the neighbours out as a grid that becomes two columns when both
|
|
544
|
+
exist. With only one — the first or last document of the record — it stays a
|
|
545
|
+
single full-width column, so a lone "next" stretched the whole row with its
|
|
546
|
+
text jammed against the right edge and read as a broken card. Hold it to half
|
|
547
|
+
the row, on the side it points to: the next card is the one the shell marks
|
|
548
|
+
`text-end`. Full width on a narrow screen, where one column is correct. */
|
|
549
|
+
@media (min-width: 48rem) {
|
|
550
|
+
#nd-page > [class*="@container"]:has(> a:only-child) {
|
|
551
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
#nd-page > [class*="@container"] > a:only-child[class*="text-end"] {
|
|
555
|
+
grid-column: 2;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/* …and it sits at the foot of the page whatever the document's length. The
|
|
560
|
+
article is a flex column stretched to the viewport, so on a short document
|
|
561
|
+
the neighbours used to land wherever the text stopped — 265px above the
|
|
562
|
+
bottom edge on `/docs/policies`, measured — which reads as content, not as
|
|
563
|
+
the end of the page. `margin-top: auto` gives the free space to the gap
|
|
564
|
+
above them instead. It is inert once the document is taller than the
|
|
565
|
+
viewport: with no free space the auto margin is zero, so a long document
|
|
566
|
+
keeps the shell's own 16px gap (verified both ways at 863px of viewport). */
|
|
567
|
+
#nd-page > [class*="@container"]:last-child {
|
|
568
|
+
margin-top: auto;
|
|
569
|
+
}
|
|
@@ -2,6 +2,8 @@ import { RootProvider } from "fumadocs-ui/provider/next";
|
|
|
2
2
|
import "./global.css";
|
|
3
3
|
import type { Metadata } from "next";
|
|
4
4
|
import { appTitle } from "@/lib/shared";
|
|
5
|
+
import { basePath, caveatStatusByUrl } from "@/lib/source";
|
|
6
|
+
import KsorSearchDialog from "@/components/search-dialog";
|
|
5
7
|
|
|
6
8
|
// No next/font/google: it fetches the face from Google at BUILD time, so a
|
|
7
9
|
// scaffolded project could not build offline and two builds of one commit
|
|
@@ -21,16 +23,33 @@ export default function Layout({ children }: LayoutProps<"/">) {
|
|
|
21
23
|
return (
|
|
22
24
|
<html lang="en" suppressHydrationWarning>
|
|
23
25
|
<body className="flex flex-col min-h-screen">
|
|
26
|
+
{/* Which documents carry a caveat status, for the search dialog — it
|
|
27
|
+
runs in the browser over a static index that has no field for it.
|
|
28
|
+
Delivered in the document rather than as a dialog prop because
|
|
29
|
+
RootProvider types `options` against the SHIPPED dialog's props, and
|
|
30
|
+
casting that away would hide a real break the day those props move.
|
|
31
|
+
`<` is escaped: a title or route is authored content, and closing
|
|
32
|
+
this tag early would be script injection from the record. */}
|
|
33
|
+
<script
|
|
34
|
+
type="application/json"
|
|
35
|
+
id="ksor-statuses"
|
|
36
|
+
dangerouslySetInnerHTML={{
|
|
37
|
+
__html: JSON.stringify(caveatStatusByUrl()).replaceAll("<", "\\u003c"),
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
24
40
|
<RootProvider
|
|
25
41
|
search={{
|
|
26
42
|
// Static search: the browser downloads the index that
|
|
27
43
|
// app/api/search exports at build time (staticGET) and runs
|
|
28
44
|
// Orama client-side — no server needed, so search keeps
|
|
29
45
|
// working on any static host.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
//
|
|
47
|
+
// Our own dialog, composed from the shell's primitives, so a
|
|
48
|
+
// withdrawn document is marked in the RESULTS too — the last
|
|
49
|
+
// surface where it looked identical to the one that replaced it,
|
|
50
|
+
// and the one whose snippet quotes its obsolete figures.
|
|
51
|
+
SearchDialog: KsorSearchDialog,
|
|
52
|
+
options: { api: `${basePath}/api/search` },
|
|
34
53
|
}}
|
|
35
54
|
>
|
|
36
55
|
{children}
|
|
@@ -3,8 +3,10 @@ import { getLLMText, getSortedPages } from "@/lib/source";
|
|
|
3
3
|
export const revalidate = false;
|
|
4
4
|
|
|
5
5
|
export async function GET(): Promise<Response> {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// The whole set is threaded through so each document's successor pointer
|
|
7
|
+
// resolves to a route rather than the `./x.md` a consumer cannot follow.
|
|
8
|
+
const pages = getSortedPages();
|
|
9
|
+
const scanned = await Promise.all(pages.map((page) => getLLMText(page, pages)));
|
|
8
10
|
|
|
9
11
|
return new Response(scanned.join("\n\n"));
|
|
10
12
|
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { appName } from "@/lib/shared";
|
|
1
|
+
import { recordIndexText } from "@/lib/source";
|
|
3
2
|
|
|
4
3
|
export const revalidate = false;
|
|
5
4
|
|
|
6
5
|
// The agent-facing index of the record: this instance's name, then every
|
|
7
|
-
// document in sidebar order, each link usable as-is on a sub-path host
|
|
6
|
+
// document in sidebar order, each link usable as-is on a sub-path host — and
|
|
7
|
+
// each carrying its governance when the governance is a caveat.
|
|
8
|
+
//
|
|
9
|
+
// Without that last part a withdrawn document and the one that replaced it are
|
|
10
|
+
// two adjacent entries told apart only by whatever a human happened to type
|
|
11
|
+
// into a title, and an agent picks either (research/site-design.md F1).
|
|
12
|
+
//
|
|
13
|
+
// The bytes are built in lib/source (`recordIndexText`) because the home page
|
|
14
|
+
// shows this same index to a reader — one index, one spelling.
|
|
8
15
|
export function GET(): Response {
|
|
9
|
-
|
|
10
|
-
const link = `- [${page.data.title}](${basePath}${page.url})`;
|
|
11
|
-
return page.data.description ? `${link}: ${page.data.description}` : link;
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
return new Response(`# ${appName}\n\n${lines.join("\n")}\n`);
|
|
16
|
+
return new Response(recordIndexText());
|
|
15
17
|
}
|