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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -0
  3. data/README.md +10 -2
  4. data/app/assets/builds/tailwind.css +1 -1
  5. data/app/assets/stylesheets/plum/comments.css +1 -0
  6. data/app/controllers/plum/cp/comments_controller.rb +55 -0
  7. data/app/controllers/plum/cp/content_types_controller.rb +1 -1
  8. data/app/controllers/plum/cp/entries_controller.rb +1 -1
  9. data/app/controllers/plum/discussions_controller.rb +46 -0
  10. data/app/helpers/plum/comments_helper.rb +20 -0
  11. data/app/models/plum/comment.rb +40 -0
  12. data/app/models/plum/comment_moderation.rb +7 -0
  13. data/app/models/plum/comment_report.rb +7 -0
  14. data/app/models/plum/commentable.rb +25 -0
  15. data/app/models/plum/discussion.rb +17 -0
  16. data/app/models/plum/entry.rb +11 -0
  17. data/app/models/plum/site.rb +1 -0
  18. data/app/services/plum/comments/context.rb +70 -0
  19. data/app/services/plum/comments/rate_limited.rb +5 -0
  20. data/app/services/plum/comments/theme_renderer.rb +66 -0
  21. data/app/services/plum/config_sync.rb +3 -0
  22. data/app/services/plum/liquid_context.rb +8 -0
  23. data/app/services/plum/liquid_renderer.rb +8 -0
  24. data/app/services/plum/site_archive.rb +4 -4
  25. data/app/themes/default/layouts/base.liquid +1 -1
  26. data/app/themes/default/templates/comments/_comment.liquid +8 -0
  27. data/app/themes/default/templates/comments/_form.liquid +7 -0
  28. data/app/themes/default/templates/comments/_report.liquid +6 -0
  29. data/app/themes/default/templates/comments/show.liquid +15 -0
  30. data/app/themes/default/templates/entries/_default.liquid +1 -0
  31. data/app/themes/default/templates/entries/landing.liquid +1 -0
  32. data/app/themes/default/templates/entries/pages.liquid +1 -0
  33. data/app/themes/default/templates/entries/posts.liquid +1 -0
  34. data/app/views/layouts/plum/cp.html.erb +4 -1
  35. data/app/views/layouts/plum/discussion.html.erb +5 -0
  36. data/app/views/layouts/plum/session.html.erb +1 -1
  37. data/app/views/layouts/plum/write.html.erb +1 -1
  38. data/app/views/plum/cp/comments/index.html.erb +16 -0
  39. data/app/views/plum/cp/content_types/_form.html.erb +3 -0
  40. data/app/views/plum/cp/entries/_form.html.erb +3 -0
  41. data/app/views/plum/discussions/_comments.html.erb +13 -0
  42. data/app/views/plum/discussions/_form.html.erb +6 -0
  43. data/app/views/plum/discussions/show.html.erb +19 -0
  44. data/config/plum_routes.rb +6 -0
  45. data/db/engine_migrate/20261005190000_create_plum_comments.rb +48 -0
  46. data/docs/comments.md +140 -0
  47. data/docs/plum-cli.md +66 -0
  48. data/docs/releases/0.3.0-validation.md +15 -0
  49. data/docs/zero-to-agency.md +172 -0
  50. data/lib/generators/plum/install/install_generator.rb +13 -1
  51. data/lib/plum/comments/configuration.rb +43 -0
  52. data/lib/plum/configuration.rb +3 -1
  53. data/lib/plum/engine.rb +11 -4
  54. data/lib/plum/version.rb +1 -1
  55. data/lib/plum.rb +7 -0
  56. metadata +36 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9daf7a444c4aca840d3395ef9e290708192a904b8d7effa9aa0954819ab3a046
4
- data.tar.gz: 5987aaecba9e612f37eae6cebb8ad663fcd39fc0add1342762836339bbaf5a3c
3
+ metadata.gz: 1614aa5469fd66d91d9448f3de3d628fba36a3f9eaf9a65022d2b2c41f219a6d
4
+ data.tar.gz: 2d3e683c92c8c0bc7836325ec9c33b06536657d1ac69ef4fa9724b0ac711b27b
5
5
  SHA512:
6
- metadata.gz: f7718730fa5d92db2a89d1ef13724d2a7cb0198322ff0bb239982390f4af3125747d3261ca1a61161da64d3dd45119a37d2396181dd7631d3193294428491781
7
- data.tar.gz: 9ddd57edac63fc212ac7ddfd20f3616551f84a437c72496b31d3f86d30fafdd4ac002454fd14a66e7bbe9faea6d274274e7fd69642790ded89bc29fe39c7596d
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.1.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,7 @@
1
+ module Plum
2
+ class CommentModeration < ApplicationRecord
3
+ belongs_to :comment, class_name: "Plum::Comment"
4
+ validates :moderator_gid, :previous_status, presence: true
5
+ validates :action, inclusion: { in: %w[approve hide pin unpin resolve_reports] }
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Plum
2
+ class CommentReport < ApplicationRecord
3
+ belongs_to :comment, class_name: "Plum::Comment"
4
+ validates :reporter_gid, presence: true, uniqueness: { scope: :comment_id }
5
+ validates :reason, presence: true, length: { maximum: 1_000 }
6
+ end
7
+ 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
@@ -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
@@ -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