its-swiss 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +57 -0
  3. data/LICENSE +21 -0
  4. data/README.md +262 -0
  5. data/app/assets/javascripts/its_swiss/clipboard_controller.js +33 -0
  6. data/app/assets/stylesheets/its-swiss.css +16 -0
  7. data/app/assets/stylesheets/its_swiss/components.css +403 -0
  8. data/app/assets/stylesheets/its_swiss/grid.css +67 -0
  9. data/app/assets/stylesheets/its_swiss/reset.css +50 -0
  10. data/app/assets/stylesheets/its_swiss/specimen.css +37 -0
  11. data/app/assets/stylesheets/its_swiss/tokens.css +142 -0
  12. data/app/assets/stylesheets/its_swiss/transitions.css +47 -0
  13. data/app/assets/stylesheets/its_swiss/type.css +109 -0
  14. data/app/controllers/its_swiss/specimen_controller.rb +24 -0
  15. data/app/helpers/its_swiss/application_helper.rb +73 -0
  16. data/app/views/its_swiss/shared/_errors.html.erb +14 -0
  17. data/app/views/its_swiss/shared/_flash.html.erb +8 -0
  18. data/app/views/its_swiss/shared/_masthead.html.erb +15 -0
  19. data/app/views/its_swiss/shared/_pagination.html.erb +26 -0
  20. data/app/views/its_swiss/specimen/_buttons.html.erb +15 -0
  21. data/app/views/its_swiss/specimen/_footer.html.erb +7 -0
  22. data/app/views/its_swiss/specimen/_form.html.erb +18 -0
  23. data/app/views/its_swiss/specimen/_grid.html.erb +14 -0
  24. data/app/views/its_swiss/specimen/_masthead.html.erb +15 -0
  25. data/app/views/its_swiss/specimen/_messages.html.erb +13 -0
  26. data/app/views/its_swiss/specimen/_pagination.html.erb +4 -0
  27. data/app/views/its_swiss/specimen/_pairs.html.erb +8 -0
  28. data/app/views/its_swiss/specimen/_table.html.erb +15 -0
  29. data/app/views/its_swiss/specimen/_take.html.erb +19 -0
  30. data/app/views/its_swiss/specimen/_type.html.erb +24 -0
  31. data/app/views/its_swiss/specimen/_values.html.erb +18 -0
  32. data/app/views/its_swiss/specimen/show.html.erb +27 -0
  33. data/app/views/layouts/its_swiss/shell.html.erb +55 -0
  34. data/config/importmap.rb +5 -0
  35. data/config/routes.rb +6 -0
  36. data/lib/generators/its_swiss/install/install_generator.rb +70 -0
  37. data/lib/generators/its_swiss/install/templates/theme.css +44 -0
  38. data/lib/its-swiss.rb +3 -0
  39. data/lib/its_swiss/engine.rb +39 -0
  40. data/lib/its_swiss/form_builder.rb +140 -0
  41. data/lib/its_swiss/version.rb +3 -0
  42. data/lib/its_swiss.rb +46 -0
  43. metadata +172 -0
@@ -0,0 +1,47 @@
1
+ /* View transitions.
2
+ *
3
+ * The library names the transitions and says how long they last. Which page
4
+ * transitions to which is the application's, because it depends on what the
5
+ * pages are: a name is given to an element by whoever knows that the same
6
+ * record is on both sides of the navigation.
7
+ *
8
+ * The at-rule covers a plain cross-document navigation. Turbo drives the same
9
+ * transitions from the <meta name="view-transition"> tag the shell writes.
10
+ */
11
+
12
+ @layer its-swiss.transitions {
13
+ @view-transition { navigation: auto; }
14
+
15
+ /* The page crossfade is taken of the body rather than of :root, which is
16
+ the element that carries it by default. A snapshot of :root is sized to
17
+ the viewport including the scrollbar, while the page is laid out in the
18
+ width left after it — so for the length of every navigation the document
19
+ is wider than its own viewport and genuinely scrolls sideways. It settles
20
+ back the moment the transition ends, which is why the page measures clean
21
+ whenever it is asked standing still. */
22
+ :root { view-transition-name: none; }
23
+
24
+ body { view-transition-name: page; }
25
+
26
+ ::view-transition-old(page),
27
+ ::view-transition-new(page) {
28
+ animation-duration: var(--transition-page);
29
+ animation-timing-function: var(--transition-ease);
30
+ }
31
+
32
+ /* Anything an application has named travels rather than crossfading, and
33
+ runs longer, because the thing travelling has to stay readable while it
34
+ does. */
35
+ ::view-transition-group(*) {
36
+ animation-duration: var(--transition-morph);
37
+ animation-timing-function: var(--transition-ease);
38
+ }
39
+
40
+ @media (prefers-reduced-motion: reduce) {
41
+ @view-transition { navigation: none; }
42
+
43
+ ::view-transition-group(*),
44
+ ::view-transition-old(*),
45
+ ::view-transition-new(*) { animation: none; }
46
+ }
47
+ }
@@ -0,0 +1,109 @@
1
+ /* Typography.
2
+ *
3
+ * One family, five sizes, two weights. Hierarchy comes from size, weight and
4
+ * where a thing sits on the grid — never from a box drawn around it.
5
+ *
6
+ * Every line box is a whole number of baselines, taken from the one ladder in
7
+ * tokens.css, so a line of type and the space beneath it are measured in the
8
+ * same units.
9
+ */
10
+
11
+ @layer its-swiss.type {
12
+ body {
13
+ font-family: var(--font-family);
14
+ font-size: var(--size-2);
15
+ font-weight: 400;
16
+ line-height: var(--space-3);
17
+ text-align: left;
18
+ /* A weight the file does not carry is better absent than drawn by the
19
+ browser: a synthesised bold at a large size is visibly a smeared
20
+ regular, and this style leans on exactly two weights. */
21
+ font-synthesis: none;
22
+ }
23
+
24
+ /* Flush left, ragged right, everywhere. */
25
+ h1, h2, h3, h4, p, dt, dd, li, label, legend, caption, th, td { text-align: left; }
26
+
27
+ /* --- The five registers -------------------------------------------------
28
+ Named for what they are rather than for the elements that happen to use
29
+ them, so an application can put a page title in an <h2> when the outline
30
+ calls for it without the type changing under it. */
31
+
32
+ .page-title {
33
+ font-size: var(--size-5);
34
+ line-height: var(--space-6);
35
+ font-weight: 700;
36
+ letter-spacing: -0.028em;
37
+ text-wrap: balance;
38
+ }
39
+
40
+ h1 { font-size: var(--size-4); line-height: var(--space-5); font-weight: 700; letter-spacing: -0.02em; }
41
+
42
+ h2 { font-size: var(--size-3); line-height: var(--space-4); font-weight: 700; letter-spacing: -0.015em; }
43
+
44
+ h3, h4 { font-size: var(--size-2); line-height: var(--space-3); font-weight: 700; }
45
+
46
+ /* The micro register: labels, metadata, captions. Size and value carry it.
47
+ Nothing here is set in capitals — that is a different tradition, and in
48
+ lowercase the wide tracking it needs reads as a gap. */
49
+ .micro,
50
+ caption,
51
+ legend,
52
+ label,
53
+ .field label {
54
+ font-size: var(--size-1);
55
+ line-height: var(--space-2);
56
+ letter-spacing: 0.01em;
57
+ }
58
+
59
+ /* The same size on a line box one step taller: the micro type you can tap.
60
+ At a 16px line box these are 14px targets, under the 24px minimum. The
61
+ leading does the work so the type does not have to change. */
62
+ .micro--tap,
63
+ .pagination a,
64
+ .pagination [aria-current] { line-height: var(--space-3); }
65
+
66
+ /* Quiet: this is secondary. One rule, so the whole library says it the same
67
+ way and an application adds its own selectors to the list rather than
68
+ picking a gray. */
69
+ .quiet,
70
+ .hint,
71
+ .nav a,
72
+ .footer,
73
+ .pairs dt,
74
+ .button--quiet,
75
+ .button--danger,
76
+ .link-quiet { color: var(--ink-quiet); }
77
+
78
+ .hint {
79
+ font-size: var(--size-1);
80
+ line-height: var(--space-3);
81
+ max-width: var(--measure);
82
+ }
83
+
84
+ /* Prose stops where the field it sits in stops. */
85
+ .measure,
86
+ .lede,
87
+ p { max-width: var(--measure); }
88
+
89
+ .lede { margin-top: var(--space-2); }
90
+
91
+ /* Navigation and actions read at body size. They are how an application is
92
+ used, not annotations on it. */
93
+ .nav a,
94
+ .button { font-size: var(--size-2); line-height: var(--space-3); }
95
+
96
+ /* Anything read as a column of digits rather than as a word. */
97
+ .tabular,
98
+ .pairs dd,
99
+ td,
100
+ th { font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }
101
+
102
+ /* A button brings its own font, and a button that exists to look like the
103
+ text it replaced has to give it back. Stated here rather than in
104
+ components.css so it stays ahead of the size modifiers there: a `font`
105
+ shorthand after them would flatten every one. */
106
+ .copy { font: inherit; }
107
+
108
+ code, pre, kbd, samp { font-family: var(--font-mono); font-size: 0.9em; }
109
+ }
@@ -0,0 +1,24 @@
1
+ module ItsSwiss
2
+ # The specimen is documentation and a regression fixture, not a page an
3
+ # application serves. Refused unless the configuration has asked for it —
4
+ # the installer mounts the engine inside `if Rails.env.development?`, and
5
+ # this is the second lock, because a route is a line in a file someone can
6
+ # move and this page names every component the library has.
7
+ class SpecimenController < ActionController::Base
8
+ layout "its_swiss/shell"
9
+
10
+ before_action :refuse_unless_asked_for
11
+
12
+ def show
13
+ @sizes = (1..5).map { |step| "--size-#{step}" }
14
+ @values = (0..5).map { |step| "--value-#{step}" }
15
+ end
16
+
17
+ private
18
+ def refuse_unless_asked_for
19
+ return if ItsSwiss.config.specimen?
20
+
21
+ raise ActionController::RoutingError, "the its-swiss specimen is not enabled in #{Rails.env}"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,73 @@
1
+ module ItsSwiss
2
+ # The library's markup contracts. Each of these exists because the markup it
3
+ # writes carries a detail that is easy to leave out by hand and silent when
4
+ # it is missing.
5
+ module ApplicationHelper
6
+ # Six link tags rather than the one file that imports the six. An @import
7
+ # is a request the browser cannot start until it has read the file that
8
+ # asks for it, so the single file is a waterfall six deep — it is still
9
+ # shipped, for anything that is not Rails and can afford the wait.
10
+ #
11
+ # The order matters less than it looks like it does: every file states its
12
+ # own cascade layer, so these resolve the same way whatever order they
13
+ # arrive in. It is written in order anyway, because a file that has not
14
+ # arrived yet is a file whose layer has not been declared.
15
+ def its_swiss_stylesheet_tags(**options)
16
+ stylesheet_link_tag(*ItsSwiss::STYLESHEETS.map { |name| "its_swiss/#{name}" },
17
+ **{ "data-turbo-track": "reload" }.merge(options))
18
+ end
19
+
20
+ # A destination, and whether you are already there. aria-current rather
21
+ # than a class alone: the CSS gives the current item the accent and the
22
+ # weight, and neither of those is audible.
23
+ #
24
+ # `current` is passed rather than worked out, because only the application
25
+ # knows that /palettes/12 is still the Palettes destination.
26
+ def nav_link_to(name, url, current: nil, **options, &block)
27
+ current = current_page?(url) if current.nil?
28
+ options[:"aria-current"] = "page" if current
29
+
30
+ link_to(name, url, **options, &block)
31
+ end
32
+
33
+ # A value on screen exists to be taken somewhere else, so it is a button
34
+ # that copies itself. The value stays visible text inside it, which is
35
+ # what keeps it usable when the clipboard is not available at all.
36
+ def copy_button(value, **options)
37
+ options[:class] = token_list("copy", options[:class])
38
+
39
+ tag.button(value, type: "button", **options,
40
+ data: {
41
+ controller: "its-swiss-clipboard",
42
+ action: "its-swiss-clipboard#copy",
43
+ its_swiss_clipboard_text_value: value
44
+ },
45
+ aria: { label: "Copy #{value}" })
46
+ end
47
+
48
+ # What to call the application when a page has not titled itself. Rails
49
+ # already knows: the module the application is defined in.
50
+ def its_swiss_application_name
51
+ Rails.application.class.module_parent_name.underscore.humanize
52
+ end
53
+
54
+ # Which page numbers a run of them should show. Elided around the current
55
+ # page and at both ends, because a hundred numbers is not a control —
56
+ # nil is where the gap goes.
57
+ def its_swiss_page_numbers(page, pages, window: 2)
58
+ shown = [ 1, pages, *((page - window)..(page + window)) ].select { |n| n.between?(1, pages) }.uniq.sort
59
+
60
+ shown.each_with_object([]) do |number, run|
61
+ run << nil if run.any? && number - run.last.to_i > 1
62
+ run << number
63
+ end
64
+ end
65
+
66
+ # form_with, already holding the library's builder. An application that
67
+ # wants its own builder still can; this is the shorthand for the case
68
+ # where it does not.
69
+ def its_swiss_form_with(**options, &block)
70
+ form_with(**{ builder: ItsSwiss::FormBuilder, class: "form" }.merge(options), &block)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,14 @@
1
+ <%# Why a record was refused, above the form that was refused. The field-level
2
+ messages the builder writes say it again beside each control; this says
3
+ how many there were, which is the thing you want before you start looking.
4
+ %>
5
+ <% if record.errors.any? %>
6
+ <div class="errors" role="alert">
7
+ <p class="errors__title"><%= pluralize(record.errors.count, "problem") %> stopped this being saved.</p>
8
+ <ul>
9
+ <% record.errors.full_messages.each do |message| %>
10
+ <li><%= message %></li>
11
+ <% end %>
12
+ </ul>
13
+ </div>
14
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <%# The two things a page has to say, in the register the library keeps for
2
+ each. A refusal interrupts, with a rule rather than a filled panel; a
3
+ confirmation does not interrupt at all. %>
4
+ <% if flash[:alert].present? %>
5
+ <div class="errors" role="alert"><p class="errors__title"><%= flash[:alert] %></p></div>
6
+ <% elsif flash[:notice].present? %>
7
+ <p class="hint" role="status"><%= flash[:notice] %></p>
8
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <%# A band, a mark at one end, the destinations beside it, a hairline
2
+ underneath — and nothing at all if the application has given it neither.
3
+ A library that insisted on a bar across the top would be insisting on a
4
+ shape rather than offering one. %>
5
+ <% if content_for?(:mark) || content_for?(:nav) %>
6
+ <header class="masthead page">
7
+ <% if content_for?(:mark) %>
8
+ <p class="masthead__mark"><%= content_for(:mark) %></p>
9
+ <% end %>
10
+
11
+ <% if content_for?(:nav) %>
12
+ <nav class="nav"><%= content_for(:nav) %></nav>
13
+ <% end %>
14
+ </header>
15
+ <% end %>
@@ -0,0 +1,26 @@
1
+ <%#
2
+ A run of numbers under a hairline. Takes a page number, a total, and a block
3
+ that turns a number into a URL — the library has no paginator and no opinion
4
+ about which one an application uses.
5
+
6
+ <%= render "its_swiss/shared/pagination", page: @page, pages: @pages,
7
+ url: ->(n) { colors_path(page: n) } %>
8
+
9
+ Long runs are elided around the current page, because a hundred numbers is
10
+ not a control.
11
+ %>
12
+ <% window = local_assigns.fetch(:window, 2) %>
13
+ <% numbers = its_swiss_page_numbers(page, pages, window:) %>
14
+ <% if pages > 1 %>
15
+ <nav class="pagination" aria-label="<%= local_assigns.fetch(:label, "Pages") %>">
16
+ <% numbers.each do |number| %>
17
+ <% if number.nil? %>
18
+ <span class="pagination__gap" aria-hidden="true">…</span>
19
+ <% elsif number == page %>
20
+ <span aria-current="page"><%= number %></span>
21
+ <% else %>
22
+ <%= link_to number, url.call(number) %>
23
+ <% end %>
24
+ <% end %>
25
+ </nav>
26
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <section data-specimen="buttons">
2
+ <h3>Buttons</h3>
3
+ <div class="run">
4
+ <button type="button" class="button button--primary">Primary</button>
5
+ <button type="button" class="button">Secondary</button>
6
+ <button type="button" class="button button--accent">Accent</button>
7
+ <button type="button" class="button button--quiet">Quiet</button>
8
+ <button type="button" class="button button--danger">Delete</button>
9
+ </div>
10
+ <p class="hint">
11
+ Destroying is set apart by a gap before it is coloured on hover: a reader
12
+ scanning the row has not hovered anything yet.
13
+ </p>
14
+ <p><%= copy_button "#E30613" %> — a value on screen to be taken somewhere else.</p>
15
+ </section>
@@ -0,0 +1,7 @@
1
+ <section data-specimen="footer">
2
+ <h3>Footer</h3>
3
+ <footer class="footer">
4
+ A hairline, the micro register, and whatever belongs after the page. The
5
+ library ships no links of its own down here.
6
+ </footer>
7
+ </section>
@@ -0,0 +1,18 @@
1
+ <section data-specimen="form">
2
+ <h3>Form</h3>
3
+ <%= its_swiss_form_with url: "#form", method: :get do |form| %>
4
+ <%= form.text_field :name, value: "Brand Core", hint: "As it appears in the nav." %>
5
+ <%= form.text_area :notes, value: "" %>
6
+ <%= form.select :kind, [ %w[ Note note ], %w[ Essay essay ] ] %>
7
+ <%= form.check_box :published %>
8
+ <div class="field field--invalid">
9
+ <label for="specimen_refused">Refused</label>
10
+ <input id="specimen_refused" type="text" aria-invalid="true" aria-describedby="specimen_refused_error">
11
+ <p class="field__error" id="specimen_refused_error">Refused cannot be blank</p>
12
+ </div>
13
+ <div class="run">
14
+ <%= form.submit "Save" %>
15
+ <button type="button" class="button button--quiet">Cancel</button>
16
+ </div>
17
+ <% end %>
18
+ </section>
@@ -0,0 +1,14 @@
1
+ <section data-specimen="grid">
2
+ <h3>Grid primitives</h3>
3
+ <p class="hint">
4
+ The library ships the machinery and never the grid. This page declares
5
+ <code>--columns: <%= 6 %></code>; a child says how many fields it takes
6
+ with <code>--span</code>, and a child that says nothing runs the field.
7
+ </p>
8
+ <div class="grid specimen__grid">
9
+ <% 6.times do %><span style="--span: 1"><span class="specimen__field"></span></span><% end %>
10
+ <span style="--span: 2"><span class="specimen__field"></span></span>
11
+ <span style="--span: 4"><span class="specimen__field"></span></span>
12
+ <span><span class="specimen__field"></span></span>
13
+ </div>
14
+ </section>
@@ -0,0 +1,15 @@
1
+ <section data-specimen="masthead">
2
+ <h3>Masthead and navigation</h3>
3
+ <header class="masthead">
4
+ <p class="masthead__mark"><a href="#masthead">Wordmark</a></p>
5
+ <nav class="nav">
6
+ <a href="#masthead" aria-current="page">Where you are</a>
7
+ <a href="#masthead">Somewhere else</a>
8
+ <a href="#masthead">And another</a>
9
+ </nav>
10
+ </header>
11
+ <p class="hint">
12
+ The current destination carries the accent and the weight. Two signals,
13
+ because colour alone is not one.
14
+ </p>
15
+ </section>
@@ -0,0 +1,13 @@
1
+ <section data-specimen="messages">
2
+ <h3>What a page has to say</h3>
3
+ <p class="hint" role="status">It worked. This does not interrupt.</p>
4
+ <div class="errors">
5
+ <p class="errors__title">2 problems stopped this being saved.</p>
6
+ <ul>
7
+ <li>Name cannot be blank</li>
8
+ <li>Name is already taken</li>
9
+ </ul>
10
+ </div>
11
+ <p class="empty">Nothing here yet.</p>
12
+ <hr>
13
+ </section>
@@ -0,0 +1,4 @@
1
+ <section data-specimen="pagination">
2
+ <h3>Pagination</h3>
3
+ <%= render "its_swiss/shared/pagination", page: 4, pages: 20, url: ->(n) { "#page-#{n}" } %>
4
+ </section>
@@ -0,0 +1,8 @@
1
+ <section data-specimen="pairs">
2
+ <h3>Definition list</h3>
3
+ <dl class="pairs">
4
+ <dt>Baseline</dt><dd>8px. Everything vertical is a whole number of these.</dd>
5
+ <dt>Measure</dt><dd>Three fields wide, derived from the page rather than chosen.</dd>
6
+ <dt>Accent</dt><dd>State and emphasis only. The consumer's to set.</dd>
7
+ </dl>
8
+ </section>
@@ -0,0 +1,15 @@
1
+ <section data-specimen="table">
2
+ <h3>Table</h3>
3
+ <table class="table">
4
+ <caption>Ruled horizontally, never vertically. No zebra: a filled row is a filled area.</caption>
5
+ <thead>
6
+ <tr><th scope="col">Token</th><th scope="col">Step</th><th scope="col" class="numeric">Lightness</th></tr>
7
+ </thead>
8
+ <tbody>
9
+ <tr><td>Paper</td><td><code>--value-0</code></td><td class="numeric">98%</td></tr>
10
+ <tr><td>Hairline</td><td><code>--value-2</code></td><td class="numeric">89%</td></tr>
11
+ <tr><td>Quiet ink</td><td><code>--value-4</code></td><td class="numeric">54%</td></tr>
12
+ <tr><td>Ink</td><td><code>--value-5</code></td><td class="numeric">18%</td></tr>
13
+ </tbody>
14
+ </table>
15
+ </section>
@@ -0,0 +1,19 @@
1
+ <section data-specimen-take="<%= take %>" <%= tag.attributes(style: ("--accent: #{accent}" if accent)) %>>
2
+ <hr class="rule--heavy">
3
+ <h2><%= take.humanize %></h2>
4
+ <p class="hint"><%= caption %></p>
5
+
6
+ <%# Every section is rendered in both takes from the same partials, or the
7
+ second take is not a comparison — it is a different page. %>
8
+ <%= render "its_swiss/specimen/values" %>
9
+ <%= render "its_swiss/specimen/type" %>
10
+ <%= render "its_swiss/specimen/grid" %>
11
+ <%= render "its_swiss/specimen/masthead" %>
12
+ <%= render "its_swiss/specimen/buttons" %>
13
+ <%= render "its_swiss/specimen/form" %>
14
+ <%= render "its_swiss/specimen/table" %>
15
+ <%= render "its_swiss/specimen/pairs" %>
16
+ <%= render "its_swiss/specimen/pagination" %>
17
+ <%= render "its_swiss/specimen/messages" %>
18
+ <%= render "its_swiss/specimen/footer" %>
19
+ </section>
@@ -0,0 +1,24 @@
1
+ <section data-specimen="type">
2
+ <h3>Type scale</h3>
3
+ <p class="hint">
4
+ Five sizes on an alternating 1.33 / 1.5 ratio. Every line box is a whole
5
+ number of baselines.
6
+ </p>
7
+ <%# The rung and the line box it is set on. Written out rather than derived,
8
+ because the ladder is not every whole number: there is no --space-7, and
9
+ asking for one silently drops the line box back to the browser's. %>
10
+ <% { 1 => [ 2, "Labels and metadata" ], 2 => [ 3, "Body copy" ], 3 => [ 4, "Section" ],
11
+ 4 => [ 5, "Subhead" ], 5 => [ 6, "Page title" ] }.each do |step, (leading, name)| %>
12
+ <p data-size="<%= step %>" style="font-size: var(--size-<%= step %>); line-height: var(--space-<%= leading %>)">
13
+ <%= name %> — <code>--size-<%= step %></code> on <code>--space-<%= leading %></code>
14
+ </p>
15
+ <% end %>
16
+
17
+ <h4>Registers</h4>
18
+ <p>Body. The measure holds prose to three fields, which is where it stops rather than where the window does.</p>
19
+ <p class="quiet">Quiet. This is secondary.</p>
20
+ <p class="micro">Micro. Labels, metadata, captions — never capitals.</p>
21
+ <p class="hint">Hint. What a field needs said about it.</p>
22
+ <p class="tabular">Tabular 0123456789 — figures read down a column.</p>
23
+ <p><%= link_to "A link keeps its underline", "#type", class: "link-quiet" %> and a button does not.</p>
24
+ </section>
@@ -0,0 +1,18 @@
1
+ <section data-specimen="values">
2
+ <h3>Value scale</h3>
3
+ <p class="hint">
4
+ Six steps of OKLCH lightness, paper to ink. Neutral as shipped; a consumer
5
+ warms the whole ladder with <code>--value-chroma</code> and
6
+ <code>--value-hue</code>.
7
+ </p>
8
+ <dl class="pairs">
9
+ <% (0..5).each do |step| %>
10
+ <dt data-value="<%= step %>"><code>--value-<%= step %></code></dt>
11
+ <dd>
12
+ <span class="specimen__value" style="background: var(--value-<%= step %>)"></span>
13
+ </dd>
14
+ <% end %>
15
+ <dt><code>--accent</code></dt>
16
+ <dd><span class="specimen__value" style="background: var(--accent)"></span></dd>
17
+ </dl>
18
+ </section>
@@ -0,0 +1,27 @@
1
+ <% content_for :title, "its-swiss specimen" %>
2
+ <%# The specimen's own furniture, loaded only here: none of it is a component
3
+ an application should reach for. %>
4
+ <% content_for :head, stylesheet_link_tag("its_swiss/specimen") %>
5
+ <% content_for :mark, link_to("its-swiss", specimen_path) %>
6
+ <% content_for :nav do %>
7
+ <%= nav_link_to "Specimen", specimen_path %>
8
+ <% end %>
9
+ <% content_for :footer, "its-swiss #{ItsSwiss::VERSION} — the specimen is the documentation and the regression fixture." %>
10
+
11
+ <h1 class="page-title">its-swiss</h1>
12
+ <p class="lede">
13
+ Every component the library ships, the type scale, the value scale and the
14
+ grid primitives — rendered twice. The first take is the library exactly as
15
+ it ships, with the accent slot left unfilled and collapsed onto ink. The
16
+ second sets an accent. If the first reads correctly, the values are doing
17
+ the work.
18
+ </p>
19
+
20
+ <%# The second take differs by one custom property. Not a second stylesheet
21
+ and not a modifier class: the accent is one slot, and the comparison is
22
+ only honest if that is all that changed between them. %>
23
+ <%= render "its_swiss/specimen/take", take: "monochrome", accent: nil,
24
+ caption: "Accent unset. The library ships like this: --accent falls back to ink." %>
25
+
26
+ <%= render "its_swiss/specimen/take", take: "accent", accent: "oklch(57.7% 0.234 28)",
27
+ caption: "Accent set to a signal red. Nothing else changed." %>
@@ -0,0 +1,55 @@
1
+ <%#
2
+ The shell. Everything an application's own layout would otherwise have to
3
+ remember: the view transition opt-in, the stylesheets in layer order, a way
4
+ past the masthead for a keyboard, and the two things a page has to say.
5
+
6
+ Slots, all optional:
7
+
8
+ :title the page's title; falls back to the application's name
9
+ :head anything else that belongs in <head>
10
+ :mark the wordmark. No mark and no nav means no masthead at all
11
+ :nav the destinations
12
+ :main_class what the page's main region is, if it is a grid
13
+ :footer whatever belongs after the page
14
+
15
+ It stops there. A page layout beyond this shell is the application's, for
16
+ the same reason its grid is.
17
+ %>
18
+ <!DOCTYPE html>
19
+ <html lang="<%= I18n.locale %>">
20
+ <head>
21
+ <title><%= content_for(:title) || its_swiss_application_name %></title>
22
+ <meta name="viewport" content="width=device-width,initial-scale=1">
23
+
24
+ <%# Opts every same-origin navigation into the View Transitions API.
25
+ Turbo reads this tag; transitions.css covers the same navigation with
26
+ JavaScript switched off. %>
27
+ <meta name="view-transition" content="same-origin">
28
+
29
+ <%= csrf_meta_tags %>
30
+ <%= csp_meta_tag %>
31
+
32
+ <%= yield :head %>
33
+
34
+ <%= its_swiss_stylesheet_tags %>
35
+ <%= javascript_importmap_tags if respond_to?(:javascript_importmap_tags) %>
36
+ </head>
37
+
38
+ <body>
39
+ <%# Before anything else in the document, so it is the first thing a
40
+ keyboard reaches. Off screen until it is focused, at which point it
41
+ has to be somewhere you can read it. %>
42
+ <a class="skip-link" href="#main">Skip to content</a>
43
+
44
+ <%= render "its_swiss/shared/masthead" %>
45
+
46
+ <main id="main" class="page <%= content_for(:main_class) %>">
47
+ <%= render "its_swiss/shared/flash" %>
48
+ <%= yield %>
49
+ </main>
50
+
51
+ <% if content_for?(:footer) %>
52
+ <footer class="footer page"><%= content_for(:footer) %></footer>
53
+ <% end %>
54
+ </body>
55
+ </html>
@@ -0,0 +1,5 @@
1
+ # The library's whole JavaScript surface. A typographic system does not need
2
+ # script to set type; this is here because a value that exists to be taken
3
+ # somewhere else should be a button that copies itself, and that cannot be
4
+ # done in CSS.
5
+ pin "its_swiss/clipboard_controller", to: "its_swiss/clipboard_controller.js"
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ ItsSwiss::Engine.routes.draw do
2
+ # Mounted by the installer under `if Rails.env.development?`, and refused by
3
+ # the controller as well: a route is a line in a file someone can move, and
4
+ # the specimen names every component the library has.
5
+ get "specimen", to: "specimen#show", as: :specimen
6
+ end