domma-cms 0.6.14 → 0.6.16

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 (46) hide show
  1. package/admin/js/app.js +4 -4
  2. package/admin/js/config/sidebar-config.js +1 -1
  3. package/admin/js/lib/markdown-toolbar.js +8 -6
  4. package/admin/js/templates/pages.html +5 -1
  5. package/admin/js/views/pages.js +2 -2
  6. package/config/plugins.json +11 -1
  7. package/package.json +1 -1
  8. package/plugins/analytics/stats.json +3 -3
  9. package/plugins/form-builder/data/forms/contacts.json +66 -0
  10. package/plugins/form-builder/data/forms/enquiries.json +103 -0
  11. package/plugins/form-builder/data/forms/feedback.json +131 -0
  12. package/plugins/form-builder/data/forms/notes.json +79 -0
  13. package/plugins/form-builder/data/forms/to-do.json +100 -0
  14. package/plugins/form-builder/data/submissions/contacts.json +1 -0
  15. package/plugins/form-builder/data/submissions/enquiries.json +1 -0
  16. package/plugins/form-builder/data/submissions/feedback.json +1 -0
  17. package/plugins/form-builder/data/submissions/notes.json +1 -0
  18. package/plugins/form-builder/data/submissions/to-do.json +1 -0
  19. package/plugins/job-board/admin/templates/application-detail.html +40 -0
  20. package/plugins/job-board/admin/templates/applications.html +10 -0
  21. package/plugins/job-board/admin/templates/companies.html +24 -0
  22. package/plugins/job-board/admin/templates/dashboard.html +36 -0
  23. package/plugins/job-board/admin/templates/job-editor.html +17 -0
  24. package/plugins/job-board/admin/templates/jobs.html +15 -0
  25. package/plugins/job-board/admin/templates/profile.html +17 -0
  26. package/plugins/job-board/admin/views/application-detail.js +62 -0
  27. package/plugins/job-board/admin/views/applications.js +47 -0
  28. package/plugins/job-board/admin/views/companies.js +104 -0
  29. package/plugins/job-board/admin/views/dashboard.js +88 -0
  30. package/plugins/job-board/admin/views/job-editor.js +86 -0
  31. package/plugins/job-board/admin/views/jobs.js +53 -0
  32. package/plugins/job-board/admin/views/profile.js +47 -0
  33. package/plugins/job-board/config.js +6 -0
  34. package/plugins/job-board/plugin.js +466 -0
  35. package/plugins/job-board/plugin.json +40 -0
  36. package/plugins/job-board/schemas/jb-agent-companies.json +17 -0
  37. package/plugins/job-board/schemas/jb-applications.json +20 -0
  38. package/plugins/job-board/schemas/jb-candidate-profiles.json +20 -0
  39. package/plugins/job-board/schemas/jb-companies.json +21 -0
  40. package/plugins/job-board/schemas/jb-jobs.json +23 -0
  41. package/public/css/site.css +1 -1
  42. package/server/routes/api/collections.js +4 -0
  43. package/server/routes/api/plugins.js +9 -1
  44. package/server/server.js +1 -0
  45. package/server/services/plugins.js +30 -0
  46. package/server/services/userTypes.js +227 -0
@@ -1 +1 @@
1
- body,button,input,select,textarea{font-family:Roboto,sans-serif}.site-main{min-height:calc(100vh - 60px);padding-top:2rem;padding-bottom:4rem}.site-main.with-sidebar{display:grid;grid-template-columns:260px 1fr;gap:0}.site-sidebar{min-height:100%;border-right:1px solid var(--border-color, rgba(255,255,255,.08))}.site-content{overflow:hidden}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.container{max-width:860px;margin:0 auto;padding:0 1.5rem}.page-title{font-size:clamp(1.5rem,4vw,2rem);font-weight:700;margin-bottom:1.5rem;line-height:1.2}.page-body{line-height:1.7;font-size:1rem}.page-body h1,.page-body h2,.page-body h3,.page-body h4{margin-top:2rem;margin-bottom:.75rem;font-weight:600}.page-body h2{font-size:clamp(1.2rem,3vw,1.5rem)}.page-body h3{font-size:clamp(1.1rem,2.5vw,1.25rem)}.page-body p{margin-bottom:1rem}.page-body ul,.page-body ol{margin-bottom:1rem;padding-left:1.5rem}.page-body a{color:var(--primary, #5b8cff)}.page-body a:hover{text-decoration:underline}.page-body code{font-family:Fira Code,Courier New,monospace;font-size:.9em;background:#ffffff0f;padding:.15em .35em;border-radius:3px}.page-body pre{background:#0000004d;border:1px solid rgba(255,255,255,.08);border-radius:6px;padding:1rem;overflow-x:auto;margin-bottom:1rem}.page-body pre code{background:none;padding:0}.page-body img{max-width:100%;border-radius:6px}.page-body blockquote{border-left:3px solid var(--primary, #5b8cff);margin:1.5rem 0;padding:.75rem 1rem;background:#5b8cff0f;border-radius:0 6px 6px 0}h3.accordion-header{margin:0}.accordion-button{all:unset;display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font:inherit}.page-body .card-header h2{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.card[data-collapsible] .card-header{cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:space-between}.card[data-collapsible] .card-header:after{content:"\25be";font-size:1.1em;line-height:1;display:inline-block;transition:transform .25s ease;flex-shrink:0}.card[data-collapsible].is-collapsed .card-header:after{transform:rotate(-90deg)}.card[data-collapsible] .card-body{overflow:hidden;max-height:4000px;opacity:1;transition:max-height .3s ease,opacity .25s ease}.card[data-collapsible].is-collapsed .card-body{max-height:0;opacity:0}.navbar-link span[data-icon],.navbar-link svg,.navbar-dropdown-toggle span[data-icon],.navbar-dropdown-toggle svg,.navbar-dropdown-item span[data-icon],.navbar-dropdown-item svg{width:13px!important;height:13px!important;margin-right:10px!important}.navbar-dropdown-toggle{font-size:var(--dm-font-size-base)}@media(min-width:993px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-sm)}}@media(min-width:1201px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-xs)}}.dm-reduced-motion *,.dm-reduced-motion *:before,.dm-reduced-motion *:after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}.page-footer{border-top:1px solid var(--border-color, rgba(255,255,255,.08));padding:1.5rem 0}.footer-inner{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem}.footer-inner p{margin:0;color:var(--text-muted, #888);font-size:.875rem}.footer-links{display:flex;gap:1.25rem}.footer-links a{color:var(--text-muted, #888);font-size:.875rem;text-decoration:none}.footer-links a:hover{color:var(--text, #eee)}.footer-social{display:flex;gap:.5rem;align-items:center}.footer-social-link{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;color:var(--text-muted, #888);transition:color .15s}.footer-social-link:hover{color:var(--text, #eee)}.footer-social-link svg{width:1rem;height:1rem}.footer-motion-switch{font-size:.8rem;color:var(--text-muted, #888);white-space:nowrap}.footer-motion-switch .form-switch-label{color:var(--text-muted, #888)}.footer-motion-switch .form-switch-input{width:2rem;height:1.125rem}.footer-motion-switch .form-switch-input:after{width:.875rem;height:.875rem}.footer-motion-switch .form-switch-input:checked:after{transform:translate(.875rem)}.dm-slideover-header{display:flex;align-items:center;justify-content:space-between;padding:.875rem 1.25rem;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08));flex-shrink:0}.dm-slideover-title{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.dm-slideover-body{padding:1.25rem;overflow-y:auto;flex:1}@media(max-width:768px){.site-main.with-sidebar{grid-template-columns:1fr}.site-sidebar{display:none}}.dm-spacer{display:block;width:100%}.hero-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.site-main:has(.page-body>.hero-breakout:first-child){padding-top:0}body[data-layout=landing]>.site-main{padding-top:0}body[data-layout=landing]>.site-main .container{max-width:none;padding:0}body[data-layout=landing] .page-body{padding-left:1.5rem;padding-right:1.5rem}body[data-layout=landing] .page-body>p,body[data-layout=landing] .page-body>h1,body[data-layout=landing] .page-body>h2,body[data-layout=landing] .page-body>h3,body[data-layout=landing] .page-body>ul,body[data-layout=landing] .page-body>ol,body[data-layout=landing] .page-body>blockquote{max-width:860px;margin-left:auto;margin-right:auto}body[data-layout=landing] .page-body .hero-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem}body[data-layout=landing] .page-body .grid-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem;padding-left:1.5rem;padding-right:1.5rem}.page-body .card{transition:transform .2s ease,box-shadow .2s ease}.page-body .card:hover{transform:translateY(-3px);box-shadow:0 8px 24px #00000059}.page-body .card-header-icon-inline{display:flex;align-items:center;gap:.6rem}.page-body .card-header-icon-inline [data-icon]{flex-shrink:0;line-height:0}.page-body .card-header-icon-inline [data-icon] svg,.page-body .card-header-icon-inline>svg{display:block;width:1.25rem;height:1.25rem}.page-body .card-header-icon-stacked{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.35rem;padding-top:.25rem}.page-body .card-header-icon-stacked [data-icon],.page-body .card-header-icon-stacked svg{width:2rem;height:2rem}.hero.hero-dark{background:linear-gradient(135deg,#1f2937,#111827);color:#e2e8f0}.hero .hero-content{position:relative;z-index:2}.hero.hero-left .hero-content{text-align:left;align-items:flex-start;max-width:62%}@media(max-width:768px){.hero.hero-left .hero-content{max-width:100%}}.hero .hero-cta{display:flex;gap:.85rem;flex-wrap:wrap;margin-top:1.75rem}.hero .hero-cta a{display:inline-flex;align-items:center;gap:.4rem;padding:.55rem 1.35rem;border-radius:6px;font-size:.95rem;font-weight:500;text-decoration:none;transition:background .2s ease,border-color .2s ease,transform .15s ease,box-shadow .2s ease}.hero .hero-cta a:first-child{background:#ffffffeb;color:#111;border:1px solid transparent}.hero .hero-cta a:first-child:hover{background:#fff;box-shadow:0 4px 16px #00000040;transform:translateY(-2px)}.hero .hero-cta a:last-child{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.4)}.hero .hero-cta a:last-child:hover{border-color:#ffffffbf;background:#ffffff14;transform:translateY(-2px)}.hero .hero-label{display:inline-block;margin-bottom:.9rem;padding:.2rem .8rem;border-radius:999px;font-size:.72rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:#ffffffb3;border:1px solid rgba(255,255,255,.22)}.grid-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.dm-breadcrumbs{position:fixed;z-index:200;display:inline-flex;align-items:center;gap:.2rem;padding:.3rem .8rem;border-radius:999px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);background:#00000047;border:1px solid rgba(255,255,255,.11);box-shadow:0 2px 10px #00000038;font-size:.72rem;font-weight:500;letter-spacing:.01em;line-height:1.4;max-width:calc(100vw - 2rem);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dm-breadcrumbs .dm-breadcrumbs-item{color:#ffffffa6}.dm-breadcrumbs .dm-breadcrumbs-link{display:inline-flex;align-items:center;gap:.25rem;color:#ffffff8c;text-decoration:none;transition:color .15s}.dm-breadcrumbs .dm-breadcrumbs-home-icon{flex-shrink:0;vertical-align:middle}.dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#fffffff2}.dm-breadcrumbs .dm-breadcrumbs-current{color:#ffffffeb;font-weight:600}.dm-breadcrumbs .dm-breadcrumbs-separator{color:#ffffff47;font-size:.8em;line-height:1;margin:0 .05rem}[data-mode=light] .dm-breadcrumbs{background:#ffffff8c;border-color:#00000012;box-shadow:0 2px 10px #00000014}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-item,[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link{color:#0000008c}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#000000e6}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-current{color:#000000d9}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-separator{color:#00000040}.dm-collection-display{margin:1.5rem 0}.dm-collection-list{display:flex;flex-direction:column;gap:0}.dm-collection-list-item{padding:1rem 0;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08))}.dm-collection-list-item:last-child{border-bottom:none}.dm-collection-list-item strong{display:block;font-size:1rem;margin-bottom:.25rem}.dm-collection-list-item p{margin:0;color:var(--text-muted, #888);font-size:.9rem}.dm-collection-empty p{color:var(--text-muted, #888);font-style:italic}.hero-gradient-purple{background:linear-gradient(135deg,#ede9fe,#ddd6fe);color:#1e1b4b}.hero-gradient-blue{background:linear-gradient(135deg,#dbeafe,#bfdbfe);color:#1e3a5f}.hero-gradient-green{background:linear-gradient(135deg,#d1fae5,#a7f3d0);color:#064e3b}.hero-gradient-sunset{background:linear-gradient(135deg,#fef3c7,#fde68a);color:#78350f}.hero-gradient-ocean{background:linear-gradient(135deg,#e0f2fe,#bae6fd);color:#0c4a6e}.hero-gradient-rose{background:linear-gradient(135deg,#fce7f3,#fbcfe8);color:#831843}.hero-gradient-forest{background:linear-gradient(135deg,#dcfce7,#bbf7d0);color:#14532d}.hero-gradient-night{background:linear-gradient(135deg,#334155,#1e293b);color:#e2e8f0}.hero-gradient-ocean-light{background:linear-gradient(135deg,#e0f2fe,#caf0f8);color:#1e293b}.hero-gradient-ocean-dark{background:linear-gradient(135deg,#0c4a6e,#164e63);color:#e2e8f0}.hero-gradient-forest-light{background:linear-gradient(135deg,#d1fae5,#c6f6dc);color:#1e293b}.hero-gradient-forest-dark{background:linear-gradient(135deg,#1a4731,#166534);color:#e2e8f0}.hero-gradient-sunset-light{background:linear-gradient(135deg,#fde8d8,#fddcc9);color:#1e293b}.hero-gradient-sunset-dark{background:linear-gradient(135deg,#6b3727,#7c4036);color:#f5ede8}.hero-gradient-royal-light{background:linear-gradient(135deg,#e8f0fd,#dce8fc);color:#1e293b}.hero-gradient-royal-dark{background:linear-gradient(135deg,#1e3465,#263d7a);color:#e2e8f0}.hero-gradient-lemon-light{background:linear-gradient(135deg,#fefce8,#fef9c3);color:#1e293b}.hero-gradient-lemon-dark{background:linear-gradient(135deg,#5c4d1a,#6b5920);color:#fefce8}.hero-gradient-silver-light{background:linear-gradient(135deg,#f1f5f9,#e2e8f0);color:#1e293b}.hero-gradient-silver-dark{background:linear-gradient(135deg,#2d3748,#374151);color:#e2e8f0}.hero-gradient-charcoal-light{background:linear-gradient(135deg,#eceff1,#e1e7eb);color:#1e293b}.hero-gradient-charcoal-dark{background:linear-gradient(135deg,#2c3843,#374451);color:#e2e8f0}.hero-gradient-christmas-light{background:linear-gradient(135deg,#fde8ea,#fdd5d8);color:#1e293b}.hero-gradient-christmas-dark{background:linear-gradient(135deg,#5c0f1d,#7a1525);color:#fde8ea}.hero-gradient-unicorn-light{background:linear-gradient(135deg,#f5e8fd,#edd6fb);color:#1e293b}.hero-gradient-unicorn-dark{background:linear-gradient(135deg,#3d1a5a,#4a2068);color:#f5e8fd}.hero-gradient-dreamy-light{background:linear-gradient(135deg,#f5ede8,#eeddd4);color:#1e293b}.hero-gradient-dreamy-dark{background:linear-gradient(135deg,#3d2820,#503328);color:#f5ede8}.hero-gradient-grayve-light{background:linear-gradient(135deg,#e0f7f9,#cbf2f5);color:#1e293b}.hero-gradient-grayve-dark{background:linear-gradient(135deg,#00363d,#00444d);color:#e0f7f9}.hero-gradient-mint-light{background:linear-gradient(135deg,#d8f5ea,#c5efdd);color:#1e293b}.hero-gradient-mint-dark{background:linear-gradient(135deg,#134d33,#195f3f);color:#d8f5ea}.hero-gradient-wedding-light{background:linear-gradient(135deg,#faf3e0,#f5e9c7);color:#1e293b}.hero-gradient-wedding-dark{background:linear-gradient(135deg,#5c4418,#6f5320);color:#faf3e0}.tabs-centered{text-align:center}.tabs-centered .tab-list{display:inline-flex}.tabs-centered .tab-content{text-align:left}.site-main{overflow-x:hidden}@media(max-width:768px){.hero .hero-cta a,.dm-so-trigger,.dm-cta-trigger{min-height:44px;padding:.6rem 1.25rem}}
1
+ body,button,input,select,textarea{font-family:Roboto,sans-serif}.navbar-actions{order:4;margin-left:auto;display:flex;align-items:center;gap:.5rem}.navbar-dark .navbar-actions{color:var(--dm-text-inverse, rgba(255, 255, 255, .85))}.navbar-dark .site-search-shortcut-hint{color:#fff9;border-color:#ffffff40}.navbar-light .navbar-actions{color:var(--dm-text, #111)}.site-main{min-height:calc(100vh - 60px);padding-top:2rem;padding-bottom:4rem}.site-main.with-sidebar{display:grid;grid-template-columns:260px 1fr;gap:0}.site-sidebar{min-height:100%;border-right:1px solid var(--border-color, rgba(255,255,255,.08))}.site-content{overflow:hidden}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.container{max-width:860px;margin:0 auto;padding:0 1.5rem}.page-title{font-size:clamp(1.5rem,4vw,2rem);font-weight:700;margin-bottom:1.5rem;line-height:1.2}.page-body{line-height:1.7;font-size:1rem}.page-body h1,.page-body h2,.page-body h3,.page-body h4{margin-top:2rem;margin-bottom:.75rem;font-weight:600}.page-body h2{font-size:clamp(1.2rem,3vw,1.5rem)}.page-body h3{font-size:clamp(1.1rem,2.5vw,1.25rem)}.page-body p{margin-bottom:1rem}.page-body ul,.page-body ol{margin-bottom:1rem;padding-left:1.5rem}.page-body a{color:var(--primary, #5b8cff)}.page-body a:hover{text-decoration:underline}.page-body code{font-family:Fira Code,Courier New,monospace;font-size:.9em;background:#ffffff0f;padding:.15em .35em;border-radius:3px}.page-body pre{background:#0000004d;border:1px solid rgba(255,255,255,.08);border-radius:6px;padding:1rem;overflow-x:auto;margin-bottom:1rem}.page-body pre code{background:none;padding:0}.page-body img{max-width:100%;border-radius:6px}.page-body blockquote{border-left:3px solid var(--primary, #5b8cff);margin:1.5rem 0;padding:.75rem 1rem;background:#5b8cff0f;border-radius:0 6px 6px 0}h3.accordion-header{margin:0}.accordion-button{all:unset;display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font:inherit}.page-body .card-header h2{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.card[data-collapsible] .card-header{cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:space-between}.card[data-collapsible] .card-header:after{content:"\25be";font-size:1.1em;line-height:1;display:inline-block;transition:transform .25s ease;flex-shrink:0}.card[data-collapsible].is-collapsed .card-header:after{transform:rotate(-90deg)}.card[data-collapsible] .card-body{overflow:hidden;max-height:4000px;opacity:1;transition:max-height .3s ease,opacity .25s ease}.card[data-collapsible].is-collapsed .card-body{max-height:0;opacity:0}.navbar-link span[data-icon],.navbar-link svg,.navbar-dropdown-toggle span[data-icon],.navbar-dropdown-toggle svg,.navbar-dropdown-item span[data-icon],.navbar-dropdown-item svg{width:13px!important;height:13px!important;margin-right:10px!important}.navbar-dropdown-toggle{font-size:var(--dm-font-size-base)}@media(min-width:993px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-sm)}}@media(min-width:1201px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-xs)}}.dm-reduced-motion *,.dm-reduced-motion *:before,.dm-reduced-motion *:after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}.page-footer{border-top:1px solid var(--border-color, rgba(255,255,255,.08));padding:1.5rem 0}.footer-inner{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem}.footer-inner p{margin:0;color:var(--text-muted, #888);font-size:.875rem}.footer-links{display:flex;gap:1.25rem}.footer-links a{color:var(--text-muted, #888);font-size:.875rem;text-decoration:none}.footer-links a:hover{color:var(--text, #eee)}.footer-social{display:flex;gap:.5rem;align-items:center}.footer-social-link{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;color:var(--text-muted, #888);transition:color .15s}.footer-social-link:hover{color:var(--text, #eee)}.footer-social-link svg{width:1rem;height:1rem}.footer-motion-switch{font-size:.8rem;color:var(--text-muted, #888);white-space:nowrap}.footer-motion-switch .form-switch-label{color:var(--text-muted, #888)}.footer-motion-switch .form-switch-input{width:2rem;height:1.125rem}.footer-motion-switch .form-switch-input:after{width:.875rem;height:.875rem}.footer-motion-switch .form-switch-input:checked:after{transform:translate(.875rem)}.dm-slideover-header{display:flex;align-items:center;justify-content:space-between;padding:.875rem 1.25rem;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08));flex-shrink:0}.dm-slideover-title{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.dm-slideover-body{padding:1.25rem;overflow-y:auto;flex:1}@media(max-width:768px){.site-main.with-sidebar{grid-template-columns:1fr}.site-sidebar{display:none}}.dm-spacer{display:block;width:100%}.hero-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.site-main:has(.page-body>.hero-breakout:first-child){padding-top:0}body[data-layout=landing]>.site-main{padding-top:0}body[data-layout=landing]>.site-main .container{max-width:none;padding:0}body[data-layout=landing] .page-body{padding-left:1.5rem;padding-right:1.5rem}body[data-layout=landing] .page-body>p,body[data-layout=landing] .page-body>h1,body[data-layout=landing] .page-body>h2,body[data-layout=landing] .page-body>h3,body[data-layout=landing] .page-body>ul,body[data-layout=landing] .page-body>ol,body[data-layout=landing] .page-body>blockquote{max-width:860px;margin-left:auto;margin-right:auto}body[data-layout=landing] .page-body .hero-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem}body[data-layout=landing] .page-body .grid-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem;padding-left:1.5rem;padding-right:1.5rem}.page-body .card{transition:transform .2s ease,box-shadow .2s ease}.page-body .card:hover{transform:translateY(-3px);box-shadow:0 8px 24px #00000059}.page-body .card-header-icon-inline{display:flex;align-items:center;gap:.6rem}.page-body .card-header-icon-inline [data-icon]{flex-shrink:0;line-height:0}.page-body .card-header-icon-inline [data-icon] svg,.page-body .card-header-icon-inline>svg{display:block;width:1.25rem;height:1.25rem}.page-body .card-header-icon-stacked{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.35rem;padding-top:.25rem}.page-body .card-header-icon-stacked [data-icon],.page-body .card-header-icon-stacked svg{width:2rem;height:2rem}.hero.hero-dark{background:linear-gradient(135deg,#1f2937,#111827);color:#e2e8f0}.hero .hero-content{position:relative;z-index:2}.hero.hero-left .hero-content{text-align:left;align-items:flex-start;max-width:62%}@media(max-width:768px){.hero.hero-left .hero-content{max-width:100%}}.hero .hero-cta{display:flex;gap:.85rem;flex-wrap:wrap;margin-top:1.75rem}.hero .hero-cta a{display:inline-flex;align-items:center;gap:.4rem;padding:.55rem 1.35rem;border-radius:6px;font-size:.95rem;font-weight:500;text-decoration:none;transition:background .2s ease,border-color .2s ease,transform .15s ease,box-shadow .2s ease}.hero .hero-cta a:first-child{background:#ffffffeb;color:#111;border:1px solid transparent}.hero .hero-cta a:first-child:hover{background:#fff;box-shadow:0 4px 16px #00000040;transform:translateY(-2px)}.hero .hero-cta a:last-child{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.4)}.hero .hero-cta a:last-child:hover{border-color:#ffffffbf;background:#ffffff14;transform:translateY(-2px)}.hero .hero-label{display:inline-block;margin-bottom:.9rem;padding:.2rem .8rem;border-radius:999px;font-size:.72rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:#ffffffb3;border:1px solid rgba(255,255,255,.22)}.grid-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.dm-breadcrumbs{position:fixed;z-index:200;display:inline-flex;align-items:center;gap:.2rem;padding:.3rem .8rem;border-radius:999px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);background:#00000047;border:1px solid rgba(255,255,255,.11);box-shadow:0 2px 10px #00000038;font-size:.72rem;font-weight:500;letter-spacing:.01em;line-height:1.4;max-width:calc(100vw - 2rem);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dm-breadcrumbs .dm-breadcrumbs-item{color:#ffffffa6}.dm-breadcrumbs .dm-breadcrumbs-link{display:inline-flex;align-items:center;gap:.25rem;color:#ffffff8c;text-decoration:none;transition:color .15s}.dm-breadcrumbs .dm-breadcrumbs-home-icon{flex-shrink:0;vertical-align:middle}.dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#fffffff2}.dm-breadcrumbs .dm-breadcrumbs-current{color:#ffffffeb;font-weight:600}.dm-breadcrumbs .dm-breadcrumbs-separator{color:#ffffff47;font-size:.8em;line-height:1;margin:0 .05rem}[data-mode=light] .dm-breadcrumbs{background:#ffffff8c;border-color:#00000012;box-shadow:0 2px 10px #00000014}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-item,[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link{color:#0000008c}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#000000e6}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-current{color:#000000d9}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-separator{color:#00000040}.dm-collection-display{margin:1.5rem 0}.dm-collection-list{display:flex;flex-direction:column;gap:0}.dm-collection-list-item{padding:1rem 0;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08))}.dm-collection-list-item:last-child{border-bottom:none}.dm-collection-list-item strong{display:block;font-size:1rem;margin-bottom:.25rem}.dm-collection-list-item p{margin:0;color:var(--text-muted, #888);font-size:.9rem}.dm-collection-empty p{color:var(--text-muted, #888);font-style:italic}.hero-gradient-purple{background:linear-gradient(135deg,#ede9fe,#ddd6fe);color:#1e1b4b}.hero-gradient-blue{background:linear-gradient(135deg,#dbeafe,#bfdbfe);color:#1e3a5f}.hero-gradient-green{background:linear-gradient(135deg,#d1fae5,#a7f3d0);color:#064e3b}.hero-gradient-sunset{background:linear-gradient(135deg,#fef3c7,#fde68a);color:#78350f}.hero-gradient-ocean{background:linear-gradient(135deg,#e0f2fe,#bae6fd);color:#0c4a6e}.hero-gradient-rose{background:linear-gradient(135deg,#fce7f3,#fbcfe8);color:#831843}.hero-gradient-forest{background:linear-gradient(135deg,#dcfce7,#bbf7d0);color:#14532d}.hero-gradient-night{background:linear-gradient(135deg,#334155,#1e293b);color:#e2e8f0}.hero-gradient-ocean-light{background:linear-gradient(135deg,#e0f2fe,#caf0f8);color:#1e293b}.hero-gradient-ocean-dark{background:linear-gradient(135deg,#0c4a6e,#164e63);color:#e2e8f0}.hero-gradient-forest-light{background:linear-gradient(135deg,#d1fae5,#c6f6dc);color:#1e293b}.hero-gradient-forest-dark{background:linear-gradient(135deg,#1a4731,#166534);color:#e2e8f0}.hero-gradient-sunset-light{background:linear-gradient(135deg,#fde8d8,#fddcc9);color:#1e293b}.hero-gradient-sunset-dark{background:linear-gradient(135deg,#6b3727,#7c4036);color:#f5ede8}.hero-gradient-royal-light{background:linear-gradient(135deg,#e8f0fd,#dce8fc);color:#1e293b}.hero-gradient-royal-dark{background:linear-gradient(135deg,#1e3465,#263d7a);color:#e2e8f0}.hero-gradient-lemon-light{background:linear-gradient(135deg,#fefce8,#fef9c3);color:#1e293b}.hero-gradient-lemon-dark{background:linear-gradient(135deg,#5c4d1a,#6b5920);color:#fefce8}.hero-gradient-silver-light{background:linear-gradient(135deg,#f1f5f9,#e2e8f0);color:#1e293b}.hero-gradient-silver-dark{background:linear-gradient(135deg,#2d3748,#374151);color:#e2e8f0}.hero-gradient-charcoal-light{background:linear-gradient(135deg,#eceff1,#e1e7eb);color:#1e293b}.hero-gradient-charcoal-dark{background:linear-gradient(135deg,#2c3843,#374451);color:#e2e8f0}.hero-gradient-christmas-light{background:linear-gradient(135deg,#fde8ea,#fdd5d8);color:#1e293b}.hero-gradient-christmas-dark{background:linear-gradient(135deg,#5c0f1d,#7a1525);color:#fde8ea}.hero-gradient-unicorn-light{background:linear-gradient(135deg,#f5e8fd,#edd6fb);color:#1e293b}.hero-gradient-unicorn-dark{background:linear-gradient(135deg,#3d1a5a,#4a2068);color:#f5e8fd}.hero-gradient-dreamy-light{background:linear-gradient(135deg,#f5ede8,#eeddd4);color:#1e293b}.hero-gradient-dreamy-dark{background:linear-gradient(135deg,#3d2820,#503328);color:#f5ede8}.hero-gradient-grayve-light{background:linear-gradient(135deg,#e0f7f9,#cbf2f5);color:#1e293b}.hero-gradient-grayve-dark{background:linear-gradient(135deg,#00363d,#00444d);color:#e0f7f9}.hero-gradient-mint-light{background:linear-gradient(135deg,#d8f5ea,#c5efdd);color:#1e293b}.hero-gradient-mint-dark{background:linear-gradient(135deg,#134d33,#195f3f);color:#d8f5ea}.hero-gradient-wedding-light{background:linear-gradient(135deg,#faf3e0,#f5e9c7);color:#1e293b}.hero-gradient-wedding-dark{background:linear-gradient(135deg,#5c4418,#6f5320);color:#faf3e0}.tabs-centered{text-align:center}.tabs-centered .tab-list{display:inline-flex}.tabs-centered .tab-content{text-align:left}.site-main{overflow-x:hidden}@media(max-width:768px){.hero .hero-cta a,.dm-so-trigger,.dm-cta-trigger{min-height:44px;padding:.6rem 1.25rem}}
@@ -43,6 +43,7 @@ import {getRoleLevel, invalidate as invalidateRoles} from '../../services/roles.
43
43
  import {getConfig, saveConfig} from '../../config.js';
44
44
  import {PRESET_COLLECTION_SLUGS} from '../../services/presetCollections.js';
45
45
  import {ensureFormForCollection} from '../../services/forms.js';
46
+ import {hooks} from '../../services/hooks.js';
46
47
 
47
48
  const ALL_PRESET_SLUGS = new Set(['roles', 'user-profiles', ...PRESET_COLLECTION_SLUGS]);
48
49
 
@@ -206,6 +207,7 @@ export async function collectionsRoutes(fastify) {
206
207
  source: 'admin'
207
208
  });
208
209
  if (request.params.slug === 'roles') await invalidateRoles();
210
+ hooks.emit('collection:entryCreated', { slug: request.params.slug, entryId: entry.id, data: entry.data, userId: user?.id || null });
209
211
  return reply.status(201).send(entry);
210
212
  } catch (err) {
211
213
  return reply.status(400).send({ error: err.message });
@@ -216,6 +218,7 @@ export async function collectionsRoutes(fastify) {
216
218
  try {
217
219
  const entry = await updateEntry(request.params.slug, request.params.id, request.body?.data || {});
218
220
  if (request.params.slug === 'roles') await invalidateRoles();
221
+ hooks.emit('collection:entryUpdated', { slug: request.params.slug, entryId: entry.id, data: entry.data, userId: request.user?.id || null });
219
222
  return entry;
220
223
  } catch (err) {
221
224
  const status = err.message === 'Entry not found' ? 404 : 400;
@@ -233,6 +236,7 @@ export async function collectionsRoutes(fastify) {
233
236
  try {
234
237
  await deleteEntry(request.params.slug, request.params.id);
235
238
  if (request.params.slug === 'roles') await invalidateRoles();
239
+ hooks.emit('collection:entryDeleted', { slug: request.params.slug, entryId: request.params.id, userId: request.user?.id || null });
236
240
  return { success: true };
237
241
  } catch (err) {
238
242
  return reply.status(404).send({ error: err.message });
@@ -5,7 +5,7 @@
5
5
  * GET /api/plugins/admin-config - sidebar/routes/views for enabled plugins (authenticated)
6
6
  */
7
7
  import { authenticate, requireAdmin, requirePermission } from '../../middleware/auth.js';
8
- import { discoverPlugins, getPluginStates, savePluginState, getAdminPluginConfig } from '../../services/plugins.js';
8
+ import { discoverPlugins, getPluginStates, savePluginState, getAdminPluginConfig, runLifecycleHook } from '../../services/plugins.js';
9
9
 
10
10
  export async function pluginsRoutes(fastify) {
11
11
  const canRead = { preHandler: [authenticate, requirePermission('plugins', 'read')] };
@@ -38,7 +38,15 @@ export async function pluginsRoutes(fastify) {
38
38
  const manifest = manifests.find(m => m.name === name);
39
39
  if (!manifest) return reply.status(404).send({ error: 'Plugin not found' });
40
40
 
41
+ const prevState = getPluginStates()[name] || {};
41
42
  savePluginState(name, { enabled: !!enabled, settings: settings || {} });
43
+
44
+ if (!prevState.enabled && !!enabled) {
45
+ await runLifecycleHook(name, 'onEnable', fastify);
46
+ } else if (prevState.enabled && !enabled) {
47
+ await runLifecycleHook(name, 'onDisable', fastify);
48
+ }
49
+
42
50
  return { success: true };
43
51
  });
44
52
 
package/server/server.js CHANGED
@@ -75,6 +75,7 @@ await app.register(helmet, {
75
75
  }
76
76
  },
77
77
  crossOriginEmbedderPolicy: false, // allow embedding images/resources
78
+ hsts: false, // disable HSTS — server runs HTTP only; HSTS would force browser to https
78
79
  });
79
80
 
80
81
  await app.register(jwt, { secret: process.env.JWT_SECRET });
@@ -224,6 +224,36 @@ export async function getInjectionSnippets() {
224
224
  return {head, headLate, bodyEnd};
225
225
  }
226
226
 
227
+ /**
228
+ * Run a lifecycle hook (onEnable or onDisable) for a plugin if it exports one.
229
+ * Dynamically imports plugin.js and calls the named export with a context object.
230
+ * Errors are logged but do not crash the process.
231
+ *
232
+ * @param {string} name - Plugin directory name
233
+ * @param {string} hook - Export name to call ('onEnable' or 'onDisable')
234
+ * @param {import('fastify').FastifyInstance} fastify
235
+ * @returns {Promise<void>}
236
+ */
237
+ export async function runLifecycleHook(name, hook, fastify) {
238
+ // Validate hook name
239
+ if (!['onEnable', 'onDisable'].includes(hook)) return;
240
+
241
+ const pluginJsPath = path.join(PLUGINS_DIR, name, 'plugin.js');
242
+ try {
243
+ const mod = await import(pluginJsPath);
244
+ if (typeof mod[hook] !== 'function') return;
245
+
246
+ const [collections, roles] = await Promise.all([
247
+ import(path.resolve('server/services/collections.js')),
248
+ import(path.resolve('server/services/roles.js')),
249
+ ]);
250
+
251
+ await mod[hook]({ fastify, services: { collections, roles } });
252
+ } catch (err) {
253
+ fastify.log.error(`Plugin "${name}" lifecycle hook "${hook}" failed: ${err.message}`);
254
+ }
255
+ }
256
+
227
257
  /**
228
258
  * Return merged sidebar items, routes, and views from all enabled plugins.
229
259
  * Used by the frontend to dynamically extend the admin panel.
@@ -0,0 +1,227 @@
1
+ /**
2
+ * User Types Service
3
+ * Preset Collection — seeds roles on first startup, caches them in memory.
4
+ * Auth middleware calls getRoleMap() / getPermissionsFor() at request time.
5
+ */
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import {v4 as uuidv4} from 'uuid';
9
+ import {config} from '../config.js';
10
+
11
+ const COLLECTIONS_DIR = path.resolve(config.content.collectionsDir);
12
+ const SLUG = 'user-types';
13
+ const DIR = path.join(COLLECTIONS_DIR, SLUG);
14
+ const SCHEMA_PATH = path.join(DIR, 'schema.json');
15
+ const DATA_PATH = path.join(DIR, 'data.json');
16
+
17
+ export const RESOURCES = ['pages', 'settings', 'navigation', 'layouts', 'media', 'users', 'plugins', 'collections', 'views', 'actions', 'blocks'];
18
+ export const ACTIONS = ['read', 'create', 'update', 'delete'];
19
+
20
+ const PRESET_SCHEMA = {
21
+ slug: SLUG,
22
+ title: 'User Types',
23
+ description: 'CMS role definitions — managed by the system.',
24
+ preset: true,
25
+ fields: [
26
+ {name: 'name', label: 'Name (slug)', type: 'text', required: true},
27
+ {name: 'label', label: 'Label', type: 'text', required: true},
28
+ {name: 'level', label: 'Level', type: 'number', required: true},
29
+ {
30
+ name: 'permissions',
31
+ label: 'Permissions',
32
+ type: 'multi-select',
33
+ options: RESOURCES.flatMap(r => [r, ...ACTIONS.map(a => `${r}.${a}`)])
34
+ },
35
+ {
36
+ name: 'badgeClass', label: 'Badge Class', type: 'select',
37
+ options: ['badge-danger', 'badge-warning', 'badge-info', 'badge-secondary', 'badge-success', 'badge-primary']
38
+ }
39
+ ],
40
+ api: {
41
+ create: {enabled: false, access: 'admin'},
42
+ read: {enabled: false, access: 'admin'},
43
+ update: {enabled: false, access: 'admin'},
44
+ delete: {enabled: false, access: 'admin'}
45
+ }
46
+ };
47
+
48
+ const SEED_ENTRIES = [
49
+ {name: 'admin', label: 'Admin', level: 0, permissions: RESOURCES, badgeClass: 'badge-danger'},
50
+ {
51
+ name: 'manager',
52
+ label: 'Manager',
53
+ level: 1,
54
+ permissions: ['pages', 'settings', 'navigation', 'layouts', 'media', 'users', 'collections', 'views', 'actions'],
55
+ badgeClass: 'badge-warning'
56
+ },
57
+ {
58
+ name: 'editor',
59
+ label: 'Editor',
60
+ level: 2,
61
+ permissions: ['pages.read', 'pages.create', 'pages.update', 'media'],
62
+ badgeClass: 'badge-info'
63
+ },
64
+ {name: 'subscriber', label: 'Subscriber', level: 3, permissions: [], badgeClass: 'badge-secondary'}
65
+ ];
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // In-memory cache
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /** @type {Map<string,{label:string,level:number,badgeClass:string}>} */
72
+ let roleMap = new Map();
73
+
74
+ /** @type {Map<string,string[]>} resource (and resource.action) → role names */
75
+ let permissionsMap = new Map();
76
+
77
+ /** @type {Map<string,string[]>} role name → raw permissions array */
78
+ let rawPermissionsMap = new Map();
79
+
80
+ /**
81
+ * Build in-memory maps from an array of data entries.
82
+ * Supports both bare resource names ('pages') and dotted action strings ('pages.read').
83
+ * Bare names expand to all four actions for backward compatibility.
84
+ *
85
+ * @param {object[]} entries
86
+ */
87
+ function buildCache(entries) {
88
+ roleMap = new Map();
89
+ permissionsMap = new Map();
90
+ rawPermissionsMap = new Map();
91
+
92
+ const addTo = (key, role) => {
93
+ if (!permissionsMap.has(key)) permissionsMap.set(key, []);
94
+ if (!permissionsMap.get(key).includes(role)) permissionsMap.get(key).push(role);
95
+ };
96
+
97
+ for (const entry of entries) {
98
+ const d = entry.data;
99
+ roleMap.set(d.name, {label: d.label, level: d.level, badgeClass: d.badgeClass || ''});
100
+ rawPermissionsMap.set(d.name, d.permissions || []);
101
+ for (const perm of (d.permissions || [])) {
102
+ if (perm.includes('.')) {
103
+ const [res] = perm.split('.');
104
+ addTo(perm, d.name); // 'pages.read' → [role]
105
+ addTo(res, d.name); // 'pages' → [role] (any-action union)
106
+ } else {
107
+ addTo(perm, d.name); // 'pages' (bare) → [role]
108
+ for (const action of ACTIONS) {
109
+ addTo(`${perm}.${action}`, d.name); // expand to all actions
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Public API
118
+ // ---------------------------------------------------------------------------
119
+
120
+ /**
121
+ * Seed the preset collection on first startup (no-op if data.json already exists).
122
+ *
123
+ * @returns {Promise<void>}
124
+ */
125
+ export async function seed() {
126
+ await fs.mkdir(DIR, {recursive: true});
127
+
128
+ // Always write schema (overwrite to keep in sync with code)
129
+ await fs.writeFile(SCHEMA_PATH, JSON.stringify(PRESET_SCHEMA, null, 2) + '\n', 'utf8');
130
+
131
+ // Only write data if it doesn't exist yet
132
+ try {
133
+ await fs.access(DATA_PATH);
134
+ } catch {
135
+ const entries = SEED_ENTRIES.map(data => ({
136
+ id: uuidv4(),
137
+ data,
138
+ createdAt: new Date().toISOString(),
139
+ updatedAt: new Date().toISOString()
140
+ }));
141
+ await fs.writeFile(DATA_PATH, JSON.stringify(entries, null, 2) + '\n', 'utf8');
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Load the collection from disk into the in-memory cache.
147
+ *
148
+ * @returns {Promise<void>}
149
+ */
150
+ export async function load() {
151
+ try {
152
+ const raw = await fs.readFile(DATA_PATH, 'utf8');
153
+ const entries = JSON.parse(raw);
154
+ buildCache(entries);
155
+ } catch (err) {
156
+ console.warn('[userTypes] Failed to load user-types collection:', err.message);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Reload from disk — call after any CRUD on the user-types collection.
162
+ *
163
+ * @returns {Promise<void>}
164
+ */
165
+ export async function invalidate() {
166
+ await load();
167
+ }
168
+
169
+ /**
170
+ * Return the full role map.
171
+ *
172
+ * @returns {Map<string,{label:string,level:number,badgeClass:string}>}
173
+ */
174
+ export function getRoleMap() {
175
+ return roleMap;
176
+ }
177
+
178
+ /**
179
+ * Return the level for a named role, or Infinity if not found.
180
+ *
181
+ * @param {string} roleName
182
+ * @returns {number}
183
+ */
184
+ export function getRoleLevel(roleName) {
185
+ return roleMap.get(roleName)?.level ?? Infinity;
186
+ }
187
+
188
+ /**
189
+ * Return the role names allowed to access a resource (and optional action).
190
+ * - getPermissionsFor('pages') → roles with ANY action on pages (backward compat)
191
+ * - getPermissionsFor('pages', 'delete')→ roles with delete on pages
192
+ * - getPermissionsFor('pages.delete') → same as above (dot-notation shorthand)
193
+ *
194
+ * @param {string} resource - Resource key, or 'resource.action' dot notation
195
+ * @param {string} [action] - Optional action (read | create | update | delete)
196
+ * @returns {string[]}
197
+ */
198
+ export function getPermissionsFor(resource, action) {
199
+ if (action) {
200
+ return permissionsMap.get(`${resource}.${action}`) ?? [];
201
+ }
202
+ if (resource.includes('.')) {
203
+ return permissionsMap.get(resource) ?? [];
204
+ }
205
+ return permissionsMap.get(resource) ?? [];
206
+ }
207
+
208
+ /**
209
+ * Return the raw permissions array for a role — used by the /api/auth/permissions endpoint.
210
+ *
211
+ * @param {string} roleName
212
+ * @returns {string[]}
213
+ */
214
+ export function getPermissionsForRole(roleName) {
215
+ return rawPermissionsMap.get(roleName) ?? [];
216
+ }
217
+
218
+ /**
219
+ * Return role names ordered from most to least privileged.
220
+ *
221
+ * @returns {string[]}
222
+ */
223
+ export function getRoleHierarchy() {
224
+ return [...roleMap.entries()]
225
+ .sort((a, b) => a[1].level - b[1].level)
226
+ .map(([key]) => key);
227
+ }