fuji_admin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +143 -0
  4. data/app/assets/javascripts/fuji_admin/base.js +6 -0
  5. data/app/assets/javascripts/fuji_admin/filters.js +282 -0
  6. data/app/assets/javascripts/fuji_admin/floats.js +73 -0
  7. data/app/assets/javascripts/fuji_admin/menu.js +112 -0
  8. data/app/assets/javascripts/fuji_admin/palettes.js +237 -0
  9. data/app/assets/javascripts/fuji_admin/row_actions.js +123 -0
  10. data/app/assets/stylesheets/fuji_admin/_base.scss +23 -0
  11. data/app/assets/stylesheets/fuji_admin/_base_typography.scss +56 -0
  12. data/app/assets/stylesheets/fuji_admin/_grid.scss +111 -0
  13. data/app/assets/stylesheets/fuji_admin/_reset.scss +48 -0
  14. data/app/assets/stylesheets/fuji_admin/components/_buttons.scss +106 -0
  15. data/app/assets/stylesheets/fuji_admin/components/_comments.scss +44 -0
  16. data/app/assets/stylesheets/fuji_admin/components/_components.scss +22 -0
  17. data/app/assets/stylesheets/fuji_admin/components/_date_picker.scss +147 -0
  18. data/app/assets/stylesheets/fuji_admin/components/_dropdown_menu.scss +76 -0
  19. data/app/assets/stylesheets/fuji_admin/components/_filter_chips.scss +71 -0
  20. data/app/assets/stylesheets/fuji_admin/components/_filter_drawer.scss +224 -0
  21. data/app/assets/stylesheets/fuji_admin/components/_filter_form.scss +85 -0
  22. data/app/assets/stylesheets/fuji_admin/components/_flash.scss +55 -0
  23. data/app/assets/stylesheets/fuji_admin/components/_float_labels.scss +77 -0
  24. data/app/assets/stylesheets/fuji_admin/components/_inputs.scss +237 -0
  25. data/app/assets/stylesheets/fuji_admin/components/_menu_toggle.scss +61 -0
  26. data/app/assets/stylesheets/fuji_admin/components/_pagination.scss +70 -0
  27. data/app/assets/stylesheets/fuji_admin/components/_palette_switcher.scss +600 -0
  28. data/app/assets/stylesheets/fuji_admin/components/_panel.scss +44 -0
  29. data/app/assets/stylesheets/fuji_admin/components/_row_actions.scss +110 -0
  30. data/app/assets/stylesheets/fuji_admin/components/_scopes.scss +58 -0
  31. data/app/assets/stylesheets/fuji_admin/components/_select2.scss +194 -0
  32. data/app/assets/stylesheets/fuji_admin/components/_status_tag.scss +59 -0
  33. data/app/assets/stylesheets/fuji_admin/components/_table_tools.scss +14 -0
  34. data/app/assets/stylesheets/fuji_admin/components/_tables.scss +262 -0
  35. data/app/assets/stylesheets/fuji_admin/components/_watchlist_bar.scss +119 -0
  36. data/app/assets/stylesheets/fuji_admin/layouts/_footer.scss +21 -0
  37. data/app/assets/stylesheets/fuji_admin/layouts/_header.scss +80 -0
  38. data/app/assets/stylesheets/fuji_admin/layouts/_layouts.scss +7 -0
  39. data/app/assets/stylesheets/fuji_admin/layouts/_main_content.scss +118 -0
  40. data/app/assets/stylesheets/fuji_admin/layouts/_sidebar.scss +124 -0
  41. data/app/assets/stylesheets/fuji_admin/layouts/_sizes.scss +12 -0
  42. data/app/assets/stylesheets/fuji_admin/layouts/_wrapper.scss +28 -0
  43. data/app/assets/stylesheets/fuji_admin/mixins/_media.scss +30 -0
  44. data/app/assets/stylesheets/fuji_admin/mixins/_mixins.scss +2 -0
  45. data/app/assets/stylesheets/fuji_admin/pages/_form.scss +61 -0
  46. data/app/assets/stylesheets/fuji_admin/pages/_index.scss +77 -0
  47. data/app/assets/stylesheets/fuji_admin/pages/_login.scss +77 -0
  48. data/app/assets/stylesheets/fuji_admin/pages/_pages.scss +5 -0
  49. data/app/assets/stylesheets/fuji_admin/pages/_show.scss +19 -0
  50. data/app/assets/stylesheets/fuji_admin/variables/_breakpoints.scss +25 -0
  51. data/app/assets/stylesheets/fuji_admin/variables/_colors.scss +51 -0
  52. data/app/assets/stylesheets/fuji_admin/variables/_radii.scss +13 -0
  53. data/app/assets/stylesheets/fuji_admin/variables/_shadows.scss +10 -0
  54. data/app/assets/stylesheets/fuji_admin/variables/_spacing.scss +20 -0
  55. data/app/assets/stylesheets/fuji_admin/variables/_typography.scss +25 -0
  56. data/app/assets/stylesheets/fuji_admin/variables/_variables.scss +9 -0
  57. data/lib/fuji_admin/active_admin_patch.rb +19 -0
  58. data/lib/fuji_admin/configuration.rb +29 -0
  59. data/lib/fuji_admin/version.rb +3 -0
  60. data/lib/fuji_admin.rb +24 -0
  61. metadata +124 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 84d5ee21faecdb6bc1cfc795a561d6048b78c6fc5606a1677f1f7e3f2c98255e
4
+ data.tar.gz: 752b254a7fa3769e3edc3dc7145793d1212ce9a6ff220830fb1ad42bebec468a
5
+ SHA512:
6
+ metadata.gz: f749ae04c4dbb606f8f5ee790b1af74f37fe8138ba7f71d79dc151004a0a18296784c8095da90fef088dbd23e8e0581f53f7c6986e20b2b6208667c7bbfa4b35
7
+ data.tar.gz: b9798fe929298c2b856ccb858095cd270fdd542d08a03a33e5ea7191834203118be47ce8078b1cd39769e55c6163ac008d1b1c2bf5f4af61a73edefb9dae7fe1
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dario
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # Fuji Admin
2
+
3
+ A responsive [ActiveAdmin](https://github.com/activeadmin/activeadmin) theme.
4
+ Clean card-based layout, slide-in filter drawer, float-label inputs,
5
+ row-action dropdowns, and a live palette switcher with 30 built-in palettes.
6
+
7
+ Inspired by [arctic_admin](https://github.com/cprodhomme/arctic_admin) (SCSS
8
+ structure) and [PrimeReact Verona](https://verona.primereact.org/) (visual
9
+ language).
10
+
11
+ ## Features
12
+
13
+ - **Responsive layout** — fixed header + sidebar nav at `lg+`, auto-injected
14
+ hamburger drawer below, edge-to-edge content on mobile.
15
+ - **Filter drawer** — AA's `#sidebar` is turned into a right-side slide-in
16
+ panel with a "Filters" toggle in the title bar. Includes a chip strip
17
+ above the table showing each active Ransack filter (click × to remove).
18
+ - **Float labels** on text inputs, matching the Verona feel.
19
+ - **Row-action dropdown** — 2+ row actions collapse into a single `⋯`
20
+ menu per row, rendered above any clipping parents via `position: fixed`.
21
+ - **Palette switcher** — 30 palettes (Coolors-trending), 8 of them curated
22
+ full themes that repaint surfaces + text + borders. Non-themed palettes
23
+ auto-derive tinted surfaces from the primary via `color-mix()`.
24
+ - **Refactored form chrome** — fieldset legends rendered as proper card
25
+ headers, not floating `<legend>` text over borders.
26
+ - **Full component set** — buttons, inputs, selects, Select2, jQuery UI
27
+ date picker, tables (index / attributes / summary / `table_for`),
28
+ pagination, status tags, flash banners, scopes, dropdown menus, comments,
29
+ watchlist bar.
30
+
31
+ ## Requirements
32
+
33
+ - Ruby `>= 3.1.0`
34
+ - ActiveAdmin `>= 3.0`, `< 4.0`
35
+
36
+ ## Installation
37
+
38
+ ### From GitHub (recommended)
39
+
40
+ In your host app's `Gemfile`:
41
+
42
+ ```ruby
43
+ gem "fuji_admin", github: "BarbaricCorgi/fuji_admin"
44
+ ```
45
+
46
+ Then:
47
+
48
+ ```bash
49
+ bundle install
50
+ ```
51
+
52
+ The Gemfile.lock pins the exact commit SHA, so CI / Docker / Kamal builds
53
+ are reproducible without needing to publish to RubyGems.
54
+
55
+ ### Asset imports
56
+
57
+ In `app/assets/stylesheets/active_admin.scss`:
58
+
59
+ ```scss
60
+ @import "fuji_admin/base";
61
+ ```
62
+
63
+ In `app/assets/javascripts/active_admin.js`:
64
+
65
+ ```javascript
66
+ //= require fuji_admin/base
67
+ ```
68
+
69
+ ## Configuration
70
+
71
+ Add a Rails initializer — all attributes are optional.
72
+
73
+ ```ruby
74
+ # config/initializers/fuji_admin.rb
75
+ FujiAdmin.configure do |config|
76
+ # Id of the palette to apply before any user selection is stored.
77
+ # Must match one of the ids in app/assets/javascripts/fuji_admin/palettes.js.
78
+ config.default_palette = "forest-meadow"
79
+
80
+ # Render the floating palette-picker UI. Defaults to false. Flip on in
81
+ # development to audition palettes live.
82
+ config.palette_picker = Rails.env.development?
83
+ end
84
+ ```
85
+
86
+ Configuration is surfaced to the browser via `<meta>` tags injected into
87
+ ActiveAdmin's `<head>` — no host-layout changes required.
88
+
89
+ ## Development against a host app
90
+
91
+ Clone both repos side-by-side:
92
+
93
+ ```
94
+ ~/development/
95
+ ├── fuji_admin/
96
+ └── my_admin_app/
97
+ ```
98
+
99
+ Point the host's `Gemfile` at the github source (as above), then tell
100
+ Bundler to resolve it locally so edits in `fuji_admin/` reflect immediately:
101
+
102
+ ```bash
103
+ bundle config local.fuji_admin ~/development/fuji_admin
104
+ ```
105
+
106
+ This is a per-machine config stored in `~/.bundle/config` — CI and
107
+ production never see it and fall back to the github source.
108
+
109
+ ## Customising palettes
110
+
111
+ The 30 bundled palettes live in
112
+ `app/assets/javascripts/fuji_admin/palettes.js`. Each has:
113
+
114
+ ```javascript
115
+ {
116
+ id: "navy-amber",
117
+ name: "Navy & Amber",
118
+ primary: "#219ebc",
119
+ swatch: ["#8ecae6","#219ebc","#023047","#ffb703","#fb8500"],
120
+ theme: { // optional — promotes to a full theme
121
+ surface: "#fdfcdc",
122
+ surfaceAlt: "#f0ebc5",
123
+ text: "#003844",
124
+ textMuted: "#00afb5",
125
+ border: "#e6e3b5"
126
+ }
127
+ }
128
+ ```
129
+
130
+ Palettes without a `theme` block get auto-derived surfaces + text by mixing
131
+ the `primary` with white / black at runtime via `color-mix()`, so every
132
+ palette reads as a coherent mini-theme.
133
+
134
+ ## Credits
135
+
136
+ - [arctic_admin](https://github.com/cprodhomme/arctic_admin) — SCSS
137
+ structure (variables → mixins → layouts → components → pages).
138
+ - [PrimeReact Verona](https://verona.primereact.org/) — visual language
139
+ (layout proportions, rounded cards, float labels, filter chips).
140
+
141
+ ## License
142
+
143
+ MIT — see [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,6 @@
1
+ //= require active_admin/base
2
+ //= require fuji_admin/menu
3
+ //= require fuji_admin/filters
4
+ //= require fuji_admin/floats
5
+ //= require fuji_admin/row_actions
6
+ //= require fuji_admin/palettes
@@ -0,0 +1,282 @@
1
+ // Fuji Admin — filters drawer + active-filter chip strip.
2
+ //
3
+ // On any index page with ActiveAdmin filters (sidebar containing
4
+ // `form.filter_form`):
5
+ //
6
+ // 1. Injects a "Filters" button into #titlebar_right.
7
+ // 2. Turns #sidebar into a right drawer, hidden until the button is clicked.
8
+ // 3. Parses the current URL's Ransack query params (`q[...]`) and renders
9
+ // a chip strip above the main content, with one-click removal per filter
10
+ // and a "Clear all" link.
11
+
12
+ (function () {
13
+ "use strict";
14
+
15
+ var BODY_OPEN_CLASS = "fuji-filters-open";
16
+
17
+ // --- DOM helpers --------------------------------------------------------
18
+
19
+ // AA sometimes renders the #sidebar as a child of #active_admin_content
20
+ // (resource index pages) and sometimes as a direct child of #wrapper
21
+ // (custom register_page index pages). Match either.
22
+ function filterForm() {
23
+ return document.querySelector("#sidebar form.filter_form");
24
+ }
25
+
26
+ function sidebar() {
27
+ var sb = document.querySelector("#sidebar");
28
+ return sb && sb.querySelector("form.filter_form") ? sb : null;
29
+ }
30
+
31
+ function toggleBtn() {
32
+ return document.querySelector(".fuji-filters-toggle");
33
+ }
34
+
35
+ // --- Drawer -------------------------------------------------------------
36
+
37
+ function ensureToggle() {
38
+ var rightCol = document.querySelector("#title_bar #titlebar_right");
39
+ if (!rightCol || rightCol.querySelector(".fuji-filters-toggle")) return;
40
+
41
+ var btn = document.createElement("button");
42
+ btn.type = "button";
43
+ btn.className = "fuji-filters-toggle";
44
+ btn.setAttribute("aria-label", "Open filters");
45
+ btn.innerHTML =
46
+ '<svg class="fuji-filters-toggle__icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
47
+ '<line x1="4" y1="6" x2="20" y2="6"></line>' +
48
+ '<line x1="7" y1="12" x2="17" y2="12"></line>' +
49
+ '<line x1="10" y1="18" x2="14" y2="18"></line>' +
50
+ "</svg>" +
51
+ "<span>Filters</span>" +
52
+ '<span class="fuji-filters-toggle__count" hidden></span>';
53
+ rightCol.insertBefore(btn, rightCol.firstChild);
54
+ }
55
+
56
+ function ensureDrawerHeader() {
57
+ var sb = sidebar();
58
+ if (!sb || sb.querySelector(".fuji-filters-drawer__header")) return;
59
+
60
+ var header = document.createElement("div");
61
+ header.className = "fuji-filters-drawer__header";
62
+ header.innerHTML =
63
+ '<h3 class="fuji-filters-drawer__title">Filters</h3>' +
64
+ '<button type="button" class="fuji-filters-drawer__close" aria-label="Close filters">' +
65
+ '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">' +
66
+ '<line x1="6" y1="6" x2="18" y2="18"></line>' +
67
+ '<line x1="18" y1="6" x2="6" y2="18"></line>' +
68
+ "</svg></button>";
69
+ sb.insertBefore(header, sb.firstChild);
70
+ }
71
+
72
+ function openDrawer() {
73
+ document.body.classList.add(BODY_OPEN_CLASS);
74
+ }
75
+
76
+ function closeDrawer() {
77
+ document.body.classList.remove(BODY_OPEN_CLASS);
78
+ }
79
+
80
+ function onToggleClick(e) {
81
+ e.stopPropagation();
82
+ if (document.body.classList.contains(BODY_OPEN_CLASS)) closeDrawer();
83
+ else openDrawer();
84
+ }
85
+
86
+ function onDocumentClick(e) {
87
+ if (!document.body.classList.contains(BODY_OPEN_CLASS)) return;
88
+ var sb = sidebar();
89
+ if (sb && sb.contains(e.target)) return;
90
+ var t = toggleBtn();
91
+ if (t && t.contains(e.target)) return;
92
+ closeDrawer();
93
+ }
94
+
95
+ function onKey(e) {
96
+ if (e.key === "Escape") closeDrawer();
97
+ }
98
+
99
+ // --- Active filter chips ------------------------------------------------
100
+
101
+ // Matchers Ransack commonly uses. Order matters — longer suffixes first so
102
+ // we don't misparse `_not_eq` as `_eq`.
103
+ var MATCHERS = [
104
+ { suffix: "_not_eq", op: " ≠ " },
105
+ { suffix: "_not_in", op: " not in " },
106
+ { suffix: "_gteq", op: " ≥ " },
107
+ { suffix: "_lteq", op: " ≤ " },
108
+ { suffix: "_gt", op: " > " },
109
+ { suffix: "_lt", op: " < " },
110
+ { suffix: "_cont", op: ": " },
111
+ { suffix: "_start", op: " starts " },
112
+ { suffix: "_end", op: " ends " },
113
+ { suffix: "_in", op: ": " },
114
+ { suffix: "_eq", op: ": " },
115
+ ];
116
+
117
+ function humanize(field) {
118
+ return field
119
+ .replace(/_id$/, "")
120
+ .replace(/_/g, " ")
121
+ .replace(/\b\w/g, function (c) { return c.toUpperCase(); });
122
+ }
123
+
124
+ function prettyValue(raw) {
125
+ if (raw === "1" || raw === "true") return "Yes";
126
+ if (raw === "0" || raw === "false") return "No";
127
+ return raw;
128
+ }
129
+
130
+ function parseChipKey(key) {
131
+ // Matches `q[field_suffix]` or `q[field_suffix][]`.
132
+ var m = key.match(/^q\[([^\]]+)\](?:\[\])?$/);
133
+ if (!m) return null;
134
+ var inner = m[1];
135
+ for (var i = 0; i < MATCHERS.length; i++) {
136
+ var suffix = MATCHERS[i].suffix;
137
+ if (inner.length > suffix.length && inner.slice(-suffix.length) === suffix) {
138
+ return {
139
+ field: inner.slice(0, -suffix.length),
140
+ op: MATCHERS[i].op,
141
+ };
142
+ }
143
+ }
144
+ return { field: inner, op: ": " };
145
+ }
146
+
147
+ function readChips() {
148
+ var params = new URLSearchParams(window.location.search);
149
+ var byKey = {};
150
+
151
+ params.forEach(function (value, key) {
152
+ if (!key.startsWith("q[") || value === "") return;
153
+ if (!byKey[key]) byKey[key] = [];
154
+ byKey[key].push(value);
155
+ });
156
+
157
+ var chips = [];
158
+ Object.keys(byKey).forEach(function (key) {
159
+ var parsed = parseChipKey(key);
160
+ if (!parsed) return;
161
+ var values = byKey[key].map(prettyValue).join(", ");
162
+ chips.push({
163
+ key: key,
164
+ label: humanize(parsed.field) + parsed.op + values,
165
+ });
166
+ });
167
+ return chips;
168
+ }
169
+
170
+ function buildRemoveHref(key) {
171
+ var url = new URL(window.location.href);
172
+ // Remove every entry matching this key (handles [] arrays too).
173
+ var remaining = new URLSearchParams();
174
+ url.searchParams.forEach(function (v, k) {
175
+ if (k !== key) remaining.append(k, v);
176
+ });
177
+ // Drop pagination so removing a filter doesn't land you on page 7 of 2.
178
+ remaining.delete("page");
179
+ url.search = remaining.toString();
180
+ return url.toString();
181
+ }
182
+
183
+ function buildClearAllHref() {
184
+ var url = new URL(window.location.href);
185
+ var remaining = new URLSearchParams();
186
+ url.searchParams.forEach(function (v, k) {
187
+ if (!k.startsWith("q[")) remaining.append(k, v);
188
+ });
189
+ remaining.delete("page");
190
+ url.search = remaining.toString();
191
+ return url.toString();
192
+ }
193
+
194
+ function escapeHtml(s) {
195
+ var d = document.createElement("div");
196
+ d.textContent = s;
197
+ return d.innerHTML;
198
+ }
199
+
200
+ function renderChips(chips) {
201
+ var main = document.querySelector("#main_content");
202
+ if (!main) return;
203
+
204
+ var existing = main.querySelector(".fuji-filter-chips");
205
+ if (existing) existing.remove();
206
+ if (!chips.length) return;
207
+
208
+ var strip = document.createElement("div");
209
+ strip.className = "fuji-filter-chips";
210
+
211
+ chips.forEach(function (chip) {
212
+ var pill = document.createElement("a");
213
+ pill.className = "fuji-filter-chips__pill";
214
+ pill.href = buildRemoveHref(chip.key);
215
+ pill.setAttribute("aria-label", "Remove filter: " + chip.label);
216
+ pill.innerHTML =
217
+ '<span class="fuji-filter-chips__label">' + escapeHtml(chip.label) + "</span>" +
218
+ '<span class="fuji-filter-chips__remove" aria-hidden="true">×</span>';
219
+ strip.appendChild(pill);
220
+ });
221
+
222
+ if (chips.length > 1) {
223
+ var clear = document.createElement("a");
224
+ clear.className = "fuji-filter-chips__clear";
225
+ clear.href = buildClearAllHref();
226
+ clear.textContent = "Clear all";
227
+ strip.appendChild(clear);
228
+ }
229
+
230
+ main.insertBefore(strip, main.firstChild);
231
+ }
232
+
233
+ function updateToggleCount(chips) {
234
+ var t = toggleBtn();
235
+ if (!t) return;
236
+ var badge = t.querySelector(".fuji-filters-toggle__count");
237
+ if (!badge) return;
238
+ if (chips.length) {
239
+ badge.textContent = String(chips.length);
240
+ badge.hidden = false;
241
+ t.classList.add("fuji-filters-toggle--active");
242
+ } else {
243
+ badge.hidden = true;
244
+ t.classList.remove("fuji-filters-toggle--active");
245
+ }
246
+ }
247
+
248
+ // --- Bind ---------------------------------------------------------------
249
+
250
+ function bind() {
251
+ closeDrawer(); // reset state across turbo navigations
252
+ if (!filterForm()) return;
253
+
254
+ ensureToggle();
255
+ ensureDrawerHeader();
256
+
257
+ var t = toggleBtn();
258
+ if (t) {
259
+ t.removeEventListener("click", onToggleClick);
260
+ t.addEventListener("click", onToggleClick);
261
+ }
262
+
263
+ var closeX = document.querySelector(".fuji-filters-drawer__close");
264
+ if (closeX) {
265
+ closeX.removeEventListener("click", closeDrawer);
266
+ closeX.addEventListener("click", closeDrawer);
267
+ }
268
+
269
+ document.removeEventListener("click", onDocumentClick);
270
+ document.addEventListener("click", onDocumentClick);
271
+ document.removeEventListener("keydown", onKey);
272
+ document.addEventListener("keydown", onKey);
273
+
274
+ var chips = readChips();
275
+ renderChips(chips);
276
+ updateToggleCount(chips);
277
+ }
278
+
279
+ document.addEventListener("DOMContentLoaded", bind);
280
+ document.addEventListener("turbo:load", bind);
281
+ document.addEventListener("turbolinks:load", bind);
282
+ })();
@@ -0,0 +1,73 @@
1
+ // Fuji Admin — float labels for text-like form inputs.
2
+ //
3
+ // Scans `fieldset.inputs > ol > li` wrappers with formtastic's type classes
4
+ // (.string, .email, .password, .numeric, .url, .tel, .search, .text) and
5
+ // adds:
6
+ //
7
+ // .fuji-float — always present when eligible
8
+ // .fuji-float-focused — when the inner input has focus
9
+ // .fuji-float-filled — when the inner input has a non-empty value
10
+ //
11
+ // CSS in components/_float_labels.scss reads these state classes to animate
12
+ // the label from inside the input (resting) to sitting on the top border
13
+ // (active/filled).
14
+ //
15
+ // Idempotent — wiring is guarded by a data attribute so turbo re-navigations
16
+ // don't stack listeners.
17
+
18
+ (function () {
19
+ "use strict";
20
+
21
+ var SELECTOR = [
22
+ "fieldset.inputs > ol > li.string",
23
+ "fieldset.inputs > ol > li.email",
24
+ "fieldset.inputs > ol > li.password",
25
+ "fieldset.inputs > ol > li.numeric",
26
+ "fieldset.inputs > ol > li.url",
27
+ "fieldset.inputs > ol > li.tel",
28
+ "fieldset.inputs > ol > li.search",
29
+ "fieldset.inputs > ol > li.text",
30
+ ].join(",");
31
+
32
+ function fieldOf(li) {
33
+ return li.querySelector(":scope > input, :scope > textarea");
34
+ }
35
+
36
+ function labelOf(li) {
37
+ return li.querySelector(":scope > label");
38
+ }
39
+
40
+ function sync(li, input) {
41
+ li.classList.toggle("fuji-float-filled", input.value !== "" && input.value != null);
42
+ li.classList.toggle("fuji-float-focused", document.activeElement === input);
43
+ }
44
+
45
+ function wire(li) {
46
+ if (li.dataset.fujiFloatWired === "1") return;
47
+ var input = fieldOf(li);
48
+ var label = labelOf(li);
49
+ if (!input || !label) return;
50
+
51
+ li.classList.add("fuji-float");
52
+ sync(li, input);
53
+
54
+ var handler = function () { sync(li, input); };
55
+ input.addEventListener("focus", handler);
56
+ input.addEventListener("blur", handler);
57
+ input.addEventListener("input", handler);
58
+ input.addEventListener("change", handler);
59
+
60
+ li.dataset.fujiFloatWired = "1";
61
+ }
62
+
63
+ function bind() {
64
+ // Logged-out pages (login, password reset) usually carry host-supplied
65
+ // custom templates with their own label treatment — leave them alone.
66
+ if (document.body.classList.contains("logged_out")) return;
67
+ document.querySelectorAll(SELECTOR).forEach(wire);
68
+ }
69
+
70
+ document.addEventListener("DOMContentLoaded", bind);
71
+ document.addEventListener("turbo:load", bind);
72
+ document.addEventListener("turbolinks:load", bind);
73
+ })();
@@ -0,0 +1,112 @@
1
+ // Fuji Admin — menu interactions.
2
+ //
3
+ // Responsibilities:
4
+ // * Inject a hamburger toggle into the header when one isn't present.
5
+ // * Toggle the mobile drawer (adds .tabs_open to #tabs and
6
+ // .fuji-menu-open to <body> so we can style a backdrop).
7
+ // * Close the drawer on outside click, ESC, or nav link click.
8
+ // * Toggle ActiveAdmin nested menu items (li.has_nested).
9
+ //
10
+ // Idempotent — safe to re-run on turbo:load.
11
+
12
+ (function () {
13
+ "use strict";
14
+
15
+ var MENU_OPEN_CLASS = "tabs_open";
16
+ var BODY_OPEN_CLASS = "fuji-menu-open";
17
+
18
+ function header() {
19
+ return document.querySelector("#header, .header");
20
+ }
21
+
22
+ function menu() {
23
+ return document.querySelector("#tabs");
24
+ }
25
+
26
+ function toggle() {
27
+ return document.querySelector(".fuji-menu-toggle");
28
+ }
29
+
30
+ function closeMenu() {
31
+ var m = menu();
32
+ if (m) m.classList.remove(MENU_OPEN_CLASS);
33
+ document.body.classList.remove(BODY_OPEN_CLASS);
34
+ }
35
+
36
+ function openMenu() {
37
+ var m = menu();
38
+ if (m) m.classList.add(MENU_OPEN_CLASS);
39
+ document.body.classList.add(BODY_OPEN_CLASS);
40
+ }
41
+
42
+ function ensureToggle() {
43
+ var h = header();
44
+ if (!h || toggle()) return;
45
+
46
+ var btn = document.createElement("button");
47
+ btn.type = "button";
48
+ btn.className = "fuji-menu-toggle";
49
+ btn.setAttribute("aria-label", "Toggle navigation menu");
50
+ btn.innerHTML =
51
+ '<span class="fuji-menu-toggle__bar"></span>' +
52
+ '<span class="fuji-menu-toggle__bar"></span>' +
53
+ '<span class="fuji-menu-toggle__bar"></span>';
54
+ h.insertBefore(btn, h.firstChild);
55
+ }
56
+
57
+ function onToggleClick(e) {
58
+ e.stopPropagation();
59
+ var m = menu();
60
+ if (!m) return;
61
+ if (m.classList.contains(MENU_OPEN_CLASS)) closeMenu();
62
+ else openMenu();
63
+ }
64
+
65
+ function onDocumentClick(e) {
66
+ var m = menu();
67
+ if (!m || !m.classList.contains(MENU_OPEN_CLASS)) return;
68
+ if (m.contains(e.target)) return;
69
+ var t = toggle();
70
+ if (t && t.contains(e.target)) return;
71
+ closeMenu();
72
+ }
73
+
74
+ function onKey(e) {
75
+ if (e.key === "Escape") closeMenu();
76
+ }
77
+
78
+ function onNestedClick(e) {
79
+ // `this` is the <a>; the expandable wrapper is its parent <li>.
80
+ var li = this.parentElement;
81
+ if (!li || !li.classList.contains("has_nested")) return;
82
+ e.preventDefault(); // parent anchors are `href="#"` in AA
83
+ e.stopPropagation(); // don't let onDocumentClick close the drawer
84
+ li.classList.toggle("open");
85
+ }
86
+
87
+ function bind() {
88
+ ensureToggle();
89
+
90
+ var t = toggle();
91
+ if (t) {
92
+ t.removeEventListener("click", onToggleClick);
93
+ t.addEventListener("click", onToggleClick);
94
+ }
95
+
96
+ document.removeEventListener("click", onDocumentClick);
97
+ document.addEventListener("click", onDocumentClick);
98
+
99
+ document.removeEventListener("keydown", onKey);
100
+ document.addEventListener("keydown", onKey);
101
+
102
+ var nested = document.querySelectorAll("#tabs li.has_nested > a");
103
+ nested.forEach(function (a) {
104
+ a.removeEventListener("click", onNestedClick);
105
+ a.addEventListener("click", onNestedClick);
106
+ });
107
+ }
108
+
109
+ document.addEventListener("DOMContentLoaded", bind);
110
+ document.addEventListener("turbo:load", bind);
111
+ document.addEventListener("turbolinks:load", bind);
112
+ })();