plum-cms 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +178 -0
- data/README.md +64 -0
- data/app/assets/builds/tailwind.css +1 -1
- data/app/controllers/plum/cp/entries_controller.rb +88 -5
- data/app/controllers/plum/cp/static_cache_controller.rb +12 -0
- data/app/controllers/plum/form_submissions_controller.rb +13 -0
- data/app/controllers/plum/pages_controller.rb +8 -0
- data/app/controllers/plum/theme_assets_controller.rb +2 -0
- data/app/javascript/controllers/plum/write_controller.js +174 -0
- data/app/models/plum/asset.rb +1 -0
- data/app/models/plum/content_type.rb +1 -0
- data/app/models/plum/entry.rb +57 -0
- data/app/models/plum/entry_term.rb +1 -0
- data/app/models/plum/form_definition.rb +1 -0
- data/app/models/plum/global.rb +1 -0
- data/app/models/plum/nav_item.rb +1 -0
- data/app/models/plum/nav_menu.rb +1 -0
- data/app/models/plum/site.rb +2 -0
- data/app/models/plum/site_setting.rb +1 -0
- data/app/models/plum/static_cache_invalidation.rb +30 -0
- data/app/models/plum/taxonomy.rb +1 -0
- data/app/models/plum/term.rb +1 -0
- data/app/services/plum/config_sync.rb +252 -0
- data/app/services/plum/draft_diff.rb +177 -0
- data/app/services/plum/form_renderer.rb +12 -1
- data/app/services/plum/liquid_context.rb +0 -2
- data/app/views/layouts/plum/write.html.erb +140 -0
- data/app/views/plum/cp/dashboard/show.html.erb +10 -3
- data/app/views/plum/cp/entries/_form.html.erb +2 -2
- data/app/views/plum/cp/entries/diff.html.erb +40 -0
- data/app/views/plum/cp/entries/edit.html.erb +24 -0
- data/app/views/plum/cp/entries/index.html.erb +3 -0
- data/app/views/plum/cp/entries/write.html.erb +60 -0
- data/config/plum_routes.rb +5 -0
- data/db/engine_migrate/20260811090000_add_draft_data_to_plum_entries.rb +5 -0
- data/docs/config-as-code.md +103 -0
- data/docs/plum-cli.md +356 -0
- data/docs/static-caching.md +163 -0
- data/lib/generators/plum/install/templates/plum_initializer.rb +8 -0
- data/lib/plum/configuration.rb +12 -1
- data/lib/plum/engine.rb +7 -0
- data/lib/plum/static_cache/middleware.rb +61 -0
- data/lib/plum/static_cache.rb +103 -0
- data/lib/plum/version.rb +1 -1
- data/lib/tasks/plum_config.rake +48 -0
- data/lib/tasks/plum_portability.rake +54 -29
- data/lib/tasks/plum_styles.rake +14 -9
- metadata +21 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9daf7a444c4aca840d3395ef9e290708192a904b8d7effa9aa0954819ab3a046
|
|
4
|
+
data.tar.gz: 5987aaecba9e612f37eae6cebb8ad663fcd39fc0add1342762836339bbaf5a3c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f7718730fa5d92db2a89d1ef13724d2a7cb0198322ff0bb239982390f4af3125747d3261ca1a61161da64d3dd45119a37d2396181dd7631d3193294428491781
|
|
7
|
+
data.tar.gz: 9ddd57edac63fc212ac7ddfd20f3616551f84a437c72496b31d3f86d30fafdd4ac002454fd14a66e7bbe9faea6d274274e7fd69642790ded89bc29fe39c7596d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,183 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2 — 2026-08-13
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Fixed `plum:site:replace` (and site destroy generally) aborting with
|
|
8
|
+
`RecordNotDestroyed` on any site that has a homepage entry — i.e. every
|
|
9
|
+
real site. The homepage's can't-be-deleted guard now stands down when the
|
|
10
|
+
entry is being removed as part of its site's dependent cascade. Found by
|
|
11
|
+
the first live `plum pull` against production.
|
|
12
|
+
- Fixed `plum connect`'s deployment detection treating an unedited
|
|
13
|
+
`config/deploy.yml` scaffold (the placeholder file `rails new` ships) as
|
|
14
|
+
proof of a Kamal deployment, which steered Once-deployed repos toward a
|
|
15
|
+
broken `via: kamal` remote. A deploy.yml still carrying `kamal init`
|
|
16
|
+
placeholders now counts as absent.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Added the Plum CLI (`cli/`, Go): `plum pull` replaces your local site with
|
|
21
|
+
a remote's, `plum sync`/`plum check` apply and verify config-as-code files,
|
|
22
|
+
`plum backup` and `plum run` wrap the remaining rake tasks. Remotes are
|
|
23
|
+
defined per-project in `plum.yml` (`plum init`). Every command wraps an
|
|
24
|
+
engine rake task — the CLI adds transport, never behavior. See
|
|
25
|
+
`docs/plum-cli.md`.
|
|
26
|
+
- Added three transports for CLI remotes (`via: ssh|kamal|once`): plain
|
|
27
|
+
ssh/scp (default, inherits ~/.ssh/config/agent/jump hosts); `kamal`, which
|
|
28
|
+
shells out to a local Kamal binary so `config/deploy.yml` is the only
|
|
29
|
+
source of truth for where the app lives; and `once` for 37signals' Once
|
|
30
|
+
tool, whose CLI runs on the server itself — the transport ssh-es to
|
|
31
|
+
`host:` and runs `once exec <once_app> ...` there (`once_app:` is the
|
|
32
|
+
hostname you gave `once deploy --host ...`, distinct from how you reach
|
|
33
|
+
the box). Verified against a live Once deployment. Picking a transport is
|
|
34
|
+
the only thing that changes — the rake tasks behind every command are
|
|
35
|
+
identical either way.
|
|
36
|
+
- Added `plum logs [remote] [--follow]` — recent app logs, heroku-style
|
|
37
|
+
tailing with `--follow`. Plain ssh tails `log/production.log`, Kamal runs
|
|
38
|
+
`kamal app logs`, Once streams `docker logs` from the app's container
|
|
39
|
+
(resolved by Once's own container label; the Rails image logs to stdout,
|
|
40
|
+
not a file).
|
|
41
|
+
- Gave the CLI a heroku-style voice: `▸` step headers, animated spinners
|
|
42
|
+
that settle into timed `✓ ... (700ms)` result lines, remote rake output
|
|
43
|
+
framed behind a dim `│` gutter, arrow-key selects and styled prompts in
|
|
44
|
+
`plum connect`, a colorized help screen and projects list, and a plum
|
|
45
|
+
accent color adaptive to light/dark terminals (via lipgloss + huh).
|
|
46
|
+
Everything degrades to the old plain sequential output when piped, in
|
|
47
|
+
CI, under `NO_COLOR`, or with `TERM=dumb` — scripted stdin answers and
|
|
48
|
+
log parsing are unaffected; Ctrl-C at any prompt exits quietly (130).
|
|
49
|
+
- `plum connect` now answers its own questions where it can: re-runs
|
|
50
|
+
prefill every prompt from the existing plum.yml (Enter-through-everything
|
|
51
|
+
reproduces the current setup, and the stored deployment type outranks
|
|
52
|
+
file detection), and for Once remotes it asks the server itself — `once
|
|
53
|
+
list` over the just-verified SSH connection — offering the deployed apps
|
|
54
|
+
as a pick-list instead of a blank hostname prompt.
|
|
55
|
+
- On an already-configured project, bare `plum connect` skips the interview
|
|
56
|
+
entirely and becomes a health check: it prints the stored connection and
|
|
57
|
+
verifies SSH login, server tooling, and that the app is deployed, exiting
|
|
58
|
+
nonzero on failure (CI-friendly). The wizard runs only on first setup,
|
|
59
|
+
with `--reconfigure`, or when a new ip/host is passed explicitly.
|
|
60
|
+
- A brand-new project's host prompt now offers the servers you already
|
|
61
|
+
have: plum-* aliases from ~/.ssh/config (the ones `plum connect` itself
|
|
62
|
+
wrote) appear as a pick-list, so connecting a second site to the same
|
|
63
|
+
VPS never re-asks for the IP.
|
|
64
|
+
- `plum check`/`plum push` bootstrap themselves on first run: with no
|
|
65
|
+
plum/ directory in the project they offer to run
|
|
66
|
+
`bin/rails plum:config:export` right there (interactive sessions only;
|
|
67
|
+
scripts keep the hard error). Verified live: a first-time `plum check`
|
|
68
|
+
exported the content model, uploaded it through `once exec`'s stdin tar
|
|
69
|
+
pipe — the last transport path that had never run against a real
|
|
70
|
+
server — and production reported no drift.
|
|
71
|
+
- Renamed `plum sync` to `plum push`: push moves structure up, pull moves
|
|
72
|
+
content down — the familiar "push code, pull data" asymmetry, since in
|
|
73
|
+
Plum's model the content model is code. `plum sync` keeps working as an
|
|
74
|
+
alias with a gentle pointer to the new name.
|
|
75
|
+
- `plum connect` now registers the project in the global registry under
|
|
76
|
+
the directory's name (activating it when nothing is active), so
|
|
77
|
+
`plum use`/`--project` work without a separate `plum projects add`.
|
|
78
|
+
- `plum logs` prettifies structured JSON log lines on a terminal: dim
|
|
79
|
+
local-time stamps, color-coded levels and HTTP statuses, and request
|
|
80
|
+
lines compacted to `GET /up 200 (2ms)` with noisy fields dropped. Plain
|
|
81
|
+
Rails log text passes through untouched, and piped output stays raw so
|
|
82
|
+
`plum logs | jq` keeps working. The CLI is now documented in the README
|
|
83
|
+
alongside the engine features.
|
|
84
|
+
- Added `plum tutorial`: a built-in, full-screen interactive tour of Plum
|
|
85
|
+
and the CLI (bubbletea + glamour). Eight chapters — content model,
|
|
86
|
+
themes, writing mode and drafts, static caching, config as code, and
|
|
87
|
+
the CLI workflow — as styled markdown with ←/→ chapter navigation,
|
|
88
|
+
1–9 jumps, and a plain-markdown fallback when piped. Chapters slide in
|
|
89
|
+
on harmonica spring physics under an animated gradient progress bar;
|
|
90
|
+
the CLI chapters carry live demos (press `d` to watch `plum pull` or a
|
|
91
|
+
connect health check type itself out, spinners included); and the tour
|
|
92
|
+
ends with a three-question interactive quiz. Harmonica earns its name
|
|
93
|
+
throughout: the tour opens with a splash — the PLUM wordmark springs up
|
|
94
|
+
letter by letter while a plum bounces across it on projectile physics —
|
|
95
|
+
wrong quiz answers shake the question on an underdamped spring, and
|
|
96
|
+
finishing the quiz starts a continuous confetti rain — plum-palette
|
|
97
|
+
particles drifting down the finale on gentle projectile physics for as
|
|
98
|
+
long as you stay to enjoy it.
|
|
99
|
+
The splash is a proper title screen: the show loops (the plum relaunches
|
|
100
|
+
while you watch) and holds until you press a pulsing lipgloss "Start the
|
|
101
|
+
tutorial ↵" button — enter begins, q quits, nothing rushes you.
|
|
102
|
+
- Gamified the tutorial. Progress persists to ~/.config/plum/tutorial.json:
|
|
103
|
+
chapters check off as read, demos as watched, and the tour resumes where
|
|
104
|
+
you left off (the title button becomes "Continue the tutorial"). Reading,
|
|
105
|
+
watching, typing, and passing the quiz earn XP, with plum-themed ranks
|
|
106
|
+
climbing in the header (Seedling → Sprout → Sapling → Grower →
|
|
107
|
+
Orchardist). CLI chapters gained typing challenges: press `t` and type
|
|
108
|
+
the real command with strict per-character feedback — typos flash and
|
|
109
|
+
sting but don't land — and nailing it banks XP and rolls the demo as
|
|
110
|
+
your reward. And the title screen hides Plum Drop (`p`): an arcade game
|
|
111
|
+
of catching falling plums (harmonica projectiles) with an arrow-key
|
|
112
|
+
basket, combo multipliers, and a persisted high score. It's endless —
|
|
113
|
+
misses just break your streak, new bests are celebrated the moment they
|
|
114
|
+
happen, and you play until you quit. SSH-served sessions play everything with an ephemeral save.
|
|
115
|
+
- `plum tutorial --serve [addr]` hosts the tour over SSH via charm's wish:
|
|
116
|
+
`ssh -p 2222 your-server` gets the full animated tutorial with nothing
|
|
117
|
+
installed. Sessions run only the tutorial (no shell, no exec, any or no
|
|
118
|
+
key accepted); the host key persists under ~/.config/plum so returning
|
|
119
|
+
visitors aren't warned.
|
|
120
|
+
- Added a Firebase-CLI-style global project registry (`plum projects
|
|
121
|
+
add/list/remove`, `plum use`, `--project`) so a fleet of Plum sites can be
|
|
122
|
+
managed from one dev machine without `cd`-ing into each repo — a local
|
|
123
|
+
`plum.yml` still always takes precedence when one is present.
|
|
124
|
+
- Added `plum connect [ip-or-host]`: guided first-time server setup with no
|
|
125
|
+
YAML editing required. Detects or generates a local SSH key, tests and (on
|
|
126
|
+
confirmation) fixes passwordless login with `ssh-copy-id`, optionally adds
|
|
127
|
+
a `Host` alias to `~/.ssh/config`, and writes `plum.yml` from the answers.
|
|
128
|
+
It also detects the repo's deployment shape (`config/deploy.yml` → Kamal,
|
|
129
|
+
`Dockerfile` alone → Once) and asks the right questions for it, then
|
|
130
|
+
checks the server's tooling — Docker Engine for Kamal/Once, plus the
|
|
131
|
+
`once` binary for Once — offering to install whatever's missing, each
|
|
132
|
+
step gated on explicit confirmation. Deploying the app itself (images,
|
|
133
|
+
registry credentials) intentionally stays manual.
|
|
134
|
+
- Fixed destroying an entry (or its site) crashing with a foreign-key
|
|
135
|
+
violation when the entry was linked from a nav menu — nav items are now
|
|
136
|
+
removed with their entry.
|
|
137
|
+
- Added `rails plum:site:replace` — the "pull" refresh: swaps the local site
|
|
138
|
+
for a production archive in one transactional step, database-agnostic
|
|
139
|
+
(Postgres prod → SQLite dev), refusing to run in production without FORCE.
|
|
140
|
+
- Fixed Plum rake tasks running their bodies twice in the standalone repo
|
|
141
|
+
(engine railtie and application both load lib/tasks; Rake appends actions
|
|
142
|
+
on re-definition).
|
|
143
|
+
- Added config-as-code (phase 1): `Plum::ConfigSync` syncs content types and
|
|
144
|
+
fieldsets between YAML files (`plum/content_types/*.yml`) and the database
|
|
145
|
+
via `rails plum:config:export`, `plum:config:sync` (with PRUNE/FORCE
|
|
146
|
+
guards that never touch entry data), and `plum:config:check` for CI drift
|
|
147
|
+
detection. See `docs/config-as-code.md`.
|
|
148
|
+
- Added full-page static caching: rendered public pages and theme assets are
|
|
149
|
+
written to disk keyed by host + path, served without touching the database,
|
|
150
|
+
and flushed automatically when site content changes. **Off by default,
|
|
151
|
+
explicit opt-in** (`config.static_cache_enabled = true`) — it is only
|
|
152
|
+
correct on a single-server deployment; enabling it on 2+ nodes/dynos
|
|
153
|
+
silently serves stale pages. The file layout supports direct nginx/Caddy
|
|
154
|
+
`try_files` serving. See `docs/static-caching.md`.
|
|
155
|
+
- Added a "Clear page cache" control to the dashboard for administrators.
|
|
156
|
+
- Added a distraction-free writing mode for entries with a rich text field:
|
|
157
|
+
full-screen typographic surface with debounced autosave, Cmd/Ctrl+S,
|
|
158
|
+
word count, and unsaved-changes protection, reachable from the entry editor.
|
|
159
|
+
- Write-mode saves merge into existing entry data, so partial saves never
|
|
160
|
+
wipe unsubmitted blueprint fields, and identical consecutive saves no
|
|
161
|
+
longer stack duplicate revisions.
|
|
162
|
+
- Added a git-style "Review changes" screen for working drafts: field-by-field
|
|
163
|
+
word diffs of draft vs. live content (rich text compared as readable text,
|
|
164
|
+
structured fields as JSON), with publish/discard actions inline. Linked from
|
|
165
|
+
the entry editor's draft banner and the writing surface.
|
|
166
|
+
- Added working drafts for published entries: writing-mode autosaves on a
|
|
167
|
+
published entry are stored in a new `draft_data` column and never touch the
|
|
168
|
+
live content or flush its cached pages. Editors publish or discard the
|
|
169
|
+
draft explicitly (from the writing surface or the entry editor's draft
|
|
170
|
+
banner); publishing records a revision. Draft-status entries still save
|
|
171
|
+
directly.
|
|
172
|
+
|
|
173
|
+
### Changed
|
|
174
|
+
|
|
175
|
+
- Public `{% form %}` forms now use a honeypot spam trap instead of a
|
|
176
|
+
per-session CSRF token, so cached pages stay valid and public page views no
|
|
177
|
+
longer set session cookies. The control panel keeps standard CSRF
|
|
178
|
+
protection.
|
|
179
|
+
- Theme assets are served with `Cache-Control: public, max-age=3600`.
|
|
180
|
+
|
|
3
181
|
## 0.2.1 — 2026-08-07
|
|
4
182
|
|
|
5
183
|
### Added
|
data/README.md
CHANGED
|
@@ -243,6 +243,30 @@ bin/rails plum:backup:restore ARCHIVE=/var/backups/plum/site.plum.zip
|
|
|
243
243
|
See [the portability guide](docs/portability.md) for archive guarantees and
|
|
244
244
|
production backup guidance.
|
|
245
245
|
|
|
246
|
+
## The Plum CLI
|
|
247
|
+
|
|
248
|
+
`cli/` ships a Go command line that wraps the engine's rake tasks over SSH,
|
|
249
|
+
[Kamal](https://kamalmanual.com), or [Once](https://once.com) — heroku-style
|
|
250
|
+
ergonomics for self-hosted Plum sites:
|
|
251
|
+
|
|
252
|
+
```sh
|
|
253
|
+
plum tutorial # interactive guided tour, right in the terminal
|
|
254
|
+
plum connect 203.0.113.5 # guided setup: SSH keys, server tooling, plum.yml
|
|
255
|
+
plum connect # already configured? verifies the setup instead
|
|
256
|
+
plum pull # replace your local site with production's content
|
|
257
|
+
plum push # apply plum/ config-as-code files to the remote
|
|
258
|
+
plum check # fail if the remote drifted from plum/ (CI-friendly)
|
|
259
|
+
plum logs --follow # tail the app's logs, pretty-printed on a TTY
|
|
260
|
+
plum backup # timestamped site archive on the server
|
|
261
|
+
plum run -- TASK # any rake task, any remote
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
A Firebase-style project registry (`plum use`, `--project`) manages a fleet
|
|
265
|
+
of sites from one machine. Every command degrades to plain, script-friendly
|
|
266
|
+
output when piped or under `NO_COLOR`. See [the CLI guide](docs/plum-cli.md)
|
|
267
|
+
for transports (`via: ssh|kamal|once`), fleet management, and how `pull`
|
|
268
|
+
works across database engines.
|
|
269
|
+
|
|
246
270
|
## Content API
|
|
247
271
|
|
|
248
272
|
Published entries are available through a read-only, site-scoped JSON API:
|
|
@@ -287,6 +311,46 @@ Images uploaded through Lexxy use Active Storage Direct Upload and render on the
|
|
|
287
311
|
front-end as standard `<img>` tags. Text colors are resolved from Lexxy's CSS
|
|
288
312
|
variables to real color values at render time.
|
|
289
313
|
|
|
314
|
+
## Writing Mode
|
|
315
|
+
|
|
316
|
+
Entries whose content type has a rich text field get a distraction-free
|
|
317
|
+
writing surface ("Write" in the entry editor): a full-screen typographic page
|
|
318
|
+
with just the title and body, debounced autosave, Cmd/Ctrl+S, and a live word
|
|
319
|
+
count. Saves merge into the entry's existing data, so fields that aren't on
|
|
320
|
+
the writing surface are never touched.
|
|
321
|
+
|
|
322
|
+
On a **published** entry, writing-mode saves become a working draft: the live
|
|
323
|
+
version (and its cached pages) stay exactly as they are while you write, and
|
|
324
|
+
nothing goes public until you hit "Publish changes". Drafts can also be
|
|
325
|
+
published or discarded from the entry editor, and publishing records a
|
|
326
|
+
revision. Unpublished (draft-status) entries save directly, since they aren't
|
|
327
|
+
visible on the site anyway.
|
|
328
|
+
|
|
329
|
+
## Static Page Caching
|
|
330
|
+
|
|
331
|
+
Optional: rendered public pages can be written to disk and served as static
|
|
332
|
+
files — no database or Liquid work on repeat visits. Content saves flush the
|
|
333
|
+
site's cache automatically, and pages re-render lazily on their next hit. The
|
|
334
|
+
on-disk layout (`{host}/{path}/index.html`) can be served directly by nginx or
|
|
335
|
+
synced to a CDN, so page delivery scales independently of the app.
|
|
336
|
+
|
|
337
|
+
**This is only correct on a single-server deployment** and is **off by
|
|
338
|
+
default** — it must be enabled explicitly:
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
Plum.configure do |config|
|
|
342
|
+
config.static_cache_enabled = true
|
|
343
|
+
config.static_cache_path = "/var/www/site-cache"
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Enabling it on a multi-node platform (2+ Heroku dynos, 2+ Render instances,
|
|
348
|
+
any load-balanced deployment) causes visitors to silently see stale content
|
|
349
|
+
served forever from whichever node missed the invalidation — not a crash,
|
|
350
|
+
just wrong pages. Read
|
|
351
|
+
[docs/static-caching.md](docs/static-caching.md) first, especially "Should
|
|
352
|
+
you enable this?", before turning this on.
|
|
353
|
+
|
|
290
354
|
## Globals and Navigation
|
|
291
355
|
|
|
292
356
|
Globals are site-wide JSON objects (company info, social links):
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/*! tailwindcss v4.2.1 | 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-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--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-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, 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;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-900:oklch(39.6% .141 25.723);--color-amber-700:oklch(55.5% .163 48.998);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-800:oklch(47.6% .114 61.907);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-300:oklch(82.7% .119 306.383);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-900:oklch(38.1% .176 304.987);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--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);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-normal:0em;--tracking-wider:.05em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--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)}}@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,ui-sans-serif, system-ui, 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{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}button:not(:disabled),[role=button]:not([aria-disabled=true]){cursor:pointer}button:disabled,[role=button][aria-disabled=true]{cursor:not-allowed}}@layer components{.plum-blueprint-field{background:#f9fafb;border:1px solid #e5e7eb;border-radius:.75rem;grid-template-columns:minmax(0,1fr);gap:1rem;padding:1rem;display:grid}.plum-blueprint-field-header{border-bottom:1px solid #e5e7eb;justify-content:space-between;align-items:center;gap:1rem;padding-bottom:.75rem;display:flex}.plum-blueprint-field-title{color:#111827;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem;font-weight:650;overflow:hidden}.plum-blueprint-field-type{color:#6b7280;margin-left:.5rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.75rem;font-weight:400}@media (min-width:640px){.plum-blueprint-field{grid-template-columns:repeat(3,minmax(0,1fr))}}.plum-rich-text-content{outline:none;min-height:14rem}.plum-rich-text-content>*+*{margin-top:.75rem}.plum-rich-text-content h2{font-size:1.25rem;font-weight:700;line-height:1.3}.plum-rich-text-content ul{padding-left:1.5rem;list-style:outside}.plum-rich-text-content ol{padding-left:1.5rem;list-style:decimal}.plum-rich-text-content blockquote{color:#4b5563;border-left:.25rem solid #d1d5db;padding-left:1rem}.plum-rich-text-content a{color:#7c3aed;text-decoration:underline}.plum-rich-text-content p.is-editor-empty:first-child:before{color:#9ca3af;content:attr(data-placeholder);float:left;pointer-events:none;height:0}}@layer utilities{.\!visible{visibility:visible!important}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.left-0{left:calc(var(--spacing) * 0)}.col-span-full{grid-column:1/-1}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-28{margin-top:calc(var(--spacing) * 28)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-2{margin-left:calc(var(--spacing) * 2)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-square{aspect-ratio:1}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.h-32{height:calc(var(--spacing) * 32)}.h-44{height:calc(var(--spacing) * 44)}.h-full{height:100%}.min-h-16{min-height:calc(var(--spacing) * 16)}.min-h-48{min-height:calc(var(--spacing) * 48)}.min-h-full{min-height:100%}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-14{width:calc(var(--spacing) * 14)}.w-16{width:calc(var(--spacing) * 16)}.w-32{width:calc(var(--spacing) * 32)}.w-64{width:calc(var(--spacing) * 64)}.w-full{width:100%}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-full{min-width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.divide-y>: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)))}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.justify-self-start{justify-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-200{border-color:var(--color-green-200)}.border-purple-200{border-color:var(--color-purple-200)}.border-purple-300{border-color:var(--color-purple-300)}.border-purple-500{border-color:var(--color-purple-500)}.border-red-200{border-color:var(--color-red-200)}.border-red-300{border-color:var(--color-red-300)}.border-transparent{border-color:#0000}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.bg-\[\#F6F3EF\]{background-color:#f6f3ef}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-purple-50{background-color:var(--color-purple-50)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-purple-600{background-color:var(--color-purple-600)}.bg-red-50{background-color:var(--color-red-50)}.bg-white{background-color:var(--color-white)}.bg-yellow-100{background-color:var(--color-yellow-100)}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.p-0{padding:calc(var(--spacing) * 0)}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-64{padding-left:calc(var(--spacing) * 64)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-700{color:var(--color-amber-700)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.text-yellow-800{color:var(--color-yellow-800)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.line-through{text-decoration-line:line-through}.underline{text-decoration-line:underline}.shadow,.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px 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)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + 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)}.ring-purple-500{--tw-ring-color:var(--color-purple-500)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition-colors{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))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.peer-checked\:border-purple-500:is(:where(.peer):checked~*){border-color:var(--color-purple-500)}.peer-checked\:bg-purple-50:is(:where(.peer):checked~*){background-color:var(--color-purple-50)}.peer-checked\:text-purple-700:is(:where(.peer):checked~*){color:var(--color-purple-700)}.peer-focus\:ring-2:is(:where(.peer):focus~*){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + 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)}.peer-focus\:ring-purple-500:is(:where(.peer):focus~*){--tw-ring-color:var(--color-purple-500)}.peer-focus\:ring-offset-2:is(:where(.peer):focus~*){--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}@media (hover:hover){.hover\:border-purple-400:hover{border-color:var(--color-purple-400)}.hover\:border-purple-500:hover{border-color:var(--color-purple-500)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-purple-50:hover{background-color:var(--color-purple-50)}.hover\:bg-purple-100:hover{background-color:var(--color-purple-100)}.hover\:bg-purple-700:hover{background-color:var(--color-purple-700)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-purple-700:hover{color:var(--color-purple-700)}.hover\:text-purple-900:hover{color:var(--color-purple-900)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:text-red-900:hover{color:var(--color-red-900)}.hover\:shadow-md: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)}}.focus\:border-purple-500:focus{border-color:var(--color-purple-500)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + 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)}.focus\:ring-purple-500:focus{--tw-ring-color:var(--color-purple-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.has-checked\:border-purple-500:has(:checked){border-color:var(--color-purple-500)}.has-checked\:ring-2:has(:checked){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + 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)}.has-checked\:ring-purple-500:has(:checked){--tw-ring-color:var(--color-purple-500)}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media (min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-start-3{grid-column-start:3}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@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-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
|
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-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--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-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, 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;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-800:oklch(47.6% .114 61.907);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-300:oklch(82.7% .119 306.383);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-900:oklch(38.1% .176 304.987);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--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);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-normal:0em;--tracking-wider:.05em;--leading-tight:1.25;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--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)}}@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,ui-sans-serif, system-ui, 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{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}button:not(:disabled),[role=button]:not([aria-disabled=true]){cursor:pointer}button:disabled,[role=button][aria-disabled=true]{cursor:not-allowed}}@layer components{.plum-blueprint-field{background:#f9fafb;border:1px solid #e5e7eb;border-radius:.75rem;grid-template-columns:minmax(0,1fr);gap:1rem;padding:1rem;display:grid}.plum-blueprint-field-header{border-bottom:1px solid #e5e7eb;justify-content:space-between;align-items:center;gap:1rem;padding-bottom:.75rem;display:flex}.plum-blueprint-field-title{color:#111827;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem;font-weight:650;overflow:hidden}.plum-blueprint-field-type{color:#6b7280;margin-left:.5rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.75rem;font-weight:400}@media (min-width:640px){.plum-blueprint-field{grid-template-columns:repeat(3,minmax(0,1fr))}}.plum-rich-text-content{outline:none;min-height:14rem}.plum-rich-text-content>*+*{margin-top:.75rem}.plum-rich-text-content h2{font-size:1.25rem;font-weight:700;line-height:1.3}.plum-rich-text-content ul{padding-left:1.5rem;list-style:outside}.plum-rich-text-content ol{padding-left:1.5rem;list-style:decimal}.plum-rich-text-content blockquote{color:#4b5563;border-left:.25rem solid #d1d5db;padding-left:1rem}.plum-rich-text-content a{color:#7c3aed;text-decoration:underline}.plum-rich-text-content p.is-editor-empty:first-child:before{color:#9ca3af;content:attr(data-placeholder);float:left;pointer-events:none;height:0}}@layer utilities{.\!visible{visibility:visible!important}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.left-0{left:calc(var(--spacing) * 0)}.col-span-full{grid-column:1/-1}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-28{margin-top:calc(var(--spacing) * 28)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-2{margin-left:calc(var(--spacing) * 2)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-square{aspect-ratio:1}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.h-32{height:calc(var(--spacing) * 32)}.h-44{height:calc(var(--spacing) * 44)}.h-full{height:100%}.min-h-16{min-height:calc(var(--spacing) * 16)}.min-h-48{min-height:calc(var(--spacing) * 48)}.min-h-full{min-height:100%}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-14{width:calc(var(--spacing) * 14)}.w-16{width:calc(var(--spacing) * 16)}.w-32{width:calc(var(--spacing) * 32)}.w-64{width:calc(var(--spacing) * 64)}.w-full{width:100%}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-full{min-width:100%}.flex-1{flex:1}.flex-shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.divide-y>: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)))}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.justify-self-start{justify-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-200{border-color:var(--color-amber-200)}.border-amber-300{border-color:var(--color-amber-300)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-200{border-color:var(--color-green-200)}.border-purple-200{border-color:var(--color-purple-200)}.border-purple-300{border-color:var(--color-purple-300)}.border-purple-500{border-color:var(--color-purple-500)}.border-red-200{border-color:var(--color-red-200)}.border-red-300{border-color:var(--color-red-300)}.border-transparent{border-color:#0000}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.bg-\[\#F6F3EF\]{background-color:#f6f3ef}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-200{background-color:var(--color-green-200)}.bg-purple-50{background-color:var(--color-purple-50)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-purple-600{background-color:var(--color-purple-600)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-200{background-color:var(--color-red-200)}.bg-white{background-color:var(--color-white)}.bg-yellow-100{background-color:var(--color-yellow-100)}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.p-0{padding:calc(var(--spacing) * 0)}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-10{padding:calc(var(--spacing) * 10)}.px-0\.5{padding-inline:calc(var(--spacing) * .5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-64{padding-left:calc(var(--spacing) * 64)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.9375rem\]{font-size:.9375rem}.leading-7{--tw-leading:calc(var(--spacing) * 7);line-height:calc(var(--spacing) * 7)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-white{color:var(--color-white)}.text-yellow-800{color:var(--color-yellow-800)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.decoration-red-400{-webkit-text-decoration-color:var(--color-red-400);-webkit-text-decoration-color:var(--color-red-400);text-decoration-color:var(--color-red-400)}.shadow,.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px 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)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + 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)}.ring-purple-500{--tw-ring-color:var(--color-purple-500)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{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))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.peer-checked\:border-purple-500:is(:where(.peer):checked~*){border-color:var(--color-purple-500)}.peer-checked\:bg-purple-50:is(:where(.peer):checked~*){background-color:var(--color-purple-50)}.peer-checked\:text-purple-700:is(:where(.peer):checked~*){color:var(--color-purple-700)}.peer-focus\:ring-2:is(:where(.peer):focus~*){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + 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)}.peer-focus\:ring-purple-500:is(:where(.peer):focus~*){--tw-ring-color:var(--color-purple-500)}.peer-focus\:ring-offset-2:is(:where(.peer):focus~*){--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}@media (hover:hover){.hover\:border-purple-400:hover{border-color:var(--color-purple-400)}.hover\:border-purple-500:hover{border-color:var(--color-purple-500)}.hover\:bg-amber-100:hover{background-color:var(--color-amber-100)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-purple-50:hover{background-color:var(--color-purple-50)}.hover\:bg-purple-100:hover{background-color:var(--color-purple-100)}.hover\:bg-purple-700:hover{background-color:var(--color-purple-700)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-purple-700:hover{color:var(--color-purple-700)}.hover\:text-purple-900:hover{color:var(--color-purple-900)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:text-red-900:hover{color:var(--color-red-900)}.hover\:shadow-md: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)}}.focus\:border-purple-500:focus{border-color:var(--color-purple-500)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + 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)}.focus\:ring-purple-500:focus{--tw-ring-color:var(--color-purple-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.has-checked\:border-purple-500:has(:checked){border-color:var(--color-purple-500)}.has-checked\:ring-2:has(:checked){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + 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)}.has-checked\:ring-purple-500:has(:checked){--tw-ring-color:var(--color-purple-500)}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media (min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-start-3{grid-column-start:3}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@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-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
|
|
@@ -11,9 +11,9 @@ module Plum
|
|
|
11
11
|
].freeze
|
|
12
12
|
|
|
13
13
|
before_action :set_content_type
|
|
14
|
-
before_action :set_entry, only: [ :edit, :update, :destroy, :image_field, :translate ]
|
|
14
|
+
before_action :set_entry, only: [ :edit, :update, :destroy, :image_field, :translate, :write, :diff, :publish_draft, :discard_draft ]
|
|
15
15
|
before_action :set_form_collections, only: [ :new, :create, :edit, :update ]
|
|
16
|
-
before_action :require_editor, only: [ :new, :create, :edit, :update, :destroy, :image_field, :translate ]
|
|
16
|
+
before_action :require_editor, only: [ :new, :create, :edit, :update, :destroy, :image_field, :translate, :write, :diff, :publish_draft, :discard_draft ]
|
|
17
17
|
|
|
18
18
|
def index
|
|
19
19
|
@entries = @content_type.entries.order(updated_at: :desc)
|
|
@@ -38,13 +38,45 @@ module Plum
|
|
|
38
38
|
def edit
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# Distraction-free writing surface for the entry's first rich text
|
|
42
|
+
# field. Saves go through #update with write_mode, which merges the
|
|
43
|
+
# submitted fields into the existing data instead of replacing it.
|
|
44
|
+
def write
|
|
45
|
+
@field = @content_type.fields.find { |field| field["type"] == "rich_text" && field["handle"].present? }
|
|
46
|
+
unless @field
|
|
47
|
+
return redirect_to edit_cp_content_type_entry_path(@content_type, @entry),
|
|
48
|
+
alert: "This content type has no rich text field to write in"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
render layout: "plum/write"
|
|
52
|
+
end
|
|
53
|
+
|
|
41
54
|
def update
|
|
55
|
+
# Writing-mode saves on a live entry become a working draft so the
|
|
56
|
+
# published version (and its cached pages) stay untouched.
|
|
57
|
+
return save_write_mode_draft if write_mode? && @entry.published?
|
|
58
|
+
|
|
42
59
|
@entry.assign_attributes(entry_params)
|
|
60
|
+
# The full form displays draft values when a draft exists, so a form
|
|
61
|
+
# save publishes what the editor saw — the draft is no longer pending.
|
|
62
|
+
@entry.draft_data = nil unless write_mode?
|
|
43
63
|
|
|
44
64
|
if save_entry(@entry)
|
|
45
|
-
|
|
65
|
+
respond_to do |format|
|
|
66
|
+
format.html do
|
|
67
|
+
if write_mode?
|
|
68
|
+
redirect_to write_cp_content_type_entry_path(@content_type, @entry), notice: "Saved"
|
|
69
|
+
else
|
|
70
|
+
redirect_to edit_cp_content_type_entry_path(@content_type, @entry), notice: "Entry updated"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
format.json { render json: { saved: true, saved_at: Time.current.iso8601 } }
|
|
74
|
+
end
|
|
46
75
|
else
|
|
47
|
-
|
|
76
|
+
respond_to do |format|
|
|
77
|
+
format.html { render :edit, status: :unprocessable_entity }
|
|
78
|
+
format.json { render json: { saved: false, errors: @entry.errors.full_messages }, status: :unprocessable_entity }
|
|
79
|
+
end
|
|
48
80
|
end
|
|
49
81
|
end
|
|
50
82
|
|
|
@@ -68,6 +100,34 @@ module Plum
|
|
|
68
100
|
end
|
|
69
101
|
end
|
|
70
102
|
|
|
103
|
+
# Git-style review of draft vs. live content.
|
|
104
|
+
def diff
|
|
105
|
+
unless @entry.has_draft?
|
|
106
|
+
return redirect_to edit_cp_content_type_entry_path(@content_type, @entry),
|
|
107
|
+
notice: "No draft changes to review"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
@diff = Plum::DraftDiff.new(@entry)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def publish_draft
|
|
114
|
+
published = @entry.publish_draft!(editor: current_user)
|
|
115
|
+
|
|
116
|
+
respond_to do |format|
|
|
117
|
+
format.html do
|
|
118
|
+
redirect_back fallback_location: edit_cp_content_type_entry_path(@content_type, @entry),
|
|
119
|
+
notice: published ? "Draft published" : "No draft changes to publish"
|
|
120
|
+
end
|
|
121
|
+
format.json { render json: { published: published, published_at: Time.current.iso8601 } }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def discard_draft
|
|
126
|
+
@entry.discard_draft!
|
|
127
|
+
redirect_back fallback_location: edit_cp_content_type_entry_path(@content_type, @entry),
|
|
128
|
+
notice: "Draft discarded"
|
|
129
|
+
end
|
|
130
|
+
|
|
71
131
|
def destroy
|
|
72
132
|
@entry.destroy
|
|
73
133
|
redirect_to cp_content_type_entries_path(@content_type), notice: "Entry deleted"
|
|
@@ -121,11 +181,34 @@ module Plum
|
|
|
121
181
|
def entry_params
|
|
122
182
|
permitted = params.require(:entry).permit(:title, :slug, :status, :published_at, :locale)
|
|
123
183
|
if params[:entry][:data].is_a?(ActionController::Parameters)
|
|
124
|
-
|
|
184
|
+
submitted = params[:entry][:data].permit!.to_h
|
|
185
|
+
# Writing mode only submits the field being written, so merge it
|
|
186
|
+
# into the stored data rather than wiping the other fields.
|
|
187
|
+
submitted = @entry.data.to_h.merge(submitted) if write_mode? && @entry&.persisted?
|
|
188
|
+
permitted[:data] = submitted
|
|
125
189
|
end
|
|
126
190
|
permitted
|
|
127
191
|
end
|
|
128
192
|
|
|
193
|
+
def write_mode?
|
|
194
|
+
params.dig(:entry, :write_mode).present?
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def save_write_mode_draft
|
|
198
|
+
submitted = params[:entry][:data].is_a?(ActionController::Parameters) ? params[:entry][:data].permit!.to_h : {}
|
|
199
|
+
@entry.save_draft!(title: params.dig(:entry, :title), data: submitted)
|
|
200
|
+
|
|
201
|
+
respond_to do |format|
|
|
202
|
+
format.html { redirect_to write_cp_content_type_entry_path(@content_type, @entry), notice: "Draft saved" }
|
|
203
|
+
format.json { render json: { saved: true, draft: true, saved_at: Time.current.iso8601 } }
|
|
204
|
+
end
|
|
205
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
206
|
+
respond_to do |format|
|
|
207
|
+
format.html { redirect_to write_cp_content_type_entry_path(@content_type, @entry), alert: e.record.errors.full_messages.to_sentence }
|
|
208
|
+
format.json { render json: { saved: false, errors: e.record.errors.full_messages }, status: :unprocessable_entity }
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
129
212
|
def save_entry(entry)
|
|
130
213
|
saved = false
|
|
131
214
|
entry.transaction do
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
module Cp
|
|
3
|
+
class StaticCacheController < BaseController
|
|
4
|
+
before_action :require_admin
|
|
5
|
+
|
|
6
|
+
def destroy
|
|
7
|
+
Plum::StaticCache.flush_site!(current_site)
|
|
8
|
+
redirect_back fallback_location: cp_root_path, notice: "Page cache cleared"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
module Plum
|
|
2
2
|
class FormSubmissionsController < ApplicationController
|
|
3
|
+
# Public forms are served from the static cache, so they can't carry a
|
|
4
|
+
# per-session CSRF token. Submissions are unauthenticated writes guarded
|
|
5
|
+
# by the honeypot below instead.
|
|
6
|
+
skip_forgery_protection
|
|
7
|
+
|
|
3
8
|
def create
|
|
9
|
+
# Pretend success when the honeypot is filled so bots don't learn
|
|
10
|
+
# they were caught.
|
|
11
|
+
return redirect_to safe_return_path, notice: "Form submitted" if honeypot_tripped?
|
|
12
|
+
|
|
4
13
|
form_definition = current_site.form_definitions.find_by!(handle: params[:handle])
|
|
5
14
|
submission = form_definition.form_submissions.build(
|
|
6
15
|
site: current_site,
|
|
@@ -17,6 +26,10 @@ module Plum
|
|
|
17
26
|
|
|
18
27
|
private
|
|
19
28
|
|
|
29
|
+
def honeypot_tripped?
|
|
30
|
+
params.dig(:form_submission, :website).present?
|
|
31
|
+
end
|
|
32
|
+
|
|
20
33
|
def deliver_submission_notification(submission)
|
|
21
34
|
return if submission.form_definition.notification_email.blank?
|
|
22
35
|
|
|
@@ -2,6 +2,10 @@ module Plum
|
|
|
2
2
|
class PagesController < ApplicationController
|
|
3
3
|
HOMEPAGE_SLUG = "home".freeze
|
|
4
4
|
|
|
5
|
+
# Search stays dynamic (query-dependent); the middleware also refuses to
|
|
6
|
+
# store any response with a query string or a non-200 status.
|
|
7
|
+
after_action :mark_static_cacheable, only: [ :home, :localized_home, :show ]
|
|
8
|
+
|
|
5
9
|
def home
|
|
6
10
|
@site_settings = SiteSetting.instance(current_site)
|
|
7
11
|
@entry = Entry.for_site(current_site).live
|
|
@@ -91,6 +95,10 @@ module Plum
|
|
|
91
95
|
|
|
92
96
|
PER_PAGE = 12
|
|
93
97
|
|
|
98
|
+
def mark_static_cacheable
|
|
99
|
+
response.headers[Plum::StaticCache::MARKER_HEADER] = "store" if response.status == 200
|
|
100
|
+
end
|
|
101
|
+
|
|
94
102
|
def render_collection(content_type)
|
|
95
103
|
context = build_context
|
|
96
104
|
page = [ params.fetch(:page, 1).to_i, 1 ].max
|
|
@@ -6,6 +6,8 @@ module Plum
|
|
|
6
6
|
asset_path = ThemeResolver.new.find_asset(params[:theme_handle], params[:path])
|
|
7
7
|
return head :not_found unless asset_path
|
|
8
8
|
|
|
9
|
+
expires_in 1.hour, public: true
|
|
10
|
+
response.headers[Plum::StaticCache::MARKER_HEADER] = "store"
|
|
9
11
|
send_file asset_path,
|
|
10
12
|
type: Rack::Mime.mime_type(asset_path.extname, "application/octet-stream"),
|
|
11
13
|
disposition: "inline"
|