domma-cms 0.74.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 (55) hide show
  1. package/CLAUDE.md +3 -1
  2. package/admin/js/app.js +2 -2
  3. package/admin/js/lib/actions-arrange.js +1 -0
  4. package/admin/js/lib/api-endpoints-arrange.js +1 -0
  5. package/admin/js/lib/blocks-arrange.js +1 -0
  6. package/admin/js/lib/collections-arrange.js +1 -0
  7. package/admin/js/lib/components-arrange.js +1 -0
  8. package/admin/js/lib/context-menus-arrange.js +1 -0
  9. package/admin/js/lib/forms-arrange.js +1 -0
  10. package/admin/js/lib/media-arrange.js +1 -0
  11. package/admin/js/lib/menus-arrange.js +1 -0
  12. package/admin/js/lib/pages-arrange.js +1 -0
  13. package/admin/js/lib/sidebar-renderer.js +3 -3
  14. package/admin/js/lib/theme-settings-arrange.js +1 -0
  15. package/admin/js/lib/views-arrange.js +1 -0
  16. package/admin/js/templates/actions-list.html +73 -14
  17. package/admin/js/templates/api-endpoints.html +63 -10
  18. package/admin/js/templates/blocks.html +76 -15
  19. package/admin/js/templates/collections.html +108 -52
  20. package/admin/js/templates/components.html +82 -15
  21. package/admin/js/templates/context-menus.html +55 -13
  22. package/admin/js/templates/forms.html +67 -14
  23. package/admin/js/templates/media.html +99 -13
  24. package/admin/js/templates/menus.html +57 -11
  25. package/admin/js/templates/pages.html +78 -23
  26. package/admin/js/templates/theme.html +164 -132
  27. package/admin/js/templates/views-list.html +63 -9
  28. package/admin/js/views/actions-list.js +37 -1
  29. package/admin/js/views/api-endpoints.js +11 -7
  30. package/admin/js/views/blocks.js +34 -5
  31. package/admin/js/views/collections.js +36 -1
  32. package/admin/js/views/components.js +27 -5
  33. package/admin/js/views/context-menus.js +10 -5
  34. package/admin/js/views/forms.js +57 -1
  35. package/admin/js/views/media.js +40 -1
  36. package/admin/js/views/menus.js +11 -5
  37. package/admin/js/views/pages.js +28 -8
  38. package/admin/js/views/theme.js +55 -1
  39. package/admin/js/views/views-list.js +11 -1
  40. package/package.json +1 -1
  41. package/server/routes/api/actions.js +64 -2
  42. package/server/routes/api/api-endpoints.js +41 -0
  43. package/server/routes/api/blocks.js +170 -8
  44. package/server/routes/api/collections.js +53 -1
  45. package/server/routes/api/components.js +46 -0
  46. package/server/routes/api/context-menus.js +8 -0
  47. package/server/routes/api/forms.js +123 -10
  48. package/server/routes/api/media.js +56 -1
  49. package/server/routes/api/menus.js +53 -6
  50. package/server/routes/api/pages.js +46 -0
  51. package/server/routes/api/theme.js +55 -0
  52. package/server/routes/api/views.js +27 -3
  53. package/server/server.js +16 -1
  54. package/server/services/contextMenus.js +41 -0
  55. package/server/services/views.js +38 -0
@@ -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>