@gjermundgaraba-designs/simple-rat 0.0.0-stage → 0.7.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/DESIGN.md +397 -0
- package/LICENSE +21 -0
- package/README.md +64 -2
- package/dist/highlight/highlight.d.ts +5 -0
- package/dist/highlight/highlight.d.ts.map +1 -0
- package/dist/highlight/highlight.js +120 -0
- package/dist/highlight/vite.d.ts +19 -0
- package/dist/highlight/vite.d.ts.map +1 -0
- package/dist/highlight/vite.js +38 -0
- package/dist/src/code/code.d.ts +20 -0
- package/dist/src/code/code.d.ts.map +1 -0
- package/dist/src/code/code.js +9 -0
- package/dist/src/code/index.d.ts +3 -0
- package/dist/src/code/index.d.ts.map +1 -0
- package/dist/src/code/index.js +1 -0
- package/dist/src/codeBlock/codeBlock.d.ts +32 -0
- package/dist/src/codeBlock/codeBlock.d.ts.map +1 -0
- package/dist/src/codeBlock/codeBlock.js +35 -0
- package/dist/src/codeBlock/index.d.ts +3 -0
- package/dist/src/codeBlock/index.d.ts.map +1 -0
- package/dist/src/codeBlock/index.js +1 -0
- package/dist/src/copy/copy.d.ts +83 -0
- package/dist/src/copy/copy.d.ts.map +1 -0
- package/dist/src/copy/copy.js +76 -0
- package/dist/src/copyTab/copyTab.d.ts +47 -0
- package/dist/src/copyTab/copyTab.d.ts.map +1 -0
- package/dist/src/copyTab/copyTab.js +31 -0
- package/dist/src/copyTab/index.d.ts +3 -0
- package/dist/src/copyTab/index.d.ts.map +1 -0
- package/dist/src/copyTab/index.js +1 -0
- package/dist/src/diagram/diagram.d.ts +18 -0
- package/dist/src/diagram/diagram.d.ts.map +1 -0
- package/dist/src/diagram/diagram.js +18 -0
- package/dist/src/diagram/index.d.ts +3 -0
- package/dist/src/diagram/index.d.ts.map +1 -0
- package/dist/src/diagram/index.js +1 -0
- package/dist/src/footer/footer.d.ts +9 -0
- package/dist/src/footer/footer.d.ts.map +1 -0
- package/dist/src/footer/footer.js +3 -0
- package/dist/src/footer/index.d.ts +3 -0
- package/dist/src/footer/index.d.ts.map +1 -0
- package/dist/src/footer/index.js +1 -0
- package/dist/src/footnotes/footnotes.d.ts +12 -0
- package/dist/src/footnotes/footnotes.d.ts.map +1 -0
- package/dist/src/footnotes/footnotes.js +16 -0
- package/dist/src/footnotes/index.d.ts +3 -0
- package/dist/src/footnotes/index.d.ts.map +1 -0
- package/dist/src/footnotes/index.js +1 -0
- package/dist/src/header/header.d.ts +11 -0
- package/dist/src/header/header.d.ts.map +1 -0
- package/dist/src/header/header.js +8 -0
- package/dist/src/header/index.d.ts +3 -0
- package/dist/src/header/index.d.ts.map +1 -0
- package/dist/src/header/index.js +1 -0
- package/dist/src/heading/heading.d.ts +12 -0
- package/dist/src/heading/heading.d.ts.map +1 -0
- package/dist/src/heading/heading.js +12 -0
- package/dist/src/heading/index.d.ts +3 -0
- package/dist/src/heading/index.d.ts.map +1 -0
- package/dist/src/heading/index.js +1 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +20 -0
- package/dist/src/navLink/index.d.ts +3 -0
- package/dist/src/navLink/index.d.ts.map +1 -0
- package/dist/src/navLink/index.js +1 -0
- package/dist/src/navLink/navLink.d.ts +13 -0
- package/dist/src/navLink/navLink.d.ts.map +1 -0
- package/dist/src/navLink/navLink.js +7 -0
- package/dist/src/note/index.d.ts +3 -0
- package/dist/src/note/index.d.ts.map +1 -0
- package/dist/src/note/index.js +1 -0
- package/dist/src/note/note.d.ts +12 -0
- package/dist/src/note/note.d.ts.map +1 -0
- package/dist/src/note/note.js +7 -0
- package/dist/src/pager/index.d.ts +3 -0
- package/dist/src/pager/index.d.ts.map +1 -0
- package/dist/src/pager/index.js +1 -0
- package/dist/src/pager/pager.d.ts +17 -0
- package/dist/src/pager/pager.d.ts.map +1 -0
- package/dist/src/pager/pager.js +15 -0
- package/dist/src/postList/index.d.ts +3 -0
- package/dist/src/postList/index.d.ts.map +1 -0
- package/dist/src/postList/index.js +1 -0
- package/dist/src/postList/postList.d.ts +13 -0
- package/dist/src/postList/postList.d.ts.map +1 -0
- package/dist/src/postList/postList.js +3 -0
- package/dist/src/postMeta/index.d.ts +3 -0
- package/dist/src/postMeta/index.d.ts.map +1 -0
- package/dist/src/postMeta/index.js +1 -0
- package/dist/src/postMeta/postMeta.d.ts +11 -0
- package/dist/src/postMeta/postMeta.d.ts.map +1 -0
- package/dist/src/postMeta/postMeta.js +10 -0
- package/dist/src/scheme/index.d.ts +4 -0
- package/dist/src/scheme/index.d.ts.map +1 -0
- package/dist/src/scheme/index.js +3 -0
- package/dist/src/scheme/scheme.d.ts +10 -0
- package/dist/src/scheme/scheme.d.ts.map +1 -0
- package/dist/src/scheme/scheme.js +9 -0
- package/dist/src/scheme/schemeSwitch.d.ts +18 -0
- package/dist/src/scheme/schemeSwitch.d.ts.map +1 -0
- package/dist/src/scheme/schemeSwitch.js +25 -0
- package/dist/src/scheme/script.d.ts +9 -0
- package/dist/src/scheme/script.d.ts.map +1 -0
- package/dist/src/scheme/script.js +8 -0
- package/dist/src/scheme/switch.d.ts +64 -0
- package/dist/src/scheme/switch.d.ts.map +1 -0
- package/dist/src/scheme/switch.js +72 -0
- package/dist/src/scrolls/index.d.ts +2 -0
- package/dist/src/scrolls/index.d.ts.map +1 -0
- package/dist/src/scrolls/index.js +1 -0
- package/dist/src/scrolls/scrolls.d.ts +19 -0
- package/dist/src/scrolls/scrolls.d.ts.map +1 -0
- package/dist/src/scrolls/scrolls.js +49 -0
- package/dist/src/skipLink/index.d.ts +2 -0
- package/dist/src/skipLink/index.d.ts.map +1 -0
- package/dist/src/skipLink/index.js +1 -0
- package/dist/src/skipLink/skipLink.d.ts +7 -0
- package/dist/src/skipLink/skipLink.d.ts.map +1 -0
- package/dist/src/skipLink/skipLink.js +6 -0
- package/dist/src/swatch/index.d.ts +2 -0
- package/dist/src/swatch/index.d.ts.map +1 -0
- package/dist/src/swatch/index.js +1 -0
- package/dist/src/swatch/swatch.d.ts +4 -0
- package/dist/src/swatch/swatch.d.ts.map +1 -0
- package/dist/src/swatch/swatch.js +3 -0
- package/dist/src/table/index.d.ts +3 -0
- package/dist/src/table/index.d.ts.map +1 -0
- package/dist/src/table/index.js +1 -0
- package/dist/src/table/table.d.ts +15 -0
- package/dist/src/table/table.d.ts.map +1 -0
- package/dist/src/table/table.js +23 -0
- package/highlight/highlight.ts +128 -0
- package/highlight/vite.ts +42 -0
- package/package.json +75 -3
- package/src/code/code.ts +23 -0
- package/src/code/index.ts +2 -0
- package/src/code/lines.d.ts +39 -0
- package/src/codeBlock/codeBlock.ts +83 -0
- package/src/codeBlock/index.ts +2 -0
- package/src/copy/copy.ts +106 -0
- package/src/copyTab/copyTab.ts +61 -0
- package/src/copyTab/index.ts +2 -0
- package/src/diagram/diagram.ts +43 -0
- package/src/diagram/index.ts +2 -0
- package/src/footer/footer.ts +11 -0
- package/src/footer/index.ts +2 -0
- package/src/footnotes/footnotes.ts +38 -0
- package/src/footnotes/index.ts +2 -0
- package/src/header/header.ts +24 -0
- package/src/header/index.ts +2 -0
- package/src/heading/heading.ts +21 -0
- package/src/heading/index.ts +2 -0
- package/src/index.ts +20 -0
- package/src/navLink/index.ts +2 -0
- package/src/navLink/navLink.ts +18 -0
- package/src/note/index.ts +2 -0
- package/src/note/note.ts +19 -0
- package/src/pager/index.ts +2 -0
- package/src/pager/pager.ts +30 -0
- package/src/postList/index.ts +2 -0
- package/src/postList/postList.ts +19 -0
- package/src/postMeta/index.ts +2 -0
- package/src/postMeta/postMeta.ts +23 -0
- package/src/scheme/index.ts +3 -0
- package/src/scheme/scheme.ts +25 -0
- package/src/scheme/schemeSwitch.ts +43 -0
- package/src/scheme/script.ts +10 -0
- package/src/scheme/switch.ts +99 -0
- package/src/scrolls/index.ts +1 -0
- package/src/scrolls/scrolls.ts +56 -0
- package/src/simple-rat.css +601 -0
- package/src/skipLink/index.ts +1 -0
- package/src/skipLink/skipLink.ts +8 -0
- package/src/swatch/index.ts +1 -0
- package/src/swatch/swatch.ts +5 -0
- package/src/table/index.ts +2 -0
- package/src/table/table.ts +50 -0
- package/src/tokens.css +67 -0
package/DESIGN.md
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: simple rat
|
|
3
|
+
description: A plain page design system for references, docs and blogs. One monospace font, one 80ch column, a warm grey page, underlined blue links, the browser's own lists and tables; tokens, one stylesheet and Foldkit components.
|
|
4
|
+
colors:
|
|
5
|
+
ground: "#e8e7e3"
|
|
6
|
+
ground-dark: "#080807"
|
|
7
|
+
ink: "#34322b"
|
|
8
|
+
ink-dark: "#e3ded5"
|
|
9
|
+
muted: "#5f5b53"
|
|
10
|
+
muted-dark: "#958f85"
|
|
11
|
+
rule: "#b7b5ad"
|
|
12
|
+
rule-dark: "#3a3834"
|
|
13
|
+
link: "#1f3fc4"
|
|
14
|
+
link-dark: "#9db8ff"
|
|
15
|
+
visited: "#5a2e91"
|
|
16
|
+
visited-dark: "#c9a9f2"
|
|
17
|
+
code-fill: "#dad8d1"
|
|
18
|
+
code-fill-dark: "#1a1917"
|
|
19
|
+
code-rule: "#a9a69d"
|
|
20
|
+
code-rule-dark: "#44423d"
|
|
21
|
+
code-ink: "#3d3a34"
|
|
22
|
+
code-ink-dark: "#d4cec4"
|
|
23
|
+
mark: "#eed596"
|
|
24
|
+
mark-dark: "#3b3222"
|
|
25
|
+
red: "#b0342d"
|
|
26
|
+
red-dark: "#f2645b"
|
|
27
|
+
t-kw: "#7832d2"
|
|
28
|
+
t-kw-dark: "#cba6f7"
|
|
29
|
+
t-str: "#2a6a1c"
|
|
30
|
+
t-str-dark: "#a6e3a1"
|
|
31
|
+
t-num: "#9d3e07"
|
|
32
|
+
t-num-dark: "#fab387"
|
|
33
|
+
t-ty: "#815211"
|
|
34
|
+
t-ty-dark: "#f9e2af"
|
|
35
|
+
t-fn: "#1955cb"
|
|
36
|
+
t-fn-dark: "#89b4fa"
|
|
37
|
+
t-prop: "#10666b"
|
|
38
|
+
t-prop-dark: "#94e2d5"
|
|
39
|
+
t-op: "#026389"
|
|
40
|
+
t-op-dark: "#89dceb"
|
|
41
|
+
t-com: "#595b6a"
|
|
42
|
+
t-com-dark: "#9399b2"
|
|
43
|
+
t-del: "#b90d32"
|
|
44
|
+
t-del-dark: "#f38ba8"
|
|
45
|
+
typography:
|
|
46
|
+
display:
|
|
47
|
+
fontFamily: '"Berkeley Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace'
|
|
48
|
+
fontSize: "2em"
|
|
49
|
+
fontWeight: 700
|
|
50
|
+
lineHeight: 1.25
|
|
51
|
+
headline:
|
|
52
|
+
fontFamily: '"Berkeley Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace'
|
|
53
|
+
fontSize: "1.5em"
|
|
54
|
+
fontWeight: 700
|
|
55
|
+
lineHeight: 1.25
|
|
56
|
+
title:
|
|
57
|
+
fontFamily: '"Berkeley Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace'
|
|
58
|
+
fontSize: "1.17em"
|
|
59
|
+
fontWeight: 700
|
|
60
|
+
lineHeight: 1.25
|
|
61
|
+
body:
|
|
62
|
+
fontFamily: '"Berkeley Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace'
|
|
63
|
+
fontSize: "100%"
|
|
64
|
+
fontWeight: 400
|
|
65
|
+
lineHeight: 1.5
|
|
66
|
+
code-block:
|
|
67
|
+
fontFamily: '"Berkeley Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace'
|
|
68
|
+
fontSize: "0.875em"
|
|
69
|
+
fontWeight: 400
|
|
70
|
+
lineHeight: 1.25
|
|
71
|
+
diagram:
|
|
72
|
+
fontFamily: '"Berkeley Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace'
|
|
73
|
+
fontSize: "0.875em"
|
|
74
|
+
fontWeight: 400
|
|
75
|
+
lineHeight: 1.15
|
|
76
|
+
spacing:
|
|
77
|
+
hair: "0.25rem"
|
|
78
|
+
sm: "0.5rem"
|
|
79
|
+
md: "0.75rem"
|
|
80
|
+
base: "1rem"
|
|
81
|
+
lg: "1.5rem"
|
|
82
|
+
xl: "2.75rem"
|
|
83
|
+
footer: "3rem"
|
|
84
|
+
indent: "4ch"
|
|
85
|
+
column: "80ch"
|
|
86
|
+
components:
|
|
87
|
+
code-inline:
|
|
88
|
+
backgroundColor: "{colors.code-fill}"
|
|
89
|
+
textColor: "{colors.ink}"
|
|
90
|
+
padding: "0.1em 0.25em"
|
|
91
|
+
code-block:
|
|
92
|
+
backgroundColor: "{colors.code-fill}"
|
|
93
|
+
textColor: "{colors.code-ink}"
|
|
94
|
+
typography: "{typography.code-block}"
|
|
95
|
+
padding: "0.75rem 1rem"
|
|
96
|
+
code-tab:
|
|
97
|
+
backgroundColor: "{colors.code-fill}"
|
|
98
|
+
textColor: "{colors.muted}"
|
|
99
|
+
padding: "0 1ch"
|
|
100
|
+
copy-tab:
|
|
101
|
+
backgroundColor: "{colors.code-fill}"
|
|
102
|
+
textColor: "{colors.link}"
|
|
103
|
+
padding: "0 1ch"
|
|
104
|
+
prose-block:
|
|
105
|
+
backgroundColor: "{colors.code-fill}"
|
|
106
|
+
textColor: "{colors.ink}"
|
|
107
|
+
typography: "{typography.body}"
|
|
108
|
+
padding: "0.75rem 1rem"
|
|
109
|
+
scheme-switch:
|
|
110
|
+
textColor: "{colors.link}"
|
|
111
|
+
skip-link:
|
|
112
|
+
backgroundColor: "{colors.ground}"
|
|
113
|
+
textColor: "{colors.link}"
|
|
114
|
+
padding: "0.25rem 1ch"
|
|
115
|
+
note:
|
|
116
|
+
textColor: "{colors.ink}"
|
|
117
|
+
padding: "0.75rem 1rem"
|
|
118
|
+
table-cell:
|
|
119
|
+
textColor: "{colors.ink}"
|
|
120
|
+
padding: "0.25rem 0.5rem"
|
|
121
|
+
kbd:
|
|
122
|
+
textColor: "{colors.ink}"
|
|
123
|
+
padding: "0 0.25ch"
|
|
124
|
+
swatch:
|
|
125
|
+
width: "2ch"
|
|
126
|
+
height: "1em"
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
# Design System: simple rat
|
|
130
|
+
|
|
131
|
+
## Overview
|
|
132
|
+
|
|
133
|
+
**Creative North Star: "The Rendered Markdown File"**
|
|
134
|
+
|
|
135
|
+
simple rat is a page that reads like the Markdown it came from, made with more care than it lets on. Every word is set in one monospace, named Berkeley Mono first (the user's licensed font, which the package never ships; the specimen uses an installed copy) with the system monospace behind it, which is the design for anyone without a licence. Text sits at the reader's own default size, inside one 80-character column on a warm grey page (warm near-black in dark). Links are blue and underlined, purple once visited (nav links excepted), lists have bullets, the browser draws the disclosure triangles and the scrollbars. Code sits on a warm fill a clear step off the ground behind a 1px border, coloured with Catppuccin syntax (Latte in light, Mocha in dark), diagrams are drawn in box-drawing characters, and a footer is an `<hr>` and one line. The content is the interface; nothing on the page is chrome.
|
|
136
|
+
|
|
137
|
+
It ships as a package with three layers on one set of values: `tokens.css` holds every colour, type, space and motion value; `simple-rat.css` styles plain elements and the few classes the design names, using the tokens only; the Foldkit components write exactly the markup the stylesheet styles and add the behaviour a stylesheet cannot. The stylesheet is the design: a page written in plain HTML against it looks the same as one built from the components.
|
|
138
|
+
|
|
139
|
+
The look is a faithful port of [ratstack.sh](https://ratstack.sh/), pinned by the brief and not open for redesign. Everything the source does is kept as the source does it, except the page colours, which the user moved to anoma.ly's warm neutral, and the font, which is the user's licensed Berkeley Mono where licensed. What simple rat adds sits in two clearly separate groups: **extensions** (elements the source does not have: file tabs, notes and warnings, colour swatches, boxed keys, the copy tab, heading self-links, the `:target` wash, the scheme switch, the skip link, prose blocks) and **refinements** (precision applied to source behaviour: AA-darkened Latte accents, warm page colours in both modes, a muted token for metadata, a symmetric figure inset, diagram leading, underline offset, a focus ring). Refinement adds precision, never decoration.
|
|
140
|
+
|
|
141
|
+
The world refuses the design-system showcase (swatch grids, component cards, a hero) and the developer-docs chrome (sidebars, search boxes, icon callouts); its own specimen is a set of plain pages, not a catalogue. There is no title mark before the h1: the user dropped the source's emoji slot. Nothing from Rat Stack carries over except the look and one credit line ("The look comes from ratstack.sh.").
|
|
142
|
+
|
|
143
|
+
**From ratstack.sh (kept as the source does it):**
|
|
144
|
+
|
|
145
|
+
- One monospace family for every word at the reader's default size (100%) with 1.5 leading.
|
|
146
|
+
- One 80ch body column with 1rem padding, centred.
|
|
147
|
+
- Lowercase text nav separated by middots, over an `<hr>`.
|
|
148
|
+
- Blue underlined links.
|
|
149
|
+
- Code and pre on a fill with a 1px border; pre set a step under the text at 1.25 leading; Catppuccin Latte syntax colours.
|
|
150
|
+
- ASCII box-drawing diagrams in a `figure`, labelled as an image with `role="img"` and an `aria-label`, with a `figcaption`.
|
|
151
|
+
- 1px currentColor table rules; under 40rem, stacked tables become labelled cards.
|
|
152
|
+
- Footer = `<hr>` + one line.
|
|
153
|
+
|
|
154
|
+
**simple rat extensions:**
|
|
155
|
+
|
|
156
|
+
- File tabs (`figure.file`): a file name tab on a code block's top-left edge.
|
|
157
|
+
- Notes (`aside.note`) and warnings (`aside.note.warn`).
|
|
158
|
+
- Colour swatches (`.swatch`) inline in table cells.
|
|
159
|
+
- Boxed `kbd`.
|
|
160
|
+
- The `copy` tab on a copyable block, announced to screen readers.
|
|
161
|
+
- Prose blocks: a copyable block with no language (a prompt) wraps at text size.
|
|
162
|
+
- Heading self-links (`#`), shown on hover and focus, named "Link to <heading>".
|
|
163
|
+
- A "Skip to content" link, visible only on keyboard focus.
|
|
164
|
+
- The `:target` yellow wash, the only motion; reduced motion gets a dashed outline.
|
|
165
|
+
- The scheme switch at the end of the nav: `theme: system/light/dark`, the current choice in plain text, the others as underlined words.
|
|
166
|
+
|
|
167
|
+
**simple rat refinements:**
|
|
168
|
+
|
|
169
|
+
- Latte accents darkened where needed to pass AA on the code fill (re-fitted to the stronger fill).
|
|
170
|
+
- Page colours from [anoma.ly's notes](https://anoma.ly/notes/opencode-reloaded/) (user's choice): its warm grey ground in light and warm near-black in dark, with text one step brighter than that site's prose; muted, rules, links, visited, code fill, rule and ink, the mark wash and red all tuned to that warm neutral. Catppuccin stays in the syntax colours only (Latte, AA-darkened, in light; Mocha in dark). Both palettes via `light-dark()`; the browser baseline (evergreen, about two years) needs no fallback.
|
|
171
|
+
- Berkeley Mono ([U.S. Graphics](https://usgraphics.com/products/berkeley-mono), TX-02 v2.004) named first (user's choice): the system SF Mono read too thick, Commit Mono was tried, then the user chose their licensed Berkeley Mono. Text antialiased on macOS so strokes stay thin; contextual alternates off, so text shows as typed.
|
|
172
|
+
- A muted token, used only for dates, captions, file names and sample labels.
|
|
173
|
+
- Figures inset symmetrically by 4ch on both sides.
|
|
174
|
+
- Code blocks at 0.875em (14px at a 16px default; user's choice, 14.5px read slightly too big).
|
|
175
|
+
- Diagram leading 1.15: Berkeley Mono's box glyphs are exactly 1.2em tall, so 1.2 opened hairline gaps where rows rounded to pixels; `role="img"` and the `aria-label` sit on the diagram's `pre`, not the figure, so a diagram too wide for the screen can take focus and scroll.
|
|
176
|
+
- Link underline offset 0.2em, 1px thick, 2px on hover.
|
|
177
|
+
- A 2px link-colour focus ring, offset 2px.
|
|
178
|
+
- Nav links never show visited purple; the current page is plain text, the current section plain but underlined.
|
|
179
|
+
- Coarse-pointer touch sizing on nav links, footnote numbers, the copy tab and the scheme switch, none of which moves the line it sits on; code blocks with a top-edge tab get 3.25rem of room above them so the taller tab never overlaps the text before it.
|
|
180
|
+
- On a phone (≤40rem), a copyable block outside a file figure wraps instead of hiding its end.
|
|
181
|
+
|
|
182
|
+
**Key Characteristics:**
|
|
183
|
+
|
|
184
|
+
- One font family, one column, browser-native affordances on a warm neutral page.
|
|
185
|
+
- Flat: no shadow, no radius, no gradient anywhere.
|
|
186
|
+
- Every rule is 1px; the one exception is the warning's 3px double border.
|
|
187
|
+
- Two palettes from one set of custom properties; the page follows the system unless `html[data-scheme]` pins it.
|
|
188
|
+
- Tokens, one stylesheet on the tokens, and components that only write its markup; without a Berkeley Mono licence the system monospace is the design.
|
|
189
|
+
|
|
190
|
+
## Colors
|
|
191
|
+
|
|
192
|
+
A warm neutral page taken from anoma.ly's notes, blue underlined links, and Catppuccin only in the syntax colours inside code. Light is a warm grey ground with dark warm ink; dark is a warm near-black with warm off-white ink. Each token in `tokens.css` holds both values through `light-dark()`; the `-dark` keys in the frontmatter are the second value of the same custom property, not separate tokens.
|
|
193
|
+
|
|
194
|
+
### Primary
|
|
195
|
+
|
|
196
|
+
- **Softened Link Blue / Dark Link Blue** (`link`): every link, the `copy` tab, the scheme switch, the skip link, and the focus ring. Still unmistakably link blue, softened from pure browser blue to sit on the warm ground; a light periwinkle in dark.
|
|
197
|
+
- **Visited Purple / Visited Lilac** (`visited`): visited links in the text only. Kept because the page keeps the browser's history signal; links inside a `nav` stay link blue, because a nav is a map, not a reading history.
|
|
198
|
+
|
|
199
|
+
### Neutral
|
|
200
|
+
|
|
201
|
+
- **Warm Grey Page / Warm Near-Black Page** (`ground`): the page background, anoma.ly's own ground in both modes, and the skip link's backing when it shows.
|
|
202
|
+
- **Warm Ink / Warm Off-White Ink** (`ink`): all body text, headings, table rules (as currentColor), note borders, kbd borders, swatch outlines. One step brighter than anoma.ly's prose colour in each mode.
|
|
203
|
+
- **Warm Stone / Dark Warm Stone** (`muted`): dates, figure captions, file-name tabs, sample labels (`.muted`), post meta lines, the heading `#`. Metadata only.
|
|
204
|
+
- **Warm Rule Grey / Dark Warm Rule** (`rule`): the `<hr>`, blockquote bar, page scrollbar thumb, and the reduced-motion target outline.
|
|
205
|
+
- **Warm Code Fill / Dark Warm Code Fill** (`code-fill`): the fill behind inline code, code blocks, file tabs and the copy tab. A clear step darker than the ground in light, a clear step lighter in dark, so a code block reads as a surface at a glance.
|
|
206
|
+
- **Warm Code Rule / Dark Warm Code Rule** (`code-rule`): the 1px border on code and tabs, the text selection fill, and the code scrollbar thumb.
|
|
207
|
+
- **Warm Code Ink / Dark Warm Code Ink** (`code-ink`): default text inside highlighted code blocks.
|
|
208
|
+
|
|
209
|
+
### Tertiary
|
|
210
|
+
|
|
211
|
+
- **Warm Yellow Wash / Dark Umber Wash** (`mark`): `<mark>` highlighting and the start colour of the `:target` wash.
|
|
212
|
+
- **Warning Brick Red / Dark Warning Red** (`red`): the bold first word of a warning. Nowhere else outside code.
|
|
213
|
+
- **Syntax colours** (`t-kw`, `t-str`, `t-num`, `t-ty`, `t-fn`, `t-prop`, `t-op`, `t-com`, `t-del`): Catppuccin Latte in light, every one darkened until it passes AA on the warm code fill (lowest: function blue at 4.6:1); Catppuccin Mocha, unchanged, in dark. Comments and shell command words are also italic. Diff insertions share the string green; deletions take the syntax red (`t-del`), not the page red.
|
|
214
|
+
|
|
215
|
+
### Named Rules
|
|
216
|
+
|
|
217
|
+
**The Warm Page Rule.** The page colours are anoma.ly's warm neutral: warm grey ground and dark warm ink in light, warm near-black ground and warm off-white ink in dark, with every other neutral tuned to the same warm hue. Links stay blue, underlined, and purple when visited. Catppuccin lives only in the syntax colours inside code. Don't cool the ground, swap link blue for a brand hue, or drop the underline.
|
|
218
|
+
|
|
219
|
+
**The Muted-Is-Metadata Rule.** The muted token is for dates, captions, file names, sample labels and the heading `#`. Body prose, headings and table text are never muted.
|
|
220
|
+
|
|
221
|
+
**The Earned AA Rule.** Every colour that carries text passes AA on its own fill in both palettes, syntax colours included. A new Latte accent is darkened until it passes on the code fill before it ships; if the code fill changes, every light syntax colour is re-fitted to it.
|
|
222
|
+
|
|
223
|
+
## Typography
|
|
224
|
+
|
|
225
|
+
**Display Font:** none separate; headings use the body stack.
|
|
226
|
+
**Body Font:** Berkeley Mono, where licensed (with ui-monospace, SFMono-Regular, Menlo, Consolas, monospace, which is the design without a licence)
|
|
227
|
+
**Label/Mono Font:** the same family; there is only one.
|
|
228
|
+
|
|
229
|
+
**Character:** One neutral monospace for every word, shown exactly as typed, at whatever size the reader set. Hierarchy comes from the browser's own heading sizes and bold, nothing else.
|
|
230
|
+
|
|
231
|
+
**Font files.** The stack lives in the `--font` token. Berkeley Mono ([U.S. Graphics](https://usgraphics.com/products/berkeley-mono), TX-02 v2.004) is licensed to the user, so the package declares no `@font-face` and ships no font. A site with its own licence serves the four unmodified vendor faces under the family name `"Berkeley Mono"`: `BerkeleyMono-Regular` (400 normal), `BerkeleyMono-Oblique` (400, declared `font-style: italic`), `BerkeleyMono-Bold` (700) and `BerkeleyMono-Bold-Oblique` (700 italic), about 30 KB each, `font-display: swap`; it may preload the regular face. The specimen serves no font files: `site/fonts.css` declares the four faces from a copy installed on the machine (`local()`, by PostScript name, the static faces or the variable font), so the font is never in the repository and nothing is requested; without an installed copy the page uses the system monospace. A site without a licence does nothing, and the system monospace is the design. On macOS and Linux it has Berkeley Mono's 0.6em advance; on Windows, Consolas is narrower (0.55em), so there the column widens about 9% when Berkeley Mono arrives. Lines never re-wrap: the column is measured in `ch`. The page is antialiased on macOS (`-webkit-font-smoothing: antialiased`, `-moz-osx-font-smoothing: grayscale`) and contextual alternates are off (`font-variant-ligatures: no-contextual`) so `=>` never turns into an arrow and `...` never merges.
|
|
232
|
+
|
|
233
|
+
### Hierarchy
|
|
234
|
+
|
|
235
|
+
- **Display** (bold, 2em, 1.25): the page h1, once per page. Balanced wrapping.
|
|
236
|
+
- **Headline** (bold, 1.5em, 1.25): h2 sections, with 2.75rem above and 1rem below.
|
|
237
|
+
- **Title** (bold, 1.17em, 1.25): h3 subsections, 2rem above and 0.75rem below.
|
|
238
|
+
- **Body** (normal, 100% of the reader's default, 1.5): prose, lists, tables, nav, footer. The line is capped at 80ch by the column.
|
|
239
|
+
- **Code block** (normal, 0.875em, 1.25): every `pre`; 14px at a 16px default, a step under the text so code and prose read as two voices. Measured capacity: a little over 80 characters in a full-width block on desktop (83), 74 in an indented figure, 38 on a 390px phone. Tab size 2.
|
|
240
|
+
- **Prose block** (normal, 1em of the body, 1.5): a copyable block whose code has no language, such as an agent prompt; it wraps (`pre-wrap`) and reads like prose on the code fill.
|
|
241
|
+
- **Diagram** (normal, 0.875em, 1.15): `pre[role="img"]`. Keep a diagram within 70 characters to fit an indented figure on a wide screen, within 36 to fit a phone (36 fits a 375px phone). Wider ones scroll. Draw in light box drawing, double for at most one box, with ▶ (U+25B6), ▲ and ▼ as arrowheads. Berkeley Mono has light and double lines and those arrowheads, but no heavy lines (━ ┃ ┏ …), no rounded corners (╭ ╮ ╯ ╰) and no ► (U+25BA).
|
|
242
|
+
- Inline `code`, `kbd` and `small` inherit the body size; `sup` is 0.75em.
|
|
243
|
+
|
|
244
|
+
### Named Rules
|
|
245
|
+
|
|
246
|
+
**The One Stack Rule.** Every word is Berkeley Mono where it is licensed, with the system monospace stack (ui-monospace, SFMono-Regular, Menlo, Consolas, monospace) behind it, the same width on macOS and Linux and a little narrower on Windows. One family: no second family, no display face.
|
|
247
|
+
|
|
248
|
+
**The Reader's Size Rule.** The root is 100%; all sizes are em or rem from there. Never pin the root in px.
|
|
249
|
+
|
|
250
|
+
**The Three Leadings Rule.** Prose (and prose blocks) 1.5, code and headings 1.25, diagrams 1.15 (`--leading`, `--leading-tight`, `--leading-diagram`). Nothing else.
|
|
251
|
+
|
|
252
|
+
**The Own Licence Rule.** Berkeley Mono is licensed to the user and is never part of the package. Use it only with a licence of your own; otherwise the system monospace fallback is the design.
|
|
253
|
+
|
|
254
|
+
## Layout
|
|
255
|
+
|
|
256
|
+
One column, 80ch wide (`--column`), centred, with 1rem padding on every side and every viewport. There is no grid, sidebar or multi-column section; wide tables, code and diagrams scroll sideways inside the column instead of breaking it. Long words that would overflow break (`overflow-wrap: break-word`) in prose; table cells do not break words, and code in a table cell does not wrap.
|
|
257
|
+
|
|
258
|
+
Vertical rhythm is in rem, from the spacing tokens: 0.25rem between list items (`--space-hair`), 0.5rem around the `<hr>` and under the nav (`--space-sm`), 1rem between paragraphs and blocks (`--space-base`), 1.5rem around figures, tables and notes (`--space-lg`), 2.75rem above an h2 (`--space-xl`), 3rem above the footer (`--space-footer`). Horizontal insets are in ch: lists, definitions and blockquotes indent 4ch (`--indent`); figures inset 4ch on both sides (file figures sit flush). The post list is a two-track grid: a 10ch date track and the title, 2ch apart. A few single-use geometries stay literal in the stylesheet: 2.25rem of room above a tabbed block (3.25rem on coarse pointers), 2.5rem above the pager and footnotes, 0.625rem touch padding.
|
|
259
|
+
|
|
260
|
+
At 40rem and below: stacked tables (cells carrying `data-label`) drop their header and stack each row into a 1px-bordered card whose cells are prefixed by their bold column label and break long words anywhere; tables without labels stay tables and scroll sideways; figures lose their inset; a copyable block outside a file figure wraps (`pre-wrap`, `overflow-wrap: anywhere`) so a one-line command keeps its end in view. On coarse pointers, nav links and footnote numbers get `0.625rem 0.5ch` of padding taken back by `-0.5ch` inline margin, the copy tab and scheme switch grow 0.625rem of padding top and bottom (the switch takes it back in negative margin, so the nav line does not move).
|
|
261
|
+
|
|
262
|
+
**The One Column Rule.** Every page is the same 80ch column. Anything wider scrolls inside it.
|
|
263
|
+
|
|
264
|
+
## Elevation & Depth
|
|
265
|
+
|
|
266
|
+
Flat. There are no shadows, no gradients and no layering. Depth is conveyed only by the code fill (a warm surface one step off the ground: darker in light, lighter in dark) and 1px rules. The one moment of change is the `:target` wash: a followed anchor, footnote or back-reference washes yellow and fades to transparent (`--wash`: 1.6s, `cubic-bezier(0.16, 1, 0.3, 1)`, 0.1s delay); under reduced motion it is a static 1px dashed muted outline instead.
|
|
267
|
+
|
|
268
|
+
**The Flat Paper Rule.** Nothing lifts. If an element needs separation, it gets a 1px rule or the code fill.
|
|
269
|
+
|
|
270
|
+
**The One Moment Rule.** The `:target` wash is the only motion. Links and controls change underline thickness on hover, instantly.
|
|
271
|
+
|
|
272
|
+
## Shapes
|
|
273
|
+
|
|
274
|
+
Square corners everywhere (no radius is declared). Borders are 1px solid: `currentColor` for tables, notes and kbd; `code-rule` for code and tabs; `rule` for the hr and blockquote bar. The warning is the one heavier line, a 3px double border. Tabs sit on a code block's top edge, open at the bottom, so tab and block read as one shape. Diagrams are drawn in box-drawing characters, not CSS boxes.
|
|
275
|
+
|
|
276
|
+
**The 1px Rule.** Every line is 1px. The warning's 3px double is the only exception.
|
|
277
|
+
|
|
278
|
+
## Components
|
|
279
|
+
|
|
280
|
+
The components are Foldkit: Views and Submodels over `foldkit`, each a namespace exported from `src/index.ts` and living in `src/<name>/`. Each renders exactly the markup below it and nothing the stylesheet does not already style; plain elements (paragraphs, lists, quotes, definitions, `kbd`, `mark`, `small`, `time`, `details`) have no component. A View is `X.view(config, h)`; a Submodel (`Scheme.SchemeSwitch`, `CopyTab`) has a Model, Message, `init`, `update` and a `view` the page embeds with `h.submodel`.
|
|
281
|
+
|
|
282
|
+
### Links and text controls
|
|
283
|
+
|
|
284
|
+
- **Links:** link blue, 1px underline at 0.2em offset; visited purple; hover thickens the underline to 2px. A link wrapping `code` takes the link colour.
|
|
285
|
+
- **Focus:** 2px solid link-colour outline, 2px offset, on links, buttons, summaries and scrollable `pre`.
|
|
286
|
+
- **`SkipLink`** (`a.skip`): "Skip to content" first in the body, held off-screen until focused, then shown at 1rem, 1rem on the ground with 0.25rem 1ch padding; lands on `main#main`. A Foldkit application hands a fragment on the page being read back to the browser with `load(href)` from `foldkit/navigation`, so `:target` and the skip land.
|
|
287
|
+
- **`.visually-hidden`** (class, no component): a 1px clipped box for words only assistive tech needs, such as the copy tab's status.
|
|
288
|
+
- **Text controls** (the `copy` tab, the scheme switch): buttons that look like links, link blue and underlined, with no fill (the copy tab keeps the code fill so it reads as a tab).
|
|
289
|
+
|
|
290
|
+
**The Text Control Rule.** Controls are words. No filled buttons, no icons, no pills.
|
|
291
|
+
|
|
292
|
+
### Navigation
|
|
293
|
+
|
|
294
|
+
- **`Header`** writes `header > nav[aria-label="Primary navigation"] + hr`. Its first item stands alone; each later item sits in a `span` with a middot and a space before it (`<span>· …</span>`, an `inline-block` at most the line wide), so a wrapped line starts with the middot and never ends on one; an item wider than the line breaks inside (`overflow-wrap: anywhere`) rather than widening the page.
|
|
295
|
+
- **`NavLink`** (`a`) is a lowercase link. `current: "Page"` writes `aria-current="page"`: plain ink, no underline. `current: "Section"` writes `aria-current="true"`: ink, underline kept (a post's blog link). Nav links never turn visited purple.
|
|
296
|
+
- **`Scheme.SchemeSwitch`** (a Submodel; `span.theme[role=group][aria-label=Theme]` of three `button[type=button][tabindex="0"][aria-pressed]`, each built on `@foldkit/ui`'s Button), last in the header: `theme: system/light/dark`, the words written in the view. The current choice is plain text in the ink colour; the other two are underlined link-blue words. System follows the reader's system through `color-scheme` alone, with no script. Light or dark pins `html[data-scheme]` (`light` or `dark`) and saves the same value in `localStorage` under `simple-rat-scheme`, through its own `PinScheme` Command (storage that refuses still switches the page; System takes both away); `Scheme.schemeScript`, inline in the head, applies a saved `light` or `dark` before first paint and ignores anything else, which falls back to System. The switch needs no service and no Subscription: it renders on a server as System, and its `init` returns a `ReadScheme` Command, which the browser runs after hydration (the server ignores it) to read the scheme the script pinned. A choice made in another tab is seen on reload, not live.
|
|
297
|
+
|
|
298
|
+
### Code
|
|
299
|
+
|
|
300
|
+
- **Inline:** code fill, 1px code-rule border, 0.1em 0.25em padding, body size.
|
|
301
|
+
- **`CodeBlock`** writes `pre > code`: code fill, 1px code-rule border, 0.75rem 1rem padding, 0.875em / 1.25, scrolls sideways. `lang` (one of `Code.LANGUAGES`: `ts`, `tsx`, `sh`, `json`, `css`, `html`, `diff`) sets `code.language-*`, which gives the block the code-ink. `lines` colours the code: spans whose inline colour is a `var(--t-*)` token, italic for comments and shell commands; the rest takes code-ink, and a block without `lines` is plain text. The lines are data, made by Shiki with the package's theme while the site is built, so a page carries the colours and no highlighter: an import with the query `?lines=<lang>` is a file's `{ code, lines }`, spread into the config (`codeLines()`, see Adopting the system). Only a block that actually overflows takes focus: `Scrolls.WatchOverflow`, a Mount on the `pre`, makes it `role="region"` named "Code: <file or first line>, scrolls sideways" with `tabindex="0"`, re-checked whenever the block changes size, and takes the attributes away when the block leaves the page; blocks that fit stay out of the tab order. The Mount reports through `toParentMessage`, which the page may ignore.
|
|
302
|
+
- **Prose block:** a block with a `copy` tab and no `lang`; same fill and border, body size, 1.5 leading, wrapping.
|
|
303
|
+
- **File tab:** a `file` wraps the block in `figure.file` with the name in its `figcaption`, a muted tab on the top-left edge. The tabs stand in a row of their own on the block's top edge: a long name wraps inside its tab, and the copy tab keeps its width beside it, so the two never overlap and the page never widens.
|
|
304
|
+
- **Copy tab:** `copy` is the page's **`CopyTab`** (a Submodel) rendered through `h.submodel` with `CodeBlock.copyInputs(code, file?)` and the block's `toBlockView`; it sets `pre[data-copy]` and wraps it in `div.copyable` with a `button[type=button][tabindex="0"]` after it, built on `@foldkit/ui`'s Button, and a visually hidden `role="status"`. The link-blue `copy` tab on the top-right answers `copied` (or `select and copy` when the clipboard refuses) and turns back after 1.6s; it takes a press only while it reads `copy`, so one answer ends before the next copy starts (from the press until it reads `copy` again, the button is held: `aria-disabled="true"` and `data-disabled`, with no click handler); its accessible name is "Copy <file name or first line>". It writes through Effect's `Clipboard` port: provide `Clipboard.layer` from `@effect/platform-browser` in the application's `resources`. Both tabs: code fill, 1px code-rule border with no bottom edge, 0 1ch padding, 1.5 leading; a block with only the copy tab gets 2.25rem of room above for it.
|
|
305
|
+
|
|
306
|
+
### Figures and diagrams
|
|
307
|
+
|
|
308
|
+
- **`Diagram`** writes `figure > pre[role="img"][aria-label] > code` then a muted `figcaption` 0.5rem below (the `caption`, defaulting to the `label`). The role sits on the `pre`, so an overflowing diagram can take focus and scroll (`Scrolls.WatchOverflow`, given no name, gives it `tabindex="0"` and leaves its image role alone). Inset 4ch each side; 0.875em, leading 1.15; at most 70 characters wide, 36 to fit a phone. Light lines, double for at most one box, arrowheads ▶ ▲ ▼; never a glyph the font lacks.
|
|
309
|
+
|
|
310
|
+
### Tables
|
|
311
|
+
|
|
312
|
+
- **`Table`** writes `div.table-scroll > table > thead + tbody`. The table keeps `display: table`; the wrapper (`overflow-x: auto`) scrolls a table wider than the column sideways, on a phone too. 1px currentColor cell rules, 0.25rem 0.5rem padding, left and top aligned. Headers, code in cells and swatch cells never wrap. **`isStacked: true`** (opt-in; off by default) also writes `data-label` on each cell from its column and the ARIA table roles on every part (`table`, `rowgroup`, `row`, `columnheader`, `cell`), because WebKit drops table semantics once CSS changes a table's display, which the stacked cards do: under 40rem each labelled row becomes a bordered card with bold `label:` prefixes. A table that is not stacked needs no roles.
|
|
313
|
+
- **`Swatch`** (`span.swatch`): a 2ch × 1em box with a 1px ink outline before a colour value in a cell.
|
|
314
|
+
|
|
315
|
+
### Notes and warnings
|
|
316
|
+
|
|
317
|
+
- **`Note`** (`aside.note > p > strong + …`): a 1px currentColor box, 0.75rem 1rem padding, the first word bold ("Note."). At most one per screen.
|
|
318
|
+
- **`isWarn`** (`aside.note.warn`): a 3px double border and a red first word ("Warning."). Only for real consequences.
|
|
319
|
+
|
|
320
|
+
### Footnotes
|
|
321
|
+
|
|
322
|
+
- **`Footnotes`**: `refView` writes `sup > a#ref-N[href="#fn-N"]`; `view` writes `section.footnotes[aria-label="Footnotes"] > hr + ol > li#fn-N`, each note ending in a `Back` link to its reference, named "Back to reference N" so each note's link says where it goes. The section sits 2.5rem below the text; both ends take the `:target` wash.
|
|
323
|
+
|
|
324
|
+
### Blog pieces
|
|
325
|
+
|
|
326
|
+
- **`PostList`** (`ul.posts > li > time + a`): a two-track grid, a 10ch date track and the title, 2ch apart, grouped under year headings.
|
|
327
|
+
- **`PostMeta`** (`p.meta`): everything about a post on one muted line, pulled 0.5rem up under the h1: the `time`, then " · " and the rest, e.g. "5 min read · sample post, fictional date".
|
|
328
|
+
- **`Pager`** (`nav.pager`): Newer, Older and the way back to all posts, one line each, 2.5rem below; a missing neighbour becomes a muted "This is the newest post." or "This is the oldest post."
|
|
329
|
+
|
|
330
|
+
### Small pieces
|
|
331
|
+
|
|
332
|
+
- **kbd** (extension): a 1px currentColor box, 0 0.25ch padding, the body font.
|
|
333
|
+
- **Blockquote** (source): 1px rule bar on the left, 4ch indent.
|
|
334
|
+
- **`Heading`** (extension; `h2`/`h3 > a.self`): writes the heading (level 2 or 3) with a muted `a.self` `#` after it, visible on hover and focus only, named "Link to <text>". The page title is a plain `h1`.
|
|
335
|
+
- **`Footer`** (source): `footer > hr + p`, one line, ending with the credit "The look comes from ratstack.sh."
|
|
336
|
+
- **Not found:** a plain page like the rest; the host must serve it with a 404 status for every unknown address. The specimen writes it at `/404`, which is no route of its own: it parses to the `NotFound` fallback like any address nothing lives at.
|
|
337
|
+
|
|
338
|
+
### Adopting the system
|
|
339
|
+
|
|
340
|
+
Install the package (README.md beside this file says how), install its peers (`foldkit`, `@foldkit/ui`, `effect`, `@effect/platform-browser`, and `@foldkit/vite-plugin` for `./highlight/vite`) and import the stylesheet once. The package is compiled JavaScript with its types and its views already branded, so the site's build needs only `foldkit()` beside what it imports:
|
|
341
|
+
|
|
342
|
+
- `.`: every component above as a namespace, `Code` (`LANGUAGES` and the types of a block's coloured lines), `Scheme` (`Scheme`, `SchemeSwitch`, `schemeScript`) and `Scrolls`. Nothing it reaches imports Shiki. Every component renders on a server.
|
|
343
|
+
- `./highlight`: `highlight(code, lang)`, which colours code with Shiki and the package's theme (its scopes mapped to the `--t-*` tokens) and returns the lines `CodeBlock` takes. It is for a build, never a page: importing it builds the highlighter. Shiki comes with the package.
|
|
344
|
+
- `./highlight/vite`: `codeLines()`, a Vite plugin that runs `highlight` while the site is built; add it to the build's `plugins`, beside `foldkit()`. An import with the query `?lines=<lang>`, as in `import retry from "./code/retry.txt?lines=ts"`, is the file's text and its lines, `{ code, lines }`, and the file is watched, so a change colours it again. Code the toolchain should not check or format goes in a `.txt`; a source file can be imported twice, to run and to show. The imports are typed by `src/code/lines.d.ts`, an ambient module declaration for each language, which `src/code/code.ts` references: a program that imports `simple-rat` has them, with nothing to add to its `tsconfig`. A build without Vite calls `highlight` itself and passes the lines on.
|
|
345
|
+
- `./scheme`: `schemeScript`, `SCHEME_STORAGE_KEY` and `SCHEME_ATTRIBUTE` alone, for a build that writes `schemeScript` into a page's head.
|
|
346
|
+
- `./tokens.css`: the tokens alone.
|
|
347
|
+
- `./simple-rat.css`: the stylesheet; it imports `tokens.css` from beside it.
|
|
348
|
+
|
|
349
|
+
Without Foldkit, link `simple-rat.css` (with `tokens.css` beside it) and write the markup each component above names: the page frame is `body > a.skip + header + main#main + footer`; for code colours write a `span` for each coloured run, as the code blocks on the reference do (the colours name the `--t-*` tokens, so they follow the page's scheme); wrap each table in `div.table-scroll` so a wide one scrolls; only a table that should stack needs `data-label` cells and the ARIA table roles (`role="table"`, `rowgroup`, `row`, `columnheader`, `cell`), and a table that stays a table needs neither; put `schemeScript` inline in the head and build the switch from the Navigation contract, or leave dark mode to the system. Behaviour that needs script (copy tabs, the scheme switch, focus on scrolling blocks) is the components' job; a plain page reads the same without it.
|
|
350
|
+
|
|
351
|
+
## Building components
|
|
352
|
+
|
|
353
|
+
How a component in this package is made. The components follow these rules, and a new one follows them too; a review finding against one of them is a defect, not a style note.
|
|
354
|
+
|
|
355
|
+
**The two kinds.** A **View** is a stateless render helper, `X.view(config, h)`: the caller owns any state and passes its Messages in the config. Copy `src/note/` or `src/footer/`; a View that owns a Mount (`src/codeBlock/`, `src/diagram/`) takes a required `toParentMessage`. A **Submodel** has a Model, Message, `init`, `update` and a `view` made with `defineView`, embedded with `h.submodel`. Copy `src/copyTab/` (a Command through a platform port; `src/copy/copy.ts` holds the Model, Messages, Commands and update, the same file in every system, and `copyTab.ts` adds this system's view) or `src/scheme/` (Commands that touch the document directly and a Command from `init`; `switch.ts` holds the Model, Messages, Commands and update, the same file in every system, and `schemeSwitch.ts` adds this system's view). A browser service that Effect already ports (`Clipboard`) is required from the application; what the component can do itself in a Command (an attribute on `<html>`, a `localStorage` entry) it does there, with no service.
|
|
356
|
+
|
|
357
|
+
Each component is `src/<name>/<name>.ts` with an explicit `index.ts` (its public surface, which only `src/index.ts` imports; a sibling or a test imports the file that defines what it needs), a colocated `<name>.test.ts` (a Story for update, a Scene for view), an `export * as` line in `src/index.ts`, a place on the specimen (the reference page, or for the blog pieces the blog and post pages), and an entry in this document.
|
|
358
|
+
|
|
359
|
+
1. **Markup is the contract.** A component writes exactly the markup the Components section names, with the classes, roles and ids a page without Foldkit writes by hand; the stylesheet styles that markup and nothing else.
|
|
360
|
+
2. **One vocabulary.** `label` is a control's or a link's words (`NavLink`'s, each of `Pager`'s links) or a group's name; `text` is a heading's words; `body` is content, `ReadonlyArray<Html | string>`. Booleans read as questions (`isWarn`, `isStacked`); an `Option` is `maybe*` (`Scrolls.WatchOverflow`'s `maybeName`); literal choices are capitalised in the domain (`"System"`, `"Light"`, `"Dark"`; `NavLink`'s `"Page"`, `"Section"`) and shown in the page's own words, written in the view (`system`, `light`, `dark`).
|
|
361
|
+
3. **Slots.** A content slot in a View's config is a value. In a Submodel's view inputs, markup that carries the page's handlers or Mounts is a top-level function, `toXView: () => Html` (`CopyTab`'s `toBlockView`), never nested Html, because a Submodel's view runs in its own Message boundary.
|
|
362
|
+
4. **Parents never read a child's Model.** A Submodel exports accessors for what a parent needs (`CopyTab.id`, `Scheme.SchemeSwitch.id`), and the parent calls them, including for `slotId`.
|
|
363
|
+
5. **Instances are scoped by id.** A Submodel's `slotId` is its own id from `init`.
|
|
364
|
+
6. **A Mount owns what it writes.** A Mount that adds attributes (`Scrolls.WatchOverflow`'s `tabindex`, `role`, `aria-label`) takes them away in its release, and reports that it ran (`CompletedWatchOverflow`, named for the Mount) through the parent's `toParentMessage`; in a Scene, let the test's parent take the Mount's Message as its own (an identity `toParentMessage`) and resolve the Mount by its definition.
|
|
365
|
+
7. **Platform services are the application's.** A component that needs one says so in its TSDoc and here (`CopyTab` writes through Effect's `Clipboard`), and the application provides the Layer in its `resources`; `SchemeSwitch` needs none.
|
|
366
|
+
8. **A Scene test for every accessibility contract** a component adds: roles, accessible names, `aria-pressed`, `aria-current`, `data-label`, the status region. Behaviour that only a real element shows (the overflow Mount) gets a test with real DOM in happy-dom, no mocks.
|
|
367
|
+
9. **Fragment links go back to the browser.** A link to `#id` (the skip link, footnotes, heading links) is routed by Foldkit, so the application hands a fragment on the page being read back to the browser with `load(href)` from `foldkit/navigation`, which marks `:target` and moves the keyboard.
|
|
368
|
+
10. **No module-level state and no clocks in views.** Build-time code lives in `highlight/`, beside `src/` and never inside it: Shiki's highlighter, built once as a module constant (`highlight/highlight.ts`), and the Vite plugin that reads files (`highlight/vite.ts`), published as `./highlight` and `./highlight/vite`. The folder is the boundary: `highlight/` may import from `src/`, and nothing in `src/` imports from `highlight/`. Everything that changes lives in a Model or inside a Mount's scope.
|
|
369
|
+
|
|
370
|
+
## Do's and Don'ts
|
|
371
|
+
|
|
372
|
+
### Do:
|
|
373
|
+
|
|
374
|
+
- **Do** adopt by installing the package and importing the `./simple-rat.css` export once; write plain semantic HTML and reach for a component only where the design names one.
|
|
375
|
+
- **Do** put `schemeScript` inline in the head of every page, before the stylesheet, when the site has a `SchemeSwitch`.
|
|
376
|
+
- **Do** keep every word in one monospace (Berkeley Mono where licensed, the system monospace otherwise) at the reader's default size, 1.5 leading, in one 80ch column with 1rem padding.
|
|
377
|
+
- **Do** give every diagram a `label` and a visible caption (`Diagram` does both).
|
|
378
|
+
- **Do** keep diagrams within 70 characters for a wide screen and within 36 to fit a phone (36 fits 375px).
|
|
379
|
+
- **Do** draw diagrams in light box drawing, double for at most one box, with ▶ ▲ ▼ arrowheads.
|
|
380
|
+
- **Do** keep sample content labelled on the page and in source; a sample post says so at the end of its meta line ("sample post, fictional date"), and a sample index says so once in a muted line.
|
|
381
|
+
- **Do** darken any new light-mode syntax accent until it passes AA on the code fill.
|
|
382
|
+
- **Do** change values in `tokens.css` only; the stylesheet and components read them from there.
|
|
383
|
+
- **Do** keep the footer to an `<hr>` and one line carrying the credit "The look comes from ratstack.sh."
|
|
384
|
+
- **Do** serve the 404 page with a 404 status for every unknown address.
|
|
385
|
+
|
|
386
|
+
### Don't:
|
|
387
|
+
|
|
388
|
+
- **Don't** add a second family or a display face; the one font is Berkeley Mono, with the system monospace as its fallback.
|
|
389
|
+
- **Don't** use a glyph Berkeley Mono lacks inside a diagram: heavy lines (━ ┃ ┏ …), rounded corners (╭ ╮ ╯ ╰) or ► (U+25BA); the fallback font draws it at another width and breaks the grid.
|
|
390
|
+
- **Don't** ship or declare Berkeley Mono in the package; it is licensed per user, and without your own licence the system monospace is the design.
|
|
391
|
+
- **Don't** add shadows, radius, gradients or any rule heavier than 1px (the warning's 3px double excepted).
|
|
392
|
+
- **Don't** add a title mark or emoji before the h1.
|
|
393
|
+
- **Don't** add sidebars, search boxes, icon callouts, a hero, or grids of component cards or swatches.
|
|
394
|
+
- **Don't** use icons for controls; controls are underlined words.
|
|
395
|
+
- **Don't** add motion beyond the `:target` wash.
|
|
396
|
+
- **Don't** give a component markup the stylesheet does not style, or a look the plain HTML cannot reach.
|
|
397
|
+
- **Don't** name Rat Stack, its products or its vocabulary anywhere but the footer credit and one sentence on the name.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gjermund Garaba
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,65 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @gjermundgaraba-designs/simple-rat
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A design system from [my-designs](https://github.com/gjermundgaraba/my-designs): tokens, a
|
|
4
|
+
stylesheet, fonts where it ships its own, and [Foldkit](https://foldkit.dev) components.
|
|
5
|
+
`DESIGN.md`, beside this file, is the design record: what each component is for, its markup
|
|
6
|
+
and classes, and the rules. It ships in the package, so an adopting project's agents can
|
|
7
|
+
read it in `node_modules`.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @gjermundgaraba-designs/simple-rat
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Install its peers beside it, the `peerDependencies` in `package.json`: `foldkit`,
|
|
16
|
+
`@foldkit/ui`, `effect` and `@effect/platform-browser`. Foldkit pins `effect` and
|
|
17
|
+
`@effect/platform-browser` exactly as peers; an app on a later patch keeps one Effect
|
|
18
|
+
instance by allowing it in pnpm:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
# pnpm-workspace.yaml
|
|
22
|
+
peerDependencyRules:
|
|
23
|
+
allowedVersions:
|
|
24
|
+
effect: <the app's version>
|
|
25
|
+
"@effect/platform-browser": <the app's version>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Set up
|
|
29
|
+
|
|
30
|
+
The package is compiled JavaScript with its types, its views already branded for Foldkit's
|
|
31
|
+
view identity, so it needs nothing in the app's build beside `foldkit()`. Import the
|
|
32
|
+
stylesheet once, from the client entry:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import "@gjermundgaraba-designs/simple-rat/simple-rat.css";
|
|
36
|
+
import { Scheme } from "@gjermundgaraba-designs/simple-rat";
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The stylesheet follows the reader's colour scheme with no script; `Scheme.SchemeSwitch`
|
|
40
|
+
lets them pin one, and `schemeScript` applies it before first paint. Write it into the
|
|
41
|
+
head, in a Vite app from `vite.config.ts`:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { schemeScript } from "@gjermundgaraba-designs/simple-rat/scheme";
|
|
45
|
+
|
|
46
|
+
export default defineConfig({
|
|
47
|
+
plugins: [
|
|
48
|
+
foldkit(),
|
|
49
|
+
{
|
|
50
|
+
name: "scheme-script",
|
|
51
|
+
transformIndexHtml: () => [
|
|
52
|
+
{ tag: "script", children: schemeScript, injectTo: "head-prepend" },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Provide `Clipboard.layer` as the application's resources where a component copies.
|
|
60
|
+
`DESIGN.md` has the rest: each component, how links and fragments are handled, and the
|
|
61
|
+
rules a new component follows.
|
|
62
|
+
|
|
63
|
+
## Licence
|
|
64
|
+
|
|
65
|
+
MIT. Fonts in `fonts/`, where the package has them, carry their own licence beside them.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Language, Lines } from "../src/code/code.ts";
|
|
2
|
+
/** A block's runs, line by line, for `CodeBlock`'s `lines`. A run in the plain code ink,
|
|
3
|
+
upright, has no colour. */
|
|
4
|
+
export declare const highlight: (code: string, lang: Language) => Lines;
|
|
5
|
+
//# sourceMappingURL=highlight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"highlight.d.ts","sourceRoot":"","sources":["../../highlight/highlight.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAwG3D;8BAC8B;AAC9B,eAAO,MAAM,SAAS,SAAU,MAAM,QAAQ,QAAQ,KAAG,KAQtD,CAAC"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { brandViewResult as __foldkitBrandViewResult } from 'foldkit/brand'
|
|
2
|
+
// Syntax colours for code blocks, by Shiki, for a build to run: the theme maps TextMate
|
|
3
|
+
// scopes to the --t-* tokens in tokens.css (Catppuccin: Latte in light, darkened for AA;
|
|
4
|
+
// Mocha in dark), so the colours switch with the page. It sits outside src/, which never
|
|
5
|
+
// imports it: a page is given the lines `highlight` made (vite.ts makes them of a file).
|
|
6
|
+
import { createHighlighterCoreSync } from "shiki/core";
|
|
7
|
+
import { createJavaScriptRawEngine } from "shiki/engine/javascript";
|
|
8
|
+
import css from "@shikijs/langs-precompiled/css";
|
|
9
|
+
import diff from "@shikijs/langs-precompiled/diff";
|
|
10
|
+
import html from "@shikijs/langs-precompiled/html";
|
|
11
|
+
import json from "@shikijs/langs-precompiled/json";
|
|
12
|
+
import shellscript from "@shikijs/langs-precompiled/shellscript";
|
|
13
|
+
import tsx from "@shikijs/langs-precompiled/tsx";
|
|
14
|
+
const t = (token) => __foldkitBrandViewResult((`var(--t-${token})`), "@gjermundgaraba-designs/simple-rat/highlight/highlight.js#t");
|
|
15
|
+
const codeTheme = {
|
|
16
|
+
name: "simple-rat",
|
|
17
|
+
type: "light",
|
|
18
|
+
colors: { "editor.foreground": "var(--code-ink)", "editor.background": "var(--code-fill)" },
|
|
19
|
+
tokenColors: [
|
|
20
|
+
{
|
|
21
|
+
scope: ["comment", "punctuation.definition.comment", "meta.diff.header"],
|
|
22
|
+
settings: { foreground: t("com"), fontStyle: "italic" },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
scope: [
|
|
26
|
+
"string",
|
|
27
|
+
"punctuation.definition.string",
|
|
28
|
+
"string.unquoted.argument",
|
|
29
|
+
"markup.inserted",
|
|
30
|
+
],
|
|
31
|
+
settings: { foreground: t("str") },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
scope: [
|
|
35
|
+
"constant.numeric",
|
|
36
|
+
"constant.language",
|
|
37
|
+
"constant.character",
|
|
38
|
+
"constant.other.color",
|
|
39
|
+
"keyword.other.unit",
|
|
40
|
+
"support.constant.property-value",
|
|
41
|
+
"variable.language.this",
|
|
42
|
+
],
|
|
43
|
+
settings: { foreground: t("num") },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
scope: [
|
|
47
|
+
"keyword",
|
|
48
|
+
"storage.type",
|
|
49
|
+
"storage.modifier",
|
|
50
|
+
"keyword.control.at-rule",
|
|
51
|
+
"support.function.misc.css",
|
|
52
|
+
"support.function.transform.css",
|
|
53
|
+
"meta.diff.range",
|
|
54
|
+
],
|
|
55
|
+
settings: { foreground: t("kw") },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
scope: [
|
|
59
|
+
"keyword.operator",
|
|
60
|
+
"punctuation.definition.tag",
|
|
61
|
+
"storage.type.function.arrow",
|
|
62
|
+
"punctuation.separator.pipe",
|
|
63
|
+
],
|
|
64
|
+
settings: { foreground: t("op") },
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
scope: [
|
|
68
|
+
"entity.name.type",
|
|
69
|
+
"support.type.primitive",
|
|
70
|
+
"support.class",
|
|
71
|
+
"entity.other.inherited-class",
|
|
72
|
+
"entity.other.attribute-name",
|
|
73
|
+
"entity.name.tag.css",
|
|
74
|
+
"support.type.builtin",
|
|
75
|
+
],
|
|
76
|
+
settings: { foreground: t("ty") },
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
scope: [
|
|
80
|
+
"entity.name.function",
|
|
81
|
+
"support.function",
|
|
82
|
+
"entity.name.tag",
|
|
83
|
+
"support.class.component",
|
|
84
|
+
"support.type.property-name",
|
|
85
|
+
"variable.css",
|
|
86
|
+
],
|
|
87
|
+
settings: { foreground: t("fn") },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
scope: ["entity.name.command", "support.function.builtin.shell"],
|
|
91
|
+
settings: { foreground: t("fn"), fontStyle: "italic" },
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
scope: [
|
|
95
|
+
"variable.other.property",
|
|
96
|
+
"variable.other.object.property",
|
|
97
|
+
"meta.object-literal.key",
|
|
98
|
+
"variable.argument.css",
|
|
99
|
+
],
|
|
100
|
+
settings: { foreground: t("prop") },
|
|
101
|
+
},
|
|
102
|
+
{ scope: "markup.deleted", settings: { foreground: t("del") } },
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
// Built once, when a build first imports this file. The grammars come precompiled, so
|
|
106
|
+
// nothing translates their patterns. TypeScript is read with the TSX grammar.
|
|
107
|
+
const highlighter = createHighlighterCoreSync({
|
|
108
|
+
themes: [codeTheme],
|
|
109
|
+
langs: [css, diff, html, json, shellscript, tsx],
|
|
110
|
+
langAlias: { ts: "tsx" },
|
|
111
|
+
engine: createJavaScriptRawEngine(),
|
|
112
|
+
});
|
|
113
|
+
/** A block's runs, line by line, for `CodeBlock`'s `lines`. A run in the plain code ink,
|
|
114
|
+
upright, has no colour. */
|
|
115
|
+
export const highlight = (code, lang) => __foldkitBrandViewResult((highlighter.codeToTokens(code, { lang, theme: "simple-rat" }).tokens.map((line) => __foldkitBrandViewResult((line.map((token) => {
|
|
116
|
+
// Italic is bit 1 of Shiki's font style.
|
|
117
|
+
const isItalic = ((token.fontStyle ?? 0) & 1) === 1;
|
|
118
|
+
const isPlain = token.color === "var(--code-ink)" && !isItalic;
|
|
119
|
+
return __foldkitBrandViewResult(({ content: token.content, color: isPlain ? undefined : token.color, isItalic }), "@gjermundgaraba-designs/simple-rat/highlight/highlight.js#anonymous~2");
|
|
120
|
+
})), "@gjermundgaraba-designs/simple-rat/highlight/highlight.js#anonymous"))), "@gjermundgaraba-designs/simple-rat/highlight/highlight.js#highlight");
|