active_admin_theme 3.1.1 → 3.2.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 +185 -34
- data/app/assets/javascripts/wigu/all.js +10 -0
- data/app/assets/javascripts/wigu/scrollbar_width.js +72 -0
- data/app/assets/javascripts/wigu/server_clock.js +171 -0
- data/app/assets/javascripts/wigu/table_crisscross.js +49 -0
- data/app/assets/stylesheets/wigu/active_admin_theme.scss +473 -12
- data/lib/active_admin_theme/version.rb +1 -1
- data/lib/active_admin_theme.rb +127 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c9fc50db0360c19eaa74317c03e490285afb8c9fcb0b02bed8adf5c529806d91
|
|
4
|
+
data.tar.gz: 5c7efcc79d8773432c75479bffc4cd63d3bf06cd592ba21d1decc5496faaf574
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49cd0bf392f5568d9cfc8c0f83c964599f74d3f1d6ede4e0cc153de2309e6b99e5db333df0c069e21805e695dfe82b4e32e3e749d0cca0f3d757d8fbdcf211ed
|
|
7
|
+
data.tar.gz: c32caeaabee4c474b2535cdeff00651ff5e1b643d02f915ef3bb4f5c125dc3394c522dc731b7ae38e6fb88bc7ed132f713e88c100cfc51a9489d8904a3e5c8ac
|
data/README.md
CHANGED
|
@@ -174,8 +174,158 @@ The control is a link with no destination, so <kbd>Tab</kbd> reaches it and
|
|
|
174
174
|
<kbd>Enter</kbd> and <kbd>Space</kbd> operate it. Changing the theme in one tab
|
|
175
175
|
applies it in the others.
|
|
176
176
|
|
|
177
|
+
### Disabling dark mode
|
|
178
|
+
|
|
179
|
+
To ship light-only, two manual steps (there is no build flag):
|
|
180
|
+
|
|
181
|
+
- Don't include `wigu/theme_toggle` in your JS manifest. That removes the switch
|
|
182
|
+
and the automatic `data-theme` switching it drives.
|
|
183
|
+
- Pin the document to light: add `data-theme="light"` to the `<html>` element
|
|
184
|
+
(e.g. by overriding the ActiveAdmin layout), so the
|
|
185
|
+
`@media (prefers-color-scheme: dark)` styles never apply.
|
|
186
|
+
|
|
187
|
+
### Server clock
|
|
188
|
+
|
|
189
|
+
The `#utility_nav` now carries a ticking server clock — two lines, time + zone
|
|
190
|
+
over the date — rendered by the theme's `server_clock.js` and injected per
|
|
191
|
+
namespace by its Ruby backend (`ActiveAdminTheme.install!`), with the logout
|
|
192
|
+
button beside it.
|
|
193
|
+
|
|
194
|
+
The backend injects only the markup; load the script the same way as the
|
|
195
|
+
theme switch, or the nav shows a static one-line label and never ticks:
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
// app/assets/javascripts/active_admin.js
|
|
199
|
+
//= require wigu/server_clock
|
|
200
|
+
|
|
201
|
+
// or, as an npm module
|
|
202
|
+
import "@activeadmin-plugins/active_admin_theme/src/server_clock";
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
[](./img/clock.png)
|
|
206
|
+
|
|
207
|
+
The theme also keeps a logout item in the nav. If your app has no logout route
|
|
208
|
+
(custom auth) or you deliberately run a logout-free nav, use ActiveAdmin's own
|
|
209
|
+
per-namespace switch — the theme honours it:
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
# config/initializers/active_admin.rb
|
|
213
|
+
config.logout_link_path = nil
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The restored current-user item has a theme switch (ActiveAdmin offers none
|
|
217
|
+
short of unsetting `current_user_method`, which breaks authorization):
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
ActiveAdminTheme.add_current_user = false
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Crisscross table highlighting
|
|
224
|
+
|
|
225
|
+
Clicking a cell in an index table highlights its whole row and column — the
|
|
226
|
+
cells get class `selected`, which the theme paints with `--aa-selected`, so the
|
|
227
|
+
clicked value sits at the centre of a cross, with an accent outline on the cell
|
|
228
|
+
itself. Handy for tracing a value to its row and column in a wide table.
|
|
229
|
+
|
|
230
|
+
The `selected` fill is always in the theme (it also covers ActiveAdmin's own
|
|
231
|
+
batch-action row selection); the click behaviour is opt-in — load the script to
|
|
232
|
+
enable it, leave it out to keep tables static:
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
// app/assets/javascripts/active_admin.js
|
|
236
|
+
//= require wigu/table_crisscross
|
|
237
|
+
|
|
238
|
+
// or, as an npm module
|
|
239
|
+
import "@activeadmin-plugins/active_admin_theme/src/table_crisscross";
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
It uses jQuery, which ActiveAdmin already loads.
|
|
243
|
+
|
|
244
|
+
[](./img/crisscross-light.png)
|
|
245
|
+
|
|
246
|
+
### Pinned bars and sticky table headers
|
|
247
|
+
|
|
248
|
+
On pages with a table wider than the viewport the whole page scrolls sideways;
|
|
249
|
+
the theme pins `#header`, `#title_bar` (title and action buttons) and the flash
|
|
250
|
+
bar to the visible viewport so they stay put. The bars are sized with `100vw`,
|
|
251
|
+
which includes the vertical scrollbar gutter — load the shipped
|
|
252
|
+
`scrollbar_width.js` to subtract it, or the bars sit ~15px past the visible
|
|
253
|
+
viewport at the far-right scroll position:
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
// app/assets/javascripts/active_admin.js
|
|
257
|
+
//= require wigu/scrollbar_width
|
|
258
|
+
|
|
259
|
+
// or, as an npm module
|
|
260
|
+
import "@activeadmin-plugins/active_admin_theme/src/scrollbar_width";
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Without the script the CSS falls back to plain `100vw` — nothing breaks, you
|
|
264
|
+
just keep the small far-right shift.
|
|
265
|
+
|
|
266
|
+
Index-table column headers also stick to the viewport top while a long listing
|
|
267
|
+
scrolls vertically. This is on by default for ActiveAdmin index tables. AA 3.x
|
|
268
|
+
hardcodes the index table's classes (`index class:` is ignored), so opt out
|
|
269
|
+
app-wide with one line in your stylesheet:
|
|
270
|
+
|
|
271
|
+
```scss
|
|
272
|
+
.paginated_collection table.index_table thead tr th { position: static; }
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
For tables whose markup you render yourself, `skip-sticky-headers` on an
|
|
276
|
+
`index_table` opts out, and `with-sticky-headers` opts any hand-built table in
|
|
277
|
+
(single-row theads: only the first header row pins).
|
|
278
|
+
|
|
279
|
+
`//= require wigu/all` pulls all four shipped scripts at once; each stays
|
|
280
|
+
individually requirable.
|
|
281
|
+
|
|
282
|
+
### Trix / ActionText editor
|
|
283
|
+
|
|
284
|
+
Trix ships with ActionText (a Rails built-in), so any ActiveAdmin app can have a
|
|
285
|
+
rich-text field. The theme styles the editor chrome — toolbar, editor surface,
|
|
286
|
+
link dialog, content `pre`/`blockquote` — off the theme variables in both light
|
|
287
|
+
and dark, and inverts Trix's black toolbar icons in dark mode so they stay
|
|
288
|
+
legible. Nothing to opt into: your app still imports Trix's own base CSS
|
|
289
|
+
(`trix/dist/trix`), the theme only restyles it, and the rules are inert when no
|
|
290
|
+
editor is on the page.
|
|
291
|
+
|
|
292
|
+
[](./img/trix-light.png)
|
|
293
|
+
[](./img/trix-dark.png)
|
|
294
|
+
|
|
177
295
|
### Upgrading
|
|
178
296
|
|
|
297
|
+
#### 3.2.0
|
|
298
|
+
|
|
299
|
+
The variable contract only grows — `$skinTitleBarHeight` joins it and nothing
|
|
300
|
+
that worked against 3.1.1 stops working — but several defaults now look
|
|
301
|
+
different, and two of them take layout away from the host app:
|
|
302
|
+
|
|
303
|
+
* `#header`, `#title_bar` and the flash bar are pinned to the visible viewport,
|
|
304
|
+
so they stay put while a table wider than the window scrolls the page
|
|
305
|
+
sideways. Apps that pinned those bars with their own stylesheet should drop
|
|
306
|
+
those rules: the theme lays the title bar out with flexbox and resets
|
|
307
|
+
`#titlebar_right` to `position: static`, and a leftover `position:
|
|
308
|
+
absolute/sticky` fights it. Load `wigu/scrollbar_width` to subtract the
|
|
309
|
+
scrollbar gutter — see *Pinned bars and sticky table headers*.
|
|
310
|
+
* Index-table column headers stick to the viewport top on every ActiveAdmin
|
|
311
|
+
index table. One line in your stylesheet opts out app-wide:
|
|
312
|
+
`.paginated_collection table.index_table thead tr th { position: static; }`.
|
|
313
|
+
* The title bar owns its own metrics now: the record title is 16px, on one line
|
|
314
|
+
with an ellipsis, instead of ActiveAdmin's large wrapping heading; action
|
|
315
|
+
items are spaced; the band is `$skinTitleBarHeight` (37px) tall, with
|
|
316
|
+
`$skinTitleBarButtonPaddingY` / `$skinTitleBarButtonPaddingX` on the buttons.
|
|
317
|
+
Title-bar dropdown panels right-align under their own button.
|
|
318
|
+
* The utility nav carries the server clock, the theme switch and a logout drawn
|
|
319
|
+
as a sign-out glyph rather than the word *Logout*. `ActiveAdminTheme
|
|
320
|
+
.add_current_user = false` drops the added current-user item, and
|
|
321
|
+
ActiveAdmin's own `config.logout_link_path = nil` drops the logout one.
|
|
322
|
+
|
|
323
|
+
New and opt-in, nothing to change on upgrade: the Trix / ActionText editor is
|
|
324
|
+
themed in both modes, crisscross cell highlighting (`wigu/table_crisscross`),
|
|
325
|
+
`--aa-datetimepicker-*` bridge variables for the companion datetimepicker, dark
|
|
326
|
+
styling for select2 and tom-select, and `//= require wigu/all` to pull every
|
|
327
|
+
shipped script at once.
|
|
328
|
+
|
|
179
329
|
#### 3.1.0
|
|
180
330
|
|
|
181
331
|
* Status tag fills are darker, so the label can be white and still clear 4.5:1 —
|
|
@@ -211,38 +361,38 @@ Three things changed shape and are worth knowing if you already set variables:
|
|
|
211
361
|
|
|
212
362
|
| Variable | Default (light / dark) | |
|
|
213
363
|
|---|---|---|
|
|
214
|
-
| `$skinMainFirstColor` | `#23282f` | |
|
|
215
|
-
| `$skinMainSecondColor` | `#5ea3d3` | |
|
|
364
|
+
| `$skinMainFirstColor` |  `#23282f` | |
|
|
365
|
+
| `$skinMainSecondColor` |  `#5ea3d3` | |
|
|
216
366
|
| `$skinBorderRadius` | `4px` | |
|
|
217
|
-
| `$skinBorderWindowColor` | `#e6e9ee` | |
|
|
367
|
+
| `$skinBorderWindowColor` |  `#e6e9ee` | |
|
|
218
368
|
| `$skinTablePadding` | `10px` | |
|
|
219
369
|
|
|
220
370
|
#### Surfaces, text and borders
|
|
221
371
|
|
|
222
372
|
| Variable | Default (light / dark) | |
|
|
223
373
|
|---|---|---|
|
|
224
|
-
| `$skinPageBgColor` / `$skinPageBgColorDark` | `#f7f9fb` / `#161a1e` | page background |
|
|
225
|
-
| `$skinSurfaceColor` / `$skinSurfaceColorDark` | `#ffffff` / `#25292f` | panels / cards / content |
|
|
226
|
-
| `$skinSurface2Color` / `$skinSurface2ColorDark` | `#f0f2f5` / `#2c3137` | table headers / striping / subtle fills |
|
|
227
|
-
| `$skinSurfaceHoverColor` / `$skinSurfaceHoverColorDark` | `#f5f7fa` / `#3f454d` | row / item hover |
|
|
228
|
-
| `$skinSelectedRowColor` / `$skinSelectedRowColorDark` | `#d9e4ec` / `#304457` | checked table row |
|
|
229
|
-
| `$skinElevatedColor` / `$skinElevatedColorDark` | `$skinSurfaceColor` / `#363c43` | tool buttons and dropdown panels floating above the page |
|
|
230
|
-
| `$skinTextColor` / `$skinTextColorDark` | `#323537` / `#dde2e8` | body text |
|
|
231
|
-
| `$skinTextMutedColor` / `$skinTextMutedColorDark` | `#6b7177` / `#b0b8c2` | secondary text / axis labels |
|
|
232
|
-
| `$skinBorderColor` / `$skinBorderColorDark` | `#e0e4e9` / `#404750` | borders / grid lines |
|
|
233
|
-
| `$skinInputBgColor` / `$skinInputBgColorDark` | `#ffffff` / `#1e2227` | form control background |
|
|
234
|
-
| `$skinInputBorderColor` / `$skinInputBorderColorDark` | `#c9ced4` / `#4d555f` | form control border |
|
|
374
|
+
| `$skinPageBgColor` / `$skinPageBgColorDark` |  `#f7f9fb` /  `#161a1e` | page background |
|
|
375
|
+
| `$skinSurfaceColor` / `$skinSurfaceColorDark` |  `#ffffff` /  `#25292f` | panels / cards / content |
|
|
376
|
+
| `$skinSurface2Color` / `$skinSurface2ColorDark` |  `#f0f2f5` /  `#2c3137` | table headers / striping / subtle fills |
|
|
377
|
+
| `$skinSurfaceHoverColor` / `$skinSurfaceHoverColorDark` |  `#f5f7fa` /  `#3f454d` | row / item hover |
|
|
378
|
+
| `$skinSelectedRowColor` / `$skinSelectedRowColorDark` |  `#d9e4ec` /  `#304457` | checked table row |
|
|
379
|
+
| `$skinElevatedColor` / `$skinElevatedColorDark` | `$skinSurfaceColor` /  `#363c43` | tool buttons and dropdown panels floating above the page |
|
|
380
|
+
| `$skinTextColor` / `$skinTextColorDark` |  `#323537` /  `#dde2e8` | body text |
|
|
381
|
+
| `$skinTextMutedColor` / `$skinTextMutedColorDark` |  `#6b7177` /  `#b0b8c2` | secondary text / axis labels |
|
|
382
|
+
| `$skinBorderColor` / `$skinBorderColorDark` |  `#e0e4e9` /  `#404750` | borders / grid lines |
|
|
383
|
+
| `$skinInputBgColor` / `$skinInputBgColorDark` |  `#ffffff` /  `#1e2227` | form control background |
|
|
384
|
+
| `$skinInputBorderColor` / `$skinInputBorderColorDark` |  `#c9ced4` /  `#4d555f` | form control border |
|
|
235
385
|
|
|
236
386
|
#### Header menu
|
|
237
387
|
|
|
238
388
|
| Variable | Default (light / dark) | |
|
|
239
389
|
|---|---|---|
|
|
240
|
-
| `$skinMenuPillColor` | `#2e3236` | top-level current/hover pill |
|
|
241
|
-
| `$skinMenuPillTextColor` | `#6cb0de` | text on that pill; follows the dropdown text so a |
|
|
242
|
-
| `$skinMenuPanelColor` | `#2e3236` | dropdown panel bg + hover "bridge" border |
|
|
243
|
-
| `$skinMenuTextColor` | `#dfe2e6` | dropdown item text (was: inherited #fff) |
|
|
244
|
-
| `$skinMenuItemHoverColor` | `#3f454c` | dropdown item hover/current bg (was: none) |
|
|
245
|
-
| `$skinMenuItemHoverTextColor` | `#6cb0de` | hover/current dropdown item text, same reason |
|
|
390
|
+
| `$skinMenuPillColor` |  `#2e3236` | top-level current/hover pill |
|
|
391
|
+
| `$skinMenuPillTextColor` |  `#6cb0de` | text on that pill; follows the dropdown text so a |
|
|
392
|
+
| `$skinMenuPanelColor` |  `#2e3236` | dropdown panel bg + hover "bridge" border |
|
|
393
|
+
| `$skinMenuTextColor` |  `#dfe2e6` | dropdown item text (was: inherited #fff) |
|
|
394
|
+
| `$skinMenuItemHoverColor` |  `#3f454c` | dropdown item hover/current bg (was: none) |
|
|
395
|
+
| `$skinMenuItemHoverTextColor` |  `#6cb0de` | hover/current dropdown item text, same reason |
|
|
246
396
|
| `$skinMenuFontSize` | `13px` | header menu text size |
|
|
247
397
|
| `$skinMenuItemPaddingY` | `5px` | dropdown item top/bottom padding (was 6px/4px + a 7px border) |
|
|
248
398
|
| `$skinMenuItemLineHeight` | `1.35` | dropdown item line-height |
|
|
@@ -256,11 +406,12 @@ Three things changed shape and are worth knowing if you already set variables:
|
|
|
256
406
|
|
|
257
407
|
| Variable | Default (light / dark) | |
|
|
258
408
|
|---|---|---|
|
|
259
|
-
| `$skinTitleBarColor` | `#343c46` | |
|
|
409
|
+
| `$skinTitleBarColor` |  `#343c46` | |
|
|
260
410
|
| `$skinTitleBarBorderColor` | `$skinMainSecondColor` | |
|
|
261
411
|
| `$skinTitleBarBorderWidth` | `0` | |
|
|
262
412
|
| `$skinTitleBarButtonPaddingY` | `6px` | action button vertical padding |
|
|
263
413
|
| `$skinTitleBarButtonPaddingX` | `10px` | action button horizontal padding |
|
|
414
|
+
| `$skinTitleBarHeight` | `37px` | minimum bar height; grows to fit content |
|
|
264
415
|
|
|
265
416
|
#### Panels, tabs and labels
|
|
266
417
|
|
|
@@ -269,17 +420,17 @@ Three things changed shape and are worth knowing if you already set variables:
|
|
|
269
420
|
| `$skinPanelHeaderColor` / `$skinPanelHeaderColorDark` | `var(--aa-page-bg)` / `var(--aa-page-bg)` | |
|
|
270
421
|
| `$skinPanelHeaderTextColor` / `$skinPanelHeaderTextColorDark` | `var(--aa-inactive-tab-text)` / `var(--aa-inactive-tab-text)` | |
|
|
271
422
|
| `$skinPanelHeaderPaddingY` | `5px` | panel + sidebar header height |
|
|
272
|
-
| `$skinLabelColor` / `$skinLabelColorDark` | `#8494a8` / `$skinTextColorDark` | |
|
|
273
|
-
| `$skinTabInactiveColor` / `$skinTabInactiveColorDark` | `#f7f9fb` / `#161a1e` | inactive tab fill |
|
|
274
|
-
| `$skinActiveTabTextColor` / `$skinActiveTabTextColorDark` | `$skinMainSecondColor` / `#7cc0ec` | selected tab label |
|
|
275
|
-
| `$skinInactiveTabTextColor` / `$skinInactiveTabTextColorDark` | `#5e6469` / `#b0b8c2` | inactive tab label |
|
|
423
|
+
| `$skinLabelColor` / `$skinLabelColorDark` |  `#8494a8` / `$skinTextColorDark` | |
|
|
424
|
+
| `$skinTabInactiveColor` / `$skinTabInactiveColorDark` |  `#f7f9fb` /  `#161a1e` | inactive tab fill |
|
|
425
|
+
| `$skinActiveTabTextColor` / `$skinActiveTabTextColorDark` | `$skinMainSecondColor` /  `#7cc0ec` | selected tab label |
|
|
426
|
+
| `$skinInactiveTabTextColor` / `$skinInactiveTabTextColorDark` |  `#5e6469` /  `#b0b8c2` | inactive tab label |
|
|
276
427
|
| `$skinTableHeaderTextColor` / `$skinTableHeaderTextColorDark` | `$skinTextColor` / `$skinTextColorDark` | index-table column header text; the body text colour, so headings read as strongly as the rows |
|
|
277
|
-
| `$skinStatusTagTextColor` | `#ffffff` | label inside a filled status tag; `empty` / `unknown` / `none` have no fill and keep `$skinTextMutedColor` |
|
|
278
|
-
| `$skinStatusTagNeutralColor` | `#707681` | unclassified tags: `No`, protocol tags |
|
|
279
|
-
| `$skinStatusTagOkColor` | `#5e7e63` | `ok` `published` `complete` `completed` `green` `yes` |
|
|
280
|
-
| `$skinStatusTagNoticeColor` | `#3874d2` | `notice` `blue` |
|
|
281
|
-
| `$skinStatusTagWarnColor` | `#9e6c15` | `warn` `warning` `orange` |
|
|
282
|
-
| `$skinStatusTagErrorColor` | `#ce483b` | `error` `errored` `red` |
|
|
428
|
+
| `$skinStatusTagTextColor` |  `#ffffff` | label inside a filled status tag; `empty` / `unknown` / `none` have no fill and keep `$skinTextMutedColor` |
|
|
429
|
+
| `$skinStatusTagNeutralColor` |  `#707681` | unclassified tags: `No`, protocol tags |
|
|
430
|
+
| `$skinStatusTagOkColor` |  `#5e7e63` | `ok` `published` `complete` `completed` `green` `yes` |
|
|
431
|
+
| `$skinStatusTagNoticeColor` |  `#3874d2` | `notice` `blue` |
|
|
432
|
+
| `$skinStatusTagWarnColor` |  `#9e6c15` | `warn` `warning` `orange` |
|
|
433
|
+
| `$skinStatusTagErrorColor` |  `#ce483b` | `error` `errored` `red` |
|
|
283
434
|
| `$skinTabPaddingY` | `8px` | tab height |
|
|
284
435
|
| `$skinTabPaddingX` | `15px` | tab label horizontal padding (text → border) |
|
|
285
436
|
|
|
@@ -288,7 +439,7 @@ Three things changed shape and are worth knowing if you already set variables:
|
|
|
288
439
|
| Variable | Default (light / dark) | |
|
|
289
440
|
|---|---|---|
|
|
290
441
|
| `$skinButtonColor` / `$skinButtonColorDark` | `darken($skinMainSecondColor, 20%)` / `$skinButtonColor` | one tone in both modes; white on it is 5.35:1 |
|
|
291
|
-
| `$skinButtonTextColor` / `$skinButtonTextColorDark` | `#ffffff` / `$skinButtonTextColor` | label on those buttons |
|
|
442
|
+
| `$skinButtonTextColor` / `$skinButtonTextColorDark` |  `#ffffff` / `$skinButtonTextColor` | label on those buttons |
|
|
292
443
|
| `$skinTableToolsHeight` | `30px` | |
|
|
293
444
|
| `$skinTableToolsPaddingX` | `$skinTableToolsHeight * 0.4` | 12px at 30px |
|
|
294
445
|
|
|
@@ -297,8 +448,8 @@ Three things changed shape and are worth knowing if you already set variables:
|
|
|
297
448
|
| Variable | Default (light / dark) | |
|
|
298
449
|
|---|---|---|
|
|
299
450
|
| `$skinAccentColor` / `$skinAccentColorDark` | `$skinMainSecondColor` / `$skinAccentColor` | focus ring / accent outline |
|
|
300
|
-
| `$skinLinkColor` / `$skinLinkColorDark` | `#38678b` / `#7cc0ec` | |
|
|
301
|
-
| `$skinDeleteLinkColor` / `$skinDeleteLinkColorDark` | `$skinLinkColor` / `#f49b9b` | |
|
|
451
|
+
| `$skinLinkColor` / `$skinLinkColorDark` |  `#38678b` /  `#7cc0ec` | |
|
|
452
|
+
| `$skinDeleteLinkColor` / `$skinDeleteLinkColorDark` | `$skinLinkColor` /  `#f49b9b` | |
|
|
302
453
|
|
|
303
454
|
## Screen
|
|
304
455
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Umbrella require for every script the theme ships, so a Sprockets manifest
|
|
2
|
+
// carries one line instead of a hand-maintained list that silently drifts:
|
|
3
|
+
// theme CSS can depend on a JS-set property (scrollbar_width feeds the pinned
|
|
4
|
+
// bars' width), and a missed require degrades visually with no error. Each
|
|
5
|
+
// script stays individually requirable for apps that want a subset; see the
|
|
6
|
+
// README for what each one does and how to opt out.
|
|
7
|
+
//= require wigu/theme_toggle
|
|
8
|
+
//= require wigu/server_clock
|
|
9
|
+
//= require wigu/scrollbar_width
|
|
10
|
+
//= require wigu/table_crisscross
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Expose the vertical scrollbar width as a CSS custom property
|
|
2
|
+
// (--scrollbar-width) on the pinned bars. The sticky #header, #title_bar and
|
|
3
|
+
// .flashes are pinned to the viewport's left edge and sized with 100vw, but
|
|
4
|
+
// 100vw INCLUDES the vertical scrollbar gutter — so when a page has a vertical
|
|
5
|
+
// scrollbar the bars are ~15px wider than the visible viewport and, clamped
|
|
6
|
+
// inside their full-content-width containing block, get pushed left by that
|
|
7
|
+
// amount at the far-right horizontal scroll position. Subtracting this
|
|
8
|
+
// variable (0 when there is no scrollbar) sizes them to the visible viewport
|
|
9
|
+
// instead, so they stay put. Optional: the CSS falls back to
|
|
10
|
+
// `var(--scrollbar-width, 0px)` = plain 100vw when this script is absent.
|
|
11
|
+
//
|
|
12
|
+
// The value is written on the three consuming bars, NOT on <html>: a custom
|
|
13
|
+
// property changed on the root invalidates style for the entire document —
|
|
14
|
+
// one full recalc of a 500-row index table per write — while var() resolves
|
|
15
|
+
// from the element itself just as well, confining invalidation to three tiny
|
|
16
|
+
// subtrees.
|
|
17
|
+
//
|
|
18
|
+
// The property name is deliberately the bare --scrollbar-width, not --aa-*:
|
|
19
|
+
// consumer apps that already set the same variable with the same semantics
|
|
20
|
+
// from their own legacy script keep working before their manifest is updated
|
|
21
|
+
// (a root-level legacy value still reaches the bars through inheritance when
|
|
22
|
+
// this script is absent). The trade-off is a shared name — load one writer,
|
|
23
|
+
// not both.
|
|
24
|
+
(function () {
|
|
25
|
+
"use strict";
|
|
26
|
+
|
|
27
|
+
var BARS = "#header, #title_bar, .flashes";
|
|
28
|
+
var last = null;
|
|
29
|
+
|
|
30
|
+
// Idempotent and cheap: reads the inline style (no layout) and writes only
|
|
31
|
+
// real changes, so it is safe to call on every observer delivery — which is
|
|
32
|
+
// what picks up bars parsed after the value was first computed, and bars
|
|
33
|
+
// replaced by a Turbo/Turbolinks body swap.
|
|
34
|
+
function apply() {
|
|
35
|
+
if (last === null) return;
|
|
36
|
+
var val = last + "px";
|
|
37
|
+
var bars = document.querySelectorAll(BARS);
|
|
38
|
+
for (var i = 0; i < bars.length; i++) {
|
|
39
|
+
if (bars[i].style.getPropertyValue("--scrollbar-width") !== val) {
|
|
40
|
+
bars[i].style.setProperty("--scrollbar-width", val);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function update() {
|
|
46
|
+
var w = window.innerWidth - document.documentElement.clientWidth;
|
|
47
|
+
if (w < 0) w = 0;
|
|
48
|
+
last = w;
|
|
49
|
+
apply();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (window.ResizeObserver) {
|
|
53
|
+
// The observer alone is enough: it delivers once right after observe()
|
|
54
|
+
// (post-layout, pre-paint, so the read forces nothing) and again whenever
|
|
55
|
+
// the root's client box changes — which covers window resizes AND content
|
|
56
|
+
// growth toggling the scrollbar. A resize listener on top of it would
|
|
57
|
+
// force a synchronous style+layout per event during a window drag (the
|
|
58
|
+
// clientWidth read lands before the frame's layout) for no extra
|
|
59
|
+
// information. documentElement exists at parse time and survives body
|
|
60
|
+
// swaps, so there is no DOMContentLoaded gate and nothing goes stale.
|
|
61
|
+
new ResizeObserver(update).observe(document.documentElement);
|
|
62
|
+
} else {
|
|
63
|
+
// No ResizeObserver: fall back to resize events plus one eager read.
|
|
64
|
+
window.addEventListener("resize", update);
|
|
65
|
+
update();
|
|
66
|
+
}
|
|
67
|
+
// A body swap replaces the bars the value was written on without
|
|
68
|
+
// necessarily resizing the root; re-apply the cached value to the fresh
|
|
69
|
+
// elements.
|
|
70
|
+
document.addEventListener("turbo:load", apply);
|
|
71
|
+
document.addEventListener("turbolinks:load", apply);
|
|
72
|
+
})();
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Server clock for the utility navigation. Shows the server's time ticking once
|
|
2
|
+
// a second in li#servertime inside #utility_nav, where the stylesheet styles it.
|
|
3
|
+
//
|
|
4
|
+
// The instant comes from a [data-servertime] element the host app renders — the
|
|
5
|
+
// body in some apps, a footer element in others — holding
|
|
6
|
+
// "YYYY MM DD HH MM SS ZONE" (space-separated, exactly what Ruby's
|
|
7
|
+
// Time#strftime('%Y %m %d %H %M %S %Z') produces). The clock keeps that server
|
|
8
|
+
// instant and advances it by the client's own elapsed time, so a skewed browser
|
|
9
|
+
// clock does not throw it off. With no such element it renders nothing: a
|
|
10
|
+
// fabricated client clock would bypass every server-side suppression (anonymous
|
|
11
|
+
// visitors, hosts without current_user, consumer opt-outs) and show browser
|
|
12
|
+
// time masquerading as the server clock.
|
|
13
|
+
//
|
|
14
|
+
// Vanilla, no deps, mounts itself into #utility_nav. Turbo/Turbolinks swap the
|
|
15
|
+
// body without firing DOMContentLoaded, so the clock re-reads the fresh
|
|
16
|
+
// [data-servertime] on their load events and re-queries the DOM every tick
|
|
17
|
+
// instead of holding node references — a held reference goes detached on the
|
|
18
|
+
// first navigation and the visible clock freezes.
|
|
19
|
+
(function () {
|
|
20
|
+
"use strict";
|
|
21
|
+
|
|
22
|
+
var serverTime, clientTime, zone, fmt;
|
|
23
|
+
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
24
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
25
|
+
|
|
26
|
+
function pad2(n) {
|
|
27
|
+
return (n < 10 ? "0" : "") + n;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ready(fn) {
|
|
31
|
+
if (document.readyState !== "loading") fn();
|
|
32
|
+
else document.addEventListener("DOMContentLoaded", fn);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// (Re)read the server instant from the current document.
|
|
36
|
+
function sync() {
|
|
37
|
+
// Prefer the theme's own span inside the nav: a host mid-migration may
|
|
38
|
+
// still render its old body[data-servertime] (possibly in another format),
|
|
39
|
+
// which would win on first-match-in-tree-order and shadow the fresh
|
|
40
|
+
// per-request instant. Fall back to anywhere in the document for hosts
|
|
41
|
+
// that only render the attribute on the body or a footer element.
|
|
42
|
+
var nav = document.getElementById("utility_nav");
|
|
43
|
+
var source = (nav && nav.querySelector("[data-servertime]")) ||
|
|
44
|
+
document.querySelector("[data-servertime]");
|
|
45
|
+
|
|
46
|
+
// Preferred source: an absolute instant plus the server's IANA zone
|
|
47
|
+
// (rendered by the gem's Ruby backend). Intl re-applies the zone's offset
|
|
48
|
+
// on every tick, so a long-lived page follows the server through its own
|
|
49
|
+
// DST transitions — the wall-clock path below keeps the offset and label
|
|
50
|
+
// frozen from page load.
|
|
51
|
+
fmt = null;
|
|
52
|
+
serverTime = undefined; // no source on this page: tick() renders nothing
|
|
53
|
+
var epoch = source && source.getAttribute("data-servertime-epoch");
|
|
54
|
+
var tz = source && source.getAttribute("data-servertime-zone");
|
|
55
|
+
if (epoch && tz && window.Intl &&
|
|
56
|
+
Intl.DateTimeFormat.prototype.formatToParts) {
|
|
57
|
+
try {
|
|
58
|
+
fmt = new Intl.DateTimeFormat("en-US", {
|
|
59
|
+
timeZone: tz, hour12: false, hourCycle: "h23",
|
|
60
|
+
year: "numeric", month: "short", day: "2-digit",
|
|
61
|
+
hour: "2-digit", minute: "2-digit", second: "2-digit",
|
|
62
|
+
timeZoneName: "short"
|
|
63
|
+
});
|
|
64
|
+
serverTime = epoch * 1000;
|
|
65
|
+
// A malformed epoch is truthy but NaN; formatToParts(new Date(NaN))
|
|
66
|
+
// would throw on every tick. Drop to the wall-clock path instead.
|
|
67
|
+
if (!isFinite(serverTime)) fmt = null;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
fmt = null; // unknown zone name: fall through to the wall-clock path
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
var raw = !fmt && source && source.getAttribute("data-servertime");
|
|
74
|
+
|
|
75
|
+
if (raw) {
|
|
76
|
+
var bits = raw.split(" ");
|
|
77
|
+
// A fixed epoch via Date.UTC, NOT new Date(y,m,d,...): local-zone
|
|
78
|
+
// parsing shifts a server instant that falls into the client's DST gap,
|
|
79
|
+
// and a tab left open across a client transition jumps the display an
|
|
80
|
+
// hour while the server had none. The zone label is carried verbatim.
|
|
81
|
+
serverTime = Date.UTC(bits[0], bits[1] - 1, bits[2], bits[3], bits[4], bits[5]);
|
|
82
|
+
// No fabricated label: a missing %Z just renders no zone.
|
|
83
|
+
zone = bits[6] || "";
|
|
84
|
+
}
|
|
85
|
+
clientTime = Date.now();
|
|
86
|
+
tick();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The two display lines for this li, created on first use and reused every
|
|
90
|
+
// later tick. The server-rendered inner span [data-servertime] is the data
|
|
91
|
+
// source for later sync() calls (turbo:load, bfcache restores) — never
|
|
92
|
+
// overwrite it; hide it and append the lines beside it. textContent only,
|
|
93
|
+
// no innerHTML: the zone token of a host-rendered attribute is untrusted
|
|
94
|
+
// text and must not be able to become markup — and skipping the per-second
|
|
95
|
+
// innerHTML re-parse stops the node churn too.
|
|
96
|
+
function lines(li) {
|
|
97
|
+
var time = li.querySelector(".clock-time");
|
|
98
|
+
if (time) return { time: time, date: li.querySelector(".clock-date") };
|
|
99
|
+
var data = li.querySelector("[data-servertime]");
|
|
100
|
+
// Append target: the outer Arbre span when the gem rendered the item, the
|
|
101
|
+
// li's own span otherwise. No inline colour — the span inherits the nav's
|
|
102
|
+
// text colour from the stylesheet cascade.
|
|
103
|
+
var host = data ? data.parentNode : li.querySelector("span");
|
|
104
|
+
if (!host) {
|
|
105
|
+
host = document.createElement("span");
|
|
106
|
+
li.appendChild(host);
|
|
107
|
+
}
|
|
108
|
+
if (data) data.style.display = "none";
|
|
109
|
+
time = document.createElement("span");
|
|
110
|
+
time.className = "clock-time";
|
|
111
|
+
var date = document.createElement("span");
|
|
112
|
+
date.className = "clock-date";
|
|
113
|
+
host.appendChild(time);
|
|
114
|
+
host.appendChild(date);
|
|
115
|
+
return { time: time, date: date };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function tick() {
|
|
119
|
+
var nav = document.getElementById("utility_nav");
|
|
120
|
+
// isFinite, not an undefined check: a host-rendered data-servertime in an
|
|
121
|
+
// unexpected format parses to NaN, which must render nothing — not a
|
|
122
|
+
// ticking "NaN:NaN:NaN".
|
|
123
|
+
if (!nav || !isFinite(serverTime)) return;
|
|
124
|
+
|
|
125
|
+
// Reuse an existing li (idempotent across re-renders); never duplicate.
|
|
126
|
+
var li = nav.querySelector("#servertime");
|
|
127
|
+
if (!li) {
|
|
128
|
+
li = document.createElement("li");
|
|
129
|
+
li.id = "servertime";
|
|
130
|
+
// Mark it as ours, so the ownership check below keeps passing.
|
|
131
|
+
li.setAttribute("data-theme-clock", "");
|
|
132
|
+
nav.appendChild(li);
|
|
133
|
+
} else if (!li.hasAttribute("data-theme-clock") &&
|
|
134
|
+
!li.querySelector("[data-servertime]")) {
|
|
135
|
+
// The li belongs to the host — their own servertime menu item, e.g.
|
|
136
|
+
// rendered as a plain link. Appending display lines would show the
|
|
137
|
+
// host's label and a second clock side by side in one item.
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
var out = lines(li);
|
|
141
|
+
|
|
142
|
+
// Two lines, as in yeti-web: time + timezone on top (larger), year month
|
|
143
|
+
// day below (smaller). The stylesheet sizes .clock-time / .clock-date.
|
|
144
|
+
var now = new Date(serverTime + (Date.now() - clientTime));
|
|
145
|
+
var time, date;
|
|
146
|
+
if (fmt) {
|
|
147
|
+
// serverTime is an absolute instant; Intl renders it in the server's
|
|
148
|
+
// zone, offset and abbreviation recomputed per tick.
|
|
149
|
+
var p = {};
|
|
150
|
+
fmt.formatToParts(now).forEach(function (part) { p[part.type] = part.value; });
|
|
151
|
+
time = p.hour + ":" + p.minute + ":" + p.second + " " + p.timeZoneName;
|
|
152
|
+
date = p.year + " " + p.month + " " + p.day;
|
|
153
|
+
} else {
|
|
154
|
+
// UTC getters only — serverTime is a fixed epoch holding a wall clock,
|
|
155
|
+
// so the client's zone and DST rules never touch the math.
|
|
156
|
+
time = pad2(now.getUTCHours()) + ":" + pad2(now.getUTCMinutes()) + ":" +
|
|
157
|
+
pad2(now.getUTCSeconds()) + (zone ? " " + zone : "");
|
|
158
|
+
date = now.getUTCFullYear() + " " + MONTHS[now.getUTCMonth()] + " " +
|
|
159
|
+
pad2(now.getUTCDate());
|
|
160
|
+
}
|
|
161
|
+
out.time.textContent = time;
|
|
162
|
+
out.date.textContent = date;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
ready(function () {
|
|
166
|
+
sync();
|
|
167
|
+
setInterval(tick, 1000);
|
|
168
|
+
});
|
|
169
|
+
document.addEventListener("turbo:load", sync);
|
|
170
|
+
document.addEventListener("turbolinks:load", sync);
|
|
171
|
+
})();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Crisscross highlighting for ActiveAdmin index tables. Clicking a cell marks
|
|
2
|
+
// its whole row and column with class "selected", which the stylesheet paints
|
|
3
|
+
// with var(--aa-selected) — so the clicked cell sits at the centre of a
|
|
4
|
+
// highlighted cross, making it easy to trace a value to its row and column in a
|
|
5
|
+
// wide table.
|
|
6
|
+
//
|
|
7
|
+
// Uses jQuery, which ActiveAdmin loads before this file (its own
|
|
8
|
+
// checkbox-toggler is jQuery too); the rest of the theme JS is dependency-free,
|
|
9
|
+
// but this behaviour lives entirely in AA's jQuery-driven index table.
|
|
10
|
+
//
|
|
11
|
+
// AA's table-checkbox-toggler selects the row when a cell is clicked; we undo
|
|
12
|
+
// that (re-click the checkbox) so crisscross highlighting never leaves rows
|
|
13
|
+
// ticked for batch actions.
|
|
14
|
+
$(document).ready(function () {
|
|
15
|
+
var marker = 'selected';
|
|
16
|
+
|
|
17
|
+
// "Select all" clears every row marker left by AA's toggler.
|
|
18
|
+
$('table.index_table .toggle_all').click(function () {
|
|
19
|
+
$(this).closest('table').find('tr.' + marker).removeClass(marker);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Clicking the row checkbox itself must not bubble into the cell handler.
|
|
23
|
+
$('table.index_table').find(':checkbox').click(function (e) {
|
|
24
|
+
e.stopPropagation();
|
|
25
|
+
$(this).closest('tr.' + marker).removeClass(marker);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
$('table.index_table').on('click', 'td', function (e) {
|
|
29
|
+
var tdIndex = e.currentTarget.cellIndex,
|
|
30
|
+
$cell = $(e.currentTarget),
|
|
31
|
+
$table = $cell.closest('table'),
|
|
32
|
+
$row = $cell.closest('tr'),
|
|
33
|
+
$columnTds = $table.find('td:nth-child(' + (tdIndex + 1) + ')'),
|
|
34
|
+
$rowTds = $row.find('td');
|
|
35
|
+
|
|
36
|
+
// AA ticks the row checkbox on a cell click; click it again to revert.
|
|
37
|
+
$row.find(':checkbox').click();
|
|
38
|
+
|
|
39
|
+
if (!$cell.hasClass('col-selectable')) {
|
|
40
|
+
$table.find('td.' + marker).removeClass(marker);
|
|
41
|
+
$table.find('td.selected-cell').removeClass('selected-cell');
|
|
42
|
+
$columnTds.addClass(marker);
|
|
43
|
+
$rowTds.addClass(marker);
|
|
44
|
+
// Mark the clicked cell itself so the stylesheet can set it apart as the
|
|
45
|
+
// centre of the cross.
|
|
46
|
+
$cell.addClass('selected-cell');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|