@panaversity/ksor 0.0.39 → 0.0.40
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 +96 -0
- package/README.md +18 -2
- package/package.json +1 -1
- package/templates/scaffold/AGENTS.md +59 -6
- package/templates/scaffold/gitignore +3 -0
- package/templates/scaffold/knowledge/governance-ladder.md +5 -0
- package/templates/scaffold/knowledge/surfaces/for-agents.md +6 -0
- package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +9 -1
- package/templates/scaffold/system/site/app/global.css +185 -1
- package/templates/scaffold/system/site/components/code-block.tsx +87 -0
- package/templates/scaffold/system/site/components/embed.tsx +276 -0
- package/templates/scaffold/system/site/components/governance.tsx +15 -1
- package/templates/scaffold/system/site/components/mdx.tsx +10 -2
- package/templates/scaffold/system/site/components/record-views.tsx +6 -17
- package/templates/scaffold/system/site/lib/alert-rule.ts +214 -0
- package/templates/scaffold/system/site/lib/embed-rule.ts +246 -0
- package/templates/scaffold/system/site/lib/stage-knowledge.ts +100 -3
- package/templates/scaffold/system/site/source.config.ts +32 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,101 @@
|
|
|
1
1
|
# @panaversity/ksor
|
|
2
2
|
|
|
3
|
+
## 0.0.40
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- cd42f81: Three readability changes to a document's page.
|
|
8
|
+
|
|
9
|
+
A long line in a code block can be **unwrapped by the reader**, with a button
|
|
10
|
+
that appears only on a block that actually overflows. The record's commands run
|
|
11
|
+
to hundreds of characters, and in a docs column that was a scrollbar with the
|
|
12
|
+
left edge of every line going out of view. Wrapping is not the default, because
|
|
13
|
+
it is worse for the blocks that do not need it.
|
|
14
|
+
|
|
15
|
+
A table's rows **alternate**, faintly, so a row holds together across three
|
|
16
|
+
columns of wrapped prose.
|
|
17
|
+
|
|
18
|
+
A callout carries a **rule down its left edge**, in its kind's colour — the
|
|
19
|
+
same device a verbatim block uses, so "this passage is set apart" looks the
|
|
20
|
+
same everywhere it happens.
|
|
21
|
+
|
|
22
|
+
- e19f226: A passage a reader must not miss can now be a callout. Write GitHub's alert
|
|
23
|
+
syntax — a blockquote whose first line is `[!WARNING]` — and the site renders a
|
|
24
|
+
styled panel tinted with that kind's own colour:
|
|
25
|
+
|
|
26
|
+
```markdown
|
|
27
|
+
> [!TIP]
|
|
28
|
+
> Climb one rung at a time, and only when something has gone wrong without it.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
It stays a plain blockquote everywhere else the record is read, carrying a
|
|
32
|
+
visible label, and `/md/` and `llms-full.txt` keep the author's blockquote
|
|
33
|
+
rather than this site's component.
|
|
34
|
+
|
|
35
|
+
Not `:::warning`: a dialect renders as literal punctuation everywhere outside
|
|
36
|
+
this site.
|
|
37
|
+
|
|
38
|
+
- 704e949: A document can now show something running, where the prose puts it. Give a link
|
|
39
|
+
the title `embed` and the site renders it as a click-to-load frame:
|
|
40
|
+
|
|
41
|
+
```markdown
|
|
42
|
+
[Play run-until-done](goal-loop.sim.html "embed")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
It stays an ordinary CommonMark link everywhere else — GitHub, a plain editor,
|
|
46
|
+
`/md/`, `llms-full.txt` — so nothing was added to `knowledge/`.
|
|
47
|
+
|
|
48
|
+
Prefer carrying the page in. A `<name>.sim.html` beside its document, exactly
|
|
49
|
+
like a figure, is published by the build and served from your own site: it
|
|
50
|
+
works offline, tells nobody outside what someone is reading, and is versioned
|
|
51
|
+
with the document. An `https:` link works too, but many hosts send
|
|
52
|
+
`X-Frame-Options: SAMEORIGIN` and a browser will refuse to frame them.
|
|
53
|
+
|
|
54
|
+
Nothing is requested until a reader clicks, so a built page still makes zero
|
|
55
|
+
external requests. A carried page is measured, so the frame is exactly as tall
|
|
56
|
+
as what it holds — you never write a height into a document.
|
|
57
|
+
|
|
58
|
+
- a55e93e: A numbered list now counts in the record's accent, and the term a list item
|
|
59
|
+
defines takes it too — `**Heartbeat:** a schedule that…` puts the term in
|
|
60
|
+
accent and leaves the sentence in ink. The markers were grey at regular weight,
|
|
61
|
+
lighter than the text they counted, so a list of six steps read as six
|
|
62
|
+
paragraphs that happened to start with digits.
|
|
63
|
+
- a59a7af: The documented way to run `ksor init` pins a version, so a stale runner cache
|
|
64
|
+
can no longer decide which ksor an adopter meets.
|
|
65
|
+
|
|
66
|
+
`npx @panaversity/ksor init my-sor` is spec `*`, and any cached version
|
|
67
|
+
satisfies it — so npx runs whatever that machine already has without consulting
|
|
68
|
+
the registry. Found live on a Windows box following the README: it replayed
|
|
69
|
+
`0.0.0`, the name-reservation stub published on the first day of the project,
|
|
70
|
+
whose whole implementation prints "the name is reserved; this is not a release"
|
|
71
|
+
and exits 2. Thirty-nine releases later, the first command in the README
|
|
72
|
+
produced a placeholder, and nothing in that output points at the cause.
|
|
73
|
+
|
|
74
|
+
Both READMEs now say `@panaversity/ksor@latest`. The three "Start here" forms
|
|
75
|
+
change together — `pnpm dlx` reuses its cache for 24 hours by default and
|
|
76
|
+
`bunx` resolves from the install cache before the registry, so pinning only npx
|
|
77
|
+
would have left two of the three supported managers in the trap. `npm install
|
|
78
|
+
-g` is unchanged: an install resolves the `latest` dist-tag by definition.
|
|
79
|
+
|
|
80
|
+
If you have run ksor before, your own cache is still warm. Run the `@latest`
|
|
81
|
+
form once and it resolves the current release.
|
|
82
|
+
|
|
83
|
+
- 9e92e9e: A document with no summary no longer carries an empty view strip. The reading
|
|
84
|
+
time moved into the governance row, beside the owner and the effective date,
|
|
85
|
+
so the commonest document — one with no summary — gets one bar of facts rather
|
|
86
|
+
than a second bar holding a single number at the far end of a rule.
|
|
87
|
+
- d723487: A table's head now reads as a head: the record's accent as a wash behind it,
|
|
88
|
+
the column labels in that accent's ink, and an accent rule under the band. They
|
|
89
|
+
were grey on near-white, so on a three-column table the eye had to work out
|
|
90
|
+
which row was the label.
|
|
91
|
+
- 8ec1a10: A fenced block with no language is set as a passage to reproduce rather than as
|
|
92
|
+
code: prose leading, and the record's accent down its left edge. Prompts,
|
|
93
|
+
commands to say and messages to paste were arriving as walls of tight
|
|
94
|
+
monospace, set for scanning columns of code when what is in them is sentences.
|
|
95
|
+
|
|
96
|
+
Nothing to author — the highlighter's own output is the signal, so a block that
|
|
97
|
+
was never highlighted is the one that changes.
|
|
98
|
+
|
|
3
99
|
## 0.0.39
|
|
4
100
|
|
|
5
101
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ when the corpus does not cover the question.
|
|
|
10
10
|
## Start
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npx @panaversity/ksor init my-sor
|
|
13
|
+
npx @panaversity/ksor@latest init my-sor
|
|
14
14
|
cd my-sor
|
|
15
15
|
pnpm install
|
|
16
16
|
pnpm dev # the site, live at http://localhost:3000
|
|
@@ -32,6 +32,22 @@ scaffold is deterministic and offline, and every refusal explains itself.
|
|
|
32
32
|
> and the released version number are authoritative for the exact released
|
|
33
33
|
> functionality.
|
|
34
34
|
|
|
35
|
+
### What a document can do
|
|
36
|
+
|
|
37
|
+
`knowledge/` is CommonMark, and stays CommonMark — every affordance below is
|
|
38
|
+
syntax that other readers already handle, so a record renders honestly on
|
|
39
|
+
GitHub, in a plain editor, at `/md/` and in `llms-full.txt`.
|
|
40
|
+
|
|
41
|
+
| Write | The site renders |
|
|
42
|
+
| ----- | ---------------- |
|
|
43
|
+
| `> [!WARNING]` as a blockquote's first line | a callout, tinted in that kind's colour |
|
|
44
|
+
| ` ```bash tab="Claude Code" ` on consecutive fences | one tab group, the choice remembered |
|
|
45
|
+
| a fence with no language | a passage to reproduce, set for reading |
|
|
46
|
+
|
|
47
|
+
Nothing to author for the rest: a table's head reads as a head, a numbered list
|
|
48
|
+
counts in the record's accent, and a code block wider than the column gets a
|
|
49
|
+
button that unwraps it.
|
|
50
|
+
|
|
35
51
|
### Study attachments
|
|
36
52
|
|
|
37
53
|
A document may carry companions named after it, and the site renders each on
|
|
@@ -72,7 +88,7 @@ Full concept, design goals, and project status:
|
|
|
72
88
|
|
|
73
89
|
```bash
|
|
74
90
|
npm install -g @panaversity/ksor # command installed: ksor
|
|
75
|
-
npx @panaversity/ksor
|
|
91
|
+
npx @panaversity/ksor@latest # or run without installing
|
|
76
92
|
```
|
|
77
93
|
|
|
78
94
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panaversity/ksor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.40",
|
|
4
4
|
"description": "Knowledge System of Record — compile governed markdown into a static site for people and an MCP server for AI agents, with citations and measured abstention.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abstention",
|
|
@@ -449,12 +449,14 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
|
|
|
449
449
|
publishes that as fact) and `superseded` (a legacy marker — prefer `status`)
|
|
450
450
|
are available. No other keys; never
|
|
451
451
|
`id:` or `name:` — the path is the identity.
|
|
452
|
-
- **Each page says how long it takes to read**,
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
452
|
+
- **Each page says how long it takes to read**, counted from the document's own
|
|
453
|
+
words when the site is built. Fenced code and frontmatter do not count toward
|
|
454
|
+
it, so a short page carrying a long example is not reported as a long read.
|
|
455
|
+
Nothing to author — it is derived. It sits with the owner and the effective
|
|
456
|
+
date, in the row under the title; a document with a SUMMARY moves it onto
|
|
457
|
+
that view's own strip instead, where it reports the view you are reading
|
|
458
|
+
rather than the document. Each tab names its own figure to a screen reader,
|
|
459
|
+
so the cost of each view is available without switching to it.
|
|
458
460
|
- **The governance keys are rendered, so they are worth filling in.** Each
|
|
459
461
|
page shows its owner and effective date under the title, lists every
|
|
460
462
|
`provenance` entry separately at the foot, and — for a superseded document —
|
|
@@ -664,6 +666,57 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
|
|
|
664
666
|
renders in the site's accent. The list lives in `system/site/app/global.css`
|
|
665
667
|
and is yours — adding a tool is one colour and one mark.
|
|
666
668
|
|
|
669
|
+
- **A passage the reader must not miss, as a CALLOUT.** GitHub's alert syntax —
|
|
670
|
+
a blockquote whose first line is the kind in brackets:
|
|
671
|
+
|
|
672
|
+
```markdown
|
|
673
|
+
> [!WARNING]
|
|
674
|
+
> This threshold changed in March. The figure below is the old one.
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
Five kinds: `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`. The site tints
|
|
678
|
+
the panel in that kind's colour and rules its left edge; every other reader
|
|
679
|
+
of the record — GitHub, a plain editor, `/md/`, `llms-full.txt` — shows an
|
|
680
|
+
ordinary blockquote carrying a visible label, so nothing is lost and nothing
|
|
681
|
+
becomes punctuation.
|
|
682
|
+
|
|
683
|
+
**Not `:::warning`.** That form is a dialect: a record written in one renders
|
|
684
|
+
as literal colons in every reader except the one site that understands it.
|
|
685
|
+
|
|
686
|
+
- **A long line is the reader's to unwrap.** Nothing to author — a fenced block
|
|
687
|
+
wider than the column gets a button beside its copy button that wraps it, and
|
|
688
|
+
a block that fits gets no button at all.
|
|
689
|
+
- **Something running, as an embed.** A document that wants to show a page in
|
|
690
|
+
motion — a simulation, a player, a dashboard — links to it and gives the link
|
|
691
|
+
the title `embed`:
|
|
692
|
+
|
|
693
|
+
```markdown
|
|
694
|
+
[Play run-until-done](goal-loop.sim.html "embed")
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
Still CommonMark: a link title is a tooltip everywhere else, so GitHub, a
|
|
698
|
+
plain editor, `/md/` and `llms-full.txt` all show the author's link. Nothing
|
|
699
|
+
loads until a reader clicks, which is what keeps a built page free of
|
|
700
|
+
external requests — and the panel names what it is about to reach, so the
|
|
701
|
+
click is informed.
|
|
702
|
+
|
|
703
|
+
**You do not state a height.** A page carried in the record is measured, so
|
|
704
|
+
the frame is exactly as tall as what it holds — on this record's own seven,
|
|
705
|
+
to the pixel. A number written into a document would be a number some other
|
|
706
|
+
measure makes wrong.
|
|
707
|
+
|
|
708
|
+
**Carry the page in where you can.** A file named `<name>.sim.html`, sitting
|
|
709
|
+
beside its document exactly like a figure, is published by the build and
|
|
710
|
+
served from this site — so it works offline, tells nobody outside what
|
|
711
|
+
someone is reading, and is versioned with the document instead of changing
|
|
712
|
+
under it. It is an ASSET, not an attachment: named freely, as many per
|
|
713
|
+
document as the prose needs.
|
|
714
|
+
|
|
715
|
+
An `https:` link works too, for a page you cannot carry. It is the weaker
|
|
716
|
+
option for a reason worth knowing: many hosts send `X-Frame-Options:
|
|
717
|
+
SAMEORIGIN`, which forbids any other site from framing them, and a browser
|
|
718
|
+
enforces that whatever this record does. Check before you rely on one.
|
|
719
|
+
|
|
667
720
|
- Copy load-bearing values (numbers, thresholds, dates) exactly from their
|
|
668
721
|
source, and name the source in `provenance`.
|
|
669
722
|
|
|
@@ -11,6 +11,9 @@ system/site/.staged-knowledge/
|
|
|
11
11
|
# and the lock that keeps one evaluation of a build staging at a time; it only
|
|
12
12
|
# outlives a build that was killed mid-stage, and the next build clears it
|
|
13
13
|
system/site/.staged-knowledge.lock
|
|
14
|
+
# a build's copy of every `.sim.html` in the record, put where it can be
|
|
15
|
+
# SERVED; the record owns the sim, this is only where the site publishes it
|
|
16
|
+
system/site/public/sims/
|
|
14
17
|
*.tsbuildinfo
|
|
15
18
|
|
|
16
19
|
# secrets never enter the record — system/ is their future home (serve)
|
|
@@ -18,6 +18,11 @@ record of such documents is already publishable, searchable and citable.
|
|
|
18
18
|
|
|
19
19
|
Each rung is worth climbing when the domain asks for it, and not before.
|
|
20
20
|
|
|
21
|
+
> [!TIP]
|
|
22
|
+
> Climb one rung at a time, and only when something has gone wrong without it.
|
|
23
|
+
> A record that adds owners before anyone disputes a document spends review
|
|
24
|
+
> effort buying nothing.
|
|
25
|
+
|
|
21
26
|
### Owners and sources
|
|
22
27
|
|
|
23
28
|
An owner names who stands behind a document. Provenance names where its claims
|
|
@@ -22,6 +22,12 @@ be checked against the document that carries it rather than taken on trust.
|
|
|
22
22
|
"Not in this record" is a correct answer. It is never an error, and never a
|
|
23
23
|
licence to fall back on what a model happens to remember.
|
|
24
24
|
|
|
25
|
+
> [!WARNING]
|
|
26
|
+
>
|
|
27
|
+
> An agent that fills a gap from its own memory has not used this record — it
|
|
28
|
+
> has used it as an opening paragraph. Nothing in the answer says which half
|
|
29
|
+
> came from where.
|
|
30
|
+
|
|
25
31
|
## Files beside the interface
|
|
26
32
|
|
|
27
33
|
The build publishes the same knowledge as plain files an agent can fetch without
|
|
@@ -130,7 +130,15 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
|
|
|
130
130
|
<DocsTitle>{page.data.title}</DocsTitle>
|
|
131
131
|
<DocsDescription>{page.data.description}</DocsDescription>
|
|
132
132
|
{showGovernance ? (
|
|
133
|
-
<GovernanceMeta
|
|
133
|
+
<GovernanceMeta
|
|
134
|
+
governance={governance}
|
|
135
|
+
replaces={replaces}
|
|
136
|
+
markdownUrl={markdownUrl}
|
|
137
|
+
// Only when there is no view strip to carry it — with a summary,
|
|
138
|
+
// the number belongs to the view the reader picked, not to the
|
|
139
|
+
// document as a whole.
|
|
140
|
+
minutes={summary === null ? minutes : undefined}
|
|
141
|
+
/>
|
|
134
142
|
) : null}
|
|
135
143
|
{/* grow-0, against the shell's own `flex-1`: the article is a flex column
|
|
136
144
|
stretched to the viewport, so the body inflated from ~150px of text to
|
|
@@ -678,13 +678,53 @@ html > body[data-scroll-locked] {
|
|
|
678
678
|
font-weight: 500;
|
|
679
679
|
letter-spacing: 0.14em;
|
|
680
680
|
text-transform: uppercase;
|
|
681
|
-
|
|
681
|
+
/* The record's own accent, at reading strength. The labels were grey on
|
|
682
|
+
near-white and the head did not read as a head at all — on a three-column
|
|
683
|
+
table the eye had to work out which row was the label. */
|
|
684
|
+
color: color-mix(in oklab, var(--color-fd-primary) 72%, var(--color-fd-foreground));
|
|
685
|
+
background: color-mix(in oklab, var(--color-fd-primary) 7%, var(--color-fd-muted));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/* The head is a band, so it is ruled off from the body rather than floating
|
|
689
|
+
above it — and the rule is the accent's, not the hairline's, so the two read
|
|
690
|
+
as one object. */
|
|
691
|
+
#nd-page .prose thead {
|
|
692
|
+
border-bottom: 2px solid color-mix(in oklab, var(--color-fd-primary) 28%, var(--color-fd-border));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.dark #nd-page .prose thead th {
|
|
696
|
+
/* Against near-black the same 7% is almost nothing, and the same 72% ink is
|
|
697
|
+
too bright to sit under body text. */
|
|
698
|
+
color: color-mix(in oklab, var(--color-fd-primary) 60%, var(--color-fd-foreground));
|
|
699
|
+
background: color-mix(in oklab, var(--color-fd-primary) 12%, var(--color-fd-muted));
|
|
682
700
|
}
|
|
683
701
|
|
|
684
702
|
#nd-page .prose tbody tr {
|
|
685
703
|
border-bottom: 1px solid var(--color-fd-border);
|
|
686
704
|
}
|
|
687
705
|
|
|
706
|
+
/* And every other row carries a wash. A hairline alone is enough on a
|
|
707
|
+
two-column table; across three columns of wrapped prose the eye loses which
|
|
708
|
+
cells belong together, because the gap between rows and the gap between
|
|
709
|
+
lines INSIDE a cell are the same distance. The band is what holds a row
|
|
710
|
+
together over that distance.
|
|
711
|
+
|
|
712
|
+
Faint on purpose: it groups, it does not label. Anything stronger competes
|
|
713
|
+
with the head, which is the row that is meant to be read first. */
|
|
714
|
+
#nd-page .prose tbody tr:nth-child(even) {
|
|
715
|
+
background: var(--color-fd-background);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
#nd-page .prose tbody tr:nth-child(odd) {
|
|
719
|
+
background: color-mix(in oklab, var(--color-fd-border) 14%, var(--color-fd-muted));
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.dark #nd-page .prose tbody tr:nth-child(odd) {
|
|
723
|
+
/* A step of the same SIZE reads much stronger against near-black, so the
|
|
724
|
+
dark pair takes less of the border to land at the same apparent distance. */
|
|
725
|
+
background: color-mix(in oklab, var(--color-fd-border) 8%, var(--color-fd-muted));
|
|
726
|
+
}
|
|
727
|
+
|
|
688
728
|
#nd-page .prose tbody tr:last-child {
|
|
689
729
|
border-bottom: 0;
|
|
690
730
|
}
|
|
@@ -731,6 +771,117 @@ html > body[data-scroll-locked] {
|
|
|
731
771
|
background: var(--color-fd-muted);
|
|
732
772
|
}
|
|
733
773
|
|
|
774
|
+
/* A block with no language is not code — it is a passage to reproduce
|
|
775
|
+
verbatim: a prompt to type, a command to say, a message to paste. This
|
|
776
|
+
record has eight, and they arrived as walls of tight monospace, set for
|
|
777
|
+
scanning columns of code when what is in them is sentences.
|
|
778
|
+
|
|
779
|
+
The highlighter's own output is the selector, so nothing has to be authored
|
|
780
|
+
and nothing changes for the record: shiki gives every token in a HIGHLIGHTED
|
|
781
|
+
block an inline `--shiki-light`, and a plain one has none. `:has()` asks
|
|
782
|
+
that question directly.
|
|
783
|
+
|
|
784
|
+
Prose leading, because the lines are sentences; and the accent down the left
|
|
785
|
+
edge, which is what the rest of this site uses to mark structure — here it
|
|
786
|
+
says "reproduce this exactly", which is the one thing these blocks have in
|
|
787
|
+
common. */
|
|
788
|
+
#nd-page .prose figure.shiki:not(:has(code span[style*="--shiki-light"])) {
|
|
789
|
+
border-left: 3px solid color-mix(in oklab, var(--color-fd-primary) 55%, var(--color-fd-border));
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
#nd-page .prose figure.shiki:not(:has(code span[style*="--shiki-light"])) code {
|
|
793
|
+
line-height: 1.7;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/* Showing the whole of a long line, when the reader asks for it.
|
|
797
|
+
components/code-block.tsx puts the button on the block and sets the
|
|
798
|
+
attribute; this is what the attribute means.
|
|
799
|
+
|
|
800
|
+
`pre` is `w-max` in the shell, which sizes it to the longest line — that is
|
|
801
|
+
what makes wrapping impossible, so the width has to come back to the column
|
|
802
|
+
before `pre-wrap` can do anything. A wrapped line is then indented under its
|
|
803
|
+
own, the way a terminal marks a continuation, because otherwise a two-line
|
|
804
|
+
command reads as two commands.
|
|
805
|
+
|
|
806
|
+
Wrapping is NOT the default. It was, for one commit, and it is worse for the
|
|
807
|
+
blocks that do not need it: a yaml file rewrapped at the column reads as
|
|
808
|
+
though its indentation means something else. */
|
|
809
|
+
#nd-page .prose figure.shiki[data-wrapped] pre {
|
|
810
|
+
width: 100%;
|
|
811
|
+
min-width: 0;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
#nd-page .prose figure.shiki[data-wrapped] code > * {
|
|
815
|
+
white-space: pre-wrap;
|
|
816
|
+
overflow-wrap: anywhere;
|
|
817
|
+
/* The hang, plus the gutter below — so a continuation is indented from the
|
|
818
|
+
text, not from the block's edge. */
|
|
819
|
+
padding-left: calc(1rem + 2ch);
|
|
820
|
+
text-indent: -2ch;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/* A block needs a gutter on both sides. Unwrapped, the shell gives one on the
|
|
824
|
+
left with `ps-*` and lets a long line run under the buttons on the right,
|
|
825
|
+
because the line can always be scrolled clear of them. Wrapped, it cannot:
|
|
826
|
+
the first line ends under the copy button with no way to move it, and the
|
|
827
|
+
left edge sits on the rule (seen live). So the wrapped state pays for its
|
|
828
|
+
own margins — a gutter left, and enough right for two 24px buttons. */
|
|
829
|
+
#nd-page .prose figure.shiki[data-wrapped] code {
|
|
830
|
+
padding-inline: 0 4rem;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
#nd-page .prose figure.shiki[data-wrapped] .fd-scroll-container {
|
|
834
|
+
padding-inline-start: 0;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/* And a hairline where a scrollbar appears, in place of the platform's slab. */
|
|
838
|
+
#nd-page .prose figure.shiki .fd-scroll-container {
|
|
839
|
+
scrollbar-width: thin;
|
|
840
|
+
scrollbar-color: var(--color-fd-border) transparent;
|
|
841
|
+
}
|
|
842
|
+
/* A callout has to look like a callout. The same `bg-fd-card` trap as the code
|
|
843
|
+
block above: fumadocs paints the panel with `--card`, which in this palette
|
|
844
|
+
is all but the page colour, so a Note and a Warning both arrived as a white
|
|
845
|
+
box with a coloured hair down one edge — the kind was legible only in the
|
|
846
|
+
icon.
|
|
847
|
+
|
|
848
|
+
Fumadocs already puts the kind's own colour on the element as
|
|
849
|
+
`--callout-color`, so ONE rule tints every kind: a wash of that colour over
|
|
850
|
+
`--muted`, the token that means "a surface on the page". No per-kind
|
|
851
|
+
selector, and a kind added upstream is tinted without touching this.
|
|
852
|
+
|
|
853
|
+
6% in light and 10% in dark, because the same fraction reads as a different
|
|
854
|
+
amount of colour against near-white and against near-black. Kept low on
|
|
855
|
+
purpose: this is the record's own accent discipline — the tint says which
|
|
856
|
+
kind, it does not compete with the prose. */
|
|
857
|
+
#nd-page .prose [style*="--callout-color"] {
|
|
858
|
+
background: color-mix(in oklab, var(--callout-color) 6%, var(--color-fd-muted));
|
|
859
|
+
border-color: color-mix(in oklab, var(--callout-color) 20%, var(--color-fd-border));
|
|
860
|
+
/* The shadow was doing the work the colour now does, and lifted the panel
|
|
861
|
+
off the page in a way nothing else on it does. */
|
|
862
|
+
box-shadow: none;
|
|
863
|
+
|
|
864
|
+
/* THE RULE DOWN THE LEFT EDGE — the same device the verbatim block uses, so
|
|
865
|
+
"this passage is set apart" looks the same everywhere it happens, whatever
|
|
866
|
+
kind of thing is set apart. Fumadocs draws a half-pixel bar INSIDE the
|
|
867
|
+
panel instead, which is too faint to read as a marker at all.
|
|
868
|
+
|
|
869
|
+
In the kind's own colour, which for a Note — much the commonest — is the
|
|
870
|
+
accent, so the standard case is the blue line. */
|
|
871
|
+
border-left: 3px solid color-mix(in oklab, var(--callout-color) 65%, var(--color-fd-border));
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/* And the inner bar goes, now that the edge carries it. Two rules for the same
|
|
875
|
+
job, a pixel apart, read as a mistake. */
|
|
876
|
+
#nd-page .prose [style*="--callout-color"] > div[class*="w-0.5"] {
|
|
877
|
+
display: none;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.dark #nd-page .prose [style*="--callout-color"] {
|
|
881
|
+
background: color-mix(in oklab, var(--callout-color) 10%, var(--color-fd-muted));
|
|
882
|
+
border-color: color-mix(in oklab, var(--callout-color) 24%, var(--color-fd-border));
|
|
883
|
+
}
|
|
884
|
+
|
|
734
885
|
/* Re-assert the shell's own reset, which the ramp above broke. The prose
|
|
735
886
|
plugin zeroes the first block's top margin, but writes that rule as
|
|
736
887
|
`.prose :where(> :first-child)`, and `:where()` contributes nothing to
|
|
@@ -743,6 +894,39 @@ html > body[data-scroll-locked] {
|
|
|
743
894
|
margin-top: 0;
|
|
744
895
|
}
|
|
745
896
|
|
|
897
|
+
/* A numbered list is a sequence, and its numbers should say so. The marker
|
|
898
|
+
arrived grey at regular weight, lighter than the text it counts, so a list
|
|
899
|
+
of six steps read as six paragraphs that happened to start with digits.
|
|
900
|
+
|
|
901
|
+
The accent, and only here: a number is structure, which is what this
|
|
902
|
+
record's accent is for. */
|
|
903
|
+
#nd-page .prose ol > li::marker {
|
|
904
|
+
color: var(--color-fd-primary);
|
|
905
|
+
font-weight: 600;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/* And the TERM a list item defines. `**Heartbeat:** a schedule that…` is the
|
|
909
|
+
commonest shape in this record — a term, then what it means — and the term
|
|
910
|
+
is the thing a reader scans for.
|
|
911
|
+
|
|
912
|
+
Only the term. Bold elsewhere in the sentence stays ink, because a page
|
|
913
|
+
where every emphasis is coloured has no emphasis: the accent stops meaning
|
|
914
|
+
"this is the one to find" and starts meaning "this is bold". */
|
|
915
|
+
#nd-page .prose li > strong:first-child,
|
|
916
|
+
#nd-page .prose li > p:first-child > strong:first-child {
|
|
917
|
+
color: var(--color-fd-primary);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/* The same rule, one level in. The document's first block is not a child of
|
|
921
|
+
`.prose` — it is inside the views wrapper, and inside a tab panel when the
|
|
922
|
+
document has a summary — so the reset above never reached it and the top of
|
|
923
|
+
every document sat 20px lower than the rule above it intended (measured
|
|
924
|
+
after the empty view strip was removed, which is what exposed it). */
|
|
925
|
+
#nd-page .prose > .ksor-views > :first-child,
|
|
926
|
+
#nd-page .prose > .ksor-views [role="tabpanel"] > :first-child {
|
|
927
|
+
margin-top: 0;
|
|
928
|
+
}
|
|
929
|
+
|
|
746
930
|
/* ── The page pager ──────────────────────────────────────────────────────────
|
|
747
931
|
Fumadocs lays the neighbours out as a grid that becomes two columns when both
|
|
748
932
|
exist. With only one — the first or last document of the record — it stays a
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock";
|
|
4
|
+
import { WrapText } from "lucide-react";
|
|
5
|
+
import { useCallback, useEffect, useRef, useState, type ReactElement } from "react";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A code block that can be asked to show the whole line.
|
|
9
|
+
*
|
|
10
|
+
* A record's commands are long — one in the fixture runs to 292 characters —
|
|
11
|
+
* and in a 672px column that is a scrollbar, with the left edge of every line
|
|
12
|
+
* going out of view as you drag it. Wrapping everything by default was tried
|
|
13
|
+
* and is worse for the blocks that do not need it: a yaml file rewrapped at
|
|
14
|
+
* the column reads as though its indentation means something else.
|
|
15
|
+
*
|
|
16
|
+
* So the reader decides, per block. The button appears ONLY on a block that
|
|
17
|
+
* actually overflows, which is measured after layout rather than guessed —
|
|
18
|
+
* most blocks fit, and a control that does nothing is worse than no control.
|
|
19
|
+
*
|
|
20
|
+
* The `Actions` slot is fumadocs' own, so the button sits with the copy button
|
|
21
|
+
* and inherits its placement rather than being positioned against it.
|
|
22
|
+
*/
|
|
23
|
+
export function WrappableCodeBlock(props: React.ComponentProps<"pre">): ReactElement {
|
|
24
|
+
const figure = useRef<HTMLElement>(null);
|
|
25
|
+
const [overflows, setOverflows] = useState(false);
|
|
26
|
+
const [wrapped, setWrapped] = useState(false);
|
|
27
|
+
|
|
28
|
+
const measure = useCallback((): void => {
|
|
29
|
+
const viewport = figure.current?.querySelector<HTMLElement>(".fd-scroll-container");
|
|
30
|
+
if (!viewport) return;
|
|
31
|
+
// While wrapped there is nothing to overflow, so the question is only
|
|
32
|
+
// asked in the state that can answer it — otherwise turning wrapping on
|
|
33
|
+
// would remove the button that turns it off.
|
|
34
|
+
setOverflows((was) => (wrapped ? was : viewport.scrollWidth > viewport.clientWidth + 1));
|
|
35
|
+
}, [wrapped]);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
measure();
|
|
39
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
40
|
+
const viewport = figure.current?.querySelector<HTMLElement>(".fd-scroll-container");
|
|
41
|
+
if (!viewport) return;
|
|
42
|
+
// The column changes width with the window, and with the table of contents
|
|
43
|
+
// appearing; a block that fits at one width overflows at another.
|
|
44
|
+
const observer = new ResizeObserver(measure);
|
|
45
|
+
observer.observe(viewport);
|
|
46
|
+
return () => observer.disconnect();
|
|
47
|
+
}, [measure]);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<CodeBlock
|
|
51
|
+
ref={figure}
|
|
52
|
+
data-wrapped={wrapped ? "" : undefined}
|
|
53
|
+
Actions={({ className, children }) => (
|
|
54
|
+
// `children` IS the copy button — fumadocs hands the default actions
|
|
55
|
+
// in, and a slot that ignores them REPLACES the copy button instead of
|
|
56
|
+
// joining it (found live: the block lost copy entirely). The wrapper
|
|
57
|
+
// mirrors the default's own `empty:hidden`, so a block with neither
|
|
58
|
+
// button still renders nothing.
|
|
59
|
+
<div className={`flex items-center empty:hidden ${className ?? ""}`}>
|
|
60
|
+
{children}
|
|
61
|
+
{overflows ? (
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
aria-pressed={wrapped}
|
|
65
|
+
onClick={() => setWrapped((on) => !on)}
|
|
66
|
+
// The copy button's own metrics — measured, not guessed: 24x24
|
|
67
|
+
// with a 3.5 icon. Mine came out 30x30 and the pair read as two
|
|
68
|
+
// controls of different importance.
|
|
69
|
+
className="inline-flex size-6 items-center justify-center rounded-lg transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground [&_svg]:size-3.5"
|
|
70
|
+
// Said as what it does, not as what it is: the reader wants the
|
|
71
|
+
// rest of the line, not a setting.
|
|
72
|
+
title={wrapped ? "Show one line per line" : "Show the whole line"}
|
|
73
|
+
>
|
|
74
|
+
<WrapText aria-hidden />
|
|
75
|
+
<span className="sr-only">
|
|
76
|
+
{wrapped ? "Show one line per line" : "Show the whole line"}
|
|
77
|
+
</span>
|
|
78
|
+
</button>
|
|
79
|
+
) : null}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
{...props}
|
|
83
|
+
>
|
|
84
|
+
<Pre>{props.children}</Pre>
|
|
85
|
+
</CodeBlock>
|
|
86
|
+
);
|
|
87
|
+
}
|