trykeyline 0.0.0-stage → 0.1.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/.claude-plugin/marketplace.json +18 -0
- package/LICENSE +37 -0
- package/README.md +117 -2
- package/bin/keyline.mjs +280 -0
- package/package.json +38 -4
- package/plugins/keyline/.claude-plugin/plugin.json +14 -0
- package/plugins/keyline/skills/keyline/SKILL.md +57 -0
- package/plugins/keyline/skills/keyline/commands/audit.md +25 -0
- package/plugins/keyline/skills/keyline/commands/color.md +7 -0
- package/plugins/keyline/skills/keyline/commands/critique.md +43 -0
- package/plugins/keyline/skills/keyline/commands/depth.md +7 -0
- package/plugins/keyline/skills/keyline/commands/init.md +15 -0
- package/plugins/keyline/skills/keyline/commands/layout.md +7 -0
- package/plugins/keyline/skills/keyline/commands/motion.md +7 -0
- package/plugins/keyline/skills/keyline/commands/patterns.md +7 -0
- package/plugins/keyline/skills/keyline/commands/polish.md +19 -0
- package/plugins/keyline/skills/keyline/commands/refine.md +29 -0
- package/plugins/keyline/skills/keyline/commands/sync.md +17 -0
- package/plugins/keyline/skills/keyline/commands/type.md +7 -0
- package/plugins/keyline/skills/keyline/domains/color/deriving-a-ramp.md +144 -0
- package/plugins/keyline/skills/keyline/domains/color/guide.md +243 -0
- package/plugins/keyline/skills/keyline/domains/color/status-and-exceptions.md +174 -0
- package/plugins/keyline/skills/keyline/domains/color/theming-mechanics.md +121 -0
- package/plugins/keyline/skills/keyline/domains/depth/elevation-recipes.md +191 -0
- package/plugins/keyline/skills/keyline/domains/depth/fallbacks-and-performance.md +132 -0
- package/plugins/keyline/skills/keyline/domains/depth/guide.md +255 -0
- package/plugins/keyline/skills/keyline/domains/depth/translucent-materials.md +159 -0
- package/plugins/keyline/skills/keyline/domains/layout/forks-gating-and-disclosures.md +201 -0
- package/plugins/keyline/skills/keyline/domains/layout/guide.md +284 -0
- package/plugins/keyline/skills/keyline/domains/layout/measure-and-rhythm.md +193 -0
- package/plugins/keyline/skills/keyline/domains/layout/radius-lines-and-layers.md +122 -0
- package/plugins/keyline/skills/keyline/domains/motion/guards.md +162 -0
- package/plugins/keyline/skills/keyline/domains/motion/guide.md +234 -0
- package/plugins/keyline/skills/keyline/domains/motion/ladder-and-curves.md +139 -0
- package/plugins/keyline/skills/keyline/domains/motion/patterns.md +196 -0
- package/plugins/keyline/skills/keyline/domains/motion/performance.md +121 -0
- package/plugins/keyline/skills/keyline/domains/patterns/actions-and-forms.md +204 -0
- package/plugins/keyline/skills/keyline/domains/patterns/agents-and-live-states.md +111 -0
- package/plugins/keyline/skills/keyline/domains/patterns/collections-and-dashboards.md +131 -0
- package/plugins/keyline/skills/keyline/domains/patterns/copy.md +123 -0
- package/plugins/keyline/skills/keyline/domains/patterns/guide.md +216 -0
- package/plugins/keyline/skills/keyline/domains/patterns/iteration.md +78 -0
- package/plugins/keyline/skills/keyline/domains/type/display-tier.md +108 -0
- package/plugins/keyline/skills/keyline/domains/type/guide.md +251 -0
- package/plugins/keyline/skills/keyline/domains/type/ladders.md +169 -0
- package/plugins/keyline/skills/keyline/domains/type/numerals-and-loading.md +107 -0
- package/plugins/keyline/skills/keyline/domains/type/prose-and-emphasis.md +125 -0
- package/plugins/keyline/skills/keyline/domains/type/type-audit.md +90 -0
- package/plugins/keyline/skills/keyline/glass/color/assets/color-tokens.css +319 -0
- package/plugins/keyline/skills/keyline/glass/color/assets/theme.ts +119 -0
- package/plugins/keyline/skills/keyline/glass/color/guide.md +222 -0
- package/plugins/keyline/skills/keyline/glass/color/intent-ramp.md +136 -0
- package/plugins/keyline/skills/keyline/glass/color/theming.md +112 -0
- package/plugins/keyline/skills/keyline/glass/depth/assets/GlassPanel.tsx +171 -0
- package/plugins/keyline/skills/keyline/glass/depth/assets/glass.css +322 -0
- package/plugins/keyline/skills/keyline/glass/depth/blur-and-tint.md +50 -0
- package/plugins/keyline/skills/keyline/glass/depth/depth-cues.md +87 -0
- package/plugins/keyline/skills/keyline/glass/depth/guide.md +216 -0
- package/plugins/keyline/skills/keyline/glass/depth/performance-and-fallbacks.md +37 -0
- package/plugins/keyline/skills/keyline/glass/layout/adaptive-presentations.md +231 -0
- package/plugins/keyline/skills/keyline/glass/layout/assets/SectionNav.tsx +244 -0
- package/plugins/keyline/skills/keyline/glass/layout/assets/layout-tokens.css +199 -0
- package/plugins/keyline/skills/keyline/glass/layout/guide.md +258 -0
- package/plugins/keyline/skills/keyline/glass/layout/measure-and-rhythm.md +134 -0
- package/plugins/keyline/skills/keyline/glass/layout/radius-and-elevation.md +102 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/DropMenu.tsx +106 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/motion-presets.ts +210 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/motion-tokens.css +176 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/useReveal.ts +65 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/useScrollTick.ts +183 -0
- package/plugins/keyline/skills/keyline/glass/motion/easing-and-duration.md +108 -0
- package/plugins/keyline/skills/keyline/glass/motion/guide.md +286 -0
- package/plugins/keyline/skills/keyline/glass/motion/reduced-motion.md +81 -0
- package/plugins/keyline/skills/keyline/glass/motion/scroll-performance.md +58 -0
- package/plugins/keyline/skills/keyline/glass/motion/shared-element.md +161 -0
- package/plugins/keyline/skills/keyline/glass/motion/sound.md +192 -0
- package/plugins/keyline/skills/keyline/glass/motion/springs.md +51 -0
- package/plugins/keyline/skills/keyline/glass/type/assets/prose.tsx +268 -0
- package/plugins/keyline/skills/keyline/glass/type/assets/type-tokens.css +414 -0
- package/plugins/keyline/skills/keyline/glass/type/guide.md +222 -0
- package/plugins/keyline/skills/keyline/glass/type/long-form-prose.md +151 -0
- package/plugins/keyline/skills/keyline/glass/type/scale.md +69 -0
- package/plugins/keyline/skills/keyline/glass/type/weight-and-tracking.md +61 -0
- package/plugins/keyline/skills/keyline/scripts/check/css.mjs +112 -0
- package/plugins/keyline/skills/keyline/scripts/check/design.mjs +67 -0
- package/plugins/keyline/skills/keyline/scripts/check/files.mjs +44 -0
- package/plugins/keyline/skills/keyline/scripts/check/markup.mjs +149 -0
- package/plugins/keyline/skills/keyline/scripts/check/rules.mjs +584 -0
- package/plugins/keyline/skills/keyline/scripts/check.mjs +294 -0
- package/plugins/keyline/skills/keyline/scripts/contrast.mjs +153 -0
- package/plugins/keyline/skills/keyline/scripts/scan.mjs +340 -0
- package/plugins/keyline/skills/keyline/system/design-md.md +136 -0
- package/plugins/keyline/skills/keyline/system/detection.md +53 -0
- package/plugins/keyline/skills/keyline/system/guide.md +101 -0
- package/plugins/keyline/skills/keyline/system/presets.md +58 -0
- package/plugins/keyline/skills/keyline/system/roles.md +150 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
2
|
+
Layout and hierarchy — tokens and structural primitives
|
|
3
|
+
───────────────────────────────────────────────────────────────────────── */
|
|
4
|
+
|
|
5
|
+
@theme {
|
|
6
|
+
/* ── The measure ────────────────────────────────────────────────────────
|
|
7
|
+
640px outer, 32px padding → 576px of text. At 16px prose that's ~75–85
|
|
8
|
+
characters per line: the top of the comfortable range, which is the
|
|
9
|
+
right place for a site that expects sustained reading (long lines are
|
|
10
|
+
efficient once the reader is committed, and 1.75 leading pays for the
|
|
11
|
+
longer return sweep).
|
|
12
|
+
|
|
13
|
+
EVERY page uses this. The payoff isn't visual consistency, it's
|
|
14
|
+
positional continuity: clicking from a summary into its detail view
|
|
15
|
+
doesn't move the text, so the transition reads as expansion rather than
|
|
16
|
+
as arriving at a different document. */
|
|
17
|
+
--container-col: 640px;
|
|
18
|
+
|
|
19
|
+
/* The outer frame that rails anchor to, alongside the column. Anchoring
|
|
20
|
+
both to the SAME frame is what makes the gap between them hold steady
|
|
21
|
+
from 1280px up — a rail positioned against the viewport instead drifts
|
|
22
|
+
away from the content it indexes as the window grows. */
|
|
23
|
+
--container-frame: 1280px;
|
|
24
|
+
--rail-width: 196px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@layer base {
|
|
28
|
+
/* Apple-style continuous corners on every rounded element.
|
|
29
|
+
|
|
30
|
+
A standard border-radius is a quarter-circle: constant curvature that
|
|
31
|
+
drops abruptly to zero where it meets the straight edge. The eye reads
|
|
32
|
+
that discontinuity as a slight hardness. A superellipse varies curvature
|
|
33
|
+
continuously into the edge.
|
|
34
|
+
|
|
35
|
+
superellipse(k) uses exponent 2^k — k=1 is a normal round corner, k=2 a
|
|
36
|
+
full iOS squircle, so 1.5 lands about halfway. Not 2, because a full
|
|
37
|
+
squircle is noticeable at small sizes (it starts to look like a swollen
|
|
38
|
+
rectangle); halfway reads as "well-drawn corner" rather than as an
|
|
39
|
+
effect.
|
|
40
|
+
|
|
41
|
+
Unsupported browsers ignore it and fall back to normal rounding. Keep it
|
|
42
|
+
that way — nothing else in the layout should depend on it. */
|
|
43
|
+
*,
|
|
44
|
+
*::before,
|
|
45
|
+
*::after {
|
|
46
|
+
corner-shape: superellipse(1.5);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@layer components {
|
|
51
|
+
.col {
|
|
52
|
+
max-width: var(--container-col);
|
|
53
|
+
margin: 0 auto;
|
|
54
|
+
padding: 0 32px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Skip link. Parked off-screen with a TRANSFORM, never display:none —
|
|
58
|
+
which would take it out of the tab order and defeat the point. Every
|
|
59
|
+
page needs a matching id="main". */
|
|
60
|
+
.skip {
|
|
61
|
+
position: absolute;
|
|
62
|
+
left: 16px;
|
|
63
|
+
top: 16px;
|
|
64
|
+
z-index: 100;
|
|
65
|
+
background: var(--color-gray-1000);
|
|
66
|
+
color: var(--color-background-100);
|
|
67
|
+
padding: 10px 14px;
|
|
68
|
+
font-size: 13px;
|
|
69
|
+
text-decoration: none;
|
|
70
|
+
border-radius: 6px;
|
|
71
|
+
transform: translateY(-200%);
|
|
72
|
+
transition: transform 150ms var(--ease-geist);
|
|
73
|
+
}
|
|
74
|
+
.skip:focus {
|
|
75
|
+
transform: translateY(0);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
80
|
+
Print. A real presentation, not an afterthought.
|
|
81
|
+
|
|
82
|
+
The .reveal reset is the one people forget: scroll-triggered content is at
|
|
83
|
+
opacity: 0 until an IntersectionObserver fires, and printing never fires
|
|
84
|
+
it — so the page prints blank without this.
|
|
85
|
+
───────────────────────────────────────────────────────────────────────── */
|
|
86
|
+
@media print {
|
|
87
|
+
nav,
|
|
88
|
+
footer,
|
|
89
|
+
.skip {
|
|
90
|
+
display: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
body {
|
|
94
|
+
background: #fff;
|
|
95
|
+
color: #000;
|
|
96
|
+
font-size: 11pt;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.reveal {
|
|
100
|
+
opacity: 1;
|
|
101
|
+
transform: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* An underlined URL-less link is noise on paper. */
|
|
105
|
+
a {
|
|
106
|
+
color: inherit;
|
|
107
|
+
text-decoration: none;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
112
|
+
REFERENCE — apply these in components; they're documentation, not classes.
|
|
113
|
+
|
|
114
|
+
── The radius ladder ──────────────────────────────────────────────────
|
|
115
|
+
Nesting rule: INNER = OUTER − the padding between them. Geometry, not
|
|
116
|
+
taste — two concentric rounded rectangles share a centre of curvature
|
|
117
|
+
only if their radii differ by exactly the gap between their edges.
|
|
118
|
+
|
|
119
|
+
rounded-full pills, tags, dots, markers
|
|
120
|
+
rounded-[5px] a lettermark box
|
|
121
|
+
rounded-md 6 tooltips, the skip link
|
|
122
|
+
rounded-[9px] 9 small cards in a stack
|
|
123
|
+
rounded-[10px]10 avatar inside a button · a list highlight bar
|
|
124
|
+
rounded-xl 12 cards, hover panels, disclosures, icon buttons
|
|
125
|
+
rounded-2xl 16 outer containers — a dock, a sheet, a pocket
|
|
126
|
+
|
|
127
|
+
Worked example:
|
|
128
|
+
Dock rounded-2xl (16) p-1 (4)
|
|
129
|
+
Icon button rounded-[12px] 16 − 4 = 12 ✓
|
|
130
|
+
Avatar rounded-[10px] 12 − 2 = 10 ✓
|
|
131
|
+
|
|
132
|
+
Radius scales with SIZE, not importance — and sub-proportionally, because
|
|
133
|
+
a large element with a proportional radius reads as a lozenge.
|
|
134
|
+
|
|
135
|
+
── Hairline weights ───────────────────────────────────────────────────
|
|
136
|
+
The choice is about what the line is DOING:
|
|
137
|
+
|
|
138
|
+
0.5px internal articulation — a divider between icon groups inside
|
|
139
|
+
one dock. Renders as a true sub-pixel hairline on retina and
|
|
140
|
+
rounds UP to 1px elsewhere, so it degrades rather than vanishes.
|
|
141
|
+
1px a real edge — where a card ends.
|
|
142
|
+
2px a mark with editorial weight — a blockquote rule, a focus ring.
|
|
143
|
+
|
|
144
|
+
On translucent surfaces use the ALPHA ramp (border-gray-alpha-400). A
|
|
145
|
+
solid border on glass doesn't participate in what's behind it and reads as
|
|
146
|
+
a pasted-on frame.
|
|
147
|
+
|
|
148
|
+
An <hr> needs border-0 before you give it a background, or you get the UA
|
|
149
|
+
border AND your line. Non-semantic dividers get aria-hidden.
|
|
150
|
+
|
|
151
|
+
── The z-index ladder ─────────────────────────────────────────────────
|
|
152
|
+
Ordered by PERMANENCE, not importance. Gaps of 10 so something can be
|
|
153
|
+
inserted without renumbering.
|
|
154
|
+
|
|
155
|
+
100 skip link — must beat everything, including modals
|
|
156
|
+
60 a once-a-session overlay — over the dock, under the skip link
|
|
157
|
+
50 fixed navigation
|
|
158
|
+
40 tap-outside catcher — under the nav, over the page
|
|
159
|
+
30 a pocket front, above the cards it contains
|
|
160
|
+
20 hover cards, dropdowns
|
|
161
|
+
10 a hovered item lifting above its siblings
|
|
162
|
+
-10 a sliding highlight, behind content inside `isolate`
|
|
163
|
+
|
|
164
|
+
60 is what the gaps were for: a greeting overlay arrived long after this
|
|
165
|
+
ladder was set and slotted in without touching an existing value.
|
|
166
|
+
|
|
167
|
+
Two pairs to understand rather than copy. 40/50: a full-screen catcher
|
|
168
|
+
must be above the page (to catch the click) and below the menu it
|
|
169
|
+
dismisses (or it eats the menu's own clicks). 60/100: a splash overlay
|
|
170
|
+
covers the navigation — or it isn't covering the page — but must never
|
|
171
|
+
cover the skip link, because a keyboard user tabbing during a two-second
|
|
172
|
+
greeting has to be able to get past it.
|
|
173
|
+
|
|
174
|
+
── Two techniques ─────────────────────────────────────────────────────
|
|
175
|
+
isolate + negative z: a highlight bar behind icons but in front of its
|
|
176
|
+
container's background. `-z-10` alone drops it behind the container;
|
|
177
|
+
`isolate` scopes the negative z inside.
|
|
178
|
+
|
|
179
|
+
pointer-events-none on a full-width strip, auto on the content: a fixed
|
|
180
|
+
nav strip spans the viewport for centring but must not swallow every click
|
|
181
|
+
along the bottom of the page.
|
|
182
|
+
|
|
183
|
+
And the strip carries NO TRANSFORM. A transform makes an element the
|
|
184
|
+
containing block for position:fixed descendants — a full-bleed w-screen
|
|
185
|
+
overlay inside it would be trapped in the strip's bounds. Same class of
|
|
186
|
+
problem as transforms weakening backdrop-filter: when something positioned
|
|
187
|
+
or filtered behaves inexplicably, check every ancestor for transform,
|
|
188
|
+
filter, perspective, contain, and will-change.
|
|
189
|
+
|
|
190
|
+
── Viewport units ─────────────────────────────────────────────────────
|
|
191
|
+
Use dvh, never vh. `vh` refers to the viewport WITHOUT mobile browser
|
|
192
|
+
chrome, so a 100vh element extends under the address bar and its bottom is
|
|
193
|
+
unreachable. `dvh` tracks the actual visible area.
|
|
194
|
+
|
|
195
|
+
── Bottom padding ─────────────────────────────────────────────────────
|
|
196
|
+
Anything with fixed bottom chrome needs pb-28 (112px) audited page by
|
|
197
|
+
page, or the last line sits under the nav. The most common "looks fine on
|
|
198
|
+
my long page" bug.
|
|
199
|
+
───────────────────────────────────────────────────────────────────────── */
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Glass Layout
|
|
2
|
+
|
|
3
|
+
> **Glass preset.** This is the layout system running on [modimihir.com](https://modimihir.com), with its real values, meant to be copied whole. Read it beside `domains/layout/guide.md`, which holds the laws; this guide holds glass's values, recipes and decisions for them, and says where glass differs. Under any other preset, `domains/layout/guide.md` leads and this guide is only a worked example.
|
|
4
|
+
|
|
5
|
+
For mostly-text interfaces, where layout should get out of the way. One decision drives the rest: **a single 640px reading measure on every page**, so moving between pages never moves the text.
|
|
6
|
+
|
|
7
|
+
## 1. One measure
|
|
8
|
+
|
|
9
|
+
```css
|
|
10
|
+
.col {
|
|
11
|
+
max-width: 640px;
|
|
12
|
+
margin: 0 auto;
|
|
13
|
+
padding: 0 32px;
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
At most 576px of text, roughly 75–85 characters at 16px: the top of comfortable, chosen for sustained reading. **Every page uses the class** (layout law 1).
|
|
18
|
+
|
|
19
|
+
- **Non-text content may leave the column**: a masonry image wall has no reading measure, so it goes full-bleed.
|
|
20
|
+
- **An article's cover image stays in it.** Bled wide, it becomes a banner *above* the article, not part of it.
|
|
21
|
+
- **Dividers are column width** (layout law 1).
|
|
22
|
+
|
|
23
|
+
## 2. Rails live outside the column, anchored to a wider frame
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<div className="relative mx-auto max-w-[1280px]">
|
|
27
|
+
{/* Absolute, so it sits outside the reading column entirely */}
|
|
28
|
+
<div className="absolute inset-y-0 left-8 hidden w-[196px] xl:block">
|
|
29
|
+
<div className="sticky top-28 max-h-[calc(100dvh-11rem)] overflow-y-auto">
|
|
30
|
+
<SectionNav … />
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className="col">{/* the reading column, untouched */}</div>
|
|
35
|
+
</div>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Rail and column share one centred 1280px frame, so their gap holds from 1280px up (layout law 4, which explains the absolute wrapper, the sticky child and the `dvh` cap). **`xl:block`**: below 1280px there's no margin, and the rail becomes a disclosure above the text (§6).
|
|
39
|
+
|
|
40
|
+
## 3. Spacing groups; asymmetry is the mechanism
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
mt-11 (44px) ← heading
|
|
44
|
+
mb-3 (12px)
|
|
45
|
+
← the paragraph it introduces
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
~3.5:1 above to below makes them one unit (layout law 2). It holds at every scale:
|
|
49
|
+
|
|
50
|
+
| Relationship | Inner | Outer | Ratio |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| Heading → its paragraph | 12px below | 44px above | 3.7:1 |
|
|
53
|
+
| List items → the next block | 8px between | 24px below | 3:1 |
|
|
54
|
+
| Bio paragraphs → the links after | 16px between | 32px after last | 2:1 |
|
|
55
|
+
| Section heading → its content | 16–32px | 36px+ between sections | ~2:1 |
|
|
56
|
+
|
|
57
|
+
**Home sections** use `py-9` (36px each side), symmetric, since they're peers. Long pages use `mt-16 border-t pt-10` (64px, hairline, 40px): the rule marks a real change of subject.
|
|
58
|
+
|
|
59
|
+
**Glass's divider** (layout law 3): the home page rules off its three substantial sections — Work, Experience, Life — and lets the two that read as a coda to Life, Watching and Socials, follow on without a rule.
|
|
60
|
+
|
|
61
|
+
→ The full spacing scale and where each value is used: `glass/layout/measure-and-rhythm.md`
|
|
62
|
+
|
|
63
|
+
## 4. The radius ladder nests
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
rounded-full pills, tags, dots
|
|
67
|
+
rounded-md 6 tooltips
|
|
68
|
+
rounded-[9px] 9 small cards in a stack
|
|
69
|
+
rounded-[10px]10 an avatar inside a button
|
|
70
|
+
rounded-xl 12 cards, hover panels, disclosures
|
|
71
|
+
rounded-[12px]12 icon buttons, highlight bars
|
|
72
|
+
rounded-2xl 16 the outer container (a dock, a sheet)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Each nesting is derived by layout law 5; the dock and the sheet are worked through in `glass/layout/radius-and-elevation.md`.
|
|
76
|
+
|
|
77
|
+
```css
|
|
78
|
+
* , *::before, *::after {
|
|
79
|
+
corner-shape: superellipse(1.5);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Glass sets the corner shape on everything, pseudo-elements included, with no exception for pills and circles.
|
|
84
|
+
|
|
85
|
+
## 5. Hairlines: 0.5px vs 1px
|
|
86
|
+
|
|
87
|
+
| Weight | For |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `0.5px` | Chrome borders, dividers, glass edges |
|
|
90
|
+
| `1px` | Card borders, rings, disclosure outlines |
|
|
91
|
+
| `2px` | Quote rules, focus rings |
|
|
92
|
+
| `1px` (`h-px`) | Table-of-contents markers |
|
|
93
|
+
|
|
94
|
+
Weight follows the job (layout law 6): dividing icon groups inside one dock, 0.5px; defining a card's edge, 1px. On translucent surfaces the edge is the **alpha ramp** (`border-gray-alpha-400`; see `glass/color/guide.md`).
|
|
95
|
+
|
|
96
|
+
## 6. Two presentations, not one responsive layout
|
|
97
|
+
|
|
98
|
+
Where pointer and touch interact differently, glass ships **two components** (layout law 8):
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
<nav aria-label="Primary" className="pointer-events-none fixed inset-x-0 bottom-5 z-50 flex justify-center">
|
|
102
|
+
<MobileDock /> {/* sm:hidden — avatar + a menu button opening a sheet */}
|
|
103
|
+
<DesktopDock /> {/* hidden sm:block — hover-revealed icons, sliding highlight */}
|
|
104
|
+
</nav>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The desktop dock reveals pages and settings **on hover**, collapsing on scroll; touch fires none of it. `SectionNav` (a sticky rail) and `SectionNavCompact` (a `<details>` disclosure) likewise render one tree through a shared `Rows`.
|
|
108
|
+
|
|
109
|
+
### The same fork, chosen by the reader
|
|
110
|
+
|
|
111
|
+
Galleries offer a masonry **wall** (everything at once) or a **roll** (one photograph above a filmstrip), picked from a small dock: same data, same route. The state lives in a module both sides subscribe to, as with theme or sound:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
let current: GalleryLayout = "grid";
|
|
115
|
+
export function setGalleryLayout(layout: GalleryLayout) {
|
|
116
|
+
current = layout;
|
|
117
|
+
for (const fn of listeners) fn(layout);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**The gallery layout doesn't persist** (layout law 9). The theme persists forever; the gallery layout lives in module memory and resets to the wall as each collection mounts. *The wall shows the whole collection, what you want on arrival; the roll is somewhere you go on purpose, and neither a previous visit nor the other gallery should decide it for you.* The switch opens on the photograph nearest the viewport's centre (`glass/motion/shared-element.md`), and each tile declares its own aspect ratio so one image travels between layouts undistorted.
|
|
122
|
+
|
|
123
|
+
→ Both docks, the table of contents, the gallery, the touch audit and the breakpoints in use: `glass/layout/adaptive-presentations.md`
|
|
124
|
+
|
|
125
|
+
## 7. Content gating: no empty states
|
|
126
|
+
|
|
127
|
+
One flag, three places (layout law 10):
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// content/index.ts — one derivation, everything else reads it.
|
|
131
|
+
export const has = { writing: posts.length > 0, /* … */ };
|
|
132
|
+
|
|
133
|
+
export const sectionHasContent = { /* home-page sections */ };
|
|
134
|
+
/** Route path → whether it should exist. Unlisted paths always exist. */
|
|
135
|
+
export const routeEnabled: Record<string, boolean> = { "/writing": has.writing, /* … */ };
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const PAGES = [
|
|
140
|
+
has.experience && { to: "/experience", … },
|
|
141
|
+
has.playground && { to: "/playground", … },
|
|
142
|
+
has.writing && { to: "/writing", … },
|
|
143
|
+
].filter(Boolean);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
An empty collection loses its **nav icon**, **route** and **home-page entry** from one flag. On the file-based router the gate is in the page, and a catch-all sends the rest, old links included, somewhere real:
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
// app/writing/page.tsx
|
|
150
|
+
export default function Page() {
|
|
151
|
+
if (!has.writing) redirect("/");
|
|
152
|
+
return <Writing />;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// app/[...rest]/page.tsx — anything that isn't a real route lands somewhere real.
|
|
156
|
+
export default function CatchAll() {
|
|
157
|
+
redirect("/");
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**`routeEnabled` outlived the client router**: the sitemap, metadata and nav still read it. Sections drop out *before* dividers are placed:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
const shown = site.sections.filter((name) => SECTIONS[name] && sectionHasContent[name]);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
// One or two folders stay folder-sized and centred rather than stretching
|
|
169
|
+
// across a three-up grid. (640px column − gaps) ÷ 3 ≈ 200px each.
|
|
170
|
+
const COLS = ["", "grid-cols-1", "grid-cols-2", "grid-cols-3"];
|
|
171
|
+
const WIDTH = ["", "max-w-[200px]", "max-w-[420px]", ""];
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Asked to shrink the folders by a quarter, shrink them and keep three to a row (layout law 8).
|
|
175
|
+
|
|
176
|
+
## 8. The z-index ladder
|
|
177
|
+
|
|
178
|
+
| z | Layer |
|
|
179
|
+
|---|---|
|
|
180
|
+
| 100 | Skip link — must beat everything |
|
|
181
|
+
| 60 | A once-a-session overlay — over the dock, under the skip link |
|
|
182
|
+
| 50 | Fixed navigation |
|
|
183
|
+
| 40 | Tap-outside catcher — under the nav, over the page |
|
|
184
|
+
| 30 | A pocket front, over its own contents |
|
|
185
|
+
| 20 | Hover cards, dropdowns |
|
|
186
|
+
| 10 | A hovered item lifting above its siblings |
|
|
187
|
+
| −10 | A sliding highlight, behind content inside `isolate` |
|
|
188
|
+
|
|
189
|
+
Ordered by permanence (layout law 7). The gaps paid off: a later greeting overlay took 60 without renumbering, covering the dock — or it wouldn't cover the page — but never the skip link, so keyboard users can tab past a two-second greeting. The pocket front at 30 is glass's own rung.
|
|
190
|
+
|
|
191
|
+
The nav strip spans the viewport for centring, so it passes clicks through:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
<nav className="pointer-events-none fixed inset-x-0 bottom-5 z-50 flex justify-center">
|
|
195
|
+
<div className="pointer-events-auto">{/* the dock */}</div>
|
|
196
|
+
</nav>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The strip carries **no transform**, which would trap the mobile sheet's full-bleed overlay inside it (layout law 7).
|
|
200
|
+
|
|
201
|
+
→ The radius nestings, hairline classes and stacking-context code: `glass/layout/radius-and-elevation.md`
|
|
202
|
+
|
|
203
|
+
## 9. Structural accessibility
|
|
204
|
+
|
|
205
|
+
Layout law 12, in glass's markup:
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
<a href="#main" className="skip">Skip to content</a>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- **`<nav>` labels**: "Primary", "On this page".
|
|
212
|
+
- **Heading levels**: `h1` page, `h2` sections, `h3` sub-sections, `h4` cards; two elements at 14px/500 can be `h3` and `h4`.
|
|
213
|
+
- **Menu triggers** carry `aria-haspopup` beside `aria-expanded` and `aria-controls`.
|
|
214
|
+
|
|
215
|
+
### A disclosure should hide its content, not remove it
|
|
216
|
+
|
|
217
|
+
Where a custom animation rules out `<details>`, glass collapses height to zero (layout law 11):
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
<motion.div
|
|
221
|
+
initial={false}
|
|
222
|
+
animate={{ height: open ? "auto" : 0 }}
|
|
223
|
+
inert={!open}
|
|
224
|
+
aria-hidden={!open}
|
|
225
|
+
className="overflow-hidden"
|
|
226
|
+
>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The content stays in the served HTML, for crawlers, `llms.txt` and find-in-page.
|
|
230
|
+
|
|
231
|
+
```css
|
|
232
|
+
@media print {
|
|
233
|
+
nav, footer, .skip { display: none; }
|
|
234
|
+
.reveal { opacity: 1; transform: none; }
|
|
235
|
+
a { color: inherit; text-decoration: none; }
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Assets
|
|
240
|
+
|
|
241
|
+
- `glass/layout/assets/layout-tokens.css` — column, spacing, radius ladder, continuous corners, hairlines, skip link, print.
|
|
242
|
+
- `glass/layout/assets/SectionNav.tsx` — two presentations of one tree, with shared row rendering.
|
|
243
|
+
|
|
244
|
+
## Checklist
|
|
245
|
+
|
|
246
|
+
The domain checklist applies in full; these are glass's values for it.
|
|
247
|
+
|
|
248
|
+
- [ ] `.col` — 640px, 32px padding — on every page; article covers stay in it.
|
|
249
|
+
- [ ] Rails in the 1280px frame: `left-8`, `w-[196px]`, `sticky top-28`, `max-h-[calc(100dvh-11rem)]`, shown from `xl`.
|
|
250
|
+
- [ ] Prose headings `mt-11` / `mb-3`; home sections `py-9`; long pages `mt-16 border-t pt-10`, the first `mt-14`.
|
|
251
|
+
- [ ] Rules between Work, Experience and Life only; Watching and Socials follow on.
|
|
252
|
+
- [ ] Radii from the ladder: 6 / 9 / 10 / 12 / 16px and full; `superellipse(1.5)` on everything.
|
|
253
|
+
- [ ] 0.5px chrome, 1px object edges, 2px marks; `gray-alpha` on translucent surfaces.
|
|
254
|
+
- [ ] `MobileDock` / `DesktopDock` and `SectionNav` / `SectionNavCompact`, sharing data and rows.
|
|
255
|
+
- [ ] The gallery opens on the wall; its layout lives in module memory.
|
|
256
|
+
- [ ] `pb-28` under the dock on every page that can end near the viewport bottom.
|
|
257
|
+
- [ ] z-index on the ladder: 100 / 60 / 50 / 40 / 30 / 20 / 10 / −10.
|
|
258
|
+
- [ ] Dynamic Tailwind classes written as literal strings.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Measure and rhythm
|
|
2
|
+
|
|
3
|
+
Glass's values for layout laws 1–3; the derivations and reasons every preset shares are in `domains/layout/measure-and-rhythm.md`.
|
|
4
|
+
|
|
5
|
+
## The measure
|
|
6
|
+
|
|
7
|
+
`.col` (§1 of the guide) is 640px outer with 32px padding each side → **576px of text**.
|
|
8
|
+
|
|
9
|
+
At 16px prose that's roughly 75–85 characters per line, at the top of the comfortable range on purpose: long lines are efficient once the reader is committed, and 1.75 leading compensates for the longer return sweep. At 14px UI copy the column holds ~85–95 characters, which is fine because that text is scanned, not read.
|
|
10
|
+
|
|
11
|
+
The padding does double duty: on a 375px phone the column is `375 − 64 = 311px`, and on desktop the 32px keeps text off the edge of a maximised window.
|
|
12
|
+
|
|
13
|
+
That 311px once forced a responsive type step (a 30px title wrapping to three lines) until an 18px type ceiling removed it: layout law 1's narrowest-measure check, settled in the scale.
|
|
14
|
+
|
|
15
|
+
640px also equals Tailwind's `sm` breakpoint — convenient, not meaningful: below it the column is padding-limited, so the two coincide naturally.
|
|
16
|
+
|
|
17
|
+
The summary and detail pairs it keeps still: home-page role summary → `/experience`, writing index → article.
|
|
18
|
+
|
|
19
|
+
## What's allowed outside the column
|
|
20
|
+
|
|
21
|
+
**Non-text content.** The image galleries are a full-bleed CSS-columns masonry on a grey field with 4px gaps; confining it to 640px would only make the images small.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
<main className="min-h-[100dvh] bg-gray-100 p-1 pb-28">
|
|
25
|
+
<div className="columns-3 gap-1 sm:columns-4">
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Note `columns-3` then `sm:columns-4`: fewer columns on a phone, more on desktop — against the mobile-first instinct for grids, and right here because the tiles are photographs.
|
|
29
|
+
|
|
30
|
+
**Rails**, anchored to a wider frame (§2 of the guide), and **fixed chrome**: the nav strip spans the viewport for centring.
|
|
31
|
+
|
|
32
|
+
## Dividers at column width
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
export function Divider() {
|
|
36
|
+
return (
|
|
37
|
+
<div className="col my-3" aria-hidden>
|
|
38
|
+
<hr className="border-0 h-[0.5px] bg-gray-alpha-300" />
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Reusing `.col` makes it exactly as wide as the text it separates. The `<hr>` reset and `aria-hidden` are layout law 6's; the half-pixel box is a gap the site still has (`glass/layout/radius-and-elevation.md`).
|
|
45
|
+
|
|
46
|
+
`my-3` (12px) on top of the sections' own `py-9` (36px) → 48px between the content of adjacent sections, the hairline centred in it.
|
|
47
|
+
|
|
48
|
+
## The spacing scale
|
|
49
|
+
|
|
50
|
+
Tailwind's 4px scale, with a preference for a few well-worn values:
|
|
51
|
+
|
|
52
|
+
| Value | px | Typical use |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `0.5` | 2 | Nudges between adjacent icons |
|
|
55
|
+
| `1` | 4 | Gallery gaps, dock padding |
|
|
56
|
+
| `1.5` | 6 | Sub-title under a heading |
|
|
57
|
+
| `2` | 8 | List items, tag gaps |
|
|
58
|
+
| `2.5` | 10 | Row padding, marker gaps |
|
|
59
|
+
| `3` | 12 | Heading → paragraph, container padding |
|
|
60
|
+
| `3.5` | 14 | Heading → list |
|
|
61
|
+
| `4` | 16 | Paragraph gaps, grid gaps |
|
|
62
|
+
| `5` | 20 | Quote indent, section heading → content |
|
|
63
|
+
| `6` | 24 | Prose paragraph gaps, meta rows |
|
|
64
|
+
| `7` | 28 | Capability groups |
|
|
65
|
+
| `8` | 32 | Section heading → content, column padding |
|
|
66
|
+
| `9` | 36 | Home section padding |
|
|
67
|
+
| `10` | 40 | Section content after a rule |
|
|
68
|
+
| `11` | 44 | Prose heading top margin |
|
|
69
|
+
| `14` | 56 | First section on a long page |
|
|
70
|
+
| `16` | 64 | Between major sections on a long page |
|
|
71
|
+
| `20`/`28` | 80/112 | Page top/bottom padding |
|
|
72
|
+
|
|
73
|
+
The values in heaviest use are `2.5`, `3.5`, `9`, `11` — the half-steps and odd numbers. They exist because 8/12/36/44 are where the *asymmetry ratios* land, not because the scale demanded them.
|
|
74
|
+
|
|
75
|
+
## Asymmetry, in detail
|
|
76
|
+
|
|
77
|
+
Layout law 2 at glass's values, beyond the guide's table (§3):
|
|
78
|
+
|
|
79
|
+
| Relationship | Inner | Outer | Ratio |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| Role title → its description | 2px | 6px | 3:1 |
|
|
82
|
+
| Case study title → body | 6px | 20px (between cards) | 3.3:1 |
|
|
83
|
+
| Capability heading → body | 8px | 32px (between capabilities) | 4:1 |
|
|
84
|
+
|
|
85
|
+
## Section rhythm
|
|
86
|
+
|
|
87
|
+
**Home page — peers.**
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
<section className="py-9"> {/* 36px symmetric */}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Long pages — hierarchy.**
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
<section className="mt-16 border-t border-gray-alpha-300 pt-10">
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
64px above the rule, 40px below. The first section on such a page uses `mt-14` (56px) with no rule — there's nothing above it to separate from.
|
|
100
|
+
|
|
101
|
+
## Not every boundary gets a rule
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
const SECTIONS = {
|
|
105
|
+
work: { node: <Work />, rule: true },
|
|
106
|
+
experience: { node: <Experience />, rule: true },
|
|
107
|
+
life: { node: <MyLife />, rule: true },
|
|
108
|
+
watching: { node: <Watching />, rule: false },
|
|
109
|
+
socials: { node: <Socials />, rule: false },
|
|
110
|
+
};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Glass's divider decision (layout law 3): Work, Experience and Life are the substantial blocks and get separated; Watching and Socials read as a **coda** to Life, so they follow without a rule. The flag lives in layout code beside the sections, never in config (`domains/layout/measure-and-rhythm.md`, section rhythm).
|
|
114
|
+
|
|
115
|
+
## Page padding
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
className="pb-20 pt-28 sm:pt-20" // long page
|
|
119
|
+
className="pb-28 pt-28 sm:pt-20" // index page
|
|
120
|
+
className="pt-28 pb-14 sm:pt-20 sm:pb-11" // hero
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Top padding is generous** — 112px, 80px on phones; a long top margin signals "this is a document."
|
|
124
|
+
|
|
125
|
+
**Bottom padding accounts for the fixed dock.** `pb-28` (112px) on pages whose content can end near the viewport bottom, so the last line is never under the nav. A page ending in a footer can use less, because the footer's own `pb-28` covers it.
|
|
126
|
+
|
|
127
|
+
## `dvh` over `vh`
|
|
128
|
+
|
|
129
|
+
Anything sized to the viewport uses `dvh` (`domains/layout/measure-and-rhythm.md`, viewport units):
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
className="min-h-[100dvh]"
|
|
133
|
+
className="max-h-[calc(100dvh-11rem)]"
|
|
134
|
+
```
|