recourse 3.0.3 → 4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +234 -0
- data/README.md +150 -20
- data/app/controllers/bookmarks_controller.rb +7 -0
- data/app/controllers/concerns/recourse/landing.rb +49 -0
- data/app/controllers/concerns/recourse/list_resolution.rb +28 -0
- data/app/controllers/concerns/recourse/paging.rb +17 -0
- data/app/controllers/concerns/recourse/parent_naming.rb +24 -0
- data/app/controllers/concerns/recourse/parent_resolution.rb +63 -0
- data/app/controllers/concerns/recourse/reference_resolution.rb +70 -0
- data/app/controllers/concerns/recourse/resource_resolution.rb +68 -0
- data/app/controllers/concerns/recourse/zoning.rb +29 -0
- data/app/controllers/recourse/base_controller.rb +85 -0
- data/app/controllers/recourse/bookmarks_controller.rb +76 -0
- data/app/controllers/recourses_controller.rb +6 -25
- data/app/javascript/recourse/bookmark_controller.js +100 -0
- data/app/javascript/recourse/clear_controller.js +22 -0
- data/app/javascript/recourse/combobox_controller.js +68 -0
- data/app/javascript/recourse/confirm.js +46 -0
- data/app/javascript/recourse/deselect_controller.js +37 -0
- data/app/javascript/recourse/flash.js +35 -0
- data/app/javascript/recourse/limit_controller.js +23 -0
- data/app/javascript/recourse/phone_controller.js +33 -0
- data/app/javascript/recourse/relative_time_controller.js +45 -0
- data/app/javascript/recourse/reveal_controller.js +16 -0
- data/app/javascript/recourse/scheme_controller.js +51 -0
- data/app/javascript/recourse/search_controller.js +78 -0
- data/app/javascript/recourse/shortcuts_controller.js +42 -0
- data/app/javascript/recourse/timezone_controller.js +27 -0
- data/app/javascript/recourse/toast_controller.js +35 -0
- data/app/javascript/recourse/tooltip_controller.js +16 -0
- data/app/javascript/recourse/written.js +29 -0
- data/app/javascript/recourse/written_controller.js +28 -0
- data/app/views/layouts/recourses.html.erb +442 -0
- data/app/views/recourses/_breadcrumb.html.erb +35 -0
- data/app/views/recourses/_card.html.erb +33 -0
- data/app/views/recourses/_color.html.erb +20 -0
- data/app/views/recourses/_combobox.html.erb +68 -0
- data/app/views/recourses/_confirm.html.erb +14 -0
- data/app/views/recourses/_fields.html.erb +3 -0
- data/app/views/recourses/_flash.html.erb +26 -0
- data/app/views/recourses/_footer.html.erb +22 -0
- data/app/views/recourses/_form.html.erb +11 -0
- data/app/views/recourses/_none.html.erb +1 -0
- data/app/views/recourses/_results.html.erb +10 -0
- data/app/views/recourses/_row.html.erb +4 -10
- data/app/views/recourses/_scheme.html.erb +14 -0
- data/app/views/recourses/_search.html.erb +24 -0
- data/app/views/recourses/_sidebar.html.erb +40 -0
- data/app/views/recourses/_table.html.erb +47 -42
- data/app/views/recourses/_values.html.erb +3 -0
- data/app/views/recourses/edit.html.erb +10 -0
- data/app/views/recourses/index.html.erb +27 -24
- data/app/views/recourses/new.html.erb +3 -0
- data/app/views/recourses/show.html.erb +10 -0
- data/config/locales/recourse.en.yml +72 -0
- data/lib/recourse/bookmarks.rb +51 -0
- data/lib/recourse/broadcasting.rb +33 -0
- data/lib/recourse/colors.rb +32 -0
- data/lib/recourse/columns.rb +80 -0
- data/lib/recourse/controllers.rb +26 -0
- data/lib/recourse/engine.rb +33 -13
- data/lib/recourse/helpers/actions.rb +56 -0
- data/lib/recourse/helpers/bookmarks.rb +88 -0
- data/lib/recourse/helpers/buttons.rb +65 -0
- data/lib/recourse/helpers/cards.rb +67 -0
- data/lib/recourse/helpers/cells.rb +89 -0
- data/lib/recourse/helpers/choices.rb +49 -0
- data/lib/recourse/helpers/colors.rb +20 -0
- data/lib/recourse/helpers/comboboxes.rb +87 -0
- data/lib/recourse/helpers/constraints.rb +92 -0
- data/lib/recourse/helpers/counters.rb +75 -0
- data/lib/recourse/helpers/deletions.rb +87 -0
- data/lib/recourse/helpers/details.rb +41 -0
- data/lib/recourse/helpers/examples.rb +35 -0
- data/lib/recourse/helpers/fields.rb +78 -0
- data/lib/recourse/helpers/filters.rb +91 -0
- data/lib/recourse/helpers/formats.rb +94 -0
- data/lib/recourse/helpers/inputs.rb +92 -0
- data/lib/recourse/helpers/kinds.rb +87 -0
- data/lib/recourse/helpers/limits.rb +37 -0
- data/lib/recourse/helpers/names.rb +44 -0
- data/lib/recourse/helpers/navigation.rb +75 -0
- data/lib/recourse/helpers/parents.rb +82 -0
- data/lib/recourse/helpers/pictures.rb +33 -0
- data/lib/recourse/helpers/references.rb +99 -0
- data/lib/recourse/helpers/refreshes.rb +27 -0
- data/lib/recourse/helpers/resources.rb +47 -0
- data/lib/recourse/helpers/routing.rb +28 -0
- data/lib/recourse/helpers/rows.rb +33 -0
- data/lib/recourse/helpers/schemes.rb +14 -0
- data/lib/recourse/helpers/searches.rb +57 -0
- data/lib/recourse/helpers/shortcuts.rb +30 -0
- data/lib/recourse/helpers/sidebars.rb +39 -0
- data/lib/recourse/helpers/sorts.rb +62 -0
- data/lib/recourse/helpers/tabs.rb +82 -0
- data/lib/recourse/helpers/times.rb +43 -0
- data/lib/recourse/helpers/values.rb +64 -0
- data/lib/recourse/helpers/zones.rb +38 -0
- data/lib/recourse/helpers.rb +81 -0
- data/lib/recourse/icons.rb +21 -0
- data/lib/recourse/limits.rb +12 -0
- data/lib/recourse/recoursive.rb +66 -19
- data/lib/recourse/registry.rb +36 -0
- data/lib/recourse/routes/nested.rb +36 -0
- data/lib/recourse/routes.rb +57 -0
- data/lib/recourse/routing.rb +11 -32
- data/lib/recourse/schemes.rb +6 -0
- data/lib/recourse/scopes.rb +37 -0
- data/lib/recourse/search.rb +65 -0
- data/lib/recourse/searchable/columns.rb +99 -0
- data/lib/recourse/searchable/filters.rb +58 -0
- data/lib/recourse/searchable/terms.rb +56 -0
- data/lib/recourse/searchable.rb +47 -22
- data/lib/recourse/titles.rb +56 -0
- data/lib/recourse/version.rb +2 -1
- data/lib/recourse/writes.rb +12 -0
- data/lib/recourse/zones.rb +6 -0
- data/lib/recourse.rb +79 -7
- data/vendor/recourse/bootstrap-icons.min.css +5 -0
- data/vendor/recourse/bootstrap.bundle.min.js +9 -0
- data/vendor/recourse/bootstrap.min.css +2 -0
- data/vendor/recourse/fonts/bootstrap-icons.woff +0 -0
- data/vendor/recourse/fonts/bootstrap-icons.woff2 +0 -0
- data/vendor/recourse/stimulus.js +2563 -0
- metadata +169 -30
- data/Rakefile +0 -6
- data/app/helpers/navigable_helper.rb +0 -35
- data/app/helpers/recoursive_helper.rb +0 -10
- data/app/helpers/searchable_helper.rb +0 -54
- data/db/migrate/20260323234318_add_baby_to_posts.rb +0 -5
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang='en'>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset='utf-8'>
|
|
5
|
+
<title><%= content_for(:title) || 'Recourse' %></title>
|
|
6
|
+
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
|
7
|
+
<meta name='color-scheme' content='light dark'>
|
|
8
|
+
<%= csrf_meta_tags %>
|
|
9
|
+
<%= csp_meta_tag %>
|
|
10
|
+
|
|
11
|
+
<%= yield :head %>
|
|
12
|
+
|
|
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'>
|
|
18
|
+
<%# After the stylesheet, which is what makes it win: both selectors are `:root`. -%>
|
|
19
|
+
<%= render 'recourses/scheme' %>
|
|
20
|
+
<%= primary_color_style %>
|
|
21
|
+
|
|
22
|
+
<%# The host's own icon, under whichever of the four names it ships it: Rails 8
|
|
23
|
+
writes `icon.png` and `icon.svg`, and an older app has a `favicon`. The browser
|
|
24
|
+
takes the first it can fetch, so an app that has done nothing but draw a route
|
|
25
|
+
still wears its own tab. -%>
|
|
26
|
+
<link rel='icon' href='/icon.png' type='image/png'>
|
|
27
|
+
<link rel='icon' href='/icon.svg' type='image/svg+xml'>
|
|
28
|
+
<link rel='icon' href='/favicon.ico' sizes='any'>
|
|
29
|
+
<link rel='icon' href='/favicon.png' type='image/png'>
|
|
30
|
+
<link rel='apple-touch-icon' href='/icon.png'>
|
|
31
|
+
|
|
32
|
+
<script type='module'>
|
|
33
|
+
import { Application } from '/recourse/stimulus.js'
|
|
34
|
+
import BookmarkController from '/recourse/bookmark_controller.js'
|
|
35
|
+
import ClearController from '/recourse/clear_controller.js'
|
|
36
|
+
import ComboboxController from '/recourse/combobox_controller.js'
|
|
37
|
+
import DeselectController from '/recourse/deselect_controller.js'
|
|
38
|
+
import LimitController from '/recourse/limit_controller.js'
|
|
39
|
+
import PhoneController from '/recourse/phone_controller.js'
|
|
40
|
+
import RelativeTimeController from '/recourse/relative_time_controller.js'
|
|
41
|
+
import RevealController from '/recourse/reveal_controller.js'
|
|
42
|
+
import SchemeController from '/recourse/scheme_controller.js'
|
|
43
|
+
import SearchController from '/recourse/search_controller.js'
|
|
44
|
+
import ShortcutsController from '/recourse/shortcuts_controller.js'
|
|
45
|
+
import TimezoneController from '/recourse/timezone_controller.js'
|
|
46
|
+
import ToastController from '/recourse/toast_controller.js'
|
|
47
|
+
import WrittenController from '/recourse/written_controller.js'
|
|
48
|
+
import TooltipController from '/recourse/tooltip_controller.js'
|
|
49
|
+
|
|
50
|
+
// In the head, and guarded: Turbo re-runs body scripts on every visit, and a
|
|
51
|
+
// second application would connect every controller twice.
|
|
52
|
+
if (!window.Stimulus) {
|
|
53
|
+
window.Stimulus = Application.start()
|
|
54
|
+
window.Stimulus.register('bookmark', BookmarkController)
|
|
55
|
+
window.Stimulus.register('clear', ClearController)
|
|
56
|
+
window.Stimulus.register('combobox', ComboboxController)
|
|
57
|
+
window.Stimulus.register('deselect', DeselectController)
|
|
58
|
+
window.Stimulus.register('limit', LimitController)
|
|
59
|
+
window.Stimulus.register('phone', PhoneController)
|
|
60
|
+
window.Stimulus.register('relative-time', RelativeTimeController)
|
|
61
|
+
window.Stimulus.register('reveal', RevealController)
|
|
62
|
+
window.Stimulus.register('scheme', SchemeController)
|
|
63
|
+
window.Stimulus.register('search', SearchController)
|
|
64
|
+
window.Stimulus.register('shortcuts', ShortcutsController)
|
|
65
|
+
window.Stimulus.register('timezone', TimezoneController)
|
|
66
|
+
window.Stimulus.register('toast', ToastController)
|
|
67
|
+
window.Stimulus.register('tooltip', TooltipController)
|
|
68
|
+
window.Stimulus.register('written', WrittenController)
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<style>
|
|
73
|
+
/* System faces at a compact size, and no webfont: nothing to download, no
|
|
74
|
+
swap flash, and the last external host the layout reached for is gone.
|
|
75
|
+
14px as rem, so a browser's own text-size setting still scales it. */
|
|
76
|
+
:root {
|
|
77
|
+
--bs-body-font-family: helvetica, verdana, arial, sans-serif;
|
|
78
|
+
--bs-body-font-size: 0.875rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Bootstrap's .15s fade reads as vanishing. Only the hide ever transitions here
|
|
82
|
+
— the toast is born visible — and reduced-motion still wins, because its rule
|
|
83
|
+
sets `transition: none` outright rather than through this variable. */
|
|
84
|
+
.toast { --bs-transition-fade: opacity 1s linear; }
|
|
85
|
+
|
|
86
|
+
/* A marked match is part of the word it sits in, so it carries no padding of
|
|
87
|
+
its own: 'Nash' in 'Nashville' must not read as a word standing apart. */
|
|
88
|
+
mark { padding: 0; }
|
|
89
|
+
|
|
90
|
+
/* A crumb that is not a link keeps its resting colors under the cursor.
|
|
91
|
+
Bootstrap lights every .breadcrumb-link on hover, which on a <span>
|
|
92
|
+
promises a click that goes nowhere — and the element is the whole
|
|
93
|
+
discriminator, since a crumb that links is an <a>. Redefining the hover
|
|
94
|
+
tokens, rather than out-cascading the hover rule, leaves Bootstrap's own
|
|
95
|
+
selector in charge and the active crumb its own resting color. */
|
|
96
|
+
span.breadcrumb-link {
|
|
97
|
+
--bs-breadcrumb-link-hover-color: var(--bs-breadcrumb-link-color);
|
|
98
|
+
--bs-breadcrumb-link-hover-bg: transparent;
|
|
99
|
+
}
|
|
100
|
+
span.breadcrumb-link.active {
|
|
101
|
+
--bs-breadcrumb-link-hover-color: var(--bs-breadcrumb-link-active-color);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* An action column is as wide as a row is tall — an icon in a square — and a
|
|
105
|
+
counter column starts at the same square, widening only when a figure like
|
|
106
|
+
38,405 needs the room: a table laying out to 100% reads the width as a
|
|
107
|
+
preference, never crushes content into it, and hands what these cells do
|
|
108
|
+
not use to the columns carrying text. Not while the table is stacked, where
|
|
109
|
+
every cell is a block and a square one is a squashed one — the query
|
|
110
|
+
matches `.sm:table-stacked`'s. */
|
|
111
|
+
@container (width >= 576px) {
|
|
112
|
+
.recourse-actions, .recourse-counter {
|
|
113
|
+
width: calc(1em * var(--bs-body-line-height) + 2 * var(--bs-table-cell-padding-y));
|
|
114
|
+
white-space: nowrap;
|
|
115
|
+
text-align: center;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* A counter says what it counts in one of two ways, and the table's own width is
|
|
120
|
+
what picks: the icon and a bare figure where the column is a square, the
|
|
121
|
+
counted model's words where there is room to read them. Bootstrap's `xl`, so
|
|
122
|
+
words arrive on a wide monitor and nowhere narrower — the width above is a
|
|
123
|
+
preference a table never crushes content into, so the column simply widens to
|
|
124
|
+
take them. Against `.table-responsive` like every query here, which is the
|
|
125
|
+
table's width rather than the window's: a sidebar takes a few hundred pixels
|
|
126
|
+
the window knows nothing about. */
|
|
127
|
+
.recourse-counter-word { display: none; }
|
|
128
|
+
@container (width >= 1280px) {
|
|
129
|
+
.recourse-counter-icon, .recourse-counter-figure { display: none; }
|
|
130
|
+
.recourse-counter-word { display: inline; }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* One rule per row of the show page, so a value and its heading read as one thing
|
|
134
|
+
and the next pair as another. On the cells rather than between them: a column's
|
|
135
|
+
gutter is padding inside it, so two side by side draw one unbroken line.
|
|
136
|
+
Every row reserves the width a rule takes, in `transparent`, and the show page
|
|
137
|
+
only colors it in — which is what leaves a field at exactly the height of the
|
|
138
|
+
value it edits, row after row down two pages. */
|
|
139
|
+
.recourse-row {
|
|
140
|
+
border-block-end: var(--bs-border-width) solid transparent;
|
|
141
|
+
}
|
|
142
|
+
.recourse-values > .recourse-row {
|
|
143
|
+
border-block-end-color: var(--bs-border-color);
|
|
144
|
+
}
|
|
145
|
+
/* And a value stands as tall as the control that edits it, so a row on one page
|
|
146
|
+
is a row of the same height on the other. Bootstrap gives
|
|
147
|
+
`.form-control-plaintext` a control's padding and line height but no minimum,
|
|
148
|
+
and an icon or a badge is shorter than a box. */
|
|
149
|
+
.recourse-values .form-control-plaintext {
|
|
150
|
+
min-height: var(--bs-control-min-height);
|
|
151
|
+
}
|
|
152
|
+
/* The reveal is a word beside a value rather than a control beside it, so it takes
|
|
153
|
+
the line's height and not `.btn-sm`'s minimum — which would leave a masked row
|
|
154
|
+
taller than the field that edits the same attribute. */
|
|
155
|
+
.form-control-plaintext .btn {
|
|
156
|
+
min-height: 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* A long page scrolls its content, not its chrome. Above the width the sidebar
|
|
160
|
+
becomes a column at, the shell is exactly the viewport and `main` is the only
|
|
161
|
+
thing inside it that scrolls, so the navbar and the sidebar stay where they
|
|
162
|
+
are however far down the table a reader is. Two things make it hold. Every step
|
|
163
|
+
between takes `min-height: 0`, a flex child being as tall as its content unless
|
|
164
|
+
told otherwise. And the row stops wrapping: a wrapped line is as tall as the
|
|
165
|
+
tallest thing on it, and stretching only ever grows an item to the line, never
|
|
166
|
+
shrinks it to one — so `main` kept its own height and took the page with it.
|
|
167
|
+
Below that width the sidebar is a band across the top, the row wraps to stack
|
|
168
|
+
the two, and the page scrolls as one, which is the only thing that reads on a
|
|
169
|
+
phone.
|
|
170
|
+
Named by the path down to it rather than as any `.row` in the shell: a show
|
|
171
|
+
page lays its values out in one and a form lays its fields out in another, and
|
|
172
|
+
`flex-wrap: nowrap` reaches those too as a descendant — which is three `col-6`
|
|
173
|
+
values squeezed onto one line instead of two rows of two. Only the row holding
|
|
174
|
+
the sidebar and the content is meant here, and only it is a child of the
|
|
175
|
+
container.
|
|
176
|
+
`main` is the only thing that scrolls inside the shell, and the sidebar
|
|
177
|
+
deliberately is not. A window dragged shorter than the sidebar itself is the one
|
|
178
|
+
case the viewport cannot hold, and there the sidebar is allowed to be taller
|
|
179
|
+
than the line it was stretched to, spill past the shell, and let the browser
|
|
180
|
+
scroll the page — which is what a reader expects of something taller than the
|
|
181
|
+
screen. Were it to scroll on its own instead, the same overflow would be hidden
|
|
182
|
+
inside it and the page would look whole while its foot was unreachable. */
|
|
183
|
+
@media (width >= 768px) {
|
|
184
|
+
.recourse-shell { min-height: 0; height: 100dvh; }
|
|
185
|
+
.recourse-shell > .container-fluid,
|
|
186
|
+
.recourse-shell > .container-fluid > .row { min-height: 0; }
|
|
187
|
+
.recourse-shell > .container-fluid > .row { flex-wrap: nowrap; }
|
|
188
|
+
.recourse-shell main { overflow-y: auto; }
|
|
189
|
+
/* Stretched to the line like any item, but never squeezed under its own
|
|
190
|
+
content: the floor is what carries the rule below down past the fold. */
|
|
191
|
+
.recourse-shell .recourse-sidebar { min-height: min-content; }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* The sidebar rules off the content below it while it sits under the navbar,
|
|
195
|
+
and off the content beside it once it is a column again. v6 generates no
|
|
196
|
+
responsive border utilities, so the switch is written out. */
|
|
197
|
+
.recourse-sidebar {
|
|
198
|
+
border-block-end: var(--bs-border-width) solid var(--bs-border-color);
|
|
199
|
+
}
|
|
200
|
+
@media (width >= 768px) {
|
|
201
|
+
.recourse-sidebar {
|
|
202
|
+
border-block-end: 0;
|
|
203
|
+
border-inline-end: var(--bs-border-width) solid var(--bs-border-color);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* The search and its filters take the width the breadcrumb and the buttons
|
|
208
|
+
leave, and give it back as the viewport narrows: the box shrinks to a floor
|
|
209
|
+
that still shows a word, and below 768px the row is allowed to wrap. */
|
|
210
|
+
.recourse-search { flex: 1 1 auto; max-width: 48rem; }
|
|
211
|
+
/* The box grows and the filters do not, so the field someone types into is the
|
|
212
|
+
widest thing in the row on a large viewport. A toggle is `width: 100%`, which
|
|
213
|
+
as a flex item would otherwise mean the whole form. */
|
|
214
|
+
.recourse-search-box { flex: 2 1 16rem; min-width: 8rem; }
|
|
215
|
+
|
|
216
|
+
.recourse-search .combobox-toggle { flex: 0 1 10rem; }
|
|
217
|
+
/* The per-page switch is a word in the sentence beside it rather than a control
|
|
218
|
+
next to one, so it takes the line's height and not `.btn`'s own minimum —
|
|
219
|
+
which would leave the row under every table taller than the words in it. */
|
|
220
|
+
.recourse-limit { min-height: 0; }
|
|
221
|
+
/* Stacked, each control is a row of its own rather than a fragment of one:
|
|
222
|
+
there is no second control beside it to share the line with. */
|
|
223
|
+
@media (width < 768px) {
|
|
224
|
+
.recourse-search { max-width: none; }
|
|
225
|
+
.recourse-search-box,
|
|
226
|
+
.recourse-search .combobox-toggle { flex: 1 1 100%; }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* Held Option marks the letter each sidebar link answers to. Marked rather than
|
|
230
|
+
bracketed: '[C]ontacts' would shift the whole sidebar two characters wide the
|
|
231
|
+
moment the key went down. Swap the two rules below for the bracketed form. */
|
|
232
|
+
.recourse-keys .recourse-key {
|
|
233
|
+
font-weight: var(--bs-font-weight-semibold);
|
|
234
|
+
text-decoration: underline;
|
|
235
|
+
text-underline-offset: 2px;
|
|
236
|
+
}
|
|
237
|
+
/* .recourse-keys .recourse-key::before { content: '['; }
|
|
238
|
+
.recourse-keys .recourse-key::after { content: ']'; } */
|
|
239
|
+
|
|
240
|
+
/* The button that empties a combobox search sits inside the field it clears. */
|
|
241
|
+
.combobox-search { position: relative; }
|
|
242
|
+
.combobox-search-input { padding-inline-end: 2rem; }
|
|
243
|
+
/* `:not(.d-none)`, because v6's display utilities carry no `!important` and
|
|
244
|
+
this rule is later in the cascade: a plain `display` here would beat the
|
|
245
|
+
`.d-none` the button is hidden with, and show an X over an empty field. */
|
|
246
|
+
.combobox-search-clear:not(.d-none) { display: flex; }
|
|
247
|
+
.combobox-search-clear {
|
|
248
|
+
position: absolute;
|
|
249
|
+
inset-block-start: 50%;
|
|
250
|
+
inset-inline-end: calc(var(--bs-menu-padding-x) + .35rem);
|
|
251
|
+
transform: translateY(-50%);
|
|
252
|
+
padding: .15rem;
|
|
253
|
+
border: 0;
|
|
254
|
+
border-radius: var(--bs-radius-5);
|
|
255
|
+
background: transparent;
|
|
256
|
+
color: var(--bs-fg-2);
|
|
257
|
+
line-height: 1;
|
|
258
|
+
}
|
|
259
|
+
.combobox-search-clear:hover { background-color: var(--bs-bg-1); color: var(--bs-fg-body); }
|
|
260
|
+
|
|
261
|
+
/* A payload in a block of its own, bounded so it scrolls rather than grows. `pre`
|
|
262
|
+
for the indentation that makes JSON readable, but wrapping rather than running
|
|
263
|
+
off to the side: a flex item's automatic minimum size is the min-content width
|
|
264
|
+
of whatever is inside it, and `overflow` on this box does not lower it — so one
|
|
265
|
+
unbroken 4,000-character token became the width of the value's column, then of
|
|
266
|
+
`main`, which then wrapped below the sidebar it belongs beside. `pre-wrap` keeps
|
|
267
|
+
the indentation and takes the wrapping, and `anywhere` is what lowers
|
|
268
|
+
min-content, which `break-word` in its place would not. */
|
|
269
|
+
.recourse-payload {
|
|
270
|
+
max-height: 12rem;
|
|
271
|
+
margin-block-end: 0;
|
|
272
|
+
overflow: auto;
|
|
273
|
+
white-space: pre-wrap;
|
|
274
|
+
overflow-wrap: anywhere;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* And the columns holding it may be narrower than it is. A flex item's automatic
|
|
278
|
+
minimum size is the min-content width of what is inside it, and `overflow` on
|
|
279
|
+
the box inside does not lower it — so without these the 32,000px one unbroken
|
|
280
|
+
token wants becomes the width of the value's column, then of `main`, then of
|
|
281
|
+
the page, and `main` wraps below the sidebar it should be sitting beside. These
|
|
282
|
+
two are every flex item in this layout that holds a page's own content, which
|
|
283
|
+
is what makes `overflow` on the box inside mean what it says. */
|
|
284
|
+
|
|
285
|
+
/* A row the viewer has kept, and the only report a click ever gets, so it has to
|
|
286
|
+
be seen. A twelfth of the primary mixed into the page rather than
|
|
287
|
+
`--bs-primary-bg-subtle`: that token is a fixed step of the ramp, far enough
|
|
288
|
+
from the page on a low-contrast palette to cost the text a point of contrast
|
|
289
|
+
it has not got — mixing into the page keeps the tint the same distance from it
|
|
290
|
+
whatever the palette. A background rather than `.table-active`, which sets the
|
|
291
|
+
very variable `.table-hover` sets and would read as the row under the cursor,
|
|
292
|
+
and lose its tint the moment it became one; a background lets the hover shadow
|
|
293
|
+
lay over it. */
|
|
294
|
+
.table > tbody > .recourse-kept > * {
|
|
295
|
+
background-color: color-mix(in srgb, var(--bs-primary-fg) 12%, var(--bs-bg-body));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* The row a write just landed on, tinted for as long as the message saying so
|
|
299
|
+
stands. On the cells rather than on the row, like the kept tint beside it: the
|
|
300
|
+
table collapses its borders, so the cells are what paint, and one tint across
|
|
301
|
+
all of them is what reads as a single row — where anything drawn per cell reads
|
|
302
|
+
as a box around each of them instead.
|
|
303
|
+
A background rather than a border. A border on a collapsing table is a width
|
|
304
|
+
the table then measures around, and taking it away again leaves the line it
|
|
305
|
+
drew sitting between two rows that no longer want one.
|
|
306
|
+
Mixed into the page exactly as the kept tint below is, and never
|
|
307
|
+
`--bs-success-bg-subtle`: that token is `light-dark(--bs-green-100,
|
|
308
|
+
--bs-green-900)`, and a palette builds those steps by mixing toward white or
|
|
309
|
+
black rather than toward the page — so how far it lands from the page is the
|
|
310
|
+
palette's business and not ours. Mixing here keeps it the same distance on all
|
|
311
|
+
nine. The success family rather than the primary one is what tells it apart
|
|
312
|
+
from the kept tint it may land on top of: a different hue reads as a different
|
|
313
|
+
thing, where a second shade of the primary would read as more of the same. */
|
|
314
|
+
.table > tbody > .recourse-written > *,
|
|
315
|
+
.table > tbody > .recourse-written-out > * {
|
|
316
|
+
--recourse-written: color-mix(in srgb, var(--bs-success-fg) 12%, var(--bs-bg-body));
|
|
317
|
+
}
|
|
318
|
+
.table > tbody > .recourse-written > * {
|
|
319
|
+
background-color: var(--recourse-written);
|
|
320
|
+
}
|
|
321
|
+
/* Let go of over the second the toast beside it takes to fade, and started by the
|
|
322
|
+
same event. The class above comes off as this one goes on, so the animation is
|
|
323
|
+
the only thing painting the row while it runs and nothing paints it afterwards
|
|
324
|
+
— which hands a kept row back its own tint rather than leaving it wearing this
|
|
325
|
+
one. */
|
|
326
|
+
.table > tbody > .recourse-written-out > * {
|
|
327
|
+
animation: recourse-written-out 1s linear;
|
|
328
|
+
}
|
|
329
|
+
@keyframes recourse-written-out {
|
|
330
|
+
from { background-color: var(--recourse-written); }
|
|
331
|
+
to { background-color: var(--bs-bg-body); }
|
|
332
|
+
}
|
|
333
|
+
/* The tint itself is not motion; only its going is. */
|
|
334
|
+
@media (prefers-reduced-motion: reduce) {
|
|
335
|
+
.table > tbody > .recourse-written-out > * { animation: none; }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* The scheme toggle sits at the foot of the sidebar and centered across it while
|
|
339
|
+
the sidebar is a column, and is simply the last item of the row while it is not
|
|
340
|
+
— the query matches the `md:` the sidebar's own classes use. Both lines are
|
|
341
|
+
needed: `.nav-item` ships `flex: auto`, so every item would take an equal share
|
|
342
|
+
of a full-height nav and the auto margin would never have anything left to
|
|
343
|
+
push against. Pin the items to their own height and the toggle takes the rest. */
|
|
344
|
+
@media (width >= 768px) {
|
|
345
|
+
/* A floor rather than a height, and never wrapped. `.nav` ships `flex-wrap:
|
|
346
|
+
wrap`, and a column that may wrap, whose items are pinned to their own height
|
|
347
|
+
by the line below, and which is told exactly how tall to be has one way to
|
|
348
|
+
place a link that does not fit: a second column. Which is what a short window
|
|
349
|
+
used to draw. The floor lets it be taller than the space it was given. */
|
|
350
|
+
.recourse-sidebar .nav { min-height: 100%; flex-wrap: nowrap; }
|
|
351
|
+
.recourse-sidebar .nav-item { flex: 0 0 auto; }
|
|
352
|
+
/* And it rides at the foot of the screen, which the auto margin alone is
|
|
353
|
+
enough to do: the nav's floor is the sidebar's full height, so while the
|
|
354
|
+
links leave room the margin takes all of it and the toggle sits on the fold,
|
|
355
|
+
rising with it as the window is shortened. Once the links fill the nav there
|
|
356
|
+
is no room left to take, and the toggle comes to rest immediately below the
|
|
357
|
+
last of them and goes under the fold with it, to be scrolled to.
|
|
358
|
+
Deliberately not sticky. Sticking it to the bottom of the viewport would lift
|
|
359
|
+
it off that resting place and hold it over the links it had passed — which is
|
|
360
|
+
what the background color was for, masking whichever one it covered. Both go
|
|
361
|
+
together: nothing is painted over, so nothing needs painting out. */
|
|
362
|
+
.recourse-sidebar .recourse-scheme {
|
|
363
|
+
margin-block-start: auto;
|
|
364
|
+
align-self: center;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* The toggle draws both icons and shows the one that names where a click
|
|
369
|
+
would go: a moon on a light page, a sun on a dark one. Three states rather than
|
|
370
|
+
two, because a page nobody has chosen for carries no attribute at all, and the
|
|
371
|
+
media query is then the only thing that knows which mode it is in. Both are
|
|
372
|
+
declared at every step rather than one left to its default, so that no state
|
|
373
|
+
can end up hiding the pair and leaving a button with nothing in it. */
|
|
374
|
+
.recourse-scheme-light { display: inline; }
|
|
375
|
+
.recourse-scheme-dark { display: none; }
|
|
376
|
+
@media (prefers-color-scheme: dark) {
|
|
377
|
+
:root:not([data-bs-theme='light']) .recourse-scheme-light { display: none; }
|
|
378
|
+
:root:not([data-bs-theme='light']) .recourse-scheme-dark { display: inline; }
|
|
379
|
+
}
|
|
380
|
+
:root[data-bs-theme='light'] .recourse-scheme-light { display: inline; }
|
|
381
|
+
:root[data-bs-theme='light'] .recourse-scheme-dark { display: none; }
|
|
382
|
+
:root[data-bs-theme='dark'] .recourse-scheme-light { display: none; }
|
|
383
|
+
:root[data-bs-theme='dark'] .recourse-scheme-dark { display: inline; }
|
|
384
|
+
|
|
385
|
+
/* The icon font drops every glyph an eighth of an em to sit on a text
|
|
386
|
+
baseline, which inside an icon link reads as the arrow resting on the
|
|
387
|
+
line. A sixteenth up clears the baseline without floating — on the
|
|
388
|
+
::before, so the hover transform on the .bi box itself is untouched. */
|
|
389
|
+
.icon-link > .bi::before { vertical-align: .0625em; }
|
|
390
|
+
</style>
|
|
391
|
+
</head>
|
|
392
|
+
|
|
393
|
+
<%# The reader's zone is told to the server here rather than from a page of its
|
|
394
|
+
own: every screen the gem draws is one to read against their clock. -%>
|
|
395
|
+
<body class='recourse-shell d-flex flex-column min-vh-100'
|
|
396
|
+
data-controller='timezone' data-timezone-storage-value='<%= Recourse::ZONE_STORAGE %>'>
|
|
397
|
+
<nav class='navbar border-bottom'>
|
|
398
|
+
<%# `row-gap` rather than a margin on the search: a margin is there whether the
|
|
399
|
+
form wrapped or not, and on the line it shares with the breadcrumb it drops
|
|
400
|
+
the field a few pixels below it. A row gap only exists between lines. -%>
|
|
401
|
+
<div class='container-fluid px-3 justify-content-start row-gap-2'>
|
|
402
|
+
<%= render 'recourses/breadcrumb' %>
|
|
403
|
+
<%= yield :actions %>
|
|
404
|
+
<%= yield :search %>
|
|
405
|
+
</div>
|
|
406
|
+
</nav>
|
|
407
|
+
|
|
408
|
+
<div class='container-fluid flex-grow-1 d-flex'>
|
|
409
|
+
<%# Stacked, the sidebar and the content are two lines of a wrapping flex row,
|
|
410
|
+
and `align-content: stretch` would hand each of them half the height the
|
|
411
|
+
page has left over — an empty gap under a dozen links. Packed to the top
|
|
412
|
+
below 768px, and stretched again above it, where the single line being full
|
|
413
|
+
height is what draws the sidebar's rule the whole way down. -%>
|
|
414
|
+
<div class='row flex-grow-1 align-content-start md:align-content-stretch'>
|
|
415
|
+
<aside class='recourse-sidebar col-12 md:col-auto py-3 px-3 mx-0'>
|
|
416
|
+
<%= render 'recourses/sidebar' %>
|
|
417
|
+
</aside>
|
|
418
|
+
|
|
419
|
+
<main class='col-12 md:col py-4 px-3'>
|
|
420
|
+
<%= yield %>
|
|
421
|
+
</main>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<%= render 'recourses/flash' %>
|
|
426
|
+
<%= render 'recourses/confirm' %>
|
|
427
|
+
|
|
428
|
+
<%# turbo-rails' own bundle, in the version that gem signs its streams for: the
|
|
429
|
+
Turbo behind every frame and morph here, plus the cable element and Action
|
|
430
|
+
Cable client that turn an index's subscription tag into a connection. -%>
|
|
431
|
+
<script type='module' src='/recourse/turbo.min.js'></script>
|
|
432
|
+
<script type='module' src='/recourse/bootstrap.bundle.min.js'></script>
|
|
433
|
+
<script type='module'>
|
|
434
|
+
import confirm from '/recourse/confirm.js'
|
|
435
|
+
|
|
436
|
+
// The browser's confirm() becomes the Bootstrap dialog. Re-assigned on every
|
|
437
|
+
// visit, since body scripts re-run — harmless, the module hands back the same
|
|
438
|
+
// function each time.
|
|
439
|
+
window.Turbo.config.forms.confirm = confirm
|
|
440
|
+
</script>
|
|
441
|
+
</body>
|
|
442
|
+
</html>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<nav aria-label='breadcrumb'>
|
|
2
|
+
<ol class='breadcrumb mb-0'>
|
|
3
|
+
<% crumbs = resource_breadcrumbs %>
|
|
4
|
+
<% crumbs.each_with_index do |(resource, title, path), index| %>
|
|
5
|
+
<% unless index.zero? %>
|
|
6
|
+
<li class='breadcrumb-divider'></li>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% if path %>
|
|
9
|
+
<%# A crumb naming a record — the parent a nested page is under — links
|
|
10
|
+
without an icon: the crumb before it already carries the resource's. -%>
|
|
11
|
+
<li class='breadcrumb-item'>
|
|
12
|
+
<%= link_to resource ? resource_label(resource, title) : title, path,
|
|
13
|
+
class: class_names('breadcrumb-link', 'gap-2': resource) %>
|
|
14
|
+
</li>
|
|
15
|
+
<% elsif index < crumbs.size - 1 %>
|
|
16
|
+
<%# A crumb standing between links without being one — the record a nested
|
|
17
|
+
page is under — reads as plain text, with no icon of its own: the crumb
|
|
18
|
+
before it already carries the parent resource's. -%>
|
|
19
|
+
<li class='breadcrumb-item'>
|
|
20
|
+
<span class='breadcrumb-link'><%= title %></span>
|
|
21
|
+
</li>
|
|
22
|
+
<% elsif resource %>
|
|
23
|
+
<li class='breadcrumb-item' aria-current='page'>
|
|
24
|
+
<span class='breadcrumb-link active gap-2'><%= resource_label resource, title %></span>
|
|
25
|
+
</li>
|
|
26
|
+
<% else %>
|
|
27
|
+
<%# A leaf names a page rather than a resource — `New market`, or a record by
|
|
28
|
+
its label — and the crumb before it already carries the icon. -%>
|
|
29
|
+
<li class='breadcrumb-item' aria-current='page'>
|
|
30
|
+
<span class='breadcrumb-link active'><%= title %></span>
|
|
31
|
+
</li>
|
|
32
|
+
<% end %>
|
|
33
|
+
<% end %>
|
|
34
|
+
</ol>
|
|
35
|
+
</nav>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<%# locals: (record:) -%>
|
|
2
|
+
<%# The record's own pages, worked out here rather than passed in, so a host's index
|
|
3
|
+
template of its own can put its page in this card by naming only the record. -%>
|
|
4
|
+
<%# Every bare action the routes drew under this record: a nested resource with no
|
|
5
|
+
index of its own is reached from nowhere, so its button sits beside the
|
|
6
|
+
breadcrumbs wherever the record is shown, on whichever of its pages that is. -%>
|
|
7
|
+
<% content_for :actions do %>
|
|
8
|
+
<% bare_action_buttons(record).each do |label, path, method| %>
|
|
9
|
+
<%= button_to label, path, method:, form_class: 'ms-3',
|
|
10
|
+
class: 'btn theme-primary btn-sm btn-solid' %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% end %>
|
|
13
|
+
<%# `card-header-tabs` is what pulls the row up into the header's padding, so the
|
|
14
|
+
active tab meets the body it belongs to instead of floating above it. -%>
|
|
15
|
+
<div class='card'>
|
|
16
|
+
<div class='card-header'>
|
|
17
|
+
<ul class='nav nav-tabs card-header-tabs'>
|
|
18
|
+
<% resource_tabs(record).each do |label, path, current| %>
|
|
19
|
+
<li class='nav-item'>
|
|
20
|
+
<%= link_to label, path, class: class_names('nav-link', active: current),
|
|
21
|
+
aria: { current: ('page' if current) } %>
|
|
22
|
+
</li>
|
|
23
|
+
<% end %>
|
|
24
|
+
</ul>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<%# `align-items-stretch`, since a card body is a column flex container that packs
|
|
28
|
+
its children to the start: a form of two fields would take the width of two
|
|
29
|
+
fields rather than the width of the card. -%>
|
|
30
|
+
<div class='card-body align-items-stretch'>
|
|
31
|
+
<%= yield %>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<%# locals: (color:, ink:) -%>
|
|
2
|
+
<%# Bootstrap's own nine `:root` declarations for the primary color, with the family
|
|
3
|
+
swapped — upstream's order and upstream's shapes, so a later Bootstrap can be
|
|
4
|
+
diffed against this block. The ninth is the label a solid fill carries, and it is
|
|
5
|
+
the one that names a color rather than a family, so it arrives already decided:
|
|
6
|
+
`white` or `gray-975`, whichever reads better on the 500 step. Both are shapes
|
|
7
|
+
upstream ships. A host overrides this partial to say it differently. -%>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bs-primary-base: var(--bs-<%= color %>-500);
|
|
11
|
+
--bs-primary-fg: light-dark(var(--bs-<%= color %>-600), var(--bs-<%= color %>-400));
|
|
12
|
+
--bs-primary-fg-emphasis: light-dark(var(--bs-<%= color %>-800), var(--bs-<%= color %>-200));
|
|
13
|
+
--bs-primary-bg: var(--bs-<%= color %>-500);
|
|
14
|
+
--bs-primary-bg-subtle: light-dark(var(--bs-<%= color %>-100), var(--bs-<%= color %>-900));
|
|
15
|
+
--bs-primary-bg-muted: light-dark(var(--bs-<%= color %>-200), var(--bs-<%= color %>-800));
|
|
16
|
+
--bs-primary-border: light-dark(var(--bs-<%= color %>-300), var(--bs-<%= color %>-600));
|
|
17
|
+
--bs-primary-focus-ring: light-dark(color-mix(in oklch, var(--bs-<%= color %>-500) 50%, var(--bs-bg-body)), color-mix(in oklch, var(--bs-<%= color %>-500) 75%, var(--bs-bg-body)));
|
|
18
|
+
--bs-primary-contrast: var(--bs-<%= ink %>);
|
|
19
|
+
}
|
|
20
|
+
</style>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<%# locals: (name:, id:, placeholder:, invalid: false, feedback: nil, described: nil, required: false, label: nil, counter: nil, recourses: nil, values: [], multiple: false, selected: [], aria_label: nil, small: false, all: nil, none: nil, shown: nil) -%>
|
|
2
|
+
<button class='form-control<%= ' form-control-sm' if small %><%= ' is-invalid' if invalid %> combobox-toggle' type='button' id='<%= id %>'
|
|
3
|
+
data-bs-toggle='combobox' data-bs-name='<%= name %>' data-controller='combobox'
|
|
4
|
+
data-combobox-more-value='<%= t 'recourse.more' %>'
|
|
5
|
+
data-bs-placeholder='<%= placeholder %>' data-bs-search='true'
|
|
6
|
+
aria-required='<%= required %>' <%= tag.attributes 'data-bs-multiple': ('true' if multiple), 'aria-label': aria_label, 'aria-describedby': described %>>
|
|
7
|
+
<span class='combobox-value'><%= placeholder %></span>
|
|
8
|
+
<i class='bi bi-chevron-down combobox-caret'></i>
|
|
9
|
+
</button>
|
|
10
|
+
<div class='menu'>
|
|
11
|
+
<div class='combobox-search' data-controller='clear'>
|
|
12
|
+
<input type='text' class='form-control combobox-search-input'
|
|
13
|
+
placeholder='<%= t 'recourse.search' %>'
|
|
14
|
+
autocomplete='off' aria-label='<%= t 'recourse.search' %>'
|
|
15
|
+
data-clear-target='input' data-action='input->clear#toggle'>
|
|
16
|
+
<button type='button' class='combobox-search-clear d-none' aria-label='<%= t 'recourse.clear' %>'
|
|
17
|
+
data-clear-target='button' data-action='clear#clear'>
|
|
18
|
+
<i class='bi bi-x-lg'></i>
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
<%# No `data-bs-value`, so the plugin's own click handler passes it by and this one
|
|
22
|
+
is ours; and never `.selected`, which it counts whatever the element is. -%>
|
|
23
|
+
<% if all -%>
|
|
24
|
+
<button type='button' class='menu-item' data-controller='deselect' data-action='deselect#all'
|
|
25
|
+
data-deselect-multiple-value='<%= multiple %>'><%= all %></button>
|
|
26
|
+
<div class='menu-divider'></div>
|
|
27
|
+
<% end -%>
|
|
28
|
+
<%# A value of its own, and an empty one: the plugin builds its hidden input from
|
|
29
|
+
whichever item is selected, so this is what sets an optional key back to
|
|
30
|
+
nothing. `deselect` cannot serve here — in a single menu the plugin selects
|
|
31
|
+
whatever is clicked rather than toggling it, so re-clicking the chosen item
|
|
32
|
+
would change nothing at all. -%>
|
|
33
|
+
<% if none -%>
|
|
34
|
+
<button type='button' class='menu-item' data-bs-value='' aria-selected='false'><span class='menu-item-content'><span><%= none %></span></span></button>
|
|
35
|
+
<div class='menu-divider'></div>
|
|
36
|
+
<% end -%>
|
|
37
|
+
<%# Records are cached on the relation, and a list of words is not worth caching:
|
|
38
|
+
an enum has a dozen of them and they are already in memory. Nor is a table with
|
|
39
|
+
no timestamp on it, which has no version for a key to carry. -%>
|
|
40
|
+
<% if recourses -%>
|
|
41
|
+
<% cache_if keepable_menu?(recourses), [recourses, multiple, selected] do %>
|
|
42
|
+
<% recourses.each do |recourse| %>
|
|
43
|
+
<% chosen = selected.include? recourse.id.to_s -%>
|
|
44
|
+
<%# In the menu but not on it: an option accounting for none of the rows being
|
|
45
|
+
filtered is `d-none` until `All` asks for it. One already ticked stays put,
|
|
46
|
+
or the box would name a filter its own menu does not offer. -%>
|
|
47
|
+
<% empty = counter && !chosen && recourse.attributes[counter].to_i.zero? -%>
|
|
48
|
+
<%# The name in a `.menu-item-content > span`, which is what the plugin copies into
|
|
49
|
+
the closed box: a count belongs in the menu and not on the box. Its wrapper
|
|
50
|
+
is `flex: 1`, so the count needs no margin of its own to sit at the right. -%>
|
|
51
|
+
<button class='menu-item<%= ' selected' if chosen %><%= ' d-none' if empty %>' type='button' data-bs-value='<%= recourse.id %>' aria-selected='<%= chosen %>'><span class='menu-item-content'><span><%= recourse.attributes[label] %></span></span><% if counter %><span class='recourse-count fg-2'><%= number_with_delimiter recourse.attributes[counter] %></span><% end %><% if multiple %><i class='bi bi-check menu-item-check'></i><% end %></button>
|
|
52
|
+
<% end %>
|
|
53
|
+
<% end %>
|
|
54
|
+
<% else -%>
|
|
55
|
+
<% values.each do |option| %>
|
|
56
|
+
<%# Compared as text, because that is what a request carries it back as. -%>
|
|
57
|
+
<% chosen = selected.include? option.to_s -%>
|
|
58
|
+
<%# In the menu but not on it, the way an unused filter option is: a list too long
|
|
59
|
+
to read opens on the few worth reading, and `All` asks for the rest. -%>
|
|
60
|
+
<% waiting = shown && !chosen && shown.exclude?(option.to_s) -%>
|
|
61
|
+
<button class='menu-item<%= ' selected' if chosen %><%= ' d-none' if waiting %>' type='button' data-bs-value='<%= option %>' aria-selected='<%= chosen %>'><span class='menu-item-content'><span><%= option %></span></span><% if multiple %><i class='bi bi-check menu-item-check'></i><% end %></button>
|
|
62
|
+
<% end %>
|
|
63
|
+
<% end -%>
|
|
64
|
+
<div class='combobox-no-results d-none'><%= t 'recourse.no_results' %></div>
|
|
65
|
+
</div>
|
|
66
|
+
<% if feedback -%>
|
|
67
|
+
<small class='invalid-feedback' id='<%= id %>_error'><%= feedback %></small>
|
|
68
|
+
<% end -%>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<%# The delete warning's dialog, empty until the confirm hook fills it: the wording
|
|
2
|
+
still travels in the button's `data-turbo-confirm`, and JavaScript is what moves
|
|
3
|
+
it here. `dialog-slide-down` is the animation, shipped by Bootstrap 6. -%>
|
|
4
|
+
<dialog class='dialog dialog-slide-down' id='recourse-confirm' aria-labelledby='recourse-confirm-title'>
|
|
5
|
+
<div class='dialog-header'>
|
|
6
|
+
<h1 class='dialog-title' id='recourse-confirm-title'></h1>
|
|
7
|
+
</div>
|
|
8
|
+
<div class='dialog-body'></div>
|
|
9
|
+
<div class='dialog-footer'>
|
|
10
|
+
<%# `autofocus` puts Enter on the safe answer: `showModal` focuses it first. -%>
|
|
11
|
+
<button type='button' class='btn btn-solid theme-secondary' data-bs-dismiss='dialog' autofocus><%= t 'recourse.cancel' %></button>
|
|
12
|
+
<button type='button' class='btn btn-solid theme-danger recourse-confirm-delete'><%= t 'recourse.deletion.delete' %></button>
|
|
13
|
+
</div>
|
|
14
|
+
</dialog>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<% if flash.any? -%>
|
|
2
|
+
<%# `data-turbo-temporary`: a toast born visible would otherwise replay from the
|
|
3
|
+
page snapshot on every Back, announcing a save that happened a page ago. -%>
|
|
4
|
+
<%# The row a write landed on rides here rather than on a toast: this exists once
|
|
5
|
+
per page, `hidden.bs.toast` bubbles up to it from whichever toast hides, and
|
|
6
|
+
`data-turbo-temporary` already takes the whole thing away on a Back. -%>
|
|
7
|
+
<div class='toast-container position-fixed bottom-0 end-0 p-3' data-turbo-temporary
|
|
8
|
+
<%= tag.attributes data: written_data %>>
|
|
9
|
+
<%# Every key here becomes a toast, whoever invented it — so the one carrying the
|
|
10
|
+
written row, which is data rather than a message, is taken out first. -%>
|
|
11
|
+
<% flash.to_hash.except(Recourse::WRITTEN).each do |key, message| -%>
|
|
12
|
+
<%# `fade show` from the server: visible at first paint, before any JavaScript.
|
|
13
|
+
The `toast` controller owns the timed hide, and hovering or focusing holds
|
|
14
|
+
the toast open while someone reads it or aims for the X. -%>
|
|
15
|
+
<div class='toast fade show <%= flash_theme key %>' role='alert' aria-live='assertive' aria-atomic='true'
|
|
16
|
+
data-controller='toast'
|
|
17
|
+
data-action='mouseenter->toast#stopTimer mouseleave->toast#startTimer focusin->toast#stopTimer focusout->toast#startTimer'>
|
|
18
|
+
<div class='toast-header border-0'>
|
|
19
|
+
<span class='me-auto'><%= message %></span>
|
|
20
|
+
<button type='button' class='btn-close' data-bs-dismiss='toast' aria-label='Close'></button>
|
|
21
|
+
</div>
|
|
22
|
+
<div class='toast-body d-none'></div>
|
|
23
|
+
</div>
|
|
24
|
+
<% end -%>
|
|
25
|
+
</div>
|
|
26
|
+
<% end -%>
|