Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
369 lines (342 loc) · 29.7 KB
/
Copy pathindex.html
File metadata and controls
369 lines (342 loc) · 29.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spinbox brand</title>
<link rel="icon" href="logos/favicon-64.png">
<link rel="stylesheet" href="dist/tokens.css">
<style>
@font-face { font-family: 'IBM Plex Sans'; src: url(fonts/ibm-plex-sans-latin-400-normal.woff2) format('woff2'); font-weight: 400; }
@font-face { font-family: 'IBM Plex Sans'; src: url(fonts/ibm-plex-sans-latin-500-normal.woff2) format('woff2'); font-weight: 500; }
@font-face { font-family: 'IBM Plex Sans'; src: url(fonts/ibm-plex-sans-latin-600-normal.woff2) format('woff2'); font-weight: 600; }
@font-face { font-family: 'IBM Plex Mono'; src: url(fonts/ibm-plex-mono-latin-400-normal.woff2) format('woff2'); font-weight: 400; }
@font-face { font-family: 'IBM Plex Mono'; src: url(fonts/ibm-plex-mono-latin-600-normal.woff2) format('woff2'); font-weight: 600; }
/* Everything below is drawn from dist/tokens.css: a color here that is not a token is a bug. */
* { box-sizing: border-box; }
body { margin: 0; background: var(--spin-page); color: var(--spin-text); font: var(--spin-text-reading)/var(--spin-line-height) var(--spin-font-sans); }
.see-protan { filter: url(#protan); } .see-deutan { filter: url(#deutan); } .see-tritan { filter: url(#tritan); }
.bar { position: sticky; top: 0; z-index: 5; display: flex; flex-wrap: wrap; gap: .75rem 1.5rem; align-items: center; padding: .75rem 1.25rem; background: var(--spin-surface); border-bottom: 1px solid var(--spin-border); font: 500 13px var(--spin-font-mono); }
.bar label { display: inline-flex; gap: .4rem; align-items: center; color: var(--spin-muted); }
.bar select, .bar button { font: inherit; color: var(--spin-text); background: var(--spin-raised); border: 1px solid var(--spin-border-strong); border-radius: var(--spin-radius-sm); min-height: var(--spin-target); padding: 0 .5rem; }
.wrap { max-width: 1280px; margin: 0 auto; padding: 1.5rem 1.25rem 4rem; }
h1, h2, h3 { font-family: var(--spin-font-mono); font-weight: var(--spin-heading-weight); letter-spacing: var(--spin-heading-tracking); color: var(--spin-text); }
.label { font: 600 12px var(--spin-font-mono); letter-spacing: .1em; text-transform: uppercase; color: var(--spin-label); }
section { margin-top: 3rem; }
section > .label { display: block; border-top: 1px solid var(--spin-border); padding-top: 1.5rem; margin-bottom: 1rem; }
.muted { color: var(--spin-muted); }
a { color: var(--spin-accent-text); text-underline-offset: .2em; }
:focus-visible { outline: 2px solid var(--spin-accent); outline-offset: 2px; }
/* The masthead: what this page is, and the view's controls. */
.masthead { background: var(--spin-surface); border-bottom: 1px solid var(--spin-border); }
.masthead .wrap { padding-block: 1.5rem 1.25rem; }
.masthead h1 { font-size: clamp(1.6rem, 1rem + 2vw, 2.4rem); margin: .75rem 0 .25rem; }
.masthead p { margin: 0 0 1rem; max-width: 60rem; }
.controls { display: flex; flex-wrap: wrap; gap: .75rem 1.25rem; }
.controls label { display: inline-flex; gap: .5rem; align-items: center; font: 500 13px var(--spin-font-mono); color: var(--spin-muted); }
.controls select { appearance: none; font: 500 13px var(--spin-font-mono); color: var(--spin-text); background: var(--spin-raised) linear-gradient(45deg, transparent 50%, var(--spin-muted) 50%) right 12px center / 5px 5px no-repeat; border: 1px solid var(--spin-border-strong); border-radius: var(--spin-radius-sm); min-height: 32px; padding: 0 28px 0 .6rem; cursor: pointer; }
.lockup { display: inline-flex; align-items: center; gap: .5rem; font: 600 20px var(--spin-font-mono); color: var(--spin-text); }
.lockup img { width: 32px; height: 32px; border-radius: 22%; }
.lockup.big { font-size: 40px; gap: 1rem; } .lockup.big img { width: 64px; height: 64px; }
:root[data-theme='light'] .on-dark, :root:not([data-theme='light']) .on-light { display: none !important; }
/* The index: every section a click away, while the page scrolls. */
.index { position: sticky; top: 0; z-index: 4; background: color-mix(in srgb, var(--spin-page) 92%, transparent); backdrop-filter: blur(6px); border-bottom: 1px solid var(--spin-border); }
.index .wrap { display: flex; gap: .25rem 1rem; padding-block: .5rem; overflow-x: auto; white-space: nowrap; scrollbar-width: none; }
.index a { font: 500 12px var(--spin-font-mono); color: var(--spin-muted); text-decoration: none; padding: .35rem .15rem; }
.index a:hover { color: var(--spin-accent-text); }
.index a[href="#in-use"], .index a[href="#decisions"], .index a[href="#checks"] { color: var(--spin-label); margin-left: .5rem; }
section, .part { scroll-margin-top: 3rem; }
/* A part: what follows is sample content, a decision, or a check. */
.part { display: flex; gap: 1rem; align-items: baseline; margin-top: 4rem; padding: 1.25rem 0; border-top: 2px solid var(--spin-border-strong); }
.part .num { font: 600 2rem var(--spin-font-mono); color: var(--spin-label); line-height: 1; }
.part h2 { margin: 0; font-size: 1.4rem; } .part p { margin: .25rem 0 0; max-width: 52rem; }
#page > .part:first-child { margin-top: 1.5rem; border-top: 0; }
.perc-wrap summary { cursor: pointer; font: 500 13px var(--spin-font-mono); padding: .5rem 0; color: var(--spin-text); }
/* The mark's guide */
.guide { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 2rem; align-items: start; }
.tiles { display: grid; gap: 1rem; }
.tiles figure, .donts figure { margin: 0; }
.ground { display: grid; place-items: center; height: 200px; border-radius: var(--spin-radius-lg); border: 1px solid var(--spin-border); }
.ground.dark { background: var(--spin-brand-navy); } .ground.light { background: #fff; }
.ground.dark img { border-radius: 22%; }
figcaption { font: 12px var(--spin-font-mono); color: var(--spin-muted); padding-top: .4rem; }
.rules h3 { font-size: 14px; margin: 1.25rem 0 .5rem; } .rules h3:first-child { margin-top: 0; }
.sizes { display: flex; gap: 1.25rem; align-items: flex-end; flex-wrap: wrap; }
.space { display: inline-block; padding: 18px; outline: 1px dashed var(--spin-border-strong); }
.space img { display: block; }
.donts { display: flex; gap: 1.25rem; flex-wrap: wrap; }
.donts figure { text-align: center; } .donts figcaption::before { content: '✕ '; color: var(--spin-error); }
/* The palette, by what each color is for */
.group { display: grid; grid-template-columns: minmax(10rem, 14rem) 1fr; gap: 1rem 1.5rem; padding: 1rem 0; border-top: 1px solid var(--spin-border); }
.group h3 { font-size: 14px; margin: 0 0 .25rem; } .group p { margin: 0; font-size: 13px; }
.group .example { margin-top: .75rem; display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; font-size: 14px; }
/* The docs' contents on a narrow screen: a disclosure where the side column was. */
.otp { display: none; margin-bottom: 1rem; border: 1px solid var(--spin-border); border-radius: var(--spin-radius-sm); background: var(--spin-raised); }
.otp summary { cursor: pointer; padding: .6rem .8rem; min-height: var(--spin-target-touch); display: flex; align-items: center; font: 500 13px var(--spin-font-mono); }
.otp a { display: block; padding: .5rem .8rem; border-top: 1px solid var(--spin-border); text-decoration: none; }
/* Swatches */
.swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: .5rem; }
.sw { border: 1px solid var(--spin-border); border-radius: var(--spin-radius); overflow: hidden; background: var(--spin-raised); }
.sw i { display: block; height: 56px; }
.sw b, .sw code { display: block; padding: .3rem .6rem; font: 12px var(--spin-font-mono); }
.sw code { color: var(--spin-muted); padding-top: 0; }
.sw-inline { display: inline-flex; align-items: center; padding: .35rem .7rem; border: 1px solid var(--spin-border); border-radius: var(--spin-radius-sm); font: 12px var(--spin-font-mono); }
/* The site */
.site { display: grid; grid-template-columns: 1fr; border: 1px solid var(--spin-border); border-radius: var(--spin-radius-lg); overflow: hidden; }
.site header { display: flex; align-items: center; gap: .75rem; padding: .75rem 1rem; background: var(--spin-surface); border-bottom: 1px solid var(--spin-border); }
.site header img { width: 32px; height: 32px; border-radius: 22%; }
.site header b { font: 600 18px var(--spin-font-mono); }
.site header .search { margin-left: auto; min-width: 12rem; min-height: var(--spin-target); border: 1px solid var(--spin-border); border-radius: var(--spin-radius); padding: .25rem .75rem; color: var(--spin-muted); background: var(--spin-page); }
.banner { padding: .5rem 1rem; text-align: center; font-size: 14px; background: var(--spin-accent-selected); border-bottom: 1px solid var(--spin-border); }
.hero { display: grid; gap: 2rem; padding: 3rem 2rem; grid-template-columns: 7fr 5fr; align-items: center; }
.hero h1 { font-size: clamp(2rem, 1rem + 3vw, 3.2rem); line-height: 1.08; margin: .5rem 0 1rem; }
.hero img { width: 100%; max-width: 20rem; justify-self: center; border-radius: 22%; }
.btn { display: inline-flex; align-items: center; justify-content: center; min-height: 40px; padding: 0 1.1rem; border-radius: 999px; font: 500 15px var(--spin-font-sans); border: 1px solid transparent; cursor: pointer; text-decoration: none; }
.btn.primary { background: var(--spin-accent); color: var(--spin-on-accent); }
.btn.ghost { color: var(--spin-text); border-color: var(--spin-border-strong); background: transparent; }
.btn.danger { background: var(--spin-destructive); color: #fff; border-radius: var(--spin-radius); }
.doc { display: grid; grid-template-columns: var(--spin-sidebar) 1fr 15rem; border-top: 1px solid var(--spin-border); }
.doc nav { background: var(--spin-surface); padding: 1rem; font-size: 14px; border-right: 1px solid var(--spin-border); }
.doc nav a { display: block; color: var(--spin-text); text-decoration: none; padding: .3rem .6rem; border-radius: var(--spin-radius-sm); min-height: var(--spin-target); }
.doc nav a:hover { background: var(--spin-accent-hover); }
.doc nav a.current { background: var(--spin-accent-selected); color: var(--spin-accent-text); font-weight: 600; }
.doc article { padding: 2rem; max-width: calc(var(--spin-measure) + 4rem); }
.doc aside { padding: 1rem; font-size: 13px; border-left: 1px solid var(--spin-border); color: var(--spin-muted); }
pre { background: var(--spin-raised); border: 1px solid var(--spin-border); border-radius: var(--spin-radius-sm); padding: .9rem 1rem; overflow-x: auto; font: 14px/1.55 var(--spin-font-mono); color: var(--spin-text); }
code { font-family: var(--spin-font-mono); }
p code, td code { background: var(--spin-raised); border: 1px solid var(--spin-border); border-radius: 4px; padding: 0 .3em; font-size: .9em; }
.aside { border-left: 3px solid var(--spin-warning); background: var(--spin-raised); padding: .75rem 1rem; border-radius: var(--spin-radius-sm); }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { text-align: left; padding: .55rem .6rem; border-bottom: 1px solid var(--spin-border); }
th { color: var(--spin-muted); font-weight: 500; }
/* The dashboard */
.dash { display: grid; grid-template-columns: 14rem 1fr; border: 1px solid var(--spin-border); border-radius: var(--spin-radius-lg); overflow: hidden; font-size: var(--spin-text-dense); line-height: 1.45; }
.dash nav { background: var(--spin-surface); padding: 1rem .75rem; border-right: 1px solid var(--spin-border); }
.dash nav .cap { font: 600 11px var(--spin-font-mono); color: var(--spin-muted); letter-spacing: .08em; text-transform: uppercase; margin: 1rem .5rem .4rem; }
.dash nav a { display: block; padding: .35rem .5rem; border-radius: var(--spin-radius-sm); color: var(--spin-text); text-decoration: none; }
.dash nav a.current { background: var(--spin-accent-selected); color: var(--spin-accent-text); }
.dash main { padding: 1.25rem 1.5rem; }
.dash .tools { display: flex; gap: .5rem; align-items: center; margin: .75rem 0 1rem; flex-wrap: wrap; }
.dash input { font: inherit; color: var(--spin-text); background: var(--spin-raised); border: 1px solid var(--spin-border-strong); border-radius: var(--spin-radius-sm); min-height: 34px; padding: 0 .6rem; min-width: 14rem; }
.dash input::placeholder { color: var(--spin-muted); }
.dash .btn { min-height: 34px; border-radius: var(--spin-radius); font-size: 14px; }
.state { display: inline-flex; align-items: center; gap: .4rem; font: 500 12px var(--spin-font-mono); }
.state::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
.state.ok { color: var(--spin-success); } .state.warn { color: var(--spin-warning); } .state.err { color: var(--spin-error); } .state.idle { color: var(--spin-muted); }
.state.warn::before { border-radius: 1px; transform: rotate(45deg); } .state.err::before { border-radius: 0; }
.panel { background: var(--spin-raised); border: 1px solid var(--spin-border); border-radius: var(--spin-radius); padding: 1rem; }
.dialog { background: var(--spin-overlay); border: 1px solid var(--spin-border); border-radius: var(--spin-radius-lg); padding: 1.25rem; max-width: 26rem; box-shadow: 0 16px 40px rgb(0 0 0 / .35); }
.toast { display: inline-flex; gap: .6rem; align-items: center; background: var(--spin-overlay); border: 1px solid var(--spin-border); border-left: 3px solid var(--spin-success); border-radius: var(--spin-radius); padding: .6rem .9rem; }
.row { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; margin-top: 1rem; }
/* Widths */
.frames { display: flex; gap: 1.5rem; flex-wrap: wrap; align-items: flex-start; }
.frame { border: 1px solid var(--spin-border-strong); border-radius: 28px; padding: 10px; background: var(--spin-surface); }
.frame iframe { display: block; border: 0; border-radius: 18px; background: var(--spin-page); }
.frame figcaption { font: 12px var(--spin-font-mono); color: var(--spin-muted); text-align: center; padding-top: .4rem; }
/* Perception */
.perc td.fail { color: var(--spin-error); font-weight: 600; }
.perc { font: 12px var(--spin-font-mono); }
/* A grid's column shrinks below what is in it, so a long command scrolls in its block rather
than pushing the page wider than the phone. */
.hero > *, .doc > *, .dash > *, .doc article, .dash main { min-width: 0; }
/* Narrow: the same pieces, stacked, as a phone shows them. */
@media (max-width: 760px) {
.hero, .doc, .dash { grid-template-columns: 1fr; }
.doc nav, .doc aside, .dash nav { display: none; }
.hero { padding: 2rem 1rem; } .doc article { padding: 1.25rem 1rem; }
.hero { gap: 1rem; padding: 1.5rem 1rem; } .hero h1 { font-size: 1.6rem; margin-top: .25rem; } .hero img { max-width: 6.5rem; }
.part { margin-top: 2.5rem; } .part .num { font-size: 1.5rem; }
.wrap { padding-inline: 1rem; } table { font-size: 13px; }
.site header .search { display: none; }
.btn, .dash input, .dash .btn { min-height: var(--spin-target-touch); }
.otp { display: block; }
.guide, .group { grid-template-columns: 1fr; }
.controls select { min-height: var(--spin-target-touch); }
}
body.embed .bar, body.embed .only-full { display: none; }
body.embed .wrap { padding-top: 0; }
</style>
</head>
<body>
<div id="filters"></div>
<header class="masthead only-full">
<div class="wrap">
<div class="lockup"><img class="on-dark" src="logos/mark-256.png" alt=""><img class="on-light" src="logos/mark-clear-256.png" alt=""><span>Spinbox</span></div>
<h1>Spinbox brand system</h1>
<p class="muted">One palette, one mark and one type for the site, the dashboard and everything else that draws Spinbox - each color checked here, and copied from here by each of them. This page is the laboratory; the product is at <a href="https://spin-stack.github.io/website/">the website</a>.</p>
<div class="controls" role="group" aria-label="View">
<label>Mode <select id="mode"><option>dark</option><option>light</option></select></label>
<label>See as <select id="see"><option value="">as drawn</option><option value="protan">protanopia</option><option value="deutan">deuteranopia</option><option value="tritan">tritanopia</option></select></label>
</div>
</div>
</header>
<nav class="index only-full" aria-label="On this page">
<div class="wrap">
<a href="#in-use">1 · In use</a><a href="#site">Site</a><a href="#dashboard">Dashboard</a><a href="#app">App</a>
<a href="#decisions">2 · Decisions</a><a href="#mark">Mark</a><a href="#palette-section">Palette</a>
<a href="#checks">3 · Checks</a><a href="#widths">Widths</a><a href="#perception">Perception</a><a href="screens.html">Every screen →</a>
</div>
</nav>
<div class="wrap" id="page">
<div class="part only-full" id="in-use"><span class="num">1</span><div><h2>In use</h2><p class="muted">Sample screens drawn from the tokens. The words, rows and names in them are fixtures, there to show the palette at work: nothing in them is a decision.</p></div></div>
<section id="site">
<span class="label">The site</span>
<div class="site">
<header><img class="on-dark" src="logos/mark-256.png" alt=""><img class="on-light" src="logos/mark-clear-256.png" alt=""><b>Spinbox</b><span class="search">Search</span></header>
<div class="banner"><strong>Spinbox is experimental: APIs change and data may be lost.</strong> <a href="#">Status</a> lists what is built and what proves it.</div>
<div class="hero">
<div>
<span class="label">spin-stack · self-hosted sandboxes</span>
<h1>A virtual machine per workspace. A snapshot before, a diff after.</h1>
<p class="muted">Sandboxes for people and agents that run in your own AWS account.</p>
<p><a class="btn primary" href="#">Evaluate →</a> <a class="btn ghost" href="#">Install on AWS</a></p>
</div>
<img class="on-dark" src="logos/mark-512.png" alt=""><img class="on-light" src="logos/mark-clear-512.png" alt="">
</div>
<div class="doc">
<nav><a href="#">What is Spinbox</a><a href="#">Status</a><a class="current" href="#">Snapshots and rollback</a><a href="#">Network and egress</a><a href="#">Ports and sharing</a></nav>
<article>
<details class="otp"><summary>On this page: Snapshots and rollback</summary><a href="#">Taking one</a><a href="#">Going back</a><a href="#">Seeing what changed</a></details>
<h2>Snapshots and rollback</h2>
<p>A snapshot is a point in a workspace's life that you can come back to. It lives in the installation's bucket, so it does not depend on any runner. <a href="#">Restore</a> puts the disk back; <code>--with-memory</code> keeps the processes.</p>
<pre>spin snapshot create demo --description clean --with-memory --wait
spin snapshot diff demo <after-id> --from <clean-id> --under /etc</pre>
<pre>A /etc/greeting
M /etc/hosts
D /etc/issue</pre>
<div class="aside"><strong>Caution.</strong> A memory snapshot resumes only on a runner that carries the same machine.</div>
<table><tr><th>You want to</th><th>Spinbox gives you</th></tr><tr><td>See what changed</td><td>A file-by-file diff between two snapshots</td></tr><tr><td>Undo</td><td>Restore the disk, or resume with the processes</td></tr></table>
</article>
<aside><div class="label">On this page</div><p>Taking one<br>Going back<br>Seeing what changed</p></aside>
</div>
</div>
</section>
<section id="dashboard">
<span class="label">The dashboard</span>
<div class="dash">
<nav><div class="cap">Work</div><a class="current" href="#">Workspaces</a><a href="#">Snapshots</a><a href="#">Teams</a><div class="cap">Administer</div><a href="#">Hosts</a><a href="#">Settings</a></nav>
<main>
<h3 style="margin:0">Workspaces</h3>
<div class="tools"><input placeholder="Search workspaces"><button class="btn primary">New workspace</button><button class="btn ghost">Refresh</button></div>
<table>
<tr><th>Name</th><th>State</th><th>Host</th><th>Last snapshot</th></tr>
<tr><td><a href="#">api</a></td><td><span class="state ok">running</span></td><td><code>runner-a1</code></td><td>2 min ago</td></tr>
<tr><td><a href="#">agent-1</a></td><td><span class="state warn">waiting for a host</span></td><td>—</td><td>—</td></tr>
<tr><td><a href="#">ci-job</a></td><td><span class="state err">failed</span></td><td><code>runner-b2</code></td><td>1 h ago</td></tr>
<tr><td><a href="#">notes</a></td><td><span class="state idle">stopped</span></td><td>—</td><td>yesterday</td></tr>
</table>
<div class="row">
<div class="dialog"><h3 style="margin-top:0">Delete agent-1?</h3><p class="muted">It goes to the trash with its disk; you can undelete it until its days are up.</p><button class="btn danger">Delete</button> <button class="btn ghost">Cancel</button></div>
<div class="toast"><span class="state ok">done</span> Snapshot <code>clean</code> published</div>
</div>
</main>
</div>
</section>
<section class="only-full" id="app">
<span class="label">The app, as it is</span>
<p class="muted">Every screen of it in both themes, and what each draws that this palette does not have, is <a href="screens.html">the catalog</a>. The screenshots the site shows are of the spin release it documents, put on that release by spin's site-shots.yml: <a href="https://spin-stack.github.io/website/use/dashboard/">the dashboard</a>.</p>
</section>
<div class="part only-full" id="decisions"><span class="num">2</span><div><h2>Decisions</h2><p class="muted">What the brand is: the mark and how it is used, and the palette, each color by what it is for. These are what palette.toml and the mark's cut hold.</p></div></div>
<section class="only-full" id="mark">
<span class="label">The mark</span>
<div class="guide">
<div class="tiles">
<figure><div class="ground dark"><img src="logos/mark-512.png" alt="The mark on its own ground" width="128"></div><figcaption>The tile, on its own ground: a dark page, the favicon, a link preview.</figcaption></figure>
<figure><div class="ground light"><img src="logos/mark-clear-512.png" alt="The mark without its ground" width="128"></div><figcaption>Clear: on a light page, a light header.</figcaption></figure>
</div>
<div class="rules">
<h3>Sizes</h3>
<div class="sizes"><img src="logos/mark-clear-256.png" width="64" alt=""><img src="logos/mark-clear-256.png" width="32" alt=""><img src="logos/favicon-32.png" width="16" alt=""><span class="muted">64 · 32 · 16 px. Below 24 px the favicon's tile, never the clear mark: its seams close up.</span></div>
<h3>Clear space</h3>
<div class="space"><img src="logos/mark-clear-256.png" width="72" alt=""></div>
<p class="muted">A quarter of the mark's height on every side, dashed above: no text or edge inside it.</p>
<h3>Lockup</h3>
<div class="lockup big"><img class="on-dark" src="logos/mark-256.png" alt=""><img class="on-light" src="logos/mark-clear-256.png" alt=""><span>Spinbox</span></div>
<p class="muted">The mark, then <code>Spinbox</code> in IBM Plex Mono 600 - the word the prose uses, capitalised, its cap height the mark's middle disk.</p>
<h3>Not</h3>
<div class="donts">
<figure><img src="logos/mark-clear-256.png" width="56" alt="" style="transform:scaleX(1.5)"><figcaption>stretched</figcaption></figure>
<figure><img src="logos/mark-clear-256.png" width="56" alt="" style="filter:hue-rotate(140deg)"><figcaption>recolored</figcaption></figure>
<figure><div style="background:var(--spin-accent);padding:6px;border-radius:8px"><img src="logos/mark-clear-256.png" width="44" alt=""></div><figcaption>on the accent</figcaption></figure>
<figure><div style="background:#fff;padding:6px;border-radius:8px"><img src="logos/mark-256.png" width="44" alt="" style="border-radius:22%"></div><figcaption>the tile on white</figcaption></figure>
</div>
</div>
</div>
</section>
<section class="only-full" id="compositions">
<span class="label">Compositions</span>
<div class="rules">
<p class="muted">The same type and colours, three ways of laying a page out - each a page of this laboratory above, not a mock from the colours. Where the tokens stop and a decision begins: a token says how a border is drawn, these say where one is.</p>
<h3>Marketing</h3>
<p class="muted">The site's landing: a hero that says what to do before what it is, the story as one terminal, three capabilities as cards with their proof beside them and the rest as a list. The reading measure is <code>--spin-measure</code>; a section is a heading and what is under it, never a box.</p>
<h3>Documentation</h3>
<p class="muted">A Starlight page: one column at the reading measure, the sidebar at <code>--spin-sidebar</code>, headings in the mono. A claim links to the row of the status page that proves it.</p>
<h3>Application</h3>
<p class="muted">The dashboard, read for diagnosis. A page grows only as wide as what it shows: a document or a form to <code>--spin-width-contained</code>, one thing and what is around it to <code>--spin-width-detail</code>, a table of many to <code>--spin-width-wide</code>, an explorer to the viewport. A page is a header - the title, its state, its context and its one primary action on one row - and sections of rows between hairlines; the name beside the mark is <code>--spin-wordmark</code>, which the catalog audit holds the sidebar to.</p>
</div>
</section>
<section class="only-full" id="palette-section">
<span class="label">Palette</span>
<div id="palette"></div>
</section>
<div class="part only-full" id="checks"><span class="num">3</span><div><h2>Checks</h2><p class="muted">What holds the decisions: the same page at a phone's and a tablet's width, and every pair of colors task palette measures.</p></div></div>
<section class="only-full" id="widths">
<span class="label">Widths</span>
<p class="muted">This page at a phone's and a tablet's width (<code>layout.phone</code>, <code>layout.tablet</code>), in the mode and the view chosen above.</p>
<div class="frames">
<figure class="frame"><iframe id="phone" width="390" height="760" title="Phone"></iframe><figcaption>390 px</figcaption></figure>
<figure class="frame"><iframe id="tablet" width="768" height="760" title="Tablet"></iframe><figcaption>768 px</figcaption></figure>
</div>
</section>
<section class="only-full" id="perception">
<span class="label">Perception</span>
<p class="muted">Every pair <code>task palette</code> holds, as drawn and in each view (<code>dist/perception.js</code>). Contrast is WCAG 2.2's; distance is CIEDE2000.</p>
<details class="perc-wrap"><summary id="perc-summary">Every pair</summary><table class="perc" id="perc"></table></details>
</section>
</div>
<script type="module">
import { perception } from './dist/perception.js';
const q = new URLSearchParams(location.search);
const mode = document.getElementById('mode'), see = document.getElementById('see');
if (q.has('embed')) document.body.classList.add('embed');
function apply() {
document.documentElement.dataset.theme = mode.value;
document.getElementById('page').className = 'wrap' + (see.value ? ' see-' + see.value : '');
for (const id of ['phone', 'tablet']) document.getElementById(id).src = `index.html?embed=1&mode=${mode.value}&see=${see.value}`;
const s = new URLSearchParams(location.search); s.set('mode', mode.value); s.set('see', see.value);
if (!q.has('embed')) history.replaceState(null, '', '?' + s);
swatches();
}
mode.value = q.get('mode') || 'dark'; see.value = q.get('see') || '';
mode.onchange = see.onchange = apply;
fetch('dist/simulate.svg').then((r) => r.text()).then((t) => (document.getElementById('filters').innerHTML = t));
const groups = [
['Surfaces', 'Behind everything, a step off it, cards and code, menus and dialogs, and the lines between.', ['page', 'surface', 'raised', 'overlay', 'border', 'border-strong'],
'<span class="sw-inline" style="background:var(--spin-surface)">surface</span><span class="sw-inline" style="background:var(--spin-raised)">raised</span><span class="sw-inline" style="background:var(--spin-overlay)">overlay</span>'],
['Content', 'Text, secondary text, and a label that names a section or a command.', ['text', 'muted', 'label'],
'<span>Text</span> <span class="muted">secondary</span> <span class="label">a label</span>'],
['Actions', 'The one primary action per view, a link, and what destroys.', ['accent', 'accent-text', 'on-accent', 'destructive'],
'<a class="btn primary" href="#">Primary</a> <a href="#">a link</a> <a class="btn danger" href="#">Delete</a>'],
['States', 'Ran or succeeded, waits, failed: each with a shape too, never by hue alone.', ['success', 'warning', 'error'],
'<span class="state ok">running</span> <span class="state warn">waiting</span> <span class="state err">failed</span>'],
['Interaction', 'The accent over a surface: a row under the pointer, and a selected one.', ['accent-hover', 'accent-selected'],
'<span class="sw-inline" style="background:var(--spin-accent-hover)">hovered</span><span class="sw-inline" style="background:var(--spin-accent-selected);color:var(--spin-accent-text)">selected</span>'],
];
function swatches() {
const cs = getComputedStyle(document.documentElement);
document.getElementById('palette').innerHTML = groups.map(([name, what, roles, example]) =>
`<div class="group"><div><h3>${name}</h3><p class="muted">${what}</p><div class="example">${example}</div></div><div class="swatches">` +
roles.map((r) => { const v = cs.getPropertyValue('--spin-' + r).trim(); return `<div class="sw"><i style="background:${v}"></i><b>${r}</b><code>${v}</code></div>`; }).join('') +
'</div></div>').join('');
}
const views = ['drawn','ips','dim','protan','deutan','tritan','protan+ips','deutan+ips'];
document.getElementById('perc').innerHTML = '<tr><th>mode</th><th>kind</th><th>pair</th><th>floor</th>' + views.map((v) => `<th>${v}</th>`).join('') + '</tr>' +
perception.map((r) => `<tr><td>${r.Mode}</td><td>${r.Kind}</td><td>${r.What}</td><td>${r.Distance ? r.Floor + '/' + r.Worst : r.Floor + ':1'}</td>` +
(r.Distance ? views.map((v) => `<td class="${r.Distance[v] < (v === 'drawn' ? r.Floor : r.Worst) ? 'fail' : ''}">${r.Distance[v]}</td>`).join('')
: `<td class="${r.OK ? '' : 'fail'}" colspan="8">${r.Contrast}:1</td>`) + '</tr>').join('');
const held = perception.filter((r) => r.OK).length;
document.getElementById('perc-summary').textContent = `Every pair: ${held} of ${perception.length} hold their floors - open the table`;
apply();
</script>
</body>
</html>