madmin 2.6.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +107 -4
  3. data/app/assets/stylesheets/madmin/actiontext.css +31 -0
  4. data/app/assets/stylesheets/madmin/base.css +22 -11
  5. data/app/assets/stylesheets/madmin/buttons.css +9 -9
  6. data/app/assets/stylesheets/madmin/forms.css +263 -7
  7. data/app/assets/stylesheets/madmin/pagination.css +10 -23
  8. data/app/assets/stylesheets/madmin/reset.css +1 -1
  9. data/app/assets/stylesheets/madmin/sidebar.css +6 -6
  10. data/app/assets/stylesheets/madmin/tables.css +1 -1
  11. data/app/controllers/madmin/base_controller.rb +1 -5
  12. data/app/controllers/madmin/resource_controller.rb +38 -17
  13. data/app/helpers/madmin/application_helper.rb +15 -2
  14. data/app/helpers/madmin/filters_helper.rb +26 -0
  15. data/app/helpers/madmin/sort_helper.rb +1 -1
  16. data/app/javascript/madmin/controllers/filters_controller.js +35 -0
  17. data/app/javascript/madmin/controllers/nested_form_controller.js +5 -0
  18. data/app/javascript/madmin/controllers/select_controller.js +269 -22
  19. data/app/views/layouts/madmin/application.html.erb +1 -0
  20. data/app/views/madmin/application/_active_filters.html.erb +15 -0
  21. data/app/views/madmin/application/_filter.html.erb +13 -0
  22. data/app/views/madmin/application/_filters.html.erb +32 -0
  23. data/app/views/madmin/application/_javascript.html.erb +0 -2
  24. data/app/views/madmin/application/_pagination.html.erb +32 -0
  25. data/app/views/madmin/application/edit.html.erb +1 -1
  26. data/app/views/madmin/application/index.html.erb +13 -12
  27. data/app/views/madmin/application/new.html.erb +1 -1
  28. data/app/views/madmin/application/show.html.erb +1 -1
  29. data/app/views/madmin/fields/array/_form.html.erb +1 -0
  30. data/app/views/madmin/fields/array/_index.html.erb +1 -0
  31. data/app/views/madmin/fields/array/_show.html.erb +1 -0
  32. data/app/views/madmin/fields/attachment/_form.html.erb +0 -4
  33. data/app/views/madmin/fields/attachment/_show.html.erb +3 -1
  34. data/app/views/madmin/fields/attachments/_index.html.erb +1 -1
  35. data/app/views/madmin/fields/belongs_to/_form.html.erb +1 -1
  36. data/app/views/madmin/fields/belongs_to/_index.html.erb +5 -2
  37. data/app/views/madmin/fields/belongs_to/_show.html.erb +1 -1
  38. data/app/views/madmin/fields/enum/_form.html.erb +1 -1
  39. data/app/views/madmin/fields/has_many/_form.html.erb +1 -1
  40. data/app/views/madmin/fields/has_many/_index.html.erb +1 -1
  41. data/app/views/madmin/fields/has_many/_show.html.erb +3 -11
  42. data/app/views/madmin/fields/has_one/_index.html.erb +5 -1
  43. data/app/views/madmin/fields/has_one/_show.html.erb +1 -1
  44. data/app/views/madmin/fields/json/_form.html.erb +1 -1
  45. data/app/views/madmin/fields/json/_show.html.erb +1 -1
  46. data/app/views/madmin/fields/nested_has_many/_index.html.erb +1 -1
  47. data/app/views/madmin/fields/nested_has_many/_show.html.erb +3 -11
  48. data/app/views/madmin/fields/nested_has_one/_form.html.erb +18 -0
  49. data/app/views/madmin/fields/polymorphic/_form.html.erb +1 -2
  50. data/app/views/madmin/fields/polymorphic/_index.html.erb +5 -1
  51. data/app/views/madmin/fields/polymorphic/_show.html.erb +1 -1
  52. data/app/views/madmin/fields/select/_form.html.erb +1 -1
  53. data/config/importmap.rb +0 -1
  54. data/config/locales/el.yml +44 -0
  55. data/config/locales/en.yml +44 -0
  56. data/config/locales/fr.yml +44 -0
  57. data/config/locales/ko.yml +44 -0
  58. data/lib/madmin/field.rb +27 -1
  59. data/lib/madmin/fields/array.rb +16 -0
  60. data/lib/madmin/fields/attachment.rb +7 -0
  61. data/lib/madmin/fields/belongs_to.rb +2 -1
  62. data/lib/madmin/fields/boolean.rb +1 -0
  63. data/lib/madmin/fields/currency.rb +7 -0
  64. data/lib/madmin/fields/date.rb +1 -0
  65. data/lib/madmin/fields/date_time.rb +1 -0
  66. data/lib/madmin/fields/decimal.rb +1 -0
  67. data/lib/madmin/fields/float.rb +1 -0
  68. data/lib/madmin/fields/has_many.rb +6 -23
  69. data/lib/madmin/fields/integer.rb +1 -0
  70. data/lib/madmin/fields/json.rb +21 -0
  71. data/lib/madmin/fields/nested_has_many.rb +2 -2
  72. data/lib/madmin/fields/nested_has_one.rb +48 -0
  73. data/lib/madmin/fields/polymorphic.rb +5 -0
  74. data/lib/madmin/fields/string.rb +2 -0
  75. data/lib/madmin/fields/text.rb +2 -0
  76. data/lib/madmin/filter.rb +96 -0
  77. data/lib/madmin/menu.rb +2 -1
  78. data/lib/madmin/page.rb +61 -0
  79. data/lib/madmin/pagination.rb +50 -0
  80. data/lib/madmin/resource.rb +85 -7
  81. data/lib/madmin/search.rb +5 -7
  82. data/lib/madmin/version.rb +1 -1
  83. data/lib/madmin.rb +45 -2
  84. metadata +17 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35abef3e18e33da02efc68140fbbffe237a15b5054f99a6299265b1219181069
4
- data.tar.gz: 6cd25c564e51d1d663b9b8525caa793d6395e8534f831a02fdf41b36411b6161
3
+ metadata.gz: 502a8cbe4b6c86b1fa6f45d2f4d0f1f5b7852b48805a8d980cfb177ed4151c51
4
+ data.tar.gz: 41d8fbfd222f0758f81e2b90762d54ddbefd279ebd18b455ac2def6bd2a60e1e
5
5
  SHA512:
6
- metadata.gz: 62b04e88f49fbbb3c680c482e6c73bfa71710471c858e872f11d8eb5ba6491fb8c12607adadea92f64c8ed3f1b6da10afa616fa3e13114dbe0a2a5365d41c897
7
- data.tar.gz: 3e11e615a5720928b8997fa684549a2e67cc17e06d689a0856ba9d350d6037ccb63d6f6dd543e353712ce32616f8af09efbe66dfa545a91eae7cef98896cb6fe
6
+ metadata.gz: c390c0a27fcfe25bee8b1ab32f7d53644cc68cad9ee386f17adf810839a511212dfa7fe821cf275edd63a95d1dccbdbeffb22bad56f1ffa5db6c0e67e56d3a74
7
+ data.tar.gz: be7ec1e7fa71db380147c44f154e539e56719b35be488f7068473dfccfeeba44175abb58ee281bf4eb1089ab8a188eb5b66c0e5d46d847f639f1bbe15895107d
data/README.md CHANGED
@@ -7,11 +7,14 @@
7
7
  Why another Ruby on Rails admin? We wanted an admin that was:
8
8
 
9
9
  - Familiar and customizable like Rails scaffolds (less DSL)
10
- - Supports all the Rails features out of the box (ActionText, ActionMailbox, has_secure_password, etc)
11
- - Stimulus / Turbolinks / Hotwire ready
12
- - Works with Import maps and Sprockets
10
+ - Supports Rails features out of the box (Action Text, Active Storage, Action Mailbox, has_secure_password, etc)
11
+ - Built with Hotwire: Turbo, Stimulus and import maps, with no Node or build step
12
+ - Search, filters and scopes on index pages, and dark mode
13
+ - Works with Propshaft and Sprockets
13
14
 
14
- ![Madmin Screenshot](docs/images/screenshot.png)
15
+ ![Madmin index page with filters](docs/images/index.png)
16
+
17
+ ![Madmin edit form in dark mode, searching for a user](docs/images/edit-dark.png)
15
18
 
16
19
  ## Installation
17
20
 
@@ -72,6 +75,66 @@ end
72
75
 
73
76
  The new, edit, and delete links are hidden for read-only resources, and any write actions redirect back to the index.
74
77
 
78
+ ### Scopes
79
+
80
+ Declare scopes on a resource to render filter buttons on the index page. Each scope must be a scope or class method on the model:
81
+
82
+ ```ruby
83
+ class PostResource < Madmin::Resource
84
+ scope :published
85
+ scope :draft
86
+ end
87
+ ```
88
+
89
+ Button labels default to the humanized scope name. To customize or translate them, define an I18n key — `madmin.scopes.<param_key>.<scope>` for one resource, or `madmin.scopes.<scope>` shared across resources:
90
+
91
+ ```yaml
92
+ en:
93
+ madmin:
94
+ scopes:
95
+ post:
96
+ published: Live
97
+ ```
98
+
99
+ ### Filters
100
+
101
+ Index pages have a Filters button for narrowing records by their columns, like "Admin is true" or "Created at after Aug 15". Filters combine with search, scopes and sorting, and they're kept in the URL so filtered lists can be bookmarked and shared.
102
+
103
+ String, text, number, date, datetime and boolean columns can be filtered. Encrypted columns and attributes that aren't database columns are left out. Turn filtering off for an attribute with `filter: false`:
104
+
105
+ ```ruby
106
+ class UserResource < Madmin::Resource
107
+ attribute :api_token, filter: false
108
+ end
109
+ ```
110
+
111
+ Custom fields can offer a filter by returning `:string`, `:number`, `:date`, `:datetime` or `:boolean` from `self.filter_type`.
112
+
113
+ ### Resource Names
114
+
115
+ Resource names in the menu, headings and buttons come from the model's translation when it has one, so they follow Rails' own `activerecord.models` keys:
116
+
117
+ ```yaml
118
+ en:
119
+ activerecord:
120
+ models:
121
+ post:
122
+ one: Article
123
+ other: Articles
124
+ ```
125
+
126
+ Models without a translation keep their default names, like `Posts` and `Active Storage / Blobs`.
127
+
128
+ For labels that need logic (e.g. looking up a record name), override `scope_label`:
129
+
130
+ ```ruby
131
+ class PostResource < Madmin::Resource
132
+ def self.scope_label(name)
133
+ name.to_s.titleize
134
+ end
135
+ end
136
+ ```
137
+
75
138
  ## Configuring Views
76
139
 
77
140
  The views packaged within the gem are a great starting point, but inevitably people will need to be able to customize those views.
@@ -147,6 +210,25 @@ class BookResource < Madmin::Resource
147
210
  end
148
211
  ```
149
212
 
213
+ ### Nested Forms
214
+
215
+ Use `:nested_has_many` or `:nested_has_one` to edit associated records inside the parent's form. The model needs `accepts_nested_attributes_for`, with `allow_destroy: true` for the Remove link to work, and the associated model needs a resource. Use `skip` to leave out attributes of the associated resource:
216
+
217
+ ```ruby
218
+ class User < ApplicationRecord
219
+ has_many :posts
220
+ has_one :profile
221
+ accepts_nested_attributes_for :posts, :profile, allow_destroy: true
222
+ end
223
+
224
+ class UserResource < Madmin::Resource
225
+ attribute :posts, :nested_has_many, skip: %I[user]
226
+ attribute :profile, :nested_has_one, skip: %I[user]
227
+ end
228
+ ```
229
+
230
+ A nested has one shows its fields when the record exists, or an "Add" link when it doesn't, so saving the parent doesn't create an empty record.
231
+
150
232
  ## Custom Fields
151
233
 
152
234
  You can generate a custom field with:
@@ -250,6 +332,27 @@ Hooks registered after a class has already loaded run immediately, so ordering i
250
332
  You can use a couple of strategies to authenticate users who are trying to
251
333
  access your madmin panel: [Authentication Docs](docs/authentication.md)
252
334
 
335
+ ## Dark Mode
336
+
337
+ Madmin follows the system's light or dark setting. To always use one, add a stylesheet with `Madmin.stylesheets << "madmin_overrides"` (see [Assets](docs/assets.md)) and set `color-scheme`:
338
+
339
+ ```css
340
+ :root {
341
+ color-scheme: light;
342
+ }
343
+ ```
344
+
345
+ Madmin's colors are CSS variables on `:root`, defined with `light-dark()` so each has a light and a dark value. Custom fields and views can use them to match both modes, and you can override them to change the theme:
346
+
347
+ ```css
348
+ :root {
349
+ --primary-color: rgb(79 70 229); /* Button backgrounds */
350
+ --link-color: light-dark(rgb(79 70 229), rgb(129 140 248));
351
+ }
352
+ ```
353
+
354
+ The variables are `--primary-color`, `--link-color`, `--text-color`, `--light-text-color`, `--surface-color` (page and input backgrounds), `--background-color` (table headers), `--hover-color`, `--border-color`, `--input-border-color` and `--danger-color`.
355
+
253
356
  ## Assets
254
357
  You can customize the JavaScript and CSS assets used by Madmin for your application. To learn how
255
358
  see the [Assets Doc](docs/assets.md)
@@ -29,3 +29,34 @@
29
29
  padding: 0 !important;
30
30
  max-width: 100% !important;
31
31
  }
32
+
33
+ /* Trix draws its toolbar icons in black, so its buttons stay light */
34
+ trix-toolbar .trix-button-group {
35
+ background: light-dark(transparent, rgb(209 213 219));
36
+ }
37
+
38
+ trix-toolbar .trix-dialog {
39
+ background: var(--background-color);
40
+ }
41
+
42
+ /* Lexxy has no dark palette. `html:root` outranks Lexxy's own :root variables, which load after Madmin's */
43
+ html:root {
44
+ --lexxy-color-ink: light-dark(oklch(20% 0 0), var(--text-color));
45
+ --lexxy-color-ink-medium: light-dark(oklch(40% 0 0), rgb(209 213 219));
46
+ --lexxy-color-ink-light: light-dark(oklch(60% 0 0), var(--light-text-color));
47
+ --lexxy-color-ink-lighter: light-dark(oklch(85% 0 0), var(--border-color));
48
+ --lexxy-color-ink-lightest: light-dark(oklch(96% 0 0), var(--background-color));
49
+ --lexxy-color-ink-inverted: var(--surface-color);
50
+
51
+ --lexxy-color-accent-light: light-dark(oklch(88% 0.026 254), rgb(30 64 175));
52
+ --lexxy-color-accent-lightest: light-dark(oklch(92% 0.026 254), rgb(30 58 138));
53
+
54
+ --lexxy-color-code-token-att: light-dark(#d73a49, #f97583);
55
+ --lexxy-color-code-token-comment: light-dark(#6a737d, #959da5);
56
+ --lexxy-color-code-token-function: light-dark(#6f42c1, #b392f0);
57
+ --lexxy-color-code-token-operator: light-dark(#d73a49, #f97583);
58
+ --lexxy-color-code-token-property: light-dark(#005cc5, #79b8ff);
59
+ --lexxy-color-code-token-punctuation: light-dark(#24292e, #e1e4e8);
60
+ --lexxy-color-code-token-selector: light-dark(#22863a, #85e89d);
61
+ --lexxy-color-code-token-variable: light-dark(#e36209, #ffab70);
62
+ }
@@ -1,13 +1,23 @@
1
1
  :root {
2
+ /* Follows the system setting. Set `color-scheme: light` or `dark` on :root to force one */
3
+ color-scheme: light dark;
4
+
5
+ /* Button backgrounds, with white text */
2
6
  --primary-color: rgb(37 99 235);
3
- --border-color: rgb(229 231 235);
4
- --background-color: rgb(249 250 251);
5
- --text-color: rgb(2 6 23);
6
- --light-text-color: rgb(71 85 105);
7
+ --link-color: light-dark(rgb(37 99 235), rgb(96 165 250));
8
+ --border-color: light-dark(rgb(229 231 235), rgb(55 65 81));
9
+ --input-border-color: rgb(156 163 175);
10
+ --surface-color: light-dark(white, rgb(17 24 39));
11
+ --background-color: light-dark(rgb(249 250 251), rgb(31 41 55));
12
+ --hover-color: light-dark(rgb(243 244 246), rgb(31 41 55));
13
+ --text-color: light-dark(rgb(2 6 23), rgb(243 244 246));
14
+ --light-text-color: light-dark(rgb(71 85 105), rgb(156 163 175));
15
+ --danger-color: light-dark(rgb(239 68 68), rgb(248 113 113));
7
16
  --sidebar-width: 16rem;
8
17
  }
9
18
 
10
19
  body {
20
+ background-color: var(--surface-color);
11
21
  color: var(--text-color);
12
22
  font-size: 14px;
13
23
  -webkit-font-smoothing: antialiased;
@@ -19,7 +29,7 @@ body {
19
29
  }
20
30
 
21
31
  a {
22
- color: var(--primary-color);
32
+ color: var(--link-color);
23
33
  }
24
34
 
25
35
  .size-5 {
@@ -46,8 +56,8 @@ a {
46
56
  }
47
57
 
48
58
  &.alert-danger {
49
- background-color: oklch(.936 .032 17.717);
50
- color: oklch(.444 .177 26.899);
59
+ background-color: light-dark(oklch(.936 .032 17.717), oklch(.258 .092 26.042));
60
+ color: light-dark(oklch(.444 .177 26.899), oklch(.885 .062 18.334));
51
61
 
52
62
  svg {
53
63
  color: oklch(.637 .237 25.331);
@@ -55,13 +65,14 @@ a {
55
65
  }
56
66
 
57
67
  &.alert-notice {
58
- background-color: oklch(.962 .044 156.743);
59
- color: oklch(.448 .119 151.328);
68
+ background-color: light-dark(oklch(.962 .044 156.743), oklch(.266 .065 152.934));
69
+ color: light-dark(oklch(.448 .119 151.328), oklch(.925 .084 155.995));
60
70
 
61
71
  svg {
62
72
  color: oklch(.723 .219 149.579);
63
73
  }
64
74
  }
75
+
65
76
  }
66
77
 
67
78
  .search {
@@ -104,14 +115,14 @@ a {
104
115
 
105
116
 
106
117
  .metric {
107
- border: 1px solid rgb(229 231 235);
118
+ border: 1px solid var(--border-color);
108
119
  border-radius: 0.25rem;
109
120
  padding: 1rem;
110
121
  margin: 1rem;
111
122
  flex: 1 1 0%;
112
123
 
113
124
  h4 {
114
- color: rgb(75 85 99);
125
+ color: var(--light-text-color);
115
126
  font-weight: 600;
116
127
  margin-top: 0;
117
128
  margin-bottom: 0.5rem;
@@ -28,26 +28,26 @@
28
28
  }
29
29
 
30
30
  &.btn-secondary {
31
- background-color: white;
32
- color: rgb(31 41 55);
33
- box-shadow: inset 0 0 0 1px rgb(156 163 175);
31
+ background-color: var(--surface-color);
32
+ color: var(--text-color);
33
+ box-shadow: inset 0 0 0 1px var(--input-border-color);
34
34
 
35
35
  &:hover {
36
- background-color: rgb(243 244 246);
36
+ background-color: var(--hover-color);
37
37
  }
38
38
 
39
39
  &.active {
40
- background-color: rgb(243 244 246);
40
+ background-color: var(--hover-color);
41
41
  }
42
42
  }
43
43
 
44
44
  &.btn-danger {
45
- background-color: white;
46
- color: rgb(239 68 68);
47
- box-shadow: inset 0 0 0 1px rgb(239 68 68);
45
+ background-color: var(--surface-color);
46
+ color: var(--danger-color);
47
+ box-shadow: inset 0 0 0 1px var(--danger-color);
48
48
 
49
49
  &:hover {
50
- background-color: rgb(243 244 246);
50
+ background-color: var(--hover-color);
51
51
  }
52
52
  }
53
53
  }
@@ -1,5 +1,5 @@
1
1
  .error {
2
- color: rgb(239 68 68);
2
+ color: var(--danger-color);
3
3
  }
4
4
 
5
5
  .form-hint {
@@ -20,7 +20,7 @@
20
20
  }
21
21
 
22
22
  label {
23
- color: rgb(17 24 39);
23
+ color: var(--text-color);
24
24
  line-height: 1.5rem;
25
25
  font-weight: 500;
26
26
  font-size: 0.875rem;
@@ -62,10 +62,10 @@ select {
62
62
  -webkit-appearance: none;
63
63
  -moz-appearance: none;
64
64
  appearance: none;
65
- background-color: #fff;
65
+ background-color: var(--surface-color);
66
66
 
67
67
  border-radius: 0.375rem;
68
- box-shadow: inset 0 0 0 1px rgb(156 163 175);
68
+ box-shadow: inset 0 0 0 1px var(--input-border-color);
69
69
  line-height: 1.5rem;
70
70
  padding-bottom: 0.375rem;
71
71
  padding-left: 0.75rem;
@@ -73,18 +73,274 @@ select {
73
73
  padding-top: 0.375rem;
74
74
 
75
75
  &:focus {
76
- box-shadow: inset 0 0 0 1px rgb(var(--primary-color));
76
+ box-shadow: inset 0 0 0 1px var(--primary-color);
77
77
  }
78
78
  }
79
79
 
80
80
  .required {
81
- color: rgb(239 68 68);
81
+ color: var(--danger-color);
82
82
  font-weight: 600;
83
83
  }
84
84
 
85
85
  .nested-fields {
86
- border: 1px solid #e5e7eb;
86
+ border: 1px solid var(--border-color);
87
87
  border-radius: 0.5rem;
88
88
  padding: 1.25rem;
89
89
  margin-bottom: 1rem;
90
90
  }
91
+
92
+ /* Native selects lose their arrow with `appearance: none` */
93
+ select:not([multiple]) {
94
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%239ca3af' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
95
+ background-position: right 0.5rem center;
96
+ background-repeat: no-repeat;
97
+ background-size: 1.5em 1.5em;
98
+ padding-right: 2.5rem;
99
+ }
100
+
101
+ /* Combobox (select_controller.js) */
102
+ .combobox {
103
+ position: relative;
104
+ }
105
+
106
+ .combobox-control {
107
+ display: flex;
108
+ flex-wrap: wrap;
109
+ align-items: center;
110
+ gap: 0.25rem;
111
+ background-color: var(--surface-color);
112
+ border-radius: 0.375rem;
113
+ box-shadow: inset 0 0 0 1px var(--input-border-color);
114
+ padding: 0.375rem 0.75rem;
115
+ cursor: text;
116
+
117
+ &:focus-within {
118
+ box-shadow: inset 0 0 0 1px var(--primary-color);
119
+ }
120
+ }
121
+
122
+ .combobox .combobox-input,
123
+ .combobox .combobox-input:focus {
124
+ flex: 1;
125
+ min-width: 8rem;
126
+ background: transparent;
127
+ box-shadow: none;
128
+ outline: none;
129
+ padding: 0;
130
+ }
131
+
132
+ .combobox-chip {
133
+ display: inline-flex;
134
+ align-items: center;
135
+ gap: 0.25rem;
136
+ background-color: var(--hover-color);
137
+ border-radius: 0.25rem;
138
+ padding-left: 0.5rem;
139
+ font-size: 0.875rem;
140
+
141
+ button {
142
+ background: none;
143
+ border: none;
144
+ color: var(--light-text-color);
145
+ cursor: pointer;
146
+ padding: 0 0.5rem;
147
+
148
+ &:hover {
149
+ color: var(--text-color);
150
+ }
151
+ }
152
+ }
153
+
154
+ .combobox-listbox {
155
+ position: absolute;
156
+ z-index: 10;
157
+ top: 100%;
158
+ left: 0;
159
+ right: 0;
160
+ margin-top: 0.25rem;
161
+ max-height: 16rem;
162
+ overflow-y: auto;
163
+ list-style: none;
164
+ padding: 0.25rem 0;
165
+ background-color: var(--surface-color);
166
+ border: 1px solid var(--border-color);
167
+ border-radius: 0.375rem;
168
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
169
+
170
+ &[hidden] {
171
+ display: none;
172
+ }
173
+
174
+ li {
175
+ padding: 0.375rem 0.75rem;
176
+ cursor: pointer;
177
+ }
178
+
179
+ [aria-selected="true"] {
180
+ background-color: var(--hover-color);
181
+ }
182
+
183
+ .combobox-empty {
184
+ color: var(--light-text-color);
185
+ cursor: default;
186
+ }
187
+ }
188
+
189
+ /* Index filters (filters_controller.js) */
190
+ .filters-count {
191
+ background-color: var(--primary-color);
192
+ border-radius: 999px;
193
+ color: white;
194
+ display: inline-block;
195
+ font-size: 0.75rem;
196
+ line-height: 1.125rem;
197
+ margin-left: 0.25rem;
198
+ min-width: 1.125rem;
199
+ padding: 0 0.3rem;
200
+ text-align: center;
201
+ }
202
+
203
+ .filters {
204
+ background-color: var(--surface-color);
205
+ border: 1px solid var(--border-color);
206
+ border-radius: 0.5rem;
207
+ box-shadow: 0 10px 25px -5px rgb(0 0 0 / 0.2);
208
+ color: var(--text-color);
209
+ inset: auto;
210
+ margin: 0;
211
+ padding: 1rem;
212
+ right: 1rem;
213
+ top: 4.5rem;
214
+ width: min(44rem, calc(100vw - 2rem));
215
+ }
216
+
217
+ /* Place it under the button where anchor positioning is supported */
218
+ @supports (position-anchor: --filters) {
219
+ #filters-button {
220
+ anchor-name: --filters;
221
+ }
222
+
223
+ .filters {
224
+ position-anchor: --filters;
225
+ right: anchor(right);
226
+ top: calc(anchor(bottom) + 0.5rem);
227
+
228
+ /* Phones use the full width instead of lining up with the button */
229
+ @media (max-width: 767px) {
230
+ left: 1rem;
231
+ right: 1rem;
232
+ width: auto;
233
+ }
234
+ }
235
+ }
236
+
237
+ .filters-heading {
238
+ font-weight: 600;
239
+ margin: 0 0 0.75rem;
240
+ }
241
+
242
+ .filter-rows {
243
+ display: grid;
244
+ gap: 0.5rem;
245
+ }
246
+
247
+ .filter-row {
248
+ align-items: center;
249
+ display: grid;
250
+ gap: 0.5rem;
251
+ grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr) minmax(0, 1.2fr) auto;
252
+
253
+ select, input {
254
+ min-width: 0;
255
+ width: 100%;
256
+ }
257
+
258
+ .filter-remove {
259
+ grid-column: 4;
260
+ grid-row: 1;
261
+ }
262
+
263
+ @media (max-width: 767px) {
264
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
265
+
266
+ input {
267
+ grid-column: 1 / 3;
268
+ }
269
+
270
+ .filter-remove {
271
+ grid-column: 3;
272
+ }
273
+ }
274
+ }
275
+
276
+ .filter-remove {
277
+ background: none;
278
+ border: none;
279
+ border-radius: 0.375rem;
280
+ color: var(--light-text-color);
281
+ cursor: pointer;
282
+ font-size: 1.125rem;
283
+ height: 2rem;
284
+ width: 2rem;
285
+
286
+ &:hover {
287
+ background-color: var(--hover-color);
288
+ color: var(--danger-color);
289
+ }
290
+ }
291
+
292
+ .filters-footer {
293
+ align-items: center;
294
+ border-top: 1px solid var(--border-color);
295
+ display: flex;
296
+ flex-wrap: wrap;
297
+ gap: 0.5rem;
298
+ justify-content: space-between;
299
+ margin-top: 0.875rem;
300
+ padding-top: 0.875rem;
301
+ }
302
+
303
+ .btn.btn-link {
304
+ background: none;
305
+ color: var(--link-color);
306
+ font-weight: 500;
307
+ padding-left: 0.25rem;
308
+ padding-right: 0.25rem;
309
+ }
310
+
311
+ .active-filters {
312
+ align-items: center;
313
+ display: flex;
314
+ flex-wrap: wrap;
315
+ gap: 0.5rem;
316
+ margin-bottom: 1rem;
317
+ }
318
+
319
+ .filter-chip {
320
+ align-items: center;
321
+ background-color: light-dark(rgb(239 246 255), rgb(30 41 82));
322
+ border-radius: 999px;
323
+ color: light-dark(rgb(30 64 175), rgb(191 219 254));
324
+ display: inline-flex;
325
+ font-size: 0.8125rem;
326
+ font-weight: 500;
327
+ gap: 0.25rem;
328
+ padding: 0.125rem 0.25rem 0.125rem 0.75rem;
329
+
330
+ strong {
331
+ font-weight: 600;
332
+ }
333
+
334
+ a {
335
+ border-radius: 999px;
336
+ color: inherit;
337
+ line-height: 1.5rem;
338
+ text-align: center;
339
+ text-decoration: none;
340
+ width: 1.5rem;
341
+
342
+ &:hover {
343
+ background-color: var(--surface-color);
344
+ }
345
+ }
346
+ }
@@ -2,7 +2,7 @@
2
2
  padding: 0.5rem;
3
3
  }
4
4
 
5
- .pagy {
5
+ .pagination .pages {
6
6
  display: inline-flex;
7
7
  isolation: isolate;
8
8
  border-radius: 0.375rem;
@@ -17,21 +17,21 @@
17
17
  align-items: center;
18
18
  box-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
19
19
  --tw-ring-inset: inset;
20
- --tw-ring-color: #D1D5DB;
20
+ --tw-ring-color: var(--input-border-color);
21
21
  font-size: 0.875rem;
22
22
  line-height: 1.25rem;
23
- color: #111827;
24
- background-color: #ffffff;
23
+ color: var(--text-color);
24
+ background-color: var(--surface-color);
25
25
  border-radius: 0.25rem;
26
26
  text-decoration: none;
27
27
  }
28
28
 
29
29
  a:hover {
30
- background-color: #F3F4F6;
30
+ background-color: var(--hover-color);
31
31
  }
32
32
 
33
- a:not([href]) {
34
- color: #D1D5DB;
33
+ a[aria-disabled="true"] {
34
+ color: var(--input-border-color);
35
35
  cursor: default;
36
36
  }
37
37
 
@@ -39,21 +39,8 @@
39
39
  color: #ffffff;
40
40
  background-color: var(--primary-color);
41
41
  }
42
+ }
42
43
 
43
- label {
44
- display: inline-block;
45
- padding-top: 0.125rem;
46
- padding-bottom: 0.125rem;
47
- padding-left: 0.75rem;
48
- padding-right: 0.75rem;
49
- border-radius: 0.5rem;
50
- white-space: nowrap;
51
- background-color: #E5E7EB;
52
- }
53
-
54
- label input {
55
- border-radius: 0.375rem;
56
- border-style: none;
57
- background-color: #F3F4F6;
58
- }
44
+ .pagination-info {
45
+ margin-left: 0.5rem;
59
46
  }
@@ -2,7 +2,7 @@
2
2
  box-sizing: border-box;
3
3
  border-width: 0;
4
4
  border-style: solid;
5
- border-color: #e5e7eb;
5
+ border-color: var(--border-color, #e5e7eb);
6
6
  }
7
7
 
8
8
  body {