domma-cms 0.75.0 → 0.77.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 (150) hide show
  1. package/CLAUDE.md +1 -1
  2. package/admin/js/api.js +1 -1
  3. package/admin/js/app.js +4 -4
  4. package/admin/js/lib/action-editor-arrange.js +1 -0
  5. package/admin/js/lib/api-endpoint-editor-arrange.js +2 -0
  6. package/admin/js/lib/api-tokens-arrange.js +3 -0
  7. package/admin/js/lib/block-editor-arrange.js +2 -0
  8. package/admin/js/lib/collection-editor-arrange.js +3 -0
  9. package/admin/js/lib/collection-entries-arrange.js +1 -0
  10. package/admin/js/lib/component-editor-arrange.js +27 -0
  11. package/admin/js/lib/context-menu-editor-arrange.js +1 -0
  12. package/admin/js/lib/dashboard-arrange.js +1 -0
  13. package/admin/js/lib/effects-arrange.js +3 -0
  14. package/admin/js/lib/form-editor-arrange.js +1 -0
  15. package/admin/js/lib/form-submissions-arrange.js +2 -0
  16. package/admin/js/lib/layouts-arrange.js +1 -0
  17. package/admin/js/lib/menu-editor-arrange.js +2 -0
  18. package/admin/js/lib/my-profile-arrange.js +1 -0
  19. package/admin/js/lib/notifications-arrange.js +1 -0
  20. package/admin/js/lib/page-editor-arrange.js +1 -0
  21. package/admin/js/lib/plugin-code-arrange.js +1 -0
  22. package/admin/js/lib/plugins-arrange.js +2 -0
  23. package/admin/js/lib/project-page-arrange.js +1 -0
  24. package/admin/js/lib/role-editor-arrange.js +3 -0
  25. package/admin/js/lib/roles-arrange.js +1 -0
  26. package/admin/js/lib/search-settings-arrange.js +1 -0
  27. package/admin/js/lib/sidebar-renderer.js +2 -2
  28. package/admin/js/lib/site-settings-arrange.js +1 -0
  29. package/admin/js/lib/user-editor-arrange.js +1 -0
  30. package/admin/js/lib/users-arrange.js +1 -0
  31. package/admin/js/lib/view-editor-arrange.js +14 -0
  32. package/admin/js/templates/action-editor.html +223 -147
  33. package/admin/js/templates/api-endpoint-editor.html +172 -117
  34. package/admin/js/templates/api-tokens.html +83 -10
  35. package/admin/js/templates/block-editor.html +194 -203
  36. package/admin/js/templates/collection-editor.html +191 -113
  37. package/admin/js/templates/collection-entries.html +99 -36
  38. package/admin/js/templates/component-editor.html +128 -126
  39. package/admin/js/templates/context-menu-editor.html +208 -148
  40. package/admin/js/templates/dashboard.html +184 -22
  41. package/admin/js/templates/effects.html +155 -72
  42. package/admin/js/templates/form-editor.html +41 -26
  43. package/admin/js/templates/form-submissions.html +78 -26
  44. package/admin/js/templates/layouts.html +126 -43
  45. package/admin/js/templates/menu-editor.html +57 -17
  46. package/admin/js/templates/menu-locations.html +54 -11
  47. package/admin/js/templates/my-profile.html +100 -12
  48. package/admin/js/templates/notifications.html +70 -10
  49. package/admin/js/templates/page-editor.html +47 -26
  50. package/admin/js/templates/plugin-code.html +91 -73
  51. package/admin/js/templates/plugins.html +100 -28
  52. package/admin/js/templates/project-detail.html +83 -145
  53. package/admin/js/templates/project-settings.html +88 -53
  54. package/admin/js/templates/role-editor.html +158 -83
  55. package/admin/js/templates/roles.html +78 -7
  56. package/admin/js/templates/search.html +193 -216
  57. package/admin/js/templates/settings.html +404 -472
  58. package/admin/js/templates/user-editor.html +150 -14
  59. package/admin/js/templates/users.html +69 -9
  60. package/admin/js/templates/view-editor.html +203 -180
  61. package/admin/js/templates/view-preview.html +45 -45
  62. package/admin/js/views/action-editor.js +7 -1
  63. package/admin/js/views/api-endpoint-editor.js +2 -3
  64. package/admin/js/views/api-tokens.js +62 -8
  65. package/admin/js/views/block-editor.js +14 -8
  66. package/admin/js/views/collection-editor.js +64 -5
  67. package/admin/js/views/collection-entries.js +50 -1
  68. package/admin/js/views/component-editor.js +2 -28
  69. package/admin/js/views/context-menu-editor.js +54 -55
  70. package/admin/js/views/dashboard.js +7 -1
  71. package/admin/js/views/effects.js +31 -1
  72. package/admin/js/views/form-editor.js +7 -7
  73. package/admin/js/views/form-submissions.js +11 -1
  74. package/admin/js/views/index.js +1 -1
  75. package/admin/js/views/layouts.js +81 -1
  76. package/admin/js/views/login.js +2 -2
  77. package/admin/js/views/menu-editor.js +21 -43
  78. package/admin/js/views/menu-locations.js +16 -1
  79. package/admin/js/views/my-profile.js +1 -1
  80. package/admin/js/views/notifications.js +18 -1
  81. package/admin/js/views/page-editor.js +26 -26
  82. package/admin/js/views/plugin-code.js +23 -4
  83. package/admin/js/views/plugins.js +97 -39
  84. package/admin/js/views/project-detail.js +9 -1
  85. package/admin/js/views/project-settings.js +1 -1
  86. package/admin/js/views/role-editor.js +11 -3
  87. package/admin/js/views/roles.js +49 -4
  88. package/admin/js/views/search.js +7 -1
  89. package/admin/js/views/settings.js +13 -3
  90. package/admin/js/views/user-editor.js +1 -1
  91. package/admin/js/views/users.js +13 -4
  92. package/admin/js/views/view-editor.js +1 -1
  93. package/admin/js/views/view-preview.js +1 -1
  94. package/package.json +1 -1
  95. package/plugins/_lib/admin/mail/compose-window.js +97 -40
  96. package/plugins/_lib/admin/mail/contacts.js +4 -1
  97. package/plugins/_lib/admin/mail/diagnostics-section.js +4 -1
  98. package/plugins/_lib/admin/mail/folder-tree.js +1 -1
  99. package/plugins/_lib/admin/mail/image-senders-section.js +69 -90
  100. package/plugins/_lib/admin/mail/mail.css +1 -1
  101. package/plugins/_lib/admin/mail/mail.html +1 -1
  102. package/plugins/_lib/admin/mail/reader-view.js +303 -44
  103. package/plugins/_lib/admin/mail/when-closed.js +26 -0
  104. package/plugins/_lib/mail/readRoutes.js +43 -1
  105. package/plugins/_lib/mail/unreadBadge.js +42 -0
  106. package/plugins/free-tier.lock.json +5 -5
  107. package/plugins/mail-reader/CLAUDE.md +1 -0
  108. package/plugins/mail-reader/plugin.js +3 -1
  109. package/plugins/mail-reader/plugin.json +5 -4
  110. package/server/routes/api/api-tokens.js +56 -0
  111. package/server/routes/api/auth.js +7 -1
  112. package/server/routes/api/collections.js +13 -1
  113. package/server/routes/api/dashboard.js +32 -4
  114. package/server/routes/api/effects.js +68 -13
  115. package/server/routes/api/forms.js +104 -13
  116. package/server/routes/api/layouts.js +93 -2
  117. package/server/routes/api/notifications.js +140 -14
  118. package/server/routes/api/plugins.js +135 -4
  119. package/server/routes/api/roles.js +111 -0
  120. package/server/routes/api/search.js +38 -4
  121. package/server/routes/api/settings.js +52 -0
  122. package/server/routes/api/users.js +65 -3
  123. package/server/routes/api/views.js +55 -1
  124. package/server/server.js +12 -0
  125. package/server/services/components.js +9 -0
  126. package/server/services/plugins.js +7 -0
  127. package/server/services/roles.js +3 -0
  128. package/server/services/search.js +58 -0
  129. package/server/services/users.js +16 -2
  130. package/server/services/views.js +14 -0
  131. package/admin/js/templates/dashboard/activity-feed.html +0 -3
  132. package/admin/js/templates/dashboard/cache.html +0 -32
  133. package/admin/js/templates/dashboard/health-detail.html +0 -2
  134. package/admin/js/templates/dashboard/journeys.html +0 -17
  135. package/admin/js/templates/dashboard/kpi-strip.html +0 -34
  136. package/admin/js/templates/dashboard/spike-feed.html +0 -3
  137. package/admin/js/templates/dashboard/top-pages.html +0 -3
  138. package/admin/js/templates/dashboard/traffic-chart.html +0 -3
  139. package/admin/js/templates/plugin-marketplace.html +0 -29
  140. package/admin/js/views/block-editor-enhance.js +0 -1
  141. package/admin/js/views/dashboard/lib/escape.js +0 -1
  142. package/admin/js/views/dashboard/widgets/activity-feed.js +0 -1
  143. package/admin/js/views/dashboard/widgets/cache.js +0 -1
  144. package/admin/js/views/dashboard/widgets/health-detail.js +0 -1
  145. package/admin/js/views/dashboard/widgets/journeys.js +0 -1
  146. package/admin/js/views/dashboard/widgets/kpi-strip.js +0 -1
  147. package/admin/js/views/dashboard/widgets/spike-feed.js +0 -6
  148. package/admin/js/views/dashboard/widgets/top-pages.js +0 -1
  149. package/admin/js/views/dashboard/widgets/traffic-chart.js +0 -1
  150. package/admin/js/views/plugin-marketplace.js +0 -1
@@ -1,112 +1,123 @@
1
- <div class="page-header">
2
- <h2><span data-icon="menu"></span> <span id="ctx-title">New context menu</span></h2>
3
- <div class="header-actions">
4
- <a class="btn btn-secondary" href="#/context-menus"><span data-icon="arrow-left"></span> Back</a>
5
- <button class="btn btn-primary" id="ctx-save"><span data-icon="check"></span> Save</button>
6
- </div>
7
- </div>
1
+ <!-- Context menu editor. Bound with M.applyBindings - see admin/js/views/context-menu-editor.js, which also
2
+ inserts the Items row template (the router renders a templateUrl through _.render, eating any {{…}}).
3
+ No page heading or Back button: the Tool banner carries the name ("Context Menus › <menu>"), Save and
4
+ "All context menus". -->
5
+ <div class="cmx">
6
+ <p class="cmx-intro">
7
+ A context menu is not placed on a page the way a nav menu is. It attaches to a <strong>region</strong>
8
+ and opens when someone right-clicks inside it. So there are two questions to answer: which region, and
9
+ what the menu offers. Everything else has a sensible default.
10
+ <span data-help-title="Nested menus" data-help="Menus can sit inside one another. A right-click picks the INNERMOST menu covering the spot clicked, never the one that happened to load first. A menu that does not apply to a page is not sent to that page at all."></span>
11
+ </p>
8
12
 
9
- <div id="ctx-errors" class="alert alert-danger mb-4" style="display:none;"></div>
10
-
11
- <p class="text-muted ctx-intro">
12
- A context menu is not placed on a page the way a nav menu is. It attaches to a <strong>region</strong>
13
- and opens when someone right-clicks inside it. So there are two questions to answer: which region, and
14
- what the menu offers. Everything else has a sensible default.
15
- </p>
13
+ <div class="cmx-alert is-bad" data-bind-hidden="!serverError.value">
14
+ <strong>Not saved.</strong>
15
+ <div class="cmx-server-errors" data-each="serverErrors.value key=key"></div>
16
+ </div>
16
17
 
17
- <div class="row ctx-workspace">
18
- <div class="col-8">
19
- <div class="tabs" id="ctx-tabs">
20
- <div class="tab-list">
21
- <button class="tab-item active">Binding</button>
22
- <button class="tab-item">Items</button>
23
- <button class="tab-item">Appearance</button>
24
- <button class="tab-item">Scope</button>
18
+ <div class="cmx-unsaved" data-bind-hidden="!dirty.value">
19
+ <span class="cmx-dot" aria-hidden="true"></span>
20
+ <span>Unsaved changes<span data-bind-text="problemText.value"></span></span>
21
+ <span class="cmx-spacer"></span>
22
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="discard" data-bind-hidden="isNew">Discard</button>
23
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="saving.value">
24
+ <span data-icon="save" data-icon-size="13"></span> <span data-bind-text="saveText.value">Save</span></button>
25
25
  </div>
26
26
 
27
- <!-- Binding ------------------------------------------------------- -->
28
- <div class="tab-panel active">
29
- <div class="row">
30
- <div class="col-12">
31
- <div class="card mb-4">
32
- <div class="card-header"><h3 class="m-0">Where it binds</h3></div>
33
- <div class="card-body">
34
- <div class="form-group">
35
- <label class="form-label" for="ctx-name">Name<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="Your label for this menu in the admin list. Visitors never see it.">?</button></label>
36
- <input class="form-input" id="ctx-name" type="text" placeholder="Entry actions">
27
+ <div class="row ctx-workspace">
28
+ <div class="col-8">
29
+ <div class="tabs cmx-tabs" id="ctx-tabs">
30
+ <div class="tab-list">
31
+ <button type="button" class="tab-item active">Binding <span class="cmx-tab-dot" data-bind-hidden="!tabBad.value.binding" title="Something here needs fixing"></span></button>
32
+ <button type="button" class="tab-item">Items <span class="cmx-count" data-bind-text="items.value.length"></span> <span class="cmx-tab-dot" data-bind-hidden="!tabBad.value.items" title="Something here needs fixing"></span></button>
33
+ <button type="button" class="tab-item">Appearance <span class="cmx-tab-dot" data-bind-hidden="!tabBad.value.appearance" title="Something here needs fixing"></span></button>
34
+ <button type="button" class="tab-item">Scope</button>
35
+ </div>
36
+
37
+ <!-- Binding -->
38
+ <div class="tab-panel active">
39
+ <div class="cmx-card">
40
+ <div class="cmx-card-title">Where it binds</div>
41
+ <div class="cmx-grid">
42
+ <div>
43
+ <label class="form-label" for="ctx-name">Name <span class="text-danger">*</span>
44
+ <span data-help="Your label for this menu in the admin list. Visitors never see it."></span></label>
45
+ <input class="form-input" id="ctx-name" type="text" maxlength="80" placeholder="Entry actions" data-model="name.value">
46
+ <small class="cmx-err" data-bind-hidden="!err.value.name" data-bind-text="err.value.name"></small>
37
47
  </div>
38
- <div class="form-group">
39
- <label class="form-label" for="ctx-slug">Slug<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="The filename this menu is saved as. Fixed once created, because the file is the record.">?</button></label>
40
- <input class="form-input" id="ctx-slug" type="text" placeholder="entry-actions">
41
- <small class="text-muted">Lowercase letters, numbers and hyphens. Cannot be changed later.</small>
48
+ <div>
49
+ <label class="form-label" for="ctx-slug">Slug
50
+ <span data-help="The filename this menu is saved as. Made from the name as you type; fixed once created, because the file is the record."></span></label>
51
+ <input class="form-input font-mono" id="ctx-slug" type="text" maxlength="64" placeholder="entry-actions" data-model="slug.value" data-bind-disabled="notNew">
52
+ <small class="cmx-err" data-bind-hidden="!err.value.slug" data-bind-text="err.value.slug"></small>
53
+ <small class="cmx-hint" data-bind-hidden="!notNew">Fixed - it is the file's name.</small>
42
54
  </div>
43
- <div class="form-group">
44
- <label class="form-label" for="ctx-selector">Container selector<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="A CSS selector for the region, e.g. #product-grid, .entry-card or main. Right-clicks anywhere inside it are offered to this menu. body, html, * and :root are refused, because they would swallow every other menu on the page.">?</button></label>
45
- <input class="form-input" id="ctx-selector" type="text" placeholder="#product-grid">
46
- <small class="text-muted">The region this menu covers. Right-clicks inside it are offered to this menu.</small>
55
+ <div class="cmx-wide">
56
+ <label class="form-label" for="ctx-selector">Container selector <span class="text-danger">*</span>
57
+ <span data-help-title="Container selector" data-help="A CSS selector for the region, e.g. #product-grid, .entry-card or main. Right-clicks anywhere inside it are offered to this menu. body, html, * and :root are refused, because they would swallow every other menu on the page."></span></label>
58
+ <input class="form-input font-mono" id="ctx-selector" type="text" placeholder="#product-grid" data-model="selector.value">
59
+ <small class="cmx-err" data-bind-hidden="!err.value.selector" data-bind-text="err.value.selector"></small>
47
60
  </div>
48
- <div class="form-group">
49
- <label class="form-label" for="ctx-match">Match (optional)<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="Narrows it: only claim the click when it landed on something matching this, e.g. [data-entry-id] to answer on rows but not the space between them. Blank means the whole container.">?</button></label>
50
- <input class="form-input" id="ctx-match" type="text" placeholder="[data-entry-id]">
51
- <small class="text-muted">Only claim right-clicks on descendants matching this. Leave empty to cover the whole container.</small>
61
+ <div>
62
+ <label class="form-label" for="ctx-match">Match <small class="cmx-hint-inline">(optional)</small>
63
+ <span data-help="Narrows it: only claim the click when it landed on something matching this, e.g. [data-entry-id] to answer on rows but not the space between them. Blank means the whole container."></span></label>
64
+ <input class="form-input font-mono" id="ctx-match" type="text" placeholder="[data-entry-id]" data-model="match.value">
52
65
  </div>
53
- <div class="form-group">
54
- <label class="form-label" for="ctx-exclude">Exclude (optional)<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="Holes in the region. A right-click on one of these falls through to whatever encloses it, which is useful for links and inputs where the browser's own menu is more use.">?</button></label>
55
- <input class="form-input" id="ctx-exclude" type="text" placeholder="a, button, input">
56
- <small class="text-muted">Regions that decline, so the click falls through to whatever encloses them.</small>
66
+ <div>
67
+ <label class="form-label" for="ctx-exclude">Exclude <small class="cmx-hint-inline">(optional)</small>
68
+ <span data-help="Holes in the region. A right-click on one of these falls through to whatever encloses it - useful for links and inputs, where the browser's own menu is more use."></span></label>
69
+ <input class="form-input font-mono" id="ctx-exclude" type="text" placeholder="a, button, input" data-model="exclude.value">
57
70
  </div>
58
71
  </div>
59
72
  </div>
60
- </div>
61
73
 
62
- <div class="col-12">
63
- <div class="card mb-4">
64
- <div class="card-header"><h3 class="m-0">Behaviour<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="Menus can sit inside one another. A right-click picks the INNERMOST menu covering the spot clicked, never the one that happened to load first.">?</button></h3></div>
65
- <div class="card-body">
66
- <div class="form-group">
67
- <label class="form-label" for="ctx-inherit">When nested inside another menu<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="Append puts the outer menu's items after yours, Prepend before, and Stand alone shows only yours.">?</button></label>
68
- <select class="form-select" id="ctx-inherit">
74
+ <div class="cmx-card">
75
+ <div class="cmx-card-title">Behaviour</div>
76
+ <div class="cmx-grid">
77
+ <div class="cmx-wide">
78
+ <label class="form-label" for="ctx-inherit">When nested inside another menu
79
+ <span data-help="Show my items, then theirs puts the outer menu's items after yours; the reverse puts them before; Show only my items hides the outer menu's."></span></label>
80
+ <select class="form-select" id="ctx-inherit" data-model="inherit.value">
69
81
  <option value="append">Show my items, then theirs (default)</option>
70
82
  <option value="prepend">Show their items, then mine</option>
71
83
  <option value="false">Show only my items</option>
72
84
  </select>
73
85
  </div>
74
- <div class="form-group">
75
- <label class="form-check">
76
- <input type="checkbox" id="ctx-enabled" checked> Enabled
77
- </label>
78
- <small class="text-muted">Turn this off to park a menu without deleting it. A disabled menu is never sent to the browser.</small>
86
+ <div class="cmx-setting cmx-wide">
87
+ <label class="form-label" for="ctx-enabled">Enabled
88
+ <span data-help="Off parks a menu without deleting it. A disabled menu is never sent to the browser."></span></label>
89
+ <input type="checkbox" class="form-switch" id="ctx-enabled" data-model="enabled.value">
79
90
  </div>
80
91
  </div>
81
92
  </div>
82
93
  </div>
83
- </div>
84
- </div>
85
94
 
86
- <!-- Items --------------------------------------------------------- -->
87
- <div class="tab-panel">
88
- <div class="card">
89
- <div class="card-header">
90
- <h3 class="m-0">Items<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="What the menu offers when it opens, top to bottom. An Item does something when clicked, a Divider draws a line, a Heading labels a group. A menu whose items are all dividers and headings is refused on save, because there would be nothing to click.">?</button></h3>
91
- <button class="btn btn-sm btn-secondary" id="ctx-add-item"><span data-icon="plus"></span> Add item</button>
92
- </div>
93
- <div class="card-body">
94
- <div id="ctx-items"></div>
95
- <p class="text-muted mb-0" id="ctx-items-empty">No items yet. Every menu needs at least one.</p>
95
+ <!-- Items -->
96
+ <div class="tab-panel">
97
+ <div class="cmx-card">
98
+ <div class="cmx-card-title">Items
99
+ <span data-help-title="Items" data-help="What the menu offers when it opens, top to bottom. An Item does something when clicked, a Divider draws a line, a Heading labels a group. Click an item to edit it; right-click it (or ⋮, or Shift+F10) to move, duplicate, add beside it or remove it. Alt+↑ / Alt+↓ move the focused item. A menu of nothing but dividers and headings is refused, because there would be nothing to click."></span>
100
+ <span class="cmx-spacer"></span>
101
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="addHeader"><span data-icon="type" data-icon-size="13"></span> Heading</button>
102
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="addDivider"><span data-icon="minus" data-icon-size="13"></span> Divider</button>
103
+ <button type="button" class="btn btn-sm btn-secondary" id="ctx-add-item" data-on-click="addItem"><span data-icon="plus" data-icon-size="13"></span> Add item</button>
104
+ </div>
105
+ <p class="cmx-note" data-bind-hidden="items.value.length">No items yet. Every menu needs at least one.</p>
106
+ <p class="cmx-err" data-bind-hidden="!err.value.items" data-bind-text="err.value.items"></p>
107
+ <div class="cmx-list" id="ctx-items" data-each="rows.value key=key"></div>
108
+ </div>
96
109
  </div>
97
- </div>
98
- </div>
99
110
 
100
- <!-- Appearance ---------------------------------------------------- -->
101
- <div class="tab-panel">
102
- <div class="card">
103
- <div class="card-header"><h3 class="m-0">Appearance<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="How the panel looks when it opens. Leave anything blank to inherit the site theme, which is usually what you want so the menu matches everything around it.">?</button></h3></div>
104
- <div class="card-body">
105
- <div class="row">
106
- <div class="col-6">
107
- <div class="form-group">
108
- <label class="form-label" for="ctx-accent">Accent<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="The colour used for hover and the active item.">?</button></label>
109
- <select class="form-select" id="ctx-accent">
111
+ <!-- Appearance -->
112
+ <div class="tab-panel">
113
+ <div class="cmx-card">
114
+ <div class="cmx-card-title">Appearance
115
+ <span data-help="How the panel looks when it opens. Leave anything on its default to inherit the site theme - usually what you want, so the menu matches everything around it."></span></div>
116
+ <div class="cmx-grid">
117
+ <div>
118
+ <label class="form-label" for="ctx-accent">Accent
119
+ <span data-help="Tints the border and the hover wash. Labels keep the theme's text colour so they stay readable."></span></label>
120
+ <select class="form-select" id="ctx-accent" data-model="accent.value">
110
121
  <option value="">Theme default</option>
111
122
  <option value="primary">Primary</option>
112
123
  <option value="success">Success</option>
@@ -114,29 +125,11 @@
114
125
  <option value="warning">Warning</option>
115
126
  <option value="info">Info</option>
116
127
  </select>
117
- <small class="text-muted">Tints the border and hover wash. Labels keep the theme's text colour so they stay readable.</small>
118
- </div>
119
- <div class="form-group">
120
- <label class="form-label" for="ctx-transition">Transition</label>
121
- <select class="form-select" id="ctx-transition">
122
- <option value="">Default (scale)</option>
123
- <option value="fade">Fade</option>
124
- <option value="slide">Slide</option>
125
- <option value="none">None</option>
126
- </select>
128
+ <small class="cmx-err" data-bind-hidden="!err.value.accent" data-bind-text="err.value.accent"></small>
127
129
  </div>
128
- <div class="form-group">
129
- <label class="form-label" for="ctx-density">Density</label>
130
- <select class="form-select" id="ctx-density">
131
- <option value="">Comfortable</option>
132
- <option value="compact">Compact</option>
133
- </select>
134
- </div>
135
- </div>
136
- <div class="col-6">
137
- <div class="form-group">
130
+ <div>
138
131
  <label class="form-label" for="ctx-radius">Corner radius</label>
139
- <select class="form-select" id="ctx-radius">
132
+ <select class="form-select" id="ctx-radius" data-model="radius.value">
140
133
  <option value="">Default</option>
141
134
  <option value="none">Square</option>
142
135
  <option value="sm">Small</option>
@@ -145,9 +138,18 @@
145
138
  <option value="xl">Extra large</option>
146
139
  </select>
147
140
  </div>
148
- <div class="form-group">
141
+ <div>
142
+ <label class="form-label" for="ctx-transition">Transition</label>
143
+ <select class="form-select" id="ctx-transition" data-model="transition.value">
144
+ <option value="">Default (scale)</option>
145
+ <option value="fade">Fade</option>
146
+ <option value="slide">Slide</option>
147
+ <option value="none">None</option>
148
+ </select>
149
+ </div>
150
+ <div>
149
151
  <label class="form-label" for="ctx-shadow">Shadow</label>
150
- <select class="form-select" id="ctx-shadow">
152
+ <select class="form-select" id="ctx-shadow" data-model="shadow.value">
151
153
  <option value="">Default</option>
152
154
  <option value="none">None</option>
153
155
  <option value="sm">Small</option>
@@ -156,57 +158,115 @@
156
158
  <option value="xl">Extra large</option>
157
159
  </select>
158
160
  </div>
159
- <div class="form-group">
160
- <label class="form-label" for="ctx-opacity">Opacity<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="Below 100 the panel lets the page show through. Anything under about 80 gets hard to read over busy content.">?</button></label>
161
- <input class="form-input" id="ctx-opacity" type="number" min="20" max="100" placeholder="100">
162
- <small class="text-muted">Below 100 gives a translucent panel with a blurred backdrop. Text stays fully opaque.</small>
161
+ <div>
162
+ <label class="form-label" for="ctx-density">Density</label>
163
+ <select class="form-select" id="ctx-density" data-model="density.value">
164
+ <option value="">Comfortable</option>
165
+ <option value="compact">Compact</option>
166
+ </select>
167
+ </div>
168
+ <div>
169
+ <label class="form-label" for="ctx-opacity">Opacity
170
+ <span data-help="Below 100 the panel lets the page show through, with a blurred backdrop; text stays fully opaque. Much under 80 gets hard to read over busy content."></span></label>
171
+ <input class="form-input" id="ctx-opacity" type="number" min="20" max="100" placeholder="100" data-model="opacity.value">
172
+ <small class="cmx-err" data-bind-hidden="!err.value.opacity" data-bind-text="err.value.opacity"></small>
163
173
  </div>
164
174
  </div>
165
175
  </div>
166
176
  </div>
167
- </div>
168
- </div>
169
177
 
170
- <!-- Scope --------------------------------------------------------- -->
171
- <div class="tab-panel">
172
- <div class="card">
173
- <div class="card-header"><h3 class="m-0">Scope<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="Which pages carry this menu. Leave BOTH blank and it applies site-wide. A menu that does not apply to a page is not sent to that page at all.">?</button></h3></div>
174
- <div class="card-body">
175
- <div class="row">
176
- <div class="col-6">
177
- <div class="form-group">
178
- <label class="form-label" for="ctx-pages">Pages<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="Comma separated URL paths. Supports * as a wildcard, so /shop/* covers everything under /shop. Blank means every page.">?</button></label>
179
- <input class="form-input" id="ctx-pages" type="text" placeholder="/shop/*, /about">
180
- <small class="text-muted">Comma separated. Supports a trailing <code>/*</code>. Leave empty for site-wide.</small>
178
+ <!-- Scope -->
179
+ <div class="tab-panel">
180
+ <div class="cmx-card">
181
+ <div class="cmx-card-title">Scope
182
+ <span data-help="Which pages carry this menu. Leave BOTH blank and it applies site-wide. A menu that does not apply to a page is not sent to that page at all."></span></div>
183
+ <div class="cmx-grid">
184
+ <div>
185
+ <label class="form-label" for="ctx-pages">Pages
186
+ <span data-help="Comma separated URL paths. A trailing /* covers everything under it, so /shop/* covers the whole shop. Blank means every page."></span></label>
187
+ <input class="form-input font-mono" id="ctx-pages" type="text" placeholder="/shop/*, /about" data-model="pages.value">
181
188
  </div>
182
- </div>
183
- <div class="col-6">
184
- <div class="form-group">
185
- <label class="form-label" for="ctx-projects">Projects<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="Comma separated project slugs. The menu then applies only to pages belonging to those projects.">?</button></label>
186
- <input class="form-input" id="ctx-projects" type="text" placeholder="core, shop">
187
- <small class="text-muted">Comma separated project slugs.</small>
189
+ <div>
190
+ <label class="form-label" for="ctx-projects">Projects
191
+ <span data-help="Comma separated project slugs. The menu then applies only to pages belonging to those projects."></span></label>
192
+ <input class="form-input font-mono" id="ctx-projects" type="text" placeholder="core, shop" data-model="projects.value">
188
193
  </div>
189
194
  </div>
195
+ <p class="cmx-note" data-bind-text="scopeText.value"></p>
190
196
  </div>
191
197
  </div>
192
198
  </div>
193
- </div>
194
- </div>
195
-
196
- </div><!-- /col-8 -->
199
+ </div><!-- /col-8 -->
197
200
 
198
- <!-- One preview, beside the tabs rather than inside them: four copies would
199
- be four elements sharing #ctx-preview, and the menu binds by selector.
200
- Sticky, so it stays put while a long Items tab scrolls past it. -->
201
- <div class="col-4">
202
- <div class="card ctx-preview-card">
203
- <div class="card-header"><h3 class="m-0">Try it<button type="button" class="ctx-help" aria-label="What is this?" data-tooltip="The real menu, built from what you have entered, updating as you type. This is the component itself rather than a drawing of it.">?</button></h3></div>
204
- <div class="card-body">
205
- <p class="text-muted">Right-click the panel below to see this menu as configured.</p>
206
- <div id="ctx-preview" class="p-4 text-center" style="border:1px dashed var(--dm-border); border-radius:var(--dm-radius-md);">
207
- Right-click to preview
201
+ <!-- One preview, beside the tabs rather than inside them: four copies would be four elements sharing
202
+ #ctx-preview, and the menu binds by selector. Sticky, so it stays put while a long Items tab scrolls. -->
203
+ <div class="col-4">
204
+ <div class="cmx-card ctx-preview-card">
205
+ <div class="cmx-card-title">Try it
206
+ <span data-help="The real menu, built from what you have entered and updating as you type - the component itself, not a drawing of it. Its actions do nothing here."></span></div>
207
+ <div class="cmx-chips" data-each="chips.value key=text"></div>
208
+ <div id="ctx-preview" class="cmx-preview" tabindex="0">
209
+ <span data-bind-text="previewText.value">Right-click to preview</span>
210
+ </div>
208
211
  </div>
209
212
  </div>
210
- </div>
211
- </div>
212
- </div><!-- /ctx-workspace -->
213
+ </div><!-- /ctx-workspace -->
214
+ </div>
215
+
216
+ <style>
217
+ .cmx { display: flex; flex-direction: column; gap: .75rem; }
218
+ .cmx [hidden], .cmx-form [hidden] { display: none !important; }
219
+ .cmx-intro { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
220
+ .cmx-alert { margin: 0; padding: .6rem .8rem; border-radius: var(--dm-radius-md); background: color-mix(in srgb, var(--dm-danger) 12%, transparent); color: var(--dm-text); border: 1px solid color-mix(in srgb, var(--dm-danger) 35%, transparent); }
221
+ .cmx-alert small, .cmx-server-errors div { display: block; font-size: var(--dm-text-sm); }
222
+ .cmx-unsaved { position: sticky; top: 0; z-index: 5; display: flex; align-items: center; gap: .5rem; padding: .45rem .75rem; border-radius: var(--dm-radius-md);
223
+ background: color-mix(in srgb, var(--dm-warning) 14%, var(--dm-surface)); border: 1px solid color-mix(in srgb, var(--dm-warning) 40%, transparent); color: var(--dm-text); font-size: var(--dm-text-sm); }
224
+ .cmx-unsaved .btn { display: inline-flex; align-items: center; gap: .3rem; }
225
+ .cmx-dot { width: .55rem; height: .55rem; border-radius: 50%; background: var(--dm-warning); flex-shrink: 0; }
226
+ .cmx-spacer { flex: 1; }
227
+ .cmx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .7rem; margin-top: .75rem; background: var(--dm-surface); }
228
+ .cmx-card-title { display: flex; align-items: center; gap: .35rem; font-weight: 600; }
229
+ .cmx-card-title .btn { display: inline-flex; align-items: center; gap: .3rem; }
230
+ .cmx-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; }
231
+ .cmx-wide { grid-column: 1 / -1; }
232
+ .cmx .form-label, .cmx-form .form-label { display: flex; align-items: center; gap: .3rem; }
233
+ .cmx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
234
+ p.cmx-err { margin: 0; }
235
+ .cmx-hint, .cmx-hint-inline { color: var(--dm-text-muted); font-size: var(--dm-text-xs); font-weight: 400; }
236
+ .cmx-hint { display: block; margin-top: .25rem; }
237
+ .cmx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
238
+ .cmx-setting { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
239
+ .cmx-setting .form-label { margin: 0; }
240
+ .cmx-count { opacity: .65; font-size: var(--dm-text-xs); margin-left: .15rem; }
241
+ .cmx-tab-dot { display: inline-block; width: .45rem; height: .45rem; border-radius: 50%; background: var(--dm-danger); margin-left: .25rem; vertical-align: middle; }
242
+ .cmx-list { display: flex; flex-direction: column; }
243
+ .cmx-row { display: flex; align-items: center; gap: .6rem; padding: .5rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
244
+ .cmx-row:last-child { border-bottom: 0; }
245
+ .cmx-row:hover, .cmx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
246
+ .cmx-row.is-divider .cmx-main strong { color: var(--dm-text-muted); letter-spacing: .1em; font-weight: 400; }
247
+ .cmx-row.is-header .cmx-main strong { text-transform: uppercase; font-size: var(--dm-text-xs); letter-spacing: .05em; color: var(--dm-text-muted); }
248
+ .cmx-row.is-bad { box-shadow: inset 3px 0 0 var(--dm-danger); }
249
+ .cmx-icon { width: 1.4rem; display: inline-flex; justify-content: center; color: var(--dm-text-muted); flex-shrink: 0; }
250
+ .cmx-type { font-size: var(--dm-text-xs); color: var(--dm-text-muted); min-width: 4rem; flex-shrink: 0; }
251
+ .cmx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
252
+ .cmx-main strong, .cmx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
253
+ .cmx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
254
+ .cmx-main small.cmx-bad { color: var(--dm-danger); }
255
+ .cmx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
256
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
257
+ .cmx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
258
+ .cmx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
259
+ .cmx-chip.is-danger { background: color-mix(in srgb, var(--dm-danger) 14%, transparent); color: var(--dm-danger); }
260
+ .cmx-chips { display: flex; flex-wrap: wrap; gap: .3rem; }
261
+ .cmx-move { display: inline-flex; gap: .1rem; flex-shrink: 0; }
262
+ .cmx-move button, .cmx-kebab { border: 0; background: none; cursor: pointer; color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .35rem; font-size: var(--dm-text-base); line-height: 1.6; }
263
+ .cmx-move button:hover:not(:disabled), .cmx-kebab:hover { color: var(--dm-primary); }
264
+ .cmx-move button:disabled { opacity: .3; cursor: default; }
265
+ .cmx-kebab { font-size: var(--dm-text-lg); }
266
+ .cmx-preview { padding: 2rem 1rem; text-align: center; border: 1px dashed var(--dm-border); border-radius: var(--dm-radius-md); color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
267
+ /* The item panel (slideover) */
268
+ .cmx-form { display: flex; flex-direction: column; gap: .85rem; }
269
+ .cmx-form .cmx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
270
+ .cmx-form .cmx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
271
+ @media (max-width: 640px) { .cmx-grid { grid-template-columns: 1fr; } .cmx-type { display: none; } }
272
+ </style>