domma-cms 0.76.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 (88) hide show
  1. package/CLAUDE.md +1 -1
  2. package/admin/js/api.js +1 -1
  3. package/admin/js/app.js +2 -2
  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/block-editor-arrange.js +2 -0
  7. package/admin/js/lib/collection-editor-arrange.js +3 -0
  8. package/admin/js/lib/collection-entries-arrange.js +1 -0
  9. package/admin/js/lib/component-editor-arrange.js +27 -0
  10. package/admin/js/lib/context-menu-editor-arrange.js +1 -0
  11. package/admin/js/lib/dashboard-arrange.js +1 -0
  12. package/admin/js/lib/form-editor-arrange.js +1 -0
  13. package/admin/js/lib/form-submissions-arrange.js +2 -0
  14. package/admin/js/lib/menu-editor-arrange.js +2 -0
  15. package/admin/js/lib/page-editor-arrange.js +1 -0
  16. package/admin/js/lib/plugin-code-arrange.js +1 -0
  17. package/admin/js/lib/project-page-arrange.js +1 -0
  18. package/admin/js/lib/role-editor-arrange.js +3 -0
  19. package/admin/js/lib/user-editor-arrange.js +1 -0
  20. package/admin/js/lib/view-editor-arrange.js +14 -0
  21. package/admin/js/templates/action-editor.html +223 -147
  22. package/admin/js/templates/api-endpoint-editor.html +172 -117
  23. package/admin/js/templates/block-editor.html +194 -203
  24. package/admin/js/templates/collection-editor.html +191 -113
  25. package/admin/js/templates/collection-entries.html +99 -36
  26. package/admin/js/templates/component-editor.html +128 -126
  27. package/admin/js/templates/context-menu-editor.html +208 -148
  28. package/admin/js/templates/dashboard.html +184 -22
  29. package/admin/js/templates/form-editor.html +41 -26
  30. package/admin/js/templates/form-submissions.html +78 -26
  31. package/admin/js/templates/menu-editor.html +57 -17
  32. package/admin/js/templates/menu-locations.html +54 -11
  33. package/admin/js/templates/page-editor.html +47 -26
  34. package/admin/js/templates/plugin-code.html +91 -73
  35. package/admin/js/templates/project-detail.html +83 -145
  36. package/admin/js/templates/project-settings.html +88 -53
  37. package/admin/js/templates/role-editor.html +158 -83
  38. package/admin/js/templates/user-editor.html +150 -14
  39. package/admin/js/templates/view-editor.html +203 -180
  40. package/admin/js/templates/view-preview.html +45 -45
  41. package/admin/js/views/action-editor.js +7 -1
  42. package/admin/js/views/api-endpoint-editor.js +2 -3
  43. package/admin/js/views/block-editor.js +14 -8
  44. package/admin/js/views/collection-editor.js +64 -5
  45. package/admin/js/views/collection-entries.js +50 -1
  46. package/admin/js/views/component-editor.js +2 -28
  47. package/admin/js/views/context-menu-editor.js +54 -55
  48. package/admin/js/views/dashboard.js +7 -1
  49. package/admin/js/views/form-editor.js +7 -7
  50. package/admin/js/views/form-submissions.js +11 -1
  51. package/admin/js/views/index.js +1 -1
  52. package/admin/js/views/login.js +2 -2
  53. package/admin/js/views/menu-editor.js +21 -43
  54. package/admin/js/views/menu-locations.js +16 -1
  55. package/admin/js/views/page-editor.js +26 -26
  56. package/admin/js/views/plugin-code.js +23 -4
  57. package/admin/js/views/project-detail.js +9 -1
  58. package/admin/js/views/project-settings.js +1 -1
  59. package/admin/js/views/role-editor.js +11 -3
  60. package/admin/js/views/user-editor.js +1 -1
  61. package/admin/js/views/view-editor.js +1 -1
  62. package/admin/js/views/view-preview.js +1 -1
  63. package/package.json +1 -1
  64. package/server/routes/api/dashboard.js +32 -4
  65. package/server/routes/api/forms.js +104 -13
  66. package/server/routes/api/users.js +12 -2
  67. package/server/routes/api/views.js +55 -1
  68. package/server/services/components.js +9 -0
  69. package/server/services/users.js +2 -2
  70. package/server/services/views.js +14 -0
  71. package/admin/js/templates/dashboard/activity-feed.html +0 -3
  72. package/admin/js/templates/dashboard/cache.html +0 -32
  73. package/admin/js/templates/dashboard/health-detail.html +0 -2
  74. package/admin/js/templates/dashboard/journeys.html +0 -17
  75. package/admin/js/templates/dashboard/kpi-strip.html +0 -34
  76. package/admin/js/templates/dashboard/spike-feed.html +0 -3
  77. package/admin/js/templates/dashboard/top-pages.html +0 -3
  78. package/admin/js/templates/dashboard/traffic-chart.html +0 -3
  79. package/admin/js/views/block-editor-enhance.js +0 -1
  80. package/admin/js/views/dashboard/lib/escape.js +0 -1
  81. package/admin/js/views/dashboard/widgets/activity-feed.js +0 -1
  82. package/admin/js/views/dashboard/widgets/cache.js +0 -1
  83. package/admin/js/views/dashboard/widgets/health-detail.js +0 -1
  84. package/admin/js/views/dashboard/widgets/journeys.js +0 -1
  85. package/admin/js/views/dashboard/widgets/kpi-strip.js +0 -1
  86. package/admin/js/views/dashboard/widgets/spike-feed.js +0 -6
  87. package/admin/js/views/dashboard/widgets/top-pages.js +0 -1
  88. package/admin/js/views/dashboard/widgets/traffic-chart.js +0 -1
@@ -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,19 +1,155 @@
1
- <div class="view-header">
2
- <h1><span data-icon="user"></span> <span id="editor-title">New User</span></h1>
3
- <div class="view-header-actions">
4
- <button id="cancel-btn" class="btn btn-ghost">Cancel</button>
5
- </div>
6
- </div>
1
+ <!-- User editor (#/users/new, #/users/edit/:id). Bound with M.applyBindings - see
2
+ admin/js/views/user-editor.js, which also draws the profile fields from the user-profiles schema and inserts
3
+ the role / project chip templates (the router renders a templateUrl through _.render, eating any {{…}}).
4
+ No page heading: the Tool banner ("Users › <name>") is the heading; Back and Save are banner actions. -->
5
+ <div class="uex">
6
+ <p class="uex-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
7
+ <p class="uex-note" data-bind-hidden="!loading.value">Loading…</p>
8
+
9
+ <p class="uex-alert" data-bind-hidden="loading.value || !lockedReason.value">
10
+ <span data-icon="lock" data-icon-size="15"></span>
11
+ <span data-bind-text="lockedReason.value"></span>
12
+ </p>
7
13
 
8
- <div class="card">
9
- <div class="card-body">
10
- <div id="user-form-container"></div>
14
+ <div class="uex-dirty" data-bind-hidden="!dirty.value">
15
+ <span data-icon="edit" data-icon-size="14"></span>
16
+ <span data-bind-text="dirtyText">You have unsaved changes.</span>
17
+ <span class="uex-spacer"></span>
18
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="discard">Discard</button>
19
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannotSave.value">
20
+ <span data-icon="check" data-icon-size="13"></span> <span data-bind-text="saveText">Save</span></button>
11
21
  </div>
12
- </div>
13
22
 
14
- <div class="card mt-4" id="profile-card" style="display:none;">
15
- <div class="card-header"><h2><span data-icon="user"></span> Profile</h2></div>
16
- <div class="card-body">
17
- <div id="profile-form-container"></div>
23
+ <div class="uex-card uex-who" data-bind-hidden="loading.value || loadError.value">
24
+ <span class="uex-avatar" aria-hidden="true" data-bind-text="avatar.value"></span>
25
+ <span class="uex-who-main">
26
+ <strong data-bind-text="shownName.value"></strong>
27
+ <small data-bind-text="shownEmail.value"></small>
28
+ <small class="uex-facts" data-bind-text="factsText.value"></small>
29
+ </span>
30
+ <span class="uex-chips">
31
+ <span class="uex-chip is-primary" data-bind-hidden="!isSelf.value">You</span>
32
+ <span class="uex-chip is-ok" data-bind-text="roleText.value"></span>
33
+ <span class="uex-chip is-warn" data-bind-hidden="isActive.value">Inactive</span>
34
+ </span>
18
35
  </div>
36
+
37
+ <fieldset class="uex-fields" data-bind-disabled="locked.value || saving.value" data-bind-hidden="loading.value || loadError.value">
38
+ <div class="uex-card">
39
+ <div class="uex-card-title">Account
40
+ <span data-help="The name shown in the admin and on anything they author. The email is what they sign in with, and where password resets go - no two accounts may share one."></span></div>
41
+ <div class="uex-grid">
42
+ <div>
43
+ <label class="form-label" for="uex-name">Full name <span class="text-danger">*</span></label>
44
+ <input id="uex-name" type="text" class="form-input" maxlength="100" autocomplete="off" placeholder="Jane Smith" data-model="name.value">
45
+ <small class="uex-err" data-bind-hidden="!err.value.name" data-bind-text="err.value.name"></small>
46
+ </div>
47
+ <div>
48
+ <label class="form-label" for="uex-email">Email <span class="text-danger">*</span></label>
49
+ <input id="uex-email" type="email" class="form-input" maxlength="200" autocomplete="off" placeholder="jane@example.com" data-model="email.value">
50
+ <small class="uex-err" data-bind-hidden="!emailErr.value" data-bind-text="emailErr.value"></small>
51
+ </div>
52
+ <div>
53
+ <label class="form-label" for="uex-pw"><span data-bind-text="pwLabel.value">Password</span>
54
+ <span data-help-title="Password" data-help="At least 8 characters. When editing, leave it empty to keep the current password. It is never kept as a draft - if you leave the screen, it is typed again."></span></label>
55
+ <input id="uex-pw" type="password" data-bind-type="showPw.value ? 'text' : 'password'" class="form-input" autocomplete="new-password"
56
+ data-bind-placeholder="pwPlaceholder.value" data-model="password.value">
57
+ <small class="uex-err" data-bind-hidden="!err.value.password" data-bind-text="err.value.password"></small>
58
+ <label class="uex-show"><input type="checkbox" data-model="showPw.value"> Show what I type</label>
59
+ </div>
60
+ <div class="uex-setting">
61
+ <label class="form-label" for="uex-active">Active account
62
+ <span data-help="An inactive account cannot sign in. Nothing it made is removed - switch it back on at any time. You cannot switch off your own account."></span></label>
63
+ <input id="uex-active" type="checkbox" class="form-switch" data-model="isActive.value" data-bind-disabled="isSelf.value"
64
+ data-bind-title="isSelf.value ? 'You cannot switch off your own account' : ''">
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ <div class="uex-card">
70
+ <div class="uex-card-title">Roles
71
+ <span data-help-title="Roles" data-help="The primary role is the badge shown beside their name, and what seniority is compared on. Additional roles add their permissions on top - someone can be, say, a recruiter AND a content editor; the most senior role they hold decides who may manage them. You can only give roles below your own; a role they already hold can always be kept."></span></div>
72
+ <div class="uex-grid">
73
+ <div>
74
+ <label class="form-label" for="uex-role">Primary role <span class="text-danger">*</span></label>
75
+ <select id="uex-role" class="form-input uex-role-select" data-model="role.value"></select>
76
+ <small class="uex-err" data-bind-hidden="!err.value.role" data-bind-text="err.value.role"></small>
77
+ </div>
78
+ <div class="uex-wide">
79
+ <span class="form-label">Additional roles</span>
80
+ <div class="uex-pick uex-roles" data-each="extraChips.value key=name"></div>
81
+ <small class="uex-hint" data-bind-hidden="extraChips.value.length">No other roles to add.</small>
82
+ <small class="uex-err" data-bind-hidden="!err.value.additionalRoles" data-bind-text="err.value.additionalRoles"></small>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ <div class="uex-card" data-bind-hidden="!hasProjects.value">
88
+ <div class="uex-card-title">Projects
89
+ <span data-help-title="Projects" data-help="Owned by: where this user is filed in the sidebar's Projects section - administrative only. Access: which projects' pages, collections and forms they can see and edit - leave it all unticked for no restriction. Core is always visible."></span></div>
90
+ <div class="uex-grid">
91
+ <div>
92
+ <label class="form-label" for="uex-owner">Owned by project</label>
93
+ <select id="uex-owner" name="ownedByProject" class="form-input uex-owner-select" data-model="ownedByProject.value"></select>
94
+ </div>
95
+ <div class="uex-wide">
96
+ <span class="form-label">Project access</span>
97
+ <div class="uex-pick uex-projects" data-each="projectChips.value key=slug"></div>
98
+ <small class="uex-hint" data-bind-text="accessHint.value"></small>
99
+ </div>
100
+ </div>
101
+ </div>
102
+
103
+ <div class="uex-card" data-bind-hidden="!hasProfile.value">
104
+ <div class="uex-card-title">Profile
105
+ <span data-help="Extra details about this user. Which ones are asked for is set in the User Profiles collection."></span></div>
106
+ <div class="uex-grid uex-profile"></div>
107
+ </div>
108
+ </fieldset>
19
109
  </div>
110
+
111
+ <style>
112
+ .uex { display: flex; flex-direction: column; gap: .75rem; max-width: 60rem; }
113
+ .uex [hidden] { display: none !important; }
114
+ .uex-fields { border: 0; margin: 0; padding: 0; min-width: 0; display: flex; flex-direction: column; gap: .75rem; }
115
+ .uex-fields:disabled { opacity: .75; }
116
+ .uex-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
117
+ .uex-note.is-bad { color: var(--dm-danger); }
118
+ .uex-alert { margin: 0; display: flex; align-items: center; gap: .5rem; padding: .55rem .8rem; border-radius: var(--dm-radius-md);
119
+ background: color-mix(in srgb, var(--dm-warning) 16%, transparent); color: var(--dm-text); }
120
+ .uex-dirty { display: flex; align-items: center; gap: .5rem; padding: .5rem .8rem; border-radius: var(--dm-radius-md);
121
+ background: color-mix(in srgb, var(--dm-warning) 14%, transparent); color: var(--dm-text); position: sticky; top: 0; z-index: 2; }
122
+ .uex-dirty .btn { display: inline-flex; align-items: center; gap: .3rem; }
123
+ .uex-spacer { flex: 1; }
124
+ .uex-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .8rem .9rem; display: flex; flex-direction: column; gap: .7rem; }
125
+ .uex-card-title { display: flex; align-items: center; gap: .35rem; font-weight: 600; }
126
+ .uex-who { flex-direction: row; align-items: center; gap: .9rem; flex-wrap: wrap; }
127
+ .uex-avatar { display: inline-flex; align-items: center; justify-content: center; width: 3rem; height: 3rem; border-radius: 50%; flex-shrink: 0;
128
+ font-weight: 700; color: var(--dm-primary); background: color-mix(in srgb, var(--dm-primary) 14%, transparent); }
129
+ .uex-who-main { flex: 1; min-width: 12rem; display: flex; flex-direction: column; }
130
+ .uex-who-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
131
+ .uex-facts { margin-top: .15rem; }
132
+ .uex-chips { display: inline-flex; gap: .3rem; flex-wrap: wrap; }
133
+ .uex-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .55rem; border-radius: 999px; white-space: nowrap;
134
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
135
+ .uex-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
136
+ .uex-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
137
+ .uex-chip.is-primary { background: color-mix(in srgb, var(--dm-primary) 16%, transparent); color: var(--dm-primary); }
138
+ .uex-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; }
139
+ .uex-wide { grid-column: 1 / -1; }
140
+ .uex .form-label { display: flex; align-items: center; gap: .3rem; }
141
+ .uex-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
142
+ .uex-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
143
+ .uex-show { display: inline-flex; align-items: center; gap: .4rem; font-size: var(--dm-text-xs); color: var(--dm-text-muted); cursor: pointer; margin-top: .3rem; }
144
+ .uex-setting { display: flex; align-items: center; justify-content: space-between; gap: .75rem; align-self: end; padding-bottom: .4rem; }
145
+ .uex-setting .form-label { margin: 0; }
146
+ .uex-pick { display: flex; flex-wrap: wrap; gap: .35rem; }
147
+ .uex-pick-chip { display: inline-flex; align-items: center; gap: .3rem; border: 1px solid var(--dm-border); border-radius: 999px; padding: .2rem .65rem;
148
+ background: none; color: var(--dm-text); font: inherit; font-size: var(--dm-text-sm); cursor: pointer; }
149
+ .uex-pick-chip:hover:not(:disabled) { border-color: var(--dm-primary); }
150
+ .uex-pick-chip.is-on { background: color-mix(in srgb, var(--dm-primary) 14%, transparent); border-color: var(--dm-primary); color: var(--dm-primary); }
151
+ .uex-pick-chip:disabled { opacity: .5; cursor: not-allowed; }
152
+ .uex-pick-chip small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
153
+ .uex-check { display: inline-flex; align-items: center; gap: .4rem; cursor: pointer; margin-right: .8rem; }
154
+ @media (max-width: 640px) { .uex-grid { grid-template-columns: 1fr; } }
155
+ </style>