recourse 4.8.0 → 5.0.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/README.md +7 -4
  4. data/app/views/layouts/recourses.html.erb +29 -501
  5. data/app/views/recourses/_combobox.html.erb +26 -44
  6. data/lib/recourse/engine.rb +0 -38
  7. data/lib/recourse/helpers/constraints.rb +4 -7
  8. data/lib/recourse/helpers/filters.rb +2 -2
  9. data/lib/recourse/helpers/formats.rb +6 -7
  10. data/lib/recourse/helpers/refreshes.rb +3 -13
  11. data/lib/recourse/themes.rb +2 -1
  12. data/lib/recourse/version.rb +1 -1
  13. metadata +1 -36
  14. data/app/javascript/recourse/bookmark_controller.js +0 -97
  15. data/app/javascript/recourse/clear_controller.js +0 -22
  16. data/app/javascript/recourse/combobox_controller.js +0 -68
  17. data/app/javascript/recourse/confirm.js +0 -46
  18. data/app/javascript/recourse/density_controller.js +0 -21
  19. data/app/javascript/recourse/deselect_controller.js +0 -37
  20. data/app/javascript/recourse/flash.js +0 -35
  21. data/app/javascript/recourse/limit_controller.js +0 -23
  22. data/app/javascript/recourse/phone_controller.js +0 -33
  23. data/app/javascript/recourse/relative_time_controller.js +0 -45
  24. data/app/javascript/recourse/reveal_controller.js +0 -16
  25. data/app/javascript/recourse/scheme_controller.js +0 -99
  26. data/app/javascript/recourse/search_controller.js +0 -78
  27. data/app/javascript/recourse/shortcuts_controller.js +0 -42
  28. data/app/javascript/recourse/timezone_controller.js +0 -27
  29. data/app/javascript/recourse/toast_controller.js +0 -35
  30. data/app/javascript/recourse/tooltip_controller.js +0 -23
  31. data/app/javascript/recourse/written.js +0 -20
  32. data/app/javascript/recourse/written_controller.js +0 -28
  33. data/app/stylesheets/recourse/themes/bootstrap.css +0 -11
  34. data/app/stylesheets/recourse/themes/dawn.css +0 -189
  35. data/app/stylesheets/recourse/themes/dracula.css +0 -189
  36. data/app/stylesheets/recourse/themes/gruvbox.css +0 -189
  37. data/app/stylesheets/recourse/themes/monokai.css +0 -190
  38. data/app/stylesheets/recourse/themes/nord.css +0 -189
  39. data/app/stylesheets/recourse/themes/one_dark.css +0 -189
  40. data/app/stylesheets/recourse/themes/solarized.css +0 -188
  41. data/app/stylesheets/recourse/themes/tokyo_night.css +0 -189
  42. data/app/views/recourses/_confirm.html.erb +0 -14
  43. data/vendor/recourse/bootstrap-icons.min.css +0 -5
  44. data/vendor/recourse/bootstrap.bundle.min.js +0 -9
  45. data/vendor/recourse/bootstrap.min.css +0 -2
  46. data/vendor/recourse/fonts/bootstrap-icons.woff +0 -0
  47. data/vendor/recourse/fonts/bootstrap-icons.woff2 +0 -0
  48. data/vendor/recourse/stimulus.js +0 -2563
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2de6346d2ca14ff89cc09ff75c23b72b6cc7b7502d9dcfebcb29253a53209c8c
4
- data.tar.gz: aadab8161994fb71579e46a1ab1343f929f11fdf7bec5b113768f06c16887bc8
3
+ metadata.gz: d160dff229773bd862eb24c774a64aaac87c073b5fde920261502c624f17d372
4
+ data.tar.gz: d500cd966e2faf93f6955ef09b8c6713ba54c1be081408d040d7367307dcb31e
5
5
  SHA512:
6
- metadata.gz: 655ce30df22d4cccd72dd6a48e559eba1e8d14720d14f2a032de9f811a510f81f5a99dbf1be8082599371eff60c24906932b682e910099cd6bc72a6dbb12bd29
7
- data.tar.gz: cb32000306a3b2f20e27f6898aed2c6d3ffb78bbf32411779f85a6e28bb4c61e80d327fddf2fbca0cf82163d9e0f198997eb0b22cea5f211fc23d2d4f7b30822
6
+ metadata.gz: 8408d91069b9fdbff6fc8136e73ff02507e1be63cd9a1e4b3dc38654bb42f9c85ab21280a540eaba613b85fa3ac16dc32e08ca2f5a0687081923323870b8542b
7
+ data.tar.gz: dacc5528b67ec9ca0118c03ed8005392babaedbf69aa70d1cefb91cc697e188ebae147da37d0854e372cf56c3a5e47a04235f6c56c751c00f675881f15b19128
data/CHANGELOG.md CHANGED
@@ -5,6 +5,47 @@ All notable changes to this project will be documented in this file.
5
5
  For more information about changelogs, check [Keep a Changelog](http://keepachangelog.com) and
6
6
  [Vandamme](http://tech-angels.github.io/vandamme).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## 5.0.0 - 2026-09-14
11
+
12
+ * [Breaking change] Every page is styled and scripted by `https://design.houseaccount.com`, and nothing is served by the gem
13
+
14
+ The vendored Bootstrap, its icons and fonts, Stimulus, the sixteen controllers, the nine
15
+ palettes and the layout's own stylesheet have moved to the `design` repo, which publishes
16
+ them at `/v<version>/css/houseaccount.css`, `/v<version>/js/houseaccount.js` and
17
+ `/v<version>/theme/<name>.css` on that origin, kept forever per version. The layout links
18
+ the first two at a pinned version, and `Recourse::THEMES_PATH` names the third at the
19
+ same one; a fix there reaches these pages when the pin moves, and not before. The
20
+ `Rack::Static` layers under `/recourse/` are gone with the files they served.
21
+
22
+ A host needs that origin reachable from its readers' browsers. A host with a Content
23
+ Security Policy allows it for `style-src`, `script-src` and `font-src`. A host on the
24
+ `houseaccount` gem serves the same paths itself, but this gem still links the site: it
25
+ never names that gem.
26
+
27
+ * [Breaking change] A combobox is a `<select>`, and a filter submits one value per pick
28
+
29
+ `_combobox.html.erb` renders a plain select carrying its words as data attributes, and
30
+ the design bundle dresses it as Bootstrap's combobox; the toggle, the menu and the
31
+ plugin's hidden input are no longer in the page. A filter's select is `multiple` and
32
+ named `q[status_in][]`, so a request carries `q[status_in][]=draft&q[status_in][]=sent`
33
+ rather than one comma-joined value — a host linking to a filtered index, or reading
34
+ `params[:q]`, updates the shape. A host overriding the partial rewrites it as a select.
35
+
36
+ * [Feature] The delete dialog is the bundle's
37
+
38
+ `_confirm.html.erb` is gone: `confirm.js` in the design bundle builds the dialog on
39
+ the first ask, and its answer wears the words of the button that asked. A host that
40
+ overrode the partial deletes its copy.
41
+
42
+ * [Fix] A phone is formatted in the browser
43
+
44
+ A cell reads `<span data-controller='phone'>4155550000</span>` and a field arrives with
45
+ ten digits, the controller and nothing about its shape: no `pattern`, `placeholder` or
46
+ `title`. The design bundle's controller decides `555-555-5555` in both places. A host
47
+ asserting the formatted number in its own tests updates the assertion.
48
+
8
49
  ## 4.8.0 - 2026-09-13
9
50
 
10
51
  * [Feature] A table is redrawn when a record its rows draw changes, so a `touch: true` kept for that alone can go
data/README.md CHANGED
@@ -10,13 +10,16 @@ gem install recourse
10
10
 
11
11
  ```ruby
12
12
  # Gemfile
13
- gem 'recourse', '~> 4.0'
13
+ gem 'recourse', '~> 5.0'
14
14
  ```
15
15
 
16
- `~> 4.0` follows Semantic Versioning: `bundle update` takes every 4.x and never a
16
+ `~> 5.0` follows Semantic Versioning: `bundle update` takes every 5.x and never a
17
17
  breaking change. Rails 8.1 and Ruby 3.2 are the minimum; the pages need Turbo, which
18
- `turbo-rails` brings, and nothing else — Bootstrap and its icons are vendored and served
19
- by the engine, so a host without an asset pipeline gets the same screens.
18
+ `turbo-rails` brings, and nothing else in the host. Everything a page is styled and
19
+ scripted by — Bootstrap 6, its icons, Turbo, Stimulus and the controllers behind these
20
+ screens — is linked from https://design.houseaccount.com, so a host without an asset
21
+ pipeline gets the same screens, and a host with a Content Security Policy allows that
22
+ origin for `style-src`, `script-src` and `font-src`.
20
23
 
21
24
  ## Step 1. Edit your routes
22
25
 
@@ -5,16 +5,36 @@
5
5
  <title><%= content_for(:title) || 'Recourse' %></title>
6
6
  <meta name='viewport' content='width=device-width, initial-scale=1'>
7
7
  <meta name='color-scheme' content='light dark'>
8
+ <%# A refresh Turbo is sent morphs the page in place and keeps the scroll; without these it
9
+ replaces the whole body, which drops the caret, the scroll and any menu that was open.
10
+ On every page rather than on the ones that broadcast today, so a model that starts to
11
+ never has to be remembered here. -%>
12
+ <meta name='turbo-refresh-method' content='morph'>
13
+ <meta name='turbo-refresh-scroll' content='preserve'>
8
14
  <%= csrf_meta_tags %>
9
15
  <%= csp_meta_tag %>
10
16
 
11
17
  <%= yield :head %>
12
18
 
13
- <link rel='stylesheet' href='/recourse/bootstrap.min.css'>
14
- <link rel='stylesheet' href='/recourse/bootstrap-icons.min.css'>
15
- <%# Preloaded from the head instead of discovered at the end of the body, so the
16
- module the toast controller and the dismiss X import is already in flight. -%>
17
- <link rel='modulepreload' href='/recourse/bootstrap.bundle.min.js'>
19
+ <%# Everything the page is styled by, from the one place every HouseAccount app draws
20
+ on: Bootstrap 6, its icons and the house's own rules for this gem's markup. Pinned
21
+ to a version, so a change there reaches these pages when somebody moves the pin
22
+ here and not before; the same version stands in the script below and in
23
+ `Recourse::THEMES_PATH`. Two connections opened before the parser finds either tag:
24
+ the stylesheet's plain one and the CORS one a module script and a font are fetched
25
+ over. -%>
26
+ <link rel='preconnect' href='https://design.houseaccount.com'>
27
+ <link rel='preconnect' href='https://design.houseaccount.com' crossorigin>
28
+ <link rel='stylesheet' href='https://design.houseaccount.com/v0.3.1/css/houseaccount.css'>
29
+ <%# The one thing these screens set that the house's stylesheet does not: system faces
30
+ at a compact size, for pages dense with figures. 14px as rem, so a browser's own
31
+ text-size setting still scales it. After the stylesheet, since both are `:root`. -%>
32
+ <style>
33
+ :root {
34
+ --bs-body-font-family: helvetica, verdana, arial, sans-serif;
35
+ --bs-body-font-size: 0.875rem;
36
+ }
37
+ </style>
18
38
  <%# After the Bootstrap link, which is what makes either win: every selector involved
19
39
  is `:root`. The palette the host named, then the one the reader chose last time. -%>
20
40
  <%= theme_stylesheet_link %>
@@ -31,487 +51,10 @@
31
51
  <link rel='icon' href='/favicon.png' type='image/png'>
32
52
  <link rel='apple-touch-icon' href='/icon.png'>
33
53
 
34
- <script type='module'>
35
- import { Application } from '/recourse/stimulus.js'
36
- import BookmarkController from '/recourse/bookmark_controller.js'
37
- import ClearController from '/recourse/clear_controller.js'
38
- import ComboboxController from '/recourse/combobox_controller.js'
39
- import DensityController from '/recourse/density_controller.js'
40
- import DeselectController from '/recourse/deselect_controller.js'
41
- import LimitController from '/recourse/limit_controller.js'
42
- import PhoneController from '/recourse/phone_controller.js'
43
- import RelativeTimeController from '/recourse/relative_time_controller.js'
44
- import RevealController from '/recourse/reveal_controller.js'
45
- import SchemeController from '/recourse/scheme_controller.js'
46
- import SearchController from '/recourse/search_controller.js'
47
- import ShortcutsController from '/recourse/shortcuts_controller.js'
48
- import TimezoneController from '/recourse/timezone_controller.js'
49
- import ToastController from '/recourse/toast_controller.js'
50
- import WrittenController from '/recourse/written_controller.js'
51
- import TooltipController from '/recourse/tooltip_controller.js'
52
-
53
- // In the head, and guarded: Turbo re-runs body scripts on every visit, and a
54
- // second application would connect every controller twice.
55
- if (!window.Stimulus) {
56
- window.Stimulus = Application.start()
57
- window.Stimulus.register('bookmark', BookmarkController)
58
- window.Stimulus.register('clear', ClearController)
59
- window.Stimulus.register('combobox', ComboboxController)
60
- window.Stimulus.register('density', DensityController)
61
- window.Stimulus.register('deselect', DeselectController)
62
- window.Stimulus.register('limit', LimitController)
63
- window.Stimulus.register('phone', PhoneController)
64
- window.Stimulus.register('relative-time', RelativeTimeController)
65
- window.Stimulus.register('reveal', RevealController)
66
- window.Stimulus.register('scheme', SchemeController)
67
- window.Stimulus.register('search', SearchController)
68
- window.Stimulus.register('shortcuts', ShortcutsController)
69
- window.Stimulus.register('timezone', TimezoneController)
70
- window.Stimulus.register('toast', ToastController)
71
- window.Stimulus.register('tooltip', TooltipController)
72
- window.Stimulus.register('written', WrittenController)
73
- }
74
- </script>
75
-
76
- <style>
77
- /* System faces at a compact size, and no webfont: nothing to download, no
78
- swap flash, and the last external host the layout reached for is gone.
79
- 14px as rem, so a browser's own text-size setting still scales it. */
80
- :root {
81
- --bs-body-font-family: helvetica, verdana, arial, sans-serif;
82
- --bs-body-font-size: 0.875rem;
83
- }
84
-
85
- /* Bootstrap's .15s fade reads as vanishing. Only the hide ever transitions here
86
- — the toast is born visible — and reduced-motion still wins, because its rule
87
- sets `transition: none` outright rather than through this variable. */
88
- .toast { --bs-transition-fade: opacity 1s linear; }
89
-
90
- /* A marked match is part of the word it sits in, so it carries no padding of
91
- its own: 'Nash' in 'Nashville' must not read as a word standing apart. */
92
- mark { padding: 0; }
93
-
94
- /* A crumb that is not a link keeps its resting colors under the cursor.
95
- Bootstrap lights every .breadcrumb-link on hover, which on a <span>
96
- promises a click that goes nowhere — and the element is the whole
97
- discriminator, since a crumb that links is an <a>. Redefining the hover
98
- tokens, rather than out-cascading the hover rule, leaves Bootstrap's own
99
- selector in charge and the active crumb its own resting color. */
100
- span.breadcrumb-link {
101
- --bs-breadcrumb-link-hover-color: var(--bs-breadcrumb-link-color);
102
- --bs-breadcrumb-link-hover-bg: transparent;
103
- }
104
- span.breadcrumb-link.active {
105
- --bs-breadcrumb-link-hover-color: var(--bs-breadcrumb-link-active-color);
106
- }
107
-
108
- /* Nothing here is meant to be wider than the window: the one thing that is, a
109
- table, scrolls inside its own wrapper. Safari lays a stacked table's hidden
110
- header row out at its full width all the same, and a page that can be
111
- scrolled to it is a page Safari zooms out of and draws the navbar short on.
112
- `clip` on the shell rather than `hidden`: a `hidden` axis turns the other into
113
- `auto`, and the body then cut off whatever hung below it — a filter's menu, on a
114
- page whose table had no rows to be taller than the menu. `clip` leaves the
115
- other axis alone. */
116
- html { overflow-x: hidden; }
117
- .recourse-shell { overflow-x: clip; }
118
-
119
- /* A finger does not hover. The tint a pointer leaves on the row under it, or on
120
- the link under it, would stay on whatever was last touched — or land on whatever
121
- the redrawn page put under the finger — so a touch screen gets none. */
122
- @media (hover: none) {
123
- .table-hover > tbody > tr:hover > * {
124
- --bs-table-color-state: initial;
125
- --bs-table-bg-state: initial;
126
- }
127
- .nav-link:hover:not(.active) { background-color: transparent; }
128
- }
129
-
130
- /* The navbar stacks over the content: a menu opened from a filter drops over the
131
- first rows of the table rather than under them. */
132
- .recourse-shell > nav { position: relative; z-index: 1; }
133
-
134
- /* An action column is as wide as a row is tall — an icon in a square — and a
135
- counter column starts at the same square, widening only when a figure like
136
- 38,405 needs the room: a table laying out to 100% reads the width as a
137
- preference, never crushes content into it, and hands what these cells do
138
- not use to the columns carrying text. Not while the table is stacked, where
139
- every cell is a block and a square one is a squashed one — the query
140
- matches `.sm:table-stacked`'s. */
141
- @container (width >= 576px) {
142
- .recourse-actions, .recourse-counter {
143
- width: calc(1em * var(--bs-body-line-height) + 2 * var(--bs-table-cell-padding-y));
144
- white-space: nowrap;
145
- text-align: center;
146
- }
147
- }
148
-
149
- /* Stacked, a cell reads as one line — `Estimate hi: 600`, the heading semibold
150
- and the value after it — where Bootstrap puts the heading on a line of its own
151
- and leaves the row's first cell with no heading at all, bold in its place: a
152
- status opening a row would be a bare badge. Every cell is headed here. And a
153
- value with no space in it — an address, a token — breaks rather than widening
154
- the row past the phone. */
155
- @container (width < 576px) {
156
- .sm\:table-stacked > tbody > tr > td[data-cell]::before {
157
- content: attr(data-cell) ': ';
158
- display: inline;
159
- font-weight: var(--bs-font-weight-semibold);
160
- }
161
- .sm\:table-stacked > tbody > tr > td:first-child { font-weight: inherit; }
162
- /* A counter's heading goes inside its link, so the whole `Franchises: 21` is
163
- one thing to tap; the cell's own heading steps aside for it. */
164
- .sm\:table-stacked > tbody > tr > td.recourse-counter::before { content: none; }
165
- .sm\:table-stacked > tbody > tr > td.recourse-counter > [data-cell]::before {
166
- content: attr(data-cell) ': ';
167
- font-weight: var(--bs-font-weight-semibold);
168
- }
169
- .sm\:table-stacked > tbody > tr > td:not(:first-child) + td::before { margin-block-start: 0; }
170
- .sm\:table-stacked > tbody > tr > td { overflow-wrap: anywhere; }
171
- }
172
-
173
- /* A counter says what it counts in one of two ways, and the table's own width is
174
- what picks: the icon and a bare figure where the column is a square, the
175
- counted model's words where there is room to read them. Bootstrap's `xl`, so
176
- words arrive on a wide monitor and nowhere narrower — the width above is a
177
- preference a table never crushes content into, so the column simply widens to
178
- take them. Against `.table-responsive` like every query here, which is the
179
- table's width rather than the window's: a sidebar takes a few hundred pixels
180
- the window knows nothing about. */
181
- .recourse-counter-word { display: none; }
182
- @container (width >= 1280px) {
183
- .recourse-counter-icon, .recourse-counter-figure { display: none; }
184
- .recourse-counter-word { display: inline; }
185
- }
186
-
187
- /* One rule per row of the show page, so a value and its heading read as one thing
188
- and the next pair as another. On the cells rather than between them: a column's
189
- gutter is padding inside it, so two side by side draw one unbroken line.
190
- Every row reserves the width a rule takes, in `transparent`, and the show page
191
- only colors it in — which is what leaves a field at exactly the height of the
192
- value it edits, row after row down two pages. */
193
- .recourse-row {
194
- border-block-end: var(--bs-border-width) solid transparent;
195
- }
196
- .recourse-values > .recourse-row {
197
- border-block-end-color: var(--bs-border-color);
198
- }
199
- /* And a value stands as tall as the control that edits it, so a row on one page
200
- is a row of the same height on the other. Bootstrap gives
201
- `.form-control-plaintext` a control's padding and line height but no minimum,
202
- and an icon or a badge is shorter than a box. */
203
- .recourse-values .form-control-plaintext {
204
- min-height: var(--bs-control-min-height);
205
- }
206
- /* The reveal is a word beside a value rather than a control beside it, so it takes
207
- the line's height and not `.btn-sm`'s minimum — which would leave a masked row
208
- taller than the field that edits the same attribute. */
209
- .form-control-plaintext .btn {
210
- min-height: 0;
211
- }
212
-
213
- /* The shell is three things — the sidebar, the navbar, the content — written in
214
- the order a phone reads them: a row of icons at the very top, the trail and the
215
- search under it, the page under that, scrolling as one. From the width the
216
- sidebar becomes a column at, a grid seats them the other way round. The navbar
217
- runs across the top, the sidebar takes the column under it and the content the
218
- rest, the shell is exactly the viewport, and `main` is the only thing in it that
219
- scrolls — so the navbar and the sidebar stay where they are however far down the
220
- table a reader is. `minmax(0, 1fr)` is what lets the second row be shorter than
221
- what is in it; a `1fr` alone is as tall as its content, and the page would scroll
222
- after all.
223
- The sidebar deliberately does not scroll. A window dragged shorter than the
224
- sidebar itself is the one case the viewport cannot hold, and there the sidebar
225
- is allowed to spill past the shell and let the browser scroll the page — which is
226
- what a reader expects of something taller than the screen. Were it to scroll on
227
- its own instead, the same overflow would be hidden inside it and the page would
228
- look whole while its foot was unreachable. */
229
- @media (width >= 768px) {
230
- .recourse-shell {
231
- display: grid;
232
- grid-template-columns: auto minmax(0, 1fr);
233
- grid-template-rows: auto minmax(0, 1fr);
234
- grid-template-areas: 'nav nav' 'aside main';
235
- height: 100dvh;
236
- }
237
- .recourse-shell > nav { grid-area: nav; }
238
- .recourse-sidebar { grid-area: aside; }
239
- .recourse-shell > main { grid-area: main; overflow-y: auto; }
240
- }
241
-
242
- /* The sidebar rules off the navbar below it while it is the band at the top of a
243
- phone, and off the content beside it once it is a column. v6 generates no
244
- responsive border utilities, so the switch is written out. */
245
- .recourse-sidebar {
246
- border-block-end: var(--bs-border-width) solid var(--bs-border-color);
247
- }
248
- @media (width >= 768px) {
249
- .recourse-sidebar {
250
- border-block-end: 0;
251
- border-inline-end: var(--bs-border-width) solid var(--bs-border-color);
252
- }
253
- }
254
-
255
- /* The search and its filters take the width the breadcrumb and the buttons
256
- leave, and give it back as the viewport narrows: the box shrinks to a floor
257
- that still shows a word, and below 768px the row is allowed to wrap. */
258
- .recourse-search { flex: 1 1 auto; max-width: 48rem; }
259
- /* The box grows and the filters do not, so the field someone types into is the
260
- widest thing in the row on a large viewport. A toggle is `width: 100%`, which
261
- as a flex item would otherwise mean the whole form. */
262
- .recourse-search-box { flex: 2 1 16rem; min-width: 8rem; }
263
-
264
- .recourse-search .combobox-toggle { flex: 0 1 10rem; }
265
- /* The per-page switch is a word in the sentence beside it rather than a control
266
- next to one, so it takes the line's height and not `.btn`'s own minimum —
267
- which would leave the row under every table taller than the words in it. */
268
- .recourse-limit { min-height: 0; }
269
- /* The bookmark square is an icon in a row of text, not a control in a form, so it
270
- takes the line's height too: `.btn`'s minimum made every row with one taller
271
- than the rows without. */
272
- .recourse-bookmark { min-height: 0; }
273
- /* Stacked, each control is a row of its own rather than a fragment of one:
274
- there is no second control beside it to share the line with. */
275
- @media (width < 768px) {
276
- /* The room the breadcrumb leaves, and never more: a form sized by its content
277
- widened with the words a picked option put on a toggle, and wrapped under
278
- the crumbs. A basis of nothing and leave to shrink hold it in its row. */
279
- .recourse-search { flex: 1 1 0; min-width: 0; max-width: none; }
280
- .recourse-search-box,
281
- .recourse-search .combobox-toggle { flex: 1 1 100%; min-width: 0; }
282
- /* Safari zooms in on a field whose text is under 16px, an absolute figure: the
283
- small controls are 14px, and so is the body on a phone, so the controls say
284
- 16px themselves and focusing one moves nothing. */
285
- .recourse-search .form-control { font-size: 16px; }
286
- /* A page under a record has a trail two or three crumbs long, and what sat beside
287
- the trail — the search, its filters, a button — was squeezed into what those
288
- left: `Filter by na`. Under such a trail each takes a row of its own, the form
289
- the whole width and every control in it a row in turn, and the margin that set
290
- a button apart from the trail goes, there being no trail beside it. An index at
291
- the top keeps its form beside its one crumb, where it fits. */
292
- .recourse-trailed > :not(nav) { flex: 0 0 100%; margin-inline-start: 0; }
293
- }
294
-
295
- /* Held Option marks the letter each sidebar link answers to. Marked rather than
296
- bracketed: '[C]ontacts' would shift the whole sidebar two characters wide the
297
- moment the key went down. Swap the two rules below for the bracketed form. */
298
- .recourse-keys .recourse-key {
299
- font-weight: var(--bs-font-weight-semibold);
300
- text-decoration: underline;
301
- text-underline-offset: 2px;
302
- }
303
- /* .recourse-keys .recourse-key::before { content: '['; }
304
- .recourse-keys .recourse-key::after { content: ']'; } */
305
-
306
- /* The button that empties a combobox search sits inside the field it clears. */
307
- .combobox-search { position: relative; }
308
- .combobox-search-input { padding-inline-end: 2rem; }
309
- /* `:not(.d-none)`, because v6's display utilities carry no `!important` and
310
- this rule is later in the cascade: a plain `display` here would beat the
311
- `.d-none` the button is hidden with, and show an X over an empty field. */
312
- .combobox-search-clear:not(.d-none) { display: flex; }
313
- .combobox-search-clear {
314
- position: absolute;
315
- inset-block-start: 50%;
316
- inset-inline-end: calc(var(--bs-menu-padding-x) + .35rem);
317
- transform: translateY(-50%);
318
- padding: .15rem;
319
- border: 0;
320
- border-radius: var(--bs-radius-5);
321
- background: transparent;
322
- color: var(--bs-fg-2);
323
- line-height: 1;
324
- }
325
- .combobox-search-clear:hover { background-color: var(--bs-bg-1); color: var(--bs-fg-body); }
326
-
327
- /* A payload in a block of its own, bounded so it scrolls rather than grows. `pre`
328
- for the indentation that makes JSON readable, but wrapping rather than running
329
- off to the side: a flex item's automatic minimum size is the min-content width
330
- of whatever is inside it, and `overflow` on this box does not lower it — so one
331
- unbroken 4,000-character token became the width of the value's column, then of
332
- `main`, which then wrapped below the sidebar it belongs beside. `pre-wrap` keeps
333
- the indentation and takes the wrapping, and `anywhere` is what lowers
334
- min-content, which `break-word` in its place would not. */
335
- .recourse-payload {
336
- max-height: 12rem;
337
- margin-block-end: 0;
338
- overflow: auto;
339
- white-space: pre-wrap;
340
- overflow-wrap: anywhere;
341
- }
342
-
343
- /* And the columns holding it may be narrower than it is. A flex item's automatic
344
- minimum size is the min-content width of what is inside it, and `overflow` on
345
- the box inside does not lower it — so without these the 32,000px one unbroken
346
- token wants becomes the width of the value's column, then of `main`, then of
347
- the page, and `main` wraps below the sidebar it should be sitting beside. These
348
- two are every flex item in this layout that holds a page's own content, which
349
- is what makes `overflow` on the box inside mean what it says. */
350
-
351
- /* A row the viewer has kept, and the only report a click ever gets, so it has to
352
- be seen. A twelfth of the primary mixed into the page rather than
353
- `--bs-primary-bg-subtle`: that token is a fixed step of the ramp, far enough
354
- from the page on a low-contrast palette to cost the text a point of contrast
355
- it has not got — mixing into the page keeps the tint the same distance from it
356
- whatever the palette. A background rather than `.table-active`, which sets the
357
- very variable `.table-hover` sets and would read as the row under the cursor,
358
- and lose its tint the moment it became one; a background lets the hover shadow
359
- lay over it. */
360
- .table > tbody > .recourse-kept > * {
361
- background-color: color-mix(in srgb, var(--bs-primary-fg) 12%, var(--bs-bg-body));
362
- }
363
-
364
- /* The row a write just landed on, tinted for as long as the message saying so
365
- stands. On the cells rather than on the row, like the kept tint beside it: the
366
- table collapses its borders, so the cells are what paint, and one tint across
367
- all of them is what reads as a single row — where anything drawn per cell reads
368
- as a box around each of them instead.
369
- A background rather than a border. A border on a collapsing table is a width
370
- the table then measures around, and taking it away again leaves the line it
371
- drew sitting between two rows that no longer want one.
372
- Mixed into the page exactly as the kept tint below is, and never
373
- `--bs-success-bg-subtle`: that token is `light-dark(--bs-green-100,
374
- --bs-green-900)`, and a palette builds those steps by mixing toward white or
375
- black rather than toward the page — so how far it lands from the page is the
376
- palette's business and not ours. Mixing here keeps it the same distance on all
377
- nine. The success family rather than the primary one is what tells it apart
378
- from the kept tint it may land on top of: a different hue reads as a different
379
- thing, where a second shade of the primary would read as more of the same. */
380
- .table > tbody > .recourse-written > *,
381
- .table > tbody > .recourse-written-out > * {
382
- --recourse-written: color-mix(in srgb, var(--bs-success-fg) 12%, var(--bs-bg-body));
383
- }
384
- .table > tbody > .recourse-written > * {
385
- background-color: var(--recourse-written);
386
- }
387
- /* Let go of over the second the toast beside it takes to fade, and started by the
388
- same event. The class above comes off as this one goes on, so the animation is
389
- the only thing painting the row while it runs and nothing paints it afterwards
390
- — which hands a kept row back its own tint rather than leaving it wearing this
391
- one. */
392
- .table > tbody > .recourse-written-out > * {
393
- animation: recourse-written-out 1s linear;
394
- }
395
- @keyframes recourse-written-out {
396
- from { background-color: var(--recourse-written); }
397
- to { background-color: var(--bs-bg-body); }
398
- }
399
- /* The tint itself is not motion; only its going is. */
400
- @media (prefers-reduced-motion: reduce) {
401
- .table > tbody > .recourse-written-out > * { animation: none; }
402
- }
403
-
404
- /* The foot of the sidebar holds the scheme toggle and the way out side by side,
405
- each centered in its own half of the row: the toggle alone is in the middle, and
406
- the two together are at a quarter and at three quarters. Each control sits in
407
- a wrapper — the toggle's own, the form `button_to` draws — and the wrapper is
408
- the half, so a button is only as wide as its icon and so is its hover. */
409
- .recourse-foot { display: flex; }
410
- .recourse-foot > * { flex: 1 1 0; display: flex; justify-content: center; }
411
- .recourse-foot .nav-link { border-radius: var(--bs-radius-5); }
412
- /* A hand cursor alone says little on an icon: a tint under it says it is a control.
413
- Only where something can hover: a finger's tap leaves `:hover` set until the next
414
- tap elsewhere, and a tint that stays reads as a button stuck down. */
415
- @media (hover: hover) {
416
- .recourse-foot .nav-link:hover { background-color: var(--bs-bg-1); color: var(--bs-fg-body); }
417
- }
418
-
419
- /* `.nav-item` ships `flex: auto`: in a row every entry would take an equal share
420
- of the width and in a full-height column an equal share of the height, and the
421
- foot's auto margin would never have anything left to push against. Every item is
422
- its own size instead, in both directions. */
423
- .recourse-sidebar .nav-item { flex: 0 0 auto; }
424
-
425
- /* On a phone the sidebar is one row of icons across the top, centered, each entry
426
- with less room around it than a column gives — a dozen have to fit — and wrapping
427
- to a second line only where a host has more than a screen's width of them. */
428
- @media (width < 768px) {
429
- .recourse-sidebar .nav { justify-content: center; --bs-nav-link-padding-x: .5rem; }
430
- }
431
-
432
- /* The foot sits at the bottom of the sidebar and stretches across it while the
433
- sidebar is a column, and is simply the last item of the row while it is not
434
- — the query matches the `md:` the sidebar's own classes use. */
435
- @media (width >= 768px) {
436
- /* A floor rather than a height, and never wrapped. `.nav` ships `flex-wrap:
437
- wrap`, and a column that may wrap, whose items are pinned to their own height
438
- above, and which is told exactly how tall to be has one way to place a link
439
- that does not fit: a second column. Which is what a short window used to draw.
440
- The floor lets it be taller than the space it was given. */
441
- .recourse-sidebar .nav { min-height: 100%; flex-wrap: nowrap; }
442
- /* And it rides at the foot of the screen, which the auto margin alone is
443
- enough to do: the nav's floor is the sidebar's full height, so while the
444
- links leave room the margin takes all of it and the foot sits on the fold,
445
- rising with it as the window is shortened. Once the links fill the nav there
446
- is no room left to take, and the foot comes to rest immediately below the
447
- last of them and goes under the fold with it, to be scrolled to.
448
- Deliberately not sticky. Sticking it to the bottom of the viewport would lift
449
- it off that resting place and hold it over the links it had passed — which is
450
- what the background color was for, masking whichever one it covered. Both go
451
- together: nothing is painted over, so nothing needs painting out. */
452
- .recourse-sidebar .recourse-foot {
453
- margin-block-start: auto;
454
- align-self: stretch;
455
- }
456
- }
457
-
458
- /* A phone reads the chrome as pictures. A sidebar entry is its icon, a crumb its
459
- icon, a tab its icon and its figure — `17` rather than `17 franchises` — and the
460
- foot's controls their icons, so the sidebar is one row of squares and the trail
461
- leaves the search its room. The words are there all the same, read but not seen
462
- the way Bootstrap's `.visually-hidden` does it, and the arrows at the foot put
463
- them back: the cookie they write has the server draw the shell `.recourse-expanded`,
464
- and then none of this applies. A tab or a crumb with no icon keeps its word at
465
- every width, there being nothing else to stand for it.
466
- The same hiding, at every width, for the foot's words once the sidebar is a
467
- column: there is room for an icon and not for a word beside it. */
468
- .recourse-foot-word { margin-inline-start: .4em; }
469
- @media (width < 768px) {
470
- .recourse-shell:not(.recourse-expanded) :is(.recourse-nav-word, .recourse-crumb-word,
471
- .recourse-tab-word, .recourse-foot-word) {
472
- position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
473
- overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
474
- }
475
- }
476
- @media (width >= 768px) {
477
- .recourse-foot-word {
478
- position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
479
- overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
480
- }
481
- /* Nothing to expand: a column has its words already. */
482
- .recourse-density { display: none; }
483
- }
484
-
485
- /* The arrows say where a tap goes, like the moon: parting, with `Expand`, while
486
- the chrome is icons; meeting, with `Compact`, once the words are out. Both faces
487
- are drawn and the shell's class picks, so the button needs no script to redraw. */
488
- .recourse-density-expanded { display: none; }
489
- .recourse-expanded .recourse-density-compact { display: none; }
490
- .recourse-expanded .recourse-density-expanded { display: inline; }
491
-
492
- /* The toggle draws both icons and shows the one that names where a click
493
- would go: a moon on a light page, a sun on a dark one. Three states rather than
494
- two, because a page nobody has chosen for carries no attribute at all, and the
495
- media query is then the only thing that knows which mode it is in. Both are
496
- declared at every step rather than one left to its default, so that no state
497
- can end up hiding the pair and leaving a button with nothing in it. */
498
- .recourse-scheme-light { display: inline; }
499
- .recourse-scheme-dark { display: none; }
500
- @media (prefers-color-scheme: dark) {
501
- :root:not([data-bs-theme='light']) .recourse-scheme-light { display: none; }
502
- :root:not([data-bs-theme='light']) .recourse-scheme-dark { display: inline; }
503
- }
504
- :root[data-bs-theme='light'] .recourse-scheme-light { display: inline; }
505
- :root[data-bs-theme='light'] .recourse-scheme-dark { display: none; }
506
- :root[data-bs-theme='dark'] .recourse-scheme-light { display: none; }
507
- :root[data-bs-theme='dark'] .recourse-scheme-dark { display: inline; }
508
-
509
- /* The icon font drops every glyph an eighth of an em to sit on a text
510
- baseline, which inside an icon link reads as the arrow resting on the
511
- line. A sixteenth up clears the baseline without floating — on the
512
- ::before, so the hover transform on the .bi box itself is untouched. */
513
- .icon-link > .bi::before { vertical-align: .0625em; }
514
- </style>
54
+ <%# Turbo, Bootstrap and a Stimulus application with every controller this markup
55
+ names, started once under `window.Stimulus`. In the head, so a controller exists
56
+ before the body's first `data-controller`. -%>
57
+ <script type='module' src='https://design.houseaccount.com/v0.3.1/js/houseaccount.js'></script>
515
58
  </head>
516
59
 
517
60
  <%# The reader's zone is told to the server here rather than from a page of its
@@ -544,20 +87,5 @@
544
87
  </main>
545
88
 
546
89
  <%= render 'recourses/flash' %>
547
- <%= render 'recourses/confirm' %>
548
-
549
- <%# turbo-rails' own bundle, in the version that gem signs its streams for: the
550
- Turbo behind every frame and morph here, plus the cable element and Action
551
- Cable client that turn an index's subscription tag into a connection. -%>
552
- <script type='module' src='/recourse/turbo.min.js'></script>
553
- <script type='module' src='/recourse/bootstrap.bundle.min.js'></script>
554
- <script type='module'>
555
- import confirm from '/recourse/confirm.js'
556
-
557
- // The browser's confirm() becomes the Bootstrap dialog. Re-assigned on every
558
- // visit, since body scripts re-run — harmless, the module hands back the same
559
- // function each time.
560
- window.Turbo.config.forms.confirm = confirm
561
- </script>
562
90
  </body>
563
91
  </html>