trykeyline 0.1.0 → 0.1.2
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/.claude-plugin/marketplace.json +1 -1
- package/LICENSE +0 -16
- package/bin/keyline.mjs +2 -1
- package/package.json +1 -1
- package/plugins/keyline/.claude-plugin/plugin.json +1 -1
- package/plugins/keyline/plugin.json +22 -0
- package/plugins/keyline/skills/keyline/SKILL.md +22 -32
- package/plugins/keyline/skills/keyline/commands/audit.md +8 -3
- package/plugins/keyline/skills/keyline/commands/critique.md +3 -26
- package/plugins/keyline/skills/keyline/commands/init.md +5 -4
- package/plugins/keyline/skills/keyline/commands/polish.md +2 -2
- package/plugins/keyline/skills/keyline/commands/refine.md +6 -16
- package/plugins/keyline/skills/keyline/commands/sync.md +1 -3
- package/plugins/keyline/skills/keyline/commands/ux-laws.md +26 -0
- package/plugins/keyline/skills/keyline/domains/color/deriving-a-ramp.md +1 -1
- package/plugins/keyline/skills/keyline/domains/color/guide.md +10 -46
- package/plugins/keyline/skills/keyline/domains/color/status-and-exceptions.md +2 -2
- package/plugins/keyline/skills/keyline/domains/depth/elevation-recipes.md +2 -8
- package/plugins/keyline/skills/keyline/domains/depth/guide.md +12 -41
- package/plugins/keyline/skills/keyline/domains/layout/guide.md +31 -56
- package/plugins/keyline/skills/keyline/domains/layout/measure-and-rhythm.md +16 -16
- package/plugins/keyline/skills/keyline/domains/layout/radius-lines-and-layers.md +2 -2
- package/plugins/keyline/skills/keyline/domains/motion/guide.md +12 -47
- package/plugins/keyline/skills/keyline/domains/motion/patterns.md +1 -1
- package/plugins/keyline/skills/keyline/domains/motion/performance.md +1 -1
- package/plugins/keyline/skills/keyline/domains/patterns/guide.md +20 -62
- package/plugins/keyline/skills/keyline/domains/patterns/iteration.md +1 -9
- package/plugins/keyline/skills/keyline/domains/type/guide.md +13 -37
- package/plugins/keyline/skills/keyline/domains/type/ladders.md +1 -1
- package/plugins/keyline/skills/keyline/domains/type/type-audit.md +5 -8
- package/plugins/keyline/skills/keyline/glass/color/assets/color-tokens.css +34 -165
- package/plugins/keyline/skills/keyline/glass/color/assets/theme.ts +10 -70
- package/plugins/keyline/skills/keyline/glass/color/guide.md +3 -30
- package/plugins/keyline/skills/keyline/glass/color/intent-ramp.md +2 -34
- package/plugins/keyline/skills/keyline/glass/color/theming.md +5 -72
- package/plugins/keyline/skills/keyline/glass/depth/assets/GlassPanel.tsx +14 -121
- package/plugins/keyline/skills/keyline/glass/depth/assets/glass.css +29 -188
- package/plugins/keyline/skills/keyline/glass/depth/depth-cues.md +1 -16
- package/plugins/keyline/skills/keyline/glass/depth/guide.md +4 -73
- package/plugins/keyline/skills/keyline/glass/depth/performance-and-fallbacks.md +3 -11
- package/plugins/keyline/skills/keyline/glass/layout/adaptive-presentations.md +5 -91
- package/plugins/keyline/skills/keyline/glass/layout/assets/SectionNav.tsx +3 -6
- package/plugins/keyline/skills/keyline/glass/layout/assets/layout-tokens.css +11 -138
- package/plugins/keyline/skills/keyline/glass/layout/guide.md +10 -15
- package/plugins/keyline/skills/keyline/glass/layout/measure-and-rhythm.md +4 -6
- package/plugins/keyline/skills/keyline/glass/layout/radius-and-elevation.md +3 -2
- package/plugins/keyline/skills/keyline/glass/motion/assets/DropMenu.tsx +4 -6
- package/plugins/keyline/skills/keyline/glass/motion/assets/motion-presets.ts +10 -184
- package/plugins/keyline/skills/keyline/glass/motion/assets/motion-tokens.css +21 -105
- package/plugins/keyline/skills/keyline/glass/motion/easing-and-duration.md +2 -31
- package/plugins/keyline/skills/keyline/glass/motion/guide.md +10 -41
- package/plugins/keyline/skills/keyline/glass/motion/reduced-motion.md +1 -7
- package/plugins/keyline/skills/keyline/glass/motion/scroll-performance.md +3 -32
- package/plugins/keyline/skills/keyline/glass/motion/shared-element.md +1 -1
- package/plugins/keyline/skills/keyline/glass/motion/sound.md +10 -80
- package/plugins/keyline/skills/keyline/glass/type/assets/prose.tsx +8 -43
- package/plugins/keyline/skills/keyline/glass/type/assets/type-tokens.css +49 -86
- package/plugins/keyline/skills/keyline/glass/type/guide.md +5 -7
- package/plugins/keyline/skills/keyline/glass/type/long-form-prose.md +9 -74
- package/plugins/keyline/skills/keyline/glass/type/scale.md +2 -6
- package/plugins/keyline/skills/keyline/glass/type/weight-and-tracking.md +4 -20
- package/plugins/keyline/skills/keyline/scripts/check/css.mjs +5 -14
- package/plugins/keyline/skills/keyline/scripts/check/design.mjs +3 -1
- package/plugins/keyline/skills/keyline/scripts/check/files.mjs +70 -26
- package/plugins/keyline/skills/keyline/scripts/check/markup.mjs +3 -3
- package/plugins/keyline/skills/keyline/scripts/check/rules.mjs +9 -1
- package/plugins/keyline/skills/keyline/scripts/check.mjs +6 -3
- package/plugins/keyline/skills/keyline/scripts/contrast.mjs +2 -0
- package/plugins/keyline/skills/keyline/scripts/scan.mjs +29 -45
- package/plugins/keyline/skills/keyline/system/design-md.md +4 -4
- package/plugins/keyline/skills/keyline/system/guide.md +4 -8
- package/plugins/keyline/skills/keyline/system/presets.md +16 -16
- package/plugins/keyline/skills/keyline/system/roles.md +8 -8
|
@@ -74,73 +74,20 @@ export const playClick = () => tok(900, 0.14, 0.13);
|
|
|
74
74
|
|
|
75
75
|
Hover is higher, shorter and **2.6× quieter**: you cross dozens of links moving around a page, and hover at click volume is intolerable within seconds. Click, a completed action, is louder and lower, since lower pitch reads as more consequential. Roughly a fourth apart, they sound pleasant when a click follows a hover.
|
|
76
76
|
|
|
77
|
-
##
|
|
77
|
+
## Wiring
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
The rules — arm on every activation gesture, mouse-only and deduplicated hover, a dense-surface opt-out, a persisted mute checked at playback — are `domains/motion/patterns.md`, sound. Glass's values for them:
|
|
80
80
|
|
|
81
81
|
```ts
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
82
|
+
const UNLOCK_EVENTS = ["pointerdown", "keydown", "touchstart"] as const; // resume() is idempotent
|
|
83
|
+
const SELECTOR = "a, button, [role='button'], summary"; // one delegated listener set
|
|
84
|
+
if (el && !el.closest("[data-quiet-hover]")) playHover(); // image wall, filmstrip
|
|
86
85
|
```
|
|
87
86
|
|
|
88
|
-
Every playback path checks state
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
That's a feature, not a workaround: a page you've merely scrolled past makes no sound. Fail silently; never surface an error for this.
|
|
95
|
-
|
|
96
|
-
### Arm it on every activation gesture
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
const UNLOCK_EVENTS = ["pointerdown", "keydown", "touchstart"] as const;
|
|
100
|
-
|
|
101
|
-
/* Idempotent — resume() on a running context is a no-op, which is cheaper
|
|
102
|
-
than the bookkeeping to detach these after the first one lands. */
|
|
103
|
-
const onUnlock = () => unlockAudio();
|
|
104
|
-
for (const type of UNLOCK_EVENTS) {
|
|
105
|
-
document.addEventListener(type, onUnlock, { passive: true });
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
The reference implementation shipped `pointerdown` alone for months. A reader who only moved the mouse across the dock heard nothing — *the hover was the interaction* — which hid because testers click something within seconds; and keyboard-only readers never heard the site. `keydown` is the fix that matters most: the affected group is the one least able to work around it.
|
|
110
|
-
|
|
111
|
-
## Delegation, not per-element wiring
|
|
112
|
-
|
|
113
|
-
One set of document listeners covers every link and button on the site.
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
const SELECTOR = "a, button, [role='button'], summary";
|
|
117
|
-
|
|
118
|
-
const onOver = (e: PointerEvent) => {
|
|
119
|
-
// Mouse only — touch has no hover, and we don't want a tick on tap.
|
|
120
|
-
if (e.pointerType && e.pointerType !== "mouse") return;
|
|
121
|
-
const el = (e.target as Element | null)?.closest?.(SELECTOR) ?? null;
|
|
122
|
-
if (el === lastHover) return; // don't re-fire while moving within one element
|
|
123
|
-
lastHover = el;
|
|
124
|
-
if (el) playHover();
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
document.addEventListener("pointerover", onOver);
|
|
128
|
-
document.addEventListener("click", onClick);
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
- **`pointerType !== "mouse"` bails out**, or every tap plays both sounds 15ms apart, like a stutter.
|
|
132
|
-
- **`lastHover` deduplication.** `pointerover` fires for descendants too, so an icon *inside* a button would re-trigger without the identity check.
|
|
133
|
-
- **`closest(SELECTOR)`** covers anything interactive, including elements added later, with no per-component wiring. `summary` is in it because a `<details>` disclosure is a real control that should feel like one.
|
|
134
|
-
|
|
135
|
-
### Dense surfaces opt out of the hover tick
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
// Surfaces of a hundred small targets (an image wall, a filmstrip) opt out with
|
|
139
|
-
// data-quiet-hover: a tick per tile is a machine gun. Clicks still tick.
|
|
140
|
-
if (el && !el.closest("[data-quiet-hover]")) playHover();
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
One attribute on the container quiets the subtree on hover while clicks stay audible. It's a rate limit, not "this surface is unimportant".
|
|
87
|
+
- Every playback path checks `c.state !== "running"` and fails silently, so a page merely scrolled past makes no sound.
|
|
88
|
+
- The site shipped `pointerdown` alone for months; mouse-only readers and keyboard readers heard nothing, and testers, who click within seconds, never noticed.
|
|
89
|
+
- `summary` is in the selector because a `<details>` disclosure is a real control.
|
|
90
|
+
- Mute persists as `localStorage["ui-sound"] = "off" | "on"`, from an `aria-pressed` control in the nav's settings box beside the theme toggle.
|
|
144
91
|
|
|
145
92
|
## Sound as a phrase
|
|
146
93
|
|
|
@@ -166,27 +113,10 @@ export const playLanding = () => tok(587, 0.26, 0.16);
|
|
|
166
113
|
|
|
167
114
|
It's still a set piece, not a texture — once a session. See §14 of `glass/motion/guide.md`.
|
|
168
115
|
|
|
169
|
-
## Mute, and persistence
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
let muted = localStorage.getItem("ui-sound") === "off";
|
|
173
|
-
|
|
174
|
-
export function setMuted(value: boolean): void {
|
|
175
|
-
muted = value;
|
|
176
|
-
try { localStorage.setItem("ui-sound", value ? "off" : "on"); } catch {}
|
|
177
|
-
}
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
The mute control sits in the nav's settings box beside the theme toggle, with an `aria-pressed` state and a label.
|
|
181
|
-
|
|
182
116
|
## Should the default be on?
|
|
183
117
|
|
|
184
118
|
Arguable. On a personal site where sound is part of the character, on is defensible — the only way most people will discover it, and it's quiet, short and muteable. On anything the user didn't choose to visit (a tool, an app, a client site), default to off. If it's on, the mute control must be *findable within seconds*; that's the price.
|
|
185
119
|
|
|
186
120
|
## What's deliberately absent
|
|
187
121
|
|
|
188
|
-
**Haptics
|
|
189
|
-
|
|
190
|
-
**Sounds for anything but hover, click and the one greeting phrase** — no navigation, reveal or error sounds. The riffle earns its place by playing once per tab.
|
|
191
|
-
|
|
192
|
-
**Pitch variation** by element type or position. It turns an ambient texture into something the user starts trying to interpret.
|
|
122
|
+
**Haptics**: `navigator.vibrate()` would fire only on Android, and a feedback channel on one platform is worse than none. **Pitch variation** by element type or position, which turns an ambient texture into something the user tries to interpret. And no sound beyond hover, click and the one greeting phrase.
|
|
@@ -1,33 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Long-form prose renderer — the reading half of the type system.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* blocks sharing 14px have to separate on other axes — each on a different
|
|
7
|
-
* one, or they collide:
|
|
4
|
+
* Two sizes: 16px headings, 14px for everything read, so the 14px blocks
|
|
5
|
+
* each separate on a different axis (the table in glass/type/long-form-prose.md).
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* quote a 3px rounded bar and a 16px indent
|
|
13
|
-
* note 13px, the faint tier, and a 1px rule
|
|
14
|
-
*
|
|
15
|
-
* The rule that produced this: A TIER MUST NEVER OUTRANK THE HEADING IT SITS
|
|
16
|
-
* UNDER. An earlier pass ran the keyline at 17px, above the 16px heading it
|
|
17
|
-
* belonged to, and the page read as though the paragraphs were shouting over
|
|
18
|
-
* the titles. Pulling everything to one size removes any chance of that
|
|
19
|
-
* returning.
|
|
20
|
-
*
|
|
21
|
-
* Spacing is deliberately asymmetric around headings: 40px above, 12px below,
|
|
22
|
-
* so a heading belongs to the paragraph that follows it rather than floating
|
|
23
|
-
* between two sections. The keyline is the one symmetric block, because it
|
|
24
|
-
* belongs to neither neighbour.
|
|
25
|
-
*
|
|
26
|
-
* Colour comes from the Glass colour guide (`glass/color/guide.md`): `gray-1000`/`gray-900` for
|
|
27
|
-
* the two upper text tiers, and the `muted`/`quiet`/`faint` tokens below them
|
|
28
|
-
* (which are deliberately NOT ramp steps — 500–800 are border values and fail
|
|
29
|
-
* as text in a light theme). `gray-500` stays on decorative marks, which carry
|
|
30
|
-
* no text. `reveal` comes from glass/motion.
|
|
7
|
+
* Spacing: 40px above a heading, 12px below; the keyline is the one
|
|
8
|
+
* symmetric block (glass/type/long-form-prose.md). Text tiers come from
|
|
9
|
+
* glass/color/guide.md; `reveal` from glass/motion.
|
|
31
10
|
*/
|
|
32
11
|
|
|
33
12
|
import { Fragment, type ReactNode } from "react";
|
|
@@ -188,23 +167,9 @@ export function Prose({ blocks }: { blocks: Block[] }) {
|
|
|
188
167
|
}
|
|
189
168
|
|
|
190
169
|
/**
|
|
191
|
-
* Inline markdown for **strong
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* **strong** → weight 500 + primary colour (advances)
|
|
195
|
-
* *em* → muted colour, no slant (recedes)
|
|
196
|
-
*
|
|
197
|
-
* This gives genuinely two-directional emphasis, which a bold-only system
|
|
198
|
-
* can't express. The semantic tags stay — screen readers still announce them.
|
|
199
|
-
*
|
|
200
|
-
* One caution specific to a two-size reading column: <strong> and the keyline
|
|
201
|
-
* are now the same weight at the same size, so a paragraph with a long bolded
|
|
202
|
-
* clause reads as a keyline that failed to get its own line. Keep strong
|
|
203
|
-
* emphasis to a few words; anything longer wants to BE a keyline.
|
|
204
|
-
*
|
|
205
|
-
* Links are here because the strongest sentences often live in data rather
|
|
206
|
-
* than JSX, and a claim that can't carry a link to its own evidence is just an
|
|
207
|
-
* assertion. One link treatment everywhere prose carries one.
|
|
170
|
+
* Inline markdown for **strong** (500 + primary), *em* (muted, no slant) and
|
|
171
|
+
* [links](/there) — §5 of glass/type/guide.md. Keep strong to a few words:
|
|
172
|
+
* at this size it is the keyline's weight.
|
|
208
173
|
*/
|
|
209
174
|
const TOKENS = /(\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)\s]+\))/g;
|
|
210
175
|
const LINK = /^\[([^\]]+)\]\(([^)\s]+)\)$/;
|
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
2
2
|
Typographic system — tokens
|
|
3
|
-
|
|
4
|
-
Swap "Archivo" for your family; keep the ladder
|
|
5
|
-
|
|
6
|
-
The ladder tops out at 18px on purpose. See glass/type/scale.md — a low
|
|
7
|
-
ceiling is what forces every template onto one scale, and it's the single
|
|
8
|
-
most consequential choice in this system.
|
|
3
|
+
Tailwind v4 @theme tokens; outside Tailwind, put the same custom
|
|
4
|
+
properties on :root. Swap "Archivo" for your family; keep the ladder,
|
|
5
|
+
which tops out at 18px on purpose (glass/type/scale.md).
|
|
9
6
|
───────────────────────────────────────────────────────────────────────── */
|
|
10
7
|
|
|
11
8
|
@theme {
|
|
12
|
-
/* Self-hosted
|
|
13
|
-
|
|
14
|
-
system faces after it are belt and braces for a request that never lands.
|
|
15
|
-
A bare `display: swap` without metric matching is a reflow under the
|
|
16
|
-
reader's eye — see §7 of glass/type/guide.md. */
|
|
9
|
+
/* Self-hosted with a loader-generated metric-matched fallback (§7 of
|
|
10
|
+
glass/type/guide.md). */
|
|
17
11
|
--font-sans: var(--font-archivo), system-ui, -apple-system, "Helvetica Neue", sans-serif;
|
|
18
12
|
|
|
19
|
-
/* Code, commands and paths
|
|
20
|
-
a handful of monospace runs isn't worth the request. A second family
|
|
21
|
-
earns a download only if it's carrying reading copy. */
|
|
13
|
+
/* Code, commands and paths: a system stack on purpose. */
|
|
22
14
|
--font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
23
15
|
|
|
24
16
|
/* ── Size ladder ──────────────────────────────────────────────────────
|
|
@@ -33,14 +25,10 @@
|
|
|
33
25
|
--text-micro: 12px; /* tags · micro-labels */
|
|
34
26
|
--text-tip: 11px; /* tooltips · initials */
|
|
35
27
|
--text-mark: 8.5px; /* lettermarks — recognised, not read */
|
|
36
|
-
--text-card: 7.5px; /* card lettering
|
|
37
|
-
|
|
38
|
-
/* ── Leading: three curves, one per job ───────────────────────────────
|
|
39
|
-
Display tightens as size grows. Reading opens as passages lengthen.
|
|
40
|
-
UI collapses to 1 so fixed-height boxes centre exactly.
|
|
28
|
+
--text-card: 7.5px; /* card lettering */
|
|
41
29
|
|
|
42
|
-
|
|
43
|
-
|
|
30
|
+
/* ── Leading: three curves, one per job (§3 of the guide). With a ladder
|
|
31
|
+
this short, leading is a role signal: lead / body / dek differ only here. */
|
|
44
32
|
--leading-page: 1.25;
|
|
45
33
|
--leading-title: 1.3; /* a detail-page title, which wraps to two lines */
|
|
46
34
|
--leading-heading: 1.2; /* a section heading, which doesn't */
|
|
@@ -52,9 +40,7 @@
|
|
|
52
40
|
--leading-tight: 1.4;
|
|
53
41
|
--leading-none: 1;
|
|
54
42
|
|
|
55
|
-
/* ── Tracking
|
|
56
|
-
Two values, 2px apart, because there are only two sizes above 14px.
|
|
57
|
-
A system tracking six steps has a scale doing too much work. */
|
|
43
|
+
/* ── Tracking (§4 of the guide) ───────────────────────────────────── */
|
|
58
44
|
--tracking-page: -0.02em; /* 18px */
|
|
59
45
|
--tracking-heading: -0.015em; /* 16px */
|
|
60
46
|
--tracking-card: -0.01em; /* 7.5px, set solid */
|
|
@@ -63,24 +49,11 @@
|
|
|
63
49
|
/* ── Weight: two, and only two ─────────────────────────────────────── */
|
|
64
50
|
--font-weight-normal: 400; /* all body, all prose, all quiet UI */
|
|
65
51
|
--font-weight-medium: 500; /* headings, <strong>, active state, marks */
|
|
66
|
-
/* The one exception
|
|
67
|
-
doesn't survive rasterisation, so the choice is 600 or nothing. One
|
|
68
|
-
instance of this in a codebase is the right number. */
|
|
52
|
+
/* The one exception: §2 of the guide. */
|
|
69
53
|
--font-weight-card: 600;
|
|
70
54
|
}
|
|
71
55
|
|
|
72
|
-
/* Text
|
|
73
|
-
reference them by name:
|
|
74
|
-
primary → --color-gray-1000 17.93:1
|
|
75
|
-
secondary → --color-gray-900 8.45:1
|
|
76
|
-
muted → --color-muted 6.48:1
|
|
77
|
-
quiet → --color-quiet 5.49:1
|
|
78
|
-
faint → --color-faint 4.61:1
|
|
79
|
-
|
|
80
|
-
Only the top two are ramp steps. muted/quiet/faint are their OWN tokens,
|
|
81
|
-
because a border needs 3:1 against its surface and text needs 4.5:1 — one
|
|
82
|
-
step can't serve both. Reusing mid-ramp greys (500-800) for text fails in
|
|
83
|
-
a light theme while looking perfectly fine in dark. */
|
|
56
|
+
/* Text tiers (primary → faint) and their contrast: glass/color/guide.md. */
|
|
84
57
|
|
|
85
58
|
@layer base {
|
|
86
59
|
body {
|
|
@@ -89,18 +62,14 @@
|
|
|
89
62
|
font-size: var(--text-copy);
|
|
90
63
|
line-height: var(--leading-body);
|
|
91
64
|
|
|
92
|
-
/* Tabular by default
|
|
93
|
-
column that should align — dates, timestamps, read times, clocks.
|
|
94
|
-
A live-updating number is unreadable without this. */
|
|
65
|
+
/* Tabular by default (§6 of the guide). */
|
|
95
66
|
font-variant-numeric: tabular-nums;
|
|
96
67
|
|
|
97
68
|
-webkit-font-smoothing: antialiased;
|
|
98
69
|
text-rendering: optimizeLegibility;
|
|
99
70
|
}
|
|
100
71
|
|
|
101
|
-
/* Emphasis
|
|
102
|
-
fights geometric letterforms, and a 700 bold blots open prose. Keep the
|
|
103
|
-
semantic tags — only the visual mapping changed. */
|
|
72
|
+
/* Emphasis maps onto weight and colour (§5 of the guide). */
|
|
104
73
|
em,
|
|
105
74
|
i {
|
|
106
75
|
font-style: normal;
|
|
@@ -115,11 +84,7 @@
|
|
|
115
84
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
116
85
|
Role classes — optional. Use these, or apply the tokens inline through
|
|
117
86
|
Tailwind arbitrary values (text-[14px] leading-[1.65]) as the reference
|
|
118
|
-
implementation does.
|
|
119
|
-
|
|
120
|
-
Note how many of these share --text-copy. That's the system working: once
|
|
121
|
-
the ladder is short, a role is a (size, leading, weight, rule) tuple and
|
|
122
|
-
size is usually the part that doesn't vary.
|
|
87
|
+
implementation does.
|
|
123
88
|
───────────────────────────────────────────────────────────────────────── */
|
|
124
89
|
|
|
125
90
|
@layer components {
|
|
@@ -260,27 +225,26 @@
|
|
|
260
225
|
}
|
|
261
226
|
|
|
262
227
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
263
|
-
Imported prose —
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
than letting it invent a second ladder.
|
|
228
|
+
Imported prose — rendered Markdown that can't carry utility classes,
|
|
229
|
+
styled by element in one scoped class on the ladder above (§8 of the
|
|
230
|
+
guide). `.skill-prose` is the class the reference site uses.
|
|
267
231
|
───────────────────────────────────────────────────────────────────────── */
|
|
268
232
|
|
|
269
233
|
@layer components {
|
|
270
|
-
.prose
|
|
234
|
+
.skill-prose {
|
|
271
235
|
font-size: var(--text-copy);
|
|
272
236
|
line-height: var(--leading-read);
|
|
273
237
|
color: var(--color-muted);
|
|
274
238
|
}
|
|
275
239
|
/* The document starts with whatever it starts with; a leading <h2> would
|
|
276
240
|
otherwise push 44px of nothing above itself. */
|
|
277
|
-
.prose
|
|
241
|
+
.skill-prose > :first-child {
|
|
278
242
|
margin-top: 0;
|
|
279
243
|
}
|
|
280
244
|
|
|
281
245
|
/* Three levels, not two — an imported document nests deeper than authored
|
|
282
246
|
prose, so h4 gets a real role instead of a browser default. */
|
|
283
|
-
.prose
|
|
247
|
+
.skill-prose h2 {
|
|
284
248
|
margin: 44px 0 12px;
|
|
285
249
|
font-size: var(--text-section);
|
|
286
250
|
font-weight: var(--font-weight-medium);
|
|
@@ -288,14 +252,14 @@
|
|
|
288
252
|
letter-spacing: var(--tracking-heading);
|
|
289
253
|
color: var(--color-gray-1000);
|
|
290
254
|
}
|
|
291
|
-
.prose
|
|
255
|
+
.skill-prose h3 {
|
|
292
256
|
margin: 32px 0 10px;
|
|
293
257
|
font-size: var(--text-copy);
|
|
294
258
|
font-weight: var(--font-weight-medium);
|
|
295
259
|
line-height: var(--leading-tight);
|
|
296
260
|
color: var(--color-gray-1000);
|
|
297
261
|
}
|
|
298
|
-
.prose
|
|
262
|
+
.skill-prose h4 {
|
|
299
263
|
margin: 24px 0 8px;
|
|
300
264
|
font-size: var(--text-dense);
|
|
301
265
|
font-weight: var(--font-weight-medium);
|
|
@@ -303,23 +267,22 @@
|
|
|
303
267
|
color: var(--color-gray-900);
|
|
304
268
|
}
|
|
305
269
|
|
|
306
|
-
.prose
|
|
307
|
-
.prose
|
|
308
|
-
.prose
|
|
309
|
-
.prose
|
|
270
|
+
.skill-prose p,
|
|
271
|
+
.skill-prose ul,
|
|
272
|
+
.skill-prose ol,
|
|
273
|
+
.skill-prose blockquote {
|
|
310
274
|
margin: 12px 0;
|
|
311
275
|
}
|
|
312
276
|
|
|
313
|
-
/*
|
|
314
|
-
|
|
315
|
-
.prose-imported ul {
|
|
277
|
+
/* The 3px dot of the hand-rolled bullets (glass/type/long-form-prose.md). */
|
|
278
|
+
.skill-prose ul {
|
|
316
279
|
list-style: none;
|
|
317
280
|
padding-left: 18px;
|
|
318
281
|
}
|
|
319
|
-
.prose
|
|
282
|
+
.skill-prose ul > li {
|
|
320
283
|
position: relative;
|
|
321
284
|
}
|
|
322
|
-
.prose
|
|
285
|
+
.skill-prose ul > li::before {
|
|
323
286
|
content: "";
|
|
324
287
|
position: absolute;
|
|
325
288
|
left: -14px;
|
|
@@ -329,22 +292,22 @@
|
|
|
329
292
|
border-radius: 999px;
|
|
330
293
|
background: var(--color-gray-500);
|
|
331
294
|
}
|
|
332
|
-
.prose
|
|
295
|
+
.skill-prose ol {
|
|
333
296
|
list-style: decimal;
|
|
334
297
|
padding-left: 18px;
|
|
335
298
|
}
|
|
336
299
|
/* Tabular, so a run past 9 doesn't shift its text. */
|
|
337
|
-
.prose
|
|
300
|
+
.skill-prose li::marker {
|
|
338
301
|
color: var(--color-faint);
|
|
339
302
|
font-variant-numeric: tabular-nums;
|
|
340
303
|
}
|
|
341
304
|
|
|
342
|
-
.prose
|
|
305
|
+
.skill-prose strong {
|
|
343
306
|
font-weight: var(--font-weight-medium);
|
|
344
307
|
color: var(--color-gray-1000);
|
|
345
308
|
}
|
|
346
309
|
|
|
347
|
-
.prose
|
|
310
|
+
.skill-prose code {
|
|
348
311
|
font-family: var(--font-mono);
|
|
349
312
|
font-size: 0.87em;
|
|
350
313
|
background: var(--color-gray-100);
|
|
@@ -353,10 +316,8 @@
|
|
|
353
316
|
color: var(--color-gray-1000);
|
|
354
317
|
}
|
|
355
318
|
|
|
356
|
-
/* Code blocks and tables
|
|
357
|
-
|
|
358
|
-
own box — the page body must never scroll sideways. */
|
|
359
|
-
.prose-imported pre {
|
|
319
|
+
/* Code blocks and tables scroll inside their own box, never the page. */
|
|
320
|
+
.skill-prose pre {
|
|
360
321
|
margin: 16px 0;
|
|
361
322
|
padding: 14px 16px;
|
|
362
323
|
background: var(--color-background-200);
|
|
@@ -364,7 +325,7 @@
|
|
|
364
325
|
border-radius: 12px;
|
|
365
326
|
overflow-x: auto;
|
|
366
327
|
}
|
|
367
|
-
.prose
|
|
328
|
+
.skill-prose pre code {
|
|
368
329
|
background: none;
|
|
369
330
|
padding: 0;
|
|
370
331
|
font-size: var(--text-code);
|
|
@@ -373,40 +334,42 @@
|
|
|
373
334
|
}
|
|
374
335
|
|
|
375
336
|
/* Wrap each <table> in a div with this class at render time. */
|
|
376
|
-
.prose
|
|
337
|
+
.skill-prose .table-scroll {
|
|
377
338
|
margin: 16px 0;
|
|
378
339
|
overflow-x: auto;
|
|
379
340
|
}
|
|
380
|
-
.prose
|
|
341
|
+
.skill-prose table {
|
|
381
342
|
width: 100%;
|
|
382
343
|
border-collapse: collapse;
|
|
383
344
|
font-size: var(--text-dense);
|
|
384
345
|
line-height: 1.5;
|
|
385
346
|
}
|
|
386
|
-
.prose
|
|
387
|
-
.prose
|
|
347
|
+
.skill-prose th,
|
|
348
|
+
.skill-prose td {
|
|
388
349
|
padding: 8px 12px 8px 0;
|
|
389
350
|
text-align: left;
|
|
390
351
|
vertical-align: top;
|
|
391
352
|
border-bottom: 1px solid var(--color-gray-alpha-300);
|
|
392
353
|
}
|
|
393
|
-
.prose
|
|
354
|
+
.skill-prose th {
|
|
394
355
|
font-weight: var(--font-weight-medium);
|
|
395
356
|
color: var(--color-gray-1000);
|
|
396
357
|
white-space: nowrap;
|
|
397
358
|
}
|
|
398
359
|
|
|
399
|
-
|
|
400
|
-
|
|
360
|
+
/* Imported Markdown can't take the authored pull quote's 3px rounded bar
|
|
361
|
+
(glass/type/assets/prose.tsx), so it keeps a plain 2px rule. */
|
|
362
|
+
.skill-prose blockquote {
|
|
363
|
+
padding-left: 15px;
|
|
401
364
|
border-left: 2px solid var(--color-gray-alpha-300);
|
|
402
365
|
color: var(--color-faint);
|
|
403
366
|
}
|
|
404
|
-
.prose
|
|
367
|
+
.skill-prose hr {
|
|
405
368
|
margin: 32px 0;
|
|
406
369
|
border: 0;
|
|
407
370
|
border-top: 1px solid var(--color-gray-alpha-300);
|
|
408
371
|
}
|
|
409
|
-
.prose
|
|
372
|
+
.skill-prose img {
|
|
410
373
|
max-width: 100%;
|
|
411
374
|
height: auto;
|
|
412
375
|
border-radius: 12px;
|
|
@@ -33,7 +33,7 @@ Each role gets a size in steps of 1–2px, as densely as the roles demand (domai
|
|
|
33
33
|
|
|
34
34
|
Text tiers (`primary` → `quiet`) come from the Glass colour guide (`glass/color/guide.md`). Only the top two are grey-ramp steps (`gray-1000`, `gray-900`); `muted` / `quiet` / `faint` are separate tokens picked for contrast (colour law 2).
|
|
35
35
|
|
|
36
|
-
**Eighteen is the ceiling, and it's lower than you think.**
|
|
36
|
+
**Eighteen is the ceiling, and it's lower than you think.** The site runs on **two sizes**, 16 for headings and 14 for everything read; the detail templates that once opened two steps above every other page came down to those (domain law 2; the deleted sizes are in `glass/type/scale.md`). One element likewise takes one role on every page, and sibling controls match (domain law 2).
|
|
37
37
|
|
|
38
38
|
### When size stops doing the work
|
|
39
39
|
|
|
@@ -50,7 +50,7 @@ At one size, the 17px keyline that once shouted over a 16px heading can't return
|
|
|
50
50
|
|
|
51
51
|
The cost is a flatter page: titles and section headings are both 16px, separated by position and spacing. It's the right flat, because the home page already worked that way and the detail pages were the outlier.
|
|
52
52
|
|
|
53
|
-
**Nothing in this ladder resizes
|
|
53
|
+
**Nothing in this ladder resizes** (the domain's responsive-sizes dial).
|
|
54
54
|
|
|
55
55
|
→ Full derivation, including how to add a role without bloating the ladder: `glass/type/scale.md`
|
|
56
56
|
|
|
@@ -67,7 +67,7 @@ Quiet or lifted is set by what the line is doing there, one lift per group, one
|
|
|
67
67
|
|
|
68
68
|
A contents rail's active row takes `font-medium` *and* primary colour, so state is never on colour alone; no state greys a title (domain laws 3 and 5).
|
|
69
69
|
|
|
70
|
-
→ Weight and tracking in depth
|
|
70
|
+
→ Weight and tracking in depth: `glass/type/weight-and-tracking.md`
|
|
71
71
|
|
|
72
72
|
## 3. Leading runs inverse to size
|
|
73
73
|
|
|
@@ -108,7 +108,7 @@ Archivo's true italic is a separate file whose slant fights its geometric letter
|
|
|
108
108
|
```tsx
|
|
109
109
|
// From the inline markdown renderer
|
|
110
110
|
if (bold) return <strong className="font-medium text-gray-1000">{text}</strong>;
|
|
111
|
-
if (em) return <em className="text-
|
|
111
|
+
if (em) return <em className="text-muted">{text}</em>;
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
Why the mapping works, and the semantic tags it keeps: domain law 9 and `domains/type/prose-and-emphasis.md`.
|
|
@@ -184,9 +184,7 @@ Glass's two systems (domain law 8):
|
|
|
184
184
|
|
|
185
185
|
The ranges overlap, so leading and measure carry the distinction. Reading leading on a UI row is the usual symptom here: it grows, and the dock loses its rhythm.
|
|
186
186
|
|
|
187
|
-
**
|
|
188
|
-
|
|
189
|
-
**Icons beside text** follow domain law 13: 13px text takes a 13–14px icon with a 4–6px gap. It is the mass problem `glass/color/guide.md` solves for colour, by resting icons a tier below, solved here for size.
|
|
187
|
+
**Labels are sentence case, generated ones included** (patterns law 1), and **icons beside text** follow domain law 13: 13px text, a 13–14px icon, a 4–6px gap.
|
|
190
188
|
|
|
191
189
|
→ How prose blocks are set — headings, quotes, lists, spacing: `glass/type/long-form-prose.md`
|
|
192
190
|
|
|
@@ -10,7 +10,7 @@ How Glass sets the reading half: looser leading and spacing than anything else o
|
|
|
10
10
|
|
|
11
11
|
Prose body is 14/1.75: a UI paragraph's size, noticeably more open (1.75 vs 1.55).
|
|
12
12
|
|
|
13
|
-
Why 1.75:
|
|
13
|
+
Why 1.75: the ~90-character measure (`glass/layout/measure-and-rhythm.md`). A shorter measure could pull it back toward 1.6.
|
|
14
14
|
|
|
15
15
|
## The reading column is two sizes
|
|
16
16
|
|
|
@@ -24,25 +24,9 @@ Why 1.75: a 576px text column at 14px holds roughly 85–95 characters a line, p
|
|
|
24
24
|
| Pull quote | 14 | a 3px rounded bar and a 16px indent |
|
|
25
25
|
| Note | 13 | a 1px rule, the faint tier, and a step down in size |
|
|
26
26
|
|
|
27
|
-
##
|
|
27
|
+
## The blocks, in code
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
**`keyline`** — body size, weight 500, `my-9`.
|
|
32
|
-
|
|
33
|
-
```tsx
|
|
34
|
-
case "keyline":
|
|
35
|
-
return <p className="reveal my-9 text-[14px] font-medium leading-[1.6] text-gray-1000">…</p>;
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
**`note`** — 13px, faint tier, a 1px left rule and 16px of indent.
|
|
39
|
-
|
|
40
|
-
```tsx
|
|
41
|
-
case "note":
|
|
42
|
-
return <p className="reveal my-7 border-l border-gray-alpha-300 pl-4 text-[13px] leading-[1.6] text-faint">…</p>;
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
The note's rule is 1px and the quote's 3px, with the type inside four steps apart, so the two indented rules can't be mistaken for each other.
|
|
29
|
+
Every block's classes — lead, body, keyline, note, h2, quote, figure, lists — are in `glass/type/assets/prose.tsx`, with the reason for each beside it. Keyline and note both came from one diagnosis, a body of writing that was 114 paragraphs and almost nothing else; what each is for, and the build check that keeps them honest: `domains/type/prose-and-emphasis.md`, **Two block types worth having**.
|
|
46
30
|
|
|
47
31
|
## Block spacing
|
|
48
32
|
|
|
@@ -59,61 +43,12 @@ The note's rule is 1px and the quote's 3px, with the type inside four steps apar
|
|
|
59
43
|
|
|
60
44
|
The heading's 40 above and 12 below, ~3.3:1, is the domain's asymmetric heading space; the keyline is its symmetric exception.
|
|
61
45
|
|
|
62
|
-
##
|
|
63
|
-
|
|
64
|
-
```tsx
|
|
65
|
-
<h2 className="mt-10 mb-3 text-[16px] font-medium leading-[1.2] tracking-[-0.015em]">
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
16px against 14px body is one step; the weight (500), the tight leading (1.2) and the 40px above do the real work.
|
|
69
|
-
|
|
70
|
-
It matches a page-level section heading and the detail page's own `<h1>`: three levels at one size, separated by position and spacing. Flatter than most article templates, and deliberate — the alternative was an in-body heading larger than the page title.
|
|
71
|
-
|
|
72
|
-
## Quotes
|
|
73
|
-
|
|
74
|
-
```tsx
|
|
75
|
-
<blockquote className="my-9 flex gap-4">
|
|
76
|
-
<span aria-hidden className="w-[3px] shrink-0 rounded-full bg-gray-500" />
|
|
77
|
-
<div className="text-[14px] leading-[1.75] text-gray-1000">…</div>
|
|
78
|
-
</blockquote>
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
The type matches the body around it, so the bar is the entire signal: the 2px `gray-alpha-300` border it replaced is domain law 4's example of auditing what's left after a signal goes. Why a flex child with no vertical margin: `domains/type/prose-and-emphasis.md`, **Quotes**.
|
|
82
|
-
|
|
83
|
-
- **`bg-gray-500`, from the grey ramp, not the alpha ramp.** It's a decorative mark on an opaque surface — see the Glass colour guide (`glass/color/guide.md`) on why decorative marks stay on the solid ramp.
|
|
84
|
-
|
|
85
|
-
Still absent: italic (globally disabled), quotation marks, background, colour step. Stepping the quote *down* to secondary, as earlier versions did, at body size just makes a de-emphasised paragraph.
|
|
86
|
-
|
|
87
|
-
The citation goes quieter and explicitly kills the browser's italic (domain law 9):
|
|
88
|
-
|
|
89
|
-
```tsx
|
|
90
|
-
<cite className="mt-2 block text-[13px] not-italic text-quiet">— {cite}</cite>
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Figures
|
|
94
|
-
|
|
95
|
-
```tsx
|
|
96
|
-
<figure className="reveal my-9">
|
|
97
|
-
<div className="overflow-hidden rounded-[10px] bg-gray-100 ring-1 ring-gray-alpha-300">
|
|
98
|
-
<div style={{ aspectRatio: b.aspect ?? "16 / 9" }}>
|
|
99
|
-
<img src={b.src} alt={b.alt} loading="lazy" decoding="async" className="h-full w-full object-cover" />
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
<figcaption className="mt-2.5 text-[13px] leading-[1.55] text-faint">{caption}</figcaption>
|
|
103
|
-
</figure>
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
The aspect ratio sits on a wrapper so the box is reserved before the image loads. `ring-1` rather than a border, so the outline stays out of the box model and the corner radius stays exact.
|
|
107
|
-
|
|
108
|
-
**The caption is the point of the figure, not a label on it**: what the thing was for, and which constraint shaped it. It sits at the note/meta tier (13px, faint).
|
|
109
|
-
|
|
110
|
-
## Lists
|
|
111
|
-
|
|
112
|
-
```tsx
|
|
113
|
-
<ul className="mb-6 list-disc space-y-2 pl-5 marker:text-gray-500">
|
|
114
|
-
```
|
|
46
|
+
## What the code doesn't say
|
|
115
47
|
|
|
116
|
-
|
|
48
|
+
- **Prose headings** match a page-level section heading and the detail page's own `<h1>`: three levels at 16px, separated by position and spacing. The alternative was an in-body heading larger than the page title.
|
|
49
|
+
- **The pull quote's bar is the entire signal**, since its type matches the body: the 2px `gray-alpha-300` border it replaced is domain law 4's example of auditing what's left after a signal goes. `bg-gray-500` is from the solid ramp, a decorative mark on an opaque surface (`glass/color/guide.md`). Still absent: italic, quotation marks, background, a colour step.
|
|
50
|
+
- **A figure's caption is the point of the figure**, not a label on it: what the thing was for, and which constraint shaped it.
|
|
51
|
+
- **Authored lists keep the UA marker** (`list-disc` / `list-decimal`) on `gray-500`, the domain's quiet marker; imported Markdown uses the 3px dot instead (§8 of `glass/type/guide.md`). Both are what the site ships.
|
|
117
52
|
|
|
118
53
|
## Hand-rolled bullets in UI context
|
|
119
54
|
|
|
@@ -144,7 +79,7 @@ Every block carries `reveal`, so paragraphs fade up one by one as they enter the
|
|
|
144
79
|
<p className="mt-2 text-[14px] leading-[1.55] text-muted">{dek}</p>
|
|
145
80
|
```
|
|
146
81
|
|
|
147
|
-
Body size, muted, tighter leading
|
|
82
|
+
Body size, muted, tighter leading: it separates on colour and leading alone, 1.55 against the body's 1.75 and the lead's 1.9. Dek and lead are the domain's tightest pairing; check them first.
|
|
148
83
|
|
|
149
84
|
## Cover art and the measure
|
|
150
85
|
|