domma-cms 0.36.11 → 0.36.12

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.
@@ -1 +1 @@
1
- .dashboard-layout{display:flex;flex-direction:column;min-height:100vh}.dashboard-wrapper{display:flex;flex:1;min-height:0}.dashboard-main{flex:1;min-width:0;overflow-y:auto}.view-container{padding:1.5rem 2rem;max-width:1200px}.view-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;gap:1rem}.view-header h1{font-size:1.5rem;font-weight:700;margin:0;display:flex;align-items:center;gap:.5rem}.view-header-actions{display:flex;gap:.5rem}.stat-card .card-body{display:flex;align-items:center;gap:1rem}.stat-icon{width:44px;height:44px;border-radius:8px;background:color-mix(in srgb,var(--dm-primary) 14%,transparent);color:var(--dm-primary, #5b8cff);display:flex;align-items:center;justify-content:center;font-size:1.25rem;flex-shrink:0}.stat-icon--success{background:color-mix(in srgb,var(--dm-success) 15%,transparent);color:var(--dm-success)}.stat-icon--warning{background:color-mix(in srgb,var(--dm-warning) 15%,transparent);color:var(--dm-warning)}.stat-value{font-size:1.75rem;font-weight:700;line-height:1}.stat-label{font-size:.8rem;color:var(--dm-text-muted, #888);margin-top:.25rem}.toolbar{display:flex;align-items:center;gap:.75rem}.form-check-label{display:flex;align-items:center;gap:.5rem;cursor:pointer;font-size:.9rem}.form-hint{display:block;font-size:.8rem;color:var(--dm-text-muted, #888);margin-top:.25rem}.dashboard-main:has(#editor-meta-tabs){overflow:hidden}.view-container:has(#editor-meta-tabs){display:flex;flex-direction:column;height:calc(100dvh - 60px);max-width:100%;padding-bottom:0}.view-container:has(#block-editor-body){max-width:100%}.view-container:has(#ep-editor-tabs){max-width:100%}.ep-editor-grid{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,1fr) minmax(0,1.3fr);gap:1rem;align-items:start}.ep-editor-grid>.ep-tryit-col,.ep-editor-grid>.ep-results-col{position:sticky;top:1rem;display:flex;flex-direction:column;gap:1rem;min-width:0}@media(max-width:1500px){.ep-editor-grid{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.ep-editor-grid>:first-child{grid-column:1 / -1}.ep-editor-grid>.ep-tryit-col,.ep-editor-grid>.ep-results-col{position:static}}@media(max-width:1024px){.ep-editor-grid{grid-template-columns:minmax(0,1fr)}}#editor-meta-tabs{flex:1;min-height:0;display:flex;flex-direction:column}#editor-meta-tabs .tab-content{flex:1;min-height:0;overflow:hidden}#editor-meta-tabs .tab-panel{height:100%;overflow-y:auto}#editor-meta-tabs .tab-panel:has(.editor-card),#live-preview-panel{overflow:hidden}.page-live-preview-frame{display:block;width:100%;height:calc(100vh - 180px);border:none;border-radius:6px}.editor-card{display:flex;flex-direction:column;height:100%}.editor-card .card-body{display:flex;flex-direction:column;flex:1;min-height:0;padding:0!important}.editor-toolbar{display:flex;align-items:center;gap:2px;padding:6px 10px;border-bottom:1px solid var(--dm-border, rgba(255, 255, 255, .08));flex-wrap:wrap;flex-shrink:0}.editor-toolbar-btn{background:none;border:none;color:var(--dm-text-muted, #888);padding:6px 8px;border-radius:4px;cursor:pointer;display:flex;align-items:center;transition:color .15s,background .15s}.editor-toolbar-btn:hover{color:var(--dm-text, #eee);background:var(--dm-hover-bg)}.editor-toolbar-sep{width:1px;height:20px;background:var(--dm-border, rgba(255, 255, 255, .08));margin:0 4px;flex-shrink:0}.editor-toolbar-right{margin-left:auto;display:flex;align-items:center;gap:2px}.editor-view-btn{background:none;border:none;color:var(--dm-text-muted, #888);padding:6px 8px;border-radius:4px;cursor:pointer;display:flex;align-items:center;transition:color .15s,background .15s}.editor-view-btn:hover{color:var(--dm-text, #eee);background:var(--dm-hover-bg)}.editor-view-btn.active{color:var(--dm-primary, #5b8cff);background:color-mix(in srgb,var(--dm-primary) 14%,transparent)}.editor-body{display:flex;flex:1;min-height:0}.editor-pane{flex:1;display:flex;flex-direction:column;min-width:0}.editor-pane--write,.editor-pane--preview{overflow:hidden}.editor-pane--write{flex-direction:row}.editor-line-numbers{padding:1rem .6rem 1rem .75rem;background:var(--dm-background-alt, rgba(0, 0, 0, .15));border-right:1px solid var(--dm-border, rgba(255, 255, 255, .08));color:var(--dm-text-muted, #666);font-family:Fira Code,Courier New,monospace;font-size:.9rem;line-height:1.6;text-align:right;user-select:none;white-space:pre;overflow:hidden;flex-shrink:0;min-width:2.5rem}.editor-line-numbers--foldable{padding:1rem 0;min-width:3rem;white-space:normal;text-align:left;display:flex;flex-direction:column}.editor-line-number-row{display:flex;align-items:center;justify-content:flex-end;padding-right:.6rem;gap:.15rem}.fold-toggle{display:inline-flex;align-items:center;justify-content:center;width:1em;font-size:.6rem;cursor:default;color:transparent}.fold-toggle--open,.fold-toggle--folded{cursor:pointer;color:var(--dm-text-muted, #666);transition:color .1s}.fold-toggle--open:hover,.fold-toggle--folded:hover{color:var(--dm-primary, #4a9eff)}.editor-line-num{min-width:1.5rem;text-align:right}.editor-mode-write .editor-pane--preview,.editor-mode-write .editor-divider,.editor-mode-preview .editor-pane--write,.editor-mode-preview .editor-divider{display:none}.editor-divider{width:1px;background:var(--dm-border, rgba(255,255,255,.08));flex-shrink:0}.editor-textarea{flex:1;resize:none;border:none;border-radius:0;background:transparent;font-family:Fira Code,Courier New,monospace;font-size:.9rem;padding:1rem;outline:none;color:inherit;line-height:1.6}.editor-preview{flex:1;padding:1rem;overflow-y:auto;line-height:1.7}.editor-preview h1,.editor-preview h2,.editor-preview h3{margin-top:1.5rem}.editor-preview p{margin-bottom:.75rem}.editor-preview code{background:var(--dm-code-bg);padding:.1em .3em;border-radius:3px;font-size:.9em}.editor-fullscreen{position:fixed;inset:0;z-index:1000;border-radius:0;margin:0}.editor-fullscreen .card-body{height:100vh}.media-picker-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:.75rem;max-height:400px;overflow-y:auto;padding:.25rem}.media-picker-item{cursor:pointer;border:2px solid transparent;border-radius:6px;overflow:hidden;transition:border-color .15s}.media-picker-item:hover{border-color:var(--dm-primary, #5b8cff)}.media-picker-item img{width:100%;height:80px;object-fit:cover;display:block}.media-picker-item span{display:block;font-size:.75rem;padding:4px 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.docs-body h3{margin-top:1.25rem;margin-bottom:.5rem}.docs-body p{margin-bottom:.5rem;line-height:1.6}.docs-body ol,.docs-body ul{margin-bottom:.75rem;padding-left:1.25rem}.docs-body li{margin-bottom:.25rem}.docs-body hr{margin:1.25rem 0;border-color:var(--dm-border, rgba(255, 255, 255, .08))}.docs-body .table{margin-bottom:.75rem}.nav-items-header,.nav-item-row{display:flex;gap:.5rem;align-items:center;padding:.35rem .75rem}.nav-items-header{border-bottom:1px solid var(--dm-border, rgba(255,255,255,.08));padding-bottom:.4rem;margin-bottom:.25rem}.nav-item-row{border-bottom:1px solid var(--dm-border, rgba(255,255,255,.04))}.nav-item-row:last-child{border-bottom:none}.nav-col-indent{width:18px;flex-shrink:0;color:var(--dm-text-muted, #888);text-align:center;font-size:.85rem}.nav-col-main{flex:2;min-width:0}.nav-col-icon{flex:0 0 90px}.nav-col-parent{flex:2;min-width:0}.nav-col-action{flex-shrink:0}.nav-item-row--child{background:color-mix(in srgb,var(--dm-text) 3%,transparent)}.nav-col-action{display:flex;align-items:center;gap:.25rem}.nav-item-row--hidden{opacity:.45}.nav-item-row--hidden .item-text,.nav-item-row--hidden .footer-link-text{text-decoration:line-through}.btn-toggle-hidden{color:var(--dm-text-muted, #888)}.btn-toggle-hidden.active{color:var(--dm-warning, #f59e0b)}.fb-field-card.fb-drag-over{outline:2px dashed var(--dm-primary, #6366f1);outline-offset:-2px}.view-container:has(.fb-fields-layout){max-width:none}.fb-fields-layout{display:grid;grid-template-columns:minmax(0,6fr) minmax(320px,4fr);gap:1.25rem;align-items:start}.fb-preview-pane{position:sticky;top:1rem;min-width:0}@media(max-width:1100px){.fb-fields-layout{grid-template-columns:1fr}.fb-preview-pane{position:static}}.toggle-label{display:flex;align-items:center;gap:.4rem;font-size:.9rem;cursor:pointer;margin-right:1rem}.preset-toggles{display:flex;flex-wrap:wrap;margin-top:.75rem}.presets-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}@media(max-width:900px){.presets-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:600px){.presets-grid{grid-template-columns:1fr}}.card-header{padding:.5rem 1rem}.card-header h2{margin:0;font-size:.9rem;font-weight:600;letter-spacing:.02em}.preset-card .card-header{display:flex;align-items:center;justify-content:space-between}.preset-card .card-header h3{margin:0;font-size:1rem}.media-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:1rem}.media-card{background:var(--dm-card-bg, rgba(255,255,255,.04));border:1px solid var(--dm-border, rgba(255,255,255,.08));border-radius:8px;overflow:hidden}.media-preview{height:130px;display:flex;align-items:center;justify-content:center;background:#0003;overflow:hidden}.media-thumb{width:100%;height:100%;object-fit:cover}.media-thumb--file{font-size:2rem;color:var(--dm-text-muted, #888)}.media-info{padding:.5rem .75rem;display:flex;flex-direction:column;gap:.15rem}.media-name{font-size:.8rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:default}.media-rename-input{width:100%;font-size:.8rem;font-weight:500;padding:.1rem .25rem;border:1px solid var(--dm-primary, #7c6af7);border-radius:3px;background:var(--dm-input-bg, transparent);color:inherit;outline:none}.media-size{font-size:.75rem;color:var(--dm-text-muted, #888)}.media-actions{padding:.5rem .75rem;display:flex;gap:.4rem;border-top:1px solid var(--dm-border, rgba(255,255,255,.08))}#admin-topbar{height:60px;display:flex;align-items:center;gap:1rem;padding:0 1.25rem;background:var(--dm-navbar-bg, rgba(0,0,0,.3));border-bottom:1px solid var(--dm-border, rgba(255,255,255,.08));position:sticky;top:0;z-index:200;flex-shrink:0}.topbar-brand{display:flex;align-items:center;gap:.5rem;font-weight:700;font-size:.95rem;color:inherit;flex-shrink:0;margin-right:.5rem}.topbar-brand span[data-icon],.topbar-brand svg{color:var(--dm-primary, #5b8cff);width:22px;height:22px}.topbar-brand-text{opacity:.85}.topbar-brand-logo{height:28px;width:auto;display:inline-block;vertical-align:middle;margin-right:.4em;object-fit:contain}.topbar-user{display:flex;align-items:center;gap:.6rem;flex:1}.topbar-user-name{font-size:.875rem;font-weight:500}.topbar-role-badge{font-size:.7rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;padding:.15rem .45rem;border-radius:4px}.topbar-role-badge--admin{background:color-mix(in srgb,var(--dm-primary) 15%,transparent);color:var(--dm-primary)}.topbar-role-badge--manager{background:color-mix(in srgb,var(--dm-success) 15%,transparent);color:var(--dm-success)}.topbar-role-badge--editor{background:color-mix(in srgb,var(--dm-warning) 15%,transparent);color:var(--dm-warning)}.topbar-role-badge--subscriber{background:color-mix(in srgb,var(--dm-text-muted) 15%,transparent);color:var(--dm-text-muted)}.topbar-actions{display:flex;align-items:center;gap:.5rem;flex-shrink:0}.topbar-action-link{display:flex;align-items:center;gap:.35rem;font-size:.875rem;color:var(--dm-text-muted, #888);text-decoration:none;padding:.4rem .65rem;border-radius:6px;transition:color .15s,background .15s}.topbar-action-link:hover{color:var(--dm-text, #eee);background:var(--dm-hover-bg)}.topbar-action-link span[data-icon],.topbar-action-link svg{width:16px;height:16px}.topbar-signout{display:flex;align-items:center;gap:.35rem;font-size:.875rem}.topbar-signout span[data-icon],.topbar-signout svg{width:16px;height:16px}.login-wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:1rem;background:var(--dm-background, #0f1117);z-index:100;overflow-y:auto}.login-card{width:100%;max-width:460px}.ob-done-icon{font-size:3rem;color:var(--dm-success);margin-bottom:1rem;text-align:center}.btn-skip{display:block;text-align:center;margin-top:.75rem;font-size:.85rem;color:var(--dm-text-muted, #888);text-decoration:none}.btn-skip:hover{text-decoration:underline}.theme-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.6rem}.theme-swatch{border:2px solid var(--dm-border, rgba(255,255,255,.1));border-radius:8px;overflow:hidden;cursor:pointer;transition:border-color .15s,transform .1s}.theme-swatch:hover{border-color:var(--dm-primary, #5b8cff);transform:translateY(-1px)}.theme-swatch.selected{border-color:var(--dm-primary, #5b8cff);box-shadow:0 0 0 2px var(--dm-primary, #5b8cff)}.theme-swatch-preview{height:52px;position:relative;display:flex;align-items:flex-end;padding:.35rem}.theme-swatch-accent{width:28px;height:6px;border-radius:3px}.theme-swatch-label{display:flex;justify-content:space-between;align-items:center;padding:.3rem .45rem;font-size:.7rem;background:var(--dm-card-bg, rgba(255,255,255,.04))}.theme-swatch-mode{color:var(--dm-text-muted, #888);font-size:.65rem}.login-logo{display:flex;align-items:center;gap:.75rem;margin-bottom:1.75rem;font-size:1.5rem}.login-logo span[data-icon]{font-size:2rem;color:var(--dm-primary, #5b8cff)}.login-logo h1{margin:0;font-size:1.5rem;font-weight:700}.login-heading{font-size:1.1rem;font-weight:600;margin-bottom:1.25rem}.btn-block{width:100%}.plugins-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}.plugin-card{display:flex;flex-direction:column}.plugin-card .card-body{flex:1}.plugin-header{display:flex;align-items:flex-start;gap:.75rem;margin-bottom:.75rem}.plugin-icon{width:40px;height:40px;border-radius:8px;background:color-mix(in srgb,var(--dm-primary) 14%,transparent);color:var(--dm-primary, #5b8cff);display:flex;align-items:center;justify-content:center;flex-shrink:0}.plugin-meta{flex:1;min-width:0}.plugin-name{font-weight:600;margin-bottom:.15rem}.plugin-version{font-size:.75rem;color:var(--dm-text-muted, #888)}.plugin-desc{font-size:.875rem;color:var(--dm-text-muted, #888);margin-bottom:1rem}.plugin-footer{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;border-top:1px solid var(--dm-border, rgba(255,255,255,.08))}.plugin-footer-actions{display:flex;align-items:center;gap:.5rem}#admin-sidebar .sidebar-link{position:relative!important}#admin-sidebar .sidebar-badge{position:absolute!important;right:.75rem!important;top:50%!important;transform:translateY(-50%)!important;margin-left:0!important;padding-left:.45rem!important;padding-right:.45rem!important}#admin-sidebar .sidebar-text{padding-right:2rem!important}#admin-sidebar.sidebar-dark{background:var(--dm-surface);border-color:var(--dm-border)}#admin-sidebar.sidebar-dark .sidebar-header{background:var(--dm-surface-raised);border-color:var(--dm-border)}#admin-sidebar.sidebar-dark .sidebar-header-title{color:var(--dm-text)}#admin-sidebar.sidebar-dark .sidebar-link{color:var(--dm-text-muted)}#admin-sidebar.sidebar-dark .sidebar-link:hover{color:var(--dm-text);background:var(--dm-surface-overlay)}#admin-sidebar.sidebar-dark .sidebar-link.active{color:var(--dm-primary);background:color-mix(in srgb,var(--dm-primary) 14%,transparent);border-left-color:var(--dm-primary)}#admin-sidebar.sidebar-dark .sidebar-heading{color:var(--dm-text-muted)}#admin-sidebar.sidebar-dark .sidebar-divider{background:var(--dm-border)}#admin-sidebar.sidebar-dark .sidebar-footer{background:var(--dm-surface-raised);border-color:var(--dm-border)}.sidebar-heading--collapsible{display:flex!important;align-items:center;justify-content:space-between;cursor:pointer;user-select:none}.sidebar-heading-toggle{display:flex;align-items:center;opacity:.5;flex-shrink:0;transition:transform .2s ease}.sidebar-heading--collapsible.is-collapsed .sidebar-heading-toggle{transform:rotate(-90deg)}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.p-0{padding:0!important}.text-muted{color:var(--dm-text-muted, #888)}@media(max-width:768px){.view-container{padding:1rem}.editor-body{flex-direction:column}.editor-divider{width:auto;height:1px}}.image-editor{display:flex;flex-direction:column;gap:0}.image-editor-toolbar{display:flex;flex-direction:row;align-items:center;gap:.25rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border, rgba(255, 255, 255, .1))}.image-editor-sep{display:inline-block;width:1px;height:1.5rem;background:var(--dm-border, rgba(255, 255, 255, .15));margin:0 .25rem;flex-shrink:0}.editor-toolbar-btn.active{color:var(--dm-primary, #7c6af7);background:color-mix(in srgb,var(--dm-primary, #7c6af7) 15%,transparent)}.editor-toolbar-caret{font-size:.6rem;line-height:1;margin-left:2px;opacity:.6}.editor-toolbar-dropdown{position:absolute;z-index:1000;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, rgba(255, 255, 255, .1));border-radius:8px;box-shadow:0 8px 24px #0006;padding:4px;min-width:160px}.editor-toolbar-dropdown-item{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:none;border:none;color:var(--dm-text, #ddd);padding:7px 10px;border-radius:5px;cursor:pointer;font-size:.85rem;transition:background .12s,color .12s}.editor-toolbar-dropdown-item:hover{background:var(--dm-hover-bg);color:var(--dm-text, #fff)}.editor-toolbar-dropdown-item span[data-icon],.editor-toolbar-dropdown-item svg{width:15px;height:15px;flex-shrink:0;opacity:.75}.editor-toolbar{position:relative}.editor-effects-dropdown-menu{position:absolute;top:100%;z-index:1000;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, rgba(255, 255, 255, .1));border-radius:8px;box-shadow:0 8px 24px #0006;padding:6px;min-width:180px;max-height:340px;overflow-y:auto}.editor-effects-category{padding:6px 8px 3px;font-size:.7rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--dm-text-muted, #888)}.editor-effects-item{display:block;width:100%;text-align:left;background:none;border:none;color:var(--dm-text, #ddd);padding:6px 10px;border-radius:5px;cursor:pointer;font-size:.85rem;transition:background .12s,color .12s}.editor-effects-item:hover{background:var(--dm-hover-bg);color:var(--dm-text, #fff)}.editor-icon-picker{position:absolute;z-index:1000;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, rgba(255, 255, 255, .1));border-radius:8px;box-shadow:0 8px 24px #0006;padding:8px;width:320px}.editor-icon-picker-search{width:100%;margin-bottom:8px;font-size:.8rem}.editor-spacer-picker{position:absolute;z-index:9999;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, #333);border-radius:8px;padding:12px;width:240px;box-shadow:0 8px 24px #00000073}.editor-spacer-picker-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--dm-text-muted, #888);margin-bottom:10px}.editor-spacer-picker-row{display:flex;align-items:center;gap:10px;margin-bottom:12px}.editor-spacer-slider{flex:1;accent-color:var(--dm-primary, #6366f1);cursor:pointer}.editor-spacer-slider-value{font-size:13px;font-weight:600;min-width:38px;text-align:right;color:var(--dm-text, #cdd6f4)}.editor-spacer-insert-btn{width:100%}.editor-icon-picker-size-row{display:flex;align-items:center;gap:8px;padding:0 8px 8px}.editor-icon-picker-size-row label{font-size:11px;color:var(--dm-text-muted, #888);white-space:nowrap}.editor-icon-picker-size{width:70px;font-size:12px;padding:3px 6px}.editor-icon-picker-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(52px,1fr));gap:2px;max-height:280px;overflow-y:auto}.editor-icon-picker-item{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:6px 2px;border-radius:5px;cursor:pointer;background:none;border:none;color:var(--dm-text, #ddd);font-size:.58rem;text-align:center;gap:4px;overflow:hidden;transition:background .12s,color .12s}.editor-icon-picker-item:hover{background:var(--dm-hover-bg);color:var(--dm-text, #fff)}.editor-icon-picker-item span[data-icon],.editor-icon-picker-item svg{width:16px;height:16px;flex-shrink:0}.editor-icon-picker-item>span:last-child{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.2}.editor-icon-picker-empty{grid-column:1 / -1;text-align:center;color:var(--dm-text-muted, #888);font-size:.8rem;padding:1.5rem}.image-editor-canvas{min-height:320px;max-height:520px;background:#111;overflow:hidden;display:flex;align-items:center;justify-content:center}.image-editor-canvas img{display:block;max-width:100%;max-height:520px}.image-editor-resize{display:flex;flex-direction:row;align-items:center;gap:.5rem;padding:.6rem .75rem;border-top:1px solid var(--dm-border, rgba(255, 255, 255, .1));font-size:.85rem}.image-editor-resize label{font-weight:600;color:var(--dm-text-muted, #888);font-size:.8rem}.image-editor-resize .form-input{width:5rem;padding:.25rem .5rem;font-size:.85rem}.image-editor-footer{display:flex;flex-direction:row;align-items:center;justify-content:space-between;padding:.75rem;border-top:1px solid var(--dm-border, rgba(255, 255, 255, .1));gap:.5rem}.image-editor-effects{border-top:1px solid var(--dm-border, rgba(255, 255, 255, .1))}.image-editor-tab-bar{display:flex;flex-direction:row;overflow-x:auto;border-bottom:1px solid var(--dm-border, rgba(255, 255, 255, .1));scrollbar-width:none}.image-editor-tab-bar::-webkit-scrollbar{display:none}.image-editor-tab-btn{display:inline-flex;align-items:center;gap:.35rem;padding:.5rem .85rem;font-size:.8rem;font-weight:500;border:none;border-bottom:2px solid transparent;background:transparent;cursor:pointer;color:var(--dm-text-muted, #888);white-space:nowrap;transition:color .15s,border-color .15s;flex-shrink:0}.image-editor-tab-btn:hover{color:var(--dm-text, #eee)}.image-editor-tab-btn.active{color:var(--dm-primary, #7c6af7);border-bottom-color:var(--dm-primary, #7c6af7)}.image-editor-tab-panel{padding:.75rem;max-height:200px;overflow-y:auto}.ie-presets-grid{display:flex;flex-wrap:wrap;gap:.35rem;margin-bottom:.5rem}.ie-preset-btn.active{color:var(--dm-primary, #7c6af7);background:color-mix(in srgb,var(--dm-primary, #7c6af7) 15%,transparent);border-color:var(--dm-primary, #7c6af7)}.ie-server-note{font-size:.73rem;color:var(--dm-text-muted, #888);font-style:italic;margin:.25rem 0 0;line-height:1.4}.ie-slider-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.4rem}.ie-slider-label{width:6rem;font-size:.8rem;color:var(--dm-text-muted, #888);font-weight:600;flex-shrink:0}.ie-slider{flex:1;accent-color:var(--dm-primary, #7c6af7);cursor:pointer}.ie-slider-val{width:3rem;font-size:.8rem;text-align:right;color:var(--dm-text, #eee);flex-shrink:0}.ie-select{flex:1;font-size:.85rem;padding:.25rem .5rem}.ie-colour-input{width:40px;height:28px;padding:0;border:1px solid var(--dm-border, rgba(255, 255, 255, .15));border-radius:4px;cursor:pointer;background:transparent;flex-shrink:0}.ie-wm-preview-row{display:flex;align-items:center;gap:.5rem;margin:.4rem 0}.ie-wm-preview-img{width:48px;height:48px;object-fit:contain;border-radius:4px;background:var(--dm-surface-raised, rgba(255, 255, 255, .06));flex-shrink:0}.ie-wm-preview-name{font-size:.8rem;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dm-text, #eee)}.ie-media-picker{padding:.25rem 0}.ie-media-picker-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:.5rem;max-height:300px;overflow-y:auto}.ie-media-thumb{display:flex;flex-direction:column;align-items:center;gap:.25rem;padding:.4rem;border-radius:6px;cursor:pointer;border:2px solid transparent;transition:border-color .15s,background .15s}.ie-media-thumb:hover{background:var(--dm-surface-raised, rgba(255, 255, 255, .08));border-color:var(--dm-primary, #7c6af7)}.ie-media-thumb img{width:72px;height:72px;object-fit:cover;border-radius:4px}.ie-media-thumb-name{font-size:.7rem;color:var(--dm-text-muted, #888);text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:72px}.dm-slideover-left,.dm-slideover-right{width:min(40rem,95vw)!important;min-width:min(36rem,100vw)!important;max-width:100vw!important}.dm-slideover-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid var(--dm-border, #333);flex-shrink:0}.dm-slideover-title{margin:0;font-size:1rem;font-weight:600}.dm-slideover-close{padding:.2rem .35rem!important;line-height:1;font-size:.75rem}.dm-slideover-body{flex:1;overflow-y:auto;min-height:0;padding:1.5rem!important;box-sizing:border-box}.dm-slideover>div:not(.dm-slideover-header):not(.dm-slideover-body):not(.dm-slideover-close){padding:1.5rem!important;box-sizing:border-box}.dm-slideover-body>div{padding-left:0!important;padding-right:0!important}.dm-slideover-body>button.btn:last-child{margin-top:.5rem}.dconfig-textarea{font-family:monospace;min-height:100px;resize:vertical}.dm-editor-line-numbers{white-space:pre}.dm-code-inline{font-family:Fira Code,Courier New,monospace;font-size:.82em;padding:.15em .35em;border-radius:3px;background:var(--dm-background-alt, rgba(0, 0, 0, .18));color:var(--dm-text, inherit);border:1px solid var(--dm-border, rgba(255, 255, 255, .08))}.dm-code-block{font-family:Fira Code,Courier New,monospace;font-size:.82rem;line-height:1.6;padding:.65rem .9rem;border-radius:6px;background:var(--dm-background-alt, rgba(0, 0, 0, .18));color:var(--dm-text, inherit);border:1px solid var(--dm-border, rgba(255, 255, 255, .08));white-space:pre;overflow-x:auto;display:block;margin:.4rem 0}.tabs-centered{text-align:center}.tabs-centered .tab-list{display:inline-flex}.tabs-centered .tab-content{text-align:left}@media(max-width:768px){.view-header{flex-direction:column;align-items:flex-start}.theme-grid{grid-template-columns:repeat(2,1fr)}.nav-item-row{flex-wrap:wrap}.nav-col-icon,.nav-col-parent{display:none}#admin-topbar{padding:0 .75rem}.topbar-brand-text{display:none}.image-editor-toolbar{flex-wrap:wrap}}#topbar-bell{position:relative}.topbar-bell-badge{position:absolute;top:2px;right:2px;min-width:16px;height:16px;padding:0 4px;background:var(--dm-danger, #dc2626);color:var(--dm-danger-text, #fff);font-size:10px;font-weight:700;line-height:16px;border-radius:9999px;text-align:center;pointer-events:none}.cb-scaffolder{padding:1rem 1.1rem;background:var(--dm-background-alt);border:1px solid var(--dm-primary);border-radius:8px;color:var(--dm-text)}.cb-scaffolder-head{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}.cb-scaffolder-head .cb-icon{flex-shrink:0;color:var(--dm-primary)}.cb-scaffolder-head h2{margin:0;font-size:1.05rem;font-weight:700;color:var(--dm-text)}.cb-scaffolder-blurb{margin:0 0 .75rem;font-size:.88rem;line-height:1.5;color:var(--dm-text-muted)}.cb-recipe-list{display:flex;flex-direction:column;gap:.5rem}.cb-recipe-card{display:flex;align-items:flex-start;gap:.75rem;text-align:left;padding:.75rem 1rem;background:var(--dm-surface);border:1px solid var(--dm-border);border-radius:6px;cursor:pointer;transition:border-color .12s;width:100%;color:var(--dm-text)}.cb-recipe-card:hover{border-color:var(--dm-primary)}.cb-recipe-card .cb-icon{flex-shrink:0;font-size:1.5rem;color:var(--dm-primary);margin-top:.1rem}.cb-recipe-meta{flex:1}.cb-recipe-name{font-weight:600;margin-bottom:.15rem;font-size:.95rem;color:var(--dm-text)}.cb-recipe-desc{font-size:.83rem;line-height:1.45;color:var(--dm-text-muted)}.cb-apply-panel{padding:1rem 1.1rem;background:var(--dm-surface);border:1px solid var(--dm-primary);border-radius:8px;color:var(--dm-text)}.cb-apply-head{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}.cb-apply-head .cb-icon{color:var(--dm-primary);font-size:1.4rem}.cb-apply-head h3{margin:0;font-size:1rem;font-weight:600;color:var(--dm-text)}.cb-apply-desc,.cb-apply-hint{margin:.25rem 0 .75rem;font-size:.85rem;line-height:1.5;color:var(--dm-text-muted)}.cb-apply-hint{font-size:.75rem;margin-top:.2rem}.cb-apply-field{margin-bottom:.6rem}.cb-apply-field label{display:block;font-size:.82rem;font-weight:600;margin-bottom:.2rem;color:var(--dm-text)}.cb-apply-field input{width:100%;padding:.4rem .55rem;border:1px solid var(--dm-border);border-radius:4px;font-size:.88rem;background:var(--dm-surface);color:var(--dm-text)}.cb-apply-status{margin:.5rem 0;font-size:.85rem;min-height:1.2rem;color:var(--dm-text)}.cb-apply-status.cb-error{color:var(--dm-danger, #dc2626)}.cb-apply-buttons{display:flex;gap:.5rem;justify-content:flex-end;margin-top:.5rem}.cb-success-panel{padding:1rem 1.1rem;background:var(--dm-surface);border:1px solid var(--dm-success, #16a34a);border-radius:8px;color:var(--dm-text)}.cb-success-panel .cb-icon{color:var(--dm-success, #16a34a);font-size:1.4rem}.cb-success-summary{margin:.25rem 0 .5rem;padding-left:1.2rem;font-size:.86rem;line-height:1.6;color:var(--dm-text)}.cb-success-warning{margin:.5rem 0;padding:.5rem .75rem;background:var(--dm-background-alt);border-left:3px solid var(--dm-warning, #eab308);border-radius:0 4px 4px 0;font-size:.82rem;color:var(--dm-text-muted)}.cb-success-snippet-label{margin:0 0 .25rem;font-size:.82rem;font-weight:600;color:var(--dm-text)}.cb-success-actions{display:flex;gap:.5rem;margin-top:.5rem;flex-wrap:wrap}.cb-code{position:relative}.cb-code pre{background:var(--dm-background-alt);color:var(--dm-text);display:block;padding:.6rem 2.4rem .6rem .75rem;white-space:pre;overflow-x:auto;margin:0;border-radius:6px;font-size:.82em;font-family:monospace;line-height:1.45}.cb-code .cb-copy{position:absolute;top:.35rem;right:.35rem;background:none;border:none;cursor:pointer;opacity:.45;padding:.2rem;line-height:1;color:var(--dm-text)}.cb-code .cb-copy:hover{opacity:1}.cb-intro{padding:.75rem 1rem;border-left:3px solid var(--dm-primary);background:var(--dm-background-alt);border-radius:0 6px 6px 0}.cb-intro p{margin:0;font-size:.92rem;line-height:1.5;color:var(--dm-text)}.cb-tutorial-link{display:flex;align-items:center;gap:.6rem;padding:.65rem .85rem;border:1px solid var(--dm-border);border-radius:6px;text-decoration:none;color:var(--dm-text);font-size:.88rem;transition:border-color .12s}.cb-tutorial-link:hover{border-color:var(--dm-primary)}.cb-tutorial-link .cb-icon{flex-shrink:0;color:var(--dm-primary)}.cb-tutorial-link .cb-arrow{color:var(--dm-text-muted);font-size:1.1rem;margin-left:auto}.menu-tree-row{display:grid;grid-template-columns:16px 1.4fr 1.4fr .9fr .7fr .9fr repeat(6,auto);gap:6px;align-items:center;padding:4px 0;transition:opacity .15s ease}.menu-tree-row--hidden{opacity:.5}.menu-tree-grip{cursor:grab;color:var(--dm-text-muted)}.menu-tree{padding:8px 0}.menu-tree-row--separator{display:grid;grid-template-columns:16px minmax(80px,1fr) repeat(7,auto);gap:6px;align-items:center;padding:2px 0;opacity:.85}.menu-tree-row--separator:hover{opacity:1}.menu-tree-row--separator .me-sep-line{display:flex;align-items:center;gap:10px;width:100%}.menu-tree-row--separator .me-sep-line:before,.menu-tree-row--separator .me-sep-line:after{content:"";flex:1;border-top:1px dashed var(--dm-border, rgba(128, 128, 128, .55))}.menu-tree-row--separator .me-sep-label{font-size:.66rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--dm-text-muted, #8a8a8a);white-space:nowrap}.dm-admin-sidebar .sidebar-divider{border:0;border-top:1px solid var(--dm-border, rgba(128, 128, 128, .5));margin:.5rem .75rem;opacity:.8}.menu-tree-row .me-icon-wrap{display:flex;gap:2px}.menu-tree-row .me-icon-wrap .me-icon{flex:1;min-width:0}.menu-tree-row .me-icon-wrap .me-icon-pick{flex-shrink:0;padding:.2rem .45rem;font-size:.85em}.dm-admin-sidebar{display:flex;flex-direction:column;gap:2px;padding:8px 0;font-size:.875rem}.dm-admin-sidebar details{margin:0}.dm-admin-sidebar details>summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:.5rem;padding:.4rem .75rem;border-radius:4px;user-select:none;font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--dm-text-muted, #888)}.dm-admin-sidebar details details>summary{font-size:.875rem;font-weight:500;text-transform:none;letter-spacing:0;color:inherit}.dm-admin-sidebar .dm-menu-badge{font-size:.7rem;font-weight:600}.dm-admin-sidebar details>summary::-webkit-details-marker{display:none}.dm-admin-sidebar details>summary:after{content:"\25b8";margin-left:auto;transition:transform .15s ease;color:var(--dm-text-muted, #999);font-size:.85em}.dm-admin-sidebar details[open]>summary:after{transform:rotate(90deg)}.dm-admin-sidebar details>div{padding-left:1rem;border-left:1px solid var(--dm-border-light, rgba(0, 0, 0, .08));margin-left:.95rem}.dm-admin-sidebar .sidebar-link{display:flex;align-items:center;gap:.5rem;padding:.35rem .75rem;text-decoration:none;color:var(--dm-text, inherit);border-radius:4px}.dm-admin-sidebar .sidebar-link:hover,.dm-admin-sidebar details>summary:hover{background:var(--dm-hover-bg, rgba(0, 0, 0, .05))}.dm-admin-sidebar .sidebar-link.active{background:var(--dm-active-bg, rgba(0, 0, 0, .08));font-weight:600}.dm-admin-sidebar .sidebar-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dm-admin-sidebar [data-icon]{flex-shrink:0}.dm-admin-sidebar-header{display:flex;align-items:center;gap:.5rem;padding:.65rem .85rem;margin:0 0 .5rem;font-weight:600;font-size:.95rem;letter-spacing:.01em;color:var(--dm-text, inherit);border-bottom:1px solid var(--dm-border-light, rgba(0, 0, 0, .08))}.dm-admin-sidebar-header [data-icon]{color:var(--dm-text-muted, #999)}.dm-admin-sidebar .sidebar-badge{margin-left:auto;padding:0 .4rem;min-width:1.25rem;height:1.1rem;line-height:1.1rem;text-align:center;font-size:.7rem;font-weight:600;border-radius:999px;background:var(--dm-active-bg, rgba(0, 0, 0, .12));color:var(--dm-text-muted, #888)}.dm-admin-sidebar .sidebar-link.active .sidebar-badge{background:var(--dm-primary, #4a86e8);color:var(--dm-primary-text, #fff)}.dm-admin-sidebar .sidebar-children{overflow:hidden;transition:max-height .22s ease,opacity .18s ease}.dm-admin-sidebar details:not([open])>.sidebar-children{opacity:0}.dm-admin-sidebar{position:relative}.dm-admin-sidebar-handle{position:absolute;top:0;right:-3px;width:6px;height:100%;cursor:col-resize;background:transparent;z-index:10;transition:background .15s}.dm-admin-sidebar-handle:hover,.dm-admin-sidebar-handle:active{background:var(--dm-primary, #4a86e8);opacity:.35}.dm-admin-sidebar--dark{background:#1a1d24;color:#e3e6eb}.dm-admin-sidebar--dark .sidebar-link,.dm-admin-sidebar--dark summary{color:#e3e6eb}.dm-admin-sidebar--dark .sidebar-link:hover,.dm-admin-sidebar--dark details>summary:hover{background:#ffffff0f}.dm-admin-sidebar--dark .sidebar-link.active{background:#4a86e840}.dm-admin-sidebar--dark .sidebar-badge{background:#ffffff1f;color:#cbd0d8}.dm-admin-sidebar--light{background:#fff;color:#1a1d24;border-right:1px solid rgba(0,0,0,.08)}.dm-admin-sidebar--light .sidebar-link,.dm-admin-sidebar--light summary{color:#1a1d24}.dm-admin-sidebar--light .sidebar-link:hover,.dm-admin-sidebar--light details>summary:hover{background:#0000000a}.dm-admin-sidebar--light .sidebar-link.active{background:#4a86e826}.dm-admin-sidebar--transparent{background:transparent}.page-header{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin:0 0 1.25rem;padding-bottom:.75rem;border-bottom:1px solid var(--dm-border-light, rgba(0, 0, 0, .08))}.page-header h2{margin:0;display:flex;align-items:center;gap:.5rem;font-size:1.4rem;line-height:1.2}.page-header h2 [data-icon]{color:var(--dm-text-muted, #888)}.header-actions{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.card+.card,.card+p,p+.card,.card+.toolbar,.toolbar+.card,.card+.grid{margin-top:1rem}.toolbar{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem}.btn+.btn,.btn+a.btn,a.btn+.btn,a.btn+a.btn{margin-left:.35rem}[style*=inline-flex] .btn+.btn,[style*=inline-flex] .btn+a.btn,[style*=inline-flex] a.btn+.btn{margin-left:0}.card-body>[id$=-form],.card-body>form{margin:.25rem 0}.card-header{display:flex;align-items:center;gap:.5rem}.card-header h3{margin:0;font-size:1.05rem}.card-header [data-icon]{color:var(--dm-text-muted, #888)}.page-header+p.text-muted{margin:-.5rem 0 1rem}.dm-pe-grid{gap:.75rem 1rem}.dm-pe-grid .form-group{display:flex;flex-direction:column;gap:.25rem;margin:0}.dm-pe-grid .form-group.col-span-2{grid-column:1 / -1}.dm-pe-grid label{font-size:.85rem;font-weight:600;color:var(--dm-text, inherit)}.dm-pe-grid label .required{color:var(--dm-danger, #c33);margin-left:2px}.dm-pe-grid .form-hint{font-size:.75rem;color:var(--dm-text-muted, #888);margin-top:.1rem}.dm-pe-grid .form-input{width:100%;padding:.4rem .6rem}.dm-pe-grid textarea.form-input{resize:vertical;min-height:2.5rem}.dm-menu-badge{display:inline-block;margin-left:6px;padding:1px 7px;font-size:.7em;font-weight:700;line-height:1.4;border-radius:999px;background:var(--dm-text-muted);color:var(--dm-text-inverse, #fff);vertical-align:middle}.menu-tree-row--slim{display:grid;grid-template-columns:16px 1fr auto repeat(9,auto);gap:6px;align-items:center}.menu-tree-row--slim .me-row-label{font-weight:600}.menu-tree-row--slim .me-row-url{color:var(--dm-text-muted);font-size:.85em}.me-item-form{display:flex;flex-direction:column;gap:10px;padding:4px 2px}.me-item-form .form-field{display:flex;flex-direction:column;gap:4px}.me-item-form .form-label{font-size:.85em;color:var(--dm-text-muted)}.me-form-section{margin:12px 0 2px;padding-top:10px;border-top:1px solid var(--dm-border-light, rgba(0,0,0,.15));font-size:.78em;text-transform:uppercase;letter-spacing:.06em;color:var(--dm-text-muted)}.me-item-form .me-form-section:first-child{margin-top:0;padding-top:0;border-top:0}.me-item-form .me-colour-hex{width:48px;height:32px;padding:2px}.me-item-form .me-field-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.me-item-form .me-field-grid .form-field{margin:0}.me-item-form .me-icon-field{display:flex;align-items:center;gap:8px}.me-item-form .me-icon-field .form-input{flex:1}.me-item-form .me-icon-preview{width:24px;height:24px;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;color:var(--dm-text-muted)}
1
+ .dashboard-layout{display:flex;flex-direction:column;min-height:100vh}.dashboard-wrapper{display:flex;flex:1;min-height:0}.dashboard-main{flex:1;min-width:0;overflow-y:auto}.view-container{padding:1.5rem 2rem;max-width:1200px}.view-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;gap:1rem}.view-header h1{font-size:1.5rem;font-weight:700;margin:0;display:flex;align-items:center;gap:.5rem}.view-header-actions{display:flex;gap:.5rem}.stat-card .card-body{display:flex;align-items:center;gap:1rem}.stat-icon{width:44px;height:44px;border-radius:8px;background:color-mix(in srgb,var(--dm-primary) 14%,transparent);color:var(--dm-primary, #5b8cff);display:flex;align-items:center;justify-content:center;font-size:1.25rem;flex-shrink:0}.stat-icon--success{background:color-mix(in srgb,var(--dm-success) 15%,transparent);color:var(--dm-success)}.stat-icon--warning{background:color-mix(in srgb,var(--dm-warning) 15%,transparent);color:var(--dm-warning)}.stat-value{font-size:1.75rem;font-weight:700;line-height:1}.stat-label{font-size:.8rem;color:var(--dm-text-muted, #888);margin-top:.25rem}.toolbar{display:flex;align-items:center;gap:.75rem}.form-check-label{display:flex;align-items:center;gap:.5rem;cursor:pointer;font-size:.9rem}.form-hint{display:block;font-size:.8rem;color:var(--dm-text-muted, #888);margin-top:.25rem}.dashboard-main:has(#editor-meta-tabs){overflow:hidden}.view-container:has(#editor-meta-tabs){display:flex;flex-direction:column;height:calc(100dvh - 60px);max-width:100%;padding-bottom:0}.view-container:has(#block-editor-body){max-width:100%}.view-container:has(#ep-editor-tabs){max-width:100%}.ep-editor-grid{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,1fr) minmax(0,1.3fr);gap:1rem;align-items:start}.ep-editor-grid>.ep-tryit-col,.ep-editor-grid>.ep-results-col{position:sticky;top:1rem;display:flex;flex-direction:column;gap:1rem;min-width:0}@media(max-width:1500px){.ep-editor-grid{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.ep-editor-grid>:first-child{grid-column:1 / -1}.ep-editor-grid>.ep-tryit-col,.ep-editor-grid>.ep-results-col{position:static}}@media(max-width:1024px){.ep-editor-grid{grid-template-columns:minmax(0,1fr)}}#editor-meta-tabs{flex:1;min-height:0;display:flex;flex-direction:column}#editor-meta-tabs .tab-content{flex:1;min-height:0;overflow:hidden}#editor-meta-tabs .tab-panel{height:100%;overflow-y:auto}#editor-meta-tabs .tab-panel:has(.editor-card),#live-preview-panel{overflow:hidden}.page-live-preview-frame{display:block;width:100%;height:calc(100vh - 180px);border:none;border-radius:6px}.editor-card{display:flex;flex-direction:column;height:100%}.editor-card .card-body{display:flex;flex-direction:column;flex:1;min-height:0;padding:0!important}.editor-toolbar{display:flex;align-items:center;gap:2px;padding:6px 10px;border-bottom:1px solid var(--dm-border, rgba(255, 255, 255, .08));flex-wrap:wrap;flex-shrink:0}.editor-toolbar-btn{background:none;border:none;color:var(--dm-text-muted, #888);padding:6px 8px;border-radius:4px;cursor:pointer;display:flex;align-items:center;transition:color .15s,background .15s}.editor-toolbar-btn:hover{color:var(--dm-text, #eee);background:var(--dm-hover-bg)}.editor-toolbar-sep{width:1px;height:20px;background:var(--dm-border, rgba(255, 255, 255, .08));margin:0 4px;flex-shrink:0}.editor-toolbar-right{margin-left:auto;display:flex;align-items:center;gap:2px}.editor-view-btn{background:none;border:none;color:var(--dm-text-muted, #888);padding:6px 8px;border-radius:4px;cursor:pointer;display:flex;align-items:center;transition:color .15s,background .15s}.editor-view-btn:hover{color:var(--dm-text, #eee);background:var(--dm-hover-bg)}.editor-view-btn.active{color:var(--dm-primary, #5b8cff);background:color-mix(in srgb,var(--dm-primary) 14%,transparent)}.editor-body{display:flex;flex:1;min-height:0}.editor-pane{flex:1;display:flex;flex-direction:column;min-width:0}.editor-pane--write,.editor-pane--preview{overflow:hidden}.editor-pane--write{flex-direction:row}.editor-line-numbers{padding:1rem .6rem 1rem .75rem;background:var(--dm-background-alt, rgba(0, 0, 0, .15));border-right:1px solid var(--dm-border, rgba(255, 255, 255, .08));color:var(--dm-text-muted, #666);font-family:Fira Code,Courier New,monospace;font-size:.9rem;line-height:1.6;text-align:right;user-select:none;white-space:pre;overflow:hidden;flex-shrink:0;min-width:2.5rem}.editor-line-numbers--foldable{padding:1rem 0;min-width:3rem;white-space:normal;text-align:left;display:flex;flex-direction:column}.editor-line-number-row{display:flex;align-items:center;justify-content:flex-end;padding-right:.6rem;gap:.15rem}.fold-toggle{display:inline-flex;align-items:center;justify-content:center;width:1em;font-size:.6rem;cursor:default;color:transparent}.fold-toggle--open,.fold-toggle--folded{cursor:pointer;color:var(--dm-text-muted, #666);transition:color .1s}.fold-toggle--open:hover,.fold-toggle--folded:hover{color:var(--dm-primary, #4a9eff)}.editor-line-num{min-width:1.5rem;text-align:right}.editor-mode-write .editor-pane--preview,.editor-mode-write .editor-divider,.editor-mode-preview .editor-pane--write,.editor-mode-preview .editor-divider{display:none}.editor-divider{width:1px;background:var(--dm-border, rgba(255,255,255,.08));flex-shrink:0}.editor-textarea{flex:1;resize:none;border:none;border-radius:0;background:transparent;font-family:Fira Code,Courier New,monospace;font-size:.9rem;padding:1rem;outline:none;color:inherit;line-height:1.6}.editor-preview{flex:1;padding:1rem;overflow-y:auto;line-height:1.7}.editor-preview h1,.editor-preview h2,.editor-preview h3{margin-top:1.5rem}.editor-preview p{margin-bottom:.75rem}.editor-preview code{background:var(--dm-code-bg);padding:.1em .3em;border-radius:3px;font-size:.9em}.editor-fullscreen{position:fixed;inset:0;z-index:1000;border-radius:0;margin:0}.editor-fullscreen .card-body{height:100vh}.media-picker-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:.75rem;max-height:400px;overflow-y:auto;padding:.25rem}.media-picker-item{cursor:pointer;border:2px solid transparent;border-radius:6px;overflow:hidden;transition:border-color .15s}.media-picker-item:hover{border-color:var(--dm-primary, #5b8cff)}.media-picker-item img{width:100%;height:80px;object-fit:cover;display:block}.media-picker-item span{display:block;font-size:.75rem;padding:4px 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.docs-body h3{margin-top:1.25rem;margin-bottom:.5rem}.docs-body p{margin-bottom:.5rem;line-height:1.6}.docs-body ol,.docs-body ul{margin-bottom:.75rem;padding-left:1.25rem}.docs-body li{margin-bottom:.25rem}.docs-body hr{margin:1.25rem 0;border-color:var(--dm-border, rgba(255, 255, 255, .08))}.docs-body .table{margin-bottom:.75rem}.nav-items-header,.nav-item-row{display:flex;gap:.5rem;align-items:center;padding:.35rem .75rem}.nav-items-header{border-bottom:1px solid var(--dm-border, rgba(255,255,255,.08));padding-bottom:.4rem;margin-bottom:.25rem}.nav-item-row{border-bottom:1px solid var(--dm-border, rgba(255,255,255,.04))}.nav-item-row:last-child{border-bottom:none}.nav-col-indent{width:18px;flex-shrink:0;color:var(--dm-text-muted, #888);text-align:center;font-size:.85rem}.nav-col-main{flex:2;min-width:0}.nav-col-icon{flex:0 0 90px}.nav-col-parent{flex:2;min-width:0}.nav-col-action{flex-shrink:0}.nav-item-row--child{background:color-mix(in srgb,var(--dm-text) 3%,transparent)}.nav-col-action{display:flex;align-items:center;gap:.25rem}.nav-item-row--hidden{opacity:.45}.nav-item-row--hidden .item-text,.nav-item-row--hidden .footer-link-text{text-decoration:line-through}.btn-toggle-hidden{color:var(--dm-text-muted, #888)}.btn-toggle-hidden.active{color:var(--dm-warning, #f59e0b)}.fb-field-card.fb-drag-over{outline:2px dashed var(--dm-primary, #6366f1);outline-offset:-2px}.btn-ghost span[data-icon=trash]{color:var(--dm-danger, #dc2626)}.view-container:has(.fb-fields-layout){max-width:none}.fb-fields-layout{display:grid;grid-template-columns:minmax(0,6fr) minmax(320px,4fr);gap:1.25rem;align-items:start}.fb-preview-pane{position:sticky;top:1rem;min-width:0}@media(max-width:1100px){.fb-fields-layout{grid-template-columns:1fr}.fb-preview-pane{position:static}}.toggle-label{display:flex;align-items:center;gap:.4rem;font-size:.9rem;cursor:pointer;margin-right:1rem}.preset-toggles{display:flex;flex-wrap:wrap;margin-top:.75rem}.presets-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}@media(max-width:900px){.presets-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:600px){.presets-grid{grid-template-columns:1fr}}.card-header{padding:.5rem 1rem}.card-header h2{margin:0;font-size:.9rem;font-weight:600;letter-spacing:.02em}.preset-card .card-header{display:flex;align-items:center;justify-content:space-between}.preset-card .card-header h3{margin:0;font-size:1rem}.media-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:1rem}.media-card{background:var(--dm-card-bg, rgba(255,255,255,.04));border:1px solid var(--dm-border, rgba(255,255,255,.08));border-radius:8px;overflow:hidden}.media-preview{height:130px;display:flex;align-items:center;justify-content:center;background:#0003;overflow:hidden}.media-thumb{width:100%;height:100%;object-fit:cover}.media-thumb--file{font-size:2rem;color:var(--dm-text-muted, #888)}.media-info{padding:.5rem .75rem;display:flex;flex-direction:column;gap:.15rem}.media-name{font-size:.8rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:default}.media-rename-input{width:100%;font-size:.8rem;font-weight:500;padding:.1rem .25rem;border:1px solid var(--dm-primary, #7c6af7);border-radius:3px;background:var(--dm-input-bg, transparent);color:inherit;outline:none}.media-size{font-size:.75rem;color:var(--dm-text-muted, #888)}.media-actions{padding:.5rem .75rem;display:flex;gap:.4rem;border-top:1px solid var(--dm-border, rgba(255,255,255,.08))}#admin-topbar{height:60px;display:flex;align-items:center;gap:1rem;padding:0 1.25rem;background:var(--dm-navbar-bg, rgba(0,0,0,.3));border-bottom:1px solid var(--dm-border, rgba(255,255,255,.08));position:sticky;top:0;z-index:200;flex-shrink:0}.topbar-brand{display:flex;align-items:center;gap:.5rem;font-weight:700;font-size:.95rem;color:inherit;flex-shrink:0;margin-right:.5rem}.topbar-brand span[data-icon],.topbar-brand svg{color:var(--dm-primary, #5b8cff);width:22px;height:22px}.topbar-brand-text{opacity:.85}.topbar-brand-logo{height:28px;width:auto;display:inline-block;vertical-align:middle;margin-right:.4em;object-fit:contain}.topbar-user{display:flex;align-items:center;gap:.6rem;flex:1}.topbar-user-name{font-size:.875rem;font-weight:500}.topbar-role-badge{font-size:.7rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;padding:.15rem .45rem;border-radius:4px}.topbar-role-badge--admin{background:color-mix(in srgb,var(--dm-primary) 15%,transparent);color:var(--dm-primary)}.topbar-role-badge--manager{background:color-mix(in srgb,var(--dm-success) 15%,transparent);color:var(--dm-success)}.topbar-role-badge--editor{background:color-mix(in srgb,var(--dm-warning) 15%,transparent);color:var(--dm-warning)}.topbar-role-badge--subscriber{background:color-mix(in srgb,var(--dm-text-muted) 15%,transparent);color:var(--dm-text-muted)}.topbar-actions{display:flex;align-items:center;gap:.5rem;flex-shrink:0}.topbar-action-link{display:flex;align-items:center;gap:.35rem;font-size:.875rem;color:var(--dm-text-muted, #888);text-decoration:none;padding:.4rem .65rem;border-radius:6px;transition:color .15s,background .15s}.topbar-action-link:hover{color:var(--dm-text, #eee);background:var(--dm-hover-bg)}.topbar-action-link span[data-icon],.topbar-action-link svg{width:16px;height:16px}.topbar-signout{display:flex;align-items:center;gap:.35rem;font-size:.875rem}.topbar-signout span[data-icon],.topbar-signout svg{width:16px;height:16px}.login-wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:1rem;background:var(--dm-background, #0f1117);z-index:100;overflow-y:auto}.login-card{width:100%;max-width:460px}.ob-done-icon{font-size:3rem;color:var(--dm-success);margin-bottom:1rem;text-align:center}.btn-skip{display:block;text-align:center;margin-top:.75rem;font-size:.85rem;color:var(--dm-text-muted, #888);text-decoration:none}.btn-skip:hover{text-decoration:underline}.theme-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.6rem}.theme-swatch{border:2px solid var(--dm-border, rgba(255,255,255,.1));border-radius:8px;overflow:hidden;cursor:pointer;transition:border-color .15s,transform .1s}.theme-swatch:hover{border-color:var(--dm-primary, #5b8cff);transform:translateY(-1px)}.theme-swatch.selected{border-color:var(--dm-primary, #5b8cff);box-shadow:0 0 0 2px var(--dm-primary, #5b8cff)}.theme-swatch-preview{height:52px;position:relative;display:flex;align-items:flex-end;padding:.35rem}.theme-swatch-accent{width:28px;height:6px;border-radius:3px}.theme-swatch-label{display:flex;justify-content:space-between;align-items:center;padding:.3rem .45rem;font-size:.7rem;background:var(--dm-card-bg, rgba(255,255,255,.04))}.theme-swatch-mode{color:var(--dm-text-muted, #888);font-size:.65rem}.login-logo{display:flex;align-items:center;gap:.75rem;margin-bottom:1.75rem;font-size:1.5rem}.login-logo span[data-icon]{font-size:2rem;color:var(--dm-primary, #5b8cff)}.login-logo h1{margin:0;font-size:1.5rem;font-weight:700}.login-heading{font-size:1.1rem;font-weight:600;margin-bottom:1.25rem}.btn-block{width:100%}.plugins-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}.plugin-card{display:flex;flex-direction:column}.plugin-card .card-body{flex:1}.plugin-header{display:flex;align-items:flex-start;gap:.75rem;margin-bottom:.75rem}.plugin-icon{width:40px;height:40px;border-radius:8px;background:color-mix(in srgb,var(--dm-primary) 14%,transparent);color:var(--dm-primary, #5b8cff);display:flex;align-items:center;justify-content:center;flex-shrink:0}.plugin-meta{flex:1;min-width:0}.plugin-name{font-weight:600;margin-bottom:.15rem}.plugin-version{font-size:.75rem;color:var(--dm-text-muted, #888)}.plugin-desc{font-size:.875rem;color:var(--dm-text-muted, #888);margin-bottom:1rem}.plugin-footer{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;border-top:1px solid var(--dm-border, rgba(255,255,255,.08))}.plugin-footer-actions{display:flex;align-items:center;gap:.5rem}#admin-sidebar .sidebar-link{position:relative!important}#admin-sidebar .sidebar-badge{position:absolute!important;right:.75rem!important;top:50%!important;transform:translateY(-50%)!important;margin-left:0!important;padding-left:.45rem!important;padding-right:.45rem!important}#admin-sidebar .sidebar-text{padding-right:2rem!important}#admin-sidebar.sidebar-dark{background:var(--dm-surface);border-color:var(--dm-border)}#admin-sidebar.sidebar-dark .sidebar-header{background:var(--dm-surface-raised);border-color:var(--dm-border)}#admin-sidebar.sidebar-dark .sidebar-header-title{color:var(--dm-text)}#admin-sidebar.sidebar-dark .sidebar-link{color:var(--dm-text-muted)}#admin-sidebar.sidebar-dark .sidebar-link:hover{color:var(--dm-text);background:var(--dm-surface-overlay)}#admin-sidebar.sidebar-dark .sidebar-link.active{color:var(--dm-primary);background:color-mix(in srgb,var(--dm-primary) 14%,transparent);border-left-color:var(--dm-primary)}#admin-sidebar.sidebar-dark .sidebar-heading{color:var(--dm-text-muted)}#admin-sidebar.sidebar-dark .sidebar-divider{background:var(--dm-border)}#admin-sidebar.sidebar-dark .sidebar-footer{background:var(--dm-surface-raised);border-color:var(--dm-border)}.sidebar-heading--collapsible{display:flex!important;align-items:center;justify-content:space-between;cursor:pointer;user-select:none}.sidebar-heading-toggle{display:flex;align-items:center;opacity:.5;flex-shrink:0;transition:transform .2s ease}.sidebar-heading--collapsible.is-collapsed .sidebar-heading-toggle{transform:rotate(-90deg)}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.p-0{padding:0!important}.text-muted{color:var(--dm-text-muted, #888)}@media(max-width:768px){.view-container{padding:1rem}.editor-body{flex-direction:column}.editor-divider{width:auto;height:1px}}.image-editor{display:flex;flex-direction:column;gap:0}.image-editor-toolbar{display:flex;flex-direction:row;align-items:center;gap:.25rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border, rgba(255, 255, 255, .1))}.image-editor-sep{display:inline-block;width:1px;height:1.5rem;background:var(--dm-border, rgba(255, 255, 255, .15));margin:0 .25rem;flex-shrink:0}.editor-toolbar-btn.active{color:var(--dm-primary, #7c6af7);background:color-mix(in srgb,var(--dm-primary, #7c6af7) 15%,transparent)}.editor-toolbar-caret{font-size:.6rem;line-height:1;margin-left:2px;opacity:.6}.editor-toolbar-dropdown{position:absolute;z-index:1000;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, rgba(255, 255, 255, .1));border-radius:8px;box-shadow:0 8px 24px #0006;padding:4px;min-width:160px}.editor-toolbar-dropdown-item{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:none;border:none;color:var(--dm-text, #ddd);padding:7px 10px;border-radius:5px;cursor:pointer;font-size:.85rem;transition:background .12s,color .12s}.editor-toolbar-dropdown-item:hover{background:var(--dm-hover-bg);color:var(--dm-text, #fff)}.editor-toolbar-dropdown-item span[data-icon],.editor-toolbar-dropdown-item svg{width:15px;height:15px;flex-shrink:0;opacity:.75}.editor-toolbar{position:relative}.editor-effects-dropdown-menu{position:absolute;top:100%;z-index:1000;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, rgba(255, 255, 255, .1));border-radius:8px;box-shadow:0 8px 24px #0006;padding:6px;min-width:180px;max-height:340px;overflow-y:auto}.editor-effects-category{padding:6px 8px 3px;font-size:.7rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--dm-text-muted, #888)}.editor-effects-item{display:block;width:100%;text-align:left;background:none;border:none;color:var(--dm-text, #ddd);padding:6px 10px;border-radius:5px;cursor:pointer;font-size:.85rem;transition:background .12s,color .12s}.editor-effects-item:hover{background:var(--dm-hover-bg);color:var(--dm-text, #fff)}.editor-icon-picker{position:absolute;z-index:1000;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, rgba(255, 255, 255, .1));border-radius:8px;box-shadow:0 8px 24px #0006;padding:8px;width:320px}.editor-icon-picker-search{width:100%;margin-bottom:8px;font-size:.8rem}.editor-spacer-picker{position:absolute;z-index:9999;background:var(--dm-surface, #1e1e2e);border:1px solid var(--dm-border, #333);border-radius:8px;padding:12px;width:240px;box-shadow:0 8px 24px #00000073}.editor-spacer-picker-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--dm-text-muted, #888);margin-bottom:10px}.editor-spacer-picker-row{display:flex;align-items:center;gap:10px;margin-bottom:12px}.editor-spacer-slider{flex:1;accent-color:var(--dm-primary, #6366f1);cursor:pointer}.editor-spacer-slider-value{font-size:13px;font-weight:600;min-width:38px;text-align:right;color:var(--dm-text, #cdd6f4)}.editor-spacer-insert-btn{width:100%}.editor-icon-picker-size-row{display:flex;align-items:center;gap:8px;padding:0 8px 8px}.editor-icon-picker-size-row label{font-size:11px;color:var(--dm-text-muted, #888);white-space:nowrap}.editor-icon-picker-size{width:70px;font-size:12px;padding:3px 6px}.editor-icon-picker-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(52px,1fr));gap:2px;max-height:280px;overflow-y:auto}.editor-icon-picker-item{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:6px 2px;border-radius:5px;cursor:pointer;background:none;border:none;color:var(--dm-text, #ddd);font-size:.58rem;text-align:center;gap:4px;overflow:hidden;transition:background .12s,color .12s}.editor-icon-picker-item:hover{background:var(--dm-hover-bg);color:var(--dm-text, #fff)}.editor-icon-picker-item span[data-icon],.editor-icon-picker-item svg{width:16px;height:16px;flex-shrink:0}.editor-icon-picker-item>span:last-child{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.2}.editor-icon-picker-empty{grid-column:1 / -1;text-align:center;color:var(--dm-text-muted, #888);font-size:.8rem;padding:1.5rem}.image-editor-canvas{min-height:320px;max-height:520px;background:#111;overflow:hidden;display:flex;align-items:center;justify-content:center}.image-editor-canvas img{display:block;max-width:100%;max-height:520px}.image-editor-resize{display:flex;flex-direction:row;align-items:center;gap:.5rem;padding:.6rem .75rem;border-top:1px solid var(--dm-border, rgba(255, 255, 255, .1));font-size:.85rem}.image-editor-resize label{font-weight:600;color:var(--dm-text-muted, #888);font-size:.8rem}.image-editor-resize .form-input{width:5rem;padding:.25rem .5rem;font-size:.85rem}.image-editor-footer{display:flex;flex-direction:row;align-items:center;justify-content:space-between;padding:.75rem;border-top:1px solid var(--dm-border, rgba(255, 255, 255, .1));gap:.5rem}.image-editor-effects{border-top:1px solid var(--dm-border, rgba(255, 255, 255, .1))}.image-editor-tab-bar{display:flex;flex-direction:row;overflow-x:auto;border-bottom:1px solid var(--dm-border, rgba(255, 255, 255, .1));scrollbar-width:none}.image-editor-tab-bar::-webkit-scrollbar{display:none}.image-editor-tab-btn{display:inline-flex;align-items:center;gap:.35rem;padding:.5rem .85rem;font-size:.8rem;font-weight:500;border:none;border-bottom:2px solid transparent;background:transparent;cursor:pointer;color:var(--dm-text-muted, #888);white-space:nowrap;transition:color .15s,border-color .15s;flex-shrink:0}.image-editor-tab-btn:hover{color:var(--dm-text, #eee)}.image-editor-tab-btn.active{color:var(--dm-primary, #7c6af7);border-bottom-color:var(--dm-primary, #7c6af7)}.image-editor-tab-panel{padding:.75rem;max-height:200px;overflow-y:auto}.ie-presets-grid{display:flex;flex-wrap:wrap;gap:.35rem;margin-bottom:.5rem}.ie-preset-btn.active{color:var(--dm-primary, #7c6af7);background:color-mix(in srgb,var(--dm-primary, #7c6af7) 15%,transparent);border-color:var(--dm-primary, #7c6af7)}.ie-server-note{font-size:.73rem;color:var(--dm-text-muted, #888);font-style:italic;margin:.25rem 0 0;line-height:1.4}.ie-slider-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.4rem}.ie-slider-label{width:6rem;font-size:.8rem;color:var(--dm-text-muted, #888);font-weight:600;flex-shrink:0}.ie-slider{flex:1;accent-color:var(--dm-primary, #7c6af7);cursor:pointer}.ie-slider-val{width:3rem;font-size:.8rem;text-align:right;color:var(--dm-text, #eee);flex-shrink:0}.ie-select{flex:1;font-size:.85rem;padding:.25rem .5rem}.ie-colour-input{width:40px;height:28px;padding:0;border:1px solid var(--dm-border, rgba(255, 255, 255, .15));border-radius:4px;cursor:pointer;background:transparent;flex-shrink:0}.ie-wm-preview-row{display:flex;align-items:center;gap:.5rem;margin:.4rem 0}.ie-wm-preview-img{width:48px;height:48px;object-fit:contain;border-radius:4px;background:var(--dm-surface-raised, rgba(255, 255, 255, .06));flex-shrink:0}.ie-wm-preview-name{font-size:.8rem;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dm-text, #eee)}.ie-media-picker{padding:.25rem 0}.ie-media-picker-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:.5rem;max-height:300px;overflow-y:auto}.ie-media-thumb{display:flex;flex-direction:column;align-items:center;gap:.25rem;padding:.4rem;border-radius:6px;cursor:pointer;border:2px solid transparent;transition:border-color .15s,background .15s}.ie-media-thumb:hover{background:var(--dm-surface-raised, rgba(255, 255, 255, .08));border-color:var(--dm-primary, #7c6af7)}.ie-media-thumb img{width:72px;height:72px;object-fit:cover;border-radius:4px}.ie-media-thumb-name{font-size:.7rem;color:var(--dm-text-muted, #888);text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:72px}.dm-slideover-left,.dm-slideover-right{width:min(40rem,95vw)!important;min-width:min(36rem,100vw)!important;max-width:100vw!important}.dm-slideover-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid var(--dm-border, #333);flex-shrink:0}.dm-slideover-title{margin:0;font-size:1rem;font-weight:600}.dm-slideover-close{padding:.2rem .35rem!important;line-height:1;font-size:.75rem}.dm-slideover-body{flex:1;overflow-y:auto;min-height:0;padding:1.5rem!important;box-sizing:border-box}.dm-slideover>div:not(.dm-slideover-header):not(.dm-slideover-body):not(.dm-slideover-close){padding:1.5rem!important;box-sizing:border-box}.dm-slideover-body>div{padding-left:0!important;padding-right:0!important}.dm-slideover-body>button.btn:last-child{margin-top:.5rem}.dconfig-textarea{font-family:monospace;min-height:100px;resize:vertical}.dm-editor-line-numbers{white-space:pre}.dm-code-inline{font-family:Fira Code,Courier New,monospace;font-size:.82em;padding:.15em .35em;border-radius:3px;background:var(--dm-background-alt, rgba(0, 0, 0, .18));color:var(--dm-text, inherit);border:1px solid var(--dm-border, rgba(255, 255, 255, .08))}.dm-code-block{font-family:Fira Code,Courier New,monospace;font-size:.82rem;line-height:1.6;padding:.65rem .9rem;border-radius:6px;background:var(--dm-background-alt, rgba(0, 0, 0, .18));color:var(--dm-text, inherit);border:1px solid var(--dm-border, rgba(255, 255, 255, .08));white-space:pre;overflow-x:auto;display:block;margin:.4rem 0}.tabs-centered{text-align:center}.tabs-centered .tab-list{display:inline-flex}.tabs-centered .tab-content{text-align:left}@media(max-width:768px){.view-header{flex-direction:column;align-items:flex-start}.theme-grid{grid-template-columns:repeat(2,1fr)}.nav-item-row{flex-wrap:wrap}.nav-col-icon,.nav-col-parent{display:none}#admin-topbar{padding:0 .75rem}.topbar-brand-text{display:none}.image-editor-toolbar{flex-wrap:wrap}}#topbar-bell{position:relative}.topbar-bell-badge{position:absolute;top:2px;right:2px;min-width:16px;height:16px;padding:0 4px;background:var(--dm-danger, #dc2626);color:var(--dm-danger-text, #fff);font-size:10px;font-weight:700;line-height:16px;border-radius:9999px;text-align:center;pointer-events:none}.cb-scaffolder{padding:1rem 1.1rem;background:var(--dm-background-alt);border:1px solid var(--dm-primary);border-radius:8px;color:var(--dm-text)}.cb-scaffolder-head{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}.cb-scaffolder-head .cb-icon{flex-shrink:0;color:var(--dm-primary)}.cb-scaffolder-head h2{margin:0;font-size:1.05rem;font-weight:700;color:var(--dm-text)}.cb-scaffolder-blurb{margin:0 0 .75rem;font-size:.88rem;line-height:1.5;color:var(--dm-text-muted)}.cb-recipe-list{display:flex;flex-direction:column;gap:.5rem}.cb-recipe-card{display:flex;align-items:flex-start;gap:.75rem;text-align:left;padding:.75rem 1rem;background:var(--dm-surface);border:1px solid var(--dm-border);border-radius:6px;cursor:pointer;transition:border-color .12s;width:100%;color:var(--dm-text)}.cb-recipe-card:hover{border-color:var(--dm-primary)}.cb-recipe-card .cb-icon{flex-shrink:0;font-size:1.5rem;color:var(--dm-primary);margin-top:.1rem}.cb-recipe-meta{flex:1}.cb-recipe-name{font-weight:600;margin-bottom:.15rem;font-size:.95rem;color:var(--dm-text)}.cb-recipe-desc{font-size:.83rem;line-height:1.45;color:var(--dm-text-muted)}.cb-apply-panel{padding:1rem 1.1rem;background:var(--dm-surface);border:1px solid var(--dm-primary);border-radius:8px;color:var(--dm-text)}.cb-apply-head{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}.cb-apply-head .cb-icon{color:var(--dm-primary);font-size:1.4rem}.cb-apply-head h3{margin:0;font-size:1rem;font-weight:600;color:var(--dm-text)}.cb-apply-desc,.cb-apply-hint{margin:.25rem 0 .75rem;font-size:.85rem;line-height:1.5;color:var(--dm-text-muted)}.cb-apply-hint{font-size:.75rem;margin-top:.2rem}.cb-apply-field{margin-bottom:.6rem}.cb-apply-field label{display:block;font-size:.82rem;font-weight:600;margin-bottom:.2rem;color:var(--dm-text)}.cb-apply-field input{width:100%;padding:.4rem .55rem;border:1px solid var(--dm-border);border-radius:4px;font-size:.88rem;background:var(--dm-surface);color:var(--dm-text)}.cb-apply-status{margin:.5rem 0;font-size:.85rem;min-height:1.2rem;color:var(--dm-text)}.cb-apply-status.cb-error{color:var(--dm-danger, #dc2626)}.cb-apply-buttons{display:flex;gap:.5rem;justify-content:flex-end;margin-top:.5rem}.cb-success-panel{padding:1rem 1.1rem;background:var(--dm-surface);border:1px solid var(--dm-success, #16a34a);border-radius:8px;color:var(--dm-text)}.cb-success-panel .cb-icon{color:var(--dm-success, #16a34a);font-size:1.4rem}.cb-success-summary{margin:.25rem 0 .5rem;padding-left:1.2rem;font-size:.86rem;line-height:1.6;color:var(--dm-text)}.cb-success-warning{margin:.5rem 0;padding:.5rem .75rem;background:var(--dm-background-alt);border-left:3px solid var(--dm-warning, #eab308);border-radius:0 4px 4px 0;font-size:.82rem;color:var(--dm-text-muted)}.cb-success-snippet-label{margin:0 0 .25rem;font-size:.82rem;font-weight:600;color:var(--dm-text)}.cb-success-actions{display:flex;gap:.5rem;margin-top:.5rem;flex-wrap:wrap}.cb-code{position:relative}.cb-code pre{background:var(--dm-background-alt);color:var(--dm-text);display:block;padding:.6rem 2.4rem .6rem .75rem;white-space:pre;overflow-x:auto;margin:0;border-radius:6px;font-size:.82em;font-family:monospace;line-height:1.45}.cb-code .cb-copy{position:absolute;top:.35rem;right:.35rem;background:none;border:none;cursor:pointer;opacity:.45;padding:.2rem;line-height:1;color:var(--dm-text)}.cb-code .cb-copy:hover{opacity:1}.cb-intro{padding:.75rem 1rem;border-left:3px solid var(--dm-primary);background:var(--dm-background-alt);border-radius:0 6px 6px 0}.cb-intro p{margin:0;font-size:.92rem;line-height:1.5;color:var(--dm-text)}.cb-tutorial-link{display:flex;align-items:center;gap:.6rem;padding:.65rem .85rem;border:1px solid var(--dm-border);border-radius:6px;text-decoration:none;color:var(--dm-text);font-size:.88rem;transition:border-color .12s}.cb-tutorial-link:hover{border-color:var(--dm-primary)}.cb-tutorial-link .cb-icon{flex-shrink:0;color:var(--dm-primary)}.cb-tutorial-link .cb-arrow{color:var(--dm-text-muted);font-size:1.1rem;margin-left:auto}.menu-tree-row{display:grid;grid-template-columns:16px 1.4fr 1.4fr .9fr .7fr .9fr repeat(6,auto);gap:6px;align-items:center;padding:4px 0;transition:opacity .15s ease}.menu-tree-row--hidden{opacity:.5}.menu-tree-grip{cursor:grab;color:var(--dm-text-muted)}.menu-tree{padding:8px 0}.menu-tree-row--separator{display:grid;grid-template-columns:16px minmax(80px,1fr) repeat(7,auto);gap:6px;align-items:center;padding:2px 0;opacity:.85}.menu-tree-row--separator:hover{opacity:1}.menu-tree-row--separator .me-sep-line{display:flex;align-items:center;gap:10px;width:100%}.menu-tree-row--separator .me-sep-line:before,.menu-tree-row--separator .me-sep-line:after{content:"";flex:1;border-top:1px dashed var(--dm-border, rgba(128, 128, 128, .55))}.menu-tree-row--separator .me-sep-label{font-size:.66rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--dm-text-muted, #8a8a8a);white-space:nowrap}.dm-admin-sidebar .sidebar-divider{border:0;border-top:1px solid var(--dm-border, rgba(128, 128, 128, .5));margin:.5rem .75rem;opacity:.8}.menu-tree-row .me-icon-wrap{display:flex;gap:2px}.menu-tree-row .me-icon-wrap .me-icon{flex:1;min-width:0}.menu-tree-row .me-icon-wrap .me-icon-pick{flex-shrink:0;padding:.2rem .45rem;font-size:.85em}.dm-admin-sidebar{display:flex;flex-direction:column;gap:2px;padding:8px 0;font-size:.875rem}.dm-admin-sidebar details{margin:0}.dm-admin-sidebar details>summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:.5rem;padding:.4rem .75rem;border-radius:4px;user-select:none;font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--dm-text-muted, #888)}.dm-admin-sidebar details details>summary{font-size:.875rem;font-weight:500;text-transform:none;letter-spacing:0;color:inherit}.dm-admin-sidebar .dm-menu-badge{font-size:.7rem;font-weight:600}.dm-admin-sidebar details>summary::-webkit-details-marker{display:none}.dm-admin-sidebar details>summary:after{content:"\25b8";margin-left:auto;transition:transform .15s ease;color:var(--dm-text-muted, #999);font-size:.85em}.dm-admin-sidebar details[open]>summary:after{transform:rotate(90deg)}.dm-admin-sidebar details>div{padding-left:1rem;border-left:1px solid var(--dm-border-light, rgba(0, 0, 0, .08));margin-left:.95rem}.dm-admin-sidebar .sidebar-link{display:flex;align-items:center;gap:.5rem;padding:.35rem .75rem;text-decoration:none;color:var(--dm-text, inherit);border-radius:4px}.dm-admin-sidebar .sidebar-link:hover,.dm-admin-sidebar details>summary:hover{background:var(--dm-hover-bg, rgba(0, 0, 0, .05))}.dm-admin-sidebar .sidebar-link.active{background:var(--dm-active-bg, rgba(0, 0, 0, .08));font-weight:600}.dm-admin-sidebar .sidebar-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dm-admin-sidebar [data-icon]{flex-shrink:0}.dm-admin-sidebar-header{display:flex;align-items:center;gap:.5rem;padding:.65rem .85rem;margin:0 0 .5rem;font-weight:600;font-size:.95rem;letter-spacing:.01em;color:var(--dm-text, inherit);border-bottom:1px solid var(--dm-border-light, rgba(0, 0, 0, .08))}.dm-admin-sidebar-header [data-icon]{color:var(--dm-text-muted, #999)}.dm-admin-sidebar .sidebar-badge{margin-left:auto;padding:0 .4rem;min-width:1.25rem;height:1.1rem;line-height:1.1rem;text-align:center;font-size:.7rem;font-weight:600;border-radius:999px;background:var(--dm-active-bg, rgba(0, 0, 0, .12));color:var(--dm-text-muted, #888)}.dm-admin-sidebar .sidebar-link.active .sidebar-badge{background:var(--dm-primary, #4a86e8);color:var(--dm-primary-text, #fff)}.dm-admin-sidebar .sidebar-children{overflow:hidden;transition:max-height .22s ease,opacity .18s ease}.dm-admin-sidebar details:not([open])>.sidebar-children{opacity:0}.dm-admin-sidebar{position:relative}.dm-admin-sidebar-handle{position:absolute;top:0;right:-3px;width:6px;height:100%;cursor:col-resize;background:transparent;z-index:10;transition:background .15s}.dm-admin-sidebar-handle:hover,.dm-admin-sidebar-handle:active{background:var(--dm-primary, #4a86e8);opacity:.35}.dm-admin-sidebar--dark{background:#1a1d24;color:#e3e6eb}.dm-admin-sidebar--dark .sidebar-link,.dm-admin-sidebar--dark summary{color:#e3e6eb}.dm-admin-sidebar--dark .sidebar-link:hover,.dm-admin-sidebar--dark details>summary:hover{background:#ffffff0f}.dm-admin-sidebar--dark .sidebar-link.active{background:#4a86e840}.dm-admin-sidebar--dark .sidebar-badge{background:#ffffff1f;color:#cbd0d8}.dm-admin-sidebar--light{background:#fff;color:#1a1d24;border-right:1px solid rgba(0,0,0,.08)}.dm-admin-sidebar--light .sidebar-link,.dm-admin-sidebar--light summary{color:#1a1d24}.dm-admin-sidebar--light .sidebar-link:hover,.dm-admin-sidebar--light details>summary:hover{background:#0000000a}.dm-admin-sidebar--light .sidebar-link.active{background:#4a86e826}.dm-admin-sidebar--transparent{background:transparent}.page-header{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin:0 0 1.25rem;padding-bottom:.75rem;border-bottom:1px solid var(--dm-border-light, rgba(0, 0, 0, .08))}.page-header h2{margin:0;display:flex;align-items:center;gap:.5rem;font-size:1.4rem;line-height:1.2}.page-header h2 [data-icon]{color:var(--dm-text-muted, #888)}.header-actions{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.card+.card,.card+p,p+.card,.card+.toolbar,.toolbar+.card,.card+.grid{margin-top:1rem}.toolbar{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem}.btn+.btn,.btn+a.btn,a.btn+.btn,a.btn+a.btn{margin-left:.35rem}[style*=inline-flex] .btn+.btn,[style*=inline-flex] .btn+a.btn,[style*=inline-flex] a.btn+.btn{margin-left:0}.card-body>[id$=-form],.card-body>form{margin:.25rem 0}.card-header{display:flex;align-items:center;gap:.5rem}.card-header h3{margin:0;font-size:1.05rem}.card-header [data-icon]{color:var(--dm-text-muted, #888)}.page-header+p.text-muted{margin:-.5rem 0 1rem}.dm-pe-grid{gap:.75rem 1rem}.dm-pe-grid .form-group{display:flex;flex-direction:column;gap:.25rem;margin:0}.dm-pe-grid .form-group.col-span-2{grid-column:1 / -1}.dm-pe-grid label{font-size:.85rem;font-weight:600;color:var(--dm-text, inherit)}.dm-pe-grid label .required{color:var(--dm-danger, #c33);margin-left:2px}.dm-pe-grid .form-hint{font-size:.75rem;color:var(--dm-text-muted, #888);margin-top:.1rem}.dm-pe-grid .form-input{width:100%;padding:.4rem .6rem}.dm-pe-grid textarea.form-input{resize:vertical;min-height:2.5rem}.dm-menu-badge{display:inline-block;margin-left:6px;padding:1px 7px;font-size:.7em;font-weight:700;line-height:1.4;border-radius:999px;background:var(--dm-text-muted);color:var(--dm-text-inverse, #fff);vertical-align:middle}.menu-tree-row--slim{display:grid;grid-template-columns:16px 1fr auto repeat(9,auto);gap:6px;align-items:center}.menu-tree-row--slim .me-row-label{font-weight:600}.menu-tree-row--slim .me-row-url{color:var(--dm-text-muted);font-size:.85em}.me-item-form{display:flex;flex-direction:column;gap:10px;padding:4px 2px}.me-item-form .form-field{display:flex;flex-direction:column;gap:4px}.me-item-form .form-label{font-size:.85em;color:var(--dm-text-muted)}.me-form-section{margin:12px 0 2px;padding-top:10px;border-top:1px solid var(--dm-border-light, rgba(0,0,0,.15));font-size:.78em;text-transform:uppercase;letter-spacing:.06em;color:var(--dm-text-muted)}.me-item-form .me-form-section:first-child{margin-top:0;padding-top:0;border-top:0}.me-item-form .me-colour-hex{width:48px;height:32px;padding:2px}.me-item-form .me-field-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.me-item-form .me-field-grid .form-field{margin:0}.me-item-form .me-icon-field{display:flex;align-items:center;gap:8px}.me-item-form .me-icon-field .form-input{flex:1}.me-item-form .me-icon-preview{width:24px;height:24px;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;color:var(--dm-text-muted)}
@@ -1 +1 @@
1
- import{api as _}from"../api.js";let N=null,O={};async function D(e){const t=e.find("#action-project").get(0);if(t)try{(await _.projects.list()).forEach(a=>{const n=document.createElement("option");n.value=a.slug,n.textContent=a.name||a.slug,t.appendChild(n)})}catch{}}const $={deleteEntry:[],moveToCollection:[{name:"targetCollection",label:"Target Collection Slug",placeholder:"e.g. archived-applications"}],webhook:[{name:"url",label:"URL",placeholder:"https://hooks.example.com/notify"},{name:"method",label:"Method",placeholder:"POST"},{name:"body",label:"Body (JSON)",placeholder:'{"email": "{{entry.data.email}}"}',multiline:!0}],email:[{name:"to",label:"To",placeholder:"{{entry.data.email}}"},{name:"subject",label:"Subject",placeholder:"Your application update"},{name:"template",label:"Body",placeholder:"Your application has been approved.",multiline:!0}]};export const actionEditorView={templateUrl:"/admin/js/templates/action-editor.html",async onMount(e){N=null,O={};const t=window.location.hash.match(/\/actions\/edit\/([^/?#]+)/);t&&(N=t[1]),E.tabs(e.find("#action-editor-tabs").get(0)),await I(e),await P(e),await D(e),N&&(e.find("#action-editor-title").text("Edit Action"),await B(e,N)),e.find("#add-step-btn").off("click").on("click",()=>{const l=e.find("#add-step-type").val()||"updateField";J(e,{type:l,config:{}})}),e.find("#save-action-btn").off("click").on("click",async()=>{await Y(e)}),U(e),Domma.icons.scan()}};async function I(e){const t=e.find("#action-collection").get(0);if(t)try{(await _.collections.list()).forEach(l=>{const a=document.createElement("option");a.value=l.slug,a.textContent=`${l.title} (${l.slug})`,t.appendChild(a)})}catch{}}async function P(e){const t=e.find("#action-roles-checkboxes").get(0);t&&["admin","manager","editor","subscriber"].forEach(l=>{const a=document.createElement("label");a.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const n=document.createElement("input");n.type="checkbox",n.value=l,n.dataset.role=l,n.className="action-role-cb",n.checked=l==="admin",a.appendChild(n),a.appendChild(document.createTextNode(l)),t.appendChild(a)})}async function B(e,t){try{const l=await _.actions.get(t);if(!l){E.toast("Action not found.",{type:"error"}),R.navigate("/actions");return}K(e,l)}catch(l){E.toast(l.message||"Failed to load action.",{type:"error"}),R.navigate("/actions")}}function U(e){const t=e.find("#action-rowlevel-enabled").get(0),l=e.find("#action-rowlevel-config").get(0),a=e.find("#action-rowlevel-mode").get(0),n=e.find("#action-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{l&&(l.style.display=t.checked?"flex":"none")}),a&&a.addEventListener("change",()=>{n&&(n.style.display=a.value==="field"?"":"none")}))}function K(e,t){e.find("#action-title").val(t.title||""),e.find("#action-slug").val(t.slug||""),e.find("#action-description").val(t.description||""),e.find("#action-collection").val(t.collection||""),O=t.meta||{},e.find("#action-project").val(t.meta?.project||""),e.find("#action-bundled").prop("checked",!!t.bundled),e.find("#action-trigger-type").val(t.trigger?.type||"manual"),e.find("#action-trigger-label").val(t.trigger?.label||"Run"),e.find("#action-trigger-icon").val(t.trigger?.icon||"zap"),e.find("#action-trigger-confirm").val(t.trigger?.confirmMessage||"");const l=t.access?.roles||["admin"];e.find(".action-role-cb").each(function(){this.checked=l.includes(this.value)});const a=t.access?.rowLevel;a&&(e.find("#action-rowlevel-enabled").prop("checked",!0),e.find("#action-rowlevel-config").css("display","flex"),e.find("#action-rowlevel-mode").val(a.mode||"owner"),e.find("#action-rowlevel-userkey").val(a.userKey||"id"),a.mode==="field"&&(e.find("#action-rowlevel-field-group").css("display",""),e.find("#action-rowlevel-field").val(a.field||"")));const n=e.find("#action-steps-list").get(0);if(n){const c=n.querySelector(".steps-empty-placeholder");for(c&&c.remove();n.firstChild;)n.removeChild(n.firstChild);const i=document.createElement("p");i.className="text-muted steps-empty-placeholder",i.textContent="No steps yet. Add a step to define what this action does.",i.style.cssText="text-align:center;padding:2rem 0;",n.appendChild(i)}(t.steps||[]).forEach(c=>J(e,c))}function J(e,t){const l=e.find("#action-steps-list").get(0);if(!l)return;const a=l.querySelector(".steps-empty-placeholder");a&&a.remove();const n=$[t.type]||[],c=document.createElement("div");c.className="card mb-2 step-card",c.dataset.stepType=t.type;const i=document.createElement("div");i.className="card-header",i.style.cssText="display:flex;align-items:center;gap:.5rem;";const p=document.createElement("code");p.textContent=t.type,p.style.cssText="flex:1;font-size:.85rem;";const r=document.createElement("button");r.type="button",r.className="btn btn-sm btn-danger";const f=document.createElement("span");f.setAttribute("data-icon","trash"),r.appendChild(f),r.addEventListener("click",()=>{if(c.remove(),!l.querySelector(".step-card")){const o=document.createElement("p");o.className="text-muted steps-empty-placeholder",o.textContent="No steps yet. Add a step to define what this action does.",o.style.cssText="text-align:center;padding:2rem 0;",l.appendChild(o)}}),i.appendChild(p),i.appendChild(r);const h=document.createElement("div");if(h.className="card-body",n.length===0){const o=document.createElement("p");o.className="text-muted",o.textContent=t.type==="deleteEntry"?"This step deletes the entry. No configuration required.":"No additional configuration required.",o.style.margin="0",h.appendChild(o)}t.type==="updateField"?G(h,t,e):n.forEach(o=>{const g=document.createElement("div");g.style.cssText="margin-bottom:.75rem;";const b=document.createElement("label");b.className="form-label",b.textContent=o.label,g.appendChild(b);let m;o.multiline?(m=document.createElement("textarea"),m.rows=3,m.style.cssText="font-family:monospace;font-size:.8rem;resize:vertical;",m.value=typeof t.config?.[o.name]=="object"?JSON.stringify(t.config[o.name],null,2):t.config?.[o.name]??""):(m=document.createElement("input"),m.type="text",m.value=t.config?.[o.name]??""),m.className=`form-input step-field-${o.name}`,m.placeholder=o.placeholder||"",m.dataset.field=o.name,g.appendChild(m),h.appendChild(g)}),c.appendChild(i),c.appendChild(h),l.appendChild(c),Domma.icons.scan(c)}async function G(e,t,l){const a=l.find("#action-collection").val(),n=document.createElement("div");n.style.cssText="margin-bottom:.75rem;";const c=document.createElement("label");c.className="form-label",c.textContent="Field",n.appendChild(c);const i=document.createElement("select");i.className="form-input step-field-field",i.dataset.field="field";const p=document.createElement("input");p.type="text",p.className="form-input mt-2 step-field-field-custom",p.placeholder="Field name, e.g. status",p.style.display="none";const r=document.createElement("div");r.style.cssText="margin-bottom:.75rem;";const f=document.createElement("label");f.className="form-label",f.textContent="New Value",r.appendChild(f);const h=document.createElement("div");r.appendChild(h);const o=document.createElement("button");o.type="button",o.className="btn btn-ghost btn-sm mt-2",o.style.cssText="font-size:.75rem;padding:.2rem .5rem;",o.textContent="Template Variables";const g=document.createElement("div");g.style.cssText="display:none;background:var(--dm-surface-raised,#1e1e2e);border-radius:.4rem;padding:.75rem;margin-top:.5rem;font-size:.8rem;line-height:1.7;";const b=document.createElement("strong");b.textContent="Available template variables:",g.appendChild(b);const m=document.createElement("table");m.style.cssText="width:100%;border-collapse:collapse;margin-top:.4rem;",[["{{now}}","Current ISO timestamp"],["{{user.id}}","Executing user's ID"],["{{user.name}}","Executing user's name"],["{{user.email}}","Executing user's email"],["{{entry.id}}","Entry's unique ID"],["{{entry.data.fieldName}}","Any field value from the entry"],["{{env.CMS_PUBLIC_*}}","Public environment variables"]].forEach(([d,u])=>{const x=document.createElement("tr"),w=document.createElement("td");w.style.cssText="padding:.2rem .5rem .2rem 0;opacity:.7;white-space:nowrap;";const T=document.createElement("code");T.textContent=d,w.appendChild(T);const s=document.createElement("td");s.textContent=u,x.appendChild(w),x.appendChild(s),m.appendChild(x)}),g.appendChild(m),o.addEventListener("click",()=>{const d=g.style.display!=="none";g.style.display=d?"none":"",o.textContent=d?"Template Variables":"Hide Variables"}),r.appendChild(o),r.appendChild(g),e.appendChild(n),e.appendChild(r);let S=[];if(a)try{S=(await _.collections.get(a)).fields||[]}catch{}const j=document.createElement("option");j.value="",j.textContent=S.length?"\u2014 select a field \u2014":"\u2014 no fields available \u2014",i.appendChild(j),S.forEach(d=>{const u=document.createElement("option");u.value=d.name,u.textContent=`${d.label} (${d.name})`,u.dataset.fieldType=d.type,u.dataset.fieldOptions=d.type==="select"?JSON.stringify(d.options||[]):"",i.appendChild(u)});const q=document.createElement("option");q.value="__custom__",q.textContent="\u2014 enter manually \u2014",i.appendChild(q);const C=t.config?.field||"",z=C&&[...i.options].find(d=>d.value===C);z?i.value=C:C&&(i.value="__custom__",p.value=C,p.style.display=""),n.appendChild(i),n.appendChild(p);function M(d,u){h.textContent="";const x=d?[...i.options].find(s=>s.value===d):null,w=x?.dataset.fieldType==="select",T=w?JSON.parse(x.dataset.fieldOptions||"[]"):[];if(w&&T.length){const s=document.createElement("select");s.className="form-input step-field-value",s.dataset.field="value";const A=document.createElement("option");A.value="",A.textContent="\u2014 select a value \u2014",s.appendChild(A),T.forEach(y=>{const k=typeof y=="string"?y:y.value??"",V=typeof y=="string"?y:y.label||y.value||k;if(!k||k==="undefined")return;const F=document.createElement("option");F.value=k,F.textContent=V,s.appendChild(F)});const L=document.createElement("option");L.value="__custom__",L.textContent="\u2014 enter manually \u2014",s.appendChild(L);const v=document.createElement("input");v.type="text",v.className="form-input mt-2",v.placeholder="e.g. approved or {{now}}",v.style.display="none",u&&[...s.options].find(y=>y.value===u&&y.value!=="__custom__")?s.value=u:u&&(s.value="__custom__",v.value=u,v.style.display=""),s.addEventListener("change",()=>{const y=s.value==="__custom__";v.style.display=y?"":"none",y||(v.value="")}),h.appendChild(s),h.appendChild(v)}else{const s=document.createElement("input");s.type="text",s.className="form-input step-field-value",s.dataset.field="value",s.placeholder="e.g. approved or {{now}}",s.value=u||"",h.appendChild(s)}}M(z?C:null,t.config?.value||""),i.addEventListener("change",()=>{const d=i.value==="__custom__";p.style.display=d?"":"none",d||(p.value=""),M(d?null:i.value,"")})}function H(e){const t=[];return e.find(".step-card").each(function(){const l=this.dataset.stepType,a={};if(l==="updateField"){let n=this.querySelector(".step-field-field")?.value?.trim()||"";n==="__custom__"&&(n=this.querySelector(".step-field-field-custom")?.value?.trim()||""),a.field=n;const c=this.querySelector(".step-field-value");let i=c?.value?.trim()||"";i==="__custom__"&&(i=c?.nextElementSibling?.value?.trim()||""),a.value=i}else($[l]||[]).forEach(n=>{const c=this.querySelector(`.step-field-${n.name}`);if(!c)return;const i=c.value.trim();if(n.multiline&&i)try{a[n.name]=JSON.parse(i)}catch{a[n.name]=i}else a[n.name]=i});t.push({type:l,config:a})}),t}async function Y(e){const t=e.find("#action-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const l=e.find("#action-collection").val();if(!l){E.toast("Target collection is required (General tab).",{type:"warning"});return}const a=[];e.find(".action-role-cb:checked").each(function(){a.push(this.value)});const n=e.find("#action-rowlevel-enabled").is(":checked");let c=null;if(n){const f=e.find("#action-rowlevel-mode").val()||"owner",h=e.find("#action-rowlevel-userkey").val()||"id";if(c={mode:f,userKey:h},f==="field"){const o=e.find("#action-rowlevel-field").val().trim();if(!o){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}c.field=o}}const i=e.find("#action-project").val()||"",p={title:t,slug:e.find("#action-slug").val().trim()||void 0,description:e.find("#action-description").val().trim(),collection:l,...e.find("#action-bundled").is(":checked")?{bundled:!0}:{},trigger:{type:e.find("#action-trigger-type").val()||"manual",label:e.find("#action-trigger-label").val().trim()||"Run",icon:e.find("#action-trigger-icon").val().trim()||"zap",confirmMessage:e.find("#action-trigger-confirm").val().trim()||null},steps:H(e),access:{roles:a,rowLevel:c},meta:{...O||{},project:i||null}},r=e.find("#save-action-btn").get(0);r&&(r.disabled=!0);try{if(N)await _.actions.update(N,p),E.toast("Action updated.",{type:"success"});else{const f=await _.actions.create(p);E.toast("Action created.",{type:"success"}),R.navigate(`/actions/edit/${f.slug}`)}}catch(f){E.toast(f.message||"Failed to save action.",{type:"error"})}finally{r&&(r.disabled=!1)}}
1
+ import{api as _}from"../api.js";let N=null,O={};async function D(e){const t=e.find("#action-project").get(0);if(t)try{(await _.projects.list()).forEach(a=>{const n=document.createElement("option");n.value=a.slug,n.textContent=a.name||a.slug,t.appendChild(n)})}catch{}}const $={deleteEntry:[],moveToCollection:[{name:"targetCollection",label:"Target Collection Slug",placeholder:"e.g. archived-applications"}],webhook:[{name:"url",label:"URL",placeholder:"https://hooks.example.com/notify"},{name:"method",label:"Method",placeholder:"POST"},{name:"body",label:"Body (JSON)",placeholder:'{"email": "{{entry.data.email}}"}',multiline:!0}],email:[{name:"to",label:"To",placeholder:"{{entry.data.email}}"},{name:"subject",label:"Subject",placeholder:"Your application update"},{name:"template",label:"Body",placeholder:"Your application has been approved.",multiline:!0}]};export const actionEditorView={templateUrl:"/admin/js/templates/action-editor.html",async onMount(e){N=null,O={};const t=window.location.hash.match(/\/actions\/edit\/([^/?#]+)/);t&&(N=t[1]),E.tabs(e.find("#action-editor-tabs").get(0)),await I(e),await P(e),await D(e),N&&(e.find("#action-editor-title").text("Edit Action"),await B(e,N)),e.find("#add-step-btn").off("click").on("click",()=>{const l=e.find("#add-step-type").val()||"updateField";J(e,{type:l,config:{}})}),e.find("#save-action-btn").off("click").on("click",async()=>{await Y(e)}),U(e),Domma.icons.scan()}};async function I(e){const t=e.find("#action-collection").get(0);if(t)try{(await _.collections.list()).forEach(l=>{const a=document.createElement("option");a.value=l.slug,a.textContent=`${l.title} (${l.slug})`,t.appendChild(a)})}catch{}}async function P(e){const t=e.find("#action-roles-checkboxes").get(0);t&&["admin","manager","editor","subscriber"].forEach(l=>{const a=document.createElement("label");a.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const n=document.createElement("input");n.type="checkbox",n.value=l,n.dataset.role=l,n.className="action-role-cb",n.checked=l==="admin",a.appendChild(n),a.appendChild(document.createTextNode(l)),t.appendChild(a)})}async function B(e,t){try{const l=await _.actions.get(t);if(!l){E.toast("Action not found.",{type:"error"}),R.navigate("/actions");return}K(e,l)}catch(l){E.toast(l.message||"Failed to load action.",{type:"error"}),R.navigate("/actions")}}function U(e){const t=e.find("#action-rowlevel-enabled").get(0),l=e.find("#action-rowlevel-config").get(0),a=e.find("#action-rowlevel-mode").get(0),n=e.find("#action-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{l&&(l.style.display=t.checked?"flex":"none")}),a&&a.addEventListener("change",()=>{n&&(n.style.display=a.value==="field"?"":"none")}))}function K(e,t){e.find("#action-title").val(t.title||""),e.find("#action-slug").val(t.slug||""),e.find("#action-description").val(t.description||""),e.find("#action-collection").val(t.collection||""),O=t.meta||{},e.find("#action-project").val(t.meta?.project||""),e.find("#action-bundled").prop("checked",!!t.bundled),e.find("#action-trigger-type").val(t.trigger?.type||"manual"),e.find("#action-trigger-label").val(t.trigger?.label||"Run"),e.find("#action-trigger-icon").val(t.trigger?.icon||"zap"),e.find("#action-trigger-confirm").val(t.trigger?.confirmMessage||"");const l=t.access?.roles||["admin"];e.find(".action-role-cb").each(function(){this.checked=l.includes(this.value)});const a=t.access?.rowLevel;a&&(e.find("#action-rowlevel-enabled").prop("checked",!0),e.find("#action-rowlevel-config").css("display","flex"),e.find("#action-rowlevel-mode").val(a.mode||"owner"),e.find("#action-rowlevel-userkey").val(a.userKey||"id"),a.mode==="field"&&(e.find("#action-rowlevel-field-group").css("display",""),e.find("#action-rowlevel-field").val(a.field||"")));const n=e.find("#action-steps-list").get(0);if(n){const c=n.querySelector(".steps-empty-placeholder");for(c&&c.remove();n.firstChild;)n.removeChild(n.firstChild);const i=document.createElement("p");i.className="text-muted steps-empty-placeholder",i.textContent="No steps yet. Add a step to define what this action does.",i.style.cssText="text-align:center;padding:2rem 0;",n.appendChild(i)}(t.steps||[]).forEach(c=>J(e,c))}function J(e,t){const l=e.find("#action-steps-list").get(0);if(!l)return;const a=l.querySelector(".steps-empty-placeholder");a&&a.remove();const n=$[t.type]||[],c=document.createElement("div");c.className="card mb-2 step-card",c.dataset.stepType=t.type;const i=document.createElement("div");i.className="card-header",i.style.cssText="display:flex;align-items:center;gap:.5rem;";const p=document.createElement("code");p.textContent=t.type,p.style.cssText="flex:1;font-size:.85rem;";const r=document.createElement("button");r.type="button",r.className="btn btn-sm btn-ghost";const f=document.createElement("span");f.setAttribute("data-icon","trash"),r.appendChild(f),r.addEventListener("click",()=>{if(c.remove(),!l.querySelector(".step-card")){const o=document.createElement("p");o.className="text-muted steps-empty-placeholder",o.textContent="No steps yet. Add a step to define what this action does.",o.style.cssText="text-align:center;padding:2rem 0;",l.appendChild(o)}}),i.appendChild(p),i.appendChild(r);const h=document.createElement("div");if(h.className="card-body",n.length===0){const o=document.createElement("p");o.className="text-muted",o.textContent=t.type==="deleteEntry"?"This step deletes the entry. No configuration required.":"No additional configuration required.",o.style.margin="0",h.appendChild(o)}t.type==="updateField"?G(h,t,e):n.forEach(o=>{const g=document.createElement("div");g.style.cssText="margin-bottom:.75rem;";const b=document.createElement("label");b.className="form-label",b.textContent=o.label,g.appendChild(b);let m;o.multiline?(m=document.createElement("textarea"),m.rows=3,m.style.cssText="font-family:monospace;font-size:.8rem;resize:vertical;",m.value=typeof t.config?.[o.name]=="object"?JSON.stringify(t.config[o.name],null,2):t.config?.[o.name]??""):(m=document.createElement("input"),m.type="text",m.value=t.config?.[o.name]??""),m.className=`form-input step-field-${o.name}`,m.placeholder=o.placeholder||"",m.dataset.field=o.name,g.appendChild(m),h.appendChild(g)}),c.appendChild(i),c.appendChild(h),l.appendChild(c),Domma.icons.scan(c)}async function G(e,t,l){const a=l.find("#action-collection").val(),n=document.createElement("div");n.style.cssText="margin-bottom:.75rem;";const c=document.createElement("label");c.className="form-label",c.textContent="Field",n.appendChild(c);const i=document.createElement("select");i.className="form-input step-field-field",i.dataset.field="field";const p=document.createElement("input");p.type="text",p.className="form-input mt-2 step-field-field-custom",p.placeholder="Field name, e.g. status",p.style.display="none";const r=document.createElement("div");r.style.cssText="margin-bottom:.75rem;";const f=document.createElement("label");f.className="form-label",f.textContent="New Value",r.appendChild(f);const h=document.createElement("div");r.appendChild(h);const o=document.createElement("button");o.type="button",o.className="btn btn-ghost btn-sm mt-2",o.style.cssText="font-size:.75rem;padding:.2rem .5rem;",o.textContent="Template Variables";const g=document.createElement("div");g.style.cssText="display:none;background:var(--dm-surface-raised,#1e1e2e);border-radius:.4rem;padding:.75rem;margin-top:.5rem;font-size:.8rem;line-height:1.7;";const b=document.createElement("strong");b.textContent="Available template variables:",g.appendChild(b);const m=document.createElement("table");m.style.cssText="width:100%;border-collapse:collapse;margin-top:.4rem;",[["{{now}}","Current ISO timestamp"],["{{user.id}}","Executing user's ID"],["{{user.name}}","Executing user's name"],["{{user.email}}","Executing user's email"],["{{entry.id}}","Entry's unique ID"],["{{entry.data.fieldName}}","Any field value from the entry"],["{{env.CMS_PUBLIC_*}}","Public environment variables"]].forEach(([d,u])=>{const x=document.createElement("tr"),w=document.createElement("td");w.style.cssText="padding:.2rem .5rem .2rem 0;opacity:.7;white-space:nowrap;";const T=document.createElement("code");T.textContent=d,w.appendChild(T);const s=document.createElement("td");s.textContent=u,x.appendChild(w),x.appendChild(s),m.appendChild(x)}),g.appendChild(m),o.addEventListener("click",()=>{const d=g.style.display!=="none";g.style.display=d?"none":"",o.textContent=d?"Template Variables":"Hide Variables"}),r.appendChild(o),r.appendChild(g),e.appendChild(n),e.appendChild(r);let S=[];if(a)try{S=(await _.collections.get(a)).fields||[]}catch{}const j=document.createElement("option");j.value="",j.textContent=S.length?"\u2014 select a field \u2014":"\u2014 no fields available \u2014",i.appendChild(j),S.forEach(d=>{const u=document.createElement("option");u.value=d.name,u.textContent=`${d.label} (${d.name})`,u.dataset.fieldType=d.type,u.dataset.fieldOptions=d.type==="select"?JSON.stringify(d.options||[]):"",i.appendChild(u)});const q=document.createElement("option");q.value="__custom__",q.textContent="\u2014 enter manually \u2014",i.appendChild(q);const C=t.config?.field||"",z=C&&[...i.options].find(d=>d.value===C);z?i.value=C:C&&(i.value="__custom__",p.value=C,p.style.display=""),n.appendChild(i),n.appendChild(p);function M(d,u){h.textContent="";const x=d?[...i.options].find(s=>s.value===d):null,w=x?.dataset.fieldType==="select",T=w?JSON.parse(x.dataset.fieldOptions||"[]"):[];if(w&&T.length){const s=document.createElement("select");s.className="form-input step-field-value",s.dataset.field="value";const A=document.createElement("option");A.value="",A.textContent="\u2014 select a value \u2014",s.appendChild(A),T.forEach(y=>{const k=typeof y=="string"?y:y.value??"",V=typeof y=="string"?y:y.label||y.value||k;if(!k||k==="undefined")return;const F=document.createElement("option");F.value=k,F.textContent=V,s.appendChild(F)});const L=document.createElement("option");L.value="__custom__",L.textContent="\u2014 enter manually \u2014",s.appendChild(L);const v=document.createElement("input");v.type="text",v.className="form-input mt-2",v.placeholder="e.g. approved or {{now}}",v.style.display="none",u&&[...s.options].find(y=>y.value===u&&y.value!=="__custom__")?s.value=u:u&&(s.value="__custom__",v.value=u,v.style.display=""),s.addEventListener("change",()=>{const y=s.value==="__custom__";v.style.display=y?"":"none",y||(v.value="")}),h.appendChild(s),h.appendChild(v)}else{const s=document.createElement("input");s.type="text",s.className="form-input step-field-value",s.dataset.field="value",s.placeholder="e.g. approved or {{now}}",s.value=u||"",h.appendChild(s)}}M(z?C:null,t.config?.value||""),i.addEventListener("change",()=>{const d=i.value==="__custom__";p.style.display=d?"":"none",d||(p.value=""),M(d?null:i.value,"")})}function H(e){const t=[];return e.find(".step-card").each(function(){const l=this.dataset.stepType,a={};if(l==="updateField"){let n=this.querySelector(".step-field-field")?.value?.trim()||"";n==="__custom__"&&(n=this.querySelector(".step-field-field-custom")?.value?.trim()||""),a.field=n;const c=this.querySelector(".step-field-value");let i=c?.value?.trim()||"";i==="__custom__"&&(i=c?.nextElementSibling?.value?.trim()||""),a.value=i}else($[l]||[]).forEach(n=>{const c=this.querySelector(`.step-field-${n.name}`);if(!c)return;const i=c.value.trim();if(n.multiline&&i)try{a[n.name]=JSON.parse(i)}catch{a[n.name]=i}else a[n.name]=i});t.push({type:l,config:a})}),t}async function Y(e){const t=e.find("#action-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const l=e.find("#action-collection").val();if(!l){E.toast("Target collection is required (General tab).",{type:"warning"});return}const a=[];e.find(".action-role-cb:checked").each(function(){a.push(this.value)});const n=e.find("#action-rowlevel-enabled").is(":checked");let c=null;if(n){const f=e.find("#action-rowlevel-mode").val()||"owner",h=e.find("#action-rowlevel-userkey").val()||"id";if(c={mode:f,userKey:h},f==="field"){const o=e.find("#action-rowlevel-field").val().trim();if(!o){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}c.field=o}}const i=e.find("#action-project").val()||"",p={title:t,slug:e.find("#action-slug").val().trim()||void 0,description:e.find("#action-description").val().trim(),collection:l,...e.find("#action-bundled").is(":checked")?{bundled:!0}:{},trigger:{type:e.find("#action-trigger-type").val()||"manual",label:e.find("#action-trigger-label").val().trim()||"Run",icon:e.find("#action-trigger-icon").val().trim()||"zap",confirmMessage:e.find("#action-trigger-confirm").val().trim()||null},steps:H(e),access:{roles:a,rowLevel:c},meta:{...O||{},project:i||null}},r=e.find("#save-action-btn").get(0);r&&(r.disabled=!0);try{if(N)await _.actions.update(N,p),E.toast("Action updated.",{type:"success"});else{const f=await _.actions.create(p);E.toast("Action created.",{type:"success"}),R.navigate(`/actions/edit/${f.slug}`)}}catch(f){E.toast(f.message||"Failed to save action.",{type:"error"})}finally{r&&(r.disabled=!1)}}
@@ -1 +1 @@
1
- import{api as r}from"../api.js";import{filterByProject as p,getProjectFromHash as m}from"../lib/project-context.js";import{keepTableIconsAlive as u}from"../lib/table-icons.js";function i(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export const actionsListView={templateUrl:"/admin/js/templates/actions-list.html",async onMount(e){await d(e),u(e.get(0)),e.off("click",".js-delete-action").on("click",".js-delete-action",async function(){const a=this.dataset.slug;if(await E.confirm(`Delete action "${a}"? This cannot be undone.`))try{await r.actions.delete(a),E.toast("Action deleted.",{type:"success"}),await d(e)}catch{E.toast("Failed to delete action.",{type:"error"})}}),e.find("#create-action-btn").off("click").on("click",()=>{R.navigate("/actions/new")}),Domma.icons.scan()}};async function d(e){let a=[];const c=E.loader(e.get(0),{type:"dots"});try{a=await r.actions.list(),e.find("#actions-pro-notice").hide()}catch(t){t.message?.includes("MongoDB")||t.message?.includes("pro mode")||t.message?.includes("connection")?e.find("#actions-pro-notice").show():E.toast("Could not load actions.",{type:"error"})}finally{c.destroy()}const l=m();l&&(a=p(a,l)),T.create("#actions-table",{data:a,columns:[{key:"title",title:"Title",render:(t,n)=>{const o=document.createElement("a");return o.href=`#/actions/edit/${i(n.slug)}`,o.textContent=t,o.style.fontWeight="600",o.outerHTML}},{key:"slug",title:"Slug",render:t=>`<code>${i(t)}</code>`},{key:"collection",title:"Collection",render:t=>`<code>${i(t||"\u2014")}</code>`},{key:"trigger",title:"Trigger",render:t=>i(t?.label||t?.type||"manual")},{key:"steps",title:"Steps",render:t=>String(Array.isArray(t)?t.length:0)},{key:"access",title:"Roles",render:t=>(t?.roles||[]).map(n=>`<span class="badge badge-secondary">${i(n)}</span>`).join(" ")},{key:"slug",title:"Actions",render:t=>{const n=document.createElement("div");n.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;";const o=document.createElement("a");o.href=`#/actions/edit/${i(t)}`,o.className="btn btn-sm btn-ghost",o.setAttribute("data-tooltip","Edit"),o.innerHTML='<span data-icon="edit" data-icon-size="14"></span>';const s=document.createElement("button");return s.className="btn btn-sm btn-danger js-delete-action",s.dataset.slug=t,s.setAttribute("data-tooltip","Delete"),s.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',n.appendChild(o),n.appendChild(s),n.outerHTML}}],emptyMessage:'No actions yet. Click "New Action" to create your first action.'}),Domma.icons.scan(),document.querySelectorAll("#actions-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})})}
1
+ import{api as r}from"../api.js";import{filterByProject as p,getProjectFromHash as m}from"../lib/project-context.js";import{keepTableIconsAlive as u}from"../lib/table-icons.js";function i(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export const actionsListView={templateUrl:"/admin/js/templates/actions-list.html",async onMount(e){await d(e),u(e.get(0)),e.off("click",".js-delete-action").on("click",".js-delete-action",async function(){const a=this.dataset.slug;if(await E.confirm(`Delete action "${a}"? This cannot be undone.`))try{await r.actions.delete(a),E.toast("Action deleted.",{type:"success"}),await d(e)}catch{E.toast("Failed to delete action.",{type:"error"})}}),e.find("#create-action-btn").off("click").on("click",()=>{R.navigate("/actions/new")}),Domma.icons.scan()}};async function d(e){let a=[];const c=E.loader(e.get(0),{type:"dots"});try{a=await r.actions.list(),e.find("#actions-pro-notice").hide()}catch(t){t.message?.includes("MongoDB")||t.message?.includes("pro mode")||t.message?.includes("connection")?e.find("#actions-pro-notice").show():E.toast("Could not load actions.",{type:"error"})}finally{c.destroy()}const l=m();l&&(a=p(a,l)),T.create("#actions-table",{data:a,columns:[{key:"title",title:"Title",render:(t,n)=>{const o=document.createElement("a");return o.href=`#/actions/edit/${i(n.slug)}`,o.textContent=t,o.style.fontWeight="600",o.outerHTML}},{key:"slug",title:"Slug",render:t=>`<code>${i(t)}</code>`},{key:"collection",title:"Collection",render:t=>`<code>${i(t||"\u2014")}</code>`},{key:"trigger",title:"Trigger",render:t=>i(t?.label||t?.type||"manual")},{key:"steps",title:"Steps",render:t=>String(Array.isArray(t)?t.length:0)},{key:"access",title:"Roles",render:t=>(t?.roles||[]).map(n=>`<span class="badge badge-secondary">${i(n)}</span>`).join(" ")},{key:"slug",title:"Actions",render:t=>{const n=document.createElement("div");n.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;";const o=document.createElement("a");o.href=`#/actions/edit/${i(t)}`,o.className="btn btn-sm btn-ghost",o.setAttribute("data-tooltip","Edit"),o.innerHTML='<span data-icon="edit" data-icon-size="14"></span>';const s=document.createElement("button");return s.className="btn btn-sm btn-ghost js-delete-action",s.dataset.slug=t,s.setAttribute("data-tooltip","Delete"),s.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',n.appendChild(o),n.appendChild(s),n.outerHTML}}],emptyMessage:'No actions yet. Click "New Action" to create your first action.'}),Domma.icons.scan(),document.querySelectorAll("#actions-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})})}
@@ -2,6 +2,6 @@ import{api as s}from"../api.js";import{filterByProject as p,getProjectFromHash a
2
2
  <span style="display:inline-flex;gap:0.25rem;">
3
3
  <a class="btn btn-sm btn-ghost" href="#/api-endpoints/edit/${encodeURIComponent(t.id)}" data-tooltip="Edit"><span data-icon="edit" data-icon-size="14"></span></a>
4
4
  <button class="btn btn-sm btn-ghost btn-toggle-endpoint" data-id="${n(t.id)}" data-enabled="${t.enabled?"1":""}" data-tooltip="${t.enabled?"Disable":"Enable"}"><span data-icon="${t.enabled?"pause":"play"}" data-icon-size="14"></span></button>
5
- <button class="btn btn-sm btn-danger btn-delete-endpoint" data-id="${n(t.id)}" data-name="${n(t.name)}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>
5
+ <button class="btn btn-sm btn-ghost btn-delete-endpoint" data-id="${n(t.id)}" data-name="${n(t.name)}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>
6
6
  </span>
7
7
  `}]}),Domma.icons.scan(a.get(0)),r(a.get(0)),document.querySelectorAll("#api-endpoints-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})}),a.off("click",".btn-toggle-endpoint").on("click",".btn-toggle-endpoint",async function(){const e=$(this).data("id"),t=!!$(this).data("enabled");try{await s.apiEndpoints.update(e,{enabled:!t}),E.toast(`Endpoint ${t?"disabled":"enabled"}.`,{type:"success"}),location.reload()}catch(o){E.toast(`Update failed: ${o.message||o}`,{type:"error"})}}),a.off("click",".btn-delete-endpoint").on("click",".btn-delete-endpoint",async function(){const e=$(this).data("id"),t=$(this).data("name");if(await E.confirm(`Delete endpoint "${t}"? External callers using it will immediately get 404s. This cannot be undone.`))try{await s.apiEndpoints.remove(e),E.toast("Endpoint deleted.",{type:"success"}),location.reload()}catch(o){E.toast(`Delete failed: ${o.message||o}`,{type:"error"})}})}};
@@ -3,6 +3,6 @@ import{api as m}from"../api.js";import{keepTableIconsAlive as f}from"../lib/tabl
3
3
  enquiries: create, read`,n.appendChild(b("Name",o)),n.appendChild(b("Project",e,"The token only works on collections in this project.")),n.appendChild(b("Expires (optional)",t,"Leave empty for a non-expiring token.")),n.appendChild(b("Scopes (optional)",s,'One per line: "collection: verb, verb". Leave empty to allow every collection in the project.'));const i=document.createElement("button");i.className="btn btn-primary",i.textContent="Create token",i.addEventListener("click",async()=>{const c=o.value.trim();if(!c){E.toast("A token name is required.",{type:"error"});return}const{scopes:p,error:y}=h(s.value);if(y){E.toast(y,{type:"error"});return}const k={name:c,project:e.value,scopes:p,expiresAt:t.value?new Date(t.value).toISOString():null};try{i.disabled=!0;const{plaintext:u}=await m.apiTokens.create(k);r.close(),v(u,l)}catch(u){i.disabled=!1,E.toast(`Create failed: ${u.message||u}`,{type:"error"})}}),n.appendChild(i),r.element.appendChild(n),r.open(),setTimeout(()=>o.focus(),100)}export const apiTokensView={templateUrl:"/admin/js/templates/api-tokens.html",async onMount(a){const l=E.loader(a.get(0),{type:"dots"}),[r,n]=await Promise.all([m.apiTokens.list().catch(()=>[]),m.projects.list().catch(()=>[])]);l.destroy(),T.create("#api-tokens-table",{data:r,emptyMessage:'No API tokens yet. Click "New token" to create one.',columns:[{key:"name",title:"Name",sortable:!0},{key:"project",title:"Project",sortable:!0,render:e=>`<code>${d(e)}</code>`},{key:"tokenHint",title:"Token",render:e=>`<code>dcms_\u2026${d(e||"????")}</code>`},{key:"scopes",title:"Scopes",render:e=>g(e)},{key:"enabled",title:"Status",render:(e,t)=>t.expiresAt&&Date.parse(t.expiresAt)<Date.now()?'<span class="badge badge-warning">Expired</span>':e?'<span class="badge badge-success">Enabled</span>':'<span class="badge badge-secondary">Disabled</span>'},{key:"expiresAt",title:"Expires",render:e=>e?D(e).format("D MMM YYYY, HH:mm"):'<span class="text-muted">\u2014</span>'},{key:"lastUsedAt",title:"Last used",render:e=>e?D(e).format("D MMM YYYY, HH:mm"):'<span class="text-muted">Never</span>'},{key:"_actions",title:"Actions",render:(e,t)=>`
4
4
  <span style="display:inline-flex;gap:0.25rem;">
5
5
  <button class="btn btn-sm btn-ghost btn-toggle-token" data-id="${d(t.id)}" data-enabled="${t.enabled?"1":""}" data-tooltip="${t.enabled?"Disable":"Enable"}"><span data-icon="${t.enabled?"pause":"play"}" data-icon-size="14"></span></button>
6
- <button class="btn btn-sm btn-danger btn-revoke-token" data-id="${d(t.id)}" data-name="${d(t.name)}" data-tooltip="Revoke"><span data-icon="trash" data-icon-size="14"></span></button>
6
+ <button class="btn btn-sm btn-ghost btn-revoke-token" data-id="${d(t.id)}" data-name="${d(t.name)}" data-tooltip="Revoke"><span data-icon="trash" data-icon-size="14"></span></button>
7
7
  </span>
8
8
  `}]}),Domma.icons.scan(a.get(0)),f(a.get(0)),document.querySelectorAll("#api-tokens-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})});const o=()=>location.reload();a.off("click","#btn-new-token").on("click","#btn-new-token",()=>{if(!n.length){E.toast("No projects available \u2014 create a project first.",{type:"error"});return}x(n,o)}),a.off("click",".btn-toggle-token").on("click",".btn-toggle-token",async function(){const e=$(this).data("id"),t=!!$(this).data("enabled");try{await m.apiTokens.update(e,{enabled:!t}),E.toast(`Token ${t?"disabled":"enabled"}.`,{type:"success"}),o()}catch(s){E.toast(`Update failed: ${s.message||s}`,{type:"error"})}}),a.off("click",".btn-revoke-token").on("click",".btn-revoke-token",async function(){const e=$(this).data("id"),t=$(this).data("name");if(await E.confirm(`Revoke token "${t}"? External callers using it will immediately lose access. This cannot be undone.`))try{await m.apiTokens.revoke(e),E.toast("Token revoked.",{type:"success"}),o()}catch(s){E.toast(`Revoke failed: ${s.message||s}`,{type:"error"})}})}};
@@ -1,5 +1,5 @@
1
1
  import{api as i}from"../api.js";import{filterByProject as m,getProjectFromHash as u}from"../lib/project-context.js";import{keepTableIconsAlive as f}from"../lib/table-icons.js";function d(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function k(e){for(;e.firstChild;)e.removeChild(e.firstChild)}export const blocksView={templateUrl:"/admin/js/templates/blocks.html",async onMount(e){b(),await l(e),h(e),Domma.icons.scan(e.get(0)),f(e.get(0))}};let p=!1;function b(){p||(p=!0,I.register("download",{viewBox:"0 0 24 24",paths:["M12 3v12","M7 10l5 5 5-5","M5 19h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("upload",{viewBox:"0 0 24 24",paths:["M12 21V9","M7 14l5-5 5 5","M5 5h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("plus",{viewBox:"0 0 24 24",paths:["M12 5v14","M5 12h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}))}async function l(e){const o=e.find("#blocks-table-container").get(0);if(!o)return;let r=[];const n=E.loader(e.get(0),{type:"dots"});try{r=await i.blocks.list()}catch(t){k(o);const s=document.createElement("p");s.className="text-muted",s.textContent=`Failed to load blocks: ${t.message}`,o.appendChild(s);return}finally{n.destroy()}const a=u();a&&(r=m(r,a)),T.create(o,{data:r,emptyMessage:'No blocks yet. Click "New Block" to create your first template.',columns:[{key:"name",title:"Name",render:t=>`<a href="#/blocks/edit/${d(t)}">${d(t)}.html</a>`},{key:"size",title:"Size",render:t=>`${t} B`},{key:"updatedAt",title:"Updated",render:t=>t?new Date(t).toLocaleString():"\u2014"},{key:"name",title:"Actions",render:t=>{const s=d(t);return`<div style="display:flex;gap:.4rem;justify-content:flex-end;">
2
2
  <a href="#/blocks/edit/${s}" class="btn btn-sm btn-ghost" data-tooltip="Edit"><span data-icon="edit" data-icon-size="14"></span></a>
3
3
  <button class="btn btn-sm btn-ghost js-export-block" data-name="${s}" data-tooltip="Export as .dmblock.json"><span data-icon="download" data-icon-size="14"></span></button>
4
- <button class="btn btn-sm btn-danger js-delete-block" data-name="${s}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>
4
+ <button class="btn btn-sm btn-ghost js-delete-block" data-name="${s}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>
5
5
  </div>`}}]}),e.off("click",".js-delete-block").on("click",".js-delete-block",async function(){await y(this.dataset.name,e)}),e.off("click",".js-export-block").on("click",".js-export-block",async function(){try{await i.blocks.exportBundle(this.dataset.name)}catch(t){E.toast(t.message||"Export failed.",{type:"error"})}}),Domma.icons.scan(o),o.querySelectorAll("[data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})})}async function y(e,o){if(await E.confirm(`Delete block "${e}"? This cannot be undone.`))try{await i.blocks.delete(e),E.toast("Block deleted.",{type:"success"}),await l(o)}catch(n){E.toast(n.message||"Failed to delete block.",{type:"error"})}}function h(e){const o=e.find("#import-block-btn").get(0),r=e.find("#import-block-file").get(0);!o||!r||(o.addEventListener("click",()=>r.click()),r.addEventListener("change",async()=>{const n=r.files?.[0];if(!n)return;r.value="";let a;try{const t=await n.text();a=JSON.parse(t)}catch{E.toast("Not a valid .dmblock.json file (could not parse JSON).",{type:"error"});return}if(!a||typeof a!="object"||!a.name||typeof a.html!="string"){E.toast("Bundle is missing a name or html field.",{type:"error"});return}try{const t=await i.blocks.importBundle(a);E.toast(`Imported "${t.name}".`,{type:"success"}),await l(e)}catch(t){if(t.code==="CONFLICT"){if(!await E.confirm(`A block named "${t.name}" already exists. Overwrite it with the imported version?`)){E.toast("Import cancelled.",{type:"info"});return}try{const c=await i.blocks.importBundle(a,{overwrite:!0});E.toast(`Overwrote "${c.name}".`,{type:"success"}),await l(e)}catch(c){E.toast(c.message||"Import failed.",{type:"error"})}return}E.toast(t.message||"Import failed.",{type:"error"})}}))}
@@ -1 +1 @@
1
- import{api as h}from"../api.js";import{keepTableIconsAlive as M}from"../lib/table-icons.js";function L(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}let u=null,b=null,d=[],N=1,w=[],x={};const O=50;export const collectionEntriesView={templateUrl:"/admin/js/templates/collection-entries.html",async onMount(t){d=[],N=1,b=null,w=[],x={};const i=window.location.hash.match(/\/collections\/([^/?#]+)\/entries/);if(u=i?i[1]:null,!u){E.toast("No collection selected.",{type:"error"});return}const s=E.loader(t.get(0),{type:"dots"});try{if(b=await h.collections.get(u),!b){s.destroy(),E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const o=t.find("#entries-title").get(0);o&&(o.textContent=b.title+" \u2014 Entries");const e=t.find("#edit-schema-btn").get(0);e&&e.setAttribute("href",`#/collections/edit/${u}`);const l=t.find("#form-btn").get(0);l&&(new Set(["roles","user-profiles"]).has(u)?l.style.display="none":l.setAttribute("href",`#/forms/edit/${u}`))}catch{s.destroy(),E.toast("Failed to load collection schema.",{type:"error"});return}try{w=await h.actions.forCollection(u)}catch{w=[]}try{await v(t)}finally{s.destroy()}await k(t);const r=t.find("#entry-search").get(0);r&&r.addEventListener("input",()=>{const o=r.value.toLowerCase().trim(),e=o?d.filter(l=>Object.values(l.data||{}).some(a=>String(a).toLowerCase().includes(o))):d;C(e,t)}),t.find("#add-entry-btn").off("click").on("click",()=>{A(null,t)}),t.find("#export-btn").off("click").on("click",()=>{const o=E.slideover({title:"Export Entries",size:"sm",position:"right"}),e=document.createElement("div");e.style.cssText="padding:1.25rem;display:flex;flex-direction:column;gap:1rem;",["json","csv"].forEach(l=>{const a=document.createElement("button");a.className="btn btn-ghost",a.style.cssText="justify-content:flex-start;gap:.5rem;",a.textContent=l==="json"?"Export as JSON":"Export as CSV",a.addEventListener("click",()=>{fetch(`/api/collections/${u}/export?format=${l}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}}).then(c=>c.blob()).then(c=>{const n=document.createElement("a");n.href=URL.createObjectURL(c),n.download=`${u}-entries.${l}`,document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(n.href),o.close()}).catch(()=>E.toast("Export failed.",{type:"error"}))}),e.appendChild(a)}),o.setContent(e),o.open()}),t.find("#import-btn").off("click").on("click",()=>{I(t)}),t.find("#clear-all-btn").off("click").on("click",async()=>{if(await E.confirm("Delete ALL entries? This cannot be undone."))try{await h.collections.clearEntries(u),d=[],E.toast("All entries cleared.",{type:"success"}),C([],t)}catch{E.toast("Failed to clear entries.",{type:"error"})}}),t.off("click",".js-run-action").on("click",".js-run-action",async function(o){o.stopPropagation();const e=this,{actionSlug:l,entryId:a,confirmMessage:c}=e.dataset;if(!(c&&!await E.confirm(c))){e.disabled=!0;try{const n=await h.actions.execute(l,a);if(n.success)E.toast(`Action completed (${n.stepsCompleted} step${n.stepsCompleted!==1?"s":""}).`,{type:"success"}),await v(t);else{const p=n.results?.find(f=>!f.success);E.toast(p?.error||"Action failed.",{type:"error"})}}catch(n){E.toast(n.message||"Action failed.",{type:"error"})}finally{e.disabled=!1}}}),t.off("click",".js-edit-entry").on("click",".js-edit-entry",function(o){o.stopPropagation();const e=d.find(l=>l.id===this.dataset.id);e&&A(e,t)}),t.off("click",".js-delete-entry").on("click",".js-delete-entry",async function(o){o.stopPropagation();const e=this.dataset.id;if(await E.confirm("Delete this entry?"))try{await h.collections.deleteEntry(u,e),d=d.filter(a=>a.id!==e),E.toast("Entry deleted.",{type:"success"}),C(d,t)}catch{E.toast("Failed to delete entry.",{type:"error"})}}),t.off("click","#entries-table tbody tr").on("click","#entries-table tbody tr",function(o){if(o.target.closest(".js-run-action, .js-edit-entry, .js-delete-entry"))return;const e=this.querySelector(".js-edit-entry")?.dataset.id,l=d.find(a=>a.id===e);l&&B(l)}),Domma.icons.scan(),M(t.get(0))}};async function k(t){const m=w.filter(s=>s.access?.rowLevel);if(m.length===0)return;const i=d.map(s=>s.id);i.length!==0&&(await Promise.all(m.map(async s=>{try{const{allowed:r}=await h.actions.checkAccess(s.slug,i);x[s.slug]=new Set(r)}catch{x[s.slug]=null}})),C(d,t))}async function v(t){try{const m=await h.collections.listEntries(u,{limit:500});d=m.entries||m||[]}catch{d=[],E.toast("Could not load entries.",{type:"error"})}C(d,t),await k(t)}function C(t,m){const i=m.find("#entry-count").get(0);i&&(i.textContent=t.length===d.length?`${d.length} entr${d.length!==1?"ies":"y"}`:`Showing ${t.length} of ${d.length}`);const r=[...(b?.fields||[]).slice(0,5).map(e=>({key:`data.${e.name}`,title:e.label||e.name,render:(l,a)=>{const c=a.data?.[e.name]??"",n=String(c),p=document.createElement("span");return p.title=n,p.textContent=n.length>60?n.slice(0,60)+"\u2026":n,p.outerHTML}})),{key:"meta",title:"Created",render:e=>D(e?.createdAt).format("DD MMM YYYY HH:mm")},{key:"id",title:"",render:e=>{const l=document.createElement("div");l.style.cssText="display:flex;gap:.3rem;justify-content:flex-end;flex-wrap:wrap;",w.forEach(n=>{if(x[n.slug]!==void 0&&x[n.slug]!==null&&!x[n.slug].has(e))return;const p=document.createElement("button");p.className="btn btn-sm btn-ghost js-run-action",p.dataset.actionSlug=n.slug,p.dataset.entryId=e,p.dataset.confirmMessage=n.trigger?.confirmMessage||"",p.title=n.title||n.slug;const f=document.createElement("span");f.setAttribute("data-icon",L(n.trigger?.icon||"zap")),f.setAttribute("data-icon-size","14"),p.appendChild(f),p.appendChild(document.createTextNode(" "+(n.trigger?.label||n.title))),l.appendChild(p)});const a=document.createElement("button");a.className="btn btn-sm btn-ghost js-edit-entry",a.dataset.id=e,a.setAttribute("data-tooltip","Edit"),a.innerHTML='<span data-icon="edit" data-icon-size="14"></span>';const c=document.createElement("button");return c.className="btn btn-sm btn-danger js-delete-entry",c.dataset.id=e,c.setAttribute("data-tooltip","Delete"),c.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',l.appendChild(a),l.appendChild(c),l.outerHTML}}];T.create("#entries-table",{data:t,columns:r,emptyMessage:'No entries yet. Click "Add Entry" to get started.'});const o=document.querySelector("#entries-table");o&&(o.querySelectorAll("tbody tr").forEach(e=>{e.style.cursor="pointer"}),Domma.icons.scan(o),o.querySelectorAll("[data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})}))}function z(t,m){const i={};return t.forEach(s=>{const r={};s.fullWidth?r.span=m:s.span>1&&(r.span=s.span),s.placeholder&&(r.placeholder=s.placeholder),s.helper&&(r.hint=s.helper);const o=s.type==="checkbox"?"boolean":s.type==="date"?"string":s.type;i[s.name]={type:o,label:s.label,required:s.required,options:s.options,...Object.keys(r).length?{formConfig:r}:{}}}),i}function A(t,m){const i=!!t,s=E.modal({title:i?"Edit Entry":"Add Entry",size:"md"}),r=document.createElement("div");r.style.cssText="padding:.25rem 0 .5rem;";const o=b?.fields||[],e=b?.columns||2,l=b?.layout||"stacked",a=document.createElement("div");F.create(z(o,e),t?.data||{},{layout:l,columns:e,showSubmitButton:!1}).renderTo(a),(o||[]).forEach(f=>{if(f.type!=="date"||!f.name)return;const y=a.querySelector(`[name="${f.name}"]`);y&&y.type!=="date"&&(y.type="date")}),r.appendChild(a);const c=document.createElement("div");c.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.5rem;";const n=document.createElement("button");n.className="btn btn-ghost",n.textContent="Cancel";const p=document.createElement("button");p.className="btn btn-primary",p.textContent=i?"Save Changes":"Add Entry",c.appendChild(n),c.appendChild(p),r.appendChild(c),s.element.appendChild(r),s.open(),n.addEventListener("click",()=>s.close()),p.addEventListener("click",async()=>{const f={};o.forEach(y=>{const g=a.querySelector(`[name="${y.name}"]`);g&&(f[y.name]=y.type==="checkbox"?g.checked:g.value)}),p.disabled=!0;try{if(i){const y=await h.collections.updateEntry(u,t.id,f),g=d.findIndex(j=>j.id===t.id);g!==-1&&(d[g]=y),E.toast("Entry updated.",{type:"success"})}else{const y=await h.collections.createEntry(u,f);d.unshift(y),E.toast("Entry added.",{type:"success"})}s.close(),C(d,m||{find:()=>({get:()=>null})})}catch(y){E.toast(y.message||"Failed to save entry.",{type:"error"}),p.disabled=!1}})}function B(t){const m=document.createElement("div"),i=b?.fields||[],s=document.createElement("div");s.style.cssText="display:flex;flex-direction:column;gap:.75rem;margin-bottom:1.25rem;",i.forEach(e=>{const l=t.data?.[e.name];if(l==null||l==="")return;const a=document.createElement("div");a.style.cssText="border-bottom:1px solid var(--dm-border,#333);padding-bottom:.6rem;";const c=document.createElement("strong");c.textContent=e.label||e.name,c.style.cssText="display:block;font-size:.8rem;color:var(--dm-text-muted,#888);margin-bottom:.2rem;";const n=document.createElement("p");n.textContent=String(l),n.style.cssText="margin:0;word-break:break-word;",a.appendChild(c),a.appendChild(n),s.appendChild(a)});const r=document.createElement("div");if(r.style.cssText="font-size:.8rem;color:var(--dm-text-muted,#888);display:flex;flex-direction:column;gap:.3rem;border-top:1px solid var(--dm-border,#333);padding-top:.75rem;",t.meta?.createdAt){const e=document.createElement("span");e.textContent=`Created: ${D(t.meta.createdAt).format("DD MMM YYYY HH:mm")}`,r.appendChild(e)}if(t.meta?.source){const e=document.createElement("span");e.textContent=`Source: ${t.meta.source}`,r.appendChild(e)}m.appendChild(s),m.appendChild(r);const o=E.modal({title:"Entry Details",size:"md"});o.element.appendChild(m),o.open()}function I(t){const m=E.modal({title:"Import Entries",size:"md"}),i=document.createElement("div");i.style.cssText="padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem;";const s=document.createElement("p");s.textContent='Paste a JSON array of entries. Each item should have a "data" object with field values.',s.style.cssText="font-size:.875rem;color:var(--dm-text-muted,#888);margin:0;",i.appendChild(s);const r=document.createElement("textarea");r.className="form-input",r.rows=10,r.placeholder='[{"data": {"name": "Example"}}, ...]',i.appendChild(r);const o=document.createElement("p");o.style.cssText="font-size:.875rem;margin:0;display:none;",i.appendChild(o);const e=document.createElement("div");e.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;";const l=document.createElement("button");l.className="btn btn-ghost",l.textContent="Cancel";const a=document.createElement("button");a.className="btn btn-primary",a.textContent="Import",e.appendChild(l),e.appendChild(a),i.appendChild(e),m.element.appendChild(i),m.open(),l.addEventListener("click",()=>m.close()),a.addEventListener("click",async()=>{let c;try{if(c=JSON.parse(r.value),!Array.isArray(c))throw new Error("Must be a JSON array")}catch(n){o.style.display="",o.style.color="var(--dm-danger,#f87171)",o.textContent=`Invalid JSON: ${n.message}`;return}a.disabled=!0;try{const n=await h.collections.import(u,c);o.style.display="",o.style.color="var(--dm-success,#4ade80)",o.textContent=`Imported ${n.imported} entries. Skipped: ${n.skipped}.`,await v(t||{find:()=>({get:()=>null,off:()=>({on:()=>{}})})}),setTimeout(()=>m.close(),1500)}catch(n){o.style.display="",o.style.color="var(--dm-danger,#f87171)",o.textContent=n.message||"Import failed."}finally{a.disabled=!1}})}
1
+ import{api as h}from"../api.js";import{keepTableIconsAlive as M}from"../lib/table-icons.js";function L(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}let u=null,b=null,d=[],N=1,w=[],x={};const O=50;export const collectionEntriesView={templateUrl:"/admin/js/templates/collection-entries.html",async onMount(t){d=[],N=1,b=null,w=[],x={};const i=window.location.hash.match(/\/collections\/([^/?#]+)\/entries/);if(u=i?i[1]:null,!u){E.toast("No collection selected.",{type:"error"});return}const s=E.loader(t.get(0),{type:"dots"});try{if(b=await h.collections.get(u),!b){s.destroy(),E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const o=t.find("#entries-title").get(0);o&&(o.textContent=b.title+" \u2014 Entries");const e=t.find("#edit-schema-btn").get(0);e&&e.setAttribute("href",`#/collections/edit/${u}`);const l=t.find("#form-btn").get(0);l&&(new Set(["roles","user-profiles"]).has(u)?l.style.display="none":l.setAttribute("href",`#/forms/edit/${u}`))}catch{s.destroy(),E.toast("Failed to load collection schema.",{type:"error"});return}try{w=await h.actions.forCollection(u)}catch{w=[]}try{await v(t)}finally{s.destroy()}await k(t);const r=t.find("#entry-search").get(0);r&&r.addEventListener("input",()=>{const o=r.value.toLowerCase().trim(),e=o?d.filter(l=>Object.values(l.data||{}).some(a=>String(a).toLowerCase().includes(o))):d;C(e,t)}),t.find("#add-entry-btn").off("click").on("click",()=>{A(null,t)}),t.find("#export-btn").off("click").on("click",()=>{const o=E.slideover({title:"Export Entries",size:"sm",position:"right"}),e=document.createElement("div");e.style.cssText="padding:1.25rem;display:flex;flex-direction:column;gap:1rem;",["json","csv"].forEach(l=>{const a=document.createElement("button");a.className="btn btn-ghost",a.style.cssText="justify-content:flex-start;gap:.5rem;",a.textContent=l==="json"?"Export as JSON":"Export as CSV",a.addEventListener("click",()=>{fetch(`/api/collections/${u}/export?format=${l}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}}).then(c=>c.blob()).then(c=>{const n=document.createElement("a");n.href=URL.createObjectURL(c),n.download=`${u}-entries.${l}`,document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(n.href),o.close()}).catch(()=>E.toast("Export failed.",{type:"error"}))}),e.appendChild(a)}),o.setContent(e),o.open()}),t.find("#import-btn").off("click").on("click",()=>{I(t)}),t.find("#clear-all-btn").off("click").on("click",async()=>{if(await E.confirm("Delete ALL entries? This cannot be undone."))try{await h.collections.clearEntries(u),d=[],E.toast("All entries cleared.",{type:"success"}),C([],t)}catch{E.toast("Failed to clear entries.",{type:"error"})}}),t.off("click",".js-run-action").on("click",".js-run-action",async function(o){o.stopPropagation();const e=this,{actionSlug:l,entryId:a,confirmMessage:c}=e.dataset;if(!(c&&!await E.confirm(c))){e.disabled=!0;try{const n=await h.actions.execute(l,a);if(n.success)E.toast(`Action completed (${n.stepsCompleted} step${n.stepsCompleted!==1?"s":""}).`,{type:"success"}),await v(t);else{const p=n.results?.find(f=>!f.success);E.toast(p?.error||"Action failed.",{type:"error"})}}catch(n){E.toast(n.message||"Action failed.",{type:"error"})}finally{e.disabled=!1}}}),t.off("click",".js-edit-entry").on("click",".js-edit-entry",function(o){o.stopPropagation();const e=d.find(l=>l.id===this.dataset.id);e&&A(e,t)}),t.off("click",".js-delete-entry").on("click",".js-delete-entry",async function(o){o.stopPropagation();const e=this.dataset.id;if(await E.confirm("Delete this entry?"))try{await h.collections.deleteEntry(u,e),d=d.filter(a=>a.id!==e),E.toast("Entry deleted.",{type:"success"}),C(d,t)}catch{E.toast("Failed to delete entry.",{type:"error"})}}),t.off("click","#entries-table tbody tr").on("click","#entries-table tbody tr",function(o){if(o.target.closest(".js-run-action, .js-edit-entry, .js-delete-entry"))return;const e=this.querySelector(".js-edit-entry")?.dataset.id,l=d.find(a=>a.id===e);l&&B(l)}),Domma.icons.scan(),M(t.get(0))}};async function k(t){const m=w.filter(s=>s.access?.rowLevel);if(m.length===0)return;const i=d.map(s=>s.id);i.length!==0&&(await Promise.all(m.map(async s=>{try{const{allowed:r}=await h.actions.checkAccess(s.slug,i);x[s.slug]=new Set(r)}catch{x[s.slug]=null}})),C(d,t))}async function v(t){try{const m=await h.collections.listEntries(u,{limit:500});d=m.entries||m||[]}catch{d=[],E.toast("Could not load entries.",{type:"error"})}C(d,t),await k(t)}function C(t,m){const i=m.find("#entry-count").get(0);i&&(i.textContent=t.length===d.length?`${d.length} entr${d.length!==1?"ies":"y"}`:`Showing ${t.length} of ${d.length}`);const r=[...(b?.fields||[]).slice(0,5).map(e=>({key:`data.${e.name}`,title:e.label||e.name,render:(l,a)=>{const c=a.data?.[e.name]??"",n=String(c),p=document.createElement("span");return p.title=n,p.textContent=n.length>60?n.slice(0,60)+"\u2026":n,p.outerHTML}})),{key:"meta",title:"Created",render:e=>D(e?.createdAt).format("DD MMM YYYY HH:mm")},{key:"id",title:"",render:e=>{const l=document.createElement("div");l.style.cssText="display:flex;gap:.3rem;justify-content:flex-end;flex-wrap:wrap;",w.forEach(n=>{if(x[n.slug]!==void 0&&x[n.slug]!==null&&!x[n.slug].has(e))return;const p=document.createElement("button");p.className="btn btn-sm btn-ghost js-run-action",p.dataset.actionSlug=n.slug,p.dataset.entryId=e,p.dataset.confirmMessage=n.trigger?.confirmMessage||"",p.title=n.title||n.slug;const f=document.createElement("span");f.setAttribute("data-icon",L(n.trigger?.icon||"zap")),f.setAttribute("data-icon-size","14"),p.appendChild(f),p.appendChild(document.createTextNode(" "+(n.trigger?.label||n.title))),l.appendChild(p)});const a=document.createElement("button");a.className="btn btn-sm btn-ghost js-edit-entry",a.dataset.id=e,a.setAttribute("data-tooltip","Edit"),a.innerHTML='<span data-icon="edit" data-icon-size="14"></span>';const c=document.createElement("button");return c.className="btn btn-sm btn-ghost js-delete-entry",c.dataset.id=e,c.setAttribute("data-tooltip","Delete"),c.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',l.appendChild(a),l.appendChild(c),l.outerHTML}}];T.create("#entries-table",{data:t,columns:r,emptyMessage:'No entries yet. Click "Add Entry" to get started.'});const o=document.querySelector("#entries-table");o&&(o.querySelectorAll("tbody tr").forEach(e=>{e.style.cursor="pointer"}),Domma.icons.scan(o),o.querySelectorAll("[data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})}))}function z(t,m){const i={};return t.forEach(s=>{const r={};s.fullWidth?r.span=m:s.span>1&&(r.span=s.span),s.placeholder&&(r.placeholder=s.placeholder),s.helper&&(r.hint=s.helper);const o=s.type==="checkbox"?"boolean":s.type==="date"?"string":s.type;i[s.name]={type:o,label:s.label,required:s.required,options:s.options,...Object.keys(r).length?{formConfig:r}:{}}}),i}function A(t,m){const i=!!t,s=E.modal({title:i?"Edit Entry":"Add Entry",size:"md"}),r=document.createElement("div");r.style.cssText="padding:.25rem 0 .5rem;";const o=b?.fields||[],e=b?.columns||2,l=b?.layout||"stacked",a=document.createElement("div");F.create(z(o,e),t?.data||{},{layout:l,columns:e,showSubmitButton:!1}).renderTo(a),(o||[]).forEach(f=>{if(f.type!=="date"||!f.name)return;const y=a.querySelector(`[name="${f.name}"]`);y&&y.type!=="date"&&(y.type="date")}),r.appendChild(a);const c=document.createElement("div");c.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.5rem;";const n=document.createElement("button");n.className="btn btn-ghost",n.textContent="Cancel";const p=document.createElement("button");p.className="btn btn-primary",p.textContent=i?"Save Changes":"Add Entry",c.appendChild(n),c.appendChild(p),r.appendChild(c),s.element.appendChild(r),s.open(),n.addEventListener("click",()=>s.close()),p.addEventListener("click",async()=>{const f={};o.forEach(y=>{const g=a.querySelector(`[name="${y.name}"]`);g&&(f[y.name]=y.type==="checkbox"?g.checked:g.value)}),p.disabled=!0;try{if(i){const y=await h.collections.updateEntry(u,t.id,f),g=d.findIndex(j=>j.id===t.id);g!==-1&&(d[g]=y),E.toast("Entry updated.",{type:"success"})}else{const y=await h.collections.createEntry(u,f);d.unshift(y),E.toast("Entry added.",{type:"success"})}s.close(),C(d,m||{find:()=>({get:()=>null})})}catch(y){E.toast(y.message||"Failed to save entry.",{type:"error"}),p.disabled=!1}})}function B(t){const m=document.createElement("div"),i=b?.fields||[],s=document.createElement("div");s.style.cssText="display:flex;flex-direction:column;gap:.75rem;margin-bottom:1.25rem;",i.forEach(e=>{const l=t.data?.[e.name];if(l==null||l==="")return;const a=document.createElement("div");a.style.cssText="border-bottom:1px solid var(--dm-border,#333);padding-bottom:.6rem;";const c=document.createElement("strong");c.textContent=e.label||e.name,c.style.cssText="display:block;font-size:.8rem;color:var(--dm-text-muted,#888);margin-bottom:.2rem;";const n=document.createElement("p");n.textContent=String(l),n.style.cssText="margin:0;word-break:break-word;",a.appendChild(c),a.appendChild(n),s.appendChild(a)});const r=document.createElement("div");if(r.style.cssText="font-size:.8rem;color:var(--dm-text-muted,#888);display:flex;flex-direction:column;gap:.3rem;border-top:1px solid var(--dm-border,#333);padding-top:.75rem;",t.meta?.createdAt){const e=document.createElement("span");e.textContent=`Created: ${D(t.meta.createdAt).format("DD MMM YYYY HH:mm")}`,r.appendChild(e)}if(t.meta?.source){const e=document.createElement("span");e.textContent=`Source: ${t.meta.source}`,r.appendChild(e)}m.appendChild(s),m.appendChild(r);const o=E.modal({title:"Entry Details",size:"md"});o.element.appendChild(m),o.open()}function I(t){const m=E.modal({title:"Import Entries",size:"md"}),i=document.createElement("div");i.style.cssText="padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem;";const s=document.createElement("p");s.textContent='Paste a JSON array of entries. Each item should have a "data" object with field values.',s.style.cssText="font-size:.875rem;color:var(--dm-text-muted,#888);margin:0;",i.appendChild(s);const r=document.createElement("textarea");r.className="form-input",r.rows=10,r.placeholder='[{"data": {"name": "Example"}}, ...]',i.appendChild(r);const o=document.createElement("p");o.style.cssText="font-size:.875rem;margin:0;display:none;",i.appendChild(o);const e=document.createElement("div");e.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;";const l=document.createElement("button");l.className="btn btn-ghost",l.textContent="Cancel";const a=document.createElement("button");a.className="btn btn-primary",a.textContent="Import",e.appendChild(l),e.appendChild(a),i.appendChild(e),m.element.appendChild(i),m.open(),l.addEventListener("click",()=>m.close()),a.addEventListener("click",async()=>{let c;try{if(c=JSON.parse(r.value),!Array.isArray(c))throw new Error("Must be a JSON array")}catch(n){o.style.display="",o.style.color="var(--dm-danger,#f87171)",o.textContent=`Invalid JSON: ${n.message}`;return}a.disabled=!0;try{const n=await h.collections.import(u,c);o.style.display="",o.style.color="var(--dm-success,#4ade80)",o.textContent=`Imported ${n.imported} entries. Skipped: ${n.skipped}.`,await v(t||{find:()=>({get:()=>null,off:()=>({on:()=>{}})})}),setTimeout(()=>m.close(),1500)}catch(n){o.style.display="",o.style.color="var(--dm-danger,#f87171)",o.textContent=n.message||"Import failed."}finally{a.disabled=!1}})}
@@ -1 +1 @@
1
- import{api as y}from"../api.js";import{filterByProject as M,getProjectFromHash as B}from"../lib/project-context.js";import{keepTableIconsAlive as A}from"../lib/table-icons.js";function h(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}let d={};export const collectionsView={templateUrl:"/admin/js/templates/collections.html",async onMount(e){const s=E.loader(e.get(0),{type:"dots"});E.tabs(e.find("#collections-tabs").get(0));const p=e.find("#collections-tabs").get(0)?.querySelector(".tab-list");if(p){const t=e.find("#collections-header-actions").get(0);Array.from(p.querySelectorAll(".tab-item")).forEach((n,c)=>{n.addEventListener("click",()=>{t&&(t.style.display=c===0?"":"none")})})}await Promise.all([j(e),q(e)]),s.destroy(),A(e.get(0)),e.off("click",".js-delete-collection").on("click",".js-delete-collection",async function(){const t=this.dataset.slug,n=this.dataset.plugin,c=n?`This collection is managed by the <strong>${n}</strong> plugin. Deleting it may cause the plugin to malfunction. Continue?`:`Delete collection "${t}" and all its data? This cannot be undone.`;if(await E.confirm(c))try{await y.collections.delete(t),E.toast("Collection deleted.",{type:"success"}),await j(e)}catch{E.toast("Failed to delete collection.",{type:"error"})}}),e.find("#create-collection-btn").off("click").on("click",()=>{const t=E.modal({title:"New Collection",size:"sm"}),n=document.createElement("div");n.style.cssText="padding:.25rem 0 .5rem;";const c=document.createElement("div");F.create({title:{type:"string",label:"Collection Title",placeholder:"e.g. Products, Blog Posts\u2026",required:!0}},{},{showSubmitButton:!1}).renderTo(c),n.appendChild(c);const a=document.createElement("div");a.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const o=document.createElement("button");o.className="btn btn-ghost",o.textContent="Cancel";const r=document.createElement("button");r.className="btn btn-primary",r.textContent="Create",a.appendChild(o),a.appendChild(r),n.appendChild(a),t.element.appendChild(n),t.open();const l=c.querySelector('input[name="title"]');setTimeout(()=>l?.focus(),50);async function i(){const m=l?.value.trim();if(m)try{const u=await y.collections.create({title:m});t.close(),R.navigate(`/collections/edit/${u.slug}`)}catch(u){E.toast(u.message||"Failed to create collection.",{type:"error"})}}o.addEventListener("click",()=>t.close()),r.addEventListener("click",i),l?.addEventListener("keydown",m=>{m.key==="Enter"&&i()})}),e.find("#connections-raw-toggle").on("change",function(){if(this.checked)d=x(e),v(e),e.find("#connections-form-view").hide(),e.find("#connections-raw-view").show();else{const t=e.find("#connections-raw-json").get(0)?.value||"{}";try{d=JSON.parse(t),C(e),e.find("#connections-form-view").show(),e.find("#connections-raw-view").hide()}catch{E.toast("Invalid JSON \u2014 fix before switching to form view.",{type:"warning"}),this.checked=!0}}}),e.find("#add-connection-btn").on("click",()=>{d=x(e);let t=Object.keys(d).length+1,n=`connection${t}`;for(;d[n];)t++,n=`connection${t}`;d[n]={type:"mongodb",uri:"",database:"",options:{}},C(e),v(e)}),e.find("#save-connections-btn").on("click",async()=>{const t=e.find("#connections-raw-toggle").get(0)?.checked;let n;if(t)try{n=JSON.parse(e.find("#connections-raw-json").get(0)?.value||"{}")}catch{E.toast("Invalid JSON \u2014 fix before saving.",{type:"warning"});return}else n=x(e);try{await y.collections.saveConnections(n),d=n,E.toast("Connections saved.",{type:"success"})}catch(c){E.toast(c.message||"Failed to save connections.",{type:"error"})}}),Domma.icons.scan()}};async function j(e){let s=[];try{s=await y.collections.list()}catch{E.toast("Could not load collections.",{type:"error"})}const p=B();p&&(s=M(s,p)),T.create("#collections-table",{data:s,columns:[{key:"title",title:"Title",render:(t,n)=>{const c=document.createElement("span");c.style.cssText="display:flex;align-items:center;gap:.4rem;";const a=document.createElement("a");if(a.href=`#/collections/${h(n.slug)}/entries`,a.textContent=t,a.style.fontWeight="600",c.appendChild(a),n.storage?.adapter==="mongodb"){const o=document.createElement("span");o.className="badge badge-outline",o.textContent="MongoDB",o.title=`Connection: ${n.storage.connection||"default"}`,o.style.cssText="font-size:0.65rem;padding:1px 6px;color:var(--dm-success,#16a34a);border-color:var(--dm-success,#16a34a);flex-shrink:0;",c.appendChild(o)}if(n.bundled){const o=document.createElement("span");o.className="badge badge-outline",o.textContent="Bundled",o.title="Seeded on fresh installs",o.style.cssText="font-size:0.65rem;padding:1px 6px;color:var(--dm-info,#2563eb);border-color:var(--dm-info,#2563eb);flex-shrink:0;",c.appendChild(o)}if(n.plugin){const o=document.createElement("span");o.className="badge badge-outline",o.textContent=n.plugin,o.title=`Managed by the ${n.plugin} plugin`,o.style.cssText="font-size:0.65rem;padding:1px 6px;color:var(--dm-warning,#d97706);border-color:var(--dm-warning,#d97706);flex-shrink:0;",c.appendChild(o)}return c.outerHTML}},{key:"slug",title:"Slug",render:t=>{const n=document.createElement("code");return n.textContent=t,n.outerHTML}},{key:"fields",title:"Field Count",render:t=>String(t?.length??0)},{key:"entryCount",title:"Entry Count",render:t=>String(t??0)},{key:"slug",title:"Actions",render:(t,n)=>{const c=document.createElement("div");c.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;flex-wrap:wrap;";const a=document.createElement("a");a.href=`#/collections/edit/${h(t)}`,a.className="btn btn-sm btn-ghost",a.setAttribute("data-tooltip","Edit schema"),a.innerHTML='<span data-icon="edit-3" data-icon-size="14"></span>';const o=document.createElement("a");if(o.href=`#/collections/${h(t)}/entries`,o.className="btn btn-sm btn-ghost",o.setAttribute("data-tooltip","Entries"),o.innerHTML='<span data-icon="database" data-icon-size="14"></span>',c.appendChild(a),c.appendChild(o),!new Set(["roles","user-profiles"]).has(t)){const i=document.createElement("a");i.href=`#/forms/edit/${h(t)}`,i.className="btn btn-sm btn-ghost",i.setAttribute("data-tooltip","Form"),i.innerHTML='<span data-icon="layout" data-icon-size="14"></span>',c.appendChild(i)}const l=document.createElement("button");return l.className="btn btn-sm btn-danger js-delete-collection",l.dataset.slug=t,l.dataset.plugin=n.plugin||"",l.setAttribute("data-tooltip","Delete"),l.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',c.appendChild(l),c.outerHTML}}],emptyMessage:'No collections yet. Click "New Collection" to get started.'}),Domma.icons.scan(),document.querySelectorAll("#collections-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})})}async function q(e){try{d=await y.collections.getConnections()}catch{d={}}C(e),v(e)}function C(e){const s=e.find("#connections-list").get(0),p=e.find("#connections-empty").get(0);if(!s)return;s.textContent="";const t=Object.keys(d);p&&(p.style.display=t.length?"none":""),t.forEach(n=>{const c=d[n],a=document.createElement("div");a.className="conn-card card mb-3",a.dataset.connKey=n;const o=document.createElement("div");o.className="card-header",o.style.cssText="display:flex;align-items:center;gap:.5rem;";const r=document.createElement("input");r.type="text",r.className="form-input conn-name",r.value=n,r.placeholder="Connection name",r.style.cssText="flex:1;";const l=document.createElement("button");l.type="button",l.className="btn btn-sm btn-danger",l.title="Delete connection",l.innerHTML='<span data-icon="trash"></span>',l.addEventListener("click",()=>{d=x(e);const L=r.value.trim();L&&delete d[L],C(e),v(e)}),o.appendChild(r),o.appendChild(l);const i=document.createElement("div");i.className="card-body",i.style.cssText="display:grid;grid-template-columns:1fr 2fr 1fr;gap:.6rem;";const m=document.createElement("div"),u=document.createElement("label");u.className="form-label",u.textContent="Type";const f=document.createElement("input");f.type="text",f.className="form-input conn-type",f.value=c.type||"mongodb",f.disabled=!0,m.appendChild(u),m.appendChild(f);const w=document.createElement("div"),N=document.createElement("label");N.className="form-label",N.textContent="URI";const g=document.createElement("input");g.type="text",g.className="form-input conn-uri",g.value=c.uri||"",g.placeholder="mongodb://localhost:27017",w.appendChild(N),w.appendChild(g);const S=document.createElement("div"),k=document.createElement("label");k.className="form-label",k.textContent="Database";const b=document.createElement("input");b.type="text",b.className="form-input conn-database",b.value=c.database||"",b.placeholder="mydb",S.appendChild(k),S.appendChild(b),i.appendChild(m),i.appendChild(w),i.appendChild(S),a.appendChild(o),a.appendChild(i),s.appendChild(a)}),Domma.icons.scan(s)}function x(e){const s={},p=e.find("#connections-list").get(0)?.querySelectorAll(".conn-card")||[];for(const t of p){const n=t.querySelector(".conn-name")?.value.trim(),c=t.querySelector(".conn-uri")?.value.trim()||"",a=t.querySelector(".conn-database")?.value.trim()||"";n&&(s[n]={type:"mongodb",uri:c,database:a,options:{}})}return s}function v(e){const s=e.find("#connections-raw-json").get(0);s&&(s.value=JSON.stringify(d,null,2))}
1
+ import{api as y}from"../api.js";import{filterByProject as M,getProjectFromHash as B}from"../lib/project-context.js";import{keepTableIconsAlive as A}from"../lib/table-icons.js";function h(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}let d={};export const collectionsView={templateUrl:"/admin/js/templates/collections.html",async onMount(e){const s=E.loader(e.get(0),{type:"dots"});E.tabs(e.find("#collections-tabs").get(0));const p=e.find("#collections-tabs").get(0)?.querySelector(".tab-list");if(p){const t=e.find("#collections-header-actions").get(0);Array.from(p.querySelectorAll(".tab-item")).forEach((n,c)=>{n.addEventListener("click",()=>{t&&(t.style.display=c===0?"":"none")})})}await Promise.all([j(e),q(e)]),s.destroy(),A(e.get(0)),e.off("click",".js-delete-collection").on("click",".js-delete-collection",async function(){const t=this.dataset.slug,n=this.dataset.plugin,c=n?`This collection is managed by the <strong>${n}</strong> plugin. Deleting it may cause the plugin to malfunction. Continue?`:`Delete collection "${t}" and all its data? This cannot be undone.`;if(await E.confirm(c))try{await y.collections.delete(t),E.toast("Collection deleted.",{type:"success"}),await j(e)}catch{E.toast("Failed to delete collection.",{type:"error"})}}),e.find("#create-collection-btn").off("click").on("click",()=>{const t=E.modal({title:"New Collection",size:"sm"}),n=document.createElement("div");n.style.cssText="padding:.25rem 0 .5rem;";const c=document.createElement("div");F.create({title:{type:"string",label:"Collection Title",placeholder:"e.g. Products, Blog Posts\u2026",required:!0}},{},{showSubmitButton:!1}).renderTo(c),n.appendChild(c);const a=document.createElement("div");a.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const o=document.createElement("button");o.className="btn btn-ghost",o.textContent="Cancel";const r=document.createElement("button");r.className="btn btn-primary",r.textContent="Create",a.appendChild(o),a.appendChild(r),n.appendChild(a),t.element.appendChild(n),t.open();const l=c.querySelector('input[name="title"]');setTimeout(()=>l?.focus(),50);async function i(){const m=l?.value.trim();if(m)try{const u=await y.collections.create({title:m});t.close(),R.navigate(`/collections/edit/${u.slug}`)}catch(u){E.toast(u.message||"Failed to create collection.",{type:"error"})}}o.addEventListener("click",()=>t.close()),r.addEventListener("click",i),l?.addEventListener("keydown",m=>{m.key==="Enter"&&i()})}),e.find("#connections-raw-toggle").on("change",function(){if(this.checked)d=x(e),v(e),e.find("#connections-form-view").hide(),e.find("#connections-raw-view").show();else{const t=e.find("#connections-raw-json").get(0)?.value||"{}";try{d=JSON.parse(t),C(e),e.find("#connections-form-view").show(),e.find("#connections-raw-view").hide()}catch{E.toast("Invalid JSON \u2014 fix before switching to form view.",{type:"warning"}),this.checked=!0}}}),e.find("#add-connection-btn").on("click",()=>{d=x(e);let t=Object.keys(d).length+1,n=`connection${t}`;for(;d[n];)t++,n=`connection${t}`;d[n]={type:"mongodb",uri:"",database:"",options:{}},C(e),v(e)}),e.find("#save-connections-btn").on("click",async()=>{const t=e.find("#connections-raw-toggle").get(0)?.checked;let n;if(t)try{n=JSON.parse(e.find("#connections-raw-json").get(0)?.value||"{}")}catch{E.toast("Invalid JSON \u2014 fix before saving.",{type:"warning"});return}else n=x(e);try{await y.collections.saveConnections(n),d=n,E.toast("Connections saved.",{type:"success"})}catch(c){E.toast(c.message||"Failed to save connections.",{type:"error"})}}),Domma.icons.scan()}};async function j(e){let s=[];try{s=await y.collections.list()}catch{E.toast("Could not load collections.",{type:"error"})}const p=B();p&&(s=M(s,p)),T.create("#collections-table",{data:s,columns:[{key:"title",title:"Title",render:(t,n)=>{const c=document.createElement("span");c.style.cssText="display:flex;align-items:center;gap:.4rem;";const a=document.createElement("a");if(a.href=`#/collections/${h(n.slug)}/entries`,a.textContent=t,a.style.fontWeight="600",c.appendChild(a),n.storage?.adapter==="mongodb"){const o=document.createElement("span");o.className="badge badge-outline",o.textContent="MongoDB",o.title=`Connection: ${n.storage.connection||"default"}`,o.style.cssText="font-size:0.65rem;padding:1px 6px;color:var(--dm-success,#16a34a);border-color:var(--dm-success,#16a34a);flex-shrink:0;",c.appendChild(o)}if(n.bundled){const o=document.createElement("span");o.className="badge badge-outline",o.textContent="Bundled",o.title="Seeded on fresh installs",o.style.cssText="font-size:0.65rem;padding:1px 6px;color:var(--dm-info,#2563eb);border-color:var(--dm-info,#2563eb);flex-shrink:0;",c.appendChild(o)}if(n.plugin){const o=document.createElement("span");o.className="badge badge-outline",o.textContent=n.plugin,o.title=`Managed by the ${n.plugin} plugin`,o.style.cssText="font-size:0.65rem;padding:1px 6px;color:var(--dm-warning,#d97706);border-color:var(--dm-warning,#d97706);flex-shrink:0;",c.appendChild(o)}return c.outerHTML}},{key:"slug",title:"Slug",render:t=>{const n=document.createElement("code");return n.textContent=t,n.outerHTML}},{key:"fields",title:"Field Count",render:t=>String(t?.length??0)},{key:"entryCount",title:"Entry Count",render:t=>String(t??0)},{key:"slug",title:"Actions",render:(t,n)=>{const c=document.createElement("div");c.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;flex-wrap:wrap;";const a=document.createElement("a");a.href=`#/collections/edit/${h(t)}`,a.className="btn btn-sm btn-ghost",a.setAttribute("data-tooltip","Edit schema"),a.innerHTML='<span data-icon="edit-3" data-icon-size="14"></span>';const o=document.createElement("a");if(o.href=`#/collections/${h(t)}/entries`,o.className="btn btn-sm btn-ghost",o.setAttribute("data-tooltip","Entries"),o.innerHTML='<span data-icon="database" data-icon-size="14"></span>',c.appendChild(a),c.appendChild(o),!new Set(["roles","user-profiles"]).has(t)){const i=document.createElement("a");i.href=`#/forms/edit/${h(t)}`,i.className="btn btn-sm btn-ghost",i.setAttribute("data-tooltip","Form"),i.innerHTML='<span data-icon="layout" data-icon-size="14"></span>',c.appendChild(i)}const l=document.createElement("button");return l.className="btn btn-sm btn-ghost js-delete-collection",l.dataset.slug=t,l.dataset.plugin=n.plugin||"",l.setAttribute("data-tooltip","Delete"),l.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',c.appendChild(l),c.outerHTML}}],emptyMessage:'No collections yet. Click "New Collection" to get started.'}),Domma.icons.scan(),document.querySelectorAll("#collections-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})})}async function q(e){try{d=await y.collections.getConnections()}catch{d={}}C(e),v(e)}function C(e){const s=e.find("#connections-list").get(0),p=e.find("#connections-empty").get(0);if(!s)return;s.textContent="";const t=Object.keys(d);p&&(p.style.display=t.length?"none":""),t.forEach(n=>{const c=d[n],a=document.createElement("div");a.className="conn-card card mb-3",a.dataset.connKey=n;const o=document.createElement("div");o.className="card-header",o.style.cssText="display:flex;align-items:center;gap:.5rem;";const r=document.createElement("input");r.type="text",r.className="form-input conn-name",r.value=n,r.placeholder="Connection name",r.style.cssText="flex:1;";const l=document.createElement("button");l.type="button",l.className="btn btn-sm btn-ghost",l.title="Delete connection",l.innerHTML='<span data-icon="trash"></span>',l.addEventListener("click",()=>{d=x(e);const L=r.value.trim();L&&delete d[L],C(e),v(e)}),o.appendChild(r),o.appendChild(l);const i=document.createElement("div");i.className="card-body",i.style.cssText="display:grid;grid-template-columns:1fr 2fr 1fr;gap:.6rem;";const m=document.createElement("div"),u=document.createElement("label");u.className="form-label",u.textContent="Type";const f=document.createElement("input");f.type="text",f.className="form-input conn-type",f.value=c.type||"mongodb",f.disabled=!0,m.appendChild(u),m.appendChild(f);const w=document.createElement("div"),N=document.createElement("label");N.className="form-label",N.textContent="URI";const g=document.createElement("input");g.type="text",g.className="form-input conn-uri",g.value=c.uri||"",g.placeholder="mongodb://localhost:27017",w.appendChild(N),w.appendChild(g);const S=document.createElement("div"),k=document.createElement("label");k.className="form-label",k.textContent="Database";const b=document.createElement("input");b.type="text",b.className="form-input conn-database",b.value=c.database||"",b.placeholder="mydb",S.appendChild(k),S.appendChild(b),i.appendChild(m),i.appendChild(w),i.appendChild(S),a.appendChild(o),a.appendChild(i),s.appendChild(a)}),Domma.icons.scan(s)}function x(e){const s={},p=e.find("#connections-list").get(0)?.querySelectorAll(".conn-card")||[];for(const t of p){const n=t.querySelector(".conn-name")?.value.trim(),c=t.querySelector(".conn-uri")?.value.trim()||"",a=t.querySelector(".conn-database")?.value.trim()||"";n&&(s[n]={type:"mongodb",uri:c,database:a,options:{}})}return s}function v(e){const s=e.find("#connections-raw-json").get(0);s&&(s.value=JSON.stringify(d,null,2))}
@@ -1,5 +1,5 @@
1
- import{api as c}from"../api.js";import{filterByProject as u,getProjectFromHash as f}from"../lib/project-context.js";import{keepTableIconsAlive as g}from"../lib/table-icons.js";function p(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function y(t){for(;t.firstChild;)t.removeChild(t.firstChild)}export const componentsView={templateUrl:"/admin/js/templates/components.html",async onMount(t){h(),await l(t),b(t),Domma.icons.scan(t.get(0)),g(t.get(0))}};let d=!1;function h(){d||(d=!0,I.register("download",{viewBox:"0 0 24 24",paths:["M12 3v12","M7 10l5 5 5-5","M5 19h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("upload",{viewBox:"0 0 24 24",paths:["M12 21V9","M7 14l5-5 5 5","M5 5h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("plus",{viewBox:"0 0 24 24",paths:["M12 5v14","M5 12h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}))}async function l(t){const o=t.find("#components-table-container").get(0);if(!o)return;let n=[];const i=E.loader(t.get(0),{type:"dots"});try{n=await c.components.list()}catch(e){y(o);const r=document.createElement("p");r.className="text-muted",r.textContent=`Failed to load components: ${e.message}`,o.appendChild(r);return}finally{i.destroy()}const s=f();s&&(n=u(n,s)),T.create(o,{data:n,emptyMessage:'No components yet. Click "New Component" to create your first one.',columns:[{key:"name",title:"Name",render:(e,r)=>{const a=r.origin==="plugin"?' <span class="badge badge-secondary" title="Provided by a plugin \u2014 read-only">plugin</span>':"";return`<a href="#/components/edit/${p(e)}">${p(e)}.dmc</a>${a}`}},{key:"props",title:"Props",render:e=>{const r=Object.keys(e||{});return r.length===0?'<span class="text-muted">\u2014</span>':r.map(a=>`<span class="badge badge-secondary" style="margin-right:.25rem;">${p(a)}</span>`).join("")}},{key:"size",title:"Size",render:e=>`${e} B`},{key:"updatedAt",title:"Updated",render:e=>e?new Date(e).toLocaleString():"\u2014"},{key:"name",title:"Actions",render:(e,r)=>{const a=p(e),m=r.origin==="plugin"?"":`<button class="btn btn-sm btn-danger js-delete-component" data-name="${a}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>`;return`<div style="display:flex;gap:.4rem;justify-content:flex-end;">
2
- <a href="#/components/edit/${a}" class="btn btn-sm btn-ghost" data-tooltip="Edit"><span data-icon="edit" data-icon-size="14"></span></a>
3
- <button class="btn btn-sm btn-ghost js-export-component" data-name="${a}" data-tooltip="Export as .dmcomponent.json"><span data-icon="download" data-icon-size="14"></span></button>
1
+ import{api as c}from"../api.js";import{filterByProject as u,getProjectFromHash as f}from"../lib/project-context.js";import{keepTableIconsAlive as g}from"../lib/table-icons.js";function p(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function y(e){for(;e.firstChild;)e.removeChild(e.firstChild)}export const componentsView={templateUrl:"/admin/js/templates/components.html",async onMount(e){h(),await l(e),b(e),Domma.icons.scan(e.get(0)),g(e.get(0))}};let d=!1;function h(){d||(d=!0,I.register("download",{viewBox:"0 0 24 24",paths:["M12 3v12","M7 10l5 5 5-5","M5 19h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("upload",{viewBox:"0 0 24 24",paths:["M12 21V9","M7 14l5-5 5 5","M5 5h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("plus",{viewBox:"0 0 24 24",paths:["M12 5v14","M5 12h14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}))}async function l(e){const o=e.find("#components-table-container").get(0);if(!o)return;let n=[];const i=E.loader(e.get(0),{type:"dots"});try{n=await c.components.list()}catch(t){y(o);const r=document.createElement("p");r.className="text-muted",r.textContent=`Failed to load components: ${t.message}`,o.appendChild(r);return}finally{i.destroy()}const a=f();a&&(n=u(n,a)),T.create(o,{data:n,emptyMessage:'No components yet. Click "New Component" to create your first one.',columns:[{key:"name",title:"Name",render:(t,r)=>{const s=r.origin==="plugin"?' <span class="badge badge-secondary" title="Provided by a plugin \u2014 read-only">plugin</span>':"";return`<a href="#/components/edit/${p(t)}">${p(t)}.dmc</a>${s}`}},{key:"props",title:"Props",render:t=>{const r=Object.keys(t||{});return r.length===0?'<span class="text-muted">\u2014</span>':r.map(s=>`<span class="badge badge-secondary" style="margin-right:.25rem;">${p(s)}</span>`).join("")}},{key:"size",title:"Size",render:t=>`${t} B`},{key:"updatedAt",title:"Updated",render:t=>t?new Date(t).toLocaleString():"\u2014"},{key:"name",title:"Actions",render:(t,r)=>{const s=p(t),m=r.origin==="plugin"?"":`<button class="btn btn-sm btn-ghost js-delete-component" data-name="${s}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>`;return`<div style="display:flex;gap:.4rem;justify-content:flex-end;">
2
+ <a href="#/components/edit/${s}" class="btn btn-sm btn-ghost" data-tooltip="Edit"><span data-icon="edit" data-icon-size="14"></span></a>
3
+ <button class="btn btn-sm btn-ghost js-export-component" data-name="${s}" data-tooltip="Export as .dmcomponent.json"><span data-icon="download" data-icon-size="14"></span></button>
4
4
  ${m}
5
- </div>`}}]}),t.off("click",".js-delete-component").on("click",".js-delete-component",async function(){await k(this.dataset.name,t)}),t.off("click",".js-export-component").on("click",".js-export-component",async function(){try{await c.components.exportBundle(this.dataset.name)}catch(e){E.toast(e.message||"Export failed.",{type:"error"})}}),Domma.icons.scan(o),o.querySelectorAll("[data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})}async function k(t,o){if(await E.confirm(`Delete component "${t}"? This cannot be undone.`))try{await c.components.delete(t),E.toast("Component deleted.",{type:"success"}),await l(o)}catch(i){E.toast(i.message||"Failed to delete component.",{type:"error"})}}function b(t){const o=t.find("#import-component-btn").get(0),n=t.find("#import-component-file").get(0);!o||!n||(o.addEventListener("click",()=>n.click()),n.addEventListener("change",async()=>{const i=n.files?.[0];if(!i)return;n.value="";let s;try{const e=await i.text();s=JSON.parse(e)}catch{E.toast("Not a valid .dmcomponent.json file (could not parse JSON).",{type:"error"});return}if(!s||typeof s!="object"||!s.name||typeof s.source!="string"){E.toast("Bundle is missing a name or source field.",{type:"error"});return}try{const e=await c.components.importBundle(s);E.toast(`Imported "${e.name}".`,{type:"success"}),await l(t)}catch(e){if(e.code==="CONFLICT"){if(!await E.confirm(`A component named "${e.name}" already exists. Overwrite it with the imported version?`)){E.toast("Import cancelled.",{type:"info"});return}try{const a=await c.components.importBundle(s,{overwrite:!0});E.toast(`Overwrote "${a.name}".`,{type:"success"}),await l(t)}catch(a){E.toast(a.message||"Import failed.",{type:"error"})}return}E.toast(e.message||"Import failed.",{type:"error"})}}))}
5
+ </div>`}}]}),e.off("click",".js-delete-component").on("click",".js-delete-component",async function(){await k(this.dataset.name,e)}),e.off("click",".js-export-component").on("click",".js-export-component",async function(){try{await c.components.exportBundle(this.dataset.name)}catch(t){E.toast(t.message||"Export failed.",{type:"error"})}}),Domma.icons.scan(o),o.querySelectorAll("[data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})})}async function k(e,o){if(await E.confirm(`Delete component "${e}"? This cannot be undone.`))try{await c.components.delete(e),E.toast("Component deleted.",{type:"success"}),await l(o)}catch(i){E.toast(i.message||"Failed to delete component.",{type:"error"})}}function b(e){const o=e.find("#import-component-btn").get(0),n=e.find("#import-component-file").get(0);!o||!n||(o.addEventListener("click",()=>n.click()),n.addEventListener("change",async()=>{const i=n.files?.[0];if(!i)return;n.value="";let a;try{const t=await i.text();a=JSON.parse(t)}catch{E.toast("Not a valid .dmcomponent.json file (could not parse JSON).",{type:"error"});return}if(!a||typeof a!="object"||!a.name||typeof a.source!="string"){E.toast("Bundle is missing a name or source field.",{type:"error"});return}try{const t=await c.components.importBundle(a);E.toast(`Imported "${t.name}".`,{type:"success"}),await l(e)}catch(t){if(t.code==="CONFLICT"){if(!await E.confirm(`A component named "${t.name}" already exists. Overwrite it with the imported version?`)){E.toast("Import cancelled.",{type:"info"});return}try{const s=await c.components.importBundle(a,{overwrite:!0});E.toast(`Overwrote "${s.name}".`,{type:"success"}),await l(e)}catch(s){E.toast(s.message||"Import failed.",{type:"error"})}return}E.toast(t.message||"Import failed.",{type:"error"})}}))}
@@ -1 +1 @@
1
- import{apiRequest as x}from"/admin/js/api.js";import{keepTableIconsAlive as A}from"/admin/js/lib/table-icons.js";let c=null,k=[],r=[];export const formSubmissionsView={templateUrl:"/admin/js/templates/form-submissions.html",async onMount(e){const l=window.location.hash.match(/\/forms\/([^/?#]+)\/submissions/);if(c=l?l[1]:null,!c){E.toast("No form selected.",{type:"error"});return}const u=E.loader(e.get(0),{type:"dots"});try{const s=await x(`/forms/${c}`);k=(s.fields||[]).filter(n=>n.type!=="page-break"),e.find("#submissions-title").get(0).textContent=`${s.title} \u2014 Submissions`}catch{E.toast("Could not load form definition.",{type:"error"})}try{await M(e)}finally{u.destroy()}A(e.get(0)),e.off("click",".js-delete-submission").on("click",".js-delete-submission",async function(s){s.stopPropagation();const n=this.dataset.id;if(await E.confirm("Delete this submission?"))try{await x(`/forms/${c}/submissions/${n}`,{method:"DELETE"}),r=r.filter(d=>d.id!==n),E.toast("Submission deleted.",{type:"success"}),v(r,e)}catch{E.toast("Failed to delete submission.",{type:"error"})}}),e.find("#export-btn").off("click").on("click",()=>{const s=E.slideover({title:"Export Submissions",size:"sm",position:"right"}),n=document.createElement("div");n.style.cssText="padding:1.25rem;display:flex;flex-direction:column;gap:1rem;";function i(d,y,f,g){const C=document.createElement("div");C.style.cssText="display:flex;flex-direction:column;gap:.4rem;";const b=document.createElement("button");b.className="btn btn-ghost",b.style.cssText="justify-content:flex-start;gap:.5rem;";const L=document.createElement("span");return L.setAttribute("data-icon",y),b.appendChild(L),b.appendChild(document.createTextNode(" "+d)),b.addEventListener("click",()=>{fetch(f,{headers:{Authorization:"Bearer "+(S.get("auth_token")||"")}}).then(w=>w.blob()).then(w=>{const h=document.createElement("a");h.href=URL.createObjectURL(w),h.download=g,document.body.appendChild(h),h.click(),document.body.removeChild(h),URL.revokeObjectURL(h.href),s.close()}).catch(()=>E.toast("Export failed.",{type:"error"}))}),C.appendChild(b),C}n.appendChild(i("Export as CSV","file-text",`/api/forms/${c}/submissions/export`,`${c}-submissions.csv`)),n.appendChild(i("Export as JSON","code",`/api/forms/${c}/submissions/export/json`,`${c}-submissions.json`)),s.setContent(n),Domma.icons.scan(n),s.open()}),e.find("#clear-all-btn").off("click").on("click",async()=>{if(await E.confirm("Delete all submissions? This cannot be undone."))try{await x(`/forms/${c}/submissions`,{method:"DELETE"}),r=[],E.toast("All submissions cleared.",{type:"success"}),v([],e)}catch{E.toast("Failed to clear submissions.",{type:"error"})}});const m=e.find("#sub-search").get(0),o=e.find("#sub-date-from").get(0),t=e.find("#sub-date-to").get(0);function a(){const s=m?.value.toLowerCase()||"",n=o?.value?new Date(o.value):null,i=t?.value?new Date(t.value+"T23:59:59"):null,d=r.filter(y=>{if(s&&!Object.values(y.data||{}).map(g=>String(g).toLowerCase()).some(g=>g.includes(s)))return!1;if(n||i){const f=y.meta?.createdAt?new Date(y.meta.createdAt):null;if(!f||n&&f<n||i&&f>i)return!1}return!0});v(d,e)}m&&m.addEventListener("input",a),o&&o.addEventListener("change",a),t&&t.addEventListener("change",a),Domma.icons.scan()}};async function M(e){try{r=await x(`/forms/${c}/submissions`)}catch{r=[],E.toast("Could not load submissions.",{type:"error"})}v(r,e)}function v(e,p){const l=p.find("#sub-count").get(0);l&&(e.length===r.length?l.textContent=`${r.length} submission${r.length!==1?"s":""}`:l.textContent=`Showing ${e.length} of ${r.length}`);const m=[...k.map(t=>({key:`data.${t.name}`,title:t.label||t.name,render:(a,s)=>{const n=s.data?.[t.name]??"",i=String(n),d=document.createElement("span");return d.title=i,d.textContent=i.length>80?i.slice(0,80)+"\u2026":i,d.outerHTML}})),{key:"meta",title:"Date",render:t=>D(t?.createdAt).format("DD MMM YYYY HH:mm")},{key:"id",title:"",render:t=>{const a=document.createElement("button");return a.className="btn btn-sm btn-danger js-delete-submission",a.dataset.id=t,a.setAttribute("data-tooltip","Delete"),a.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',a.outerHTML}}];T.create("#submissions-table",{data:e,columns:m,emptyMessage:"No submissions yet."});const o=document.querySelector("#submissions-table");o&&(o.querySelectorAll("tbody tr").forEach((t,a)=>{const s=e[a];s&&(t.style.cursor="pointer",t.addEventListener("click",n=>{n.target.closest(".js-delete-submission")||j(s)}))}),Domma.icons.scan(o),o.querySelectorAll("[data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}))}function j(e){const p=document.createElement("div"),l=document.createElement("div");l.style.cssText="display:flex;flex-direction:column;gap:.75rem;margin-bottom:1.25rem;",k.forEach(o=>{const t=e.data?.[o.name];if(t==null||t==="")return;const a=document.createElement("div");a.style.cssText="border-bottom:1px solid var(--dm-border,#333);padding-bottom:.6rem;";const s=document.createElement("strong");s.textContent=o.label||o.name,s.style.cssText="display:block;font-size:.8rem;color:var(--dm-text-muted,#888);margin-bottom:.2rem;";const n=document.createElement("p");n.textContent=String(t),n.style.cssText="margin:0;word-break:break-word;",a.appendChild(s),a.appendChild(n),l.appendChild(a)});const u=document.createElement("div");if(u.style.cssText="font-size:.8rem;color:var(--dm-text-muted,#888);display:flex;flex-direction:column;gap:.3rem;border-top:1px solid var(--dm-border,#333);padding-top:.75rem;",e.meta?.createdAt){const o=document.createElement("span");o.textContent=`Submitted: ${D(e.meta.createdAt).format("DD MMM YYYY HH:mm")}`,u.appendChild(o)}if(e.meta?.ip){const o=document.createElement("span");o.textContent=`IP: ${e.meta.ip}`,u.appendChild(o)}p.appendChild(l),p.appendChild(u);const m=E.modal({title:"Submission Details",size:"md"});m.element.appendChild(p),m.open()}
1
+ import{apiRequest as x}from"/admin/js/api.js";import{keepTableIconsAlive as A}from"/admin/js/lib/table-icons.js";let c=null,k=[],r=[];export const formSubmissionsView={templateUrl:"/admin/js/templates/form-submissions.html",async onMount(e){const l=window.location.hash.match(/\/forms\/([^/?#]+)\/submissions/);if(c=l?l[1]:null,!c){E.toast("No form selected.",{type:"error"});return}const u=E.loader(e.get(0),{type:"dots"});try{const o=await x(`/forms/${c}`);k=(o.fields||[]).filter(s=>s.type!=="page-break"),e.find("#submissions-title").get(0).textContent=`${o.title} \u2014 Submissions`}catch{E.toast("Could not load form definition.",{type:"error"})}try{await M(e)}finally{u.destroy()}A(e.get(0)),e.off("click",".js-delete-submission").on("click",".js-delete-submission",async function(o){o.stopPropagation();const s=this.dataset.id;if(await E.confirm("Delete this submission?"))try{await x(`/forms/${c}/submissions/${s}`,{method:"DELETE"}),r=r.filter(d=>d.id!==s),E.toast("Submission deleted.",{type:"success"}),v(r,e)}catch{E.toast("Failed to delete submission.",{type:"error"})}}),e.find("#export-btn").off("click").on("click",()=>{const o=E.slideover({title:"Export Submissions",size:"sm",position:"right"}),s=document.createElement("div");s.style.cssText="padding:1.25rem;display:flex;flex-direction:column;gap:1rem;";function i(d,y,f,g){const C=document.createElement("div");C.style.cssText="display:flex;flex-direction:column;gap:.4rem;";const b=document.createElement("button");b.className="btn btn-ghost",b.style.cssText="justify-content:flex-start;gap:.5rem;";const L=document.createElement("span");return L.setAttribute("data-icon",y),b.appendChild(L),b.appendChild(document.createTextNode(" "+d)),b.addEventListener("click",()=>{fetch(f,{headers:{Authorization:"Bearer "+(S.get("auth_token")||"")}}).then(w=>w.blob()).then(w=>{const h=document.createElement("a");h.href=URL.createObjectURL(w),h.download=g,document.body.appendChild(h),h.click(),document.body.removeChild(h),URL.revokeObjectURL(h.href),o.close()}).catch(()=>E.toast("Export failed.",{type:"error"}))}),C.appendChild(b),C}s.appendChild(i("Export as CSV","file-text",`/api/forms/${c}/submissions/export`,`${c}-submissions.csv`)),s.appendChild(i("Export as JSON","code",`/api/forms/${c}/submissions/export/json`,`${c}-submissions.json`)),o.setContent(s),Domma.icons.scan(s),o.open()}),e.find("#clear-all-btn").off("click").on("click",async()=>{if(await E.confirm("Delete all submissions? This cannot be undone."))try{await x(`/forms/${c}/submissions`,{method:"DELETE"}),r=[],E.toast("All submissions cleared.",{type:"success"}),v([],e)}catch{E.toast("Failed to clear submissions.",{type:"error"})}});const m=e.find("#sub-search").get(0),n=e.find("#sub-date-from").get(0),t=e.find("#sub-date-to").get(0);function a(){const o=m?.value.toLowerCase()||"",s=n?.value?new Date(n.value):null,i=t?.value?new Date(t.value+"T23:59:59"):null,d=r.filter(y=>{if(o&&!Object.values(y.data||{}).map(g=>String(g).toLowerCase()).some(g=>g.includes(o)))return!1;if(s||i){const f=y.meta?.createdAt?new Date(y.meta.createdAt):null;if(!f||s&&f<s||i&&f>i)return!1}return!0});v(d,e)}m&&m.addEventListener("input",a),n&&n.addEventListener("change",a),t&&t.addEventListener("change",a),Domma.icons.scan()}};async function M(e){try{r=await x(`/forms/${c}/submissions`)}catch{r=[],E.toast("Could not load submissions.",{type:"error"})}v(r,e)}function v(e,p){const l=p.find("#sub-count").get(0);l&&(e.length===r.length?l.textContent=`${r.length} submission${r.length!==1?"s":""}`:l.textContent=`Showing ${e.length} of ${r.length}`);const m=[...k.map(t=>({key:`data.${t.name}`,title:t.label||t.name,render:(a,o)=>{const s=o.data?.[t.name]??"",i=String(s),d=document.createElement("span");return d.title=i,d.textContent=i.length>80?i.slice(0,80)+"\u2026":i,d.outerHTML}})),{key:"meta",title:"Date",render:t=>D(t?.createdAt).format("DD MMM YYYY HH:mm")},{key:"id",title:"",render:t=>{const a=document.createElement("button");return a.className="btn btn-sm btn-ghost js-delete-submission",a.dataset.id=t,a.setAttribute("data-tooltip","Delete"),a.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',a.outerHTML}}];T.create("#submissions-table",{data:e,columns:m,emptyMessage:"No submissions yet."});const n=document.querySelector("#submissions-table");n&&(n.querySelectorAll("tbody tr").forEach((t,a)=>{const o=e[a];o&&(t.style.cursor="pointer",t.addEventListener("click",s=>{s.target.closest(".js-delete-submission")||j(o)}))}),Domma.icons.scan(n),n.querySelectorAll("[data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}))}function j(e){const p=document.createElement("div"),l=document.createElement("div");l.style.cssText="display:flex;flex-direction:column;gap:.75rem;margin-bottom:1.25rem;",k.forEach(n=>{const t=e.data?.[n.name];if(t==null||t==="")return;const a=document.createElement("div");a.style.cssText="border-bottom:1px solid var(--dm-border,#333);padding-bottom:.6rem;";const o=document.createElement("strong");o.textContent=n.label||n.name,o.style.cssText="display:block;font-size:.8rem;color:var(--dm-text-muted,#888);margin-bottom:.2rem;";const s=document.createElement("p");s.textContent=String(t),s.style.cssText="margin:0;word-break:break-word;",a.appendChild(o),a.appendChild(s),l.appendChild(a)});const u=document.createElement("div");if(u.style.cssText="font-size:.8rem;color:var(--dm-text-muted,#888);display:flex;flex-direction:column;gap:.3rem;border-top:1px solid var(--dm-border,#333);padding-top:.75rem;",e.meta?.createdAt){const n=document.createElement("span");n.textContent=`Submitted: ${D(e.meta.createdAt).format("DD MMM YYYY HH:mm")}`,u.appendChild(n)}if(e.meta?.ip){const n=document.createElement("span");n.textContent=`IP: ${e.meta.ip}`,u.appendChild(n)}p.appendChild(l),p.appendChild(u);const m=E.modal({title:"Submission Details",size:"md"});m.element.appendChild(p),m.open()}
@@ -1 +1 @@
1
- import{apiRequest as p}from"/admin/js/api.js";import{filterByProject as y,getProjectFromHash as h}from"/admin/js/lib/project-context.js";import{keepTableIconsAlive as C}from"/admin/js/lib/table-icons.js";function u(r){return String(r).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}export const formsView={templateUrl:"/admin/js/templates/forms.html",async onMount(r){await g(r),C(r.get(0)),r.off("click",".js-delete-form").on("click",".js-delete-form",async function(){const c=this.dataset.slug,i=this.dataset.plugin,l=i?`This form is managed by the <strong>${i}</strong> plugin. Deleting it may cause the plugin to malfunction. Continue?`:`Delete form "${c}" and all its submissions? This cannot be undone.`;if(await E.confirm(l))try{await p(`/forms/${c}`,{method:"DELETE"}),E.toast("Form deleted.",{type:"success"}),await g(r)}catch{E.toast("Failed to delete form.",{type:"error"})}}),r.find("#create-form-btn").off("click").on("click",()=>{const c=E.modal({title:"Create Form",size:"sm"}),i=document.createElement("div");i.style.cssText="padding:.25rem 0 .5rem;";const l=document.createElement("div");F.create({title:{type:"string",label:"Form Title",placeholder:"e.g. Contact, Feedback\u2026",required:!0}},{},{showSubmitButton:!1}).renderTo(l),i.appendChild(l);const e=document.createElement("div");e.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const n=document.createElement("button");n.className="btn btn-ghost",n.textContent="Cancel";const s=document.createElement("button");s.className="btn btn-primary",s.textContent="Create",e.appendChild(n),e.appendChild(s),i.appendChild(e),c.element.appendChild(i),c.open();const t=l.querySelector('input[name="title"]');setTimeout(()=>t?.focus(),50);async function a(){const o=t?.value.trim();if(o)try{const d=await p("/forms",{method:"POST",body:JSON.stringify({title:o})});c.close(),R.navigate(`/forms/edit/${d.slug}`)}catch(d){E.toast(d.message||"Failed to create form.",{type:"error"})}}n.addEventListener("click",()=>c.close()),s.addEventListener("click",a),t?.addEventListener("keydown",o=>{o.key==="Enter"&&a()})}),r.find("#test-email-btn").off("click").on("click",async()=>{const i=(window.__CMS_SITE__?.smtp||{}).fromAddress||"",l=E.modal({title:"Send Test Email",size:"sm"}),e=document.createElement("div");e.style.cssText="padding:.25rem 0 .5rem;";const n=document.createElement("div");n.className="mb-3";const s=document.createElement("label");s.className="form-label",s.textContent="Send to";const t=document.createElement("input");t.type="email",t.className="form-input",t.value=i,t.placeholder="test@example.com",n.appendChild(s),n.appendChild(t),e.appendChild(n);const a=document.createElement("p");a.className="text-muted",a.style.cssText="font-size:.8rem;margin-bottom:.75rem;",a.textContent="SMTP settings are configured in Site Settings.",e.appendChild(a);const o=document.createElement("div");o.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const d=document.createElement("button");d.className="btn btn-ghost",d.textContent="Cancel";const m=document.createElement("button");m.className="btn btn-primary",m.textContent="Send",o.appendChild(d),o.appendChild(m),e.appendChild(o),l.element.appendChild(e),l.open(),setTimeout(()=>t?.focus(),50),d.addEventListener("click",()=>l.close()),m.addEventListener("click",async()=>{const f=t.value.trim();if(f){m.disabled=!0;try{await p("/forms/test-email",{method:"POST",body:JSON.stringify({to:f})}),l.close(),E.toast("Test email sent.",{type:"success"})}catch(b){E.toast(b.message||"Failed to send test email.",{type:"error"})}finally{m.disabled=!1}}})}),Domma.icons.scan()}};async function g(r){const c=E.loader(r.get(0),{type:"dots"});let i=[];try{i=await p("/forms")}catch{E.toast("Could not load forms.",{type:"error"})}finally{c.destroy()}const l=h();l&&(i=y(i,l)),T.create("#forms-table",{data:i,columns:[{key:"title",title:"Title",render:(e,n)=>{const s=document.createElement("span");s.style.cssText="display:inline-flex;align-items:center;gap:.4rem;flex-wrap:wrap;";const t=document.createElement("a");if(t.href=`#/forms/edit/${u(n.slug)}`,t.textContent=e,t.style.fontWeight="600",s.appendChild(t),n.plugin){const a=document.createElement("span");a.className="badge badge-outline",a.textContent=n.plugin,a.title=`Managed by the ${n.plugin} plugin`,a.style.cssText="font-size:0.65rem;padding:1px 6px;color:var(--dm-warning,#d97706);border-color:var(--dm-warning,#d97706);flex-shrink:0;",s.appendChild(a)}return s.outerHTML}},{key:"slug",title:"Slug",render:e=>{const n=document.createElement("code");return n.textContent=e,n.outerHTML}},{key:"fields",title:"Field Count",render:e=>String(e?.length??0)},{key:"submissionCount",title:"Submission Count",render:e=>String(e??0)},{key:"slug",title:"Actions",render:(e,n)=>{const s=document.createElement("div");s.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;";const t=document.createElement("a");t.href=`#/forms/edit/${u(e)}`,t.className="btn btn-sm btn-ghost",t.setAttribute("data-tooltip","Edit"),t.innerHTML='<span data-icon="edit" data-icon-size="14"></span>';const a=document.createElement("a");a.href=`#/forms/${u(e)}/submissions`,a.className="btn btn-sm btn-ghost",a.setAttribute("data-tooltip","Submissions"),a.innerHTML='<span data-icon="inbox" data-icon-size="14"></span>';const o=document.createElement("button");return o.className="btn btn-sm btn-danger js-delete-form",o.dataset.slug=e,o.dataset.plugin=n.plugin||"",o.setAttribute("data-tooltip","Delete"),o.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',s.appendChild(t),s.appendChild(a),s.appendChild(o),s.outerHTML}}],emptyMessage:'No forms yet. Click "Create Form" to get started.'}),Domma.icons.scan(),document.querySelectorAll("#forms-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})}
1
+ import{apiRequest as p}from"/admin/js/api.js";import{filterByProject as b,getProjectFromHash as h}from"/admin/js/lib/project-context.js";import{keepTableIconsAlive as C}from"/admin/js/lib/table-icons.js";function u(d){return String(d).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}export const formsView={templateUrl:"/admin/js/templates/forms.html",async onMount(d){await g(d),C(d.get(0)),d.off("click",".js-delete-form").on("click",".js-delete-form",async function(){const r=this.dataset.slug,e=this.dataset.plugin,c=e?`This form is managed by the <strong>${e}</strong> plugin. Deleting it may cause the plugin to malfunction. Continue?`:`Delete form "${r}" and all its submissions? This cannot be undone.`;if(await E.confirm(c))try{await p(`/forms/${r}`,{method:"DELETE"}),E.toast("Form deleted.",{type:"success"}),await g(d)}catch{E.toast("Failed to delete form.",{type:"error"})}}),d.off("click",".js-clone-form").on("click",".js-clone-form",async function(){const r=this.dataset.slug;try{const e=await p(`/forms/${r}`),{collection:c,...t}=e.actions||{},n=await p("/forms",{method:"POST",body:JSON.stringify({title:`${e.title} (Copy)`,description:e.description||"",fields:e.fields||[],settings:e.settings||{},actions:t})});E.toast(`Form duplicated as "${n.title}".`,{type:"success"}),R.navigate(`/forms/edit/${n.slug}`)}catch(e){E.toast(e.message||"Failed to duplicate form.",{type:"error"})}}),d.find("#create-form-btn").off("click").on("click",()=>{const r=E.modal({title:"Create Form",size:"sm"}),e=document.createElement("div");e.style.cssText="padding:.25rem 0 .5rem;";const c=document.createElement("div");F.create({title:{type:"string",label:"Form Title",placeholder:"e.g. Contact, Feedback\u2026",required:!0}},{},{showSubmitButton:!1}).renderTo(c),e.appendChild(c);const t=document.createElement("div");t.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const n=document.createElement("button");n.className="btn btn-ghost",n.textContent="Cancel";const o=document.createElement("button");o.className="btn btn-primary",o.textContent="Create",t.appendChild(n),t.appendChild(o),e.appendChild(t),r.element.appendChild(e),r.open();const s=c.querySelector('input[name="title"]');setTimeout(()=>s?.focus(),50);async function a(){const i=s?.value.trim();if(i)try{const l=await p("/forms",{method:"POST",body:JSON.stringify({title:i})});r.close(),R.navigate(`/forms/edit/${l.slug}`)}catch(l){E.toast(l.message||"Failed to create form.",{type:"error"})}}n.addEventListener("click",()=>r.close()),o.addEventListener("click",a),s?.addEventListener("keydown",i=>{i.key==="Enter"&&a()})}),d.find("#test-email-btn").off("click").on("click",async()=>{const e=(window.__CMS_SITE__?.smtp||{}).fromAddress||"",c=E.modal({title:"Send Test Email",size:"sm"}),t=document.createElement("div");t.style.cssText="padding:.25rem 0 .5rem;";const n=document.createElement("div");n.className="mb-3";const o=document.createElement("label");o.className="form-label",o.textContent="Send to";const s=document.createElement("input");s.type="email",s.className="form-input",s.value=e,s.placeholder="test@example.com",n.appendChild(o),n.appendChild(s),t.appendChild(n);const a=document.createElement("p");a.className="text-muted",a.style.cssText="font-size:.8rem;margin-bottom:.75rem;",a.textContent="SMTP settings are configured in Site Settings.",t.appendChild(a);const i=document.createElement("div");i.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const l=document.createElement("button");l.className="btn btn-ghost",l.textContent="Cancel";const m=document.createElement("button");m.className="btn btn-primary",m.textContent="Send",i.appendChild(l),i.appendChild(m),t.appendChild(i),c.element.appendChild(t),c.open(),setTimeout(()=>s?.focus(),50),l.addEventListener("click",()=>c.close()),m.addEventListener("click",async()=>{const f=s.value.trim();if(f){m.disabled=!0;try{await p("/forms/test-email",{method:"POST",body:JSON.stringify({to:f})}),c.close(),E.toast("Test email sent.",{type:"success"})}catch(y){E.toast(y.message||"Failed to send test email.",{type:"error"})}finally{m.disabled=!1}}})}),Domma.icons.scan()}};async function g(d){const r=E.loader(d.get(0),{type:"dots"});let e=[];try{e=await p("/forms")}catch{E.toast("Could not load forms.",{type:"error"})}finally{r.destroy()}const c=h();c&&(e=b(e,c)),T.create("#forms-table",{data:e,columns:[{key:"title",title:"Title",render:(t,n)=>{const o=document.createElement("span");o.style.cssText="display:inline-flex;align-items:center;gap:.4rem;flex-wrap:wrap;";const s=document.createElement("a");if(s.href=`#/forms/edit/${u(n.slug)}`,s.textContent=t,s.style.fontWeight="600",o.appendChild(s),n.plugin){const a=document.createElement("span");a.className="badge badge-outline",a.textContent=n.plugin,a.title=`Managed by the ${n.plugin} plugin`,a.style.cssText="font-size:0.65rem;padding:1px 6px;color:var(--dm-warning,#d97706);border-color:var(--dm-warning,#d97706);flex-shrink:0;",o.appendChild(a)}return o.outerHTML}},{key:"slug",title:"Slug",render:t=>{const n=document.createElement("code");return n.textContent=t,n.outerHTML}},{key:"fields",title:"Field Count",render:t=>String(t?.length??0)},{key:"submissionCount",title:"Submission Count",render:t=>String(t??0)},{key:"slug",title:"Actions",render:(t,n)=>{const o=document.createElement("div");o.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;";const s=document.createElement("a");s.href=`#/forms/edit/${u(t)}`,s.className="btn btn-sm btn-ghost",s.setAttribute("data-tooltip","Edit"),s.innerHTML='<span data-icon="edit" data-icon-size="14"></span>';const a=document.createElement("a");a.href=`#/forms/${u(t)}/submissions`,a.className="btn btn-sm btn-ghost",a.setAttribute("data-tooltip","Submissions"),a.innerHTML='<span data-icon="inbox" data-icon-size="14"></span>';const i=document.createElement("button");i.className="btn btn-sm btn-ghost js-clone-form",i.dataset.slug=t,i.setAttribute("data-tooltip","Duplicate"),i.innerHTML='<span data-icon="copy" data-icon-size="14"></span>';const l=document.createElement("button");return l.className="btn btn-sm btn-ghost js-delete-form",l.dataset.slug=t,l.dataset.plugin=n.plugin||"",l.setAttribute("data-tooltip","Delete"),l.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',o.appendChild(s),o.appendChild(a),o.appendChild(i),o.appendChild(l),o.outerHTML}}],emptyMessage:'No forms yet. Click "Create Form" to get started.'}),Domma.icons.scan(),document.querySelectorAll("#forms-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})})}
@@ -1 +1 @@
1
- import{api as m}from"../api.js";import{openImageEditor as B}from"../lib/image-editor.js";export const mediaView={templateUrl:"/admin/js/templates/media.html",async onMount(t){const i=c=>{const d=t.find("#media-grid").empty().get(0);if(!c.length){const e=document.createElement("p");e.className="text-muted",e.textContent="No media files yet. Upload one above.",d.appendChild(e);return}c.forEach(e=>{const r=/\.(png|jpe?g|gif|webp|svg)$/i.test(e.name),p=/\.(png|jpe?g|gif|webp|tiff?)$/i.test(e.name),n=document.createElement("div");n.className="media-card";const u=document.createElement("div");if(u.className="media-preview",r){const o=document.createElement("img");o.src=e.url+"?t="+Date.now(),o.alt=e.name,o.className="media-thumb",u.appendChild(o)}else{const o=document.createElement("div");o.className="media-thumb media-thumb--file";const w=document.createElement("span");w.setAttribute("data-icon","file"),o.appendChild(w),u.appendChild(o)}n.appendChild(u);const C=document.createElement("div");C.className="media-info";const l=document.createElement("span");l.className="media-name",l.title="Double-click to rename",l.textContent=e.name;const v=document.createElement("span");v.className="media-size",v.textContent=L(e.size),C.appendChild(l),C.appendChild(v),n.appendChild(C),l.addEventListener("dblclick",()=>D(l,e,i));const h=document.createElement("div");h.className="media-actions";const b=document.createElement("button");b.className="btn btn-sm btn-ghost";const N=document.createElement("span");N.setAttribute("data-icon","copy"),b.appendChild(N),b.addEventListener("click",()=>{navigator.clipboard.writeText(window.location.origin+e.url).then(()=>E.toast("URL copied.",{type:"success"})).catch(()=>E.toast("Copy failed.",{type:"error"}))});const g=document.createElement("button");g.className="btn btn-sm btn-ghost";const k=document.createElement("span");k.setAttribute("data-icon","type"),g.appendChild(k),g.addEventListener("click",()=>D(l,e,i));let s=null;if(p){s=document.createElement("button"),s.className="btn btn-sm btn-ghost";const o=document.createElement("span");o.setAttribute("data-icon","edit"),s.appendChild(o),s.addEventListener("click",async()=>{await B(e)&&i(await m.media.list().catch(()=>[]))})}const y=document.createElement("button");y.className="btn btn-sm btn-danger";const x=document.createElement("span");x.setAttribute("data-icon","trash"),y.appendChild(x),y.addEventListener("click",async()=>{if(await E.confirm(`Delete "${e.name}"?`))try{await m.media.delete(e.name),E.toast("File deleted.",{type:"success"}),i(await m.media.list().catch(()=>[]))}catch{E.toast("Delete failed.",{type:"error"})}}),h.appendChild(b),h.appendChild(g),s&&h.appendChild(s),h.appendChild(y),n.appendChild(h),d.appendChild(n),E.tooltip(b,{content:"Copy URL",position:"top"}),E.tooltip(g,{content:"Rename",position:"top"}),s&&E.tooltip(s,{content:"Edit image",position:"top"}),E.tooltip(y,{content:"Delete file",position:"top"})}),Domma.icons.scan(),Domma.effects.reveal(".media-card",{animation:"fade",stagger:40,duration:350})},f=E.loader(t.get(0),{type:"dots"}),a=await m.media.list().catch(()=>[]);f.destroy(),i(a),Domma.effects.ripple("#upload-btn"),t.find("#upload-btn").on("click",()=>t.find("#file-input").get(0).click()),t.find("#file-input").on("change",async function(){if(!this.files.length)return;const c=new FormData;for(const r of this.files)c.append("file",r);const d=t.find("#upload-btn").get(0),e=d.lastChild;d.disabled=!0,e.textContent=" Uploading\u2026";try{if(!(await fetch("/api/media",{method:"POST",body:c,headers:{Authorization:"Bearer "+(S.get("auth_token")||"")}})).ok)throw new Error("Upload failed");E.toast("File uploaded.",{type:"success"}),i(await m.media.list().catch(()=>[]))}catch{E.toast("Upload failed.",{type:"error"})}finally{d.disabled=!1,e.textContent=" Upload",this.value=""}})}};function D(t,i,f){if(t.querySelector("input"))return;const a=i.name,c=a.lastIndexOf("."),d=c>0?a.slice(0,c):a,e=document.createElement("input");e.type="text",e.className="media-rename-input",e.value=a,e.title="",t.textContent="",t.appendChild(e),e.focus(),e.setSelectionRange(0,d.length);async function r(){const n=e.value.trim();if(!n||n===a)return p();e.disabled=!0;try{await m.media.rename(a,n),E.toast("File renamed.",{type:"success"}),f(await m.media.list().catch(()=>[]))}catch(u){E.toast(u.message||"Rename failed.",{type:"error"}),p()}}function p(){t.textContent=a,t.title="Double-click to rename"}e.addEventListener("keydown",n=>{n.key==="Enter"&&(n.preventDefault(),r()),n.key==="Escape"&&(n.preventDefault(),p())}),e.addEventListener("blur",()=>{setTimeout(()=>{t.contains(e)&&p()},150)})}function L(t){if(!t)return"0 B";const i=1024,f=["B","KB","MB","GB"],a=Math.floor(Math.log(t)/Math.log(i));return`${(t/Math.pow(i,a)).toFixed(1)} ${f[a]}`}
1
+ import{api as m}from"../api.js";import{openImageEditor as B}from"../lib/image-editor.js";export const mediaView={templateUrl:"/admin/js/templates/media.html",async onMount(t){const i=c=>{const s=t.find("#media-grid").empty().get(0);if(!c.length){const e=document.createElement("p");e.className="text-muted",e.textContent="No media files yet. Upload one above.",s.appendChild(e);return}c.forEach(e=>{const r=/\.(png|jpe?g|gif|webp|svg)$/i.test(e.name),p=/\.(png|jpe?g|gif|webp|tiff?)$/i.test(e.name),n=document.createElement("div");n.className="media-card";const u=document.createElement("div");if(u.className="media-preview",r){const o=document.createElement("img");o.src=e.url+"?t="+Date.now(),o.alt=e.name,o.className="media-thumb",u.appendChild(o)}else{const o=document.createElement("div");o.className="media-thumb media-thumb--file";const w=document.createElement("span");w.setAttribute("data-icon","file"),o.appendChild(w),u.appendChild(o)}n.appendChild(u);const C=document.createElement("div");C.className="media-info";const l=document.createElement("span");l.className="media-name",l.title="Double-click to rename",l.textContent=e.name;const v=document.createElement("span");v.className="media-size",v.textContent=L(e.size),C.appendChild(l),C.appendChild(v),n.appendChild(C),l.addEventListener("dblclick",()=>D(l,e,i));const h=document.createElement("div");h.className="media-actions";const b=document.createElement("button");b.className="btn btn-sm btn-ghost";const N=document.createElement("span");N.setAttribute("data-icon","copy"),b.appendChild(N),b.addEventListener("click",()=>{navigator.clipboard.writeText(window.location.origin+e.url).then(()=>E.toast("URL copied.",{type:"success"})).catch(()=>E.toast("Copy failed.",{type:"error"}))});const g=document.createElement("button");g.className="btn btn-sm btn-ghost";const k=document.createElement("span");k.setAttribute("data-icon","type"),g.appendChild(k),g.addEventListener("click",()=>D(l,e,i));let d=null;if(p){d=document.createElement("button"),d.className="btn btn-sm btn-ghost";const o=document.createElement("span");o.setAttribute("data-icon","edit"),d.appendChild(o),d.addEventListener("click",async()=>{await B(e)&&i(await m.media.list().catch(()=>[]))})}const y=document.createElement("button");y.className="btn btn-sm btn-ghost";const x=document.createElement("span");x.setAttribute("data-icon","trash"),y.appendChild(x),y.addEventListener("click",async()=>{if(await E.confirm(`Delete "${e.name}"?`))try{await m.media.delete(e.name),E.toast("File deleted.",{type:"success"}),i(await m.media.list().catch(()=>[]))}catch{E.toast("Delete failed.",{type:"error"})}}),h.appendChild(b),h.appendChild(g),d&&h.appendChild(d),h.appendChild(y),n.appendChild(h),s.appendChild(n),E.tooltip(b,{content:"Copy URL",position:"top"}),E.tooltip(g,{content:"Rename",position:"top"}),d&&E.tooltip(d,{content:"Edit image",position:"top"}),E.tooltip(y,{content:"Delete file",position:"top"})}),Domma.icons.scan(),Domma.effects.reveal(".media-card",{animation:"fade",stagger:40,duration:350})},f=E.loader(t.get(0),{type:"dots"}),a=await m.media.list().catch(()=>[]);f.destroy(),i(a),Domma.effects.ripple("#upload-btn"),t.find("#upload-btn").on("click",()=>t.find("#file-input").get(0).click()),t.find("#file-input").on("change",async function(){if(!this.files.length)return;const c=new FormData;for(const r of this.files)c.append("file",r);const s=t.find("#upload-btn").get(0),e=s.lastChild;s.disabled=!0,e.textContent=" Uploading\u2026";try{if(!(await fetch("/api/media",{method:"POST",body:c,headers:{Authorization:"Bearer "+(S.get("auth_token")||"")}})).ok)throw new Error("Upload failed");E.toast("File uploaded.",{type:"success"}),i(await m.media.list().catch(()=>[]))}catch{E.toast("Upload failed.",{type:"error"})}finally{s.disabled=!1,e.textContent=" Upload",this.value=""}})}};function D(t,i,f){if(t.querySelector("input"))return;const a=i.name,c=a.lastIndexOf("."),s=c>0?a.slice(0,c):a,e=document.createElement("input");e.type="text",e.className="media-rename-input",e.value=a,e.title="",t.textContent="",t.appendChild(e),e.focus(),e.setSelectionRange(0,s.length);async function r(){const n=e.value.trim();if(!n||n===a)return p();e.disabled=!0;try{await m.media.rename(a,n),E.toast("File renamed.",{type:"success"}),f(await m.media.list().catch(()=>[]))}catch(u){E.toast(u.message||"Rename failed.",{type:"error"}),p()}}function p(){t.textContent=a,t.title="Double-click to rename"}e.addEventListener("keydown",n=>{n.key==="Enter"&&(n.preventDefault(),r()),n.key==="Escape"&&(n.preventDefault(),p())}),e.addEventListener("blur",()=>{setTimeout(()=>{t.contains(e)&&p()},150)})}function L(t){if(!t)return"0 B";const i=1024,f=["B","KB","MB","GB"],a=Math.floor(Math.log(t)/Math.log(i));return`${(t/Math.pow(i,a)).toFixed(1)} ${f[a]}`}
@@ -8,7 +8,7 @@ import{api as k}from"../api.js";import{colourToCss as U}from"/public/js/menu-dec
8
8
  <button class="btn btn-sm btn-ghost me-indent" data-tooltip="Indent"><span data-icon="chevron-right"></span></button>
9
9
  <button class="btn btn-sm btn-ghost me-up" data-tooltip="Move up"><span data-icon="arrow-up"></span></button>
10
10
  <button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
11
- <button class="btn btn-sm btn-danger me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
11
+ <button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
12
12
  </div>
13
13
  `);continue}const y=i.badge&&(i.badge.text||i.badge.countFrom)?`<span class="dm-menu-badge">${O(String(i.badge.text||"#"))}</span>`:"",w=U(i.colour),x=w?` style="color:${O(w)}"`:"";t.append(`
14
14
  <div class="menu-tree-row menu-tree-row--slim${i.hidden?" menu-tree-row--hidden":""}" data-id="${i.id}" style="margin-left:${b*28}px">
@@ -23,6 +23,6 @@ import{api as k}from"../api.js";import{colourToCss as U}from"/public/js/menu-dec
23
23
  <button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
24
24
  <button class="btn btn-sm btn-ghost me-hidden ${i.hidden?"active":""}" data-tooltip="${i.hidden?"Show":"Hide"}"><span data-icon="eye-off"></span></button>
25
25
  <button class="btn btn-sm btn-secondary me-edit" data-tooltip="Edit"><span data-icon="edit"></span></button>
26
- <button class="btn btn-sm btn-danger me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
26
+ <button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
27
27
  </div>
28
28
  `),e(i.id)}}e(null),Domma.icons.scan(t.get(0)),t.get(0).querySelectorAll("[data-tooltip]").forEach(n=>{E.tooltip(n,{content:n.getAttribute("data-tooltip"),position:"top"})})}function S(t,r){t.find(".menu-tree-row").each(function(){const l=$(this).data("id"),e=r.find(x=>x.id===l);if(!e||e.type==="separator")return;const n=$(this).find(".me-text");n.length&&(e.text=n.val());const i=$(this).find(".me-url");i.length&&(e.url=i.val());const b=$(this).find(".me-icon");b.length&&(e.icon=b.val());const y=$(this).find(".me-vis");y.length&&(e.visibility=y.val());const w=$(this).find(".me-perm");w.length&&(e.permission=w.val())})}function it(t,r){const l={};for(const e of r){const n=t.querySelector(`[name="${e}"]`);n&&(l[e]=n.type==="checkbox"?n.checked:n.value)}return l}function g(t,r={},...l){const e=document.createElement(t);Object.assign(e,r);for(const n of l)n!=null&&e.append(n);return e}function I(t,r,l){const e=g("label",{className:"form-field"},g("span",{className:"form-label",textContent:t}),r);return l&&e.append(g("small",{className:"form-hint",textContent:l,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),e}function Q(t,r){const l=[["","\u2014 Everyone \u2014"],["public","Public \u2014 anyone, incl. logged-out"],...(r||[]).map(n=>[n.name,`${n.label||n.name} and above`]),["private","Private \u2014 super-admin only"]];t&&!l.some(([n])=>n===t)&&l.push([t,`${t} (custom)`]);const e=P(l,t||"");return e.name="visibility",e}function P(t,r){const l=g("select",{className:"form-select"});return t.forEach(([e,n])=>l.add(new Option(n,e))),l.value=r??"",l}function W(t,r){const l=typeof r=="string"&&r.startsWith("#"),e=P([["","\u2014 none \u2014"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],l?"__custom__":r||""),n=g("input",{type:"color",className:"form-input me-colour-hex",value:l?r:"#000000"});n.style.display=l?"":"none",e.addEventListener("change",()=>{n.style.display=e.value==="__custom__"?"":"none"});const i=I(t,e);return i.append(n),i._read=()=>e.value==="__custom__"?n.value:e.value,i}function Y(t,r){const l=g("input",{className:"form-input me-icon",name:"icon",value:r||""}),e=g("span",{className:"me-icon-preview"});r&&e.setAttribute("data-icon",r);const n=g("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),i=b=>{b?e.setAttribute("data-icon",b):e.removeAttribute("data-icon"),Domma.icons.scan(e.parentNode||e)};return n.addEventListener("click",()=>V(b=>{l.value=b,i(b)})),l.addEventListener("input",()=>i(l.value.trim())),I(t,g("div",{className:"me-icon-field"},l,n,e))}function M(...t){return g("div",{className:"me-field-grid"},...t)}async function Z(t,r,l,e,n){const i=m=>g("h4",{className:"me-form-section",textContent:m}),b=I("Label",g("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),y=I("URL",g("input",{className:"form-input",name:"url",value:t.url||""}),"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items."),w=Y("Icon",t.icon||""),x=I("Visibility",Q(t.visibility||"",e),"Who can see this item, by role seniority. Picking a role shows it to that role and anyone more senior. Blank = everyone."),z=P([["","\u2014 no permission gate \u2014"],...(r||[]).map(m=>[m.key,m.label||m.key])],t.permission||"");z.name="permission";const T=I("Permission",z,"Hide unless the viewer holds this specific permission. Independent of Visibility \u2014 both must pass."),B=I("Badge text",g("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),h=P([["","\u2014 none \u2014"],...(l||[]).map(m=>[m.slug,m.title||m.slug])],t.badge?.countFrom||"");h.name="badgeCount";const o=I("Badge count from",h,"Show a live count of this collection\u2019s entries as the badge."),u=W("Badge colour",t.badge?.variant||""),_=P([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");_.name="renderAs";const j=I("Render as",_,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),C=I("Pill style",P([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));C.querySelector("select").name="pillStyle";const L=W("Pill colour",t.pill?.variant||""),F=g("div",{className:"me-pill-wrap"},M(C,L));F.style.display=t.pill?"":"none",_.addEventListener("change",()=>{F.style.display=_.value==="pill"?"":"none"});const s=W("Text colour",t.colour||""),a=g("button",{className:"btn btn-primary",textContent:"Apply"}),d=g("div",{className:"me-item-form"},i("Link"),b,y,w,i("Access"),M(x,T),i("Badge"),M(B,u),o,i("Appearance"),j,F,s,a),p=E.slideover({title:"Edit menu item",size:"lg",position:"right"});p.element.appendChild(d),p.open();const f=p.element.closest(".dm-slideover")||p.element.querySelector(".dm-slideover")||p.element;f.style.setProperty("width",K+"px","important"),f.style.setProperty("max-width","95vw","important"),Domma.icons.scan(d);const c=m=>d.querySelector(`[name="${m}"]`)?.value||"";a.addEventListener("click",()=>{t.text=c("text"),t.url=c("url"),t.icon=c("icon"),t.visibility=c("visibility"),t.permission=c("permission");const m=c("badgeText"),A=c("badgeCount"),R=u._read();if(t.badge=m||A?{...m&&{text:m},...A&&{countFrom:A},...R&&{variant:R}}:null,c("renderAs")==="pill"){const D=L._read();t.pill={style:c("pillStyle")||"filled",...D&&{variant:D}}}else t.pill=null;t.colour=s._read(),p.close(),n()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){const r=location.hash.match(/^#\/menus\/edit\/(.+)$/),l=r?decodeURIComponent(r[1]):null,e=!l,n=E.loader(t.get(0),{type:"dots"});let i,b,y,w,x;try{if(e)i={slug:"",name:"",description:"",items:[]};else try{i=await k.menus.get(l)}catch(s){E.toast(`Failed to load menu: ${s.message||s}`,{type:"error"}),location.hash="#/menus";return}b=await k.projects.list().catch(()=>[]),y=await H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),w=await k.collections.list().catch(()=>[]),x=await k.get("/collections/roles/entries?limit=100").catch(()=>null)}finally{n.destroy()}const T=(Array.isArray(x)?x:x?.entries||[]).map(s=>({name:s.data?.name,label:s.data?.label,level:s.data?.level??99})).filter(s=>s.name).sort((s,a)=>a.level-s.level),B='<option value="">\u2014 no permission gate \u2014</option>',h=s=>B+(y.resources||[]).map(a=>{const d=(s||"")===a.key?" selected":"";return`<option value="${a.key}"${d}>${a.label||a.key}</option>`}).join("");t.find("#me-title").text(e?"New menu":`Edit "${i.slug}"`);let o=q(i.items);const u=t.find("#me-items-list");v(u,o,h),t.find("#me-add-item").on("click",()=>{S(u,o),o.push({id:N(),parentId:null,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}),v(u,o,h)}),t.find("#me-add-separator").on("click",()=>{S(u,o),o.push({id:N(),parentId:null,type:"separator"}),v(u,o,h)});function _(s){return function(){S(u,o);const a=$(this).closest(".menu-tree-row").data("id"),d=o.findIndex(c=>c.id===a);if(d===-1)return;const p=o[d],f=s==="separator"?{id:N(),parentId:p.parentId,type:"separator"}:{id:N(),parentId:p.parentId,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1};o.splice(d+1,0,f),v(u,o,h)}}t.off("click",".me-ins-item").on("click",".me-ins-item",_("item")),t.off("click",".me-ins-sep").on("click",".me-ins-sep",_("separator")),t.off("click",".me-remove").on("click",".me-remove",function(){const s=$(this).closest(".menu-tree-row").data("id");o=o.filter(a=>a.id!==s&&a.parentId!==s),v(u,o,h)}),t.off("click",".me-indent").on("click",".me-indent",function(){S(u,o);const s=$(this).closest(".menu-tree-row").data("id"),a=o.find(f=>f.id===s);if(!a)return;const d=o.filter(f=>f.parentId===a.parentId),p=d.findIndex(f=>f.id===s);p<=0||d[p-1].type!=="separator"&&(a.parentId=d[p-1].id,v(u,o,h))}),t.off("click",".me-outdent").on("click",".me-outdent",function(){S(u,o);const s=$(this).closest(".menu-tree-row").data("id"),a=o.find(p=>p.id===s);if(!a||a.parentId==null)return;const d=o.find(p=>p.id===a.parentId);a.parentId=d?d.parentId:null,v(u,o,h)}),t.off("click",".me-up").on("click",".me-up",function(){S(u,o);const s=$(this).closest(".menu-tree-row").data("id"),a=o.filter(c=>c.parentId===o.find(m=>m.id===s)?.parentId),d=a.findIndex(c=>c.id===s);if(d<=0)return;const p=o.indexOf(a[d]),f=o.indexOf(a[d-1]);[o[p],o[f]]=[o[f],o[p]],v(u,o,h)}),t.off("click",".me-down").on("click",".me-down",function(){S(u,o);const s=$(this).closest(".menu-tree-row").data("id"),a=o.filter(c=>c.parentId===o.find(m=>m.id===s)?.parentId),d=a.findIndex(c=>c.id===s);if(d===-1||d>=a.length-1)return;const p=o.indexOf(a[d]),f=o.indexOf(a[d+1]);[o[p],o[f]]=[o[f],o[p]],v(u,o,h)}),t.off("click",".me-hidden").on("click",".me-hidden",function(){S(u,o);const s=$(this).closest(".menu-tree-row").data("id"),a=o.find(d=>d.id===s);a&&(a.hidden=!a.hidden),v(u,o,h)}),t.off("click",".me-edit").on("click",".me-edit",function(){const s=$(this).closest(".menu-tree-row").data("id"),a=o.find(d=>d.id===s);!a||a.type==="separator"||Z(a,y.resources||[],w,T,()=>v(u,o,h))}),E.tabs(t.find("#me-tabs").get(0)),t.find("#me-variant").val(i.variant||""),t.find("#me-position").val(i.position||"");const j=t.find("#me-appearOnHover").get(0);j&&(j.checked=i.appearOnHover===!0),t.find("#me-fontFamily").val(i.style?.fontFamily||""),t.find("#me-fontSize").val(i.style?.fontSize||""),t.find("#me-fontWeight").val(i.style?.fontWeight||""),t.find("#me-letterSpacing").val(i.style?.letterSpacing||""),t.find("#me-iconSize").val(i.style?.iconSize||"");const{getProjectFromHash:C}=await import("../lib/project-context.js"),L=e?C():null,F=t.find("#me-project");F.html('<option value="">\u2014 none \u2014</option>'+b.map(s=>`<option value="${O(s.slug)}">${O(s.name||s.slug)}</option>`).join("")),t.find("#me-slug").val(i.slug||""),t.find("#me-name").val(i.name||""),t.find("#me-description").val(i.description||""),F.val(i.meta?.project||L||""),Domma.icons.scan(t.get(0)),t.find("#me-save").on("click",async()=>{S(u,o);const s={variant:t.find("#me-variant").val(),position:t.find("#me-position").val(),appearOnHover:!!(t.find("#me-appearOnHover").get(0)||{}).checked,fontFamily:t.find("#me-fontFamily").val(),fontSize:t.find("#me-fontSize").val(),fontWeight:t.find("#me-fontWeight").val(),letterSpacing:t.find("#me-letterSpacing").val(),iconSize:t.find("#me-iconSize").val()},a={slug:t.find("#me-slug").val().trim(),name:t.find("#me-name").val().trim(),description:t.find("#me-description").val().trim(),project:t.find("#me-project").val()},d=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"],p=Object.fromEntries(Object.entries(s).filter(([c,m])=>d.includes(c)&&m)),f={slug:a.slug,name:a.name,description:a.description,...s.variant&&{variant:s.variant},...s.position&&{position:s.position},...s.appearOnHover&&{appearOnHover:!0},...Object.keys(p).length&&{style:p},items:X(o),meta:{...i.meta||{},project:a.project||null}};try{if(e)await k.menus.create(f);else if(f.slug!==i.slug){await k.menus.create(f);const c=await k.menuLocations.get();let m=!1;for(const[A,R]of Object.entries(c))R===i.slug&&(c[A]=f.slug,m=!0);m&&await k.menuLocations.save(c),await k.menus.remove(i.slug)}else await k.menus.update(i.slug,f);E.toast("Saved.",{type:"success"}),location.hash="#/menus/edit/"+encodeURIComponent(f.slug)}catch(c){E.toast(`Save failed: ${c.message||c}`,{type:"error"})}})}};
@@ -1,5 +1,5 @@
1
- import{api as o}from"../api.js";import{filterByProject as b,getProjectFromHash as y}from"../lib/project-context.js";import{keepTableIconsAlive as f}from"../lib/table-icons.js";function l(n){return String(n).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}export const menusView={templateUrl:"/admin/js/templates/menus.html",async onMount(n){const p=E.loader(n.get(0),{type:"dots"});let c,r;try{[c,r]=await Promise.all([o.menus.list().catch(()=>[]),o.menuLocations.get().catch(()=>({}))])}finally{p.destroy()}const d=y(),m=d?b(c,d):c,s={};for(const[e,t]of Object.entries(r||{}))t&&(s[t]||(s[t]=[]),s[t].push(e));T.create("#menus-table",{data:m,emptyMessage:'No menus yet. Click "New menu" to create one.',columns:[{key:"slug",title:"Slug",sortable:!0,render:e=>`<code>${l(e)}</code>`},{key:"name",title:"Name",sortable:!0,render:(e,t)=>`<a href="#/menus/edit/${encodeURIComponent(t.slug)}" style="font-weight:600;">${l(e||t.slug)}</a>`},{key:"itemsCount",title:"Items",render:(e,t)=>String(t.itemsCount??(Array.isArray(t.items)?t.items.length:0))},{key:"mapped",title:"Mapped",render:(e,t)=>{const a=s[t.slug]||[];return a.length?a.map(i=>`<span class="badge badge-info">${l(i)}</span>`).join(" "):'<span class="text-muted">\u2014</span>'}},{key:"bundled",title:"Bundled",render:(e,t)=>t.meta?.bundled?'<span class="badge badge-secondary">bundled</span>':""},{key:"_actions",title:"Actions",render:(e,t)=>{const a=(s[t.slug]||[]).length>0,i=a?'data-tooltip="Unmap this menu before deleting"':"",g=a?"disabled":"",u=l(t.slug);return`
1
+ import{api as o}from"../api.js";import{filterByProject as b,getProjectFromHash as y}from"../lib/project-context.js";import{keepTableIconsAlive as f}from"../lib/table-icons.js";function l(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}export const menusView={templateUrl:"/admin/js/templates/menus.html",async onMount(s){const p=E.loader(s.get(0),{type:"dots"});let c,r;try{[c,r]=await Promise.all([o.menus.list().catch(()=>[]),o.menuLocations.get().catch(()=>({}))])}finally{p.destroy()}const d=y(),m=d?b(c,d):c,n={};for(const[e,t]of Object.entries(r||{}))t&&(n[t]||(n[t]=[]),n[t].push(e));T.create("#menus-table",{data:m,emptyMessage:'No menus yet. Click "New menu" to create one.',columns:[{key:"slug",title:"Slug",sortable:!0,render:e=>`<code>${l(e)}</code>`},{key:"name",title:"Name",sortable:!0,render:(e,t)=>`<a href="#/menus/edit/${encodeURIComponent(t.slug)}" style="font-weight:600;">${l(e||t.slug)}</a>`},{key:"itemsCount",title:"Items",render:(e,t)=>String(t.itemsCount??(Array.isArray(t.items)?t.items.length:0))},{key:"mapped",title:"Mapped",render:(e,t)=>{const a=n[t.slug]||[];return a.length?a.map(i=>`<span class="badge badge-info">${l(i)}</span>`).join(" "):'<span class="text-muted">\u2014</span>'}},{key:"bundled",title:"Bundled",render:(e,t)=>t.meta?.bundled?'<span class="badge badge-secondary">bundled</span>':""},{key:"_actions",title:"Actions",render:(e,t)=>{const a=(n[t.slug]||[]).length>0,i=a?'data-tooltip="Unmap this menu before deleting"':"",g=a?"disabled":"",u=l(t.slug);return`
2
2
  <a class="btn btn-sm btn-ghost" href="#/menus/edit/${encodeURIComponent(t.slug)}" data-tooltip="Edit"><span data-icon="edit" data-icon-size="14"></span></a>
3
3
  <button class="btn btn-sm btn-ghost btn-duplicate" data-slug="${u}" data-tooltip="Duplicate"><span data-icon="copy" data-icon-size="14"></span></button>
4
- <button class="btn btn-sm btn-danger btn-delete" data-slug="${u}" ${g} ${i}><span data-icon="trash" data-icon-size="14"></span></button>
5
- `}}]}),Domma.icons.scan("#menus-table"),f(n.get(0)),document.querySelectorAll("#menus-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})}),n.off("click",".btn-duplicate").on("click",".btn-duplicate",async function(){const e=$(this).data("slug");try{const t=await o.menus.duplicate(e);E.toast(`Duplicated as "${t.slug}".`,{type:"success"}),location.reload()}catch(t){E.toast(`Duplicate failed: ${t.message||t}`,{type:"error"})}}),n.off("click",".btn-delete").on("click",".btn-delete",async function(){if(this.disabled)return;const e=$(this).data("slug");if(await E.confirm(`Delete menu "${e}"? This cannot be undone.`))try{await o.menus.remove(e),E.toast("Menu deleted.",{type:"success"}),location.reload()}catch(a){E.toast(`Delete failed: ${a.message||a}`,{type:"error"})}})}};
4
+ <button class="btn btn-sm btn-ghost btn-delete" data-slug="${u}" ${g} ${i}><span data-icon="trash" data-icon-size="14"></span></button>
5
+ `}}]}),Domma.icons.scan("#menus-table"),f(s.get(0)),document.querySelectorAll("#menus-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})}),s.off("click",".btn-duplicate").on("click",".btn-duplicate",async function(){const e=$(this).data("slug");try{const t=await o.menus.duplicate(e);E.toast(`Duplicated as "${t.slug}".`,{type:"success"}),location.reload()}catch(t){E.toast(`Duplicate failed: ${t.message||t}`,{type:"error"})}}),s.off("click",".btn-delete").on("click",".btn-delete",async function(){if(this.disabled)return;const e=$(this).data("slug");if(await E.confirm(`Delete menu "${e}"? This cannot be undone.`))try{await o.menus.remove(e),E.toast("Menu deleted.",{type:"success"}),location.reload()}catch(a){E.toast(`Delete failed: ${a.message||a}`,{type:"error"})}})}};
@@ -8,7 +8,7 @@ import{api as x}from"../api.js";let k=1;const v=n=>String(n||"").replace(/&/g,"&
8
8
  <span class="nav-col-action">
9
9
  <button class="btn btn-sm btn-ghost btn-toggle-bundled${t.bundled?" active":""}" data-id="${t._id}" data-bundled="${!!t.bundled}" data-tooltip="${t.bundled?"Unbundle":"Bundle (seed on install)"}"><span data-icon="package"></span></button>
10
10
  <button class="btn btn-sm btn-ghost btn-toggle-hidden${c?" active":""}" data-id="${t._id}" data-hidden="${c}" data-tooltip="${c?"Show":"Hide"}"><span data-icon="eye-off"></span></button>
11
- <button class="btn btn-sm btn-danger btn-remove-item" data-id="${t._id}" data-tooltip="Remove"><span data-icon="trash"></span></button>
11
+ <button class="btn btn-sm btn-ghost btn-remove-item" data-id="${t._id}" data-tooltip="Remove"><span data-icon="trash"></span></button>
12
12
  </span>
13
13
  </div>
14
14
  `)}),Domma.icons.scan("#nav-items-list"),document.querySelectorAll("#nav-items-list [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})})},m=()=>{const d=n.find("#footer-links-list").empty();e.forEach((i,t)=>{const s=i.hidden===!0;d.append(`
@@ -17,7 +17,7 @@ import{api as x}from"../api.js";let k=1;const v=n=>String(n||"").replace(/&/g,"&
17
17
  <input type="text" class="form-input footer-link-url nav-col-main" value="${v(i.url)}" placeholder="/url">
18
18
  <span class="nav-col-action">
19
19
  <button class="btn btn-sm btn-ghost btn-toggle-hidden${s?" active":""}" data-idx="${t}" data-hidden="${s}" data-tooltip="${s?"Show":"Hide"}"><span data-icon="eye-off"></span></button>
20
- <button class="btn btn-sm btn-danger btn-remove-footer" data-idx="${t}" data-tooltip="Remove"><span data-icon="trash"></span></button>
20
+ <button class="btn btn-sm btn-ghost btn-remove-footer" data-idx="${t}" data-tooltip="Remove"><span data-icon="trash"></span></button>
21
21
  </span>
22
22
  </div>
23
23
  `)}),Domma.icons.scan("#footer-links-list"),document.querySelectorAll("#footer-links-list [data-tooltip]").forEach(i=>{E.tooltip(i,{content:i.getAttribute("data-tooltip"),position:"top"})})},h=()=>{e=[],n.find("#footer-links-list .nav-item-row").each(function(){const d=$(this).find(".btn-toggle-hidden").attr("data-hidden")==="true",i={text:$(this).find(".footer-link-text").val().trim(),url:$(this).find(".footer-link-url").val().trim()};d&&(i.hidden=!0),e.push(i)})};n.find("#add-footer-link").on("click",()=>{h(),e.push({text:"",url:"",hidden:!1}),m()}),n.off("click",".btn-remove-footer").on("click",".btn-remove-footer",function(){h();const d=parseInt($(this).data("idx"),10);e.splice(d,1),m()}),n.find("#field-brand-text").val(a.brand?.text||""),n.find("#field-brand-url").val(a.brand?.url||"/"),n.find("#field-brand-icon").val(a.brand?.icon||""),n.find("#field-nav-variant").val(a.variant||"dark"),n.find("#field-brand-tagline").val(a.brand?.tagline||""),n.find("#field-brand-logo").val(a.brand?.logo||""),n.find("#field-nav-position").val(a.position||"sticky"),n.find("#field-brand-size").val(a.brand?.size||"md"),n.find("#field-nav-font-family").val(a.style?.fontFamily||""),n.find("#field-nav-font-size").val(a.style?.fontSize||""),n.find("#field-nav-font-weight").val(a.style?.fontWeight||""),n.find("#field-nav-letter-spacing").val(a.style?.letterSpacing||""),n.find("#field-nav-icon-size").val(a.style?.iconSize||""),n.find("#field-admin-brand-title").val(l.adminBrand?.title||""),n.find("#field-admin-brand-icon").val(l.adminBrand?.icon||""),n.find("#field-admin-brand-logo").val(l.adminBrand?.logo||""),p(),m(),E.tabs(n.find("#nav-tabs").get(0)),n.find("#add-nav-item").on("click",()=>{u(),o.push({_id:k++,text:"",url:"",icon:"",hidden:!1,parentId:null}),p()}),n.off("click",".btn-remove-item").on("click",".btn-remove-item",function(){u();const d=parseInt($(this).data("id"),10);o=o.filter(i=>i._id!==d&&i.parentId!==d),p()}),n.off("click","#nav-items-list .btn-toggle-bundled").on("click","#nav-items-list .btn-toggle-bundled",function(d){d.stopPropagation(),u();const i=parseInt($(this).data("id"),10),t=o.find(s=>s._id===i);t&&(t.bundled=!t.bundled),p()}),n.off("click","#nav-items-list .btn-toggle-hidden").on("click","#nav-items-list .btn-toggle-hidden",function(d){d.stopPropagation(),u();const i=parseInt($(this).data("id"),10),t=o.find(s=>s._id===i);t&&(t.hidden=!t.hidden),p()}),n.off("click","#footer-links-list .btn-toggle-hidden").on("click","#footer-links-list .btn-toggle-hidden",function(d){d.stopPropagation(),h();const i=parseInt($(this).data("idx"),10);e[i]&&(e[i].hidden=!e[i].hidden),m()}),n.off("change",".item-parent").on("change",".item-parent",function(){u(),p()}),n.find("#save-nav-btn").on("click",async()=>{u(),h();const d=B(o.map(r=>({...r,text:r.text.trim(),url:r.url.trim(),icon:r.icon.trim()}))).filter(r=>r.text||r.url),i=n.find("#field-brand-icon").val().trim(),t=n.find("#field-brand-tagline").val().trim(),s=n.find("#field-brand-logo").val().trim(),c=n.find("#field-brand-size").val(),g={brand:{text:n.find("#field-brand-text").val().trim(),url:n.find("#field-brand-url").val().trim()||"/",...s&&{logo:s},...i&&!s&&{icon:i},...t&&{tagline:t},...c!=="md"&&{size:c}},items:d,variant:n.find("#field-nav-variant").val(),position:n.find("#field-nav-position").val()||"sticky",style:{...n.find("#field-nav-font-family").val()&&{fontFamily:n.find("#field-nav-font-family").val()},...n.find("#field-nav-font-size").val()&&{fontSize:n.find("#field-nav-font-size").val()},...n.find("#field-nav-font-weight").val()&&{fontWeight:n.find("#field-nav-font-weight").val()},...n.find("#field-nav-letter-spacing").val()&&{letterSpacing:n.find("#field-nav-letter-spacing").val()},...n.find("#field-nav-icon-size").val()&&{iconSize:n.find("#field-nav-icon-size").val()}}},f=e.filter(r=>r.text||r.url);try{await x.navigation.save(g),a=g,o=S(a.items),p(),E.toast("Navigation saved.",{type:"success"})}catch(r){console.error("[navigation] save failed:",r),E.toast("Failed to save navigation.",{type:"error"});return}const y=n.find("#field-admin-brand-logo").val().trim(),I=n.find("#field-admin-brand-icon").val().trim(),_=n.find("#field-admin-brand-title").val().trim(),b={..._&&{title:_},...y&&{logo:y},...!y&&I&&{icon:I}};try{await x.settings.save({adminBrand:b,footer:{links:f}}),l={...l,adminBrand:b},e=f,m();const r=b.title||"CMS Admin",A=b.icon||"layout",w=b.logo||"",z=$("#admin-topbar .topbar-brand");w?z.html(`<img src="${w}" class="topbar-brand-logo" alt="${r}"><span class="topbar-brand-text">${r}</span>`):z.html(`<span data-icon="${A}"></span><span class="topbar-brand-text">${r}</span>`),Domma.icons.scan("#admin-topbar .topbar-brand")}catch(r){console.error("[navigation] admin brand / footer save failed:",r),E.toast(`Admin brand save failed: ${r.message||r}`,{type:"error"})}})}};
@@ -1,8 +1,8 @@
1
- import{api as p}from"../api.js";import{filterByProject as m,getProjectFromHash as f}from"../lib/project-context.js";export const pagesView={templateUrl:"/admin/js/templates/pages.html",async onMount(n){const g=E.loader(n.get(0),{type:"dots"});let o=await p.pages.list().catch(()=>[]);g.destroy();const c=f();c&&(o=m(o,c));const r=s=>{T.create("#pages-table",{data:s,columns:[{key:"title",title:"Title",render:(t,e)=>{const a=e.bundled?'<span class="badge badge-outline" style="font-size:0.65rem;padding:1px 6px;color:var(--dm-info,#2563eb);border-color:var(--dm-info,#2563eb);margin-left:.35rem;">Bundled</span>':"",i=e.plugin?`<span class="badge badge-outline" style="font-size:0.65rem;padding:1px 6px;color:var(--dm-warning,#d97706);border-color:var(--dm-warning,#d97706);margin-left:.35rem;" title="Managed by the ${e.plugin} plugin">${e.plugin}</span>`:"";return`<a href="#/pages/edit${e.urlPath}" style="font-weight:600;">${t}</a>${a}${i}`}},{key:"urlPath",title:"URL",render:t=>`<code>${t}</code>`},{key:"layout",title:"Layout"},{key:"status",title:"Status",render:t=>`<span class="badge badge-${t==="published"?"success":"warning"}">${t}</span>`},{key:"tags",title:"Tags",render:t=>Array.isArray(t)&&t.length?t.map(e=>`<span class="badge badge-info badge-pill badge-sm">${e}</span>`).join(" "):"\u2014"},{key:"versionCount",title:"Revisions",render:t=>Number.isFinite(t)&&t>0?`<span class="badge badge-secondary badge-pill badge-sm">${t}</span>`:'<span style="color:var(--dm-text-muted);">0</span>'},{key:"updatedAt",title:"Updated",render:t=>t?D(t).format("DD MMM YYYY"):"\u2014"},{key:"actions",title:"Actions",render:(t,e)=>`
1
+ import{api as p}from"../api.js";import{filterByProject as m,getProjectFromHash as f}from"../lib/project-context.js";export const pagesView={templateUrl:"/admin/js/templates/pages.html",async onMount(n){const g=E.loader(n.get(0),{type:"dots"});let i=await p.pages.list().catch(()=>[]);g.destroy();const c=f();c&&(i=m(i,c));const r=s=>{T.create("#pages-table",{data:s,columns:[{key:"title",title:"Title",render:(t,e)=>{const a=e.bundled?'<span class="badge badge-outline" style="font-size:0.65rem;padding:1px 6px;color:var(--dm-info,#2563eb);border-color:var(--dm-info,#2563eb);margin-left:.35rem;">Bundled</span>':"",o=e.plugin?`<span class="badge badge-outline" style="font-size:0.65rem;padding:1px 6px;color:var(--dm-warning,#d97706);border-color:var(--dm-warning,#d97706);margin-left:.35rem;" title="Managed by the ${e.plugin} plugin">${e.plugin}</span>`:"";return`<a href="#/pages/edit${e.urlPath}" style="font-weight:600;">${t}</a>${a}${o}`}},{key:"urlPath",title:"URL",render:t=>`<code>${t}</code>`},{key:"layout",title:"Layout"},{key:"status",title:"Status",render:t=>`<span class="badge badge-${t==="published"?"success":"warning"}">${t}</span>`},{key:"tags",title:"Tags",render:t=>Array.isArray(t)&&t.length?t.map(e=>`<span class="badge badge-info badge-pill badge-sm">${e}</span>`).join(" "):"\u2014"},{key:"versionCount",title:"Revisions",render:t=>Number.isFinite(t)&&t>0?`<span class="badge badge-secondary badge-pill badge-sm">${t}</span>`:'<span style="color:var(--dm-text-muted);">0</span>'},{key:"updatedAt",title:"Updated",render:t=>t?D(t).format("DD MMM YYYY"):"\u2014"},{key:"actions",title:"Actions",render:(t,e)=>`
2
2
  <a href="#/pages/edit${e.urlPath}" class="btn btn-sm btn-ghost" data-tooltip="Edit"><span data-icon="edit"></span></a>
3
3
  <a href="${e.urlPath}" target="_blank" class="btn btn-sm btn-ghost" data-tooltip="View"><span data-icon="external-link"></span></a>
4
4
  <button class="btn btn-sm btn-ghost btn-prune" data-path="${e.urlPath}" data-count="${e.versionCount||0}" data-tooltip="Prune revisions"${(e.versionCount||0)===0?" disabled":""}><span data-icon="archive"></span></button>
5
- <button class="btn btn-sm btn-danger btn-delete" data-path="${e.urlPath}" data-plugin="${e.plugin||""}" data-tooltip="Delete"><span data-icon="trash"></span></button>
6
- `}],emptyMessage:'No pages found. <a href="#/pages/new">Create one</a>.'}),Domma.icons.scan(),document.querySelectorAll("#pages-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),Domma.effects.reveal(".card",{animation:"fade",duration:350})};r(o);const b=s=>{const t=n.find("#pages-tree").empty().get(0);if(!s.length){t.textContent="No pages found.";return}const e=s.map(a=>{const i=a.urlPath.split("/").filter(Boolean),l=i.length>1?"/"+i.slice(0,-1).join("/"):null,d=l&&s.some(h=>h.urlPath===l);return{id:a.urlPath,parent_id:d?l:null,name:a.title||a.urlPath,icon:a.status==="published"?"check-circle":"file-text"}});E.treeView(t,{data:e,idKey:"id",parentKey:"parent_id",labelKey:"name",iconKey:"icon",expandedByDefault:!0,onSelect:a=>R.navigate(`/pages/edit${a}`)}),Domma.icons.scan(t)};n.find("#view-table-btn").on("click",function(){n.find("#pages-table").show(),n.find("#pages-tree").hide(),$(this).addClass("btn-primary").removeClass("btn-ghost"),n.find("#view-tree-btn").addClass("btn-ghost").removeClass("btn-primary")}),n.find("#view-tree-btn").on("click",function(){n.find("#pages-table").hide(),n.find("#pages-tree").show(),$(this).addClass("btn-primary").removeClass("btn-ghost"),n.find("#view-table-btn").addClass("btn-ghost").removeClass("btn-primary"),b(o)}),n.find("#view-table-btn, #view-tree-btn").each(function(){E.tooltip(this,{content:this.getAttribute("data-tooltip"),position:"top"})});const u=()=>{const s=n.find("#status-filter").val(),t=n.find("#pages-search").val().toLowerCase().trim(),e=o.filter(a=>!(s&&a.status!==s||t&&!`${a.title} ${a.urlPath} ${(a.tags||[]).join(" ")}`.toLowerCase().includes(t)));r(e)};n.find("#status-filter").off("change").on("change",u),n.find("#pages-search").get(0).addEventListener("input",u),n.off("click",".btn-delete").on("click",".btn-delete",async function(){const s=$(this).data("path"),t=$(this).data("plugin"),e=t?`This page is managed by the <strong>${t}</strong> plugin. Deleting it may cause the plugin to malfunction. Continue?`:`Delete page at <strong>${s}</strong>? This cannot be undone.`;if(await E.confirm(e))try{await p.pages.delete(s),E.toast("Page deleted.",{type:"success"}),o=o.filter(a=>a.urlPath!==s),r(o)}catch{E.toast("Failed to delete page.",{type:"error"})}}),n.off("click",".btn-prune").on("click",".btn-prune",async function(){const s=$(this).data("path"),t=Number($(this).data("count"))||0,e=window.prompt(`This page has ${t} revision${t===1?"":"s"}.
5
+ <button class="btn btn-sm btn-ghost btn-delete" data-path="${e.urlPath}" data-plugin="${e.plugin||""}" data-tooltip="Delete"><span data-icon="trash"></span></button>
6
+ `}],emptyMessage:'No pages found. <a href="#/pages/new">Create one</a>.'}),Domma.icons.scan(),document.querySelectorAll("#pages-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),Domma.effects.reveal(".card",{animation:"fade",duration:350})};r(i);const b=s=>{const t=n.find("#pages-tree").empty().get(0);if(!s.length){t.textContent="No pages found.";return}const e=s.map(a=>{const o=a.urlPath.split("/").filter(Boolean),l=o.length>1?"/"+o.slice(0,-1).join("/"):null,d=l&&s.some(h=>h.urlPath===l);return{id:a.urlPath,parent_id:d?l:null,name:a.title||a.urlPath,icon:a.status==="published"?"check-circle":"file-text"}});E.treeView(t,{data:e,idKey:"id",parentKey:"parent_id",labelKey:"name",iconKey:"icon",expandedByDefault:!0,onSelect:a=>R.navigate(`/pages/edit${a}`)}),Domma.icons.scan(t)};n.find("#view-table-btn").on("click",function(){n.find("#pages-table").show(),n.find("#pages-tree").hide(),$(this).addClass("btn-primary").removeClass("btn-ghost"),n.find("#view-tree-btn").addClass("btn-ghost").removeClass("btn-primary")}),n.find("#view-tree-btn").on("click",function(){n.find("#pages-table").hide(),n.find("#pages-tree").show(),$(this).addClass("btn-primary").removeClass("btn-ghost"),n.find("#view-table-btn").addClass("btn-ghost").removeClass("btn-primary"),b(i)}),n.find("#view-table-btn, #view-tree-btn").each(function(){E.tooltip(this,{content:this.getAttribute("data-tooltip"),position:"top"})});const u=()=>{const s=n.find("#status-filter").val(),t=n.find("#pages-search").val().toLowerCase().trim(),e=i.filter(a=>!(s&&a.status!==s||t&&!`${a.title} ${a.urlPath} ${(a.tags||[]).join(" ")}`.toLowerCase().includes(t)));r(e)};n.find("#status-filter").off("change").on("change",u),n.find("#pages-search").get(0).addEventListener("input",u),n.off("click",".btn-delete").on("click",".btn-delete",async function(){const s=$(this).data("path"),t=$(this).data("plugin"),e=t?`This page is managed by the <strong>${t}</strong> plugin. Deleting it may cause the plugin to malfunction. Continue?`:`Delete page at <strong>${s}</strong>? This cannot be undone.`;if(await E.confirm(e))try{await p.pages.delete(s),E.toast("Page deleted.",{type:"success"}),i=i.filter(a=>a.urlPath!==s),r(i)}catch{E.toast("Failed to delete page.",{type:"error"})}}),n.off("click",".btn-prune").on("click",".btn-prune",async function(){const s=$(this).data("path"),t=Number($(this).data("count"))||0,e=window.prompt(`This page has ${t} revision${t===1?"":"s"}.
7
7
 
8
- How many of the most recent revisions do you want to keep?`,"5");if(e===null)return;const a=Number.parseInt(e,10);if(!Number.isFinite(a)||a<0){E.toast("Please enter a non-negative number.",{type:"error"});return}try{const i=await p.versions.prune(s,a);E.toast(`Pruned ${i.deleted} revision${i.deleted===1?"":"s"}; kept ${i.kept}.`,{type:"success"});const l=o.find(d=>d.urlPath===s);l&&(l.versionCount=i.kept),r(o)}catch(i){E.toast(i.message||"Failed to prune revisions.",{type:"error"})}})}};
8
+ How many of the most recent revisions do you want to keep?`,"5");if(e===null)return;const a=Number.parseInt(e,10);if(!Number.isFinite(a)||a<0){E.toast("Please enter a non-negative number.",{type:"error"});return}try{const o=await p.versions.prune(s,a);E.toast(`Pruned ${o.deleted} revision${o.deleted===1?"":"s"}; kept ${o.kept}.`,{type:"success"});const l=i.find(d=>d.urlPath===s);l&&(l.versionCount=o.kept),r(i)}catch(o){E.toast(o.message||"Failed to prune revisions.",{type:"error"})}})}};
@@ -1,3 +1,3 @@
1
1
  import{api as l}from"../api.js";import{keepTableIconsAlive as m}from"../lib/table-icons.js";function s(n){return String(n??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export const projectsView={templateUrl:"/admin/js/templates/projects.html",async onMount(n){const d=E.loader(n.get(0),{type:"dots"});let i,r;try{i=await l.projects.list().catch(()=>[]),r=await Promise.all(i.map(t=>l.projects.artefacts(t.slug).then(e=>Object.values(e).reduce((a,c)=>a+c.length,0)).catch(()=>0)))}finally{d.destroy()}const b=i.map((t,e)=>({...t,artefactCount:r[e]})),o=t=>t.enabled===!1;T.create("#projects-table",{data:b,emptyMessage:'No projects yet. Click "New project" to create one.',columns:[{key:"slug",title:"Slug",sortable:!0,render:(t,e)=>o(e)?`<span class="text-muted">${s(t)}</span>`:s(t)},{key:"name",title:"Name",sortable:!0,render:(t,e)=>{const a=o(e)?`<span class="text-muted">${s(t)}</span>`:s(t),c=e.protected?' <span class="badge badge-secondary" data-tooltip="Built-in project \u2014 cannot be deleted">System</span>':"",p=o(e)?' <span class="badge badge-warning" data-tooltip="Disabled \u2014 public pages and external API are off">Disabled</span>':"";return a+c+p}},{key:"icon",title:"Icon",render:(t,e)=>t?`<span class="${o(e)?"text-muted":""}" data-icon="${s(t)}"></span>`:""},{key:"rootUrl",title:"Root URL",render:t=>t?`<code>${s(t)}</code>`:'<span class="text-muted">\u2014</span>'},{key:"artefactCount",title:"Artefacts",render:t=>`<span class="badge badge-info">${t??0}</span>`},{key:"_actions",title:"Actions",render:(t,e)=>{const a=encodeURIComponent(e.slug),c=o(e)?`<span class="btn btn-sm btn-ghost disabled" aria-disabled="true" style="opacity:.4;pointer-events:none;"><span data-icon="eye" data-icon-size="14"></span></span>
2
2
  <span class="btn btn-sm btn-ghost disabled" aria-disabled="true" style="opacity:.4;pointer-events:none;"><span data-icon="settings" data-icon-size="14"></span></span>`:`<a class="btn btn-sm btn-ghost" href="#/projects/${a}" data-tooltip="Overview"><span data-icon="eye" data-icon-size="14"></span></a>
3
- <a class="btn btn-sm btn-ghost" href="#/projects/${a}/settings" data-tooltip="Settings"><span data-icon="settings" data-icon-size="14"></span></a>`,g=e.slug==="core"?"":o(e)?`<button class="btn btn-sm btn-ghost btn-toggle-project" data-slug="${s(e.slug)}" data-enable="1" data-tooltip="Enable"><span class="text-success" data-icon="play" data-icon-size="14"></span></button>`:`<button class="btn btn-sm btn-ghost btn-toggle-project" data-slug="${s(e.slug)}" data-enable="0" data-tooltip="Disable"><span class="text-warning" data-icon="pause" data-icon-size="14"></span></button>`,u=e.protected?"":`<button class="btn btn-sm btn-danger btn-delete-project" data-slug="${s(e.slug)}" data-tooltip="Delete (refused while tagged)"><span data-icon="trash" data-icon-size="14"></span></button>`;return`<span style="display:inline-flex;gap:0.25rem;">${c}${g}${u}</span>`}}]}),Domma.icons.scan(n.get(0)),m(n.get(0)),document.querySelectorAll("#projects-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),n.off("click",".btn-toggle-project").on("click",".btn-toggle-project",async function(){const t=$(this).data("slug"),e=String($(this).data("enable"))==="1";if(!(!e&&!await E.confirm(`Disable project "${t}"? Its public pages will 404 and its external API will stop responding. Admin editing is unaffected.`)))try{await l.projects.update(t,{enabled:e}),E.toast(e?"Project enabled.":"Project disabled.",{type:"success"}),location.reload()}catch(a){E.toast(`Failed: ${a.message||a}`,{type:"error"})}}),n.off("click",".btn-delete-project").on("click",".btn-delete-project",async function(){const t=$(this).data("slug");if(await E.confirm(`Delete project "${t}"? This cannot be undone.`))try{await l.projects.remove(t),E.toast("Project deleted.",{type:"success"}),location.reload()}catch(e){E.toast(`Delete refused: ${e.message||e}`,{type:"error"})}})}};
3
+ <a class="btn btn-sm btn-ghost" href="#/projects/${a}/settings" data-tooltip="Settings"><span data-icon="settings" data-icon-size="14"></span></a>`,g=e.slug==="core"?"":o(e)?`<button class="btn btn-sm btn-ghost btn-toggle-project" data-slug="${s(e.slug)}" data-enable="1" data-tooltip="Enable"><span class="text-success" data-icon="play" data-icon-size="14"></span></button>`:`<button class="btn btn-sm btn-ghost btn-toggle-project" data-slug="${s(e.slug)}" data-enable="0" data-tooltip="Disable"><span class="text-warning" data-icon="pause" data-icon-size="14"></span></button>`,u=e.protected?"":`<button class="btn btn-sm btn-ghost btn-delete-project" data-slug="${s(e.slug)}" data-tooltip="Delete (refused while tagged)"><span data-icon="trash" data-icon-size="14"></span></button>`;return`<span style="display:inline-flex;gap:0.25rem;">${c}${g}${u}</span>`}}]}),Domma.icons.scan(n.get(0)),m(n.get(0)),document.querySelectorAll("#projects-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),n.off("click",".btn-toggle-project").on("click",".btn-toggle-project",async function(){const t=$(this).data("slug"),e=String($(this).data("enable"))==="1";if(!(!e&&!await E.confirm(`Disable project "${t}"? Its public pages will 404 and its external API will stop responding. Admin editing is unaffected.`)))try{await l.projects.update(t,{enabled:e}),E.toast(e?"Project enabled.":"Project disabled.",{type:"success"}),location.reload()}catch(a){E.toast(`Failed: ${a.message||a}`,{type:"error"})}}),n.off("click",".btn-delete-project").on("click",".btn-delete-project",async function(){const t=$(this).data("slug");if(await E.confirm(`Delete project "${t}"? This cannot be undone.`))try{await l.projects.remove(t),E.toast("Project deleted.",{type:"success"}),location.reload()}catch(e){E.toast(`Delete refused: ${e.message||e}`,{type:"error"})}})}};
@@ -1,4 +1,4 @@
1
- import{api as y,getUser as C}from"../api.js";import{getProjectFromHash as N}from"../lib/project-context.js";import{keepTableIconsAlive as k}from"../lib/table-icons.js";const $=[{value:"badge-danger",label:"Red (Admin)"},{value:"badge-warning",label:"Orange (Manager)"},{value:"badge-info",label:"Blue (Editor)"},{value:"badge-secondary",label:"Grey (Subscriber)"},{value:"badge-success",label:"Green"},{value:"badge-primary",label:"Primary"}];function w(t){if(!t||!t.length)return"<em>None</em>";const f=new Set(t.map(c=>c.split(".")[0])),b=t.every(c=>!c.includes(".")),l=f.size;return`${l} resource${l!==1?"s":""} ${b?"(full)":"(partial)"}`}function x(t){return String(t).replace(/"/g,"&quot;")}export const rolesView={templateUrl:"/admin/js/templates/roles.html",async onMount(t){if(!C()){R.navigate("/");return}const b=E.loader(t.get(0),{type:"dots"});let l=[];const c=N(),v=async()=>{l=(await y.get("/collections/roles/entries?limit=100")).entries||[],c&&(l=l.filter(a=>(a?.data?.meta?.project||"core")===c))},h=()=>{T.create("#roles-table",{data:l,columns:[{key:"data",title:"Label",render:e=>`<span class="badge ${e.badgeClass||"badge-secondary"}">${e.label||""}</span>`},{key:"data",title:"Name",render:e=>`<code>${e.name||""}</code>`},{key:"data",title:"Level",render:e=>e.level??""},{key:"data",title:"Permissions",render:e=>w(e.permissions)},{key:"id",title:"Actions",render:(e,a)=>{const n=a.data?.level===0;return`
1
+ import{api as y,getUser as C}from"../api.js";import{getProjectFromHash as N}from"../lib/project-context.js";import{keepTableIconsAlive as k}from"../lib/table-icons.js";const $=[{value:"badge-danger",label:"Red (Admin)"},{value:"badge-warning",label:"Orange (Manager)"},{value:"badge-info",label:"Blue (Editor)"},{value:"badge-secondary",label:"Grey (Subscriber)"},{value:"badge-success",label:"Green"},{value:"badge-primary",label:"Primary"}];function w(t){if(!t||!t.length)return"<em>None</em>";const f=new Set(t.map(c=>c.split(".")[0])),b=t.every(c=>!c.includes(".")),o=f.size;return`${o} resource${o!==1?"s":""} ${b?"(full)":"(partial)"}`}function x(t){return String(t).replace(/"/g,"&quot;")}export const rolesView={templateUrl:"/admin/js/templates/roles.html",async onMount(t){if(!C()){R.navigate("/");return}const b=E.loader(t.get(0),{type:"dots"});let o=[];const c=N(),v=async()=>{o=(await y.get("/collections/roles/entries?limit=100")).entries||[],c&&(o=o.filter(a=>(a?.data?.meta?.project||"core")===c))},h=()=>{T.create("#roles-table",{data:o,columns:[{key:"data",title:"Label",render:e=>`<span class="badge ${e.badgeClass||"badge-secondary"}">${e.label||""}</span>`},{key:"data",title:"Name",render:e=>`<code>${e.name||""}</code>`},{key:"data",title:"Level",render:e=>e.level??""},{key:"data",title:"Permissions",render:e=>w(e.permissions)},{key:"id",title:"Actions",render:(e,a)=>{const n=a.data?.level===0;return`
2
2
  <a href="#/roles/edit/${e}" class="btn btn-sm btn-ghost" data-tooltip="Edit"><span data-icon="edit" data-icon-size="14"></span></a>
3
- ${n?"":`<button class="btn btn-sm btn-danger btn-delete-role" data-id="${e}" data-name="${x(a.data?.label)}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>`}
4
- `}}],emptyMessage:"No roles found."}),Domma.icons.scan(),document.querySelectorAll("#roles-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})};try{await v()}finally{b.destroy()}h(),k(t.get(0)),document.addEventListener("click",async function e(a){if(!document.contains(t.get(0))){document.removeEventListener("click",e);return}const n=a.target.closest(".btn-delete-role");if(!n)return;const m=n.dataset.id,p=n.dataset.name;if(await E.confirm(`Delete role <strong>${p}</strong>? This cannot be undone.`))try{await y.delete(`/collections/roles/entries/${m}`),E.toast("Role deleted.",{type:"success"}),await v(),h()}catch(u){E.toast(`Failed: ${u.message}`,{type:"error"})}}),t.find("#btn-add-role").on("click",()=>{const e=document.createElement("div");e.style.cssText="display:flex;flex-direction:column;gap:1rem;padding:1rem;";const a=(o,i)=>{const s=document.createElement("div"),r=document.createElement("label");return r.textContent=o,r.style.cssText="display:block;font-size:.85rem;margin-bottom:.25rem;font-weight:600;",s.appendChild(r),s.appendChild(i),s},n=Object.assign(document.createElement("input"),{type:"text",className:"form-input",placeholder:"e.g. moderator"}),m=Object.assign(document.createElement("input"),{type:"text",className:"form-input",placeholder:"e.g. Moderator"}),p=Object.assign(document.createElement("input"),{type:"number",className:"form-input",value:"5",min:"1"}),d=document.createElement("select");d.className="form-input",$.forEach(o=>{const i=Object.assign(document.createElement("option"),{value:o.value,textContent:o.label});d.appendChild(i)});const u=Object.assign(document.createElement("button"),{className:"btn btn-primary",textContent:"Create Role"});e.appendChild(a("Name (slug)",n)),e.appendChild(a("Display Label",m)),e.appendChild(a("Level",p)),e.appendChild(a("Badge Colour",d)),e.appendChild(u);const g=E.modal({title:"Add Role",size:"sm"});g.element.appendChild(e),g.open(),u.addEventListener("click",async()=>{const o=n.value.trim().toLowerCase().replace(/[^a-z0-9-]/g,"-"),i=m.value.trim(),s=parseInt(p.value,10);if(!o||!i||isNaN(s)||s<1){E.toast("Name, label and level (\u2265 1) are required.",{type:"error"});return}try{const r=await y.post("/collections/roles/entries",{data:{name:o,label:i,level:s,permissions:[],badgeClass:d.value}});g.close(),E.toast("Role created. Set permissions in the editor.",{type:"success"}),R.navigate(`/roles/edit/${r.id}`)}catch(r){E.toast(`Error: ${r.message}`,{type:"error"})}})})}};
3
+ ${n?"":`<button class="btn btn-sm btn-ghost btn-delete-role" data-id="${e}" data-name="${x(a.data?.label)}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>`}
4
+ `}}],emptyMessage:"No roles found."}),Domma.icons.scan(),document.querySelectorAll("#roles-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})};try{await v()}finally{b.destroy()}h(),k(t.get(0)),document.addEventListener("click",async function e(a){if(!document.contains(t.get(0))){document.removeEventListener("click",e);return}const n=a.target.closest(".btn-delete-role");if(!n)return;const m=n.dataset.id,p=n.dataset.name;if(await E.confirm(`Delete role <strong>${p}</strong>? This cannot be undone.`))try{await y.delete(`/collections/roles/entries/${m}`),E.toast("Role deleted.",{type:"success"}),await v(),h()}catch(u){E.toast(`Failed: ${u.message}`,{type:"error"})}}),t.find("#btn-add-role").on("click",()=>{const e=document.createElement("div");e.style.cssText="display:flex;flex-direction:column;gap:1rem;padding:1rem;";const a=(l,i)=>{const s=document.createElement("div"),r=document.createElement("label");return r.textContent=l,r.style.cssText="display:block;font-size:.85rem;margin-bottom:.25rem;font-weight:600;",s.appendChild(r),s.appendChild(i),s},n=Object.assign(document.createElement("input"),{type:"text",className:"form-input",placeholder:"e.g. moderator"}),m=Object.assign(document.createElement("input"),{type:"text",className:"form-input",placeholder:"e.g. Moderator"}),p=Object.assign(document.createElement("input"),{type:"number",className:"form-input",value:"5",min:"1"}),d=document.createElement("select");d.className="form-input",$.forEach(l=>{const i=Object.assign(document.createElement("option"),{value:l.value,textContent:l.label});d.appendChild(i)});const u=Object.assign(document.createElement("button"),{className:"btn btn-primary",textContent:"Create Role"});e.appendChild(a("Name (slug)",n)),e.appendChild(a("Display Label",m)),e.appendChild(a("Level",p)),e.appendChild(a("Badge Colour",d)),e.appendChild(u);const g=E.modal({title:"Add Role",size:"sm"});g.element.appendChild(e),g.open(),u.addEventListener("click",async()=>{const l=n.value.trim().toLowerCase().replace(/[^a-z0-9-]/g,"-"),i=m.value.trim(),s=parseInt(p.value,10);if(!l||!i||isNaN(s)||s<1){E.toast("Name, label and level (\u2265 1) are required.",{type:"error"});return}try{const r=await y.post("/collections/roles/entries",{data:{name:l,label:i,level:s,permissions:[],badgeClass:d.value}});g.close(),E.toast("Role created. Set permissions in the editor.",{type:"success"}),R.navigate(`/roles/edit/${r.id}`)}catch(r){E.toast(`Error: ${r.message}`,{type:"error"})}})})}};
@@ -1,4 +1,4 @@
1
- import{api as d,getUser as A}from"../api.js";import{filterByProject as k,getProjectFromHash as M}from"../lib/project-context.js";import{keepTableIconsAlive as j}from"../lib/table-icons.js";export const usersView={templateUrl:"/admin/js/templates/users.html",async onMount(r){const m=A(),f=E.loader(r.get(0),{type:"dots"});let[n,y]=await Promise.all([d.users.list().catch(()=>[]),d.get("/collections/roles/entries?limit=100").catch(()=>({entries:[]}))]);f.destroy();const p=M();p&&(n=k(n,p));const c=y.entries||[],u=o=>{T.create("#users-table",{data:o,columns:[{key:"name",title:"Name"},{key:"email",title:"Email"},{key:"role",title:"Role",render:(e,t)=>{const a=c.find(s=>s.data?.name===e),i=a?.data?.badgeClass||"badge-secondary",b=a?.data?.label||e,g=`<span class="badge ${i}">${b}</span>`,l=Array.isArray(t.additionalRoles)?t.additionalRoles.filter(s=>s&&s!==e).map(s=>`<span class="badge badge-outline" title="Additional role" style="margin-left:.25rem;">${c.find(h=>h.data?.name===s)?.data?.label||s}</span>`).join(""):"";return g+l}},{key:"isActive",title:"Status",render:e=>e?'<span class="badge badge-success">Active</span>':'<span class="badge badge-secondary">Inactive</span>'},{key:"lastLogin",title:"Last login",render:e=>e?D(e).format("DD MMM YYYY HH:mm"):"Never"},{key:"actions",title:"Actions",render:(e,t)=>{const a=t.id===m?.id,i=Object.fromEntries(c.map(l=>[l.data?.name,l.data?.level??99]));return(i[m?.role]??99)<(i[t.role]??99)?`
1
+ import{api as d,getUser as A}from"../api.js";import{filterByProject as k,getProjectFromHash as M}from"../lib/project-context.js";import{keepTableIconsAlive as j}from"../lib/table-icons.js";export const usersView={templateUrl:"/admin/js/templates/users.html",async onMount(o){const m=A(),f=E.loader(o.get(0),{type:"dots"});let[n,y]=await Promise.all([d.users.list().catch(()=>[]),d.get("/collections/roles/entries?limit=100").catch(()=>({entries:[]}))]);f.destroy();const p=M();p&&(n=k(n,p));const c=y.entries||[],u=r=>{T.create("#users-table",{data:r,columns:[{key:"name",title:"Name"},{key:"email",title:"Email"},{key:"role",title:"Role",render:(e,t)=>{const a=c.find(s=>s.data?.name===e),i=a?.data?.badgeClass||"badge-secondary",b=a?.data?.label||e,g=`<span class="badge ${i}">${b}</span>`,l=Array.isArray(t.additionalRoles)?t.additionalRoles.filter(s=>s&&s!==e).map(s=>`<span class="badge badge-outline" title="Additional role" style="margin-left:.25rem;">${c.find(h=>h.data?.name===s)?.data?.label||s}</span>`).join(""):"";return g+l}},{key:"isActive",title:"Status",render:e=>e?'<span class="badge badge-success">Active</span>':'<span class="badge badge-secondary">Inactive</span>'},{key:"lastLogin",title:"Last login",render:e=>e?D(e).format("DD MMM YYYY HH:mm"):"Never"},{key:"actions",title:"Actions",render:(e,t)=>{const a=t.id===m?.id,i=Object.fromEntries(c.map(l=>[l.data?.name,l.data?.level??99]));return(i[m?.role]??99)<(i[t.role]??99)?`
2
2
  <a href="#/users/edit/${t.id}" class="btn btn-sm btn-ghost" data-tooltip="Edit"><span data-icon="edit" data-icon-size="14"></span></a>
3
- ${a?"":`<button class="btn btn-sm btn-danger btn-delete-user" data-id="${t.id}" data-name="${v(t.name)}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>`}
4
- `:""}}],emptyMessage:"No users found."}),Domma.icons.scan(),document.querySelectorAll("#users-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})};u(n),j(r.get(0)),r.off("click",".btn-delete-user").on("click",".btn-delete-user",async function(){const o=$(this).data("id"),e=$(this).data("name");if(await E.confirm(`Delete user <strong>${e}</strong>? This cannot be undone.`))try{await d.delete(`/users/${o}`),E.toast("User deleted.",{type:"success"}),n=n.filter(a=>a.id!==o),u(n)}catch(a){E.toast(`Failed to delete: ${a.message}`,{type:"error"})}})}};function v(r){return String(r).replace(/"/g,"&quot;")}
3
+ ${a?"":`<button class="btn btn-sm btn-ghost btn-delete-user" data-id="${t.id}" data-name="${v(t.name)}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>`}
4
+ `:""}}],emptyMessage:"No users found."}),Domma.icons.scan(),document.querySelectorAll("#users-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})};u(n),j(o.get(0)),o.off("click",".btn-delete-user").on("click",".btn-delete-user",async function(){const r=$(this).data("id"),e=$(this).data("name");if(await E.confirm(`Delete user <strong>${e}</strong>? This cannot be undone.`))try{await d.delete(`/users/${r}`),E.toast("User deleted.",{type:"success"}),n=n.filter(a=>a.id!==r),u(n)}catch(a){E.toast(`Failed to delete: ${a.message}`,{type:"error"})}})}};function v(o){return String(o).replace(/"/g,"&quot;")}
@@ -1 +1 @@
1
- import{api as v}from"../api.js";let g=null,f=null,C={};async function T(e){const t=e.find("#view-project").get(0);if(t)try{(await v.projects.list()).forEach(a=>{const l=document.createElement("option");l.value=a.slug,l.textContent=a.name||a.slug,t.appendChild(l)})}catch{}}export const viewEditorView={templateUrl:"/admin/js/templates/view-editor.html",async onMount(e){g=null,f=null,C={};const t=window.location.hash.match(/\/views\/edit\/([^/?#]+)/);t&&(g=t[1]),E.tabs(e.find("#view-editor-tabs").get(0));const n=E.loader(e.get(0),{type:"dots"});try{await L(e),await _(e),await O(e),await D(e),await T(e),e.find("#view-source").get(0)?.addEventListener("change",async()=>{await k(e)}),g&&(e.find("#view-editor-title").text("Edit View"),await V(e,g))}finally{n.destroy()}e.find("#add-stage-btn").off("click").on("click",()=>{const a=e.find("#add-stage-type").val()||"$match";q(e,{type:a,config:{}})}),e.find("#save-view-btn").off("click").on("click",async()=>{await X(e)}),Q(e),H(e),Domma.icons.scan()}};async function L(e){const t=e.find("#view-source").get(0);if(t)try{(await v.collections.list()).forEach(n=>{const a=document.createElement("option");a.value=n.slug,a.textContent=`${n.title} (${n.slug})`,t.appendChild(a)})}catch{}}async function D(e){const t=e.find("#view-block-name").get(0);if(t)try{(await v.blocks.list()).forEach(n=>{const a=document.createElement("option");a.value=n.name,a.textContent=`${n.name}.html`,t.appendChild(a)})}catch{}}async function _(e){const t=e.find("#view-connection").get(0);if(t)try{const n=await v.collections.getConnections();Object.keys(n).forEach(a=>{if(!t.querySelector(`option[value="${a}"]`)){const l=document.createElement("option");l.value=a,l.textContent=a,t.appendChild(l)}})}catch{}}async function O(e){const t=e.find("#view-roles-checkboxes").get(0);t&&["admin","manager","editor","subscriber"].forEach(n=>{const a=document.createElement("label");a.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const l=document.createElement("input");l.type="checkbox",l.value=n,l.dataset.role=n,l.className="view-role-cb",l.checked=n==="admin",a.appendChild(l),a.appendChild(document.createTextNode(n)),t.appendChild(a)})}async function k(e){const t=e.find("#view-source").val();if(!t){f=null;return}try{f=(await v.collections.get(t))?.fields||[]}catch{f=null}e.find(".stage-card[data-guided]").each(function(){const n=this.dataset.stageType,a=this.querySelector(".stage-guided");a&&(n==="$match"&&J(a),n==="$sort"&&U(a))}),A(e,null)}async function V(e,t){try{const n=await v.views.get(t);if(!n){E.toast("View not found.",{type:"error"}),R.navigate("/views");return}const a=n.pipeline?.source;a&&(e.find("#view-source").val(a),await k(e)),z(e,n)}catch(n){E.toast(n.message||"Failed to load view.",{type:"error"}),R.navigate("/views")}}function z(e,t){e.find("#view-title").val(t.title||""),e.find("#view-slug").val(t.slug||""),e.find("#view-description").val(t.description||""),e.find("#view-source").val(t.pipeline?.source||""),e.find("#view-bundled").prop("checked",!!t.bundled),C=t.meta||{},e.find("#view-project").val(t.meta?.project||""),e.find("#view-connection").val(t.connection||"default"),e.find("#view-display-mode").val(t.display?.mode||"table"),e.find("#view-page-size").val(t.display?.pageSize||25),e.find("#view-block-name").val(t.display?.block||"");const n=t.access?.roles||["admin"];e.find(".view-role-cb").each(function(){this.checked=n.includes(this.value)}),e.find("#view-public").prop("checked",t.access?.public||!1);const a=t.access?.rowLevel;a&&(e.find("#view-rowlevel-enabled").prop("checked",!0),e.find("#view-rowlevel-config").css("display","flex"),e.find("#view-rowlevel-mode").val(a.mode||"owner"),e.find("#view-rowlevel-userkey").val(a.userKey||"id"),a.mode==="field"&&(e.find("#view-rowlevel-field-group").css("display",""),e.find("#view-rowlevel-field").val(a.field||"")));const l=e.find("#pipeline-stages-list").get(0);if(l)for(;l.firstChild;)l.removeChild(l.firstChild);(t.pipeline?.stages||[]).forEach(i=>q(e,i)),A(e,t.display?.columns||[]),N(e)}const F=[{value:"eq",label:"= equals"},{value:"ne",label:"\u2260 not equals"},{value:"gt",label:"> greater than"},{value:"lt",label:"< less than"},{value:"gte",label:"\u2265 greater or equal"},{value:"lte",label:"\u2264 less or equal"},{value:"contains",label:"~ contains (regex)"},{value:"in",label:"\u2208 in (comma list)"}];function y(e=!1){const t=[];return e&&t.push({value:"",label:"\u2014 select field \u2014"}),(f||[]).forEach(n=>{t.push({value:`data.${n.name}`,label:`${n.label||n.name} (${n.name})`})}),t.length===0||t.length===1&&e?(t.push({value:"__meta.createdAt",label:"Created At"}),t.push({value:"__meta.updatedAt",label:"Updated At"})):(t.push({value:"__meta.createdAt",label:"Created At (meta)"}),t.push({value:"__meta.updatedAt",label:"Updated At (meta)"})),t}function h(e,t){const n=document.createElement("select");return n.className="form-input form-input--sm",e.forEach(a=>{const l=document.createElement("option");l.value=a.value,l.textContent=a.label,String(a.value)===String(t)&&(l.selected=!0),n.appendChild(l)}),n}function S(e,t,n="text"){const a=document.createElement("input");return a.type=n,a.className="form-input form-input--sm",a.placeholder=e,a.value=t??"",a}function B(e){if(!f?.length)return null;const t=e?.startsWith("data.")?e.slice(5):null;return t&&f.find(n=>n.name===t)||null}function b(e,t,n=""){const a=B(e);if(a?.type==="select"&&(t==="eq"||t==="ne")&&a.options?.length){const d=document.createElement("select");d.className="form-input form-input--sm match-val",d.style.flex="1";const c=document.createElement("option");return c.value="",c.textContent="\u2014 select value \u2014",d.appendChild(c),(a.options||[]).forEach(o=>{const s=typeof o=="string"?o:o.value??"",r=typeof o=="string"?o:o.label||s;if(!s||s==="undefined")return;const u=document.createElement("option");u.value=s,u.textContent=r,s===n&&(u.selected=!0),d.appendChild(u)}),d}const l=t==="in"?"val1, val2, val3":t==="contains"?"search pattern (regex)":t==="gt"||t==="lt"||t==="gte"||t==="lte"?"0":"value",i=document.createElement("input");return i.type="text",i.className="form-input form-input--sm match-val",i.style.flex="1",i.placeholder=l,i.value=n,i}function $(e){const t=[],n={$eq:"eq",$ne:"ne",$gt:"gt",$lt:"lt",$gte:"gte",$lte:"lte",$in:"in"};return Object.entries(e||{}).forEach(([a,l])=>{if(typeof l=="object"&&l!==null&&!Array.isArray(l)){if("$regex"in l){t.push({field:a,op:"contains",val:String(l.$regex??"")});return}Object.entries(l).forEach(([i,d])=>{const c=n[i];c&&t.push({field:a,op:c,val:Array.isArray(d)?d.join(", "):String(d)})})}else t.push({field:a,op:"eq",val:String(l??"")})}),t}function x(e,t,n="",a="eq",l=""){const i=document.createElement("div");i.className="match-condition-row",i.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;flex-wrap:wrap;";const d=h([{value:"",label:"\u2014 field \u2014"},...y()],n);d.className+=" match-field",d.style.minWidth="140px";const c=h(F,a);c.className+=" match-op",c.style.minWidth="160px";const o=document.createElement("div");o.className="match-val-wrap",o.style.cssText="flex:1;min-width:120px;display:flex;",o.appendChild(b(n,a,l));const s=()=>{const m=o.querySelector(".match-val")?.value??"";for(;o.firstChild;)o.removeChild(o.firstChild);o.appendChild(b(d.value,c.value,m))};d.addEventListener("change",s),c.addEventListener("change",s);const r=document.createElement("button");r.type="button",r.className="btn btn-sm btn-ghost",r.title="Remove";const u=document.createElement("span");u.setAttribute("data-icon","x"),r.appendChild(u),r.addEventListener("click",()=>i.remove()),i.appendChild(d),i.appendChild(c),i.appendChild(o),i.appendChild(r),e.insertBefore(i,t),Domma.icons.scan(i)}function W(e,t={}){for(;e.firstChild;)e.removeChild(e.firstChild);const n=t.$match||t||{},a="$or"in n;let l=[];a?(n.$or||[]).forEach(r=>$(r).forEach(u=>l.push(u))):l=$(n);const i=document.createElement("div");i.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.6rem;";const d=document.createElement("label");d.className="form-label",d.style.marginBottom="0",d.textContent="Match:";const c=document.createElement("select");c.className="form-input form-input--sm match-logic",c.style.width="auto",[{value:"and",label:"ALL conditions (AND)"},{value:"or",label:"ANY condition (OR)"}].forEach(r=>{const u=document.createElement("option");u.value=r.value,u.textContent=r.label,(a?"or":"and")===r.value&&(u.selected=!0),c.appendChild(u)}),i.appendChild(d),i.appendChild(c),e.appendChild(i);const o=document.createElement("button");o.type="button",o.className="btn btn-ghost btn-sm match-add-btn";const s=document.createElement("span");s.setAttribute("data-icon","plus"),o.appendChild(s),o.appendChild(document.createTextNode(" Add Condition")),o.addEventListener("click",()=>{x(e,o),Domma.icons.scan(e)}),e.appendChild(o),l.forEach(r=>x(e,o,r.field,r.op,r.val)),l.length===0&&x(e,o),Domma.icons.scan(e)}function J(e){e.querySelectorAll(".match-condition-row").forEach(t=>{const n=t.querySelector(".match-field");if(n){const d=n.value,c=h([{value:"",label:"\u2014 field \u2014"},...y()],d);c.className=n.className,c.style.cssText=n.style.cssText;const o=t.querySelector(".match-val-wrap"),s=t.querySelector(".match-op");o&&s&&c.addEventListener("change",()=>{const r=o.querySelector(".match-val")?.value??"";for(;o.firstChild;)o.removeChild(o.firstChild);o.appendChild(b(c.value,s.value,r))}),n.parentNode.replaceChild(c,n)}const a=t.querySelector(".match-val-wrap"),l=t.querySelector(".match-field")?.value,i=t.querySelector(".match-op")?.value;if(a&&l&&i){const d=a.querySelector(".match-val")?.value??"";for(;a.firstChild;)a.removeChild(a.firstChild);a.appendChild(b(l,i,d))}})}function M(e){const t=e.querySelector(".match-logic")?.value||"and",n=l=>{const i=l.querySelector(".match-field")?.value?.trim(),d=l.querySelector(".match-op")?.value,c=l.querySelector(".match-val")?.value?.trim()??"";if(!i)return null;const o=i.startsWith("__")?i.slice(2):i,s={};switch(d){case"eq":s[o]=c;break;case"ne":s[o]={$ne:c};break;case"gt":s[o]={$gt:isNaN(c)?c:Number(c)};break;case"lt":s[o]={$lt:isNaN(c)?c:Number(c)};break;case"gte":s[o]={$gte:isNaN(c)?c:Number(c)};break;case"lte":s[o]={$lte:isNaN(c)?c:Number(c)};break;case"contains":s[o]={$regex:c,$options:"i"};break;case"in":s[o]={$in:c.split(",").map(r=>r.trim()).filter(Boolean)};break}return Object.keys(s).length?s:null},a=[];return e.querySelectorAll(".match-condition-row").forEach(l=>{const i=n(l);i&&a.push(i)}),a.length===0?{}:t==="or"?{$or:a}:Object.assign({},...a)}function I(e,t={}){for(;e.firstChild;)e.removeChild(e.firstChild);const n=t.$sort||t||{},a=Object.entries(n),l=document.createElement("button");l.type="button",l.className="btn btn-ghost btn-sm";const i=document.createElement("span");i.setAttribute("data-icon","plus"),l.appendChild(i),l.appendChild(document.createTextNode(" Add Sort")),e.appendChild(l);const d=(c="",o=1)=>{const s=document.createElement("div");s.className="sort-row",s.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;";const r=h([{value:"",label:"\u2014 field \u2014"},...y()],c);r.className+=" sort-field";const u=h([{value:"1",label:"\u2191 Ascending"},{value:"-1",label:"\u2193 Descending"}],String(o));u.className+=" sort-dir";const m=document.createElement("button");m.type="button",m.className="btn btn-sm btn-ghost",m.title="Remove";const p=document.createElement("span");p.setAttribute("data-icon","x"),m.appendChild(p),m.addEventListener("click",()=>{s.remove()}),s.appendChild(r),s.appendChild(u),s.appendChild(m),e.insertBefore(s,l),Domma.icons.scan(s)};l.addEventListener("click",()=>{d()}),a.forEach(([c,o])=>d(c,o)),a.length===0&&d(),Domma.icons.scan(e)}function U(e){e.querySelectorAll(".sort-row").forEach(t=>{const n=t.querySelector(".sort-field"),a=n?.value,l=h([{value:"",label:"\u2014 field \u2014"},...y()],a);l.className=n.className,n.parentNode.replaceChild(l,n)})}function K(e){const t={};return e.querySelectorAll(".sort-row").forEach(n=>{const a=n.querySelector(".sort-field")?.value?.trim(),l=parseInt(n.querySelector(".sort-dir")?.value,10)||1;if(!a)return;const i=a.startsWith("__")?a.slice(2):a;t[i]=l}),t}function q(e,t){const n=e.find("#pipeline-stages-list").get(0);if(!n)return;const a=n.querySelector(".stage-empty-placeholder");a&&a.remove();const l=["$match","$sort"].includes(t.type),i=document.createElement("div");i.className="card mb-2 stage-card",i.dataset.stageType=t.type,l&&(i.dataset.guided="true");const d=document.createElement("div");d.className="card-header",d.style.cssText="display:flex;align-items:center;gap:.5rem;";const c=document.createElement("code");c.textContent=t.type,c.style.cssText="flex:1;font-size:.85rem;";const o=document.createElement("button");o.type="button",o.className="btn btn-sm btn-danger";const s=document.createElement("span");s.setAttribute("data-icon","trash"),o.appendChild(s),o.addEventListener("click",()=>{i.remove(),n.querySelector(".stage-card")||n.appendChild(P())}),d.appendChild(c),d.appendChild(o);const r=document.createElement("div");if(r.className="card-body",l){const u=document.createElement("div");u.className="stage-guided",r.appendChild(u),t.type==="$match"&&W(u,t.config),t.type==="$sort"&&I(u,t.config)}else{const u=document.createElement("label");u.className="form-label",u.textContent="Stage Config (JSON)";const m=document.createElement("small");m.className="text-muted",m.style.cssText="display:block;margin-bottom:.4rem;",m.textContent=`Enter the inner config for ${t.type} \u2014 e.g. for $lookup: { from, localField, foreignField, as }. Do not wrap in { "${t.type}": ... }.`;const p=document.createElement("textarea");p.className="form-input stage-config",p.rows=5,p.style.cssText="font-family:monospace;font-size:.8rem;resize:vertical;",p.placeholder="{}",p.value=Object.keys(t.config||{}).length?JSON.stringify(t.config,null,2):"",r.appendChild(u),r.appendChild(m),r.appendChild(p)}i.appendChild(d),i.appendChild(r),n.appendChild(i),Domma.icons.scan(i)}function P(){const e=document.createElement("p");return e.className="text-muted stage-empty-placeholder",e.textContent="No stages yet. Add a stage to filter, join, or transform your data.",e.style.cssText="text-align:center;padding:2rem 0;",e}function Y(e){const t=[];return e.find(".stage-card").each(function(){const n=this.dataset.stageType,a=this.querySelector(".stage-guided");let l;if(a)n==="$match"&&(l=M(a)),n==="$sort"&&(l=K(a));else{const i=this.querySelector(".stage-config")?.value?.trim()||"{}";try{l=JSON.parse(i)}catch{throw new Error(`Invalid JSON in ${n} stage config`)}}t.push({type:n,config:l})}),t}function A(e,t){const n=e.find("#view-columns-builder").get(0);if(!n)return;for(;n.firstChild;)n.removeChild(n.firstChild);const a=t??(f||[]).map(c=>({key:`data.${c.name}`,label:c.label||c.name})),l=(c="",o="")=>{const s=document.createElement("div");s.className="col-row",s.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;";const r=f?.length?h([{value:"",label:"\u2014 field \u2014"},...y()],c):S("data.fieldName",c);r.className+=" col-key";const u=S("Display label",o);u.className+=" col-label",u.style.flex="1",f?.length&&r.tagName==="SELECT"&&r.addEventListener("change",()=>{if(!u.value){const w=(f||[]).find(j=>`data.${j.name}`===r.value);w&&(u.value=w.label||w.name)}});const m=document.createElement("button");m.type="button",m.className="btn btn-sm btn-ghost",m.title="Remove column";const p=document.createElement("span");p.setAttribute("data-icon","x"),m.appendChild(p),m.addEventListener("click",()=>{s.remove(),Domma.icons.scan(n)}),s.appendChild(r),s.appendChild(u),s.appendChild(m),n.insertBefore(s,n.lastChild),Domma.icons.scan(s)};a.forEach(c=>l(c.key,c.label));const i=document.createElement("button");i.type="button",i.className="btn btn-ghost btn-sm";const d=document.createElement("span");d.setAttribute("data-icon","plus"),i.appendChild(d),i.appendChild(document.createTextNode(" Add Column")),i.addEventListener("click",()=>{l(),Domma.icons.scan(n)}),n.appendChild(i),Domma.icons.scan(n)}function G(e){const t=[];return e.find(".col-row").each(function(){const n=this.querySelector(".col-key")?.value?.trim(),a=this.querySelector(".col-label")?.value?.trim();n&&t.push({key:n,label:a||n})}),t}function H(e){const t=e.find("#view-display-mode").get(0);t&&(t.addEventListener("change",()=>N(e)),N(e))}function N(e){const t=e.find("#view-display-mode").val()||"table",n=e.find("#view-columns-section").get(0),a=e.find("#view-block-section").get(0);n&&(n.style.display=t==="table"?"":"none"),a&&(a.style.display=t==="block"?"":"none")}function Q(e){const t=e.find("#view-rowlevel-enabled").get(0),n=e.find("#view-rowlevel-config").get(0),a=e.find("#view-rowlevel-mode").get(0),l=e.find("#view-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{n&&(n.style.display=t.checked?"flex":"none")}),a&&a.addEventListener("change",()=>{l&&(l.style.display=a.value==="field"?"":"none")}))}async function X(e){const t=e.find("#view-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const n=e.find("#view-source").val();if(!n){E.toast("Source collection is required (Source tab).",{type:"warning"});return}let a;try{a=Y(e)}catch(u){E.toast(u.message,{type:"error"});return}const l=G(e),i=[];e.find(".view-role-cb:checked").each(function(){i.push(this.value)});const d=e.find("#view-rowlevel-enabled").is(":checked");let c=null;if(d){const u=e.find("#view-rowlevel-mode").val()||"owner",m=e.find("#view-rowlevel-userkey").val()||"id";if(c={mode:u,userKey:m},u==="field"){const p=e.find("#view-rowlevel-field").val().trim();if(!p){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}c.field=p}}const o=e.find("#view-project").val()||"",s={title:t,slug:e.find("#view-slug").val().trim()||void 0,description:e.find("#view-description").val().trim(),...e.find("#view-bundled").is(":checked")?{bundled:!0}:{},connection:e.find("#view-connection").val()||"default",pipeline:{source:n,stages:a},display:{mode:e.find("#view-display-mode").val()||"table",columns:l,pageSize:parseInt(e.find("#view-page-size").val(),10)||25,block:e.find("#view-block-name").val()||""},access:{roles:i,public:e.find("#view-public").is(":checked"),rowLevel:c},meta:{...C||{},project:o||null}},r=e.find("#save-view-btn").get(0);r&&(r.disabled=!0);try{if(g)await v.views.update(g,s),E.toast("View updated.",{type:"success"});else{const u=await v.views.create(s);E.toast("View created.",{type:"success"}),R.navigate(`/views/edit/${u.slug}`)}}catch(u){E.toast(u.message||"Failed to save view.",{type:"error"})}finally{r&&(r.disabled=!1)}}
1
+ import{api as v}from"../api.js";let g=null,f=null,C={};async function T(e){const t=e.find("#view-project").get(0);if(t)try{(await v.projects.list()).forEach(a=>{const l=document.createElement("option");l.value=a.slug,l.textContent=a.name||a.slug,t.appendChild(l)})}catch{}}export const viewEditorView={templateUrl:"/admin/js/templates/view-editor.html",async onMount(e){g=null,f=null,C={};const t=window.location.hash.match(/\/views\/edit\/([^/?#]+)/);t&&(g=t[1]),E.tabs(e.find("#view-editor-tabs").get(0));const n=E.loader(e.get(0),{type:"dots"});try{await L(e),await _(e),await O(e),await D(e),await T(e),e.find("#view-source").get(0)?.addEventListener("change",async()=>{await k(e)}),g&&(e.find("#view-editor-title").text("Edit View"),await V(e,g))}finally{n.destroy()}e.find("#add-stage-btn").off("click").on("click",()=>{const a=e.find("#add-stage-type").val()||"$match";q(e,{type:a,config:{}})}),e.find("#save-view-btn").off("click").on("click",async()=>{await X(e)}),Q(e),H(e),Domma.icons.scan()}};async function L(e){const t=e.find("#view-source").get(0);if(t)try{(await v.collections.list()).forEach(n=>{const a=document.createElement("option");a.value=n.slug,a.textContent=`${n.title} (${n.slug})`,t.appendChild(a)})}catch{}}async function D(e){const t=e.find("#view-block-name").get(0);if(t)try{(await v.blocks.list()).forEach(n=>{const a=document.createElement("option");a.value=n.name,a.textContent=`${n.name}.html`,t.appendChild(a)})}catch{}}async function _(e){const t=e.find("#view-connection").get(0);if(t)try{const n=await v.collections.getConnections();Object.keys(n).forEach(a=>{if(!t.querySelector(`option[value="${a}"]`)){const l=document.createElement("option");l.value=a,l.textContent=a,t.appendChild(l)}})}catch{}}async function O(e){const t=e.find("#view-roles-checkboxes").get(0);t&&["admin","manager","editor","subscriber"].forEach(n=>{const a=document.createElement("label");a.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const l=document.createElement("input");l.type="checkbox",l.value=n,l.dataset.role=n,l.className="view-role-cb",l.checked=n==="admin",a.appendChild(l),a.appendChild(document.createTextNode(n)),t.appendChild(a)})}async function k(e){const t=e.find("#view-source").val();if(!t){f=null;return}try{f=(await v.collections.get(t))?.fields||[]}catch{f=null}e.find(".stage-card[data-guided]").each(function(){const n=this.dataset.stageType,a=this.querySelector(".stage-guided");a&&(n==="$match"&&J(a),n==="$sort"&&U(a))}),A(e,null)}async function V(e,t){try{const n=await v.views.get(t);if(!n){E.toast("View not found.",{type:"error"}),R.navigate("/views");return}const a=n.pipeline?.source;a&&(e.find("#view-source").val(a),await k(e)),z(e,n)}catch(n){E.toast(n.message||"Failed to load view.",{type:"error"}),R.navigate("/views")}}function z(e,t){e.find("#view-title").val(t.title||""),e.find("#view-slug").val(t.slug||""),e.find("#view-description").val(t.description||""),e.find("#view-source").val(t.pipeline?.source||""),e.find("#view-bundled").prop("checked",!!t.bundled),C=t.meta||{},e.find("#view-project").val(t.meta?.project||""),e.find("#view-connection").val(t.connection||"default"),e.find("#view-display-mode").val(t.display?.mode||"table"),e.find("#view-page-size").val(t.display?.pageSize||25),e.find("#view-block-name").val(t.display?.block||"");const n=t.access?.roles||["admin"];e.find(".view-role-cb").each(function(){this.checked=n.includes(this.value)}),e.find("#view-public").prop("checked",t.access?.public||!1);const a=t.access?.rowLevel;a&&(e.find("#view-rowlevel-enabled").prop("checked",!0),e.find("#view-rowlevel-config").css("display","flex"),e.find("#view-rowlevel-mode").val(a.mode||"owner"),e.find("#view-rowlevel-userkey").val(a.userKey||"id"),a.mode==="field"&&(e.find("#view-rowlevel-field-group").css("display",""),e.find("#view-rowlevel-field").val(a.field||"")));const l=e.find("#pipeline-stages-list").get(0);if(l)for(;l.firstChild;)l.removeChild(l.firstChild);(t.pipeline?.stages||[]).forEach(i=>q(e,i)),A(e,t.display?.columns||[]),N(e)}const F=[{value:"eq",label:"= equals"},{value:"ne",label:"\u2260 not equals"},{value:"gt",label:"> greater than"},{value:"lt",label:"< less than"},{value:"gte",label:"\u2265 greater or equal"},{value:"lte",label:"\u2264 less or equal"},{value:"contains",label:"~ contains (regex)"},{value:"in",label:"\u2208 in (comma list)"}];function y(e=!1){const t=[];return e&&t.push({value:"",label:"\u2014 select field \u2014"}),(f||[]).forEach(n=>{t.push({value:`data.${n.name}`,label:`${n.label||n.name} (${n.name})`})}),t.length===0||t.length===1&&e?(t.push({value:"__meta.createdAt",label:"Created At"}),t.push({value:"__meta.updatedAt",label:"Updated At"})):(t.push({value:"__meta.createdAt",label:"Created At (meta)"}),t.push({value:"__meta.updatedAt",label:"Updated At (meta)"})),t}function h(e,t){const n=document.createElement("select");return n.className="form-input form-input--sm",e.forEach(a=>{const l=document.createElement("option");l.value=a.value,l.textContent=a.label,String(a.value)===String(t)&&(l.selected=!0),n.appendChild(l)}),n}function S(e,t,n="text"){const a=document.createElement("input");return a.type=n,a.className="form-input form-input--sm",a.placeholder=e,a.value=t??"",a}function B(e){if(!f?.length)return null;const t=e?.startsWith("data.")?e.slice(5):null;return t&&f.find(n=>n.name===t)||null}function b(e,t,n=""){const a=B(e);if(a?.type==="select"&&(t==="eq"||t==="ne")&&a.options?.length){const d=document.createElement("select");d.className="form-input form-input--sm match-val",d.style.flex="1";const c=document.createElement("option");return c.value="",c.textContent="\u2014 select value \u2014",d.appendChild(c),(a.options||[]).forEach(o=>{const s=typeof o=="string"?o:o.value??"",r=typeof o=="string"?o:o.label||s;if(!s||s==="undefined")return;const u=document.createElement("option");u.value=s,u.textContent=r,s===n&&(u.selected=!0),d.appendChild(u)}),d}const l=t==="in"?"val1, val2, val3":t==="contains"?"search pattern (regex)":t==="gt"||t==="lt"||t==="gte"||t==="lte"?"0":"value",i=document.createElement("input");return i.type="text",i.className="form-input form-input--sm match-val",i.style.flex="1",i.placeholder=l,i.value=n,i}function $(e){const t=[],n={$eq:"eq",$ne:"ne",$gt:"gt",$lt:"lt",$gte:"gte",$lte:"lte",$in:"in"};return Object.entries(e||{}).forEach(([a,l])=>{if(typeof l=="object"&&l!==null&&!Array.isArray(l)){if("$regex"in l){t.push({field:a,op:"contains",val:String(l.$regex??"")});return}Object.entries(l).forEach(([i,d])=>{const c=n[i];c&&t.push({field:a,op:c,val:Array.isArray(d)?d.join(", "):String(d)})})}else t.push({field:a,op:"eq",val:String(l??"")})}),t}function x(e,t,n="",a="eq",l=""){const i=document.createElement("div");i.className="match-condition-row",i.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;flex-wrap:wrap;";const d=h([{value:"",label:"\u2014 field \u2014"},...y()],n);d.className+=" match-field",d.style.minWidth="140px";const c=h(F,a);c.className+=" match-op",c.style.minWidth="160px";const o=document.createElement("div");o.className="match-val-wrap",o.style.cssText="flex:1;min-width:120px;display:flex;",o.appendChild(b(n,a,l));const s=()=>{const m=o.querySelector(".match-val")?.value??"";for(;o.firstChild;)o.removeChild(o.firstChild);o.appendChild(b(d.value,c.value,m))};d.addEventListener("change",s),c.addEventListener("change",s);const r=document.createElement("button");r.type="button",r.className="btn btn-sm btn-ghost",r.title="Remove";const u=document.createElement("span");u.setAttribute("data-icon","x"),r.appendChild(u),r.addEventListener("click",()=>i.remove()),i.appendChild(d),i.appendChild(c),i.appendChild(o),i.appendChild(r),e.insertBefore(i,t),Domma.icons.scan(i)}function W(e,t={}){for(;e.firstChild;)e.removeChild(e.firstChild);const n=t.$match||t||{},a="$or"in n;let l=[];a?(n.$or||[]).forEach(r=>$(r).forEach(u=>l.push(u))):l=$(n);const i=document.createElement("div");i.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.6rem;";const d=document.createElement("label");d.className="form-label",d.style.marginBottom="0",d.textContent="Match:";const c=document.createElement("select");c.className="form-input form-input--sm match-logic",c.style.width="auto",[{value:"and",label:"ALL conditions (AND)"},{value:"or",label:"ANY condition (OR)"}].forEach(r=>{const u=document.createElement("option");u.value=r.value,u.textContent=r.label,(a?"or":"and")===r.value&&(u.selected=!0),c.appendChild(u)}),i.appendChild(d),i.appendChild(c),e.appendChild(i);const o=document.createElement("button");o.type="button",o.className="btn btn-ghost btn-sm match-add-btn";const s=document.createElement("span");s.setAttribute("data-icon","plus"),o.appendChild(s),o.appendChild(document.createTextNode(" Add Condition")),o.addEventListener("click",()=>{x(e,o),Domma.icons.scan(e)}),e.appendChild(o),l.forEach(r=>x(e,o,r.field,r.op,r.val)),l.length===0&&x(e,o),Domma.icons.scan(e)}function J(e){e.querySelectorAll(".match-condition-row").forEach(t=>{const n=t.querySelector(".match-field");if(n){const d=n.value,c=h([{value:"",label:"\u2014 field \u2014"},...y()],d);c.className=n.className,c.style.cssText=n.style.cssText;const o=t.querySelector(".match-val-wrap"),s=t.querySelector(".match-op");o&&s&&c.addEventListener("change",()=>{const r=o.querySelector(".match-val")?.value??"";for(;o.firstChild;)o.removeChild(o.firstChild);o.appendChild(b(c.value,s.value,r))}),n.parentNode.replaceChild(c,n)}const a=t.querySelector(".match-val-wrap"),l=t.querySelector(".match-field")?.value,i=t.querySelector(".match-op")?.value;if(a&&l&&i){const d=a.querySelector(".match-val")?.value??"";for(;a.firstChild;)a.removeChild(a.firstChild);a.appendChild(b(l,i,d))}})}function M(e){const t=e.querySelector(".match-logic")?.value||"and",n=l=>{const i=l.querySelector(".match-field")?.value?.trim(),d=l.querySelector(".match-op")?.value,c=l.querySelector(".match-val")?.value?.trim()??"";if(!i)return null;const o=i.startsWith("__")?i.slice(2):i,s={};switch(d){case"eq":s[o]=c;break;case"ne":s[o]={$ne:c};break;case"gt":s[o]={$gt:isNaN(c)?c:Number(c)};break;case"lt":s[o]={$lt:isNaN(c)?c:Number(c)};break;case"gte":s[o]={$gte:isNaN(c)?c:Number(c)};break;case"lte":s[o]={$lte:isNaN(c)?c:Number(c)};break;case"contains":s[o]={$regex:c,$options:"i"};break;case"in":s[o]={$in:c.split(",").map(r=>r.trim()).filter(Boolean)};break}return Object.keys(s).length?s:null},a=[];return e.querySelectorAll(".match-condition-row").forEach(l=>{const i=n(l);i&&a.push(i)}),a.length===0?{}:t==="or"?{$or:a}:Object.assign({},...a)}function I(e,t={}){for(;e.firstChild;)e.removeChild(e.firstChild);const n=t.$sort||t||{},a=Object.entries(n),l=document.createElement("button");l.type="button",l.className="btn btn-ghost btn-sm";const i=document.createElement("span");i.setAttribute("data-icon","plus"),l.appendChild(i),l.appendChild(document.createTextNode(" Add Sort")),e.appendChild(l);const d=(c="",o=1)=>{const s=document.createElement("div");s.className="sort-row",s.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;";const r=h([{value:"",label:"\u2014 field \u2014"},...y()],c);r.className+=" sort-field";const u=h([{value:"1",label:"\u2191 Ascending"},{value:"-1",label:"\u2193 Descending"}],String(o));u.className+=" sort-dir";const m=document.createElement("button");m.type="button",m.className="btn btn-sm btn-ghost",m.title="Remove";const p=document.createElement("span");p.setAttribute("data-icon","x"),m.appendChild(p),m.addEventListener("click",()=>{s.remove()}),s.appendChild(r),s.appendChild(u),s.appendChild(m),e.insertBefore(s,l),Domma.icons.scan(s)};l.addEventListener("click",()=>{d()}),a.forEach(([c,o])=>d(c,o)),a.length===0&&d(),Domma.icons.scan(e)}function U(e){e.querySelectorAll(".sort-row").forEach(t=>{const n=t.querySelector(".sort-field"),a=n?.value,l=h([{value:"",label:"\u2014 field \u2014"},...y()],a);l.className=n.className,n.parentNode.replaceChild(l,n)})}function K(e){const t={};return e.querySelectorAll(".sort-row").forEach(n=>{const a=n.querySelector(".sort-field")?.value?.trim(),l=parseInt(n.querySelector(".sort-dir")?.value,10)||1;if(!a)return;const i=a.startsWith("__")?a.slice(2):a;t[i]=l}),t}function q(e,t){const n=e.find("#pipeline-stages-list").get(0);if(!n)return;const a=n.querySelector(".stage-empty-placeholder");a&&a.remove();const l=["$match","$sort"].includes(t.type),i=document.createElement("div");i.className="card mb-2 stage-card",i.dataset.stageType=t.type,l&&(i.dataset.guided="true");const d=document.createElement("div");d.className="card-header",d.style.cssText="display:flex;align-items:center;gap:.5rem;";const c=document.createElement("code");c.textContent=t.type,c.style.cssText="flex:1;font-size:.85rem;";const o=document.createElement("button");o.type="button",o.className="btn btn-sm btn-ghost";const s=document.createElement("span");s.setAttribute("data-icon","trash"),o.appendChild(s),o.addEventListener("click",()=>{i.remove(),n.querySelector(".stage-card")||n.appendChild(P())}),d.appendChild(c),d.appendChild(o);const r=document.createElement("div");if(r.className="card-body",l){const u=document.createElement("div");u.className="stage-guided",r.appendChild(u),t.type==="$match"&&W(u,t.config),t.type==="$sort"&&I(u,t.config)}else{const u=document.createElement("label");u.className="form-label",u.textContent="Stage Config (JSON)";const m=document.createElement("small");m.className="text-muted",m.style.cssText="display:block;margin-bottom:.4rem;",m.textContent=`Enter the inner config for ${t.type} \u2014 e.g. for $lookup: { from, localField, foreignField, as }. Do not wrap in { "${t.type}": ... }.`;const p=document.createElement("textarea");p.className="form-input stage-config",p.rows=5,p.style.cssText="font-family:monospace;font-size:.8rem;resize:vertical;",p.placeholder="{}",p.value=Object.keys(t.config||{}).length?JSON.stringify(t.config,null,2):"",r.appendChild(u),r.appendChild(m),r.appendChild(p)}i.appendChild(d),i.appendChild(r),n.appendChild(i),Domma.icons.scan(i)}function P(){const e=document.createElement("p");return e.className="text-muted stage-empty-placeholder",e.textContent="No stages yet. Add a stage to filter, join, or transform your data.",e.style.cssText="text-align:center;padding:2rem 0;",e}function Y(e){const t=[];return e.find(".stage-card").each(function(){const n=this.dataset.stageType,a=this.querySelector(".stage-guided");let l;if(a)n==="$match"&&(l=M(a)),n==="$sort"&&(l=K(a));else{const i=this.querySelector(".stage-config")?.value?.trim()||"{}";try{l=JSON.parse(i)}catch{throw new Error(`Invalid JSON in ${n} stage config`)}}t.push({type:n,config:l})}),t}function A(e,t){const n=e.find("#view-columns-builder").get(0);if(!n)return;for(;n.firstChild;)n.removeChild(n.firstChild);const a=t??(f||[]).map(c=>({key:`data.${c.name}`,label:c.label||c.name})),l=(c="",o="")=>{const s=document.createElement("div");s.className="col-row",s.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;";const r=f?.length?h([{value:"",label:"\u2014 field \u2014"},...y()],c):S("data.fieldName",c);r.className+=" col-key";const u=S("Display label",o);u.className+=" col-label",u.style.flex="1",f?.length&&r.tagName==="SELECT"&&r.addEventListener("change",()=>{if(!u.value){const w=(f||[]).find(j=>`data.${j.name}`===r.value);w&&(u.value=w.label||w.name)}});const m=document.createElement("button");m.type="button",m.className="btn btn-sm btn-ghost",m.title="Remove column";const p=document.createElement("span");p.setAttribute("data-icon","x"),m.appendChild(p),m.addEventListener("click",()=>{s.remove(),Domma.icons.scan(n)}),s.appendChild(r),s.appendChild(u),s.appendChild(m),n.insertBefore(s,n.lastChild),Domma.icons.scan(s)};a.forEach(c=>l(c.key,c.label));const i=document.createElement("button");i.type="button",i.className="btn btn-ghost btn-sm";const d=document.createElement("span");d.setAttribute("data-icon","plus"),i.appendChild(d),i.appendChild(document.createTextNode(" Add Column")),i.addEventListener("click",()=>{l(),Domma.icons.scan(n)}),n.appendChild(i),Domma.icons.scan(n)}function G(e){const t=[];return e.find(".col-row").each(function(){const n=this.querySelector(".col-key")?.value?.trim(),a=this.querySelector(".col-label")?.value?.trim();n&&t.push({key:n,label:a||n})}),t}function H(e){const t=e.find("#view-display-mode").get(0);t&&(t.addEventListener("change",()=>N(e)),N(e))}function N(e){const t=e.find("#view-display-mode").val()||"table",n=e.find("#view-columns-section").get(0),a=e.find("#view-block-section").get(0);n&&(n.style.display=t==="table"?"":"none"),a&&(a.style.display=t==="block"?"":"none")}function Q(e){const t=e.find("#view-rowlevel-enabled").get(0),n=e.find("#view-rowlevel-config").get(0),a=e.find("#view-rowlevel-mode").get(0),l=e.find("#view-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{n&&(n.style.display=t.checked?"flex":"none")}),a&&a.addEventListener("change",()=>{l&&(l.style.display=a.value==="field"?"":"none")}))}async function X(e){const t=e.find("#view-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const n=e.find("#view-source").val();if(!n){E.toast("Source collection is required (Source tab).",{type:"warning"});return}let a;try{a=Y(e)}catch(u){E.toast(u.message,{type:"error"});return}const l=G(e),i=[];e.find(".view-role-cb:checked").each(function(){i.push(this.value)});const d=e.find("#view-rowlevel-enabled").is(":checked");let c=null;if(d){const u=e.find("#view-rowlevel-mode").val()||"owner",m=e.find("#view-rowlevel-userkey").val()||"id";if(c={mode:u,userKey:m},u==="field"){const p=e.find("#view-rowlevel-field").val().trim();if(!p){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}c.field=p}}const o=e.find("#view-project").val()||"",s={title:t,slug:e.find("#view-slug").val().trim()||void 0,description:e.find("#view-description").val().trim(),...e.find("#view-bundled").is(":checked")?{bundled:!0}:{},connection:e.find("#view-connection").val()||"default",pipeline:{source:n,stages:a},display:{mode:e.find("#view-display-mode").val()||"table",columns:l,pageSize:parseInt(e.find("#view-page-size").val(),10)||25,block:e.find("#view-block-name").val()||""},access:{roles:i,public:e.find("#view-public").is(":checked"),rowLevel:c},meta:{...C||{},project:o||null}},r=e.find("#save-view-btn").get(0);r&&(r.disabled=!0);try{if(g)await v.views.update(g,s),E.toast("View updated.",{type:"success"});else{const u=await v.views.create(s);E.toast("View created.",{type:"success"}),R.navigate(`/views/edit/${u.slug}`)}}catch(u){E.toast(u.message||"Failed to save view.",{type:"error"})}finally{r&&(r.disabled=!1)}}
@@ -1 +1 @@
1
- import{api as l}from"../api.js";import{filterByProject as m,getProjectFromHash as u}from"../lib/project-context.js";import{keepTableIconsAlive as w}from"../lib/table-icons.js";function n(i){return String(i).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export const viewsListView={templateUrl:"/admin/js/templates/views-list.html",async onMount(i){await d(i),i.find("#create-view-btn").off("click").on("click",()=>{R.navigate("/views/new")}),Domma.icons.scan(),w(i.get(0))}};async function d(i){const p=E.loader(i.get(0),{type:"dots"});let r=[];try{r=await l.views.list()}catch{E.toast("Could not load views.",{type:"error"})}finally{p.destroy()}const c=u();c&&(r=m(r,c)),T.create("#views-table",{data:r,columns:[{key:"title",title:"Title",render:(e,t)=>{const a=document.createElement("a");return a.href=`#/views/${n(t.slug)}/preview`,a.textContent=e,a.style.fontWeight="600",a.outerHTML}},{key:"slug",title:"Slug",render:e=>`<code>${n(e)}</code>`},{key:"pipeline",title:"Source",render:e=>`<code>${n(e?.source||"\u2014")}</code>`},{key:"display",title:"Mode",render:e=>n(e?.mode||"table")},{key:"access",title:"Roles",render:e=>(e?.roles||[]).map(t=>`<span class="badge badge-secondary">${n(t)}</span>`).join(" ")},{key:"slug",title:"Actions",render:e=>{const t=document.createElement("div");t.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;";const a=document.createElement("a");a.href=`#/views/${n(e)}/preview`,a.className="btn btn-sm btn-ghost",a.setAttribute("data-tooltip","Preview"),a.innerHTML='<span data-icon="eye" data-icon-size="14"></span>';const s=document.createElement("a");s.href=`#/views/edit/${n(e)}`,s.className="btn btn-sm btn-ghost",s.setAttribute("data-tooltip","Edit"),s.innerHTML='<span data-icon="edit" data-icon-size="14"></span>';const o=document.createElement("button");return o.className="btn btn-sm btn-danger js-delete-view",o.dataset.slug=e,o.setAttribute("data-tooltip","Delete"),o.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',t.appendChild(a),t.appendChild(s),t.appendChild(o),t.outerHTML}}],emptyMessage:'No views yet. Click "New View" to create your first view.'}),i.off("click",".js-delete-view").on("click",".js-delete-view",async function(){const e=this.dataset.slug;if(await E.confirm(`Delete view "${e}"? This cannot be undone.`))try{await l.views.delete(e),E.toast("View deleted.",{type:"success"}),await d(i)}catch{E.toast("Failed to delete view.",{type:"error"})}}),Domma.icons.scan(),document.querySelectorAll("#views-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})}
1
+ import{api as r}from"../api.js";import{filterByProject as m,getProjectFromHash as u}from"../lib/project-context.js";import{keepTableIconsAlive as w}from"../lib/table-icons.js";function s(a){return String(a).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export const viewsListView={templateUrl:"/admin/js/templates/views-list.html",async onMount(a){await d(a),a.find("#create-view-btn").off("click").on("click",()=>{R.navigate("/views/new")}),Domma.icons.scan(),w(a.get(0))}};async function d(a){const p=E.loader(a.get(0),{type:"dots"});let c=[];try{c=await r.views.list()}catch{E.toast("Could not load views.",{type:"error"})}finally{p.destroy()}const l=u();l&&(c=m(c,l)),T.create("#views-table",{data:c,columns:[{key:"title",title:"Title",render:(e,t)=>{const i=document.createElement("a");return i.href=`#/views/${s(t.slug)}/preview`,i.textContent=e,i.style.fontWeight="600",i.outerHTML}},{key:"slug",title:"Slug",render:e=>`<code>${s(e)}</code>`},{key:"pipeline",title:"Source",render:e=>`<code>${s(e?.source||"\u2014")}</code>`},{key:"display",title:"Mode",render:e=>s(e?.mode||"table")},{key:"access",title:"Roles",render:e=>(e?.roles||[]).map(t=>`<span class="badge badge-secondary">${s(t)}</span>`).join(" ")},{key:"slug",title:"Actions",render:e=>{const t=document.createElement("div");t.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;";const i=document.createElement("a");i.href=`#/views/${s(e)}/preview`,i.className="btn btn-sm btn-ghost",i.setAttribute("data-tooltip","Preview"),i.innerHTML='<span data-icon="eye" data-icon-size="14"></span>';const o=document.createElement("a");o.href=`#/views/edit/${s(e)}`,o.className="btn btn-sm btn-ghost",o.setAttribute("data-tooltip","Edit"),o.innerHTML='<span data-icon="edit" data-icon-size="14"></span>';const n=document.createElement("button");return n.className="btn btn-sm btn-ghost js-delete-view",n.dataset.slug=e,n.setAttribute("data-tooltip","Delete"),n.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',t.appendChild(i),t.appendChild(o),t.appendChild(n),t.outerHTML}}],emptyMessage:'No views yet. Click "New View" to create your first view.'}),a.off("click",".js-delete-view").on("click",".js-delete-view",async function(){const e=this.dataset.slug;if(await E.confirm(`Delete view "${e}"? This cannot be undone.`))try{await r.views.delete(e),E.toast("View deleted.",{type:"success"}),await d(a)}catch{E.toast("Failed to delete view.",{type:"error"})}}),Domma.icons.scan(),document.querySelectorAll("#views-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.36.11",
3
+ "version": "0.36.12",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -4,7 +4,7 @@
4
4
  * Draft pages are not served publicly.
5
5
  * The admin panel is excluded (handled by static serving).
6
6
  */
7
- import {getPage} from '../services/content.js';
7
+ import {getPage, getPageMtime} from '../services/content.js';
8
8
  import {getProjectForPage, isProjectEnabled} from '../services/projects.js';
9
9
  import {renderPage} from '../services/renderer.js';
10
10
  import {buildRobotsTxt, generate as generateSitemap} from '../services/sitemap.js';
@@ -155,7 +155,11 @@ export async function publicRoutes(fastify) {
155
155
  }
156
156
 
157
157
  const baseUrl = getBaseUrl(request);
158
- const cacheKey = `page:${urlPath}:role${userRole ?? 'anon'}:o${baseUrl}`;
158
+ // mtime in the key: any change to the backing file — editor save,
159
+ // script, git pull — yields a fresh entry even if no invalidation
160
+ // hook fired. Stale-mtime entries age out via TTL/LRU.
161
+ const pageMtime = await getPageMtime(page.urlPath || urlPath);
162
+ const cacheKey = `page:${urlPath}:m${pageMtime}:role${userRole ?? 'anon'}:o${baseUrl}`;
159
163
  const cacheTags = [`page:${urlPath}`, ...(page.tags || []), 'nav', 'site'];
160
164
  const html = await cache.wrap(
161
165
  cacheKey,
@@ -48,6 +48,25 @@ export async function listPages() {
48
48
  * that gate output by role (currently `[menu]`) can filter correctly.
49
49
  * @returns {Promise<object|null>}
50
50
  */
51
+ /**
52
+ * Millisecond mtime of the page's backing file. The public route folds this
53
+ * into the response-cache key so out-of-band file edits (scripts, git pulls,
54
+ * rsync deploys) invalidate cached pages naturally, without depending on
55
+ * service-layer hooks having fired.
56
+ *
57
+ * @param {string} urlPath
58
+ * @returns {Promise<number>} 0 when the file cannot be resolved/statted.
59
+ */
60
+ export async function getPageMtime(urlPath) {
61
+ try {
62
+ const filePath = await resolveExistingFilePath(urlPath);
63
+ if (!filePath) return 0;
64
+ return Math.trunc((await fs.stat(filePath)).mtimeMs);
65
+ } catch {
66
+ return 0;
67
+ }
68
+ }
69
+
51
70
  export async function getPage(urlPath, opts = {}) {
52
71
  const filePath = await resolveExistingFilePath(urlPath);
53
72
  try {