administratum 0.2.4
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/DESIGN.md +68 -0
- data/MIT-LICENSE +20 -0
- data/README.md +431 -0
- data/Rakefile +6 -0
- data/app/admin/administratum/user_admin.rb +39 -0
- data/app/assets/builds/administratum.css +2 -0
- data/app/assets/javascripts/administratum.js +250 -0
- data/app/assets/tailwind/administratum/components.css +199 -0
- data/app/assets/tailwind/administratum/index.css +8 -0
- data/app/assets/tailwind/administratum/theme.css +62 -0
- data/app/controllers/administratum/application_controller.rb +31 -0
- data/app/controllers/administratum/dashboard_controller.rb +7 -0
- data/app/controllers/administratum/page.rb +51 -0
- data/app/controllers/administratum/password_resets_controller.rb +42 -0
- data/app/controllers/administratum/resource.rb +224 -0
- data/app/controllers/administratum/sessions_controller.rb +33 -0
- data/app/helpers/administratum/application_helper.rb +11 -0
- data/app/helpers/administratum/form_helper.rb +115 -0
- data/app/helpers/administratum/island_helper.rb +16 -0
- data/app/helpers/administratum/layout_helper.rb +81 -0
- data/app/helpers/administratum/resource_helper.rb +199 -0
- data/app/mailers/administratum/application_mailer.rb +8 -0
- data/app/mailers/administratum/user_mailer.rb +11 -0
- data/app/models/administratum/application_record.rb +7 -0
- data/app/models/administratum/user.rb +13 -0
- data/app/views/administratum/dashboard/index.html.erb +16 -0
- data/app/views/administratum/fields/_belongs_to.html.erb +1 -0
- data/app/views/administratum/fields/_boolean.html.erb +1 -0
- data/app/views/administratum/fields/_date.html.erb +1 -0
- data/app/views/administratum/fields/_datetime.html.erb +1 -0
- data/app/views/administratum/fields/_email.html.erb +1 -0
- data/app/views/administratum/fields/_has_many_ids.html.erb +1 -0
- data/app/views/administratum/fields/_hidden.html.erb +1 -0
- data/app/views/administratum/fields/_island.html.erb +4 -0
- data/app/views/administratum/fields/_json.html.erb +1 -0
- data/app/views/administratum/fields/_multi_select.html.erb +1 -0
- data/app/views/administratum/fields/_number.html.erb +1 -0
- data/app/views/administratum/fields/_password.html.erb +1 -0
- data/app/views/administratum/fields/_select.html.erb +1 -0
- data/app/views/administratum/fields/_string.html.erb +1 -0
- data/app/views/administratum/fields/_textarea.html.erb +1 -0
- data/app/views/administratum/fields/_wrapper.html.erb +11 -0
- data/app/views/administratum/pages/index.html.erb +7 -0
- data/app/views/administratum/password_resets/edit.html.erb +17 -0
- data/app/views/administratum/password_resets/new.html.erb +11 -0
- data/app/views/administratum/resources/_attributes.html.erb +11 -0
- data/app/views/administratum/resources/_form.html.erb +14 -0
- data/app/views/administratum/resources/_table.html.erb +42 -0
- data/app/views/administratum/resources/edit.html.erb +7 -0
- data/app/views/administratum/resources/index.html.erb +17 -0
- data/app/views/administratum/resources/new.html.erb +7 -0
- data/app/views/administratum/resources/show.html.erb +7 -0
- data/app/views/administratum/sessions/new.html.erb +18 -0
- data/app/views/administratum/shared/_card.html.erb +9 -0
- data/app/views/administratum/shared/_filters.html.erb +43 -0
- data/app/views/administratum/shared/_flash.html.erb +9 -0
- data/app/views/administratum/shared/_form_errors.html.erb +10 -0
- data/app/views/administratum/shared/_nav.html.erb +10 -0
- data/app/views/administratum/shared/_page_header.html.erb +17 -0
- data/app/views/administratum/shared/_panel.html.erb +24 -0
- data/app/views/administratum/shared/_topbar.html.erb +29 -0
- data/app/views/administratum/user_mailer/reset_password.html.erb +4 -0
- data/app/views/administratum/user_mailer/reset_password.text.erb +7 -0
- data/app/views/layouts/administratum/application.html.erb +25 -0
- data/app/views/layouts/administratum/authentication.html.erb +32 -0
- data/config/locales/en.yml +65 -0
- data/config/routes.rb +16 -0
- data/db/migrate/20261016000000_create_administratum_users.rb +17 -0
- data/lib/administratum/configuration.rb +66 -0
- data/lib/administratum/definition.rb +283 -0
- data/lib/administratum/dsl.rb +195 -0
- data/lib/administratum/engine.rb +31 -0
- data/lib/administratum/field_types.rb +77 -0
- data/lib/administratum/page_dsl.rb +48 -0
- data/lib/administratum/paths.rb +85 -0
- data/lib/administratum/ransackable.rb +41 -0
- data/lib/administratum/registry.rb +95 -0
- data/lib/administratum/routing.rb +57 -0
- data/lib/administratum/tailwind.rb +34 -0
- data/lib/administratum/version.rb +5 -0
- data/lib/administratum.rb +43 -0
- data/lib/generators/administratum/install/install_generator.rb +42 -0
- data/lib/generators/administratum/install/templates/administratum.rb +41 -0
- data/lib/generators/administratum/resource/resource_generator.rb +30 -0
- data/lib/generators/administratum/resource/templates/resource.rb.tt +20 -0
- data/lib/tasks/administratum.rake +32 -0
- metadata +209 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: fcaca7de2a8fed0707058baa27c24e2a9c2ab553dcdbec4a8f3b953896fc28fb
|
|
4
|
+
data.tar.gz: 8d3d03d1d3faa80e0b70f326cac95d271f768e70c68c8a4b52da4c442732e408
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e447dab0a88331180f092c0a86997df9108790d4e8d5cca733b35dae7b8fc451c144e95c49a8820683c47c65b7bf7e1a6f0fe319816de6a6b4a8f8a530ee57e9
|
|
7
|
+
data.tar.gz: df80245537c76698d02d2a60b340b5dfe446872b0bdd1dee7ecce34cdba5e339826467e70d7ccb080e568fd1ca4284173967dca614f97e4f92776c6f75f5557c
|
data/DESIGN.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Administratum design system
|
|
2
|
+
|
|
3
|
+
Read this before changing any view, helper or stylesheet in the gem.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
- **Colours come only from tokens.** Never write a raw colour or a stock palette utility (`bg-blue-600`) in a view, helper or component. Use `bg-adm-primary`, `text-adm-muted`, `border-adm-border`, and so on.
|
|
8
|
+
- **Markup uses `adm-*` component classes.** They're defined in `app/assets/tailwind/administratum/components.css` with `@apply` on the tokens. Add a new component class when a pattern repeats; one-off layout tweaks can use plain utilities.
|
|
9
|
+
- **Class names must appear literally** in `app/views`, `app/helpers` or `app/assets/javascripts`, so Tailwind's `@source` finds them. Never interpolate class names.
|
|
10
|
+
- **Rebuild after changes.** Run `bin/build-css` after touching views, helpers or CSS, and commit `app/assets/builds/administratum.css`.
|
|
11
|
+
- **No inline styles or inline event handlers.** Behaviour lives in `administratum.js` and is attached through `data-adm-*` attributes.
|
|
12
|
+
|
|
13
|
+
## Tokens (`theme.css`)
|
|
14
|
+
|
|
15
|
+
| Token | Role |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `--color-adm-bg` | Page background |
|
|
18
|
+
| `--color-adm-surface` | Cards, bars, panels |
|
|
19
|
+
| `--color-adm-surface-muted` | Table headers, card footers, hover rows |
|
|
20
|
+
| `--color-adm-border` | All borders and dividers |
|
|
21
|
+
| `--color-adm-text` | Primary text |
|
|
22
|
+
| `--color-adm-muted` | Secondary text, labels in tables |
|
|
23
|
+
| `--color-adm-primary` / `--color-adm-primary-fg` | Primary buttons, active nav, links |
|
|
24
|
+
| `--color-adm-danger` / `--color-adm-danger-fg` | Destructive actions, errors, alert flash |
|
|
25
|
+
| `--color-adm-success` | Positive badges |
|
|
26
|
+
| `--color-adm-warning` | Reserved for warnings |
|
|
27
|
+
| `--color-adm-ring` | Focus rings |
|
|
28
|
+
| `--color-adm-overlay` | Offcanvas backdrop |
|
|
29
|
+
| `--radius-adm` | Corner radius for cards, inputs and buttons |
|
|
30
|
+
| `--font-adm`, `--font-adm-mono` | Body and code fonts |
|
|
31
|
+
|
|
32
|
+
Dark values are set under `:root.dark`, and under `prefers-color-scheme: dark` unless `:root.light` is set, which covers pages with JS disabled.
|
|
33
|
+
|
|
34
|
+
An inline script always puts `.dark` or `.light` on `<html>` before first paint: the stored choice from `localStorage["administratum-theme"]` if there is one, otherwise the OS preference. Until the viewer picks a theme, it follows OS changes. Host styles can therefore rely on `.dark`, for example through Tailwind's class-based `dark` variant, which `theme.css` defines.
|
|
35
|
+
|
|
36
|
+
Overrides, in order of precedence:
|
|
37
|
+
1. `config.theme` / `config.dark_theme`, rendered as an inline `<style>`.
|
|
38
|
+
2. The host app's `@theme`, in `:host` stylesheet mode.
|
|
39
|
+
3. The defaults above.
|
|
40
|
+
|
|
41
|
+
## Components (`components.css`)
|
|
42
|
+
|
|
43
|
+
| Area | Classes |
|
|
44
|
+
|---|---|
|
|
45
|
+
| Layout | `adm-container`, `adm-main`, `adm-page-body`, `adm-grid` |
|
|
46
|
+
| Chrome | `adm-topbar`, `adm-brand`, `adm-nav`, `adm-nav-link`, `adm-nav-link-active`, `adm-dropdown`, `adm-dropdown-menu`, `adm-dropdown-item` |
|
|
47
|
+
| Page header | `adm-page-header`, `adm-page-title`, `adm-page-actions`, `adm-breadcrumb`, `adm-breadcrumb-item` |
|
|
48
|
+
| Cards | `adm-card`, `adm-card-header`, `adm-card-title`, `adm-card-body`, `adm-card-footer`, `adm-tile` |
|
|
49
|
+
| Buttons | `adm-btn` plus one of `adm-btn-primary`, `-secondary`, `-danger`, `-ghost`; sizes `adm-btn-sm`, `adm-btn-block`; `adm-btn-group`, `adm-icon-btn`, `adm-link` |
|
|
50
|
+
| Tables | `adm-table-wrap`, `adm-table`, `adm-table-actions`, `adm-table-footer`, `adm-empty`, `adm-pagy` |
|
|
51
|
+
| Show page | `adm-attributes`, `adm-attribute` |
|
|
52
|
+
| Forms | `adm-form`, `adm-field`, `adm-field-horizontal`, `adm-field-control`, `adm-label`, `adm-input`, `adm-checkbox`, `adm-check`, `adm-hint`, `adm-error`, `adm-form-footer`, `adm-alert` |
|
|
53
|
+
| Feedback | `adm-badge`, `adm-badge-success`, `adm-flash`, `adm-flash-notice`, `adm-flash-alert` |
|
|
54
|
+
| Panels | `adm-offcanvas`, `adm-offcanvas-panel`, `adm-offcanvas-header`, `adm-offcanvas-body` |
|
|
55
|
+
| Auth | `adm-auth`, `adm-auth-panel`, `adm-auth-title` |
|
|
56
|
+
|
|
57
|
+
## Behaviour hooks (`administratum.js`)
|
|
58
|
+
|
|
59
|
+
| Attribute | Effect |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `data-adm-offcanvas-open="name"` | Opens `[data-adm-offcanvas="name"]` |
|
|
62
|
+
| `data-adm-offcanvas-close` | Closes the open panel; Escape does too |
|
|
63
|
+
| `data-adm-dropdown`, `-toggle`, `-menu` | Dropdowns |
|
|
64
|
+
| `data-adm-confirm="…"` (on a form) | Asks for confirmation before submitting |
|
|
65
|
+
| `data-adm-toggle="id"` | Toggles `adm-open` on the element with that id |
|
|
66
|
+
| `data-adm-theme-toggle` | Switches between light and dark |
|
|
67
|
+
| `data-adm-flash`, `data-adm-dismiss` | Flash messages auto-dismiss after 6s, or close on click |
|
|
68
|
+
| `data-administratum-island="name"` | Island mount point; rendered by the `island` helper |
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright buffpojken
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# Administratum
|
|
2
|
+
|
|
3
|
+
A Rails admin engine where every adminable resource is one file in `app/admin`.
|
|
4
|
+
You get a Tailwind v4 CRUD UI with Devise login out of the box. When the defaults
|
|
5
|
+
stop fitting, you can replace any part of it: a field, a partial, a whole page, or
|
|
6
|
+
a JS island running Vue, React or anything else.
|
|
7
|
+
|
|
8
|
+
- One class per resource, and that class is a real controller.
|
|
9
|
+
- Index, show, new, edit and delete, with filters (Ransack) and pagination (Pagy).
|
|
10
|
+
- A field DSL with built-in types, plus your own types backed by a partial.
|
|
11
|
+
- Template overrides by file location: `app/views/admin/<resource>/edit.html.erb`.
|
|
12
|
+
- Nested resources, shallow or not.
|
|
13
|
+
- Custom member and collection actions, and buttons that trigger them.
|
|
14
|
+
- Framework-agnostic JS islands, mounted with props from ERB.
|
|
15
|
+
- Styling through design tokens. Use the bundled CSS, or build it with your own Tailwind v4.
|
|
16
|
+
|
|
17
|
+
Requires Rails 8, with Propshaft or Sprockets. With Sprockets, add `//= link administratum.css` and
|
|
18
|
+
`//= link administratum.js` to `app/assets/config/manifest.js`.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem "administratum", "~> 0.2"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Switching from CodeArtifact or a git source
|
|
27
|
+
|
|
28
|
+
- Replace the `source "https://greyseal-…codeartifact…" do` block (or the `git:` line) with the line above,
|
|
29
|
+
then run `bundle update administratum`.
|
|
30
|
+
- Drop the CodeArtifact token from the Kamal `builder.secrets`, `.kamal/secrets` and the
|
|
31
|
+
`RUN --mount=type=secret` in the Dockerfile.
|
|
32
|
+
- Unset `bundle config local.administratum` if you set it (add `--global` if it was set there).
|
|
33
|
+
- To work against a local checkout, temporarily use `gem "administratum", path: "../administratum"`. Don't commit it.
|
|
34
|
+
|
|
35
|
+
### Setting up an app
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
bin/rails g administratum:install # initializer, migration, app/admin, mount at /administratum
|
|
39
|
+
bin/rails db:migrate
|
|
40
|
+
bin/rails administratum:create_user EMAIL=you@example.com
|
|
41
|
+
bin/rails g administratum:resource Post # app/admin/post_admin.rb
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Alongside ActiveAdmin
|
|
45
|
+
|
|
46
|
+
ActiveAdmin also uses `app/admin`, and removes it from Zeitwerk. Administratum skips that directory
|
|
47
|
+
with a warning in the log, so nothing breaks. Its resources then need a directory of their own, and
|
|
48
|
+
**this initializer should ship in the same deploy as the Gemfile change**:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
# config/initializers/administratum.rb
|
|
52
|
+
Administratum.configure do |config|
|
|
53
|
+
config.resource_paths = ["app/admin_next"]
|
|
54
|
+
config.view_prefix = "admin_next" # templates in app/views/admin_next/<resource>/
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Mount the engine away from ActiveAdmin's path with `bin/rails g administratum:install --path=/admin_next`.
|
|
59
|
+
Once ActiveAdmin is gone, move everything back to `app/admin` and drop both settings.
|
|
60
|
+
|
|
61
|
+
## Resources
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# app/admin/post_admin.rb
|
|
65
|
+
class PostAdmin < Administratum::Resource
|
|
66
|
+
model Post # inferred from the class name when omitted
|
|
67
|
+
title :title # display name of a record (default: title/name)
|
|
68
|
+
label "Article", plural: "Articles"
|
|
69
|
+
|
|
70
|
+
filters :title, :published, :category_id # default: the title attribute
|
|
71
|
+
filter :language, as: :select, collection: %w[en sv]
|
|
72
|
+
|
|
73
|
+
index sort: "created_at desc", per_page: 50, csv: true do
|
|
74
|
+
column :title
|
|
75
|
+
column :category # belongs_to: links to the record
|
|
76
|
+
column :published # booleans render as badges
|
|
77
|
+
column(:comments, sortable: false) { |post| post.comments.size } # runs in the view
|
|
78
|
+
column(:preview, csv: false) { |post| link_to "Preview", post } # left out of the CSV
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
form do
|
|
82
|
+
field :title, hint: "Shown in listings"
|
|
83
|
+
field :body, as: :textarea, rows: 8
|
|
84
|
+
field :published
|
|
85
|
+
field :category_id # inferred :belongs_to select
|
|
86
|
+
field :tag_ids # inferred :has_many_ids multi-select
|
|
87
|
+
field :status, as: :select, collection: -> { Post::STATUSES }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
show do # default: id, form fields, timestamps
|
|
91
|
+
attribute :title
|
|
92
|
+
attribute(:words) { |post| post.body.to_s.split.size }
|
|
93
|
+
attribute :published_at, if: ->(post) { post.published? } # row hidden otherwise
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
permit :title, :body, :published, :category_id, tag_ids: [] # default: the form fields
|
|
97
|
+
scope { |posts| posts.includes(:category) } # every query starts here
|
|
98
|
+
scope :published # index tab, calls Post.published
|
|
99
|
+
actions except: :destroy # :index :show :new :edit :destroy
|
|
100
|
+
menu label: "Articles", priority: 10 # `menu false` hides it
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A resource class is a controller (`Administratum::Resource < ActionController::Base`
|
|
105
|
+
by default). Everything you'd do in a controller works: `before_action`, `rescue_from`,
|
|
106
|
+
`helper`, overriding `resource_params` or `scoped_collection`, and so on. In views, the
|
|
107
|
+
current record is `@resource`, the index records are `@resources`, and the parent of a
|
|
108
|
+
nested resource is `@parent`.
|
|
109
|
+
|
|
110
|
+
For a namespaced model (`model ActsAsTaggableOn::Tag`), Rails derives the route key and form
|
|
111
|
+
params from the full name: `/acts_as_taggable_on_tags` and `params[:acts_as_taggable_on_tag]`.
|
|
112
|
+
Set `route :tags` for a shorter URL. The param key stays the same, so `permit` and
|
|
113
|
+
`resource_params` work as usual.
|
|
114
|
+
|
|
115
|
+
Resource files are loaded to draw routes, so the DSL never touches the database at
|
|
116
|
+
class load. Types and defaults are resolved when a request is made.
|
|
117
|
+
|
|
118
|
+
### Field types
|
|
119
|
+
|
|
120
|
+
| Type | Notes |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `string` | Default for string columns |
|
|
123
|
+
| `textarea` | `rows:` |
|
|
124
|
+
| `number` | `step:`, `min:`, `max:` |
|
|
125
|
+
| `boolean` | |
|
|
126
|
+
| `select` | `collection:` takes an array, a relation, or a lambda (optionally receiving the record); `include_blank:` |
|
|
127
|
+
| `multi_select` | Permitted as an array |
|
|
128
|
+
| `belongs_to` | Inferred for `*_id` belongs_to columns |
|
|
129
|
+
| `has_many_ids` | Inferred for `*_ids`; permitted as an array |
|
|
130
|
+
| `date` | Text input for ISO dates (YYYY-MM-DD), whatever the browser locale |
|
|
131
|
+
| `datetime` | Text input for YYYY-MM-DD HH:MM, parsed in the app time zone |
|
|
132
|
+
| `email` | |
|
|
133
|
+
| `password` | |
|
|
134
|
+
| `hidden` | |
|
|
135
|
+
| `json` | Pretty-printed textarea |
|
|
136
|
+
| `island` | Rendered by a JS island (see below): `island: "name", props: ->(record) { {...} }` |
|
|
137
|
+
|
|
138
|
+
Every field also takes these options:
|
|
139
|
+
|
|
140
|
+
| Option | Effect |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `label:` | Overrides the label. Defaults to the attribute's human name, with `tag_ids` shown as "Tags". |
|
|
143
|
+
| `hint:` | Help text shown under the input and linked to it with `aria-describedby`. |
|
|
144
|
+
| `placeholder:` | Placeholder text. On selects it becomes the blank option's text. |
|
|
145
|
+
| `required:` | Shows or hides the required marker. Defaults to whether the model validates presence. |
|
|
146
|
+
| `input_html: {}` | Extra HTML attributes on the input. |
|
|
147
|
+
|
|
148
|
+
Custom field partials get the same wiring by building their attributes with `input_options(field, form:, class: "adm-input")`.
|
|
149
|
+
|
|
150
|
+
Register your own type with a partial:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# config/initializers/administratum.rb
|
|
154
|
+
config.field_types.register :color, partial: "admin/fields/color" # param: :scalar (default)
|
|
155
|
+
config.field_types.register :tag_list, partial: "admin/fields/tags", param: :array
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```erb
|
|
159
|
+
<%# app/views/admin/fields/_color.html.erb — locals: form, field, resource, value %>
|
|
160
|
+
<%= form.color_field field.name, class: "adm-input" %>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The label, hint and errors wrapper is added for you. To restyle a built-in type,
|
|
164
|
+
shadow its partial at `app/views/administratum/fields/_<type>.html.erb`.
|
|
165
|
+
|
|
166
|
+
### Index scopes
|
|
167
|
+
|
|
168
|
+
Named scopes become tabs above the index table, each with a record count:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
class PostAdmin < Administratum::Resource
|
|
172
|
+
scope :all # Post.all
|
|
173
|
+
scope :published # Post.published
|
|
174
|
+
scope(:recent, label: "This week") { |posts| posts.where(created_at: 1.week.ago..) }
|
|
175
|
+
scope :archived, count: false # skip the count query
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- The first tab is active unless one says `default: true`. The URL carries `?scope=published`.
|
|
180
|
+
- Counts and results respect the current filters. Switching tabs keeps filters and sorting, and goes back to page 1.
|
|
181
|
+
- The CSV export follows the active tab.
|
|
182
|
+
- Labels default to `administratum.scopes.<name>` in your locale, then to the humanized name.
|
|
183
|
+
- `scope` with only a block (no name) still sets the base query for every request, not a tab.
|
|
184
|
+
- In a custom `index.html.erb`, render the tabs with `<%= index_scope_tabs %>`.
|
|
185
|
+
|
|
186
|
+
### Template overrides
|
|
187
|
+
|
|
188
|
+
Templates are looked up in `app/views/admin/<route_key>/` first, then in the engine.
|
|
189
|
+
To replace a page, add a file:
|
|
190
|
+
|
|
191
|
+
| File | Replaces |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `index.html.erb` | The index page |
|
|
194
|
+
| `show.html.erb` | The show page |
|
|
195
|
+
| `new.html.erb` / `edit.html.erb` | The whole page |
|
|
196
|
+
| `_form.html.erb` | Only the form, on both new and edit |
|
|
197
|
+
| `_table.html.erb` / `_attributes.html.erb` | The index table / the show attribute list |
|
|
198
|
+
| `<custom_action>.html.erb` | A custom action's page |
|
|
199
|
+
|
|
200
|
+
You can also point `form template: "admin/posts/editor"` at a partial elsewhere.
|
|
201
|
+
To keep these templates somewhere other than `app/views/admin` (for example while an
|
|
202
|
+
older admin still lives there), set `config.view_prefix = "admin_next"`.
|
|
203
|
+
|
|
204
|
+
Helpers available in templates:
|
|
205
|
+
|
|
206
|
+
```erb
|
|
207
|
+
<%= page_header title: "Write #{@resource.title}" do %>
|
|
208
|
+
<%= buttons_for(:show) %> <%# built-in plus custom buttons for a page %>
|
|
209
|
+
<% end %>
|
|
210
|
+
|
|
211
|
+
<%= administratum_form do |form| %> <%# form_with with the right URL and method %>
|
|
212
|
+
<%= render_fields form, except: [:body] %> <%# DSL fields %>
|
|
213
|
+
<%= render_field form, :body %> <%# a single DSL field %>
|
|
214
|
+
<%= render_field form, :notes, as: :textarea %> <%# an ad-hoc field %>
|
|
215
|
+
<%= adm_date_field form, :ends_on %> <%# ISO date input; also adm_datetime_field %>
|
|
216
|
+
<%= form.submit class: "adm-btn adm-btn-primary" %>
|
|
217
|
+
<% end %>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Path helpers:
|
|
221
|
+
- `collection_path`, `new_resource_path`, `resource_path(record)`, `edit_resource_path(record)`
|
|
222
|
+
- `resource_form_path(record)`, `action_path(:write, record)`, `parent_path`
|
|
223
|
+
- `paths_for(OtherAdmin).index`, for paths to another resource
|
|
224
|
+
|
|
225
|
+
Layout helpers: `card(title:) { }`, `island`, `record_title`, `format_value`.
|
|
226
|
+
|
|
227
|
+
### Nested resources
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
class CommentAdmin < Administratum::Resource
|
|
231
|
+
parent :post, shallow: true # association defaults to post.comments
|
|
232
|
+
end
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
- Collection routes (index, new, create) nest under the parent: `/posts/1/comments`.
|
|
236
|
+
- Shallow member routes stand alone: `/comments/5/edit`. Without `shallow:`, they also nest.
|
|
237
|
+
- Queries go through the parent's association, so a child of another parent returns 404.
|
|
238
|
+
- Breadcrumbs include the parent.
|
|
239
|
+
- Nested resources are left out of the menu. Link to them with a button (below).
|
|
240
|
+
|
|
241
|
+
One level of nesting is supported.
|
|
242
|
+
|
|
243
|
+
### Custom actions and buttons
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
class PostAdmin < Administratum::Resource
|
|
247
|
+
member_action :write # GET, renders admin/posts/write.html.erb
|
|
248
|
+
member_action :save_body, method: :patch
|
|
249
|
+
member_action :publish, method: :post
|
|
250
|
+
collection_action :export
|
|
251
|
+
collection_action :import, method: :post
|
|
252
|
+
|
|
253
|
+
button :write, on: %i[show row], style: :primary
|
|
254
|
+
button :publish, on: :show, confirm: "Publish?", if: ->(post) { !post.published? }
|
|
255
|
+
button :comments, on: :show, to: ->(post) { post_comments_path(post) } # plain link
|
|
256
|
+
button :import, on: :index, upload: ".json" # file upload panel
|
|
257
|
+
button :add_tag, on: :show, panel: "admin/posts/add_tag" # your partial in a side panel
|
|
258
|
+
|
|
259
|
+
def save_body
|
|
260
|
+
@resource.update!(body: params.require(:body))
|
|
261
|
+
render json: { body: @resource.body }
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def publish
|
|
265
|
+
@resource.update!(published: true)
|
|
266
|
+
redirect_to resource_path, notice: "Published"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def export = send_data(Post.export_json, filename: "posts.json")
|
|
270
|
+
|
|
271
|
+
def import
|
|
272
|
+
Post.import_json(params.require(:file).read)
|
|
273
|
+
redirect_to collection_path
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Button options:
|
|
279
|
+
- `on:` takes `:index`, `:show`, `:new`, `:edit` or `:row` (one button per index row).
|
|
280
|
+
- `style:` takes `:primary`, `:secondary`, `:danger` or `:ghost`.
|
|
281
|
+
- `if:` and `to:` lambdas run in the view and receive the record when they take an argument.
|
|
282
|
+
|
|
283
|
+
Non-GET buttons render as small forms, so no rails-ujs is needed.
|
|
284
|
+
|
|
285
|
+
A panel partial receives `resource:` and `button:`.
|
|
286
|
+
|
|
287
|
+
## Pages
|
|
288
|
+
|
|
289
|
+
A page is a screen that isn't CRUD on one model, like a report, a tool or a status page. It
|
|
290
|
+
lives in `app/admin` like a resource, gets a menu entry and a route, and is a controller:
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
# app/admin/reports_page.rb
|
|
294
|
+
class ReportsPage < Administratum::Page
|
|
295
|
+
label "Reports" # default: from the class name
|
|
296
|
+
menu priority: 5 # or `menu false`
|
|
297
|
+
page_action :refresh, method: :post # POST /administratum/reports/refresh
|
|
298
|
+
page_action :weekly # GET /administratum/reports/weekly
|
|
299
|
+
button :refresh, confirm: "Refresh?", style: :primary # buttons go in the page header
|
|
300
|
+
button :weekly
|
|
301
|
+
|
|
302
|
+
def index
|
|
303
|
+
@stats = Stats.compute
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def refresh
|
|
307
|
+
Stats.refresh!
|
|
308
|
+
redirect_to page_path, notice: "Refreshed"
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```erb
|
|
314
|
+
<%# app/views/admin/reports/_content.html.erb: the default index renders the header, then this %>
|
|
315
|
+
<%= card title: "Posts" do %><%= @stats[:posts] %><% end %>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
- Templates live in `app/views/admin/<route_key>/`. The route key comes from the class name
|
|
319
|
+
(`ReportsPage` is `reports`), or set it with `route :stats`.
|
|
320
|
+
- Replace the whole page with `index.html.erb`. A GET `page_action` without a method renders `<name>.html.erb`.
|
|
321
|
+
- In templates and the class: `page_path`, `action_path(:refresh)`, and `page_class`.
|
|
322
|
+
- From elsewhere: `administratum.reports_page_path` and `administratum.refresh_reports_page_path`.
|
|
323
|
+
- Pages have the same sign-in, layout, `before_action` and `helper` support as resources.
|
|
324
|
+
- Pagy is included, so `@pagy, @rows = pagy(scope)` and `<%== pagy_nav(@pagy) %>` work on pages too.
|
|
325
|
+
|
|
326
|
+
## JS islands
|
|
327
|
+
|
|
328
|
+
An island is a mount point with JSON props. What runs inside it is up to you:
|
|
329
|
+
|
|
330
|
+
```erb
|
|
331
|
+
<%= island :post_writer, props: { body: @resource.body, saveUrl: action_path(:save_body) } %>
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
// your admin JS, loaded through config.head
|
|
336
|
+
import { createApp } from "vue"
|
|
337
|
+
import PostWriter from "./PostWriter.vue"
|
|
338
|
+
|
|
339
|
+
window.Administratum.islands.register("post-writer", (el, props) => {
|
|
340
|
+
const app = createApp(PostWriter, props) // props.csrfToken is always present
|
|
341
|
+
app.mount(el)
|
|
342
|
+
return () => app.unmount() // teardown, called before Turbo swaps the page
|
|
343
|
+
})
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
- Islands mount on `DOMContentLoaded` and `turbo:load`, and never twice.
|
|
347
|
+
- If your script can run before Administratum's, queue registrations: `(window.Administratum ||= { islands: { queue: [] } }).islands.queue.push([name, fn])`.
|
|
348
|
+
- `Administratum.csrfToken()` is also available.
|
|
349
|
+
|
|
350
|
+
Load your own JS and CSS with any asset pipeline:
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
config.head = ->(view) { view.javascript_importmap_tags("admin") } # importmap
|
|
354
|
+
config.head = ->(view) { view.vite_javascript_tag("admin") } # vite_ruby
|
|
355
|
+
config.head_partial = "admin/head" # or a partial
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Styling
|
|
359
|
+
|
|
360
|
+
All colours are CSS variables (`--color-adm-*`), so theming never needs a rebuild:
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
config.theme = { primary: "#7c3aed", bg: "#fafafa" }
|
|
364
|
+
config.dark_theme = { primary: "#a78bfa" }
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
The available tokens are listed in [DESIGN.md](DESIGN.md).
|
|
368
|
+
|
|
369
|
+
**Host Tailwind v4.** If your app already builds Tailwind v4, compile Administratum into
|
|
370
|
+
your own build so tokens live in your `@theme`:
|
|
371
|
+
|
|
372
|
+
```sh
|
|
373
|
+
bin/rails administratum:tailwind:sync # writes app/assets/tailwind/administratum.gen.css (gitignore it)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
```css
|
|
377
|
+
@import "tailwindcss";
|
|
378
|
+
@import "./administratum.gen.css";
|
|
379
|
+
@theme { --color-adm-primary: var(--color-brand); }
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
config.stylesheet = :host # stop loading the bundled stylesheet
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Once the generated file exists, the sync task runs before `assets:precompile` and `vite:build`.
|
|
387
|
+
|
|
388
|
+
## Configuration
|
|
389
|
+
|
|
390
|
+
See the generated `config/initializers/administratum.rb` for every option. The main ones:
|
|
391
|
+
- `app_name`, `logo`, `dashboard`
|
|
392
|
+
- `parent_controller`, `helpers`, `per_page`
|
|
393
|
+
- `mailer_sender`, `manage_users`
|
|
394
|
+
- `head`, `head_partial`, `theme`, `stylesheet`, `field_types`
|
|
395
|
+
|
|
396
|
+
## Authentication
|
|
397
|
+
|
|
398
|
+
- **Accounts.** Admins are `Administratum::User` records (table `administratum_users`) on a separate Devise scope, `:administratum_user`. They never collide with your app's own users.
|
|
399
|
+
- **Sign-out.** Signing out of the admin leaves the app's session alone.
|
|
400
|
+
- **Password reset.** Built in. The reset form doesn't reveal whether an email exists.
|
|
401
|
+
- **Current admin.** Available in views as `current_admin`.
|
|
402
|
+
|
|
403
|
+
## Development
|
|
404
|
+
|
|
405
|
+
```sh
|
|
406
|
+
bundle install
|
|
407
|
+
bin/rails test
|
|
408
|
+
bundle exec rubocop
|
|
409
|
+
bin/build-css # rebuild app/assets/builds/administratum.css after changing views or CSS
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
The dummy app in `test/dummy` exercises each extension point:
|
|
413
|
+
- custom field type, write page with an island, and export/import (posts)
|
|
414
|
+
- shallow nesting (comments) and non-shallow nesting (attachments)
|
|
415
|
+
- custom index template (categories) and custom form partial (tags)
|
|
416
|
+
|
|
417
|
+
## Releasing
|
|
418
|
+
|
|
419
|
+
Releases go to [rubygems.org](https://rubygems.org/gems/administratum).
|
|
420
|
+
|
|
421
|
+
1. Bump `Administratum::VERSION` in `lib/administratum/version.rb`, run `bundle install` to update
|
|
422
|
+
`Gemfile.lock`, and commit and push to `main`.
|
|
423
|
+
2. In GitHub, run the **Release** workflow (Actions → Release → Run workflow).
|
|
424
|
+
|
|
425
|
+
The workflow builds the gem, tags `vX.Y.Z`, pushes the tag and pushes the gem. It uses rubygems.org
|
|
426
|
+
trusted publishing, so no API key is stored anywhere.
|
|
427
|
+
|
|
428
|
+
To release from your own machine instead, run `bundle exec rake release` with a clean working tree.
|
|
429
|
+
It asks for your rubygems.org login and an MFA code.
|
|
430
|
+
|
|
431
|
+
Host apps pick up the new version with `bundle update administratum`.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Administratum
|
|
4
|
+
# Built-in management of admin users. Disable with config.manage_users = false.
|
|
5
|
+
class UserAdmin < Administratum::Resource
|
|
6
|
+
model Administratum::User
|
|
7
|
+
title :email
|
|
8
|
+
label "Admin", plural: "Admins"
|
|
9
|
+
route :admin_users
|
|
10
|
+
menu priority: 1000
|
|
11
|
+
|
|
12
|
+
filters :email
|
|
13
|
+
index sort: "email asc" do
|
|
14
|
+
column :email
|
|
15
|
+
column :created_at
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
form do
|
|
19
|
+
field :email
|
|
20
|
+
field :password
|
|
21
|
+
field :password_confirmation, as: :password
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
show do
|
|
25
|
+
attribute :email
|
|
26
|
+
attribute :created_at
|
|
27
|
+
attribute :updated_at
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Leaving the password blank on edit keeps the current one.
|
|
33
|
+
def resource_params
|
|
34
|
+
attrs = super
|
|
35
|
+
attrs = attrs.except(:password, :password_confirmation) if action_name == "update" && attrs[:password].blank?
|
|
36
|
+
attrs
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-divide-y-reverse:0;--tw-leading:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--container-sm:24rem;--container-lg:32rem;--container-3xl:48rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-relaxed:1.625;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-adm-bg:oklch(97.5% .004 255);--color-adm-surface:oklch(100% 0 0);--color-adm-surface-muted:oklch(96.5% .006 255);--color-adm-border:oklch(91% .008 255);--color-adm-text:oklch(24% .02 260);--color-adm-muted:oklch(52% .02 260);--color-adm-primary:oklch(52% .18 262);--color-adm-primary-fg:oklch(99% 0 0);--color-adm-danger:oklch(56% .2 25);--color-adm-danger-fg:oklch(99% 0 0);--color-adm-success:oklch(58% .14 155);--color-adm-ring:oklch(52% .18 262/.35);--color-adm-overlay:oklch(20% .02 260/.45);--radius-adm:.5rem;--font-adm:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;--font-adm-mono:ui-monospace, SFMono-Regular, Menlo, monospace}:root{color-scheme:light dark}:root.light{color-scheme:light}:root.dark{color-scheme:dark;--color-adm-bg:oklch(18% .012 260);--color-adm-surface:oklch(23% .014 260);--color-adm-surface-muted:oklch(27% .014 260);--color-adm-border:oklch(33% .014 260);--color-adm-text:oklch(93% .008 260);--color-adm-muted:oklch(70% .015 260);--color-adm-primary:oklch(68% .15 262);--color-adm-primary-fg:oklch(16% .02 260);--color-adm-ring:oklch(68% .15 262/.4);--color-adm-overlay:oklch(5% 0 0/.6)}@media (prefers-color-scheme:dark){:root:not(.light){--color-adm-bg:oklch(18% .012 260);--color-adm-surface:oklch(23% .014 260);--color-adm-surface-muted:oklch(27% .014 260);--color-adm-border:oklch(33% .014 260);--color-adm-text:oklch(93% .008 260);--color-adm-muted:oklch(70% .015 260);--color-adm-primary:oklch(68% .15 262);--color-adm-primary-fg:oklch(16% .02 260);--color-adm-ring:oklch(68% .15 262/.4);--color-adm-overlay:oklch(5% 0 0/.6)}}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}.adm-body{background-color:var(--color-adm-bg);min-height:100vh;font-family:var(--font-adm);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-adm-text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.adm-scroll-lock{overflow:hidden}}@layer components{.adm-container{width:100%;max-width:var(--container-7xl);padding-inline:calc(var(--spacing) * 4);margin-inline:auto}@media (min-width:40rem){.adm-container{padding-inline:calc(var(--spacing) * 6)}}@media (min-width:64rem){.adm-container{padding-inline:calc(var(--spacing) * 8)}}.adm-main{padding-bottom:calc(var(--spacing) * 12);flex-direction:column;display:flex}.adm-page-body{gap:calc(var(--spacing) * 6);padding-top:calc(var(--spacing) * 6);flex-direction:column;display:flex}.adm-grid{gap:calc(var(--spacing) * 4);display:grid}@media (min-width:40rem){.adm-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:64rem){.adm-grid{grid-template-columns:repeat(4,minmax(0,1fr))}}.adm-sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.adm-muted{color:var(--color-adm-muted)}.adm-topbar{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-adm-border);background-color:var(--color-adm-surface)}.adm-topbar-inner{height:calc(var(--spacing) * 14);align-items:center;gap:calc(var(--spacing) * 3);display:flex}.adm-brand{align-items:center;gap:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-adm-text);text-decoration-line:none;display:flex}.adm-brand-name{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.adm-logo{height:calc(var(--spacing) * 7);width:auto}.adm-topbar-actions{align-items:center;gap:var(--spacing);margin-left:auto;display:flex}.adm-nav{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-adm-border);background-color:var(--color-adm-surface);display:none}@media (min-width:48rem){.adm-nav{display:block}}.adm-nav.adm-open{display:block}.adm-nav-list{gap:var(--spacing);padding-block:calc(var(--spacing) * 2);flex-direction:column;margin-bottom:-1px;display:flex}@media (min-width:48rem){.adm-nav-list{gap:calc(var(--spacing) * 6);flex-direction:row;padding-block:0}}.adm-nav-link{border-bottom-style:var(--tw-border-style);padding-block:calc(var(--spacing) * 3);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-adm-muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-color:#0000;border-bottom-width:2px;text-decoration-line:none;display:block}@media (hover:hover){.adm-nav-link:hover{color:var(--color-adm-text)}}.adm-nav-link-active{border-color:var(--color-adm-primary);color:var(--color-adm-text)}.adm-dropdown{position:relative}.adm-dropdown-menu{z-index:30;margin-top:calc(var(--spacing) * 2);min-width:calc(var(--spacing) * 44);border-radius:var(--radius-adm);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-adm-border);background-color:var(--color-adm-surface);padding-block:var(--spacing);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);position:absolute;right:0;overflow:hidden}.adm-dropdown-item{cursor:pointer;width:100%;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);text-align:left;color:var(--color-adm-text);display:block}@media (hover:hover){.adm-dropdown-item:hover{background-color:var(--color-adm-surface-muted)}}.adm-page-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-adm-border);background-color:var(--color-adm-surface)}.adm-page-header-inner{align-items:flex-end;gap:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 5);flex-wrap:wrap;display:flex}.adm-page-heading{flex:1;min-width:0}.adm-page-title{text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);color:var(--color-adm-text);overflow:hidden}.adm-page-actions{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.adm-breadcrumb{margin-bottom:var(--spacing);align-items:center;gap:calc(var(--spacing) * 1.5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-adm-muted);flex-wrap:wrap;display:flex}.adm-breadcrumb-item a{color:var(--color-adm-muted);text-decoration-line:none}@media (hover:hover){.adm-breadcrumb-item a:hover{color:var(--color-adm-text)}}.adm-breadcrumb-item+.adm-breadcrumb-item:before{content:"/";margin-right:calc(var(--spacing) * 1.5);color:var(--color-adm-border)}.adm-card{border-radius:var(--radius-adm);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-adm-border);background-color:var(--color-adm-surface);--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);overflow:hidden}.adm-card-header{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 4);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-adm-border);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 3.5);display:flex}.adm-card-title{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-adm-text)}.adm-card-body{padding:calc(var(--spacing) * 5)}.adm-card-footer{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-adm-border);background-color:var(--color-adm-surface-muted);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 3)}.adm-tile{gap:var(--spacing);padding:calc(var(--spacing) * 5);transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex-direction:column;text-decoration-line:none;display:flex}@media (hover:hover){.adm-tile:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.adm-tile-label{color:var(--color-adm-muted)}.adm-tile-value{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-adm-text);--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.adm-btn{cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 1.5);border-radius:var(--radius-adm);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-width:1px;text-decoration-line:none;display:inline-flex}.adm-btn:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-adm-ring);--tw-outline-style:none;outline-style:none}.adm-btn:disabled{cursor:not-allowed;opacity:.5}.adm-btn-primary{border-color:var(--color-adm-primary);background-color:var(--color-adm-primary);color:var(--color-adm-primary-fg)}@media (hover:hover){.adm-btn-primary:hover{opacity:.9}}.adm-btn-secondary{border-color:var(--color-adm-border);background-color:var(--color-adm-surface);color:var(--color-adm-text)}@media (hover:hover){.adm-btn-secondary:hover{background-color:var(--color-adm-surface-muted)}}.adm-btn-danger{border-color:var(--color-adm-border);background-color:var(--color-adm-surface);color:var(--color-adm-danger)}@media (hover:hover){.adm-btn-danger:hover{border-color:var(--color-adm-danger);background-color:var(--color-adm-danger);color:var(--color-adm-danger-fg)}}.adm-btn-ghost{color:var(--color-adm-text);background-color:#0000;border-color:#0000}@media (hover:hover){.adm-btn-ghost:hover{background-color:var(--color-adm-surface-muted)}}.adm-btn-sm{padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.adm-btn-block{width:100%}.adm-btn-group{justify-content:flex-end;align-items:center;gap:var(--spacing);display:flex}.adm-inline-form{display:inline-flex}.adm-icon-btn{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9);cursor:pointer;border-radius:var(--radius-adm);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));color:var(--color-adm-muted);justify-content:center;align-items:center;display:inline-flex}@media (hover:hover){.adm-icon-btn:hover{background-color:var(--color-adm-surface-muted);color:var(--color-adm-text)}}.adm-icon-btn:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-adm-ring);--tw-outline-style:none;outline-style:none}.adm-icon{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.adm-link{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-adm-primary);text-decoration-line:none}@media (hover:hover){.adm-link:hover{text-decoration-line:underline}}.adm-table-wrap{overflow-x:auto}.adm-table{border-collapse:collapse;text-align:left;width:100%}.adm-table th{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-adm-border);background-color:var(--color-adm-surface-muted);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide);white-space:nowrap;color:var(--color-adm-muted);text-transform:uppercase}.adm-table th a{color:var(--color-adm-muted);text-decoration-line:none}@media (hover:hover){.adm-table th a:hover{color:var(--color-adm-text)}}.adm-table td{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-adm-border);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);vertical-align:middle}.adm-table tbody tr:last-child td{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.adm-table tbody tr:hover td{background-color:var(--color-adm-surface-muted)}.adm-table-actions{text-align:right;white-space:nowrap;width:1px}.adm-table-footer{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);flex-wrap:wrap;display:flex}.adm-empty{padding-block:calc(var(--spacing) * 10);text-align:center;color:var(--color-adm-muted)}.adm-tabs{margin-bottom:calc(var(--spacing) * -2);column-gap:calc(var(--spacing) * 6);row-gap:var(--spacing);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-adm-border);flex-wrap:wrap;display:flex}.adm-tab{align-items:center;gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-adm-muted);border-color:#0000;border-bottom-width:2px;margin-bottom:-1px;text-decoration-line:none;display:inline-flex}@media (hover:hover){.adm-tab:hover{color:var(--color-adm-text)}}.adm-tab-active{border-color:var(--color-adm-primary);color:var(--color-adm-text)}.adm-tab-count{background-color:var(--color-adm-surface-muted);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-adm-muted);border-radius:3.40282e38px}.adm-pagy .pagy{align-items:center;gap:var(--spacing);display:flex}.adm-pagy .pagy a{border-radius:var(--radius-adm);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);color:var(--color-adm-text);text-decoration-line:none}@media (hover:hover){.adm-pagy .pagy a:hover{background-color:var(--color-adm-surface)}}.adm-pagy .pagy a.current,.adm-pagy .pagy a[aria-current=page]{background-color:var(--color-adm-primary);color:var(--color-adm-primary-fg)}.adm-pagy .pagy a:not([href]){color:var(--color-adm-muted)}:where(.adm-attributes>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-color:var(--color-adm-border)}.adm-attribute{gap:var(--spacing);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 3);display:grid}@media (min-width:40rem){.adm-attribute{gap:calc(var(--spacing) * 6);grid-template-columns:12rem 1fr}}.adm-attribute dt{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-adm-muted)}.adm-attribute dd{overflow-wrap:break-word;min-width:0;color:var(--color-adm-text)}.adm-form{gap:calc(var(--spacing) * 5);flex-direction:column;display:flex}.adm-form-flush{gap:0}.adm-form .field_with_errors{display:contents}.adm-form-errors{padding-inline:calc(var(--spacing) * 6);padding-top:calc(var(--spacing) * 6)}.adm-form-body{gap:calc(var(--spacing) * 6);padding:calc(var(--spacing) * 6);flex-direction:column;display:flex}.adm-field{gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}.adm-field-horizontal{gap:calc(var(--spacing) * 2)}@media (min-width:40rem){.adm-field-horizontal{column-gap:calc(var(--spacing) * 8);grid-template-columns:minmax(0,13rem) minmax(0,1fr);display:grid}}.adm-field-label{flex-direction:column;display:flex}@media (min-width:40rem){.adm-field-label{padding-top:calc(var(--spacing) * 2.5)}}.adm-field-control{width:100%;max-width:var(--container-3xl);gap:calc(var(--spacing) * 2);flex-direction:column;min-width:0;display:flex}.adm-field-row{gap:calc(var(--spacing) * 2);display:flex}.adm-label{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-adm-text)}.adm-required{margin-left:calc(var(--spacing) * .5);color:var(--color-adm-danger)}.adm-hint{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);color:var(--color-adm-muted)}.adm-error{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-adm-danger)}.adm-input{min-height:calc(var(--spacing) * 10);border-radius:var(--radius-adm);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-adm-border);background-color:var(--color-adm-surface);width:100%;padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-adm-text);--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:block}.adm-input::placeholder{color:#626975b3}@supports (color:color-mix(in lab, red, red)){.adm-input::placeholder{color:color-mix(in oklab, var(--color-adm-muted) 70%, transparent)}}.adm-input:focus{border-color:var(--color-adm-primary);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-adm-ring);--tw-outline-style:none;outline-style:none}textarea.adm-input{min-height:calc(var(--spacing) * 24);--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}select[multiple].adm-input{padding-block:calc(var(--spacing) * 1.5)}select.adm-input:not([multiple]){appearance:none;padding-right:calc(var(--spacing) * 9);background-repeat:no-repeat;background-image:linear-gradient(45deg, transparent 50%, var(--color-adm-muted) 50%), linear-gradient(135deg, var(--color-adm-muted) 50%, transparent 50%);background-position:calc(100% - 1.125rem) 52%,calc(100% - .8125rem) 52%;background-size:.3125rem .3125rem}.adm-field-invalid .adm-input{border-color:var(--color-adm-danger)}.adm-checkbox{width:calc(var(--spacing) * 4.5);height:calc(var(--spacing) * 4.5);accent-color:var(--color-adm-primary);border-radius:.25rem}@media (min-width:40rem){.adm-field-horizontal .adm-field-control>.adm-checkbox{margin-top:calc(var(--spacing) * 3)}}.adm-form-actions{align-items:center;gap:calc(var(--spacing) * 2);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-adm-border);background-color:var(--color-adm-surface-muted);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4);flex-wrap:wrap;display:flex}@media (min-width:40rem){.adm-form-actions{padding-left:16.5rem}}.adm-check{align-items:center;gap:calc(var(--spacing) * 2);color:var(--color-adm-text);display:flex}.adm-code{font-family:var(--font-adm-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.adm-form-footer{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.adm-alert{border-radius:var(--radius-adm);border-style:var(--tw-border-style);border-width:1px;border-color:#d02b3166}@supports (color:color-mix(in lab, red, red)){.adm-alert{border-color:color-mix(in oklab, var(--color-adm-danger) 40%, transparent)}}.adm-alert{background-color:#d02b311a}@supports (color:color-mix(in lab, red, red)){.adm-alert{background-color:color-mix(in oklab, var(--color-adm-danger) 10%, transparent)}}.adm-alert{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);color:var(--color-adm-text)}.adm-alert-title{margin-bottom:var(--spacing);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-adm-danger)}.adm-alert ul{padding-left:calc(var(--spacing) * 5);list-style-type:disc}.adm-badge{background-color:var(--color-adm-surface-muted);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-adm-muted);border-radius:3.40282e38px;align-items:center;display:inline-flex}.adm-badge-success{background-color:#0e925426}@supports (color:color-mix(in lab, red, red)){.adm-badge-success{background-color:color-mix(in oklab, var(--color-adm-success) 15%, transparent)}}.adm-badge-success{color:var(--color-adm-success)}.adm-offcanvas{z-index:40;position:fixed;inset:0}.adm-offcanvas-backdrop{background-color:var(--color-adm-overlay);opacity:0;transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;transition-duration:.2s;position:absolute;inset:0}.adm-offcanvas-panel{width:100%;max-width:var(--container-sm);--tw-translate-x:100%;translate:var(--tw-translate-x) var(--tw-translate-y);background-color:var(--color-adm-surface);--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;inset-block:0;flex-direction:column;transition-duration:.2s;display:flex;position:absolute;right:0}.adm-offcanvas-open .adm-offcanvas-backdrop{opacity:1}.adm-offcanvas-open .adm-offcanvas-panel{--tw-translate-x:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.adm-offcanvas-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-adm-border);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 3.5);justify-content:space-between;align-items:center;display:flex}.adm-offcanvas-body{padding:calc(var(--spacing) * 5);flex:1;overflow-y:auto}.adm-flash-stack{pointer-events:none;inset-inline:0;top:calc(var(--spacing) * 4);z-index:50;align-items:center;gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 4);flex-direction:column;display:flex;position:fixed}.adm-flash{pointer-events:auto;max-width:var(--container-lg);align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-adm);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);border-width:1px;display:flex}.adm-flash-notice{border-color:var(--color-adm-border);background-color:var(--color-adm-text);color:var(--color-adm-surface)}.adm-flash-alert{border-color:var(--color-adm-danger);background-color:var(--color-adm-danger);color:var(--color-adm-danger-fg)}.adm-flash-close{cursor:pointer;font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-leading:1;opacity:.7;line-height:1}@media (hover:hover){.adm-flash-close:hover{opacity:1}}.adm-auth{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 16);justify-content:center;align-items:center;display:flex}.adm-auth-panel{width:100%;max-width:var(--container-sm);gap:calc(var(--spacing) * 6);flex-direction:column;display:flex}.adm-auth-brand{text-align:center;font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-adm-text)}.adm-auth-title{margin-bottom:calc(var(--spacing) * 4);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-adm-text)}.adm-auth-lead{margin-bottom:calc(var(--spacing) * 4)}.adm-auth-footer{margin-top:calc(var(--spacing) * 4);text-align:center}}@layer utilities{.block{display:block}.hidden{display:none}.table{display:table}@media (min-width:48rem){.md\:hidden{display:none}}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-leading{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}
|