its-swiss 0.7.2 → 0.9.1
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 +95 -0
- data/README.md +107 -2
- data/app/assets/javascripts/its_swiss/live_search_controller.js +31 -0
- data/app/assets/stylesheets/its_swiss/components.css +315 -0
- data/app/assets/stylesheets/its_swiss/tokens.css +8 -0
- data/app/assets/stylesheets/its_swiss/type.css +22 -4
- data/app/helpers/its_swiss/application_helper.rb +114 -0
- data/app/views/its_swiss/specimen/_filters.html.erb +38 -0
- data/app/views/its_swiss/specimen/_masthead.html.erb +21 -2
- data/app/views/its_swiss/specimen/_page_head.html.erb +33 -0
- data/app/views/its_swiss/specimen/_take.html.erb +2 -0
- data/app/views/layouts/its_swiss/shell.html.erb +5 -0
- data/config/importmap.rb +6 -4
- data/lib/its_swiss/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ab6df97c9cfca52b616b05f5e985aa37df4253563fd1b1177c9c67890706cad
|
|
4
|
+
data.tar.gz: 6e9d6316b70b2670b7b307b33ec64955e88d562d085c95a86e07836e635d1a97
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a3f8671a25c996f2883add93ead145a89244a8a5bbec670022c0b3d49d587d19dd1b5a664eb94bd49908b1d8a4b28877e66b180ac49d3b653b47b0493cf7d02
|
|
7
|
+
data.tar.gz: 286de25b2203d9928f9a4cbf596341aa8d450c3cfaecf016cafff44737586df4d4cd65d98271d102f2dcf2af9e8f9e058413ead233ad86602a3e242a5f471a4d
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,101 @@
|
|
|
2
2
|
|
|
3
3
|
Semver. Consumers pin `~> 0.1`.
|
|
4
4
|
|
|
5
|
+
## 0.9.1 — 2026-09-07
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **The suite, and so the release, under json 3.0.0.** json 3.0.0 came out
|
|
10
|
+
twenty minutes after 0.9.0's pull request went green and changed the
|
|
11
|
+
signature of `JSON.parse`; Active Support 8.1.3.1 still calls it the old
|
|
12
|
+
way when it reads a signed cookie, so the session and the flash raised on
|
|
13
|
+
every request and the release of 0.9.0 stopped at its own tests. The
|
|
14
|
+
Gemfile holds json below 3 until a Rails that takes it. Nothing in the
|
|
15
|
+
library changed; 0.9.1 is 0.9.0 released.
|
|
16
|
+
|
|
17
|
+
## 0.9.0 — 2026-09-07
|
|
18
|
+
|
|
19
|
+
A day of use, and what a day asks of a style: less talk and one red.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Sections.** `page_head(title, sections:)` names the surfaces of one
|
|
24
|
+
page under its title — what a thing is made of, what it wears, how it
|
|
25
|
+
leaves — the one shown in the weight, and a page shows that one alone.
|
|
26
|
+
A working page must not scroll: what is done daily is reached from the
|
|
27
|
+
head, in the same position on every page, never by scrolling past what
|
|
28
|
+
was done yesterday. `page_sections(sections)` writes the nav on its own.
|
|
29
|
+
|
|
30
|
+
- **Explanations.** `explain(text)` puts what a section means behind one
|
|
31
|
+
mark: a question mark on the line, closed, and the text in the hint
|
|
32
|
+
register under it when it is asked for. A sentence over every table is
|
|
33
|
+
needed on the first day and never again.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **One red per page.** The chosen filter and a current menu are in the
|
|
38
|
+
weight, in ink. The accent is for where you are on the site — the nav,
|
|
39
|
+
the subnav, the page numbers — and for the one thing that cannot be
|
|
40
|
+
undone. A page that said "this one" in red five times above the fold
|
|
41
|
+
had no signal left.
|
|
42
|
+
|
|
43
|
+
- **Hints are prose.** `.hint` is set at body size. The small size is for
|
|
44
|
+
labels and metadata, and a hint set in it at a body line's leading read
|
|
45
|
+
as a list of afterthoughts. Table headings go the other way: the label
|
|
46
|
+
register, bold, on the line.
|
|
47
|
+
|
|
48
|
+
- **The filter block is ruled once, below.** The rule above it was a third
|
|
49
|
+
rule in ten lines, and the rule is the one mark this style draws.
|
|
50
|
+
|
|
51
|
+
## 0.8.0 — 2026-09-07
|
|
52
|
+
|
|
53
|
+
The third consumer, and what it and the second found in common.
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- **A page head.** `page_head(title, lede:) { actions }` writes the one
|
|
58
|
+
shape every page opens with: the title in the page-title register, its
|
|
59
|
+
lede when it has one, and the actions in a run beside them, wrapping
|
|
60
|
+
under the title when the page is narrow. Pandatone, Stripeclub and
|
|
61
|
+
Badger had each drawn their own, and no two agreed on the size of the
|
|
62
|
+
title or where the buttons went. The document is titled from it unless
|
|
63
|
+
the view has titled itself.
|
|
64
|
+
|
|
65
|
+
- **Filters.** A block of registers that narrow a list, each a quiet label
|
|
66
|
+
and then the choices, in the same two positions every time — a search, a
|
|
67
|
+
tag, an order, a size are the same kind of control, and are built the
|
|
68
|
+
same way. `filter_register(label, choices, name:)` writes one; the choice
|
|
69
|
+
in force carries `aria-current`, which the CSS colours and weights, for
|
|
70
|
+
the reason the nav says it twice. `search_form(url, frame:, keep:)`
|
|
71
|
+
writes the first register: a search that narrows the list as you type,
|
|
72
|
+
into the Turbo Frame named, carrying the other choices so a search does
|
|
73
|
+
not drop them. Two slots go with it: `--filter-label`, the column the
|
|
74
|
+
labels occupy, and `--card`.
|
|
75
|
+
|
|
76
|
+
- **Cards.** `.cards` on a `.grid` lays a list out on the page's own
|
|
77
|
+
fields, each item `--card` fields wide, two lines between the rows.
|
|
78
|
+
`.card__link`, `.card__figure`, `.card__name`, `.card__meta` are the
|
|
79
|
+
parts. No box: the picture is the only filled area a card has.
|
|
80
|
+
|
|
81
|
+
- **A menu in the nav, and a subnav under the masthead.** `nav_menu(label,
|
|
82
|
+
current:) { links }` is a destination that opens into destinations — a
|
|
83
|
+
details element, so it opens with no script — and carries the accent and
|
|
84
|
+
the weight when where you are is inside it. The shell renders `:subnav`,
|
|
85
|
+
when an application fills it, as a shaded band under the masthead: a
|
|
86
|
+
second layer of destinations, inside the one you are in. A host that
|
|
87
|
+
carries several tools puts the tools in the menu and each tool's own
|
|
88
|
+
sections in the band.
|
|
89
|
+
|
|
90
|
+
- **A live search controller.** `its_swiss/live_search_controller.js`,
|
|
91
|
+
pinned by the engine like the clipboard, submits a search form as you
|
|
92
|
+
type and takes away the button it makes redundant. The host registers it
|
|
93
|
+
as `its-swiss-live-search`, beside the clipboard.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- The micro register and the quiet colour take `.filter__label` and
|
|
98
|
+
`.card__meta`.
|
|
99
|
+
|
|
5
100
|
## 0.7.2 — 2026-09-04
|
|
6
101
|
|
|
7
102
|
### Changed
|
data/README.md
CHANGED
|
@@ -34,7 +34,7 @@ current one at `/<version>.html` for anything that needs to pin.
|
|
|
34
34
|
| In the gem | Stays in the application |
|
|
35
35
|
| --- | --- |
|
|
36
36
|
| Tokens, reset, base typography | The grid itself — which blocks span which fields |
|
|
37
|
-
| Masthead, nav, footer, table, form, button, definition list, pagination | Domain components |
|
|
37
|
+
| Masthead, nav, page head, footer, table, form, button, definition list, pagination, filters, cards | Domain components |
|
|
38
38
|
| A base layout shell with `content_for` slots | Page layouts beyond the shell |
|
|
39
39
|
| View transition names and durations | Which pages transition to which |
|
|
40
40
|
| The value scale and the accent slot | Any hue, any palette knowledge |
|
|
@@ -71,6 +71,8 @@ holding all of them. Nothing here has a default the gem could pick honestly.
|
|
|
71
71
|
| `--columns`, `--gutter` | How many fields this problem has |
|
|
72
72
|
| `--line` | The baseline: the interval everything vertical registers to |
|
|
73
73
|
| `--ratio` | A picture's aspect ratio, per figure — the library cannot read one |
|
|
74
|
+
| `--card` | How many fields a card takes, and at which widths |
|
|
75
|
+
| `--filter-label` | The column a filter's label occupies, so every register's choices start on one line |
|
|
74
76
|
| `--face-150`, `--face-200`, `--face-100` | Read, not set: the face a register names for its ratio of leading to size |
|
|
75
77
|
|
|
76
78
|
### The baseline
|
|
@@ -313,6 +315,83 @@ it stops on a field line:
|
|
|
313
315
|
--measure: calc(var(--field) * 3 + var(--gutter) * 2);
|
|
314
316
|
```
|
|
315
317
|
|
|
318
|
+
### The page head
|
|
319
|
+
|
|
320
|
+
What a page is and what can be done to it, in one shape on every page: the
|
|
321
|
+
title in the page-title register, its lede when it has one, and the actions in
|
|
322
|
+
a run beside them, wrapping under the title when the page is narrow.
|
|
323
|
+
|
|
324
|
+
```erb
|
|
325
|
+
<%= page_head "Palettes", lede: "Every palette in the library." do %>
|
|
326
|
+
<%= link_to "New palette", new_palette_path, class: "button button--primary" %>
|
|
327
|
+
<% end %>
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The document is titled from it unless the view has already set `:title`.
|
|
331
|
+
|
|
332
|
+
A page with several surfaces — what a thing is made of, what it wears, how it
|
|
333
|
+
leaves — names them under the title with `sections:`, each a name, a URL and
|
|
334
|
+
whether it is the one shown, and shows that one alone. A working page must
|
|
335
|
+
not scroll: what is done daily is reached from the head, in the same position
|
|
336
|
+
on every page.
|
|
337
|
+
|
|
338
|
+
```erb
|
|
339
|
+
<%= page_head @pattern.name, sections: [ [ "Compose", pattern_path(@pattern), @section == "compose" ],
|
|
340
|
+
[ "Dress", pattern_path(@pattern, section: "dress"), @section == "dress" ] ] do %>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Explanations
|
|
344
|
+
|
|
345
|
+
What a section means, behind one mark. A sentence over every table is needed
|
|
346
|
+
on the first day and never again, and a tool used daily is read on every
|
|
347
|
+
other day; so `explain` writes it in the hint register, closed, and a
|
|
348
|
+
question mark on the line opens it.
|
|
349
|
+
|
|
350
|
+
```erb
|
|
351
|
+
<%= explain "The repeat, in order along the stripe normal." %>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Filters and cards
|
|
355
|
+
|
|
356
|
+
What narrows a list, and the list. A search that filters as you type, and a
|
|
357
|
+
register for each other way of narrowing it — a tag, an order, a size — in the
|
|
358
|
+
same two positions every time: a quiet label, then the choices, the one in
|
|
359
|
+
force carrying `aria-current`, which the CSS sets in the weight. In ink, not
|
|
360
|
+
the accent: the accent is for where you are on the site and for the one
|
|
361
|
+
thing that cannot be undone, and a page has one red.
|
|
362
|
+
|
|
363
|
+
```erb
|
|
364
|
+
<div class="filters">
|
|
365
|
+
<%= search_form palettes_path, frame: "palettes", keep: { tag: params[:tag], sort: params[:sort] } %>
|
|
366
|
+
<%= filter_register "Tagged", [ [ "All", palettes_path, params[:tag].blank? ],
|
|
367
|
+
*tags.map { |t| [ t, palettes_path(tag: t), params[:tag] == t ] } ],
|
|
368
|
+
name: "tag" %>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
<%= turbo_frame_tag "palettes", target: "_top" do %>
|
|
372
|
+
<ul class="grid cards">
|
|
373
|
+
<li class="card">
|
|
374
|
+
<a class="card__link" href="…">
|
|
375
|
+
<figure class="card__figure figure" style="--ratio: 1.5"><img src="…" alt=""></figure>
|
|
376
|
+
<span class="card__name">Brand Core</span>
|
|
377
|
+
</a>
|
|
378
|
+
<span class="card__meta">4 swatches</span>
|
|
379
|
+
</li>
|
|
380
|
+
</ul>
|
|
381
|
+
<% end %>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
The search form belongs outside the frame it fills, so only the results are
|
|
385
|
+
replaced and the field keeps its cursor. `keep:` carries the other choices as
|
|
386
|
+
hidden fields, so a search does not drop the tag or the order you were reading
|
|
387
|
+
in. The button is there for a browser that runs no script; the controller
|
|
388
|
+
takes it away once it has connected.
|
|
389
|
+
|
|
390
|
+
A card is `--card` fields wide on the page's own grid — say how many once, and
|
|
391
|
+
again at each width that changes it. The picture is a `.figure` with its
|
|
392
|
+
`--ratio` declared, so its box is whole lines at any width and the name under
|
|
393
|
+
it stays on the grid.
|
|
394
|
+
|
|
316
395
|
## The layout shell
|
|
317
396
|
|
|
318
397
|
The installer writes `app/views/layouts/application.html.erb` as a layout
|
|
@@ -345,6 +424,7 @@ Slots, all optional:
|
|
|
345
424
|
| `:head` | Anything else that belongs in `<head>` |
|
|
346
425
|
| `:mark` | The wordmark. No mark and no nav means no masthead at all |
|
|
347
426
|
| `:nav` | The destinations |
|
|
427
|
+
| `:subnav` | A second layer of destinations, inside the one you are in: a shaded band under the masthead |
|
|
348
428
|
| `:main_class` | What the page's main region is, if it is a grid |
|
|
349
429
|
| `:footer` | Whatever belongs after the page |
|
|
350
430
|
|
|
@@ -359,9 +439,15 @@ beyond the shell is the application's, for the same reason its grid is.
|
|
|
359
439
|
| `its_swiss_stylesheet_tags` | The seven links, tracked for Turbo |
|
|
360
440
|
| `its_swiss_typeface(regular:, bold:)` | The application's typeface, declared under the library's face names |
|
|
361
441
|
| `nav_link_to(name, url, current:)` | A destination, with `aria-current` when you are at it |
|
|
442
|
+
| `nav_menu(label, current:) { links }` | A destination that opens into destinations, with no script |
|
|
362
443
|
| `copy_button(value)` | A value that copies itself |
|
|
363
444
|
| `its_swiss_form_with(...)` | `form_with`, already holding the library's builder |
|
|
364
445
|
| `its_swiss_page_numbers(page, pages)` | Which numbers a run of them shows, elided |
|
|
446
|
+
| `page_head(title, lede:, sections:) { actions }` | The one shape every page opens with |
|
|
447
|
+
| `page_sections(sections)` | The surfaces of one page, the one shown in the weight |
|
|
448
|
+
| `explain(text) { }` | What a section means, behind one mark |
|
|
449
|
+
| `filter_register(label, choices, name:)` | One register of a filter block |
|
|
450
|
+
| `search_form(url, frame:, keep:)` | A search that narrows a list as you type |
|
|
365
451
|
|
|
366
452
|
### Pagination
|
|
367
453
|
|
|
@@ -376,6 +462,24 @@ a page, a total, and something that turns a number into a URL:
|
|
|
376
462
|
Long runs are elided around the current page. `window:` (default 2) sets how
|
|
377
463
|
many neighbours show; `label:` names the `<nav>` for a screen reader.
|
|
378
464
|
|
|
465
|
+
### JavaScript
|
|
466
|
+
|
|
467
|
+
Two Stimulus controllers, pinned by the engine so an application that upgrades
|
|
468
|
+
the gem gets the new file without touching its importmap. They are outside
|
|
469
|
+
`controllers/`, so an application registers them by hand, once:
|
|
470
|
+
|
|
471
|
+
```js
|
|
472
|
+
// app/javascript/controllers/index.js
|
|
473
|
+
import ItsSwissClipboardController from "its_swiss/clipboard_controller"
|
|
474
|
+
import ItsSwissLiveSearchController from "its_swiss/live_search_controller"
|
|
475
|
+
application.register("its-swiss-clipboard", ItsSwissClipboardController)
|
|
476
|
+
application.register("its-swiss-live-search", ItsSwissLiveSearchController)
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
`copy_button` and `search_form` write the `data-controller` attributes; a page
|
|
480
|
+
without the registrations still works, with the value selectable and the
|
|
481
|
+
search's button on the page.
|
|
482
|
+
|
|
379
483
|
## The form builder
|
|
380
484
|
|
|
381
485
|
One shape for every field: a label, a control, and — when there is something to
|
|
@@ -464,7 +568,8 @@ ladder; the names are the library's rather than the application's.
|
|
|
464
568
|
| `.masthead__nav` | `.nav` |
|
|
465
569
|
| `.channels` | `.pairs` |
|
|
466
570
|
| `.form`, `.field`, `.button*`, `.copy`, `.errors`, `.hint`, `.empty` | unchanged |
|
|
467
|
-
|
|
|
571
|
+
| `.tag*`, `.filter*`, `.page-head`, `live-search` | `.filter*`, `.page-head`, `.cards`, `its-swiss-live-search` — in the gem since 0.8.0 |
|
|
572
|
+
| `--columns-dense`, `--card-wide`, `.swatch*` | stay in Pandatone |
|
|
468
573
|
|
|
469
574
|
Two behavioural differences to know about:
|
|
470
575
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Submits a search form as you type, into the Turbo Frame it targets.
|
|
4
|
+
//
|
|
5
|
+
// The filtering stays on the server, in the same scope the button uses: one
|
|
6
|
+
// definition of what a search matches, not a Ruby one and a drifting
|
|
7
|
+
// JavaScript copy. The form sits outside its frame so only the results are
|
|
8
|
+
// replaced and the field keeps its focus and its cursor.
|
|
9
|
+
//
|
|
10
|
+
// The button goes when this connects, because from that moment it is a second
|
|
11
|
+
// way to do the thing that has just been done. It is hidden here rather than
|
|
12
|
+
// left out of the markup for the browser that never runs this: without the
|
|
13
|
+
// controller the button is the only way to search, so it has to be on the
|
|
14
|
+
// page for the page to work.
|
|
15
|
+
export default class extends Controller {
|
|
16
|
+
static targets = [ "submit" ]
|
|
17
|
+
static values = { delay: { type: Number, default: 200 } }
|
|
18
|
+
|
|
19
|
+
connect() {
|
|
20
|
+
if (this.hasSubmitTarget) this.submitTarget.hidden = true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
search() {
|
|
24
|
+
clearTimeout(this.timeout)
|
|
25
|
+
this.timeout = setTimeout(() => this.element.requestSubmit(), this.delayValue)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
disconnect() {
|
|
29
|
+
clearTimeout(this.timeout)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -70,6 +70,92 @@
|
|
|
70
70
|
font-weight: 700;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/* A menu in the nav: a destination that opens into destinations. A
|
|
74
|
+
details element, so it opens and closes with no script, and a summary
|
|
75
|
+
set like the other destinations with a small mark that says it opens.
|
|
76
|
+
The list floats under it on paper with a keyline, the one floating box
|
|
77
|
+
in the style — a menu that pushed the page down would not be a menu. */
|
|
78
|
+
.menu {
|
|
79
|
+
position: relative;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.menu > summary {
|
|
83
|
+
list-style: none;
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.menu > summary::-webkit-details-marker { display: none; }
|
|
89
|
+
|
|
90
|
+
.menu > summary::after {
|
|
91
|
+
content: " \25BE";
|
|
92
|
+
color: var(--ink-quiet);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.menu > summary:hover,
|
|
96
|
+
.menu[open] > summary { color: var(--ink); }
|
|
97
|
+
|
|
98
|
+
/* Where you are, when where you are is inside the menu: the weight, in
|
|
99
|
+
ink. Not the accent — a page has one red, and the subnav under a menu
|
|
100
|
+
that is current already names the place in it. */
|
|
101
|
+
.menu--current > summary {
|
|
102
|
+
color: var(--ink);
|
|
103
|
+
font-weight: 700;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Out of the flow, but still on the grid: a line of padding above and
|
|
107
|
+
below, and the keyline drawn inside, so every link in it sits on a
|
|
108
|
+
line of the page under it. */
|
|
109
|
+
.menu__list {
|
|
110
|
+
position: absolute;
|
|
111
|
+
inset-inline-start: calc(-1 * var(--space-2));
|
|
112
|
+
top: 100%;
|
|
113
|
+
z-index: 1;
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
min-width: max-content;
|
|
117
|
+
padding: var(--line) var(--space-2);
|
|
118
|
+
background: var(--paper);
|
|
119
|
+
box-shadow: inset 0 0 0 var(--rule-hair) var(--rule);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.menu__list a {
|
|
123
|
+
display: block;
|
|
124
|
+
text-decoration: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.menu__list a:hover { color: var(--ink); }
|
|
128
|
+
|
|
129
|
+
/* --- Subnav -------------------------------------------------------------
|
|
130
|
+
A second layer of destinations, inside the one you are in: what a tool
|
|
131
|
+
has beside what the application has. A shaded band under the masthead,
|
|
132
|
+
three lines tall the way the masthead is, with the same run of links
|
|
133
|
+
the nav has and the same marking of where you are. Rendered by the
|
|
134
|
+
shell when the application fills :subnav, and not at all otherwise. */
|
|
135
|
+
/* The rule is drawn inside the band rather than under it, so the band and
|
|
136
|
+
the page inside it are both three whole lines. */
|
|
137
|
+
.subnav {
|
|
138
|
+
background: var(--paper-shaded);
|
|
139
|
+
box-shadow: inset 0 calc(-1 * var(--rule-hair)) 0 var(--rule);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.subnav > .page {
|
|
143
|
+
display: flex;
|
|
144
|
+
flex-wrap: wrap;
|
|
145
|
+
align-items: end;
|
|
146
|
+
gap: var(--line) var(--space-3);
|
|
147
|
+
padding-block: var(--line);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.subnav a { text-decoration: none; }
|
|
151
|
+
|
|
152
|
+
.subnav a:hover { color: var(--ink); }
|
|
153
|
+
|
|
154
|
+
.subnav [aria-current="page"] {
|
|
155
|
+
color: var(--accent);
|
|
156
|
+
font-weight: 700;
|
|
157
|
+
}
|
|
158
|
+
|
|
73
159
|
/* --- Footer -------------------------------------------------------------
|
|
74
160
|
A hairline, the micro register, and whatever the application has to say
|
|
75
161
|
after the page. It carries no links of its own: what belongs down there
|
|
@@ -89,6 +175,94 @@
|
|
|
89
175
|
line-height: var(--line);
|
|
90
176
|
}
|
|
91
177
|
|
|
178
|
+
/* --- Page head ----------------------------------------------------------
|
|
179
|
+
What a page is and what can be done to it. The title and, when it has
|
|
180
|
+
one, its lede on the left; the actions on the right, wrapping under
|
|
181
|
+
the title when the page is narrow. Everything after it starts two
|
|
182
|
+
lines down, which is the gap a section gets.
|
|
183
|
+
|
|
184
|
+
One shape for every page, so a reader arriving from another application
|
|
185
|
+
set in this style already knows where the name is and where the buttons
|
|
186
|
+
are. The title is the page-title register: a page has one, and it is
|
|
187
|
+
the largest thing on it. */
|
|
188
|
+
|
|
189
|
+
.page-head {
|
|
190
|
+
display: flex;
|
|
191
|
+
flex-wrap: wrap;
|
|
192
|
+
justify-content: space-between;
|
|
193
|
+
align-items: flex-start;
|
|
194
|
+
gap: var(--line) var(--space-3);
|
|
195
|
+
margin-block-end: var(--line-2);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* The title takes the measure and the actions take what they need. A
|
|
199
|
+
basis rather than a share, so a long name wraps inside the measure
|
|
200
|
+
instead of pushing the actions off the line. */
|
|
201
|
+
.page-head__title {
|
|
202
|
+
flex: 1 1 var(--measure);
|
|
203
|
+
min-width: 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* The actions shrink to their widest button and wrap inside it, rather
|
|
207
|
+
than holding their one-line width and pushing a phone's page sideways. */
|
|
208
|
+
.page-head__actions { flex: 0 1 auto; }
|
|
209
|
+
|
|
210
|
+
/* --- Sections -----------------------------------------------------------
|
|
211
|
+
One page, several surfaces: what a thing is made of, what it wears,
|
|
212
|
+
how it leaves. A run of names under the title, the one you are on in
|
|
213
|
+
the weight, and the surface below it the only thing on the page. A
|
|
214
|
+
working page must not scroll: what is done every day is reached from
|
|
215
|
+
the head, in the same position on every page, and never by scrolling
|
|
216
|
+
past what was done yesterday. */
|
|
217
|
+
.sections {
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-wrap: wrap;
|
|
220
|
+
gap: var(--line) var(--space-3);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* Its own row in the head, under the title and the actions. */
|
|
224
|
+
.page-head > .sections { flex-basis: 100%; }
|
|
225
|
+
|
|
226
|
+
.sections a {
|
|
227
|
+
display: inline-block;
|
|
228
|
+
color: var(--ink-quiet);
|
|
229
|
+
text-decoration: none;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.sections a:hover { color: var(--ink); }
|
|
233
|
+
|
|
234
|
+
/* Where you are, in the weight. Ink, not the accent: the accent is for
|
|
235
|
+
where you are on the site, and this is where you are on the page. */
|
|
236
|
+
.sections [aria-current] {
|
|
237
|
+
color: var(--ink);
|
|
238
|
+
font-weight: 700;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* --- Explanations -------------------------------------------------------
|
|
242
|
+
What a section means, behind one mark, for the day it is first read.
|
|
243
|
+
A sentence over every table is needed on the first day and never
|
|
244
|
+
again, and a tool used daily is read on every other day. So the
|
|
245
|
+
explanation is there, in the hint register, and closed: a question
|
|
246
|
+
mark on the line, and the text under it when it is asked for. A
|
|
247
|
+
details element, so it opens with no script. */
|
|
248
|
+
/* A block as wide as its one character, so the line it sits on is whole
|
|
249
|
+
and the target is the mark and not the row. */
|
|
250
|
+
.explain > summary {
|
|
251
|
+
list-style: none;
|
|
252
|
+
display: block;
|
|
253
|
+
width: max-content;
|
|
254
|
+
cursor: pointer;
|
|
255
|
+
color: var(--ink-quiet);
|
|
256
|
+
font-weight: 700;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.explain > summary::-webkit-details-marker { display: none; }
|
|
260
|
+
|
|
261
|
+
.explain > summary:hover,
|
|
262
|
+
.explain[open] > summary { color: var(--ink); }
|
|
263
|
+
|
|
264
|
+
.explain__body { margin-block-start: var(--line); }
|
|
265
|
+
|
|
92
266
|
/* --- Rules --------------------------------------------------------------
|
|
93
267
|
The one horizontal mark in the style, at the two weights tokens.css
|
|
94
268
|
declares. */
|
|
@@ -184,9 +358,15 @@
|
|
|
184
358
|
vertical-align: top;
|
|
185
359
|
}
|
|
186
360
|
|
|
361
|
+
/* A heading is a label: the label register, bold, on the line. Body size
|
|
362
|
+
made it a second row of values. */
|
|
187
363
|
.table th {
|
|
364
|
+
font-family: var(--face-200), var(--font-family);
|
|
365
|
+
font-size: var(--size-1);
|
|
366
|
+
line-height: var(--line);
|
|
188
367
|
color: var(--ink-quiet);
|
|
189
368
|
font-weight: 700;
|
|
369
|
+
letter-spacing: 0.01em;
|
|
190
370
|
}
|
|
191
371
|
|
|
192
372
|
.table thead th { border-bottom-color: var(--ink); }
|
|
@@ -490,6 +670,141 @@
|
|
|
490
670
|
|
|
491
671
|
.pagination__gap { color: var(--ink-quiet); }
|
|
492
672
|
|
|
673
|
+
/* --- Filters ------------------------------------------------------------
|
|
674
|
+
What narrows a list. One shape for every way of narrowing it — a
|
|
675
|
+
search, a tag, an order, a size — because they are the same kind of
|
|
676
|
+
control: pick one of a handful. A register is a quiet label and then
|
|
677
|
+
the choices, in the same two positions every time, so filtering and
|
|
678
|
+
ordering read as the registers they are rather than as two rows of
|
|
679
|
+
identical words. Ruled below, once: the block belongs to the list
|
|
680
|
+
under it, and the space above it is the page head's. A second rule
|
|
681
|
+
above it was a third rule in ten lines, and the rule is the one mark
|
|
682
|
+
this style draws, so it has to be rare. */
|
|
683
|
+
|
|
684
|
+
.filters {
|
|
685
|
+
display: flex;
|
|
686
|
+
flex-wrap: wrap;
|
|
687
|
+
align-items: flex-start;
|
|
688
|
+
gap: var(--line) var(--space-3);
|
|
689
|
+
/* The rule is drawn inside the box, so the block stays whole lines. */
|
|
690
|
+
padding-block-end: calc(var(--line) - var(--rule-hair));
|
|
691
|
+
border-block-end: var(--rule-hair) solid var(--rule);
|
|
692
|
+
margin-block-end: var(--line);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.filter {
|
|
696
|
+
display: flex;
|
|
697
|
+
flex-wrap: wrap;
|
|
698
|
+
align-items: flex-start;
|
|
699
|
+
flex-basis: 100%;
|
|
700
|
+
gap: var(--line) var(--space-2);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/* The label column. A width off the ladder, so every register's choices
|
|
704
|
+
start on the same line down the page rather than a few pixels apart
|
|
705
|
+
because one word is longer than another. An application with longer
|
|
706
|
+
labels sets --filter-label once. */
|
|
707
|
+
.filter__label,
|
|
708
|
+
.filters .form--inline label { min-width: var(--filter-label); }
|
|
709
|
+
|
|
710
|
+
.filter__choices {
|
|
711
|
+
display: flex;
|
|
712
|
+
flex-wrap: wrap;
|
|
713
|
+
gap: var(--line) var(--space-2);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/* The choices are the flex items themselves, the way the nav's
|
|
717
|
+
destinations are: blockified, so each is a line box that is the tap
|
|
718
|
+
target and can be trimmed where the faces are not honoured. A list
|
|
719
|
+
item between them would be a second box to correct. */
|
|
720
|
+
.filter__choices a {
|
|
721
|
+
color: var(--ink-quiet);
|
|
722
|
+
text-decoration: none;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.filter__choices a:hover { color: var(--ink); }
|
|
726
|
+
|
|
727
|
+
/* Which one is chosen: the weight, in ink. The accent is for where you
|
|
728
|
+
are on the site and for the one thing that cannot be undone; a page
|
|
729
|
+
with four registers each saying "this one" in red has no signal left. */
|
|
730
|
+
.filter__choices [aria-current] {
|
|
731
|
+
color: var(--ink);
|
|
732
|
+
font-weight: 700;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/* The search is the first register, built like the others rather than as
|
|
736
|
+
a stacked field above them: the label in the label column, the control
|
|
737
|
+
where the choices start. Label and control meet on the under edge — the
|
|
738
|
+
label's baseline is its under edge, and the control's text stands on
|
|
739
|
+
the rule that closes it — so the two read as one line. The button is
|
|
740
|
+
there for a browser that runs no script and wraps under the field at a
|
|
741
|
+
phone's width; it goes when the live search connects, because from then
|
|
742
|
+
on typing has already done what it does. */
|
|
743
|
+
.filters .form--inline {
|
|
744
|
+
flex: 1 1 var(--measure);
|
|
745
|
+
max-width: var(--measure);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.filters .form--inline .field--inline {
|
|
749
|
+
flex: 1 1 auto;
|
|
750
|
+
align-items: flex-end;
|
|
751
|
+
min-width: 0;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/* A basis of nothing, so the control takes what the row has left rather
|
|
755
|
+
than the twenty characters a browser gives a text control by default. */
|
|
756
|
+
.filters .form--inline .field--inline input {
|
|
757
|
+
flex: 1 1 0;
|
|
758
|
+
width: auto;
|
|
759
|
+
min-width: 0;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/* --- Cards --------------------------------------------------------------
|
|
763
|
+
A list of things, each a picture over its name and a line about it,
|
|
764
|
+
laid out on the page's own fields: a card is --card fields wide, and
|
|
765
|
+
the application says how many, once, at each width it cares about. Two
|
|
766
|
+
lines between the rows, because a row of cards is a row of sections.
|
|
767
|
+
|
|
768
|
+
No box. A card is the thing itself, and the picture is the only filled
|
|
769
|
+
area it has; what separates one from the next is the gutter. */
|
|
770
|
+
|
|
771
|
+
.cards { row-gap: var(--line-2); }
|
|
772
|
+
|
|
773
|
+
.cards > * {
|
|
774
|
+
--span: var(--card);
|
|
775
|
+
min-width: 0;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/* The picture and the name are one link, so a card costs one tab stop and
|
|
779
|
+
announces its destination once. */
|
|
780
|
+
.card__link {
|
|
781
|
+
display: block;
|
|
782
|
+
color: inherit;
|
|
783
|
+
text-decoration: none;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.card__link:hover .card__name {
|
|
787
|
+
text-decoration: underline;
|
|
788
|
+
text-underline-offset: 0.2em;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/* The picture is a .figure with its --ratio declared, so its box is a
|
|
792
|
+
whole number of lines at any width and the name under it stays on the
|
|
793
|
+
grid; what this adds is the line between them. */
|
|
794
|
+
.card__figure { margin-block-end: var(--line); }
|
|
795
|
+
|
|
796
|
+
/* Blocks, so each is a line of its own and can be trimmed where the faces
|
|
797
|
+
are not honoured. */
|
|
798
|
+
.card__name,
|
|
799
|
+
.card__meta { display: block; }
|
|
800
|
+
|
|
801
|
+
.card__name {
|
|
802
|
+
font-family: var(--face-150), var(--font-family);
|
|
803
|
+
font-size: var(--size-2);
|
|
804
|
+
line-height: var(--line);
|
|
805
|
+
font-weight: 700;
|
|
806
|
+
}
|
|
807
|
+
|
|
493
808
|
/* --- What a page has to say --------------------------------------------
|
|
494
809
|
Three registers: it worked, it was refused, there is nothing here. The
|
|
495
810
|
refusal is the only one that interrupts, and it interrupts with a rule
|
|
@@ -181,6 +181,14 @@
|
|
|
181
181
|
);
|
|
182
182
|
--measure: calc(var(--field) * 3 + var(--gutter) * 2);
|
|
183
183
|
|
|
184
|
+
/* --- Lists -------------------------------------------------------------
|
|
185
|
+
Two more slots an index fills. How many fields a card takes, which is
|
|
186
|
+
the application's to say and to change at each width it cares about;
|
|
187
|
+
and the column a filter's label occupies, so the choices of every
|
|
188
|
+
register start on one line down the page. */
|
|
189
|
+
--card: 1;
|
|
190
|
+
--filter-label: var(--space-8);
|
|
191
|
+
|
|
184
192
|
/* --- Transitions -------------------------------------------------------
|
|
185
193
|
The page crossfade is short enough to read as a replacement rather than
|
|
186
194
|
an animation. A morph runs longer, because the thing travelling has to
|
|
@@ -81,7 +81,9 @@
|
|
|
81
81
|
caption,
|
|
82
82
|
legend,
|
|
83
83
|
label,
|
|
84
|
-
.field label
|
|
84
|
+
.field label,
|
|
85
|
+
.filter__label,
|
|
86
|
+
.card__meta {
|
|
85
87
|
font-family: var(--face-200), var(--font-family);
|
|
86
88
|
font-size: var(--size-1);
|
|
87
89
|
line-height: var(--line);
|
|
@@ -93,16 +95,23 @@
|
|
|
93
95
|
picking a gray. */
|
|
94
96
|
.quiet,
|
|
95
97
|
.hint,
|
|
98
|
+
.filter__label,
|
|
99
|
+
.card__meta,
|
|
96
100
|
.nav a,
|
|
101
|
+
.menu > summary,
|
|
102
|
+
.subnav a,
|
|
97
103
|
.footer,
|
|
98
104
|
.pairs dt,
|
|
99
105
|
.button--quiet,
|
|
100
106
|
.button--danger,
|
|
101
107
|
.link-quiet { color: var(--ink-quiet); }
|
|
102
108
|
|
|
109
|
+
/* A hint is prose — a sentence about the thing beside it — and prose is
|
|
110
|
+
read at body size. The small size is for labels and metadata, and a
|
|
111
|
+
hint set in it read as a list of afterthoughts. */
|
|
103
112
|
.hint {
|
|
104
|
-
font-family: var(--face-
|
|
105
|
-
font-size: var(--size-
|
|
113
|
+
font-family: var(--face-150), var(--font-family);
|
|
114
|
+
font-size: var(--size-2);
|
|
106
115
|
line-height: var(--line);
|
|
107
116
|
max-width: var(--measure);
|
|
108
117
|
}
|
|
@@ -119,6 +128,8 @@
|
|
|
119
128
|
inherited, because a button does not inherit a font and a nav may sit
|
|
120
129
|
inside a register that is not the body's. */
|
|
121
130
|
.nav a,
|
|
131
|
+
.menu > summary,
|
|
132
|
+
.subnav a,
|
|
122
133
|
.button {
|
|
123
134
|
font-family: var(--face-150), var(--font-family);
|
|
124
135
|
font-size: var(--size-2);
|
|
@@ -220,11 +231,18 @@
|
|
|
220
231
|
.page-title,
|
|
221
232
|
h1, h2, h3, h4,
|
|
222
233
|
p, dt, th, td, figcaption, caption, legend, label,
|
|
223
|
-
li:not(:has(> :is(p, ul, ol, dl, div, figure, table, pre, blockquote))),
|
|
234
|
+
li:not(:has(> :is(p, ul, ol, dl, div, figure, table, pre, blockquote, .card__link))),
|
|
224
235
|
dd:not(:has(> :is(p, ul, ol, dl, div, figure, table, pre, blockquote))),
|
|
225
236
|
.micro, .hint, .lede, .empty, .masthead__mark, .field__error, .errors__title,
|
|
237
|
+
.filter__label, .card__name, .card__meta,
|
|
238
|
+
.explain > summary,
|
|
226
239
|
/* Flex items are blockified, so the text in a row is trimmable too. */
|
|
227
240
|
.nav a,
|
|
241
|
+
.sections a,
|
|
242
|
+
.menu > summary,
|
|
243
|
+
.menu__list a,
|
|
244
|
+
.subnav a,
|
|
245
|
+
.filter__choices a,
|
|
228
246
|
.pagination a,
|
|
229
247
|
.pagination [aria-current],
|
|
230
248
|
.pagination__gap,
|
|
@@ -80,6 +80,21 @@ module ItsSwiss
|
|
|
80
80
|
link_to(name, url, **options, &block)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# A destination in the nav that opens into destinations: a details
|
|
84
|
+
# element with the label as its summary and the block as its list, so it
|
|
85
|
+
# opens and closes with no script. current: when one of the destinations
|
|
86
|
+
# inside is where you are, the summary carries the accent and the weight
|
|
87
|
+
# the way a current link does.
|
|
88
|
+
#
|
|
89
|
+
# <%= nav_menu "Tools", current: in_a_tool? do %>
|
|
90
|
+
# <%= nav_link_to "Pandatone", "/pandatone", current: in_engine?(pandatone) %>
|
|
91
|
+
# <% end %>
|
|
92
|
+
def nav_menu(label, current: false, &block)
|
|
93
|
+
tag.details(class: token_list("menu", ("menu--current" if current))) do
|
|
94
|
+
safe_join([ tag.summary(label), tag.div(capture(&block), class: "menu__list") ])
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
83
98
|
# A value on screen exists to be taken somewhere else, so it is a button
|
|
84
99
|
# that copies itself. The value stays visible text inside it, which is
|
|
85
100
|
# what keeps it usable when the clipboard is not available at all.
|
|
@@ -133,6 +148,105 @@ module ItsSwiss
|
|
|
133
148
|
end
|
|
134
149
|
private :its_swiss_face
|
|
135
150
|
|
|
151
|
+
# What a page is and what can be done to it: the title, a lede when the
|
|
152
|
+
# page has one, and the actions in the block. One shape for every page,
|
|
153
|
+
# and the document is titled from it unless the view has titled itself.
|
|
154
|
+
#
|
|
155
|
+
# <%= page_head "Palettes", lede: "Every palette in the library." do %>
|
|
156
|
+
# <%= link_to "New palette", new_palette_path, class: "button button--primary" %>
|
|
157
|
+
# <% end %>
|
|
158
|
+
#
|
|
159
|
+
# A page with several surfaces names them under the title, and the one
|
|
160
|
+
# you are on carries the weight:
|
|
161
|
+
#
|
|
162
|
+
# <%= page_head @pattern.name, sections: [ [ "Compose", pattern_path(@pattern), true ],
|
|
163
|
+
# [ "Dress", pattern_path(@pattern, section: "dress"), false ] ] do %>
|
|
164
|
+
def page_head(title, lede: nil, sections: nil, &block)
|
|
165
|
+
content_for(:title, strip_tags(title.to_s)) unless content_for?(:title)
|
|
166
|
+
actions = capture(&block) if block
|
|
167
|
+
|
|
168
|
+
tag.header(class: "page-head") do
|
|
169
|
+
safe_join([
|
|
170
|
+
tag.div(class: "page-head__title") do
|
|
171
|
+
safe_join([ tag.h1(title, class: "page-title"), (tag.p(lede, class: "lede") if lede) ].compact)
|
|
172
|
+
end,
|
|
173
|
+
(tag.div(actions, class: "page-head__actions run") if actions.present?),
|
|
174
|
+
(page_sections(sections) if sections.present?)
|
|
175
|
+
].compact)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# The surfaces of one page: a name, a URL and whether it is the one
|
|
180
|
+
# shown, each a whole navigation so it survives a reload and can be
|
|
181
|
+
# sent to somebody. The page head places it; on its own it is a nav.
|
|
182
|
+
def page_sections(sections, label: "Sections")
|
|
183
|
+
tag.nav(class: "sections", aria: { label: label }) do
|
|
184
|
+
safe_join(sections.map { |text, url, current| link_to(text, url, aria: { current: ("page" if current) }) })
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# What a section means, behind one mark. The explanation is written in
|
|
189
|
+
# the hint register and closed: a page used daily is read on every day
|
|
190
|
+
# but the first, and a sentence over every table is for the first.
|
|
191
|
+
#
|
|
192
|
+
# <%= explain "The repeat, in order along the stripe normal." %>
|
|
193
|
+
# <%= explain do %><p class="hint">…</p><% end %>
|
|
194
|
+
def explain(text = nil, label: "Explain", &block)
|
|
195
|
+
body = block ? capture(&block) : tag.p(text, class: "hint")
|
|
196
|
+
|
|
197
|
+
tag.details(class: "explain") do
|
|
198
|
+
safe_join([ tag.summary("?", aria: { label: label }), tag.div(body, class: "explain__body") ])
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# One register of a filter block: a quiet label, then the choices, each
|
|
203
|
+
# a name, a URL and whether it is the one in force. Filtering and
|
|
204
|
+
# ordering are the same kind of control — pick one of a handful — so
|
|
205
|
+
# they are built the same way and told apart by their label.
|
|
206
|
+
#
|
|
207
|
+
# <%= filter_register "Tagged", [ [ "All", colors_path, params[:tag].blank? ],
|
|
208
|
+
# *tags.map { |t| [ t, colors_path(tag: t), params[:tag] == t ] } ],
|
|
209
|
+
# name: "tag" %>
|
|
210
|
+
#
|
|
211
|
+
# The one in force carries aria-current, which the CSS colours and
|
|
212
|
+
# weights, and a test can find without reading a class.
|
|
213
|
+
def filter_register(label, choices, name: nil)
|
|
214
|
+
tag.div(class: "filter", data: { filter: name }) do
|
|
215
|
+
safe_join([
|
|
216
|
+
tag.span(label, class: "filter__label"),
|
|
217
|
+
tag.div(class: "filter__choices") do
|
|
218
|
+
safe_join(choices.map { |text, url, current| link_to(text, url, aria: { current: ("true" if current) }) })
|
|
219
|
+
end
|
|
220
|
+
])
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# A search that narrows a list as you type, into the Turbo Frame named.
|
|
225
|
+
# The form belongs outside that frame, so only the results are replaced
|
|
226
|
+
# and the field keeps its cursor; the button is for a browser that runs
|
|
227
|
+
# no script, and the controller takes it away once it has connected.
|
|
228
|
+
#
|
|
229
|
+
# <%= search_form palettes_path, frame: "palettes", keep: { tag: params[:tag], sort: params[:sort] } %>
|
|
230
|
+
#
|
|
231
|
+
# keep: the other choices on the page, carried as hidden fields so a
|
|
232
|
+
# search does not drop the tag or the order you were reading in. The
|
|
233
|
+
# host registers the controller as its-swiss-live-search.
|
|
234
|
+
def search_form(url, frame:, param: :q, label: "Search", value: nil, keep: {}, delay: nil, **options)
|
|
235
|
+
value = params[param] if value.nil? && respond_to?(:params)
|
|
236
|
+
data = { controller: "its-swiss-live-search", action: "input->its-swiss-live-search#search",
|
|
237
|
+
turbo_frame: frame, its_swiss_live_search_delay_value: delay }.compact
|
|
238
|
+
|
|
239
|
+
form_with(url: url, method: :get, class: token_list("form form--inline", options[:class]), data: data) do
|
|
240
|
+
safe_join([
|
|
241
|
+
*keep.compact_blank.map { |name, kept| hidden_field_tag(name, kept, id: nil) },
|
|
242
|
+
tag.div(class: "field field--inline") do
|
|
243
|
+
safe_join([ label_tag(param, label), search_field_tag(param, value, autocomplete: "off") ])
|
|
244
|
+
end,
|
|
245
|
+
submit_tag(label, name: nil, class: "button", data: { its_swiss_live_search_target: "submit" })
|
|
246
|
+
])
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
136
250
|
# form_with, already holding the library's builder. An application that
|
|
137
251
|
# wants its own builder still can; this is the shorthand for the case
|
|
138
252
|
# where it does not.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<section data-specimen="filters">
|
|
2
|
+
<h3>Filters and cards</h3>
|
|
3
|
+
<p class="hint">
|
|
4
|
+
What narrows a list, and the list. A search that filters as you type, and
|
|
5
|
+
a register for each other way of narrowing it: the same two positions
|
|
6
|
+
every time, a quiet label and the choices, the one in force carrying the
|
|
7
|
+
weight. Under it, cards on the page's own fields — this page says a card
|
|
8
|
+
is two fields wide.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<div class="filters">
|
|
12
|
+
<%= search_form "#filters", frame: "specimen-results", value: "", keep: { tag: "print" } %>
|
|
13
|
+
<%= filter_register "Tagged",
|
|
14
|
+
[ [ "All", "#filters", false ], [ "print", "#filters", true ], [ "screen", "#filters", false ] ],
|
|
15
|
+
name: "tag" %>
|
|
16
|
+
<%= filter_register "Sort by",
|
|
17
|
+
[ [ "Name", "#filters", true ], [ "Newest", "#filters", false ], [ "Most used", "#filters", false ] ],
|
|
18
|
+
name: "sort" %>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<p class="hint">3 of 12 palettes</p>
|
|
22
|
+
|
|
23
|
+
<ul class="grid cards" style="--card: 2">
|
|
24
|
+
<% [ [ "Brand Core", "4 swatches", 1 ], [ "Press", "6 swatches", 2 ], [ "Screen", "3 swatches", 3 ] ].each do |name, meta, value| %>
|
|
25
|
+
<li class="card">
|
|
26
|
+
<a class="card__link" href="#filters">
|
|
27
|
+
<figure class="card__figure figure" style="--ratio: 1.5">
|
|
28
|
+
<svg viewBox="0 0 3 2" role="img" aria-label="A block of value <%= value %>">
|
|
29
|
+
<rect width="3" height="2" fill="var(--value-<%= value %>)"/>
|
|
30
|
+
</svg>
|
|
31
|
+
</figure>
|
|
32
|
+
<span class="card__name"><%= name %></span>
|
|
33
|
+
</a>
|
|
34
|
+
<span class="card__meta"><%= meta %></span>
|
|
35
|
+
</li>
|
|
36
|
+
<% end %>
|
|
37
|
+
</ul>
|
|
38
|
+
</section>
|
|
@@ -5,11 +5,30 @@
|
|
|
5
5
|
<nav class="nav">
|
|
6
6
|
<a href="#masthead" aria-current="page">Where you are</a>
|
|
7
7
|
<a href="#masthead">Somewhere else</a>
|
|
8
|
-
|
|
8
|
+
<%= nav_menu "A menu", current: false do %>
|
|
9
|
+
<a href="#masthead">One of several</a>
|
|
10
|
+
<a href="#masthead">Another of them</a>
|
|
11
|
+
<a href="#masthead">And a third</a>
|
|
12
|
+
<% end %>
|
|
13
|
+
<%= nav_menu "Where you also are", current: true do %>
|
|
14
|
+
<a href="#masthead" aria-current="page">Inside the menu</a>
|
|
15
|
+
<a href="#masthead">Beside it</a>
|
|
16
|
+
<% end %>
|
|
9
17
|
</nav>
|
|
10
18
|
</header>
|
|
19
|
+
<nav class="subnav" aria-label="Inside this section">
|
|
20
|
+
<div class="page">
|
|
21
|
+
<a href="#masthead">The section's mark</a>
|
|
22
|
+
<a href="#masthead" aria-current="page">Where you are in it</a>
|
|
23
|
+
<a href="#masthead">Elsewhere in it</a>
|
|
24
|
+
</div>
|
|
25
|
+
</nav>
|
|
11
26
|
<p class="hint">
|
|
12
27
|
The current destination carries the accent and the weight. Two signals,
|
|
13
|
-
because colour alone is not one.
|
|
28
|
+
because colour alone is not one. A menu is a destination that opens into
|
|
29
|
+
destinations, and carries both when where you are is inside it. The
|
|
30
|
+
subnav is a second layer, inside the one you are in: a shaded band under
|
|
31
|
+
the masthead, rendered by the shell when the application fills
|
|
32
|
+
<code>:subnav</code>.
|
|
14
33
|
</p>
|
|
15
34
|
</section>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<section data-specimen="page-head">
|
|
2
|
+
<h3>Page head</h3>
|
|
3
|
+
<p class="hint">
|
|
4
|
+
What a page is and what can be done to it: the title and its lede on the
|
|
5
|
+
left, the actions on the right, wrapping under when the page is narrow.
|
|
6
|
+
One shape for every page, so the name and the buttons are always where
|
|
7
|
+
they were on the last one.
|
|
8
|
+
</p>
|
|
9
|
+
<%= page_head "Palettes", lede: "Every palette in the library, including the ones no colour has claimed yet." do %>
|
|
10
|
+
<a href="#page-head" class="button button--primary">New palette</a>
|
|
11
|
+
<a href="#page-head" class="button">Import from a file</a>
|
|
12
|
+
<a href="#page-head" class="button">New from this</a>
|
|
13
|
+
<a href="#page-head" class="button button--danger">Delete palette</a>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<h4>Sections</h4>
|
|
17
|
+
<p class="hint">
|
|
18
|
+
A page with several surfaces names them under the title, the one shown
|
|
19
|
+
in the weight, and shows that one alone. What is done daily is reached
|
|
20
|
+
from the head, never by scrolling past what was done yesterday.
|
|
21
|
+
</p>
|
|
22
|
+
<%= page_head "Deck Chair", sections: [ [ "Compose", "#page-head", true ], [ "Dress", "#page-head", false ], [ "Export", "#page-head", false ] ] do %>
|
|
23
|
+
<a href="#page-head" class="button button--primary">Dress this pattern</a>
|
|
24
|
+
<a href="#page-head" class="button button--quiet">Rename</a>
|
|
25
|
+
<% end %>
|
|
26
|
+
|
|
27
|
+
<h4>Explanations</h4>
|
|
28
|
+
<p class="hint">
|
|
29
|
+
What a section means, behind one mark, for the day it is first read.
|
|
30
|
+
Closed, in the hint register, and there when it is asked for.
|
|
31
|
+
</p>
|
|
32
|
+
<%= explain "The repeat, in order along the stripe normal. Widths sum to one." %>
|
|
33
|
+
</section>
|
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
<%= render "its_swiss/specimen/type" %>
|
|
10
10
|
<%= render "its_swiss/specimen/grid" %>
|
|
11
11
|
<%= render "its_swiss/specimen/masthead" %>
|
|
12
|
+
<%= render "its_swiss/specimen/page_head" %>
|
|
12
13
|
<%= render "its_swiss/specimen/buttons" %>
|
|
13
14
|
<%= render "its_swiss/specimen/form" %>
|
|
14
15
|
<%= render "its_swiss/specimen/figure" %>
|
|
15
16
|
<%= render "its_swiss/specimen/table" %>
|
|
16
17
|
<%= render "its_swiss/specimen/pairs" %>
|
|
18
|
+
<%= render "its_swiss/specimen/filters" %>
|
|
17
19
|
<%= render "its_swiss/specimen/pagination" %>
|
|
18
20
|
<%= render "its_swiss/specimen/messages" %>
|
|
19
21
|
<%= render "its_swiss/specimen/footer" %>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
:head anything else that belongs in <head>
|
|
10
10
|
:mark the wordmark. No mark and no nav means no masthead at all
|
|
11
11
|
:nav the destinations
|
|
12
|
+
:subnav a second layer of destinations, inside the one you are in
|
|
12
13
|
:main_class what the page's main region is, if it is a grid
|
|
13
14
|
:footer whatever belongs after the page
|
|
14
15
|
|
|
@@ -48,6 +49,10 @@
|
|
|
48
49
|
|
|
49
50
|
<%= render "its_swiss/shared/masthead" %>
|
|
50
51
|
|
|
52
|
+
<% if content_for?(:subnav) %>
|
|
53
|
+
<nav class="subnav" aria-label="Inside this section"><div class="page"><%= content_for(:subnav) %></div></nav>
|
|
54
|
+
<% end %>
|
|
55
|
+
|
|
51
56
|
<main id="main" class="page <%= content_for(:main_class) %>">
|
|
52
57
|
<%= render "its_swiss/shared/flash" %>
|
|
53
58
|
<%= yield %>
|
data/config/importmap.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
# The library's whole JavaScript surface. A typographic
|
|
2
|
-
# script to set type;
|
|
3
|
-
# somewhere else should be a button that copies
|
|
4
|
-
#
|
|
1
|
+
# The library's whole JavaScript surface: two controllers. A typographic
|
|
2
|
+
# system does not need script to set type; these are here because a value
|
|
3
|
+
# that exists to be taken somewhere else should be a button that copies
|
|
4
|
+
# itself, and a search should narrow the list as you type, and neither can
|
|
5
|
+
# be done in CSS.
|
|
5
6
|
pin "its_swiss/clipboard_controller", to: "its_swiss/clipboard_controller.js"
|
|
7
|
+
pin "its_swiss/live_search_controller", to: "its_swiss/live_search_controller.js"
|
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.9.1
|
|
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-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|
|
@@ -105,6 +105,7 @@ files:
|
|
|
105
105
|
- LICENSE
|
|
106
106
|
- README.md
|
|
107
107
|
- app/assets/javascripts/its_swiss/clipboard_controller.js
|
|
108
|
+
- app/assets/javascripts/its_swiss/live_search_controller.js
|
|
108
109
|
- app/assets/stylesheets/its-swiss.css
|
|
109
110
|
- app/assets/stylesheets/its_swiss/components.css
|
|
110
111
|
- app/assets/stylesheets/its_swiss/faces.css
|
|
@@ -122,11 +123,13 @@ files:
|
|
|
122
123
|
- app/views/its_swiss/shared/_pagination.html.erb
|
|
123
124
|
- app/views/its_swiss/specimen/_buttons.html.erb
|
|
124
125
|
- app/views/its_swiss/specimen/_figure.html.erb
|
|
126
|
+
- app/views/its_swiss/specimen/_filters.html.erb
|
|
125
127
|
- app/views/its_swiss/specimen/_footer.html.erb
|
|
126
128
|
- app/views/its_swiss/specimen/_form.html.erb
|
|
127
129
|
- app/views/its_swiss/specimen/_grid.html.erb
|
|
128
130
|
- app/views/its_swiss/specimen/_masthead.html.erb
|
|
129
131
|
- app/views/its_swiss/specimen/_messages.html.erb
|
|
132
|
+
- app/views/its_swiss/specimen/_page_head.html.erb
|
|
130
133
|
- app/views/its_swiss/specimen/_pagination.html.erb
|
|
131
134
|
- app/views/its_swiss/specimen/_pairs.html.erb
|
|
132
135
|
- app/views/its_swiss/specimen/_table.html.erb
|