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.
- package/CLAUDE.md +1 -1
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/action-editor-arrange.js +1 -0
- package/admin/js/lib/api-endpoint-editor-arrange.js +2 -0
- package/admin/js/lib/api-tokens-arrange.js +3 -0
- package/admin/js/lib/block-editor-arrange.js +2 -0
- package/admin/js/lib/collection-editor-arrange.js +3 -0
- package/admin/js/lib/collection-entries-arrange.js +1 -0
- package/admin/js/lib/component-editor-arrange.js +27 -0
- package/admin/js/lib/context-menu-editor-arrange.js +1 -0
- package/admin/js/lib/dashboard-arrange.js +1 -0
- package/admin/js/lib/effects-arrange.js +3 -0
- package/admin/js/lib/form-editor-arrange.js +1 -0
- package/admin/js/lib/form-submissions-arrange.js +2 -0
- package/admin/js/lib/layouts-arrange.js +1 -0
- package/admin/js/lib/menu-editor-arrange.js +2 -0
- package/admin/js/lib/my-profile-arrange.js +1 -0
- package/admin/js/lib/notifications-arrange.js +1 -0
- package/admin/js/lib/page-editor-arrange.js +1 -0
- package/admin/js/lib/plugin-code-arrange.js +1 -0
- package/admin/js/lib/plugins-arrange.js +2 -0
- package/admin/js/lib/project-page-arrange.js +1 -0
- package/admin/js/lib/role-editor-arrange.js +3 -0
- package/admin/js/lib/roles-arrange.js +1 -0
- package/admin/js/lib/search-settings-arrange.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +2 -2
- package/admin/js/lib/site-settings-arrange.js +1 -0
- package/admin/js/lib/user-editor-arrange.js +1 -0
- package/admin/js/lib/users-arrange.js +1 -0
- package/admin/js/lib/view-editor-arrange.js +14 -0
- package/admin/js/templates/action-editor.html +223 -147
- package/admin/js/templates/api-endpoint-editor.html +172 -117
- package/admin/js/templates/api-tokens.html +83 -10
- package/admin/js/templates/block-editor.html +194 -203
- package/admin/js/templates/collection-editor.html +191 -113
- package/admin/js/templates/collection-entries.html +99 -36
- package/admin/js/templates/component-editor.html +128 -126
- package/admin/js/templates/context-menu-editor.html +208 -148
- package/admin/js/templates/dashboard.html +184 -22
- package/admin/js/templates/effects.html +155 -72
- package/admin/js/templates/form-editor.html +41 -26
- package/admin/js/templates/form-submissions.html +78 -26
- package/admin/js/templates/layouts.html +126 -43
- package/admin/js/templates/menu-editor.html +57 -17
- package/admin/js/templates/menu-locations.html +54 -11
- package/admin/js/templates/my-profile.html +100 -12
- package/admin/js/templates/notifications.html +70 -10
- package/admin/js/templates/page-editor.html +47 -26
- package/admin/js/templates/plugin-code.html +91 -73
- package/admin/js/templates/plugins.html +100 -28
- package/admin/js/templates/project-detail.html +83 -145
- package/admin/js/templates/project-settings.html +88 -53
- package/admin/js/templates/role-editor.html +158 -83
- package/admin/js/templates/roles.html +78 -7
- package/admin/js/templates/search.html +193 -216
- package/admin/js/templates/settings.html +404 -472
- package/admin/js/templates/user-editor.html +150 -14
- package/admin/js/templates/users.html +69 -9
- package/admin/js/templates/view-editor.html +203 -180
- package/admin/js/templates/view-preview.html +45 -45
- package/admin/js/views/action-editor.js +7 -1
- package/admin/js/views/api-endpoint-editor.js +2 -3
- package/admin/js/views/api-tokens.js +62 -8
- package/admin/js/views/block-editor.js +14 -8
- package/admin/js/views/collection-editor.js +64 -5
- package/admin/js/views/collection-entries.js +50 -1
- package/admin/js/views/component-editor.js +2 -28
- package/admin/js/views/context-menu-editor.js +54 -55
- package/admin/js/views/dashboard.js +7 -1
- package/admin/js/views/effects.js +31 -1
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/form-submissions.js +11 -1
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/layouts.js +81 -1
- package/admin/js/views/login.js +2 -2
- package/admin/js/views/menu-editor.js +21 -43
- package/admin/js/views/menu-locations.js +16 -1
- package/admin/js/views/my-profile.js +1 -1
- package/admin/js/views/notifications.js +18 -1
- package/admin/js/views/page-editor.js +26 -26
- package/admin/js/views/plugin-code.js +23 -4
- package/admin/js/views/plugins.js +97 -39
- package/admin/js/views/project-detail.js +9 -1
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/role-editor.js +11 -3
- package/admin/js/views/roles.js +49 -4
- package/admin/js/views/search.js +7 -1
- package/admin/js/views/settings.js +13 -3
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +13 -4
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/view-preview.js +1 -1
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +97 -40
- package/plugins/_lib/admin/mail/contacts.js +4 -1
- package/plugins/_lib/admin/mail/diagnostics-section.js +4 -1
- package/plugins/_lib/admin/mail/folder-tree.js +1 -1
- package/plugins/_lib/admin/mail/image-senders-section.js +69 -90
- package/plugins/_lib/admin/mail/mail.css +1 -1
- package/plugins/_lib/admin/mail/mail.html +1 -1
- package/plugins/_lib/admin/mail/reader-view.js +303 -44
- package/plugins/_lib/admin/mail/when-closed.js +26 -0
- package/plugins/_lib/mail/readRoutes.js +43 -1
- package/plugins/_lib/mail/unreadBadge.js +42 -0
- package/plugins/free-tier.lock.json +5 -5
- package/plugins/mail-reader/CLAUDE.md +1 -0
- package/plugins/mail-reader/plugin.js +3 -1
- package/plugins/mail-reader/plugin.json +5 -4
- package/server/routes/api/api-tokens.js +56 -0
- package/server/routes/api/auth.js +7 -1
- package/server/routes/api/collections.js +13 -1
- package/server/routes/api/dashboard.js +32 -4
- package/server/routes/api/effects.js +68 -13
- package/server/routes/api/forms.js +104 -13
- package/server/routes/api/layouts.js +93 -2
- package/server/routes/api/notifications.js +140 -14
- package/server/routes/api/plugins.js +135 -4
- package/server/routes/api/roles.js +111 -0
- package/server/routes/api/search.js +38 -4
- package/server/routes/api/settings.js +52 -0
- package/server/routes/api/users.js +65 -3
- package/server/routes/api/views.js +55 -1
- package/server/server.js +12 -0
- package/server/services/components.js +9 -0
- package/server/services/plugins.js +7 -0
- package/server/services/roles.js +3 -0
- package/server/services/search.js +58 -0
- package/server/services/users.js +16 -2
- package/server/services/views.js +14 -0
- package/admin/js/templates/dashboard/activity-feed.html +0 -3
- package/admin/js/templates/dashboard/cache.html +0 -32
- package/admin/js/templates/dashboard/health-detail.html +0 -2
- package/admin/js/templates/dashboard/journeys.html +0 -17
- package/admin/js/templates/dashboard/kpi-strip.html +0 -34
- package/admin/js/templates/dashboard/spike-feed.html +0 -3
- package/admin/js/templates/dashboard/top-pages.html +0 -3
- package/admin/js/templates/dashboard/traffic-chart.html +0 -3
- package/admin/js/templates/plugin-marketplace.html +0 -29
- package/admin/js/views/block-editor-enhance.js +0 -1
- package/admin/js/views/dashboard/lib/escape.js +0 -1
- package/admin/js/views/dashboard/widgets/activity-feed.js +0 -1
- package/admin/js/views/dashboard/widgets/cache.js +0 -1
- package/admin/js/views/dashboard/widgets/health-detail.js +0 -1
- package/admin/js/views/dashboard/widgets/journeys.js +0 -1
- package/admin/js/views/dashboard/widgets/kpi-strip.js +0 -1
- package/admin/js/views/dashboard/widgets/spike-feed.js +0 -6
- package/admin/js/views/dashboard/widgets/top-pages.js +0 -1
- package/admin/js/views/dashboard/widgets/traffic-chart.js +0 -1
- package/admin/js/views/plugin-marketplace.js +0 -1
|
@@ -1,67 +1,102 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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="
|
|
10
|
-
|
|
11
|
-
<span
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
21
|
-
<label
|
|
22
|
-
|
|
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="
|
|
25
|
-
<label for="
|
|
26
|
-
<textarea id="
|
|
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
|
|
29
|
-
<label for="
|
|
30
|
-
|
|
31
|
-
<
|
|
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
|
|
34
|
-
<label for="
|
|
35
|
-
|
|
36
|
-
<
|
|
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
|
|
39
|
-
<label for="
|
|
40
|
-
|
|
41
|
-
<
|
|
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="
|
|
44
|
-
<label class="
|
|
45
|
-
<
|
|
46
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
<p>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
<
|
|
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
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
<!--
|
|
94
|
-
<div class="tab-panel"
|
|
95
|
-
<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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
<
|
|
7
|
-
|
|
8
|
-
<
|
|
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>
|