Jump to…
snowfeat: given a new redesign and emulated terminal homepagerxkspqmsoknz1mo
1//! The design system reference: a component sheet and a rationale page.
2//!
3//! These are internal documentation that renders itself. A swatch sheet built
4//! from the same stylesheet as the product cannot drift from it — which is the
5//! whole reason to have one, and the reason it is a route rather than a
6//! screenshot in a wiki.
7
8use maud::{html, Markup};
9
10use crate::views::change::{change_chip, state_badge};
11
12/// Every colour token, as a swatch grid.
13const NEUTRALS: &[(&str, &str)] = &[
14 ("--bg", "page ground"),
15 ("--surface", "panels, cards, bars"),
16 ("--surface-raised", "controls on a surface"),
17 ("--border", "hairlines"),
18 ("--border-strong", "control edges"),
19 ("--text", "body"),
20 ("--text-dim", "secondary"),
21 ("--text-faint", "tertiary, metadata"),
22];
23
24const ACCENTS: &[(&str, &str)] = &[
25 ("--identity", "change ids, stacks, revisions"),
26 ("--action", "links, the one primary move"),
27 ("--identity-wash", "stacked rows, selected revisions"),
28];
29
30const STATES: &[(&str, &str)] = &[
31 ("--open", "open"),
32 ("--merged", "merged"),
33 ("--abandoned", "abandoned"),
34 ("--conflict", "conflicted"),
35 ("--danger", "destructive or broken"),
36];
37
38fn swatches(title: &str, tokens: &[(&str, &str)]) -> Markup {
39 html! {
40 h3 .swatch-group-title { (title) }
41 div .swatches {
42 @for (token, use_for) in tokens {
43 div .swatch {
44 span .swatch-chip style=(format!("background:var({token})")) {}
45 code .swatch-name { (token) }
46 span .faint { (use_for) }
47 }
48 }
49 }
50 }
51}
52
53fn section(id: &str, title: &str, body: Markup) -> Markup {
54 html! {
55 section .design-section id=(id) aria-labelledby=(format!("{id}-h")) {
56 h2 #(format!("{id}-h")) .design-section-title { (title) }
57 (body)
58 }
59 }
60}
61
62/// `/design` — the component sheet.
63pub fn sheet() -> Markup {
64 html! {
65 div .design-head {
66 h1 .display.design-title { "Component sheet" }
67 p .dim {
68 "Every token and control, rendered from the same stylesheet the product uses. "
69 a href="/design/rationale" { "Read the rationale" } "."
70 }
71 }
72
73 (section("tokens", "Colour tokens", html! {
74 (swatches("Neutrals", NEUTRALS))
75 (swatches("Accents", ACCENTS))
76 (swatches("State", STATES))
77 p .hint {
78 "Every token has a light-theme counterpart. Switching the theme re-renders this
79 page from the same variables, so a swatch that looks wrong here is wrong in the
80 product."
81 }
82 }))
83
84 (section("type", "Typography", html! {
85 div .type-specimens {
86 @for (spec, sample, style) in [
87 ("44 / 48 · 600", "Display", "font-size:var(--text-3xl);line-height:48px;font-weight:600;letter-spacing:-0.02em"),
88 ("24 / 32 · 600", "Page title", "font-size:var(--text-xl);line-height:32px;font-weight:600"),
89 ("20 / 28 · 600", "Section title", "font-size:var(--text-lg);line-height:28px;font-weight:600"),
90 ("16 / 24 · 400", "Lead paragraph", "font-size:var(--text-md);line-height:24px"),
91 ("14 / 21 · 400", "Body and row titles", "font-size:var(--text-base);line-height:21px"),
92 ("12.5 / 18 · 400", "Metadata, secondary", "font-size:var(--text-sm);line-height:18px;color:var(--text-dim)"),
93 ("11 · mono", "kntqzsqtwrln +18 −4", "font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-faint)"),
94 ("11 · condensed", "EYEBROW LABEL", "font-family:var(--font-condensed);font-size:var(--text-xs);font-weight:500;letter-spacing:0.06em;text-transform:uppercase;color:var(--text-dim)"),
95 ] {
96 div .type-row {
97 span .type-spec { (spec) }
98 span style=(style) { (sample) }
99 }
100 }
101 }
102 }))
103
104 (section("states", "Change states", html! {
105 div .state-list {
106 @for (state, conflicted, meaning) in [
107 ("open", false, "pushed and reviewable"),
108 ("open", true, "mid-thought, still reviewable"),
109 ("merged", false, "landed on a bookmark"),
110 ("abandoned", false, "closed without landing"),
111 ("draft", false, "the author's own flag; never inferred from a push"),
112 ] {
113 div .state-row {
114 (state_badge(state, conflicted))
115 span .dim { (meaning) }
116 }
117 }
118 }
119 p .hint.measure {
120 "Conflicted is not a fifth state — it is something an open change can be. \
121 That is why it renders as one pill rather than two, and why it is magenta \
122 rather than red: a conflict is a state, not a failure."
123 }
124 }))
125
126 (section("keys", "Keycaps", html! {
127 div .design-row {
128 @for k in ["⌘K", "j", "k", "↵", "y", "t", "esc", "?"] {
129 span .kbd { (k) }
130 }
131 }
132 p .hint.measure {
133 "Only ⌘K is bound. The rest are drawn but not wired — a keycap that does \
134 nothing is a promise the product has not kept, so they appear here and \
135 nowhere else."
136 }
137 }))
138
139 (section("diffrows", "Diff rows", html! {
140 div .filelist style="max-width:720px" {
141 @for (class, ln, text) in [
142 ("diff-hunk", "", "@@ hunk header — surface-raised, no rule"),
143 ("line-add", "137", "+ addition — add-bg with a 2px add rule"),
144 ("line-del", "136", "- deletion — del-bg with a 2px del rule"),
145 ("line-ctx", "149", " context — transparent, body colour"),
146 ] {
147 div .diffline.(class) {
148 span .diff-ln { (ln) }
149 span .diff-text { (text) }
150 }
151 }
152 }
153 }))
154
155 (section("stats", "Stat lines", html! {
156 div style="max-width:240px" {
157 @for (k, v, colour) in [
158 ("Open changes", "128", "var(--open)"),
159 ("Conflicted", "3", "var(--conflict)"),
160 ("Median time to first review", "42m", "var(--text-dim)"),
161 ] {
162 div .dotline {
163 span .dotline-key { (k) }
164 span .dotline-val style=(format!("color:{colour}")) { (v) }
165 }
166 }
167 }
168 p .hint.measure {
169 "The leader is a flexing border rather than a run of periods, so both ends \
170 sit on the same baseline whatever their lengths are."
171 }
172 }))
173
174 (section("buttons", "Buttons", html! {
175 div .design-row {
176 a .btn.btn-primary href="#buttons" { "Primary" }
177 a .btn href="#buttons" { "Default" }
178 a .btn.btn-danger href="#buttons" { "Danger" }
179 span .btn.btn-quiet-danger { "Quiet danger" }
180 span .btn.is-disabled aria-disabled="true" { "Disabled" }
181 }
182 div .design-row {
183 a .btn.btn-primary.btn-lg href="#buttons" { "Large primary" }
184 a .btn.btn-lg href="#buttons" { "Large default" }
185 }
186 p .hint.measure {
187 "Primary is filled `--action` and there is at most one on a screen. A second
188 primary button means the page has not decided what it is for. Everything
189 else is the transparent default, which reaches for the accent only on hover."
190 }
191 }))
192
193 (section("segmented", "Segmented control", html! {
194 div .design-row {
195 div .segmented role="group" aria-label="Example" {
196 a .segmented-item.is-on href="#segmented" { "rendered" }
197 a .segmented-item href="#segmented" { "source" }
198 }
199 }
200 p .hint {
201 "Two links, not a scripted toggle — each half is a real URL, so the control
202 works without JavaScript and either view can be linked to."
203 }
204 }))
205
206 (section("chips", "Chips and badges", html! {
207 div .design-row {
208 (change_chip("kmnwolvtqrsz", false))
209 (change_chip("anything", true))
210 span .chip { "private" }
211 span .chip { "draft" }
212 }
213 div .design-row {
214 (state_badge("open", false))
215 (state_badge("merged", false))
216 (state_badge("abandoned", false))
217 (state_badge("open", true))
218 }
219 p .hint {
220 "A state is never colour alone — every badge carries its own name as text, so
221 it survives a monochrome display and a colour-blind reader."
222 }
223 }))
224
225 (section("forms", "Form controls", html! {
226 div .field {
227 label for="d-text" { "Text input" }
228 input type="text" id="d-text" value="dogfood";
229 p .hint { "A hint sits under the control it explains." }
230 }
231 div .field {
232 label for="d-area" { "Textarea" }
233 textarea id="d-area" rows="3" { "Monospace, because it usually holds code." }
234 }
235 div .field {
236 label for="d-select" { "Select" }
237 select id="d-select" {
238 option { "main" }
239 option { "release" }
240 }
241 }
242 }))
243
244 (section("banners", "Banners", html! {
245 div .banner role="status" { "A neutral banner." }
246 div .banner.banner-ok role="status" { "Something worked." }
247 div .banner.banner-error role="alert" { "Something failed, and here is why." }
248 }))
249
250 (section("empty", "Empty state", html! {
251 div .empty {
252 h2 { "No changes yet" }
253 p { "Push one with " code { "jj git push" } "." }
254 p { a .btn.btn-primary href="#empty" { "New repository" } }
255 }
256 }))
257
258 (section("compare", "Comparison list", html! {
259 ul .compare {
260 li .compare-item {
261 span .compare-them {
262 span .compare-sign aria-hidden="true" { "−" }
263 "the old way"
264 }
265 span .compare-us {
266 span .compare-sign aria-hidden="true" { "+" }
267 "the new way"
268 }
269 }
270 }
271 }))
272 }
273}
274
275/// `/design/rationale` — why the system is the way it is.
276///
277/// Written against the current tokens. The prototype's own rationale page still
278/// describes the pre-brand palette, so this is not a port of that text.
279pub fn rationale() -> Markup {
280 html! {
281 article .rationale {
282 header {
283 h1 .display.design-title { "Design rationale" }
284 p .dim {
285 "One page, for the cases the component sheet does not cover. "
286 a href="/design" { "Back to the component sheet" } "."
287 }
288 }
289
290 section {
291 h2 { "Two accents, two jobs" }
292 p .dim {
293 "Gold is " em { "identity" } " — change ids, stack rails, the revision
294 timeline, the wash behind a stacked row. Every place the interface is
295 pointing at a thing that keeps its name through a rewrite. Teal is "
296 em { "action" } " — links, and the one primary control on a page. The test
297 when adding an element: does it " em { "name" } " a change, or does it "
298 em { "do" } " something? It is never both. A gold button or a teal change
299 id breaks the only colour rule the interface has, and once either starts
300 leaking the other stops meaning anything."
301 }
302 p .dim {
303 "There is deliberately no third brand colour. An earlier revision of this
304 system had one — a vermilion reserved for the wordmark and marketing
305 surfaces — and it spent its force competing with the two accents that
306 carry meaning. Identity and action are the whole palette; everything else
307 is neutral or a state."
308 }
309 }
310
311 section {
312 h2 { "Conflict is magenta, not red" }
313 p .dim {
314 "In jj a conflict is a committed, reviewable state — not an error. Colouring
315 it red would teach the wrong model on the first encounter and take a long
316 time to unteach. Red is reserved for destructive actions and for things that
317 are actually broken."
318 }
319 }
320
321 section {
322 h2 { "The type pairing" }
323 p .dim {
324 "Mono carries the content: identifiers, paths, diffs, timestamps, numbers.
325 IBM Plex Sans carries the interface around it. Plex Sans Condensed does two
326 jobs — uppercase at 11px for labels and column headers, where it buys
327 horizontal room in dense tables and subordinates the label to its value; and
328 large and bold for display headings, where it gives the marketing surfaces a
329 voice the product surfaces do not have. Base is 14px on 21px. If a new
330 element holds data, set it in mono; if it explains data, sans; if it names a
331 column of data, condense it."
332 }
333 p .dim {
334 "All three faces are self-hosted Latin subsets. A forge should not tell a
335 third party which repository you are reading, which is what a font CDN
336 learns on every page load."
337 }
338 }
339
340 section {
341 h2 { "Density over decoration" }
342 p .dim {
343 "Radius is 3px, borders are one hairline, and shadows appear only on things
344 that genuinely float. The pages that matter — change lists, diffs, file
345 trees — are read for hours, and every pixel of chrome is one not spent on
346 code. Rows are 28 to 34px, tables are separated by hairlines rather than
347 boxed, and the only decorative element in the whole system is the tagline
348 strip on the landing page."
349 }
350 p .dim {
351 "Nothing animates. The design this was ported from types into a terminal
352 and scrolls a marquee; neither ships. A console that types at you is
353 indistinguishable from one showing live output, and a permanently moving
354 strip is a permanently moving object on the page for a reader who did not
355 ask for one."
356 }
357 }
358
359 section {
360 h2 { "Nothing depends on JavaScript" }
361 p .dim {
362 "Progressive enhancement is a hard requirement, so every control here has a
363 no-script form: toggles are links to real URLs, filters are GET forms, and
364 the line numbers in a blob are anchors. htmx is loaded for enhancement only.
365 A control that cannot be built this way is a control this system does not
366 have."
367 }
368 }
369 }
370 }
371}

371 lines · Rust