recourse 4.2.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37d576acaaf1fd87766eb000852a30004862d606e02ea950577f76a3d4f8c8ee
4
- data.tar.gz: af168a49adf4d3df402ee1f6f093cefe3528dbcfb77727ada7b1270539006475
3
+ metadata.gz: 063f5551f5b89bbb95660522a2a0177b9570f96961c5d5e28cfa83877ece91bf
4
+ data.tar.gz: 9f778ea100e50837483382e814936c1fc0697e462151445b144b7472e115132b
5
5
  SHA512:
6
- metadata.gz: bf99a21586c91bb03722206dc18f289cc49338c659e6ef0c3ffeac37ede0aba147ac80f1da437df3ab21c0b59951fcb2ecf86e0d6c2b411e8c465a59a17b48c3
7
- data.tar.gz: 966f579457d4917bf4b9f17f2efe65bda9445b0e013d7cff33712fc07e027bfe6606cb0e78c45d2b11c26c42dfdcaf014c78e54ad3e9df8ca71c167b94cfef8c
6
+ metadata.gz: 8538e6114d90e004f0b2f895bb5a0431c3569ddb6c05e4c93e3618aaa5056d7b44d24e974a197f546a387bfde58922ade7a25f5697444fce60637ff60864ce0c
7
+ data.tar.gz: 1b7776b83766984e18861aade3f0f79938358c24cba295c4bedca84c8f21b0aaefb1bad095da258bbcead01ea9a2ac6a1d5a2598d155935335189482a78283a0
data/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ 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
+ ## 4.4.0 - 2026-09-08
9
+
10
+ * [Fix] Tapping the light/dark toggle or the log out on a phone leaves no tint behind
11
+ * [Feature] On a phone a resource's breadcrumb is its icon alone, leaving the row to the search box and the button
12
+ * [Feature] On a phone a card's tabs are their icons and figures: Show and Edit their
13
+ icons, a counted tab its icon and its number
14
+ * [Feature] On a phone the whole `Franchises: 21` of a stacked counter cell is the link
15
+ * [Fix] A touch screen shows no tooltips, a tap being what would open the link beneath one
16
+ * [Fix] Safari no longer zooms in on the search box or a filter: the controls are 16px on a phone, where the body itself is 14
17
+ * [Fix] A sidebar link tapped on a phone leaves no hover tint on it or on the link the next page puts under the finger
18
+
19
+ ## 4.3.0 - 2026-09-08
20
+
21
+ * [Feature] On a phone a stacked cell reads as one line, `Estimate hi: 600`
22
+ * [Feature] On a phone the sidebar's icons carry their words: `Dark mode`, `Light mode`, `Exit`
23
+ * [Fix] A page never scrolls sideways on a phone, and Safari no longer zooms out of it
24
+
25
+ Safari laid the stacked table's hidden header row out at its full width; the page
26
+ now clips what would run past the window.
27
+
28
+ * [Fix] Safari no longer zooms in on the search box or a filter when it is focused
29
+
30
+ The small controls were 14px, and Safari zooms on any field under 16; on a phone
31
+ they take the body's size.
32
+
33
+ * [Fix] The footer's sentence stands above the pages on a phone rather than beside them
34
+ * [Fix] A filter's menu drops over the first rows of the table rather than under them
35
+ * [Feature] A date or a time is headed `Created` and `Updated`, not `Created at` and `Updated at`
36
+ * [Fix] A touch screen gets no hover tint on a table's rows
37
+
8
38
  ## 4.2.0 - 2026-09-08
9
39
 
10
40
  * [Feature] The log-out button sits beside the light/dark toggle
data/README.md CHANGED
@@ -1,14 +1,6 @@
1
1
  # Recourse
2
2
 
3
- Admin screens for a Rails app, drawn from its models. One word in `config/routes.rb`
4
- serves a resource's seven actions — a paginated, searchable, sortable, filterable index
5
- table, a record's page, its form and its delete — with no controller, view or partial
6
- added to the app. What each screen shows is read off the model: its columns, its
7
- validators, its associations and its indexes.
8
-
9
- Everything a host may call is listed below with a link to its reference on
10
- [RubyDoc](https://rubydoc.info/gems/recourse). Every default can be overridden by
11
- defining the same thing in the app.
3
+ Swap two characters in your `config/routes.rb` and get awesome admin screens for your Rails app.
12
4
 
13
5
  ## How to install
14
6
 
@@ -26,80 +18,44 @@ breaking change. Rails 8.1 and Ruby 3.2 are the minimum; the pages need Turbo, w
26
18
  `turbo-rails` brings, and nothing else — Bootstrap and its icons are vendored and served
27
19
  by the engine, so a host without an asset pipeline gets the same screens.
28
20
 
29
- ## In `config/routes.rb`
21
+ ## Step 1. Edit your routes
30
22
 
31
- [`recourses`](https://rubydoc.info/gems/recourse/Recourse/Routes#recourses-instance_method)
32
- is `resources` with a controller and views supplied. It takes the same `only:` and
33
- `except:`, and a block nests children under the parent:
23
+ This is as easy as replacing any call to `resources` to use [`recourses`](https://rubydoc.info/gems/recourse/Recourse/Routes#recourses-instance_method) instead:
34
24
 
35
- ```ruby
36
- scope module: :admin do
37
- recourses :providers do
38
- recourses :markets, only: :index # a counted tab on each provider
39
- recourses :retrievals, only: :create # a button on each provider
40
- end
41
- recourses :searches, only: %i[index show new create]
25
+ ```diff
26
+ - resources :posts, only: %i[ index show ] do
27
+ + recourses :posts, only: %i[ index show ] do
28
+ - resources :comments, except: :destroy
29
+ + recourses :comments, except: :destroy
42
30
  end
43
31
  ```
44
32
 
45
- - Every resource with an `index` gets a sidebar entry, in routes order, with a keyboard
46
- shortcut on its first letter. `only: []` declares a resource with no page of its own.
47
- - A nested `index` is a tab on the parent's card, counted where a `counter_cache` exists.
48
- - A nested `create` with no index is a button on the parent's page, named after the
49
- route. The host answers it in a controller of its own, or the gem creates the record.
50
- - Nested controllers are namespaced after the parent (`Admin::Providers::MarketsController`),
51
- and a nested index lists only the parent's rows.
52
- - Do you name a route `exit`? The sidebar ends with a log-out button submitting a
53
- `DELETE` to it:
33
+ Every recourse with `index` gets a sidebar entry (in routes order), a keyboard shortcut on its
34
+ first letter, and a paginated, searchable, sortable, filterable index table:
54
35
 
55
- ```ruby
56
- resource :session, only: :destroy, as: :exit
57
- ```
36
+ [image]
58
37
 
59
- ## In `app/controllers/recourses_controller.rb`
38
+ Every recourse with `show` gets a detail page where attributes are displayed with the appropriate
39
+ formatting, or masked if sensitive:
60
40
 
61
- Every controller the gem defines inherits
62
- [`RecoursesController`](https://rubydoc.info/gems/recourse/Recourse/BaseController),
63
- which the host defines to put behavior above every screen at once:
41
+ [image]
64
42
 
65
- ```ruby
66
- class RecoursesController < Recourse::BaseController
67
- before_action :authenticate!
68
- end
69
- ```
43
+ Every recourse with `new` or `edit` gets a form with appropriate browser formatting and validation:
70
44
 
71
- A controller the app already defines is left alone.
45
+ [image]
72
46
 
73
- ## In `config/initializers/recourse.rb`
47
+ Every recourse with `destroy` gets a button with a detailed confirmation message:
74
48
 
75
- - [`Recourse.color=`](https://rubydoc.info/gems/recourse/Recourse#color%3D-class_method):
76
- which Bootstrap family is primary on every page. Readers switch light and dark themselves.
77
- - [`Recourse.bookmarks=`](https://rubydoc.info/gems/recourse/Recourse#bookmarks%3D-class_method):
78
- a Proc answering the viewer's bookmark rows. Every table of a model with a `has_many`
79
- at that class then opens with a square to keep a row by, kept rows first.
49
+ [image]
80
50
 
81
- ```ruby
82
- Recourse.color = :orange
83
- Recourse.bookmarks = -> { Keepsake.where agent: Current.agent }
84
- ```
51
+ Every nested recourse gets namespaced after the parent:
85
52
 
86
- ## In a model
53
+ [image]
87
54
 
88
- Every Active Record model answers these; a model overrides one in a
89
- [`Recoursive`](https://rubydoc.info/gems/recourse/Recourse/Recoursive) concern of its own.
90
55
 
91
- ```ruby
92
- # app/models/market/recoursive.rb
93
- module Market::Recoursive extend ActiveSupport::Concern
94
- class_methods do
95
- def recourse_label = :slug # what a menu and a cell call one
96
- def recourse_hidden = %i[callback_url payload] # off every screen
97
- def recourse_displayed = :created_at # back on the table
98
- def recourse_order = { size: :desc } # how the index sorts
99
- def recourse_icon = :question # a Unicon concept
100
- end
101
- end
102
- ```
56
+ ## Step 2. Enhance your models
57
+
58
+ To change the content displayed in the `index` table of a model, override any of these class methods:
103
59
 
104
60
  | Hook | Default | Decides |
105
61
  | --- | --- | --- |
@@ -109,44 +65,74 @@ end
109
65
  | [`recourse_order`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_order-instance_method) | `:id` | the index's order, a Symbol or a Hash; rows with nothing in the column come last |
110
66
  | [`recourse_icon`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_icon-instance_method) | the model's name | the icon on the sidebar, the crumbs and the tabs |
111
67
 
112
- What needs no hook, being read off the model:
113
-
114
- - The index eager-loads every `belongs_to` the table names, sorts by whatever an index
115
- covers, searches through its indexed string columns and the labels behind its foreign
116
- keys, marks what it matched, and offers a filter menu per enum, boolean and `belongs_to`.
117
- - Saving a row refreshes every open index of it.
118
- - A form field per column, carrying the rules the validators state: a length is a
119
- `maxlength`, a format a `pattern`, a numericality a numeric keyboard.
120
- - An enum is a badge on a page and a menu on a form; a foreign key is a menu of the
121
- records it points at, or a field to type the label into where there are too many.
122
- - Values formatted by kind: delimited integers, money, phone numbers, dates and times in
123
- the reader's own time zone, a `<time>` saying how long ago, a URL as a link, a list as
124
- a count that opens.
125
- - An encrypted column stays off the table, arrives masked on the record's page behind a
126
- `Show`, and is edited in the clear.
127
- - The delete button names what goes with the record, counted one association down.
128
-
129
- ## In `app/views`
130
-
131
- Do you want your own cells? Add `app/views/<resources>/_row.html.erb` and build it from
132
- [`column`](https://rubydoc.info/gems/recourse/Recourse/Helpers/Cells#column-instance_method),
68
+ For instance, this would yield a more compact `index` than the default configuration:
69
+
70
+ ```ruby
71
+ class Post < ActiveRecord::Base
72
+ def self.recourse_label = :title # Label a post with its title
73
+ def self.recourse_hidden = %i[ip_address] # Hide the IP address from the views
74
+ def self.recourse_displayed = :created_at # Display the created_at in the views
75
+ def self.recourse_order = { published_at: :desc } # Sort posts by last published first
76
+ def self.recourse_icon = :question # Represent Posts with a question icon
77
+ end
78
+ ````
79
+
80
+ [image]
81
+
82
+ ## Step 3. Delete your views
83
+
84
+ If you are happy with the generated HTML files, then be happy to skip this step!
85
+ If you want even more configuration, then just add a `app/views/<resources>/_row.html.erb` partial.
86
+ You can use [`column`](https://rubydoc.info/gems/recourse/Recourse/Helpers/Cells#column-instance_method),
133
87
  [`sort_header`](https://rubydoc.info/gems/recourse/Recourse/Helpers/Sorts#sort_header-instance_method)
134
- and [`search_highlight`](https://rubydoc.info/gems/recourse/Recourse/Helpers/Searches#search_highlight-instance_method):
88
+ and [`search_highlight`](https://rubydoc.info/gems/recourse/Recourse/Helpers/Searches#search_highlight-instance_method)
89
+ to define the columns you want:
135
90
 
136
91
  ```erb
137
92
  <%# app/views/admin/providers/_row.html.erb %>
138
93
  <%# locals: (provider:) -%>
94
+
139
95
  <%= column header: sort_header(:name) do %>
140
96
  <%= search_highlight provider.name, :name %>
141
97
  <% end %>
142
- <%= column header: 'Phone' do %><%= number_to_phone provider.phone %><% end %>
98
+
99
+ <%= column header: 'Phone' do %>
100
+ <%= number_to_phone provider.phone %>
101
+ <% end %>
102
+ ```
103
+
104
+ [image]
105
+
106
+ ## Step 4. Secure your controllers
107
+
108
+ Every controller the gem defines inherits
109
+ [`RecoursesController`](https://rubydoc.info/gems/recourse/Recourse/BaseController),
110
+ which the host defines to put behavior above every screen at once:
111
+
112
+ ```ruby
113
+ class RecoursesController < Recourse::BaseController
114
+ before_action :authenticate!
115
+ end
143
116
  ```
144
117
 
145
- Any template or partial of the gem's is replaced by defining it in the app, under the
146
- resource (`app/views/admin/providers/index.html.erb`) or for every resource at once
147
- (`app/views/recourses/_sidebar.html.erb`). Every string the gem shows is a key under
148
- `recourse` in `config/locales/recourse.en.yml`, so a host rewords `Add provider` in a
149
- locale file of its own.
118
+ A controller the app already defines is left alone.
119
+
120
+
121
+ ## Step 5. Enjoy the extras
122
+
123
+ Here are some bonus features the gem provides. Customize them by editing their values in
124
+ your `config/initializers/recourse.rb` file:
125
+
126
+ - [`Recourse.color=`](https://rubydoc.info/gems/recourse/Recourse#color%3D-class_method):
127
+ which Bootstrap family is primary on every page. Readers switch light and dark themselves.
128
+ - [`Recourse.bookmarks=`](https://rubydoc.info/gems/recourse/Recourse#bookmarks%3D-class_method):
129
+ a Proc answering the viewer's bookmark rows. Every table of a model with a `has_many`
130
+ at that class then opens with a square to keep a row by, kept rows first.
131
+
132
+ ```ruby
133
+ Recourse.color = :orange
134
+ Recourse.bookmarks = -> { Keepsake.where agent: Current.agent }
135
+ ```
150
136
 
151
137
  ## Development
152
138
 
@@ -5,12 +5,17 @@ import { Tooltip } from '/recourse/bootstrap.bundle.min.js'
5
5
  // and a Stimulus lifecycle is what survives Turbo redrawing the table: connect
6
6
  // makes it, disconnect takes it down before the element goes, so a sorted or
7
7
  // searched table never strands one over an element that left.
8
+ //
9
+ // None where nothing can hover: on a touch screen the tap that would open a link
10
+ // is also what shows a tooltip, and a word popping up over a tap reads as a hitch.
8
11
  export default class extends Controller {
9
12
  connect() {
13
+ if (matchMedia('(hover: none)').matches) return
14
+
10
15
  this.tooltip = Tooltip.getOrCreateInstance(this.element)
11
16
  }
12
17
 
13
18
  disconnect() {
14
- this.tooltip.dispose()
19
+ this.tooltip?.dispose()
15
20
  }
16
21
  }
@@ -101,6 +101,27 @@
101
101
  --bs-breadcrumb-link-hover-color: var(--bs-breadcrumb-link-active-color);
102
102
  }
103
103
 
104
+ /* Nothing here is meant to be wider than the window: the one thing that is, a
105
+ table, scrolls inside its own wrapper. Safari lays a stacked table's hidden
106
+ header row out at its full width all the same, and a page that can be
107
+ scrolled to it is a page Safari zooms out of and draws the navbar short on. */
108
+ html, .recourse-shell { overflow-x: hidden; }
109
+
110
+ /* A finger does not hover. The tint a pointer leaves on the row under it, or on
111
+ the link under it, would stay on whatever was last touched — or land on whatever
112
+ the redrawn page put under the finger — so a touch screen gets none. */
113
+ @media (hover: none) {
114
+ .table-hover > tbody > tr:hover > * {
115
+ --bs-table-color-state: initial;
116
+ --bs-table-bg-state: initial;
117
+ }
118
+ .nav-link:hover:not(.active) { background-color: transparent; }
119
+ }
120
+
121
+ /* The navbar and the sidebar stack over the main column: a menu opened from a
122
+ filter or a link's tooltip drops over the first rows rather than under them. */
123
+ .recourse-shell > nav, .recourse-sidebar { position: relative; z-index: 2; }
124
+
104
125
  /* An action column is as wide as a row is tall — an icon in a square — and a
105
126
  counter column starts at the same square, widening only when a figure like
106
127
  38,405 needs the room: a table laying out to 100% reads the width as a
@@ -116,6 +137,30 @@
116
137
  }
117
138
  }
118
139
 
140
+ /* Stacked, a cell reads as one line — `Estimate hi: 600`, the heading semibold
141
+ and the value after it — where Bootstrap puts the heading on a line of its own
142
+ and leaves the row's first cell with no heading at all, bold in its place: a
143
+ status opening a row would be a bare badge. Every cell is headed here. And a
144
+ value with no space in it — an address, a token — breaks rather than widening
145
+ the row past the phone. */
146
+ @container (width < 576px) {
147
+ .sm\:table-stacked > tbody > tr > td[data-cell]::before {
148
+ content: attr(data-cell) ': ';
149
+ display: inline;
150
+ font-weight: var(--bs-font-weight-semibold);
151
+ }
152
+ .sm\:table-stacked > tbody > tr > td:first-child { font-weight: inherit; }
153
+ /* A counter's heading goes inside its link, so the whole `Franchises: 21` is
154
+ one thing to tap; the cell's own heading steps aside for it. */
155
+ .sm\:table-stacked > tbody > tr > td.recourse-counter::before { content: none; }
156
+ .sm\:table-stacked > tbody > tr > td.recourse-counter > [data-cell]::before {
157
+ content: attr(data-cell) ': ';
158
+ font-weight: var(--bs-font-weight-semibold);
159
+ }
160
+ .sm\:table-stacked > tbody > tr > td:not(:first-child) + td::before { margin-block-start: 0; }
161
+ .sm\:table-stacked > tbody > tr > td { overflow-wrap: anywhere; }
162
+ }
163
+
119
164
  /* A counter says what it counts in one of two ways, and the table's own width is
120
165
  what picks: the icon and a bare figure where the column is a square, the
121
166
  counted model's words where there is room to read them. Bootstrap's `xl`, so
@@ -224,6 +269,10 @@
224
269
  .recourse-search { max-width: none; }
225
270
  .recourse-search-box,
226
271
  .recourse-search .combobox-toggle { flex: 1 1 100%; }
272
+ /* Safari zooms in on a field whose text is under 16px, an absolute figure: the
273
+ small controls are 14px, and so is the body on a phone, so the controls say
274
+ 16px themselves and focusing one moves nothing. */
275
+ .recourse-search .form-control { font-size: 16px; }
227
276
  }
228
277
 
229
278
  /* Held Option marks the letter each sidebar link answers to. Marked rather than
@@ -343,8 +392,12 @@
343
392
  .recourse-foot { display: flex; }
344
393
  .recourse-foot > * { flex: 1 1 0; display: flex; justify-content: center; }
345
394
  .recourse-foot .nav-link { border-radius: var(--bs-radius-5); }
346
- /* A hand cursor alone says little on an icon: a tint under it says it is a control. */
347
- .recourse-foot .nav-link:hover { background-color: var(--bs-bg-1); color: var(--bs-fg-body); }
395
+ /* A hand cursor alone says little on an icon: a tint under it says it is a control.
396
+ Only where something can hover: a finger's tap leaves `:hover` set until the next
397
+ tap elsewhere, and a tint that stays reads as a button stuck down. */
398
+ @media (hover: hover) {
399
+ .recourse-foot .nav-link:hover { background-color: var(--bs-bg-1); color: var(--bs-fg-body); }
400
+ }
348
401
 
349
402
  /* The foot sits at the bottom of the sidebar and stretches across it while the
350
403
  sidebar is a column, and is simply the last item of the row while it is not
@@ -376,6 +429,31 @@
376
429
  }
377
430
  }
378
431
 
432
+ /* A phone's card has room for a row of tabs only as pictures and figures: Show
433
+ and Edit are their icons, a counted tab its icon and its number. A tab with
434
+ neither keeps its word, there being nothing else to stand for it. */
435
+ @media (width < 768px) {
436
+ .recourse-tab-word { display: none; }
437
+ }
438
+
439
+ /* And the trail at the top: a resource's crumb is its icon alone on a phone, so the
440
+ search box and the button beside it have the row. A record's crumb has no icon
441
+ and keeps its words. */
442
+ @media (width < 768px) {
443
+ .recourse-crumb-word { display: none; }
444
+ }
445
+
446
+ /* While the sidebar is a row its foot has room for words, so each icon is followed
447
+ by its own; as a column the words are read but not seen, the way Bootstrap's
448
+ `.visually-hidden` does it. */
449
+ .recourse-foot-word { margin-inline-start: .4em; }
450
+ @media (width >= 768px) {
451
+ .recourse-foot-word {
452
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
453
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
454
+ }
455
+ }
456
+
379
457
  /* The toggle draws both icons and shows the one that names where a click
380
458
  would go: a moon on a light page, a sun on a dark one. Three states rather than
381
459
  two, because a page nobody has chosen for carries no attribute at all, and the
@@ -9,7 +9,7 @@
9
9
  <%# A crumb naming a record — the parent a nested page is under — links
10
10
  without an icon: the crumb before it already carries the resource's. -%>
11
11
  <li class='breadcrumb-item'>
12
- <%= link_to resource ? resource_label(resource, title) : title, path,
12
+ <%= link_to resource ? crumb_label(resource, title) : title, path,
13
13
  class: class_names('breadcrumb-link', 'gap-2': resource) %>
14
14
  </li>
15
15
  <% elsif index < crumbs.size - 1 %>
@@ -21,7 +21,7 @@
21
21
  </li>
22
22
  <% elsif resource %>
23
23
  <li class='breadcrumb-item' aria-current='page'>
24
- <span class='breadcrumb-link active gap-2'><%= resource_label resource, title %></span>
24
+ <span class='breadcrumb-link active gap-2'><%= crumb_label resource, title %></span>
25
25
  </li>
26
26
  <% else %>
27
27
  <%# A leaf names a page rather than a resource — `New market`, or a record by
@@ -1,6 +1,7 @@
1
1
  <%# locals: (pagy:) %>
2
2
  <% if pagy %>
3
- <div class='d-flex align-items-center' <%= tag.attributes data: limit_data(pagy) %>>
3
+ <%# A column on a phone, the sentence above the pages, and one row from `sm` up. -%>
4
+ <div class='d-flex flex-column sm:flex-row sm:align-items-center' <%= tag.attributes data: limit_data(pagy) %>>
4
5
  <%# The gem's own sentence from pagy's public readers, every figure delimited —
5
6
  rewriting pagy's English with a regex delimited one figure, only in English.
6
7
  The switch follows it after a dot: how much of the table is showing, then the
@@ -17,6 +18,6 @@
17
18
  <%= t 'recourse.displaying', count: pagy.count %>
18
19
  <% end %>
19
20
  </div>
20
- <div class='pe-2'><%== pagy.series_nav(:bootstrap, classes: 'pagination mb-0') if pagy.last > 1 %></div>
21
+ <div class='px-2 mt-2 sm:mt-0'><%== pagy.series_nav(:bootstrap, classes: 'pagination mb-0') if pagy.last > 1 %></div>
21
22
  </div>
22
23
  <% end %>
@@ -24,11 +24,11 @@
24
24
  data: scheme_data do %>
25
25
  <span class='recourse-scheme-light'>
26
26
  <i class='bi bi-moon-fill'></i>
27
- <span class='visually-hidden'><%= t 'recourse.darken' %></span>
27
+ <span class='recourse-foot-word'><%= t 'recourse.darken' %></span>
28
28
  </span>
29
29
  <span class='recourse-scheme-dark'>
30
30
  <i class='bi bi-sun-fill'></i>
31
- <span class='visually-hidden'><%= t 'recourse.lighten' %></span>
31
+ <span class='recourse-foot-word'><%= t 'recourse.lighten' %></span>
32
32
  </span>
33
33
  <% end %>
34
34
  </div>
@@ -38,7 +38,7 @@
38
38
  <%= button_to exit_path, method: :delete, class: 'nav-link border-0 bg-transparent',
39
39
  form: { data: { turbo: false } } do %>
40
40
  <i class='bi bi-box-arrow-right'></i>
41
- <span class='visually-hidden'><%= t 'recourse.exit' %></span>
41
+ <span class='recourse-foot-word'><%= t 'recourse.exit' %></span>
42
42
  <% end %>
43
43
  <% end %>
44
44
  </li>
@@ -21,10 +21,10 @@ en:
21
21
  create: Create
22
22
  created: "%{model} was created."
23
23
  created_error: "%{model} could not be created."
24
- darken: Darken the page
24
+ darken: Dark mode
25
25
  delete: Delete %{model}
26
26
  deleted: "%{model} was deleted."
27
- exit: Log out
27
+ exit: Exit
28
28
  displaying:
29
29
  one: Displaying 1 item
30
30
  other: Displaying %{count} items
@@ -37,7 +37,7 @@ en:
37
37
  items:
38
38
  one: 1 item
39
39
  other: "%{count} items"
40
- lighten: Lighten the page
40
+ lighten: Light mode
41
41
  missing_model: You declared `recourses :%{name}` in your routes file, but this app
42
42
  has no %{model} model.
43
43
  nested: You nested `recourses %{names}` inside `resources :%{parent}`, which does
@@ -60,7 +60,9 @@ module Recourse
60
60
  end
61
61
 
62
62
  def tab_label(action)
63
- safe_join [icon_tag(Actions::ICONS[action]), t("recourse.#{action}")], ' '
63
+ word = tag.span t("recourse.#{action}"), class: 'recourse-tab-word'
64
+
65
+ safe_join [icon_tag(Actions::ICONS[action]), word], ' '
64
66
  end
65
67
  end
66
68
  end
@@ -59,11 +59,20 @@ module Recourse
59
59
  # column holds a number and the heading says what the number is of.
60
60
  def resource_column_title(column)
61
61
  counted = resource_model.recourse_counters[column]
62
- return resource_model.human_attribute_name column unless counted
62
+ return dated_title column unless counted
63
63
 
64
64
  Recourse.model_title counted.klass
65
65
  end
66
66
 
67
+ # The model's own name for a column, less the `at` or `on` a date's name ends in:
68
+ # `Created`, not `Created at`, since when it happened is what the column holds.
69
+ def dated_title(column)
70
+ title = resource_model.human_attribute_name column
71
+ dated = column.end_with?('_at', '_on') && Columns::DATE_KINDS.include?(attribute_type(column))
72
+
73
+ dated ? title.sub(/ (at|on)\z/, '') : title
74
+ end
75
+
67
76
  # Value for one cell, formatted according to what the column holds.
68
77
  def resource_cell(resource, column)
69
78
  association = belongs_to_association column
@@ -21,6 +21,9 @@ module Recourse
21
21
  count = number_with_delimiter value
22
22
  counted = counter_counted count, value, association
23
23
  named = counter_naming count, association
24
+ # The heading again, for a stacked table to draw inside the link rather than
25
+ # before it: `Franchises: 21` is then one thing to tap.
26
+ named[:data] = { cell: Recourse.model_title(association.klass) }
24
27
  path = resource_controller_path
25
28
  nested = nested_path_of path, association
26
29
  return tag.span(counted, **named) unless nested && routed?(nested, 'index')
@@ -63,6 +63,17 @@ module Recourse
63
63
  safe_join [icon && tag.i(class: "bi bi-#{icon}"), name].compact, ' '
64
64
  end
65
65
 
66
+ # The same for a crumb, whose words a phone drops where the icon stands for them:
67
+ # the row at the top has a search box and a button to fit beside the trail.
68
+ def crumb_label(resource, title)
69
+ icon = Recourse.known_icon resource
70
+ return title unless icon
71
+
72
+ word = tag.span title, class: 'recourse-crumb-word'
73
+
74
+ safe_join [tag.i(class: "bi bi-#{icon}"), word], ' '
75
+ end
76
+
66
77
  # Only a page beneath the index names itself, and names what it is showing.
67
78
  def breadcrumb_leaf
68
79
  case controller.action_name
@@ -29,27 +29,30 @@ module Recourse
29
29
  [lead.presence&.upcase_first, title].compact.join ' '
30
30
  end
31
31
 
32
- # The icon is the counted model's own, whatever leads the words beside it.
32
+ # The icon is the counted model's own, whatever leads the words beside it. The
33
+ # words are wrapped where a picture or a figure stands beside them, so a phone,
34
+ # which has room for neither, keeps the picture and the figure and drops the words.
33
35
  def association_tab_label(record, association, namespace)
34
36
  icon = Recourse.known_model_icon association.klass
37
+ count = tab_count record, association
38
+ words = tab_words association, namespace, count
39
+ words = tag.span words, class: 'recourse-tab-word' if icon || count
35
40
 
36
- words = [icon && tag.i(class: "bi bi-#{icon}"), tab_name(record, association, namespace)]
37
-
38
- safe_join words.compact, ' '
41
+ safe_join [icon && tag.i(class: "bi bi-#{icon}"), count, words].compact, ' '
39
42
  end
40
43
 
41
- # `8 ZIPs` where the record keeps a count read off the record itself, no
42
- # query, like the column and the bare `ZIPs` where it keeps none. A namespace
43
- # the routes drew leads either of those, so two nestings of one model read
44
- # apart: `10 visited places` beside `4 booked places`, `Visited places` beside
45
- # `Booked places`. Whatever leads earns the downcase, and a title that leads
46
- # keeps its capital — which is what puts these beside Show and Edit.
47
- def tab_name(record, association, namespace)
48
- count = tab_count record, association
49
- lead = [count, namespace_words(namespace)].compact_blank.join ' '
50
- title = Recourse.model_title association.klass, count: count, lower: lead.present?
44
+ # `ZIPs` where the record keeps no count, and `places` after the `8` where it
45
+ # does read off the record itself, no query, like the column. A namespace the
46
+ # routes drew leads either, so two nestings of one model read apart: `10 visited
47
+ # places` beside `4 booked places`, `Visited places` beside `Booked places`.
48
+ # Whatever leads earns the downcase, and a title that leads keeps its capital —
49
+ # which is what puts these beside Show and Edit.
50
+ def tab_words(association, namespace, count)
51
+ lead = namespace_words namespace
52
+ lead = lead.upcase_first unless count
53
+ title = Recourse.model_title association.klass, count: count, lower: count || lead.present?
51
54
 
52
- [lead.presence&.upcase_first, title].compact.join ' '
55
+ [lead.presence, title].compact.join ' '
53
56
  end
54
57
 
55
58
  def tab_count(record, association)
@@ -1,4 +1,4 @@
1
1
  module Recourse
2
2
  # Version of the gem, read by the gemspec and by hosts checking compatibility.
3
- VERSION = '4.2.0'
3
+ VERSION = '4.4.0'
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recourse
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.0
4
+ version: 4.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - claudiob