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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e87eed3705a969f109f9e21d5c4242173bcbcbbb6d0700441fbeb588c5aa6845
4
- data.tar.gz: ac3ab6f0964280e6ea4f2ac67db52975ab7f55cfd9a341976515a76718275d2b
3
+ metadata.gz: 23a617f3f75e3b652becb681c8b54a38d122ba3aa0d3b7cfa4cc3187eb2177b5
4
+ data.tar.gz: d7d384f67cf5653cb917322ccb560c05fa6a97858f8668c46785c7520e86a562
5
5
  SHA512:
6
- metadata.gz: 38eb6ffa9089d649da9a1f6fd57ebc71e54515445051147acb4aad296c2d68bf1641926113f45b8a79f757cc52f1a5034a19f447d11ce01b2cd51c8b4e32b055
7
- data.tar.gz: f48734b201bf2528f749c1990065d7ae68c1da803d3d67aec8f717f11bf48da95620f9a1b469ec0294fc794d603c3ad2be3c038475d4e670e5e9eefddcb76201
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
- A Swiss typographic style for Rails applications, in two layers.
3
+ sensible Swiss defaults for Rails
4
4
 
5
- The **core** is plain CSS and one small piece of JavaScript: tokens, faces, a
6
- reset, typography, grid primitives, components and view transition rules. It
7
- needs nothing but a `<link>` tag.
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
- The **engine** ships that core through `app/assets`, plus a base layout shell,
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
- gem "its-swiss", "~> 1.0"
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
- Then open `/its-swiss/specimen`.
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
- | In the gem | Stays in the application |
24
+ | It writes | Which is |
35
25
  | --- | --- |
36
- | Tokens, reset, base typography | The grid itself — which blocks span which fields |
37
- | Masthead, nav, page head, footer, table, form, button, definition list, pagination, filters, cards | Domain components |
38
- | A base layout shell with `content_for` slots | Page layouts beyond the shell |
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 the gem ships is inside a cascade layer. An application's own CSS is
48
- unlayered, and an unlayered rule beats every layered one whatever its
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
- Each file declares its own layer, so linking the seven files individually (what
58
- `its_swiss_stylesheet_tags` does) and linking the single `its-swiss.css` that
59
- imports them resolve identically.
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
- ## The slots an application fills
37
+ ## the shell
62
38
 
63
- `bin/rails generate its_swiss:install` writes `app/assets/stylesheets/theme.css`
64
- holding all of them. Nothing here has a default the gem could pick honestly.
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 | What it is |
42
+ | Slot | Is |
67
43
  | --- | --- |
68
- | `--accent`, `--accent-ink` | State and emphasis only. Unset, the accent is ink |
69
- | `--font-family` | The typeface. The gem ships none — declare yours with `its_swiss_typeface`, and this is what a machine without it falls through to |
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 of destinations, inside the one you are in: a shaded band under the masthead |
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
- The shell writes the view transition opt-in, the seven stylesheet links, the
330
- importmap tags, the library's JavaScript module, a skip link, and the flash. It stops there — a page layout
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
- ## Helpers
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
- Long runs are elided around the current page. `window:` (default 2) sets how
361
- many neighbours show; `label:` names the `<nav>` for a screen reader.
57
+ Set in `theme.css`; everything else reads them.
362
58
 
363
- ### JavaScript
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
- Two Stimulus controllers, pinned by the engine, and a module that registers
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
- ## The form builder
74
+ `.page`, `.grid`, `.run`, `.stack`, `.modules`, `.fields`, `.measure`.
373
75
 
374
- One shape for every field: a label, a control, and — when there is something to
375
- say — a hint or the reason it was refused.
76
+ ## components
376
77
 
377
- ```erb
378
- <%= its_swiss_form_with model: @palette do |form| %>
379
- <%= form.text_field :name, hint: "As it appears in the nav." %>
380
- <%= form.check_box :published %>
381
- <%= form.submit "Save" %>
382
- <% end %>
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
- It exists because the parts that get left out by hand are the parts nobody
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
- `label: false` hides the label rather than removing it — the name moves onto
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
- A control's rule is the strong rule at rest, ink under the hand, and the
398
- accent, heavy, when the control has the focus or has been refused. The focus
399
- is the rule, not a box.
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
- ## The specimen
116
+ ## JavaScript
402
117
 
403
- `/its-swiss/specimen`, mounted by the installer under `if Rails.env.development?`
404
- and refused a second time by the controller, because a route is a line in a file
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
- It renders every component, the type scale, the value scale and the grid
408
- primitives — **twice**. The first take is the library exactly as it ships, with
409
- the accent collapsed onto ink; the second sets an accent and changes nothing
410
- else. If the first reads correctly, the value scale is doing the work.
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
- It is the documentation and the regression fixture.
127
+ ## the cascade
413
128
 
414
- ## Tests
129
+ Every file states its own layer, and the order is declared before the first
130
+ import:
415
131
 
416
- ```sh
417
- bin/test # everything; browser tests skip
418
- CHROME_BINARY=... CHROMEDRIVER=... bin/test # including the browser tests
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
- Tests come first, and a guard is only kept if removing what it guards makes it
423
- fail.
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
- Most of the suite reads the CSS rather than rendering it, which is enough for
426
- the decisions that are decisions about the source: that the value scale is a
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
- The rest needs a browser, because a rule on the wrong selector reads correctly
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
- The two grid questions are one function, `test/support/on_the_grid.js`, and
440
- `test/browsers/grid.mjs` asks it of the published specimen in Chromium, WebKit
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
- There are no pixel tests.
448
-
449
- ## Coming from Pandatone
149
+ ```sh
150
+ bundle exec rake test
151
+ ```
450
152
 
451
- The token names changed where the boundary moved. The value scale is the same
452
- ladder; the names are the library's rather than the application's.
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
- | Pandatone | its-swiss |
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
@@ -1,3 +1,3 @@
1
1
  module ItsSwiss
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
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.0.0
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-07 00:00:00.000000000 Z
11
+ date: 2026-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties