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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +143 -0
- data/app/assets/javascripts/fuji_admin/base.js +6 -0
- data/app/assets/javascripts/fuji_admin/filters.js +282 -0
- data/app/assets/javascripts/fuji_admin/floats.js +73 -0
- data/app/assets/javascripts/fuji_admin/menu.js +112 -0
- data/app/assets/javascripts/fuji_admin/palettes.js +237 -0
- data/app/assets/javascripts/fuji_admin/row_actions.js +123 -0
- data/app/assets/stylesheets/fuji_admin/_base.scss +23 -0
- data/app/assets/stylesheets/fuji_admin/_base_typography.scss +56 -0
- data/app/assets/stylesheets/fuji_admin/_grid.scss +111 -0
- data/app/assets/stylesheets/fuji_admin/_reset.scss +48 -0
- data/app/assets/stylesheets/fuji_admin/components/_buttons.scss +106 -0
- data/app/assets/stylesheets/fuji_admin/components/_comments.scss +44 -0
- data/app/assets/stylesheets/fuji_admin/components/_components.scss +22 -0
- data/app/assets/stylesheets/fuji_admin/components/_date_picker.scss +147 -0
- data/app/assets/stylesheets/fuji_admin/components/_dropdown_menu.scss +76 -0
- data/app/assets/stylesheets/fuji_admin/components/_filter_chips.scss +71 -0
- data/app/assets/stylesheets/fuji_admin/components/_filter_drawer.scss +224 -0
- data/app/assets/stylesheets/fuji_admin/components/_filter_form.scss +85 -0
- data/app/assets/stylesheets/fuji_admin/components/_flash.scss +55 -0
- data/app/assets/stylesheets/fuji_admin/components/_float_labels.scss +77 -0
- data/app/assets/stylesheets/fuji_admin/components/_inputs.scss +237 -0
- data/app/assets/stylesheets/fuji_admin/components/_menu_toggle.scss +61 -0
- data/app/assets/stylesheets/fuji_admin/components/_pagination.scss +70 -0
- data/app/assets/stylesheets/fuji_admin/components/_palette_switcher.scss +600 -0
- data/app/assets/stylesheets/fuji_admin/components/_panel.scss +44 -0
- data/app/assets/stylesheets/fuji_admin/components/_row_actions.scss +110 -0
- data/app/assets/stylesheets/fuji_admin/components/_scopes.scss +58 -0
- data/app/assets/stylesheets/fuji_admin/components/_select2.scss +194 -0
- data/app/assets/stylesheets/fuji_admin/components/_status_tag.scss +59 -0
- data/app/assets/stylesheets/fuji_admin/components/_table_tools.scss +14 -0
- data/app/assets/stylesheets/fuji_admin/components/_tables.scss +262 -0
- data/app/assets/stylesheets/fuji_admin/components/_watchlist_bar.scss +119 -0
- data/app/assets/stylesheets/fuji_admin/layouts/_footer.scss +21 -0
- data/app/assets/stylesheets/fuji_admin/layouts/_header.scss +80 -0
- data/app/assets/stylesheets/fuji_admin/layouts/_layouts.scss +7 -0
- data/app/assets/stylesheets/fuji_admin/layouts/_main_content.scss +118 -0
- data/app/assets/stylesheets/fuji_admin/layouts/_sidebar.scss +124 -0
- data/app/assets/stylesheets/fuji_admin/layouts/_sizes.scss +12 -0
- data/app/assets/stylesheets/fuji_admin/layouts/_wrapper.scss +28 -0
- data/app/assets/stylesheets/fuji_admin/mixins/_media.scss +30 -0
- data/app/assets/stylesheets/fuji_admin/mixins/_mixins.scss +2 -0
- data/app/assets/stylesheets/fuji_admin/pages/_form.scss +61 -0
- data/app/assets/stylesheets/fuji_admin/pages/_index.scss +77 -0
- data/app/assets/stylesheets/fuji_admin/pages/_login.scss +77 -0
- data/app/assets/stylesheets/fuji_admin/pages/_pages.scss +5 -0
- data/app/assets/stylesheets/fuji_admin/pages/_show.scss +19 -0
- data/app/assets/stylesheets/fuji_admin/variables/_breakpoints.scss +25 -0
- data/app/assets/stylesheets/fuji_admin/variables/_colors.scss +51 -0
- data/app/assets/stylesheets/fuji_admin/variables/_radii.scss +13 -0
- data/app/assets/stylesheets/fuji_admin/variables/_shadows.scss +10 -0
- data/app/assets/stylesheets/fuji_admin/variables/_spacing.scss +20 -0
- data/app/assets/stylesheets/fuji_admin/variables/_typography.scss +25 -0
- data/app/assets/stylesheets/fuji_admin/variables/_variables.scss +9 -0
- data/lib/fuji_admin/active_admin_patch.rb +19 -0
- data/lib/fuji_admin/configuration.rb +29 -0
- data/lib/fuji_admin/version.rb +3 -0
- data/lib/fuji_admin.rb +24 -0
- 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,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
|
+
})();
|