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 +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +81 -95
- data/app/javascript/recourse/tooltip_controller.js +6 -1
- data/app/views/layouts/recourses.html.erb +80 -2
- data/app/views/recourses/_breadcrumb.html.erb +2 -2
- data/app/views/recourses/_footer.html.erb +3 -2
- data/app/views/recourses/_sidebar.html.erb +3 -3
- data/config/locales/recourse.en.yml +3 -3
- data/lib/recourse/helpers/cards.rb +3 -1
- data/lib/recourse/helpers/cells.rb +10 -1
- data/lib/recourse/helpers/counters.rb +3 -0
- data/lib/recourse/helpers/navigation.rb +11 -0
- data/lib/recourse/helpers/tabs.rb +18 -15
- data/lib/recourse/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 063f5551f5b89bbb95660522a2a0177b9570f96961c5d5e28cfa83877ece91bf
|
|
4
|
+
data.tar.gz: 9f778ea100e50837483382e814936c1fc0697e462151445b144b7472e115132b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
```
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
56
|
-
resource :session, only: :destroy, as: :exit
|
|
57
|
-
```
|
|
36
|
+
[image]
|
|
58
37
|
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
+
[image]
|
|
72
46
|
|
|
73
|
-
|
|
47
|
+
Every recourse with `destroy` gets a button with a detailed confirmation message:
|
|
74
48
|
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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
|
-
|
|
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 ?
|
|
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'><%=
|
|
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
|
-
|
|
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='
|
|
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='
|
|
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='
|
|
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='
|
|
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:
|
|
24
|
+
darken: Dark mode
|
|
25
25
|
delete: Delete %{model}
|
|
26
26
|
deleted: "%{model} was deleted."
|
|
27
|
-
exit:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
# `
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
lead =
|
|
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
|
|
55
|
+
[lead.presence, title].compact.join ' '
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
def tab_count(record, association)
|
data/lib/recourse/version.rb
CHANGED