artifacty 0.15.0 → 0.16.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/README.ko.md +50 -8
- package/README.md +51 -9
- package/THIRD_PARTY_NOTICES.md +53 -0
- package/docs/README.md +6 -0
- package/docs/ko/README.md +6 -0
- package/docs/ko/language-packs.md +40 -0
- package/docs/ko/smtp.md +27 -0
- package/docs/language-packs.md +38 -0
- package/docs/roadmap-design.md +31 -2
- package/docs/smtp.md +25 -0
- package/package.json +10 -1
- package/scripts/build-ui.mjs +16 -0
- package/src/cli.js +4 -4
- package/src/client/api-docs.js +32 -0
- package/src/client/auto-apply.js +20 -0
- package/src/client/sidebar.js +31 -0
- package/src/client/theme.js +42 -0
- package/src/client/ui-controls.css +2 -0
- package/src/client/viewer.js +17 -6
- package/src/lib/events.js +4 -4
- package/src/lib/i18n.js +497 -11
- package/src/lib/language-settings.js +114 -0
- package/src/lib/openapi.js +93 -74
- package/src/lib/render.js +551 -397
- package/src/lib/smtp-client.js +206 -0
- package/src/lib/smtp-settings.js +109 -0
- package/src/lib/storage.js +71 -17
- package/src/lib/ui-icons.js +32 -0
- package/src/mcp-server.js +3 -3
- package/src/server.js +213 -16
- package/src/styles/ui-controls.scss +63 -0
package/src/lib/render.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { uiIcon } from "./ui-icons.js";
|
|
1
2
|
import { readFileSync } from "node:fs";
|
|
2
3
|
import { EDITOR_CLIENT_PATH, VIEWER_CLIENT_PATH, editorImportMapJson } from "./editor-assets.js";
|
|
3
|
-
import { createI18n, DEFAULT_LOCALE, editorMessages, localizedHref,
|
|
4
|
+
import { createI18n, DEFAULT_LOCALE, editorMessages, localizedHref, availableLanguages } from "./i18n.js";
|
|
4
5
|
import { ARTIFACT_FORMATS, ARTIFACT_TYPES, REVIEW_STATUSES, SAVED_VIEW_FILTER_KEYS, VISIBILITY_VALUES } from "./storage.js";
|
|
5
6
|
import { normalizeMaxEntries as resolveMaxDiffEntries } from "./diff.js";
|
|
6
7
|
import { parseCsv } from "./csv.js";
|
|
@@ -20,7 +21,7 @@ export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination,
|
|
|
20
21
|
const total = pagination?.total ?? artifacts.length;
|
|
21
22
|
const start = artifacts.length ? (pagination?.offset ?? 0) + 1 : 0;
|
|
22
23
|
const end = artifacts.length ? (pagination?.offset ?? 0) + artifacts.length : 0;
|
|
23
|
-
const pager = renderDashboardPager({ pagination, filters, view });
|
|
24
|
+
const pager = renderDashboardPager({ pagination: pagination || { total, limit: Number(filters.limit) || 50, offset: 0 }, filters, view });
|
|
24
25
|
const rowsHtml = renderArtifactRows(artifacts, filters.groupBy, view);
|
|
25
26
|
const activeFilters = [
|
|
26
27
|
["tag", "filter.tag"], ["sourceAgent", "filter.source"], ["artifactType", "filter.artifactType"],
|
|
@@ -28,7 +29,7 @@ export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination,
|
|
|
28
29
|
["reviewStatus", "filter.reviewStatus"], ["mode", "filter.mode"], ["groupBy", "filter.groupBy"],
|
|
29
30
|
["relatedTo", "filter.relatedTo"], ["relation", "filter.relation"], ["includeArchived", "filter.archived"]
|
|
30
31
|
].filter(([key]) => filters[key]);
|
|
31
|
-
const field = (key, label, type = "text") => `<label class="dashboard-field"><span>${view.text(label)}</span><input type="${type}" name="${key}" value="${escapeAttribute(filters[key] || "")}"></label>`;
|
|
32
|
+
const field = (key, label, type = "text") => `<label class="dashboard-field"><span>${view.text(label)}</span><input type="${type}" name="${key}" value="${escapeAttribute(filters[key] || "")}" class="form-control"></label>`;
|
|
32
33
|
const selectField = (label, select) => `<label class="dashboard-field"><span>${view.text(label)}</span>${select}</label>`;
|
|
33
34
|
|
|
34
35
|
return pageShell({
|
|
@@ -37,14 +38,14 @@ export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination,
|
|
|
37
38
|
${workspaceHeader(user, view)}
|
|
38
39
|
<div class="dashboard-layout">
|
|
39
40
|
<main class="dashboard" id="main-content">
|
|
40
|
-
<div class="dashboard-heading"><h1>${view.text("nav.artifacts")}</h1><nav><a class="button-link" href="${view.href("/new")}">${view.text("nav.new")}</a><a href="${view.href("/import")}">${view.text("nav.import")}</a></nav></div>
|
|
41
|
-
<form class="filter-form" method="get" action="/">
|
|
41
|
+
<div class="dashboard-heading"><h1>${view.text("nav.artifacts")}</h1><nav><a class="button-link btn btn-primary" href="${view.href("/new")}">${view.text("nav.new")}</a><a href="${view.href("/import")}">${view.text("nav.import")}</a></nav></div>
|
|
42
|
+
<form class="filter-form" data-auto-apply="filters" method="get" action="/">
|
|
42
43
|
${localeInput(view.locale)}
|
|
43
44
|
${filters.limit ? `<input type="hidden" name="limit" value="${escapeAttribute(filters.limit)}">` : ""}
|
|
44
45
|
${!embeddingsAvailable && filters.mode ? `<input type="hidden" name="mode" value="${escapeAttribute(filters.mode)}">` : ""}
|
|
45
46
|
<div class="dashboard-search-row">
|
|
46
|
-
<label class="dashboard-search"><span class="dashboard-sr-only">${view.text("filter.search")}</span><input type="search" name="q" value="${escapeAttribute(filters.query || "")}" placeholder="${view.attr("dashboard.searchPlaceholder")}"></label>
|
|
47
|
-
<button type="submit">${view.text("filter.search")}</button>
|
|
47
|
+
<label class="dashboard-search"><span class="dashboard-sr-only">${view.text("filter.search")}</span><input type="search" name="q" value="${escapeAttribute(filters.query || "")}" placeholder="${view.attr("dashboard.searchPlaceholder")}" class="form-control"></label>
|
|
48
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("filter.search")}</button>
|
|
48
49
|
<a class="dashboard-clear" href="${view.href("/")}">${view.text("filter.clear")}</a>
|
|
49
50
|
</div>
|
|
50
51
|
<details class="dashboard-advanced">
|
|
@@ -58,8 +59,8 @@ export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination,
|
|
|
58
59
|
${embeddingsAvailable ? selectField("filter.mode", filterModeSelect(filters.mode, view)) : ""}
|
|
59
60
|
${selectField("filter.groupBy", filterGroupBySelect(filters.groupBy, view))}
|
|
60
61
|
${field("relatedTo", "filter.relatedTo")}${field("relation", "filter.relation")}
|
|
61
|
-
<label class="checkbox-field"><input type="checkbox" name="includeArchived" value="true"${filters.includeArchived ? " checked" : ""}> ${view.text("filter.archived")}</label>
|
|
62
|
-
<div class="dashboard-filter-actions"><button type="submit">${view.text("filter.submit")}</button></div>
|
|
62
|
+
<label class="checkbox-field"><input type="checkbox" name="includeArchived" value="true"${filters.includeArchived ? " checked" : ""} class="form-check-input"> ${view.text("filter.archived")}</label>
|
|
63
|
+
<div class="dashboard-filter-actions"><button type="submit" class="btn btn-outline-secondary">${view.text("filter.submit")}</button></div>
|
|
63
64
|
</div>
|
|
64
65
|
</details>
|
|
65
66
|
${activeFilters.length ? `<p class="dashboard-active-filters">${activeFilters.map(([key, label]) => `<span>${view.text(label)}${key === "includeArchived" ? "" : `: ${escapeHtml(filters[key])}`}</span>`).join("")}</p>` : ""}
|
|
@@ -77,7 +78,7 @@ export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination,
|
|
|
77
78
|
});
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
function renderArtifactRow(artifact, view) {
|
|
81
|
+
function renderArtifactRow(artifact, view, readOnly = false) {
|
|
81
82
|
const tags = artifact.tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("");
|
|
82
83
|
const status = artifact.archivedAt ? statusBadge("archived") : "";
|
|
83
84
|
const visibility = visibilityBadge(artifact, view);
|
|
@@ -86,7 +87,7 @@ function renderArtifactRow(artifact, view) {
|
|
|
86
87
|
? `<span class="row-snippet">${escapeHtml(artifact.searchSnippet)}</span>`
|
|
87
88
|
: "";
|
|
88
89
|
return `
|
|
89
|
-
|
|
90
|
+
<${readOnly ? "div" : "a"} class="artifact-row"${readOnly ? "" : ` href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}`)}"`}>
|
|
90
91
|
<span class="row-main">
|
|
91
92
|
<strong>${escapeHtml(artifact.title)}</strong>
|
|
92
93
|
<span class="dashboard-sr-only">${escapeHtml(artifact.id)}</span>
|
|
@@ -95,15 +96,15 @@ function renderArtifactRow(artifact, view) {
|
|
|
95
96
|
<span class="row-agent"><span>${escapeHtml(artifact.sourceAgent)}</span>${artifact.publisherId || artifact.publisherName ? `<span class="publisher-cell" title="${escapeAttribute(publisherTitle(artifact, view))}">${escapeHtml(publisher)}</span>` : ""}</span>
|
|
96
97
|
<span class="row-formats">${typeBadge(artifact.artifactType || "document")}${formatBadge(artifact.format || "text")}<span>v${artifact.latestVersion}</span></span>
|
|
97
98
|
<span class="tags">${status}${visibility}${tags}</span>
|
|
98
|
-
|
|
99
|
+
</${readOnly ? "div" : "a"}>
|
|
99
100
|
`;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
// groupBy is purely presentational: it groups the already-fetched, already-
|
|
103
104
|
// sorted page of artifacts into labeled sections without changing the query.
|
|
104
|
-
function renderArtifactRows(artifacts, groupBy, view) {
|
|
105
|
+
function renderArtifactRows(artifacts, groupBy, view, readOnly = false) {
|
|
105
106
|
if (!GROUP_BY_VALUES.includes(groupBy)) {
|
|
106
|
-
return artifacts.map((artifact) => renderArtifactRow(artifact, view)).join("");
|
|
107
|
+
return artifacts.map((artifact) => renderArtifactRow(artifact, view, readOnly)).join("");
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
const groups = new Map();
|
|
@@ -119,7 +120,7 @@ function renderArtifactRows(artifacts, groupBy, view) {
|
|
|
119
120
|
.map(([key, group]) => `
|
|
120
121
|
<section class="artifact-group">
|
|
121
122
|
<h2 class="artifact-group-heading">${escapeHtml(groupHeadingLabel(key, groupBy, view))}</h2>
|
|
122
|
-
${group.map((artifact) => renderArtifactRow(artifact, view)).join("")}
|
|
123
|
+
${group.map((artifact) => renderArtifactRow(artifact, view, readOnly)).join("")}
|
|
123
124
|
</section>
|
|
124
125
|
`)
|
|
125
126
|
.join("");
|
|
@@ -132,21 +133,21 @@ function groupHeadingLabel(key, groupBy, view) {
|
|
|
132
133
|
return key || view.raw("dashboard.groupUnknown");
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
// Shared by every <select> whose options are just ARTIFACT_TYPES rendered
|
|
136
|
+
// Shared by every <select class="form-select"> whose options are just ARTIFACT_TYPES rendered
|
|
136
137
|
// as their own value and (escaped) label.
|
|
137
138
|
function artifactTypeOptions(selected) {
|
|
138
139
|
return ARTIFACT_TYPES.map((type) => `<option value="${type}"${type === selected ? " selected" : ""}>${escapeHtml(type)}</option>`).join("");
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
function filterArtifactTypeSelect(selected, view) {
|
|
142
|
-
return `<select name="artifactType">
|
|
143
|
+
return `<select name="artifactType" class="form-select">
|
|
143
144
|
<option value="">${view.text("filter.artifactTypeAny")}</option>
|
|
144
145
|
${artifactTypeOptions(selected)}
|
|
145
146
|
</select>`;
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
function filterReviewStatusSelect(selected, view) {
|
|
149
|
-
return `<select name="reviewStatus">
|
|
150
|
+
return `<select name="reviewStatus" class="form-select">
|
|
150
151
|
<option value="">${view.text("filter.reviewStatusAny")}</option>
|
|
151
152
|
${REVIEW_STATUSES.map((status) => `<option value="${status}"${status === selected ? " selected" : ""}>${view.text(`reviewStatus.${status}`)}</option>`).join("")}
|
|
152
153
|
</select>`;
|
|
@@ -158,14 +159,14 @@ const SEARCH_MODE_VALUES = ["keyword", "semantic", "hybrid"];
|
|
|
158
159
|
// configured (roadmap section 6): semantic/hybrid search silently falls
|
|
159
160
|
// back to keyword without one, so the toggle would otherwise mislead.
|
|
160
161
|
function filterModeSelect(selected, view) {
|
|
161
|
-
return `<select name="mode">
|
|
162
|
+
return `<select name="mode" class="form-select">
|
|
162
163
|
<option value="">${view.text("filter.modeDefault")}</option>
|
|
163
164
|
${SEARCH_MODE_VALUES.map((value) => `<option value="${value}"${value === selected ? " selected" : ""}>${view.text(`filter.mode.${value}`)}</option>`).join("")}
|
|
164
165
|
</select>`;
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
function filterGroupBySelect(selected, view) {
|
|
168
|
-
return `<select name="groupBy">
|
|
169
|
+
return `<select name="groupBy" class="form-select">
|
|
169
170
|
<option value="">${view.text("filter.groupByNone")}</option>
|
|
170
171
|
${GROUP_BY_VALUES.map((value) => `<option value="${value}"${value === selected ? " selected" : ""}>${view.text(`filter.groupBy.${value}`)}</option>`).join("")}
|
|
171
172
|
</select>`;
|
|
@@ -179,7 +180,7 @@ function renderSavedViewsSidebar({ savedViews, filters, view }) {
|
|
|
179
180
|
${savedView.shared ? `<span class="badge saved-view-shared">${view.text("views.shared")}</span>` : ""}
|
|
180
181
|
<form method="post" action="${view.href(`/views/${encodeURIComponent(savedView.id)}/delete`)}" class="saved-view-delete">
|
|
181
182
|
${localeInput(view.locale)}
|
|
182
|
-
<button type="submit">${view.text("views.delete")}</button>
|
|
183
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("views.delete")}</button>
|
|
183
184
|
</form>
|
|
184
185
|
</li>
|
|
185
186
|
`).join("")
|
|
@@ -193,9 +194,9 @@ function renderSavedViewsSidebar({ savedViews, filters, view }) {
|
|
|
193
194
|
<form method="post" action="${view.href("/views")}" class="saved-view-form">
|
|
194
195
|
${localeInput(view.locale)}
|
|
195
196
|
${savedViewFilterHiddenInputs(filters)}
|
|
196
|
-
<label class="dashboard-field"><span>${view.text("views.namePlaceholder")}</span><input name="name" placeholder="${view.attr("views.namePlaceholder")}" required></label>
|
|
197
|
-
<label class="checkbox-field"><input type="checkbox" name="shared" value="true"> ${view.text("views.shared")}</label>
|
|
198
|
-
<button type="submit">${view.text("views.save")}</button>
|
|
197
|
+
<label class="dashboard-field"><span>${view.text("views.namePlaceholder")}</span><input name="name" placeholder="${view.attr("views.namePlaceholder")}" required class="form-control"></label>
|
|
198
|
+
<label class="checkbox-field"><input type="checkbox" name="shared" value="true" class="form-check-input"> ${view.text("views.shared")}</label>
|
|
199
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("views.save")}</button>
|
|
199
200
|
</form>
|
|
200
201
|
</div>
|
|
201
202
|
</details>
|
|
@@ -204,7 +205,7 @@ function renderSavedViewsSidebar({ savedViews, filters, view }) {
|
|
|
204
205
|
|
|
205
206
|
// Every SAVED_VIEW_FILTER_KEYS entry as a hidden form field, so a filter
|
|
206
207
|
// added there (e.g. relatedTo/relation) is captured by "save current
|
|
207
|
-
// filters" automatically instead of needing a hand-added <input> here too.
|
|
208
|
+
// filters" automatically instead of needing a hand-added <input class="form-control"> here too.
|
|
208
209
|
function savedViewFilterHiddenInputs(filters) {
|
|
209
210
|
return SAVED_VIEW_FILTER_KEYS.map((key) => {
|
|
210
211
|
if (key === "includeArchived") {
|
|
@@ -215,18 +216,42 @@ function savedViewFilterHiddenInputs(filters) {
|
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
function renderDashboardPager({ pagination, filters, view }) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
219
|
+
const limit = Math.max(1, Math.min(200, Number(pagination.limit) || 50));
|
|
220
|
+
const offset = Math.max(0, Number(pagination.offset) || 0);
|
|
221
|
+
const total = Math.max(0, Number(pagination.total) || 0);
|
|
222
|
+
const pages = Math.max(1, Math.ceil(total / limit));
|
|
223
|
+
const page = Math.min(pages, Math.floor(offset / limit) + 1);
|
|
224
|
+
const current = new URL(view.currentPath || "/", "http://artifacty.local");
|
|
225
|
+
const params = new URLSearchParams(current.search);
|
|
226
|
+
const resolved = new URL(dashboardPageHref(filters, 0), "http://artifacty.local");
|
|
227
|
+
for (const [name, value] of resolved.searchParams) if (!params.has(name)) params.append(name, value);
|
|
228
|
+
params.delete("limit");
|
|
229
|
+
params.delete("offset");
|
|
230
|
+
params.set("lang", view.locale);
|
|
231
|
+
const pageHref = (nextOffset) => {
|
|
232
|
+
const query = new URLSearchParams(params);
|
|
233
|
+
query.set("limit", String(limit));
|
|
234
|
+
query.set("offset", String(nextOffset));
|
|
235
|
+
return escapeAttribute(`${current.pathname}?${query}${current.hash}`);
|
|
236
|
+
};
|
|
237
|
+
const previousOffset = pagination.previousOffset === undefined ? Math.max(0, offset - limit) : pagination.previousOffset;
|
|
238
|
+
const nextOffset = pagination.nextOffset === undefined ? offset + limit : pagination.nextOffset;
|
|
239
|
+
const previous = offset === 0 || previousOffset === null
|
|
240
|
+
? `<span class="pager-disabled" aria-disabled="true">${view.text("dashboard.previous")}</span>`
|
|
241
|
+
: `<a rel="prev" href="${pageHref(previousOffset)}">${view.text("dashboard.previous")}</a>`;
|
|
242
|
+
const next = offset + limit >= total || nextOffset === null
|
|
243
|
+
? `<span class="pager-disabled" aria-disabled="true">${view.text("dashboard.next")}</span>`
|
|
244
|
+
: `<a rel="next" href="${pageHref(nextOffset)}">${view.text("dashboard.next")}</a>`;
|
|
245
|
+
const sizes = [...new Set([10, 25, 50, 100, limit])].sort((a, b) => a - b);
|
|
246
|
+
return `<section class="dashboard-pagination">
|
|
247
|
+
<nav class="pager" aria-label="${view.attr("dashboard.pagination")}">${previous}<span class="pager-position">${view.text("dashboard.pagePosition", { page, pages })}</span>${next}</nav>
|
|
248
|
+
<form class="page-size-form" data-auto-apply="all" method="get" action="${escapeAttribute(`${current.pathname}${current.hash}`)}">
|
|
249
|
+
${[...params].map(([name, value]) => `<input type="hidden" name="${escapeAttribute(name)}" value="${escapeAttribute(value)}">`).join("")}
|
|
250
|
+
<input type="hidden" name="offset" value="0">
|
|
251
|
+
<label><span>${view.text("dashboard.pageSize")}</span><select name="limit" class="form-select">${sizes.map((size) => `<option value="${size}"${size === limit ? " selected" : ""}>${size}</option>`).join("")}</select></label>
|
|
252
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("language.apply")}</button>
|
|
253
|
+
</form>
|
|
254
|
+
</section>`;
|
|
230
255
|
}
|
|
231
256
|
|
|
232
257
|
function dashboardPageHref(filters, offset) {
|
|
@@ -301,7 +326,7 @@ export function renderArtifactFormPage({ user, mode, baseUrl, artifact, version,
|
|
|
301
326
|
<section class="editor-fields">
|
|
302
327
|
<label class="field">
|
|
303
328
|
<span>${view.text("form.title")}</span>
|
|
304
|
-
<input name="title" value="${escapeAttribute(artifact?.title || "")}" autocomplete="off" required>
|
|
329
|
+
<input name="title" value="${escapeAttribute(artifact?.title || "")}" autocomplete="off" required class="form-control">
|
|
305
330
|
</label>
|
|
306
331
|
<label class="field">
|
|
307
332
|
<span>${view.text("form.format")}</span>
|
|
@@ -309,7 +334,7 @@ export function renderArtifactFormPage({ user, mode, baseUrl, artifact, version,
|
|
|
309
334
|
</label>
|
|
310
335
|
<label class="field">
|
|
311
336
|
<span>${view.text("form.source")}</span>
|
|
312
|
-
<input name="sourceAgent" value="${escapeAttribute(sourceAgent)}" autocomplete="off">
|
|
337
|
+
<input name="sourceAgent" value="${escapeAttribute(sourceAgent)}" autocomplete="off" class="form-control">
|
|
313
338
|
</label>
|
|
314
339
|
<label class="field">
|
|
315
340
|
<span>${view.text("form.type")}</span>
|
|
@@ -317,20 +342,21 @@ export function renderArtifactFormPage({ user, mode, baseUrl, artifact, version,
|
|
|
317
342
|
</label>
|
|
318
343
|
<label class="field">
|
|
319
344
|
<span>${view.text("form.tags")}</span>
|
|
320
|
-
<input name="tags" value="${escapeAttribute(tags)}" autocomplete="off">
|
|
345
|
+
<input name="tags" value="${escapeAttribute(tags)}" autocomplete="off" class="form-control">
|
|
321
346
|
</label>
|
|
322
347
|
${isEdit ? "" : `<label class="field">
|
|
323
348
|
<span>${view.text("artifact.visibility.label")}</span>
|
|
324
349
|
${visibilitySelect(artifact?.visibility || "team", view)}
|
|
325
350
|
</label>`}
|
|
326
351
|
</section>
|
|
352
|
+
<p class="muted">${view.text("artifact.visibility.help")}</p>
|
|
327
353
|
<section class="field content-field">
|
|
328
354
|
<label for="artifact-content">${view.text("form.content")}</label>
|
|
329
|
-
<textarea id="artifact-content" name="content" data-artifacty-editor data-editor-format="${escapeAttribute(format)}" spellcheck="false" required>${escapeHtml(body)}</textarea>
|
|
355
|
+
<textarea id="artifact-content" name="content" data-artifacty-editor data-editor-format="${escapeAttribute(format)}" spellcheck="false" required class="form-control">${escapeHtml(body)}</textarea>
|
|
330
356
|
</section>
|
|
331
357
|
<footer class="editor-actions">
|
|
332
358
|
<a href="${view.href(isEdit ? `/artifacts/${encodeURIComponent(artifact.id)}` : "/")}">${view.text("nav.cancel")}</a>
|
|
333
|
-
<button type="submit">${isEdit ? view.text("form.saveVersion") : view.text("form.create")}</button>
|
|
359
|
+
<button type="submit" class="btn btn-primary">${isEdit ? view.text("form.saveVersion") : view.text("form.create")}</button>
|
|
334
360
|
</footer>
|
|
335
361
|
</form>
|
|
336
362
|
</main>
|
|
@@ -355,14 +381,14 @@ export function renderAdminArtifactVersionsPage({ artifact, selectedVersion, con
|
|
|
355
381
|
<tr>
|
|
356
382
|
<td><a class="version${active}" href="${view.href(`/admin/artifacts/${encodeURIComponent(artifact.id)}/versions?version=${version.version}`)}">v${version.version}</a></td>
|
|
357
383
|
<td>${formatBadge(version.format)}</td>
|
|
358
|
-
<td>${
|
|
384
|
+
<td>${renderDateTime(version.createdAt, view)}</td>
|
|
359
385
|
<td><code>${escapeHtml(version.sha256.slice(0, 12))}</code></td>
|
|
360
386
|
<td>${escapeHtml(String(version.sizeBytes))}</td>
|
|
361
387
|
<td>
|
|
362
388
|
<form class="inline-action" method="post" action="/admin/artifacts/${encodeURIComponent(artifact.id)}/versions/${version.version}/delete">
|
|
363
389
|
${localeInput(view.locale)}
|
|
364
|
-
<input name="reason" placeholder="
|
|
365
|
-
<button type="submit"${deleteDisabled}
|
|
390
|
+
<input name="reason" placeholder="${view.text("ui.reason")}" aria-label="${view.text("ui.reason")}" class="form-control">
|
|
391
|
+
<button type="submit"${deleteDisabled} class="btn btn-outline-danger">${view.text("ui.delete")}</button>
|
|
366
392
|
</form>
|
|
367
393
|
</td>
|
|
368
394
|
</tr>
|
|
@@ -370,12 +396,12 @@ export function renderAdminArtifactVersionsPage({ artifact, selectedVersion, con
|
|
|
370
396
|
}).join("");
|
|
371
397
|
|
|
372
398
|
return pageShell({
|
|
373
|
-
title:
|
|
399
|
+
title: `${view.raw("versions.manage")} · ${artifact.title}`,
|
|
374
400
|
body: `
|
|
375
401
|
${workspaceHeader(user, view)}
|
|
376
402
|
<header class="topbar">
|
|
377
403
|
<div>
|
|
378
|
-
<h1
|
|
404
|
+
<h1>${view.text("versions.manage")}</h1>
|
|
379
405
|
<p>${escapeHtml(artifact.title)} · ${escapeHtml(artifact.id)} · ${escapeHtml(baseUrl)}</p>
|
|
380
406
|
</div>
|
|
381
407
|
<nav>
|
|
@@ -388,9 +414,9 @@ export function renderAdminArtifactVersionsPage({ artifact, selectedVersion, con
|
|
|
388
414
|
<main id="main-content" class="artifact-editor">
|
|
389
415
|
${error ? `<p class="auth-error">${escapeHtml(error)}</p>` : ""}
|
|
390
416
|
<section class="meta-card">
|
|
391
|
-
<h2
|
|
417
|
+
<h2>${view.text("nav.versions")}</h2>
|
|
392
418
|
<table class="data-table">
|
|
393
|
-
<thead><tr><th
|
|
419
|
+
<thead><tr><th>${view.text("ui.version")}</th><th>${view.text("form.format")}</th><th>${view.text("ui.created")}</th><th>SHA</th><th>${view.text("ui.bytes")}</th><th>${view.text("ui.action")}</th></tr></thead>
|
|
394
420
|
<tbody>${versionRows}</tbody>
|
|
395
421
|
</table>
|
|
396
422
|
</section>
|
|
@@ -402,17 +428,17 @@ export function renderAdminArtifactVersionsPage({ artifact, selectedVersion, con
|
|
|
402
428
|
${formatSelect(selectedVersion.format)}
|
|
403
429
|
</label>
|
|
404
430
|
<label class="field">
|
|
405
|
-
<span
|
|
406
|
-
<input name="reason" value="" autocomplete="off">
|
|
431
|
+
<span>${view.text("ui.reason")}</span>
|
|
432
|
+
<input name="reason" value="" autocomplete="off" class="form-control">
|
|
407
433
|
</label>
|
|
408
434
|
</section>
|
|
409
435
|
<section class="field content-field">
|
|
410
|
-
<label for="artifact-content"
|
|
411
|
-
<textarea id="artifact-content" name="content" data-artifacty-editor data-editor-format="${escapeAttribute(selectedVersion.format)}" spellcheck="false" required>${escapeHtml(content)}</textarea>
|
|
436
|
+
<label for="artifact-content">${view.text("versions.content", { version: selectedVersion.version })}</label>
|
|
437
|
+
<textarea id="artifact-content" name="content" data-artifacty-editor data-editor-format="${escapeAttribute(selectedVersion.format)}" spellcheck="false" required class="form-control">${escapeHtml(content)}</textarea>
|
|
412
438
|
</section>
|
|
413
439
|
<footer class="editor-actions">
|
|
414
|
-
<a href="${view.href(`${artifactPath}?version=${selectedVersion.version}`)}"
|
|
415
|
-
<button type="submit"
|
|
440
|
+
<a href="${view.href(`${artifactPath}?version=${selectedVersion.version}`)}">${view.text("versions.view")}</a>
|
|
441
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("versions.repair")}</button>
|
|
416
442
|
</footer>
|
|
417
443
|
</form>
|
|
418
444
|
</main>
|
|
@@ -444,36 +470,36 @@ export function renderImportArtifactPage({ user, baseUrl, authToken = "", locale
|
|
|
444
470
|
<section class="editor-fields">
|
|
445
471
|
<label class="field">
|
|
446
472
|
<span>${view.text("form.agent")}</span>
|
|
447
|
-
<select name="agent">
|
|
473
|
+
<select name="agent" class="form-select">
|
|
448
474
|
<option value="auto">${view.text("form.auto")}</option>
|
|
449
475
|
<option value="claude">Claude</option>
|
|
450
476
|
<option value="codex">Codex</option>
|
|
451
477
|
<option value="gemini">Gemini</option>
|
|
452
478
|
<option value="copilot">GitHub Copilot</option>
|
|
453
479
|
<option value="cursor">Cursor</option>
|
|
454
|
-
<option value="generic"
|
|
480
|
+
<option value="generic">${view.text("import.generic")}</option>
|
|
455
481
|
</select>
|
|
456
482
|
</label>
|
|
457
483
|
<label class="field">
|
|
458
484
|
<span>${view.text("form.title")}</span>
|
|
459
|
-
<input name="title" autocomplete="off">
|
|
485
|
+
<input name="title" autocomplete="off" class="form-control">
|
|
460
486
|
</label>
|
|
461
487
|
<label class="field">
|
|
462
488
|
<span>${view.text("form.fileName")}</span>
|
|
463
|
-
<input name="fileName" autocomplete="off">
|
|
489
|
+
<input name="fileName" autocomplete="off" class="form-control">
|
|
464
490
|
</label>
|
|
465
491
|
<label class="field">
|
|
466
492
|
<span>${view.text("form.tags")}</span>
|
|
467
|
-
<input name="tags" autocomplete="off">
|
|
493
|
+
<input name="tags" autocomplete="off" class="form-control">
|
|
468
494
|
</label>
|
|
469
495
|
</section>
|
|
470
496
|
<section class="field content-field">
|
|
471
497
|
<label for="import-content">${view.text("form.importContent")}</label>
|
|
472
|
-
<textarea id="import-content" name="content" data-artifacty-editor data-editor-format="auto" spellcheck="false" required></textarea>
|
|
498
|
+
<textarea id="import-content" name="content" data-artifacty-editor data-editor-format="auto" spellcheck="false" required class="form-control"></textarea>
|
|
473
499
|
</section>
|
|
474
500
|
<footer class="editor-actions">
|
|
475
501
|
<a href="${view.href("/")}">${view.text("nav.cancel")}</a>
|
|
476
|
-
<button type="submit">${view.text("form.importSubmit")}</button>
|
|
502
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("form.importSubmit")}</button>
|
|
477
503
|
</footer>
|
|
478
504
|
</form>
|
|
479
505
|
</main>
|
|
@@ -486,7 +512,7 @@ export function renderImportArtifactPage({ user, baseUrl, authToken = "", locale
|
|
|
486
512
|
|
|
487
513
|
export function renderLoginPage({ baseUrl, setup = false, error = "", locale = DEFAULT_LOCALE, currentPath = "/login" }) {
|
|
488
514
|
const view = viewContext(locale, currentPath);
|
|
489
|
-
const title = setup ? "
|
|
515
|
+
const title = setup ? view.raw("login.setup") : view.raw("nav.signIn");
|
|
490
516
|
return pageShell({
|
|
491
517
|
title,
|
|
492
518
|
body: `
|
|
@@ -500,24 +526,24 @@ export function renderLoginPage({ baseUrl, setup = false, error = "", locale = D
|
|
|
500
526
|
</header>
|
|
501
527
|
<main id="main-content" class="artifact-editor auth-panel">
|
|
502
528
|
${error ? `<p class="auth-error">${escapeHtml(error)}</p>` : ""}
|
|
503
|
-
${setup ? `<p class="muted"
|
|
529
|
+
${setup ? `<p class="muted">${view.text("login.setupHelp")}</p>` : ""}
|
|
504
530
|
<form class="editor-form auth-form" method="post" action="/login">
|
|
505
531
|
<section class="editor-fields">
|
|
506
532
|
<label class="field">
|
|
507
|
-
<span
|
|
508
|
-
<input type="email" name="email" autocomplete="username" required>
|
|
533
|
+
<span>${view.text("ui.email")}</span>
|
|
534
|
+
<input type="email" name="email" autocomplete="username" required class="form-control">
|
|
509
535
|
</label>
|
|
510
536
|
${setup ? `<label class="field">
|
|
511
|
-
<span
|
|
512
|
-
<input name="name" autocomplete="name">
|
|
537
|
+
<span>${view.text("ui.name")}</span>
|
|
538
|
+
<input name="name" autocomplete="name" class="form-control">
|
|
513
539
|
</label>` : ""}
|
|
514
540
|
<label class="field">
|
|
515
|
-
<span
|
|
516
|
-
<input type="password" name="password" autocomplete="${setup ? "new-password" : "current-password"}" minlength="8" required>
|
|
541
|
+
<span>${view.text("ui.password")}</span>
|
|
542
|
+
<input type="password" name="password" autocomplete="${setup ? "new-password" : "current-password"}" minlength="8" required class="form-control">
|
|
517
543
|
</label>
|
|
518
544
|
</section>
|
|
519
545
|
<footer class="editor-actions">
|
|
520
|
-
<button type="submit">${setup ? "
|
|
546
|
+
<button type="submit" class="btn btn-primary">${setup ? view.text("login.create") : view.text("nav.signIn")}</button>
|
|
521
547
|
</footer>
|
|
522
548
|
</form>
|
|
523
549
|
</main>
|
|
@@ -532,7 +558,7 @@ export function renderAccountPage({ baseUrl, user, tokens = [], createdToken = "
|
|
|
532
558
|
<tr>
|
|
533
559
|
<td><a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}`)}">${escapeHtml(artifact.title)}</a></td>
|
|
534
560
|
<td>${escapeHtml(artifact.id)}</td>
|
|
535
|
-
<td>${
|
|
561
|
+
<td>${renderDateTime(artifact.updatedAt, view)}</td>
|
|
536
562
|
<td>${artifact.archivedAt ? statusBadge(view.text("artifact.archived", { date: artifact.archivedAt })) : ""}</td>
|
|
537
563
|
</tr>
|
|
538
564
|
`).join("");
|
|
@@ -540,65 +566,65 @@ export function renderAccountPage({ baseUrl, user, tokens = [], createdToken = "
|
|
|
540
566
|
<tr>
|
|
541
567
|
<td>${escapeHtml(token.name)}</td>
|
|
542
568
|
<td>${escapeHtml((token.scopes || ["read", "write"]).join(", "))}</td>
|
|
543
|
-
<td>${
|
|
544
|
-
<td>${token.lastUsedAt ?
|
|
545
|
-
<td>${token.revokedAt ?
|
|
569
|
+
<td>${renderDateTime(token.createdAt, view)}</td>
|
|
570
|
+
<td>${token.lastUsedAt ? renderDateTime(token.lastUsedAt, view) : view.text("ui.never")}</td>
|
|
571
|
+
<td>${token.revokedAt ? renderDateTime(token.revokedAt, view) : view.text("ui.active")}</td>
|
|
546
572
|
<td>
|
|
547
573
|
${token.revokedAt ? "" : `<form method="post" action="/account/tokens/${encodeURIComponent(token.id)}/revoke">
|
|
548
|
-
<button type="submit"
|
|
574
|
+
<button type="submit" class="btn btn-outline-danger">${view.text("ui.revoke")}</button>
|
|
549
575
|
</form>`}
|
|
550
576
|
</td>
|
|
551
577
|
</tr>
|
|
552
578
|
`).join("");
|
|
553
579
|
|
|
554
580
|
return pageShell({
|
|
555
|
-
title: "
|
|
581
|
+
title: view.raw("nav.account"),
|
|
556
582
|
body: `
|
|
557
583
|
${workspaceHeader(user, view)}
|
|
558
584
|
<header class="topbar">
|
|
559
585
|
<div>
|
|
560
|
-
<h1
|
|
561
|
-
<p>${escapeHtml(user.email)} · ${
|
|
586
|
+
<h1>${view.text("nav.account")}</h1>
|
|
587
|
+
<p>${escapeHtml(user.email)} · ${view.text(`ui.role.${user.role}`)} · ${escapeHtml(baseUrl)}</p>
|
|
562
588
|
</div>
|
|
563
589
|
|
|
564
590
|
</header>
|
|
565
591
|
<main id="main-content" class="settings-page artifact-view">
|
|
566
592
|
${createdToken ? `<section class="token-once">
|
|
567
|
-
<h2
|
|
568
|
-
<p
|
|
593
|
+
<h2>${view.text("account.newToken")}</h2>
|
|
594
|
+
<p>${view.text("account.copyToken")}</p>
|
|
569
595
|
<pre class="artifact-code"><code>${escapeHtml(createdToken)}</code></pre>
|
|
570
596
|
</section>` : ""}
|
|
571
597
|
<section class="meta-card">
|
|
572
|
-
<h2
|
|
598
|
+
<h2>${view.text("account.profile")}</h2>
|
|
573
599
|
<p><strong>${escapeHtml(user.name)}</strong></p>
|
|
574
600
|
<p>${escapeHtml(user.email)}</p>
|
|
575
|
-
<p
|
|
601
|
+
<p>${view.text("ui.role")}: ${view.text(`ui.role.${user.role}`)}</p>
|
|
576
602
|
</section>
|
|
577
603
|
<section class="meta-card">
|
|
578
604
|
<h2>${view.text("tokens.createTitle")}</h2>
|
|
579
605
|
<form class="inline-action token-create-form" method="post" action="/account/tokens">
|
|
580
|
-
<input name="name" placeholder="
|
|
606
|
+
<input name="name" placeholder="${view.text("account.tokenName")}" aria-label="${view.text("account.tokenName")}" autocomplete="off" required class="form-control">
|
|
581
607
|
<fieldset class="token-scopes">
|
|
582
608
|
<legend>${view.text("tokens.scopes")}</legend>
|
|
583
|
-
<label><input type="checkbox" name="scopeRead" checked> ${view.text("tokens.scope.read")}</label>
|
|
584
|
-
<label><input type="checkbox" name="scopeWrite" checked> ${view.text("tokens.scope.write")}</label>
|
|
585
|
-
${user.role === "admin" ? `<label><input type="checkbox" name="scopeAdmin"> ${view.text("tokens.scope.admin")}</label>` : ""}
|
|
609
|
+
<label><input type="checkbox" name="scopeRead" checked class="form-check-input"> ${view.text("tokens.scope.read")}</label>
|
|
610
|
+
<label><input type="checkbox" name="scopeWrite" checked class="form-check-input"> ${view.text("tokens.scope.write")}</label>
|
|
611
|
+
${user.role === "admin" ? `<label><input type="checkbox" name="scopeAdmin" class="form-check-input"> ${view.text("tokens.scope.admin")}</label>` : ""}
|
|
586
612
|
</fieldset>
|
|
587
|
-
<button type="submit"
|
|
613
|
+
<button type="submit" class="btn btn-primary">${view.text("account.createToken")}</button>
|
|
588
614
|
</form>
|
|
589
615
|
</section>
|
|
590
616
|
<section class="meta-card">
|
|
591
|
-
<h2
|
|
617
|
+
<h2>${view.text("account.tokens")}</h2>
|
|
592
618
|
<table class="data-table">
|
|
593
|
-
<thead><tr><th
|
|
594
|
-
<tbody>${rows || `<tr><td colspan="6"
|
|
619
|
+
<thead><tr><th>${view.text("ui.name")}</th><th>${view.text("tokens.scopes")}</th><th>${view.text("ui.created")}</th><th>${view.text("ui.lastUsed")}</th><th>${view.text("ui.status")}</th><th></th></tr></thead>
|
|
620
|
+
<tbody>${rows || `<tr><td colspan="6">${view.text("account.noTokens")}</td></tr>`}</tbody>
|
|
595
621
|
</table>
|
|
596
622
|
</section>
|
|
597
623
|
<section class="meta-card">
|
|
598
624
|
<h2>${view.text("account.privateArtifacts")}</h2>
|
|
599
625
|
<table class="data-table">
|
|
600
|
-
<thead><tr><th
|
|
601
|
-
<tbody>${privateArtifactRows || `<tr><td colspan="4"
|
|
626
|
+
<thead><tr><th>${view.text("ui.title")}</th><th>ID</th><th>${view.text("ui.updated")}</th><th>${view.text("ui.status")}</th></tr></thead>
|
|
627
|
+
<tbody>${privateArtifactRows || `<tr><td colspan="4">${view.text("ui.none")}</td></tr>`}</tbody>
|
|
602
628
|
</table>
|
|
603
629
|
</section>
|
|
604
630
|
</main>
|
|
@@ -610,37 +636,37 @@ export function renderAccountPage({ baseUrl, user, tokens = [], createdToken = "
|
|
|
610
636
|
export function renderPasswordPage({ baseUrl, user, required = false, error = "", success = "", locale = DEFAULT_LOCALE, currentPath = "/account/password" }) {
|
|
611
637
|
const view = viewContext(locale, currentPath);
|
|
612
638
|
return pageShell({
|
|
613
|
-
title: "
|
|
639
|
+
title: view.raw("password.title"),
|
|
614
640
|
body: `
|
|
615
641
|
${workspaceHeader(user, view)}
|
|
616
642
|
<header class="topbar">
|
|
617
643
|
<div>
|
|
618
|
-
<h1
|
|
644
|
+
<h1>${view.text("password.title")}</h1>
|
|
619
645
|
<p>${escapeHtml(user.email)} · ${escapeHtml(baseUrl)}</p>
|
|
620
646
|
</div>
|
|
621
647
|
|
|
622
648
|
</header>
|
|
623
649
|
<main id="main-content" class="settings-page artifact-editor auth-panel">
|
|
624
|
-
${required ? `<p class="auth-warning"
|
|
650
|
+
${required ? `<p class="auth-warning">${view.text("password.required")}</p>` : ""}
|
|
625
651
|
${error ? `<p class="auth-error">${escapeHtml(error)}</p>` : ""}
|
|
626
652
|
${success ? `<p class="auth-success">${escapeHtml(success)}</p>` : ""}
|
|
627
653
|
<form class="editor-form auth-form" method="post" action="/account/password">
|
|
628
654
|
<section class="editor-fields">
|
|
629
655
|
<label class="field">
|
|
630
|
-
<span
|
|
631
|
-
<input type="password" name="currentPassword" autocomplete="current-password" required>
|
|
656
|
+
<span>${view.text("password.current")}</span>
|
|
657
|
+
<input type="password" name="currentPassword" autocomplete="current-password" required class="form-control">
|
|
632
658
|
</label>
|
|
633
659
|
<label class="field">
|
|
634
|
-
<span
|
|
635
|
-
<input type="password" name="newPassword" autocomplete="new-password" minlength="8" required>
|
|
660
|
+
<span>${view.text("password.new")}</span>
|
|
661
|
+
<input type="password" name="newPassword" autocomplete="new-password" minlength="8" required class="form-control">
|
|
636
662
|
</label>
|
|
637
663
|
<label class="field">
|
|
638
|
-
<span
|
|
639
|
-
<input type="password" name="confirmPassword" autocomplete="new-password" minlength="8" required>
|
|
664
|
+
<span>${view.text("password.confirm")}</span>
|
|
665
|
+
<input type="password" name="confirmPassword" autocomplete="new-password" minlength="8" required class="form-control">
|
|
640
666
|
</label>
|
|
641
667
|
</section>
|
|
642
668
|
<footer class="editor-actions">
|
|
643
|
-
<button type="submit"
|
|
669
|
+
<button type="submit" class="btn btn-primary">${view.text("password.submit")}</button>
|
|
644
670
|
</footer>
|
|
645
671
|
</form>
|
|
646
672
|
</main>
|
|
@@ -662,57 +688,57 @@ function renderFullImportSummary(tableCounts) {
|
|
|
662
688
|
export function renderAdminBackupPage({ user, baseUrl, integrity, importResult = null, importError = "", locale = DEFAULT_LOCALE, currentPath = "/admin/backup" }) {
|
|
663
689
|
const view = viewContext(locale, currentPath);
|
|
664
690
|
return pageShell({
|
|
665
|
-
title: "
|
|
691
|
+
title: view.raw("nav.backup"),
|
|
666
692
|
body: `
|
|
667
693
|
${workspaceHeader(user, view)}
|
|
668
694
|
<header class="topbar">
|
|
669
695
|
<div>
|
|
670
|
-
<h1
|
|
696
|
+
<h1>${view.text("nav.backup")}</h1>
|
|
671
697
|
<p>${escapeHtml(baseUrl)}</p>
|
|
672
698
|
</div>
|
|
673
699
|
|
|
674
700
|
</header>
|
|
675
701
|
<main id="main-content" class="settings-page artifact-view">
|
|
676
|
-
${importResult ? `<p class="auth-success"
|
|
702
|
+
${importResult ? `<p class="auth-success">${view.text("backup.complete", { count: importResult.artifactCount })}${importResult.scope === "full" ? renderFullImportSummary(importResult.tableCounts) : ""}.</p>` : ""}
|
|
677
703
|
${importError ? `<p class="auth-error">${escapeHtml(importError)}</p>` : ""}
|
|
678
704
|
<section class="meta-card">
|
|
679
|
-
<h2
|
|
705
|
+
<h2>${view.text("backup.status")}</h2>
|
|
680
706
|
<table class="data-table">
|
|
681
707
|
<tbody>
|
|
682
|
-
<tr><th
|
|
683
|
-
<tr><th
|
|
684
|
-
<tr><th
|
|
685
|
-
<tr><th
|
|
686
|
-
<tr><th
|
|
708
|
+
<tr><th>${view.text("backup.home")}</th><td>${escapeHtml(integrity.store)}</td></tr>
|
|
709
|
+
<tr><th>${view.text("nav.artifacts")}</th><td>${escapeHtml(integrity.artifactCount)}</td></tr>
|
|
710
|
+
<tr><th>${view.text("nav.versions")}</th><td>${escapeHtml(integrity.versionCount)}</td></tr>
|
|
711
|
+
<tr><th>${view.text("backup.bytes")}</th><td>${escapeHtml(integrity.totalBytes)}</td></tr>
|
|
712
|
+
<tr><th>${view.text("backup.integrity")}</th><td>${integrity.ok ? view.text("backup.ok") : view.text("backup.attention")}</td></tr>
|
|
687
713
|
</tbody>
|
|
688
714
|
</table>
|
|
689
|
-
${integrity.ok ? "" : `<p class="auth-warning"
|
|
715
|
+
${integrity.ok ? "" : `<p class="auth-warning">${view.text("backup.integrityHelp")}</p>`}
|
|
690
716
|
</section>
|
|
691
717
|
<section class="meta-card">
|
|
692
|
-
<h2
|
|
693
|
-
<p class="muted"
|
|
718
|
+
<h2>${view.text("backup.download")}</h2>
|
|
719
|
+
<p class="muted">${view.text("backup.exportHelp")}</p>
|
|
694
720
|
<form class="editor-form" method="get" action="/admin/backup/export">
|
|
695
721
|
<label class="field">
|
|
696
722
|
<span>${view.text("backup.scopeLabel")}</span>
|
|
697
|
-
<select name="scope">
|
|
723
|
+
<select name="scope" class="form-select">
|
|
698
724
|
<option value="artifacts">${view.text("backup.scopeArtifacts")}</option>
|
|
699
725
|
<option value="full">${view.text("backup.scopeFull")}</option>
|
|
700
726
|
</select>
|
|
701
727
|
</label>
|
|
702
|
-
<footer class="editor-actions"><button type="submit">${view.text("backup.downloadButton")}</button></footer>
|
|
728
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-outline-secondary">${view.text("backup.downloadButton")}</button></footer>
|
|
703
729
|
</form>
|
|
704
730
|
</section>
|
|
705
731
|
<section class="meta-card">
|
|
706
|
-
<h2
|
|
707
|
-
<p class="auth-warning"
|
|
732
|
+
<h2>${view.text("backup.restore")}</h2>
|
|
733
|
+
<p class="auth-warning">${view.text("backup.restoreHelp")}</p>
|
|
708
734
|
<form class="editor-form" method="post" action="/admin/backup/import">
|
|
709
735
|
<label class="field content-field">
|
|
710
|
-
<span
|
|
711
|
-
<textarea class="compact-textarea" name="backup" spellcheck="false" placeholder="{"schemaVersion":1,"artifacts":[...]}" required></textarea>
|
|
736
|
+
<span>${view.text("backup.json")}</span>
|
|
737
|
+
<textarea class="compact-textarea form-control" name="backup" spellcheck="false" placeholder="{"schemaVersion":1,"artifacts":[...]}" required></textarea>
|
|
712
738
|
</label>
|
|
713
|
-
<label class="checkbox-field"><input type="checkbox" name="confirm" value="replace-all"> ${view.text("backup.confirmLabel")}</label>
|
|
714
|
-
<label class="checkbox-field"><input type="checkbox" name="forceUsers" value="1"> ${view.text("backup.forceUsersLabel")}</label>
|
|
715
|
-
<footer class="editor-actions"><button type="submit"
|
|
739
|
+
<label class="checkbox-field"><input type="checkbox" name="confirm" value="replace-all" class="form-check-input"> ${view.text("backup.confirmLabel")}</label>
|
|
740
|
+
<label class="checkbox-field"><input type="checkbox" name="forceUsers" value="1" class="form-check-input"> ${view.text("backup.forceUsersLabel")}</label>
|
|
741
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-outline-secondary">${view.text("backup.restore")}</button></footer>
|
|
716
742
|
</form>
|
|
717
743
|
</section>
|
|
718
744
|
</main>
|
|
@@ -721,11 +747,77 @@ export function renderAdminBackupPage({ user, baseUrl, integrity, importResult =
|
|
|
721
747
|
});
|
|
722
748
|
}
|
|
723
749
|
|
|
750
|
+
export function renderAdminEmailPage({ settings = {}, user, baseUrl, locale = DEFAULT_LOCALE, currentPath = "/admin/email", error = "", success = "" }) {
|
|
751
|
+
const view = viewContext(locale, currentPath);
|
|
752
|
+
return pageShell({ title: view.raw("smtp.title"), locale: view.locale,
|
|
753
|
+
body: `${workspaceHeader(user, view)}
|
|
754
|
+
<header class="topbar"><div><h1>${view.text("smtp.title")}</h1><p>${escapeHtml(baseUrl)}</p></div></header>
|
|
755
|
+
<main id="main-content" class="settings-page artifact-view">
|
|
756
|
+
${error ? `<p class="auth-error" role="alert">${view.text("smtp.saveFailed")}</p>` : ""}
|
|
757
|
+
${success ? `<p class="auth-success" role="status">${view.text("smtp.saved")}</p>` : ""}
|
|
758
|
+
<section class="meta-card"><h2>${view.text("smtp.settings")}</h2>
|
|
759
|
+
<p>${view.text("smtp.help")}</p><p class="muted">${view.text("smtp.security")}</p>
|
|
760
|
+
<form class="editor-form smtp-settings-form" method="post" action="/admin/email">${localeInput(view.locale)}
|
|
761
|
+
<label><input type="checkbox" name="enabled" value="true"${settings.enabled ? " checked" : ""} class="form-check-input"> ${view.text("smtp.enabled")}</label>
|
|
762
|
+
<div class="smtp-field-grid">
|
|
763
|
+
<label class="field"><span>${view.text("smtp.host")}</span><input name="host" value="${escapeAttribute(settings.host || "")}" autocomplete="off" class="form-control"></label>
|
|
764
|
+
<label class="field"><span>${view.text("smtp.port")}</span><input name="port" type="number" min="1" max="65535" value="${escapeAttribute(settings.port || 465)}" class="form-control"></label>
|
|
765
|
+
<label class="field"><span>${view.text("smtp.tlsMode")}</span><select name="tlsMode" required class="form-select">
|
|
766
|
+
<option value="tls"${settings.tlsMode !== "starttls" ? " selected" : ""}>${view.text("smtp.tls")}</option>
|
|
767
|
+
<option value="starttls"${settings.tlsMode === "starttls" ? " selected" : ""}>${view.text("smtp.starttls")}</option>
|
|
768
|
+
</select></label>
|
|
769
|
+
<label class="field"><span>${view.text("smtp.from")}</span><input name="from" type="email" value="${escapeAttribute(settings.from || "")}" autocomplete="off" class="form-control"></label>
|
|
770
|
+
<label class="field"><span>${view.text("smtp.username")}</span><input name="username" value="${escapeAttribute(settings.username || "")}" autocomplete="off" class="form-control"></label>
|
|
771
|
+
<label class="field"><span>${view.text("smtp.password")}</span><input name="password" type="password" value="" autocomplete="new-password" placeholder="${view.attr(settings.hasPassword ? "smtp.keepPassword" : "smtp.enterPassword")}" class="form-control"></label>
|
|
772
|
+
</div>
|
|
773
|
+
<p class="muted">${view.text("smtp.authHelp")}</p>
|
|
774
|
+
<p class="muted">${view.text("smtp.restoreNote")}</p>
|
|
775
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-primary">${view.text("smtp.save")}</button></footer>
|
|
776
|
+
</form>
|
|
777
|
+
</section>
|
|
778
|
+
</main>` });
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
export function renderAdminLanguagesPage({ settings, user, baseUrl, locale = DEFAULT_LOCALE, currentPath = "/admin/languages", error = "", success = "" }) {
|
|
782
|
+
const view = viewContext(locale, currentPath);
|
|
783
|
+
const installed = availableLanguages();
|
|
784
|
+
return pageShell({ title: view.raw("languageAdmin.title"), locale: view.locale,
|
|
785
|
+
body: `${workspaceHeader(user, view)}
|
|
786
|
+
<header class="topbar"><div><h1>${view.text("languageAdmin.title")}</h1><p>${escapeHtml(baseUrl)}</p></div></header>
|
|
787
|
+
<main id="main-content" class="settings-page artifact-view">
|
|
788
|
+
${error ? `<p class="auth-error" role="alert">${escapeHtml(error)}</p>` : ""}
|
|
789
|
+
${success ? `<p class="auth-success" role="status">${escapeHtml(success)}</p>` : ""}
|
|
790
|
+
<section class="meta-card"><h2>${view.text("languageAdmin.settings")}</h2>
|
|
791
|
+
<form class="editor-form" method="post" action="/admin/languages">${localeInput(view.locale)}
|
|
792
|
+
<fieldset><legend>${view.text("languageAdmin.enabled")}</legend>
|
|
793
|
+
${installed.map((language) => `<label class="check-field"><input type="checkbox" name="enabledLocales" value="${escapeAttribute(language.locale)}"${settings.enabledLocales.includes(language.locale) ? " checked" : ""} class="form-check-input"> ${escapeHtml(language.nativeName)} (${escapeHtml(language.locale)})</label>`).join("")}
|
|
794
|
+
</fieldset>
|
|
795
|
+
<label class="field"><span>${view.text("languageAdmin.default")}</span><select name="defaultLocale" class="form-select">
|
|
796
|
+
${installed.map((language) => `<option value="${escapeAttribute(language.locale)}"${settings.defaultLocale === language.locale ? " selected" : ""}>${escapeHtml(language.nativeName)}</option>`).join("")}
|
|
797
|
+
</select></label>
|
|
798
|
+
<p class="muted">${view.text("languageAdmin.defaultHelp")}</p>
|
|
799
|
+
<button type="submit" class="btn btn-primary">${view.text("languageAdmin.save")}</button>
|
|
800
|
+
</form>
|
|
801
|
+
</section>
|
|
802
|
+
<section class="meta-card"><h2>${view.text("languageAdmin.import")}</h2>
|
|
803
|
+
<p><a href="${view.href("/admin/languages/template")}">${view.text("languageAdmin.template")}</a></p>
|
|
804
|
+
<p>${view.text("languageAdmin.templateHelp")}</p>
|
|
805
|
+
<p>${view.text("languageAdmin.importHelp", { count: "{count}" })}</p>
|
|
806
|
+
<p class="muted">${view.text("languageAdmin.disabledHelp")}</p>
|
|
807
|
+
<form class="editor-form" method="post" action="/admin/languages/import">${localeInput(view.locale)}
|
|
808
|
+
<label class="field"><span>${view.text("languageAdmin.pack")}</span><textarea class="compact-textarea form-control" name="pack" maxlength="524288" spellcheck="false" required placeholder="${escapeAttribute('{"locale":"fr","nativeName":"Français","messages":{"language.label":"Langue","language.apply":"Appliquer"}}')}"></textarea></label>
|
|
809
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("languageAdmin.import")}</button>
|
|
810
|
+
</form>
|
|
811
|
+
</section>
|
|
812
|
+
</main>` });
|
|
813
|
+
}
|
|
814
|
+
|
|
724
815
|
export function renderAdminUpdatePage({ baseUrl, user, status = {}, locale = DEFAULT_LOCALE, currentPath = "/admin/update" }) {
|
|
725
816
|
const view = viewContext(locale, currentPath);
|
|
726
817
|
const check = status.check;
|
|
727
818
|
const run = status.run;
|
|
728
819
|
const active = ["queued", "running"].includes(run?.state);
|
|
820
|
+
const hasUpdate = check?.updateAvailable && check.targetVersion && status.currentVersion !== check.targetVersion;
|
|
729
821
|
const canStart = check?.supported && !check.errorCode && check.targetVersion && check.updateAvailable && check.confirmationId && Date.parse(check.expiresAt) > Date.now() && !active && run?.state !== "interrupted" && status.currentVersion !== check.targetVersion;
|
|
730
822
|
const states = Object.fromEntries(["queued", "running", "succeeded", "failed", "interrupted"].map((state) => [state, view.raw(`update.${state}`)]));
|
|
731
823
|
const messages = { ...states, reconnect: view.raw("update.reconnect"), signIn: view.raw("update.signIn") };
|
|
@@ -736,20 +828,19 @@ export function renderAdminUpdatePage({ baseUrl, user, status = {}, locale = DEF
|
|
|
736
828
|
<main id="main-content" class="settings-page artifact-view">
|
|
737
829
|
<section class="meta-card"><h2>${view.text("update.installed")}</h2><p>Artifacty <strong data-update-version>${escapeHtml(status.currentVersion || APP_VERSION)}</strong></p>
|
|
738
830
|
<p>${view.text("update.help")}</p>
|
|
739
|
-
<form method="post" action="/admin/update/check">${localeInput(view.locale)}<button type="submit"${active ? " disabled" : ""}>${view.text("update.check")}</button></form>
|
|
831
|
+
<form method="post" action="/admin/update/check">${localeInput(view.locale)}<button type="submit"${active ? " disabled" : ""} class="btn btn-outline-secondary">${view.text("update.check")}</button></form>
|
|
740
832
|
</section>
|
|
741
|
-
${check ? `<section class="meta-card"
|
|
742
|
-
|
|
743
|
-
${check.
|
|
744
|
-
${check.errorCode ? `<p role="alert">${view.text("update.checkFailed")}</p>` : !check.supported ? `<p>${view.text("update.unsupported")}</p><p>${view.text("update.manual")}</p>` : !check.updateAvailable && !check.errorCode && check.targetVersion ? `<p>${view.text("update.current")}</p>` : ""}
|
|
833
|
+
${check ? `<section class="meta-card">${hasUpdate ? `<h2>${view.text("update.latest")}</h2><p>${escapeHtml(status.currentVersion || APP_VERSION)} → <strong>${escapeHtml(check.targetVersion)}</strong></p>` : ""}
|
|
834
|
+
${hasUpdate ? `<p><a href="https://github.com/raeseoklee/artifacty/releases/tag/v${escapeAttribute(encodeURIComponent(check.targetVersion))}" target="_blank" rel="noopener noreferrer">${view.text("update.releaseNotes")}</a></p>` : ""}
|
|
835
|
+
${check.errorCode ? `<p role="alert">${view.text("update.checkFailed")}</p>` : !check.supported ? `<p>${view.text("update.unsupported")}</p><p>${view.text("update.manual")}</p>` : !hasUpdate && !check.errorCode && check.targetVersion ? `<p>${view.text("update.current")}</p>` : ""}
|
|
745
836
|
${check.supported && check.updateAvailable && !canStart && !active && !check.errorCode && status.currentVersion !== check.targetVersion ? `<p>${view.text("update.checkAgain")}</p>` : ""}
|
|
746
837
|
${canStart ? `<p>${view.text("update.available", { version: check.targetVersion })}</p><p>${view.text("update.backup")}</p>
|
|
747
838
|
<form class="editor-form" method="post" action="/admin/update/start">${localeInput(view.locale)}
|
|
748
839
|
<input type="hidden" name="confirmationId" value="${escapeAttribute(check.confirmationId)}">
|
|
749
840
|
<input type="hidden" name="targetVersion" value="${escapeAttribute(check.targetVersion)}">
|
|
750
|
-
<label><input type="checkbox" name="confirmUpdate" value="true" required> ${view.text("update.confirm")}</label>
|
|
841
|
+
<label><input type="checkbox" name="confirmUpdate" value="true" required class="form-check-input"> ${view.text("update.confirm")}</label>
|
|
751
842
|
${check.expiresAt ? `<p class="muted">${view.text("update.expires")}: ${`<time datetime="${escapeAttribute(check.expiresAt)}" title="${escapeAttribute(check.expiresAt)}">${escapeHtml(new Intl.DateTimeFormat(view.locale, { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" }).format(new Date(check.expiresAt)))} UTC</time>`}</p>` : ""}
|
|
752
|
-
<button type="submit">${view.text("update.start")}</button>
|
|
843
|
+
<button type="submit" class="btn btn-primary">${view.text("update.start")}</button>
|
|
753
844
|
</form>` : ""}
|
|
754
845
|
</section>` : ""}
|
|
755
846
|
${run ? `<section class="meta-card" data-update-status data-active="${active}" data-messages="${escapeAttribute(JSON.stringify(messages))}"><h2>${view.text("update.progress")}</h2>
|
|
@@ -763,6 +854,23 @@ export function renderAdminUpdatePage({ baseUrl, user, status = {}, locale = DEF
|
|
|
763
854
|
});
|
|
764
855
|
}
|
|
765
856
|
|
|
857
|
+
export function renderAccessModePreviewPage({ user, baseUrl, mode, artifacts = [], anonymousArtifactCount = 0, locale = DEFAULT_LOCALE, currentPath = "/admin/access/preview" }) {
|
|
858
|
+
const view = viewContext(locale, currentPath);
|
|
859
|
+
const isPublic = mode === "public";
|
|
860
|
+
return pageShell({ title: view.raw("access.previewTitle"), locale: view.locale,
|
|
861
|
+
body: `<header class="topbar"><div><h1>${view.text("access.previewTitle")}</h1><p>${escapeHtml(baseUrl)}</p></div>
|
|
862
|
+
<nav><a href="${view.href(`/admin/access?mode=${isPublic ? "public" : "private"}`)}">${view.text("access.backToSettings")}</a></nav></header>
|
|
863
|
+
<main id="main-content" class="dashboard">
|
|
864
|
+
<section class="meta-card" role="status"><h2>${view.text(isPublic ? "access.public" : "access.private")}</h2>
|
|
865
|
+
<p>${view.text("access.previewAnonymous")}</p><p>${view.text("access.previewUnsaved")}</p>
|
|
866
|
+
</section>
|
|
867
|
+
${isPublic ? `<div class="dashboard-heading"><h1>${view.text("nav.artifacts")}</h1></div>
|
|
868
|
+
<p class="muted">${view.text("access.previewListCount", { shown: artifacts.length, total: anonymousArtifactCount })}</p>
|
|
869
|
+
<section class="artifact-list">${renderArtifactRows(artifacts, "", view, true) || `<div class="empty">${view.text("dashboard.empty")}</div>`}</section>`
|
|
870
|
+
: `<section class="meta-card"><h2>${view.text("access.previewSignIn")}</h2><p>${view.text("access.previewPrivate")}</p></section>`}
|
|
871
|
+
</main>` });
|
|
872
|
+
}
|
|
873
|
+
|
|
766
874
|
export function renderAdminAccessPage({ baseUrl, user, mode, teams = [], users = [], networkPolicy = { allowedCidrs: [] }, currentIp = "", preview = null, pendingNetworkPolicy = null, shareLinkPolicy = {}, locale = DEFAULT_LOCALE, currentPath = "/admin/access" }) {
|
|
767
875
|
const view = viewContext(locale, currentPath);
|
|
768
876
|
return pageShell({
|
|
@@ -774,40 +882,40 @@ export function renderAdminAccessPage({ baseUrl, user, mode, teams = [], users =
|
|
|
774
882
|
<main id="main-content" class="settings-page artifact-view">
|
|
775
883
|
<section class="meta-card"><h2>${view.text("access.mode")}</h2>
|
|
776
884
|
<form class="editor-form" method="post" action="/admin/access/preview">${localeInput(view.locale)}
|
|
777
|
-
<label><input type="radio" name="mode" value="public"${mode === "public" ? " checked" : ""}> ${view.text("access.public")}</label>
|
|
885
|
+
<label><input type="radio" name="mode" value="public"${(preview?.mode || mode) === "public" ? " checked" : ""} class="form-check-input"> ${view.text("access.public")}</label>
|
|
778
886
|
<p class="muted">${view.text("access.publicHelp")}</p>
|
|
779
|
-
<label><input type="radio" name="mode" value="private"${mode === "private" ? " checked" : ""}> ${view.text("access.private")}</label>
|
|
887
|
+
<label><input type="radio" name="mode" value="private"${(preview?.mode || mode) === "private" ? " checked" : ""} class="form-check-input"> ${view.text("access.private")}</label>
|
|
780
888
|
<p class="muted">${view.text("access.privateHelp")}</p>
|
|
781
|
-
<button type="submit">${view.text("access.preview")}</button>
|
|
889
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("access.preview")}</button>
|
|
782
890
|
</form>
|
|
783
891
|
</section>
|
|
784
892
|
${preview ? `<section class="meta-card"><h2>${view.text("access.preview")}</h2><p>${view.text("access.previewCount", { count: preview.anonymousArtifactCount, mode: preview.mode })}</p>
|
|
785
|
-
<form method="post" action="/admin/access">${localeInput(view.locale)}<input type="hidden" name="mode" value="${escapeAttribute(preview.mode)}"><button type="submit">${view.text("access.save")}</button></form></section>` : ""}
|
|
786
|
-
<section class="meta-card"><h2>${view.text("sharing.links")}</h2><form method="post" action="/admin/access/share-links">${localeInput(view.locale)}
|
|
787
|
-
<label><input type="checkbox" name="enabledInPrivate" value="true"${shareLinkPolicy.enabledInPrivate ? " checked" : ""}> ${view.text("sharing.enablePrivate")}</label><button type="submit">${view.text("access.save")}</button></form></section>
|
|
893
|
+
<form method="post" action="/admin/access">${localeInput(view.locale)}<input type="hidden" name="mode" value="${escapeAttribute(preview.mode)}"><button type="submit" class="btn btn-primary">${view.text("access.save")}</button></form></section>` : ""}
|
|
894
|
+
<section class="meta-card"><h2>${view.text("sharing.links")}</h2><form class="editor-form" method="post" action="/admin/access/share-links">${localeInput(view.locale)}
|
|
895
|
+
<label><input type="checkbox" name="enabledInPrivate" value="true"${shareLinkPolicy.enabledInPrivate ? " checked" : ""} class="form-check-input"> ${view.text("sharing.enablePrivate")}</label><footer class="editor-actions"><button type="submit" class="btn btn-primary">${view.text("access.save")}</button></footer></form></section>
|
|
788
896
|
${pendingNetworkPolicy ? `<section class="meta-card"><h2>${view.text("access.pendingNetwork")}</h2><p>${view.text("access.pendingHelp", { expiresAt: pendingNetworkPolicy.expiresAt })}</p>
|
|
789
|
-
${["confirm", "rollback"].map((action) => `<form method="post" action="/admin/access/network/${action}">${localeInput(view.locale)}<input type="hidden" name="id" value="${escapeAttribute(pendingNetworkPolicy.id)}"><button type="submit">${view.text(`access.${action}`)}</button></form>`).join("")}</section>` : ""}
|
|
897
|
+
${["confirm", "rollback"].map((action) => `<form method="post" action="/admin/access/network/${action}">${localeInput(view.locale)}<input type="hidden" name="id" value="${escapeAttribute(pendingNetworkPolicy.id)}"><button type="submit" class="btn btn-outline-secondary">${view.text(`access.${action}`)}</button></form>`).join("")}</section>` : ""}
|
|
790
898
|
<section class="meta-card"><h2>${view.text("access.network")}</h2>
|
|
791
899
|
<p class="muted">${view.text("access.networkHelp")}</p>
|
|
792
900
|
<p>${view.text("access.currentIp")}: <code>${escapeHtml(currentIp)}</code></p>
|
|
793
901
|
<form class="editor-form" method="post" action="/admin/access/network">${localeInput(view.locale)}
|
|
794
|
-
<label class="field"><span>${view.text("access.allowedCidrs")}</span><textarea name="allowedCidrs" rows="5">${escapeHtml((networkPolicy.allowedCidrs || []).join("\n"))}</textarea></label>
|
|
795
|
-
<button type="submit">${view.text("access.saveNetwork")}</button>
|
|
902
|
+
<label class="field"><span>${view.text("access.allowedCidrs")}</span><textarea name="allowedCidrs" rows="5" class="form-control">${escapeHtml((networkPolicy.allowedCidrs || []).join("\n"))}</textarea></label>
|
|
903
|
+
<button type="submit" class="btn btn-primary">${view.text("access.saveNetwork")}</button>
|
|
796
904
|
</form>
|
|
797
905
|
</section>
|
|
798
906
|
<section class="meta-card"><h2>${view.text("access.teams")}</h2>
|
|
799
907
|
<form class="editor-form auth-form" method="post" action="/admin/teams">${localeInput(view.locale)}
|
|
800
|
-
<label class="field"><span>${view.text("access.teamName")}</span><input name="name" required></label>
|
|
801
|
-
<button type="submit">${view.text("access.createTeam")}</button>
|
|
908
|
+
<label class="field"><span>${view.text("access.teamName")}</span><input name="name" required class="form-control"></label>
|
|
909
|
+
<button type="submit" class="btn btn-primary">${view.text("access.createTeam")}</button>
|
|
802
910
|
</form>
|
|
803
911
|
${teams.length ? teams.map((team) => `<section><h3>${escapeHtml(team.name)}</h3>
|
|
804
912
|
<form class="editor-form" method="post" action="/admin/teams/${encodeURIComponent(team.id)}/members">${localeInput(view.locale)}
|
|
805
913
|
<fieldset><legend>${view.text("access.members")}</legend>${accessCheckboxes(users, "userIds", team.memberIds || [], view)}</fieldset>
|
|
806
|
-
<button type="submit">${view.text("access.saveMembers")}</button>
|
|
914
|
+
<button type="submit" class="btn btn-primary">${view.text("access.saveMembers")}</button>
|
|
807
915
|
</form></section>`).join("") : `<p class="muted">${view.text("access.noTeams")}</p>`}
|
|
808
916
|
</section>
|
|
809
917
|
<section class="meta-card"><h2>${view.text("access.samples")}</h2><p>${view.text("access.samplesHelp")}</p>
|
|
810
|
-
${["install", "remove"].map((action) => `<form method="post" action="/admin/samples/${action}">${localeInput(view.locale)}<button type="submit">${view.text(`access.samples.${action}`)}</button></form>`).join("")}</section>
|
|
918
|
+
${["install", "remove"].map((action) => `<form method="post" action="/admin/samples/${action}">${localeInput(view.locale)}<button type="submit" class="btn btn-outline-secondary">${view.text(`access.samples.${action}`)}</button></form>`).join("")}</section>
|
|
811
919
|
</main>`
|
|
812
920
|
});
|
|
813
921
|
}
|
|
@@ -825,7 +933,7 @@ export function renderArtifactSharingPage({ baseUrl, user, artifact, sharing = {
|
|
|
825
933
|
<form class="editor-form" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/sharing">${localeInput(view.locale)}
|
|
826
934
|
<fieldset><legend>${view.text("sharing.users")}</legend>${accessCheckboxes(users, "userIds", sharing.userIds || [], view, sharing.userPermissions || {})}</fieldset>
|
|
827
935
|
<fieldset><legend>${view.text("sharing.teams")}</legend>${accessCheckboxes(teams, "teamIds", sharing.teamIds || [], view, sharing.teamPermissions || {})}</fieldset>
|
|
828
|
-
<button type="submit">${view.text("sharing.save")}</button>
|
|
936
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("sharing.save")}</button>
|
|
829
937
|
</form>
|
|
830
938
|
</section>
|
|
831
939
|
${accessReport && (user?.role === "admin" || user?.id === artifact.ownerUserId) ? `<section class="meta-card"><h2>${view.text("sharing.effective")}</h2><p>${view.text("sharing.networkCondition")}: ${escapeHtml(networkPolicy.allowedCidrs?.length ? networkPolicy.allowedCidrs.join(", ") : view.text("sharing.unrestricted"))}</p><p>${view.text("sharing.anonymous")}: ${view.text(accessReport.anonymousRead ? "sharing.allowed" : "sharing.denied")}</p>
|
|
@@ -833,17 +941,17 @@ export function renderArtifactSharingPage({ baseUrl, user, artifact, sharing = {
|
|
|
833
941
|
<section class="meta-card"><h2>${view.text("sharing.links")}</h2><p>${view.text("sharing.linksHelp")}</p>
|
|
834
942
|
${createdShareLink?.token ? `<p>${view.text("sharing.copyNow")}</p><a href="${escapeAttribute(`${baseUrl.replace(/\/$/, "")}/s/${encodeURIComponent(createdShareLink.token)}`)}">${escapeHtml(`${baseUrl.replace(/\/$/, "")}/s/${encodeURIComponent(createdShareLink.token)}`)}</a>` : ""}
|
|
835
943
|
<form class="editor-form" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/share-links">${localeInput(view.locale)}
|
|
836
|
-
<label class="field"><span>${view.text("sharing.version")}</span><select name="version"><option value="latest">${view.text("sharing.latest")}</option>${(artifact.versions || []).map((item) => `<option value="${Number(item.version)}">v${Number(item.version)}</option>`).join("")}</select></label>
|
|
837
|
-
<label class="field"><span>${view.text("sharing.days")}</span><input type="number" name="days" min="1" max="365" value="7" required></label><button type="submit">${view.text("sharing.createLink")}</button></form>
|
|
838
|
-
${shareLinks.map((link) => `<section><p>${escapeHtml(link.id)} · ${link.version == null ? view.text("sharing.latest") : `v${Number(link.version)}`} · ${view.text("sharing.expires")}: ${
|
|
839
|
-
${link.revokedAt ? `<p>${view.text("sharing.revoked")}</p>` : `<form method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/share-links/${encodeURIComponent(link.id)}/revoke">${localeInput(view.locale)}<button type="submit">${view.text("sharing.revoke")}</button></form>`}</section>`).join("")}
|
|
944
|
+
<label class="field"><span>${view.text("sharing.version")}</span><select name="version" class="form-select"><option value="latest">${view.text("sharing.latest")}</option>${(artifact.versions || []).map((item) => `<option value="${Number(item.version)}">v${Number(item.version)}</option>`).join("")}</select></label>
|
|
945
|
+
<label class="field"><span>${view.text("sharing.days")}</span><input type="number" name="days" min="1" max="365" value="7" required class="form-control"></label><button type="submit" class="btn btn-outline-secondary">${view.text("sharing.createLink")}</button></form>
|
|
946
|
+
${shareLinks.map((link) => `<section><p>${escapeHtml(link.id)} · ${link.version == null ? view.text("sharing.latest") : `v${Number(link.version)}`} · ${view.text("sharing.expires")}: ${renderDateTime(link.expiresAt, view)}</p>
|
|
947
|
+
${link.revokedAt ? `<p>${view.text("sharing.revoked")}</p>` : `<form method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/share-links/${encodeURIComponent(link.id)}/revoke">${localeInput(view.locale)}<button type="submit" class="btn btn-outline-danger">${view.text("sharing.revoke")}</button></form>`}</section>`).join("")}
|
|
840
948
|
</section></main>`
|
|
841
949
|
});
|
|
842
950
|
}
|
|
843
951
|
|
|
844
952
|
function accessCheckboxes(items, name, selectedIds, view, permissions = null) {
|
|
845
953
|
if (!items.length) return `<p class="muted">${view.text(name === "userIds" ? "access.noUsers" : "access.noTeams")}</p>`;
|
|
846
|
-
return items.filter((item) => item.active !== false).map((item) => `<label><input type="checkbox" name="${name}" value="${escapeAttribute(item.id)}"${selectedIds.includes(item.id) ? " checked" : ""}> ${escapeHtml(item.name || item.email || item.id)}${item.email && item.name ? ` (${escapeHtml(item.email)})` : ""}</label>${permissions ? `<label>${view.text("sharing.permission")}<select name="${name === "userIds" ? "userPermission" : "teamPermission"}:${escapeAttribute(item.id)}">${["read", "comment", "edit"].map((permission) => `<option value="${permission}"${(permissions[item.id] || "read") === permission ? " selected" : ""}>${view.text(`sharing.permission.${permission}`)}</option>`).join("")}</select></label>` : ""}`).join("");
|
|
954
|
+
return items.filter((item) => item.active !== false).map((item) => `<label><input type="checkbox" name="${name}" value="${escapeAttribute(item.id)}"${selectedIds.includes(item.id) ? " checked" : ""} class="form-check-input"> ${escapeHtml(item.name || item.email || item.id)}${item.email && item.name ? ` (${escapeHtml(item.email)})` : ""}</label>${permissions ? `<label>${view.text("sharing.permission")}<select name="${name === "userIds" ? "userPermission" : "teamPermission"}:${escapeAttribute(item.id)}" class="form-select">${["read", "comment", "edit"].map((permission) => `<option value="${permission}"${(permissions[item.id] || "read") === permission ? " selected" : ""}>${view.text(`sharing.permission.${permission}`)}</option>`).join("")}</select></label>` : ""}`).join("");
|
|
847
955
|
}
|
|
848
956
|
|
|
849
957
|
export function renderInboxPage({ baseUrl, user, notifications = [], unreadCount = 0, locale = DEFAULT_LOCALE, currentPath = "/inbox" }) {
|
|
@@ -852,9 +960,9 @@ export function renderInboxPage({ baseUrl, user, notifications = [], unreadCount
|
|
|
852
960
|
body: `${workspaceHeader(user, view)}
|
|
853
961
|
<header class="topbar"><div><h1>${view.text("inbox.title")}</h1><p>${escapeHtml(baseUrl)}</p></div></header>
|
|
854
962
|
<main id="main-content" class="artifact-view"><section class="meta-card"><p>${view.text("inbox.unread", { count: unreadCount })}</p>
|
|
855
|
-
<form method="post" action="/inbox/read-all">${localeInput(view.locale)}<button type="submit">${view.text("inbox.readAll")}</button></form>
|
|
856
|
-
${notifications.length ? notifications.map((item) => `<section><h2><a href="${view.href(`/artifacts/${encodeURIComponent(item.artifactId)}`)}">${escapeHtml(item.title)}</a></h2><p>${escapeHtml(item.type)} · ${
|
|
857
|
-
${item.readAt ? `<p>${view.text("inbox.read")}</p>` : `<form method="post" action="/inbox/${encodeURIComponent(item.id)}/read">${localeInput(view.locale)}<button type="submit">${view.text("inbox.markRead")}</button></form>`}</section>`).join("") : `<p>${view.text("inbox.empty")}</p>`}
|
|
963
|
+
<form method="post" action="/inbox/read-all">${localeInput(view.locale)}<button type="submit" class="btn btn-outline-secondary">${view.text("inbox.readAll")}</button></form>
|
|
964
|
+
${notifications.length ? notifications.map((item) => `<section><h2><a href="${view.href(`/artifacts/${encodeURIComponent(item.artifactId)}`)}">${escapeHtml(item.title)}</a></h2><p>${["shared-with-me", "comment", "new-version", "review-requested"].includes(item.type) ? view.text(`inbox.type.${item.type}`) : escapeHtml(item.type)} · ${renderDateTime(item.createdAt, view)}</p>
|
|
965
|
+
${item.readAt ? `<p>${view.text("inbox.read")}</p>` : `<form method="post" action="/inbox/${encodeURIComponent(item.id)}/read">${localeInput(view.locale)}<button type="submit" class="btn btn-outline-secondary">${view.text("inbox.markRead")}</button></form>`}</section>`).join("") : `<p>${view.text("inbox.empty")}</p>`}
|
|
858
966
|
</section></main>` });
|
|
859
967
|
}
|
|
860
968
|
|
|
@@ -873,31 +981,38 @@ export function renderSharedArtifactPage({ artifact, version, content, locale =
|
|
|
873
981
|
return pageShell({ title: artifact.title, locale: view.locale,
|
|
874
982
|
body: `<header class="topbar"><div><h1>${escapeHtml(artifact.title)}</h1><p>${view.text("sharing.readOnly")} · v${Number(version.version)}</p></div></header><main id="main-content" class="artifact-view">${rendered}</main>`,
|
|
875
983
|
head: needsViewer ? editorHead() : "",
|
|
876
|
-
afterBody: `${frameResizeScript()}${needsViewer ? viewerScript() : ""}` });
|
|
984
|
+
afterBody: `${frameResizeScript()}${needsViewer ? viewerScript(view.locale) : ""}` });
|
|
877
985
|
}
|
|
878
986
|
|
|
879
|
-
export function renderAdminUsersPage({ baseUrl, user, users = [], importResult = null, importError = "", locale = DEFAULT_LOCALE, currentPath = "/admin/users" }) {
|
|
987
|
+
export function renderAdminUsersPage({ baseUrl, user, users = [], importResult = null, importError = "", roleError = "", resetResult = null, resetError = "", locale = DEFAULT_LOCALE, currentPath = "/admin/users" }) {
|
|
880
988
|
const view = viewContext(locale, currentPath);
|
|
881
989
|
const rows = users.map((item) => `
|
|
882
990
|
<tr>
|
|
883
991
|
<td>${escapeHtml(item.email)}</td>
|
|
884
992
|
<td>${escapeHtml(item.name)}</td>
|
|
885
|
-
<td
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
993
|
+
<td><form class="user-role-form" method="post" action="/admin/users/${encodeURIComponent(item.id)}/role">${localeInput(view.locale)}
|
|
994
|
+
<select name="role" aria-label="${view.text("users.roleFor", { email: item.email })}" class="form-select">
|
|
995
|
+
${["user", "admin"].map((role) => `<option value="${role}"${item.role === role ? " selected" : ""}>${view.text(`ui.role.${role}`)}</option>`).join("")}
|
|
996
|
+
</select><button type="submit" aria-label="${view.text("users.saveRoleFor", { email: item.email })}" class="btn btn-outline-secondary">${view.text("users.saveRole")}</button>
|
|
997
|
+
</form></td>
|
|
998
|
+
<td>${item.active ? view.text("ui.active") : view.text("ui.disabled")}${item.passwordResetRequired ? ` · ${view.text("users.resetRequired")}` : ""}</td>
|
|
999
|
+
<td>${renderDateTime(item.createdAt, view)}</td>
|
|
1000
|
+
<td><div class="user-account-actions">
|
|
1001
|
+
<form method="post" action="/admin/users/${encodeURIComponent(item.id)}/reset-password">${localeInput(view.locale)}
|
|
1002
|
+
<button type="submit" aria-label="${view.text("users.resetFor", { email: item.email })}" class="btn btn-ghost">${view.text("users.resetPassword")}</button>
|
|
1003
|
+
</form>
|
|
1004
|
+
${item.id === user.id ? "" : `<form method="post" action="/admin/users/${encodeURIComponent(item.id)}/${item.active ? "disable" : "enable"}">${localeInput(view.locale)}
|
|
1005
|
+
<button type="submit" class="btn btn-outline-danger">${item.active ? view.text("ui.disable") : view.text("ui.enable")}</button>
|
|
891
1006
|
</form>`}
|
|
892
|
-
</td>
|
|
1007
|
+
</div></td>
|
|
893
1008
|
</tr>
|
|
894
1009
|
`).join("");
|
|
895
1010
|
const createdRows = (importResult?.created || []).map((item) => `
|
|
896
1011
|
<tr>
|
|
897
1012
|
<td>${escapeHtml(item.user.email)}</td>
|
|
898
|
-
<td>${
|
|
899
|
-
<td>${item.user.passwordResetRequired ? "
|
|
900
|
-
<td>${item.passwordGenerated ? `<code>${escapeHtml(item.temporaryPassword)}</code>` : "
|
|
1013
|
+
<td>${view.text(`ui.role.${item.user.role}`)}</td>
|
|
1014
|
+
<td>${item.user.passwordResetRequired ? view.text("ui.yes") : view.text("ui.no")}</td>
|
|
1015
|
+
<td>${item.passwordGenerated ? `<code>${escapeHtml(item.temporaryPassword)}</code>` : view.text("users.provided")}</td>
|
|
901
1016
|
</tr>
|
|
902
1017
|
`).join("");
|
|
903
1018
|
const skippedRows = (importResult?.skipped || []).map((item) => `
|
|
@@ -908,67 +1023,76 @@ export function renderAdminUsersPage({ baseUrl, user, users = [], importResult =
|
|
|
908
1023
|
`).join("");
|
|
909
1024
|
const importSummary = importResult ? `
|
|
910
1025
|
<section class="meta-card">
|
|
911
|
-
<h2
|
|
912
|
-
<p class="muted">${importResult.created.length
|
|
1026
|
+
<h2>${view.text("users.importResults")}</h2>
|
|
1027
|
+
<p class="muted">${view.text("users.summary", { created: importResult.created.length, skipped: importResult.skipped.length, failed: importResult.failed.length })}</p>
|
|
913
1028
|
${createdRows ? `<table class="data-table">
|
|
914
|
-
<thead><tr><th
|
|
1029
|
+
<thead><tr><th>${view.text("ui.email")}</th><th>${view.text("ui.role")}</th><th>${view.text("users.resetRequired")}</th><th>${view.text("users.temporaryPassword")}</th></tr></thead>
|
|
915
1030
|
<tbody>${createdRows}</tbody>
|
|
916
1031
|
</table>` : ""}
|
|
917
|
-
${skippedRows ? `<h3
|
|
918
|
-
<thead><tr><th
|
|
1032
|
+
${skippedRows ? `<h3>${view.text("users.skipped")}</h3><table class="data-table">
|
|
1033
|
+
<thead><tr><th>${view.text("ui.row")}</th><th>${view.text("ui.email")}</th><th>${view.text("ui.reason")}</th></tr></thead>
|
|
919
1034
|
<tbody>${skippedRows}</tbody>
|
|
920
1035
|
</table>` : ""}
|
|
921
|
-
${failedRows ? `<h3
|
|
922
|
-
<thead><tr><th
|
|
1036
|
+
${failedRows ? `<h3>${view.text("users.failed")}</h3><table class="data-table">
|
|
1037
|
+
<thead><tr><th>${view.text("ui.row")}</th><th>${view.text("ui.email")}</th><th>${view.text("ui.error")}</th></tr></thead>
|
|
923
1038
|
<tbody>${failedRows}</tbody>
|
|
924
1039
|
</table>` : ""}
|
|
925
1040
|
</section>
|
|
926
1041
|
` : "";
|
|
927
1042
|
|
|
928
1043
|
return pageShell({
|
|
929
|
-
title: "
|
|
1044
|
+
title: view.raw("nav.users"),
|
|
930
1045
|
body: `
|
|
931
1046
|
${workspaceHeader(user, view)}
|
|
932
1047
|
<header class="topbar">
|
|
933
1048
|
<div>
|
|
934
|
-
<h1
|
|
1049
|
+
<h1>${view.text("nav.users")}</h1>
|
|
935
1050
|
<p>${escapeHtml(baseUrl)}</p>
|
|
936
1051
|
</div>
|
|
937
1052
|
|
|
938
1053
|
</header>
|
|
939
1054
|
<main id="main-content" class="settings-page artifact-view">
|
|
1055
|
+
${resetError ? `<p class="auth-error" role="alert">${escapeHtml(resetError)}</p>` : ""}
|
|
1056
|
+
${resetResult ? `<section class="token-once" role="status"><h2>${view.text("users.resetReady")}</h2>
|
|
1057
|
+
<p>${view.text("users.resetAccount", { email: resetResult.user.email })}</p>
|
|
1058
|
+
<pre class="artifact-code"><code>${escapeHtml(resetResult.temporaryPassword)}</code></pre>
|
|
1059
|
+
<p>${view.text("users.resetHelp")}</p>
|
|
1060
|
+
<p>${view.text(`smtp.mailStatus.${["sent", "failed", "not-configured"].includes(resetResult.mailStatus) ? resetResult.mailStatus : "not-configured"}`)}</p>
|
|
1061
|
+
</section>` : ""}
|
|
940
1062
|
<section class="meta-card">
|
|
941
|
-
<h2
|
|
1063
|
+
<h2>${view.text("users.create")}</h2>
|
|
942
1064
|
<form class="editor-form auth-form" method="post" action="/admin/users">
|
|
943
1065
|
<section class="editor-fields">
|
|
944
|
-
<label class="field"><span
|
|
945
|
-
<label class="field"><span
|
|
946
|
-
<label class="field"><span
|
|
947
|
-
<label class="field"><span
|
|
948
|
-
<label class="check-field"><input type="checkbox" name="passwordResetRequired">
|
|
1066
|
+
<label class="field"><span>${view.text("ui.email")}</span><input type="email" name="email" required class="form-control"></label>
|
|
1067
|
+
<label class="field"><span>${view.text("ui.name")}</span><input name="name" class="form-control"></label>
|
|
1068
|
+
<label class="field"><span>${view.text("ui.role")}</span><select name="role" class="form-select"><option value="user">${view.text("ui.role.user")}</option><option value="admin">${view.text("ui.role.admin")}</option></select></label>
|
|
1069
|
+
<label class="field"><span>${view.text("ui.password")}</span><input type="password" name="password" minlength="8" required class="form-control"></label>
|
|
1070
|
+
<label class="check-field"><input type="checkbox" name="passwordResetRequired" class="form-check-input"> ${view.text("users.requirePassword")}</label>
|
|
949
1071
|
</section>
|
|
950
|
-
<footer class="editor-actions"><button type="submit"
|
|
1072
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-primary">${view.text("users.create")}</button></footer>
|
|
951
1073
|
</form>
|
|
952
1074
|
</section>
|
|
953
1075
|
<section class="meta-card">
|
|
954
|
-
<h2
|
|
955
|
-
<p class="muted"
|
|
1076
|
+
<h2>${view.text("users.importTitle")}</h2>
|
|
1077
|
+
<p class="muted">${view.text("users.csvHelp")}</p>
|
|
956
1078
|
${importError ? `<p class="auth-error">${escapeHtml(importError)}</p>` : ""}
|
|
957
1079
|
<form class="editor-form" method="post" action="/admin/users/import">
|
|
958
1080
|
<label class="field content-field">
|
|
959
1081
|
<span>CSV</span>
|
|
960
|
-
<textarea class="compact-textarea" name="csv" spellcheck="false" placeholder="email,name,role user@example.com,User,user" required></textarea>
|
|
1082
|
+
<textarea class="compact-textarea form-control" name="csv" spellcheck="false" placeholder="email,name,role user@example.com,User,user" required></textarea>
|
|
961
1083
|
</label>
|
|
962
|
-
<footer class="editor-actions"><button type="submit"
|
|
1084
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-outline-secondary">${view.text("users.import")}</button></footer>
|
|
963
1085
|
</form>
|
|
964
1086
|
</section>
|
|
965
1087
|
${importSummary}
|
|
966
1088
|
<section class="meta-card">
|
|
967
|
-
<h2
|
|
968
|
-
|
|
969
|
-
|
|
1089
|
+
<h2>${view.text("users.existing")}</h2>
|
|
1090
|
+
${roleError ? `<p class="auth-error" role="alert">${escapeHtml(roleError)}</p>` : ""}
|
|
1091
|
+
<p class="muted">${view.text("users.lastAdmin")}</p>
|
|
1092
|
+
<div class="artifact-table-scroll"><table class="data-table">
|
|
1093
|
+
<thead><tr><th>${view.text("ui.email")}</th><th>${view.text("ui.name")}</th><th>${view.text("ui.role")}</th><th>${view.text("ui.status")}</th><th>${view.text("ui.created")}</th><th></th></tr></thead>
|
|
970
1094
|
<tbody>${rows}</tbody>
|
|
971
|
-
</table>
|
|
1095
|
+
</table></div>
|
|
972
1096
|
</section>
|
|
973
1097
|
</main>
|
|
974
1098
|
`,
|
|
@@ -987,7 +1111,7 @@ export function renderAdminWebhooksPage({ user, baseUrl, webhooks = [], createdS
|
|
|
987
1111
|
<td>${webhook.failureCount}</td>
|
|
988
1112
|
<td>
|
|
989
1113
|
<form method="post" action="/admin/webhooks/${encodeURIComponent(webhook.id)}/delete" onsubmit="return confirm('${escapeAttribute(view.text("webhooks.confirmDelete"))}')">
|
|
990
|
-
<button type="submit">${view.text("webhooks.delete")}</button>
|
|
1114
|
+
<button type="submit" class="btn btn-outline-danger">${view.text("webhooks.delete")}</button>
|
|
991
1115
|
</form>
|
|
992
1116
|
</td>
|
|
993
1117
|
</tr>
|
|
@@ -1019,10 +1143,10 @@ export function renderAdminWebhooksPage({ user, baseUrl, webhooks = [], createdS
|
|
|
1019
1143
|
<h2>${view.text("webhooks.createTitle")}</h2>
|
|
1020
1144
|
<form class="editor-form" method="post" action="/admin/webhooks">
|
|
1021
1145
|
<section class="editor-fields">
|
|
1022
|
-
<label class="field"><span>${view.text("webhooks.url")}</span><input type="url" name="url" placeholder="https://example.com/hooks/artifacty" required></label>
|
|
1023
|
-
<label class="field"><span>${view.text("webhooks.eventTypes")}</span><input name="eventTypes" placeholder="${view.text("webhooks.eventTypesPlaceholder")}"></label>
|
|
1146
|
+
<label class="field"><span>${view.text("webhooks.url")}</span><input type="url" name="url" placeholder="https://example.com/hooks/artifacty" required class="form-control"></label>
|
|
1147
|
+
<label class="field"><span>${view.text("webhooks.eventTypes")}</span><input name="eventTypes" placeholder="${view.text("webhooks.eventTypesPlaceholder")}" class="form-control"></label>
|
|
1024
1148
|
</section>
|
|
1025
|
-
<footer class="editor-actions"><button type="submit">${view.text("webhooks.create")}</button></footer>
|
|
1149
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-outline-secondary">${view.text("webhooks.create")}</button></footer>
|
|
1026
1150
|
</form>
|
|
1027
1151
|
</section>
|
|
1028
1152
|
<section class="meta-card">
|
|
@@ -1049,7 +1173,7 @@ export function renderAdminRetentionPage({ user, baseUrl, policy, lastSweep = nu
|
|
|
1049
1173
|
<h2>${view.text("retention.lastSweepTitle")}</h2>
|
|
1050
1174
|
<table class="data-table">
|
|
1051
1175
|
<tbody>
|
|
1052
|
-
<tr><th>${view.text("retention.sweepAt")}</th><td>${
|
|
1176
|
+
<tr><th>${view.text("retention.sweepAt")}</th><td>${renderDateTime(lastSweep.createdAt, view)}</td></tr>
|
|
1053
1177
|
<tr><th>${view.text("retention.archived")}</th><td>${escapeHtml(lastSweep.metadata?.archived ?? 0)}</td></tr>
|
|
1054
1178
|
<tr><th>${view.text("retention.purged")}</th><td>${escapeHtml(lastSweep.metadata?.purged ?? 0)}</td></tr>
|
|
1055
1179
|
<tr><th>${view.text("retention.purgeSkipped")}</th><td>${lastSweep.metadata?.purgeSkipped ? view.text("retention.yes") : view.text("retention.no")}</td></tr>
|
|
@@ -1099,16 +1223,24 @@ export function renderAdminRetentionPage({ user, baseUrl, policy, lastSweep = nu
|
|
|
1099
1223
|
<main id="main-content" class="settings-page artifact-view">
|
|
1100
1224
|
<section class="meta-card">
|
|
1101
1225
|
<h2>${view.text("retention.policyTitle")}</h2>
|
|
1102
|
-
<form class="editor-form" method="post" action="/admin/retention"
|
|
1103
|
-
<
|
|
1104
|
-
<
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
<label class="field"><span>${view.text("retention.
|
|
1109
|
-
|
|
1110
|
-
</
|
|
1111
|
-
|
|
1226
|
+
<form class="editor-form retention-policy-form" method="post" action="/admin/retention">${localeInput(view.locale)}
|
|
1227
|
+
<fieldset class="retention-section"><legend>${view.text("retention.artifactsSection")}</legend>
|
|
1228
|
+
<div class="retention-field-grid">
|
|
1229
|
+
<label class="field"><span>${view.text("retention.archiveAfterDaysDefault")}</span><input type="number" min="1" name="archiveAfterDaysDefault" value="${policy.archiveAfterDays.default ?? ""}" class="form-control"></label>
|
|
1230
|
+
<label class="field"><span>${view.text("retention.purgeArchivedAfterDays")}</span><input type="number" min="1" name="purgeArchivedAfterDays" value="${policy.purgeArchivedAfterDays ?? ""}" class="form-control"></label>
|
|
1231
|
+
</div>
|
|
1232
|
+
<label class="field"><span>${view.text("retention.keepTags")}</span><input name="keepTags" value="${escapeAttribute((policy.keepTags || []).join(", "))}" placeholder="pinned, release" class="form-control"></label>
|
|
1233
|
+
</fieldset>
|
|
1234
|
+
<fieldset class="retention-section"><legend>${view.text("retention.typeRulesSection")}</legend>
|
|
1235
|
+
<label class="field"><span>${view.text("retention.archiveAfterDaysByType")}</span><textarea class="compact-textarea form-control" name="archiveAfterDaysByType" placeholder="test-report=30">${escapeHtml(byTypeText)}</textarea></label>
|
|
1236
|
+
</fieldset>
|
|
1237
|
+
<fieldset class="retention-section"><legend>${view.text("retention.historySection")}</legend>
|
|
1238
|
+
<div class="retention-field-grid">
|
|
1239
|
+
<label class="field"><span>${view.text("retention.auditRetentionDays")}</span><input type="number" min="1" name="auditRetentionDays" value="${policy.auditRetentionDays ?? ""}" class="form-control"></label>
|
|
1240
|
+
<label class="field"><span>${view.text("retention.eventRetentionRows")}</span><input type="number" min="1" name="eventRetentionRows" value="${policy.eventRetentionRows ?? ""}" class="form-control"></label>
|
|
1241
|
+
</div>
|
|
1242
|
+
</fieldset>
|
|
1243
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-primary">${view.text("retention.saveButton")}</button></footer>
|
|
1112
1244
|
</form>
|
|
1113
1245
|
</section>
|
|
1114
1246
|
${lastSweepSection}
|
|
@@ -1116,9 +1248,9 @@ export function renderAdminRetentionPage({ user, baseUrl, policy, lastSweep = nu
|
|
|
1116
1248
|
<h2>${view.text("retention.runTitle")}</h2>
|
|
1117
1249
|
<p class="auth-warning">${view.text("retention.purgeGateNote")}</p>
|
|
1118
1250
|
<form class="editor-form" method="post" action="/admin/retention/run">
|
|
1119
|
-
<label class="checkbox-field"><input type="checkbox" name="dryRun" value="1" checked> ${view.text("retention.dryRunLabel")}</label>
|
|
1120
|
-
<label class="checkbox-field"><input type="checkbox" name="allowPurge" value="1"> ${view.text("retention.allowPurgeLabel")}</label>
|
|
1121
|
-
<footer class="editor-actions"><button type="submit">${view.text("retention.runButton")}</button></footer>
|
|
1251
|
+
<label class="checkbox-field"><input type="checkbox" name="dryRun" value="1" checked class="form-check-input"> ${view.text("retention.dryRunLabel")}</label>
|
|
1252
|
+
<label class="checkbox-field"><input type="checkbox" name="allowPurge" value="1" class="form-check-input"> ${view.text("retention.allowPurgeLabel")}</label>
|
|
1253
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-outline-secondary">${view.text("retention.runButton")}</button></footer>
|
|
1122
1254
|
</form>
|
|
1123
1255
|
</section>
|
|
1124
1256
|
${reportSection}
|
|
@@ -1174,8 +1306,8 @@ export function renderArtifactPage({ artifact, version, content, comments = [],
|
|
|
1174
1306
|
<span>${view.text("artifact.visibility.label")}</span>
|
|
1175
1307
|
${visibilitySelect(artifact.visibility || "team", view)}
|
|
1176
1308
|
</label>
|
|
1177
|
-
<button type="submit">${view.text("artifact.visibility.change")}</button>
|
|
1178
|
-
</form>`
|
|
1309
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("artifact.visibility.change")}</button>
|
|
1310
|
+
</form><p class="muted">${view.text("artifact.visibility.help")}</p>`
|
|
1179
1311
|
: "";
|
|
1180
1312
|
|
|
1181
1313
|
return pageShell({
|
|
@@ -1208,14 +1340,14 @@ export function renderArtifactPage({ artifact, version, content, comments = [],
|
|
|
1208
1340
|
<span>${view.text("artifact.schema", { version: artifact.schemaVersion || 1 })}</span>
|
|
1209
1341
|
<span title="${escapeAttribute(publisherTitle(artifact, view))}">${view.text("artifact.publisher", { publisher })}</span>
|
|
1210
1342
|
<span>${view.text("artifact.bytes", { size: version.sizeBytes })}</span>
|
|
1211
|
-
<span>${
|
|
1343
|
+
<span>${renderDateTime(version.createdAt, view)}</span>
|
|
1212
1344
|
<span>${escapeHtml(version.sha256.slice(0, 12))}</span>
|
|
1213
1345
|
</div></details>
|
|
1214
1346
|
</section>
|
|
1215
1347
|
${canArchive ? `<form class="inline-action" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/${archiveAction}">
|
|
1216
1348
|
${hiddenToken(authToken)}
|
|
1217
1349
|
${localeInput(view.locale)}
|
|
1218
|
-
<button type="submit">${archiveLabel}</button>
|
|
1350
|
+
<button type="submit" class="btn btn-outline-secondary">${archiveLabel}</button>
|
|
1219
1351
|
</form>` : ""}
|
|
1220
1352
|
${visibilityForm}
|
|
1221
1353
|
<section class="version-strip">${versionLinks}</section>
|
|
@@ -1225,7 +1357,7 @@ export function renderArtifactPage({ artifact, version, content, comments = [],
|
|
|
1225
1357
|
</main>
|
|
1226
1358
|
`,
|
|
1227
1359
|
head: needsViewerScript ? editorHead() : "",
|
|
1228
|
-
afterBody: `${frameResizeScript()}${needsViewerScript ? viewerScript() : ""}`,
|
|
1360
|
+
afterBody: `${frameResizeScript()}${needsViewerScript ? viewerScript(view.locale) : ""}`,
|
|
1229
1361
|
locale: view.locale
|
|
1230
1362
|
});
|
|
1231
1363
|
}
|
|
@@ -1314,14 +1446,14 @@ function renderCommentEntry(artifact, comment, view, { authToken, canModerate, c
|
|
|
1314
1446
|
? `<form class="inline-action" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/comments/${encodeURIComponent(comment.id)}/resolve">
|
|
1315
1447
|
${hiddenToken(authToken)}
|
|
1316
1448
|
${localeInput(view.locale)}
|
|
1317
|
-
<button type="submit">${view.text("comments.resolve")}</button>
|
|
1449
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("comments.resolve")}</button>
|
|
1318
1450
|
</form>`
|
|
1319
1451
|
: "";
|
|
1320
1452
|
const deleteForm = canModerate
|
|
1321
1453
|
? `<form class="inline-action" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/comments/${encodeURIComponent(comment.id)}/delete">
|
|
1322
1454
|
${hiddenToken(authToken)}
|
|
1323
1455
|
${localeInput(view.locale)}
|
|
1324
|
-
<button type="submit">${view.text("comments.delete")}</button>
|
|
1456
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("comments.delete")}</button>
|
|
1325
1457
|
</form>`
|
|
1326
1458
|
: "";
|
|
1327
1459
|
|
|
@@ -1329,7 +1461,7 @@ function renderCommentEntry(artifact, comment, view, { authToken, canModerate, c
|
|
|
1329
1461
|
<li class="comment-item${isReply ? " comment-reply" : ""}${resolved ? " comment-resolved" : ""}">
|
|
1330
1462
|
<div class="comment-header">
|
|
1331
1463
|
<span class="comment-author">${escapeHtml(comment.authorLabel)}</span>
|
|
1332
|
-
<span class="comment-meta">${
|
|
1464
|
+
<span class="comment-meta">${renderDateTime(comment.createdAt, view)}</span>
|
|
1333
1465
|
${anchorLabel ? `<span class="comment-anchor">${anchorLabel}</span>` : ""}
|
|
1334
1466
|
${resolved ? `<span class="badge s-archived">${view.text("comments.resolved")}</span>` : ""}
|
|
1335
1467
|
</div>
|
|
@@ -1367,13 +1499,13 @@ function renderCommentForm(artifact, view, { version, authToken, parentId, compa
|
|
|
1367
1499
|
${parentId ? `<input type="hidden" name="parentId" value="${escapeAttribute(parentId)}">` : ""}
|
|
1368
1500
|
<label>
|
|
1369
1501
|
<span>${view.text(compact ? "comments.replyLabel" : "comments.bodyLabel")}</span>
|
|
1370
|
-
<textarea name="body" required maxlength="16384"></textarea>
|
|
1502
|
+
<textarea name="body" required maxlength="16384" class="form-control"></textarea>
|
|
1371
1503
|
</label>
|
|
1372
1504
|
${compact ? "" : `<label class="comment-line-label">
|
|
1373
1505
|
<span>${view.text("comments.lineLabel")}</span>
|
|
1374
|
-
<input type="number" name="line" min="1">
|
|
1506
|
+
<input type="number" name="line" min="1" class="form-control">
|
|
1375
1507
|
</label>`}
|
|
1376
|
-
<button type="submit">${view.text(compact ? "comments.reply" : "comments.submit")}</button>
|
|
1508
|
+
<button type="submit" class="btn btn-primary">${view.text(compact ? "comments.reply" : "comments.submit")}</button>
|
|
1377
1509
|
</form>
|
|
1378
1510
|
`;
|
|
1379
1511
|
}
|
|
@@ -1386,11 +1518,11 @@ function renderReviewStatusForm(artifact, view, authToken) {
|
|
|
1386
1518
|
${localeInput(view.locale)}
|
|
1387
1519
|
<label>
|
|
1388
1520
|
<span>${view.text("comments.reviewStatus.label")}</span>
|
|
1389
|
-
<select name="status">
|
|
1521
|
+
<select name="status" class="form-select">
|
|
1390
1522
|
${REVIEW_STATUSES.map((status) => `<option value="${status}"${status === current ? " selected" : ""}>${view.text(`comments.reviewStatus.${status}`)}</option>`).join("")}
|
|
1391
1523
|
</select>
|
|
1392
1524
|
</label>
|
|
1393
|
-
<button type="submit">${view.text("comments.reviewStatus.change")}</button>
|
|
1525
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("comments.reviewStatus.change")}</button>
|
|
1394
1526
|
</form>
|
|
1395
1527
|
`;
|
|
1396
1528
|
}
|
|
@@ -1497,8 +1629,8 @@ export function renderDiffPage({ user, artifact, fromVersion, toVersion, fromCon
|
|
|
1497
1629
|
|
|
1498
1630
|
const preview = toVersion.format === "html"
|
|
1499
1631
|
? `<section class="split-preview">
|
|
1500
|
-
<div>${renderContent(fromVersion.format, fromContent)}</div>
|
|
1501
|
-
<div>${renderContent(toVersion.format, toContent)}</div>
|
|
1632
|
+
<div>${renderContent(fromVersion.format, fromContent, {}, { view })}</div>
|
|
1633
|
+
<div>${renderContent(toVersion.format, toContent, {}, { view })}</div>
|
|
1502
1634
|
</section>`
|
|
1503
1635
|
: "";
|
|
1504
1636
|
|
|
@@ -1537,13 +1669,13 @@ export function renderDiffPage({ user, artifact, fromVersion, toVersion, fromCon
|
|
|
1537
1669
|
<input type="hidden" name="view" value="${escapeAttribute(resolvedView)}">
|
|
1538
1670
|
<label class="field">
|
|
1539
1671
|
<span>${view.text("diff.from")}</span>
|
|
1540
|
-
<select name="from">${versionOptions}</select>
|
|
1672
|
+
<select name="from" class="form-select">${versionOptions}</select>
|
|
1541
1673
|
</label>
|
|
1542
1674
|
<label class="field">
|
|
1543
1675
|
<span>${view.text("diff.to")}</span>
|
|
1544
|
-
<select name="to">${versionOptions}</select>
|
|
1676
|
+
<select name="to" class="form-select">${versionOptions}</select>
|
|
1545
1677
|
</label>
|
|
1546
|
-
<button type="submit">${view.text("diff.compare")}</button>
|
|
1678
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("diff.compare")}</button>
|
|
1547
1679
|
</form>
|
|
1548
1680
|
<script>
|
|
1549
1681
|
document.querySelector('select[name="from"]').value = ${JSON.stringify(String(fromVersion.version))};
|
|
@@ -1772,6 +1904,7 @@ function renderBundleFiles(content, options = {}) {
|
|
|
1772
1904
|
}
|
|
1773
1905
|
|
|
1774
1906
|
export function renderContent(format, content, metadata = {}, options = {}) {
|
|
1907
|
+
const view = options.view || viewContext(options.locale || DEFAULT_LOCALE, "/");
|
|
1775
1908
|
if (format === "html") {
|
|
1776
1909
|
return `<iframe class="artifact-frame" sandbox="allow-scripts allow-forms allow-popups" srcdoc="${escapeAttribute(htmlFrameContent(content))}"></iframe>`;
|
|
1777
1910
|
}
|
|
@@ -1799,15 +1932,15 @@ export function renderContent(format, content, metadata = {}, options = {}) {
|
|
|
1799
1932
|
}
|
|
1800
1933
|
|
|
1801
1934
|
if (format === "notebook") {
|
|
1802
|
-
return renderNotebook(content);
|
|
1935
|
+
return renderNotebook(content, view);
|
|
1803
1936
|
}
|
|
1804
1937
|
|
|
1805
1938
|
if (format === "sarif") {
|
|
1806
|
-
return renderSarif(content);
|
|
1939
|
+
return renderSarif(content, view);
|
|
1807
1940
|
}
|
|
1808
1941
|
|
|
1809
1942
|
if (format === "csv") {
|
|
1810
|
-
return renderCsv(content);
|
|
1943
|
+
return renderCsv(content, view);
|
|
1811
1944
|
}
|
|
1812
1945
|
|
|
1813
1946
|
if (format === "image") {
|
|
@@ -1821,7 +1954,7 @@ export function renderContent(format, content, metadata = {}, options = {}) {
|
|
|
1821
1954
|
if (format === "code") {
|
|
1822
1955
|
const language = metadata.language || metadata.artifactyImport?.language || "";
|
|
1823
1956
|
return `<section class="artifact-code-viewer" data-artifacty-code-viewer data-language="${escapeAttribute(language)}">
|
|
1824
|
-
<textarea hidden>${escapeHtml(content)}</textarea>
|
|
1957
|
+
<textarea hidden class="form-control">${escapeHtml(content)}</textarea>
|
|
1825
1958
|
<pre class="artifact-code artifact-code-fallback"><code>${escapeHtml(content)}</code></pre>
|
|
1826
1959
|
</section>`;
|
|
1827
1960
|
}
|
|
@@ -1831,7 +1964,7 @@ export function renderContent(format, content, metadata = {}, options = {}) {
|
|
|
1831
1964
|
return `<iframe class="artifact-frame artifact-react-frame" sandbox="allow-scripts" src="${escapeAttribute(options.reactFrameUrl)}"></iframe>`;
|
|
1832
1965
|
}
|
|
1833
1966
|
return `<section class="artifact-react-disabled">
|
|
1834
|
-
<p
|
|
1967
|
+
<p>${view.text("react.disabled")}</p>
|
|
1835
1968
|
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>
|
|
1836
1969
|
</section>`;
|
|
1837
1970
|
}
|
|
@@ -1922,16 +2055,35 @@ export function renderReactFramePage({ title, content }) {
|
|
|
1922
2055
|
}
|
|
1923
2056
|
|
|
1924
2057
|
export function pageShell({ title, body, head = "", afterBody = "", locale = DEFAULT_LOCALE }) {
|
|
2058
|
+
const darkThemeVariables = ` --bg: #15171b;
|
|
2059
|
+
--grid: rgba(120, 160, 200, 0.06);
|
|
2060
|
+
--panel: #1c1f24;
|
|
2061
|
+
--panel-2: #25292f;
|
|
2062
|
+
--text: #e6edf3;
|
|
2063
|
+
--muted: #adb5c0;
|
|
2064
|
+
--faint: #9aa5b2;
|
|
2065
|
+
--line: #343b44;
|
|
2066
|
+
--line-2: #596575;
|
|
2067
|
+
--accent: #9cbcf5;
|
|
2068
|
+
--accent-2: #bbd1f8;
|
|
2069
|
+
--accent-ink: #18253d;
|
|
2070
|
+
--accent-soft: #29374e;
|
|
2071
|
+
--ring: #9cbcf5;
|
|
2072
|
+
--code: #080c12;
|
|
2073
|
+
--code-text: #e6edf3;
|
|
2074
|
+
`;
|
|
1925
2075
|
return `<!doctype html>
|
|
1926
2076
|
<html lang="${escapeAttribute(locale)}">
|
|
1927
2077
|
<head>
|
|
1928
2078
|
<meta charset="utf-8">
|
|
2079
|
+
<script src="/assets/theme.js"></script><script type="module" src="/assets/auto-apply.js"></script>
|
|
2080
|
+
<script src="/assets/sidebar.js"></script>
|
|
1929
2081
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1930
2082
|
<title>${escapeHtml(title)}</title>
|
|
1931
2083
|
${head}
|
|
1932
2084
|
<style>
|
|
1933
2085
|
:root {
|
|
1934
|
-
color-scheme: light
|
|
2086
|
+
color-scheme: light;
|
|
1935
2087
|
--bg: #f5f6f8;
|
|
1936
2088
|
--grid: rgba(31, 58, 99, 0.05);
|
|
1937
2089
|
--panel: #ffffff;
|
|
@@ -1951,24 +2103,14 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
1951
2103
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans KR", sans-serif;
|
|
1952
2104
|
--mono: ui-monospace, "SFMono-Regular", "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
|
1953
2105
|
}
|
|
2106
|
+
:root[data-theme="dark"] {
|
|
2107
|
+
color-scheme: dark;
|
|
2108
|
+
${darkThemeVariables}
|
|
2109
|
+
}
|
|
1954
2110
|
@media (prefers-color-scheme: dark) {
|
|
1955
|
-
:root {
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
--panel: #1c1f24;
|
|
1959
|
-
--panel-2: #25292f;
|
|
1960
|
-
--text: #e6edf3;
|
|
1961
|
-
--muted: #adb5c0;
|
|
1962
|
-
--faint: #9aa5b2;
|
|
1963
|
-
--line: #343b44;
|
|
1964
|
-
--line-2: #596575;
|
|
1965
|
-
--accent: #9cbcf5;
|
|
1966
|
-
--accent-2: #bbd1f8;
|
|
1967
|
-
--accent-ink: #18253d;
|
|
1968
|
-
--accent-soft: #29374e;
|
|
1969
|
-
--ring: #9cbcf5;
|
|
1970
|
-
--code: #080c12;
|
|
1971
|
-
--code-text: #e6edf3;
|
|
2111
|
+
:root:not([data-theme="light"]) {
|
|
2112
|
+
color-scheme: dark;
|
|
2113
|
+
${darkThemeVariables}
|
|
1972
2114
|
}
|
|
1973
2115
|
}
|
|
1974
2116
|
* { box-sizing: border-box; }
|
|
@@ -2051,24 +2193,10 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2051
2193
|
color: var(--text);
|
|
2052
2194
|
text-decoration: none;
|
|
2053
2195
|
}
|
|
2054
|
-
.language-switcher {
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
padding-left: 9px;
|
|
2059
|
-
border-left: 1px solid var(--line);
|
|
2060
|
-
}
|
|
2061
|
-
.language-switcher a,
|
|
2062
|
-
.language-switcher .active {
|
|
2063
|
-
padding: 5px 8px;
|
|
2064
|
-
border-radius: 7px;
|
|
2065
|
-
}
|
|
2066
|
-
.language-switcher .active {
|
|
2067
|
-
color: var(--text);
|
|
2068
|
-
font-weight: 700;
|
|
2069
|
-
text-decoration: none;
|
|
2070
|
-
cursor: default;
|
|
2071
|
-
}
|
|
2196
|
+
.language-switcher { display: grid; gap: 8px; min-width: 0; }
|
|
2197
|
+
.language-switcher label { display: grid; gap: 6px; min-width: 0; }
|
|
2198
|
+
.language-switcher select { width: 100%; min-width: 0; max-width: 100%; }
|
|
2199
|
+
.language-switcher button { justify-self: start; }
|
|
2072
2200
|
.dashboard,
|
|
2073
2201
|
.artifact-view,
|
|
2074
2202
|
.artifact-editor {
|
|
@@ -2219,12 +2347,19 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2219
2347
|
background: var(--panel);
|
|
2220
2348
|
box-shadow: none;
|
|
2221
2349
|
}
|
|
2350
|
+
.dashboard-pagination { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; margin-top: 16px; }
|
|
2351
|
+
.page-size-form, .page-size-form label { display: flex; align-items: center; gap: 8px; }
|
|
2352
|
+
.page-size-form { margin: 0; }
|
|
2353
|
+
.page-size-form select { min-width: 76px; width: auto; min-height: 44px; }
|
|
2354
|
+
.page-size-form button { min-height: 44px; }
|
|
2355
|
+
.pager-position { align-self: center; white-space: nowrap; }
|
|
2222
2356
|
.pager {
|
|
2223
2357
|
display: flex;
|
|
2224
|
-
|
|
2358
|
+
flex-wrap: wrap;
|
|
2359
|
+
justify-content: flex-start;
|
|
2225
2360
|
gap: 8px;
|
|
2226
|
-
margin-top:
|
|
2227
|
-
font-family: var(--
|
|
2361
|
+
margin-top: 0;
|
|
2362
|
+
font-family: var(--sans);
|
|
2228
2363
|
font-size: 12.5px;
|
|
2229
2364
|
}
|
|
2230
2365
|
.pager a,
|
|
@@ -2349,11 +2484,12 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2349
2484
|
font-size: 13px;
|
|
2350
2485
|
}
|
|
2351
2486
|
.field > span {
|
|
2352
|
-
font-family: var(--
|
|
2353
|
-
font-size:
|
|
2487
|
+
font-family: var(--sans);
|
|
2488
|
+
font-size: 12px;
|
|
2489
|
+
font-weight: 500;
|
|
2354
2490
|
letter-spacing: 0;
|
|
2355
2491
|
text-transform: none;
|
|
2356
|
-
color: var(--
|
|
2492
|
+
color: var(--muted);
|
|
2357
2493
|
}
|
|
2358
2494
|
.check-field {
|
|
2359
2495
|
display: flex;
|
|
@@ -2368,8 +2504,6 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2368
2504
|
min-height: 0;
|
|
2369
2505
|
padding: 0;
|
|
2370
2506
|
}
|
|
2371
|
-
input:not([type="checkbox"]):not([type="radio"]),
|
|
2372
|
-
select,
|
|
2373
2507
|
textarea {
|
|
2374
2508
|
width: 100%;
|
|
2375
2509
|
border: 1px solid var(--line-2);
|
|
@@ -2455,60 +2589,13 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2455
2589
|
border: 0;
|
|
2456
2590
|
background: var(--panel);
|
|
2457
2591
|
}
|
|
2458
|
-
|
|
2459
|
-
select:focus,
|
|
2460
|
-
textarea:focus {
|
|
2461
|
-
outline: 3px solid var(--ring);
|
|
2462
|
-
outline-offset: 0;
|
|
2463
|
-
border-color: var(--accent);
|
|
2464
|
-
}
|
|
2592
|
+
button:not(.btn) { font: inherit; color: var(--text); background: var(--panel); border: 1px solid var(--line-2); border-radius: 6px; padding: 6px 10px; cursor: pointer; }
|
|
2465
2593
|
.editor-actions {
|
|
2466
2594
|
display: flex;
|
|
2467
2595
|
align-items: center;
|
|
2468
2596
|
justify-content: flex-end;
|
|
2469
2597
|
gap: 12px;
|
|
2470
2598
|
}
|
|
2471
|
-
button {
|
|
2472
|
-
min-height: 38px;
|
|
2473
|
-
border: 1px solid var(--accent);
|
|
2474
|
-
border-radius: 8px;
|
|
2475
|
-
padding: 7px 15px;
|
|
2476
|
-
background: var(--accent);
|
|
2477
|
-
color: var(--accent-ink);
|
|
2478
|
-
font: inherit;
|
|
2479
|
-
font-weight: 600;
|
|
2480
|
-
cursor: pointer;
|
|
2481
|
-
transition: background 0.15s ease, border-color 0.15s ease;
|
|
2482
|
-
}
|
|
2483
|
-
button:hover { background: var(--accent-2); border-color: var(--accent-2); }
|
|
2484
|
-
.button-link {
|
|
2485
|
-
display: inline-flex;
|
|
2486
|
-
min-height: 38px;
|
|
2487
|
-
align-items: center;
|
|
2488
|
-
justify-content: center;
|
|
2489
|
-
padding: 7px 15px;
|
|
2490
|
-
border: 1px solid var(--accent);
|
|
2491
|
-
border-radius: 8px;
|
|
2492
|
-
background: var(--accent);
|
|
2493
|
-
color: var(--accent-ink);
|
|
2494
|
-
font-weight: 600;
|
|
2495
|
-
}
|
|
2496
|
-
.button-link:hover {
|
|
2497
|
-
border-color: var(--accent-2);
|
|
2498
|
-
background: var(--accent-2);
|
|
2499
|
-
color: var(--accent-ink);
|
|
2500
|
-
text-decoration: none;
|
|
2501
|
-
}
|
|
2502
|
-
.inline-action button {
|
|
2503
|
-
min-height: 32px;
|
|
2504
|
-
padding: 5px 12px;
|
|
2505
|
-
font-family: var(--mono);
|
|
2506
|
-
font-size: 12px;
|
|
2507
|
-
font-weight: 500;
|
|
2508
|
-
border-color: var(--line-2);
|
|
2509
|
-
background: var(--panel);
|
|
2510
|
-
color: var(--muted);
|
|
2511
|
-
}
|
|
2512
2599
|
.inline-action button:hover {
|
|
2513
2600
|
background: var(--panel-2);
|
|
2514
2601
|
border-color: var(--accent);
|
|
@@ -3179,6 +3266,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3179
3266
|
.meta-card table { display: block; max-width: 100%; overflow-x: auto; }
|
|
3180
3267
|
.artifact-doc { font-size: 15px; line-height: 1.7; }
|
|
3181
3268
|
.workspace-header { background: var(--panel); border-bottom: 1px solid var(--line); padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
3269
|
+
.workspace-brand-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
3182
3270
|
.workspace-brand { display: flex; align-items: center; gap: 10px; color: var(--text); font-size: 19px; font-weight: 700; letter-spacing: -.04em; }
|
|
3183
3271
|
.workspace-brand svg { width: 27px; height: 27px; color: var(--accent); }
|
|
3184
3272
|
.workspace-desktop { display: none; }
|
|
@@ -3188,13 +3276,13 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3188
3276
|
.workspace-mobile nav { padding-top: 16px; }
|
|
3189
3277
|
.workspace-primary, .workspace-actions { display: grid; gap: 4px; }
|
|
3190
3278
|
.workspace-primary { padding-bottom: 16px; border-bottom: 1px solid var(--line); margin-bottom: 16px; }
|
|
3191
|
-
.workspace-header nav a, .workspace-dropdown summary, .workspace-dropdown .nav-form button { display:
|
|
3279
|
+
.workspace-header nav a, .workspace-dropdown summary, .workspace-dropdown .nav-form button { display: flex; align-items: center; gap: 10px; min-height: 38px; padding: 8px 10px; color: var(--muted); border-radius: 6px; font-size: 14px; text-align: left; }
|
|
3192
3280
|
.workspace-header nav a:hover, .workspace-dropdown summary:hover { background: var(--panel-2); color: var(--text); text-decoration: none; }
|
|
3193
3281
|
.workspace-header [aria-current="page"], .workspace-header .is-current { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
3194
3282
|
.workspace-dropdown summary { cursor: pointer; list-style: none; }
|
|
3195
3283
|
.workspace-dropdown summary::-webkit-details-marker { display: none; }
|
|
3196
|
-
.workspace-
|
|
3197
|
-
.workspace-dropdown[open] > summary
|
|
3284
|
+
.workspace-chevron { margin-left: auto; display: inline-flex; }
|
|
3285
|
+
.workspace-dropdown[open] > summary .workspace-chevron { transform: rotate(180deg); }
|
|
3198
3286
|
.workspace-version { color: var(--faint); font-size: 12px; padding: 10px; }
|
|
3199
3287
|
.settings-page { max-width: 1100px; }
|
|
3200
3288
|
.settings-page .meta-card { gap: 16px; }
|
|
@@ -3204,11 +3292,56 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3204
3292
|
.settings-page .field { min-width: 0; }
|
|
3205
3293
|
.settings-page textarea[name="allowedCidrs"] { min-height: 120px; height: 140px; }
|
|
3206
3294
|
.settings-page .editor-form { gap: 12px; }
|
|
3295
|
+
.settings-page .user-role-form { display: flex; flex-wrap: nowrap; gap: 8px; align-items: center; }
|
|
3296
|
+
.user-account-actions { display: grid; gap: 8px; justify-items: start; }
|
|
3297
|
+
|
|
3298
|
+
.user-role-form select { width: auto; min-width: 105px; }
|
|
3299
|
+
.user-role-form select, .user-role-form button { margin: 0; }
|
|
3300
|
+
|
|
3301
|
+
.meta-card:has(> form[action="/admin/retention/run"]) { min-width: 0; overflow-wrap: anywhere; }
|
|
3302
|
+
form[action="/admin/retention/run"], form[action="/admin/retention/run"] > * { min-width: 0; }
|
|
3303
|
+
form[action="/admin/retention/run"] .checkbox-field { display: block; white-space: normal; overflow-wrap: anywhere; line-height: 1.6; }
|
|
3304
|
+
form[action="/admin/retention/run"] .checkbox-field input { margin-right: 6px; vertical-align: middle; }
|
|
3305
|
+
.retention-policy-form .retention-section { display: grid; gap: 16px; min-width: 0; margin: 0; padding: 16px; border: 1px solid var(--line); border-radius: 6px; }
|
|
3306
|
+
.retention-policy-form legend { padding: 0 6px; font-weight: 600; }
|
|
3307
|
+
.smtp-field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; align-items: start; }
|
|
3308
|
+
.smtp-settings-form input, .smtp-settings-form select { min-width: 0; }
|
|
3309
|
+
.smtp-settings-form .field { align-content: start; }
|
|
3310
|
+
@media (max-width: 640px) { .smtp-field-grid { grid-template-columns: minmax(0, 1fr); } }
|
|
3311
|
+
.retention-field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 16px; }
|
|
3312
|
+
.retention-policy-form .field { display: grid; gap: 8px; align-content: start; }
|
|
3313
|
+
.retention-policy-form input, .retention-policy-form textarea { width: 100%; min-width: 0; box-sizing: border-box; }
|
|
3314
|
+
.retention-policy-form textarea { min-height: 112px; height: 112px; }
|
|
3315
|
+
@media (max-width: 640px) { .retention-field-grid { grid-template-columns: minmax(0, 1fr); } }
|
|
3316
|
+
|
|
3207
3317
|
.workspace-dropdown-content { display: grid; padding-left: 10px; border-left: 1px solid var(--line); margin: 4px 0 8px 10px; }
|
|
3208
3318
|
.workspace-dropdown-content .nav-form { display: block; }
|
|
3209
3319
|
.workspace-identity { overflow-wrap: anywhere; font-size: 12px; color: var(--faint); padding: 8px 10px; }
|
|
3210
|
-
.
|
|
3211
|
-
.
|
|
3320
|
+
.theme-controls { border: 0; min-width: 0; margin: 16px 10px 0; padding: 0; }
|
|
3321
|
+
.theme-controls legend { color: var(--muted); font-size: 12px; margin-bottom: 6px; }
|
|
3322
|
+
.theme-choices { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); padding: 3px; gap: 2px; background: var(--panel-2); border-radius: 8px; }
|
|
3323
|
+
.theme-choices .btn { padding: 6px; min-width: 0; border: 0; display: inline-flex; justify-content: center; align-items: center; background: transparent; color: var(--muted); }
|
|
3324
|
+
.theme-choices .btn[aria-pressed="true"] { background: var(--panel); color: var(--accent); box-shadow: 0 1px 3px #0002; }
|
|
3325
|
+
.workspace-header svg { flex-shrink: 0; }
|
|
3326
|
+
.workspace-header .sidebar-toggle { display: none; }
|
|
3327
|
+
.sidebar-toggle > span:not([hidden]) { display: flex; align-items: center; }
|
|
3328
|
+
.workspace-brand-row svg { display: block; }
|
|
3329
|
+
@media (min-width: 1100px) {
|
|
3330
|
+
.sidebar-toggle:not([hidden]) { display: inline-flex; position: static; width: 36px; height: 36px; padding: 0; flex-shrink: 0; }
|
|
3331
|
+
.workspace-desktop { padding-top: 0; }
|
|
3332
|
+
:root[data-sidebar="collapsed"] body:has(> .workspace-header) { padding-left: 68px; }
|
|
3333
|
+
:root[data-sidebar="collapsed"] .workspace-header { width: 68px; padding: 24px 10px; }
|
|
3334
|
+
:root[data-sidebar="collapsed"] .workspace-brand { display: none; }
|
|
3335
|
+
:root[data-sidebar="collapsed"] .workspace-brand-row { justify-content: center; }
|
|
3336
|
+
|
|
3337
|
+
:root[data-sidebar="collapsed"] .workspace-nav-label, :root[data-sidebar="collapsed"] .workspace-brand-name, :root[data-sidebar="collapsed"] .workspace-chevron, :root[data-sidebar="collapsed"] .workspace-version, :root[data-sidebar="collapsed"] .workspace-identity, :root[data-sidebar="collapsed"] .language-switcher, :root[data-sidebar="collapsed"] .theme-controls { display: none; }
|
|
3338
|
+
:root[data-sidebar="collapsed"] .workspace-dropdown-content { padding-left: 0; border: 0; margin-left: 0; }
|
|
3339
|
+
:root[data-sidebar="collapsed"] .workspace-header nav a, :root[data-sidebar="collapsed"] .workspace-dropdown summary, :root[data-sidebar="collapsed"] .nav-form button { justify-content: center; padding: 10px; }
|
|
3340
|
+
:root[data-sidebar="collapsed"] .workspace-header [aria-current="page"] { box-shadow: inset 3px 0 var(--accent); }
|
|
3341
|
+
:root[data-sidebar="collapsed"] .workspace-header nav a:is(:hover, :focus-visible) .workspace-nav-label, :root[data-sidebar="collapsed"] .workspace-dropdown > summary:is(:hover, :focus-visible) .workspace-nav-label, :root[data-sidebar="collapsed"] .nav-form button:is(:hover, :focus-visible) .workspace-nav-label { display: block; position: fixed; left: 76px; z-index: 50; padding: 6px 10px; max-width: 240px; background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 6px; box-shadow: 0 3px 12px #0002; pointer-events: none; }
|
|
3342
|
+
}
|
|
3343
|
+
.workspace-header .language-switcher { margin: 12px 0 0; padding: 0 10px; font-size: 12px; }
|
|
3344
|
+
|
|
3212
3345
|
.workspace-skip { position: absolute; left: 16px; top: -100px; z-index: 100; background: var(--panel); padding: 8px 16px; }
|
|
3213
3346
|
.workspace-skip:focus { top: 8px; }
|
|
3214
3347
|
:focus-visible { outline: 2px solid var(--ring); outline-offset: 3px; }
|
|
@@ -3253,7 +3386,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3253
3386
|
@media (min-width: 1100px) {
|
|
3254
3387
|
body:has(> .workspace-header) { padding-left: 196px; }
|
|
3255
3388
|
.workspace-header { position: fixed; inset: 0 auto 0 0; width: 196px; padding: 24px 12px; border-bottom: 0; border-right: 1px solid var(--line); display: block; overflow-y: auto; z-index: 20; }
|
|
3256
|
-
.workspace-brand {
|
|
3389
|
+
.workspace-brand-row { min-height: 36px; margin-bottom: 18px; }
|
|
3390
|
+
.workspace-brand { padding: 0 0 0 10px; }
|
|
3257
3391
|
.workspace-desktop { display: block; }
|
|
3258
3392
|
.workspace-mobile { display: none; }
|
|
3259
3393
|
}
|
|
@@ -3281,6 +3415,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3281
3415
|
.dashboard .row-main strong { font-size: 14px; }
|
|
3282
3416
|
}
|
|
3283
3417
|
</style>
|
|
3418
|
+
<link rel="stylesheet" href="/assets/ui-controls.css">
|
|
3284
3419
|
</head>
|
|
3285
3420
|
<body>
|
|
3286
3421
|
${body}
|
|
@@ -3290,7 +3425,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3290
3425
|
}
|
|
3291
3426
|
|
|
3292
3427
|
function formatSelect(selected) {
|
|
3293
|
-
return `<select name="format">
|
|
3428
|
+
return `<select name="format" class="form-select">
|
|
3294
3429
|
${ARTIFACT_FORMATS.map((format) => {
|
|
3295
3430
|
const label = format[0].toUpperCase() + format.slice(1);
|
|
3296
3431
|
return `<option value="${format}"${format === selected ? " selected" : ""}>${label}</option>`;
|
|
@@ -3299,13 +3434,13 @@ function formatSelect(selected) {
|
|
|
3299
3434
|
}
|
|
3300
3435
|
|
|
3301
3436
|
function artifactTypeSelect(selected) {
|
|
3302
|
-
return `<select name="artifactType">
|
|
3437
|
+
return `<select name="artifactType" class="form-select">
|
|
3303
3438
|
${artifactTypeOptions(selected)}
|
|
3304
3439
|
</select>`;
|
|
3305
3440
|
}
|
|
3306
3441
|
|
|
3307
3442
|
function visibilitySelect(selected, view) {
|
|
3308
|
-
return `<select name="visibility">
|
|
3443
|
+
return `<select name="visibility" class="form-select">
|
|
3309
3444
|
${VISIBILITY_VALUES.map((value) => `<option value="${value}"${value === selected ? " selected" : ""}>${view.text(`artifact.visibility.${value}`)}</option>`).join("")}
|
|
3310
3445
|
</select>`;
|
|
3311
3446
|
}
|
|
@@ -3333,6 +3468,13 @@ function editorScript(locale) {
|
|
|
3333
3468
|
return `<script>window.ARTIFACTY_I18N=${config};</script><script type="module" src="${EDITOR_CLIENT_PATH}"></script>`;
|
|
3334
3469
|
}
|
|
3335
3470
|
|
|
3471
|
+
function renderDateTime(value, view) {
|
|
3472
|
+
const date = new Date(value);
|
|
3473
|
+
if (!Number.isFinite(date.getTime())) return escapeHtml(value);
|
|
3474
|
+
const label = new Intl.DateTimeFormat(view.locale, { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" }).format(date);
|
|
3475
|
+
return `<time datetime="${escapeAttribute(value)}" title="${escapeAttribute(value)}">${escapeHtml(label)} UTC</time>`;
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3336
3478
|
function viewContext(locale, currentPath) {
|
|
3337
3479
|
const i18n = createI18n(locale);
|
|
3338
3480
|
return {
|
|
@@ -3394,13 +3536,11 @@ function statusBadge(label) {
|
|
|
3394
3536
|
return `<span class="badge s-archived">${label}</span>`;
|
|
3395
3537
|
}
|
|
3396
3538
|
|
|
3397
|
-
//
|
|
3398
|
-
// common case doesn't add visual noise to every row.
|
|
3539
|
+
// Highlight explicit public/private visibility while keeping default team rows compact.
|
|
3399
3540
|
function visibilityBadge(artifact, view) {
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
}
|
|
3403
|
-
return `<span class="badge v-private">${view.text("artifact.visibility.private")}</span>`;
|
|
3541
|
+
const visibility = artifact.visibility || "team";
|
|
3542
|
+
if (!["private", "public"].includes(visibility)) return "";
|
|
3543
|
+
return `<span class="badge v-${visibility}">${view.text(`artifact.visibility.${visibility}`)}</span>`;
|
|
3404
3544
|
}
|
|
3405
3545
|
|
|
3406
3546
|
function htmlFrameContent(content) {
|
|
@@ -3494,37 +3634,50 @@ function frameResizeScript() {
|
|
|
3494
3634
|
return `<script>(function(){window.addEventListener("message",function(event){var data=event.data;if(!data||typeof data.__artifactyHeight!=="number"){return;}var frames=document.querySelectorAll(".artifact-frame");for(var i=0;i<frames.length;i+=1){if(frames[i].contentWindow===event.source){var height=Math.min(Math.max(Math.ceil(data.__artifactyHeight),200),200000);frames[i].style.height=height+"px";break;}}});})();</script>`;
|
|
3495
3635
|
}
|
|
3496
3636
|
|
|
3497
|
-
function viewerScript() {
|
|
3498
|
-
|
|
3637
|
+
function viewerScript(locale = DEFAULT_LOCALE) {
|
|
3638
|
+
const view = viewContext(locale, "/");
|
|
3639
|
+
const messages = Object.fromEntries(["filter", "filterColumn", "ruleFilter", "rowsShown", "resultsShown"].map((key) => [key, view.raw(`viewer.${key}`, { column: "{column}", visible: "{visible}", total: "{total}" })]));
|
|
3640
|
+
const config = JSON.stringify(messages).replaceAll("<", "\\u003c");
|
|
3641
|
+
return `<script type="application/json" id="artifacty-viewer-i18n">${config}</script><script type="module" src="${VIEWER_CLIENT_PATH}"></script>`;
|
|
3499
3642
|
}
|
|
3500
3643
|
|
|
3501
3644
|
function reactRendererEnabled() {
|
|
3502
3645
|
return process.env.ARTIFACTY_ENABLE_REACT_RENDERER === "true";
|
|
3503
3646
|
}
|
|
3504
3647
|
|
|
3648
|
+
function themeControls(view) {
|
|
3649
|
+
return `<fieldset class="theme-controls" data-theme-controls hidden><legend>${view.text("theme.label")}</legend>
|
|
3650
|
+
<div class="theme-choices">${["system", "light", "dark"].map((theme) => `<button type="button" data-theme-choice="${theme}" aria-pressed="${theme === "system"}" aria-label="${view.attr(`theme.${theme}`)}" title="${view.attr(`theme.${theme}`)}" class="btn btn-ghost">${uiIcon({ system: "device-desktop", light: "sun", dark: "moon" }[theme])}<span class="dashboard-sr-only">${view.text(`theme.${theme}`)}</span></button>`).join("")}</div>
|
|
3651
|
+
</fieldset>`;
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3505
3654
|
function languageSwitcher(view) {
|
|
3506
|
-
const
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
const
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
return `<
|
|
3655
|
+
const current = new URL(view.currentPath || "/", "http://artifacty.local");
|
|
3656
|
+
const preserved = [...current.searchParams.entries()].filter(([name]) => name !== "lang")
|
|
3657
|
+
.map(([name, value]) => `<input type="hidden" name="${escapeAttribute(name)}" value="${escapeAttribute(value)}">`).join("");
|
|
3658
|
+
const options = availableLanguages().filter((language) => language.enabled).map((language) =>
|
|
3659
|
+
`<option value="${escapeAttribute(language.locale)}"${language.locale === view.locale ? " selected" : ""}>${escapeHtml(language.nativeName)}</option>`
|
|
3660
|
+
).join("");
|
|
3661
|
+
return `<form class="language-switcher" data-auto-apply="all" method="get" action="${escapeAttribute(`${current.pathname}${current.hash}`)}">
|
|
3662
|
+
${preserved}<label><span>${view.text("language.label")}</span><select name="lang" class="form-select">${options}</select></label>
|
|
3663
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("language.apply")}</button>
|
|
3664
|
+
</form>`;
|
|
3513
3665
|
}
|
|
3514
3666
|
|
|
3515
3667
|
function workspaceHeader(user, view) {
|
|
3516
3668
|
const pathname = new URL(view.currentPath, "http://artifacty.local").pathname;
|
|
3517
|
-
const
|
|
3669
|
+
const icons = { "/": "files", "/inbox": "inbox", "/new": "plus", "/import": "upload", "/admin/access": "settings", "/admin/users": "users", "/admin/backup": "database-export", "/admin/retention": "archive", "/admin/webhooks": "webhook", "/admin/update": "refresh", "/admin/languages": "language", "/admin/email": "mail", "/account": "user-circle", "/account/password": "key", "/login": "login", "/docs/api": "code" };
|
|
3670
|
+
const link = (href, key, active = pathname === href, className = "") => `<a href="${view.href(href)}" title="${view.attr(key)}" aria-label="${view.attr(key)}"${active ? ' aria-current="page"' : ""}${className ? ` class="${className}"` : ""}>${uiIcon(icons[href] || "files")}<span class="workspace-nav-label">${view.text(key)}</span></a>`;
|
|
3518
3671
|
const artifactActive = pathname === "/" || pathname.startsWith("/artifacts/");
|
|
3519
|
-
const adminLinks = user?.role === "admin" ? `<details class="workspace-dropdown"${pathname.startsWith("/admin/") ? " open" : ""}><summary${pathname.startsWith("/admin/") ? ' class="is-current"' : ""}>${view.text("nav.administration")}</summary><div class="workspace-dropdown-content">
|
|
3520
|
-
${link("/admin/access", "access.title")}${link("/admin/users", "nav.users")}${link("/admin/backup", "nav.backup")}${link("/admin/retention", "retention.navLabel")}${link("/admin/webhooks", "nav.webhooks")}${link("/admin/update", "update.title")}<span class="workspace-version">Artifacty v${escapeHtml(APP_VERSION)}</span>
|
|
3672
|
+
const adminLinks = user?.role === "admin" ? `<details class="workspace-dropdown"${pathname.startsWith("/admin/") ? " open" : ""}><summary${pathname.startsWith("/admin/") ? ' class="is-current"' : ""} title="${view.attr("nav.administration")}" aria-label="${view.attr("nav.administration")}">${uiIcon("settings")}<span class="workspace-nav-label">${view.text("nav.administration")}</span><span class="workspace-chevron">${uiIcon("chevron-down", { size: 14 })}</span></summary><div class="workspace-dropdown-content">
|
|
3673
|
+
${link("/admin/access", "access.title")}${link("/admin/users", "nav.users")}${link("/admin/backup", "nav.backup")}${link("/admin/retention", "retention.navLabel")}${link("/admin/webhooks", "nav.webhooks")}${link("/admin/update", "update.title")}${link("/admin/languages", "languageAdmin.title")}${link("/admin/email", "smtp.title")}<span class="workspace-version">Artifacty v${escapeHtml(APP_VERSION)}</span>
|
|
3521
3674
|
</div></details>` : "";
|
|
3522
|
-
const account = user ? `<details class="workspace-dropdown"${pathname.startsWith("/account") ? " open" : ""}><summary${pathname.startsWith("/account") ? ' class="is-current"' : ""}>${view.text("nav.account")}</summary><div class="workspace-dropdown-content">
|
|
3675
|
+
const account = user ? `<details class="workspace-dropdown"${pathname.startsWith("/account") ? " open" : ""}><summary${pathname.startsWith("/account") ? ' class="is-current"' : ""} title="${view.attr("nav.account")}" aria-label="${view.attr("nav.account")}">${uiIcon("user-circle")}<span class="workspace-nav-label">${view.text("nav.account")}</span><span class="workspace-chevron">${uiIcon("chevron-down", { size: 14 })}</span></summary><div class="workspace-dropdown-content">
|
|
3523
3676
|
<span class="workspace-identity">${escapeHtml(user.name || user.email || "")}</span>${link("/account", "nav.accountTokens")}${link("/account/password", "nav.password")}
|
|
3524
|
-
<form method="post" action="/logout" class="nav-form">${localeInput(view.locale)}<button type="submit">${view.text("nav.signOut")}</button></form>
|
|
3677
|
+
<form method="post" action="/logout" class="nav-form">${localeInput(view.locale)}<button type="submit" title="${view.attr("nav.signOut")}" aria-label="${view.attr("nav.signOut")}" class="btn btn-ghost">${uiIcon("logout")}<span class="workspace-nav-label">${view.text("nav.signOut")}</span></button></form>
|
|
3525
3678
|
</div></details>` : user === null ? link("/login", "nav.signIn") : "";
|
|
3526
|
-
const links = `<div class="workspace-primary">${link("/", "nav.artifacts", artifactActive)}${user ? link("/inbox", "inbox.title") : ""}</div><div class="workspace-actions">${link("/new", "nav.new", pathname === "/new", "workspace-create")}${link("/import", "nav.import")}${adminLinks}${account}${link("/docs/api", "nav.apiDocs")}${languageSwitcher(view)}</div>`;
|
|
3527
|
-
return `<a class="workspace-skip" href="#main-content">${view.text("nav.skip")}</a><header class="workspace-header"><a class="workspace-brand" href="${view.href("/")}">${brandMark()}<span>artifacty</span></a>
|
|
3679
|
+
const links = `<div class="workspace-primary">${link("/", "nav.artifacts", artifactActive)}${user ? link("/inbox", "inbox.title") : ""}</div><div class="workspace-actions">${link("/new", "nav.new", pathname === "/new", "workspace-create")}${link("/import", "nav.import")}${adminLinks}${account}${link("/docs/api", "nav.apiDocs")}${languageSwitcher(view)}${themeControls(view)}</div>`;
|
|
3680
|
+
return `<a class="workspace-skip" href="#main-content">${view.text("nav.skip")}</a><header class="workspace-header"><div class="workspace-brand-row"><a class="workspace-brand" aria-label="Artifacty" href="${view.href("/")}">${brandMark()}<span class="workspace-brand-name">artifacty</span></a><button type="button" class="sidebar-toggle btn btn-ghost" data-sidebar-toggle hidden aria-expanded="true" aria-label="${view.attr("nav.collapseSidebar")}" title="${view.attr("nav.collapseSidebar")}" data-collapse-label="${view.attr("nav.collapseSidebar")}" data-expand-label="${view.attr("nav.expandSidebar")}"><span data-sidebar-collapse-icon>${uiIcon("layout-sidebar-left-collapse")}</span><span data-sidebar-expand-icon hidden>${uiIcon("layout-sidebar-left-expand")}</span></button></div>
|
|
3528
3681
|
<nav class="workspace-desktop" aria-label="${view.attr("dashboard.navigation")}">${links}</nav>
|
|
3529
3682
|
<details class="workspace-mobile"><summary>${view.text("dashboard.more")}</summary><nav aria-label="${view.attr("dashboard.navigation")}">${links}</nav></details>
|
|
3530
3683
|
</header>`;
|
|
@@ -3634,7 +3787,7 @@ function markdownToHtml(markdown, diagramState = createMermaidDiagramState()) {
|
|
|
3634
3787
|
inList = true;
|
|
3635
3788
|
}
|
|
3636
3789
|
const checked = taskItem[1].toLowerCase() === "x";
|
|
3637
|
-
html.push(`<li class="task-list-item"><label><input type="checkbox" disabled${checked ? " checked" : ""}> ${inlineMarkdown(taskItem[2])}</label></li>`);
|
|
3790
|
+
html.push(`<li class="task-list-item"><label><input type="checkbox" disabled${checked ? " checked" : ""} class="form-check-input"> ${inlineMarkdown(taskItem[2])}</label></li>`);
|
|
3638
3791
|
continue;
|
|
3639
3792
|
}
|
|
3640
3793
|
|
|
@@ -3769,19 +3922,20 @@ const CSV_RENDER_COLUMN_LIMIT = 80;
|
|
|
3769
3922
|
const SARIF_RENDER_RESULT_LIMIT = 500;
|
|
3770
3923
|
|
|
3771
3924
|
function renderMediaArtifact(format, content, metadata = {}, options = {}) {
|
|
3925
|
+
const view = options.view || viewContext(options.locale || DEFAULT_LOCALE, "/");
|
|
3772
3926
|
const mimeType = mediaMimeType(metadata, format);
|
|
3773
3927
|
const source = options.rawUrl || mediaDataUrl(content, mimeType);
|
|
3774
3928
|
if (!source) {
|
|
3775
3929
|
return `<section class="artifact-media-note">
|
|
3776
|
-
|
|
3930
|
+
${view.text("media.unavailable")}
|
|
3777
3931
|
</section>
|
|
3778
3932
|
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>`;
|
|
3779
3933
|
}
|
|
3780
3934
|
|
|
3781
3935
|
const label = [
|
|
3782
3936
|
mimeType || format,
|
|
3783
|
-
metadata.encoding ?
|
|
3784
|
-
metadata.originalEncoding ?
|
|
3937
|
+
metadata.encoding ? view.raw("media.encoding", { encoding: metadata.encoding }) : "",
|
|
3938
|
+
metadata.originalEncoding ? view.raw("media.source", { encoding: metadata.originalEncoding }) : ""
|
|
3785
3939
|
].filter(Boolean).join(" · ");
|
|
3786
3940
|
|
|
3787
3941
|
if (format === "video") {
|
|
@@ -3792,7 +3946,7 @@ function renderMediaArtifact(format, content, metadata = {}, options = {}) {
|
|
|
3792
3946
|
}
|
|
3793
3947
|
|
|
3794
3948
|
return `<figure class="artifact-media artifact-image">
|
|
3795
|
-
<img src="${escapeAttribute(source)}" alt="
|
|
3949
|
+
<img src="${escapeAttribute(source)}" alt="${view.text("media.image")}">
|
|
3796
3950
|
<figcaption>${escapeHtml(label || "image artifact")}</figcaption>
|
|
3797
3951
|
</figure>`;
|
|
3798
3952
|
}
|
|
@@ -3817,17 +3971,17 @@ function mediaDataUrl(content, mimeType) {
|
|
|
3817
3971
|
return `data:${mimeType};base64,${base64}`;
|
|
3818
3972
|
}
|
|
3819
3973
|
|
|
3820
|
-
function renderCsv(content) {
|
|
3974
|
+
function renderCsv(content, view) {
|
|
3821
3975
|
const parsed = parseCsv(content);
|
|
3822
3976
|
if (!parsed.ok) {
|
|
3823
3977
|
return `<section class="artifact-csv">
|
|
3824
|
-
<p class="artifact-csv-note"
|
|
3978
|
+
<p class="artifact-csv-note">${view.text("csv.error", { error: parsed.error })}</p>
|
|
3825
3979
|
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>
|
|
3826
3980
|
</section>`;
|
|
3827
3981
|
}
|
|
3828
3982
|
|
|
3829
3983
|
if (parsed.rows.length === 0) {
|
|
3830
|
-
return `<section class="artifact-csv"><p class="artifact-csv-note"
|
|
3984
|
+
return `<section class="artifact-csv"><p class="artifact-csv-note">${view.text("csv.empty")}</p></section>`;
|
|
3831
3985
|
}
|
|
3832
3986
|
|
|
3833
3987
|
const columnCount = Math.max(...parsed.rows.map((row) => row.length));
|
|
@@ -3837,7 +3991,7 @@ function renderCsv(content) {
|
|
|
3837
3991
|
const rowTruncated = parsed.rows.length - 1 > CSV_RENDER_ROW_LIMIT;
|
|
3838
3992
|
const columnTruncated = columnCount > CSV_RENDER_COLUMN_LIMIT;
|
|
3839
3993
|
const headerHtml = Array.from({ length: visibleColumns }, (_, index) =>
|
|
3840
|
-
`<th data-csv-col="${index}">${escapeHtml(header[index] ||
|
|
3994
|
+
`<th data-csv-col="${index}">${escapeHtml(header[index] || view.raw("csv.column", { column: index + 1 }))}</th>`
|
|
3841
3995
|
).join("");
|
|
3842
3996
|
const bodyHtml = bodyRows.map((row) =>
|
|
3843
3997
|
`<tr data-csv-row>${Array.from({ length: visibleColumns }, (_, index) =>
|
|
@@ -3845,10 +3999,10 @@ function renderCsv(content) {
|
|
|
3845
3999
|
).join("")}</tr>`
|
|
3846
4000
|
).join("\n");
|
|
3847
4001
|
const notes = [
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
rowTruncated ?
|
|
3851
|
-
columnTruncated ?
|
|
4002
|
+
view.raw("csv.rows", { count: parsed.rows.length - 1 }),
|
|
4003
|
+
view.raw("csv.columns", { count: columnCount }),
|
|
4004
|
+
rowTruncated ? view.raw("csv.firstRows", { count: CSV_RENDER_ROW_LIMIT }) : "",
|
|
4005
|
+
columnTruncated ? view.raw("csv.firstColumns", { count: CSV_RENDER_COLUMN_LIMIT }) : ""
|
|
3852
4006
|
].filter(Boolean).join(" · ");
|
|
3853
4007
|
|
|
3854
4008
|
return `<section class="artifact-csv" data-artifact-csv>
|
|
@@ -3862,20 +4016,20 @@ function renderCsv(content) {
|
|
|
3862
4016
|
</section>`;
|
|
3863
4017
|
}
|
|
3864
4018
|
|
|
3865
|
-
function renderSarif(content) {
|
|
4019
|
+
function renderSarif(content, view) {
|
|
3866
4020
|
let sarif;
|
|
3867
4021
|
try {
|
|
3868
4022
|
sarif = JSON.parse(content);
|
|
3869
4023
|
} catch {
|
|
3870
4024
|
return `<section class="artifact-sarif">
|
|
3871
|
-
<p class="artifact-sarif-note"
|
|
4025
|
+
<p class="artifact-sarif-note">${view.text("sarif.invalid")}</p>
|
|
3872
4026
|
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>
|
|
3873
4027
|
</section>`;
|
|
3874
4028
|
}
|
|
3875
4029
|
|
|
3876
4030
|
if (!isSarifDocument(sarif)) {
|
|
3877
4031
|
return `<section class="artifact-sarif">
|
|
3878
|
-
<p class="artifact-sarif-note"
|
|
4032
|
+
<p class="artifact-sarif-note">${view.text("sarif.shape")}</p>
|
|
3879
4033
|
<pre class="artifact-code"><code>${escapeHtml(formatJson(content))}</code></pre>
|
|
3880
4034
|
</section>`;
|
|
3881
4035
|
}
|
|
@@ -3891,26 +4045,26 @@ function renderSarif(content) {
|
|
|
3891
4045
|
<td>${escapeHtml(result.tool)}</td>
|
|
3892
4046
|
</tr>`).join("\n");
|
|
3893
4047
|
const note = results.length > SARIF_RENDER_RESULT_LIMIT
|
|
3894
|
-
?
|
|
3895
|
-
:
|
|
4048
|
+
? view.raw("sarif.first", { count: SARIF_RENDER_RESULT_LIMIT, total: results.length })
|
|
4049
|
+
: view.raw("sarif.results", { count: results.length });
|
|
3896
4050
|
|
|
3897
4051
|
return `<section class="artifact-sarif" data-artifact-sarif>
|
|
3898
4052
|
<div class="artifact-summary-grid">
|
|
3899
|
-
${summaryCard("
|
|
3900
|
-
${summaryCard("
|
|
3901
|
-
${summaryCard("
|
|
3902
|
-
${summaryCard("
|
|
3903
|
-
${summaryCard("
|
|
4053
|
+
${summaryCard(view.raw("sarif.runs"), sarif.runs.length)}
|
|
4054
|
+
${summaryCard(view.raw("sarif.resultsTitle"), results.length)}
|
|
4055
|
+
${summaryCard(view.raw("sarif.errors"), counts.error || 0)}
|
|
4056
|
+
${summaryCard(view.raw("sarif.warnings"), counts.warning || 0)}
|
|
4057
|
+
${summaryCard(view.raw("sarif.notes"), counts.note || 0)}
|
|
3904
4058
|
</div>
|
|
3905
4059
|
<p class="artifact-sarif-note">${escapeHtml(note)}</p>
|
|
3906
4060
|
<div class="artifact-table-scroll">
|
|
3907
4061
|
<table class="artifact-table" data-sarif-table>
|
|
3908
|
-
<thead><tr><th data-sarif-sort="level"
|
|
4062
|
+
<thead><tr><th data-sarif-sort="level">${view.text("sarif.level")}</th><th data-sarif-sort="rule">${view.text("sarif.rule")}</th><th>${view.text("sarif.message")}</th><th data-sarif-sort="location">${view.text("sarif.location")}</th><th>${view.text("sarif.tool")}</th></tr></thead>
|
|
3909
4063
|
<tbody>${rowHtml}</tbody>
|
|
3910
4064
|
</table>
|
|
3911
4065
|
</div>
|
|
3912
4066
|
<details class="artifact-raw-details">
|
|
3913
|
-
<summary
|
|
4067
|
+
<summary>${view.text("sarif.raw")}</summary>
|
|
3914
4068
|
<pre class="artifact-code"><code>${escapeHtml(formatJson(content))}</code></pre>
|
|
3915
4069
|
</details>
|
|
3916
4070
|
</section>`;
|
|
@@ -3991,7 +4145,7 @@ const NOTEBOOK_OUTPUT_MIME_PRIORITY = [
|
|
|
3991
4145
|
"text/plain"
|
|
3992
4146
|
];
|
|
3993
4147
|
|
|
3994
|
-
function renderNotebook(content) {
|
|
4148
|
+
function renderNotebook(content, view) {
|
|
3995
4149
|
let notebook;
|
|
3996
4150
|
try {
|
|
3997
4151
|
notebook = JSON.parse(content);
|
|
@@ -4006,16 +4160,16 @@ function renderNotebook(content) {
|
|
|
4006
4160
|
const diagramState = createMermaidDiagramState();
|
|
4007
4161
|
const cells = notebook.cells;
|
|
4008
4162
|
const rendered = cells.slice(0, NOTEBOOK_MAX_RENDERED_CELLS)
|
|
4009
|
-
.map((cell) => renderNotebookCell(cell, language, diagramState))
|
|
4163
|
+
.map((cell) => renderNotebookCell(cell, language, diagramState, view))
|
|
4010
4164
|
.join("");
|
|
4011
4165
|
const notice = cells.length > NOTEBOOK_MAX_RENDERED_CELLS
|
|
4012
|
-
? `<p class="artifact-notebook-notice"
|
|
4166
|
+
? `<p class="artifact-notebook-notice">${view.text("notebook.cells", { count: NOTEBOOK_MAX_RENDERED_CELLS, total: cells.length })}</p>`
|
|
4013
4167
|
: "";
|
|
4014
4168
|
|
|
4015
4169
|
return `<article class="artifact-doc artifact-notebook">${notice}${rendered}</article>`;
|
|
4016
4170
|
}
|
|
4017
4171
|
|
|
4018
|
-
function renderNotebookCell(cell, language, diagramState) {
|
|
4172
|
+
function renderNotebookCell(cell, language, diagramState, view) {
|
|
4019
4173
|
const cellType = cell?.cell_type;
|
|
4020
4174
|
const source = notebookText(cell?.source);
|
|
4021
4175
|
|
|
@@ -4034,9 +4188,9 @@ function renderNotebookCell(cell, language, diagramState) {
|
|
|
4034
4188
|
const cellOutputs = Array.isArray(cell?.outputs) ? cell.outputs : [];
|
|
4035
4189
|
const visibleOutputs = cellOutputs.slice(0, NOTEBOOK_MAX_OUTPUTS_PER_CELL);
|
|
4036
4190
|
const outputsNotice = cellOutputs.length > NOTEBOOK_MAX_OUTPUTS_PER_CELL
|
|
4037
|
-
? `<p class="artifact-notebook-notice"
|
|
4191
|
+
? `<p class="artifact-notebook-notice">${view.text("notebook.outputs", { count: NOTEBOOK_MAX_OUTPUTS_PER_CELL, total: cellOutputs.length })}</p>`
|
|
4038
4192
|
: "";
|
|
4039
|
-
const outputs = visibleOutputs.map((output) => renderNotebookOutput(output, diagramState)).join("") + outputsNotice;
|
|
4193
|
+
const outputs = visibleOutputs.map((output) => renderNotebookOutput(output, diagramState, view)).join("") + outputsNotice;
|
|
4040
4194
|
return `<section class="artifact-notebook-cell artifact-notebook-cell-code">
|
|
4041
4195
|
<div class="artifact-notebook-execcount">${escapeHtml(executionLabel)}</div>
|
|
4042
4196
|
<pre><code${classAttribute}>${escapeHtml(source)}</code></pre>
|
|
@@ -4055,11 +4209,11 @@ function stripAnsiEscapes(value) {
|
|
|
4055
4209
|
return String(value ?? "").replace(ANSI_ESCAPE_PATTERN, "");
|
|
4056
4210
|
}
|
|
4057
4211
|
|
|
4058
|
-
function renderNotebookOutput(output, diagramState) {
|
|
4212
|
+
function renderNotebookOutput(output, diagramState, view) {
|
|
4059
4213
|
const outputType = output?.output_type;
|
|
4060
4214
|
|
|
4061
4215
|
if (outputType === "stream") {
|
|
4062
|
-
return renderNotebookTextOutput(notebookText(output.text));
|
|
4216
|
+
return renderNotebookTextOutput(notebookText(output.text), "", view);
|
|
4063
4217
|
}
|
|
4064
4218
|
|
|
4065
4219
|
if (outputType === "error") {
|
|
@@ -4073,18 +4227,18 @@ function renderNotebookOutput(output, diagramState) {
|
|
|
4073
4227
|
: notebookText(output.traceback);
|
|
4074
4228
|
const traceback = stripAnsiEscapes(rawTraceback) ||
|
|
4075
4229
|
`${notebookText(output.ename)}: ${notebookText(output.evalue)}`;
|
|
4076
|
-
return renderNotebookTextOutput(traceback, "artifact-notebook-error");
|
|
4230
|
+
return renderNotebookTextOutput(traceback, "artifact-notebook-error", view);
|
|
4077
4231
|
}
|
|
4078
4232
|
|
|
4079
4233
|
if (outputType === "execute_result" || outputType === "display_data") {
|
|
4080
|
-
return renderNotebookMimeBundle(output.data, diagramState);
|
|
4234
|
+
return renderNotebookMimeBundle(output.data, diagramState, view);
|
|
4081
4235
|
}
|
|
4082
4236
|
|
|
4083
4237
|
return "";
|
|
4084
4238
|
}
|
|
4085
4239
|
|
|
4086
|
-
function renderNotebookTextOutput(text, extraClass = "") {
|
|
4087
|
-
const truncated = notebookTruncateIfOversized(text);
|
|
4240
|
+
function renderNotebookTextOutput(text, extraClass = "", view) {
|
|
4241
|
+
const truncated = notebookTruncateIfOversized(text, view);
|
|
4088
4242
|
if (truncated) {
|
|
4089
4243
|
return truncated;
|
|
4090
4244
|
}
|
|
@@ -4092,7 +4246,7 @@ function renderNotebookTextOutput(text, extraClass = "") {
|
|
|
4092
4246
|
return `<pre class="artifact-notebook-output artifact-code${classAttribute}"><code>${escapeHtml(text)}</code></pre>`;
|
|
4093
4247
|
}
|
|
4094
4248
|
|
|
4095
|
-
function renderNotebookMimeBundle(data, diagramState) {
|
|
4249
|
+
function renderNotebookMimeBundle(data, diagramState, view) {
|
|
4096
4250
|
const bundle = data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
4097
4251
|
const keys = Object.keys(bundle);
|
|
4098
4252
|
if (keys.length === 0) {
|
|
@@ -4101,11 +4255,11 @@ function renderNotebookMimeBundle(data, diagramState) {
|
|
|
4101
4255
|
|
|
4102
4256
|
const mime = NOTEBOOK_OUTPUT_MIME_PRIORITY.find((candidate) => keys.includes(candidate));
|
|
4103
4257
|
if (!mime) {
|
|
4104
|
-
return `<p class="artifact-notebook-output-placeholder"
|
|
4258
|
+
return `<p class="artifact-notebook-output-placeholder">${view.text("notebook.unsupported", { type: keys[0] })}</p>`;
|
|
4105
4259
|
}
|
|
4106
4260
|
|
|
4107
4261
|
const value = notebookText(bundle[mime]);
|
|
4108
|
-
const truncated = notebookTruncateIfOversized(value);
|
|
4262
|
+
const truncated = notebookTruncateIfOversized(value, view);
|
|
4109
4263
|
if (truncated) {
|
|
4110
4264
|
return truncated;
|
|
4111
4265
|
}
|
|
@@ -4119,9 +4273,9 @@ function renderNotebookMimeBundle(data, diagramState) {
|
|
|
4119
4273
|
if (mime === "image/png" || mime === "image/jpeg") {
|
|
4120
4274
|
const dataUrl = mediaDataUrl(value, mime);
|
|
4121
4275
|
if (!dataUrl) {
|
|
4122
|
-
return `<p class="artifact-notebook-output-placeholder"
|
|
4276
|
+
return `<p class="artifact-notebook-output-placeholder">${view.text("notebook.unsupported", { type: mime })}</p>`;
|
|
4123
4277
|
}
|
|
4124
|
-
return `<figure class="artifact-media artifact-image artifact-notebook-output"><img src="${escapeAttribute(dataUrl)}" alt="
|
|
4278
|
+
return `<figure class="artifact-media artifact-image artifact-notebook-output"><img src="${escapeAttribute(dataUrl)}" alt="${view.text("notebook.image")}"></figure>`;
|
|
4125
4279
|
}
|
|
4126
4280
|
if (mime === "image/svg+xml") {
|
|
4127
4281
|
return `<iframe class="artifact-frame artifact-svg-frame artifact-notebook-output" sandbox srcdoc="${escapeAttribute(svgFrameContent(value))}"></iframe>`;
|
|
@@ -4132,12 +4286,12 @@ function renderNotebookMimeBundle(data, diagramState) {
|
|
|
4132
4286
|
return "";
|
|
4133
4287
|
}
|
|
4134
4288
|
|
|
4135
|
-
function notebookTruncateIfOversized(value) {
|
|
4289
|
+
function notebookTruncateIfOversized(value, view) {
|
|
4136
4290
|
const size = Buffer.byteLength(String(value ?? ""), "utf8");
|
|
4137
4291
|
if (size <= NOTEBOOK_MAX_OUTPUT_BYTES) {
|
|
4138
4292
|
return null;
|
|
4139
4293
|
}
|
|
4140
|
-
return `<p class="artifact-notebook-output-truncated"
|
|
4294
|
+
return `<p class="artifact-notebook-output-truncated">${view.text("notebook.truncated", { size })}</p>`;
|
|
4141
4295
|
}
|
|
4142
4296
|
|
|
4143
4297
|
function notebookText(value) {
|