plum-cms 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +78 -0
- data/README.md +10 -2
- data/app/assets/builds/tailwind.css +1 -1
- data/app/assets/stylesheets/plum/comments.css +1 -0
- data/app/controllers/plum/cp/comments_controller.rb +55 -0
- data/app/controllers/plum/cp/content_types_controller.rb +1 -1
- data/app/controllers/plum/cp/entries_controller.rb +1 -1
- data/app/controllers/plum/discussions_controller.rb +46 -0
- data/app/helpers/plum/comments_helper.rb +20 -0
- data/app/models/plum/comment.rb +40 -0
- data/app/models/plum/comment_moderation.rb +7 -0
- data/app/models/plum/comment_report.rb +7 -0
- data/app/models/plum/commentable.rb +25 -0
- data/app/models/plum/discussion.rb +17 -0
- data/app/models/plum/entry.rb +11 -0
- data/app/models/plum/site.rb +1 -0
- data/app/services/plum/comments/context.rb +70 -0
- data/app/services/plum/comments/rate_limited.rb +5 -0
- data/app/services/plum/comments/theme_renderer.rb +66 -0
- data/app/services/plum/config_sync.rb +3 -0
- data/app/services/plum/liquid_context.rb +8 -0
- data/app/services/plum/liquid_renderer.rb +8 -0
- data/app/services/plum/site_archive.rb +4 -4
- data/app/themes/default/layouts/base.liquid +1 -1
- data/app/themes/default/templates/comments/_comment.liquid +8 -0
- data/app/themes/default/templates/comments/_form.liquid +7 -0
- data/app/themes/default/templates/comments/_report.liquid +6 -0
- data/app/themes/default/templates/comments/show.liquid +15 -0
- data/app/themes/default/templates/entries/_default.liquid +1 -0
- data/app/themes/default/templates/entries/landing.liquid +1 -0
- data/app/themes/default/templates/entries/pages.liquid +1 -0
- data/app/themes/default/templates/entries/posts.liquid +1 -0
- data/app/views/layouts/plum/cp.html.erb +4 -1
- data/app/views/layouts/plum/discussion.html.erb +5 -0
- data/app/views/layouts/plum/session.html.erb +1 -1
- data/app/views/layouts/plum/write.html.erb +1 -1
- data/app/views/plum/cp/comments/index.html.erb +16 -0
- data/app/views/plum/cp/content_types/_form.html.erb +3 -0
- data/app/views/plum/cp/entries/_form.html.erb +3 -0
- data/app/views/plum/discussions/_comments.html.erb +13 -0
- data/app/views/plum/discussions/_form.html.erb +6 -0
- data/app/views/plum/discussions/show.html.erb +19 -0
- data/config/plum_routes.rb +6 -0
- data/db/engine_migrate/20261005190000_create_plum_comments.rb +48 -0
- data/docs/comments.md +140 -0
- data/docs/plum-cli.md +66 -0
- data/docs/releases/0.3.0-validation.md +15 -0
- data/docs/zero-to-agency.md +172 -0
- data/lib/generators/plum/install/install_generator.rb +13 -1
- data/lib/plum/comments/configuration.rb +43 -0
- data/lib/plum/configuration.rb +3 -1
- data/lib/plum/engine.rb +11 -4
- data/lib/plum/version.rb +1 -1
- data/lib/plum.rb +7 -0
- metadata +36 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1614aa5469fd66d91d9448f3de3d628fba36a3f9eaf9a65022d2b2c41f219a6d
|
|
4
|
+
data.tar.gz: 2d3e683c92c8c0bc7836325ec9c33b06536657d1ac69ef4fa9724b0ac711b27b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c36eb99f0788c3f71a9a3bf6317573f125b63e3d3b92c4aa7f1a8d48d8019d65658eaf81063dbb03c685b474882d66b722c7b3746ed0c2f488e4ab520b698b7
|
|
7
|
+
data.tar.gz: 825e2363d7202e728f41b7fa32f733159fc74ad346142394a2d5fae1ef11a06e32f9fd2e6eb7bac6770958c4bc701bd282cb3959913b16a4d603cb02fd185f79
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-10-05
|
|
4
|
+
|
|
5
|
+
- Add opt-in discussions for Plum entries and registered host models, with independent reader authentication and tenant/content authorization.
|
|
6
|
+
- Add moderated plain-text comments, one-level replies, reporting, pinning, close/reopen controls, and moderation audit records.
|
|
7
|
+
- Add theme-inherited Liquid templates and customizable Rails views/helpers, after-commit notification hooks, and content-type/entry discussion settings.
|
|
8
|
+
- Upgrade: install engine migrations and migrate on every upgrade to 0.3.0, even when comments are disabled. See `docs/comments.md` for host authorization and backup requirements.
|
|
9
|
+
|
|
10
|
+
## 0.2.3 — 2026-09-19
|
|
11
|
+
|
|
12
|
+
- `plum deploy`'s health wait no longer requires a Dockerfile HEALTHCHECK:
|
|
13
|
+
images without one are probed via HTTP /up through the proxy instead
|
|
14
|
+
(resolved to loopback so SNI routing matches).
|
|
15
|
+
- Added `plum promote`: go-live for a preview, in place. The preview app
|
|
16
|
+
is re-homed onto the production domain via `once update --host` — same
|
|
17
|
+
container, same database, all content built during the preview — with
|
|
18
|
+
TLS provisioned on the spot. Preflights preview existence, hostname
|
|
19
|
+
collisions, and DNS. Verified live: a hostname re-home round-trip on a
|
|
20
|
+
running app served on the new domain within seconds, data intact.
|
|
21
|
+
- Added `plum deploy --preview` — the agency preview workflow: ship the
|
|
22
|
+
same image as a separate Once app under a preview hostname, shareable
|
|
23
|
+
with a client before their domain points anywhere. Uses `preview_host:`
|
|
24
|
+
from plum.yml (wildcard-record agency domains) or auto-derives
|
|
25
|
+
`<app>.<server-ip>.sslip.io` with zero DNS setup. Previews are
|
|
26
|
+
independent apps: they never touch the live site, and going live later
|
|
27
|
+
is a plain `plum deploy`.
|
|
28
|
+
- `plum deploy` now preflights DNS on first deploys: Once verifies
|
|
29
|
+
https://<hostname> as its final step and rolls the app back when the
|
|
30
|
+
domain doesn't reach the server, so the CLI checks A records against
|
|
31
|
+
the server's addresses before building anything and asks before
|
|
32
|
+
proceeding (proxied DNS like Cloudflare legitimately mismatches, hence
|
|
33
|
+
a confirm rather than a refusal).
|
|
34
|
+
- Fixed the install generator mounting Plum ABOVE the app's own routes:
|
|
35
|
+
Rails' `route` helper prepends, so an at-"/" mount shadowed everything
|
|
36
|
+
below it — including `get "up"`, the health check that load balancers
|
|
37
|
+
and deploy tooling (Once, kamal-proxy, plum deploy) probe, which made
|
|
38
|
+
freshly scaffolded sites fail health checks with Plum's 404 page. The
|
|
39
|
+
mount is now appended at the bottom of routes.rb with a comment
|
|
40
|
+
explaining why it must stay last. Found by the first real `plum new` →
|
|
41
|
+
`plum deploy` run.
|
|
42
|
+
|
|
43
|
+
- Fixed `plum connect` in a directory with no plum.yml silently targeting
|
|
44
|
+
the globally active project (running it from a fresh repo could show —
|
|
45
|
+
or with --reconfigure, rewrite — a different project's connection).
|
|
46
|
+
Connect now always means "set up the current directory"; only an
|
|
47
|
+
explicit `--project NAME` redirects it.
|
|
48
|
+
- Connect now finishes the journey: when the once app isn't deployed yet
|
|
49
|
+
it offers to build and ship right there (chains into plum deploy), and
|
|
50
|
+
connecting a project always makes it the active one — working in a
|
|
51
|
+
directory means that's the project you're "in". A brand-new site's app
|
|
52
|
+
question also now defaults to "a new app on this server (type its
|
|
53
|
+
hostname)" instead of pre-selecting an already-deployed app, which
|
|
54
|
+
would have silently pointed the repo at another site's container.
|
|
55
|
+
- Untangled project vs. remote in connect's wording: the wizard now asks
|
|
56
|
+
for an "Environment name" (production, staging — plum.yml's remotes)
|
|
57
|
+
with a line explaining the concept, and the closing hint no longer
|
|
58
|
+
prints the nonsensical `plum pull --project production` — it points at
|
|
59
|
+
`plum pull` from the directory, or `--project <site>` from anywhere.
|
|
60
|
+
Terminology now spelled out in docs/plum-cli.md: projects are sites,
|
|
61
|
+
remotes are environments.
|
|
62
|
+
- Connect's server pick-list now knows every stored server, not just the
|
|
63
|
+
plum-* SSH aliases: hosts from every registered project's remotes are
|
|
64
|
+
merged in (annotated "— used by <project>"), so a server appears even
|
|
65
|
+
when its alias offer was declined.
|
|
66
|
+
- Added `plum new NAME`: scaffold a deploy-ready Plum site in one command
|
|
67
|
+
— rails new, the plum-cms gem, the install generator mounted at `/`, a
|
|
68
|
+
prepared database, and a git history. Bootstraps its own toolchain:
|
|
69
|
+
missing Ruby or Rails are offered for install (mise → ruby → rails),
|
|
70
|
+
each step confirm-gated. Refuses clearly when run inside an existing
|
|
71
|
+
Rails app, and registers the new project in the global registry.
|
|
72
|
+
- Added `plum deploy [remote]`: registry-free deploys — build the image
|
|
73
|
+
locally for linux/amd64, stream it to the server over SSH
|
|
74
|
+
(docker save | docker load; no registry, no CI, no accounts), roll the
|
|
75
|
+
Once app to it, and wait for the container health check. Auto-update is
|
|
76
|
+
disabled for these apps: deploys are explicit, never by surprise.
|
|
77
|
+
- Added docs/zero-to-agency.md and a matching tutorial chapter: empty
|
|
78
|
+
directory → client sites on a $6 DigitalOcean droplet with `plum new`,
|
|
79
|
+
`plum connect`, `plum deploy`, and the pull/push/backup rhythm.
|
|
80
|
+
|
|
3
81
|
## 0.2.2 — 2026-08-13
|
|
4
82
|
|
|
5
83
|
### Fixed
|
data/README.md
CHANGED
|
@@ -9,7 +9,7 @@ larger Rails app.
|
|
|
9
9
|
Add Plum to your Gemfile:
|
|
10
10
|
|
|
11
11
|
```ruby
|
|
12
|
-
gem "plum-cms", "~> 0.
|
|
12
|
+
gem "plum-cms", "~> 0.3.0"
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Then:
|
|
@@ -251,7 +251,9 @@ ergonomics for self-hosted Plum sites:
|
|
|
251
251
|
|
|
252
252
|
```sh
|
|
253
253
|
plum tutorial # interactive guided tour, right in the terminal
|
|
254
|
+
plum new client-site # scaffold a deploy-ready site (installs Rails if needed)
|
|
254
255
|
plum connect 203.0.113.5 # guided setup: SSH keys, server tooling, plum.yml
|
|
256
|
+
plum deploy # build locally, ship over SSH — no registry, no CI
|
|
255
257
|
plum connect # already configured? verifies the setup instead
|
|
256
258
|
plum pull # replace your local site with production's content
|
|
257
259
|
plum push # apply plum/ config-as-code files to the remote
|
|
@@ -265,7 +267,9 @@ A Firebase-style project registry (`plum use`, `--project`) manages a fleet
|
|
|
265
267
|
of sites from one machine. Every command degrades to plain, script-friendly
|
|
266
268
|
output when piped or under `NO_COLOR`. See [the CLI guide](docs/plum-cli.md)
|
|
267
269
|
for transports (`via: ssh|kamal|once`), fleet management, and how `pull`
|
|
268
|
-
works across database engines
|
|
270
|
+
works across database engines — and [the zero-to-agency
|
|
271
|
+
guide](docs/zero-to-agency.md) for running client sites on a single
|
|
272
|
+
cheap VPS with no registry, CI, or PaaS.
|
|
269
273
|
|
|
270
274
|
## Content API
|
|
271
275
|
|
|
@@ -398,3 +402,7 @@ bin/rails server
|
|
|
398
402
|
Login: `admin@example.com` / `password`
|
|
399
403
|
|
|
400
404
|
For Bagel Boy demo content: `bin/rails runner "load 'db/seeds/bagel_boy.rb'"`
|
|
405
|
+
|
|
406
|
+
### Optional comments
|
|
407
|
+
|
|
408
|
+
Add moderated discussions to Plum entries or your own Rails models, with host-owned authentication and customizable views. See [Comments and discussions](docs/comments.md).
|
|
@@ -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;--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}
|
|
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}.my-4{margin-block:calc(var(--spacing) * 4)}.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}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.plum-discussion{max-width:760px;margin:3rem auto;padding:0 1.25rem;font:inherit;color:inherit}.plum-discussion a{color:inherit;text-decoration:underline}.plum-discussion h1{font-size:var(--plum-discussion-title-size, 2rem);line-height:1.2}.plum-comment{border-top:1px solid var(--plum-comment-border, currentColor);padding:1.5rem 0}.plum-comment-body{white-space:pre-wrap;overflow-wrap:anywhere}.plum-discussion textarea{display:block;box-sizing:border-box;width:100%;font:inherit;margin:.5rem 0 1rem;padding:.6rem}.plum-discussion button,.plum-discussion input[type=submit]{font:inherit;padding:.5rem 1rem;cursor:pointer;background:var(--plum-comment-button-background, ButtonFace);color:var(--plum-comment-button-color, ButtonText)}.plum-discussion details{margin:.75rem 0}.plum-discussion summary{cursor:pointer}.plum-discussion time{font-size:.9rem}.plum-discussion nav{display:flex;gap:2rem}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
module Cp
|
|
3
|
+
class CommentsController < BaseController
|
|
4
|
+
before_action :require_comment_moderator
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@status = params[:status].presence_in(%w[pending approved hidden reported]) || "pending"
|
|
8
|
+
scope = Comment.joins(:discussion).where(plum_discussions: { site_id: current_site.id })
|
|
9
|
+
scope = @status == "reported" ? scope.where(id: CommentReport.where(resolved_at: nil).select(:comment_id)) : scope.where(status: @status)
|
|
10
|
+
@comments = scope.includes(:reports, :discussion).order(created_at: :desc).limit(50).offset((params[:page].to_i.clamp(1, 10_000) - 1) * 50).select { |comment| accessible?(comment.discussion) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def update
|
|
14
|
+
comment = Comment.joins(:discussion).where(plum_discussions: { site_id: current_site.id }).find(params[:id])
|
|
15
|
+
raise ActiveRecord::RecordNotFound unless accessible?(comment.discussion)
|
|
16
|
+
action = params[:operation].to_s
|
|
17
|
+
return head :unprocessable_entity unless %w[approve hide pin unpin resolve_reports].include?(action)
|
|
18
|
+
return head :unprocessable_entity if action == "pin" && comment.parent_id.present?
|
|
19
|
+
comment.with_lock do
|
|
20
|
+
previous = comment.status
|
|
21
|
+
case action
|
|
22
|
+
when "approve" then comment.update!(status: "approved")
|
|
23
|
+
when "hide" then comment.update!(status: "hidden", pinned: false)
|
|
24
|
+
when "pin" then comment.update!(pinned: true)
|
|
25
|
+
when "unpin" then comment.update!(pinned: false)
|
|
26
|
+
when "resolve_reports" then comment.reports.where(resolved_at: nil).update_all(resolved_at: Time.current)
|
|
27
|
+
end
|
|
28
|
+
comment.moderations.create!(moderator_gid: Comments::Context.identity(Plum.current_user(self)), action: action, previous_status: previous)
|
|
29
|
+
end
|
|
30
|
+
redirect_to cp_comments_path, notice: "Comment updated", status: :see_other
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def close
|
|
34
|
+
discussion = current_site.discussions.find(params[:id])
|
|
35
|
+
raise ActiveRecord::RecordNotFound unless accessible?(discussion)
|
|
36
|
+
return head :unprocessable_entity unless [ true, false, "true", "false", "1", "0" ].include?(params[:closed])
|
|
37
|
+
discussion.with_lock { discussion.update!(closed: ActiveModel::Type::Boolean.new.cast(params[:closed])) }
|
|
38
|
+
redirect_to cp_comments_path, notice: "Discussion updated", status: :see_other
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def require_comment_moderator
|
|
44
|
+
response.headers["Cache-Control"] = "private, no-store"
|
|
45
|
+
head :forbidden unless Plum.configuration.comments.enabled && Plum.configuration.comments.moderator_resolver.call(self, current_site) && Comments::Context.identity(Plum.current_user(self))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def accessible?(discussion)
|
|
49
|
+
Comments::Context.new(self, handle: discussion.subject_handle, id: discussion.commentable_id, action: :moderate).subject == discussion.commentable
|
|
50
|
+
rescue ActiveRecord::RecordNotFound
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -67,7 +67,7 @@ module Plum
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def content_type_params
|
|
70
|
-
permitted = params.require(:content_type).permit(:name, :handle, :singleton, :icon, :blueprint)
|
|
70
|
+
permitted = params.require(:content_type).permit(:name, :handle, :singleton, :comments_enabled, :icon, :blueprint)
|
|
71
71
|
if permitted[:blueprint].is_a?(String)
|
|
72
72
|
permitted[:blueprint] = JSON.parse(permitted[:blueprint]) rescue {}
|
|
73
73
|
end
|
|
@@ -179,7 +179,7 @@ module Plum
|
|
|
179
179
|
end
|
|
180
180
|
|
|
181
181
|
def entry_params
|
|
182
|
-
permitted = params.require(:entry).permit(:title, :slug, :status, :published_at, :locale)
|
|
182
|
+
permitted = params.require(:entry).permit(:title, :slug, :status, :published_at, :locale, :comments_mode)
|
|
183
183
|
if params[:entry][:data].is_a?(ActionController::Parameters)
|
|
184
184
|
submitted = params[:entry][:data].permit!.to_h
|
|
185
185
|
# Writing mode only submits the field being written, so merge it
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
class DiscussionsController < Plum::ApplicationController
|
|
3
|
+
protect_from_forgery with: :exception
|
|
4
|
+
layout "plum/discussion"
|
|
5
|
+
helper Plum::CommentsHelper
|
|
6
|
+
before_action :load_context
|
|
7
|
+
rescue_from Comments::RateLimited, with: -> { render plain: "Please wait a few seconds before posting again.", status: :too_many_requests }
|
|
8
|
+
rescue_from ActiveRecord::RecordInvalid, with: ->(error) { render plain: error.record.errors.full_messages.to_sentence, status: :unprocessable_entity }
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
@comments = @context.discussion&.comments&.publicly_visible&.includes(:parent)&.order(pinned: :desc, id: :asc)&.limit(50)&.offset((page - 1) * 50) || []
|
|
12
|
+
if @context.config.renderer == :theme
|
|
13
|
+
html = Comments::ThemeRenderer.new(controller: self, context: @context, comments: @comments, page: page).render
|
|
14
|
+
render html: html.html_safe, layout: false
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create
|
|
19
|
+
return head :unauthorized unless @context.author_gid
|
|
20
|
+
return head :forbidden unless @context.open?
|
|
21
|
+
@context.create_comment!(params.require(:comment).permit(:body, :parent_id, :video_seconds).to_h.symbolize_keys)
|
|
22
|
+
redirect_to discussion_path(handle: @context.handle, subject_id: @context.subject.id), notice: "Thank you. Your comment has been received.", status: :see_other
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def report
|
|
26
|
+
return head :unauthorized unless @context.author_gid
|
|
27
|
+
comment = @context.discussion&.comments&.publicly_visible&.find(params[:id])
|
|
28
|
+
raise ActiveRecord::RecordNotFound unless comment
|
|
29
|
+
report = comment.reports.find_or_initialize_by(reporter_gid: @context.author_gid)
|
|
30
|
+
report.update!(reason: params.require(:report).permit(:reason)[:reason]) unless report.persisted?
|
|
31
|
+
redirect_to discussion_path(handle: @context.handle, subject_id: @context.subject.id), notice: "Your report has been sent to the moderators.", status: :see_other
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def load_context
|
|
37
|
+
response.headers["Cache-Control"] = "private, no-store"
|
|
38
|
+
@context = Comments::Context.new(self, handle: params[:handle], id: params[:subject_id], action: action_name == "create" ? :create : (action_name == "report" ? :report : :read))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def page
|
|
42
|
+
@page ||= params[:page].to_i.clamp(1, 10_000)
|
|
43
|
+
end
|
|
44
|
+
helper_method :page
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
module CommentsHelper
|
|
3
|
+
# Safe for dynamic Rails views. Do not put personalized forms in a static cache.
|
|
4
|
+
def plum_discussion_path(subject, handle: "entries")
|
|
5
|
+
if respond_to?(:plum)
|
|
6
|
+
plum.discussion_path(handle: handle, subject_id: subject.id)
|
|
7
|
+
else
|
|
8
|
+
discussion_path(handle: handle, subject_id: subject.id)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def plum_comments_link(subject, handle: "entries", label: "Questions & discussion")
|
|
13
|
+
return unless Plum.configuration.comments.enabled
|
|
14
|
+
context = Comments::Context.new(controller, handle: handle, id: subject.id)
|
|
15
|
+
link_to label, plum_discussion_path(context.subject, handle: handle), class: "plum-comments-link"
|
|
16
|
+
rescue ActiveRecord::RecordNotFound
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
class Comment < ApplicationRecord
|
|
3
|
+
belongs_to :discussion, class_name: "Plum::Discussion"
|
|
4
|
+
belongs_to :parent, class_name: "Plum::Comment", optional: true
|
|
5
|
+
has_many :replies, class_name: "Plum::Comment", foreign_key: :parent_id, dependent: :destroy
|
|
6
|
+
has_many :reports, class_name: "Plum::CommentReport", dependent: :destroy
|
|
7
|
+
has_many :moderations, class_name: "Plum::CommentModeration", dependent: :destroy
|
|
8
|
+
|
|
9
|
+
validates :body, presence: true, length: { maximum: 10_000 }
|
|
10
|
+
validates :author_gid, presence: true, length: { maximum: 255 }
|
|
11
|
+
validates :author_name, presence: true, length: { maximum: 100 }
|
|
12
|
+
validates :author_badge, length: { maximum: 60 }, allow_nil: true
|
|
13
|
+
validates :status, inclusion: { in: %w[pending approved hidden] }
|
|
14
|
+
validates :video_seconds, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 604_800 }, allow_nil: true
|
|
15
|
+
validate :valid_parent
|
|
16
|
+
|
|
17
|
+
scope :approved, -> { where(status: "approved") }
|
|
18
|
+
scope :publicly_visible, -> { approved.where(parent_id: nil).or(approved.where(parent_id: approved.where(parent_id: nil).select(:id))) }
|
|
19
|
+
|
|
20
|
+
after_create_commit { publish_event("created") }
|
|
21
|
+
after_update_commit { publish_event("updated") }
|
|
22
|
+
|
|
23
|
+
def publish_event(event)
|
|
24
|
+
ActiveSupport::Notifications.instrument("comment.#{event}.plum", comment_id: id, discussion_id: discussion_id, site_id: discussion.site_id, status: status)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def approved?
|
|
28
|
+
status == "approved"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def valid_parent
|
|
34
|
+
return if parent_id.nil?
|
|
35
|
+
unless parent && parent.discussion_id == discussion_id && parent.parent_id.nil? && parent.id != id
|
|
36
|
+
errors.add(:parent, "must be a top-level comment in this discussion")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
module Commentable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
has_one :plum_discussion, as: :commentable, class_name: "Plum::Discussion", dependent: :destroy
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def plum_comment_site
|
|
10
|
+
site if respond_to?(:site)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def plum_comments_enabled?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def plum_comments_open?
|
|
18
|
+
plum_comments_enabled?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def plum_discussion_title
|
|
22
|
+
respond_to?(:title) ? title : self.class.model_name.human
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
class Discussion < ApplicationRecord
|
|
3
|
+
belongs_to :site, class_name: "Plum::Site"
|
|
4
|
+
belongs_to :commentable, polymorphic: true
|
|
5
|
+
has_many :comments, class_name: "Plum::Comment", dependent: :destroy
|
|
6
|
+
validates :subject_handle, presence: true
|
|
7
|
+
validate :subject_belongs_to_site
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def subject_belongs_to_site
|
|
12
|
+
unless commentable.is_a?(Plum::Commentable) && commentable.plum_comment_site == site
|
|
13
|
+
errors.add(:commentable, "must belong to this site and include Plum::Commentable")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/app/models/plum/entry.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module Plum
|
|
2
2
|
class Entry < ApplicationRecord
|
|
3
3
|
include SiteScoped
|
|
4
|
+
include Commentable
|
|
4
5
|
include StaticCacheInvalidation
|
|
5
6
|
|
|
6
7
|
belongs_to :content_type
|
|
@@ -21,6 +22,8 @@ module Plum
|
|
|
21
22
|
# page by renaming or removing it.
|
|
22
23
|
HOMEPAGE_SLUG = "home".freeze
|
|
23
24
|
|
|
25
|
+
validates :comments_mode, inclusion: { in: %w[inherit open closed disabled] }
|
|
26
|
+
|
|
24
27
|
enum :status, { draft: 0, published: 1, scheduled: 2 }
|
|
25
28
|
|
|
26
29
|
validates :title, presence: true
|
|
@@ -45,6 +48,14 @@ module Plum
|
|
|
45
48
|
where("title LIKE ? OR slug LIKE ?", term, term)
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
def plum_comments_enabled?
|
|
52
|
+
comments_mode != "disabled" && (comments_mode != "inherit" || content_type.comments_enabled?)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def plum_comments_open?
|
|
56
|
+
plum_comments_enabled? && comments_mode != "closed"
|
|
57
|
+
end
|
|
58
|
+
|
|
48
59
|
def field_value(handle)
|
|
49
60
|
data&.dig(handle)
|
|
50
61
|
end
|
data/app/models/plum/site.rb
CHANGED
|
@@ -5,6 +5,7 @@ module Plum
|
|
|
5
5
|
belongs_to :owner, polymorphic: true, optional: true
|
|
6
6
|
|
|
7
7
|
has_one :site_setting, dependent: :destroy
|
|
8
|
+
has_many :discussions, class_name: "Plum::Discussion", dependent: :destroy
|
|
8
9
|
has_many :content_types, dependent: :destroy
|
|
9
10
|
has_many :entries, dependent: :destroy
|
|
10
11
|
has_many :globals, dependent: :destroy
|