artifacty 0.15.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/network-sharing.md +17 -0
- package/docs/ko/smtp.md +27 -0
- package/docs/language-packs.md +38 -0
- package/docs/network-sharing.md +18 -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 +501 -11
- package/src/lib/language-settings.js +114 -0
- package/src/lib/network-policy.js +7 -6
- package/src/lib/openapi.js +93 -74
- package/src/lib/render.js +553 -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 +224 -19
- 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,42 @@ 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
|
-
<
|
|
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
|
+
<label><input type="checkbox" name="allowSharedLinks" value="true"${networkPolicy.allowSharedLinks ? " checked" : ""} class="form-check-input" aria-describedby="shared-network-help"> ${view.text("access.allowSharedLinks")}</label>
|
|
904
|
+
<p id="shared-network-help" class="muted">${view.text("access.sharedNetworkHelp")}</p>
|
|
905
|
+
<button type="submit" class="btn btn-primary">${view.text("access.saveNetwork")}</button>
|
|
796
906
|
</form>
|
|
797
907
|
</section>
|
|
798
908
|
<section class="meta-card"><h2>${view.text("access.teams")}</h2>
|
|
799
909
|
<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>
|
|
910
|
+
<label class="field"><span>${view.text("access.teamName")}</span><input name="name" required class="form-control"></label>
|
|
911
|
+
<button type="submit" class="btn btn-primary">${view.text("access.createTeam")}</button>
|
|
802
912
|
</form>
|
|
803
913
|
${teams.length ? teams.map((team) => `<section><h3>${escapeHtml(team.name)}</h3>
|
|
804
914
|
<form class="editor-form" method="post" action="/admin/teams/${encodeURIComponent(team.id)}/members">${localeInput(view.locale)}
|
|
805
915
|
<fieldset><legend>${view.text("access.members")}</legend>${accessCheckboxes(users, "userIds", team.memberIds || [], view)}</fieldset>
|
|
806
|
-
<button type="submit">${view.text("access.saveMembers")}</button>
|
|
916
|
+
<button type="submit" class="btn btn-primary">${view.text("access.saveMembers")}</button>
|
|
807
917
|
</form></section>`).join("") : `<p class="muted">${view.text("access.noTeams")}</p>`}
|
|
808
918
|
</section>
|
|
809
919
|
<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>
|
|
920
|
+
${["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
921
|
</main>`
|
|
812
922
|
});
|
|
813
923
|
}
|
|
@@ -825,7 +935,7 @@ export function renderArtifactSharingPage({ baseUrl, user, artifact, sharing = {
|
|
|
825
935
|
<form class="editor-form" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/sharing">${localeInput(view.locale)}
|
|
826
936
|
<fieldset><legend>${view.text("sharing.users")}</legend>${accessCheckboxes(users, "userIds", sharing.userIds || [], view, sharing.userPermissions || {})}</fieldset>
|
|
827
937
|
<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>
|
|
938
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("sharing.save")}</button>
|
|
829
939
|
</form>
|
|
830
940
|
</section>
|
|
831
941
|
${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 +943,17 @@ export function renderArtifactSharingPage({ baseUrl, user, artifact, sharing = {
|
|
|
833
943
|
<section class="meta-card"><h2>${view.text("sharing.links")}</h2><p>${view.text("sharing.linksHelp")}</p>
|
|
834
944
|
${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
945
|
<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("")}
|
|
946
|
+
<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>
|
|
947
|
+
<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>
|
|
948
|
+
${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>
|
|
949
|
+
${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
950
|
</section></main>`
|
|
841
951
|
});
|
|
842
952
|
}
|
|
843
953
|
|
|
844
954
|
function accessCheckboxes(items, name, selectedIds, view, permissions = null) {
|
|
845
955
|
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("");
|
|
956
|
+
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
957
|
}
|
|
848
958
|
|
|
849
959
|
export function renderInboxPage({ baseUrl, user, notifications = [], unreadCount = 0, locale = DEFAULT_LOCALE, currentPath = "/inbox" }) {
|
|
@@ -852,9 +962,9 @@ export function renderInboxPage({ baseUrl, user, notifications = [], unreadCount
|
|
|
852
962
|
body: `${workspaceHeader(user, view)}
|
|
853
963
|
<header class="topbar"><div><h1>${view.text("inbox.title")}</h1><p>${escapeHtml(baseUrl)}</p></div></header>
|
|
854
964
|
<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>`}
|
|
965
|
+
<form method="post" action="/inbox/read-all">${localeInput(view.locale)}<button type="submit" class="btn btn-outline-secondary">${view.text("inbox.readAll")}</button></form>
|
|
966
|
+
${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>
|
|
967
|
+
${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
968
|
</section></main>` });
|
|
859
969
|
}
|
|
860
970
|
|
|
@@ -873,31 +983,38 @@ export function renderSharedArtifactPage({ artifact, version, content, locale =
|
|
|
873
983
|
return pageShell({ title: artifact.title, locale: view.locale,
|
|
874
984
|
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
985
|
head: needsViewer ? editorHead() : "",
|
|
876
|
-
afterBody: `${frameResizeScript()}${needsViewer ? viewerScript() : ""}` });
|
|
986
|
+
afterBody: `${frameResizeScript()}${needsViewer ? viewerScript(view.locale) : ""}` });
|
|
877
987
|
}
|
|
878
988
|
|
|
879
|
-
export function renderAdminUsersPage({ baseUrl, user, users = [], importResult = null, importError = "", locale = DEFAULT_LOCALE, currentPath = "/admin/users" }) {
|
|
989
|
+
export function renderAdminUsersPage({ baseUrl, user, users = [], importResult = null, importError = "", roleError = "", resetResult = null, resetError = "", locale = DEFAULT_LOCALE, currentPath = "/admin/users" }) {
|
|
880
990
|
const view = viewContext(locale, currentPath);
|
|
881
991
|
const rows = users.map((item) => `
|
|
882
992
|
<tr>
|
|
883
993
|
<td>${escapeHtml(item.email)}</td>
|
|
884
994
|
<td>${escapeHtml(item.name)}</td>
|
|
885
|
-
<td
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
995
|
+
<td><form class="user-role-form" method="post" action="/admin/users/${encodeURIComponent(item.id)}/role">${localeInput(view.locale)}
|
|
996
|
+
<select name="role" aria-label="${view.text("users.roleFor", { email: item.email })}" class="form-select">
|
|
997
|
+
${["user", "admin"].map((role) => `<option value="${role}"${item.role === role ? " selected" : ""}>${view.text(`ui.role.${role}`)}</option>`).join("")}
|
|
998
|
+
</select><button type="submit" aria-label="${view.text("users.saveRoleFor", { email: item.email })}" class="btn btn-outline-secondary">${view.text("users.saveRole")}</button>
|
|
999
|
+
</form></td>
|
|
1000
|
+
<td>${item.active ? view.text("ui.active") : view.text("ui.disabled")}${item.passwordResetRequired ? ` · ${view.text("users.resetRequired")}` : ""}</td>
|
|
1001
|
+
<td>${renderDateTime(item.createdAt, view)}</td>
|
|
1002
|
+
<td><div class="user-account-actions">
|
|
1003
|
+
<form method="post" action="/admin/users/${encodeURIComponent(item.id)}/reset-password">${localeInput(view.locale)}
|
|
1004
|
+
<button type="submit" aria-label="${view.text("users.resetFor", { email: item.email })}" class="btn btn-ghost">${view.text("users.resetPassword")}</button>
|
|
1005
|
+
</form>
|
|
1006
|
+
${item.id === user.id ? "" : `<form method="post" action="/admin/users/${encodeURIComponent(item.id)}/${item.active ? "disable" : "enable"}">${localeInput(view.locale)}
|
|
1007
|
+
<button type="submit" class="btn btn-outline-danger">${item.active ? view.text("ui.disable") : view.text("ui.enable")}</button>
|
|
891
1008
|
</form>`}
|
|
892
|
-
</td>
|
|
1009
|
+
</div></td>
|
|
893
1010
|
</tr>
|
|
894
1011
|
`).join("");
|
|
895
1012
|
const createdRows = (importResult?.created || []).map((item) => `
|
|
896
1013
|
<tr>
|
|
897
1014
|
<td>${escapeHtml(item.user.email)}</td>
|
|
898
|
-
<td>${
|
|
899
|
-
<td>${item.user.passwordResetRequired ? "
|
|
900
|
-
<td>${item.passwordGenerated ? `<code>${escapeHtml(item.temporaryPassword)}</code>` : "
|
|
1015
|
+
<td>${view.text(`ui.role.${item.user.role}`)}</td>
|
|
1016
|
+
<td>${item.user.passwordResetRequired ? view.text("ui.yes") : view.text("ui.no")}</td>
|
|
1017
|
+
<td>${item.passwordGenerated ? `<code>${escapeHtml(item.temporaryPassword)}</code>` : view.text("users.provided")}</td>
|
|
901
1018
|
</tr>
|
|
902
1019
|
`).join("");
|
|
903
1020
|
const skippedRows = (importResult?.skipped || []).map((item) => `
|
|
@@ -908,67 +1025,76 @@ export function renderAdminUsersPage({ baseUrl, user, users = [], importResult =
|
|
|
908
1025
|
`).join("");
|
|
909
1026
|
const importSummary = importResult ? `
|
|
910
1027
|
<section class="meta-card">
|
|
911
|
-
<h2
|
|
912
|
-
<p class="muted">${importResult.created.length
|
|
1028
|
+
<h2>${view.text("users.importResults")}</h2>
|
|
1029
|
+
<p class="muted">${view.text("users.summary", { created: importResult.created.length, skipped: importResult.skipped.length, failed: importResult.failed.length })}</p>
|
|
913
1030
|
${createdRows ? `<table class="data-table">
|
|
914
|
-
<thead><tr><th
|
|
1031
|
+
<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
1032
|
<tbody>${createdRows}</tbody>
|
|
916
1033
|
</table>` : ""}
|
|
917
|
-
${skippedRows ? `<h3
|
|
918
|
-
<thead><tr><th
|
|
1034
|
+
${skippedRows ? `<h3>${view.text("users.skipped")}</h3><table class="data-table">
|
|
1035
|
+
<thead><tr><th>${view.text("ui.row")}</th><th>${view.text("ui.email")}</th><th>${view.text("ui.reason")}</th></tr></thead>
|
|
919
1036
|
<tbody>${skippedRows}</tbody>
|
|
920
1037
|
</table>` : ""}
|
|
921
|
-
${failedRows ? `<h3
|
|
922
|
-
<thead><tr><th
|
|
1038
|
+
${failedRows ? `<h3>${view.text("users.failed")}</h3><table class="data-table">
|
|
1039
|
+
<thead><tr><th>${view.text("ui.row")}</th><th>${view.text("ui.email")}</th><th>${view.text("ui.error")}</th></tr></thead>
|
|
923
1040
|
<tbody>${failedRows}</tbody>
|
|
924
1041
|
</table>` : ""}
|
|
925
1042
|
</section>
|
|
926
1043
|
` : "";
|
|
927
1044
|
|
|
928
1045
|
return pageShell({
|
|
929
|
-
title: "
|
|
1046
|
+
title: view.raw("nav.users"),
|
|
930
1047
|
body: `
|
|
931
1048
|
${workspaceHeader(user, view)}
|
|
932
1049
|
<header class="topbar">
|
|
933
1050
|
<div>
|
|
934
|
-
<h1
|
|
1051
|
+
<h1>${view.text("nav.users")}</h1>
|
|
935
1052
|
<p>${escapeHtml(baseUrl)}</p>
|
|
936
1053
|
</div>
|
|
937
1054
|
|
|
938
1055
|
</header>
|
|
939
1056
|
<main id="main-content" class="settings-page artifact-view">
|
|
1057
|
+
${resetError ? `<p class="auth-error" role="alert">${escapeHtml(resetError)}</p>` : ""}
|
|
1058
|
+
${resetResult ? `<section class="token-once" role="status"><h2>${view.text("users.resetReady")}</h2>
|
|
1059
|
+
<p>${view.text("users.resetAccount", { email: resetResult.user.email })}</p>
|
|
1060
|
+
<pre class="artifact-code"><code>${escapeHtml(resetResult.temporaryPassword)}</code></pre>
|
|
1061
|
+
<p>${view.text("users.resetHelp")}</p>
|
|
1062
|
+
<p>${view.text(`smtp.mailStatus.${["sent", "failed", "not-configured"].includes(resetResult.mailStatus) ? resetResult.mailStatus : "not-configured"}`)}</p>
|
|
1063
|
+
</section>` : ""}
|
|
940
1064
|
<section class="meta-card">
|
|
941
|
-
<h2
|
|
1065
|
+
<h2>${view.text("users.create")}</h2>
|
|
942
1066
|
<form class="editor-form auth-form" method="post" action="/admin/users">
|
|
943
1067
|
<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">
|
|
1068
|
+
<label class="field"><span>${view.text("ui.email")}</span><input type="email" name="email" required class="form-control"></label>
|
|
1069
|
+
<label class="field"><span>${view.text("ui.name")}</span><input name="name" class="form-control"></label>
|
|
1070
|
+
<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>
|
|
1071
|
+
<label class="field"><span>${view.text("ui.password")}</span><input type="password" name="password" minlength="8" required class="form-control"></label>
|
|
1072
|
+
<label class="check-field"><input type="checkbox" name="passwordResetRequired" class="form-check-input"> ${view.text("users.requirePassword")}</label>
|
|
949
1073
|
</section>
|
|
950
|
-
<footer class="editor-actions"><button type="submit"
|
|
1074
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-primary">${view.text("users.create")}</button></footer>
|
|
951
1075
|
</form>
|
|
952
1076
|
</section>
|
|
953
1077
|
<section class="meta-card">
|
|
954
|
-
<h2
|
|
955
|
-
<p class="muted"
|
|
1078
|
+
<h2>${view.text("users.importTitle")}</h2>
|
|
1079
|
+
<p class="muted">${view.text("users.csvHelp")}</p>
|
|
956
1080
|
${importError ? `<p class="auth-error">${escapeHtml(importError)}</p>` : ""}
|
|
957
1081
|
<form class="editor-form" method="post" action="/admin/users/import">
|
|
958
1082
|
<label class="field content-field">
|
|
959
1083
|
<span>CSV</span>
|
|
960
|
-
<textarea class="compact-textarea" name="csv" spellcheck="false" placeholder="email,name,role user@example.com,User,user" required></textarea>
|
|
1084
|
+
<textarea class="compact-textarea form-control" name="csv" spellcheck="false" placeholder="email,name,role user@example.com,User,user" required></textarea>
|
|
961
1085
|
</label>
|
|
962
|
-
<footer class="editor-actions"><button type="submit"
|
|
1086
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-outline-secondary">${view.text("users.import")}</button></footer>
|
|
963
1087
|
</form>
|
|
964
1088
|
</section>
|
|
965
1089
|
${importSummary}
|
|
966
1090
|
<section class="meta-card">
|
|
967
|
-
<h2
|
|
968
|
-
|
|
969
|
-
|
|
1091
|
+
<h2>${view.text("users.existing")}</h2>
|
|
1092
|
+
${roleError ? `<p class="auth-error" role="alert">${escapeHtml(roleError)}</p>` : ""}
|
|
1093
|
+
<p class="muted">${view.text("users.lastAdmin")}</p>
|
|
1094
|
+
<div class="artifact-table-scroll"><table class="data-table">
|
|
1095
|
+
<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
1096
|
<tbody>${rows}</tbody>
|
|
971
|
-
</table>
|
|
1097
|
+
</table></div>
|
|
972
1098
|
</section>
|
|
973
1099
|
</main>
|
|
974
1100
|
`,
|
|
@@ -987,7 +1113,7 @@ export function renderAdminWebhooksPage({ user, baseUrl, webhooks = [], createdS
|
|
|
987
1113
|
<td>${webhook.failureCount}</td>
|
|
988
1114
|
<td>
|
|
989
1115
|
<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>
|
|
1116
|
+
<button type="submit" class="btn btn-outline-danger">${view.text("webhooks.delete")}</button>
|
|
991
1117
|
</form>
|
|
992
1118
|
</td>
|
|
993
1119
|
</tr>
|
|
@@ -1019,10 +1145,10 @@ export function renderAdminWebhooksPage({ user, baseUrl, webhooks = [], createdS
|
|
|
1019
1145
|
<h2>${view.text("webhooks.createTitle")}</h2>
|
|
1020
1146
|
<form class="editor-form" method="post" action="/admin/webhooks">
|
|
1021
1147
|
<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>
|
|
1148
|
+
<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>
|
|
1149
|
+
<label class="field"><span>${view.text("webhooks.eventTypes")}</span><input name="eventTypes" placeholder="${view.text("webhooks.eventTypesPlaceholder")}" class="form-control"></label>
|
|
1024
1150
|
</section>
|
|
1025
|
-
<footer class="editor-actions"><button type="submit">${view.text("webhooks.create")}</button></footer>
|
|
1151
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-outline-secondary">${view.text("webhooks.create")}</button></footer>
|
|
1026
1152
|
</form>
|
|
1027
1153
|
</section>
|
|
1028
1154
|
<section class="meta-card">
|
|
@@ -1049,7 +1175,7 @@ export function renderAdminRetentionPage({ user, baseUrl, policy, lastSweep = nu
|
|
|
1049
1175
|
<h2>${view.text("retention.lastSweepTitle")}</h2>
|
|
1050
1176
|
<table class="data-table">
|
|
1051
1177
|
<tbody>
|
|
1052
|
-
<tr><th>${view.text("retention.sweepAt")}</th><td>${
|
|
1178
|
+
<tr><th>${view.text("retention.sweepAt")}</th><td>${renderDateTime(lastSweep.createdAt, view)}</td></tr>
|
|
1053
1179
|
<tr><th>${view.text("retention.archived")}</th><td>${escapeHtml(lastSweep.metadata?.archived ?? 0)}</td></tr>
|
|
1054
1180
|
<tr><th>${view.text("retention.purged")}</th><td>${escapeHtml(lastSweep.metadata?.purged ?? 0)}</td></tr>
|
|
1055
1181
|
<tr><th>${view.text("retention.purgeSkipped")}</th><td>${lastSweep.metadata?.purgeSkipped ? view.text("retention.yes") : view.text("retention.no")}</td></tr>
|
|
@@ -1099,16 +1225,24 @@ export function renderAdminRetentionPage({ user, baseUrl, policy, lastSweep = nu
|
|
|
1099
1225
|
<main id="main-content" class="settings-page artifact-view">
|
|
1100
1226
|
<section class="meta-card">
|
|
1101
1227
|
<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
|
-
|
|
1228
|
+
<form class="editor-form retention-policy-form" method="post" action="/admin/retention">${localeInput(view.locale)}
|
|
1229
|
+
<fieldset class="retention-section"><legend>${view.text("retention.artifactsSection")}</legend>
|
|
1230
|
+
<div class="retention-field-grid">
|
|
1231
|
+
<label class="field"><span>${view.text("retention.archiveAfterDaysDefault")}</span><input type="number" min="1" name="archiveAfterDaysDefault" value="${policy.archiveAfterDays.default ?? ""}" class="form-control"></label>
|
|
1232
|
+
<label class="field"><span>${view.text("retention.purgeArchivedAfterDays")}</span><input type="number" min="1" name="purgeArchivedAfterDays" value="${policy.purgeArchivedAfterDays ?? ""}" class="form-control"></label>
|
|
1233
|
+
</div>
|
|
1234
|
+
<label class="field"><span>${view.text("retention.keepTags")}</span><input name="keepTags" value="${escapeAttribute((policy.keepTags || []).join(", "))}" placeholder="pinned, release" class="form-control"></label>
|
|
1235
|
+
</fieldset>
|
|
1236
|
+
<fieldset class="retention-section"><legend>${view.text("retention.typeRulesSection")}</legend>
|
|
1237
|
+
<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>
|
|
1238
|
+
</fieldset>
|
|
1239
|
+
<fieldset class="retention-section"><legend>${view.text("retention.historySection")}</legend>
|
|
1240
|
+
<div class="retention-field-grid">
|
|
1241
|
+
<label class="field"><span>${view.text("retention.auditRetentionDays")}</span><input type="number" min="1" name="auditRetentionDays" value="${policy.auditRetentionDays ?? ""}" class="form-control"></label>
|
|
1242
|
+
<label class="field"><span>${view.text("retention.eventRetentionRows")}</span><input type="number" min="1" name="eventRetentionRows" value="${policy.eventRetentionRows ?? ""}" class="form-control"></label>
|
|
1243
|
+
</div>
|
|
1244
|
+
</fieldset>
|
|
1245
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-primary">${view.text("retention.saveButton")}</button></footer>
|
|
1112
1246
|
</form>
|
|
1113
1247
|
</section>
|
|
1114
1248
|
${lastSweepSection}
|
|
@@ -1116,9 +1250,9 @@ export function renderAdminRetentionPage({ user, baseUrl, policy, lastSweep = nu
|
|
|
1116
1250
|
<h2>${view.text("retention.runTitle")}</h2>
|
|
1117
1251
|
<p class="auth-warning">${view.text("retention.purgeGateNote")}</p>
|
|
1118
1252
|
<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>
|
|
1253
|
+
<label class="checkbox-field"><input type="checkbox" name="dryRun" value="1" checked class="form-check-input"> ${view.text("retention.dryRunLabel")}</label>
|
|
1254
|
+
<label class="checkbox-field"><input type="checkbox" name="allowPurge" value="1" class="form-check-input"> ${view.text("retention.allowPurgeLabel")}</label>
|
|
1255
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-outline-secondary">${view.text("retention.runButton")}</button></footer>
|
|
1122
1256
|
</form>
|
|
1123
1257
|
</section>
|
|
1124
1258
|
${reportSection}
|
|
@@ -1174,8 +1308,8 @@ export function renderArtifactPage({ artifact, version, content, comments = [],
|
|
|
1174
1308
|
<span>${view.text("artifact.visibility.label")}</span>
|
|
1175
1309
|
${visibilitySelect(artifact.visibility || "team", view)}
|
|
1176
1310
|
</label>
|
|
1177
|
-
<button type="submit">${view.text("artifact.visibility.change")}</button>
|
|
1178
|
-
</form>`
|
|
1311
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("artifact.visibility.change")}</button>
|
|
1312
|
+
</form><p class="muted">${view.text("artifact.visibility.help")}</p>`
|
|
1179
1313
|
: "";
|
|
1180
1314
|
|
|
1181
1315
|
return pageShell({
|
|
@@ -1208,14 +1342,14 @@ export function renderArtifactPage({ artifact, version, content, comments = [],
|
|
|
1208
1342
|
<span>${view.text("artifact.schema", { version: artifact.schemaVersion || 1 })}</span>
|
|
1209
1343
|
<span title="${escapeAttribute(publisherTitle(artifact, view))}">${view.text("artifact.publisher", { publisher })}</span>
|
|
1210
1344
|
<span>${view.text("artifact.bytes", { size: version.sizeBytes })}</span>
|
|
1211
|
-
<span>${
|
|
1345
|
+
<span>${renderDateTime(version.createdAt, view)}</span>
|
|
1212
1346
|
<span>${escapeHtml(version.sha256.slice(0, 12))}</span>
|
|
1213
1347
|
</div></details>
|
|
1214
1348
|
</section>
|
|
1215
1349
|
${canArchive ? `<form class="inline-action" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/${archiveAction}">
|
|
1216
1350
|
${hiddenToken(authToken)}
|
|
1217
1351
|
${localeInput(view.locale)}
|
|
1218
|
-
<button type="submit">${archiveLabel}</button>
|
|
1352
|
+
<button type="submit" class="btn btn-outline-secondary">${archiveLabel}</button>
|
|
1219
1353
|
</form>` : ""}
|
|
1220
1354
|
${visibilityForm}
|
|
1221
1355
|
<section class="version-strip">${versionLinks}</section>
|
|
@@ -1225,7 +1359,7 @@ export function renderArtifactPage({ artifact, version, content, comments = [],
|
|
|
1225
1359
|
</main>
|
|
1226
1360
|
`,
|
|
1227
1361
|
head: needsViewerScript ? editorHead() : "",
|
|
1228
|
-
afterBody: `${frameResizeScript()}${needsViewerScript ? viewerScript() : ""}`,
|
|
1362
|
+
afterBody: `${frameResizeScript()}${needsViewerScript ? viewerScript(view.locale) : ""}`,
|
|
1229
1363
|
locale: view.locale
|
|
1230
1364
|
});
|
|
1231
1365
|
}
|
|
@@ -1314,14 +1448,14 @@ function renderCommentEntry(artifact, comment, view, { authToken, canModerate, c
|
|
|
1314
1448
|
? `<form class="inline-action" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/comments/${encodeURIComponent(comment.id)}/resolve">
|
|
1315
1449
|
${hiddenToken(authToken)}
|
|
1316
1450
|
${localeInput(view.locale)}
|
|
1317
|
-
<button type="submit">${view.text("comments.resolve")}</button>
|
|
1451
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("comments.resolve")}</button>
|
|
1318
1452
|
</form>`
|
|
1319
1453
|
: "";
|
|
1320
1454
|
const deleteForm = canModerate
|
|
1321
1455
|
? `<form class="inline-action" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/comments/${encodeURIComponent(comment.id)}/delete">
|
|
1322
1456
|
${hiddenToken(authToken)}
|
|
1323
1457
|
${localeInput(view.locale)}
|
|
1324
|
-
<button type="submit">${view.text("comments.delete")}</button>
|
|
1458
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("comments.delete")}</button>
|
|
1325
1459
|
</form>`
|
|
1326
1460
|
: "";
|
|
1327
1461
|
|
|
@@ -1329,7 +1463,7 @@ function renderCommentEntry(artifact, comment, view, { authToken, canModerate, c
|
|
|
1329
1463
|
<li class="comment-item${isReply ? " comment-reply" : ""}${resolved ? " comment-resolved" : ""}">
|
|
1330
1464
|
<div class="comment-header">
|
|
1331
1465
|
<span class="comment-author">${escapeHtml(comment.authorLabel)}</span>
|
|
1332
|
-
<span class="comment-meta">${
|
|
1466
|
+
<span class="comment-meta">${renderDateTime(comment.createdAt, view)}</span>
|
|
1333
1467
|
${anchorLabel ? `<span class="comment-anchor">${anchorLabel}</span>` : ""}
|
|
1334
1468
|
${resolved ? `<span class="badge s-archived">${view.text("comments.resolved")}</span>` : ""}
|
|
1335
1469
|
</div>
|
|
@@ -1367,13 +1501,13 @@ function renderCommentForm(artifact, view, { version, authToken, parentId, compa
|
|
|
1367
1501
|
${parentId ? `<input type="hidden" name="parentId" value="${escapeAttribute(parentId)}">` : ""}
|
|
1368
1502
|
<label>
|
|
1369
1503
|
<span>${view.text(compact ? "comments.replyLabel" : "comments.bodyLabel")}</span>
|
|
1370
|
-
<textarea name="body" required maxlength="16384"></textarea>
|
|
1504
|
+
<textarea name="body" required maxlength="16384" class="form-control"></textarea>
|
|
1371
1505
|
</label>
|
|
1372
1506
|
${compact ? "" : `<label class="comment-line-label">
|
|
1373
1507
|
<span>${view.text("comments.lineLabel")}</span>
|
|
1374
|
-
<input type="number" name="line" min="1">
|
|
1508
|
+
<input type="number" name="line" min="1" class="form-control">
|
|
1375
1509
|
</label>`}
|
|
1376
|
-
<button type="submit">${view.text(compact ? "comments.reply" : "comments.submit")}</button>
|
|
1510
|
+
<button type="submit" class="btn btn-primary">${view.text(compact ? "comments.reply" : "comments.submit")}</button>
|
|
1377
1511
|
</form>
|
|
1378
1512
|
`;
|
|
1379
1513
|
}
|
|
@@ -1386,11 +1520,11 @@ function renderReviewStatusForm(artifact, view, authToken) {
|
|
|
1386
1520
|
${localeInput(view.locale)}
|
|
1387
1521
|
<label>
|
|
1388
1522
|
<span>${view.text("comments.reviewStatus.label")}</span>
|
|
1389
|
-
<select name="status">
|
|
1523
|
+
<select name="status" class="form-select">
|
|
1390
1524
|
${REVIEW_STATUSES.map((status) => `<option value="${status}"${status === current ? " selected" : ""}>${view.text(`comments.reviewStatus.${status}`)}</option>`).join("")}
|
|
1391
1525
|
</select>
|
|
1392
1526
|
</label>
|
|
1393
|
-
<button type="submit">${view.text("comments.reviewStatus.change")}</button>
|
|
1527
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("comments.reviewStatus.change")}</button>
|
|
1394
1528
|
</form>
|
|
1395
1529
|
`;
|
|
1396
1530
|
}
|
|
@@ -1497,8 +1631,8 @@ export function renderDiffPage({ user, artifact, fromVersion, toVersion, fromCon
|
|
|
1497
1631
|
|
|
1498
1632
|
const preview = toVersion.format === "html"
|
|
1499
1633
|
? `<section class="split-preview">
|
|
1500
|
-
<div>${renderContent(fromVersion.format, fromContent)}</div>
|
|
1501
|
-
<div>${renderContent(toVersion.format, toContent)}</div>
|
|
1634
|
+
<div>${renderContent(fromVersion.format, fromContent, {}, { view })}</div>
|
|
1635
|
+
<div>${renderContent(toVersion.format, toContent, {}, { view })}</div>
|
|
1502
1636
|
</section>`
|
|
1503
1637
|
: "";
|
|
1504
1638
|
|
|
@@ -1537,13 +1671,13 @@ export function renderDiffPage({ user, artifact, fromVersion, toVersion, fromCon
|
|
|
1537
1671
|
<input type="hidden" name="view" value="${escapeAttribute(resolvedView)}">
|
|
1538
1672
|
<label class="field">
|
|
1539
1673
|
<span>${view.text("diff.from")}</span>
|
|
1540
|
-
<select name="from">${versionOptions}</select>
|
|
1674
|
+
<select name="from" class="form-select">${versionOptions}</select>
|
|
1541
1675
|
</label>
|
|
1542
1676
|
<label class="field">
|
|
1543
1677
|
<span>${view.text("diff.to")}</span>
|
|
1544
|
-
<select name="to">${versionOptions}</select>
|
|
1678
|
+
<select name="to" class="form-select">${versionOptions}</select>
|
|
1545
1679
|
</label>
|
|
1546
|
-
<button type="submit">${view.text("diff.compare")}</button>
|
|
1680
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("diff.compare")}</button>
|
|
1547
1681
|
</form>
|
|
1548
1682
|
<script>
|
|
1549
1683
|
document.querySelector('select[name="from"]').value = ${JSON.stringify(String(fromVersion.version))};
|
|
@@ -1772,6 +1906,7 @@ function renderBundleFiles(content, options = {}) {
|
|
|
1772
1906
|
}
|
|
1773
1907
|
|
|
1774
1908
|
export function renderContent(format, content, metadata = {}, options = {}) {
|
|
1909
|
+
const view = options.view || viewContext(options.locale || DEFAULT_LOCALE, "/");
|
|
1775
1910
|
if (format === "html") {
|
|
1776
1911
|
return `<iframe class="artifact-frame" sandbox="allow-scripts allow-forms allow-popups" srcdoc="${escapeAttribute(htmlFrameContent(content))}"></iframe>`;
|
|
1777
1912
|
}
|
|
@@ -1799,15 +1934,15 @@ export function renderContent(format, content, metadata = {}, options = {}) {
|
|
|
1799
1934
|
}
|
|
1800
1935
|
|
|
1801
1936
|
if (format === "notebook") {
|
|
1802
|
-
return renderNotebook(content);
|
|
1937
|
+
return renderNotebook(content, view);
|
|
1803
1938
|
}
|
|
1804
1939
|
|
|
1805
1940
|
if (format === "sarif") {
|
|
1806
|
-
return renderSarif(content);
|
|
1941
|
+
return renderSarif(content, view);
|
|
1807
1942
|
}
|
|
1808
1943
|
|
|
1809
1944
|
if (format === "csv") {
|
|
1810
|
-
return renderCsv(content);
|
|
1945
|
+
return renderCsv(content, view);
|
|
1811
1946
|
}
|
|
1812
1947
|
|
|
1813
1948
|
if (format === "image") {
|
|
@@ -1821,7 +1956,7 @@ export function renderContent(format, content, metadata = {}, options = {}) {
|
|
|
1821
1956
|
if (format === "code") {
|
|
1822
1957
|
const language = metadata.language || metadata.artifactyImport?.language || "";
|
|
1823
1958
|
return `<section class="artifact-code-viewer" data-artifacty-code-viewer data-language="${escapeAttribute(language)}">
|
|
1824
|
-
<textarea hidden>${escapeHtml(content)}</textarea>
|
|
1959
|
+
<textarea hidden class="form-control">${escapeHtml(content)}</textarea>
|
|
1825
1960
|
<pre class="artifact-code artifact-code-fallback"><code>${escapeHtml(content)}</code></pre>
|
|
1826
1961
|
</section>`;
|
|
1827
1962
|
}
|
|
@@ -1831,7 +1966,7 @@ export function renderContent(format, content, metadata = {}, options = {}) {
|
|
|
1831
1966
|
return `<iframe class="artifact-frame artifact-react-frame" sandbox="allow-scripts" src="${escapeAttribute(options.reactFrameUrl)}"></iframe>`;
|
|
1832
1967
|
}
|
|
1833
1968
|
return `<section class="artifact-react-disabled">
|
|
1834
|
-
<p
|
|
1969
|
+
<p>${view.text("react.disabled")}</p>
|
|
1835
1970
|
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>
|
|
1836
1971
|
</section>`;
|
|
1837
1972
|
}
|
|
@@ -1922,16 +2057,35 @@ export function renderReactFramePage({ title, content }) {
|
|
|
1922
2057
|
}
|
|
1923
2058
|
|
|
1924
2059
|
export function pageShell({ title, body, head = "", afterBody = "", locale = DEFAULT_LOCALE }) {
|
|
2060
|
+
const darkThemeVariables = ` --bg: #15171b;
|
|
2061
|
+
--grid: rgba(120, 160, 200, 0.06);
|
|
2062
|
+
--panel: #1c1f24;
|
|
2063
|
+
--panel-2: #25292f;
|
|
2064
|
+
--text: #e6edf3;
|
|
2065
|
+
--muted: #adb5c0;
|
|
2066
|
+
--faint: #9aa5b2;
|
|
2067
|
+
--line: #343b44;
|
|
2068
|
+
--line-2: #596575;
|
|
2069
|
+
--accent: #9cbcf5;
|
|
2070
|
+
--accent-2: #bbd1f8;
|
|
2071
|
+
--accent-ink: #18253d;
|
|
2072
|
+
--accent-soft: #29374e;
|
|
2073
|
+
--ring: #9cbcf5;
|
|
2074
|
+
--code: #080c12;
|
|
2075
|
+
--code-text: #e6edf3;
|
|
2076
|
+
`;
|
|
1925
2077
|
return `<!doctype html>
|
|
1926
2078
|
<html lang="${escapeAttribute(locale)}">
|
|
1927
2079
|
<head>
|
|
1928
2080
|
<meta charset="utf-8">
|
|
2081
|
+
<script src="/assets/theme.js"></script><script type="module" src="/assets/auto-apply.js"></script>
|
|
2082
|
+
<script src="/assets/sidebar.js"></script>
|
|
1929
2083
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1930
2084
|
<title>${escapeHtml(title)}</title>
|
|
1931
2085
|
${head}
|
|
1932
2086
|
<style>
|
|
1933
2087
|
:root {
|
|
1934
|
-
color-scheme: light
|
|
2088
|
+
color-scheme: light;
|
|
1935
2089
|
--bg: #f5f6f8;
|
|
1936
2090
|
--grid: rgba(31, 58, 99, 0.05);
|
|
1937
2091
|
--panel: #ffffff;
|
|
@@ -1951,24 +2105,14 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
1951
2105
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans KR", sans-serif;
|
|
1952
2106
|
--mono: ui-monospace, "SFMono-Regular", "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
|
1953
2107
|
}
|
|
2108
|
+
:root[data-theme="dark"] {
|
|
2109
|
+
color-scheme: dark;
|
|
2110
|
+
${darkThemeVariables}
|
|
2111
|
+
}
|
|
1954
2112
|
@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;
|
|
2113
|
+
:root:not([data-theme="light"]) {
|
|
2114
|
+
color-scheme: dark;
|
|
2115
|
+
${darkThemeVariables}
|
|
1972
2116
|
}
|
|
1973
2117
|
}
|
|
1974
2118
|
* { box-sizing: border-box; }
|
|
@@ -2051,24 +2195,10 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2051
2195
|
color: var(--text);
|
|
2052
2196
|
text-decoration: none;
|
|
2053
2197
|
}
|
|
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
|
-
}
|
|
2198
|
+
.language-switcher { display: grid; gap: 8px; min-width: 0; }
|
|
2199
|
+
.language-switcher label { display: grid; gap: 6px; min-width: 0; }
|
|
2200
|
+
.language-switcher select { width: 100%; min-width: 0; max-width: 100%; }
|
|
2201
|
+
.language-switcher button { justify-self: start; }
|
|
2072
2202
|
.dashboard,
|
|
2073
2203
|
.artifact-view,
|
|
2074
2204
|
.artifact-editor {
|
|
@@ -2219,12 +2349,19 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2219
2349
|
background: var(--panel);
|
|
2220
2350
|
box-shadow: none;
|
|
2221
2351
|
}
|
|
2352
|
+
.dashboard-pagination { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; margin-top: 16px; }
|
|
2353
|
+
.page-size-form, .page-size-form label { display: flex; align-items: center; gap: 8px; }
|
|
2354
|
+
.page-size-form { margin: 0; }
|
|
2355
|
+
.page-size-form select { min-width: 76px; width: auto; min-height: 44px; }
|
|
2356
|
+
.page-size-form button { min-height: 44px; }
|
|
2357
|
+
.pager-position { align-self: center; white-space: nowrap; }
|
|
2222
2358
|
.pager {
|
|
2223
2359
|
display: flex;
|
|
2224
|
-
|
|
2360
|
+
flex-wrap: wrap;
|
|
2361
|
+
justify-content: flex-start;
|
|
2225
2362
|
gap: 8px;
|
|
2226
|
-
margin-top:
|
|
2227
|
-
font-family: var(--
|
|
2363
|
+
margin-top: 0;
|
|
2364
|
+
font-family: var(--sans);
|
|
2228
2365
|
font-size: 12.5px;
|
|
2229
2366
|
}
|
|
2230
2367
|
.pager a,
|
|
@@ -2349,11 +2486,12 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2349
2486
|
font-size: 13px;
|
|
2350
2487
|
}
|
|
2351
2488
|
.field > span {
|
|
2352
|
-
font-family: var(--
|
|
2353
|
-
font-size:
|
|
2489
|
+
font-family: var(--sans);
|
|
2490
|
+
font-size: 12px;
|
|
2491
|
+
font-weight: 500;
|
|
2354
2492
|
letter-spacing: 0;
|
|
2355
2493
|
text-transform: none;
|
|
2356
|
-
color: var(--
|
|
2494
|
+
color: var(--muted);
|
|
2357
2495
|
}
|
|
2358
2496
|
.check-field {
|
|
2359
2497
|
display: flex;
|
|
@@ -2368,8 +2506,6 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2368
2506
|
min-height: 0;
|
|
2369
2507
|
padding: 0;
|
|
2370
2508
|
}
|
|
2371
|
-
input:not([type="checkbox"]):not([type="radio"]),
|
|
2372
|
-
select,
|
|
2373
2509
|
textarea {
|
|
2374
2510
|
width: 100%;
|
|
2375
2511
|
border: 1px solid var(--line-2);
|
|
@@ -2455,60 +2591,13 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2455
2591
|
border: 0;
|
|
2456
2592
|
background: var(--panel);
|
|
2457
2593
|
}
|
|
2458
|
-
|
|
2459
|
-
select:focus,
|
|
2460
|
-
textarea:focus {
|
|
2461
|
-
outline: 3px solid var(--ring);
|
|
2462
|
-
outline-offset: 0;
|
|
2463
|
-
border-color: var(--accent);
|
|
2464
|
-
}
|
|
2594
|
+
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
2595
|
.editor-actions {
|
|
2466
2596
|
display: flex;
|
|
2467
2597
|
align-items: center;
|
|
2468
2598
|
justify-content: flex-end;
|
|
2469
2599
|
gap: 12px;
|
|
2470
2600
|
}
|
|
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
2601
|
.inline-action button:hover {
|
|
2513
2602
|
background: var(--panel-2);
|
|
2514
2603
|
border-color: var(--accent);
|
|
@@ -3179,6 +3268,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3179
3268
|
.meta-card table { display: block; max-width: 100%; overflow-x: auto; }
|
|
3180
3269
|
.artifact-doc { font-size: 15px; line-height: 1.7; }
|
|
3181
3270
|
.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; }
|
|
3271
|
+
.workspace-brand-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
3182
3272
|
.workspace-brand { display: flex; align-items: center; gap: 10px; color: var(--text); font-size: 19px; font-weight: 700; letter-spacing: -.04em; }
|
|
3183
3273
|
.workspace-brand svg { width: 27px; height: 27px; color: var(--accent); }
|
|
3184
3274
|
.workspace-desktop { display: none; }
|
|
@@ -3188,13 +3278,13 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3188
3278
|
.workspace-mobile nav { padding-top: 16px; }
|
|
3189
3279
|
.workspace-primary, .workspace-actions { display: grid; gap: 4px; }
|
|
3190
3280
|
.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:
|
|
3281
|
+
.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
3282
|
.workspace-header nav a:hover, .workspace-dropdown summary:hover { background: var(--panel-2); color: var(--text); text-decoration: none; }
|
|
3193
3283
|
.workspace-header [aria-current="page"], .workspace-header .is-current { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
3194
3284
|
.workspace-dropdown summary { cursor: pointer; list-style: none; }
|
|
3195
3285
|
.workspace-dropdown summary::-webkit-details-marker { display: none; }
|
|
3196
|
-
.workspace-
|
|
3197
|
-
.workspace-dropdown[open] > summary
|
|
3286
|
+
.workspace-chevron { margin-left: auto; display: inline-flex; }
|
|
3287
|
+
.workspace-dropdown[open] > summary .workspace-chevron { transform: rotate(180deg); }
|
|
3198
3288
|
.workspace-version { color: var(--faint); font-size: 12px; padding: 10px; }
|
|
3199
3289
|
.settings-page { max-width: 1100px; }
|
|
3200
3290
|
.settings-page .meta-card { gap: 16px; }
|
|
@@ -3204,11 +3294,56 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3204
3294
|
.settings-page .field { min-width: 0; }
|
|
3205
3295
|
.settings-page textarea[name="allowedCidrs"] { min-height: 120px; height: 140px; }
|
|
3206
3296
|
.settings-page .editor-form { gap: 12px; }
|
|
3297
|
+
.settings-page .user-role-form { display: flex; flex-wrap: nowrap; gap: 8px; align-items: center; }
|
|
3298
|
+
.user-account-actions { display: grid; gap: 8px; justify-items: start; }
|
|
3299
|
+
|
|
3300
|
+
.user-role-form select { width: auto; min-width: 105px; }
|
|
3301
|
+
.user-role-form select, .user-role-form button { margin: 0; }
|
|
3302
|
+
|
|
3303
|
+
.meta-card:has(> form[action="/admin/retention/run"]) { min-width: 0; overflow-wrap: anywhere; }
|
|
3304
|
+
form[action="/admin/retention/run"], form[action="/admin/retention/run"] > * { min-width: 0; }
|
|
3305
|
+
form[action="/admin/retention/run"] .checkbox-field { display: block; white-space: normal; overflow-wrap: anywhere; line-height: 1.6; }
|
|
3306
|
+
form[action="/admin/retention/run"] .checkbox-field input { margin-right: 6px; vertical-align: middle; }
|
|
3307
|
+
.retention-policy-form .retention-section { display: grid; gap: 16px; min-width: 0; margin: 0; padding: 16px; border: 1px solid var(--line); border-radius: 6px; }
|
|
3308
|
+
.retention-policy-form legend { padding: 0 6px; font-weight: 600; }
|
|
3309
|
+
.smtp-field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; align-items: start; }
|
|
3310
|
+
.smtp-settings-form input, .smtp-settings-form select { min-width: 0; }
|
|
3311
|
+
.smtp-settings-form .field { align-content: start; }
|
|
3312
|
+
@media (max-width: 640px) { .smtp-field-grid { grid-template-columns: minmax(0, 1fr); } }
|
|
3313
|
+
.retention-field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 16px; }
|
|
3314
|
+
.retention-policy-form .field { display: grid; gap: 8px; align-content: start; }
|
|
3315
|
+
.retention-policy-form input, .retention-policy-form textarea { width: 100%; min-width: 0; box-sizing: border-box; }
|
|
3316
|
+
.retention-policy-form textarea { min-height: 112px; height: 112px; }
|
|
3317
|
+
@media (max-width: 640px) { .retention-field-grid { grid-template-columns: minmax(0, 1fr); } }
|
|
3318
|
+
|
|
3207
3319
|
.workspace-dropdown-content { display: grid; padding-left: 10px; border-left: 1px solid var(--line); margin: 4px 0 8px 10px; }
|
|
3208
3320
|
.workspace-dropdown-content .nav-form { display: block; }
|
|
3209
3321
|
.workspace-identity { overflow-wrap: anywhere; font-size: 12px; color: var(--faint); padding: 8px 10px; }
|
|
3210
|
-
.
|
|
3211
|
-
.
|
|
3322
|
+
.theme-controls { border: 0; min-width: 0; margin: 16px 10px 0; padding: 0; }
|
|
3323
|
+
.theme-controls legend { color: var(--muted); font-size: 12px; margin-bottom: 6px; }
|
|
3324
|
+
.theme-choices { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); padding: 3px; gap: 2px; background: var(--panel-2); border-radius: 8px; }
|
|
3325
|
+
.theme-choices .btn { padding: 6px; min-width: 0; border: 0; display: inline-flex; justify-content: center; align-items: center; background: transparent; color: var(--muted); }
|
|
3326
|
+
.theme-choices .btn[aria-pressed="true"] { background: var(--panel); color: var(--accent); box-shadow: 0 1px 3px #0002; }
|
|
3327
|
+
.workspace-header svg { flex-shrink: 0; }
|
|
3328
|
+
.workspace-header .sidebar-toggle { display: none; }
|
|
3329
|
+
.sidebar-toggle > span:not([hidden]) { display: flex; align-items: center; }
|
|
3330
|
+
.workspace-brand-row svg { display: block; }
|
|
3331
|
+
@media (min-width: 1100px) {
|
|
3332
|
+
.sidebar-toggle:not([hidden]) { display: inline-flex; position: static; width: 36px; height: 36px; padding: 0; flex-shrink: 0; }
|
|
3333
|
+
.workspace-desktop { padding-top: 0; }
|
|
3334
|
+
:root[data-sidebar="collapsed"] body:has(> .workspace-header) { padding-left: 68px; }
|
|
3335
|
+
:root[data-sidebar="collapsed"] .workspace-header { width: 68px; padding: 24px 10px; }
|
|
3336
|
+
:root[data-sidebar="collapsed"] .workspace-brand { display: none; }
|
|
3337
|
+
:root[data-sidebar="collapsed"] .workspace-brand-row { justify-content: center; }
|
|
3338
|
+
|
|
3339
|
+
: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; }
|
|
3340
|
+
:root[data-sidebar="collapsed"] .workspace-dropdown-content { padding-left: 0; border: 0; margin-left: 0; }
|
|
3341
|
+
: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; }
|
|
3342
|
+
:root[data-sidebar="collapsed"] .workspace-header [aria-current="page"] { box-shadow: inset 3px 0 var(--accent); }
|
|
3343
|
+
: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; }
|
|
3344
|
+
}
|
|
3345
|
+
.workspace-header .language-switcher { margin: 12px 0 0; padding: 0 10px; font-size: 12px; }
|
|
3346
|
+
|
|
3212
3347
|
.workspace-skip { position: absolute; left: 16px; top: -100px; z-index: 100; background: var(--panel); padding: 8px 16px; }
|
|
3213
3348
|
.workspace-skip:focus { top: 8px; }
|
|
3214
3349
|
:focus-visible { outline: 2px solid var(--ring); outline-offset: 3px; }
|
|
@@ -3253,7 +3388,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3253
3388
|
@media (min-width: 1100px) {
|
|
3254
3389
|
body:has(> .workspace-header) { padding-left: 196px; }
|
|
3255
3390
|
.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 {
|
|
3391
|
+
.workspace-brand-row { min-height: 36px; margin-bottom: 18px; }
|
|
3392
|
+
.workspace-brand { padding: 0 0 0 10px; }
|
|
3257
3393
|
.workspace-desktop { display: block; }
|
|
3258
3394
|
.workspace-mobile { display: none; }
|
|
3259
3395
|
}
|
|
@@ -3281,6 +3417,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3281
3417
|
.dashboard .row-main strong { font-size: 14px; }
|
|
3282
3418
|
}
|
|
3283
3419
|
</style>
|
|
3420
|
+
<link rel="stylesheet" href="/assets/ui-controls.css">
|
|
3284
3421
|
</head>
|
|
3285
3422
|
<body>
|
|
3286
3423
|
${body}
|
|
@@ -3290,7 +3427,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3290
3427
|
}
|
|
3291
3428
|
|
|
3292
3429
|
function formatSelect(selected) {
|
|
3293
|
-
return `<select name="format">
|
|
3430
|
+
return `<select name="format" class="form-select">
|
|
3294
3431
|
${ARTIFACT_FORMATS.map((format) => {
|
|
3295
3432
|
const label = format[0].toUpperCase() + format.slice(1);
|
|
3296
3433
|
return `<option value="${format}"${format === selected ? " selected" : ""}>${label}</option>`;
|
|
@@ -3299,13 +3436,13 @@ function formatSelect(selected) {
|
|
|
3299
3436
|
}
|
|
3300
3437
|
|
|
3301
3438
|
function artifactTypeSelect(selected) {
|
|
3302
|
-
return `<select name="artifactType">
|
|
3439
|
+
return `<select name="artifactType" class="form-select">
|
|
3303
3440
|
${artifactTypeOptions(selected)}
|
|
3304
3441
|
</select>`;
|
|
3305
3442
|
}
|
|
3306
3443
|
|
|
3307
3444
|
function visibilitySelect(selected, view) {
|
|
3308
|
-
return `<select name="visibility">
|
|
3445
|
+
return `<select name="visibility" class="form-select">
|
|
3309
3446
|
${VISIBILITY_VALUES.map((value) => `<option value="${value}"${value === selected ? " selected" : ""}>${view.text(`artifact.visibility.${value}`)}</option>`).join("")}
|
|
3310
3447
|
</select>`;
|
|
3311
3448
|
}
|
|
@@ -3333,6 +3470,13 @@ function editorScript(locale) {
|
|
|
3333
3470
|
return `<script>window.ARTIFACTY_I18N=${config};</script><script type="module" src="${EDITOR_CLIENT_PATH}"></script>`;
|
|
3334
3471
|
}
|
|
3335
3472
|
|
|
3473
|
+
function renderDateTime(value, view) {
|
|
3474
|
+
const date = new Date(value);
|
|
3475
|
+
if (!Number.isFinite(date.getTime())) return escapeHtml(value);
|
|
3476
|
+
const label = new Intl.DateTimeFormat(view.locale, { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" }).format(date);
|
|
3477
|
+
return `<time datetime="${escapeAttribute(value)}" title="${escapeAttribute(value)}">${escapeHtml(label)} UTC</time>`;
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3336
3480
|
function viewContext(locale, currentPath) {
|
|
3337
3481
|
const i18n = createI18n(locale);
|
|
3338
3482
|
return {
|
|
@@ -3394,13 +3538,11 @@ function statusBadge(label) {
|
|
|
3394
3538
|
return `<span class="badge s-archived">${label}</span>`;
|
|
3395
3539
|
}
|
|
3396
3540
|
|
|
3397
|
-
//
|
|
3398
|
-
// common case doesn't add visual noise to every row.
|
|
3541
|
+
// Highlight explicit public/private visibility while keeping default team rows compact.
|
|
3399
3542
|
function visibilityBadge(artifact, view) {
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
}
|
|
3403
|
-
return `<span class="badge v-private">${view.text("artifact.visibility.private")}</span>`;
|
|
3543
|
+
const visibility = artifact.visibility || "team";
|
|
3544
|
+
if (!["private", "public"].includes(visibility)) return "";
|
|
3545
|
+
return `<span class="badge v-${visibility}">${view.text(`artifact.visibility.${visibility}`)}</span>`;
|
|
3404
3546
|
}
|
|
3405
3547
|
|
|
3406
3548
|
function htmlFrameContent(content) {
|
|
@@ -3494,37 +3636,50 @@ function frameResizeScript() {
|
|
|
3494
3636
|
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
3637
|
}
|
|
3496
3638
|
|
|
3497
|
-
function viewerScript() {
|
|
3498
|
-
|
|
3639
|
+
function viewerScript(locale = DEFAULT_LOCALE) {
|
|
3640
|
+
const view = viewContext(locale, "/");
|
|
3641
|
+
const messages = Object.fromEntries(["filter", "filterColumn", "ruleFilter", "rowsShown", "resultsShown"].map((key) => [key, view.raw(`viewer.${key}`, { column: "{column}", visible: "{visible}", total: "{total}" })]));
|
|
3642
|
+
const config = JSON.stringify(messages).replaceAll("<", "\\u003c");
|
|
3643
|
+
return `<script type="application/json" id="artifacty-viewer-i18n">${config}</script><script type="module" src="${VIEWER_CLIENT_PATH}"></script>`;
|
|
3499
3644
|
}
|
|
3500
3645
|
|
|
3501
3646
|
function reactRendererEnabled() {
|
|
3502
3647
|
return process.env.ARTIFACTY_ENABLE_REACT_RENDERER === "true";
|
|
3503
3648
|
}
|
|
3504
3649
|
|
|
3650
|
+
function themeControls(view) {
|
|
3651
|
+
return `<fieldset class="theme-controls" data-theme-controls hidden><legend>${view.text("theme.label")}</legend>
|
|
3652
|
+
<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>
|
|
3653
|
+
</fieldset>`;
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3505
3656
|
function languageSwitcher(view) {
|
|
3506
|
-
const
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
const
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
return `<
|
|
3657
|
+
const current = new URL(view.currentPath || "/", "http://artifacty.local");
|
|
3658
|
+
const preserved = [...current.searchParams.entries()].filter(([name]) => name !== "lang")
|
|
3659
|
+
.map(([name, value]) => `<input type="hidden" name="${escapeAttribute(name)}" value="${escapeAttribute(value)}">`).join("");
|
|
3660
|
+
const options = availableLanguages().filter((language) => language.enabled).map((language) =>
|
|
3661
|
+
`<option value="${escapeAttribute(language.locale)}"${language.locale === view.locale ? " selected" : ""}>${escapeHtml(language.nativeName)}</option>`
|
|
3662
|
+
).join("");
|
|
3663
|
+
return `<form class="language-switcher" data-auto-apply="all" method="get" action="${escapeAttribute(`${current.pathname}${current.hash}`)}">
|
|
3664
|
+
${preserved}<label><span>${view.text("language.label")}</span><select name="lang" class="form-select">${options}</select></label>
|
|
3665
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("language.apply")}</button>
|
|
3666
|
+
</form>`;
|
|
3513
3667
|
}
|
|
3514
3668
|
|
|
3515
3669
|
function workspaceHeader(user, view) {
|
|
3516
3670
|
const pathname = new URL(view.currentPath, "http://artifacty.local").pathname;
|
|
3517
|
-
const
|
|
3671
|
+
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" };
|
|
3672
|
+
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
3673
|
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>
|
|
3674
|
+
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">
|
|
3675
|
+
${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
3676
|
</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">
|
|
3677
|
+
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
3678
|
<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>
|
|
3679
|
+
<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
3680
|
</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>
|
|
3681
|
+
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>`;
|
|
3682
|
+
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
3683
|
<nav class="workspace-desktop" aria-label="${view.attr("dashboard.navigation")}">${links}</nav>
|
|
3529
3684
|
<details class="workspace-mobile"><summary>${view.text("dashboard.more")}</summary><nav aria-label="${view.attr("dashboard.navigation")}">${links}</nav></details>
|
|
3530
3685
|
</header>`;
|
|
@@ -3634,7 +3789,7 @@ function markdownToHtml(markdown, diagramState = createMermaidDiagramState()) {
|
|
|
3634
3789
|
inList = true;
|
|
3635
3790
|
}
|
|
3636
3791
|
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>`);
|
|
3792
|
+
html.push(`<li class="task-list-item"><label><input type="checkbox" disabled${checked ? " checked" : ""} class="form-check-input"> ${inlineMarkdown(taskItem[2])}</label></li>`);
|
|
3638
3793
|
continue;
|
|
3639
3794
|
}
|
|
3640
3795
|
|
|
@@ -3769,19 +3924,20 @@ const CSV_RENDER_COLUMN_LIMIT = 80;
|
|
|
3769
3924
|
const SARIF_RENDER_RESULT_LIMIT = 500;
|
|
3770
3925
|
|
|
3771
3926
|
function renderMediaArtifact(format, content, metadata = {}, options = {}) {
|
|
3927
|
+
const view = options.view || viewContext(options.locale || DEFAULT_LOCALE, "/");
|
|
3772
3928
|
const mimeType = mediaMimeType(metadata, format);
|
|
3773
3929
|
const source = options.rawUrl || mediaDataUrl(content, mimeType);
|
|
3774
3930
|
if (!source) {
|
|
3775
3931
|
return `<section class="artifact-media-note">
|
|
3776
|
-
|
|
3932
|
+
${view.text("media.unavailable")}
|
|
3777
3933
|
</section>
|
|
3778
3934
|
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>`;
|
|
3779
3935
|
}
|
|
3780
3936
|
|
|
3781
3937
|
const label = [
|
|
3782
3938
|
mimeType || format,
|
|
3783
|
-
metadata.encoding ?
|
|
3784
|
-
metadata.originalEncoding ?
|
|
3939
|
+
metadata.encoding ? view.raw("media.encoding", { encoding: metadata.encoding }) : "",
|
|
3940
|
+
metadata.originalEncoding ? view.raw("media.source", { encoding: metadata.originalEncoding }) : ""
|
|
3785
3941
|
].filter(Boolean).join(" · ");
|
|
3786
3942
|
|
|
3787
3943
|
if (format === "video") {
|
|
@@ -3792,7 +3948,7 @@ function renderMediaArtifact(format, content, metadata = {}, options = {}) {
|
|
|
3792
3948
|
}
|
|
3793
3949
|
|
|
3794
3950
|
return `<figure class="artifact-media artifact-image">
|
|
3795
|
-
<img src="${escapeAttribute(source)}" alt="
|
|
3951
|
+
<img src="${escapeAttribute(source)}" alt="${view.text("media.image")}">
|
|
3796
3952
|
<figcaption>${escapeHtml(label || "image artifact")}</figcaption>
|
|
3797
3953
|
</figure>`;
|
|
3798
3954
|
}
|
|
@@ -3817,17 +3973,17 @@ function mediaDataUrl(content, mimeType) {
|
|
|
3817
3973
|
return `data:${mimeType};base64,${base64}`;
|
|
3818
3974
|
}
|
|
3819
3975
|
|
|
3820
|
-
function renderCsv(content) {
|
|
3976
|
+
function renderCsv(content, view) {
|
|
3821
3977
|
const parsed = parseCsv(content);
|
|
3822
3978
|
if (!parsed.ok) {
|
|
3823
3979
|
return `<section class="artifact-csv">
|
|
3824
|
-
<p class="artifact-csv-note"
|
|
3980
|
+
<p class="artifact-csv-note">${view.text("csv.error", { error: parsed.error })}</p>
|
|
3825
3981
|
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>
|
|
3826
3982
|
</section>`;
|
|
3827
3983
|
}
|
|
3828
3984
|
|
|
3829
3985
|
if (parsed.rows.length === 0) {
|
|
3830
|
-
return `<section class="artifact-csv"><p class="artifact-csv-note"
|
|
3986
|
+
return `<section class="artifact-csv"><p class="artifact-csv-note">${view.text("csv.empty")}</p></section>`;
|
|
3831
3987
|
}
|
|
3832
3988
|
|
|
3833
3989
|
const columnCount = Math.max(...parsed.rows.map((row) => row.length));
|
|
@@ -3837,7 +3993,7 @@ function renderCsv(content) {
|
|
|
3837
3993
|
const rowTruncated = parsed.rows.length - 1 > CSV_RENDER_ROW_LIMIT;
|
|
3838
3994
|
const columnTruncated = columnCount > CSV_RENDER_COLUMN_LIMIT;
|
|
3839
3995
|
const headerHtml = Array.from({ length: visibleColumns }, (_, index) =>
|
|
3840
|
-
`<th data-csv-col="${index}">${escapeHtml(header[index] ||
|
|
3996
|
+
`<th data-csv-col="${index}">${escapeHtml(header[index] || view.raw("csv.column", { column: index + 1 }))}</th>`
|
|
3841
3997
|
).join("");
|
|
3842
3998
|
const bodyHtml = bodyRows.map((row) =>
|
|
3843
3999
|
`<tr data-csv-row>${Array.from({ length: visibleColumns }, (_, index) =>
|
|
@@ -3845,10 +4001,10 @@ function renderCsv(content) {
|
|
|
3845
4001
|
).join("")}</tr>`
|
|
3846
4002
|
).join("\n");
|
|
3847
4003
|
const notes = [
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
rowTruncated ?
|
|
3851
|
-
columnTruncated ?
|
|
4004
|
+
view.raw("csv.rows", { count: parsed.rows.length - 1 }),
|
|
4005
|
+
view.raw("csv.columns", { count: columnCount }),
|
|
4006
|
+
rowTruncated ? view.raw("csv.firstRows", { count: CSV_RENDER_ROW_LIMIT }) : "",
|
|
4007
|
+
columnTruncated ? view.raw("csv.firstColumns", { count: CSV_RENDER_COLUMN_LIMIT }) : ""
|
|
3852
4008
|
].filter(Boolean).join(" · ");
|
|
3853
4009
|
|
|
3854
4010
|
return `<section class="artifact-csv" data-artifact-csv>
|
|
@@ -3862,20 +4018,20 @@ function renderCsv(content) {
|
|
|
3862
4018
|
</section>`;
|
|
3863
4019
|
}
|
|
3864
4020
|
|
|
3865
|
-
function renderSarif(content) {
|
|
4021
|
+
function renderSarif(content, view) {
|
|
3866
4022
|
let sarif;
|
|
3867
4023
|
try {
|
|
3868
4024
|
sarif = JSON.parse(content);
|
|
3869
4025
|
} catch {
|
|
3870
4026
|
return `<section class="artifact-sarif">
|
|
3871
|
-
<p class="artifact-sarif-note"
|
|
4027
|
+
<p class="artifact-sarif-note">${view.text("sarif.invalid")}</p>
|
|
3872
4028
|
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>
|
|
3873
4029
|
</section>`;
|
|
3874
4030
|
}
|
|
3875
4031
|
|
|
3876
4032
|
if (!isSarifDocument(sarif)) {
|
|
3877
4033
|
return `<section class="artifact-sarif">
|
|
3878
|
-
<p class="artifact-sarif-note"
|
|
4034
|
+
<p class="artifact-sarif-note">${view.text("sarif.shape")}</p>
|
|
3879
4035
|
<pre class="artifact-code"><code>${escapeHtml(formatJson(content))}</code></pre>
|
|
3880
4036
|
</section>`;
|
|
3881
4037
|
}
|
|
@@ -3891,26 +4047,26 @@ function renderSarif(content) {
|
|
|
3891
4047
|
<td>${escapeHtml(result.tool)}</td>
|
|
3892
4048
|
</tr>`).join("\n");
|
|
3893
4049
|
const note = results.length > SARIF_RENDER_RESULT_LIMIT
|
|
3894
|
-
?
|
|
3895
|
-
:
|
|
4050
|
+
? view.raw("sarif.first", { count: SARIF_RENDER_RESULT_LIMIT, total: results.length })
|
|
4051
|
+
: view.raw("sarif.results", { count: results.length });
|
|
3896
4052
|
|
|
3897
4053
|
return `<section class="artifact-sarif" data-artifact-sarif>
|
|
3898
4054
|
<div class="artifact-summary-grid">
|
|
3899
|
-
${summaryCard("
|
|
3900
|
-
${summaryCard("
|
|
3901
|
-
${summaryCard("
|
|
3902
|
-
${summaryCard("
|
|
3903
|
-
${summaryCard("
|
|
4055
|
+
${summaryCard(view.raw("sarif.runs"), sarif.runs.length)}
|
|
4056
|
+
${summaryCard(view.raw("sarif.resultsTitle"), results.length)}
|
|
4057
|
+
${summaryCard(view.raw("sarif.errors"), counts.error || 0)}
|
|
4058
|
+
${summaryCard(view.raw("sarif.warnings"), counts.warning || 0)}
|
|
4059
|
+
${summaryCard(view.raw("sarif.notes"), counts.note || 0)}
|
|
3904
4060
|
</div>
|
|
3905
4061
|
<p class="artifact-sarif-note">${escapeHtml(note)}</p>
|
|
3906
4062
|
<div class="artifact-table-scroll">
|
|
3907
4063
|
<table class="artifact-table" data-sarif-table>
|
|
3908
|
-
<thead><tr><th data-sarif-sort="level"
|
|
4064
|
+
<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
4065
|
<tbody>${rowHtml}</tbody>
|
|
3910
4066
|
</table>
|
|
3911
4067
|
</div>
|
|
3912
4068
|
<details class="artifact-raw-details">
|
|
3913
|
-
<summary
|
|
4069
|
+
<summary>${view.text("sarif.raw")}</summary>
|
|
3914
4070
|
<pre class="artifact-code"><code>${escapeHtml(formatJson(content))}</code></pre>
|
|
3915
4071
|
</details>
|
|
3916
4072
|
</section>`;
|
|
@@ -3991,7 +4147,7 @@ const NOTEBOOK_OUTPUT_MIME_PRIORITY = [
|
|
|
3991
4147
|
"text/plain"
|
|
3992
4148
|
];
|
|
3993
4149
|
|
|
3994
|
-
function renderNotebook(content) {
|
|
4150
|
+
function renderNotebook(content, view) {
|
|
3995
4151
|
let notebook;
|
|
3996
4152
|
try {
|
|
3997
4153
|
notebook = JSON.parse(content);
|
|
@@ -4006,16 +4162,16 @@ function renderNotebook(content) {
|
|
|
4006
4162
|
const diagramState = createMermaidDiagramState();
|
|
4007
4163
|
const cells = notebook.cells;
|
|
4008
4164
|
const rendered = cells.slice(0, NOTEBOOK_MAX_RENDERED_CELLS)
|
|
4009
|
-
.map((cell) => renderNotebookCell(cell, language, diagramState))
|
|
4165
|
+
.map((cell) => renderNotebookCell(cell, language, diagramState, view))
|
|
4010
4166
|
.join("");
|
|
4011
4167
|
const notice = cells.length > NOTEBOOK_MAX_RENDERED_CELLS
|
|
4012
|
-
? `<p class="artifact-notebook-notice"
|
|
4168
|
+
? `<p class="artifact-notebook-notice">${view.text("notebook.cells", { count: NOTEBOOK_MAX_RENDERED_CELLS, total: cells.length })}</p>`
|
|
4013
4169
|
: "";
|
|
4014
4170
|
|
|
4015
4171
|
return `<article class="artifact-doc artifact-notebook">${notice}${rendered}</article>`;
|
|
4016
4172
|
}
|
|
4017
4173
|
|
|
4018
|
-
function renderNotebookCell(cell, language, diagramState) {
|
|
4174
|
+
function renderNotebookCell(cell, language, diagramState, view) {
|
|
4019
4175
|
const cellType = cell?.cell_type;
|
|
4020
4176
|
const source = notebookText(cell?.source);
|
|
4021
4177
|
|
|
@@ -4034,9 +4190,9 @@ function renderNotebookCell(cell, language, diagramState) {
|
|
|
4034
4190
|
const cellOutputs = Array.isArray(cell?.outputs) ? cell.outputs : [];
|
|
4035
4191
|
const visibleOutputs = cellOutputs.slice(0, NOTEBOOK_MAX_OUTPUTS_PER_CELL);
|
|
4036
4192
|
const outputsNotice = cellOutputs.length > NOTEBOOK_MAX_OUTPUTS_PER_CELL
|
|
4037
|
-
? `<p class="artifact-notebook-notice"
|
|
4193
|
+
? `<p class="artifact-notebook-notice">${view.text("notebook.outputs", { count: NOTEBOOK_MAX_OUTPUTS_PER_CELL, total: cellOutputs.length })}</p>`
|
|
4038
4194
|
: "";
|
|
4039
|
-
const outputs = visibleOutputs.map((output) => renderNotebookOutput(output, diagramState)).join("") + outputsNotice;
|
|
4195
|
+
const outputs = visibleOutputs.map((output) => renderNotebookOutput(output, diagramState, view)).join("") + outputsNotice;
|
|
4040
4196
|
return `<section class="artifact-notebook-cell artifact-notebook-cell-code">
|
|
4041
4197
|
<div class="artifact-notebook-execcount">${escapeHtml(executionLabel)}</div>
|
|
4042
4198
|
<pre><code${classAttribute}>${escapeHtml(source)}</code></pre>
|
|
@@ -4055,11 +4211,11 @@ function stripAnsiEscapes(value) {
|
|
|
4055
4211
|
return String(value ?? "").replace(ANSI_ESCAPE_PATTERN, "");
|
|
4056
4212
|
}
|
|
4057
4213
|
|
|
4058
|
-
function renderNotebookOutput(output, diagramState) {
|
|
4214
|
+
function renderNotebookOutput(output, diagramState, view) {
|
|
4059
4215
|
const outputType = output?.output_type;
|
|
4060
4216
|
|
|
4061
4217
|
if (outputType === "stream") {
|
|
4062
|
-
return renderNotebookTextOutput(notebookText(output.text));
|
|
4218
|
+
return renderNotebookTextOutput(notebookText(output.text), "", view);
|
|
4063
4219
|
}
|
|
4064
4220
|
|
|
4065
4221
|
if (outputType === "error") {
|
|
@@ -4073,18 +4229,18 @@ function renderNotebookOutput(output, diagramState) {
|
|
|
4073
4229
|
: notebookText(output.traceback);
|
|
4074
4230
|
const traceback = stripAnsiEscapes(rawTraceback) ||
|
|
4075
4231
|
`${notebookText(output.ename)}: ${notebookText(output.evalue)}`;
|
|
4076
|
-
return renderNotebookTextOutput(traceback, "artifact-notebook-error");
|
|
4232
|
+
return renderNotebookTextOutput(traceback, "artifact-notebook-error", view);
|
|
4077
4233
|
}
|
|
4078
4234
|
|
|
4079
4235
|
if (outputType === "execute_result" || outputType === "display_data") {
|
|
4080
|
-
return renderNotebookMimeBundle(output.data, diagramState);
|
|
4236
|
+
return renderNotebookMimeBundle(output.data, diagramState, view);
|
|
4081
4237
|
}
|
|
4082
4238
|
|
|
4083
4239
|
return "";
|
|
4084
4240
|
}
|
|
4085
4241
|
|
|
4086
|
-
function renderNotebookTextOutput(text, extraClass = "") {
|
|
4087
|
-
const truncated = notebookTruncateIfOversized(text);
|
|
4242
|
+
function renderNotebookTextOutput(text, extraClass = "", view) {
|
|
4243
|
+
const truncated = notebookTruncateIfOversized(text, view);
|
|
4088
4244
|
if (truncated) {
|
|
4089
4245
|
return truncated;
|
|
4090
4246
|
}
|
|
@@ -4092,7 +4248,7 @@ function renderNotebookTextOutput(text, extraClass = "") {
|
|
|
4092
4248
|
return `<pre class="artifact-notebook-output artifact-code${classAttribute}"><code>${escapeHtml(text)}</code></pre>`;
|
|
4093
4249
|
}
|
|
4094
4250
|
|
|
4095
|
-
function renderNotebookMimeBundle(data, diagramState) {
|
|
4251
|
+
function renderNotebookMimeBundle(data, diagramState, view) {
|
|
4096
4252
|
const bundle = data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
4097
4253
|
const keys = Object.keys(bundle);
|
|
4098
4254
|
if (keys.length === 0) {
|
|
@@ -4101,11 +4257,11 @@ function renderNotebookMimeBundle(data, diagramState) {
|
|
|
4101
4257
|
|
|
4102
4258
|
const mime = NOTEBOOK_OUTPUT_MIME_PRIORITY.find((candidate) => keys.includes(candidate));
|
|
4103
4259
|
if (!mime) {
|
|
4104
|
-
return `<p class="artifact-notebook-output-placeholder"
|
|
4260
|
+
return `<p class="artifact-notebook-output-placeholder">${view.text("notebook.unsupported", { type: keys[0] })}</p>`;
|
|
4105
4261
|
}
|
|
4106
4262
|
|
|
4107
4263
|
const value = notebookText(bundle[mime]);
|
|
4108
|
-
const truncated = notebookTruncateIfOversized(value);
|
|
4264
|
+
const truncated = notebookTruncateIfOversized(value, view);
|
|
4109
4265
|
if (truncated) {
|
|
4110
4266
|
return truncated;
|
|
4111
4267
|
}
|
|
@@ -4119,9 +4275,9 @@ function renderNotebookMimeBundle(data, diagramState) {
|
|
|
4119
4275
|
if (mime === "image/png" || mime === "image/jpeg") {
|
|
4120
4276
|
const dataUrl = mediaDataUrl(value, mime);
|
|
4121
4277
|
if (!dataUrl) {
|
|
4122
|
-
return `<p class="artifact-notebook-output-placeholder"
|
|
4278
|
+
return `<p class="artifact-notebook-output-placeholder">${view.text("notebook.unsupported", { type: mime })}</p>`;
|
|
4123
4279
|
}
|
|
4124
|
-
return `<figure class="artifact-media artifact-image artifact-notebook-output"><img src="${escapeAttribute(dataUrl)}" alt="
|
|
4280
|
+
return `<figure class="artifact-media artifact-image artifact-notebook-output"><img src="${escapeAttribute(dataUrl)}" alt="${view.text("notebook.image")}"></figure>`;
|
|
4125
4281
|
}
|
|
4126
4282
|
if (mime === "image/svg+xml") {
|
|
4127
4283
|
return `<iframe class="artifact-frame artifact-svg-frame artifact-notebook-output" sandbox srcdoc="${escapeAttribute(svgFrameContent(value))}"></iframe>`;
|
|
@@ -4132,12 +4288,12 @@ function renderNotebookMimeBundle(data, diagramState) {
|
|
|
4132
4288
|
return "";
|
|
4133
4289
|
}
|
|
4134
4290
|
|
|
4135
|
-
function notebookTruncateIfOversized(value) {
|
|
4291
|
+
function notebookTruncateIfOversized(value, view) {
|
|
4136
4292
|
const size = Buffer.byteLength(String(value ?? ""), "utf8");
|
|
4137
4293
|
if (size <= NOTEBOOK_MAX_OUTPUT_BYTES) {
|
|
4138
4294
|
return null;
|
|
4139
4295
|
}
|
|
4140
|
-
return `<p class="artifact-notebook-output-truncated"
|
|
4296
|
+
return `<p class="artifact-notebook-output-truncated">${view.text("notebook.truncated", { size })}</p>`;
|
|
4141
4297
|
}
|
|
4142
4298
|
|
|
4143
4299
|
function notebookText(value) {
|