its-swiss 0.1.0 → 0.3.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 +180 -0
- data/README.md +77 -9
- data/app/assets/stylesheets/its_swiss/components.css +33 -12
- data/app/assets/stylesheets/its_swiss/reset.css +10 -2
- data/app/assets/stylesheets/its_swiss/specimen.css +6 -0
- data/app/assets/stylesheets/its_swiss/tokens.css +19 -1
- data/app/assets/stylesheets/its_swiss/type.css +63 -0
- data/app/views/its_swiss/shared/_pagination.html.erb +11 -12
- data/app/views/its_swiss/specimen/_table.html.erb +13 -5
- data/app/views/layouts/its_swiss/shell.html.erb +7 -2
- data/lib/generators/its_swiss/install/install_generator.rb +17 -11
- data/lib/generators/its_swiss/install/templates/application.html.erb +32 -0
- data/lib/its_swiss/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 07e25008eecb7b3bdc7e35b37c934d1c948e30f36bca49283798499740fc6c14
|
|
4
|
+
data.tar.gz: 987b4c9b997effa03a2b2071a7d7072580b36d3655d3f760a6299b5e80e3eab4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2651041e9048e42c29b62456ebd9d9c8f3ad4077315ea8b73c5e839442c7994031d3e7336efcf02435326da91daa3d5ec853919d906664350fc9537c9a799a08
|
|
7
|
+
data.tar.gz: 8ec6e22df73670a4a118cf10d60a87fc756f5fdae210a2134f3a9c3a38be5210ef30187518046f638a58abff2e4c02b463f809ee7a57d22de32eaf8bd17573db
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,184 @@
|
|
|
2
2
|
|
|
3
3
|
Semver. Consumers pin `~> 0.1`.
|
|
4
4
|
|
|
5
|
+
## 0.3.0 — 2026-09-01
|
|
6
|
+
|
|
7
|
+
The vertical rhythm, twice: once to put every box on the baseline, and again
|
|
8
|
+
to put the type on it. Found from outside, in a page that embedded the
|
|
9
|
+
specimen and turned the baseline overlay on — the grid was drawn correctly and
|
|
10
|
+
nothing sat on it.
|
|
11
|
+
|
|
12
|
+
Rendering moves. Every ruled component is a pixel or two shorter, and where a
|
|
13
|
+
browser can trim a text box the leading comes out of the block, so a page is
|
|
14
|
+
tighter than 0.2.0 by a few pixels per block. Read this section before
|
|
15
|
+
upgrading.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **The vertical rhythm was decorative.** Every line box was a whole number of
|
|
20
|
+
baselines and the page was still off the grid, because a line box is not a
|
|
21
|
+
box: wherever a rule sat on a padded edge, its width was *added* to a box
|
|
22
|
+
whose padding was already whole baselines. A hairline is a pixel, so each
|
|
23
|
+
one moved everything below it by a pixel — and since the masthead, the
|
|
24
|
+
footer, `hr`, every table row, every field control, every button, the
|
|
25
|
+
pagination and the errors block all did it, the error accumulated down the
|
|
26
|
+
column rather than showing up once. On the specimen the page was one pixel
|
|
27
|
+
out by the first heading and seven by the footer.
|
|
28
|
+
|
|
29
|
+
Every rule now comes out of the padding it sits on — `calc(var(--space-1) -
|
|
30
|
+
var(--rule-hair))` — and `hr` draws its rule on the top edge of a box one
|
|
31
|
+
baseline tall instead of compensating in a margin, because a margin
|
|
32
|
+
compensation collapses away exactly where a rule leads or precedes a
|
|
33
|
+
section. Boxes that change height: a button and a table row are 40px rather
|
|
34
|
+
than 42 and 41, a field control 32 rather than 33, a `select` 32 rather than
|
|
35
|
+
29.
|
|
36
|
+
- **A token in a line of prose grew the line.** `code`, `kbd` and `samp` set
|
|
37
|
+
in the mono stack are a second font on the line, with an ascent and a
|
|
38
|
+
descent of their own that the line box had to hold — a pixel at body size,
|
|
39
|
+
four under the page title, and only on the lines that happened to mention a
|
|
40
|
+
token. Their leading is now zero, which leaves the line box to the strut;
|
|
41
|
+
the glyphs are untouched.
|
|
42
|
+
- **`select` ignored its leading.** `font: inherit` hands a control the family
|
|
43
|
+
and the size and puts its line box back to `normal`, so a select stood 29px
|
|
44
|
+
where every other control stood 32. It is now given the height the rest of
|
|
45
|
+
the ladder produces.
|
|
46
|
+
- **A checkbox row was 20px.** The label beside a checkbox took the micro
|
|
47
|
+
register's 16px line box, which is not the row a 13px control sits on. It
|
|
48
|
+
now takes the same 24px line the library already gives the micro type you
|
|
49
|
+
can tap.
|
|
50
|
+
- **Table rows landed on half pixels.** With `border-collapse: collapse` the
|
|
51
|
+
shared rule belongs to the boundary rather than to either cell, and the
|
|
52
|
+
browser splits it: every row sat at `x.5` and the header row was half a
|
|
53
|
+
pixel short. Nothing in the library draws a border two cells could share, so
|
|
54
|
+
borders are separate — with the spacing still zero — and a row is exactly as
|
|
55
|
+
tall as the ladder says.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **The type sits on the baseline now, not merely in step with it.** Boxes in
|
|
60
|
+
step are not a baseline grid. Where a line's baseline falls inside its line
|
|
61
|
+
box depends on the font's ascent and the leading either side of it, so at
|
|
62
|
+
0.2.0 a paragraph's baselines sat a pixel off the grid, a caption's four,
|
|
63
|
+
and the two were three pixels out of register with each other — every
|
|
64
|
+
register keeping its own grid, none of them the page's.
|
|
65
|
+
|
|
66
|
+
Every text register is now trimmed — `text-box: trim-both cap alphabetic` —
|
|
67
|
+
which makes a block's over edge the cap of its first line and its under edge
|
|
68
|
+
the baseline of its last. What then separates the box from the grid is the
|
|
69
|
+
cap height, and one padding rounds it up: `calc(round(up, 1cap,
|
|
70
|
+
var(--baseline)) - 1cap)`, measured by the browser in cap units, so a
|
|
71
|
+
library that leaves the typeface to the application still never has to be
|
|
72
|
+
told the font's metrics. It is published as `--cap-correction`, which any
|
|
73
|
+
component setting its own padding adds to it — `calc(var(--space-1) +
|
|
74
|
+
var(--cap-correction))` is the right padding in both paths.
|
|
75
|
+
|
|
76
|
+
Behind `@supports`, and the fallback is the box rhythm below: Chromium and
|
|
77
|
+
Safari trim, Firefox does not yet. **The two do not render identically.**
|
|
78
|
+
Trimming takes the leading out of a block's own box, so the spacing you
|
|
79
|
+
declare is the spacing you see; a trimmed page is a few pixels tighter per
|
|
80
|
+
block than the same page in Firefox, and the ladder means what it says
|
|
81
|
+
rather than what it says plus half a line. Both stay on the grid.
|
|
82
|
+
|
|
83
|
+
Controls opt out — a button is five baselines of box with its label centred,
|
|
84
|
+
and trimming the label would take the box with it — as does any block whose
|
|
85
|
+
content is not type. `text-box: normal; padding-block-start: 0` is how.
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
|
|
89
|
+
- **`rake release` did not exist.** The release workflow runs
|
|
90
|
+
`bundle exec rake release`, which is Bundler's task and arrives with
|
|
91
|
+
`require "bundler/gem_tasks"` — which the Rakefile did not have. So a
|
|
92
|
+
release passed every check it makes, minted its credentials over OIDC, and
|
|
93
|
+
stopped at *Don't know how to build task 'release'*. 0.2.0 never reached
|
|
94
|
+
RubyGems for this reason, and 0.1.0 only did because it was pushed by hand
|
|
95
|
+
before the workflow existed, which the "already published" guard then read
|
|
96
|
+
as nothing to do. Guarded now, along with the workflow's own tag-versus-
|
|
97
|
+
gemspec check.
|
|
98
|
+
- **Every browser test was skipping in CI.** The suite looks for a browser and
|
|
99
|
+
a driver, and a candidate found on the `PATH` came back as the bare name it
|
|
100
|
+
was looked up by. Selenium wants a file — given `"chromedriver"` it raises
|
|
101
|
+
*not a file* — so the harness caught that, fell back to `rack_test` and
|
|
102
|
+
skipped, on a runner that had just installed a driver for it. The job was
|
|
103
|
+
green and had checked nothing: 12 runs, 12 skips. Candidates now resolve to
|
|
104
|
+
where they actually are, and the CI job now names the browser and the driver
|
|
105
|
+
the same step installed — the runner image ships a chromedriver of its own,
|
|
106
|
+
and a driver a major version ahead of the browser refuses to start a session
|
|
107
|
+
at all — and sets `REQUIRE_BROWSER`, which turns a skip into a failure — a skipped browser test is invisible in a passing
|
|
108
|
+
job, and the assertions that need a browser are the ones about what the
|
|
109
|
+
cascade and the box model actually did.
|
|
110
|
+
- **The `:head` slot rendered before the library's own stylesheets**, so a
|
|
111
|
+
layer named there took its place in the order ahead of every layer the
|
|
112
|
+
library declares and lost to all of them. The specimen's own furniture is
|
|
113
|
+
one such stylesheet, and it could not override the library it documents. The
|
|
114
|
+
slot now comes after.
|
|
115
|
+
|
|
116
|
+
### Guards added
|
|
117
|
+
|
|
118
|
+
- **Every register's last baseline is on a baseline**, measured in the
|
|
119
|
+
browser. Under `trim-both … alphabetic` a block's under edge *is* that
|
|
120
|
+
baseline, so measuring the box measures the type — and it is measured rather
|
|
121
|
+
than probed on purpose: inserting a span to read a baseline re-lays out a
|
|
122
|
+
trimmed page and moves the thing it was measuring. Skips where the browser
|
|
123
|
+
cannot trim.
|
|
124
|
+
- **The correction is zero where the browser cannot trim**, read from the
|
|
125
|
+
source, so the enhancement can never become a requirement.
|
|
126
|
+
- **Every box on the specimen starts on a baseline and is a whole number of
|
|
127
|
+
them tall**, measured in the browser on the page that holds one of
|
|
128
|
+
everything. The suite already asserted that every *line box* was a whole
|
|
129
|
+
number of baselines, which was true throughout and is what let all of the
|
|
130
|
+
above through.
|
|
131
|
+
- The leading guard now admits `0`, the one value that is not a measurement:
|
|
132
|
+
an inline box that takes no part in the line it sits on.
|
|
133
|
+
|
|
134
|
+
## 0.2.0 — 2026-08-31
|
|
135
|
+
|
|
136
|
+
Everything here was found by the second consumer inside a day of building on
|
|
137
|
+
0.1.0, which is roughly the point of having one.
|
|
138
|
+
|
|
139
|
+
### Fixed
|
|
140
|
+
|
|
141
|
+
- **The pagination partial printed its own source.** Its opening comment held
|
|
142
|
+
a worked example written in ERB; the example's own closing delimiter ended
|
|
143
|
+
the comment, and the two lines after it were emitted as page content. On
|
|
144
|
+
every page that rendered it — including the specimen, the library's own
|
|
145
|
+
documentation and regression fixture, for the whole of 0.1.0. The example
|
|
146
|
+
now lives in the README, where it can be quoted.
|
|
147
|
+
- **`.table .numeric` zeroed the end padding of every numeric cell**, not just
|
|
148
|
+
one in the final column, because it set the padding with the `padding-inline`
|
|
149
|
+
shorthand. Anywhere else in a table the next column's text began exactly
|
|
150
|
+
where the number ended. Now sets only `padding-inline-start`, and leaves the
|
|
151
|
+
final column to the `:last-child` rule that already handled it.
|
|
152
|
+
|
|
153
|
+
### Changed
|
|
154
|
+
|
|
155
|
+
- **`its_swiss:install` generates a nested layout** — `app/views/layouts/
|
|
156
|
+
application.html.erb` ending in `render template: "layouts/its_swiss/shell"`
|
|
157
|
+
— instead of injecting `layout "its_swiss/shell"` into `ApplicationController`.
|
|
158
|
+
The shell is filled through `content_for`, which has to run while a view is
|
|
159
|
+
rendering; naming it on the controller rendered the shell but left nowhere
|
|
160
|
+
to fill it once, so an application wrote its masthead into every view. The
|
|
161
|
+
generated layout also **links `theme.css`**, which nothing did before: an
|
|
162
|
+
application that followed the README exactly got no accent and no grid, with
|
|
163
|
+
no error anywhere.
|
|
164
|
+
|
|
165
|
+
Existing installs keep working. To move: delete the `layout` line from
|
|
166
|
+
`ApplicationController` and add the layout, or re-run the generator.
|
|
167
|
+
|
|
168
|
+
### Guards added
|
|
169
|
+
|
|
170
|
+
Three of the four findings were in views, a generator and a form builder —
|
|
171
|
+
surfaces a CSS library's tests do not reach. Two new ones, both of which fail
|
|
172
|
+
on the bugs above:
|
|
173
|
+
|
|
174
|
+
- The rendered specimen contains no template delimiters. Every other assertion
|
|
175
|
+
in that file was about markup that should be present; none about output that
|
|
176
|
+
should not be.
|
|
177
|
+
- No view's comment quotes ERB. A comment ends at the first closing delimiter
|
|
178
|
+
it meets, so an example inside one is an example that truncates it.
|
|
179
|
+
|
|
180
|
+
The specimen's table now has a numeric column that is not last, and a browser
|
|
181
|
+
test measures its end padding — the case that broke.
|
|
182
|
+
|
|
5
183
|
## 0.1.0 — 2026-08-31
|
|
6
184
|
|
|
7
185
|
The first extraction, from Pandatone, which ships on it.
|
|
@@ -53,5 +231,7 @@ They are in the boundary the handoff drew and they break the library's own
|
|
|
53
231
|
rule about two applications; they are the two components most likely to move
|
|
54
232
|
when Stripeclub lands.
|
|
55
233
|
|
|
234
|
+
*(Both of them shipped broken. See 0.2.0.)*
|
|
235
|
+
|
|
56
236
|
`.footer` is the same shape of guess — the shell has the slot and Pandatone
|
|
57
237
|
does not fill it.
|
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@ the consuming application's to set — it is not a theme with the colour left
|
|
|
14
14
|
out, it is a style whose argument is that the values are enough.
|
|
15
15
|
|
|
16
16
|
```ruby
|
|
17
|
-
gem "its-swiss"
|
|
17
|
+
gem "its-swiss", "~> 0.1"
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
```sh
|
|
@@ -63,6 +63,44 @@ holding all of them. Nothing here has a default the gem could pick honestly.
|
|
|
63
63
|
| `--font-family` | The typeface. The gem ships none — declare `@font-face` and name it |
|
|
64
64
|
| `--value-chroma`, `--value-hue` | Warms the whole value scale together. Neutral as shipped |
|
|
65
65
|
| `--columns`, `--gutter`, `--baseline` | How many fields this problem has, and the unit everything vertical is measured in |
|
|
66
|
+
| `--cap-correction` | Read, not set: what a trimmed register adds above its cap to reach the next baseline |
|
|
67
|
+
|
|
68
|
+
### The baseline
|
|
69
|
+
|
|
70
|
+
Everything vertical is measured in baselines, and the type sits on them rather
|
|
71
|
+
than merely in step with them.
|
|
72
|
+
|
|
73
|
+
Boxes in step are the easy half: line boxes and spacing are whole numbers of
|
|
74
|
+
`--baseline`, so blocks that start together stay together down the column.
|
|
75
|
+
That alone is not a baseline grid — where a line's baseline falls inside its
|
|
76
|
+
line box depends on the font's ascent and the leading either side of it, so a
|
|
77
|
+
caption and a paragraph can both be in step and still be three pixels out of
|
|
78
|
+
register with each other.
|
|
79
|
+
|
|
80
|
+
So every text register is trimmed to its own type: `text-box: trim-both cap
|
|
81
|
+
alphabetic` makes a block's over edge the cap of its first line and its under
|
|
82
|
+
edge the baseline of its last, and one padding rounds the cap height up to the
|
|
83
|
+
next baseline —
|
|
84
|
+
|
|
85
|
+
```css
|
|
86
|
+
padding-block-start: calc(round(up, 1cap, var(--baseline)) - 1cap);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
— which the browser computes in cap units, so the library still never has to
|
|
90
|
+
be told the font's metrics. It is published as `--cap-correction`: a component
|
|
91
|
+
that sets its own padding on trimmed text adds it, as
|
|
92
|
+
`calc(var(--space-1) + var(--cap-correction))`, which is the right padding
|
|
93
|
+
whether or not the browser trims.
|
|
94
|
+
|
|
95
|
+
Two things opt out, with `text-box: normal; padding-block-start: 0`: a control,
|
|
96
|
+
whose box is the target and whose label is centred in it, and any block whose
|
|
97
|
+
content is not type — an image, a swatch, a diagram.
|
|
98
|
+
|
|
99
|
+
It is behind `@supports`, and the fallback is the box rhythm above. Chromium
|
|
100
|
+
and Safari trim; Firefox does not yet. The two do not render identically:
|
|
101
|
+
trimming takes the leading out of a block's own box, so a trimmed page is a
|
|
102
|
+
few pixels tighter per block and the ladder means what it says rather than
|
|
103
|
+
what it says plus half a line. Both stay on the grid.
|
|
66
104
|
|
|
67
105
|
### The value scale
|
|
68
106
|
|
|
@@ -126,12 +164,28 @@ it stops on a field line:
|
|
|
126
164
|
|
|
127
165
|
## The layout shell
|
|
128
166
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
167
|
+
The installer writes `app/views/layouts/application.html.erb` as a layout
|
|
168
|
+
**for** the shell rather than one instead of it:
|
|
169
|
+
|
|
170
|
+
```erb
|
|
171
|
+
<% content_for :head do %>
|
|
172
|
+
<%= stylesheet_link_tag "theme", "data-turbo-track": "reload" %>
|
|
173
|
+
<% end %>
|
|
174
|
+
|
|
175
|
+
<% content_for :mark do %><%= link_to "Your app", root_path %><% end %>
|
|
176
|
+
|
|
177
|
+
<%= render template: "layouts/its_swiss/shell" %>
|
|
133
178
|
```
|
|
134
179
|
|
|
180
|
+
Nested, not `layout "its_swiss/shell"` on a controller. `content_for` has to
|
|
181
|
+
run while a view is rendering, and these slots are set once for the whole
|
|
182
|
+
application — naming the shell on the controller renders it but leaves
|
|
183
|
+
nowhere to fill it, so every view ends up writing the same masthead.
|
|
184
|
+
|
|
185
|
+
Note the `:head` slot links `theme.css`. The shell links the library's six
|
|
186
|
+
stylesheets and stops; the accent and the grid live in yours, and nothing
|
|
187
|
+
links it but this.
|
|
188
|
+
|
|
135
189
|
Slots, all optional:
|
|
136
190
|
|
|
137
191
|
| Slot | |
|
|
@@ -157,6 +211,19 @@ beyond the shell is the application's, for the same reason its grid is.
|
|
|
157
211
|
| `its_swiss_form_with(...)` | `form_with`, already holding the library's builder |
|
|
158
212
|
| `its_swiss_page_numbers(page, pages)` | Which numbers a run of them shows, elided |
|
|
159
213
|
|
|
214
|
+
### Pagination
|
|
215
|
+
|
|
216
|
+
The library has no paginator and no opinion about which one you use — it takes
|
|
217
|
+
a page, a total, and something that turns a number into a URL:
|
|
218
|
+
|
|
219
|
+
```erb
|
|
220
|
+
<%= render "its_swiss/shared/pagination",
|
|
221
|
+
page: @page, pages: @pages, url: ->(n) { colors_path(page: n) } %>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Long runs are elided around the current page. `window:` (default 2) sets how
|
|
225
|
+
many neighbours show; `label:` names the `<nav>` for a screen reader.
|
|
226
|
+
|
|
160
227
|
## The form builder
|
|
161
228
|
|
|
162
229
|
One shape for every field: a label, a control, and — when there is something to
|
|
@@ -211,10 +278,11 @@ every line box is measured in baselines, that no signal rests on colour alone.
|
|
|
211
278
|
The rest needs a browser, because a rule on the wrong selector reads correctly
|
|
212
279
|
in the CSS and does nothing on a page. Those assert what Chromium actually
|
|
213
280
|
resolved: that an unlayered declaration beats the library's layered one, that
|
|
214
|
-
the accent unset is ink, that quiet ink clears 4.5:1, that every
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
281
|
+
the accent unset is ink, that quiet ink clears 4.5:1, that every box on the
|
|
282
|
+
specimen starts on a baseline and is a whole number of them tall, that every
|
|
283
|
+
register's last baseline lands on one, that nothing escapes the page at 390px,
|
|
284
|
+
and that the measure lands on a field line the browser really laid out. They
|
|
285
|
+
skip loudly rather than pretending to have checked.
|
|
218
286
|
|
|
219
287
|
There are no pixel tests.
|
|
220
288
|
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
flex-wrap: wrap;
|
|
23
23
|
align-items: baseline;
|
|
24
24
|
gap: var(--space-2) var(--space-4);
|
|
25
|
-
padding-block: var(--space-3);
|
|
25
|
+
padding-block: var(--space-3) calc(var(--space-3) - var(--rule-hair));
|
|
26
26
|
border-bottom: var(--rule-hair) solid var(--rule);
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
|
|
69
69
|
.footer {
|
|
70
70
|
margin-top: var(--space-13);
|
|
71
|
-
padding-block: var(--space-3);
|
|
71
|
+
padding-block: calc(var(--space-3) - var(--rule-hair)) var(--space-3);
|
|
72
72
|
border-top: var(--rule-hair) solid var(--rule);
|
|
73
73
|
font-size: var(--size-1);
|
|
74
74
|
line-height: var(--space-3);
|
|
@@ -78,11 +78,19 @@
|
|
|
78
78
|
The one horizontal mark in the style, at the two weights tokens.css
|
|
79
79
|
declares. */
|
|
80
80
|
|
|
81
|
+
/* The rule is drawn on the top edge of a box one baseline tall rather than
|
|
82
|
+
on a box of no height at all, so its width is inside the box and both
|
|
83
|
+
margins stay whole baselines. Compensating in a margin instead looks
|
|
84
|
+
right until the margin collapses — a rule leading a section loses its
|
|
85
|
+
top margin to the section's, a rule before one loses its bottom margin to
|
|
86
|
+
the same — and a compensation that has collapsed away is a pixel back on
|
|
87
|
+
the page. Both weights fit the same box, which is why the heavy one only
|
|
88
|
+
changes the drawing. */
|
|
81
89
|
hr {
|
|
82
|
-
height:
|
|
90
|
+
height: var(--space-1);
|
|
83
91
|
border: 0;
|
|
84
92
|
border-top: var(--rule-hair) solid var(--rule);
|
|
85
|
-
margin-block: var(--space-3);
|
|
93
|
+
margin-block: var(--space-3) var(--space-2);
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
hr.rule--heavy { border-top-width: var(--rule-heavy); border-color: var(--ink); }
|
|
@@ -104,7 +112,7 @@
|
|
|
104
112
|
|
|
105
113
|
.table th,
|
|
106
114
|
.table td {
|
|
107
|
-
padding-block: var(--space-1);
|
|
115
|
+
padding-block: calc(var(--space-1) + var(--cap-correction)) calc(var(--space-1) - var(--rule-hair));
|
|
108
116
|
padding-inline-end: var(--space-3);
|
|
109
117
|
border-bottom: var(--rule-hair) solid var(--rule);
|
|
110
118
|
vertical-align: baseline;
|
|
@@ -120,8 +128,13 @@
|
|
|
120
128
|
.table td:last-child,
|
|
121
129
|
.table th:last-child { padding-inline-end: 0; }
|
|
122
130
|
|
|
123
|
-
/* A column of numbers reads right-aligned against its own heading.
|
|
124
|
-
|
|
131
|
+
/* A column of numbers reads right-aligned against its own heading. The
|
|
132
|
+
space moves to the start, where the previous column's text now ends —
|
|
133
|
+
but only the start: written as the padding-inline shorthand this also set
|
|
134
|
+
the end to zero on every numeric cell, which is right for one in the
|
|
135
|
+
final column, already covered by the rule above, and wrong anywhere else,
|
|
136
|
+
because the next column then begins exactly where the number ends. */
|
|
137
|
+
.table .numeric { text-align: right; padding-inline-start: var(--space-3); }
|
|
125
138
|
|
|
126
139
|
/* --- Definition list ----------------------------------------------------
|
|
127
140
|
Term and value in two columns, the term set in the quiet register and the
|
|
@@ -193,9 +206,15 @@
|
|
|
193
206
|
font: inherit;
|
|
194
207
|
/* No padding above it: the rule belongs under the text, not a line's
|
|
195
208
|
height below it, or the label reads as belonging to nothing. */
|
|
196
|
-
padding-block: 0 var(--space-1);
|
|
209
|
+
padding-block: 0 calc(var(--space-1) - var(--rule-hair));
|
|
197
210
|
}
|
|
198
211
|
|
|
212
|
+
/* A select takes its height from the browser rather than from the leading
|
|
213
|
+
it was given — `font: inherit` hands it the family and the size and puts
|
|
214
|
+
the line box back to normal — so it is the one control told what it is:
|
|
215
|
+
the same four baselines the others come out at. */
|
|
216
|
+
.field select { height: var(--space-4); }
|
|
217
|
+
|
|
199
218
|
.field textarea { resize: vertical; }
|
|
200
219
|
|
|
201
220
|
/* A checkbox or a radio is the one control whose label sits beside it. */
|
|
@@ -233,7 +252,8 @@
|
|
|
233
252
|
background: transparent;
|
|
234
253
|
color: var(--ink);
|
|
235
254
|
font-family: inherit;
|
|
236
|
-
padding: var(--space-1) var(--
|
|
255
|
+
padding-block: calc(var(--space-1) - var(--rule-hair));
|
|
256
|
+
padding-inline: var(--space-2);
|
|
237
257
|
cursor: pointer;
|
|
238
258
|
text-decoration: none;
|
|
239
259
|
}
|
|
@@ -329,7 +349,7 @@
|
|
|
329
349
|
align-items: baseline;
|
|
330
350
|
gap: var(--space-1) var(--space-2);
|
|
331
351
|
margin-top: var(--space-5);
|
|
332
|
-
padding-top: var(--space-2);
|
|
352
|
+
padding-top: calc(var(--space-2) - var(--rule-hair));
|
|
333
353
|
border-top: var(--rule-hair) solid var(--rule);
|
|
334
354
|
}
|
|
335
355
|
|
|
@@ -356,7 +376,7 @@
|
|
|
356
376
|
|
|
357
377
|
.errors {
|
|
358
378
|
width: 100%;
|
|
359
|
-
padding-top: var(--space-2);
|
|
379
|
+
padding-top: calc(var(--space-2) - var(--rule-heavy));
|
|
360
380
|
border-top: var(--rule-heavy) solid var(--accent);
|
|
361
381
|
}
|
|
362
382
|
|
|
@@ -393,7 +413,8 @@
|
|
|
393
413
|
left: var(--space-2);
|
|
394
414
|
top: var(--space-2);
|
|
395
415
|
z-index: 1;
|
|
396
|
-
padding: var(--space-1) var(--
|
|
416
|
+
padding-block: calc(var(--space-1) - var(--rule-hair));
|
|
417
|
+
padding-inline: var(--space-2);
|
|
397
418
|
background: var(--paper);
|
|
398
419
|
border: var(--rule-hair) solid var(--ink);
|
|
399
420
|
transform: translateY(calc(-100% - var(--space-4)));
|
|
@@ -28,8 +28,16 @@
|
|
|
28
28
|
img, svg, video { max-width: 100%; display: block; }
|
|
29
29
|
|
|
30
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
|
-
|
|
31
|
+
spacing ladder should be setting rather than the browser — hence the zero
|
|
32
|
+
spacing.
|
|
33
|
+
|
|
34
|
+
Separate rather than collapsed, because a collapsed border belongs to the
|
|
35
|
+
boundary rather than to either cell, and the browser pays for it by
|
|
36
|
+
splitting it: every row lands on a half pixel and the first one is half a
|
|
37
|
+
pixel short. Nothing here draws a vertical rule or a border two cells
|
|
38
|
+
could share, so separate borders cost nothing and each row is exactly as
|
|
39
|
+
tall as the ladder says. */
|
|
40
|
+
table { border-collapse: separate; border-spacing: 0; }
|
|
33
41
|
|
|
34
42
|
/* The browser hides [hidden] with display: none, and any author rule that
|
|
35
43
|
sets display at all outranks it — a .button says inline-block, so a
|
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
box-shadow: inset 0 0 0 var(--rule-hair) color-mix(in oklch, var(--ink) 12%, transparent);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/* A swatch is not a line of type. The type registers carry a padding that
|
|
26
|
+
rounds their cap height up to a baseline; a dd whose whole content is a
|
|
27
|
+
block of value has no cap to correct, and the padding would just make the
|
|
28
|
+
row four pixels taller than the ladder says. */
|
|
29
|
+
.pairs dd:has(> .specimen__value) { text-box: normal; padding-block-start: 0; }
|
|
30
|
+
|
|
25
31
|
/* The field, drawn. Each child is one span, and the bar inside it shows
|
|
26
32
|
where that span starts and stops against the page. */
|
|
27
33
|
.specimen__grid { row-gap: var(--space-1); }
|
|
@@ -63,10 +63,28 @@
|
|
|
63
63
|
|
|
64
64
|
/* --- Rules -------------------------------------------------------------
|
|
65
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.
|
|
66
|
+
kept for the two or three places a page has something to insist on.
|
|
67
|
+
|
|
68
|
+
A rule is drawn inside the box it belongs to, never on top of it:
|
|
69
|
+
wherever one sits on a padded edge, its width comes out of that
|
|
70
|
+
padding — calc(var(--space-1) - var(--rule-hair)) — and wherever it is
|
|
71
|
+
the element, out of the margin above it. A pixel added instead of
|
|
72
|
+
taken is a box a pixel taller than the ladder says, which puts
|
|
73
|
+
everything below it off the baseline; and since every rule on the page
|
|
74
|
+
does it, the error accumulates down the column rather than showing up
|
|
75
|
+
once. */
|
|
67
76
|
--rule-hair: 1px;
|
|
68
77
|
--rule-heavy: 2px;
|
|
69
78
|
|
|
79
|
+
/* What a trimmed block adds above its cap to reach the next baseline.
|
|
80
|
+
Zero here and given a value in type.css where the browser can trim, so
|
|
81
|
+
a component that sets its own padding writes the same arithmetic in
|
|
82
|
+
both directions — calc(var(--space-1) + var(--cap-correction)) is the
|
|
83
|
+
padding either way. It is deliberately not resolved here: a custom
|
|
84
|
+
property is substituted where it is used, so 1cap inside it is the cap
|
|
85
|
+
height of the element that reads it rather than of the root. */
|
|
86
|
+
--cap-correction: 0px;
|
|
87
|
+
|
|
70
88
|
/* --- Type --------------------------------------------------------------
|
|
71
89
|
Five sizes on an alternating 1.33 / 1.5 ratio, every one a multiple of
|
|
72
90
|
four. Five is the count an interface actually uses: labels, body, a
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
At a 16px line box these are 14px targets, under the 24px minimum. The
|
|
61
61
|
leading does the work so the type does not have to change. */
|
|
62
62
|
.micro--tap,
|
|
63
|
+
.choice label,
|
|
63
64
|
.pagination a,
|
|
64
65
|
.pagination [aria-current] { line-height: var(--space-3); }
|
|
65
66
|
|
|
@@ -106,4 +107,66 @@
|
|
|
106
107
|
.copy { font: inherit; }
|
|
107
108
|
|
|
108
109
|
code, pre, kbd, samp { font-family: var(--font-mono); font-size: 0.9em; }
|
|
110
|
+
|
|
111
|
+
/* --- The baseline, actually -------------------------------------------
|
|
112
|
+
Everything above puts whole baselines between one line box and the next,
|
|
113
|
+
which keeps blocks in step but is not yet a baseline grid: where a line's
|
|
114
|
+
baseline falls inside its line box depends on the font's ascent and the
|
|
115
|
+
leading either side of it, so a caption's baselines and a paragraph's are
|
|
116
|
+
both 24 apart and 3px out of register with each other. The grid was the
|
|
117
|
+
boxes' and the type sat wherever the font left it.
|
|
118
|
+
|
|
119
|
+
Trimming makes the box the type's: the block's over edge becomes the cap
|
|
120
|
+
of its first line and its under edge the baseline of its last. The cap
|
|
121
|
+
height is then all that separates the box from the grid, and one padding
|
|
122
|
+
rounds it up to a whole baseline — measured by the browser, in cap units,
|
|
123
|
+
so a library that leaves the family to the application still never has to
|
|
124
|
+
be told the font's metrics. Every baseline in the block lands on a line,
|
|
125
|
+
because the rest are a whole number of them above the last.
|
|
126
|
+
|
|
127
|
+
Behind @supports, and the fallback is the rhythm above: Chromium and
|
|
128
|
+
Safari trim, Firefox does not yet, and a page that keeps its boxes on the
|
|
129
|
+
ladder is what the two have in common. */
|
|
130
|
+
@supports (text-box: trim-both cap alphabetic) and (padding-top: round(up, 1cap, 8px)) {
|
|
131
|
+
:root { --cap-correction: calc(round(up, 1cap, var(--baseline)) - 1cap); }
|
|
132
|
+
|
|
133
|
+
.page-title,
|
|
134
|
+
h1, h2, h3, h4,
|
|
135
|
+
p, li, dt, dd, th, td, blockquote, figcaption, caption, legend, label,
|
|
136
|
+
.micro, .hint, .lede, .empty,
|
|
137
|
+
/* Flex items are blockified, so the text in a row is trimmable too — and
|
|
138
|
+
has to be, or a row that aligns on the baseline is aligning a trimmed
|
|
139
|
+
box against an untrimmed one. */
|
|
140
|
+
.nav a,
|
|
141
|
+
.pagination a,
|
|
142
|
+
.pagination [aria-current],
|
|
143
|
+
.pagination__gap {
|
|
144
|
+
text-box: trim-both cap alphabetic;
|
|
145
|
+
padding-block-start: var(--cap-correction);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* A control's box is the target, not the type in it: a button is five
|
|
149
|
+
baselines of box with its label centred, and trimming the label would
|
|
150
|
+
take the box with it. */
|
|
151
|
+
.button,
|
|
152
|
+
.copy,
|
|
153
|
+
/* The label beside a checkbox is the exception among labels: it shares a
|
|
154
|
+
row with a control the browser draws at a size of its own, and the row
|
|
155
|
+
is held to a line by the leading rather than by the box. */
|
|
156
|
+
.choice label { text-box: normal; padding-block-start: 0; }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* A token set in a line of prose is a second font on that line, and a
|
|
160
|
+
second font brings its own ascent and descent. Both inline boxes align on
|
|
161
|
+
the baseline and the line box has to hold both, so the line grows — by a
|
|
162
|
+
pixel at body size, by four under the page title, and only on the lines
|
|
163
|
+
that happen to mention a token. Zero leading takes the inline box out of
|
|
164
|
+
that calculation and leaves the line to the strut, which is the one thing
|
|
165
|
+
on the line already measured in baselines. The glyphs are unaffected:
|
|
166
|
+
leading has never been what draws them.
|
|
167
|
+
|
|
168
|
+
Inline only. A pre is a block, and a block with no leading has no lines. */
|
|
169
|
+
code, kbd, samp { line-height: 0; }
|
|
170
|
+
|
|
171
|
+
pre { line-height: var(--space-3); }
|
|
109
172
|
}
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
+
<%# locals: (page:, pages:, url:, window: 2, label: "Pages") %>
|
|
1
2
|
<%#
|
|
2
|
-
A run of numbers under a hairline. Takes a page number, a total, and a
|
|
3
|
-
that turns a number into a URL — the library has no paginator and
|
|
4
|
-
about which one an application uses.
|
|
3
|
+
A run of numbers under a hairline. Takes a page number, a total, and a
|
|
4
|
+
callable that turns a number into a URL — the library has no paginator and
|
|
5
|
+
no opinion about which one an application uses. Long runs are elided around
|
|
6
|
+
the current page, because a hundred numbers is not a control.
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
not a control.
|
|
8
|
+
The worked example is in the README, and has to be: an ERB comment ends at
|
|
9
|
+
the first closing delimiter it meets, including one belonging to an example
|
|
10
|
+
written inside it, and every line after that is emitted as page content.
|
|
11
|
+
Which is what this file did, on the specimen, for the whole of 0.1.0.
|
|
11
12
|
%>
|
|
12
|
-
<% window = local_assigns.fetch(:window, 2) %>
|
|
13
|
-
<% numbers = its_swiss_page_numbers(page, pages, window:) %>
|
|
14
13
|
<% if pages > 1 %>
|
|
15
|
-
<nav class="pagination" aria-label="<%=
|
|
16
|
-
<%
|
|
14
|
+
<nav class="pagination" aria-label="<%= label %>">
|
|
15
|
+
<% its_swiss_page_numbers(page, pages, window:).each do |number| %>
|
|
17
16
|
<% if number.nil? %>
|
|
18
17
|
<span class="pagination__gap" aria-hidden="true">…</span>
|
|
19
18
|
<% elsif number == page %>
|
|
@@ -3,13 +3,21 @@
|
|
|
3
3
|
<table class="table">
|
|
4
4
|
<caption>Ruled horizontally, never vertically. No zebra: a filled row is a filled area.</caption>
|
|
5
5
|
<thead>
|
|
6
|
-
<tr
|
|
6
|
+
<tr>
|
|
7
|
+
<th scope="col">Token</th>
|
|
8
|
+
<th scope="col" class="numeric">Lightness</th>
|
|
9
|
+
<th scope="col">For</th>
|
|
10
|
+
</tr>
|
|
7
11
|
</thead>
|
|
8
12
|
<tbody>
|
|
9
|
-
<tr><td>Paper</td><td
|
|
10
|
-
<tr><td>Hairline</td><td
|
|
11
|
-
<tr><td>Quiet ink</td><td
|
|
12
|
-
<tr><td>Ink</td><td
|
|
13
|
+
<tr><td>Paper</td><td class="numeric">98%</td><td>The ground</td></tr>
|
|
14
|
+
<tr><td>Hairline</td><td class="numeric">89%</td><td>A rule you barely see</td></tr>
|
|
15
|
+
<tr><td>Quiet ink</td><td class="numeric">54%</td><td>Secondary text</td></tr>
|
|
16
|
+
<tr><td>Ink</td><td class="numeric">18%</td><td>Ink</td></tr>
|
|
13
17
|
</tbody>
|
|
14
18
|
</table>
|
|
19
|
+
<p class="hint">
|
|
20
|
+
The numeric column is deliberately not the last one: a figure that runs
|
|
21
|
+
straight into the next column is what this table shipped with.
|
|
22
|
+
</p>
|
|
15
23
|
</section>
|
|
@@ -29,10 +29,15 @@
|
|
|
29
29
|
<%= csrf_meta_tags %>
|
|
30
30
|
<%= csp_meta_tag %>
|
|
31
31
|
|
|
32
|
-
<%= yield :head %>
|
|
33
|
-
|
|
34
32
|
<%= its_swiss_stylesheet_tags %>
|
|
35
33
|
<%= javascript_importmap_tags if respond_to?(:javascript_importmap_tags) %>
|
|
34
|
+
|
|
35
|
+
<%# After the library, not before it. A layer takes its place in the order
|
|
36
|
+
from where it is first named, so a page that adds a layered stylesheet
|
|
37
|
+
through this slot — the specimen's own furniture is one — would
|
|
38
|
+
otherwise land ahead of every layer the library declares and lose to
|
|
39
|
+
all of them. %>
|
|
40
|
+
<%= yield :head %>
|
|
36
41
|
</head>
|
|
37
42
|
|
|
38
43
|
<body>
|
|
@@ -16,15 +16,20 @@ module ItsSwiss
|
|
|
16
16
|
template "theme.css", "app/assets/stylesheets/theme.css"
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
# A nested layout, not a `layout` line on the controller.
|
|
20
|
+
#
|
|
21
|
+
# 0.1.0 injected `layout "its_swiss/shell"` into ApplicationController.
|
|
22
|
+
# That renders the shell, but the shell is filled through content_for,
|
|
23
|
+
# and content_for has to run while a view is rendering — so there was
|
|
24
|
+
# nowhere to set the masthead once, and an application would write it
|
|
25
|
+
# into every view before noticing. This is the Rails idiom for the same
|
|
26
|
+
# thing, and it needs no line on the controller at all, because
|
|
27
|
+
# application.html.erb is already the default layout's name.
|
|
28
|
+
#
|
|
29
|
+
# copy_file rather than template: the file is ERB the application will
|
|
30
|
+
# run, not ERB this generator should.
|
|
31
|
+
def create_the_layout
|
|
32
|
+
copy_file "application.html.erb", "app/views/layouts/application.html.erb"
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
# Development only, and the controller refuses it a second time: the
|
|
@@ -50,8 +55,9 @@ module ItsSwiss
|
|
|
50
55
|
|
|
51
56
|
its-swiss is installed. What is left is yours:
|
|
52
57
|
|
|
53
|
-
app/assets/stylesheets/theme.css
|
|
54
|
-
/
|
|
58
|
+
app/assets/stylesheets/theme.css the accent, the typeface, the grid
|
|
59
|
+
app/views/layouts/application.html.erb the shell's slots, to fill
|
|
60
|
+
/its-swiss/specimen every component, rendered twice
|
|
55
61
|
|
|
56
62
|
The library ships no typeface and no palette. Set --accent to one
|
|
57
63
|
colour and read the specimen with it unset: if the page still works,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<%# What this application puts in the slots its_swiss/shell leaves. Everything
|
|
2
|
+
else a layout used to have to remember — the view transition opt-in, the
|
|
3
|
+
library's stylesheets, the importmap, a way past the masthead for a
|
|
4
|
+
keyboard, and the flash — is the shell's.
|
|
5
|
+
|
|
6
|
+
A layout rather than `layout "its_swiss/shell"` on the controller, because
|
|
7
|
+
content_for has to run while a view is rendering and these are set once for
|
|
8
|
+
the whole application. The last line is what makes this a layout *for* the
|
|
9
|
+
shell rather than one instead of it. %>
|
|
10
|
+
|
|
11
|
+
<% content_for :head do %>
|
|
12
|
+
<%# This application's own stylesheets. The shell links the library's six and
|
|
13
|
+
stops; theme.css holds the accent and the grid, and nothing links it but
|
|
14
|
+
this. Unlayered, so they win wherever the two meet, whatever order they
|
|
15
|
+
arrive in. %>
|
|
16
|
+
<%= stylesheet_link_tag "theme", "data-turbo-track": "reload" %>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<% content_for :mark do %>
|
|
20
|
+
<%= link_to Rails.application.class.module_parent_name.underscore.humanize, root_path %>
|
|
21
|
+
<% end %>
|
|
22
|
+
|
|
23
|
+
<% content_for :nav do %>
|
|
24
|
+
<%# nav_link_to marks where you are with aria-current, which the library
|
|
25
|
+
colours and weights. Pass current: where current_page? would be wrong —
|
|
26
|
+
on a show page that still belongs to an index, say. %>
|
|
27
|
+
<% end %>
|
|
28
|
+
|
|
29
|
+
<%# Give main the grid when this application's pages are laid out on one:
|
|
30
|
+
content_for :main_class, "grid" %>
|
|
31
|
+
|
|
32
|
+
<%= render template: "layouts/its_swiss/shell" %>
|
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: 0.
|
|
4
|
+
version: 0.3.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-
|
|
11
|
+
date: 2026-09-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|
|
@@ -136,6 +136,7 @@ files:
|
|
|
136
136
|
- config/importmap.rb
|
|
137
137
|
- config/routes.rb
|
|
138
138
|
- lib/generators/its_swiss/install/install_generator.rb
|
|
139
|
+
- lib/generators/its_swiss/install/templates/application.html.erb
|
|
139
140
|
- lib/generators/its_swiss/install/templates/theme.css
|
|
140
141
|
- lib/its-swiss.rb
|
|
141
142
|
- lib/its_swiss.rb
|