domma-cms 0.75.0 → 0.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CLAUDE.md +1 -1
  2. package/admin/js/api.js +1 -1
  3. package/admin/js/app.js +4 -4
  4. package/admin/js/lib/action-editor-arrange.js +1 -0
  5. package/admin/js/lib/api-endpoint-editor-arrange.js +2 -0
  6. package/admin/js/lib/api-tokens-arrange.js +3 -0
  7. package/admin/js/lib/block-editor-arrange.js +2 -0
  8. package/admin/js/lib/collection-editor-arrange.js +3 -0
  9. package/admin/js/lib/collection-entries-arrange.js +1 -0
  10. package/admin/js/lib/component-editor-arrange.js +27 -0
  11. package/admin/js/lib/context-menu-editor-arrange.js +1 -0
  12. package/admin/js/lib/dashboard-arrange.js +1 -0
  13. package/admin/js/lib/effects-arrange.js +3 -0
  14. package/admin/js/lib/form-editor-arrange.js +1 -0
  15. package/admin/js/lib/form-submissions-arrange.js +2 -0
  16. package/admin/js/lib/layouts-arrange.js +1 -0
  17. package/admin/js/lib/menu-editor-arrange.js +2 -0
  18. package/admin/js/lib/my-profile-arrange.js +1 -0
  19. package/admin/js/lib/notifications-arrange.js +1 -0
  20. package/admin/js/lib/page-editor-arrange.js +1 -0
  21. package/admin/js/lib/plugin-code-arrange.js +1 -0
  22. package/admin/js/lib/plugins-arrange.js +2 -0
  23. package/admin/js/lib/project-page-arrange.js +1 -0
  24. package/admin/js/lib/role-editor-arrange.js +3 -0
  25. package/admin/js/lib/roles-arrange.js +1 -0
  26. package/admin/js/lib/search-settings-arrange.js +1 -0
  27. package/admin/js/lib/sidebar-renderer.js +2 -2
  28. package/admin/js/lib/site-settings-arrange.js +1 -0
  29. package/admin/js/lib/user-editor-arrange.js +1 -0
  30. package/admin/js/lib/users-arrange.js +1 -0
  31. package/admin/js/lib/view-editor-arrange.js +14 -0
  32. package/admin/js/templates/action-editor.html +223 -147
  33. package/admin/js/templates/api-endpoint-editor.html +172 -117
  34. package/admin/js/templates/api-tokens.html +83 -10
  35. package/admin/js/templates/block-editor.html +194 -203
  36. package/admin/js/templates/collection-editor.html +191 -113
  37. package/admin/js/templates/collection-entries.html +99 -36
  38. package/admin/js/templates/component-editor.html +128 -126
  39. package/admin/js/templates/context-menu-editor.html +208 -148
  40. package/admin/js/templates/dashboard.html +184 -22
  41. package/admin/js/templates/effects.html +155 -72
  42. package/admin/js/templates/form-editor.html +41 -26
  43. package/admin/js/templates/form-submissions.html +78 -26
  44. package/admin/js/templates/layouts.html +126 -43
  45. package/admin/js/templates/menu-editor.html +57 -17
  46. package/admin/js/templates/menu-locations.html +54 -11
  47. package/admin/js/templates/my-profile.html +100 -12
  48. package/admin/js/templates/notifications.html +70 -10
  49. package/admin/js/templates/page-editor.html +47 -26
  50. package/admin/js/templates/plugin-code.html +91 -73
  51. package/admin/js/templates/plugins.html +100 -28
  52. package/admin/js/templates/project-detail.html +83 -145
  53. package/admin/js/templates/project-settings.html +88 -53
  54. package/admin/js/templates/role-editor.html +158 -83
  55. package/admin/js/templates/roles.html +78 -7
  56. package/admin/js/templates/search.html +193 -216
  57. package/admin/js/templates/settings.html +404 -472
  58. package/admin/js/templates/user-editor.html +150 -14
  59. package/admin/js/templates/users.html +69 -9
  60. package/admin/js/templates/view-editor.html +203 -180
  61. package/admin/js/templates/view-preview.html +45 -45
  62. package/admin/js/views/action-editor.js +7 -1
  63. package/admin/js/views/api-endpoint-editor.js +2 -3
  64. package/admin/js/views/api-tokens.js +62 -8
  65. package/admin/js/views/block-editor.js +14 -8
  66. package/admin/js/views/collection-editor.js +64 -5
  67. package/admin/js/views/collection-entries.js +50 -1
  68. package/admin/js/views/component-editor.js +2 -28
  69. package/admin/js/views/context-menu-editor.js +54 -55
  70. package/admin/js/views/dashboard.js +7 -1
  71. package/admin/js/views/effects.js +31 -1
  72. package/admin/js/views/form-editor.js +7 -7
  73. package/admin/js/views/form-submissions.js +11 -1
  74. package/admin/js/views/index.js +1 -1
  75. package/admin/js/views/layouts.js +81 -1
  76. package/admin/js/views/login.js +2 -2
  77. package/admin/js/views/menu-editor.js +21 -43
  78. package/admin/js/views/menu-locations.js +16 -1
  79. package/admin/js/views/my-profile.js +1 -1
  80. package/admin/js/views/notifications.js +18 -1
  81. package/admin/js/views/page-editor.js +26 -26
  82. package/admin/js/views/plugin-code.js +23 -4
  83. package/admin/js/views/plugins.js +97 -39
  84. package/admin/js/views/project-detail.js +9 -1
  85. package/admin/js/views/project-settings.js +1 -1
  86. package/admin/js/views/role-editor.js +11 -3
  87. package/admin/js/views/roles.js +49 -4
  88. package/admin/js/views/search.js +7 -1
  89. package/admin/js/views/settings.js +13 -3
  90. package/admin/js/views/user-editor.js +1 -1
  91. package/admin/js/views/users.js +13 -4
  92. package/admin/js/views/view-editor.js +1 -1
  93. package/admin/js/views/view-preview.js +1 -1
  94. package/package.json +1 -1
  95. package/plugins/_lib/admin/mail/compose-window.js +97 -40
  96. package/plugins/_lib/admin/mail/contacts.js +4 -1
  97. package/plugins/_lib/admin/mail/diagnostics-section.js +4 -1
  98. package/plugins/_lib/admin/mail/folder-tree.js +1 -1
  99. package/plugins/_lib/admin/mail/image-senders-section.js +69 -90
  100. package/plugins/_lib/admin/mail/mail.css +1 -1
  101. package/plugins/_lib/admin/mail/mail.html +1 -1
  102. package/plugins/_lib/admin/mail/reader-view.js +303 -44
  103. package/plugins/_lib/admin/mail/when-closed.js +26 -0
  104. package/plugins/_lib/mail/readRoutes.js +43 -1
  105. package/plugins/_lib/mail/unreadBadge.js +42 -0
  106. package/plugins/free-tier.lock.json +5 -5
  107. package/plugins/mail-reader/CLAUDE.md +1 -0
  108. package/plugins/mail-reader/plugin.js +3 -1
  109. package/plugins/mail-reader/plugin.json +5 -4
  110. package/server/routes/api/api-tokens.js +56 -0
  111. package/server/routes/api/auth.js +7 -1
  112. package/server/routes/api/collections.js +13 -1
  113. package/server/routes/api/dashboard.js +32 -4
  114. package/server/routes/api/effects.js +68 -13
  115. package/server/routes/api/forms.js +104 -13
  116. package/server/routes/api/layouts.js +93 -2
  117. package/server/routes/api/notifications.js +140 -14
  118. package/server/routes/api/plugins.js +135 -4
  119. package/server/routes/api/roles.js +111 -0
  120. package/server/routes/api/search.js +38 -4
  121. package/server/routes/api/settings.js +52 -0
  122. package/server/routes/api/users.js +65 -3
  123. package/server/routes/api/views.js +55 -1
  124. package/server/server.js +12 -0
  125. package/server/services/components.js +9 -0
  126. package/server/services/plugins.js +7 -0
  127. package/server/services/roles.js +3 -0
  128. package/server/services/search.js +58 -0
  129. package/server/services/users.js +16 -2
  130. package/server/services/views.js +14 -0
  131. package/admin/js/templates/dashboard/activity-feed.html +0 -3
  132. package/admin/js/templates/dashboard/cache.html +0 -32
  133. package/admin/js/templates/dashboard/health-detail.html +0 -2
  134. package/admin/js/templates/dashboard/journeys.html +0 -17
  135. package/admin/js/templates/dashboard/kpi-strip.html +0 -34
  136. package/admin/js/templates/dashboard/spike-feed.html +0 -3
  137. package/admin/js/templates/dashboard/top-pages.html +0 -3
  138. package/admin/js/templates/dashboard/traffic-chart.html +0 -3
  139. package/admin/js/templates/plugin-marketplace.html +0 -29
  140. package/admin/js/views/block-editor-enhance.js +0 -1
  141. package/admin/js/views/dashboard/lib/escape.js +0 -1
  142. package/admin/js/views/dashboard/widgets/activity-feed.js +0 -1
  143. package/admin/js/views/dashboard/widgets/cache.js +0 -1
  144. package/admin/js/views/dashboard/widgets/health-detail.js +0 -1
  145. package/admin/js/views/dashboard/widgets/journeys.js +0 -1
  146. package/admin/js/views/dashboard/widgets/kpi-strip.js +0 -1
  147. package/admin/js/views/dashboard/widgets/spike-feed.js +0 -6
  148. package/admin/js/views/dashboard/widgets/top-pages.js +0 -1
  149. package/admin/js/views/dashboard/widgets/traffic-chart.js +0 -1
  150. package/admin/js/views/plugin-marketplace.js +0 -1
@@ -1,67 +1,102 @@
1
- <div class="page-header">
2
- <h2><span id="ps-icon" data-icon="settings"></span> <span id="ps-title">Project settings</span></h2>
3
- <div class="header-actions">
4
- <a class="btn btn-secondary" id="ps-back"><span data-icon="arrow-left"></span> Back to project</a>
5
- <button class="btn btn-primary" id="ps-save"><span data-icon="check"></span> Save</button>
6
- </div>
7
- </div>
1
+ <!-- A project's settings. Bound with M.applyBindings - see admin/js/views/project-settings.js.
2
+ No page heading: the Tool banner is the heading ("Projects › <name>"), with Save, Overview and All projects. -->
3
+ <div class="psx">
4
+ <p class="psx-alert is-bad" data-bind-hidden="!errorText.value" data-bind-text="errorText.value"></p>
8
5
 
9
- <div class="card">
10
- <div class="card-header">
11
- <span data-icon="info"></span> Project details
6
+ <div class="psx-unsaved" data-bind-hidden="!dirty.value">
7
+ <span><span data-icon="edit" data-icon-size="14"></span> You have unsaved changes.</span>
8
+ <span class="psx-spacer"></span>
9
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="discard">Discard</button>
10
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">
11
+ <span data-icon="save" data-icon-size="13"></span> Save</button>
12
12
  </div>
13
- <div class="card-body">
14
- <div class="grid grid-cols-2 dm-pe-grid">
15
- <div class="form-group">
16
- <label for="ps-slug">Slug</label>
17
- <input type="text" id="ps-slug" name="slug" class="form-input" disabled>
18
- <small class="form-hint">Immutable after create</small>
13
+
14
+ <div class="psx-card">
15
+ <div class="psx-card-title">Project details
16
+ <span data-help-title="Project details" data-help="A project groups pages, collections, forms and the rest for the sidebar, and can limit what a user sees. Ctrl+S saves; leaving with unsaved changes asks first and keeps them for your next visit."></span>
17
+ </div>
18
+ <div class="psx-grid">
19
+ <div>
20
+ <label class="form-label" for="psx-name">Name <span class="text-danger">*</span></label>
21
+ <input id="psx-name" type="text" class="form-input" maxlength="80" data-model="name.value">
22
+ <small class="psx-err" data-bind-hidden="!nameErr.value" data-bind-text="nameErr.value"></small>
19
23
  </div>
20
- <div class="form-group">
21
- <label for="ps-name">Name <span class="required">*</span></label>
22
- <input type="text" id="ps-name" name="name" class="form-input" required>
24
+ <div>
25
+ <label class="form-label" for="psx-slug">Slug
26
+ <span data-help="The project's id - in its admin addresses and in every artefact filed under it. It cannot be changed."></span></label>
27
+ <input id="psx-slug" type="text" class="form-input font-mono" disabled>
23
28
  </div>
24
- <div class="form-group col-span-2">
25
- <label for="ps-description">Description</label>
26
- <textarea id="ps-description" name="description" class="form-input" rows="2"></textarea>
29
+ <div class="psx-wide">
30
+ <label class="form-label" for="psx-description">Description</label>
31
+ <textarea id="psx-description" class="form-input" rows="2" maxlength="300" data-model="description.value"></textarea>
27
32
  </div>
28
- <div class="form-group">
29
- <label for="ps-icon">Icon</label>
30
- <div id="ps-icon-mount"></div>
31
- <small class="form-hint">Click <code>⊞</code> to browse Domma icons</small>
33
+ <div>
34
+ <label class="form-label" for="psx-icon">Icon
35
+ <span data-help="Shown beside the project in the sidebar and on its overview. Type a Domma icon name or browse with the grid button."></span></label>
36
+ <div class="psx-icon-mount"></div>
32
37
  </div>
33
- <div class="form-group">
34
- <label for="ps-rootUrl">Root URL</label>
35
- <input type="text" id="ps-rootUrl" name="rootUrl" class="form-input" placeholder="/members">
36
- <small class="form-hint">URL prefix for auto-inheritance (must start with /)</small>
38
+ <div>
39
+ <label class="form-label" for="psx-rootUrl">Root URL
40
+ <span data-help-title="Root URL" data-help="Pages under this address are filed under the project without being tagged one by one - with /members, a page at /members/join belongs here. Leave it empty to file pages by hand. The core project owns /."></span></label>
41
+ <input id="psx-rootUrl" type="text" class="form-input font-mono" placeholder="/members" data-model="rootUrl.value" data-bind-disabled="rootLocked">
42
+ <small class="psx-err" data-bind-hidden="!rootErr.value" data-bind-text="rootErr.value"></small>
43
+ <small class="psx-hint" data-bind-hidden="!rootLocked">Locked - a built-in project.</small>
37
44
  </div>
38
- <div class="form-group">
39
- <label for="ps-sortOrder">Sort order</label>
40
- <input type="number" id="ps-sortOrder" name="sortOrder" class="form-input" value="0">
41
- <small class="form-hint">Lower = higher in the sidebar</small>
45
+ <div>
46
+ <label class="form-label" for="psx-sortOrder">Sort order
47
+ <span data-help="Lower numbers come first, in Manage projects and in the sidebar."></span></label>
48
+ <input id="psx-sortOrder" type="number" class="form-input" data-model="sortOrder.value">
42
49
  </div>
43
- <div class="form-group col-span-2" id="ps-enabled-group">
44
- <label class="dm-checkbox-label" style="display:inline-flex;align-items:center;gap:.5rem;">
45
- <input type="checkbox" id="ps-enabled" name="enabled" checked>
46
- <span>Enabled</span>
47
- </label>
48
- <small class="form-hint">Off = hard kill-switch: public pages 404 and the external API stops responding. Admin editing of its artefacts is unaffected.</small>
50
+ <div class="psx-setting" data-bind-hidden="!canToggle">
51
+ <label class="form-label" for="psx-enabled">Enabled
52
+ <span data-help="Off is a hard switch: the project's public pages 404 and its external API stops answering. Editing it in the admin carries on."></span></label>
53
+ <input id="psx-enabled" type="checkbox" class="form-switch" data-model="enabled.value">
49
54
  </div>
50
55
  </div>
51
56
  </div>
52
- </div>
53
57
 
54
- <div class="card" id="ps-danger-zone" style="border-color:#c33;">
55
- <div class="card-header"><h3>Danger zone</h3></div>
56
- <div class="card-body">
57
- <p>Untagging removes <code>meta.project</code> from every artefact currently tagged with this project. The artefacts remain (untagged); the project record is unchanged.</p>
58
- <p>
59
- <button class="btn btn-warning" id="ps-untag-all"><span data-icon="tag"></span> Untag all artefacts</button>
60
- </p>
61
- <hr>
62
- <p>Deleting a project is only possible when no artefacts are tagged. The server returns an explanatory error otherwise.</p>
63
- <p>
64
- <button class="btn btn-danger" id="ps-delete"><span data-icon="trash"></span> Delete project</button>
65
- </p>
58
+ <div class="psx-card psx-danger" data-bind-hidden="!dangerShown">
59
+ <div class="psx-card-title">Danger zone
60
+ <span data-help-title="Danger zone" data-help="Untagging takes the project tag off everything filed under it: the artefacts stay and belong to Core. A project can only be deleted once nothing is filed under it. Both ask first."></span>
61
+ </div>
62
+ <p class="psx-note" data-bind-text="filedText.value"></p>
63
+ <div class="psx-danger-row" data-bind-hidden="!canUntag">
64
+ <span><strong>Untag everything</strong><small>Everything filed here goes back to Core. The project is kept.</small></span>
65
+ <button type="button" class="btn btn-sm btn-warning" data-on-click="untagAll" data-bind-disabled="noneFiled.value || busy.value">
66
+ <span data-icon="tag" data-icon-size="13"></span> Untag all…</button>
67
+ </div>
68
+ <div class="psx-danger-row" data-bind-hidden="!canDelete">
69
+ <span><strong>Delete this project</strong><small data-bind-text="deleteHint.value"></small></span>
70
+ <button type="button" class="btn btn-sm btn-danger" data-on-click="remove" data-bind-disabled="deleteBlocked.value">
71
+ <span data-icon="trash" data-icon-size="13"></span> Delete…</button>
72
+ </div>
66
73
  </div>
67
74
  </div>
75
+
76
+ <style>
77
+ .psx { display: flex; flex-direction: column; gap: .9rem; }
78
+ .psx [hidden] { display: none !important; }
79
+ .psx-alert { margin: 0; padding: .6rem .8rem; border-radius: var(--dm-radius-md); color: var(--dm-text); background: color-mix(in srgb, var(--dm-danger) 14%, transparent); }
80
+ .psx-unsaved { position: sticky; top: 0; z-index: 5; display: flex; align-items: center; gap: .5rem; padding: .5rem .8rem; border-radius: var(--dm-radius-md);
81
+ border: 1px solid color-mix(in srgb, var(--dm-warning, var(--dm-primary)) 40%, transparent);
82
+ background: color-mix(in srgb, var(--dm-warning, var(--dm-primary)) 14%, var(--dm-surface)); color: var(--dm-text); }
83
+ .psx-unsaved > span:first-child { display: inline-flex; align-items: center; gap: .4rem; }
84
+ .psx-unsaved .btn, .psx-danger-row .btn { display: inline-flex; align-items: center; gap: .3rem; }
85
+ .psx-spacer { flex: 1; }
86
+ .psx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .8rem .95rem; display: flex; flex-direction: column; gap: .75rem; }
87
+ .psx-card-title { display: flex; align-items: center; gap: .35rem; font-weight: 600; }
88
+ .psx-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .85rem; }
89
+ .psx-wide { grid-column: 1 / -1; }
90
+ .psx .form-label { display: flex; align-items: center; gap: .3rem; }
91
+ .psx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
92
+ .psx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
93
+ .psx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
94
+ .psx-setting { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
95
+ .psx-setting .form-label { margin: 0; }
96
+ .psx-danger { border-color: color-mix(in srgb, var(--dm-danger) 45%, var(--dm-border)); }
97
+ .psx-danger .psx-card-title { color: var(--dm-danger); }
98
+ .psx-danger-row { display: flex; align-items: center; gap: .8rem; padding: .55rem 0; border-top: 1px solid var(--dm-border); }
99
+ .psx-danger-row > span { flex: 1; min-width: 0; display: flex; flex-direction: column; }
100
+ .psx-danger-row small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
101
+ @media (max-width: 640px) { .psx-grid { grid-template-columns: 1fr; } .psx-danger-row { flex-wrap: wrap; } }
102
+ </style>
@@ -1,98 +1,173 @@
1
- <div class="view-header">
2
- <h1><span data-icon="shield"></span> <span id="role-title">Edit Role</span></h1>
3
- <div>
4
- <a href="#/roles" class="btn btn-secondary"><span data-icon="arrow-left"></span> Back</a>
5
- <button id="btn-save-role" class="btn btn-primary"><span data-icon="save"></span> Save</button>
1
+ <!-- Role editor. Bound with M.applyBindings - see admin/views/role-editor.js, which also inserts the row
2
+ templates (the router renders a templateUrl through _.render, eating any {{…}}). No page heading: the
3
+ Tool banner is the heading ("Roles › <role>"), with Save and Back to roles in it. -->
4
+ <div class="rex">
5
+ <div class="rex-bar" data-bind-hidden="!dirty.value">
6
+ <span class="rex-chip is-warn">Unsaved</span>
7
+ <span class="rex-note">Changes to this role are not saved yet.</span>
8
+ <span class="rex-spacer"></span>
9
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="discard">Discard</button>
10
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">
11
+ <span data-icon="save" data-icon-size="13"></span> Save</button>
6
12
  </div>
7
- </div>
8
-
9
- <div class="tabs" id="role-tabs">
10
- <div class="tab-list">
11
- <button class="tab-item active">General</button>
12
- <button class="tab-item">Permissions</button>
13
+ <div class="rex-alert is-bad" data-bind-hidden="!risks.value.length">
14
+ <strong>Careful - this is your own role.</strong>
15
+ <ul class="rex-risks" data-each="riskRows.value key=id"></ul>
13
16
  </div>
14
- <div class="tab-content">
15
- <!-- General Tab -->
16
- <div class="tab-panel active" id="panel-general">
17
- <div class="card">
18
- <div class="card-body">
19
- <div class="row">
20
- <div class="col-6">
21
- <div class="mb-3">
22
- <label class="form-label">Name (slug)</label>
23
- <input id="role-name" type="text" class="form-input" placeholder="e.g. moderator">
24
- <p class="form-hint">Lowercase identifier. Cannot be changed for the root admin
25
- role.</p>
26
- </div>
27
- </div>
28
- <div class="col-6">
29
- <div class="mb-3">
30
- <label class="form-label">Display Label</label>
31
- <input id="role-label" type="text" class="form-input" placeholder="e.g. Moderator">
32
- </div>
33
- </div>
17
+ <p class="rex-alert" data-bind-hidden="!isRoot">
18
+ The root role holds every permission - core and plugins - whatever is ticked here, and keeps its name and level.
19
+ </p>
20
+
21
+ <div class="tabs rex-tabs">
22
+ <div class="tab-list">
23
+ <button type="button" class="tab-item active">General <b class="rex-dot" data-bind-hidden="!generalDirty.value" title="Unsaved changes">•</b></button>
24
+ <button type="button" class="tab-item">Permissions <span class="rex-count" data-bind-text="grantedCount.value"></span>
25
+ <b class="rex-dot" data-bind-hidden="!permsDirty.value" title="Unsaved changes">•</b></button>
26
+ <button type="button" class="tab-item">What it can do</button>
27
+ </div>
28
+
29
+ <!-- General -->
30
+ <div class="tab-panel active">
31
+ <div class="rex-card">
32
+ <div class="rex-card-title">Role
33
+ <span class="rex-badge-preview badge" data-bind-class="badgeClass.value" data-bind-text="previewLabel.value"></span>
34
+ <span class="rex-chip" data-bind-hidden="!holdersText.value" data-bind-text="holdersText.value"></span>
35
+ <span class="rex-chip" data-bind-hidden="!pluginText" data-bind-text="pluginText"></span>
36
+ </div>
37
+ <div class="rex-grid">
38
+ <div>
39
+ <label class="form-label" for="rex-label">Label <span class="text-danger">*</span>
40
+ <span data-help="The name people see - on the user list, the role badge and this screen."></span></label>
41
+ <input id="rex-label" type="text" class="form-input" maxlength="60" data-model="label.value">
42
+ <small class="rex-err" data-bind-hidden="!errors.value.label" data-bind-text="errors.value.label"></small>
43
+ </div>
44
+ <div>
45
+ <label class="form-label" for="rex-name">Name
46
+ <span data-help-title="Name" data-help="The role's id: users hold it by this name, and routes and plugins refer to it. Renaming a role leaves the users who hold it on a name that no longer exists until you move them - the screen says how many that is."></span></label>
47
+ <input id="rex-name" type="text" class="form-input font-mono" maxlength="60" data-model="name.value" data-bind-disabled="nameLockedText">
48
+ <small class="rex-hint" data-bind-hidden="!nameLockedText" data-bind-text="nameLockedText"></small>
49
+ <small class="rex-hint is-warn" data-bind-hidden="!renameNote.value" data-bind-text="renameNote.value"></small>
50
+ <small class="rex-err" data-bind-hidden="!errors.value.name" data-bind-text="errors.value.name"></small>
34
51
  </div>
35
- <div class="row">
36
- <div class="col-6">
37
- <div class="mb-3">
38
- <label class="form-label">Level</label>
39
- <input id="role-level" type="number" class="form-input" min="0">
40
- <p class="form-hint">Lower = more privileged. 0 is reserved for the root admin.</p>
41
- </div>
42
- </div>
43
- <div class="col-6">
44
- <div class="mb-3">
45
- <label class="form-label">Badge Colour</label>
46
- <select id="role-badge" class="form-input"></select>
47
- </div>
48
- </div>
52
+ <div>
53
+ <label class="form-label" for="rex-level">Level
54
+ <span data-help-title="Level" data-help="Seniority: lower is more senior. A user can manage only users whose role is strictly less senior than theirs, and a page or menu item visible to a level is visible to every more senior one. 0 is the root role alone."></span></label>
55
+ <input id="rex-level" type="number" class="form-input" min="1" data-model="level.value" data-bind-disabled="isRoot">
56
+ <small class="rex-err" data-bind-hidden="!errors.value.level" data-bind-text="errors.value.level"></small>
49
57
  </div>
50
- <div class="row">
51
- <div class="col-6">
52
- <div class="mb-3">
53
- <label class="form-label">Project</label>
54
- <select id="role-project" class="form-input">
55
- <option value="">- none -</option>
56
- </select>
57
- <p class="form-hint">Tag this role to a project for grouping.</p>
58
- </div>
59
- </div>
58
+ <div>
59
+ <label class="form-label" for="rex-badge">Badge colour</label>
60
+ <select id="rex-badge" class="form-input" data-model="badgeClass.value"></select>
61
+ </div>
62
+ <div>
63
+ <label class="form-label" for="rex-project">Project
64
+ <span data-help="Files the role under a project, for the sidebar and the project's own Roles list. It does not limit what the role can do."></span></label>
65
+ <select id="rex-project" class="form-input" data-model="project.value"><option value="">- none -</option></select>
60
66
  </div>
61
67
  </div>
62
68
  </div>
63
- <div class="card mt-3" id="role-admin-scope-card">
64
- <div class="card-body">
65
- <h3 class="card-title"><span data-icon="lock"></span> Admin area</h3>
66
- <p class="form-hint">Leave both blank to let this role use every admin screen its permissions
67
- allow. Fill in the allowed screens to confine it - customers who should only ever see their
68
- own portal, say. A user is confined only if every role they hold is; My Profile is always
69
- open.</p>
70
- <div class="row">
71
- <div class="col-6">
72
- <div class="mb-3">
73
- <label class="form-label">Allowed screens</label>
74
- <textarea id="role-admin-allow" class="form-input" rows="4"
75
- placeholder="/portal"></textarea>
76
- <p class="form-hint">One admin route per line. A route also allows everything under
77
- it (<code>/portal</code> allows <code>/portal/orders</code>).</p>
78
- </div>
79
- </div>
80
- <div class="col-6">
81
- <div class="mb-3">
82
- <label class="form-label">Home screen</label>
83
- <input id="role-admin-home" type="text" class="form-input" placeholder="/portal">
84
- <p class="form-hint">Where the user lands, and where they are sent from anywhere
85
- else. Defaults to the first allowed screen.</p>
86
- </div>
87
- </div>
69
+
70
+ <div class="rex-card" data-bind-hidden="isRoot">
71
+ <div class="rex-card-title"><span data-icon="lock" data-icon-size="15"></span> Admin area
72
+ <span data-help-title="Admin area" data-help="Leave both blank to let this role use every admin screen its permissions allow. Fill in the allowed screens to confine it - customers who should only ever see their own portal, say. A user is confined only if EVERY role they hold is; My Profile is always open."></span>
73
+ <span class="rex-chip is-warn" data-bind-hidden="!scopeText.value" data-bind-text="scopeText.value"></span></div>
74
+ <div class="rex-grid">
75
+ <div>
76
+ <label class="form-label" for="rex-allow">Allowed screens
77
+ <span data-help="One admin route per line. A route also allows everything under it: /portal allows /portal/orders."></span></label>
78
+ <textarea id="rex-allow" class="form-input font-mono" rows="4" placeholder="/portal" data-model="adminAllow.value"></textarea>
88
79
  </div>
80
+ <div>
81
+ <label class="form-label" for="rex-home">Home screen
82
+ <span data-help="Where holders land after signing in, and where they are sent from anywhere they may not go. Defaults to the first allowed screen."></span></label>
83
+ <input id="rex-home" type="text" class="form-input font-mono" placeholder="/portal" data-model="adminHome.value">
84
+ </div>
85
+ </div>
86
+ <small class="rex-err" data-bind-hidden="!errors.value.admin" data-bind-text="errors.value.admin"></small>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Permissions -->
91
+ <div class="tab-panel">
92
+ <div class="rex-card" data-bind-class="isRoot && 'is-locked'">
93
+ <div class="rex-bar-tools">
94
+ <input type="search" class="form-input rex-search" placeholder="Search permissions…" aria-label="Search permissions" data-model="query.value">
95
+ <div class="rex-seg" role="group" aria-label="Show">
96
+ <button type="button" data-on-click="showAll" data-bind-class="show.value === 'all' && 'is-on'">All</button>
97
+ <button type="button" data-on-click="showOn" data-bind-class="show.value === 'on' && 'is-on'">Granted</button>
98
+ <button type="button" data-on-click="showOff" data-bind-class="show.value === 'off' && 'is-on'">Not granted</button>
99
+ </div>
100
+ <span data-help-title="Permissions" data-help="Click an action to grant or take it away; Full turns a whole area on or off. Right-click an area (or ⋮, or Shift+F10) for view only or none; right-click a group heading to grant or clear the whole group. Plugins add their own areas under Plugins."></span>
101
+ <span class="rex-spacer"></span>
102
+ <span class="rex-note" data-bind-text="grantedText.value"></span>
89
103
  </div>
104
+ <p class="rex-note" data-bind-hidden="rows.value.length">Nothing matches.</p>
105
+ <div class="rex-matrix" data-each="rows.value key=id"></div>
90
106
  </div>
91
107
  </div>
92
108
 
93
- <!-- Permissions Tab -->
94
- <div class="tab-panel" id="panel-permissions">
95
- <div id="permissions-container"></div>
109
+ <!-- What it can do -->
110
+ <div class="tab-panel">
111
+ <div class="rex-card">
112
+ <div class="rex-card-title">What <span data-bind-text="previewLabel.value"></span> can do
113
+ <span data-help="Read back from the ticks as they are now - saved or not. Anyone holding this role and another gets both roles' permissions together."></span></div>
114
+ <div class="rex-summary" data-each="summary.value key=id"></div>
115
+ </div>
96
116
  </div>
97
117
  </div>
98
118
  </div>
119
+
120
+ <style>
121
+ .rex { display: flex; flex-direction: column; gap: .75rem; }
122
+ .rex [hidden] { display: none !important; }
123
+ .rex-bar { display: flex; align-items: center; gap: .5rem; padding: .5rem .75rem; border: 1px solid var(--dm-border);
124
+ border-radius: var(--dm-radius-md); background: color-mix(in srgb, var(--dm-warning) 10%, transparent); position: sticky; top: 0; z-index: 5; }
125
+ .rex-bar .btn, .rex-card-title .btn { display: inline-flex; align-items: center; gap: .3rem; }
126
+ .rex-alert { margin: 0; padding: .6rem .8rem; border-radius: var(--dm-radius-md); background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 12%, transparent); color: var(--dm-text); }
127
+ .rex-alert.is-bad { background: color-mix(in srgb, var(--dm-danger) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dm-danger) 35%, transparent); }
128
+ .rex-risks { margin: .35rem 0 0; padding-left: 1.2rem; }
129
+ .rex-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .7rem; margin-top: .75rem; }
130
+ .rex-card-title { display: flex; align-items: center; gap: .45rem; font-weight: 600; flex-wrap: wrap; }
131
+ .rex-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .85rem; }
132
+ .rex-grid .form-label { display: flex; align-items: center; gap: .3rem; }
133
+ .rex-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
134
+ .rex-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
135
+ .rex-hint.is-warn { color: var(--dm-warning); }
136
+ .rex-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
137
+ .rex-spacer { flex: 1; }
138
+ .rex-dot { color: var(--dm-warning); margin-left: .2rem; }
139
+ .rex-count { opacity: .65; font-size: var(--dm-text-xs); margin-left: .2rem; }
140
+ .rex-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;
141
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
142
+ .rex-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
143
+ .rex-badge-preview { font-weight: 600; }
144
+ .rex-bar-tools { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
145
+ .rex-search { flex: 1; min-width: 10rem; max-width: 20rem; }
146
+ .rex-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
147
+ .rex-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
148
+ .rex-seg button + button { border-left: 1px solid var(--dm-border); }
149
+ .rex-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
150
+ .rex-matrix { display: flex; flex-direction: column; }
151
+ .rex-row { display: flex; align-items: center; gap: .6rem; padding: .45rem .35rem; border-bottom: 1px solid var(--dm-border); }
152
+ .rex-row:focus-visible, .rex-row:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
153
+ .rex-row.is-group { background: color-mix(in srgb, var(--dm-text) 5%, transparent); border-top: 1px solid var(--dm-border); margin-top: .5rem; }
154
+ .rex-row.is-group .rex-main strong { text-transform: uppercase; letter-spacing: .04em; font-size: var(--dm-text-xs); }
155
+ .rex-icon { display: inline-flex; width: 1.6rem; justify-content: center; color: var(--dm-text-muted); flex-shrink: 0; }
156
+ .rex-main { min-width: 11rem; flex: 0 1 16rem; display: flex; flex-direction: column; }
157
+ .rex-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
158
+ .rex-acts { display: flex; flex-wrap: wrap; gap: .3rem; flex: 1; }
159
+ .rex-act, .rex-full { border: 1px solid var(--dm-border); background: none; color: var(--dm-text-muted); border-radius: 999px; padding: .15rem .6rem;
160
+ font: inherit; font-size: var(--dm-text-xs); cursor: pointer; }
161
+ .rex-act[aria-pressed="true"] { background: color-mix(in srgb, var(--dm-success) 16%, transparent); border-color: color-mix(in srgb, var(--dm-success) 45%, transparent); color: var(--dm-success); font-weight: 600; }
162
+ .rex-full[data-state="all"] { background: var(--dm-primary); border-color: var(--dm-primary); color: var(--dm-primary-text); }
163
+ .rex-full[data-state="some"] { border-style: dashed; color: var(--dm-text); }
164
+ .rex-act:disabled, .rex-full:disabled, .is-locked .rex-act { cursor: not-allowed; opacity: .6; }
165
+ .rex-state { font-size: var(--dm-text-xs); color: var(--dm-text-muted); min-width: 6rem; text-align: right; }
166
+ .rex-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; }
167
+ .rex-kebab:hover { color: var(--dm-primary); }
168
+ .rex-summary { display: flex; flex-direction: column; }
169
+ .rex-sum { display: grid; grid-template-columns: 9rem 1fr; gap: 1rem; padding: .45rem 0; border-bottom: 1px solid var(--dm-border); }
170
+ .rex-sum:last-child { border-bottom: 0; }
171
+ .rex-sum strong { font-weight: 600; }
172
+ @media (max-width: 720px) { .rex-grid { grid-template-columns: 1fr; } .rex-main { flex-basis: 100%; } .rex-state { display: none; } }
173
+ </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>