its-swiss 1.0.0 → 1.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 +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +108 -441
- data/app/assets/stylesheets/its_swiss/components.css +27 -1
- data/lib/its_swiss/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23a617f3f75e3b652becb681c8b54a38d122ba3aa0d3b7cfa4cc3187eb2177b5
|
|
4
|
+
data.tar.gz: d7d384f67cf5653cb917322ccb560c05fa6a97858f8668c46785c7520e86a562
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 56fb78910a3dd24b99bce266e8d008096ae4158efcb80e8025843a5813d958cdf1673272b834f7b15926fac0b2111b0de168880ee4f6dc1e639beb9a3ea3b52c
|
|
7
|
+
data.tar.gz: 05f4c40910d9febd7f82e66867be7c5c1ef30c617a5d0cead8376c3ac931bb1651e689c1eecc6d8ff88524841f5e0e8b19a92f0a9d4a70c1d53f4522f74d4204
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
Semver. Consumers pin `~> 1.0`.
|
|
4
4
|
|
|
5
|
+
## 1.1.0 — 2026-09-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **A textarea is ruled.** A dotted rule on every line of the grid that type
|
|
10
|
+
can stand on, on the row of pixels a rule would take there, in
|
|
11
|
+
`--rule-strong`. None on the line of air under the label, which nothing is
|
|
12
|
+
written on, and none under the last line, which the control's own rule
|
|
13
|
+
closes. The dots are painted in the text's box and scroll with it, so a
|
|
14
|
+
long message keeps a rule under every line. `--ruled-dot` sets their
|
|
15
|
+
colour.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **A textarea's line of air is a border.** It was padding, so text scrolled
|
|
20
|
+
up ran on into the air, under a dense form's label. It is a transparent
|
|
21
|
+
border of the same width, so scrolled text is cut at the first line. The
|
|
22
|
+
box, and where its text sits, are unchanged.
|
|
23
|
+
|
|
5
24
|
## 1.0.0 — 2026-09-08
|
|
6
25
|
|
|
7
26
|
The surface three applications settled on, reviewed as a whole: what a tech
|
data/README.md
CHANGED
|
@@ -1,492 +1,159 @@
|
|
|
1
1
|
# its-swiss
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
sensible Swiss defaults for Rails
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Tokens, reset, typography, grid primitives and components in the spirit of the
|
|
6
|
+
International Typographic Style, as plain CSS plus a Rails engine that ships
|
|
7
|
+
it. Monochrome by default: the value scale does the work, and the one accent
|
|
8
|
+
is the consuming application's to set.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
partials, a form builder, helpers and an install generator.
|
|
11
|
-
|
|
12
|
-
It is monochrome. A six-step value scale does the work and the one accent is
|
|
13
|
-
the consuming application's to set — it is not a theme with the colour left
|
|
14
|
-
out, it is a style whose argument is that the values are enough.
|
|
10
|
+
## quickstart
|
|
15
11
|
|
|
16
12
|
```ruby
|
|
17
|
-
|
|
13
|
+
# Gemfile
|
|
14
|
+
gem "its-swiss", github: "bobbymeyer/its-swiss"
|
|
18
15
|
```
|
|
19
16
|
|
|
20
17
|
```sh
|
|
18
|
+
bundle install
|
|
21
19
|
bin/rails generate its_swiss:install
|
|
22
20
|
```
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
The current specimen is also published, for anywhere that cannot run Rails:
|
|
27
|
-
**[bobbymeyer.github.io/its-swiss](https://bobbymeyer.github.io/its-swiss/)**.
|
|
28
|
-
`bin/specimen out` writes it as one static file, and a Pages workflow does
|
|
29
|
-
that on every push to `main`; the versions kept in `published/` are copied
|
|
30
|
-
alongside it at `/<version>.html` for anything that needs to pin.
|
|
31
|
-
|
|
32
|
-
## What is in the gem, and what stays in the application
|
|
22
|
+
The generator fills the four slots an application owns:
|
|
33
23
|
|
|
34
|
-
|
|
|
24
|
+
| It writes | Which is |
|
|
35
25
|
| --- | --- |
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
| A
|
|
39
|
-
| View transition names and durations | Which pages transition to which |
|
|
40
|
-
| The value scale and the accent slot | Any hue, any palette knowledge |
|
|
41
|
-
| The machine's grotesque, declared to sit on the baseline | The typeface |
|
|
42
|
-
|
|
43
|
-
A pattern enters the gem after it appears in two applications, not before.
|
|
44
|
-
|
|
45
|
-
### The boundary is the cascade, not a convention
|
|
26
|
+
| `app/assets/stylesheets/theme.css` | The accent, the typeface, the value scale, the field count, the baseline |
|
|
27
|
+
| `app/views/layouts/application.html.erb` | A nested layout inside the shell — no `layout` line on any controller |
|
|
28
|
+
| A route for the specimen | Development only; skipped if already mounted |
|
|
46
29
|
|
|
47
|
-
Everything
|
|
48
|
-
|
|
49
|
-
specificity — so the application always wins, without having to out-specify
|
|
50
|
-
anything or reach for `!important`.
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
@layer its-swiss.tokens, its-swiss.faces, its-swiss.reset, its-swiss.type,
|
|
54
|
-
its-swiss.grid, its-swiss.components, its-swiss.transitions;
|
|
55
|
-
```
|
|
30
|
+
Everything else is a gem dependency: Propshaft finds the stylesheets, and the
|
|
31
|
+
engine pins its own JavaScript, so upgrading the gem is enough.
|
|
56
32
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
33
|
+
[**Specimen →**](https://bobbymeyer.github.io/its-swiss/) — every component,
|
|
34
|
+
the type scale, the value scale and the grid primitives, rendered by the
|
|
35
|
+
library and published from this repository.
|
|
60
36
|
|
|
61
|
-
##
|
|
37
|
+
## the shell
|
|
62
38
|
|
|
63
|
-
`
|
|
64
|
-
|
|
39
|
+
`layouts/its_swiss/shell` carries the view-transition opt-in, the stylesheets
|
|
40
|
+
in layer order, a skip link, and the slots. All optional:
|
|
65
41
|
|
|
66
|
-
| Slot |
|
|
42
|
+
| Slot | Is |
|
|
67
43
|
| --- | --- |
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
| `--value-chroma`, `--value-hue` | Warms the whole value scale together. Neutral as shipped |
|
|
71
|
-
| `--columns`, `--gutter` | How many fields this problem has |
|
|
72
|
-
| `--line` | The baseline: the interval everything vertical registers to |
|
|
73
|
-
| `--module` | A field's height, in lines: what divides the page down |
|
|
74
|
-
| `--measure-characters` | How many characters a line of prose holds; the grid is what it stops on |
|
|
75
|
-
| `--ratio` | A picture's aspect ratio, per figure — the library cannot read one |
|
|
76
|
-
| `--card` | How many fields a card takes, and at which widths |
|
|
77
|
-
| `--filter-label` | The column a filter's label occupies, so every register's choices start on one line |
|
|
78
|
-
| `--face-150`, `--face-200`, `--face-100` | Read, not set: the face a register names for its ratio of leading to size |
|
|
79
|
-
|
|
80
|
-
### The baseline
|
|
81
|
-
|
|
82
|
-
`--line` (24px) is the interval everything vertical registers to, and the
|
|
83
|
-
leading of the body text: one number, because that is what a baseline grid
|
|
84
|
-
is. Every leading, margin, padding and gap on the vertical axis is a whole
|
|
85
|
-
number of lines; `--space-*` is the horizontal step.
|
|
86
|
-
|
|
87
|
-
Every baseline is the under edge of its line box, two ways. The library sets
|
|
88
|
-
every register in a face of its own, the machine's grotesque declared again
|
|
89
|
-
with its ascent set to the register's ratio of leading to size and no
|
|
90
|
-
descent, which Chromium and Firefox honour. Where a browser ignores what a
|
|
91
|
-
face says about its metrics, Safari, every text block is trimmed to its type
|
|
92
|
-
instead, and one line of script ahead of the stylesheets says which is in
|
|
93
|
-
force. `its_swiss_stylesheet_tags` writes it; anything linking the
|
|
94
|
-
stylesheets by hand should too:
|
|
95
|
-
|
|
96
|
-
```html
|
|
97
|
-
<script>if (!("ascentOverride" in FontFace.prototype)) document.documentElement.classList.add("no-metric-overrides")</script>
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
The whole argument, and what follows from it, is in `DESIGN.md`.
|
|
101
|
-
|
|
102
|
-
#### Your own typeface
|
|
103
|
-
|
|
104
|
-
Declared under the same names with the same descriptors, and the helper writes
|
|
105
|
-
it:
|
|
106
|
-
|
|
107
|
-
```erb
|
|
108
|
-
<%= its_swiss_typeface regular: "inter-regular.woff2", bold: "inter-bold.woff2" %>
|
|
109
|
-
<%= its_swiss_typeface variable: "inter.woff2", mono: "jetbrains-mono.woff2" %>
|
|
110
|
-
<%= its_swiss_typeface variable: "inter.woff2", family: "Inter" %>
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Put it after the library's stylesheets. `family:` declares the typeface under
|
|
114
|
-
its own name as well, which a control's text is set in; `--font-family` is
|
|
115
|
-
what a machine with none of the faces falls through to.
|
|
116
|
-
|
|
117
|
-
#### The one subgrid
|
|
118
|
-
|
|
119
|
-
A block of small type may sit on a half-line:
|
|
120
|
-
|
|
121
|
-
```html
|
|
122
|
-
<div class="subgrid">…</div>
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
It halves `--line` for the block's **children** and moves the small register
|
|
126
|
-
onto the face for its new ratio. A block, and only a block: an inline
|
|
127
|
-
`<small>` shares its paragraph's line.
|
|
128
|
-
|
|
129
|
-
### The value scale
|
|
130
|
-
|
|
131
|
-
Six steps of OKLCH lightness, paper to ink. OKLCH because the steps have to be
|
|
132
|
-
perceptually even to read as a scale at all — the same arithmetic in sRGB puts
|
|
133
|
-
four of the six inside the top quarter of the range.
|
|
134
|
-
|
|
135
|
-
| Token | Lightness | Alias | For |
|
|
136
|
-
| --- | --- | --- | --- |
|
|
137
|
-
| `--value-0` | 98% | `--paper` | The ground |
|
|
138
|
-
| `--value-1` | 94% | `--paper-shaded` | A shaded ground |
|
|
139
|
-
| `--value-2` | 89% | `--rule` | A hairline |
|
|
140
|
-
| `--value-3` | 72% | `--rule-strong` | A rule that has to be seen |
|
|
141
|
-
| `--value-4` | 54% | `--ink-quiet` | Secondary text — 4.7:1 on paper |
|
|
142
|
-
| `--value-5` | 18% | `--ink` | Ink |
|
|
143
|
-
|
|
144
|
-
The ladder is deliberately not evenly spaced: a hairline has to be barely
|
|
145
|
-
there, so the top is dense; the middle only has to hold secondary text away
|
|
146
|
-
from primary, so it is sparse. An even ladder gives a usable mid-gray and no
|
|
147
|
-
usable rule.
|
|
148
|
-
|
|
149
|
-
Neutral as shipped. `--value-chroma: 0.006; --value-hue: 95` turns the whole
|
|
150
|
-
ladder to the warm grays of a printed page — and, near enough, to Pandatone's:
|
|
151
|
-
|
|
152
|
-
| Step | Warmed | Pandatone had |
|
|
153
|
-
| --- | --- | --- |
|
|
154
|
-
| `--value-0` | `#FAF8F4` | `#FAF9F7` `--ground` |
|
|
155
|
-
| `--value-2` | `#DCDBD6` | `#DCDAD4` `--keyline` |
|
|
156
|
-
| `--value-4` | `#706F6B` | `#6F6F6A` `--ink-quiet` |
|
|
157
|
-
| `--value-5` | `#12120F` | `#111111` `--ink` |
|
|
158
|
-
|
|
159
|
-
Within three units a channel, which is the cost of the four hand-picked
|
|
160
|
-
values becoming one ladder with a single chroma and a single hue. Measured in
|
|
161
|
-
Chromium, not calculated.
|
|
162
|
-
|
|
163
|
-
### Pictures
|
|
164
|
-
|
|
165
|
-
The one case that nearly does not transfer from print. A picture's height is
|
|
166
|
-
its fluid width over its ratio, so without help one picture puts the whole
|
|
167
|
-
column below it off the grid at every width but a few.
|
|
168
|
-
|
|
169
|
-
```html
|
|
170
|
-
<figure class="figure" style="--ratio: 1.618">
|
|
171
|
-
<img src="…" alt="…">
|
|
172
|
-
<figcaption class="micro">…</figcaption>
|
|
173
|
-
</figure>
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
The box is the picture's natural height taken up to the next whole line, by
|
|
177
|
-
`round()`, recomputed as the container resizes. The picture is fitted inside
|
|
178
|
-
it — `contain` by default, because a library must not crop an image it did not
|
|
179
|
-
choose. `.figure--cover` crops instead, which is the Müller-Brockmann move and
|
|
180
|
-
yours to make where you own the picture.
|
|
181
|
-
|
|
182
|
-
`--ratio` is yours to declare; CSS cannot read an intrinsic one.
|
|
183
|
-
|
|
184
|
-
### The grid
|
|
185
|
-
|
|
186
|
-
The gem ships primitives and never a grid.
|
|
187
|
-
|
|
188
|
-
```css
|
|
189
|
-
.page /* the one measured container: --page-max wide, --page-inset either side, and the container a field is measured in */
|
|
190
|
-
.grid /* repeat(var(--columns), minmax(0, 1fr)) with --gutter between */
|
|
191
|
-
.measure /* the measure: --measure-characters of the type, stopped on a field line */
|
|
192
|
-
.fields /* --span fields wide */
|
|
193
|
-
.modules /* --modules modules tall */
|
|
194
|
-
.run /* items on a shared baseline, a space apart, wrapping when they must */
|
|
195
|
-
.stack /* the same, turned ninety degrees */
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
A child of `.grid` says how many fields it takes with `--span`; a child that
|
|
199
|
-
says nothing runs the whole field. `--span` is registered not to inherit, so a
|
|
200
|
-
grid inside a spanned item starts its own children on the whole field.
|
|
201
|
-
|
|
202
|
-
A field is the page's actual width, less the gutters, divided, measured in
|
|
203
|
-
container units from the `.page` the fields are laid out in:
|
|
204
|
-
|
|
205
|
-
```css
|
|
206
|
-
--field: calc((100cqw - var(--gutter) * (var(--columns) - 1)) / var(--columns));
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
The measure is a count of characters, and the grid is what it stops on:
|
|
210
|
-
`--measure-characters` (65) in the type's own font, taken up to the nearest
|
|
211
|
-
whole run of fields, and never past the page. The module is a field's height
|
|
212
|
-
in lines, `--module` (6): the columns divide the page across and the module
|
|
213
|
-
divides it down, and `.modules` and `.figure--modular` size a block or a
|
|
214
|
-
picture to whole ones.
|
|
215
|
-
|
|
216
|
-
### The page head
|
|
217
|
-
|
|
218
|
-
What a page is and what can be done to it, in one shape on every page: the
|
|
219
|
-
title in the page-title register, its lede when it has one, and the actions in
|
|
220
|
-
a run beside them, wrapping under the title when the page is narrow.
|
|
221
|
-
|
|
222
|
-
```erb
|
|
223
|
-
<%= page_head "Palettes", lede: "Every palette in the library." do %>
|
|
224
|
-
<%= link_to "New palette", new_palette_path, class: "button button--primary" %>
|
|
225
|
-
<% end %>
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
The document is titled from it unless the view has already set `:title`.
|
|
229
|
-
|
|
230
|
-
A page with several surfaces — what a thing is made of, what it wears, how it
|
|
231
|
-
leaves — names them under the title with `sections:`, each a name, a URL and
|
|
232
|
-
whether it is the one shown, and shows that one alone. A working page must
|
|
233
|
-
not scroll: what is done daily is reached from the head, in the same position
|
|
234
|
-
on every page.
|
|
235
|
-
|
|
236
|
-
```erb
|
|
237
|
-
<%= page_head @pattern.name, sections: [ [ "Compose", pattern_path(@pattern), @section == "compose" ],
|
|
238
|
-
[ "Dress", pattern_path(@pattern, section: "dress"), @section == "dress" ] ] do %>
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Explanations
|
|
242
|
-
|
|
243
|
-
What a section means, behind one word. A sentence over every table is needed
|
|
244
|
-
on the first day and never again, and a tool used daily is read on every
|
|
245
|
-
other day; so `explain` writes it in the hint register, closed, and the word
|
|
246
|
-
"About" in the small register opens it. `label:` says another word.
|
|
247
|
-
|
|
248
|
-
```erb
|
|
249
|
-
<%= explain "The repeat, in order along the stripe normal." %>
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
### Filters and cards
|
|
253
|
-
|
|
254
|
-
What narrows a list, and the list. A search that filters as you type, and a
|
|
255
|
-
register for each other way of narrowing it — a tag, an order, a size — in the
|
|
256
|
-
same two positions every time: a quiet label, then the choices, the one in
|
|
257
|
-
force carrying `aria-current`, which the CSS sets in the weight. In ink, not
|
|
258
|
-
the accent: the accent is for where you are on the site and for the one
|
|
259
|
-
thing that cannot be undone, and a page has one red.
|
|
260
|
-
|
|
261
|
-
```erb
|
|
262
|
-
<div class="filters">
|
|
263
|
-
<%= search_form palettes_path, frame: "palettes", keep: { tag: params[:tag], sort: params[:sort] } %>
|
|
264
|
-
<%= filter_register "Tagged", [ [ "All", palettes_path, params[:tag].blank? ],
|
|
265
|
-
*tags.map { |t| [ t, palettes_path(tag: t), params[:tag] == t ] } ],
|
|
266
|
-
name: "tag" %>
|
|
267
|
-
</div>
|
|
268
|
-
|
|
269
|
-
<%= turbo_frame_tag "palettes", target: "_top" do %>
|
|
270
|
-
<ul class="grid cards">
|
|
271
|
-
<li class="card">
|
|
272
|
-
<a class="card__link" href="…">
|
|
273
|
-
<figure class="card__figure figure" style="--ratio: 1.5"><img src="…" alt=""></figure>
|
|
274
|
-
<span class="card__name">Brand Core</span>
|
|
275
|
-
</a>
|
|
276
|
-
<span class="card__meta">4 swatches</span>
|
|
277
|
-
</li>
|
|
278
|
-
</ul>
|
|
279
|
-
<% end %>
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
The search form belongs outside the frame it fills, so only the results are
|
|
283
|
-
replaced and the field keeps its cursor. `keep:` carries the other choices as
|
|
284
|
-
hidden fields, so a search does not drop the tag or the order you were reading
|
|
285
|
-
in. The button is there for a browser that runs no script; the controller
|
|
286
|
-
takes it away once it has connected.
|
|
287
|
-
|
|
288
|
-
A card is `--card` fields wide on the page's own grid — say how many once, and
|
|
289
|
-
again at each width that changes it. The picture is a `.figure` with its
|
|
290
|
-
`--ratio` declared, so its box is whole lines at any width and the name under
|
|
291
|
-
it stays on the grid.
|
|
292
|
-
|
|
293
|
-
## The layout shell
|
|
294
|
-
|
|
295
|
-
The installer writes `app/views/layouts/application.html.erb` as a layout
|
|
296
|
-
**for** the shell rather than one instead of it:
|
|
297
|
-
|
|
298
|
-
```erb
|
|
299
|
-
<% content_for :head do %>
|
|
300
|
-
<%= stylesheet_link_tag "theme", "data-turbo-track": "reload" %>
|
|
301
|
-
<% end %>
|
|
302
|
-
|
|
303
|
-
<% content_for :mark do %><%= link_to "Your app", root_path %><% end %>
|
|
304
|
-
|
|
305
|
-
<%= render template: "layouts/its_swiss/shell" %>
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
Nested, not `layout "its_swiss/shell"` on a controller. `content_for` has to
|
|
309
|
-
run while a view is rendering, and these slots are set once for the whole
|
|
310
|
-
application — naming the shell on the controller renders it but leaves
|
|
311
|
-
nowhere to fill it, so every view ends up writing the same masthead.
|
|
312
|
-
|
|
313
|
-
Note the `:head` slot links `theme.css`. The shell links the library's seven
|
|
314
|
-
stylesheets and the module that registers its controllers, and stops; the
|
|
315
|
-
accent and the grid live in yours, and nothing links it but this.
|
|
316
|
-
|
|
317
|
-
Slots, all optional:
|
|
318
|
-
|
|
319
|
-
| Slot | |
|
|
320
|
-
| --- | --- |
|
|
321
|
-
| `:title` | Falls back to the application's name |
|
|
322
|
-
| `:head` | Anything else that belongs in `<head>` |
|
|
44
|
+
| `:title` | The page's title; falls back to the application's name |
|
|
45
|
+
| `:head` | Anything else belonging in `<head>` |
|
|
323
46
|
| `:mark` | The wordmark. No mark and no nav means no masthead at all |
|
|
324
47
|
| `:nav` | The destinations |
|
|
325
|
-
| `:subnav` | A second layer
|
|
48
|
+
| `:subnav` | A second layer, inside the one you are in |
|
|
326
49
|
| `:main_class` | What the page's main region is, if it is a grid |
|
|
327
50
|
| `:footer` | Whatever belongs after the page |
|
|
328
51
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
beyond the shell is the application's, for the same reason its grid is.
|
|
52
|
+
It stops there. A page layout beyond the shell is the application's, for the
|
|
53
|
+
same reason its grid is.
|
|
332
54
|
|
|
333
|
-
##
|
|
334
|
-
|
|
335
|
-
| | |
|
|
336
|
-
| --- | --- |
|
|
337
|
-
| `its_swiss_stylesheet_tags` | The seven links, tracked for Turbo, and the metric-override mark with the page's nonce |
|
|
338
|
-
| `its_swiss_typeface(regular:, bold:)` | The application's typeface, declared under the library's face names |
|
|
339
|
-
| `nav_link_to(name, url, current:)` | A destination, with `aria-current` when you are at it |
|
|
340
|
-
| `nav_menu(label, current:) { links }` | A destination that opens into destinations, with no script |
|
|
341
|
-
| `copy_button(value)` | A value that copies itself |
|
|
342
|
-
| `its_swiss_form_with(...)` | `form_with`, already holding the library's builder, at the measure |
|
|
343
|
-
| `its_swiss_page_numbers(page, pages)` | Which numbers a run of them shows, elided |
|
|
344
|
-
| `page_head(title, lede:, sections:) { actions }` | The one shape every page opens with |
|
|
345
|
-
| `page_sections(sections)` | The surfaces of one page, the one shown in the weight |
|
|
346
|
-
| `explain(text, label:) { }` | What a section means, behind one word |
|
|
347
|
-
| `filter_register(label, choices, name:)` | One register of a filter block |
|
|
348
|
-
| `search_form(url, frame:, keep:)` | A search that narrows a list as you type |
|
|
349
|
-
|
|
350
|
-
### Pagination
|
|
351
|
-
|
|
352
|
-
The library has no paginator and no opinion about which one you use — it takes
|
|
353
|
-
a page, a total, and something that turns a number into a URL:
|
|
354
|
-
|
|
355
|
-
```erb
|
|
356
|
-
<%= render "its_swiss/shared/pagination",
|
|
357
|
-
page: @page, pages: @pages, url: ->(n) { colors_path(page: n) } %>
|
|
358
|
-
```
|
|
55
|
+
## tokens
|
|
359
56
|
|
|
360
|
-
|
|
361
|
-
many neighbours show; `label:` names the `<nav>` for a screen reader.
|
|
57
|
+
Set in `theme.css`; everything else reads them.
|
|
362
58
|
|
|
363
|
-
|
|
59
|
+
| Group | Custom properties |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| Value scale | `--value-chroma`, `--value-hue`, `--value-0` … `--value-5` |
|
|
62
|
+
| Roles | `--paper`, `--paper-shaded`, `--rule`, `--rule-strong`, `--ink-quiet`, `--ink`, `--accent`, `--accent-ink` |
|
|
63
|
+
| Rules | `--rule-hair`, `--rule-heavy` |
|
|
64
|
+
| Type | `--font-family`, `--font-mono`, `--face-100`, `--face-150`, `--face-200`, `--face-mono`, `--cap-correction` |
|
|
65
|
+
| Scale | `--size-1` … `--size-5` |
|
|
66
|
+
| Baseline | `--line`, `--line-2`, `--line-3`, `--line-4`, `--half-line`, `--module` |
|
|
67
|
+
| Space | `--space-unit`, `--space-1`, `--space-2`, `--space-3`, `--space-4`, `--space-6`, `--space-8` |
|
|
68
|
+
| Page | `--columns`, `--gutter`, `--page-max`, `--page-inset`, `--field` |
|
|
69
|
+
| Measure | `--measure`, `--measure-characters`, `--card`, `--filter-label` |
|
|
70
|
+
| Transitions | `--transition-page`, `--transition-morph`, `--transition-ease` |
|
|
364
71
|
|
|
365
|
-
|
|
366
|
-
them with the host's Stimulus application, which the shell imports. A host
|
|
367
|
-
has nothing to write; one that registers the two by hand still can, and
|
|
368
|
-
registering them twice is harmless. `copy_button` and `search_form` write
|
|
369
|
-
the `data-controller` attributes; a page without script still works, with
|
|
370
|
-
the value selectable and the search's button on the page.
|
|
72
|
+
## grid primitives
|
|
371
73
|
|
|
372
|
-
|
|
74
|
+
`.page`, `.grid`, `.run`, `.stack`, `.modules`, `.fields`, `.measure`.
|
|
373
75
|
|
|
374
|
-
|
|
375
|
-
say — a hint or the reason it was refused.
|
|
76
|
+
## components
|
|
376
77
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
78
|
+
| Class | Is |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `.masthead`, `.nav`, `.subnav`, `.menu`, `.menu--current`, `.skip-link` | Navigation |
|
|
81
|
+
| `.page-head`, `.sections`, `.footer` | Page furniture |
|
|
82
|
+
| `.button`, `.button--primary`, `.button--quiet`, `.button--danger` | Buttons |
|
|
83
|
+
| `.form`, `.form--inline`, `.form--dense`, `.field`, `.field--inline`, `.field--invalid`, `.choice`, `.errors` | Forms |
|
|
84
|
+
| `.filters`, `.filter` | Filter bars |
|
|
85
|
+
| `.cards`, `.card` | Cards |
|
|
86
|
+
| `.table`, `.numeric` | Tables |
|
|
87
|
+
| `.pairs`, `.pairs--stacked` | Definition pairs |
|
|
88
|
+
| `.figure`, `.figure--cover`, `.figure--modular` | Figures |
|
|
89
|
+
| `.pagination` | Pagination |
|
|
90
|
+
| `.md`, `.measure`, `.copy`, `.explain`, `.link-quiet`, `.empty` | Prose and states |
|
|
91
|
+
| `.css`, `.components`, `.visually-hidden`, `.no-metric-overrides`, `.rule--heavy` | Utilities |
|
|
92
|
+
|
|
93
|
+
## helpers
|
|
94
|
+
|
|
95
|
+
| Helper | Does |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `its_swiss_form_with` | The form builder |
|
|
98
|
+
| `its_swiss_stylesheet_tags` | The seven stylesheets, in layer order |
|
|
99
|
+
| `its_swiss_page_numbers` | Page numbers for pagination |
|
|
100
|
+
| `its_swiss_application_name` | The application's name |
|
|
101
|
+
| `its_swiss_face`, `its_swiss_typeface` | The typeface slots |
|
|
102
|
+
| `page_head`, `page_sections` | Page head and section destinations |
|
|
103
|
+
| `nav_link_to`, `nav_menu` | Navigation |
|
|
104
|
+
| `search_form`, `filter_register` | Filter bars |
|
|
105
|
+
| `copy_button` | A value that copies itself |
|
|
106
|
+
| `explain` | An explanatory note |
|
|
384
107
|
|
|
385
|
-
|
|
386
|
-
sees missing: a label whose `for` does not match its input does not enlarge the
|
|
387
|
-
target; a hint beside a control is a hint a screen reader never reaches; a
|
|
388
|
-
refused field coloured by CSS alone is a refusal only some readers get. The
|
|
389
|
-
builder wires `for`, `aria-describedby`, `aria-invalid` and `.field--invalid`
|
|
390
|
-
every time.
|
|
108
|
+
Partials: `its_swiss/shared/masthead`, `flash`, `errors`, `pagination`.
|
|
391
109
|
|
|
392
|
-
|
|
393
|
-
the control as `aria-label`. A textarea's `rows:` is how many lines tall it
|
|
394
|
-
is. `class: "form form--dense"` puts each label on its control's line of
|
|
395
|
-
air, three lines a field rather than four, for a form of many fields.
|
|
110
|
+
### form builder
|
|
396
111
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
112
|
+
`its_swiss_form_with` adds `field`, `select`, `collection_select`,
|
|
113
|
+
`check_box`, `radio_button`, `text_area` and `submit`, each rendering its own
|
|
114
|
+
label, hint and error, wired with `aria-describedby`.
|
|
400
115
|
|
|
401
|
-
##
|
|
116
|
+
## JavaScript
|
|
402
117
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
someone can move.
|
|
118
|
+
Three Stimulus controllers, registered by the module the shell imports. The
|
|
119
|
+
host adds nothing to its importmap.
|
|
406
120
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
121
|
+
| Controller | Does |
|
|
122
|
+
| --- | --- |
|
|
123
|
+
| `its-swiss-clipboard` | A value that copies itself |
|
|
124
|
+
| `its-swiss-live-search` | Search on an index |
|
|
125
|
+
| `its-swiss-mono` | Monospace metric handling |
|
|
411
126
|
|
|
412
|
-
|
|
127
|
+
## the cascade
|
|
413
128
|
|
|
414
|
-
|
|
129
|
+
Every file states its own layer, and the order is declared before the first
|
|
130
|
+
import:
|
|
415
131
|
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
bin/specimen tmp/specimen && node test/browsers/grid.mjs tmp/specimen/index.html # chromium, webkit, firefox
|
|
132
|
+
```css
|
|
133
|
+
@layer its-swiss.tokens, its-swiss.faces, its-swiss.reset, its-swiss.type,
|
|
134
|
+
its-swiss.grid, its-swiss.components, its-swiss.transitions;
|
|
420
135
|
```
|
|
421
136
|
|
|
422
|
-
|
|
423
|
-
|
|
137
|
+
Everything is inside `@layer`, so any application rule wins without effort. A
|
|
138
|
+
stylesheet the application adds after the library takes its place in the order
|
|
139
|
+
from where its layer is first named.
|
|
424
140
|
|
|
425
|
-
|
|
426
|
-
the
|
|
427
|
-
descending ladder, that no stylesheet invents a colour or a column count, that
|
|
428
|
-
every line box is measured in baselines, that no signal rests on colour alone.
|
|
141
|
+
`its-swiss.css` is the whole library in one `<link>`; the Rails helper writes
|
|
142
|
+
the seven separately. Both resolve identically.
|
|
429
143
|
|
|
430
|
-
|
|
431
|
-
in the CSS and does nothing on a page. Those assert what Chromium actually
|
|
432
|
-
resolved: that an unlayered declaration beats the library's layered one, that
|
|
433
|
-
the accent unset is ink, that quiet ink clears 4.5:1, that every box on the
|
|
434
|
-
specimen starts and ends on a line, that every run of type on it has its
|
|
435
|
-
baseline on one, that nothing escapes the page at 390px, and that the measure
|
|
436
|
-
lands on a field line the browser really laid out. They skip loudly rather
|
|
437
|
-
than pretending to have checked.
|
|
144
|
+
## tech
|
|
438
145
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
and Firefox through Playwright. CI runs all three; a grid checked in one
|
|
442
|
-
browser is a claim about that browser. Chromium is also asked with the faces
|
|
443
|
-
taken away and the document marked, which is the page as Safari lays it out,
|
|
444
|
-
so the trim is measured on every push and not only in the one job that has
|
|
445
|
-
WebKit.
|
|
146
|
+
Rails engine, v1.1.0. Ruby >= 3.2. 1,457 lines of CSS in seven files. No
|
|
147
|
+
build step and no CSS framework.
|
|
446
148
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
149
|
+
```sh
|
|
150
|
+
bundle exec rake test
|
|
151
|
+
```
|
|
450
152
|
|
|
451
|
-
|
|
452
|
-
|
|
153
|
+
150 runs. 30 of them drive a real browser through Capybara and Selenium, and
|
|
154
|
+
need a Chrome matching the installed ChromeDriver. The specimen is measured in
|
|
155
|
+
Chromium, WebKit and Firefox before it is published.
|
|
453
156
|
|
|
454
|
-
|
|
455
|
-
| --- | --- |
|
|
456
|
-
| `--ground` | `--paper` |
|
|
457
|
-
| `--keyline` | `--rule` |
|
|
458
|
-
| `--ink-quiet`, `--ink` | unchanged |
|
|
459
|
-
| `--accent` | unchanged — still the application's to set |
|
|
460
|
-
| `--font` | `--font-family` |
|
|
461
|
-
| `--size-1..5`, `--space-N`, `--measure`, `--page-max` | unchanged |
|
|
462
|
-
| `--baseline` (8px) | **gone.** `--line` (24px) is the baseline now — see 0.4.0 |
|
|
463
|
-
| `--space-5`, `--space-13`, `--line-6` | **gone** in 1.0.0: nothing in the library used them. `--space-6`, `--space-8` and `--line-4` remain |
|
|
464
|
-
| `.masthead__nav` | `.nav` |
|
|
465
|
-
| `.channels` | `.pairs` |
|
|
466
|
-
| `.form`, `.field`, `.button*`, `.copy`, `.errors`, `.hint`, `.empty` | unchanged |
|
|
467
|
-
| `.tag*`, `.filter*`, `.page-head`, `live-search` | `.filter*`, `.page-head`, `.cards`, `its-swiss-live-search` — in the gem since 0.8.0 |
|
|
468
|
-
| `--columns-dense`, `--card-wide`, `.swatch*` | stay in Pandatone |
|
|
469
|
-
|
|
470
|
-
Two behavioural differences to know about:
|
|
471
|
-
|
|
472
|
-
- **The masthead no longer sits on the page's field.** It lays itself out in
|
|
473
|
-
flex, so it does not break when an application changes `--columns`.
|
|
474
|
-
- **`--field` now subtracts the page's own inset** before dividing. Pandatone's
|
|
475
|
-
`--measure` was a gutter and a half wider than three fields; anything relying
|
|
476
|
-
on the old number moves in by 24px.
|
|
477
|
-
- **`.button--danger` is set apart by a step on the ladder, not an auto
|
|
478
|
-
margin.** Pandatone scoped the auto margin to two named regions; a library
|
|
479
|
-
cannot, and an auto margin across a whole page reads as a different control
|
|
480
|
-
rather than a separated one.
|
|
481
|
-
|
|
482
|
-
## Versioning
|
|
483
|
-
|
|
484
|
-
Semver with a changelog. Consumers pin `~> 1.0`.
|
|
485
|
-
|
|
486
|
-
1.0.0 is the surface three applications settled on, and what changes it now
|
|
487
|
-
changes a major. Read `CHANGELOG.md` before upgrading, and see `RELEASING.md`
|
|
488
|
-
for how a version gets out.
|
|
489
|
-
|
|
490
|
-
## License
|
|
157
|
+
## license
|
|
491
158
|
|
|
492
159
|
MIT.
|
|
@@ -381,14 +381,40 @@
|
|
|
381
381
|
.field :is(input, select, textarea):not([type="checkbox"], [type="radio"]):focus-visible {
|
|
382
382
|
outline: none;
|
|
383
383
|
border-bottom: var(--rule-heavy) solid var(--accent);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.field :is(input, select):not([type="checkbox"], [type="radio"]):focus-visible {
|
|
384
387
|
padding-block-start: calc(var(--line) - var(--rule-heavy));
|
|
385
388
|
}
|
|
386
389
|
|
|
387
390
|
/* As many lines as it was asked for: the builder writes --rows from rows:,
|
|
388
|
-
four when it was not.
|
|
391
|
+
four when it was not.
|
|
392
|
+
|
|
393
|
+
A textarea is ruled like a page: a dotted rule on every line of the
|
|
394
|
+
baseline grid that type can stand on, on the same row of pixels a rule
|
|
395
|
+
would take, and none where it cannot. The line of air is a border rather
|
|
396
|
+
than a padding, so text scrolled up is cut at the first line and never
|
|
397
|
+
crosses the air, and a mask keeps the air clear of the one rule that
|
|
398
|
+
would fall there: the line under the label, which nothing is written on.
|
|
399
|
+
The last line is closed by the control's own rule, which is drawn over
|
|
400
|
+
its dots. The dots are painted in the text's own box and scroll with it,
|
|
401
|
+
so a long message keeps a rule under every line. */
|
|
389
402
|
.field textarea {
|
|
403
|
+
--ruled-dot: var(--rule-strong);
|
|
390
404
|
block-size: calc(var(--line) * var(--rows, 4));
|
|
391
405
|
resize: vertical;
|
|
406
|
+
border-block-start: calc(var(--line) - var(--rule-hair)) solid transparent;
|
|
407
|
+
padding-block-start: 0;
|
|
408
|
+
background-image: conic-gradient(from 270deg at var(--rule-hair) var(--rule-hair), var(--ruled-dot) 25%, transparent 0);
|
|
409
|
+
background-size: var(--space-unit) var(--line);
|
|
410
|
+
background-origin: content-box;
|
|
411
|
+
background-clip: content-box;
|
|
412
|
+
background-attachment: local;
|
|
413
|
+
mask-image: linear-gradient(transparent var(--line), var(--ink) 0);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.field textarea:focus-visible {
|
|
417
|
+
border-block-start-width: calc(var(--line) - var(--rule-heavy));
|
|
392
418
|
}
|
|
393
419
|
|
|
394
420
|
/* A checkbox or a radio is the one control whose label sits beside it, on
|
data/lib/its_swiss/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: its-swiss
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bobby Meyer
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|