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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9acbb41d69f18ef3ba70a6b4da01ab1a85328e033f7479db96196205908b769a
4
- data.tar.gz: 6bb46419bd8a22379b18f25911e0a6265f362cee313b5b0ec6ea8c1e6c66e900
3
+ metadata.gz: c9fc50db0360c19eaa74317c03e490285afb8c9fcb0b02bed8adf5c529806d91
4
+ data.tar.gz: 5c7efcc79d8773432c75479bffc4cd63d3bf06cd592ba21d1decc5496faaf574
5
5
  SHA512:
6
- metadata.gz: 4111c99001eeb83c4e40ae480e30f4e04e40b91385308dc1fba03e55a9f1c2eec848543de51c0cfe716a0780a3c5d9cfa70aa9c379b85f18b24298755b252276
7
- data.tar.gz: 2637d37a333a34362e022f2e97090384150fc2d38de4131c3b6b322e999f375ab82dd0fdeb9a671f84b2870c77c6e18fb2e683fc75db8b59fa7bf4c804818359
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
+ [![Two-line server clock in the utility nav](./img/clock.png)](./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
+ [![Crisscross cell highlighting](./img/crisscross-light.png)](./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
+ [![Trix editor, light](./img/trix-light.png)](./img/trix-light.png)
293
+ [![Trix editor, dark](./img/trix-dark.png)](./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](docs/swatches/23282f.svg) `#23282f` | |
365
+ | `$skinMainSecondColor` | ![#5ea3d3](docs/swatches/5ea3d3.svg) `#5ea3d3` | |
216
366
  | `$skinBorderRadius` | `4px` | |
217
- | `$skinBorderWindowColor` | `#e6e9ee` | |
367
+ | `$skinBorderWindowColor` | ![#e6e9ee](docs/swatches/e6e9ee.svg) `#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](docs/swatches/f7f9fb.svg) `#f7f9fb` / ![#161a1e](docs/swatches/161a1e.svg) `#161a1e` | page background |
375
+ | `$skinSurfaceColor` / `$skinSurfaceColorDark` | ![#ffffff](docs/swatches/ffffff.svg) `#ffffff` / ![#25292f](docs/swatches/25292f.svg) `#25292f` | panels / cards / content |
376
+ | `$skinSurface2Color` / `$skinSurface2ColorDark` | ![#f0f2f5](docs/swatches/f0f2f5.svg) `#f0f2f5` / ![#2c3137](docs/swatches/2c3137.svg) `#2c3137` | table headers / striping / subtle fills |
377
+ | `$skinSurfaceHoverColor` / `$skinSurfaceHoverColorDark` | ![#f5f7fa](docs/swatches/f5f7fa.svg) `#f5f7fa` / ![#3f454d](docs/swatches/3f454d.svg) `#3f454d` | row / item hover |
378
+ | `$skinSelectedRowColor` / `$skinSelectedRowColorDark` | ![#d9e4ec](docs/swatches/d9e4ec.svg) `#d9e4ec` / ![#304457](docs/swatches/304457.svg) `#304457` | checked table row |
379
+ | `$skinElevatedColor` / `$skinElevatedColorDark` | `$skinSurfaceColor` / ![#363c43](docs/swatches/363c43.svg) `#363c43` | tool buttons and dropdown panels floating above the page |
380
+ | `$skinTextColor` / `$skinTextColorDark` | ![#323537](docs/swatches/323537.svg) `#323537` / ![#dde2e8](docs/swatches/dde2e8.svg) `#dde2e8` | body text |
381
+ | `$skinTextMutedColor` / `$skinTextMutedColorDark` | ![#6b7177](docs/swatches/6b7177.svg) `#6b7177` / ![#b0b8c2](docs/swatches/b0b8c2.svg) `#b0b8c2` | secondary text / axis labels |
382
+ | `$skinBorderColor` / `$skinBorderColorDark` | ![#e0e4e9](docs/swatches/e0e4e9.svg) `#e0e4e9` / ![#404750](docs/swatches/404750.svg) `#404750` | borders / grid lines |
383
+ | `$skinInputBgColor` / `$skinInputBgColorDark` | ![#ffffff](docs/swatches/ffffff.svg) `#ffffff` / ![#1e2227](docs/swatches/1e2227.svg) `#1e2227` | form control background |
384
+ | `$skinInputBorderColor` / `$skinInputBorderColorDark` | ![#c9ced4](docs/swatches/c9ced4.svg) `#c9ced4` / ![#4d555f](docs/swatches/4d555f.svg) `#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](docs/swatches/2e3236.svg) `#2e3236` | top-level current/hover pill |
391
+ | `$skinMenuPillTextColor` | ![#6cb0de](docs/swatches/6cb0de.svg) `#6cb0de` | text on that pill; follows the dropdown text so a |
392
+ | `$skinMenuPanelColor` | ![#2e3236](docs/swatches/2e3236.svg) `#2e3236` | dropdown panel bg + hover "bridge" border |
393
+ | `$skinMenuTextColor` | ![#dfe2e6](docs/swatches/dfe2e6.svg) `#dfe2e6` | dropdown item text (was: inherited #fff) |
394
+ | `$skinMenuItemHoverColor` | ![#3f454c](docs/swatches/3f454c.svg) `#3f454c` | dropdown item hover/current bg (was: none) |
395
+ | `$skinMenuItemHoverTextColor` | ![#6cb0de](docs/swatches/6cb0de.svg) `#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](docs/swatches/343c46.svg) `#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](docs/swatches/8494a8.svg) `#8494a8` / `$skinTextColorDark` | |
424
+ | `$skinTabInactiveColor` / `$skinTabInactiveColorDark` | ![#f7f9fb](docs/swatches/f7f9fb.svg) `#f7f9fb` / ![#161a1e](docs/swatches/161a1e.svg) `#161a1e` | inactive tab fill |
425
+ | `$skinActiveTabTextColor` / `$skinActiveTabTextColorDark` | `$skinMainSecondColor` / ![#7cc0ec](docs/swatches/7cc0ec.svg) `#7cc0ec` | selected tab label |
426
+ | `$skinInactiveTabTextColor` / `$skinInactiveTabTextColorDark` | ![#5e6469](docs/swatches/5e6469.svg) `#5e6469` / ![#b0b8c2](docs/swatches/b0b8c2.svg) `#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](docs/swatches/ffffff.svg) `#ffffff` | label inside a filled status tag; `empty` / `unknown` / `none` have no fill and keep `$skinTextMutedColor` |
429
+ | `$skinStatusTagNeutralColor` | ![#707681](docs/swatches/707681.svg) `#707681` | unclassified tags: `No`, protocol tags |
430
+ | `$skinStatusTagOkColor` | ![#5e7e63](docs/swatches/5e7e63.svg) `#5e7e63` | `ok` `published` `complete` `completed` `green` `yes` |
431
+ | `$skinStatusTagNoticeColor` | ![#3874d2](docs/swatches/3874d2.svg) `#3874d2` | `notice` `blue` |
432
+ | `$skinStatusTagWarnColor` | ![#9e6c15](docs/swatches/9e6c15.svg) `#9e6c15` | `warn` `warning` `orange` |
433
+ | `$skinStatusTagErrorColor` | ![#ce483b](docs/swatches/ce483b.svg) `#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](docs/swatches/ffffff.svg) `#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](docs/swatches/38678b.svg) `#38678b` / ![#7cc0ec](docs/swatches/7cc0ec.svg) `#7cc0ec` | |
452
+ | `$skinDeleteLinkColor` / `$skinDeleteLinkColorDark` | `$skinLinkColor` / ![#f49b9b](docs/swatches/f49b9b.svg) `#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
+ });