domma-cms 0.76.0 → 0.77.1

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,72 +1,87 @@
1
- <div class="page-header">
2
- <h2><span data-icon="code"></span> <span id="ep-title">API Endpoint</span></h2>
3
- <div class="header-actions">
4
- <a class="btn btn-ghost" href="#/api-endpoints">All endpoints</a>
5
- <button class="btn btn-primary" id="ep-save"><span data-icon="save"></span> Save</button>
1
+ <!-- API Builder - endpoint editor. See admin/js/views/api-endpoint-editor.js. No page heading: the Tool banner
2
+ ("API Builder › <endpoint>") is the heading, with All endpoints and Save in it. Parts are bound with
3
+ M.applyBindings - never the Query panel, whose hints show {{params.x}} literally. #endpoint-project and
4
+ #ep-editor-tabs are relied on by app.js (?project= pre-fill) and admin.css (full-width view). -->
5
+ <div class="epx">
6
+ <div class="epx-bar" id="ep-bar" data-bind-hidden="!dirty.value">
7
+ <span class="epx-chip is-warn">Unsaved</span>
8
+ <span class="epx-bar-text" data-bind-text="barText.value"></span>
9
+ <span class="epx-spacer"></span>
10
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="discard">Discard</button>
11
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="saving.value">
12
+ <span data-icon="save" data-icon-size="13"></span> Save</button>
6
13
  </div>
7
- </div>
8
14
 
9
- <div class="ep-editor-grid">
10
- <div>
11
- <div class="tabs" id="ep-editor-tabs">
12
- <div class="tab-list">
13
- <button class="tab-item active">Definition</button>
14
- <button class="tab-item">Query</button>
15
- <button class="tab-item">Response fields</button>
16
- </div>
15
+ <div class="ep-editor-grid">
16
+ <div>
17
+ <div class="tabs" id="ep-editor-tabs">
18
+ <div class="tab-list">
19
+ <button class="tab-item active">Definition <span class="epx-dot" id="ep-def-dot" hidden></span></button>
20
+ <button class="tab-item">Query <span class="epx-dot" id="ep-query-dot" hidden></span></button>
21
+ <button class="tab-item">Response fields</button>
22
+ </div>
17
23
 
18
- <div class="tab-panel active">
19
- <div class="card">
20
- <div class="card-body">
24
+ <div class="tab-panel active" id="ep-def-panel">
25
+ <div class="epx-card">
21
26
  <div class="form-group">
22
- <label class="form-label" for="ep-name">Name</label>
23
- <input type="text" class="form-input" id="ep-name" placeholder="e.g. Fixtures by day">
27
+ <label class="form-label" for="ep-name">Name
28
+ <span data-help="For you and your team - shown in the API Builder list. Callers never see it."></span></label>
29
+ <input type="text" class="form-input" id="ep-name" placeholder="e.g. Fixtures by day" maxlength="120">
30
+ <small class="epx-err" data-bind-hidden="!err.value.name" data-bind-text="err.value.name"></small>
24
31
  </div>
25
32
  <div class="form-group">
26
- <label class="form-label" for="endpoint-project">Project</label>
33
+ <label class="form-label" for="endpoint-project">Project
34
+ <span data-help="The first part of the address, after /api/x/. It is the endpoint's namespace, so it cannot change once saved. The endpoint may only expose collections from this project or Core."></span></label>
27
35
  <select class="form-input" id="endpoint-project"></select>
28
- <p class="text-muted" style="font-size:.8rem;margin:.25rem 0 0;">The first URL segment. Fixed once saved.</p>
36
+ <small class="epx-err" data-bind-hidden="!err.value.project" data-bind-text="err.value.project"></small>
29
37
  </div>
30
38
  <div class="form-group">
31
- <label class="form-label" for="ep-path">Path</label>
32
- <input type="text" class="form-input" id="ep-path" placeholder="/fixtures-day/:date">
33
- <p class="text-muted" style="font-size:.8rem;margin:.25rem 0 0;">Lowercase segments or <code>:param</code> placeholders. Full URL: <code id="ep-url-preview">/api/x/…</code></p>
39
+ <label class="form-label" for="ep-path">Path
40
+ <span data-help-title="Path" data-help="Lowercase letters, numbers and hyphens, one segment at a time, or :name for a value the caller supplies (/fixtures-day/:date). Two endpoints in a project cannot have the same shape - /a/:x and /a/:y clash."></span></label>
41
+ <input type="text" class="form-input font-mono" id="ep-path" placeholder="/fixtures-day/:date">
42
+ <small class="epx-err" data-bind-hidden="!pathErr.value" data-bind-text="pathErr.value"></small>
43
+ <small class="epx-hint">Full address: <code id="ep-url-preview">/api/x/…</code></small>
34
44
  </div>
35
45
  <div class="form-group">
36
- <label class="form-label" for="ep-collection">Collection</label>
46
+ <label class="form-label" for="ep-collection">Collection
47
+ <span data-help="Where the answers come from. Only collections in this project or Core are offered; system collections (users, tokens…) can never be exposed."></span></label>
37
48
  <select class="form-input" id="ep-collection"></select>
49
+ <small class="epx-err" data-bind-hidden="!err.value.collection" data-bind-text="err.value.collection"></small>
38
50
  </div>
39
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem;">
51
+ <div class="epx-grid2">
40
52
  <div class="form-group">
41
- <label class="form-label" for="ep-auth">Access</label>
53
+ <label class="form-label" for="ep-auth">Access
54
+ <span data-help-title="Access" data-help="Public: anyone. Token: only a project API token (System › API Tokens) for this project - never a signed-in session. A role: a signed-in user holding that role or a more senior one."></span></label>
42
55
  <select class="form-input" id="ep-auth"></select>
43
56
  </div>
44
57
  <div class="form-group">
45
- <label class="form-label" for="ep-mode">Mode</label>
58
+ <label class="form-label" for="ep-mode">Mode
59
+ <span data-help="List answers an array of entries. Single answers the first match, or 404 when there is none - for /thing/:id style addresses."></span></label>
46
60
  <select class="form-input" id="ep-mode">
47
61
  <option value="list">List - array of entries</option>
48
62
  <option value="single">Single - first match or 404</option>
49
63
  </select>
50
64
  </div>
51
65
  </div>
52
- <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;margin-top:.5rem;padding-top:.75rem;border-top:1px solid rgba(127,127,127,.18);">
53
- <input type="checkbox" id="ep-enabled" checked> <strong>Enabled</strong>
54
- <span class="text-muted" style="font-size:.85rem;">- disabled endpoints return 404</span>
55
- </label>
66
+ <div class="epx-setting">
67
+ <label class="form-label" for="ep-enabled">Enabled
68
+ <span data-help="Off answers 404 to every caller, as if the endpoint did not exist. The definition is kept."></span></label>
69
+ <input type="checkbox" class="form-switch" id="ep-enabled" checked>
70
+ </div>
56
71
  </div>
57
72
  </div>
58
- </div>
59
73
 
60
- <div class="tab-panel">
61
- <div class="card">
62
- <div class="card-header">
63
- <h2>Query</h2>
64
- <button class="btn btn-sm btn-ghost" id="ep-add-filter"><span data-icon="plus"></span> Add filter</button>
65
- </div>
66
- <div class="card-body">
67
- <div id="ep-filter-rows"></div>
68
- <p class="text-muted" style="font-size:.8rem;margin:.5rem 0 0;">Values may use <code>{{params.name}}</code> (from a <code>:name</code> path segment, required) or <code>{{query.name}}</code> (optional - the filter is skipped when absent).</p>
69
- <div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:.75rem;margin-top:1rem;">
74
+ <div class="tab-panel">
75
+ <div class="epx-card">
76
+ <div class="epx-card-title">Filters
77
+ <span data-help-title="Filters" data-help="Every filter must match. A value can be a literal, {{params.name}} (from a :name segment - required) or {{query.name}} (from ?name= - optional: the filter is dropped when the caller leaves it out). Right-click a filter (or ⋮) to move, duplicate or remove it."></span>
78
+ <span class="epx-spacer"></span>
79
+ <button type="button" class="btn btn-sm btn-ghost" id="ep-add-filter"><span data-icon="plus" data-icon-size="13"></span> Add filter</button>
80
+ </div>
81
+ <div id="ep-filter-rows" class="epx-list"></div>
82
+ <p class="epx-note" id="ep-filter-empty">No filters - every entry matches.</p>
83
+ <small class="epx-err" id="ep-filter-err" hidden></small>
84
+ <div class="epx-grid3">
70
85
  <div class="form-group">
71
86
  <label class="form-label" for="ep-sort">Sort field</label>
72
87
  <select class="form-input" id="ep-sort"></select>
@@ -79,99 +94,139 @@
79
94
  </select>
80
95
  </div>
81
96
  <div class="form-group">
82
- <label class="form-label" for="ep-limit">Limit</label>
97
+ <label class="form-label" for="ep-limit">Limit
98
+ <span data-help="At most this many entries per answer (0 = no limit, up to 500). A caller may ask for fewer with ?limit=, never more."></span></label>
83
99
  <input type="number" class="form-input" id="ep-limit" min="0" max="500" value="50">
100
+ <small class="epx-err" id="ep-limit-err" hidden></small>
84
101
  </div>
85
102
  </div>
86
- <p class="text-muted" style="font-size:.8rem;margin:0;">Limit 0 = no limit.</p>
87
103
  </div>
88
104
  </div>
89
- </div>
90
105
 
91
- <div class="tab-panel">
92
- <div class="card">
93
- <div class="card-header"><h2>Response fields</h2></div>
94
- <div class="card-body">
95
- <p class="text-muted" style="font-size:.85rem;margin:0 0 .5rem;">Tick fields to return only those - none ticked returns everything.</p>
96
- <div id="ep-fields-list" style="display:flex;flex-wrap:wrap;gap:.5rem 1.25rem;"></div>
106
+ <div class="tab-panel">
107
+ <div class="epx-card">
108
+ <div class="epx-card-title">Response fields
109
+ <span data-help-title="Response fields" data-help="Tick the fields callers get back - none ticked returns every field. Anything not ticked cannot be read or filtered on through this endpoint. Right-click the list (or ⋮) to tick all, none, or invert."></span>
110
+ <span class="epx-spacer"></span>
111
+ <button type="button" class="epx-kebab" id="ep-fields-more" aria-label="Field actions" title="Actions">⋮</button>
112
+ </div>
113
+ <div id="ep-fields-list" class="epx-fields" tabindex="0"></div>
114
+ <p class="epx-note" id="ep-fields-note"></p>
97
115
  </div>
98
116
  </div>
99
117
  </div>
100
118
  </div>
101
- </div>
102
119
 
103
- <div class="ep-tryit-col">
104
- <div class="card">
105
- <div class="card-header">
106
- <h2><span data-icon="zap"></span> Try it</h2>
107
- <span class="text-muted" style="font-size:.75rem;">⌘/Ctrl+↵ send · ⌘/Ctrl+S save</span>
108
- </div>
109
- <div class="card-body">
110
- <div id="tryit-dirty-note" class="text-muted" style="display:none;font-size:.85rem;margin-bottom:.75rem;">
111
- <span data-icon="info"></span> Save to test your changes.
112
- </div>
113
- <div id="tryit-params"></div>
114
- <div id="tryit-query-params"></div>
115
- <div class="form-group" id="tryit-token-group" style="display:none;">
116
- <label class="form-label" for="tryit-token">API token</label>
117
- <input type="text" class="form-input" id="tryit-token" placeholder="dcms_…">
118
- </div>
119
- <div class="form-group">
120
- <label class="form-label" for="tryit-query">Extra query string (optional)</label>
121
- <input type="text" class="form-input" id="tryit-query" placeholder="nation=France&status=published">
122
- <p class="text-muted" style="font-size:.8rem;margin:.25rem 0 0;">Applied as extra filters <em>on top of</em> the definition's own filters (which always win). Only fields this endpoint returns can be filtered; operators work too - e.g. <code>title_contains=cup</code>. <code>page</code>, <code>limit</code>, <code>sort</code> and <code>order</code> control pagination/sort instead (<code>limit</code> is capped at the endpoint's own limit); <code>search</code> is reserved.</p>
123
- </div>
124
- <button class="btn btn-primary" id="tryit-send" style="width:100%;"><span data-icon="play"></span> Send</button>
125
- <div id="tryit-result" style="display:none;margin-top:.75rem;">
126
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.4rem;">
127
- <span class="form-label" style="margin:0;">Request</span>
128
- <button class="btn btn-sm btn-ghost" id="tryit-copy-curl" data-tooltip="Copy as curl"><span data-icon="copy"></span> Copy curl</button>
120
+ <div class="ep-tryit-col">
121
+ <div class="epx-card">
122
+ <div class="epx-card-title"><span data-icon="zap" data-icon-size="15"></span> Try it
123
+ <span data-help-title="Try it" data-help="Calls the SAVED endpoint exactly as an outside caller would - no admin sign-in is sent, so what you see is what they get. Ctrl+Enter sends. Unsaved changes are not tested until you save."></span></div>
124
+ <div id="tryit-dirty-note" class="epx-alert" style="display:none;">
125
+ <span>Save to test your changes.</span>
126
+ <button type="button" class="btn btn-sm btn-primary" id="tryit-save-send">Save and send</button>
129
127
  </div>
130
- <pre id="tryit-curl" style="max-height:180px;overflow:auto;font-size:.8rem;padding:.6rem;margin:0 0 .75rem;white-space:pre-wrap;word-break:break-all;"></pre>
131
-
132
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.4rem;">
133
- <span class="form-label" style="margin:0;">Response</span>
134
- <span id="tryit-status" class="badge badge-secondary">-</span>
128
+ <div id="tryit-params"></div>
129
+ <div id="tryit-query-params"></div>
130
+ <div class="form-group" id="tryit-token-group" style="display:none;">
131
+ <label class="form-label" for="tryit-token">API token
132
+ <span data-help="A dcms_… token for this endpoint's project. Used for this request only - never stored, and not kept in the history."></span></label>
133
+ <input type="password" class="form-input font-mono" id="tryit-token" placeholder="dcms_…" autocomplete="new-password">
135
134
  </div>
136
- <div id="tryit-headers-group">
137
- <p class="text-muted" style="font-size:.78rem;margin:0 0 .2rem;text-transform:uppercase;letter-spacing:.03em;">Headers</p>
138
- <pre id="tryit-headers" style="max-height:160px;overflow:auto;font-size:.78rem;padding:.6rem;margin:0;white-space:pre-wrap;word-break:break-all;"></pre>
135
+ <div class="form-group">
136
+ <label class="form-label" for="tryit-query">Extra query string
137
+ <span data-help-title="Extra query string" data-help="Added as filters on top of the definition's own (which always win). Only fields this endpoint returns can be filtered; operators work too - title_contains=cup. page, limit, sort and order page and sort instead (limit is capped at the endpoint's own); search is reserved."></span></label>
138
+ <input type="text" class="form-input font-mono" id="tryit-query" placeholder="nation=France&status=published">
139
+ </div>
140
+ <button type="button" class="btn btn-primary epx-wide" id="tryit-send"><span data-icon="play" data-icon-size="13"></span> Send</button>
141
+ <div id="tryit-result" style="display:none;">
142
+ <div class="epx-subhead"><span class="form-label">Request</span>
143
+ <button type="button" class="btn btn-sm btn-ghost" id="tryit-copy-curl"><span data-icon="copy" data-icon-size="13"></span> Copy curl</button></div>
144
+ <pre id="tryit-curl" class="epx-pre"></pre>
145
+ <div class="epx-subhead"><span class="form-label">Response</span>
146
+ <span id="tryit-status" class="epx-chip">-</span></div>
147
+ <div id="tryit-headers-group">
148
+ <p class="epx-mini">Headers</p>
149
+ <pre id="tryit-headers" class="epx-pre"></pre>
150
+ </div>
139
151
  </div>
140
152
  </div>
141
153
  </div>
142
- </div>
143
- </div>
144
154
 
145
- <div class="ep-results-col">
146
- <div class="card" id="tryit-results-placeholder">
147
- <div class="card-body" style="text-align:center;color:var(--dm-text-muted,#8a94a6);padding:2rem 1rem;">
148
- <div data-icon="zap" style="font-size:1.5rem;opacity:.5;"></div>
149
- <p style="margin:.5rem 0 0;font-size:.9rem;">Send a request to see the response here.</p>
150
- </div>
151
- </div>
152
- <div class="card" id="tryit-response-card" style="display:none;">
153
- <div class="card-header">
154
- <h2><span data-icon="code"></span> Response body</h2>
155
- <div style="display:flex;gap:.35rem;">
156
- <button class="btn btn-sm btn-ghost" id="tryit-json-toggle">Raw</button>
157
- <button class="btn btn-sm btn-ghost" id="tryit-body-copy" data-tooltip="Copy body"><span data-icon="copy"></span></button>
155
+ <div class="ep-results-col">
156
+ <div class="epx-card" id="tryit-results-placeholder">
157
+ <p class="epx-note epx-center"><span data-icon="zap" data-icon-size="20"></span><br>Send a request to see the response here.</p>
158
+ </div>
159
+ <div class="epx-card" id="tryit-response-card" style="display:none;">
160
+ <div class="epx-card-title"><span data-icon="code" data-icon-size="15"></span> Response body
161
+ <span class="epx-spacer"></span>
162
+ <button type="button" class="btn btn-sm btn-ghost" id="tryit-json-toggle">Raw</button>
163
+ <button type="button" class="btn btn-sm btn-ghost" id="tryit-body-copy" title="Copy the body" aria-label="Copy the body"><span data-icon="copy" data-icon-size="13"></span></button>
164
+ </div>
165
+ <div id="tryit-json" class="epx-json"></div>
166
+ <pre id="tryit-body" class="epx-pre epx-tall" style="display:none;"></pre>
158
167
  </div>
159
- </div>
160
- <div class="card-body" style="padding-top:.5rem;">
161
- <div id="tryit-json" style="max-height:460px;overflow:auto;font-family:var(--dm-font-mono,ui-monospace,monospace);font-size:.8rem;line-height:1.5;padding:.6rem;"></div>
162
- <pre id="tryit-body" style="display:none;max-height:460px;overflow:auto;font-size:.8rem;padding:.6rem;margin:0;white-space:pre-wrap;word-break:break-word;"></pre>
163
- </div>
164
- </div>
165
168
 
166
- <div class="card" id="tryit-history-card" style="display:none;">
167
- <div class="card-header">
168
- <h2><span data-icon="history"></span> Test history</h2>
169
- <button class="btn btn-sm btn-ghost" id="tryit-history-clear" data-tooltip="Clear history"><span data-icon="trash"></span></button>
170
- </div>
171
- <div class="card-body" style="padding-top:.5rem;">
172
- <p class="text-muted" style="font-size:.8rem;margin:0 0 .5rem;">Click a request to re-run it.</p>
173
- <div id="tryit-history-list" style="display:flex;flex-direction:column;gap:.35rem;"></div>
169
+ <div class="epx-card" id="tryit-history-card" style="display:none;">
170
+ <div class="epx-card-title"><span data-icon="clock" data-icon-size="15"></span> Test history
171
+ <span data-help="The last requests you sent from this editor, kept in this browser (never a token). Click one to run it again; right-click (or ⋮) to copy its address or remove it."></span>
172
+ <span class="epx-spacer"></span>
173
+ <button type="button" class="btn btn-sm btn-ghost" id="tryit-history-clear" title="Clear the history" aria-label="Clear the history"><span data-icon="trash" data-icon-size="13"></span></button>
174
+ </div>
175
+ <div id="tryit-history-list" class="epx-list"></div>
176
+ </div>
174
177
  </div>
175
178
  </div>
176
- </div>
177
179
  </div>
180
+
181
+ <style>
182
+ .epx { display: flex; flex-direction: column; gap: .75rem; }
183
+ .epx [hidden] { display: none !important; }
184
+ .epx-bar { display: flex; align-items: center; gap: .5rem; padding: .5rem .75rem; border-radius: var(--dm-radius-md);
185
+ background: color-mix(in srgb, var(--dm-warning) 12%, transparent); position: sticky; top: 0; z-index: 5; }
186
+ .epx-bar-text { color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
187
+ .epx-spacer { flex: 1; }
188
+ .epx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column;
189
+ gap: .6rem; margin-bottom: .75rem; background: var(--dm-surface); }
190
+ .epx-card-title { display: flex; align-items: center; gap: .35rem; font-weight: 600; }
191
+ .epx-card-title .btn { display: inline-flex; align-items: center; gap: .3rem; }
192
+ .epx-card .form-group { margin: 0; }
193
+ .epx .form-label { display: flex; align-items: center; gap: .3rem; }
194
+ .epx-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; }
195
+ .epx-grid3 { display: grid; grid-template-columns: 2fr 1fr 1fr; gap: .75rem; margin-top: .25rem; }
196
+ .epx-setting { display: flex; align-items: center; justify-content: space-between; gap: .75rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
197
+ .epx-setting .form-label { margin: 0; }
198
+ .epx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
199
+ .epx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
200
+ .epx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
201
+ .epx-center { text-align: center; padding: 1.5rem 0; }
202
+ .epx-mini { margin: 0 0 .2rem; color: var(--dm-text-muted); font-size: var(--dm-text-xs); text-transform: uppercase; letter-spacing: .03em; }
203
+ .epx-alert { display: flex; align-items: center; gap: .5rem; justify-content: space-between; padding: .45rem .6rem; border-radius: var(--dm-radius-sm);
204
+ background: color-mix(in srgb, var(--dm-warning) 12%, transparent); font-size: var(--dm-text-sm); }
205
+ .epx-dot { display: inline-block; width: .45rem; height: .45rem; border-radius: 50%; background: var(--dm-warning); margin-left: .25rem; vertical-align: middle; }
206
+ .epx-list { display: flex; flex-direction: column; gap: .35rem; }
207
+ .epx-frow { display: grid; grid-template-columns: 2fr 1.2fr 2fr auto; gap: .5rem; align-items: center; border-radius: var(--dm-radius-sm); }
208
+ .epx-frow:focus-within, .epx-frow:focus-visible { outline: none; box-shadow: 0 0 0 1px var(--dm-border); }
209
+ .epx-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; line-height: 1; }
210
+ .epx-kebab:hover { color: var(--dm-primary); }
211
+ .epx-fields { display: flex; flex-wrap: wrap; gap: .4rem 1.1rem; outline: none; }
212
+ .epx-fields label { display: inline-flex; align-items: center; gap: .35rem; cursor: pointer; font-size: var(--dm-text-sm); }
213
+ .epx-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;
214
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
215
+ .epx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
216
+ .epx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
217
+ .epx-chip.is-danger { background: color-mix(in srgb, var(--dm-danger) 16%, transparent); color: var(--dm-danger); }
218
+ .epx-subhead { display: flex; align-items: center; justify-content: space-between; margin: .6rem 0 .3rem; }
219
+ .epx-subhead .form-label { margin: 0; }
220
+ .epx-subhead .btn { display: inline-flex; align-items: center; gap: .3rem; }
221
+ .epx-pre { max-height: 180px; overflow: auto; font-size: var(--dm-text-xs); padding: .6rem; margin: 0; white-space: pre-wrap; word-break: break-all;
222
+ background: var(--dm-surface-raised, var(--dm-background)); border: 1px solid var(--dm-border); border-radius: var(--dm-radius-sm); color: var(--dm-text); }
223
+ .epx-tall { max-height: 460px; }
224
+ .epx-json { max-height: 460px; overflow: auto; font-family: var(--dm-font-mono, ui-monospace, monospace); font-size: var(--dm-text-xs); line-height: 1.5; padding: .6rem;
225
+ background: var(--dm-surface-raised, var(--dm-background)); border: 1px solid var(--dm-border); border-radius: var(--dm-radius-sm); }
226
+ .epx-wide { width: 100%; display: inline-flex; align-items: center; justify-content: center; gap: .3rem; }
227
+ .epx-hrow { display: flex; align-items: center; gap: .5rem; width: 100%; text-align: left; border: 0; background: none; cursor: pointer; color: var(--dm-text);
228
+ font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-xs); padding: .35rem .45rem; border-radius: var(--dm-radius-sm); }
229
+ .epx-hrow:hover, .epx-hrow:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
230
+ .epx-hrow .epx-url { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
231
+ @media (max-width: 640px) { .epx-grid2, .epx-grid3, .epx-frow { grid-template-columns: 1fr; } }
232
+ </style>