domma-cms 0.75.0 → 0.76.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 (66) hide show
  1. package/admin/js/app.js +4 -4
  2. package/admin/js/lib/api-tokens-arrange.js +3 -0
  3. package/admin/js/lib/effects-arrange.js +3 -0
  4. package/admin/js/lib/layouts-arrange.js +1 -0
  5. package/admin/js/lib/my-profile-arrange.js +1 -0
  6. package/admin/js/lib/notifications-arrange.js +1 -0
  7. package/admin/js/lib/plugins-arrange.js +2 -0
  8. package/admin/js/lib/roles-arrange.js +1 -0
  9. package/admin/js/lib/search-settings-arrange.js +1 -0
  10. package/admin/js/lib/sidebar-renderer.js +2 -2
  11. package/admin/js/lib/site-settings-arrange.js +1 -0
  12. package/admin/js/lib/users-arrange.js +1 -0
  13. package/admin/js/templates/api-tokens.html +83 -10
  14. package/admin/js/templates/effects.html +155 -72
  15. package/admin/js/templates/layouts.html +126 -43
  16. package/admin/js/templates/my-profile.html +100 -12
  17. package/admin/js/templates/notifications.html +70 -10
  18. package/admin/js/templates/plugins.html +100 -28
  19. package/admin/js/templates/roles.html +78 -7
  20. package/admin/js/templates/search.html +193 -216
  21. package/admin/js/templates/settings.html +404 -472
  22. package/admin/js/templates/users.html +69 -9
  23. package/admin/js/views/api-tokens.js +62 -8
  24. package/admin/js/views/effects.js +31 -1
  25. package/admin/js/views/layouts.js +81 -1
  26. package/admin/js/views/my-profile.js +1 -1
  27. package/admin/js/views/notifications.js +18 -1
  28. package/admin/js/views/plugins.js +97 -39
  29. package/admin/js/views/roles.js +49 -4
  30. package/admin/js/views/search.js +7 -1
  31. package/admin/js/views/settings.js +13 -3
  32. package/admin/js/views/users.js +13 -4
  33. package/package.json +1 -1
  34. package/plugins/_lib/admin/mail/compose-window.js +97 -40
  35. package/plugins/_lib/admin/mail/contacts.js +4 -1
  36. package/plugins/_lib/admin/mail/diagnostics-section.js +4 -1
  37. package/plugins/_lib/admin/mail/folder-tree.js +1 -1
  38. package/plugins/_lib/admin/mail/image-senders-section.js +69 -90
  39. package/plugins/_lib/admin/mail/mail.css +1 -1
  40. package/plugins/_lib/admin/mail/mail.html +1 -1
  41. package/plugins/_lib/admin/mail/reader-view.js +303 -44
  42. package/plugins/_lib/admin/mail/when-closed.js +26 -0
  43. package/plugins/_lib/mail/readRoutes.js +43 -1
  44. package/plugins/_lib/mail/unreadBadge.js +42 -0
  45. package/plugins/free-tier.lock.json +5 -5
  46. package/plugins/mail-reader/CLAUDE.md +1 -0
  47. package/plugins/mail-reader/plugin.js +3 -1
  48. package/plugins/mail-reader/plugin.json +5 -4
  49. package/server/routes/api/api-tokens.js +56 -0
  50. package/server/routes/api/auth.js +7 -1
  51. package/server/routes/api/collections.js +13 -1
  52. package/server/routes/api/effects.js +68 -13
  53. package/server/routes/api/layouts.js +93 -2
  54. package/server/routes/api/notifications.js +140 -14
  55. package/server/routes/api/plugins.js +135 -4
  56. package/server/routes/api/roles.js +111 -0
  57. package/server/routes/api/search.js +38 -4
  58. package/server/routes/api/settings.js +52 -0
  59. package/server/routes/api/users.js +53 -1
  60. package/server/server.js +12 -0
  61. package/server/services/plugins.js +7 -0
  62. package/server/services/roles.js +3 -0
  63. package/server/services/search.js +58 -0
  64. package/server/services/users.js +14 -0
  65. package/admin/js/templates/plugin-marketplace.html +0 -29
  66. package/admin/js/views/plugin-marketplace.js +0 -1
@@ -1,81 +1,104 @@
1
- <div class="view-header">
2
- <h1><span data-icon="sparkles"></span> Domma Effects</h1>
3
- <div>
4
- <button id="save-settings-btn" class="btn btn-primary">
5
- <span data-icon="save"></span> Save
6
- </button>
1
+ <!-- System › Effects. Bound with M.applyBindings - see admin/js/views/effects.js, which also inserts the
2
+ row template (the router renders a templateUrl through _.render, eating any {{…}}). No page heading:
3
+ the Tool banner above is the heading, and Save is a banner action. -->
4
+ <div class="fxx">
5
+ <div class="fxx-card fxx-live">
6
+ <div class="fxx-card-title">Live now
7
+ <span data-help-title="Live now" data-help="What the published site uses at the moment - the saved settings, not what is typed below until you Save. Every [reveal] and row reveal without its own duration, animation or threshold uses these."></span></div>
8
+ <div class="fxx-chips">
9
+ <span class="fxx-chip" data-bind-class="saved.value.respectMotion ? 'is-ok' : 'is-warn'" data-bind-text="motionText.value"></span>
10
+ <span class="fxx-chip" data-bind-text="'Reveal: ' + saved.value.defaultAnimation"></span>
11
+ <span class="fxx-chip" data-bind-text="saved.value.defaultDuration + ' ms'"></span>
12
+ <span class="fxx-chip" data-bind-text="'Threshold ' + saved.value.defaultThreshold"></span>
13
+ <span class="fxx-chip is-muted" data-bind-hidden="!defaultsText.value" data-bind-text="defaultsText.value"></span>
14
+ </div>
7
15
  </div>
8
- </div>
9
16
 
10
- <!-- Top-level tabs -->
11
- <div id="effects-tabs" class="tabs">
12
- <div class="tab-list">
13
- <button class="tab-item active">Settings</button>
14
- <button class="tab-item">Shortcode Reference</button>
15
- </div>
16
- <div class="tab-content">
17
+ <p class="fxx-unsaved" data-bind-hidden="!dirty.value">
18
+ <span data-icon="alert-circle" data-icon-size="15"></span> Unsaved changes
19
+ <span class="fxx-spacer"></span>
20
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="discard">Discard</button>
21
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannotSave.value">Save</button>
22
+ </p>
23
+
24
+ <div class="tabs fxx-tabs">
25
+ <div class="tab-list">
26
+ <button type="button" class="tab-item active">Settings <span class="fxx-dot" data-bind-hidden="!dirty.value" title="Unsaved changes">•</span></button>
27
+ <button type="button" class="tab-item">Effects <span class="fxx-count" data-bind-text="effectCount"></span></button>
28
+ <button type="button" class="tab-item">Guide</button>
29
+ </div>
17
30
 
18
- <!-- Settings tab panel -->
31
+ <!-- Settings -->
19
32
  <div class="tab-panel active">
20
- <div class="card mb-4">
21
- <div class="card-body">
22
- <div class="row mb-3">
23
- <div class="col">
24
- <label class="form-check-label">
25
- <input id="field-respect-motion" type="checkbox">
26
- Respect <code>prefers-reduced-motion</code>
27
- </label>
28
- <span class="form-hint">When enabled, JS effects are skipped for users who prefer reduced motion. Content remains visible.</span>
29
- </div>
33
+ <div class="fxx-card">
34
+ <div class="fxx-card-title">Scroll reveal defaults
35
+ <span data-help-title="Defaults" data-help="Used by [reveal] and [row reveal] when the shortcode does not set its own. A shortcode's own attribute always wins. Ctrl+S saves."></span></div>
36
+ <div class="fxx-grid">
37
+ <div class="fxx-wide fxx-setting">
38
+ <label class="form-label" for="fx-motion">Respect reduced motion
39
+ <span data-help-title="prefers-reduced-motion" data-help="Visitors can ask their device for less motion. With this on, the JavaScript effects are skipped for them and the content simply shows - nothing is hidden. Turn it off only if an effect carries meaning that would otherwise be lost."></span></label>
40
+ <input id="fx-motion" type="checkbox" class="form-switch" data-model="respectMotion.value">
30
41
  </div>
31
- <div class="row mb-3">
32
- <div class="col-6">
33
- <label class="form-label">Default animation duration (ms)</label>
34
- <input id="field-default-duration" type="number" class="form-input" min="0" max="10000"
35
- placeholder="600">
36
- <span class="form-hint">Used by reveal when no <code>duration</code> attribute is specified.</span>
37
- </div>
38
- <div class="col-6">
39
- <label class="form-label">Default reveal animation</label>
40
- <select id="field-default-animation" class="form-select">
41
- <option value="fade">Fade</option>
42
- <option value="slide-up">Slide up</option>
43
- <option value="slide-down">Slide down</option>
44
- <option value="zoom">Zoom</option>
45
- <option value="flip">Flip</option>
46
- </select>
47
- </div>
42
+ <div>
43
+ <label class="form-label" for="fx-anim">Reveal animation</label>
44
+ <select id="fx-anim" class="form-input" data-model="defaultAnimation.value">
45
+ <option value="fade">Fade</option>
46
+ <option value="slide-up">Slide up</option>
47
+ <option value="slide-down">Slide down</option>
48
+ <option value="zoom">Zoom</option>
49
+ <option value="flip">Flip</option>
50
+ </select>
51
+ <small class="fxx-err" data-bind-hidden="!animErr.value" data-bind-text="animErr.value"></small>
48
52
  </div>
49
- <div class="row">
50
- <div class="col-6">
51
- <label class="form-label">Default scroll threshold</label>
52
- <input id="field-default-threshold" type="number" class="form-input" min="0" max="1"
53
- step="0.05"
54
- placeholder="0.1">
55
- <span class="form-hint">Fraction of element visible before reveal fires (0.0-1.0).</span>
56
- </div>
53
+ <div>
54
+ <label class="form-label" for="fx-duration">Duration (ms)
55
+ <span data-help="How long a reveal takes. 600 ms is a comfortable default; above about 1000 it starts to feel slow."></span></label>
56
+ <input id="fx-duration" type="number" class="form-input" min="1" max="10000" step="50" placeholder="600" data-model="defaultDuration.value">
57
+ <small class="fxx-err" data-bind-hidden="!durErr.value" data-bind-text="durErr.value"></small>
57
58
  </div>
59
+ <div>
60
+ <label class="form-label" for="fx-threshold">Threshold
61
+ <span data-help-title="Threshold" data-help="How much of an element must be on screen before it reveals: 0 = as soon as any of it shows, 1 = only once all of it shows. 0.1 suits most pages; tall blocks need a low value or they may never reveal on a small screen."></span></label>
62
+ <input id="fx-threshold" type="number" class="form-input" min="0" max="1" step="0.05" placeholder="0.1" data-model="defaultThreshold.value">
63
+ <small class="fxx-err" data-bind-hidden="!thrErr.value" data-bind-text="thrErr.value"></small>
64
+ </div>
65
+ </div>
66
+ <div class="fxx-actions">
67
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="resetDefaults" title="Put back Domma's defaults (then Save)">Domma's defaults</button>
68
+ <span class="fxx-spacer"></span>
69
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannotSave.value">
70
+ <span data-icon="save" data-icon-size="14"></span> Save</button>
58
71
  </div>
59
72
  </div>
60
73
  </div>
61
74
 
62
- <!-- Shortcode Reference tab panel -->
75
+ <!-- Effects: the catalogue the page editor inserts from -->
63
76
  <div class="tab-panel">
64
- <p class="text-muted mb-3">Use these shortcodes in any page's Markdown content. The <strong>Effects</strong>
65
- toolbar button in the editor inserts them automatically.</p>
66
-
67
- <!-- Sub-tabs -->
68
- <div class="mb-3" style="display:flex;gap:6px;flex-wrap:wrap;">
69
- <button class="btn btn-sm effects-tab-btn active" data-fx-tab="entrance">Entrance</button>
70
- <button class="btn btn-sm effects-tab-btn" data-fx-tab="animation">Animation</button>
71
- <button class="btn btn-sm effects-tab-btn" data-fx-tab="text">Text</button>
72
- <button class="btn btn-sm effects-tab-btn" data-fx-tab="visual">Visual</button>
73
- <button class="btn btn-sm effects-tab-btn" data-fx-tab="examples">Examples</button>
74
- <button class="btn btn-sm effects-tab-btn" data-fx-tab="celebrations">Celebrations</button>
77
+ <div class="fxx-card">
78
+ <div class="fxx-bar">
79
+ <div class="fxx-seg fxx-cats" role="group" aria-label="Category" data-each="cats.value key=key"></div>
80
+ <input type="search" class="form-input fxx-search" placeholder="Search effects or settings…" aria-label="Search effects" data-model="query.value">
81
+ <span data-help-title="Effects" data-help="Every effect the page editor's Effects button can insert. Click one for its settings and an example; right-click it (or ⋮, or Shift+F10) to copy its shortcode. An effect that works inside [card], [hero] and friends can also be switched on as an attribute of that shortcode."></span>
82
+ </div>
83
+ <p class="fxx-note" data-bind-hidden="rows.value.length">No effects match.</p>
84
+ <div class="fxx-list" data-each="rows.value key=name"></div>
85
+ </div>
75
86
  </div>
76
87
 
77
- <!-- Entrance tab -->
78
- <div id="tab-entrance" class="effects-tab-panel">
88
+ <!-- Guide: the long-form reference -->
89
+ <div class="tab-panel">
90
+ <div class="fxx-card">
91
+ <p class="fxx-note">Use these shortcodes in any page's Markdown. The <strong>Effects</strong> toolbar button in
92
+ the editor inserts them for you.</p>
93
+ <div class="fxx-seg fxx-guide-tabs" role="group" aria-label="Guide section">
94
+ <button type="button" data-on-click="showGuide('entrance')" data-bind-class="guide.value === 'entrance' && 'is-on'">Entrance</button>
95
+ <button type="button" data-on-click="showGuide('animation')" data-bind-class="guide.value === 'animation' && 'is-on'">Animation</button>
96
+ <button type="button" data-on-click="showGuide('text')" data-bind-class="guide.value === 'text' && 'is-on'">Text</button>
97
+ <button type="button" data-on-click="showGuide('visual')" data-bind-class="guide.value === 'visual' && 'is-on'">Visual</button>
98
+ <button type="button" data-on-click="showGuide('examples')" data-bind-class="guide.value === 'examples' && 'is-on'">Examples</button>
99
+ <button type="button" data-on-click="showGuide('celebrations')" data-bind-class="guide.value === 'celebrations' && 'is-on'">Celebrations</button>
100
+ </div>
101
+ <div id="tab-entrance" class="fxx-guide-panel" data-bind-hidden="guide.value !== 'entrance'">
79
102
  <h3 class="mb-2">Reveal</h3>
80
103
  <p class="text-muted mb-2">Triggers a scroll-activated entrance animation when the element enters the
81
104
  viewport.
@@ -198,7 +221,7 @@ Markdown **works** inside.
198
221
  </div>
199
222
 
200
223
  <!-- Animation tab -->
201
- <div id="tab-animation" class="effects-tab-panel">
224
+ <div id="tab-animation" class="fxx-guide-panel" data-bind-hidden="guide.value !== 'animation'">
202
225
  <h3 class="mb-2">Breathe</h3>
203
226
  <p class="text-muted mb-2">Continuous gentle scaling - makes elements feel alive. Ideal for hero images or
204
227
  call-to-action buttons.</p>
@@ -256,7 +279,7 @@ Content here.
256
279
  </div>
257
280
 
258
281
  <!-- Text tab -->
259
- <div id="tab-text" class="effects-tab-panel">
282
+ <div id="tab-text" class="fxx-guide-panel" data-bind-hidden="guide.value !== 'text'">
260
283
  <h3 class="mb-2">Typewriter (Scribe)</h3>
261
284
  <p class="text-muted mb-2">Types text character by character. Supports a simple one-shot mode or a full
262
285
  action script for sequenced multi-step animations.</p>
@@ -418,7 +441,7 @@ Scrambled reveal text.
418
441
  </div>
419
442
 
420
443
  <!-- Visual tab -->
421
- <div id="tab-visual" class="effects-tab-panel">
444
+ <div id="tab-visual" class="fxx-guide-panel" data-bind-hidden="guide.value !== 'visual'">
422
445
  <h3 class="mb-2">Ripple</h3>
423
446
  <p class="text-muted mb-2">Click-triggered ripple effect on the element. Good for interactive content
424
447
  blocks.</p>
@@ -466,7 +489,7 @@ Content on animated background.
466
489
  </div>
467
490
 
468
491
  <!-- Examples tab -->
469
- <div id="tab-examples" class="effects-tab-panel">
492
+ <div id="tab-examples" class="fxx-guide-panel" data-bind-hidden="guide.value !== 'examples'">
470
493
  <h3 class="mb-2">Hero section with typewriter</h3>
471
494
  <pre class="code-block mb-3">[reveal animation="fade"]
472
495
  # Welcome to our site
@@ -530,7 +553,7 @@ A compelling subheading that draws users in.
530
553
  </div>
531
554
 
532
555
  <!-- Celebrations tab -->
533
- <div id="tab-celebrations" class="effects-tab-panel">
556
+ <div id="tab-celebrations" class="fxx-guide-panel" data-bind-hidden="guide.value !== 'celebrations'">
534
557
  <h3 class="mb-2">Firework <span class="badge badge-info">CSS only</span></h3>
535
558
  <p class="text-muted mb-2">Pure CSS particle effect - no JavaScript required. Works even without the plugin
536
559
  enabled. Use self-closing for standalone particles, or wrapping to apply the firework classes to
@@ -747,6 +770,66 @@ The strips rain down inside this block only.
747
770
  full-page overlay (<code>position:fixed</code>); the wrapping form attaches the canvas to the
748
771
  wrapping div so the parade is constrained to that block.</p>
749
772
  </div>
750
- </div><!-- /tab-panel shortcodes -->
751
- </div><!-- /tab-content -->
752
- </div><!-- /effects-tabs -->
773
+ </div>
774
+ </div>
775
+ </div>
776
+ </div>
777
+
778
+ <style>
779
+ .fxx { display: flex; flex-direction: column; gap: .75rem; }
780
+ .fxx [hidden], .fxx-form [hidden] { display: none !important; }
781
+ .fxx-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; }
782
+ .fxx-live { margin-top: 0; }
783
+ .fxx-card-title { display: flex; align-items: center; gap: .35rem; font-weight: 600; }
784
+ .fxx-chips { display: flex; flex-wrap: wrap; gap: .35rem; }
785
+ .fxx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .55rem; border-radius: 999px; white-space: nowrap;
786
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
787
+ .fxx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
788
+ .fxx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
789
+ .fxx-chip.is-info { background: color-mix(in srgb, var(--dm-primary) 14%, transparent); color: var(--dm-primary); }
790
+ .fxx-chip.is-muted { background: none; font-weight: 400; }
791
+ .fxx-unsaved { margin: 0; display: flex; align-items: center; gap: .5rem; padding: .5rem .8rem; border-radius: var(--dm-radius-md);
792
+ background: color-mix(in srgb, var(--dm-warning) 16%, transparent); color: var(--dm-text); }
793
+ .fxx-dot { color: var(--dm-warning); margin-left: .2rem; }
794
+ .fxx-count { opacity: .65; font-size: var(--dm-text-xs); margin-left: .2rem; }
795
+ .fxx-spacer { flex: 1; }
796
+ .fxx-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .85rem; }
797
+ .fxx-wide { grid-column: 1 / -1; }
798
+ .fxx-setting { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
799
+ .fxx-setting .form-label { margin: 0; }
800
+ .fxx .form-label { display: flex; align-items: center; gap: .3rem; }
801
+ .fxx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
802
+ .fxx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
803
+ .fxx-actions .btn, .fxx-unsaved .btn { display: inline-flex; align-items: center; gap: .3rem; }
804
+ .fxx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
805
+ .fxx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
806
+ .fxx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
807
+ .fxx-seg { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
808
+ .fxx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .65rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
809
+ .fxx-seg button + button { border-left: 1px solid var(--dm-border); }
810
+ .fxx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
811
+ .fxx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
812
+ .fxx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
813
+ .fxx-list { display: flex; flex-direction: column; }
814
+ .fxx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
815
+ .fxx-row:last-child { border-bottom: 0; }
816
+ .fxx-row:hover, .fxx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
817
+ .fxx-tag { flex-shrink: 0; min-width: 7.5rem; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm); color: var(--dm-primary); }
818
+ .fxx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
819
+ .fxx-main strong, .fxx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
820
+ .fxx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
821
+ .fxx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
822
+ .fxx-kebab:hover { color: var(--dm-primary); }
823
+ .fxx-guide-tabs { align-self: flex-start; }
824
+ /* The details slideover */
825
+ .fxx-form { display: flex; flex-direction: column; gap: .85rem; }
826
+ .fxx-form pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm);
827
+ padding: .6rem .75rem; border-radius: var(--dm-radius-md); border: 1px solid var(--dm-border); background: var(--dm-surface-raised, color-mix(in srgb, var(--dm-text) 5%, transparent)); color: var(--dm-text); }
828
+ .fxx-sub { display: flex; align-items: center; gap: .4rem; font-weight: 600; }
829
+ .fxx-attrs { width: 100%; border-collapse: collapse; font-size: var(--dm-text-sm); }
830
+ .fxx-attrs th, .fxx-attrs td { text-align: left; padding: .35rem .4rem; border-bottom: 1px solid var(--dm-border); vertical-align: top; }
831
+ .fxx-attrs th { color: var(--dm-text-muted); font-weight: 500; font-size: var(--dm-text-xs); }
832
+ .fxx-attrs code { color: var(--dm-primary); }
833
+ .fxx-attrs small { display: block; color: var(--dm-text-muted); }
834
+ @media (max-width: 720px) { .fxx-grid { grid-template-columns: 1fr; } .fxx-tag { min-width: 0; } }
835
+ </style>
@@ -1,49 +1,132 @@
1
- <div class="view-header">
2
- <h1><span data-icon="layout"></span> Layouts</h1>
3
- </div>
1
+ <!-- Layouts. Bound with M.applyBindings - see admin/views/layouts.js, which also inserts the row template
2
+ (the router renders a templateUrl through _.render, eating any {{…}}). No page heading: the Tool
3
+ banner above is the heading, and New layout is a banner action. -->
4
+ <div class="lyx">
5
+ <div class="tabs lyx-tabs">
6
+ <div class="tab-list">
7
+ <button type="button" class="tab-item active">Layouts <span class="lyx-count" data-bind-text="counts.value.all"></span></button>
8
+ <button type="button" class="tab-item">Options <span class="lyx-dot" data-bind-hidden="!optDirty.value" title="Unsaved changes">●</span></button>
9
+ </div>
4
10
 
5
- <div class="tabs" id="layouts-tabs">
6
- <div class="tab-list">
7
- <button class="tab-item active" data-tab="panel-presets">Layout Presets</button>
8
- <button class="tab-item" data-tab="panel-options">Layout Options</button>
9
- </div>
10
- <div class="tab-content">
11
+ <!-- Layouts -->
12
+ <div class="tab-panel active">
13
+ <p class="lyx-note">A layout sets a page's width, background, and whether it has the navbar, footer and sidebar.
14
+ Pages choose one in the page editor; a page that names none uses Default.
15
+ <span data-help-title="Layouts" data-help="The built-in layouts can be edited but not deleted. A layout you make gets a key from its name (Dark Landing becomes dark-landing) - that key is what a page's layout: line names, and it does not change if you rename the layout later. Deleting a layout that pages use leaves those pages on Default."></span></p>
11
16
 
12
- <div class="tab-panel active" id="panel-presets">
13
- <div class="view-actions mb-4"
14
- style="display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;">
15
- <p class="text-muted" style="margin:0;">Layout presets define the structure, width, and background of pages.
16
- Assign a layout to each page via the page editor.</p>
17
- <button id="create-layout-btn" class="btn btn-primary"><span data-icon="plus"></span> New Layout</button>
18
- </div>
19
- <div id="presets-grid" class="presets-grid"></div>
20
- </div>
17
+ <div class="lyx-alert" data-bind-hidden="!missing.value.length">
18
+ <span data-icon="alert-triangle" data-icon-size="16"></span>
19
+ <span data-bind-text="missingText.value"></span>
20
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="showMissing">Show them</button>
21
+ </div>
21
22
 
22
- <div class="tab-panel" id="panel-options">
23
- <div class="card mb-4">
24
- <div class="card-header"><h3>Spacer Shortcode</h3></div>
25
- <div class="card-body">
26
- <p class="text-muted mb-3">
27
- The <code>[spacer /]</code> shortcode inserts a vertical gap. Set the default height here;
28
- individual spacers can override it with <code>size="N"</code>.
29
- </p>
30
- <div class="form-group">
31
- <label class="form-label" for="spacer-size-input">Default spacer height (px)</label>
32
- <input id="spacer-size-input" type="number" class="form-input" style="max-width:120px" min="0" max="500"
33
- step="1" value="8">
34
- </div>
35
- <div class="form-group mt-3">
36
- <label class="form-label" for="spacer-class-input">Default spacer CSS class</label>
37
- <input id="spacer-class-input" type="text" class="form-input" style="max-width:240px"
38
- placeholder="e.g. my-gap">
39
- <small class="text-muted">Applied to every <code>[spacer /]</code> unless overridden per-instance with
40
- <code>class="…"</code>.</small>
41
- </div>
42
- <button id="save-options-btn" class="btn btn-primary mt-3"><span data-icon="save"></span> Save Options
43
- </button>
23
+ <div class="lyx-card">
24
+ <div class="lyx-bar">
25
+ <div class="lyx-seg" role="group" aria-label="Show">
26
+ <button type="button" data-on-click="showAll" data-bind-class="filter.value === 'all' && 'is-on'">All <b data-bind-text="counts.value.all"></b></button>
27
+ <button type="button" data-on-click="showBuiltin" data-bind-class="filter.value === 'builtin' && 'is-on'">Built in <b data-bind-text="counts.value.builtin"></b></button>
28
+ <button type="button" data-on-click="showCustom" data-bind-class="filter.value === 'custom' && 'is-on'">Yours <b data-bind-text="counts.value.custom"></b></button>
29
+ </div>
30
+ <input type="search" class="form-input lyx-search" placeholder="Search layouts…" aria-label="Search layouts" data-model="query.value">
31
+ <span data-help-title="The list" data-help="Click a layout to edit it. Right-click it (or ⋮, or Shift+F10) to see the pages that use it, duplicate it, copy the line a page's frontmatter needs, or delete it. The little drawing shows its shape: the navbar and footer bars, how wide the content is, and its background colour."></span>
32
+ </div>
33
+ <p class="lyx-note" data-bind-hidden="!loading.value">Loading…</p>
34
+ <p class="lyx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
35
+ <p class="lyx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length">No layouts match.</p>
36
+ <div class="lyx-list" data-each="rows.value key=key"></div>
37
+ </div>
44
38
  </div>
45
- </div>
46
- </div>
47
39
 
48
- </div>
40
+ <!-- Options -->
41
+ <div class="tab-panel">
42
+ <div class="lyx-card">
43
+ <div class="lyx-card-title">Spacer shortcode
44
+ <span data-help-title="[spacer /]" data-help="The [spacer /] shortcode puts a vertical gap in a page. These are its defaults; a single spacer can still say size=&quot;40&quot; or class=&quot;…&quot; for itself."></span></div>
45
+ <div class="lyx-grid">
46
+ <div>
47
+ <label class="form-label" for="lyx-o-size">Default height (px)</label>
48
+ <input id="lyx-o-size" type="number" min="0" max="500" step="1" class="form-input" data-model="spacerSize.value">
49
+ <small class="lyx-err" data-bind-hidden="!optErrSize.value" data-bind-text="optErrSize.value"></small>
50
+ </div>
51
+ <div>
52
+ <label class="form-label" for="lyx-o-class">Default CSS class</label>
53
+ <input id="lyx-o-class" type="text" class="form-input" placeholder="e.g. my-gap" data-model="spacerClass.value">
54
+ <small class="lyx-err" data-bind-hidden="!optErrClass.value" data-bind-text="optErrClass.value"></small>
55
+ </div>
56
+ </div>
57
+ <div class="lyx-actions">
58
+ <small class="lyx-hint" data-bind-text="optStatus.value"></small>
59
+ <span class="lyx-spacer"></span>
60
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="resetOptions" data-bind-hidden="!optDirty.value">Discard</button>
61
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="saveOptions" data-bind-disabled="optCannot.value">
62
+ <span data-icon="save" data-icon-size="13"></span> Save options</button>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </div>
49
67
  </div>
68
+
69
+ <style>
70
+ .lyx { display: flex; flex-direction: column; gap: .75rem; }
71
+ .lyx [hidden], .lyx-form [hidden] { display: none !important; }
72
+ .lyx-note { margin: 0 0 .6rem; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
73
+ .lyx-note.is-bad { color: var(--dm-danger); }
74
+ .lyx-count { opacity: .65; font-size: var(--dm-text-xs); margin-left: .2rem; }
75
+ .lyx-dot { color: var(--dm-warning); font-size: var(--dm-text-xs); margin-left: .25rem; }
76
+ .lyx-alert { display: flex; gap: .5rem; align-items: center; margin: 0 0 .6rem; padding: .5rem .75rem; border-radius: var(--dm-radius-md);
77
+ background: color-mix(in srgb, var(--dm-warning) 16%, transparent); color: var(--dm-text); font-size: var(--dm-text-sm); }
78
+ .lyx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
79
+ .lyx-card-title { display: flex; align-items: center; gap: .3rem; font-weight: 600; }
80
+ .lyx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
81
+ .lyx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
82
+ .lyx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
83
+ .lyx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
84
+ .lyx-seg button + button { border-left: 1px solid var(--dm-border); }
85
+ .lyx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
86
+ .lyx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
87
+ .lyx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
88
+ .lyx-list { display: flex; flex-direction: column; }
89
+ .lyx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
90
+ .lyx-row:last-child { border-bottom: 0; }
91
+ .lyx-row:hover, .lyx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
92
+ /* The little drawing of a layout: navbar, content column, footer, background. */
93
+ .lyx-shape { flex-shrink: 0; width: 3rem; height: 2.2rem; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-sm);
94
+ background: var(--dm-surface); display: flex; flex-direction: column; overflow: hidden; }
95
+ .lyx-shape-nav, .lyx-shape-foot { height: 5px; background: color-mix(in srgb, var(--dm-text-muted) 55%, transparent); }
96
+ .lyx-shape-body { flex: 1; display: flex; justify-content: center; padding: 3px 0; gap: 2px; }
97
+ .lyx-shape-col { background: color-mix(in srgb, var(--dm-primary) 45%, transparent); border-radius: 1px; }
98
+ .lyx-shape-side { width: 18%; background: color-mix(in srgb, var(--dm-text-muted) 35%, transparent); border-radius: 1px; }
99
+ .lyx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
100
+ .lyx-main strong, .lyx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
101
+ .lyx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
102
+ .lyx-chips { display: inline-flex; gap: .3rem; flex-shrink: 0; }
103
+ .lyx-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;
104
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
105
+ .lyx-chip.is-info { background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 14%, transparent); color: var(--dm-info, var(--dm-primary)); }
106
+ .lyx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
107
+ .lyx-chip.is-muted { opacity: .7; }
108
+ .lyx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
109
+ .lyx-kebab:hover { color: var(--dm-primary); }
110
+ .lyx-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .85rem; }
111
+ .lyx-wide { grid-column: 1 / -1; }
112
+ .lyx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
113
+ .lyx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
114
+ .lyx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
115
+ .lyx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
116
+ .lyx-spacer { flex: 1; }
117
+ /* The editor slideover */
118
+ .lyx-form { display: flex; flex-direction: column; gap: .85rem; }
119
+ .lyx-form .form-label { display: flex; align-items: center; gap: .3rem; }
120
+ .lyx-switches { display: flex; flex-wrap: wrap; gap: 1.25rem; }
121
+ .lyx-switches label { display: inline-flex; align-items: center; gap: .4rem; cursor: pointer; }
122
+ .lyx-colour { display: flex; align-items: center; gap: .5rem; }
123
+ .lyx-colour input[type="color"] { width: 2.4rem; height: 2rem; padding: 2px; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-sm); background: transparent; cursor: pointer; }
124
+ .lyx-preview { display: flex; align-items: center; gap: .75rem; padding: .6rem; border: 1px dashed var(--dm-border); border-radius: var(--dm-radius-md); }
125
+ .lyx-preview .lyx-shape { width: 5rem; height: 3.6rem; }
126
+ .lyx-pages { display: flex; flex-direction: column; }
127
+ .lyx-pages a { display: flex; justify-content: space-between; gap: .75rem; padding: .45rem .25rem; border-bottom: 1px solid var(--dm-border); color: var(--dm-text); text-decoration: none; }
128
+ .lyx-pages a:last-child { border-bottom: 0; }
129
+ .lyx-pages a:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
130
+ .lyx-pages small { color: var(--dm-text-muted); }
131
+ @media (max-width: 640px) { .lyx-grid { grid-template-columns: 1fr; } .lyx-chips .lyx-chip:not(.is-info) { display: none; } }
132
+ </style>
@@ -1,17 +1,105 @@
1
- <div class="view-header">
2
- <h1><span data-icon="user"></span> My Profile</h1>
3
- </div>
1
+ <!-- My Profile. Bound with M.applyBindings - see admin/views/my-profile.js, which also draws the profile
2
+ fields from the user-profiles schema. No page heading: the Tool banner above is the heading, and Save is
3
+ a banner action (and Ctrl+S). -->
4
+ <div class="mpx">
5
+ <p class="mpx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
6
+ <p class="mpx-note" data-bind-hidden="!loading.value">Loading…</p>
4
7
 
5
- <div class="card">
6
- <div class="card-header"><h2>Account</h2></div>
7
- <div class="card-body">
8
- <div id="account-form-container"></div>
8
+ <div class="mpx-dirty" data-bind-hidden="!dirty.value">
9
+ <span data-icon="edit" data-icon-size="14"></span>
10
+ <span>You have unsaved changes.</span>
11
+ <span class="mpx-spacer"></span>
12
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="discard">Discard</button>
13
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannotSave.value">
14
+ <span data-icon="check" data-icon-size="13"></span> Save</button>
9
15
  </div>
10
- </div>
11
16
 
12
- <div class="card mt-4" id="profile-card" style="display:none;">
13
- <div class="card-header"><h2>Profile</h2></div>
14
- <div class="card-body">
15
- <div id="profile-form-container"></div>
17
+ <div class="mpx-card mpx-you" data-bind-hidden="loading.value || loadError.value">
18
+ <span class="mpx-avatar" aria-hidden="true" data-bind-text="avatar.value"></span>
19
+ <span class="mpx-you-main">
20
+ <strong data-bind-text="shownName.value"></strong>
21
+ <small data-bind-text="shownEmail.value"></small>
22
+ <small class="mpx-facts" data-bind-text="factsText.value"></small>
23
+ </span>
24
+ <span class="mpx-chips">
25
+ <span class="mpx-chip is-ok" data-bind-text="rolesText.value" title="Your role - an administrator sets it"></span>
26
+ <span class="mpx-chip" data-bind-text="accessText.value" title="What you can see in the admin"></span>
27
+ </span>
28
+ <span data-help-title="Your account" data-help="Your role and which projects you can see are set by an administrator, under System › Users - they cannot be changed here. Everything below is yours to change."></span>
29
+ </div>
30
+
31
+ <div class="mpx-card" data-bind-hidden="loading.value || loadError.value">
32
+ <div class="mpx-card-title">Account
33
+ <span data-help="Your name is how you appear in the admin and on anything you author. Your email is what you sign in with, and where password resets are sent."></span></div>
34
+ <div class="mpx-grid">
35
+ <div>
36
+ <label class="form-label" for="mpx-name">Full name <span class="text-danger">*</span></label>
37
+ <input id="mpx-name" type="text" class="form-input" maxlength="100" autocomplete="name" placeholder="Jane Smith" data-model="name.value">
38
+ <small class="mpx-err" data-bind-hidden="!err.value.name" data-bind-text="err.value.name"></small>
39
+ </div>
40
+ <div>
41
+ <label class="form-label" for="mpx-email">Email <span class="text-danger">*</span></label>
42
+ <input id="mpx-email" type="email" class="form-input" maxlength="200" autocomplete="email" placeholder="jane@example.com" data-model="email.value">
43
+ <small class="mpx-err" data-bind-hidden="!err.value.email" data-bind-text="err.value.email"></small>
44
+ <small class="mpx-hint" data-bind-hidden="!emailChanged.value">You will sign in with the new address from now on.</small>
45
+ </div>
46
+ </div>
47
+ </div>
48
+
49
+ <div class="mpx-card" data-bind-hidden="loading.value || loadError.value">
50
+ <div class="mpx-card-title">Password
51
+ <span data-help-title="Changing your password" data-help="Leave both boxes empty to keep your current password. A new one needs at least 8 characters, typed twice. It is never kept as a draft - if you leave the screen, you type it again."></span></div>
52
+ <div class="mpx-grid">
53
+ <div>
54
+ <label class="form-label" for="mpx-pw">New password</label>
55
+ <input id="mpx-pw" type="password" data-bind-type="showPw.value ? 'text' : 'password'" class="form-input" autocomplete="new-password" placeholder="Leave empty to keep it" data-model="password.value">
56
+ <small class="mpx-err" data-bind-hidden="!err.value.password" data-bind-text="err.value.password"></small>
57
+ </div>
58
+ <div>
59
+ <label class="form-label" for="mpx-pw2">Type it again</label>
60
+ <input id="mpx-pw2" type="password" data-bind-type="showPw.value ? 'text' : 'password'" class="form-input" autocomplete="new-password" data-model="confirm.value">
61
+ <small class="mpx-err" data-bind-hidden="!err.value.confirm" data-bind-text="err.value.confirm"></small>
62
+ <small class="mpx-hint is-ok" data-bind-hidden="!pwMatch.value">✓ They match.</small>
63
+ </div>
64
+ </div>
65
+ <label class="mpx-show"><input type="checkbox" data-model="showPw.value"> Show what I type</label>
66
+ </div>
67
+
68
+ <div class="mpx-card" data-bind-hidden="loading.value || loadError.value || !hasProfile.value">
69
+ <div class="mpx-card-title">Profile
70
+ <span data-help="Extra details about you. Which ones are asked for is set by an administrator (the User Profiles collection)."></span></div>
71
+ <div class="mpx-grid mpx-profile"></div>
16
72
  </div>
17
73
  </div>
74
+
75
+ <style>
76
+ .mpx { display: flex; flex-direction: column; gap: .75rem; max-width: 60rem; }
77
+ .mpx [hidden] { display: none !important; }
78
+ .mpx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
79
+ .mpx-note.is-bad { color: var(--dm-danger); }
80
+ .mpx-dirty { display: flex; align-items: center; gap: .5rem; padding: .5rem .8rem; border-radius: var(--dm-radius-md);
81
+ background: color-mix(in srgb, var(--dm-warning) 14%, transparent); color: var(--dm-text); position: sticky; top: 0; z-index: 2; }
82
+ .mpx-dirty .btn { display: inline-flex; align-items: center; gap: .3rem; }
83
+ .mpx-spacer { flex: 1; }
84
+ .mpx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .8rem .9rem; display: flex; flex-direction: column; gap: .7rem; }
85
+ .mpx-card-title { display: flex; align-items: center; gap: .35rem; font-weight: 600; }
86
+ .mpx-you { flex-direction: row; align-items: center; gap: .9rem; flex-wrap: wrap; }
87
+ .mpx-avatar { display: inline-flex; align-items: center; justify-content: center; width: 3rem; height: 3rem; border-radius: 50%; flex-shrink: 0;
88
+ font-weight: 700; color: var(--dm-primary); background: color-mix(in srgb, var(--dm-primary) 14%, transparent); }
89
+ .mpx-you-main { flex: 1; min-width: 12rem; display: flex; flex-direction: column; }
90
+ .mpx-you-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
91
+ .mpx-facts { margin-top: .15rem; }
92
+ .mpx-chips { display: inline-flex; gap: .3rem; flex-wrap: wrap; }
93
+ .mpx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .55rem; border-radius: 999px; white-space: nowrap;
94
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
95
+ .mpx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
96
+ .mpx-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; }
97
+ .mpx-wide { grid-column: 1 / -1; }
98
+ .mpx .form-label { display: flex; align-items: center; gap: .3rem; }
99
+ .mpx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
100
+ .mpx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
101
+ .mpx-hint.is-ok { color: var(--dm-success); }
102
+ .mpx-show { display: inline-flex; align-items: center; gap: .4rem; font-size: var(--dm-text-sm); color: var(--dm-text-muted); cursor: pointer; width: fit-content; }
103
+ .mpx-check { display: inline-flex; align-items: center; gap: .4rem; cursor: pointer; margin-right: .8rem; }
104
+ @media (max-width: 640px) { .mpx-grid { grid-template-columns: 1fr; } }
105
+ </style>