domma-cms 0.66.0 → 0.69.4
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.
- package/CLAUDE.md +64 -1
- package/README.md +14 -10
- package/admin/css/admin.css +1 -1
- package/admin/index.html +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/analytics-shape.js +1 -0
- package/admin/js/lib/contacts-arrange.js +1 -0
- package/admin/js/lib/help-popover.js +1 -0
- package/admin/js/lib/notes-arrange.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -1
- package/admin/js/lib/plugin-chrome.js +1 -1
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-grouping.test.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +3 -3
- package/admin/js/lib/todo-arrange.js +1 -0
- package/admin/js/lib/tool-kit.js +1 -0
- package/admin/js/templates/analytics.html +138 -0
- package/admin/js/templates/contacts.html +156 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/admin/js/templates/theme.html +38 -0
- package/admin/js/templates/todo.html +113 -0
- package/admin/js/views/analytics.js +62 -0
- package/admin/js/views/contacts.js +174 -0
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/notes.js +104 -0
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +24 -22
- package/admin/js/views/theme.js +1 -1
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +4 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +14 -0
- package/config/menus/admin-sidebar.json +1 -1
- package/config/server.json +3 -1
- package/package.json +3 -7
- package/plugins/_lib/admin/mail/reader-view.js +20 -0
- package/plugins/_lib/mail/store.js +21 -1
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -1
- package/plugins/blog/admin/templates/blog.html +35 -53
- package/plugins/blog/admin/templates/categories.html +19 -6
- package/plugins/blog/admin/templates/comments.html +26 -9
- package/plugins/blog/admin/templates/post-editor.html +36 -13
- package/plugins/blog/admin/templates/settings.html +2 -11
- package/plugins/blog/admin/views/blog.js +288 -162
- package/plugins/blog/admin/views/categories.js +212 -201
- package/plugins/blog/admin/views/comments.js +207 -158
- package/plugins/blog/admin/views/kit.js +372 -0
- package/plugins/blog/admin/views/post-editor.js +269 -21
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -86
- package/plugins/blog/blocks/blog-card-compact.css +17 -0
- package/plugins/blog/blocks/blog-card-compact.html +8 -0
- package/plugins/blog/blocks/blog-card-row.css +22 -0
- package/plugins/blog/blocks/blog-card-row.html +11 -0
- package/plugins/blog/blocks/blog-card.css +35 -0
- package/plugins/blog/blocks/blog-card.html +16 -0
- package/plugins/blog/blocks/blog-post-classic.css +15 -0
- package/plugins/blog/blocks/blog-post-classic.html +28 -0
- package/plugins/blog/blocks/blog-post-feature.css +20 -0
- package/plugins/blog/blocks/blog-post-feature.html +30 -0
- package/plugins/blog/blocks/blog-post-minimal.css +10 -0
- package/plugins/blog/blocks/blog-post-minimal.html +23 -0
- package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
- package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
- package/plugins/blog/collections/posts/schema.json +3 -1
- package/plugins/blog/config.js +6 -0
- package/plugins/blog/lib/layouts.js +304 -0
- package/plugins/blog/lib/page.js +158 -0
- package/plugins/blog/lib/render.js +119 -0
- package/plugins/blog/lib/samples.js +234 -0
- package/plugins/blog/plugin.js +225 -15
- package/plugins/blog/plugin.json +21 -81
- package/plugins/blog/plugin.public.js +115 -210
- package/plugins/blog/public/blog.css +177 -0
- package/plugins/blog/public/blog.js +586 -0
- package/plugins/blog/public/samples/aurora.svg +7 -0
- package/plugins/blog/public/samples/dusk.svg +7 -0
- package/plugins/blog/public/samples/ember.svg +7 -0
- package/plugins/blog/public/samples/harbour.svg +7 -0
- package/plugins/blog/public/samples/meadow.svg +7 -0
- package/plugins/blog/public/samples/rosewood.svg +7 -0
- package/plugins/blog/templates/index.html +9 -8
- package/plugins/blog/templates/post.html +4 -17
- package/plugins/blog/tests/layouts.test.js +97 -0
- package/plugins/blog/tests/public.test.js +71 -0
- package/plugins/free-tier.lock.json +74 -0
- package/plugins/mail-reader/CLAUDE.md +78 -0
- package/plugins/mail-reader/plugin.json +2 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +41 -2
- package/scripts/run-tests.mjs +94 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/{plugins/contacts/plugin.js → server/routes/api/contacts.js} +71 -43
- package/server/routes/api/dashboard.js +8 -2
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +148 -3
- package/server/routes/api/todo.js +178 -0
- package/server/server.js +61 -1
- package/server/services/collections.js +13 -3
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +54 -1
- package/server/services/permissionRegistry.js +50 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +117 -7
- package/server/services/presetCollections.js +344 -0
- package/server/services/promoted-plugins-migration.js +254 -0
- package/server/services/publicCors.js +46 -0
- package/server/services/renderer.js +83 -3
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +310 -0
- package/server/services/themeSettings.js +39 -0
- package/config/theme.json +0 -18
- package/plugins/analytics/admin/css/index.css +0 -1
- package/plugins/analytics/admin/templates/analytics.html +0 -70
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -60
- package/plugins/analytics/public/inject-body.html +0 -60
- package/plugins/analytics/public/inject-head.html +0 -1
- package/plugins/blog/templates/author.html +0 -9
- package/plugins/blog/templates/category.html +0 -9
- package/plugins/blog/templates/tag.html +0 -9
- package/plugins/contacts/admin/templates/contacts.html +0 -254
- package/plugins/contacts/admin/views/contacts.js +0 -943
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -179
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/plugin.json +0 -36
- package/plugins/demo-viewer/config.js +0 -4
- package/plugins/demo-viewer/plugin.js +0 -36
- package/plugins/demo-viewer/plugin.json +0 -9
- package/plugins/invoice/admin/css/index.css +0 -1
- package/plugins/invoice/admin/templates/editor.html +0 -220
- package/plugins/invoice/admin/templates/index.html +0 -173
- package/plugins/invoice/admin/templates/issuers.html +0 -2
- package/plugins/invoice/admin/templates/receivers.html +0 -2
- package/plugins/invoice/admin/views/contacts-source.js +0 -266
- package/plugins/invoice/admin/views/editor.js +0 -434
- package/plugins/invoice/admin/views/export.js +0 -199
- package/plugins/invoice/admin/views/help-content.js +0 -61
- package/plugins/invoice/admin/views/index.js +0 -643
- package/plugins/invoice/admin/views/issuers.js +0 -30
- package/plugins/invoice/admin/views/media.js +0 -172
- package/plugins/invoice/admin/views/party-view.js +0 -386
- package/plugins/invoice/admin/views/payments.js +0 -127
- package/plugins/invoice/admin/views/print.js +0 -130
- package/plugins/invoice/admin/views/receivers.js +0 -55
- package/plugins/invoice/admin/views/send.js +0 -212
- package/plugins/invoice/admin/views/settings.js +0 -594
- package/plugins/invoice/admin/views/view-lifecycle.js +0 -33
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -82
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -16
- package/plugins/invoice/collections/invoices/schema.json +0 -33
- package/plugins/invoice/config.js +0 -37
- package/plugins/invoice/pdf.js +0 -164
- package/plugins/invoice/plugin.js +0 -1456
- package/plugins/invoice/plugin.json +0 -86
- package/plugins/invoice/templates/_base.css +0 -1
- package/plugins/invoice/templates/classic-nologo.html +0 -100
- package/plugins/invoice/templates/classic.html +0 -91
- package/plugins/invoice/templates/invoice-print.html +0 -237
- package/plugins/invoice/templates/minimal.html +0 -99
- package/plugins/invoice/templates/modern-nologo.html +0 -114
- package/plugins/invoice/templates/modern.html +0 -113
- package/plugins/invoice/templates/templates.json +0 -11
- package/plugins/notes/admin/templates/notes.html +0 -83
- package/plugins/notes/admin/views/notes.js +0 -281
- package/plugins/notes/collections/user-notes/schema.json +0 -54
- package/plugins/notes/config.js +0 -6
- package/plugins/notes/plugin.json +0 -33
- package/plugins/surveys/admin/templates/audience.html +0 -47
- package/plugins/surveys/admin/templates/results.html +0 -58
- package/plugins/surveys/admin/templates/survey-editor.html +0 -141
- package/plugins/surveys/admin/templates/surveys.html +0 -25
- package/plugins/surveys/admin/views/audience.js +0 -301
- package/plugins/surveys/admin/views/results.js +0 -172
- package/plugins/surveys/admin/views/survey-editor.js +0 -211
- package/plugins/surveys/admin/views/surveys.js +0 -161
- package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
- package/plugins/surveys/collections/survey-groups/schema.json +0 -11
- package/plugins/surveys/collections/survey-invites/schema.json +0 -16
- package/plugins/surveys/collections/surveys/schema.json +0 -23
- package/plugins/surveys/config.js +0 -8
- package/plugins/surveys/lib/audience.js +0 -37
- package/plugins/surveys/lib/campaigns.js +0 -43
- package/plugins/surveys/lib/ledger.js +0 -110
- package/plugins/surveys/lib/sending.js +0 -106
- package/plugins/surveys/lib/stats.js +0 -62
- package/plugins/surveys/lib/submit.js +0 -95
- package/plugins/surveys/lib/tokens.js +0 -28
- package/plugins/surveys/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- package/plugins/surveys/plugin.public.js +0 -149
- package/plugins/surveys/public/dist-shared.mjs +0 -1
- package/plugins/surveys/public/survey.css +0 -1
- package/plugins/surveys/public/survey.mjs +0 -1
- package/plugins/surveys/templates/survey-page.html +0 -63
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
- package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
- package/plugins/theme-switcher/config.js +0 -10
- package/plugins/theme-switcher/plugin.js +0 -26
- package/plugins/theme-switcher/plugin.json +0 -74
- package/plugins/theme-switcher/public/inject-body.html +0 -153
- package/plugins/theme-switcher/public/inject-head.html +0 -260
- package/plugins/todo/admin/templates/todo.html +0 -158
- package/plugins/todo/admin/views/todo.js +0 -343
- package/plugins/todo/collections/todos/schema.json +0 -60
- package/plugins/todo/config.js +0 -7
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
- /package/{plugins/contacts/admin/css/index.css → admin/css/contacts.css} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_SETTINGS=Object.freeze({layout:"cards",sort:"updated",previewLength:160,showCategories:!0,confirmDelete:!0,defaultCategories:[]});const p=new Set(["cards","list"]),f=new Set(["updated","created","title"]);export const PREVIEW_MIN=0,PREVIEW_MAX=600;export function normaliseSettings(r){const e=r&&typeof r=="object"?r:{},t=DEFAULT_SETTINGS,o=Number(e.previewLength);return{layout:p.has(e.layout)?e.layout:t.layout,sort:f.has(e.sort)?e.sort:t.sort,previewLength:Number.isFinite(o)?Math.min(PREVIEW_MAX,Math.max(PREVIEW_MIN,Math.round(o))):t.previewLength,showCategories:typeof e.showCategories=="boolean"?e.showCategories:t.showCategories,confirmDelete:typeof e.confirmDelete=="boolean"?e.confirmDelete:t.confirmDelete,defaultCategories:parseCategories(e.defaultCategories)}}export function parseCategories(r){const e=Array.isArray(r)?r:String(r??"").split(","),t=new Set,o=[];for(const i of e){const a=String(i??"").trim();!a||t.has(a.toLowerCase())||(t.add(a.toLowerCase()),o.push(a))}return o}export function collectCategories(r){return parseCategories((r||[]).flatMap(t=>Array.isArray(t.categories)?t.categories:[])).sort((t,o)=>t.localeCompare(o,void 0,{sensitivity:"base"}))}export function preview(r,e){const t=String(r??"").replace(/\s+/g," ").trim();if(!e||e<=0)return"";if(t.length<=e)return t;const o=t.slice(0,e),i=o.lastIndexOf(" ");return(i>e*.6?o.slice(0,i):o).trimEnd()+"\u2026"}const c=r=>{const e=Date.parse(r);return Number.isNaN(e)?0:e};export function arrangeNotes(r,{query:e="",category:t="",sort:o="updated"}={}){const i=String(e).trim().toLowerCase(),a=String(t).trim().toLowerCase(),l=(r||[]).filter(s=>{const n=Array.isArray(s.categories)?s.categories:[];return a&&!n.some(u=>String(u).toLowerCase()===a)?!1:i?String(s.title??"").toLowerCase().includes(i)||String(s.content??"").toLowerCase().includes(i)||n.some(u=>String(u).toLowerCase().includes(i)):!0}),d=o==="title"?(s,n)=>String(s.title||"").localeCompare(String(n.title||""),void 0,{sensitivity:"base"}):o==="created"?(s,n)=>c(n.createdAt)-c(s.createdAt):(s,n)=>c(n.updatedAt)-c(s.updatedAt);return[...l].sort(d)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
function r(e){let t=0;for(let n=0;n<e.length;n++)t=(t*31+e.charCodeAt(n))%360;return t}function a(e){return e==="_template"?0:62}export function pluginAccent(e){const t=String(e||"");return`hsl(${r(t)} ${a(t)}% 52%)`}export function pluginWatermark(e){const t=document.createElement("span");return t.className="plugin-watermark",t.setAttribute("data-icon",e||"package"),t.setAttribute("data-icon-size","148"),t.setAttribute("aria-hidden","true"),t}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const m=/^(built in|included\b)/i;export function formatReleased(e){if(!e||typeof e!="string"||!/^\d{4}-\d{2}-\d{2}/.test(e))return"";const t=new Date(e);if(Number.isNaN(t.getTime()))return"";const n=a=>String(a).padStart(2,"0");return`${n(t.getDate())}.${n(t.getMonth()+1)}.${t.getFullYear()}`}export function expiryFact(e,t=new Date){if(!e||typeof e!="object")return null;const n=formatReleased(e.date);if(!n)return null;const a=e.kind==="rental",i=e.state==="support-ended"||e.state==="expired"||!a&&Date.parse(e.date)<t.getTime(),s=Date.parse(e.date)-t.getTime()<30*864e5;let o;return a?o=e.state==="grace"?`Expired ${n} (grace)`:`Expires ${n}`:o=i?`Support ended ${n}`:`Support to ${n}`,{text:o,title:a?"Licence expiry":"Support and updates expiry - the licence itself does not expire",tone:i||e.state==="grace"?a?"danger":"warning":s?"warning":"ok"}}export function buildBanner(e){const t=e||{},n=document.createElement("header");n.className="dm-plugin-banner";const a=document.createElement("div");a.className="dm-plugin-banner-identity";const i=document.createElement("span");i.className="dm-plugin-banner-icon",i.setAttribute("data-icon",t.icon||"package"),i.setAttribute("data-icon-size","18"),a.appendChild(i);const s=document.createElement("h1");s.className="dm-plugin-banner-name",s.textContent=t.displayName||t.plugin||"Plugin",a.appendChild(s),n.appendChild(a);const o=document.createElement("div");o.className="dm-plugin-banner-facts",((c,d)=>{if(!c)return;const p=document.createElement("span");p.className="dm-plugin-fact"+(d?" "+d:""),p.textContent=c,o.appendChild(p)})(t.version?`v${t.version}`:"","dm-plugin-fact--version");const r=formatReleased(t.date);if(r){const c=document.createElement("span");c.className="dm-plugin-fact dm-plugin-fact--date",c.title="Release date",c.textContent=r,o.appendChild(c)}if(t.licence){const c=document.createElement("span");c.className="dm-plugin-fact dm-plugin-fact--licence"+(m.test(t.licence)?" is-included":" is-licensed"),c.title="Licence",c.textContent=t.licence,o.appendChild(c)}const l=expiryFact(t.expiry);if(l){const c=document.createElement("span");c.className=`dm-plugin-fact dm-plugin-fact--expiry is-${l.tone}`,c.title=l.title,c.textContent=l.text,o.appendChild(c)}return n.appendChild(o),n}export function withPluginChrome(e,t){if(!e||typeof e!="object")return e;const n=Object.create(Object.getPrototypeOf(e));Object.assign(n,e);const a=typeof e.onMount=="function"?e.onMount:null;return n.onMount=async function(...i){const s=i[0],o=s&&typeof s.get=="function"?s.get(0):s;return o&&o.nodeType===1&&!o.querySelector(":scope > .dm-plugin-shell")&&applyChrome(o,t),a?a.apply(e,i):void 0},n}export function applyChrome(e,t){const n=document.createElement("section");n.className="dm-plugin-shell",t&&t.plugin&&n.setAttribute("data-plugin",t.plugin),n.appendChild(buildBanner(t));const a=document.createElement("div");for(a.className="dm-plugin-body";e.firstChild;)a.appendChild(e.firstChild);n.appendChild(a),e.appendChild(n),h(a,t),g(a),typeof I<"u"&&I.scan?I.scan(n):typeof Domma<"u"&&Domma.icons&&Domma.icons.scan()}function f(e){return[...e.children].filter(t=>!/^(STYLE|SCRIPT|TEMPLATE|LINK)$/.test(t.tagName))}function g(e){let t=e;for(let n=0;n<2;n++){const a=f(t);if(a.length!==1)return;if(t=a[0],t.classList.contains("card")){t.classList.add("dm-plugin-unwrap");return}}}function h(e,t){const n=String(t&&t.displayName||"").trim().toLowerCase(),a=e.querySelector(":scope > .view-header, :scope > * > .view-header");if(a){const i=a.querySelector("h1, h2"),s=a.querySelector("button, a, input, select");i&&s?i.remove():i&&a.remove();return}if(n){for(const i of e.querySelectorAll("h1, h2"))if(i.textContent.trim().toLowerCase()===n){i.remove();return}}}export function addBannerAction(e,{icon:t,title:n,onClick:a,id:i="",label:s=""}){const o=e&&typeof e.get=="function"?e.get(0):e;if(!o||o.nodeType!==1)return null;const u=o.querySelector(":scope > .dm-plugin-shell > .dm-plugin-banner")||o.closest(".dm-plugin-shell")?.querySelector(":scope > .dm-plugin-banner");if(!u)return null;let r=u.querySelector(":scope > .dm-plugin-banner-facts");r||(r=document.createElement("div"),r.className="dm-plugin-banner-facts",u.appendChild(r));let l=r.querySelector(":scope > .dm-plugin-banner-actions");l||(l=document.createElement("div"),l.className="dm-plugin-banner-actions",r.insertBefore(l,r.firstChild)),i&&l.querySelector(`[data-banner-action="${CSS.escape(i)}"]`)?.remove();const c=document.createElement("button");c.type="button",c.className="btn btn-sm btn-ghost dm-plugin-banner-action",c.title=n||"",c.setAttribute("aria-label",n||s||"Action"),i&&(c.dataset.bannerAction=i);const d=document.createElement("span");return d.setAttribute("data-icon",t||"settings"),d.setAttribute("data-icon-size","16"),c.appendChild(d),s&&c.appendChild(document.createTextNode(` ${s}`)),c.addEventListener("click",a),l.appendChild(c),typeof I<"u"&&I.scan?I.scan(c):typeof Domma<"u"&&Domma.icons&&Domma.icons.scan(c),c}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const TOOLS_FOLDER_TEXT="Tools",MANAGE_PLUGINS_URL="#/plugins",MANAGE_PLUGINS_ITEM={text:"
|
|
1
|
+
export const TOOLS_FOLDER_TEXT="Tools",MANAGE_PLUGINS_URL="#/plugins",MANAGE_PLUGINS_ITEM={text:"Marketplace",url:MANAGE_PLUGINS_URL,icon:"store",permission:"plugins"};export function stripItemByUrl(n,o){const t=[];for(const s of n||[]){if(!s||s.url===o)continue;const e={...s};Array.isArray(s.items)&&s.items.length&&(e.items=stripItemByUrl(s.items,o)),t.push(e)}return t}export function groupPluginItems(n){const o=[],t=[];for(const e of n||[])if(!(!e||!e.item)){if(e.parent){o.push(e);continue}t.push(e.item)}return{toolsFolder:t.length?{text:TOOLS_FOLDER_TEXT,icon:"tool",items:t}:null,managePluginsItem:{...MANAGE_PLUGINS_ITEM},parented:o}}export function insertFoldersBeforeSystem(n,o){const t=(o||[]).filter(Boolean);if(!t.length)return n;const s=["system","documentation"],e=n.findIndex(r=>s.includes(String(r&&r.text||"").toLowerCase()));return e===-1?[...n,...t]:[...n.slice(0,e),...t,...n.slice(e)]}export function pruneEmptySynthesisedFolders(n){const o=new Set([TOOLS_FOLDER_TEXT]);return(n||[]).filter(t=>!t||!o.has(t.text)?!0:Array.isArray(t.items)&&t.items.length>0)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{test as
|
|
1
|
+
import{test as o}from"node:test";import s from"node:assert/strict";import{groupPluginItems as a,stripItemByUrl as p,insertFoldersBeforeSystem as u,pruneEmptySynthesisedFolders as i,TOOLS_FOLDER_TEXT as m,MANAGE_PLUGINS_URL as d}from"./sidebar-grouping.js";const n=(e,t=null)=>({parent:t,item:e});o("groupPluginItems routes every enabled item to Tools, whatever its core flag",()=>{const{toolsFolder:e}=a([n({text:"Analytics",url:"#/plugins/analytics",core:!0}),n({text:"Todo",url:"#/plugins/todo",core:!1}),n({text:"Notes",url:"#/plugins/notes"})]);s.equal(e.text,m),s.deepEqual(e.items.map(t=>t.text),["Analytics","Todo","Notes"])}),o("Marketplace is a standalone entry, not a folder",()=>{const{managePluginsItem:e}=a([n({text:"Todo",url:"#/plugins/todo"})]);s.equal(e.text,"Marketplace"),s.equal(e.url,d),s.equal(e.permission,"plugins"),s.ok(!("items"in e),"it must not carry a sub-tree")}),o("groupPluginItems omits the Tools folder only when nothing is enabled",()=>{const e=a([n({text:"Todo",url:"#/plugins/todo"})]);s.deepEqual(e.toolsFolder.items.map(l=>l.text),["Todo"]);const t=a([]);s.equal(t.toolsFolder,null),s.equal(t.managePluginsItem.text,"Marketplace")}),o("groupPluginItems keeps explicitly-parented items separate",()=>{const{parented:e,toolsFolder:t}=a([n({text:"Nested",url:"#/x"},"Content"),n({text:"Todo",url:"#/plugins/todo",core:!1})]);s.equal(e.length,1),s.equal(e[0].parent,"Content"),s.deepEqual(t.items.map(l=>l.text),["Todo"])}),o("each call returns its own Marketplace object",()=>{const e=a([]).managePluginsItem,t=a([]).managePluginsItem;s.notEqual(e,t),s.deepEqual(e,t)}),o("stripItemByUrl removes the management link at any depth",()=>{const e=[{text:"Overview",items:[{text:"Dashboard",url:"#/"}]},{text:"System",items:[{text:"Users",url:"#/users"},{text:"Plugins",url:"#/plugins"}]}],l=p(e,"#/plugins").find(r=>r.text==="System");s.deepEqual(l.items.map(r=>r.text),["Users"]),s.equal(e.find(r=>r.text==="System").items.length,2)}),o("insertFoldersBeforeSystem places Tools then Marketplace before System",()=>{const t=u([{text:"Overview"},{text:"Data"},{text:"System"},{text:"Documentation"}],[{text:"Tools",items:[]},{text:"Marketplace",url:"#/plugins"},null]);s.deepEqual(t.map(l=>l.text),["Overview","Data","Tools","Marketplace","System","Documentation"])}),o("insertFoldersBeforeSystem appends when no System/Documentation anchor",()=>{const t=u([{text:"Overview"},{text:"Data"}],[{text:"Marketplace",url:"#/plugins"}]);s.deepEqual(t.map(l=>l.text),["Overview","Data","Marketplace"])}),o("pruneEmptySynthesisedFolders drops an empty Tools folder but keeps built-ins",()=>{const e=[{text:"Overview",items:[]},{text:"Tools",items:[]}];s.deepEqual(i(e).map(t=>t.text),["Overview"])}),o("pruneEmptySynthesisedFolders leaves the standalone Marketplace alone",()=>{const e=[{text:"Tools",items:[{text:"Analytics"}]},{text:"Marketplace",url:"#/plugins"}];s.deepEqual(i(e).map(t=>t.text),["Tools","Marketplace"])});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
<summary${
|
|
1
|
+
import{api as f}from"../api.js";import{colourToCss as K,applyFloat as ne,applyHighlight as se,itemHighlightAttrs as ie}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{groupPluginItems as ae,stripItemByUrl as oe,insertFoldersBeforeSystem as re,pruneEmptySynthesisedFolders as le,MANAGE_PLUGINS_URL as ce}from"./sidebar-grouping.js";function g(e){return String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}const de=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu"}];function V(e,s){return s?!e||!e.length?!1:e.includes(s)?!0:["read","create","update","delete"].some(n=>e.includes(`${s}.${n}`)):!0}function G(e,s){const n=[];for(const t of e){if(t.hidden||t.permission&&!V(s,t.permission))continue;const i=Array.isArray(t.items)&&t.items.length?G(t.items,s):[];n.push({...t,items:i})}return n}function ue(e,s){if(!s||!s.length)return e;const n=(t,i)=>{for(const o of t){if((o.text||"").toLowerCase()===i.toLowerCase())return o;if(Array.isArray(o.items)){const a=n(o.items,i);if(a)return a}}return null};for(const t of s)if(t.parent){const i=n(e,t.parent);i?(i.items=Array.isArray(i.items)?i.items:[],i.items.push(t.item)):e.push(t.item)}else e.push(t.item);return e}async function me(e){if(!V(e,"projects"))return[];let s=[];try{s=await f.projects.list()}catch{return[]}const n={};await Promise.all(s.map(async i=>{try{const o=await f.projects.artefacts(i.slug);n[i.slug]=o}catch{n[i.slug]={}}}));const t=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return s.map(i=>{const a="#/projects/"+encodeURIComponent(i.slug),r=n[i.slug]||{},c=[{text:"Overview",url:a,icon:i.icon||"folder"}];for(const d of t){const b=r[d.key],h=Array.isArray(b)?b.length:0;h<=0||c.push({text:`${d.text} (${h})`,url:a+d.path,icon:d.icon})}c.push({text:"Settings",url:a+"/settings",icon:"settings"});const u=i.enabled===!1;return{text:i.name||i.slug,icon:i.icon||"folder",items:c,...u&&{colour:"neutral",badge:{text:"off"}}}})}function fe(e,s){for(const n of e)if((n.text||"").toLowerCase()==="projects"){n.items=Array.isArray(n.items)?n.items.slice():[],n.items.push(...s);return}}function J(e,s){const n=Q.includes(s.tone)?s.tone:"info",t=s.label||String(s.count);s.details?N.set(e,s.details):N.delete(e);const i=C.some(o=>o.url===e)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${n}" data-count-for="${g(e)}"${i} data-badge-label="${g(t)}" aria-label="${g(t)}">${g(String(s.count))}</span>`}function Y(e){for(const s of e.querySelectorAll("details")){const n=s.querySelector(":scope > summary");if(!n)continue;let t=0,i=!1;const o=[];for(const d of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const b=Number(d.textContent)||0;b&&(t+=b,i||(i=d.classList.contains("sidebar-badge--danger")),o.push({text:d.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:d.dataset.badgeLabel,tone:d.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const a=`folder:${s.dataset.stateKey||n.textContent.trim()}`;let r=n.querySelector(":scope > .sidebar-badge--sum");if(!t){r?.remove(),N.delete(a);continue}r||(r=document.createElement("span"),n.querySelector(".sidebar-text")?.after(r));const c=n.querySelector(".sidebar-text")?.textContent.trim()||"This folder",u=o.map(d=>`${d.text}: ${d.meta}`).join(" \xB7 ");r.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${i?"danger":"info"}`,r.dataset.countFor=a,r.dataset.badgeLabel=u,r.setAttribute("aria-label",u),r.textContent=String(t),N.set(a,{title:c,items:o})}}const N=new Map;function ge(e,s){if(!e.url||!s)return"";const n=s[e.url];return!n||!(n.count>0)?"":J(e.url,n)}const Q=["info","attention","danger","live"],pe={"#/pages":["page","pages"],"#/media":["file in the media library","files in the media library"],"#/menus":["menu","menus"],"#/context-menus":["context menu","context menus"],"#/theme":["element override in the theme","element overrides in the theme"],"#/collections":["collection","collections"],"#/forms":["form","forms"],"#/views":["view","views"],"#/actions":["action","actions"],"#/blocks":["block","blocks"],"#/components":["component","components"],"#/users":["user","users"],"#/plugins":["plugin installed","plugins installed"],"#/system/notifications":["unread notification","unread notifications"]};function T(e,{fallbackLabel:s="",tone:n="info"}={}){const t=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(t)||t<0)return null;const i=typeof e?.label=="string"&&e.label||(s?`${t} - ${s}`:String(t)),o={count:Math.round(t),label:i,tone:Q.includes(e?.tone)?e.tone:n};return e?.details&&typeof e.details=="object"&&(o.details=e.details),o}function he(e){const s=(e.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),n=(e.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${n?`project-${decodeURIComponent(n[1])}.`:""}${s}`}function Z(e,s){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="spacer"){const h=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${g(h)}" aria-hidden="true"></div>`}const n=e.icon?`<span data-icon="${g(e.icon)}"></span>`:"",t=g(e.text||""),i=e.url?g(e.url):"",o=Array.isArray(e.items)&&e.items.length>0,a=e.colour?K(e.colour):"",r=ie(e),c=[r.hover&&` data-menu-item-hover="${g(r.hover)}"`,r.active&&` data-menu-item-active="${g(r.active)}"`].filter(Boolean).join(""),u=[a&&`color:${g(a)}`,r.accent&&`--dm-menu-highlight:${g(r.accent)}`].filter(Boolean).join(";"),d=u?` style="${u}"`:"";let b="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const h=e.badge.variant?K(e.badge.variant):"";b=`<span class="dm-menu-badge"${h?` style="background:${g(h)};color:var(--dm-text-inverse,#fff)"`:""}>${g(String(e.badge.text))}</span>`}if(o){const h=he(e),v=S.get(h)!==!1,w=e.items.map(k=>Z(k,s)).join("");return`<details data-state-key="${g(h)}"${v?" open":""}>
|
|
2
|
+
<summary${d}>${n} <span class="sidebar-text">${t}</span>${b}</summary>
|
|
3
3
|
<div class="sidebar-children">${w}</div>
|
|
4
|
-
</details>`}return`<a href="${
|
|
4
|
+
</details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${d}${c}>${n} <span class="sidebar-text">${t}</span>${ge(e,s)}${b}</a>`}async function be(e=[]){const s=y=>y.then(x=>Array.isArray(x)?x.length:Array.isArray(x?.entries)?x.entries.length:0).catch(()=>0),[n,t,i,o,a,r,c,u,d,b,h,v,w,k]=await Promise.all([s(f.pages.list()),s(f.media.list()),s(f.menus.list()),s(f.contextMenus.list()),f.theme.get().then(y=>(y?.overrides||[]).length).catch(()=>0),s(f.collections.list()),s(f.forms.list()),s(f.views.list()),s(f.actions.list()),s(f.blocks.list()),s(f.components.list()),s(f.users.list()),s(f.plugins.list()),f.system?.notifications?.unreadCount?.().then(y=>y?.count??0).catch(()=>0)??Promise.resolve(0)]);C=ve(e);const B={};await Promise.all(C.map(async y=>{const x=await ee(y);x&&(B[y.url]=x)}));const E={"#/pages":n,"#/media":t,"#/menus":i,"#/context-menus":o,"#/theme":a,"#/collections":r,"#/forms":c,"#/views":u,"#/actions":d,"#/blocks":b,"#/components":h,"#/users":v,"#/plugins":w,"#/system/notifications":k},j={};for(const[y,x]of Object.entries(E)){const[U,R]=pe[y]??["item","items"],A=T(x);A&&(j[y]={...A,label:`${A.count} ${A.count===1?U:R}`})}return{...j,...B}}let C=[];const ye="dm:sidebar-count",xe=6e4;function ve(e=[]){const s=[],n=t=>{if(t){t.url&&typeof t.countUrl=="string"&&t.countUrl&&s.push({url:t.url,countUrl:t.countUrl,text:t.text||"",countLabel:t.countLabel||""});for(const i of[...t.children||[],...t.items||[]])n(i)}};for(const t of e??[])n(t?.item??t);return s}async function ee(e){try{const s=new Date,n=`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(s.getDate()).padStart(2,"0")}`,t=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${n}`;return T(await H.get(t),{fallbackLabel:e.countLabel||e.text})}catch{return null}}function te(e,s,n){const t=[...e.querySelectorAll("a.sidebar-link")].find(o=>o.dataset.url===s);if(!t)return;const i=t.querySelector(".sidebar-badge[data-count-for]");if(!n||!(n.count>0))i?.remove();else{const o=document.createRange();o.selectNodeContents(t);const a=o.createContextualFragment(J(s,n)).firstElementChild;i?i.replaceWith(a):t.querySelector(".sidebar-text")?.after(a)}O.set(s,n),Y(e)}const O=new Map;async function _(e,s=null){const n=s?C.filter(t=>t.url===s):C;await Promise.all(n.map(async t=>{const i=await ee(t),o=O.get(t.url);JSON.stringify(o??null)!==JSON.stringify(i)&&te(e,t.url,i)}))}function $e(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener(ye,s=>{const n=s.detail||{};if(n.url)if(n.count!==void 0){const t=C.find(i=>i.url===n.url);te(e,n.url,T(n,{fallbackLabel:t?.countLabel||t?.text||""}))}else _(e,n.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&_(e)},xe),document.addEventListener("visibilitychange",()=>{document.hidden||_(e)}),Se(e))}function Ce(e,s,n){const t=(a,r,c)=>{const u=document.createElement(a);return r&&(u.className=r),c!=null&&(u.textContent=String(c)),u};if(!n){e.textContent=s;return}e.appendChild(t("strong","dm-badge-pop-title",n.title||s)),(n.rows?.length||!n.items?.length)&&e.appendChild(t("p","dm-badge-pop-sub",s));const i=Array.isArray(n.rows)?n.rows.slice(0,6):[];if(i.length){const a=t("dl","dm-badge-pop-rows");for(const r of i)a.appendChild(t("dt",null,r.label)),a.appendChild(t("dd",r.tone==="danger"?"is-danger":null,r.value));e.appendChild(a)}const o=Array.isArray(n.items)?n.items.slice(0,6):[];if(o.length){const a=t("ul","dm-badge-pop-items");for(const r of o){const c=t("li",r.tone==="danger"?"is-danger":null);c.appendChild(t("span","dm-badge-pop-text",r.text)),r.meta&&c.appendChild(t("span","dm-badge-pop-meta",r.meta)),a.appendChild(c)}e.appendChild(a)}else n.empty&&e.appendChild(t("p","dm-badge-pop-empty",n.empty))}function Se(e){let s=null,n=null;const t=()=>{clearTimeout(n),s?.remove(),s=null},i=o=>{t();const a=o?.dataset.badgeLabel;a&&(n=setTimeout(()=>{s=document.createElement("div"),s.className="dm-help-pop dm-badge-pop",s.setAttribute("role","tooltip"),Ce(s,a,N.get(o.dataset.countFor)),document.body.appendChild(s);const r=o.getBoundingClientRect(),c=s.getBoundingClientRect(),u=Math.min(r.right+10,innerWidth-c.width-8);s.style.left=`${Math.max(8,u)}px`,s.style.top=`${Math.max(8,Math.min(r.top+r.height/2-c.height/2,innerHeight-c.height-8))}px`},150))};e.addEventListener("mouseover",o=>{const a=o.target.closest?.(".sidebar-badge[data-badge-label]");a&&i(a)}),e.addEventListener("mouseout",o=>{o.target.closest?.(".sidebar-badge[data-badge-label]")&&t()}),e.addEventListener("focusin",o=>{const a=o.target.closest?.("a, summary")?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");a?i(a):t()}),e.addEventListener("focusout",t),window.addEventListener("scroll",t,!0),window.addEventListener("hashchange",t)}async function we(){try{const e=await f.settings.get();return e?.adminBrand?.title||e?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:s}){const n=$(e).get(0);if(!n)return;let t,i=null,o=null,a=null,r=null,c=null;try{const l=await f.menus.get("admin-sidebar");t=Array.isArray(l?.items)?l.items:null,i=l?.variant||null,o=l?.position||null,a=l?.style||null,r=l?.float||null,c=l?.highlight||null}catch{t=null}(!t||!t.length)&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),t=de.slice());let u=[];try{u=await H.get("/api/sidebar/registered-items")||[]}catch{}t=oe(t,ce);const{toolsFolder:d,managePluginsItem:b,parented:h}=ae(u);t=ue(t,h),t=re(t,[d,b]);const v=await me(s);v.length&&fe(t,v),t=G(t,s),t=le(t);const[w,k]=await Promise.all([be(u).catch(()=>({})),we()]),B=i?` dm-admin-sidebar--${g(i)}`:"";function E(l,p="px"){if(l==null)return l;const m=String(l).trim();return/^\d+(\.\d+)?$/.test(m)?`${m}${p}`:m}let j="";if(a){const l=[];a.fontFamily&&l.push(`font-family: ${a.fontFamily}, sans-serif`),a.fontSize&&l.push(`font-size: ${E(a.fontSize)}`),a.fontWeight&&l.push(`font-weight: ${a.fontWeight}`),a.letterSpacing&&l.push(`letter-spacing: ${E(a.letterSpacing,"em")}`);const p=[];if(l.length&&p.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${l.join("; ")} }`),a.iconSize){const m=E(a.iconSize);p.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${m} !important; height: ${m} !important; }`)}p.length&&(j=`<style data-admin-sidebar-style>${p.join(" ")}</style>`)}const y=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${g(k)}</div>`,x=t.some(l=>l&&l.type==="spacer"&&l.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",U=`${j}<nav class="dm-admin-sidebar${B}${x}">${y}${t.map(l=>Z(l,w)).join("")}</nav>`,R=document.createRange();R.selectNodeContents(n);const A=R.createContextualFragment(U);n.replaceChildren(A),Domma.icons.scan(n);for(const l of C)O.set(l.url,w[l.url]??null);Y(n),$e(n);const P=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(P)&&P>=180&&P<=480&&(n.style.width=P+"px"),n.removeAttribute("data-menu-hover"),n.removeAttribute("data-menu-active"),n.style.removeProperty("--dm-menu-highlight"),se(n,{highlight:c});const L=document.querySelector(".dashboard-wrapper");if(["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(l=>{n.style[l]=""}),n.classList.remove("dm-menu-floating"),L&&(L.style.paddingLeft="",L.style.paddingRight=""),o==="floating"){ne(n,{position:"floating",float:r||{}});const p=r&&r.anchor||"TL",m=Number.isFinite(Number(r?.offsetX))?Number(r.offsetX):16,F=n.getBoundingClientRect().width||P||260;if(L){const z=Math.round(F+m*2)+"px";p==="R"||p==="TR"||p==="BR"?L.style.paddingRight=z:L.style.paddingLeft=z}}const q=document.createElement("div");q.className="dm-admin-sidebar-handle",q.setAttribute("aria-label","Resize sidebar"),n.appendChild(q),n.querySelectorAll("details").forEach(l=>{l.addEventListener("toggle",function(){const p=this.getAttribute("data-state-key");p&&S.set(p,this.open);const m=this.querySelector(":scope > .sidebar-children");if(m)if(this.open){const F=m.scrollHeight;m.style.maxHeight="0",requestAnimationFrame(()=>{m.style.maxHeight=F+"px",m.addEventListener("transitionend",function z(){m.style.maxHeight="none",m.removeEventListener("transitionend",z)})})}else{const F=m.scrollHeight;m.style.maxHeight=F+"px",requestAnimationFrame(()=>{m.style.maxHeight="0"})}})});let I=!1,D=0,W=0;q.addEventListener("mousedown",l=>{I=!0,D=l.clientX,W=n.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",l.preventDefault()}),document.addEventListener("mousemove",l=>{if(!I)return;const p=Math.max(180,Math.min(480,W+(l.clientX-D)));n.style.width=p+"px"}),document.addEventListener("mouseup",()=>{I&&(I=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(n.style.width,10)))});function X(){const l=location.hash||"#/";$(n).find(".sidebar-link").removeClass("active"),$(n).find(`.sidebar-link[data-url="${l}"]`).addClass("active")}X(),M.subscribe("router:afterChange",X)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const STATUSES=["not-started","in-progress","completed"],PRIORITIES=["highest","high","medium","low","lowest"],FILTERS=["open","all","not-started","in-progress","completed","overdue"],SORTS=["created","priority","due","status"],DEFAULT_SETTINGS=Object.freeze({defaultPriority:"medium",defaultFilter:"open",sort:"created",relativeDue:!0,confirmDelete:!0});const c=(r,e,t)=>e.includes(r)?r:t;export function normaliseSettings(r){const e=r&&typeof r=="object"?r:{},t=DEFAULT_SETTINGS;return{defaultPriority:c(e.defaultPriority,PRIORITIES,t.defaultPriority),defaultFilter:c(e.defaultFilter,FILTERS,t.defaultFilter),sort:c(e.sort,SORTS,t.sort),relativeDue:typeof e.relativeDue=="boolean"?e.relativeDue:t.relativeDue,confirmDelete:typeof e.confirmDelete=="boolean"?e.confirmDelete:t.confirmDelete}}export function startOfDay(r=new Date){const e=new Date(r);return e.setHours(0,0,0,0),e.getTime()}export function dueDay(r){if(!r)return null;const e=/^(\d{4})-(\d{2})-(\d{2})/.exec(String(r));return e?new Date(Number(e[1]),Number(e[2])-1,Number(e[3])).getTime():null}export function daysUntil(r,e=new Date){const t=dueDay(r);return t===null?null:Math.round((t-startOfDay(e))/864e5)}export function isOverdue(r,e=new Date){const t=daysUntil(r.dueAt,e);return t!==null&&t<0&&r.status!=="completed"}export function dueLabel(r,{relative:e=!0,now:t=new Date,format:i}={}){const s=daysUntil(r,t);return s===null?"":e?s===0?"Today":s===1?"Tomorrow":s===-1?"Yesterday":s>0?`In ${s} days`:`${-s} days ago`:i?i(dueDay(r)):String(r).slice(0,10)}export function isoDay(r,e=new Date){const t=new Date(startOfDay(e)+r*864e5+432e5),i=s=>String(s).padStart(2,"0");return`${t.getFullYear()}-${i(t.getMonth()+1)}-${i(t.getDate())}`}export function arrangeTodos(r,{filter:e="open",query:t="",sort:i="created",now:s=new Date}={}){const l=String(t).trim().toLowerCase(),p=(r||[]).filter(n=>e==="open"&&n.status==="completed"||STATUSES.includes(e)&&n.status!==e||e==="overdue"&&!isOverdue(n,s)?!1:!l||String(n.text??"").toLowerCase().includes(l)),u=(n,o)=>{const f=n.indexOf(o);return f===-1?n.length:f},a=(n,o)=>String(n.createdAt??"").localeCompare(String(o.createdAt??"")),y={created:a,priority:(n,o)=>u(PRIORITIES,n.priority)-u(PRIORITIES,o.priority)||a(n,o),due:(n,o)=>(dueDay(n.dueAt)??1/0)-(dueDay(o.dueAt)??1/0)||u(PRIORITIES,n.priority)-u(PRIORITIES,o.priority),status:(n,o)=>u(STATUSES,n.status)-u(STATUSES,o.status)||a(n,o)}[i]??a,d=n=>n.status==="completed"?1:0;return[...p].sort((n,o)=>d(n)-d(o)||y(n,o))}export function countTodos(r,e=new Date){const t={all:0,open:0,"not-started":0,"in-progress":0,completed:0,overdue:0};for(const i of r||[])t.all++,i.status!=="completed"&&t.open++,t[i.status]!==void 0&&t[i.status]++,isOverdue(i,e)&&t.overdue++;return t}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function onceClosed(t,e=()=>{}){const n=t.close.bind(t);let o=!1;t.close=(...c)=>{const r=n(...c);return o||(o=!0,e(),setTimeout(()=>{try{t.destroy?.()}catch{}t.element?.remove()},400)),r}}export function openMenuAt(t){const e=t.getBoundingClientRect();t.dispatchEvent(new MouseEvent("contextmenu",{bubbles:!0,cancelable:!0,clientX:e.left,clientY:e.bottom,view:window}))}export function settingsStore(t,e){return{read(){try{return e(S.get(t))}catch{return e(null)}},write(n){try{S.set(t,n)}catch{}}}}export function viewLifetime(t){let e=[];const n=()=>{const c=e;e=[],c.forEach(r=>{try{r?.()}catch{}})},o=()=>{t.test(location.hash)||n()};return{add(c){e.push(c)},reset(){n(),window.addEventListener("hashchange",o),e.push(()=>window.removeEventListener("hashchange",o))}}}export function sidebarCount(t,e={}){try{window.dispatchEvent(new CustomEvent("dm:sidebar-count",{detail:{url:t,...e}}))}catch{}}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<!-- No heading: the banner above (admin/js/lib/plugin-chrome.js) is the title.
|
|
2
|
+
Everything that changes is bound (M.applyBindings) - see admin/js/views/analytics.js, which also
|
|
3
|
+
inserts the list-row templates: the router renders this file through _.render, eating any {{…}}. -->
|
|
4
|
+
<div class="an-tool">
|
|
5
|
+
|
|
6
|
+
<div class="an-bar">
|
|
7
|
+
<select class="form-input an-range" aria-label="Range" data-model="range.value">
|
|
8
|
+
<option value="today">Today</option>
|
|
9
|
+
<option value="7d">Last 7 days</option>
|
|
10
|
+
<option value="30d">Last 30 days</option>
|
|
11
|
+
<option value="90d">Last 90 days</option>
|
|
12
|
+
<option value="all">All time</option>
|
|
13
|
+
</select>
|
|
14
|
+
<span class="an-live" data-bind-class="liveClass.value" data-bind-title="liveTitle.value">
|
|
15
|
+
<span class="an-live-dot"></span><span data-bind-text="liveText.value"></span>
|
|
16
|
+
</span>
|
|
17
|
+
<span data-help-title="Active now"
|
|
18
|
+
data-help="Visitors with a page view in the last five minutes, counted by browser session. It refreshes itself every 30 seconds while this screen is open (switch that off in settings)."></span>
|
|
19
|
+
<div class="an-bar-end">
|
|
20
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="refresh" title="Refresh">
|
|
21
|
+
<span data-icon="refresh-cw" data-icon-size="14"></span>
|
|
22
|
+
</button>
|
|
23
|
+
<button type="button" class="btn btn-sm btn-ghost an-more" aria-label="More actions" title="Export, reset">⋮</button>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="an-kpis">
|
|
28
|
+
<div class="an-kpi"><span class="an-kpi-value" data-bind-text="kpi.value.hits">-</span>
|
|
29
|
+
<span class="an-kpi-label">Page views <span data-help="Every page view in the chosen range. Admin pages, the API, and visitors whose browser asks not to be tracked are never counted."></span></span></div>
|
|
30
|
+
<div class="an-kpi"><span class="an-kpi-value" data-bind-text="kpi.value.pages">-</span>
|
|
31
|
+
<span class="an-kpi-label">Pages viewed</span></div>
|
|
32
|
+
<div class="an-kpi"><span class="an-kpi-value" data-bind-text="kpi.value.sessions">-</span>
|
|
33
|
+
<span class="an-kpi-label">Sessions <span data-help="Browser sessions seen in the range (up to 30 days back). One visitor in one browser tab session counts once, however many pages they read."></span></span></div>
|
|
34
|
+
<div class="an-kpi"><span class="an-kpi-value" data-bind-text="kpi.value.bounce">-</span>
|
|
35
|
+
<span class="an-kpi-label">Bounce rate <span data-help="The share of sessions that viewed a single page and left."></span></span></div>
|
|
36
|
+
<div class="an-kpi"><span class="an-kpi-value" data-bind-text="kpi.value.lifetime">-</span>
|
|
37
|
+
<span class="an-kpi-label">All-time views</span></div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="an-chart-wrap">
|
|
41
|
+
<canvas class="an-chart" aria-label="Page views over time"></canvas>
|
|
42
|
+
<p class="an-note" data-if="isAllTime.value">The chart shows the last 30 days; the figures above are all time.</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="an-tabs" role="tablist">
|
|
46
|
+
<button type="button" role="tab" data-on-click="showPages" data-bind-class="section.value === 'pages' && 'is-active'">Top pages</button>
|
|
47
|
+
<button type="button" role="tab" data-on-click="showJourneys" data-bind-class="section.value === 'journeys' && 'is-active'">Journeys</button>
|
|
48
|
+
<button type="button" role="tab" data-on-click="showSpikes" data-bind-class="section.value === 'spikes' && 'is-active'">
|
|
49
|
+
Spikes <span class="an-count" data-if="spikes.value.length" data-bind-text="spikes.value.length"></span></button>
|
|
50
|
+
<span data-help-title="Right-click a page"
|
|
51
|
+
data-help="Every page in these lists has a menu - right-click it, or use its ⋮ - to open it on the site, copy its address, or narrow the list to it."></span>
|
|
52
|
+
<label class="an-filter" data-if="section.value === 'pages'">
|
|
53
|
+
<span data-icon="search" data-icon-size="13"></span>
|
|
54
|
+
<input type="search" class="form-input" placeholder="Filter pages…" aria-label="Filter pages" data-model="filter.value">
|
|
55
|
+
</label>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<section class="an-panel" data-if="section.value === 'pages'">
|
|
59
|
+
<div class="an-list" data-each="pageRows.value key=key"></div>
|
|
60
|
+
<p class="an-note" data-bind-text="pagesNote.value"></p>
|
|
61
|
+
</section>
|
|
62
|
+
|
|
63
|
+
<section class="an-panel an-journeys" data-if="section.value === 'journeys'">
|
|
64
|
+
<div><h3>Entry pages <span data-help="Where sessions started."></span></h3>
|
|
65
|
+
<div class="an-list" data-each="entryRows.value key=key"></div></div>
|
|
66
|
+
<div><h3>Exit pages <span data-help="The last page a session viewed."></span></h3>
|
|
67
|
+
<div class="an-list" data-each="exitRows.value key=key"></div></div>
|
|
68
|
+
<div class="an-wide"><h3>Common paths <span data-help="The most frequent step from one page straight to another."></span></h3>
|
|
69
|
+
<div class="an-list" data-each="pathRows.value key=key"></div></div>
|
|
70
|
+
<p class="an-note" data-if="!sessionsCount.value">No sessions recorded in this range yet.</p>
|
|
71
|
+
</section>
|
|
72
|
+
|
|
73
|
+
<section class="an-panel" data-if="section.value === 'spikes'">
|
|
74
|
+
<div class="an-list" data-each="spikes.value key=key"></div>
|
|
75
|
+
<p class="an-note" data-if="!spikes.value.length">Nothing unusual today. A spike is a page running well above its last seven days at this hour.</p>
|
|
76
|
+
</section>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<style>
|
|
80
|
+
.an-tool { display: flex; flex-direction: column; gap: .9rem; }
|
|
81
|
+
.an-bar { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
|
|
82
|
+
.an-range { width: auto; height: 2rem; padding-top: 0; padding-bottom: 0; }
|
|
83
|
+
.an-live { display: inline-flex; align-items: center; gap: .4rem; font-size: var(--dm-text-xs); color: var(--dm-text-muted);
|
|
84
|
+
padding: .15rem .6rem; border-radius: 999px; background: var(--dm-surface-hover, rgba(127,127,127,.12)); }
|
|
85
|
+
.an-live-dot { width: .5rem; height: .5rem; border-radius: 50%; background: var(--dm-text-muted); }
|
|
86
|
+
.an-live.is-on .an-live-dot { background: var(--dm-success, #16a34a); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-success, #16a34a) 25%, transparent); }
|
|
87
|
+
.an-bar-end { display: flex; gap: .3rem; margin-left: auto; }
|
|
88
|
+
.an-more { font-size: var(--dm-text-lg); line-height: 1; padding: .2rem .5rem; }
|
|
89
|
+
|
|
90
|
+
.an-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: .6rem; }
|
|
91
|
+
.an-kpi { display: flex; flex-direction: column; gap: .15rem; padding: .6rem .8rem;
|
|
92
|
+
border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); }
|
|
93
|
+
.an-kpi-value { font-size: var(--dm-text-xl); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
94
|
+
.an-kpi-label { font-size: var(--dm-text-xs); color: var(--dm-text-muted); display: inline-flex; align-items: center; }
|
|
95
|
+
|
|
96
|
+
.an-chart-wrap { position: relative; height: 220px; padding-bottom: 1.2rem; }
|
|
97
|
+
.an-chart-wrap .an-note { position: absolute; bottom: 0; left: 0; margin: 0; }
|
|
98
|
+
|
|
99
|
+
.an-tabs { display: flex; align-items: center; gap: .25rem; border-bottom: 1px solid var(--dm-border); flex-wrap: wrap; }
|
|
100
|
+
.an-tabs [role="tab"] { border: 0; background: none; cursor: pointer; font: inherit; font-size: var(--dm-text-sm);
|
|
101
|
+
padding: .45rem .7rem; color: var(--dm-text-muted); border-bottom: 2px solid transparent; margin-bottom: -1px; }
|
|
102
|
+
.an-tabs [role="tab"].is-active { color: var(--dm-text); border-bottom-color: var(--dm-primary); }
|
|
103
|
+
.an-count { font-size: var(--dm-text-xs); padding: 0 .4rem; border-radius: 999px; background: var(--dm-warning, #d97706); color: #fff; }
|
|
104
|
+
.an-filter { position: relative; margin: 0 0 .3rem auto; }
|
|
105
|
+
.an-filter > svg, .an-filter > [data-icon] { position: absolute; left: .55rem; top: 50%; transform: translateY(-50%); color: var(--dm-text-muted); pointer-events: none; }
|
|
106
|
+
.an-filter .form-input { height: 1.85rem; padding-left: 1.75rem; width: 14rem; }
|
|
107
|
+
|
|
108
|
+
.an-panel { display: flex; flex-direction: column; gap: .5rem; }
|
|
109
|
+
.an-journeys { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
|
110
|
+
.an-journeys .an-wide, .an-journeys .an-note { grid-column: 1 / -1; }
|
|
111
|
+
.an-journeys h3 { margin: 0 0 .35rem; font-size: var(--dm-text-xs); font-weight: 600; color: var(--dm-text-muted);
|
|
112
|
+
text-transform: uppercase; letter-spacing: .04em; display: flex; align-items: center; }
|
|
113
|
+
@media (max-width: 760px) { .an-journeys { grid-template-columns: 1fr; } }
|
|
114
|
+
|
|
115
|
+
.an-list { display: flex; flex-direction: column; }
|
|
116
|
+
.an-row { position: relative; display: flex; align-items: center; gap: .6rem; padding: .3rem .4rem .3rem .6rem;
|
|
117
|
+
font-size: var(--dm-text-sm); border-radius: var(--dm-radius-sm); isolation: isolate; }
|
|
118
|
+
.an-row:hover { background: var(--dm-surface-hover, rgba(127,127,127,.08)); }
|
|
119
|
+
.an-bar-fill { position: absolute; inset: 2px auto 2px 0; z-index: -1; border-radius: var(--dm-radius-sm);
|
|
120
|
+
background: color-mix(in srgb, var(--dm-primary) 14%, transparent); }
|
|
121
|
+
.an-rank { flex: 0 0 1.6rem; color: var(--dm-text-muted); font-size: var(--dm-text-xs); text-align: right; }
|
|
122
|
+
.an-url { flex: 1; min-width: 0; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-xs);
|
|
123
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
124
|
+
.an-num { flex: 0 0 auto; font-variant-numeric: tabular-nums; }
|
|
125
|
+
.an-share { flex: 0 0 3.2rem; text-align: right; font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
|
|
126
|
+
.an-kebab { flex: 0 0 auto; border: 0; background: none; cursor: pointer; color: var(--dm-text-muted);
|
|
127
|
+
font-size: var(--dm-text-lg); line-height: 1; padding: 0 .3rem; border-radius: var(--dm-radius-sm); }
|
|
128
|
+
.an-kebab:hover { color: var(--dm-primary); }
|
|
129
|
+
.an-note { margin: .25rem 0; font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
|
|
130
|
+
.an-note:empty { display: none; }
|
|
131
|
+
|
|
132
|
+
.an-panel-form { display: flex; flex-direction: column; gap: 1rem; padding: .25rem 0 .5rem; }
|
|
133
|
+
.an-panel-form .form-label { display: flex; align-items: center; font-weight: 500; margin-bottom: .3rem; }
|
|
134
|
+
.an-setting-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
|
|
135
|
+
.an-panel-meta { font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
|
|
136
|
+
.an-panel-actions { display: flex; align-items: center; gap: .5rem; padding-top: .75rem; border-top: 1px solid var(--dm-border); }
|
|
137
|
+
.an-panel-actions .spacer { flex: 1; }
|
|
138
|
+
</style>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<!-- No heading: the banner above (admin/js/lib/plugin-chrome.js) is the title.
|
|
2
|
+
Everything that changes is bound (M.applyBindings) - see admin/js/views/contacts.js, which also
|
|
3
|
+
inserts the row and chip templates: the router renders this file through _.render, eating any {{…}}.
|
|
4
|
+
The editor, settings and groups panels are slideovers built in the view. -->
|
|
5
|
+
<div class="ct-tool">
|
|
6
|
+
|
|
7
|
+
<div class="ct-bar">
|
|
8
|
+
<label class="ct-search">
|
|
9
|
+
<span data-icon="search" data-icon-size="14"></span>
|
|
10
|
+
<input type="search" class="form-input" placeholder="Search contacts…" aria-label="Search contacts"
|
|
11
|
+
data-model="query.value" data-focus="searchFocus.value">
|
|
12
|
+
</label>
|
|
13
|
+
<span class="ct-count" data-bind-text="countText.value"></span>
|
|
14
|
+
<span data-help-title="Working with contacts"
|
|
15
|
+
data-help="Search matches names, emails, phones, companies, notes and groups. Right-click a contact (or its ⋮) to edit, email, call, copy, star, file it in a group, duplicate or delete it. Click a row to edit. Press / to search, N for a new contact."></span>
|
|
16
|
+
<span class="ct-spacer"></span>
|
|
17
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="create">
|
|
18
|
+
<span data-icon="plus" data-icon-size="14"></span> New contact
|
|
19
|
+
</button>
|
|
20
|
+
<button type="button" class="btn btn-sm btn-ghost ct-more" aria-label="More actions" title="Export, import, groups">⋮</button>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- Everyone / starred / each group, with counts: the filter and the stats in one row. -->
|
|
24
|
+
<div class="ct-chips-row">
|
|
25
|
+
<div class="ct-chips" data-each="chips.value key=key"></div>
|
|
26
|
+
<span data-help-title="Filter by group"
|
|
27
|
+
data-help="The numbers are how many contacts are in each. Click one to show only those; click All to show everyone. ⋮ > Manage groups renames or deletes a group."></span>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="ct-bulk" data-if="hasSelection.value">
|
|
31
|
+
<strong data-bind-text="selectionText.value"></strong>
|
|
32
|
+
<button type="button" class="btn btn-sm btn-ghost ct-bulk-group">
|
|
33
|
+
<span data-icon="folder-plus" data-icon-size="13"></span> Add to group ▾
|
|
34
|
+
</button>
|
|
35
|
+
<button type="button" class="btn btn-sm btn-ghost text-danger" data-on-click="bulkDelete">
|
|
36
|
+
<span data-icon="trash" data-icon-size="13"></span> Delete
|
|
37
|
+
</button>
|
|
38
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="clearSelection">Clear</button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<p class="ct-state" data-if="loading.value">Loading contacts…</p>
|
|
42
|
+
<p class="ct-state" data-if="isEmpty.value" data-bind-text="emptyText.value"></p>
|
|
43
|
+
|
|
44
|
+
<div class="ct-list">
|
|
45
|
+
<div class="ct-head" data-if="hasRows.value">
|
|
46
|
+
<input type="checkbox" class="ct-select-all" aria-label="Select every contact shown" title="Select all shown"
|
|
47
|
+
data-bind-checked="allSelected.value">
|
|
48
|
+
<span></span>
|
|
49
|
+
<span class="ct-h-name">Name</span>
|
|
50
|
+
<span class="ct-col ct-company">Company</span>
|
|
51
|
+
<span class="ct-col ct-email">Email</span>
|
|
52
|
+
<span class="ct-col ct-phone">Phone</span>
|
|
53
|
+
<span class="ct-col ct-groups">Groups
|
|
54
|
+
<span data-help="Groups this contact is filed in. Change them from the contact's menu (Groups) or in the editor."></span></span>
|
|
55
|
+
<span></span>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="ct-rows" data-each="rows.value key=id"></div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<input type="file" class="ct-import-file" accept=".json,application/json" hidden>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<style>
|
|
64
|
+
.ct-tool { display: flex; flex-direction: column; gap: .75rem; }
|
|
65
|
+
.ct-bar { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
|
|
66
|
+
.ct-search { position: relative; flex: 1 1 200px; max-width: 320px; margin: 0; }
|
|
67
|
+
.ct-search > svg, .ct-search > [data-icon] { position: absolute; left: .6rem; top: 50%; transform: translateY(-50%);
|
|
68
|
+
color: var(--dm-text-muted); pointer-events: none; }
|
|
69
|
+
.ct-search .form-input { height: 2rem; padding-left: 1.9rem; }
|
|
70
|
+
.ct-count { font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
|
|
71
|
+
.ct-spacer { flex: 1; }
|
|
72
|
+
.ct-more { font-size: var(--dm-text-lg); line-height: 1; padding: .2rem .5rem; }
|
|
73
|
+
|
|
74
|
+
.ct-chips-row { display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
|
|
75
|
+
.ct-chips { display: flex; gap: .35rem; flex-wrap: wrap; }
|
|
76
|
+
.ct-chip { display: inline-flex; align-items: center; gap: .35rem; padding: .15rem .65rem; border-radius: 999px; cursor: pointer;
|
|
77
|
+
border: 1px solid var(--dm-border); background: none; color: var(--dm-text-muted); font: inherit; font-size: var(--dm-text-xs); }
|
|
78
|
+
.ct-chip:hover { border-color: var(--dm-primary); color: var(--dm-text); }
|
|
79
|
+
.ct-chip.is-active { background: var(--dm-primary); border-color: var(--dm-primary); color: var(--dm-primary-text, #fff); }
|
|
80
|
+
.ct-chip b { font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
81
|
+
|
|
82
|
+
.ct-bulk { display: flex; align-items: center; gap: .5rem; padding: .4rem .75rem; font-size: var(--dm-text-sm);
|
|
83
|
+
border-radius: var(--dm-radius-md); background: color-mix(in srgb, var(--dm-primary) 10%, transparent); }
|
|
84
|
+
.ct-state { margin: 1.5rem 0; color: var(--dm-text-muted); text-align: center; }
|
|
85
|
+
|
|
86
|
+
.ct-list { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
|
|
87
|
+
.ct-list:has(.ct-rows:empty) { border: 0; }
|
|
88
|
+
.ct-head, .ct-row {
|
|
89
|
+
display: grid; align-items: center; gap: .75rem; padding: .5rem .75rem;
|
|
90
|
+
/* Set by the view from the column settings. */
|
|
91
|
+
grid-template-columns: var(--ct-cols, 1.1rem 1.5rem minmax(10rem, 1.4fr) minmax(0, 1fr) minmax(0, 1.2fr) minmax(0, .9fr) minmax(0, 1fr) 1.75rem);
|
|
92
|
+
}
|
|
93
|
+
.ct-head { font-size: var(--dm-text-xs); text-transform: uppercase; letter-spacing: .04em; color: var(--dm-text-muted);
|
|
94
|
+
border-bottom: 1px solid var(--dm-border); padding-top: .4rem; padding-bottom: .4rem; }
|
|
95
|
+
.ct-head > span { display: inline-flex; align-items: center; min-width: 0; }
|
|
96
|
+
.ct-row { border-bottom: 1px solid var(--dm-border); font-size: var(--dm-text-sm); cursor: pointer; }
|
|
97
|
+
.ct-row:last-child { border-bottom: 0; }
|
|
98
|
+
.ct-row:hover, .ct-row:focus-visible { background: var(--dm-surface-hover, rgba(127,127,127,.08)); outline: none; }
|
|
99
|
+
.ct-row.is-selected { background: color-mix(in srgb, var(--dm-primary) 8%, transparent); }
|
|
100
|
+
.ct-fav { border: 0; background: none; cursor: pointer; padding: 0; font-size: var(--dm-text-base); line-height: 1; color: var(--dm-text-muted); opacity: .35; }
|
|
101
|
+
.ct-fav:hover { opacity: .8; }
|
|
102
|
+
.ct-fav.is-on { color: var(--dm-warning, #d97706); opacity: 1; }
|
|
103
|
+
.ct-name { display: flex; align-items: center; gap: .6rem; min-width: 0; }
|
|
104
|
+
.ct-avatar { flex: 0 0 auto; width: 1.9rem; height: 1.9rem; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
|
|
105
|
+
font-size: var(--dm-text-xs); font-weight: 600; background: color-mix(in srgb, var(--dm-primary) 18%, transparent); color: var(--dm-primary); }
|
|
106
|
+
.ct-name-text { display: flex; flex-direction: column; min-width: 0; }
|
|
107
|
+
.ct-name-text strong, .ct-name-text small, .ct-col { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
108
|
+
.ct-name-text small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
|
|
109
|
+
.ct-name-text small:empty { display: none; }
|
|
110
|
+
.ct-col { min-width: 0; color: var(--dm-text-muted); }
|
|
111
|
+
.ct-row .ct-email, .ct-row .ct-phone { color: var(--dm-text); }
|
|
112
|
+
.ct-groups-text { font-size: var(--dm-text-xs); }
|
|
113
|
+
.ct-kebab { border: 0; background: none; cursor: pointer; color: var(--dm-text-muted); font-size: var(--dm-text-base); line-height: 1;
|
|
114
|
+
padding: 0 .3rem; border-radius: var(--dm-radius-sm); }
|
|
115
|
+
.ct-kebab:hover { color: var(--dm-primary); background: var(--dm-surface-hover, rgba(127,127,127,.15)); }
|
|
116
|
+
|
|
117
|
+
/* Density and hidden columns, from settings - classes on the list, so rows never re-render for them. */
|
|
118
|
+
.ct-list.is-compact .ct-row { padding-top: .25rem; padding-bottom: .25rem; }
|
|
119
|
+
.ct-list.is-compact .ct-avatar { width: 1.4rem; height: 1.4rem; font-size: var(--dm-text-xs); }
|
|
120
|
+
.ct-list.is-compact .ct-name-text small { display: none; }
|
|
121
|
+
.ct-list.hide-company .ct-company, .ct-list.hide-email .ct-email,
|
|
122
|
+
.ct-list.hide-phone .ct-phone, .ct-list.hide-groups .ct-groups { display: none; }
|
|
123
|
+
@media (max-width: 820px) {
|
|
124
|
+
.ct-head { display: none; }
|
|
125
|
+
.ct-row { grid-template-columns: 1.1rem 1.5rem 1fr 1.75rem; }
|
|
126
|
+
.ct-row .ct-col { display: none; }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Slideover panels: editor, settings, groups. */
|
|
130
|
+
.ct-panel { display: flex; flex-direction: column; gap: 1rem; padding: .25rem 0 .5rem; }
|
|
131
|
+
.ct-panel .form-label { display: flex; align-items: center; font-weight: 500; margin-bottom: .3rem; }
|
|
132
|
+
.ct-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: .75rem 1rem; }
|
|
133
|
+
.ct-grid .ct-wide { grid-column: 1 / -1; }
|
|
134
|
+
.ct-section { border-top: 1px solid var(--dm-border); padding-top: .75rem; }
|
|
135
|
+
.ct-section > summary { cursor: pointer; display: flex; align-items: center; gap: .5rem; font-weight: 600; list-style: none; }
|
|
136
|
+
.ct-section > summary::-webkit-details-marker { display: none; }
|
|
137
|
+
.ct-section > summary::before { content: '▸'; color: var(--dm-text-muted); transition: transform .15s ease; }
|
|
138
|
+
.ct-section[open] > summary::before { transform: rotate(90deg); }
|
|
139
|
+
.ct-section-sum { font-weight: 400; font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
|
|
140
|
+
.ct-section-body { display: flex; flex-direction: column; gap: 1rem; padding-top: .75rem; }
|
|
141
|
+
.ct-subhead { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: .4rem; }
|
|
142
|
+
.ct-subhead strong { display: inline-flex; align-items: center; }
|
|
143
|
+
.ct-pick { display: flex; flex-wrap: wrap; gap: .35rem; }
|
|
144
|
+
.ct-pick .ct-chip { font-size: var(--dm-text-xs); }
|
|
145
|
+
.ct-setting-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
|
|
146
|
+
.ct-panel-meta { font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
|
|
147
|
+
.ct-panel-actions { display: flex; align-items: center; gap: .5rem; padding-top: .75rem; border-top: 1px solid var(--dm-border); }
|
|
148
|
+
.ct-panel-actions .spacer { flex: 1; }
|
|
149
|
+
.ct-group-row { display: flex; align-items: center; gap: .5rem; padding: .4rem .5rem; border-bottom: 1px solid var(--dm-border); font-size: var(--dm-text-sm); }
|
|
150
|
+
.ct-group-row:last-child { border-bottom: 0; }
|
|
151
|
+
.ct-group-row span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
152
|
+
.ct-group-row small { color: var(--dm-text-muted); }
|
|
153
|
+
.ct-groups-list { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); }
|
|
154
|
+
.ct-add-group { display: flex; gap: .5rem; }
|
|
155
|
+
.ct-add-group .form-input { flex: 1; }
|
|
156
|
+
</style>
|