active_admin_theme 2.0.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.
- checksums.yaml +4 -4
- data/README.md +242 -9
- data/app/assets/javascripts/wigu/theme_toggle.js +140 -0
- data/app/assets/stylesheets/wigu/active_admin_theme.scss +1032 -165
- data/lib/active_admin_theme/version.rb +1 -1
- metadata +2 -6
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/active_admin_theme.gemspec +0 -24
- data/img/wigu.png +0 -0
- data/package.json +0 -28
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c393033738086398f691d3a9e754ece92f10ab6c647dd0357da38fc5167e4368
|
|
4
|
+
data.tar.gz: 6733dc12d9ceca058b15072be3826989334e106be7db5fd273017871e258ec41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c9c40332a46b50fd79f0d1959a2cb25c4ff23ac12be1dce88b721cfcafcbc798b9971f6cb17a4ee088985a08fdac8e1cba55275579bee8ed892eb50e23a051c
|
|
7
|
+
data.tar.gz: e1973bc1eb334a1f14cb4dd46094b1c802f0074243ac89ff30aaea894b84a744a5cce42b517feb63cc083b429517fd851ac41dc391a686a9d60cb732ff170e18
|
data/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Or add manually to `package.json`:
|
|
|
35
35
|
|
|
36
36
|
```
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@activeadmin-plugins/active_admin_theme": "^
|
|
38
|
+
"@activeadmin-plugins/active_admin_theme": "^3.0.0"
|
|
39
39
|
}
|
|
40
40
|
```
|
|
41
41
|
and execute:
|
|
@@ -57,22 +57,255 @@ In your base stylesheet entry point `active_admin.scss` (as example), add line:
|
|
|
57
57
|
@import '@activeadmin-plugins/active_admin_theme';
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
## Customising
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
$
|
|
66
|
-
$
|
|
62
|
+
Set any of the variables below *above* the import line:
|
|
63
|
+
|
|
64
|
+
```scss
|
|
65
|
+
$skinMainFirstColor: #A5A7AA;
|
|
66
|
+
$skinMainSecondColor: #0066CC;
|
|
67
|
+
$skinBorderWindowColor: #B8BABE;
|
|
68
|
+
|
|
69
|
+
@import 'wigu/active_admin_theme';
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Variables are typed. A value of the wrong kind — `none` where a colour is
|
|
73
|
+
expected, or a length without its unit — fails the build with a message
|
|
74
|
+
naming the variable, instead of silently emitting CSS the browser discards.
|
|
75
|
+
|
|
76
|
+
### Dark mode
|
|
77
|
+
|
|
78
|
+
The theme follows the operating system via `prefers-color-scheme`, and can be
|
|
79
|
+
pinned per page with `data-theme="light"` or `data-theme="dark"` on `<html>`.
|
|
80
|
+
Every colour below that has a `…Dark` twin is what dark mode uses; each twin
|
|
81
|
+
defaults to its light counterpart unless noted, so a project that only sets
|
|
82
|
+
the light value keeps one consistent colour in both modes.
|
|
83
|
+
|
|
84
|
+
### A worked example
|
|
85
|
+
|
|
86
|
+
The defaults are the configuration this theme is run with in
|
|
87
|
+
[yeti-web](https://github.com/yeti-switch/yeti-web). To go back to the blue
|
|
88
|
+
header the theme shipped before:
|
|
89
|
+
|
|
90
|
+
```scss
|
|
91
|
+
$skinMenuPillColor: $skinMainSecondColor;
|
|
92
|
+
$skinMenuPanelColor: $skinMainSecondColor;
|
|
93
|
+
$skinMenuTextColor: #ffffff;
|
|
94
|
+
$skinMenuItemHoverColor: transparent;
|
|
95
|
+
$skinMenuItemHoverTextColor: #ffffff;
|
|
96
|
+
$skinMenuFontSize: 1em;
|
|
97
|
+
$skinHeaderPaddingY: 7px; // one value top and bottom
|
|
98
|
+
$skinTitleBarColor: lighten($skinMainFirstColor, 8%);
|
|
99
|
+
$skinTitleBarBorderWidth: 3px;
|
|
100
|
+
$skinPanelHeaderColor: $skinMainSecondColor;
|
|
101
|
+
$skinPanelHeaderTextColor: #ffffff;
|
|
102
|
+
$skinTabInactiveColor: $skinMainSecondColor;
|
|
103
|
+
$skinInactiveTabTextColor: #ffffff;
|
|
104
|
+
$skinLinkColor: $skinMainSecondColor;
|
|
67
105
|
|
|
68
106
|
@import 'wigu/active_admin_theme';
|
|
69
|
-
...
|
|
70
107
|
```
|
|
71
108
|
|
|
109
|
+
Note `var(--aa-page-bg)` works as a variable value: a custom property follows
|
|
110
|
+
the mode on its own, so one line covers both themes.
|
|
111
|
+
|
|
112
|
+
### Switching themes
|
|
113
|
+
|
|
114
|
+
The stylesheet follows the operating system on its own and honours
|
|
115
|
+
`data-theme="light"` or `data-theme="dark"` on `<html>`. A switch is optional
|
|
116
|
+
and ships with the gem:
|
|
117
|
+
|
|
118
|
+
```scss
|
|
119
|
+
// app/assets/javascripts/active_admin.js
|
|
120
|
+
//= require wigu/theme_toggle
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
// or, as an npm module
|
|
125
|
+
import "@activeadmin-plugins/active_admin_theme/src/theme_toggle";
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A gem cannot add a menu item: ActiveAdmin builds the utility navigation from
|
|
129
|
+
the host application's initializer, and nothing in a stylesheet or an asset
|
|
130
|
+
runs at that point. So with nothing else to do, the script injects its own
|
|
131
|
+
`li#theme_toggle` into `#utility_nav` on load. That works, but the item is
|
|
132
|
+
inserted before the server-rendered ones and is not yours to order or hide.
|
|
133
|
+
|
|
134
|
+
Declaring it yourself costs four lines and puts it under your control — this is
|
|
135
|
+
how [yeti-web](https://github.com/yeti-switch/yeti-web) does it:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
# config/initializers/active_admin.rb
|
|
139
|
+
config.namespace :admin do |admin|
|
|
140
|
+
admin.build_menu :utility_navigation do |menu|
|
|
141
|
+
# A real url, not "#": ActiveAdmin drops a blank utility item.
|
|
142
|
+
menu.add id: "theme_toggle", label: "", url: "#theme",
|
|
143
|
+
priority: 9_999_998, html_options: { role: "button" }
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The script finds `#theme_toggle` or anything carrying `.dark-mode-toggle`,
|
|
149
|
+
binds by delegation — so the control survives a re-render — and writes nothing
|
|
150
|
+
to the page but `data-mode`. Everything visible comes from the stylesheet.
|
|
151
|
+
|
|
152
|
+
[](./img/switch.png)
|
|
153
|
+
|
|
154
|
+
Below the two pages: the three states at rest — half circle for **auto**, sun
|
|
155
|
+
for **light**, moon for **dark** — then the last two hovered. A click moves to
|
|
156
|
+
the next state, so the control costs the width of one icon in a header that is
|
|
157
|
+
usually already full. The title says where that click goes, since one icon
|
|
158
|
+
cannot show both.
|
|
159
|
+
|
|
160
|
+
`auto` removes the attribute, so the media query decides and the page follows
|
|
161
|
+
the operating system live; the other two pin the choice in `localStorage` under
|
|
162
|
+
`aa-theme`. The third state is there so that following the system stays
|
|
163
|
+
reachable: a two-state toggle writes a preference on the first click and has no
|
|
164
|
+
way back short of clearing storage by hand.
|
|
165
|
+
|
|
166
|
+
The glyphs are inline SVG used as a CSS `mask`, so the gem still ships no image
|
|
167
|
+
files, there is nothing for a host application's CSP to allow, and the icon
|
|
168
|
+
takes `currentColor` — `$skinMenuTextColor` like the rest of the bar, and the
|
|
169
|
+
hover colour on hover. To use your own icon font instead, override
|
|
170
|
+
`$theme-icon-auto` / `$theme-icon-light` / `$theme-icon-dark`, or restyle
|
|
171
|
+
`#theme_toggle > a:before` outright.
|
|
172
|
+
|
|
173
|
+
The control is a link with no destination, so <kbd>Tab</kbd> reaches it and
|
|
174
|
+
<kbd>Enter</kbd> and <kbd>Space</kbd> operate it. Changing the theme in one tab
|
|
175
|
+
applies it in the others.
|
|
176
|
+
|
|
177
|
+
### Upgrading
|
|
178
|
+
|
|
179
|
+
Two things changed shape in this release and are worth knowing if you already
|
|
180
|
+
set variables:
|
|
181
|
+
|
|
182
|
+
* Dropdown panels — the title-bar menu, the batch-actions menu and the
|
|
183
|
+
table-tools menus — now follow the surface palette (`$skinSurfaceColor` and
|
|
184
|
+
friends) instead of `$skinMainFirstColor` / `$skinMainSecondColor`. That is
|
|
185
|
+
what lets them work in both modes. If you branded those panels through the
|
|
186
|
+
two main colours, point `$skinSurfaceColor` and `$skinSurfaceHoverColor` at
|
|
187
|
+
the same values.
|
|
188
|
+
* The primary button fill is darker (`darken($skinMainSecondColor, 20%)` rather
|
|
189
|
+
than the accent itself). White on the accent is 2.74:1, under the 4.5:1 small
|
|
190
|
+
text needs, and dark mode was already using this tone — the button is now one
|
|
191
|
+
colour in both modes. Set `$skinButtonColor: $skinMainSecondColor;` for the
|
|
192
|
+
old look.
|
|
193
|
+
* The default content link colour is darker (`#1f5f8d` rather than the accent).
|
|
194
|
+
The accent is a fill colour and failed WCAG AA as body text. Set
|
|
195
|
+
`$skinLinkColor` back to `$skinMainSecondColor` if you prefer the old look.
|
|
196
|
+
|
|
197
|
+
### Variables
|
|
198
|
+
|
|
199
|
+
#### Core
|
|
200
|
+
|
|
201
|
+
| Variable | Default (light / dark) | |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| `$skinMainFirstColor` | `#23282f` | |
|
|
204
|
+
| `$skinMainSecondColor` | `#5ea3d3` | |
|
|
205
|
+
| `$skinBorderRadius` | `4px` | |
|
|
206
|
+
| `$skinBorderWindowColor` | `#e6e9ee` | |
|
|
207
|
+
| `$skinTablePadding` | `10px` | |
|
|
208
|
+
|
|
209
|
+
#### Surfaces, text and borders
|
|
210
|
+
|
|
211
|
+
| Variable | Default (light / dark) | |
|
|
212
|
+
|---|---|---|
|
|
213
|
+
| `$skinPageBgColor` / `$skinPageBgColorDark` | `#f7f9fb` / `#161a1e` | page background |
|
|
214
|
+
| `$skinSurfaceColor` / `$skinSurfaceColorDark` | `#ffffff` / `#25292f` | panels / cards / content |
|
|
215
|
+
| `$skinSurface2Color` / `$skinSurface2ColorDark` | `#f0f2f5` / `#2c3137` | table headers / striping / subtle fills |
|
|
216
|
+
| `$skinSurfaceHoverColor` / `$skinSurfaceHoverColorDark` | `#f5f7fa` / `#3f454d` | row / item hover |
|
|
217
|
+
| `$skinSelectedRowColor` / `$skinSelectedRowColorDark` | `#d9e4ec` / `#304457` | checked table row |
|
|
218
|
+
| `$skinElevatedColor` / `$skinElevatedColorDark` | `$skinSurfaceColor` / `#363c43` | tool buttons and dropdown panels floating above the page |
|
|
219
|
+
| `$skinTextColor` / `$skinTextColorDark` | `#323537` / `#dde2e8` | body text |
|
|
220
|
+
| `$skinTextMutedColor` / `$skinTextMutedColorDark` | `#6b7177` / `#b0b8c2` | secondary text / axis labels |
|
|
221
|
+
| `$skinBorderColor` / `$skinBorderColorDark` | `#e0e4e9` / `#404750` | borders / grid lines |
|
|
222
|
+
| `$skinInputBgColor` / `$skinInputBgColorDark` | `#ffffff` / `#1e2227` | form control background |
|
|
223
|
+
| `$skinInputBorderColor` / `$skinInputBorderColorDark` | `#c9ced4` / `#4d555f` | form control border |
|
|
224
|
+
|
|
225
|
+
#### Header menu
|
|
226
|
+
|
|
227
|
+
| Variable | Default (light / dark) | |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| `$skinMenuPillColor` | `#2e3236` | top-level current/hover pill |
|
|
230
|
+
| `$skinMenuPillTextColor` | `#6cb0de` | text on that pill; follows the dropdown text so a |
|
|
231
|
+
| `$skinMenuPanelColor` | `#2e3236` | dropdown panel bg + hover "bridge" border |
|
|
232
|
+
| `$skinMenuTextColor` | `#dfe2e6` | dropdown item text (was: inherited #fff) |
|
|
233
|
+
| `$skinMenuItemHoverColor` | `#3f454c` | dropdown item hover/current bg (was: none) |
|
|
234
|
+
| `$skinMenuItemHoverTextColor` | `#6cb0de` | hover/current dropdown item text, same reason |
|
|
235
|
+
| `$skinMenuFontSize` | `13px` | header menu text size |
|
|
236
|
+
| `$skinMenuItemPaddingY` | `5px` | dropdown item top/bottom padding (was 6px/4px + a 7px border) |
|
|
237
|
+
| `$skinMenuItemLineHeight` | `1.35` | dropdown item line-height |
|
|
238
|
+
| `$skinMenuPanelMaxWidth` | `260px` | dropdown panel ceiling; longer labels wrap instead of leaving the viewport |
|
|
239
|
+
| `$skinHeaderPaddingY` | `null` | sets both halves at once |
|
|
240
|
+
| `$skinHeaderPaddingTop` | `4.5px` | header top padding (base value, kept so the header does not shift) |
|
|
241
|
+
| `$skinHeaderPaddingBottom` | `4.5px` | header bottom padding |
|
|
242
|
+
| `$skinHeaderLogoMaxHeight` | `none` | cap the site_title logo image height |
|
|
243
|
+
|
|
244
|
+
#### Title bar
|
|
245
|
+
|
|
246
|
+
| Variable | Default (light / dark) | |
|
|
247
|
+
|---|---|---|
|
|
248
|
+
| `$skinTitleBarColor` | `#343c46` | |
|
|
249
|
+
| `$skinTitleBarBorderColor` | `$skinMainSecondColor` | |
|
|
250
|
+
| `$skinTitleBarBorderWidth` | `0` | |
|
|
251
|
+
| `$skinTitleBarButtonPaddingY` | `6px` | action button vertical padding |
|
|
252
|
+
| `$skinTitleBarButtonPaddingX` | `10px` | action button horizontal padding |
|
|
253
|
+
|
|
254
|
+
#### Panels, tabs and labels
|
|
255
|
+
|
|
256
|
+
| Variable | Default (light / dark) | |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| `$skinPanelHeaderColor` / `$skinPanelHeaderColorDark` | `var(--aa-page-bg)` / `var(--aa-page-bg)` | |
|
|
259
|
+
| `$skinPanelHeaderTextColor` / `$skinPanelHeaderTextColorDark` | `var(--aa-inactive-tab-text)` / `var(--aa-inactive-tab-text)` | |
|
|
260
|
+
| `$skinPanelHeaderPaddingY` | `5px` | panel + sidebar header height |
|
|
261
|
+
| `$skinLabelColor` / `$skinLabelColorDark` | `#8494a8` / `$skinTextColorDark` | |
|
|
262
|
+
| `$skinTabInactiveColor` / `$skinTabInactiveColorDark` | `#f7f9fb` / `#161a1e` | inactive tab fill |
|
|
263
|
+
| `$skinActiveTabTextColor` / `$skinActiveTabTextColorDark` | `$skinMainSecondColor` / `#7cc0ec` | selected tab label |
|
|
264
|
+
| `$skinInactiveTabTextColor` / `$skinInactiveTabTextColorDark` | `#5e6469` / `#b0b8c2` | inactive tab label |
|
|
265
|
+
| `$skinTableHeaderTextColor` / `$skinTableHeaderTextColorDark` | `#5e6469` / `#dde2e8` | index-table column header text |
|
|
266
|
+
| `$skinTabPaddingY` | `8px` | tab height |
|
|
267
|
+
| `$skinTabPaddingX` | `15px` | tab label horizontal padding (text → border) |
|
|
268
|
+
|
|
269
|
+
#### Buttons and table tools
|
|
270
|
+
|
|
271
|
+
| Variable | Default (light / dark) | |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| `$skinButtonColor` / `$skinButtonColorDark` | `darken($skinMainSecondColor, 20%)` / `$skinButtonColor` | one tone in both modes; white on it is 5.35:1 |
|
|
274
|
+
| `$skinButtonTextColor` / `$skinButtonTextColorDark` | `#ffffff` / `$skinButtonTextColor` | label on those buttons |
|
|
275
|
+
| `$skinTableToolsHeight` | `30px` | |
|
|
276
|
+
| `$skinTableToolsPaddingX` | `$skinTableToolsHeight * 0.4` | 12px at 30px |
|
|
277
|
+
|
|
278
|
+
#### Links
|
|
279
|
+
|
|
280
|
+
| Variable | Default (light / dark) | |
|
|
281
|
+
|---|---|---|
|
|
282
|
+
| `$skinAccentColor` / `$skinAccentColorDark` | `$skinMainSecondColor` / `$skinAccentColor` | focus ring / accent outline |
|
|
283
|
+
| `$skinLinkColor` / `$skinLinkColorDark` | `#38678b` / `#7cc0ec` | |
|
|
284
|
+
| `$skinDeleteLinkColor` / `$skinDeleteLinkColorDark` | `$skinLinkColor` / `#f49b9b` | |
|
|
285
|
+
|
|
72
286
|
## Screen
|
|
73
287
|
|
|
74
|
-
|
|
288
|
+
Index with filters, show page, nested `has_many` form, an open batch-actions
|
|
289
|
+
menu and the datepicker — the same admin in both modes. The theme follows the
|
|
290
|
+
operating system and can be pinned per page with `data-theme`.
|
|
291
|
+
|
|
292
|
+
#### Light
|
|
293
|
+
|
|
294
|
+
[](./img/light.png)
|
|
295
|
+
|
|
296
|
+
#### Dark
|
|
297
|
+
|
|
298
|
+
[](./img/dark.png)
|
|
299
|
+
|
|
300
|
+
#### Form and filter controls
|
|
301
|
+
|
|
302
|
+
Shown at full size, because the two shots above scale the controls down past
|
|
303
|
+
the point where you can tell what colour they are. Inputs, selects and
|
|
304
|
+
textareas take `$skinInputBgColor` / `$skinInputBorderColor` — white on
|
|
305
|
+
`#c9ced4` in light mode, a recessed `#1e2227` well on `#4d555f` in dark — and
|
|
306
|
+
the focused field (`Name`, `Title`) carries `$skinMainSecondColor`.
|
|
75
307
|
|
|
308
|
+
[](./img/inputs.png)
|
|
76
309
|
|
|
77
310
|
## Contributing
|
|
78
311
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Theme switch for active_admin_theme. Optional — the stylesheet works without
|
|
2
|
+
// it, following the operating system.
|
|
3
|
+
//
|
|
4
|
+
// Cycles auto -> light -> dark -> auto:
|
|
5
|
+
// auto no stored choice; follows prefers-color-scheme, live
|
|
6
|
+
// light/dark pins html[data-theme] and remembers it
|
|
7
|
+
//
|
|
8
|
+
// The only thing this writes to the page is data-mode on the control; the
|
|
9
|
+
// stylesheet draws the icon from it. That keeps appearance in the theme, where
|
|
10
|
+
// a project can restyle it, instead of in a script it would have to fork.
|
|
11
|
+
//
|
|
12
|
+
// Binds by delegation to anything carrying .dark-mode-toggle or #theme_toggle,
|
|
13
|
+
// so the control can live anywhere and survive a re-render. If neither exists
|
|
14
|
+
// it inserts its own entry into the utility navigation. No jQuery and no ujs,
|
|
15
|
+
// so it does not care how the admin is built.
|
|
16
|
+
(function () {
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
var KEY = "aa-theme";
|
|
20
|
+
var ORDER = { auto: "light", light: "dark", dark: "auto" };
|
|
21
|
+
var LABEL = {
|
|
22
|
+
auto: "Theme: auto (follows the system) — click for light",
|
|
23
|
+
light: "Theme: light — click for dark",
|
|
24
|
+
dark: "Theme: dark — click for auto",
|
|
25
|
+
};
|
|
26
|
+
var root = document.documentElement;
|
|
27
|
+
|
|
28
|
+
// localStorage throws in private mode in some browsers, and is absent in a few
|
|
29
|
+
// embedded webviews. Losing the preference is acceptable; breaking the admin
|
|
30
|
+
// is not.
|
|
31
|
+
function stored() {
|
|
32
|
+
try { return localStorage.getItem(KEY); } catch (e) { return null; }
|
|
33
|
+
}
|
|
34
|
+
function store(value) {
|
|
35
|
+
try { value === null ? localStorage.removeItem(KEY) : localStorage.setItem(KEY, value); } catch (e) {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The choice is held here and storage is best-effort persistence. Reading it
|
|
39
|
+
// back instead would make the switch a no-op wherever localStorage throws —
|
|
40
|
+
// private mode in some browsers — because the write is swallowed and the next
|
|
41
|
+
// read returns the old value.
|
|
42
|
+
var current = null;
|
|
43
|
+
|
|
44
|
+
function mode() {
|
|
45
|
+
if (current === null) current = normalise(stored());
|
|
46
|
+
return current;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalise(value) {
|
|
50
|
+
return value === "light" || value === "dark" ? value : "auto";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function apply() {
|
|
54
|
+
// auto drops the attribute entirely so the media query decides.
|
|
55
|
+
if (mode() === "auto") root.removeAttribute("data-theme");
|
|
56
|
+
else root.setAttribute("data-theme", mode());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Before DOMContentLoaded on purpose: applied later, the page paints in the
|
|
60
|
+
// other theme first and flashes.
|
|
61
|
+
apply();
|
|
62
|
+
|
|
63
|
+
function ready(fn) {
|
|
64
|
+
if (document.readyState !== "loading") fn();
|
|
65
|
+
else document.addEventListener("DOMContentLoaded", fn);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
var SELECTOR = ".dark-mode-toggle, #theme_toggle";
|
|
69
|
+
|
|
70
|
+
function controls() {
|
|
71
|
+
return document.querySelectorAll(SELECTOR);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function refresh() {
|
|
75
|
+
var active = mode();
|
|
76
|
+
Array.prototype.forEach.call(controls(), function (host) {
|
|
77
|
+
// The host itself may be the anchor (a menu item) or wrap one (our own li).
|
|
78
|
+
var link = host.tagName === "A" ? host : host.querySelector("a") || host;
|
|
79
|
+
host.setAttribute("data-mode", active);
|
|
80
|
+
link.setAttribute("title", LABEL[active]);
|
|
81
|
+
link.setAttribute("aria-label", LABEL[active]);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function cycle() {
|
|
86
|
+
var next = ORDER[mode()];
|
|
87
|
+
current = next;
|
|
88
|
+
store(next === "auto" ? null : next);
|
|
89
|
+
apply();
|
|
90
|
+
refresh();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Delegated, so a control added later — or replaced by a Turbo render — still
|
|
94
|
+
// works without rebinding.
|
|
95
|
+
document.addEventListener("click", function (event) {
|
|
96
|
+
if (!event.target.closest || !event.target.closest(SELECTOR)) return;
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
cycle();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// The control is a link with no destination, so it is reachable by Tab; that
|
|
102
|
+
// makes Enter and Space its keyboard contract.
|
|
103
|
+
document.addEventListener("keydown", function (event) {
|
|
104
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
105
|
+
if (!event.target.closest || !event.target.closest(SELECTOR)) return;
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
cycle();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Another tab changed the preference.
|
|
111
|
+
window.addEventListener("storage", function (event) {
|
|
112
|
+
if (event.key !== KEY) return;
|
|
113
|
+
current = normalise(event.newValue);
|
|
114
|
+
apply();
|
|
115
|
+
refresh();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// In auto mode, follow the operating system while the page is open.
|
|
119
|
+
if (window.matchMedia) {
|
|
120
|
+
var query = window.matchMedia("(prefers-color-scheme: dark)");
|
|
121
|
+
var onChange = function () { if (mode() === "auto") { apply(); refresh(); } };
|
|
122
|
+
if (query.addEventListener) query.addEventListener("change", onChange);
|
|
123
|
+
else if (query.addListener) query.addListener(onChange);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
ready(function () {
|
|
127
|
+
if (controls().length === 0) {
|
|
128
|
+
var nav = document.getElementById("utility_nav");
|
|
129
|
+
if (!nav) return;
|
|
130
|
+
var host = document.createElement("li");
|
|
131
|
+
host.id = "theme_toggle";
|
|
132
|
+
var link = host.appendChild(document.createElement("a"));
|
|
133
|
+
// Not "#": ActiveAdmin treats a bare hash as a blank menu item.
|
|
134
|
+
link.setAttribute("href", "#theme");
|
|
135
|
+
link.setAttribute("role", "button");
|
|
136
|
+
nav.insertBefore(host, nav.firstChild);
|
|
137
|
+
}
|
|
138
|
+
refresh();
|
|
139
|
+
});
|
|
140
|
+
})();
|