domma-cms 0.2.1 → 0.5.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 (166) hide show
  1. package/README.md +3 -3
  2. package/admin/css/admin.css +1 -1200
  3. package/admin/dist/domma/domma-tools.css +2313 -0
  4. package/admin/dist/domma/domma-tools.min.js +10 -0
  5. package/admin/index.html +4 -0
  6. package/admin/js/api.js +1 -242
  7. package/admin/js/app.js +9 -279
  8. package/admin/js/config/sidebar-config.js +1 -115
  9. package/admin/js/lib/card.js +1 -63
  10. package/admin/js/lib/image-editor.js +1 -869
  11. package/admin/js/lib/markdown-toolbar.js +54 -421
  12. package/admin/js/templates/action-editor.html +171 -0
  13. package/admin/js/templates/actions-list.html +19 -0
  14. package/admin/js/templates/api-reference.html +1411 -0
  15. package/admin/js/templates/block-editor.html +158 -0
  16. package/admin/js/templates/blocks.html +8 -0
  17. package/admin/js/templates/collection-editor.html +47 -0
  18. package/admin/js/templates/collection-entries.html +3 -0
  19. package/admin/js/templates/collections.html +51 -4
  20. package/admin/js/templates/documentation.html +258 -0
  21. package/admin/js/templates/form-editor.html +238 -0
  22. package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
  23. package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
  24. package/admin/js/templates/layouts.html +44 -7
  25. package/admin/js/templates/login.html +29 -4
  26. package/admin/js/templates/my-profile.html +17 -0
  27. package/admin/js/templates/page-editor.html +48 -0
  28. package/admin/js/templates/pages.html +6 -1
  29. package/admin/js/templates/pro-docs.html +259 -0
  30. package/admin/js/templates/role-editor.html +59 -0
  31. package/admin/js/templates/roles.html +10 -0
  32. package/admin/js/templates/settings.html +137 -18
  33. package/admin/js/templates/tutorials.html +81 -0
  34. package/admin/js/templates/user-editor.html +7 -0
  35. package/admin/js/templates/users.html +3 -1
  36. package/admin/js/templates/view-editor.html +201 -0
  37. package/admin/js/templates/view-preview.html +51 -0
  38. package/admin/js/templates/views-list.html +19 -0
  39. package/admin/js/views/action-editor.js +1 -0
  40. package/admin/js/views/actions-list.js +1 -0
  41. package/admin/js/views/api-reference.js +1 -0
  42. package/admin/js/views/block-editor.js +8 -0
  43. package/admin/js/views/blocks.js +4 -0
  44. package/admin/js/views/collection-editor.js +3 -487
  45. package/admin/js/views/collection-entries.js +1 -484
  46. package/admin/js/views/collections.js +1 -153
  47. package/admin/js/views/dashboard.js +1 -56
  48. package/admin/js/views/documentation.js +1 -12
  49. package/admin/js/views/form-editor.js +8 -0
  50. package/admin/js/views/form-submissions.js +1 -0
  51. package/admin/js/views/forms.js +1 -0
  52. package/admin/js/views/index.js +1 -39
  53. package/admin/js/views/layouts.js +9 -42
  54. package/admin/js/views/login.js +7 -251
  55. package/admin/js/views/media.js +1 -240
  56. package/admin/js/views/my-profile.js +1 -0
  57. package/admin/js/views/navigation.js +14 -212
  58. package/admin/js/views/page-editor.js +72 -661
  59. package/admin/js/views/pages.js +5 -72
  60. package/admin/js/views/plugins.js +13 -90
  61. package/admin/js/views/pro-docs.js +1 -0
  62. package/admin/js/views/role-editor.js +1 -0
  63. package/admin/js/views/roles.js +4 -0
  64. package/admin/js/views/settings.js +3 -199
  65. package/admin/js/views/tutorials.js +1 -12
  66. package/admin/js/views/user-editor.js +1 -88
  67. package/admin/js/views/users.js +4 -76
  68. package/admin/js/views/view-editor.js +1 -0
  69. package/admin/js/views/view-preview.js +1 -0
  70. package/admin/js/views/views-list.js +1 -0
  71. package/bin/cli.js +1 -1
  72. package/config/auth.json +2 -17
  73. package/config/connections.json.bak +9 -0
  74. package/config/connections.json.example +9 -0
  75. package/config/navigation.json +15 -0
  76. package/config/plugins.json +19 -29
  77. package/config/server.json +6 -6
  78. package/config/site.json +17 -6
  79. package/package.json +24 -10
  80. package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
  81. package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
  82. package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
  83. package/plugins/domma-effects/public/celebrations/index.js +1 -535
  84. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
  85. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
  86. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
  87. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
  88. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
  89. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
  90. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
  91. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
  92. package/plugins/example-analytics/stats.json +21 -12
  93. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  94. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  95. package/plugins/theme-roller/config.js +1 -0
  96. package/plugins/theme-roller/plugin.js +233 -0
  97. package/plugins/theme-roller/plugin.json +31 -0
  98. package/plugins/theme-roller/public/active-theme.css +0 -0
  99. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  100. package/public/css/forms.css +1 -0
  101. package/public/css/site.css +1 -302
  102. package/public/js/btt.js +1 -90
  103. package/public/js/cookie-consent.js +1 -61
  104. package/public/js/form-logic-engine.js +1 -0
  105. package/public/js/forms.js +1 -0
  106. package/public/js/site.js +1 -204
  107. package/scripts/build.js +194 -129
  108. package/scripts/pro.js +254 -0
  109. package/scripts/reset.js +33 -8
  110. package/scripts/seed.js +343 -78
  111. package/scripts/setup.js +5 -4
  112. package/server/middleware/auth.js +136 -97
  113. package/server/routes/api/actions.js +200 -0
  114. package/server/routes/api/auth.js +292 -116
  115. package/server/routes/api/blocks.js +84 -0
  116. package/server/routes/api/collections.js +88 -23
  117. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +483 -505
  118. package/server/routes/api/layouts.js +49 -25
  119. package/server/routes/api/media.js +118 -93
  120. package/server/routes/api/navigation.js +40 -37
  121. package/server/routes/api/pages.js +132 -118
  122. package/server/routes/api/plugins.js +6 -3
  123. package/server/routes/api/settings.js +104 -89
  124. package/server/routes/api/users.js +27 -21
  125. package/server/routes/api/views.js +148 -0
  126. package/server/routes/public.js +124 -108
  127. package/server/server.js +269 -173
  128. package/server/services/actions.js +387 -0
  129. package/server/services/adapterRegistry.js +98 -0
  130. package/server/services/adapters/FileAdapter.js +192 -0
  131. package/server/services/adapters/MongoAdapter.js +220 -0
  132. package/server/services/blocks.js +162 -0
  133. package/server/services/collections.js +74 -86
  134. package/server/services/connectionManager.js +102 -0
  135. package/server/services/content.js +312 -307
  136. package/{plugins/form-builder → server/services}/email.js +126 -103
  137. package/server/services/forms.js +173 -0
  138. package/server/services/markdown.js +1378 -648
  139. package/server/services/permissionRegistry.js +173 -0
  140. package/server/services/presetCollections.js +251 -0
  141. package/server/services/renderer.js +75 -1
  142. package/server/services/roles.js +227 -0
  143. package/server/services/rowAccess.js +104 -0
  144. package/server/services/userProfiles.js +199 -0
  145. package/server/services/users.js +281 -212
  146. package/server/services/views.js +280 -0
  147. package/server/templates/page.html +119 -113
  148. package/plugins/form-builder/admin/templates/form-editor.html +0 -171
  149. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  150. package/plugins/form-builder/admin/views/form-editor.js +0 -1442
  151. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  152. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  153. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  154. package/plugins/form-builder/config.js +0 -9
  155. package/plugins/form-builder/data/forms/consent.json +0 -104
  156. package/plugins/form-builder/data/forms/contact-details.json +0 -63
  157. package/plugins/form-builder/data/forms/contacts.json +0 -66
  158. package/plugins/form-builder/data/submissions/consent.json +0 -13
  159. package/plugins/form-builder/data/submissions/contact-details.json +0 -1
  160. package/plugins/form-builder/data/submissions/contacts.json +0 -26
  161. package/plugins/form-builder/plugin.json +0 -52
  162. package/plugins/form-builder/public/form-logic-engine.js +0 -568
  163. package/plugins/form-builder/public/inject-body.html +0 -352
  164. package/plugins/form-builder/public/inject-head.html +0 -58
  165. package/plugins/form-builder/public/package.json +0 -1
  166. package/scripts/copy-domma.js +0 -48
@@ -1,218 +1,20 @@
1
- /**
2
- * Navigation View
3
- */
4
- import {api} from '../api.js';
5
-
6
- let _nextId = 1;
7
-
8
- const esc = s => String(s || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
9
-
10
- function flattenItems(items) {
11
- const flat = [];
12
- (items || []).forEach(item => {
13
- const id = _nextId++;
14
- flat.push({ _id: id, text: item.text || '', url: item.url || '', icon: item.icon || '', parentId: null });
15
- (item.items || item.children || []).forEach(child => {
16
- flat.push({ _id: _nextId++, text: child.text || '', url: child.url || '', icon: child.icon || '', parentId: id });
17
- });
18
- });
19
- return flat;
20
- }
21
-
22
- function nestItems(flat) {
23
- return flat
24
- .filter(i => i.parentId === null)
25
- .map(parent => {
26
- const children = flat
27
- .filter(i => i.parentId === parent._id)
28
- .map(c => ({ text: c.text, url: c.url, ...(c.icon && { icon: c.icon }) }));
29
- const obj = { text: parent.text, url: parent.url, ...(parent.icon && { icon: parent.icon }) };
30
- if (children.length) obj.items = children;
31
- return obj;
32
- });
33
- }
34
-
35
- // Returns items sorted so children appear immediately after their parent
36
- function getOrderedItems(flat) {
37
- const result = [];
38
- flat.filter(i => i.parentId === null).forEach(parent => {
39
- result.push(parent);
40
- flat.filter(i => i.parentId === parent._id).forEach(child => result.push(child));
41
- });
42
- return result;
43
- }
44
-
45
- export const navigationView = {
46
- templateUrl: '/admin/js/templates/navigation.html',
47
-
48
- async onMount($container) {
49
- let [nav, site] = await Promise.all([
50
- api.navigation.get().catch(() => ({brand: {}, items: []})),
51
- api.settings.get().catch(() => ({}))
52
- ]);
53
- let flatItems = flattenItems(nav.items);
54
- let footerLinks = (site.footer?.links || []).map(l => ({text: l.text || '', url: l.url || ''}));
55
-
56
- const syncFromDom = () => {
57
- $container.find('.nav-item-row').each(function () {
58
- const id = parseInt($(this).data('id'), 10);
59
- const item = flatItems.find(i => i._id === id);
60
- if (!item) return;
61
- item.text = $(this).find('.item-text').val();
62
- item.url = $(this).find('.item-url').val();
63
- item.icon = $(this).find('.item-icon').val();
64
- const parentVal = $(this).find('.item-parent').val();
65
- item.parentId = parentVal ? parseInt(parentVal, 10) : null;
66
- });
67
- // Promote children whose parent is now itself a child
68
- const childIds = new Set(flatItems.filter(i => i.parentId !== null).map(i => i._id));
69
- flatItems.forEach(i => {
70
- if (i.parentId !== null && childIds.has(i.parentId)) i.parentId = null;
71
- });
72
- };
73
-
74
- const renderItems = () => {
75
- const $list = $container.find('#nav-items-list').empty();
76
- const topLevel = flatItems.filter(i => i.parentId === null);
77
-
78
- getOrderedItems(flatItems).forEach(item => {
79
- const isChild = item.parentId !== null;
80
- const parentOptions = `<option value="">— top-level —</option>` +
81
- topLevel
82
- .filter(t => t._id !== item._id)
83
- .map(t => `<option value="${t._id}"${t._id === item.parentId ? ' selected' : ''}>${esc(t.text) || '(untitled)'}</option>`)
84
- .join('');
85
-
86
- $list.append(`
87
- <div class="nav-item-row${isChild ? ' nav-item-row--child' : ''}" data-id="${item._id}">
88
- <span class="nav-col-indent">${isChild ? '↳' : ''}</span>
89
- <input type="text" class="form-input item-text nav-col-main" value="${esc(item.text)}" placeholder="Label">
90
- <input type="text" class="form-input item-url nav-col-main" value="${esc(item.url)}" placeholder="/url">
91
- <input type="text" class="form-input item-icon nav-col-icon" value="${esc(item.icon)}" placeholder="icon">
92
- <select class="form-select item-parent nav-col-parent">${parentOptions}</select>
1
+ import{api as f}from"../api.js";let x=1;const d=t=>String(t||"").replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;");function b(t){const e=[];return(t||[]).forEach(r=>{const o=x++;e.push({_id:o,text:r.text||"",url:r.url||"",icon:r.icon||"",parentId:null}),(r.items||r.children||[]).forEach(l=>{e.push({_id:x++,text:l.text||"",url:l.url||"",icon:l.icon||"",parentId:o})})}),e}function I(t){return t.filter(e=>e.parentId===null).map(e=>{const r=t.filter(l=>l.parentId===e._id).map(l=>({text:l.text,url:l.url,...l.icon&&{icon:l.icon}})),o={text:e.text,url:e.url,...e.icon&&{icon:e.icon}};return r.length&&(o.items=r),o})}function g(t){const e=[];return t.filter(r=>r.parentId===null).forEach(r=>{e.push(r),t.filter(o=>o.parentId===r._id).forEach(o=>e.push(o))}),e}export const navigationView={templateUrl:"/admin/js/templates/navigation.html",async onMount(t){let[e,r]=await Promise.all([f.navigation.get().catch(()=>({brand:{},items:[]})),f.settings.get().catch(()=>({}))]),o=b(e.items),l=(r.footer?.links||[]).map(s=>({text:s.text||"",url:s.url||""}));const v=()=>{t.find(".nav-item-row").each(function(){const i=parseInt($(this).data("id"),10),n=o.find(a=>a._id===i);if(!n)return;n.text=$(this).find(".item-text").val(),n.url=$(this).find(".item-url").val(),n.icon=$(this).find(".item-icon").val();const c=$(this).find(".item-parent").val();n.parentId=c?parseInt(c,10):null});const s=new Set(o.filter(i=>i.parentId!==null).map(i=>i._id));o.forEach(i=>{i.parentId!==null&&s.has(i.parentId)&&(i.parentId=null)})},p=()=>{const s=t.find("#nav-items-list").empty(),i=o.filter(n=>n.parentId===null);g(o).forEach(n=>{const c=n.parentId!==null,a='<option value="">\u2014 top-level \u2014</option>'+i.filter(u=>u._id!==n._id).map(u=>`<option value="${u._id}"${u._id===n.parentId?" selected":""}>${d(u.text)||"(untitled)"}</option>`).join("");s.append(`
2
+ <div class="nav-item-row${c?" nav-item-row--child":""}" data-id="${n._id}">
3
+ <span class="nav-col-indent">${c?"\u21B3":""}</span>
4
+ <input type="text" class="form-input item-text nav-col-main" value="${d(n.text)}" placeholder="Label">
5
+ <input type="text" class="form-input item-url nav-col-main" value="${d(n.url)}" placeholder="/url">
6
+ <input type="text" class="form-input item-icon nav-col-icon" value="${d(n.icon)}" placeholder="icon">
7
+ <select class="form-select item-parent nav-col-parent">${a}</select>
93
8
  <span class="nav-col-action">
94
- <button class="btn btn-sm btn-danger btn-remove-item" data-id="${item._id}" data-tooltip="Remove"><span data-icon="trash"></span></button>
9
+ <button class="btn btn-sm btn-danger btn-remove-item" data-id="${n._id}" data-tooltip="Remove"><span data-icon="trash"></span></button>
95
10
  </span>
96
11
  </div>
97
- `);
98
- });
99
- Domma.icons.scan('#nav-items-list');
100
- document.querySelectorAll('#nav-items-list [data-tooltip]').forEach(el => {
101
- E.tooltip(el, {content: el.getAttribute('data-tooltip'), position: 'top'});
102
- });
103
- };
104
-
105
- const renderFooterLinks = () => {
106
- const $list = $container.find('#footer-links-list').empty();
107
- footerLinks.forEach((link, idx) => {
108
- $list.append(`
109
- <div class="nav-item-row" data-footer-idx="${idx}">
110
- <input type="text" class="form-input footer-link-text nav-col-main" value="${esc(link.text)}" placeholder="Label">
111
- <input type="text" class="form-input footer-link-url nav-col-main" value="${esc(link.url)}" placeholder="/url">
12
+ `)}),Domma.icons.scan("#nav-items-list"),document.querySelectorAll("#nav-items-list [data-tooltip]").forEach(n=>{E.tooltip(n,{content:n.getAttribute("data-tooltip"),position:"top"})})},m=()=>{const s=t.find("#footer-links-list").empty();l.forEach((i,n)=>{s.append(`
13
+ <div class="nav-item-row" data-footer-idx="${n}">
14
+ <input type="text" class="form-input footer-link-text nav-col-main" value="${d(i.text)}" placeholder="Label">
15
+ <input type="text" class="form-input footer-link-url nav-col-main" value="${d(i.url)}" placeholder="/url">
112
16
  <span class="nav-col-action">
113
- <button class="btn btn-sm btn-danger btn-remove-footer" data-idx="${idx}" data-tooltip="Remove"><span data-icon="trash"></span></button>
17
+ <button class="btn btn-sm btn-danger btn-remove-footer" data-idx="${n}" data-tooltip="Remove"><span data-icon="trash"></span></button>
114
18
  </span>
115
19
  </div>
116
- `);
117
- });
118
- Domma.icons.scan('#footer-links-list');
119
- document.querySelectorAll('#footer-links-list [data-tooltip]').forEach(el => {
120
- E.tooltip(el, {content: el.getAttribute('data-tooltip'), position: 'top'});
121
- });
122
- };
123
-
124
- const syncFooterFromDom = () => {
125
- footerLinks = [];
126
- $container.find('#footer-links-list .nav-item-row').each(function () {
127
- footerLinks.push({
128
- text: $(this).find('.footer-link-text').val().trim(),
129
- url: $(this).find('.footer-link-url').val().trim()
130
- });
131
- });
132
- };
133
-
134
- $container.find('#add-footer-link').on('click', () => {
135
- syncFooterFromDom();
136
- footerLinks.push({text: '', url: ''});
137
- renderFooterLinks();
138
- });
139
-
140
- $container.off('click', '.btn-remove-footer').on('click', '.btn-remove-footer', function () {
141
- syncFooterFromDom();
142
- const idx = parseInt($(this).data('idx'), 10);
143
- footerLinks.splice(idx, 1);
144
- renderFooterLinks();
145
- });
146
-
147
- $container.find('#field-brand-text').val(nav.brand?.text || '');
148
- $container.find('#field-brand-url').val(nav.brand?.url || '/');
149
- $container.find('#field-brand-icon').val(nav.brand?.icon || '');
150
- $container.find('#field-nav-variant').val(nav.variant || 'dark');
151
- renderItems();
152
- renderFooterLinks();
153
-
154
- $container.find('#add-nav-item').on('click', () => {
155
- syncFromDom();
156
- flatItems.push({ _id: _nextId++, text: '', url: '', icon: '', parentId: null });
157
- renderItems();
158
- });
159
-
160
- $container.off('click', '.btn-remove-item').on('click', '.btn-remove-item', function () {
161
- syncFromDom();
162
- const id = parseInt($(this).data('id'), 10);
163
- flatItems = flatItems.filter(i => i._id !== id && i.parentId !== id);
164
- renderItems();
165
- });
166
-
167
- // Re-render when a parent changes so other dropdowns update
168
- $container.off('change', '.item-parent').on('change', '.item-parent', function () {
169
- syncFromDom();
170
- renderItems();
171
- });
172
-
173
- $container.find('#save-nav-btn').on('click', async () => {
174
- syncFromDom();
175
- syncFooterFromDom();
176
- const nested = nestItems(
177
- flatItems.map(i => ({ ...i, text: i.text.trim(), url: i.url.trim(), icon: i.icon.trim() }))
178
- ).filter(i => i.text || i.url);
179
-
180
- const brandIcon = $container.find('#field-brand-icon').val().trim();
181
- const navData = {
182
- brand: {
183
- text: $container.find('#field-brand-text').val().trim(),
184
- url: $container.find('#field-brand-url').val().trim() || '/',
185
- ...(brandIcon && {icon: brandIcon})
186
- },
187
- items: nested,
188
- variant: $container.find('#field-nav-variant').val(),
189
- position: nav.position || 'sticky'
190
- };
191
-
192
- const validFooterLinks = footerLinks.filter(l => l.text || l.url);
193
-
194
- try {
195
- await api.navigation.save(navData);
196
- nav = navData;
197
- flatItems = flattenItems(nav.items);
198
- renderItems();
199
- E.toast('Navigation saved.', {type: 'success'});
200
- } catch (e) {
201
- console.error('[navigation] save failed:', e);
202
- E.toast('Failed to save navigation.', {type: 'error'});
203
- return;
204
- }
205
-
206
- // Save footer links into site config — best-effort, independent of nav save
207
- try {
208
- const currentSite = await api.settings.get().catch(() => ({}));
209
- await api.settings.save({...currentSite, footer: {...(currentSite.footer || {}), links: validFooterLinks}});
210
- footerLinks = validFooterLinks;
211
- renderFooterLinks();
212
- } catch (e) {
213
- console.error('[navigation] footer links save failed:', e);
214
- E.toast('Footer links could not be saved.', {type: 'warning'});
215
- }
216
- });
217
- }
218
- };
20
+ `)}),Domma.icons.scan("#footer-links-list"),document.querySelectorAll("#footer-links-list [data-tooltip]").forEach(i=>{E.tooltip(i,{content:i.getAttribute("data-tooltip"),position:"top"})})},h=()=>{l=[],t.find("#footer-links-list .nav-item-row").each(function(){l.push({text:$(this).find(".footer-link-text").val().trim(),url:$(this).find(".footer-link-url").val().trim()})})};t.find("#add-footer-link").on("click",()=>{h(),l.push({text:"",url:""}),m()}),t.off("click",".btn-remove-footer").on("click",".btn-remove-footer",function(){h();const s=parseInt($(this).data("idx"),10);l.splice(s,1),m()}),t.find("#field-brand-text").val(e.brand?.text||""),t.find("#field-brand-url").val(e.brand?.url||"/"),t.find("#field-brand-icon").val(e.brand?.icon||""),t.find("#field-nav-variant").val(e.variant||"dark"),p(),m(),t.find("#add-nav-item").on("click",()=>{v(),o.push({_id:x++,text:"",url:"",icon:"",parentId:null}),p()}),t.off("click",".btn-remove-item").on("click",".btn-remove-item",function(){v();const s=parseInt($(this).data("id"),10);o=o.filter(i=>i._id!==s&&i.parentId!==s),p()}),t.off("change",".item-parent").on("change",".item-parent",function(){v(),p()}),t.find("#save-nav-btn").on("click",async()=>{v(),h();const s=I(o.map(a=>({...a,text:a.text.trim(),url:a.url.trim(),icon:a.icon.trim()}))).filter(a=>a.text||a.url),i=t.find("#field-brand-icon").val().trim(),n={brand:{text:t.find("#field-brand-text").val().trim(),url:t.find("#field-brand-url").val().trim()||"/",...i&&{icon:i}},items:s,variant:t.find("#field-nav-variant").val(),position:e.position||"sticky"},c=l.filter(a=>a.text||a.url);try{await f.navigation.save(n),e=n,o=b(e.items),p(),E.toast("Navigation saved.",{type:"success"})}catch(a){console.error("[navigation] save failed:",a),E.toast("Failed to save navigation.",{type:"error"});return}try{const a=await f.settings.get().catch(()=>({}));await f.settings.save({...a,footer:{...a.footer||{},links:c}}),l=c,m()}catch(a){console.error("[navigation] footer links save failed:",a),E.toast("Footer links could not be saved.",{type:"warning"})}})}};