its-swiss 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +57 -0
- data/LICENSE +21 -0
- data/README.md +262 -0
- data/app/assets/javascripts/its_swiss/clipboard_controller.js +33 -0
- data/app/assets/stylesheets/its-swiss.css +16 -0
- data/app/assets/stylesheets/its_swiss/components.css +403 -0
- data/app/assets/stylesheets/its_swiss/grid.css +67 -0
- data/app/assets/stylesheets/its_swiss/reset.css +50 -0
- data/app/assets/stylesheets/its_swiss/specimen.css +37 -0
- data/app/assets/stylesheets/its_swiss/tokens.css +142 -0
- data/app/assets/stylesheets/its_swiss/transitions.css +47 -0
- data/app/assets/stylesheets/its_swiss/type.css +109 -0
- data/app/controllers/its_swiss/specimen_controller.rb +24 -0
- data/app/helpers/its_swiss/application_helper.rb +73 -0
- data/app/views/its_swiss/shared/_errors.html.erb +14 -0
- data/app/views/its_swiss/shared/_flash.html.erb +8 -0
- data/app/views/its_swiss/shared/_masthead.html.erb +15 -0
- data/app/views/its_swiss/shared/_pagination.html.erb +26 -0
- data/app/views/its_swiss/specimen/_buttons.html.erb +15 -0
- data/app/views/its_swiss/specimen/_footer.html.erb +7 -0
- data/app/views/its_swiss/specimen/_form.html.erb +18 -0
- data/app/views/its_swiss/specimen/_grid.html.erb +14 -0
- data/app/views/its_swiss/specimen/_masthead.html.erb +15 -0
- data/app/views/its_swiss/specimen/_messages.html.erb +13 -0
- data/app/views/its_swiss/specimen/_pagination.html.erb +4 -0
- data/app/views/its_swiss/specimen/_pairs.html.erb +8 -0
- data/app/views/its_swiss/specimen/_table.html.erb +15 -0
- data/app/views/its_swiss/specimen/_take.html.erb +19 -0
- data/app/views/its_swiss/specimen/_type.html.erb +24 -0
- data/app/views/its_swiss/specimen/_values.html.erb +18 -0
- data/app/views/its_swiss/specimen/show.html.erb +27 -0
- data/app/views/layouts/its_swiss/shell.html.erb +55 -0
- data/config/importmap.rb +5 -0
- data/config/routes.rb +6 -0
- data/lib/generators/its_swiss/install/install_generator.rb +70 -0
- data/lib/generators/its_swiss/install/templates/theme.css +44 -0
- data/lib/its-swiss.rb +3 -0
- data/lib/its_swiss/engine.rb +39 -0
- data/lib/its_swiss/form_builder.rb +140 -0
- data/lib/its_swiss/version.rb +3 -0
- data/lib/its_swiss.rb +46 -0
- metadata +172 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/* Components.
|
|
2
|
+
*
|
|
3
|
+
* Separation is done with hairlines and whitespace. There are no cards, no
|
|
4
|
+
* rounded corners and no shadows in this file, and nothing is set in
|
|
5
|
+
* capitals. A component here says only how it differs from the registers and
|
|
6
|
+
* the two arrangements already declared in type.css and grid.css.
|
|
7
|
+
*
|
|
8
|
+
* Every one of these is laid out on its own, in flex, and never on the page's
|
|
9
|
+
* field. The field is the application's — a component that placed itself on
|
|
10
|
+
* it would be a component that broke the moment the application decided its
|
|
11
|
+
* problem had eight fields rather than six.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
@layer its-swiss.components {
|
|
15
|
+
/* --- Masthead and navigation -------------------------------------------
|
|
16
|
+
A band, a mark at one end, the destinations at the other, and a hairline
|
|
17
|
+
underneath. The mark is the application's to fill; the library only knows
|
|
18
|
+
that it does not break and that the nav sits beside it. */
|
|
19
|
+
|
|
20
|
+
.masthead {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-wrap: wrap;
|
|
23
|
+
align-items: baseline;
|
|
24
|
+
gap: var(--space-2) var(--space-4);
|
|
25
|
+
padding-block: var(--space-3);
|
|
26
|
+
border-bottom: var(--rule-hair) solid var(--rule);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* A wordmark is not free text and may not be broken, either inside itself
|
|
30
|
+
or away from whatever leads it. */
|
|
31
|
+
.masthead__mark {
|
|
32
|
+
font-weight: 700;
|
|
33
|
+
letter-spacing: -0.015em;
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.masthead__mark a { text-decoration: none; }
|
|
38
|
+
|
|
39
|
+
.masthead__mark a:hover { text-decoration: underline; text-underline-offset: 0.2em; }
|
|
40
|
+
|
|
41
|
+
/* Off the left edge rather than pushed to the right: the asymmetry is the
|
|
42
|
+
point, and it lines the nav up with the body column below it. An
|
|
43
|
+
application that wants it flush right sets margin-inline-start: auto,
|
|
44
|
+
which is one line in its own stylesheet. */
|
|
45
|
+
.nav {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-wrap: wrap;
|
|
48
|
+
align-items: baseline;
|
|
49
|
+
gap: var(--space-3);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.nav a { text-decoration: none; }
|
|
53
|
+
|
|
54
|
+
.nav a:hover { color: var(--ink); }
|
|
55
|
+
|
|
56
|
+
/* Where you are, said twice. Colour alone is not a signal — a reader who
|
|
57
|
+
cannot separate the accent from the quiet gray would have no current
|
|
58
|
+
state at all — so the weight says it as well. */
|
|
59
|
+
.nav [aria-current="page"] {
|
|
60
|
+
color: var(--accent);
|
|
61
|
+
font-weight: 700;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* --- Footer -------------------------------------------------------------
|
|
65
|
+
A hairline, the micro register, and whatever the application has to say
|
|
66
|
+
after the page. It carries no links of its own: what belongs down there
|
|
67
|
+
is not something a library can know. */
|
|
68
|
+
|
|
69
|
+
.footer {
|
|
70
|
+
margin-top: var(--space-13);
|
|
71
|
+
padding-block: var(--space-3);
|
|
72
|
+
border-top: var(--rule-hair) solid var(--rule);
|
|
73
|
+
font-size: var(--size-1);
|
|
74
|
+
line-height: var(--space-3);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* --- Rules --------------------------------------------------------------
|
|
78
|
+
The one horizontal mark in the style, at the two weights tokens.css
|
|
79
|
+
declares. */
|
|
80
|
+
|
|
81
|
+
hr {
|
|
82
|
+
height: 0;
|
|
83
|
+
border: 0;
|
|
84
|
+
border-top: var(--rule-hair) solid var(--rule);
|
|
85
|
+
margin-block: var(--space-3);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
hr.rule--heavy { border-top-width: var(--rule-heavy); border-color: var(--ink); }
|
|
89
|
+
|
|
90
|
+
/* --- Table --------------------------------------------------------------
|
|
91
|
+
A grid of values, ruled horizontally and not vertically: the columns are
|
|
92
|
+
already held apart by the type, and a vertical rule between them draws a
|
|
93
|
+
box, which this style does not do. No zebra striping for the same reason
|
|
94
|
+
— a filled row is a filled area, and filled areas are the accent's alone.
|
|
95
|
+
|
|
96
|
+
Figures are tabular, because a column of numbers is read down. */
|
|
97
|
+
|
|
98
|
+
.table { width: 100%; }
|
|
99
|
+
|
|
100
|
+
.table caption {
|
|
101
|
+
color: var(--ink-quiet);
|
|
102
|
+
margin-bottom: var(--space-1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.table th,
|
|
106
|
+
.table td {
|
|
107
|
+
padding-block: var(--space-1);
|
|
108
|
+
padding-inline-end: var(--space-3);
|
|
109
|
+
border-bottom: var(--rule-hair) solid var(--rule);
|
|
110
|
+
vertical-align: baseline;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.table th {
|
|
114
|
+
color: var(--ink-quiet);
|
|
115
|
+
font-weight: 700;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.table thead th { border-bottom-color: var(--ink); }
|
|
119
|
+
|
|
120
|
+
.table td:last-child,
|
|
121
|
+
.table th:last-child { padding-inline-end: 0; }
|
|
122
|
+
|
|
123
|
+
/* A column of numbers reads right-aligned against its own heading. */
|
|
124
|
+
.table .numeric { text-align: right; padding-inline: var(--space-3) 0; }
|
|
125
|
+
|
|
126
|
+
/* --- Definition list ----------------------------------------------------
|
|
127
|
+
Term and value in two columns, the term set in the quiet register and the
|
|
128
|
+
value in ink. The weight tells them apart at a glance rather than making
|
|
129
|
+
a reader count characters. */
|
|
130
|
+
|
|
131
|
+
.pairs {
|
|
132
|
+
display: grid;
|
|
133
|
+
grid-template-columns: max-content 1fr;
|
|
134
|
+
column-gap: var(--space-3);
|
|
135
|
+
row-gap: var(--space-1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.pairs dt { font-weight: 700; }
|
|
139
|
+
|
|
140
|
+
/* When the terms are long enough that a two-column set leaves the values
|
|
141
|
+
stranded across the page. */
|
|
142
|
+
.pairs--stacked {
|
|
143
|
+
grid-template-columns: 1fr;
|
|
144
|
+
row-gap: 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.pairs--stacked dt { margin-top: var(--space-2); }
|
|
148
|
+
|
|
149
|
+
.pairs--stacked dt:first-child { margin-top: 0; }
|
|
150
|
+
|
|
151
|
+
/* --- Forms --------------------------------------------------------------
|
|
152
|
+
A field is a label, a control and, when there is something to say, a
|
|
153
|
+
hint or the reason it was refused. The control is a rule under the text
|
|
154
|
+
rather than a box around it: the box is a border drawn for its own sake,
|
|
155
|
+
and this style does not draw them. */
|
|
156
|
+
|
|
157
|
+
.form {
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
align-items: flex-start;
|
|
161
|
+
gap: var(--space-3);
|
|
162
|
+
max-width: var(--measure);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.form--inline {
|
|
166
|
+
flex-direction: row;
|
|
167
|
+
flex-wrap: wrap;
|
|
168
|
+
align-items: flex-end;
|
|
169
|
+
gap: var(--space-2);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.field {
|
|
173
|
+
display: flex;
|
|
174
|
+
flex-direction: column;
|
|
175
|
+
gap: var(--space-1);
|
|
176
|
+
width: 100%;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.field--inline {
|
|
180
|
+
flex-direction: row;
|
|
181
|
+
align-items: baseline;
|
|
182
|
+
gap: var(--space-2);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.field input,
|
|
186
|
+
.field select,
|
|
187
|
+
.field textarea {
|
|
188
|
+
width: 100%;
|
|
189
|
+
border: 0;
|
|
190
|
+
border-bottom: var(--rule-hair) solid var(--ink);
|
|
191
|
+
background: transparent;
|
|
192
|
+
color: inherit;
|
|
193
|
+
font: inherit;
|
|
194
|
+
/* No padding above it: the rule belongs under the text, not a line's
|
|
195
|
+
height below it, or the label reads as belonging to nothing. */
|
|
196
|
+
padding-block: 0 var(--space-1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.field textarea { resize: vertical; }
|
|
200
|
+
|
|
201
|
+
/* A checkbox or a radio is the one control whose label sits beside it. */
|
|
202
|
+
.choice {
|
|
203
|
+
display: inline-flex;
|
|
204
|
+
align-items: baseline;
|
|
205
|
+
gap: var(--space-1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.choice input { width: auto; border: revert; }
|
|
209
|
+
|
|
210
|
+
/* Refused. The accent is the whole signal here and it is a rule, not a
|
|
211
|
+
fill: the field is still the field. */
|
|
212
|
+
.field--invalid input,
|
|
213
|
+
.field--invalid select,
|
|
214
|
+
.field--invalid textarea { border-bottom-color: var(--accent); }
|
|
215
|
+
|
|
216
|
+
.field__error {
|
|
217
|
+
color: var(--accent);
|
|
218
|
+
font-size: var(--size-1);
|
|
219
|
+
line-height: var(--space-2);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* --- Buttons ------------------------------------------------------------
|
|
223
|
+
A button is a block; a link is underlined text. Both at once leaves the
|
|
224
|
+
page saying "follow me" and "press me" in the same breath.
|
|
225
|
+
|
|
226
|
+
Every one of them is the same box, so a row of them sits on one line and
|
|
227
|
+
the only difference between them is what fills it. No radius, no shadow,
|
|
228
|
+
and the keyline is the hairline drawn everywhere else. */
|
|
229
|
+
|
|
230
|
+
.button {
|
|
231
|
+
display: inline-block;
|
|
232
|
+
border: var(--rule-hair) solid var(--rule);
|
|
233
|
+
background: transparent;
|
|
234
|
+
color: var(--ink);
|
|
235
|
+
font-family: inherit;
|
|
236
|
+
padding: var(--space-1) var(--space-2);
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
text-decoration: none;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.button:hover { border-color: var(--ink); }
|
|
242
|
+
|
|
243
|
+
/* A solid block, the way a Swiss poster sets a call to action. Ink rather
|
|
244
|
+
than the accent: the primary action is the one you were going to take,
|
|
245
|
+
and the accent is for the one you have to be told about. */
|
|
246
|
+
.button--primary {
|
|
247
|
+
border-color: var(--ink);
|
|
248
|
+
background: var(--ink);
|
|
249
|
+
color: var(--paper);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.button--primary:hover { border-color: var(--ink-quiet); background: var(--ink-quiet); }
|
|
253
|
+
|
|
254
|
+
/* The one filled area the accent gets, for the action a page exists to
|
|
255
|
+
offer. Used once per page or not at all. */
|
|
256
|
+
.button--accent {
|
|
257
|
+
border-color: var(--accent);
|
|
258
|
+
background: var(--accent);
|
|
259
|
+
color: var(--accent-ink);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.button--quiet:hover { color: var(--ink); }
|
|
263
|
+
|
|
264
|
+
/* Destroying and reordering are one shape apart from each other otherwise.
|
|
265
|
+
The page stays monochrome at rest; the accent arrives at the moment you
|
|
266
|
+
reach for the one thing that cannot be undone. Focus as well as hover,
|
|
267
|
+
because a keyboard reaches this button and touch never hovers at all. */
|
|
268
|
+
.button--danger:hover,
|
|
269
|
+
.button--danger:focus-visible {
|
|
270
|
+
border-color: var(--accent);
|
|
271
|
+
color: var(--accent);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* Set apart before it is coloured: in a row of actions, destroying and
|
|
275
|
+
reordering are the same gray word otherwise, and a gap is read before a
|
|
276
|
+
word is.
|
|
277
|
+
|
|
278
|
+
A step on the ladder rather than an auto margin. Auto puts the gap at
|
|
279
|
+
whatever is left over, which in a narrow row is right and across a whole
|
|
280
|
+
page is most of a page — and an action a page away from the actions it
|
|
281
|
+
belongs with reads as a different control, not a separated one.
|
|
282
|
+
|
|
283
|
+
:has, because button_to wraps its button in a form, so the flex child is
|
|
284
|
+
the form and the margin belongs to the child. */
|
|
285
|
+
.run > :has(> .button--danger),
|
|
286
|
+
.run > .button--danger { margin-inline-start: var(--space-6); }
|
|
287
|
+
|
|
288
|
+
form.button_to { display: inline; }
|
|
289
|
+
|
|
290
|
+
.link-quiet { text-underline-offset: 0.25em; }
|
|
291
|
+
|
|
292
|
+
.link-quiet:hover { color: var(--ink); }
|
|
293
|
+
|
|
294
|
+
/* --- Copy ---------------------------------------------------------------
|
|
295
|
+
A value on screen to be taken somewhere else is a button that copies
|
|
296
|
+
itself. The value stays visible text inside it, which is what keeps it
|
|
297
|
+
usable when the clipboard is not available. */
|
|
298
|
+
|
|
299
|
+
.copy {
|
|
300
|
+
border: 0;
|
|
301
|
+
padding: 0;
|
|
302
|
+
background: none;
|
|
303
|
+
color: inherit;
|
|
304
|
+
cursor: pointer;
|
|
305
|
+
text-align: left;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.copy::after {
|
|
309
|
+
content: "";
|
|
310
|
+
color: var(--ink-quiet);
|
|
311
|
+
font-weight: 400;
|
|
312
|
+
letter-spacing: 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.copy:hover::after,
|
|
316
|
+
.copy:focus-visible::after { content: " copy"; }
|
|
317
|
+
|
|
318
|
+
/* Says it worked, in the one place you are already looking. */
|
|
319
|
+
.copy[data-copied]::after,
|
|
320
|
+
.copy[data-copied]:hover::after { content: " copied"; color: var(--accent); }
|
|
321
|
+
|
|
322
|
+
/* --- Pagination ---------------------------------------------------------
|
|
323
|
+
A run of numbers under a hairline. The current page carries weight as
|
|
324
|
+
well as the accent, for the same reason the current nav item does. */
|
|
325
|
+
|
|
326
|
+
.pagination {
|
|
327
|
+
display: flex;
|
|
328
|
+
flex-wrap: wrap;
|
|
329
|
+
align-items: baseline;
|
|
330
|
+
gap: var(--space-1) var(--space-2);
|
|
331
|
+
margin-top: var(--space-5);
|
|
332
|
+
padding-top: var(--space-2);
|
|
333
|
+
border-top: var(--rule-hair) solid var(--rule);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* inline-block so the line box is the tap target: an inline box takes its
|
|
337
|
+
height from the font's metrics and ignores the leading set for it. */
|
|
338
|
+
.pagination a,
|
|
339
|
+
.pagination [aria-current] { display: inline-block; }
|
|
340
|
+
|
|
341
|
+
.pagination a { color: var(--ink-quiet); text-underline-offset: 0.25em; }
|
|
342
|
+
|
|
343
|
+
.pagination a:hover { color: var(--ink); }
|
|
344
|
+
|
|
345
|
+
.pagination [aria-current] {
|
|
346
|
+
color: var(--accent);
|
|
347
|
+
font-weight: 700;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.pagination__gap { color: var(--ink-quiet); }
|
|
351
|
+
|
|
352
|
+
/* --- What a page has to say --------------------------------------------
|
|
353
|
+
Three registers: it worked, it was refused, there is nothing here. The
|
|
354
|
+
refusal is the only one that interrupts, and it interrupts with a rule
|
|
355
|
+
rather than a filled panel. */
|
|
356
|
+
|
|
357
|
+
.errors {
|
|
358
|
+
width: 100%;
|
|
359
|
+
padding-top: var(--space-2);
|
|
360
|
+
border-top: var(--rule-heavy) solid var(--accent);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.errors__title { font-weight: 700; }
|
|
364
|
+
|
|
365
|
+
.errors ul {
|
|
366
|
+
color: var(--ink-quiet);
|
|
367
|
+
margin-top: var(--space-1);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.empty { color: var(--ink-quiet); }
|
|
371
|
+
|
|
372
|
+
/* --- Off screen ---------------------------------------------------------
|
|
373
|
+
For text that has to exist without being seen. Clipped rather than hidden
|
|
374
|
+
with display or visibility, because both of those take the element out of
|
|
375
|
+
the accessibility tree as well as off the page, which is the one thing
|
|
376
|
+
this must not do. */
|
|
377
|
+
|
|
378
|
+
.visually-hidden {
|
|
379
|
+
position: absolute;
|
|
380
|
+
width: 1px;
|
|
381
|
+
height: 1px;
|
|
382
|
+
margin: -1px;
|
|
383
|
+
padding: 0;
|
|
384
|
+
overflow: hidden;
|
|
385
|
+
clip-path: inset(50%);
|
|
386
|
+
white-space: nowrap;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* The way past the masthead for a keyboard. Off screen until it is focused,
|
|
390
|
+
at which point it has to be somewhere you can read it. */
|
|
391
|
+
.skip-link {
|
|
392
|
+
position: absolute;
|
|
393
|
+
left: var(--space-2);
|
|
394
|
+
top: var(--space-2);
|
|
395
|
+
z-index: 1;
|
|
396
|
+
padding: var(--space-1) var(--space-2);
|
|
397
|
+
background: var(--paper);
|
|
398
|
+
border: var(--rule-hair) solid var(--ink);
|
|
399
|
+
transform: translateY(calc(-100% - var(--space-4)));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.skip-link:focus { transform: none; }
|
|
403
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* Grid primitives.
|
|
2
|
+
*
|
|
3
|
+
* The library ships the machinery and never the grid: how many fields a page
|
|
4
|
+
* has depends on what it lays out, and a library that answered that for every
|
|
5
|
+
* application would be answering it for none of them. An application declares
|
|
6
|
+
* --columns in its own stylesheet, and everything here re-proportions.
|
|
7
|
+
*
|
|
8
|
+
* Three primitives, and that is the whole file. A page, a field, and a way to
|
|
9
|
+
* say how many fields something takes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
@layer its-swiss.grid {
|
|
13
|
+
/* The page: the one measured container, centred, with a margin that is a
|
|
14
|
+
step on the ladder rather than a number chosen for the look of it. */
|
|
15
|
+
.page {
|
|
16
|
+
max-width: var(--page-max);
|
|
17
|
+
margin-inline: auto;
|
|
18
|
+
padding-inline: var(--page-inset);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* The field. Every list that has to line up with the page uses this same
|
|
22
|
+
track definition, which is the point of a modular grid and the thing
|
|
23
|
+
separate per-component grids can only manage by coincidence. */
|
|
24
|
+
.grid {
|
|
25
|
+
display: grid;
|
|
26
|
+
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
|
|
27
|
+
column-gap: var(--gutter);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Placement is the default and the exception is what gets written down. A
|
|
31
|
+
child says how many fields it wants by setting --span; a child that says
|
|
32
|
+
nothing runs the whole field, because that is what nearly everything on a
|
|
33
|
+
page does, and because an unplaced item silently taking one column is the
|
|
34
|
+
bug this replaces — a frame, a panel and a line of prose each took a
|
|
35
|
+
sixth of the page in turn before it was written this way round. */
|
|
36
|
+
.grid > * { grid-column: span var(--span, var(--columns)); }
|
|
37
|
+
|
|
38
|
+
/* A custom element has no default display, so it arrives on the field as an
|
|
39
|
+
unplaced inline box and takes whatever it wraps down with it. */
|
|
40
|
+
turbo-frame { display: block; }
|
|
41
|
+
|
|
42
|
+
/* Field-width helpers. An application sizes something to a run of fields
|
|
43
|
+
with these rather than with a measured rem value that lands between two
|
|
44
|
+
field lines. */
|
|
45
|
+
.measure { max-width: var(--measure); }
|
|
46
|
+
|
|
47
|
+
.fields { width: calc(var(--field) * var(--span, 1) + var(--gutter) * (var(--span, 1) - 1)); }
|
|
48
|
+
|
|
49
|
+
/* --- Runs and columns ---------------------------------------------------
|
|
50
|
+
Almost every horizontal arrangement in an interface is one thing: items
|
|
51
|
+
on a shared baseline, a space apart, wrapping when they must. Said once
|
|
52
|
+
here the way the field's tracks are, so a component below says only how
|
|
53
|
+
it differs. */
|
|
54
|
+
.run {
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-wrap: wrap;
|
|
57
|
+
align-items: baseline;
|
|
58
|
+
gap: var(--space-2);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* The same idea turned ninety degrees. */
|
|
62
|
+
.stack {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
gap: var(--space-3);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* Reset. Only what the library's own type and spacing decisions require: a
|
|
2
|
+
* predictable box, no margins the ladder did not put there, and no list
|
|
3
|
+
* bullets, because a list in this style is set on the grid rather than
|
|
4
|
+
* indented behind a dot. */
|
|
5
|
+
|
|
6
|
+
@layer its-swiss.reset {
|
|
7
|
+
*,
|
|
8
|
+
*::before,
|
|
9
|
+
*::after { box-sizing: border-box; }
|
|
10
|
+
|
|
11
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
12
|
+
|
|
13
|
+
body {
|
|
14
|
+
margin: 0;
|
|
15
|
+
background: var(--paper);
|
|
16
|
+
color: var(--ink);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
h1, h2, h3, h4, p, ul, ol, dl, dd, figure, fieldset, legend, blockquote {
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
ul, ol { list-style: none; }
|
|
25
|
+
|
|
26
|
+
fieldset { border: 0; }
|
|
27
|
+
|
|
28
|
+
img, svg, video { max-width: 100%; display: block; }
|
|
29
|
+
|
|
30
|
+
/* A table is a grid of values, and a gap between its cells is a gap the
|
|
31
|
+
spacing ladder should be setting rather than the browser. */
|
|
32
|
+
table { border-collapse: collapse; border-spacing: 0; }
|
|
33
|
+
|
|
34
|
+
/* The browser hides [hidden] with display: none, and any author rule that
|
|
35
|
+
sets display at all outranks it — a .button says inline-block, so a
|
|
36
|
+
hidden button stays on the page. The attribute means hidden, so it is
|
|
37
|
+
made to mean it once, here, rather than remembered at every element that
|
|
38
|
+
might one day carry it. The library's one !important, and the reason it
|
|
39
|
+
needs one: the rule it has to beat is inside the same layer. */
|
|
40
|
+
[hidden] { display: none !important; }
|
|
41
|
+
|
|
42
|
+
a { color: inherit; }
|
|
43
|
+
|
|
44
|
+
/* The accent's first job. Unset it and this is an ink outline, which is
|
|
45
|
+
still a focus ring. */
|
|
46
|
+
:focus-visible {
|
|
47
|
+
outline: var(--rule-heavy) solid var(--accent);
|
|
48
|
+
outline-offset: 3px;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* The specimen's own furniture: the blocks of value, the field diagram, and
|
|
2
|
+
* the spacing between sections. Not part of the library — nothing here is a
|
|
3
|
+
* component an application should reach for. Loaded only by the specimen
|
|
4
|
+
* page, which is why it is not in ItsSwiss::STYLESHEETS.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
@layer its-swiss.specimen {
|
|
8
|
+
[data-specimen-take] { margin-top: var(--space-13); }
|
|
9
|
+
|
|
10
|
+
[data-specimen] { margin-top: var(--space-6); }
|
|
11
|
+
|
|
12
|
+
[data-specimen] > h3 { margin-bottom: var(--space-2); }
|
|
13
|
+
|
|
14
|
+
[data-specimen] > * + * { margin-top: var(--space-2); }
|
|
15
|
+
|
|
16
|
+
/* A block of value, the height of a line box so it sits on the baseline
|
|
17
|
+
beside the token that names it. */
|
|
18
|
+
.specimen__value {
|
|
19
|
+
display: block;
|
|
20
|
+
width: var(--space-13);
|
|
21
|
+
height: var(--space-3);
|
|
22
|
+
box-shadow: inset 0 0 0 var(--rule-hair) color-mix(in oklch, var(--ink) 12%, transparent);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* The field, drawn. Each child is one span, and the bar inside it shows
|
|
26
|
+
where that span starts and stops against the page. */
|
|
27
|
+
.specimen__grid { row-gap: var(--space-1); }
|
|
28
|
+
|
|
29
|
+
.specimen__field {
|
|
30
|
+
display: block;
|
|
31
|
+
height: var(--space-2);
|
|
32
|
+
background: var(--value-2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.specimen__grid > [style*="--span: 2"] .specimen__field,
|
|
36
|
+
.specimen__grid > [style*="--span: 4"] .specimen__field { background: var(--value-3); }
|
|
37
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/* Tokens.
|
|
2
|
+
*
|
|
3
|
+
* Every measurement and every value in the library is declared here and
|
|
4
|
+
* nowhere else. The four blocks below are the whole system: a value scale, an
|
|
5
|
+
* accent slot, a type scale, and one ladder for everything vertical.
|
|
6
|
+
*
|
|
7
|
+
* A consuming application fills three slots — the accent, the typeface, and
|
|
8
|
+
* how many fields its grid has. Everything else it inherits, and everything
|
|
9
|
+
* else it can still override, because these are unlayered properties read
|
|
10
|
+
* from inside a layer: setting --baseline in the application's own stylesheet
|
|
11
|
+
* reproportions the library without touching it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
@layer its-swiss.tokens {
|
|
15
|
+
:root {
|
|
16
|
+
/* --- The value scale ---------------------------------------------------
|
|
17
|
+
Six steps of OKLCH lightness from paper to ink. OKLCH because the steps
|
|
18
|
+
have to be perceptually even to be read as a scale at all: the same
|
|
19
|
+
arithmetic in sRGB puts four of the six inside the top quarter of the
|
|
20
|
+
range and leaves the interface with two grays and a cliff.
|
|
21
|
+
|
|
22
|
+
The ladder is not evenly spaced, and that is the point. A hairline has
|
|
23
|
+
to be barely there, so the top of the scale is dense; the middle only
|
|
24
|
+
has to hold secondary text away from primary, so it is sparse. An even
|
|
25
|
+
ladder gives a usable mid-gray and no usable rule.
|
|
26
|
+
|
|
27
|
+
Neutral, because hue is the application's. Warmth arrives through the
|
|
28
|
+
two slots beneath: a consumer that wants Pandatone's warm grays sets
|
|
29
|
+
--value-chroma: 0.006 and --value-hue: 95 once, and the whole scale
|
|
30
|
+
turns together. */
|
|
31
|
+
--value-chroma: 0;
|
|
32
|
+
--value-hue: 0;
|
|
33
|
+
|
|
34
|
+
--value-0: oklch(98% var(--value-chroma) var(--value-hue)); /* paper */
|
|
35
|
+
--value-1: oklch(94% var(--value-chroma) var(--value-hue)); /* a shaded ground */
|
|
36
|
+
--value-2: oklch(89% var(--value-chroma) var(--value-hue)); /* a hairline */
|
|
37
|
+
--value-3: oklch(72% var(--value-chroma) var(--value-hue)); /* a rule that has to be seen */
|
|
38
|
+
--value-4: oklch(54% var(--value-chroma) var(--value-hue)); /* secondary text: 4.7:1 on paper */
|
|
39
|
+
--value-5: oklch(18% var(--value-chroma) var(--value-hue)); /* ink */
|
|
40
|
+
|
|
41
|
+
/* The names the rest of the library uses. Aliases rather than values of
|
|
42
|
+
their own, so the day the ground is darkened the ladder still describes
|
|
43
|
+
the page. */
|
|
44
|
+
--paper: var(--value-0);
|
|
45
|
+
--paper-shaded: var(--value-1);
|
|
46
|
+
--rule: var(--value-2);
|
|
47
|
+
--rule-strong: var(--value-3);
|
|
48
|
+
--ink-quiet: var(--value-4);
|
|
49
|
+
--ink: var(--value-5);
|
|
50
|
+
|
|
51
|
+
/* --- The accent --------------------------------------------------------
|
|
52
|
+
State and emphasis only: the current item, focus, a link, the one
|
|
53
|
+
primary action, the moment before something cannot be undone. Never a
|
|
54
|
+
filled area for its own sake.
|
|
55
|
+
|
|
56
|
+
Unset, it is ink. That is the library's argument in one line — if a
|
|
57
|
+
page reads correctly with the accent collapsed onto the value scale,
|
|
58
|
+
the values were doing the work. The specimen renders both ways for
|
|
59
|
+
exactly this reason. */
|
|
60
|
+
--accent: var(--ink);
|
|
61
|
+
/* What is legible on top of the accent when it is filled. */
|
|
62
|
+
--accent-ink: var(--paper);
|
|
63
|
+
|
|
64
|
+
/* --- Rules -------------------------------------------------------------
|
|
65
|
+
Two weights. A hairline separates; a heavier one interrupts, and is
|
|
66
|
+
kept for the two or three places a page has something to insist on. */
|
|
67
|
+
--rule-hair: 1px;
|
|
68
|
+
--rule-heavy: 2px;
|
|
69
|
+
|
|
70
|
+
/* --- Type --------------------------------------------------------------
|
|
71
|
+
Five sizes on an alternating 1.33 / 1.5 ratio, every one a multiple of
|
|
72
|
+
four. Five is the count an interface actually uses: labels, body, a
|
|
73
|
+
section, a subhead, a page title. A sixth gets chosen for a page and
|
|
74
|
+
then used nowhere else.
|
|
75
|
+
|
|
76
|
+
The family is the application's, for the same reason the accent is. A
|
|
77
|
+
library that ships a typeface ships a voice, and ships a binary with
|
|
78
|
+
it. The fallback stack below is what an application gets until it says
|
|
79
|
+
otherwise, and it is a grotesque, which is the tradition. */
|
|
80
|
+
--font-family: ui-sans-serif, system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
81
|
+
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
82
|
+
|
|
83
|
+
--size-1: 0.75rem; /* 12 — labels and metadata */
|
|
84
|
+
--size-2: 1rem; /* 16 — body */
|
|
85
|
+
--size-3: 1.5rem; /* 24 — section */
|
|
86
|
+
--size-4: 2rem; /* 32 — subhead, large values */
|
|
87
|
+
--size-5: 3rem; /* 48 — page title */
|
|
88
|
+
|
|
89
|
+
/* --- The vertical ladder -----------------------------------------------
|
|
90
|
+
One ladder, numbered in baselines, used for leading as much as for
|
|
91
|
+
space. Two ladders — a set for space and another for line boxes — agree
|
|
92
|
+
at three values and diverge everywhere else, which is how a heading
|
|
93
|
+
ends up impossible to separate from the block beneath it by the height
|
|
94
|
+
of its own line: the value simply does not exist.
|
|
95
|
+
|
|
96
|
+
Changing --baseline reproportions everything. */
|
|
97
|
+
--baseline: 0.5rem;
|
|
98
|
+
|
|
99
|
+
--space-1: calc(var(--baseline) * 1); /* 8 */
|
|
100
|
+
--space-2: calc(var(--baseline) * 2); /* 16 */
|
|
101
|
+
--space-3: calc(var(--baseline) * 3); /* 24 */
|
|
102
|
+
--space-4: calc(var(--baseline) * 4); /* 32 */
|
|
103
|
+
--space-5: calc(var(--baseline) * 5); /* 40 */
|
|
104
|
+
--space-6: calc(var(--baseline) * 6); /* 48 */
|
|
105
|
+
--space-8: calc(var(--baseline) * 8); /* 64 */
|
|
106
|
+
--space-13: calc(var(--baseline) * 13); /* 104 */
|
|
107
|
+
|
|
108
|
+
/* --- The grid ----------------------------------------------------------
|
|
109
|
+
Primitives, not a grid. Six fields divide into halves and thirds and is
|
|
110
|
+
where an application starts; the point of declaring it here is that it
|
|
111
|
+
is one number to change, in the application's own stylesheet, once the
|
|
112
|
+
problem has said how many fields it has.
|
|
113
|
+
|
|
114
|
+
A field's width is derived rather than measured, and every width helper
|
|
115
|
+
in the library is built from it — which is what keeps a block of prose
|
|
116
|
+
stopping on a field line instead of three pixels short of one. */
|
|
117
|
+
--columns: 6;
|
|
118
|
+
--gutter: 1.5rem;
|
|
119
|
+
--page-max: 82rem;
|
|
120
|
+
--page-inset: var(--space-3);
|
|
121
|
+
|
|
122
|
+
/* The page's own margin comes out before the fields are divided. Left in,
|
|
123
|
+
every helper built on --field is a gutter and a half too wide, and prose
|
|
124
|
+
that is supposed to stop on the third field line stops past it — which
|
|
125
|
+
is invisible on a page with nothing beside the prose and glaring on one
|
|
126
|
+
with a column next to it. */
|
|
127
|
+
--field: calc(
|
|
128
|
+
(var(--page-max) - var(--page-inset) * 2 - var(--gutter) * (var(--columns) - 1)) / var(--columns)
|
|
129
|
+
);
|
|
130
|
+
--measure: calc(var(--field) * 3 + var(--gutter) * 2);
|
|
131
|
+
|
|
132
|
+
/* --- Transitions -------------------------------------------------------
|
|
133
|
+
The page crossfade is short enough to read as a replacement rather than
|
|
134
|
+
an animation. A morph runs longer, because the thing travelling has to
|
|
135
|
+
stay readable while it does. */
|
|
136
|
+
--transition-page: 180ms;
|
|
137
|
+
--transition-morph: 240ms;
|
|
138
|
+
--transition-ease: cubic-bezier(0.2, 0, 0.2, 1);
|
|
139
|
+
|
|
140
|
+
color-scheme: light;
|
|
141
|
+
}
|
|
142
|
+
}
|