domma-cms 0.73.0 → 0.75.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 (64) hide show
  1. package/CLAUDE.md +3 -1
  2. package/admin/js/api.js +1 -1
  3. package/admin/js/app.js +4 -4
  4. package/admin/js/lib/actions-arrange.js +1 -0
  5. package/admin/js/lib/api-endpoints-arrange.js +1 -0
  6. package/admin/js/lib/blocks-arrange.js +1 -0
  7. package/admin/js/lib/collections-arrange.js +1 -0
  8. package/admin/js/lib/components-arrange.js +1 -0
  9. package/admin/js/lib/context-menus-arrange.js +1 -0
  10. package/admin/js/lib/forms-arrange.js +1 -0
  11. package/admin/js/lib/media-arrange.js +1 -0
  12. package/admin/js/lib/menus-arrange.js +1 -0
  13. package/admin/js/lib/pages-arrange.js +1 -0
  14. package/admin/js/lib/projects-arrange.js +1 -0
  15. package/admin/js/lib/sidebar-renderer.js +3 -3
  16. package/admin/js/lib/theme-settings-arrange.js +1 -0
  17. package/admin/js/lib/views-arrange.js +1 -0
  18. package/admin/js/templates/actions-list.html +73 -14
  19. package/admin/js/templates/api-endpoints.html +63 -10
  20. package/admin/js/templates/blocks.html +76 -15
  21. package/admin/js/templates/collections.html +108 -52
  22. package/admin/js/templates/components.html +82 -15
  23. package/admin/js/templates/context-menus.html +55 -13
  24. package/admin/js/templates/forms.html +67 -14
  25. package/admin/js/templates/media.html +99 -13
  26. package/admin/js/templates/menus.html +57 -11
  27. package/admin/js/templates/pages.html +78 -23
  28. package/admin/js/templates/projects.html +69 -10
  29. package/admin/js/templates/theme.html +164 -132
  30. package/admin/js/templates/views-list.html +63 -9
  31. package/admin/js/views/actions-list.js +37 -1
  32. package/admin/js/views/api-endpoints.js +11 -7
  33. package/admin/js/views/blocks.js +34 -5
  34. package/admin/js/views/collections.js +36 -1
  35. package/admin/js/views/components.js +27 -5
  36. package/admin/js/views/context-menus.js +10 -5
  37. package/admin/js/views/forms.js +57 -1
  38. package/admin/js/views/index.js +1 -1
  39. package/admin/js/views/media.js +40 -1
  40. package/admin/js/views/menus.js +11 -5
  41. package/admin/js/views/pages.js +28 -8
  42. package/admin/js/views/projects.js +58 -3
  43. package/admin/js/views/theme.js +55 -1
  44. package/admin/js/views/views-list.js +11 -1
  45. package/package.json +1 -1
  46. package/server/routes/api/actions.js +64 -2
  47. package/server/routes/api/api-endpoints.js +41 -0
  48. package/server/routes/api/blocks.js +170 -8
  49. package/server/routes/api/collections.js +53 -1
  50. package/server/routes/api/components.js +46 -0
  51. package/server/routes/api/context-menus.js +8 -0
  52. package/server/routes/api/forms.js +123 -10
  53. package/server/routes/api/media.js +56 -1
  54. package/server/routes/api/menus.js +53 -6
  55. package/server/routes/api/pages.js +46 -0
  56. package/server/routes/api/projects.js +16 -2
  57. package/server/routes/api/theme.js +55 -0
  58. package/server/routes/api/views.js +27 -3
  59. package/server/server.js +19 -0
  60. package/server/services/contextMenus.js +41 -0
  61. package/server/services/projects.js +86 -26
  62. package/server/services/views.js +38 -0
  63. package/admin/js/templates/project-editor.html +0 -45
  64. package/admin/js/views/project-editor.js +0 -1
@@ -1,17 +1,70 @@
1
- <div class="view-header">
2
- <h1><span data-icon="layout"></span> Forms</h1>
3
- <div style="display:flex;gap:.5rem;">
4
- <button id="test-email-btn" class="btn btn-ghost btn-sm">
5
- <span data-icon="mail"></span> Test Email
6
- </button>
7
- <button id="create-form-btn" class="btn btn-primary">
8
- <span data-icon="plus"></span> Create Form
9
- </button>
10
- </div>
11
- </div>
1
+ <!-- Forms. Bound with M.applyBindings - see admin/views/forms.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; New form and Test email are banner actions. -->
4
+ <div class="fmx">
5
+ <p class="fmx-note">Forms collect answers on the site - contact, sign-up, feedback - and store, email or pass on what
6
+ is sent.
7
+ <span data-help-title="Forms" data-help="Build a form, then put it on any page with its shortcode (⋮ → Copy shortcode). Each submission is saved to the form's collection, and can also be emailed, sent to a webhook or run an Action - set in the form's Actions. Click a form to edit it; its submissions are one right-click away."></span></p>
12
8
 
13
- <div class="card">
14
- <div class="card-body">
15
- <div id="forms-table"></div>
9
+ <div class="fmx-card">
10
+ <div class="fmx-bar">
11
+ <div class="fmx-seg" role="group" aria-label="Show">
12
+ <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>
13
+ <button type="button" data-on-click="showRecent" data-bind-class="filter.value === 'recent' && 'is-on'">New this week <b data-bind-text="counts.value.recent"></b></button>
14
+ <button type="button" data-on-click="showEmpty" data-bind-class="filter.value === 'empty' && 'is-on'">No submissions <b data-bind-text="counts.value.empty"></b></button>
15
+ </div>
16
+ <input type="search" class="form-input fmx-search" placeholder="Search forms…" aria-label="Search forms" data-model="query.value">
17
+ <span data-help-title="The list" data-help="Forms with the newest submissions come first. New this week counts submissions in the last 7 days - the same number as the badge on Forms in the sidebar. Right-click a form (or ⋮, or Shift+F10) to see its submissions, copy its shortcode, duplicate it or delete it."></span>
18
+ <span class="fmx-spacer"></span>
19
+ <small class="fmx-muted" data-bind-text="summary.value"></small>
20
+ </div>
21
+ <p class="fmx-note" data-bind-hidden="!loading.value">Loading…</p>
22
+ <p class="fmx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
23
+ <p class="fmx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
24
+ <div class="fmx-list" data-each="rows.value key=slug"></div>
16
25
  </div>
17
26
  </div>
27
+
28
+ <style>
29
+ .fmx { display: flex; flex-direction: column; gap: .75rem; }
30
+ .fmx [hidden], .fmx-form [hidden] { display: none !important; }
31
+ .fmx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
32
+ .fmx-note.is-bad { color: var(--dm-danger); }
33
+ .fmx-muted { color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
34
+ .fmx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
35
+ .fmx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
36
+ .fmx-spacer { flex: 1; }
37
+ .fmx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
38
+ .fmx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
39
+ .fmx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
40
+ .fmx-seg button + button { border-left: 1px solid var(--dm-border); }
41
+ .fmx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
42
+ .fmx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
43
+ .fmx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
44
+ .fmx-list { display: flex; flex-direction: column; }
45
+ .fmx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
46
+ .fmx-row:last-child { border-bottom: 0; }
47
+ .fmx-row:hover, .fmx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
48
+ .fmx-icon { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; flex-shrink: 0;
49
+ border-radius: var(--dm-radius-md); color: var(--dm-primary); background: color-mix(in srgb, var(--dm-primary) 12%, transparent); }
50
+ .fmx-row.is-recent .fmx-icon { color: var(--dm-success); background: color-mix(in srgb, var(--dm-success) 14%, transparent); }
51
+ .fmx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
52
+ .fmx-main strong, .fmx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
53
+ .fmx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
54
+ .fmx-chips { display: inline-flex; gap: .3rem; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
55
+ .fmx-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;
56
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
57
+ .fmx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
58
+ .fmx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
59
+ .fmx-count { min-width: 3rem; text-align: right; font-variant-numeric: tabular-nums; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
60
+ .fmx-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; }
61
+ .fmx-kebab:hover { color: var(--dm-primary); }
62
+ /* Slideover forms */
63
+ .fmx-form { display: flex; flex-direction: column; gap: .85rem; }
64
+ .fmx-form .form-label { display: flex; align-items: center; gap: .3rem; }
65
+ .fmx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
66
+ .fmx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
67
+ .fmx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
68
+ .fmx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
69
+ @media (max-width: 640px) { .fmx-count { display: none; } .fmx-chips { display: none; } }
70
+ </style>
@@ -1,16 +1,102 @@
1
- <div class="view-header">
2
- <h1><span data-icon="image"></span> Media</h1>
3
- <div class="view-header-actions">
4
- <input id="file-input" type="file" multiple accept="image/*,video/*,.pdf,.svg" style="display:none">
5
- <button id="upload-btn" class="btn btn-primary"><span data-icon="upload"></span> Upload</button>
1
+ <!-- Media. Bound with M.applyBindings - see admin/views/media.js, which also inserts the tile template
2
+ (the router renders a templateUrl through _.render, eating any {{…}}). No page heading: the Tool
3
+ banner above is the heading, and Upload is a banner action. -->
4
+ <div class="mdx">
5
+ <input class="mdx-file-input" type="file" multiple accept="image/*,video/*,audio/*,.pdf,.svg,.txt,.csv,.doc,.docx,.xls,.xlsx" hidden>
6
+ <p class="mdx-note">Files for your pages, stored in <code>content/media/</code>. Drop files anywhere on this screen to
7
+ upload them.
8
+ <span data-help-title="Media" data-help="Upload with the button in the banner, or drop files onto this screen. Click a file for its details - its address, a snippet to paste into a page, a rename box, and the image editor for pictures. Right-click a file (or ⋮, or Shift+F10) for the same actions without opening it. Uploading a file with the same name as one already here replaces it; you are asked first."></span></p>
9
+ <p class="mdx-busy" data-bind-hidden="!busyText.value"><span class="mdx-spinner" aria-hidden="true"></span><span data-bind-text="busyText.value"></span></p>
10
+
11
+ <div class="mdx-card" data-bind-class="dragging.value && 'is-drop'">
12
+ <div class="mdx-bar">
13
+ <div class="mdx-seg" role="group" aria-label="Show">
14
+ <button type="button" data-on-click="kindAll" data-bind-class="kind.value === 'all' && 'is-on'">All <b data-bind-text="counts.value.all"></b></button>
15
+ <button type="button" data-on-click="kindImage" data-bind-class="kind.value === 'image' && 'is-on'" data-bind-hidden="!counts.value.image">Images <b data-bind-text="counts.value.image"></b></button>
16
+ <button type="button" data-on-click="kindVideo" data-bind-class="kind.value === 'video' && 'is-on'" data-bind-hidden="!counts.value.video">Video <b data-bind-text="counts.value.video"></b></button>
17
+ <button type="button" data-on-click="kindAudio" data-bind-class="kind.value === 'audio' && 'is-on'" data-bind-hidden="!counts.value.audio">Audio <b data-bind-text="counts.value.audio"></b></button>
18
+ <button type="button" data-on-click="kindDocument" data-bind-class="kind.value === 'document' && 'is-on'" data-bind-hidden="!counts.value.document">Documents <b data-bind-text="counts.value.document"></b></button>
19
+ <button type="button" data-on-click="kindOther" data-bind-class="kind.value === 'other' && 'is-on'" data-bind-hidden="!counts.value.other">Other <b data-bind-text="counts.value.other"></b></button>
20
+ </div>
21
+ <input type="search" class="form-input mdx-search" placeholder="Search file names…" aria-label="Search file names" data-model="query.value">
22
+ <select class="form-input mdx-sort" aria-label="Sort" data-model="sort.value">
23
+ <option value="newest">Newest first</option>
24
+ <option value="oldest">Oldest first</option>
25
+ <option value="name">Name</option>
26
+ <option value="size">Largest first</option>
27
+ </select>
28
+ <span class="mdx-total" data-bind-text="totalText.value"></span>
29
+ </div>
30
+ <p class="mdx-note" data-bind-hidden="!loading.value">Loading…</p>
31
+ <p class="mdx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
32
+ <div class="mdx-empty" data-bind-hidden="loading.value || loadError.value || tiles.value.length">
33
+ <span data-icon="upload" data-icon-size="28"></span>
34
+ <span data-bind-text="emptyText.value"></span>
35
+ </div>
36
+ <div class="mdx-grid" data-each="tiles.value key=name"></div>
37
+ <div class="mdx-drop" aria-hidden="true"><span data-icon="upload" data-icon-size="28"></span> Drop to upload</div>
6
38
  </div>
7
39
  </div>
8
40
 
9
- <p class="text-muted mb-4">
10
- Uploaded files are stored in <code>content/media/</code>.
11
- Use the copy button to get a URL you can embed in your pages.
12
- </p>
13
-
14
- <div id="media-grid" class="media-grid">
15
- <p class="text-muted">Loading…</p>
16
- </div>
41
+ <style>
42
+ .mdx { display: flex; flex-direction: column; gap: .75rem; }
43
+ .mdx [hidden], .mdx-panel [hidden] { display: none !important; }
44
+ .mdx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
45
+ .mdx-note.is-bad { color: var(--dm-danger); }
46
+ .mdx-busy { margin: 0; display: flex; align-items: center; gap: .5rem; padding: .5rem .75rem; border-radius: var(--dm-radius-md);
47
+ background: color-mix(in srgb, var(--dm-primary) 12%, transparent); color: var(--dm-text); font-size: var(--dm-text-sm); }
48
+ .mdx-spinner { width: 14px; height: 14px; border-radius: 50%; border: 2px solid color-mix(in srgb, var(--dm-primary) 30%, transparent);
49
+ border-top-color: var(--dm-primary); animation: mdx-spin .8s linear infinite; flex-shrink: 0; }
50
+ @keyframes mdx-spin { to { transform: rotate(360deg); } }
51
+ .mdx-card { position: relative; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .75rem; }
52
+ .mdx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
53
+ .mdx-search { flex: 1; min-width: 10rem; max-width: 20rem; }
54
+ .mdx-sort { width: auto; }
55
+ .mdx-total { margin-left: auto; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
56
+ .mdx-seg { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
57
+ .mdx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
58
+ .mdx-seg button + button { border-left: 1px solid var(--dm-border); }
59
+ .mdx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
60
+ .mdx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
61
+ .mdx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
62
+ .mdx-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr)); gap: .75rem; }
63
+ .mdx-tile { position: relative; display: flex; flex-direction: column; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md);
64
+ background: var(--dm-surface); overflow: hidden; cursor: pointer; outline: none; }
65
+ .mdx-tile:hover, .mdx-tile:focus-visible { border-color: var(--dm-primary); box-shadow: 0 0 0 1px var(--dm-primary); }
66
+ .mdx-thumb { aspect-ratio: 4 / 3; display: flex; align-items: center; justify-content: center; overflow: hidden;
67
+ background: color-mix(in srgb, var(--dm-text) 6%, var(--dm-surface)); color: var(--dm-text-muted); }
68
+ .mdx-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
69
+ .mdx-glyph { display: inline-flex; }
70
+ .mdx-info { display: flex; flex-direction: column; padding: .45rem .55rem .5rem; min-width: 0; }
71
+ .mdx-name { font-weight: 600; font-size: var(--dm-text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
72
+ .mdx-meta { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
73
+ .mdx-chip { position: absolute; top: .4rem; left: .4rem; font-size: var(--dm-text-xs); font-weight: 600; padding: .05rem .45rem; border-radius: 999px;
74
+ background: color-mix(in srgb, var(--dm-surface) 85%, transparent); color: var(--dm-text); border: 1px solid var(--dm-border); }
75
+ .mdx-kebab { position: absolute; top: .3rem; right: .3rem; border: 1px solid var(--dm-border); background: color-mix(in srgb, var(--dm-surface) 85%, transparent);
76
+ cursor: pointer; font-size: var(--dm-text-lg); line-height: 1; color: var(--dm-text); border-radius: var(--dm-radius-sm); padding: 0 .35rem; }
77
+ .mdx-kebab:hover { color: var(--dm-primary); }
78
+ .mdx-empty { display: flex; flex-direction: column; align-items: center; gap: .5rem; padding: 2.5rem 1rem; color: var(--dm-text-muted);
79
+ border: 2px dashed var(--dm-border); border-radius: var(--dm-radius-md); text-align: center; }
80
+ .mdx-drop { display: none; position: absolute; inset: 0; align-items: center; justify-content: center; gap: .5rem; font-weight: 600;
81
+ border: 2px dashed var(--dm-primary); border-radius: var(--dm-radius-md); color: var(--dm-primary);
82
+ background: color-mix(in srgb, var(--dm-primary) 10%, var(--dm-surface)); z-index: 2; pointer-events: none; }
83
+ .mdx-card.is-drop .mdx-drop { display: flex; }
84
+ /* The details slideover */
85
+ .mdx-panel { display: flex; flex-direction: column; gap: .85rem; }
86
+ .mdx-preview { display: flex; align-items: center; justify-content: center; min-height: 10rem; max-height: 22rem; overflow: hidden;
87
+ border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); background: color-mix(in srgb, var(--dm-text) 6%, var(--dm-surface)); color: var(--dm-text-muted); }
88
+ .mdx-preview img, .mdx-preview video { max-width: 100%; max-height: 22rem; display: block; }
89
+ .mdx-preview audio { width: 100%; margin: 1rem; }
90
+ .mdx-facts { display: grid; grid-template-columns: auto 1fr; gap: .25rem .75rem; margin: 0; font-size: var(--dm-text-sm); }
91
+ .mdx-facts dt { color: var(--dm-text-muted); }
92
+ .mdx-facts dd { margin: 0; overflow-wrap: anywhere; }
93
+ .mdx-copy { display: flex; gap: .4rem; }
94
+ .mdx-copy input { flex: 1; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm); }
95
+ .mdx-panel .form-label { display: flex; align-items: center; gap: .3rem; }
96
+ .mdx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
97
+ .mdx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
98
+ .mdx-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
99
+ .mdx-actions .btn, .mdx-copy .btn { display: inline-flex; align-items: center; gap: .3rem; }
100
+ .mdx-spacer { flex: 1; }
101
+ @media (max-width: 640px) { .mdx-grid { grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); } .mdx-total { margin-left: 0; } }
102
+ </style>
@@ -1,14 +1,60 @@
1
- <div class="page-header">
2
- <h2><span data-icon="menu"></span> Menus</h2>
3
- <div class="header-actions">
4
- <a class="btn btn-secondary" href="#/menu-locations"><span data-icon="map"></span> Manage locations</a>
5
- <a class="btn btn-primary" href="#/menus/new"><span data-icon="plus"></span> New menu</a>
6
- </div>
7
- </div>
8
- <p class="text-muted mb-4">Multi-named menus for the public site, footers, and any plugin-registered slot.</p>
1
+ <!-- Menus. Bound with M.applyBindings - see admin/views/menus.js, which also inserts the row template (the
2
+ router renders a templateUrl through _.render, eating any {{…}}). No page heading: the Tool banner
3
+ above is the heading, and New menu / Locations are banner actions. -->
4
+ <div class="mnx">
5
+ <p class="mnx-note">Menus for the site's navbar and footers, the admin sidebar, floating overlays, and any slot a
6
+ plugin adds.
7
+ <span data-help-title="Menus and locations" data-help="A menu is a list of links. A location (a slot) is where one is shown: the navbar, the footers, the admin sidebar, a page overlay, or a plugin's slot. Locations maps each slot to a menu site-wide; a menu's own binding can take a slot over on chosen pages or projects instead. Any menu can also be shown inside page content with its [menu] shortcode."></span></p>
9
8
 
10
- <div class="card">
11
- <div class="card-body">
12
- <div id="menus-table"></div>
9
+ <div class="mnx-card">
10
+ <div class="mnx-bar">
11
+ <div class="mnx-seg" role="group" aria-label="Show">
12
+ <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>
13
+ <button type="button" data-on-click="showPlaced" data-bind-class="filter.value === 'placed' && 'is-on'">In a location <b data-bind-text="counts.value.placed"></b></button>
14
+ <button type="button" data-on-click="showUnplaced" data-bind-class="filter.value === 'unplaced' && 'is-on'">Not in a location <b data-bind-text="counts.value.unplaced"></b></button>
15
+ </div>
16
+ <input type="search" class="form-input mnx-search" placeholder="Search menus…" aria-label="Search menus" data-model="query.value">
17
+ <span data-help-title="The list" data-help="Click a menu to edit it. Right-click it (or ⋮, or Shift+F10) to duplicate it, copy its [menu] shortcode, open Locations, or delete it. A menu that a location uses cannot be deleted until it is taken out of the location. 'Not in a location' does not mean unused - a page may show it with its shortcode."></span>
18
+ </div>
19
+ <p class="mnx-note" data-bind-hidden="!projectText.value" data-bind-text="projectText.value"></p>
20
+ <p class="mnx-note" data-bind-hidden="!loading.value">Loading…</p>
21
+ <p class="mnx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
22
+ <p class="mnx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
23
+ <div class="mnx-list" data-each="rows.value key=slug"></div>
13
24
  </div>
14
25
  </div>
26
+
27
+ <style>
28
+ .mnx { display: flex; flex-direction: column; gap: .75rem; }
29
+ .mnx [hidden] { display: none !important; }
30
+ .mnx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
31
+ .mnx-note.is-bad { color: var(--dm-danger); }
32
+ .mnx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
33
+ .mnx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
34
+ .mnx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
35
+ .mnx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
36
+ .mnx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
37
+ .mnx-seg button + button { border-left: 1px solid var(--dm-border); }
38
+ .mnx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
39
+ .mnx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
40
+ .mnx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
41
+ .mnx-list { display: flex; flex-direction: column; }
42
+ .mnx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
43
+ .mnx-row:last-child { border-bottom: 0; }
44
+ .mnx-row:hover, .mnx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
45
+ .mnx-icon { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; flex-shrink: 0;
46
+ border-radius: var(--dm-radius-md); color: var(--dm-primary); background: color-mix(in srgb, var(--dm-primary) 12%, transparent); }
47
+ .mnx-row.is-unplaced .mnx-icon { color: var(--dm-text-muted); background: color-mix(in srgb, var(--dm-text-muted) 12%, transparent); }
48
+ .mnx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
49
+ .mnx-main strong, .mnx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
50
+ .mnx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
51
+ .mnx-chips { display: inline-flex; flex-wrap: wrap; justify-content: flex-end; gap: .3rem; flex-shrink: 0; max-width: 45%; }
52
+ .mnx-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;
53
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
54
+ .mnx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
55
+ .mnx-chip.is-info { background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 16%, transparent); color: var(--dm-info, var(--dm-primary)); }
56
+ .mnx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
57
+ .mnx-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; }
58
+ .mnx-kebab:hover { color: var(--dm-primary); }
59
+ @media (max-width: 640px) { .mnx-chips { display: none; } }
60
+ </style>
@@ -1,27 +1,82 @@
1
- <div class="view-header">
2
- <h1><span data-icon="file-text"></span> Pages</h1>
3
- <div style="display:flex;gap:.5rem;align-items:center;margin-left:auto;">
4
- <input id="pages-search" type="text" class="form-input form-input-sm" placeholder="Search pages…"
5
- style="width:220px;">
6
- <a href="#/pages/new" class="btn btn-primary"><span data-icon="plus"></span> New Page</a>
7
- </div>
8
- </div>
1
+ <!-- Pages. Bound with M.applyBindings - see admin/views/pages.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 page is a banner action. -->
4
+ <div class="pgx">
5
+ <p class="pgx-notice" data-bind-hidden="!projectNote.value">
6
+ <span data-bind-text="projectNote.value"></span> <a href="#/pages">Show all pages</a>
7
+ </p>
9
8
 
10
- <div class="toolbar mb-3" style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;">
11
- <select id="status-filter" class="form-select form-select-sm" style="width:auto">
12
- <option value="">All statuses</option>
13
- <option value="published">Published</option>
14
- <option value="draft">Draft</option>
15
- </select>
16
- <div class="btn-group" style="margin-left:auto;">
17
- <button id="view-table-btn" class="btn btn-sm btn-primary" data-tooltip="Table view"><span data-icon="list"></span></button>
18
- <button id="view-tree-btn" class="btn btn-sm btn-ghost" data-tooltip="Tree view"><span data-icon="tree"></span></button>
9
+ <div class="pgx-card">
10
+ <div class="pgx-bar">
11
+ <div class="pgx-seg" role="group" aria-label="Status">
12
+ <button type="button" data-on-click="showAll" data-bind-class="status.value === 'all' && 'is-on'">All <b data-bind-text="counts.value.all"></b></button>
13
+ <button type="button" data-on-click="showPublished" data-bind-class="status.value === 'published' && 'is-on'">Published <b data-bind-text="counts.value.published"></b></button>
14
+ <button type="button" data-on-click="showDrafts" data-bind-class="status.value === 'draft' && 'is-on'">Drafts <b data-bind-text="counts.value.draft"></b></button>
15
+ </div>
16
+ <input type="search" class="form-input pgx-search" placeholder="Search title, URL, tag or layout…" aria-label="Search pages" data-model="query.value">
17
+ <select class="form-input pgx-sort" aria-label="Sort" data-model="sort.value" data-bind-hidden="mode.value === 'tree'">
18
+ <option value="order">Navigation order</option>
19
+ <option value="updated">Recently changed</option>
20
+ <option value="title">Title</option>
21
+ <option value="url">URL</option>
22
+ </select>
23
+ <span class="pgx-spacer"></span>
24
+ <div class="pgx-seg" role="group" aria-label="View">
25
+ <button type="button" data-on-click="showList" data-bind-class="mode.value === 'list' && 'is-on'" title="List">List</button>
26
+ <button type="button" data-on-click="showTree" data-bind-class="mode.value === 'tree' && 'is-on'" title="Tree - pages under the page whose URL they sit beneath">Tree</button>
27
+ </div>
28
+ <span data-help-title="Pages" data-help="Click a page to edit it. Right-click it (or ⋮, or Shift+F10) to view it on the site, copy its address, trim its revision history or delete it. Drafts are not public: visitors get a 404, while anyone who can edit pages sees them with a Draft banner. Tree shows each page under the page whose address it sits beneath. Your filter, sort and view are remembered."></span>
29
+ </div>
30
+ <p class="pgx-note" data-bind-hidden="!loading.value">Loading…</p>
31
+ <p class="pgx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
32
+ <p class="pgx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
33
+ <div class="pgx-list" data-each="rows.value key=urlPath" data-bind-hidden="mode.value === 'tree'"></div>
34
+ <div class="pgx-tree" data-bind-hidden="mode.value !== 'tree' || !rows.value.length"></div>
19
35
  </div>
20
36
  </div>
21
37
 
22
- <div class="card">
23
- <div class="card-body">
24
- <div id="pages-table"></div>
25
- <div id="pages-tree" style="display:none;"></div>
26
- </div>
27
- </div>
38
+ <style>
39
+ .pgx { display: flex; flex-direction: column; gap: .75rem; }
40
+ .pgx [hidden], .pgx-form [hidden] { display: none !important; }
41
+ .pgx-notice { margin: 0; padding: .5rem .75rem; border-radius: var(--dm-radius-md); font-size: var(--dm-text-sm);
42
+ background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 12%, transparent); color: var(--dm-text); }
43
+ .pgx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
44
+ .pgx-note.is-bad { color: var(--dm-danger); }
45
+ .pgx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
46
+ .pgx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
47
+ .pgx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
48
+ .pgx-sort { width: auto; }
49
+ .pgx-spacer { flex: 1; }
50
+ .pgx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
51
+ .pgx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
52
+ .pgx-seg button + button { border-left: 1px solid var(--dm-border); }
53
+ .pgx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
54
+ .pgx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
55
+ .pgx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
56
+ .pgx-list { display: flex; flex-direction: column; }
57
+ .pgx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
58
+ .pgx-row:last-child { border-bottom: 0; }
59
+ .pgx-row:hover, .pgx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
60
+ .pgx-row.is-draft .pgx-main strong { color: var(--dm-text-muted); }
61
+ .pgx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
62
+ .pgx-main strong, .pgx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
63
+ .pgx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
64
+ .pgx-tags { color: var(--dm-info, var(--dm-primary)); }
65
+ .pgx-chips { display: inline-flex; gap: .3rem; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
66
+ .pgx-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;
67
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
68
+ .pgx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
69
+ .pgx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
70
+ .pgx-chip.is-info { background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 14%, transparent); color: var(--dm-info, var(--dm-primary)); }
71
+ .pgx-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; }
72
+ .pgx-kebab:hover { color: var(--dm-primary); }
73
+ .pgx-tree { padding: .25rem 0; }
74
+ /* The prune slideover */
75
+ .pgx-form { display: flex; flex-direction: column; gap: .85rem; }
76
+ .pgx-form .form-label { display: flex; align-items: center; gap: .3rem; }
77
+ .pgx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
78
+ .pgx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
79
+ .pgx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
80
+ .pgx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
81
+ @media (max-width: 720px) { .pgx-chips { display: none; } }
82
+ </style>
@@ -1,13 +1,72 @@
1
- <div class="page-header">
2
- <h2><span data-icon="folder"></span> Projects</h2>
3
- <div class="header-actions">
4
- <a class="btn btn-primary" href="#/projects/new"><span data-icon="plus"></span> New project</a>
5
- </div>
6
- </div>
7
- <p class="text-muted mb-4">Group related artefacts under a project for navigation and per-user access scoping.</p>
1
+ <!-- Manage projects. Bound with M.applyBindings - see admin/views/projects.js, which also inserts the row
2
+ template (the router renders a templateUrl through _.render, eating any {{…}}). No page heading:
3
+ the Tool banner above is the heading, and New project is a banner action. -->
4
+ <div class="pjx">
5
+ <p class="pjx-note">Group related pages, collections, forms and the rest under a project - for the sidebar, and to
6
+ limit what a user can see.
7
+ <span data-help-title="Projects" data-help="Everything not filed under a project belongs to Core, which cannot be removed. A project's root URL files pages under it automatically (a page at /members/join belongs to the project whose root URL is /members). A user given projects sees only those, plus Core. Disabling a project takes its public pages and external API offline; editing in the admin carries on."></span></p>
8
8
 
9
- <div class="card">
10
- <div class="card-body">
11
- <div id="projects-table"></div>
9
+ <div class="pjx-card">
10
+ <div class="pjx-bar">
11
+ <div class="pjx-seg" role="group" aria-label="Show">
12
+ <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>
13
+ <button type="button" data-on-click="showEnabled" data-bind-class="filter.value === 'enabled' && 'is-on'">Enabled <b data-bind-text="counts.value.enabled"></b></button>
14
+ <button type="button" data-on-click="showDisabled" data-bind-class="filter.value === 'disabled' && 'is-on'">Disabled <b data-bind-text="counts.value.disabled"></b></button>
15
+ </div>
16
+ <input type="search" class="form-input pjx-search" placeholder="Search projects…" aria-label="Search projects" data-model="query.value">
17
+ <span data-help-title="The list" data-help="Click a project to open its overview. Right-click it (or ⋮, or Shift+F10) to edit it, open its settings, enable or disable it, untag everything filed under it, or delete it. A project can only be deleted once nothing is filed under it; built-in projects cannot be deleted."></span>
18
+ </div>
19
+ <p class="pjx-note" data-bind-hidden="!loading.value">Loading…</p>
20
+ <p class="pjx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
21
+ <p class="pjx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
22
+ <div class="pjx-list" data-each="rows.value key=slug"></div>
12
23
  </div>
13
24
  </div>
25
+
26
+ <style>
27
+ .pjx { display: flex; flex-direction: column; gap: .75rem; }
28
+ .pjx [hidden], .pjx-form [hidden] { display: none !important; }
29
+ .pjx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
30
+ .pjx-note.is-bad { color: var(--dm-danger); }
31
+ .pjx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
32
+ .pjx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
33
+ .pjx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
34
+ .pjx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
35
+ .pjx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
36
+ .pjx-seg button + button { border-left: 1px solid var(--dm-border); }
37
+ .pjx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
38
+ .pjx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
39
+ .pjx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
40
+ .pjx-list { display: flex; flex-direction: column; }
41
+ .pjx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
42
+ .pjx-row:last-child { border-bottom: 0; }
43
+ .pjx-row:hover, .pjx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
44
+ .pjx-icon { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; flex-shrink: 0;
45
+ border-radius: var(--dm-radius-md); color: var(--dm-primary); background: color-mix(in srgb, var(--dm-primary) 12%, transparent); }
46
+ .pjx-row.is-off .pjx-icon { color: var(--dm-text-muted); background: color-mix(in srgb, var(--dm-text-muted) 12%, transparent); }
47
+ .pjx-row.is-off .pjx-main strong { color: var(--dm-text-muted); }
48
+ .pjx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
49
+ .pjx-main strong, .pjx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
50
+ .pjx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
51
+ .pjx-chips { display: inline-flex; gap: .3rem; flex-shrink: 0; }
52
+ .pjx-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;
53
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
54
+ .pjx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
55
+ .pjx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
56
+ .pjx-count { min-width: 2.5rem; text-align: right; font-variant-numeric: tabular-nums; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
57
+ .pjx-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; }
58
+ .pjx-kebab:hover { color: var(--dm-primary); }
59
+ /* The editor slideover */
60
+ .pjx-form { display: flex; flex-direction: column; gap: .85rem; }
61
+ .pjx-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .85rem; }
62
+ .pjx-wide { grid-column: 1 / -1; }
63
+ .pjx-form .form-label { display: flex; align-items: center; gap: .3rem; }
64
+ .pjx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
65
+ .pjx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
66
+ .pjx-setting { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
67
+ .pjx-setting .form-label { margin: 0; }
68
+ .pjx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
69
+ .pjx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
70
+ .pjx-spacer { flex: 1; }
71
+ @media (max-width: 640px) { .pjx-grid { grid-template-columns: 1fr; } .pjx-count { display: none; } }
72
+ </style>