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,14 +1,74 @@
1
- <div class="view-container">
2
- <div class="view-header">
3
- <h1><span data-icon="bell"></span> Notifications</h1>
4
- </div>
5
-
6
- <div id="notif-list"></div>
1
+ <!-- Notifications. Bound with M.applyBindings - see admin/views/notifications.js, which also inserts the row
2
+ template (the router renders a templateUrl through _.render, eating any {{…}}). No page heading: the
3
+ Tool banner above is the heading, and Dismiss all / Refresh are banner actions. -->
4
+ <div class="ntx">
5
+ <p class="ntx-note">Messages for this site's administrators - maintenance, updates and warnings, mostly from the
6
+ manager that hosts it.
7
+ <span data-help-title="Notifications" data-help="Opening this screen marks everything in it as read - the ones that were unread stay marked New until you leave. Click a notification to read all of it; right-click it (or ⋮, or Shift+F10) to open its link, copy it, mark it unread again, dismiss it or delete it. Dismissing hides it for you only; deleting removes it for everyone and needs the delete permission. A notification with an end date disappears by itself once it passes."></span></p>
7
8
 
8
- <div id="notif-empty" hidden class="card">
9
- <div class="card-body" style="text-align:center;padding:3rem;color:var(--dm-text-muted)">
10
- <span data-icon="check-circle" style="font-size:2rem;display:block;margin-bottom:.5rem"></span>
11
- <p>No notifications - you're all caught up.</p>
9
+ <div class="ntx-card">
10
+ <div class="ntx-bar">
11
+ <div class="ntx-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="showNew" data-bind-class="filter.value === 'new' && 'is-on'" data-bind-hidden="!counts.value.new">New <b data-bind-text="counts.value.new"></b></button>
14
+ <button type="button" data-on-click="showCritical" data-bind-class="filter.value === 'critical' && 'is-on'" data-bind-hidden="!counts.value.critical">Critical <b data-bind-text="counts.value.critical"></b></button>
15
+ <button type="button" data-on-click="showWarning" data-bind-class="filter.value === 'warning' && 'is-on'" data-bind-hidden="!counts.value.warning">Warnings <b data-bind-text="counts.value.warning"></b></button>
16
+ <button type="button" data-on-click="showInfo" data-bind-class="filter.value === 'info' && 'is-on'" data-bind-hidden="!counts.value.info">Info <b data-bind-text="counts.value.info"></b></button>
17
+ <button type="button" data-on-click="showSuccess" data-bind-class="filter.value === 'success' && 'is-on'" data-bind-hidden="!counts.value.success">Success <b data-bind-text="counts.value.success"></b></button>
18
+ </div>
19
+ <input type="search" class="form-input ntx-search" placeholder="Search notifications…" aria-label="Search notifications" data-model="query.value">
12
20
  </div>
21
+ <p class="ntx-note" data-bind-hidden="!loading.value">Loading…</p>
22
+ <p class="ntx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
23
+ <div class="ntx-empty" data-bind-hidden="loading.value || loadError.value || rows.value.length">
24
+ <span data-icon="check-circle" data-icon-size="28"></span>
25
+ <span data-bind-text="emptyText.value"></span>
26
+ </div>
27
+ <div class="ntx-list" data-each="rows.value key=id"></div>
13
28
  </div>
14
29
  </div>
30
+
31
+ <style>
32
+ .ntx { display: flex; flex-direction: column; gap: .75rem; }
33
+ .ntx [hidden] { display: none !important; }
34
+ .ntx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
35
+ .ntx-note.is-bad { color: var(--dm-danger); }
36
+ .ntx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
37
+ .ntx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
38
+ .ntx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
39
+ .ntx-seg { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
40
+ .ntx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
41
+ .ntx-seg button + button { border-left: 1px solid var(--dm-border); }
42
+ .ntx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
43
+ .ntx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
44
+ .ntx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
45
+ .ntx-empty { display: flex; flex-direction: column; align-items: center; gap: .5rem; padding: 2rem 1rem; color: var(--dm-text-muted); }
46
+ .ntx-empty [data-icon], .ntx-empty svg { color: var(--dm-success); }
47
+ .ntx-list { display: flex; flex-direction: column; }
48
+ .ntx-row { display: flex; align-items: flex-start; gap: .7rem; padding: .6rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
49
+ .ntx-row:last-child { border-bottom: 0; }
50
+ .ntx-row:hover, .ntx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
51
+ .ntx-dot { width: .55rem; height: .55rem; border-radius: 50%; margin-top: .45rem; flex-shrink: 0; background: var(--dm-info, var(--dm-primary)); }
52
+ .ntx-row.is-critical .ntx-dot { background: var(--dm-danger); }
53
+ .ntx-row.is-warning .ntx-dot { background: var(--dm-warning); }
54
+ .ntx-row.is-success .ntx-dot { background: var(--dm-success); }
55
+ .ntx-row:not(.is-unread) .ntx-main strong { font-weight: 500; }
56
+ .ntx-row.is-critical { box-shadow: inset 3px 0 0 var(--dm-danger); }
57
+ .ntx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: .15rem; }
58
+ .ntx-head { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
59
+ .ntx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
60
+ .ntx-preview { color: var(--dm-text-muted); font-size: var(--dm-text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
61
+ .ntx-body { white-space: pre-wrap; font-size: var(--dm-text-sm); color: var(--dm-text); margin-top: .25rem; overflow-wrap: anywhere; }
62
+ .ntx-actions { display: flex; gap: .4rem; margin-top: .4rem; }
63
+ .ntx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
64
+ .ntx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .05rem .5rem; border-radius: 999px; white-space: nowrap;
65
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
66
+ .ntx-chip.is-danger { background: color-mix(in srgb, var(--dm-danger) 16%, transparent); color: var(--dm-danger); }
67
+ .ntx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
68
+ .ntx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
69
+ .ntx-chip.is-info { background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 14%, transparent); color: var(--dm-info, var(--dm-primary)); }
70
+ .ntx-chip.is-new { background: var(--dm-primary); color: var(--dm-primary-text); }
71
+ .ntx-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
+ .ntx-kebab:hover { color: var(--dm-primary); }
73
+ @media (max-width: 640px) { .ntx-preview { white-space: normal; } }
74
+ </style>
@@ -1,35 +1,107 @@
1
- <div class="view-header d-flex justify-content-between align-items-center">
2
- <div>
3
- <h1><span data-icon="store"></span> Marketplace</h1>
4
- <p class="text-muted" style="margin:0;font-size:.875rem">Install plugins to add features. Once enabled, a plugin appears under Tools. Changes take effect after a server restart.</p>
5
- </div>
6
- <div style="display:flex;gap:.5rem;align-items:center;">
7
- <button id="install-file-btn" class="btn btn-secondary" title="Install a .dcmsplugin file">
8
- <span data-icon="upload"></span> Install from File
9
- </button>
10
- <button id="new-plugin-btn" class="btn btn-primary">
11
- <span data-icon="plus"></span> New Plugin
12
- </button>
13
- </div>
14
- <input type="file" id="install-file-input" accept=".dcmsplugin" hidden>
15
- </div>
1
+ <!-- The Marketplace (System › Plugins). Bound with M.applyBindings - see admin/js/views/plugins.js, which
2
+ also inserts the row templates (the router renders a templateUrl through _.render, eating any {{…}}).
3
+ No page heading: the Tool banner is the heading; New plugin and Install from file are banner actions. -->
4
+ <div class="plx">
5
+ <p class="plx-note">Install plugins to add features. Once switched on, a plugin is a Tool and appears in the sidebar.
6
+ <span data-help-title="The Marketplace" data-help="Installed lists what is on this site; Browse lists what this site can get from its manager (a site that is not managed installs from a .dcmsplugin file instead - Install from file in the banner). Switching a plugin on or off restarts the server, because a plugin's routes and screens are only read at start-up. Licensed plugins carry their licence with them: one that is switched on but not licensed does not run, and says so here."></span></p>
7
+ <input type="file" class="plx-file" accept=".dcmsplugin" hidden>
16
8
 
17
- <div id="plugin-tabs" class="tabs">
18
- <div class="tab-list">
19
- <button class="tab-item active" data-tab="installed">Installed</button>
20
- <button class="tab-item" data-tab="marketplace">Browse</button>
21
- </div>
9
+ <p class="plx-alert" data-bind-hidden="!restarting.value">
10
+ <span class="plx-spinner" aria-hidden="true"></span>
11
+ <span data-bind-text="restarting.value"></span>
12
+ </p>
22
13
 
23
- <div class="tab-panel active" data-panel="installed">
24
- <div id="plugins-empty" class="card" style="display:none">
25
- <div class="card-body text-muted">
26
- No plugins found. Drop a plugin folder into the <code>plugins/</code> directory to get started.
14
+ <div class="tabs plx-tabs">
15
+ <div class="tab-list">
16
+ <button type="button" class="tab-item active">Installed <span class="plx-count" data-bind-text="plugins.value.length"></span></button>
17
+ <button type="button" class="tab-item">Browse <span class="plx-count" data-bind-hidden="!updateCount.value" data-bind-text="updateCountText.value"></span></button>
18
+ </div>
19
+
20
+ <!-- Installed -->
21
+ <div class="tab-panel active">
22
+ <div class="plx-card">
23
+ <div class="plx-bar">
24
+ <div class="plx-seg" role="group" aria-label="Show">
25
+ <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>
26
+ <button type="button" data-on-click="showOn" data-bind-class="filter.value === 'on' && 'is-on'">On <b data-bind-text="counts.value.on"></b></button>
27
+ <button type="button" data-on-click="showOff" data-bind-class="filter.value === 'off' && 'is-on'">Off <b data-bind-text="counts.value.off"></b></button>
28
+ <button type="button" data-on-click="showAttention" data-bind-class="filter.value === 'attention' && 'is-on'" data-bind-hidden="!counts.value.attention">Needs attention <b data-bind-text="counts.value.attention"></b></button>
29
+ </div>
30
+ <input type="search" class="form-input plx-search" placeholder="Search plugins…" aria-label="Search plugins" data-model="query.value">
31
+ <span data-help-title="Installed plugins" data-help="Click a plugin for its details and settings. Right-click it (or ⋮, or Shift+F10) to switch it on or off, configure it, update it, view its source, export it as a .dcmsplugin file or uninstall it. Built-in plugins are part of the CMS and always on. Licensed plugins cannot be viewed or exported here."></span>
32
+ </div>
33
+ <p class="plx-note" data-bind-hidden="!loading.value">Loading…</p>
34
+ <p class="plx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
35
+ <p class="plx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
36
+ <div class="plx-list plx-installed" data-each="rows.value key=name"></div>
27
37
  </div>
28
38
  </div>
29
- <div id="plugins-grid" class="plugins-grid"></div>
30
- </div>
31
39
 
32
- <div class="tab-panel" data-panel="marketplace">
33
- <div id="marketplace-mount"></div>
40
+ <!-- Browse -->
41
+ <div class="tab-panel">
42
+ <div class="plx-browse"></div>
43
+ </div>
34
44
  </div>
35
45
  </div>
46
+
47
+ <style>
48
+ .plx { display: flex; flex-direction: column; gap: .75rem; }
49
+ .plx [hidden], .plx-form [hidden] { display: none !important; }
50
+ .plx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
51
+ .plx-note.is-bad { color: var(--dm-danger); }
52
+ .plx-alert { margin: 0; display: flex; gap: .5rem; align-items: center; padding: .6rem .8rem; border-radius: var(--dm-radius-md);
53
+ background: color-mix(in srgb, var(--dm-primary) 14%, transparent); color: var(--dm-text); }
54
+ .plx-spinner { width: 14px; height: 14px; border-radius: 50%; border: 2px solid color-mix(in srgb, var(--dm-primary) 30%, transparent);
55
+ border-top-color: var(--dm-primary); animation: plx-spin .8s linear infinite; flex-shrink: 0; }
56
+ @keyframes plx-spin { to { transform: rotate(360deg); } }
57
+ .plx-count { opacity: .65; font-size: var(--dm-text-xs); margin-left: .2rem; }
58
+ .plx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; margin-top: .75rem; }
59
+ .plx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
60
+ .plx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
61
+ .plx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
62
+ .plx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
63
+ .plx-seg button + button { border-left: 1px solid var(--dm-border); }
64
+ .plx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
65
+ .plx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
66
+ .plx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
67
+ .plx-list { display: flex; flex-direction: column; }
68
+ .plx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
69
+ .plx-row:last-child { border-bottom: 0; }
70
+ .plx-row:hover, .plx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
71
+ .plx-row.is-attention { box-shadow: inset 3px 0 0 var(--dm-warning); }
72
+ .plx-icon { display: inline-flex; align-items: center; justify-content: center; width: 2.1rem; height: 2.1rem; flex-shrink: 0;
73
+ border-radius: var(--dm-radius-md); color: var(--plugin-accent, var(--dm-primary));
74
+ background: color-mix(in srgb, var(--plugin-accent, var(--dm-primary)) 14%, transparent); }
75
+ .plx-row.is-off .plx-icon { color: var(--dm-text-muted); background: color-mix(in srgb, var(--dm-text-muted) 12%, transparent); }
76
+ .plx-row.is-off .plx-main strong { color: var(--dm-text-muted); }
77
+ .plx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
78
+ .plx-main strong, .plx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
79
+ .plx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
80
+ .plx-chips { display: inline-flex; gap: .3rem; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
81
+ .plx-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;
82
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
83
+ .plx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
84
+ .plx-chip.is-info { background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 16%, transparent); color: var(--dm-info, var(--dm-primary)); }
85
+ .plx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
86
+ .plx-chip.is-danger { background: color-mix(in srgb, var(--dm-danger) 16%, transparent); color: var(--dm-danger); }
87
+ .plx-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; }
88
+ .plx-kebab:hover { color: var(--dm-primary); }
89
+ .plx-act { flex-shrink: 0; }
90
+ /* Slideovers */
91
+ .plx-form { display: flex; flex-direction: column; gap: .85rem; }
92
+ .plx-form .form-label { display: flex; align-items: center; gap: .3rem; }
93
+ .plx-facts { display: grid; grid-template-columns: max-content 1fr; gap: .3rem .9rem; margin: 0; font-size: var(--dm-text-sm); }
94
+ .plx-facts dt { color: var(--dm-text-muted); }
95
+ .plx-facts dd { margin: 0; }
96
+ .plx-desc { margin: 0; }
97
+ .plx-sub { margin: .25rem 0 0; font-size: var(--dm-text-sm); font-weight: 600; }
98
+ .plx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
99
+ .plx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
100
+ .plx-setting { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
101
+ .plx-setting .form-label { margin: 0; }
102
+ .plx-json { font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm); }
103
+ .plx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); flex-wrap: wrap; }
104
+ .plx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
105
+ .plx-spacer { flex: 1; }
106
+ @media (max-width: 640px) { .plx-main small { white-space: normal; } .plx-chips { display: none; } }
107
+ </style>
@@ -1,10 +1,81 @@
1
- <div class="view-header">
2
- <h1><span data-icon="shield"></span> Roles &amp; Permissions</h1>
3
- <button id="btn-add-role" class="btn btn-primary"><span data-icon="plus"></span> Add Role</button>
4
- </div>
1
+ <!-- Roles. Bound with M.applyBindings - see admin/js/views/roles.js, which also inserts the row template
2
+ (the router renders a templateUrl through _.render, eating any {{…}}). No page heading: the Tool
3
+ banner is the heading, and New role is a banner action. -->
4
+ <div class="rlx">
5
+ <p class="rlx-note">Who can do what. A user holds one role (plus any extra ones); a role's level decides who
6
+ outranks whom, and its permissions what its holders may use.
7
+ <span data-help-title="Roles" data-help="Level 0 is the root role: it holds every permission, plugins included, and cannot be deleted. A lower number outranks a higher one - an admin can only manage users below their own level. Plugins bring roles of their own; those go when the plugin is uninstalled. A role with an admin scope confines its holders to the admin screens it names."></span></p>
8
+
9
+ <p class="rlx-scope" data-bind-hidden="!projectSlug">Showing only roles in the <strong data-bind-text="projectSlug"></strong> project.
10
+ <a href="#/roles">Show all roles</a></p>
5
11
 
6
- <div class="card">
7
- <div class="card-body">
8
- <div id="roles-table"></div>
12
+ <p class="rlx-alert" data-bind-hidden="!orphanText.value">
13
+ <span data-icon="alert-triangle" data-icon-size="16"></span>
14
+ <span data-bind-text="orphanText.value"></span>
15
+ </p>
16
+
17
+ <div class="rlx-card">
18
+ <div class="rlx-bar">
19
+ <div class="rlx-seg" role="group" aria-label="Show">
20
+ <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>
21
+ <button type="button" data-on-click="showBuiltIn" data-bind-class="filter.value === 'built-in' && 'is-on'">Built in <b data-bind-text="counts.value['built-in']"></b></button>
22
+ <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>
23
+ <button type="button" data-on-click="showPlugins" data-bind-class="filter.value === 'plugins' && 'is-on'" data-bind-hidden="!counts.value.plugins">Plugins <b data-bind-text="counts.value.plugins"></b></button>
24
+ </div>
25
+ <input type="search" class="form-input rlx-search" placeholder="Search roles…" aria-label="Search roles" data-model="query.value">
26
+ <span data-help-title="The list" data-help="Most senior first. Click a role to set its permissions. Right-click it (or ⋮, or Shift+F10) to duplicate it, copy its name or delete it. Built-in roles and plugin roles cannot be deleted here; a role nobody holds says so."></span>
27
+ </div>
28
+ <p class="rlx-note" data-bind-hidden="!loading.value">Loading…</p>
29
+ <p class="rlx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
30
+ <p class="rlx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
31
+ <div class="rlx-list" data-each="rows.value key=id"></div>
9
32
  </div>
10
33
  </div>
34
+
35
+ <style>
36
+ .rlx { display: flex; flex-direction: column; gap: .75rem; }
37
+ .rlx [hidden], .rlx-form [hidden] { display: none !important; }
38
+ .rlx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
39
+ .rlx-note.is-bad { color: var(--dm-danger); }
40
+ .rlx-scope { margin: 0; padding: .5rem .75rem; border-radius: var(--dm-radius-md); font-size: var(--dm-text-sm);
41
+ background: color-mix(in srgb, var(--dm-primary) 10%, transparent); color: var(--dm-text); }
42
+ .rlx-alert { margin: 0; display: flex; gap: .5rem; align-items: center; padding: .6rem .8rem; border-radius: var(--dm-radius-md);
43
+ background: color-mix(in srgb, var(--dm-danger) 14%, transparent); color: var(--dm-text); }
44
+ .rlx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
45
+ .rlx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
46
+ .rlx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
47
+ .rlx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
48
+ .rlx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
49
+ .rlx-seg button + button { border-left: 1px solid var(--dm-border); }
50
+ .rlx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
51
+ .rlx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
52
+ .rlx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
53
+ .rlx-list { display: flex; flex-direction: column; }
54
+ .rlx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
55
+ .rlx-row:last-child { border-bottom: 0; }
56
+ .rlx-row:hover, .rlx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
57
+ .rlx-badge { flex-shrink: 0; min-width: 6.5rem; }
58
+ .rlx-badge .badge { white-space: nowrap; }
59
+ .rlx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
60
+ .rlx-main strong, .rlx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
61
+ .rlx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
62
+ .rlx-chips { display: inline-flex; gap: .3rem; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
63
+ .rlx-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;
64
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
65
+ .rlx-chip.is-info { background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 15%, transparent); color: var(--dm-info, var(--dm-primary)); }
66
+ .rlx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
67
+ .rlx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
68
+ .rlx-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; }
69
+ .rlx-kebab:hover { color: var(--dm-primary); }
70
+ /* New role slideover */
71
+ .rlx-form { display: flex; flex-direction: column; gap: .85rem; }
72
+ .rlx-form .form-label { display: flex; align-items: center; gap: .3rem; }
73
+ .rlx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
74
+ .rlx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
75
+ .rlx-preview { display: flex; align-items: center; gap: .5rem; }
76
+ .rlx-preview .badge { white-space: nowrap; flex-shrink: 0; }
77
+ .rlx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
78
+ .rlx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
79
+ .rlx-spacer { flex: 1; }
80
+ @media (max-width: 640px) { .rlx-chips { display: none; } .rlx-badge { min-width: 0; } }
81
+ </style>