domma-cms 0.54.2 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/CLAUDE.md +151 -77
  2. package/README.md +4 -4
  3. package/admin/css/admin.css +1 -1
  4. package/admin/dist/domma/domma-tools.css +3 -3
  5. package/admin/dist/domma/domma-tools.min.js +3 -3
  6. package/admin/js/api.js +1 -1
  7. package/admin/js/app.js +3 -3
  8. package/admin/js/lib/page-picker.js +1 -0
  9. package/admin/js/lib/plugin-accent.js +1 -0
  10. package/admin/js/lib/plugin-chrome.js +1 -0
  11. package/admin/js/lib/shortcode-context-menu.js +2 -2
  12. package/admin/js/lib/sidebar-grouping.js +1 -1
  13. package/admin/js/lib/sidebar-grouping.test.js +1 -1
  14. package/admin/js/lib/sidebar-renderer.js +4 -4
  15. package/admin/js/lib/slideover-resizable.js +1 -0
  16. package/admin/js/templates/context-menu-editor.html +212 -0
  17. package/admin/js/templates/context-menus.html +16 -0
  18. package/admin/js/templates/menu-editor.html +19 -25
  19. package/admin/js/templates/plugin-code.html +1 -1
  20. package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
  21. package/admin/js/templates/settings.html +27 -97
  22. package/admin/js/templates/theme.html +173 -0
  23. package/admin/js/views/context-menu-editor.js +55 -0
  24. package/admin/js/views/context-menus.js +5 -0
  25. package/admin/js/views/form-editor.js +7 -7
  26. package/admin/js/views/index.js +1 -1
  27. package/admin/js/views/menu-editor.js +13 -13
  28. package/admin/js/views/plugin-marketplace.js +1 -1
  29. package/admin/js/views/plugins.js +25 -23
  30. package/admin/js/views/search.js +1 -0
  31. package/admin/js/views/settings.js +3 -3
  32. package/admin/js/views/theme.js +1 -0
  33. package/bin/cli.js +11 -2
  34. package/bin/lib/smtp-defaults.js +53 -0
  35. package/bin/update.js +13 -2
  36. package/config/menus/admin-sidebar.json +129 -23
  37. package/config/plugins.json +5 -5
  38. package/config/search.json +13 -0
  39. package/config/theme.json +18 -0
  40. package/package.json +12 -4
  41. package/plugins/_lib/admin/mail/compose-window.js +914 -0
  42. package/plugins/_lib/admin/mail/contacts.js +301 -0
  43. package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
  44. package/plugins/_lib/admin/mail/folder-tree.js +254 -0
  45. package/plugins/_lib/admin/mail/identity.js +480 -0
  46. package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
  47. package/plugins/_lib/admin/mail/keyboard.js +136 -0
  48. package/plugins/_lib/admin/mail/mail.css +1 -0
  49. package/plugins/_lib/admin/mail/mail.html +71 -0
  50. package/plugins/_lib/admin/mail/panes.js +253 -0
  51. package/plugins/_lib/admin/mail/reader-view.js +4453 -0
  52. package/plugins/_lib/admin/mail/resizable.js +26 -0
  53. package/plugins/_lib/admin/mail/rules.js +343 -0
  54. package/plugins/_lib/admin/mail/scheduling.js +203 -0
  55. package/plugins/_lib/admin/mail/section-kit.js +277 -0
  56. package/plugins/_lib/admin/mail/templates.js +238 -0
  57. package/plugins/_lib/admin/mail/threads.js +200 -0
  58. package/plugins/_lib/admin/mail/vacation.js +269 -0
  59. package/plugins/_lib/admin/ui/help.css +1 -0
  60. package/plugins/_lib/admin/ui/help.js +174 -0
  61. package/plugins/_lib/admin/ui/resizable.js +151 -0
  62. package/plugins/_lib/dataStore.js +117 -0
  63. package/plugins/_lib/mail/accounts.js +919 -0
  64. package/plugins/_lib/mail/bodyTokens.js +101 -0
  65. package/plugins/_lib/mail/compose.js +256 -0
  66. package/plugins/_lib/mail/defaults.js +57 -0
  67. package/plugins/_lib/mail/diagnostics.js +154 -0
  68. package/plugins/_lib/mail/envelope.js +161 -0
  69. package/plugins/_lib/mail/folders.js +192 -0
  70. package/plugins/_lib/mail/handoff.js +274 -0
  71. package/plugins/_lib/mail/imapPool.js +293 -0
  72. package/plugins/_lib/mail/mbox.js +74 -0
  73. package/plugins/_lib/mail/pollSchedule.js +79 -0
  74. package/plugins/_lib/mail/poller.js +135 -0
  75. package/plugins/_lib/mail/priority.js +138 -0
  76. package/plugins/_lib/mail/readRoutes.js +680 -0
  77. package/plugins/_lib/mail/render.js +291 -0
  78. package/plugins/_lib/mail/ruleRunner.js +152 -0
  79. package/plugins/_lib/mail/scheduler.js +254 -0
  80. package/plugins/_lib/mail/secretbox.js +229 -0
  81. package/plugins/_lib/mail/send.js +396 -0
  82. package/plugins/_lib/mail/store.js +1002 -0
  83. package/plugins/_lib/mail/sync.js +292 -0
  84. package/plugins/_lib/mail/syncPlan.js +126 -0
  85. package/plugins/_lib/mail/syncSelection.js +82 -0
  86. package/plugins/_lib/mail/unsubscribe.js +183 -0
  87. package/plugins/_lib/mail/vacationRunner.js +114 -0
  88. package/plugins/_lib/mail/write.js +473 -0
  89. package/plugins/_lib/schemaSync.js +83 -0
  90. package/plugins/_template/admin/css/index.css +0 -0
  91. package/plugins/_template/admin/templates/index.html +4 -4
  92. package/plugins/_template/admin/views/index.js +7 -0
  93. package/plugins/analytics/admin/css/index.css +1 -0
  94. package/plugins/analytics/admin/templates/analytics.html +22 -13
  95. package/plugins/analytics/plugin.json +3 -0
  96. package/plugins/blog/admin/css/index.css +1 -0
  97. package/plugins/blog/admin/templates/blog.html +30 -18
  98. package/plugins/blog/admin/templates/categories.html +2 -2
  99. package/plugins/blog/admin/templates/comments.html +2 -2
  100. package/plugins/blog/admin/templates/post-editor.html +34 -34
  101. package/plugins/blog/admin/templates/settings.html +6 -3
  102. package/plugins/blog/admin/views/blog.js +8 -5
  103. package/plugins/blog/admin/views/categories.js +5 -10
  104. package/plugins/blog/admin/views/comments.js +5 -5
  105. package/plugins/blog/admin/views/post-editor.js +39 -20
  106. package/plugins/blog/admin/views/settings.js +52 -50
  107. package/plugins/blog/collections/categories/schema.json +7 -6
  108. package/plugins/blog/collections/comments/schema.json +11 -10
  109. package/plugins/blog/collections/posts/schema.json +14 -13
  110. package/plugins/blog/plugin.js +36 -13
  111. package/plugins/blog/plugin.json +13 -5
  112. package/plugins/blog/plugin.public.js +312 -0
  113. package/plugins/contacts/admin/css/index.css +1 -0
  114. package/plugins/contacts/admin/templates/contacts.html +128 -0
  115. package/plugins/contacts/admin/views/contacts.js +237 -4
  116. package/plugins/contacts/collections/user-contacts/schema.json +108 -0
  117. package/plugins/contacts/plugin.js +214 -27
  118. package/plugins/contacts/plugin.json +4 -1
  119. package/plugins/invoice/admin/css/index.css +1 -0
  120. package/plugins/invoice/admin/templates/editor.html +140 -49
  121. package/plugins/invoice/admin/templates/index.html +153 -23
  122. package/plugins/invoice/admin/templates/issuers.html +2 -5
  123. package/plugins/invoice/admin/templates/receivers.html +2 -5
  124. package/plugins/invoice/admin/views/contacts-source.js +266 -0
  125. package/plugins/invoice/admin/views/editor.js +366 -199
  126. package/plugins/invoice/admin/views/export.js +199 -0
  127. package/plugins/invoice/admin/views/help-content.js +61 -0
  128. package/plugins/invoice/admin/views/index.js +582 -94
  129. package/plugins/invoice/admin/views/issuers.js +24 -17
  130. package/plugins/invoice/admin/views/media.js +172 -0
  131. package/plugins/invoice/admin/views/party-view.js +305 -67
  132. package/plugins/invoice/admin/views/payments.js +127 -0
  133. package/plugins/invoice/admin/views/print.js +130 -0
  134. package/plugins/invoice/admin/views/receivers.js +49 -16
  135. package/plugins/invoice/admin/views/send.js +212 -0
  136. package/plugins/invoice/admin/views/settings.js +594 -0
  137. package/plugins/invoice/admin/views/view-lifecycle.js +33 -0
  138. package/plugins/invoice/collections/invoice-issuers/schema.json +77 -11
  139. package/plugins/invoice/collections/invoice-receivers/schema.json +10 -9
  140. package/plugins/invoice/collections/invoices/schema.json +19 -13
  141. package/plugins/invoice/config.js +27 -6
  142. package/plugins/invoice/pdf.js +164 -0
  143. package/plugins/invoice/plugin.js +1217 -44
  144. package/plugins/invoice/plugin.json +10 -9
  145. package/plugins/invoice/templates/_base.css +1 -0
  146. package/plugins/invoice/templates/classic-nologo.html +100 -0
  147. package/plugins/invoice/templates/classic.html +91 -0
  148. package/plugins/invoice/templates/invoice-print.html +24 -0
  149. package/plugins/invoice/templates/minimal.html +99 -0
  150. package/plugins/invoice/templates/modern-nologo.html +114 -0
  151. package/plugins/invoice/templates/modern.html +113 -0
  152. package/plugins/invoice/templates/templates.json +11 -0
  153. package/plugins/mail-reader/admin/views/mail.js +19 -0
  154. package/plugins/mail-reader/config.js +7 -0
  155. package/plugins/mail-reader/plugin.js +48 -0
  156. package/plugins/mail-reader/plugin.json +33 -0
  157. package/plugins/notes/admin/views/notes.js +1 -1
  158. package/plugins/notes/plugin.json +2 -2
  159. package/plugins/surveys/lib/audience.js +37 -0
  160. package/plugins/surveys/lib/campaigns.js +43 -0
  161. package/plugins/surveys/lib/ledger.js +110 -0
  162. package/plugins/surveys/lib/sending.js +106 -0
  163. package/plugins/surveys/lib/stats.js +62 -0
  164. package/plugins/surveys/lib/submit.js +95 -0
  165. package/plugins/surveys/lib/tokens.js +28 -0
  166. package/plugins/surveys/plugin.public.js +149 -0
  167. package/plugins/theme-switcher/admin/templates/theme-switcher.html +1 -1
  168. package/public/css/forms.css +1 -1
  169. package/public/css/menu-highlight.css +1 -1
  170. package/public/css/search.css +1 -0
  171. package/public/css/site.css +1 -1
  172. package/public/js/collection-context.js +2 -2
  173. package/public/js/context-menus.js +1 -0
  174. package/public/js/form-logic-engine.js +1 -1
  175. package/public/js/forms.js +2 -2
  176. package/public/js/menu-decor.mjs +1 -1
  177. package/public/js/search.js +1 -0
  178. package/public/js/site.js +1 -1
  179. package/scripts/build.js +37 -3
  180. package/scripts/copy-domma.js +48 -0
  181. package/scripts/seed.js +1996 -0
  182. package/scripts/setup.js +8 -0
  183. package/server/routes/api/collections.js +34 -0
  184. package/server/routes/api/context-menus.js +104 -0
  185. package/server/routes/api/forms.js +42 -3
  186. package/server/routes/api/notifications.js +69 -19
  187. package/server/routes/api/plugins.js +50 -6
  188. package/server/routes/api/search.js +43 -0
  189. package/server/routes/api/theme.js +69 -0
  190. package/server/routes/public.js +42 -7
  191. package/server/server.js +74 -0
  192. package/server/services/adapters/FileAdapter.js +6 -1
  193. package/server/services/content.js +26 -0
  194. package/server/services/contextMenus.js +477 -0
  195. package/server/services/email.js +29 -3
  196. package/server/services/health.js +23 -2
  197. package/server/services/markdown.js +70 -9
  198. package/server/services/menuRender.js +28 -4
  199. package/server/services/menus.js +25 -1
  200. package/server/services/permissionRegistry.js +24 -0
  201. package/server/services/pluginFiles.js +52 -11
  202. package/server/services/plugins.js +229 -6
  203. package/server/services/renderer.js +148 -22
  204. package/server/services/roles.js +1 -1
  205. package/server/services/search-migration.js +82 -0
  206. package/server/services/search.js +413 -0
  207. package/server/services/sidebar-migration.js +1 -0
  208. package/server/services/themeSettings.js +541 -0
  209. package/server/services/users.js +8 -0
  210. package/server/templates/page.html +4 -2
  211. package/plugins/contacts/data/contacts.json +0 -20
  212. package/plugins/notes/data/notes.json +0 -1
  213. package/plugins/site-search/admin/views/site-search.js +0 -116
  214. package/plugins/site-search/config.js +0 -15
  215. package/plugins/site-search/plugin.js +0 -188
  216. package/plugins/site-search/plugin.json +0 -40
  217. package/plugins/site-search/public/inject-body.html +0 -17
  218. package/plugins/site-search/public/inject-head.html +0 -1
  219. package/plugins/site-search/public/search.css +0 -1
  220. package/plugins/site-search/public/search.js +0 -1
  221. package/plugins/todo/data/todos.json +0 -1
@@ -0,0 +1,173 @@
1
+ <div class="page-header">
2
+ <h1 id="theme-title"><span data-icon="droplet"></span> Theme</h1>
3
+ <div style="display:flex;gap:.5rem;align-items:center;">
4
+ <a href="/" target="_blank" rel="noopener" class="btn btn-ghost btn-sm">
5
+ <span data-icon="external-link"></span> View site
6
+ </a>
7
+ <button id="theme-save-btn" class="btn btn-primary"><span data-icon="save"></span> Save</button>
8
+ </div>
9
+ </div>
10
+
11
+ <div id="theme-errors" class="alert alert-danger mb-3" style="display:none;"></div>
12
+
13
+ <div class="tabs" id="theme-tabs">
14
+ <div class="tab-list">
15
+ <button class="tab-item active">Themes</button>
16
+ <button class="tab-item">Element Overrides</button>
17
+ </div>
18
+
19
+ <!-- ------------------------------------------------------------------ -->
20
+ <!-- Themes -->
21
+ <!-- ------------------------------------------------------------------ -->
22
+ <div class="tab-panel active">
23
+ <div class="card mb-4">
24
+ <div class="card-header"><h2>Site &amp; Admin</h2></div>
25
+ <div class="card-body">
26
+ <div class="row mb-3">
27
+ <div class="col-6">
28
+ <label class="form-label">Front-end Theme</label>
29
+ <select id="theme-frontend" class="form-select"></select>
30
+ <p class="form-hint text-muted" style="margin-top:.4rem;font-size:.8rem;">
31
+ Applied to every public page.</p>
32
+ </div>
33
+ <div class="col-6">
34
+ <label class="form-label">Admin Theme</label>
35
+ <select id="theme-admin" class="form-select"></select>
36
+ <p class="form-hint text-muted" style="margin-top:.4rem;font-size:.8rem;">
37
+ Applied to this admin panel. Takes effect as soon as you save.</p>
38
+ </div>
39
+ </div>
40
+
41
+ <div class="row">
42
+ <div class="col">
43
+ <label class="form-check-label" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
44
+ <input id="theme-auto-enabled" type="checkbox" class="form-check">
45
+ Switch automatically between a day and a night theme
46
+ </label>
47
+ <p id="theme-auto-roller-hint" class="form-hint text-muted"
48
+ style="display:none;margin-top:.35rem;font-size:.8rem;">
49
+ Custom themes (Theme Roller) do not support auto-switching.</p>
50
+ </div>
51
+ </div>
52
+ <div id="theme-auto-fields" class="row mt-3" style="display:none;">
53
+ <div class="col-6">
54
+ <label class="form-label">Day Theme</label>
55
+ <select id="theme-day" class="form-select"></select>
56
+ </div>
57
+ <div class="col-6">
58
+ <label class="form-label">Night Theme</label>
59
+ <select id="theme-night" class="form-select"></select>
60
+ </div>
61
+ <div class="col-3 mt-3">
62
+ <label class="form-label">Day Starts At</label>
63
+ <input id="theme-day-start" type="time" class="form-input" value="07:00">
64
+ </div>
65
+ <div class="col-3 mt-3">
66
+ <label class="form-label">Night Starts At</label>
67
+ <input id="theme-night-start" type="time" class="form-input" value="19:00">
68
+ </div>
69
+ <div class="col-12 mt-2">
70
+ <p class="form-hint text-muted" style="font-size:.8rem;">Themes switch on each visitor's local
71
+ time. They need not be a light/dark pair.</p>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <div class="card">
78
+ <div class="card-header"><h2>Typography</h2></div>
79
+ <div class="card-body">
80
+ <div class="row">
81
+ <div class="col-6">
82
+ <label class="form-label">Font Family</label>
83
+ <select id="theme-font-family" class="form-select">
84
+ <optgroup label="Sans-serif">
85
+ <option value="Roboto">Roboto (default)</option>
86
+ <option value="Inter">Inter</option>
87
+ <option value="Open Sans">Open Sans</option>
88
+ <option value="Lato">Lato</option>
89
+ <option value="Nunito">Nunito</option>
90
+ <option value="Poppins">Poppins</option>
91
+ <option value="Source Sans 3">Source Sans 3</option>
92
+ <option value="Montserrat">Montserrat</option>
93
+ <option value="Raleway">Raleway</option>
94
+ <option value="DM Sans">DM Sans</option>
95
+ <option value="Work Sans">Work Sans</option>
96
+ </optgroup>
97
+ <optgroup label="Serif">
98
+ <option value="Merriweather">Merriweather</option>
99
+ <option value="Playfair Display">Playfair Display</option>
100
+ <option value="Lora">Lora</option>
101
+ <option value="Roboto Slab">Roboto Slab</option>
102
+ </optgroup>
103
+ <optgroup label="Dyslexia-friendly">
104
+ <option value="Lexend">Lexend</option>
105
+ <option value="Atkinson Hyperlegible">Atkinson Hyperlegible</option>
106
+ <option value="Andika">Andika</option>
107
+ </optgroup>
108
+ <optgroup label="System">
109
+ <option value="system-ui">System UI (no web font)</option>
110
+ </optgroup>
111
+ </select>
112
+ <p class="form-hint text-muted" style="margin-top:.4rem;font-size:.8rem;">Public site body text.
113
+ The dyslexia-friendly group uses wider letterforms and spacing.</p>
114
+ </div>
115
+ <div class="col-3">
116
+ <label class="form-label">Font Size</label>
117
+ <select id="theme-font-size" class="form-select">
118
+ <option value="13">13px - Compact</option>
119
+ <option value="14">14px - Small</option>
120
+ <option value="15">15px - Slightly small</option>
121
+ <option value="16">16px - Default</option>
122
+ <option value="17">17px - Slightly large</option>
123
+ <option value="18">18px - Large</option>
124
+ <option value="20">20px - Extra large</option>
125
+ </select>
126
+ <p class="form-hint text-muted" style="margin-top:.4rem;font-size:.8rem;">Root font size. Every
127
+ rem value scales with it.</p>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ <!-- ------------------------------------------------------------------ -->
135
+ <!-- Element overrides -->
136
+ <!-- ------------------------------------------------------------------ -->
137
+ <div class="tab-panel">
138
+ <div class="card mb-4">
139
+ <div class="card-body">
140
+ <p class="text-muted" style="margin:0 0 .75rem;">
141
+ An override re-themes one region of the page. Give it a CSS selector, then either adopt another
142
+ theme wholesale or set individual design tokens. Everything inside that region follows - including
143
+ components rendered later - because a Domma theme is nothing but a block of
144
+ <code>--dm-*</code> values.
145
+ </p>
146
+ <button id="theme-add-override" class="btn btn-ghost btn-sm" type="button">
147
+ <span data-icon="plus"></span> Add override
148
+ </button>
149
+ </div>
150
+ </div>
151
+
152
+ <div id="theme-overrides"></div>
153
+ <p id="theme-overrides-empty" class="text-muted" style="text-align:center;padding:2rem 0;">
154
+ No overrides yet. The whole site uses the front-end theme.
155
+ </p>
156
+
157
+ <div class="card mt-4">
158
+ <div class="card-header">
159
+ <h2>Generated CSS</h2>
160
+ <button id="theme-refresh-css" class="btn btn-ghost btn-sm" type="button">
161
+ <span data-icon="refresh-cw"></span> Refresh
162
+ </button>
163
+ </div>
164
+ <div class="card-body">
165
+ <p class="text-muted" style="margin-top:0;font-size:.85rem;">
166
+ Exactly what is injected into every public page, ahead of your custom CSS.
167
+ </p>
168
+ <pre id="theme-css-preview" class="p-3"
169
+ style="max-height:320px;overflow:auto;background:var(--dm-surface-raised);border-radius:var(--dm-radius-md);font-size:.8rem;margin:0;">(nothing yet)</pre>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
@@ -0,0 +1,55 @@
1
+ import{api as v}from"../api.js";import{makeIconInput as B}from"/admin/js/lib/shortcode-modal.js";import{makePageInput as L}from"/admin/js/lib/page-picker.js";const C=[{label:"This page",actions:{navigate:"Go to a URL",reload:"Reload the page",back:"Back",forward:"Forward",print:"Print","copy-link":"Copy link to this view","site-search":"Open site search"}},{label:"Clipboard and selection",actions:{copy:"Copy the selection",cut:"Cut",paste:"Paste","select-all":"Select all"}},{label:"Collection displays",actions:{filter:"Filter the display",search:"Search this display",sort:"Sort the display",export:"Export the display","mark-as-spam":"Mark as spam"}},{label:"This site",actions:{"open-modal":"Open a block in a modal","run-action":"Run an Action"}}],q=Object.assign({},...C.map(c=>c.actions)),I={back:"greys out when there is no history behind the page",forward:"greys out unless you have gone back",copy:"greys out with nothing selected",cut:"greys out outside an editable field, or with nothing selected",paste:"greys out outside an editable field","site-search":"greys out on a site where search is unavailable",search:"greys out unless the click lands in a collection display",filter:"greys out unless the click lands in a collection display",sort:"greys out unless the click lands in a collection display",export:"greys out unless the click lands in a collection display"},M={"mark-as-spam":"needs a right-click on an entry inside a collection display"},O={navigate:{key:"url",label:"URL",placeholder:"/shop"},"run-action":{key:"slug",label:"Action slug",placeholder:"archive-entry"},"open-modal":{key:"block",label:"Block name",placeholder:"contact-card"},export:{key:"format",label:"Format",placeholder:"csv"},sort:{key:"field",label:"Field",placeholder:"price"},filter:{key:"field",label:"Field",placeholder:"category"},"mark-as-spam":{key:"field",label:"Flag field (optional)",placeholder:"spam"}};function d(c){return String(c??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function R(c){return String(c||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,64)}let g=null;function b(){if(g){try{g.destroy()}catch{}g=null}const c=document.getElementById("ctx-preview");if(c&&E.contextMenu&&E.contextMenu.registry)for(const m of E.contextMenu.registry(c))try{m.destroy()}catch{}}function j(c){return String(c||"").split(",").map(m=>m.trim()).filter(Boolean)}export const contextMenuEditorView={templateUrl:"/admin/js/templates/context-menu-editor.html",async onMount(c){E.tabs&&E.tabs(c.find("#ctx-tabs").get(0)),b();const m=location.hash.match(/^#\/context-menus\/edit\/([^?#]+)/),y=m?decodeURIComponent(m[1]):null,x=!y;let a=[];if(!x)try{const t=await v.contextMenus.get(y);document.getElementById("ctx-title").textContent=`Edit: ${t.name||t.slug}`,$("#ctx-name").val(t.name||""),$("#ctx-slug").val(t.slug).prop("disabled",!0),$("#ctx-selector").val(t.target?.selector||""),$("#ctx-match").val(t.target?.match||""),$("#ctx-exclude").val(t.target?.exclude||""),$("#ctx-inherit").val(String(t.inherit===!1?"false":t.inherit||"append")),$("#ctx-enabled").prop("checked",t.enabled!==!1),$("#ctx-accent").val(t.theme?.accent||""),$("#ctx-transition").val(t.theme?.transition||""),$("#ctx-density").val(t.theme?.density||""),$("#ctx-radius").val(t.theme?.radius||""),$("#ctx-shadow").val(t.theme?.shadow||""),$("#ctx-opacity").val(t.theme?.opacity??""),$("#ctx-pages").val((t.binding?.pages||[]).join(", ")),$("#ctx-projects").val((t.binding?.projects||[]).join(", ")),a=Array.isArray(t.items)?t.items:[]}catch(t){E.toast(`Could not load "${y}": ${t.message||t}`,{type:"error"});return}let f=!x;$("#ctx-slug").on("input",()=>{f=!0}),$("#ctx-name").on("input",function(){f||$("#ctx-slug").val(R(this.value))});function u(){const t=document.getElementById("ctx-items"),s=document.getElementById("ctx-items-empty");s.style.display=a.length?"none":"",t.replaceChildren(),a.forEach((e,i)=>{const n=e.type||"item",o=O[e.action?.type],r=document.createElement("div");r.className="card mb-2",r.innerHTML=`
2
+ <div class="card-body">
3
+ <div class="row">
4
+ <div class="col-4">
5
+ <label class="form-label">Type</label>
6
+ <select class="form-select ctx-item-type" data-i="${i}">
7
+ <option value="item"${n==="item"?" selected":""}>Item</option>
8
+ <option value="divider"${n==="divider"?" selected":""}>Divider</option>
9
+ <option value="header"${n==="header"?" selected":""}>Heading</option>
10
+ </select>
11
+ </div>
12
+ ${n==="divider"?"":`
13
+ <div class="col-8">
14
+ <label class="form-label">Text</label>
15
+ <input class="form-input ctx-item-text" data-i="${i}" type="text" value="${d(e.text)}">
16
+ </div>`}
17
+ </div>
18
+ ${n==="item"?`
19
+ <div class="row mt-2">
20
+ <div class="col-4">
21
+ <label class="form-label">Icon</label>
22
+ <div class="ctx-icon-slot" data-i="${i}"></div>
23
+ </div>
24
+ <div class="col-4">
25
+ <label class="form-label">Does</label>
26
+ <select class="form-select ctx-item-action" data-i="${i}">
27
+ ${C.map(l=>`
28
+ <optgroup label="${d(l.label)}">
29
+ ${Object.entries(l.actions).map(([S,T])=>`<option value="${S}"${e.action?.type===S?" selected":""}>${d(T)}</option>`).join("")}
30
+ </optgroup>`).join("")}
31
+ </select>
32
+ ${I[e.action?.type]?`<small class="text-muted">${d(I[e.action.type])}</small>`:""}
33
+ ${M[e.action?.type]?`<small class="text-muted">${d(M[e.action.type])}</small>`:""}
34
+ </div>
35
+ ${o?`
36
+ <div class="col-4">
37
+ <label class="form-label">${d(o.label)}</label>
38
+ ${o.key==="url"?`<div class="ctx-url-slot" data-i="${i}"></div>`:`<input class="form-input ctx-item-arg" data-i="${i}" data-key="${o.key}"
39
+ type="text" value="${d(e.action?.[o.key])}" placeholder="${d(o.placeholder)}">`}
40
+ </div>`:""}
41
+ </div>
42
+ <div class="row mt-2">
43
+ <div class="col-6">
44
+ <label class="form-check">
45
+ <input type="checkbox" class="ctx-item-danger" data-i="${i}"${e.danger?" checked":""}>
46
+ Destructive (red)
47
+ </label>
48
+ </div>
49
+ </div>`:""}
50
+ <div class="mt-2 text-right">
51
+ <button class="btn btn-sm btn-ghost ctx-item-up" data-i="${i}" ${i===0?"disabled":""}>Up</button>
52
+ <button class="btn btn-sm btn-ghost ctx-item-down" data-i="${i}" ${i===a.length-1?"disabled":""}>Down</button>
53
+ <button class="btn btn-sm btn-ghost ctx-item-remove" data-i="${i}">Remove</button>
54
+ </div>
55
+ </div>`;const w=r.querySelector(".ctx-icon-slot");if(w){const l=B("edit",e.icon||"");l.input.className+=" ctx-item-icon",l.input.dataset.i=String(i),w.appendChild(l.el)}const N=r.querySelector(".ctx-url-slot");if(N){const l=L(o.placeholder,e.action?.url||"");l.input.className+=" ctx-item-arg",l.input.dataset.i=String(i),l.input.dataset.key="url",N.appendChild(l.el)}t.appendChild(r)}),Domma.icons.scan("#ctx-items"),p()}c.on("change",".ctx-item-type",function(){const t=Number(this.dataset.i);a[t].type=this.value,this.value!=="item"?delete a[t].action:a[t].action||(a[t].action={type:"navigate"}),u()}),c.on("input",".ctx-item-text",function(){a[Number(this.dataset.i)].text=this.value,p()}),c.on("input",".ctx-item-icon",function(){a[Number(this.dataset.i)].icon=this.value,p()}),c.on("change",".ctx-item-action",function(){const t=Number(this.dataset.i);a[t].action={type:this.value},u()}),c.on("input",".ctx-item-arg",function(){const t=Number(this.dataset.i);a[t].action=a[t].action||{},a[t].action[this.dataset.key]=this.value,p()}),c.on("change",".ctx-item-danger",function(){a[Number(this.dataset.i)].danger=this.checked,p()}),c.on("click",".ctx-item-remove",function(){a.splice(Number(this.dataset.i),1),u()}),c.on("click",".ctx-item-up",function(){const t=Number(this.dataset.i);[a[t-1],a[t]]=[a[t],a[t-1]],u()}),c.on("click",".ctx-item-down",function(){const t=Number(this.dataset.i);[a[t+1],a[t]]=[a[t],a[t+1]],u()}),$("#ctx-add-item").on("click",()=>{a.push({type:"item",text:"New item",action:{type:"navigate",url:""}}),u()});function k(){const t={},s=$("#ctx-accent").val(),e=$("#ctx-transition").val(),i=$("#ctx-density").val(),n=$("#ctx-radius").val(),o=$("#ctx-shadow").val(),r=$("#ctx-opacity").val();return s&&(t.accent=s),e&&(t.transition=e),i&&(t.density=i),n&&(t.radius=n),o&&(t.shadow=o),r!==""&&r!=null&&(t.opacity=Number(r)),t}function A(){const t=$("#ctx-inherit").val(),s=j($("#ctx-pages").val()),e=j($("#ctx-projects").val()),i={slug:$("#ctx-slug").val().trim(),name:$("#ctx-name").val().trim(),enabled:$("#ctx-enabled").is(":checked"),target:{selector:$("#ctx-selector").val().trim(),match:$("#ctx-match").val().trim(),exclude:$("#ctx-exclude").val().trim()},inherit:t==="false"?!1:t,items:a},n=k();return Object.keys(n).length&&(i.theme=n),(s.length||e.length)&&(i.binding={},s.length&&(i.binding.pages=s),e.length&&(i.binding.projects=e)),i}E.tooltip&&E.tooltip(".ctx-help",{trigger:"click",position:"right"});function p(){b();const t=a.map(e=>{const i=e.type||"item";return i==="divider"?{type:"divider"}:i==="header"?{type:"header",header:e.text||""}:{label:e.text||"(no text)",icon:e.icon||void 0,danger:!!e.danger}});if(!t.some(e=>e.label))return;const s=k();g=E.contextMenu("#ctx-preview",{items:t,inherit:!1,accent:s.accent,transition:s.transition,density:s.density,radius:s.radius,shadow:s.shadow,opacity:s.opacity})}c.on("change","#ctx-accent, #ctx-transition, #ctx-density, #ctx-radius, #ctx-shadow",p),c.on("input","#ctx-opacity",p);function h(t){const s=document.getElementById("ctx-errors");if(!t){s.style.display="none";return}s.style.display="",s.innerHTML=String(t).split("; ").map(e=>`<div>${d(e)}</div>`).join(""),s.scrollIntoView({behavior:"smooth",block:"nearest"})}$("#ctx-save").on("click",async function(){h(null);const t=A();if(!t.slug)return h("Slug is required");if(!t.name)return h("Name is required");if(!t.target.selector)return h("Container selector is required");this.disabled=!0;try{x?(await v.contextMenus.create(t),E.toast("Context menu created.",{type:"success"}),location.hash="#/context-menus"):(await v.contextMenus.update(y,t),E.toast("Context menu saved.",{type:"success"}))}catch(s){h(s.message||String(s))}finally{this.disabled=!1}}),u(),Domma.icons.scan(c.get(0))},onUnmount(){E.contextMenu&&E.contextMenu.closeAll&&E.contextMenu.closeAll(),b()}};
@@ -0,0 +1,5 @@
1
+ import{api as l}from"../api.js";import{filterByProject as u,getProjectFromHash as g}from"../lib/project-context.js";import{keepTableIconsAlive as m}from"../lib/table-icons.js";function o(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}export const contextMenusView={templateUrl:"/admin/js/templates/context-menus.html",async onMount(s){const r=E.loader(s.get(0),{type:"dots"});let c;try{c=await l.contextMenus.list().catch(()=>[])}finally{r.destroy()}const i=g(),d=i?u(c,i):c;T.create("#context-menus-table",{pageSize:25,data:d,emptyMessage:'No context menus yet. Click "New context menu" to create one.',columns:[{key:"slug",title:"Slug",sortable:!0,render:e=>`<code>${o(e)}</code>`},{key:"name",title:"Name",sortable:!0,render:(e,t)=>`<a href="#/context-menus/edit/${encodeURIComponent(t.slug)}" style="font-weight:600;">${o(e||t.slug)}</a>`},{key:"selector",title:"Binds to",render:(e,t)=>{const n=t.selector?`<code>${o(t.selector)}</code>`:'<span class="text-muted">-</span>',a=t.match?` &rarr; <code>${o(t.match)}</code>`:"";return n+a}},{key:"itemsCount",title:"Items",render:(e,t)=>String(t.itemsCount??0)},{key:"binding",title:"Scope",render:(e,t)=>{const n=t.binding;if(!n)return'<span class="badge badge-secondary">site-wide</span>';const a=[...(n.projects||[]).map(p=>`project:${p}`),...n.pages||[]];return a.length?`<span class="badge badge-info" data-tooltip="${o(a.join(", "))}">${a.length} rule${a.length===1?"":"s"}</span>`:'<span class="badge badge-warning" data-tooltip="Binding matches nothing, so this menu never appears">never matches</span>'}},{key:"enabled",title:"Enabled",render:(e,t)=>t.enabled===!1?'<span class="badge badge-secondary">off</span>':'<span class="badge badge-success">on</span>'},{key:"_actions",title:"Actions",render:(e,t)=>{const n=o(t.slug);return`
2
+ <a class="btn btn-sm btn-ghost" href="#/context-menus/edit/${encodeURIComponent(t.slug)}" data-tooltip="Edit"><span data-icon="edit" data-icon-size="14"></span></a>
3
+ <button class="btn btn-sm btn-ghost btn-duplicate" data-slug="${n}" data-tooltip="Duplicate"><span data-icon="copy" data-icon-size="14"></span></button>
4
+ <button class="btn btn-sm btn-ghost btn-delete" data-slug="${n}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>
5
+ `}}]}),Domma.icons.scan("#context-menus-table"),m(s.get(0)),document.querySelectorAll("#context-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 l.contextMenus.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(){const e=$(this).data("slug");if(await E.confirm(`Delete context menu "${e}"? This cannot be undone.`))try{await l.contextMenus.remove(e),E.toast("Context menu deleted.",{type:"success"}),location.reload()}catch(n){E.toast(`Delete failed: ${n.message||n}`,{type:"error"})}})}};