artifacty 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +54 -8
- package/README.md +56 -10
- package/THIRD_PARTY_NOTICES.md +53 -0
- package/docs/README.md +6 -0
- package/docs/ko/README.md +6 -0
- package/docs/ko/language-packs.md +40 -0
- package/docs/ko/smtp.md +27 -0
- package/docs/ko/upgrade-backups.md +16 -0
- package/docs/language-packs.md +38 -0
- package/docs/roadmap-design.md +31 -2
- package/docs/smtp.md +25 -0
- package/docs/upgrade-backups.md +16 -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/editor.js +75 -20
- 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/update.js +33 -0
- package/src/client/viewer.js +17 -6
- package/src/lib/background.js +3 -1
- package/src/lib/events.js +4 -4
- package/src/lib/i18n.js +601 -9
- package/src/lib/language-settings.js +114 -0
- package/src/lib/openapi.js +93 -74
- package/src/lib/render.js +888 -628
- package/src/lib/sample-content.js +6 -6
- package/src/lib/self-update.js +12 -0
- 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/lib/web-update.js +177 -0
- package/src/mcp-server.js +3 -3
- package/src/server.js +250 -18
- package/src/styles/ui-controls.scss +63 -0
- package/src/update-runner.js +7 -0
package/src/lib/render.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { uiIcon } from "./ui-icons.js";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
1
3
|
import { EDITOR_CLIENT_PATH, VIEWER_CLIENT_PATH, editorImportMapJson } from "./editor-assets.js";
|
|
2
|
-
import { createI18n, DEFAULT_LOCALE, editorMessages, localizedHref,
|
|
4
|
+
import { createI18n, DEFAULT_LOCALE, editorMessages, localizedHref, availableLanguages } from "./i18n.js";
|
|
3
5
|
import { ARTIFACT_FORMATS, ARTIFACT_TYPES, REVIEW_STATUSES, SAVED_VIEW_FILTER_KEYS, VISIBILITY_VALUES } from "./storage.js";
|
|
4
6
|
import { normalizeMaxEntries as resolveMaxDiffEntries } from "./diff.js";
|
|
5
7
|
import { parseCsv } from "./csv.js";
|
|
@@ -12,62 +14,62 @@ import {
|
|
|
12
14
|
import { isNotebookObject } from "./converters.js";
|
|
13
15
|
import { GROUP_BY_VALUES, groupKeyFor } from "./listing.js";
|
|
14
16
|
|
|
17
|
+
const APP_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
18
|
+
|
|
15
19
|
export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination, locale = DEFAULT_LOCALE, currentPath = "/", user = null, savedViews = [], embeddingsAvailable = false }) {
|
|
16
20
|
const view = viewContext(locale, currentPath);
|
|
17
21
|
const total = pagination?.total ?? artifacts.length;
|
|
18
22
|
const start = artifacts.length ? (pagination?.offset ?? 0) + 1 : 0;
|
|
19
23
|
const end = artifacts.length ? (pagination?.offset ?? 0) + artifacts.length : 0;
|
|
20
|
-
const
|
|
21
|
-
const pager = renderDashboardPager({ pagination, filters, view });
|
|
24
|
+
const pager = renderDashboardPager({ pagination: pagination || { total, limit: Number(filters.limit) || 50, offset: 0 }, filters, view });
|
|
22
25
|
const rowsHtml = renderArtifactRows(artifacts, filters.groupBy, view);
|
|
23
|
-
const
|
|
26
|
+
const activeFilters = [
|
|
27
|
+
["tag", "filter.tag"], ["sourceAgent", "filter.source"], ["artifactType", "filter.artifactType"],
|
|
28
|
+
["publisher", "filter.publisher"], ["createdAfter", "filter.createdAfter"], ["createdBefore", "filter.createdBefore"],
|
|
29
|
+
["reviewStatus", "filter.reviewStatus"], ["mode", "filter.mode"], ["groupBy", "filter.groupBy"],
|
|
30
|
+
["relatedTo", "filter.relatedTo"], ["relation", "filter.relation"], ["includeArchived", "filter.archived"]
|
|
31
|
+
].filter(([key]) => filters[key]);
|
|
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>`;
|
|
33
|
+
const selectField = (label, select) => `<label class="dashboard-field"><span>${view.text(label)}</span>${select}</label>`;
|
|
24
34
|
|
|
25
35
|
return pageShell({
|
|
26
36
|
title: "Artifacty",
|
|
27
37
|
body: `
|
|
28
|
-
|
|
29
|
-
<a class="brand" href="${view.href("/")}">
|
|
30
|
-
${brandMark()}
|
|
31
|
-
<span class="brand-text">
|
|
32
|
-
<span class="brand-name">artifacty</span>
|
|
33
|
-
<span class="brand-tag">local artifact exchange · ${escapeHtml(baseUrl)}</span>
|
|
34
|
-
</span>
|
|
35
|
-
</a>
|
|
36
|
-
<nav>
|
|
37
|
-
<a href="${view.href("/new")}">${view.text("nav.new")}</a>
|
|
38
|
-
<a href="${view.href("/import")}">${view.text("nav.import")}</a>
|
|
39
|
-
<a href="/api/artifacts">${view.text("nav.api")}</a>
|
|
40
|
-
${authNav(user, view)}
|
|
41
|
-
${languageSwitcher(view)}
|
|
42
|
-
</nav>
|
|
43
|
-
</header>
|
|
38
|
+
${workspaceHeader(user, view)}
|
|
44
39
|
<div class="dashboard-layout">
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
<span>${view.text("dashboard.range", { start, end, total })}</span>
|
|
49
|
-
${searchBackend ? `<span>${view.text("dashboard.searchBackend", { backend: searchBackend })}</span>` : ""}
|
|
50
|
-
</section>
|
|
51
|
-
<form class="filter-form" method="get" action="/">
|
|
40
|
+
<main class="dashboard" id="main-content">
|
|
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="/">
|
|
52
43
|
${localeInput(view.locale)}
|
|
53
44
|
${filters.limit ? `<input type="hidden" name="limit" value="${escapeAttribute(filters.limit)}">` : ""}
|
|
54
|
-
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
45
|
+
${!embeddingsAvailable && filters.mode ? `<input type="hidden" name="mode" value="${escapeAttribute(filters.mode)}">` : ""}
|
|
46
|
+
<div class="dashboard-search-row">
|
|
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>
|
|
49
|
+
<a class="dashboard-clear" href="${view.href("/")}">${view.text("filter.clear")}</a>
|
|
50
|
+
</div>
|
|
51
|
+
<details class="dashboard-advanced">
|
|
52
|
+
<summary>${view.text("dashboard.advanced")}${activeFilters.length ? `<span class="dashboard-filter-count">${view.text("dashboard.activeFilters", { count: activeFilters.length })}</span>` : ""}</summary>
|
|
53
|
+
<div class="dashboard-filter-grid">
|
|
54
|
+
${field("tag", "filter.tag")}${field("sourceAgent", "filter.source")}
|
|
55
|
+
${selectField("filter.artifactType", filterArtifactTypeSelect(filters.artifactType, view))}
|
|
56
|
+
${field("publisher", "filter.publisher")}
|
|
57
|
+
${field("createdAfter", "filter.createdAfter", "date")}${field("createdBefore", "filter.createdBefore", "date")}
|
|
58
|
+
${selectField("filter.reviewStatus", filterReviewStatusSelect(filters.reviewStatus, view))}
|
|
59
|
+
${embeddingsAvailable ? selectField("filter.mode", filterModeSelect(filters.mode, view)) : ""}
|
|
60
|
+
${selectField("filter.groupBy", filterGroupBySelect(filters.groupBy, view))}
|
|
61
|
+
${field("relatedTo", "filter.relatedTo")}${field("relation", "filter.relation")}
|
|
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>
|
|
64
|
+
</div>
|
|
65
|
+
</details>
|
|
66
|
+
${activeFilters.length ? `<p class="dashboard-active-filters">${activeFilters.map(([key, label]) => `<span>${view.text(label)}${key === "includeArchived" ? "" : `: ${escapeHtml(filters[key])}`}</span>`).join("")}</p>` : ""}
|
|
67
67
|
</form>
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
${renderSavedViewsSidebar({ savedViews, filters, view })}
|
|
69
|
+
<section class="toolbar dashboard-results">
|
|
70
|
+
<span>${view.text("dashboard.range", { start, end, total })}</span>
|
|
70
71
|
</section>
|
|
72
|
+
<section class="artifact-list">${rowsHtml || `<div class="empty">${view.text("dashboard.empty")}</div>`}</section>
|
|
71
73
|
${pager}
|
|
72
74
|
</main>
|
|
73
75
|
</div>
|
|
@@ -76,7 +78,7 @@ export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination,
|
|
|
76
78
|
});
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
function renderArtifactRow(artifact, view) {
|
|
81
|
+
function renderArtifactRow(artifact, view, readOnly = false) {
|
|
80
82
|
const tags = artifact.tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("");
|
|
81
83
|
const status = artifact.archivedAt ? statusBadge("archived") : "";
|
|
82
84
|
const visibility = visibilityBadge(artifact, view);
|
|
@@ -85,27 +87,24 @@ function renderArtifactRow(artifact, view) {
|
|
|
85
87
|
? `<span class="row-snippet">${escapeHtml(artifact.searchSnippet)}</span>`
|
|
86
88
|
: "";
|
|
87
89
|
return `
|
|
88
|
-
|
|
90
|
+
<${readOnly ? "div" : "a"} class="artifact-row"${readOnly ? "" : ` href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}`)}"`}>
|
|
89
91
|
<span class="row-main">
|
|
90
92
|
<strong>${escapeHtml(artifact.title)}</strong>
|
|
91
|
-
<span>${escapeHtml(artifact.id)}</span>
|
|
93
|
+
<span class="dashboard-sr-only">${escapeHtml(artifact.id)}</span>
|
|
92
94
|
${snippet}
|
|
93
95
|
</span>
|
|
94
|
-
<span>${escapeHtml(artifact.sourceAgent)}</span>
|
|
95
|
-
<span class="
|
|
96
|
-
${typeBadge(artifact.artifactType || "document")}
|
|
97
|
-
${formatBadge(artifact.format || "text")}
|
|
98
|
-
<span>v${artifact.latestVersion}</span>
|
|
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>
|
|
97
|
+
<span class="row-formats">${typeBadge(artifact.artifactType || "document")}${formatBadge(artifact.format || "text")}<span>v${artifact.latestVersion}</span></span>
|
|
99
98
|
<span class="tags">${status}${visibility}${tags}</span>
|
|
100
|
-
|
|
99
|
+
</${readOnly ? "div" : "a"}>
|
|
101
100
|
`;
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
// groupBy is purely presentational: it groups the already-fetched, already-
|
|
105
104
|
// sorted page of artifacts into labeled sections without changing the query.
|
|
106
|
-
function renderArtifactRows(artifacts, groupBy, view) {
|
|
105
|
+
function renderArtifactRows(artifacts, groupBy, view, readOnly = false) {
|
|
107
106
|
if (!GROUP_BY_VALUES.includes(groupBy)) {
|
|
108
|
-
return artifacts.map((artifact) => renderArtifactRow(artifact, view)).join("");
|
|
107
|
+
return artifacts.map((artifact) => renderArtifactRow(artifact, view, readOnly)).join("");
|
|
109
108
|
}
|
|
110
109
|
|
|
111
110
|
const groups = new Map();
|
|
@@ -121,7 +120,7 @@ function renderArtifactRows(artifacts, groupBy, view) {
|
|
|
121
120
|
.map(([key, group]) => `
|
|
122
121
|
<section class="artifact-group">
|
|
123
122
|
<h2 class="artifact-group-heading">${escapeHtml(groupHeadingLabel(key, groupBy, view))}</h2>
|
|
124
|
-
${group.map((artifact) => renderArtifactRow(artifact, view)).join("")}
|
|
123
|
+
${group.map((artifact) => renderArtifactRow(artifact, view, readOnly)).join("")}
|
|
125
124
|
</section>
|
|
126
125
|
`)
|
|
127
126
|
.join("");
|
|
@@ -134,21 +133,21 @@ function groupHeadingLabel(key, groupBy, view) {
|
|
|
134
133
|
return key || view.raw("dashboard.groupUnknown");
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
// 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
|
|
138
137
|
// as their own value and (escaped) label.
|
|
139
138
|
function artifactTypeOptions(selected) {
|
|
140
139
|
return ARTIFACT_TYPES.map((type) => `<option value="${type}"${type === selected ? " selected" : ""}>${escapeHtml(type)}</option>`).join("");
|
|
141
140
|
}
|
|
142
141
|
|
|
143
142
|
function filterArtifactTypeSelect(selected, view) {
|
|
144
|
-
return `<select name="artifactType">
|
|
143
|
+
return `<select name="artifactType" class="form-select">
|
|
145
144
|
<option value="">${view.text("filter.artifactTypeAny")}</option>
|
|
146
145
|
${artifactTypeOptions(selected)}
|
|
147
146
|
</select>`;
|
|
148
147
|
}
|
|
149
148
|
|
|
150
149
|
function filterReviewStatusSelect(selected, view) {
|
|
151
|
-
return `<select name="reviewStatus">
|
|
150
|
+
return `<select name="reviewStatus" class="form-select">
|
|
152
151
|
<option value="">${view.text("filter.reviewStatusAny")}</option>
|
|
153
152
|
${REVIEW_STATUSES.map((status) => `<option value="${status}"${status === selected ? " selected" : ""}>${view.text(`reviewStatus.${status}`)}</option>`).join("")}
|
|
154
153
|
</select>`;
|
|
@@ -160,14 +159,14 @@ const SEARCH_MODE_VALUES = ["keyword", "semantic", "hybrid"];
|
|
|
160
159
|
// configured (roadmap section 6): semantic/hybrid search silently falls
|
|
161
160
|
// back to keyword without one, so the toggle would otherwise mislead.
|
|
162
161
|
function filterModeSelect(selected, view) {
|
|
163
|
-
return `<select name="mode">
|
|
162
|
+
return `<select name="mode" class="form-select">
|
|
164
163
|
<option value="">${view.text("filter.modeDefault")}</option>
|
|
165
164
|
${SEARCH_MODE_VALUES.map((value) => `<option value="${value}"${value === selected ? " selected" : ""}>${view.text(`filter.mode.${value}`)}</option>`).join("")}
|
|
166
165
|
</select>`;
|
|
167
166
|
}
|
|
168
167
|
|
|
169
168
|
function filterGroupBySelect(selected, view) {
|
|
170
|
-
return `<select name="groupBy">
|
|
169
|
+
return `<select name="groupBy" class="form-select">
|
|
171
170
|
<option value="">${view.text("filter.groupByNone")}</option>
|
|
172
171
|
${GROUP_BY_VALUES.map((value) => `<option value="${value}"${value === selected ? " selected" : ""}>${view.text(`filter.groupBy.${value}`)}</option>`).join("")}
|
|
173
172
|
</select>`;
|
|
@@ -181,30 +180,32 @@ function renderSavedViewsSidebar({ savedViews, filters, view }) {
|
|
|
181
180
|
${savedView.shared ? `<span class="badge saved-view-shared">${view.text("views.shared")}</span>` : ""}
|
|
182
181
|
<form method="post" action="${view.href(`/views/${encodeURIComponent(savedView.id)}/delete`)}" class="saved-view-delete">
|
|
183
182
|
${localeInput(view.locale)}
|
|
184
|
-
<button type="submit">${view.text("views.delete")}</button>
|
|
183
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("views.delete")}</button>
|
|
185
184
|
</form>
|
|
186
185
|
</li>
|
|
187
186
|
`).join("")
|
|
188
187
|
: `<li class="empty">${view.text("views.empty")}</li>`;
|
|
189
188
|
|
|
190
189
|
return `
|
|
191
|
-
<
|
|
192
|
-
<
|
|
190
|
+
<details class="dashboard-saved-views">
|
|
191
|
+
<summary>${view.text("views.title")} <span class="dashboard-filter-count">${savedViews.length}</span></summary>
|
|
192
|
+
<div class="dashboard-saved-content">
|
|
193
193
|
<ul class="saved-view-list">${items}</ul>
|
|
194
194
|
<form method="post" action="${view.href("/views")}" class="saved-view-form">
|
|
195
195
|
${localeInput(view.locale)}
|
|
196
196
|
${savedViewFilterHiddenInputs(filters)}
|
|
197
|
-
<input name="name" placeholder="${view.attr("views.namePlaceholder")}" required>
|
|
198
|
-
<label class="checkbox-field"><input type="checkbox" name="shared" value="true"> ${view.text("views.shared")}</label>
|
|
199
|
-
<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>
|
|
200
200
|
</form>
|
|
201
|
-
|
|
201
|
+
</div>
|
|
202
|
+
</details>
|
|
202
203
|
`;
|
|
203
204
|
}
|
|
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) {
|
|
@@ -260,7 +285,7 @@ function dashboardPageHref(filters, offset) {
|
|
|
260
285
|
return query ? `/?${query}` : "/";
|
|
261
286
|
}
|
|
262
287
|
|
|
263
|
-
export function renderArtifactFormPage({ mode, baseUrl, artifact, version, content, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/", expectedVersion, conflict = null }) {
|
|
288
|
+
export function renderArtifactFormPage({ user, mode, baseUrl, artifact, version, content, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/", expectedVersion, conflict = null }) {
|
|
264
289
|
const view = viewContext(locale, currentPath);
|
|
265
290
|
const isEdit = mode === "edit";
|
|
266
291
|
const title = isEdit ? view.raw("form.editTitle", { title: artifact.title }) : view.raw("form.newTitle");
|
|
@@ -281,6 +306,7 @@ export function renderArtifactFormPage({ mode, baseUrl, artifact, version, conte
|
|
|
281
306
|
return pageShell({
|
|
282
307
|
title,
|
|
283
308
|
body: `
|
|
309
|
+
${workspaceHeader(user, view)}
|
|
284
310
|
<header class="topbar">
|
|
285
311
|
<div>
|
|
286
312
|
<h1>${escapeHtml(title)}</h1>
|
|
@@ -288,10 +314,10 @@ export function renderArtifactFormPage({ mode, baseUrl, artifact, version, conte
|
|
|
288
314
|
</div>
|
|
289
315
|
<nav>
|
|
290
316
|
<a href="${view.href(isEdit ? `/artifacts/${encodeURIComponent(artifact.id)}` : "/")}">${view.text("nav.cancel")}</a>
|
|
291
|
-
|
|
317
|
+
|
|
292
318
|
</nav>
|
|
293
319
|
</header>
|
|
294
|
-
<main class="artifact-editor">
|
|
320
|
+
<main id="main-content" class="artifact-editor">
|
|
295
321
|
${conflictBanner}
|
|
296
322
|
<form class="editor-form" method="post" action="${action}">
|
|
297
323
|
${hiddenToken(authToken)}
|
|
@@ -300,7 +326,7 @@ export function renderArtifactFormPage({ mode, baseUrl, artifact, version, conte
|
|
|
300
326
|
<section class="editor-fields">
|
|
301
327
|
<label class="field">
|
|
302
328
|
<span>${view.text("form.title")}</span>
|
|
303
|
-
<input name="title" value="${escapeAttribute(artifact?.title || "")}" autocomplete="off" required>
|
|
329
|
+
<input name="title" value="${escapeAttribute(artifact?.title || "")}" autocomplete="off" required class="form-control">
|
|
304
330
|
</label>
|
|
305
331
|
<label class="field">
|
|
306
332
|
<span>${view.text("form.format")}</span>
|
|
@@ -308,7 +334,7 @@ export function renderArtifactFormPage({ mode, baseUrl, artifact, version, conte
|
|
|
308
334
|
</label>
|
|
309
335
|
<label class="field">
|
|
310
336
|
<span>${view.text("form.source")}</span>
|
|
311
|
-
<input name="sourceAgent" value="${escapeAttribute(sourceAgent)}" autocomplete="off">
|
|
337
|
+
<input name="sourceAgent" value="${escapeAttribute(sourceAgent)}" autocomplete="off" class="form-control">
|
|
312
338
|
</label>
|
|
313
339
|
<label class="field">
|
|
314
340
|
<span>${view.text("form.type")}</span>
|
|
@@ -316,20 +342,21 @@ export function renderArtifactFormPage({ mode, baseUrl, artifact, version, conte
|
|
|
316
342
|
</label>
|
|
317
343
|
<label class="field">
|
|
318
344
|
<span>${view.text("form.tags")}</span>
|
|
319
|
-
<input name="tags" value="${escapeAttribute(tags)}" autocomplete="off">
|
|
345
|
+
<input name="tags" value="${escapeAttribute(tags)}" autocomplete="off" class="form-control">
|
|
320
346
|
</label>
|
|
321
347
|
${isEdit ? "" : `<label class="field">
|
|
322
348
|
<span>${view.text("artifact.visibility.label")}</span>
|
|
323
349
|
${visibilitySelect(artifact?.visibility || "team", view)}
|
|
324
350
|
</label>`}
|
|
325
351
|
</section>
|
|
352
|
+
<p class="muted">${view.text("artifact.visibility.help")}</p>
|
|
326
353
|
<section class="field content-field">
|
|
327
354
|
<label for="artifact-content">${view.text("form.content")}</label>
|
|
328
|
-
<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>
|
|
329
356
|
</section>
|
|
330
357
|
<footer class="editor-actions">
|
|
331
358
|
<a href="${view.href(isEdit ? `/artifacts/${encodeURIComponent(artifact.id)}` : "/")}">${view.text("nav.cancel")}</a>
|
|
332
|
-
<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>
|
|
333
360
|
</footer>
|
|
334
361
|
</form>
|
|
335
362
|
</main>
|
|
@@ -340,8 +367,8 @@ export function renderArtifactFormPage({ mode, baseUrl, artifact, version, conte
|
|
|
340
367
|
});
|
|
341
368
|
}
|
|
342
369
|
|
|
343
|
-
export function renderNewArtifactPage({ baseUrl, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/new" }) {
|
|
344
|
-
return renderArtifactFormPage({ mode: "new", baseUrl, authToken, locale, currentPath });
|
|
370
|
+
export function renderNewArtifactPage({ user, baseUrl, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/new" }) {
|
|
371
|
+
return renderArtifactFormPage({ mode: "new", baseUrl, authToken, locale, currentPath, user });
|
|
345
372
|
}
|
|
346
373
|
|
|
347
374
|
export function renderAdminArtifactVersionsPage({ artifact, selectedVersion, content, baseUrl, user, error = "", locale = DEFAULT_LOCALE, currentPath = "/" }) {
|
|
@@ -354,14 +381,14 @@ export function renderAdminArtifactVersionsPage({ artifact, selectedVersion, con
|
|
|
354
381
|
<tr>
|
|
355
382
|
<td><a class="version${active}" href="${view.href(`/admin/artifacts/${encodeURIComponent(artifact.id)}/versions?version=${version.version}`)}">v${version.version}</a></td>
|
|
356
383
|
<td>${formatBadge(version.format)}</td>
|
|
357
|
-
<td>${
|
|
384
|
+
<td>${renderDateTime(version.createdAt, view)}</td>
|
|
358
385
|
<td><code>${escapeHtml(version.sha256.slice(0, 12))}</code></td>
|
|
359
386
|
<td>${escapeHtml(String(version.sizeBytes))}</td>
|
|
360
387
|
<td>
|
|
361
388
|
<form class="inline-action" method="post" action="/admin/artifacts/${encodeURIComponent(artifact.id)}/versions/${version.version}/delete">
|
|
362
389
|
${localeInput(view.locale)}
|
|
363
|
-
<input name="reason" placeholder="
|
|
364
|
-
<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>
|
|
365
392
|
</form>
|
|
366
393
|
</td>
|
|
367
394
|
</tr>
|
|
@@ -369,26 +396,27 @@ export function renderAdminArtifactVersionsPage({ artifact, selectedVersion, con
|
|
|
369
396
|
}).join("");
|
|
370
397
|
|
|
371
398
|
return pageShell({
|
|
372
|
-
title:
|
|
399
|
+
title: `${view.raw("versions.manage")} · ${artifact.title}`,
|
|
373
400
|
body: `
|
|
401
|
+
${workspaceHeader(user, view)}
|
|
374
402
|
<header class="topbar">
|
|
375
403
|
<div>
|
|
376
|
-
<h1
|
|
404
|
+
<h1>${view.text("versions.manage")}</h1>
|
|
377
405
|
<p>${escapeHtml(artifact.title)} · ${escapeHtml(artifact.id)} · ${escapeHtml(baseUrl)}</p>
|
|
378
406
|
</div>
|
|
379
407
|
<nav>
|
|
380
|
-
<a href="${view.href(artifactPath)}"
|
|
408
|
+
<a href="${view.href(artifactPath)}">${view.text("nav.artifact")}</a>
|
|
381
409
|
<a href="${view.href(`${artifactPath}/diff`)}">${view.text("nav.diff")}</a>
|
|
382
|
-
|
|
383
|
-
|
|
410
|
+
|
|
411
|
+
|
|
384
412
|
</nav>
|
|
385
413
|
</header>
|
|
386
|
-
<main class="artifact-editor">
|
|
414
|
+
<main id="main-content" class="artifact-editor">
|
|
387
415
|
${error ? `<p class="auth-error">${escapeHtml(error)}</p>` : ""}
|
|
388
416
|
<section class="meta-card">
|
|
389
|
-
<h2
|
|
417
|
+
<h2>${view.text("nav.versions")}</h2>
|
|
390
418
|
<table class="data-table">
|
|
391
|
-
<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>
|
|
392
420
|
<tbody>${versionRows}</tbody>
|
|
393
421
|
</table>
|
|
394
422
|
</section>
|
|
@@ -400,17 +428,17 @@ export function renderAdminArtifactVersionsPage({ artifact, selectedVersion, con
|
|
|
400
428
|
${formatSelect(selectedVersion.format)}
|
|
401
429
|
</label>
|
|
402
430
|
<label class="field">
|
|
403
|
-
<span
|
|
404
|
-
<input name="reason" value="" autocomplete="off">
|
|
431
|
+
<span>${view.text("ui.reason")}</span>
|
|
432
|
+
<input name="reason" value="" autocomplete="off" class="form-control">
|
|
405
433
|
</label>
|
|
406
434
|
</section>
|
|
407
435
|
<section class="field content-field">
|
|
408
|
-
<label for="artifact-content"
|
|
409
|
-
<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>
|
|
410
438
|
</section>
|
|
411
439
|
<footer class="editor-actions">
|
|
412
|
-
<a href="${view.href(`${artifactPath}?version=${selectedVersion.version}`)}"
|
|
413
|
-
<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>
|
|
414
442
|
</footer>
|
|
415
443
|
</form>
|
|
416
444
|
</main>
|
|
@@ -421,59 +449,57 @@ export function renderAdminArtifactVersionsPage({ artifact, selectedVersion, con
|
|
|
421
449
|
});
|
|
422
450
|
}
|
|
423
451
|
|
|
424
|
-
export function renderImportArtifactPage({ baseUrl, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/import" }) {
|
|
452
|
+
export function renderImportArtifactPage({ user, baseUrl, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/import" }) {
|
|
425
453
|
const view = viewContext(locale, currentPath);
|
|
426
454
|
const title = view.raw("form.importTitle");
|
|
427
455
|
return pageShell({
|
|
428
456
|
title,
|
|
429
457
|
body: `
|
|
458
|
+
${workspaceHeader(user, view)}
|
|
430
459
|
<header class="topbar">
|
|
431
460
|
<div>
|
|
432
461
|
<h1>${escapeHtml(title)}</h1>
|
|
433
462
|
<p>${escapeHtml(baseUrl)}</p>
|
|
434
463
|
</div>
|
|
435
|
-
|
|
436
|
-
<a href="${view.href("/")}">${view.text("nav.index")}</a>
|
|
437
|
-
${languageSwitcher(view)}
|
|
438
|
-
</nav>
|
|
464
|
+
|
|
439
465
|
</header>
|
|
440
|
-
<main class="artifact-editor">
|
|
466
|
+
<main id="main-content" class="artifact-editor">
|
|
441
467
|
<form class="editor-form" method="post" action="/import">
|
|
442
468
|
${hiddenToken(authToken)}
|
|
443
469
|
${localeInput(view.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,40 +512,38 @@ export function renderImportArtifactPage({ baseUrl, authToken = "", locale = DEF
|
|
|
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: `
|
|
519
|
+
${workspaceHeader(null, view)}
|
|
493
520
|
<header class="topbar">
|
|
494
521
|
<div>
|
|
495
522
|
<h1>${escapeHtml(title)}</h1>
|
|
496
523
|
<p>${escapeHtml(baseUrl)}</p>
|
|
497
524
|
</div>
|
|
498
|
-
|
|
499
|
-
<a href="${view.href("/")}">${view.text("nav.index")}</a>
|
|
500
|
-
${languageSwitcher(view)}
|
|
501
|
-
</nav>
|
|
525
|
+
|
|
502
526
|
</header>
|
|
503
|
-
<main class="artifact-editor auth-panel">
|
|
527
|
+
<main id="main-content" class="artifact-editor auth-panel">
|
|
504
528
|
${error ? `<p class="auth-error">${escapeHtml(error)}</p>` : ""}
|
|
505
|
-
${setup ? `<p class="muted"
|
|
529
|
+
${setup ? `<p class="muted">${view.text("login.setupHelp")}</p>` : ""}
|
|
506
530
|
<form class="editor-form auth-form" method="post" action="/login">
|
|
507
531
|
<section class="editor-fields">
|
|
508
532
|
<label class="field">
|
|
509
|
-
<span
|
|
510
|
-
<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">
|
|
511
535
|
</label>
|
|
512
536
|
${setup ? `<label class="field">
|
|
513
|
-
<span
|
|
514
|
-
<input name="name" autocomplete="name">
|
|
537
|
+
<span>${view.text("ui.name")}</span>
|
|
538
|
+
<input name="name" autocomplete="name" class="form-control">
|
|
515
539
|
</label>` : ""}
|
|
516
540
|
<label class="field">
|
|
517
|
-
<span
|
|
518
|
-
<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">
|
|
519
543
|
</label>
|
|
520
544
|
</section>
|
|
521
545
|
<footer class="editor-actions">
|
|
522
|
-
<button type="submit">${setup ? "
|
|
546
|
+
<button type="submit" class="btn btn-primary">${setup ? view.text("login.create") : view.text("nav.signIn")}</button>
|
|
523
547
|
</footer>
|
|
524
548
|
</form>
|
|
525
549
|
</main>
|
|
@@ -534,7 +558,7 @@ export function renderAccountPage({ baseUrl, user, tokens = [], createdToken = "
|
|
|
534
558
|
<tr>
|
|
535
559
|
<td><a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}`)}">${escapeHtml(artifact.title)}</a></td>
|
|
536
560
|
<td>${escapeHtml(artifact.id)}</td>
|
|
537
|
-
<td>${
|
|
561
|
+
<td>${renderDateTime(artifact.updatedAt, view)}</td>
|
|
538
562
|
<td>${artifact.archivedAt ? statusBadge(view.text("artifact.archived", { date: artifact.archivedAt })) : ""}</td>
|
|
539
563
|
</tr>
|
|
540
564
|
`).join("");
|
|
@@ -542,70 +566,65 @@ export function renderAccountPage({ baseUrl, user, tokens = [], createdToken = "
|
|
|
542
566
|
<tr>
|
|
543
567
|
<td>${escapeHtml(token.name)}</td>
|
|
544
568
|
<td>${escapeHtml((token.scopes || ["read", "write"]).join(", "))}</td>
|
|
545
|
-
<td>${
|
|
546
|
-
<td>${token.lastUsedAt ?
|
|
547
|
-
<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>
|
|
548
572
|
<td>
|
|
549
573
|
${token.revokedAt ? "" : `<form method="post" action="/account/tokens/${encodeURIComponent(token.id)}/revoke">
|
|
550
|
-
<button type="submit"
|
|
574
|
+
<button type="submit" class="btn btn-outline-danger">${view.text("ui.revoke")}</button>
|
|
551
575
|
</form>`}
|
|
552
576
|
</td>
|
|
553
577
|
</tr>
|
|
554
578
|
`).join("");
|
|
555
579
|
|
|
556
580
|
return pageShell({
|
|
557
|
-
title: "
|
|
581
|
+
title: view.raw("nav.account"),
|
|
558
582
|
body: `
|
|
583
|
+
${workspaceHeader(user, view)}
|
|
559
584
|
<header class="topbar">
|
|
560
585
|
<div>
|
|
561
|
-
<h1
|
|
562
|
-
<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>
|
|
563
588
|
</div>
|
|
564
|
-
|
|
565
|
-
<a href="${view.href("/")}">${view.text("nav.index")}</a>
|
|
566
|
-
${user.role === "admin" ? `<a href="/admin/users">Users</a><a href="/admin/backup">Backup</a><a href="${view.href("/admin/access")}">${view.text("access.title")}</a>` : ""}
|
|
567
|
-
<a href="/account/password">Password</a>
|
|
568
|
-
<form class="nav-form" method="post" action="/logout"><button type="submit">Sign out</button></form>
|
|
569
|
-
${languageSwitcher(view)}
|
|
570
|
-
</nav>
|
|
589
|
+
|
|
571
590
|
</header>
|
|
572
|
-
<main class="artifact-view">
|
|
591
|
+
<main id="main-content" class="settings-page artifact-view">
|
|
573
592
|
${createdToken ? `<section class="token-once">
|
|
574
|
-
<h2
|
|
575
|
-
<p
|
|
593
|
+
<h2>${view.text("account.newToken")}</h2>
|
|
594
|
+
<p>${view.text("account.copyToken")}</p>
|
|
576
595
|
<pre class="artifact-code"><code>${escapeHtml(createdToken)}</code></pre>
|
|
577
596
|
</section>` : ""}
|
|
578
597
|
<section class="meta-card">
|
|
579
|
-
<h2
|
|
598
|
+
<h2>${view.text("account.profile")}</h2>
|
|
580
599
|
<p><strong>${escapeHtml(user.name)}</strong></p>
|
|
581
600
|
<p>${escapeHtml(user.email)}</p>
|
|
582
|
-
<p
|
|
601
|
+
<p>${view.text("ui.role")}: ${view.text(`ui.role.${user.role}`)}</p>
|
|
583
602
|
</section>
|
|
584
603
|
<section class="meta-card">
|
|
585
604
|
<h2>${view.text("tokens.createTitle")}</h2>
|
|
586
605
|
<form class="inline-action token-create-form" method="post" action="/account/tokens">
|
|
587
|
-
<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">
|
|
588
607
|
<fieldset class="token-scopes">
|
|
589
608
|
<legend>${view.text("tokens.scopes")}</legend>
|
|
590
|
-
<label><input type="checkbox" name="scopeRead" checked> ${view.text("tokens.scope.read")}</label>
|
|
591
|
-
<label><input type="checkbox" name="scopeWrite" checked> ${view.text("tokens.scope.write")}</label>
|
|
592
|
-
${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>` : ""}
|
|
593
612
|
</fieldset>
|
|
594
|
-
<button type="submit"
|
|
613
|
+
<button type="submit" class="btn btn-primary">${view.text("account.createToken")}</button>
|
|
595
614
|
</form>
|
|
596
615
|
</section>
|
|
597
616
|
<section class="meta-card">
|
|
598
|
-
<h2
|
|
617
|
+
<h2>${view.text("account.tokens")}</h2>
|
|
599
618
|
<table class="data-table">
|
|
600
|
-
<thead><tr><th
|
|
601
|
-
<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>
|
|
602
621
|
</table>
|
|
603
622
|
</section>
|
|
604
623
|
<section class="meta-card">
|
|
605
624
|
<h2>${view.text("account.privateArtifacts")}</h2>
|
|
606
625
|
<table class="data-table">
|
|
607
|
-
<thead><tr><th
|
|
608
|
-
<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>
|
|
609
628
|
</table>
|
|
610
629
|
</section>
|
|
611
630
|
</main>
|
|
@@ -617,41 +636,37 @@ export function renderAccountPage({ baseUrl, user, tokens = [], createdToken = "
|
|
|
617
636
|
export function renderPasswordPage({ baseUrl, user, required = false, error = "", success = "", locale = DEFAULT_LOCALE, currentPath = "/account/password" }) {
|
|
618
637
|
const view = viewContext(locale, currentPath);
|
|
619
638
|
return pageShell({
|
|
620
|
-
title: "
|
|
639
|
+
title: view.raw("password.title"),
|
|
621
640
|
body: `
|
|
641
|
+
${workspaceHeader(user, view)}
|
|
622
642
|
<header class="topbar">
|
|
623
643
|
<div>
|
|
624
|
-
<h1
|
|
644
|
+
<h1>${view.text("password.title")}</h1>
|
|
625
645
|
<p>${escapeHtml(user.email)} · ${escapeHtml(baseUrl)}</p>
|
|
626
646
|
</div>
|
|
627
|
-
|
|
628
|
-
<a href="${view.href("/")}">${view.text("nav.index")}</a>
|
|
629
|
-
${required ? "" : `<a href="/account">Account</a>`}
|
|
630
|
-
<form class="nav-form" method="post" action="/logout"><button type="submit">Sign out</button></form>
|
|
631
|
-
${languageSwitcher(view)}
|
|
632
|
-
</nav>
|
|
647
|
+
|
|
633
648
|
</header>
|
|
634
|
-
<main class="artifact-editor auth-panel">
|
|
635
|
-
${required ? `<p class="auth-warning"
|
|
649
|
+
<main id="main-content" class="settings-page artifact-editor auth-panel">
|
|
650
|
+
${required ? `<p class="auth-warning">${view.text("password.required")}</p>` : ""}
|
|
636
651
|
${error ? `<p class="auth-error">${escapeHtml(error)}</p>` : ""}
|
|
637
652
|
${success ? `<p class="auth-success">${escapeHtml(success)}</p>` : ""}
|
|
638
653
|
<form class="editor-form auth-form" method="post" action="/account/password">
|
|
639
654
|
<section class="editor-fields">
|
|
640
655
|
<label class="field">
|
|
641
|
-
<span
|
|
642
|
-
<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">
|
|
643
658
|
</label>
|
|
644
659
|
<label class="field">
|
|
645
|
-
<span
|
|
646
|
-
<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">
|
|
647
662
|
</label>
|
|
648
663
|
<label class="field">
|
|
649
|
-
<span
|
|
650
|
-
<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">
|
|
651
666
|
</label>
|
|
652
667
|
</section>
|
|
653
668
|
<footer class="editor-actions">
|
|
654
|
-
<button type="submit"
|
|
669
|
+
<button type="submit" class="btn btn-primary">${view.text("password.submit")}</button>
|
|
655
670
|
</footer>
|
|
656
671
|
</form>
|
|
657
672
|
</main>
|
|
@@ -670,65 +685,60 @@ function renderFullImportSummary(tableCounts) {
|
|
|
670
685
|
return parts ? ` (${parts})` : "";
|
|
671
686
|
}
|
|
672
687
|
|
|
673
|
-
export function renderAdminBackupPage({ baseUrl, integrity, importResult = null, importError = "", locale = DEFAULT_LOCALE, currentPath = "/admin/backup" }) {
|
|
688
|
+
export function renderAdminBackupPage({ user, baseUrl, integrity, importResult = null, importError = "", locale = DEFAULT_LOCALE, currentPath = "/admin/backup" }) {
|
|
674
689
|
const view = viewContext(locale, currentPath);
|
|
675
690
|
return pageShell({
|
|
676
|
-
title: "
|
|
691
|
+
title: view.raw("nav.backup"),
|
|
677
692
|
body: `
|
|
693
|
+
${workspaceHeader(user, view)}
|
|
678
694
|
<header class="topbar">
|
|
679
695
|
<div>
|
|
680
|
-
<h1
|
|
696
|
+
<h1>${view.text("nav.backup")}</h1>
|
|
681
697
|
<p>${escapeHtml(baseUrl)}</p>
|
|
682
698
|
</div>
|
|
683
|
-
|
|
684
|
-
<a href="${view.href("/")}">${view.text("nav.index")}</a>
|
|
685
|
-
<a href="/admin/users">Users</a><a href="${view.href("/admin/access")}">${view.text("access.title")}</a>
|
|
686
|
-
<a href="/admin/retention">${view.text("retention.navLabel")}</a>
|
|
687
|
-
<a href="/account">Account</a>
|
|
688
|
-
${languageSwitcher(view)}
|
|
689
|
-
</nav>
|
|
699
|
+
|
|
690
700
|
</header>
|
|
691
|
-
<main class="artifact-view">
|
|
692
|
-
${importResult ? `<p class="auth-success"
|
|
701
|
+
<main id="main-content" class="settings-page artifact-view">
|
|
702
|
+
${importResult ? `<p class="auth-success">${view.text("backup.complete", { count: importResult.artifactCount })}${importResult.scope === "full" ? renderFullImportSummary(importResult.tableCounts) : ""}.</p>` : ""}
|
|
693
703
|
${importError ? `<p class="auth-error">${escapeHtml(importError)}</p>` : ""}
|
|
694
704
|
<section class="meta-card">
|
|
695
|
-
<h2
|
|
705
|
+
<h2>${view.text("backup.status")}</h2>
|
|
696
706
|
<table class="data-table">
|
|
697
707
|
<tbody>
|
|
698
|
-
<tr><th
|
|
699
|
-
<tr><th
|
|
700
|
-
<tr><th
|
|
701
|
-
<tr><th
|
|
702
|
-
<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>
|
|
703
713
|
</tbody>
|
|
704
714
|
</table>
|
|
705
|
-
${integrity.ok ? "" : `<p class="auth-warning"
|
|
715
|
+
${integrity.ok ? "" : `<p class="auth-warning">${view.text("backup.integrityHelp")}</p>`}
|
|
706
716
|
</section>
|
|
707
717
|
<section class="meta-card">
|
|
708
|
-
<h2
|
|
709
|
-
<p class="muted"
|
|
718
|
+
<h2>${view.text("backup.download")}</h2>
|
|
719
|
+
<p class="muted">${view.text("backup.exportHelp")}</p>
|
|
710
720
|
<form class="editor-form" method="get" action="/admin/backup/export">
|
|
711
721
|
<label class="field">
|
|
712
722
|
<span>${view.text("backup.scopeLabel")}</span>
|
|
713
|
-
<select name="scope">
|
|
723
|
+
<select name="scope" class="form-select">
|
|
714
724
|
<option value="artifacts">${view.text("backup.scopeArtifacts")}</option>
|
|
715
725
|
<option value="full">${view.text("backup.scopeFull")}</option>
|
|
716
726
|
</select>
|
|
717
727
|
</label>
|
|
718
|
-
<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>
|
|
719
729
|
</form>
|
|
720
730
|
</section>
|
|
721
731
|
<section class="meta-card">
|
|
722
|
-
<h2
|
|
723
|
-
<p class="auth-warning"
|
|
732
|
+
<h2>${view.text("backup.restore")}</h2>
|
|
733
|
+
<p class="auth-warning">${view.text("backup.restoreHelp")}</p>
|
|
724
734
|
<form class="editor-form" method="post" action="/admin/backup/import">
|
|
725
735
|
<label class="field content-field">
|
|
726
|
-
<span
|
|
727
|
-
<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>
|
|
728
738
|
</label>
|
|
729
|
-
<label class="checkbox-field"><input type="checkbox" name="confirm" value="replace-all"> ${view.text("backup.confirmLabel")}</label>
|
|
730
|
-
<label class="checkbox-field"><input type="checkbox" name="forceUsers" value="1"> ${view.text("backup.forceUsersLabel")}</label>
|
|
731
|
-
<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>
|
|
732
742
|
</form>
|
|
733
743
|
</section>
|
|
734
744
|
</main>
|
|
@@ -737,50 +747,175 @@ export function renderAdminBackupPage({ baseUrl, integrity, importResult = null,
|
|
|
737
747
|
});
|
|
738
748
|
}
|
|
739
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
|
+
|
|
815
|
+
export function renderAdminUpdatePage({ baseUrl, user, status = {}, locale = DEFAULT_LOCALE, currentPath = "/admin/update" }) {
|
|
816
|
+
const view = viewContext(locale, currentPath);
|
|
817
|
+
const check = status.check;
|
|
818
|
+
const run = status.run;
|
|
819
|
+
const active = ["queued", "running"].includes(run?.state);
|
|
820
|
+
const hasUpdate = check?.updateAvailable && check.targetVersion && status.currentVersion !== check.targetVersion;
|
|
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;
|
|
822
|
+
const states = Object.fromEntries(["queued", "running", "succeeded", "failed", "interrupted"].map((state) => [state, view.raw(`update.${state}`)]));
|
|
823
|
+
const messages = { ...states, reconnect: view.raw("update.reconnect"), signIn: view.raw("update.signIn") };
|
|
824
|
+
return pageShell({
|
|
825
|
+
title: view.text("update.title"), locale: view.locale,
|
|
826
|
+
body: `${workspaceHeader(user, view)}
|
|
827
|
+
<header class="topbar"><div><h1>${view.text("update.title")}</h1><p>${escapeHtml(baseUrl)}</p></div></header>
|
|
828
|
+
<main id="main-content" class="settings-page artifact-view">
|
|
829
|
+
<section class="meta-card"><h2>${view.text("update.installed")}</h2><p>Artifacty <strong data-update-version>${escapeHtml(status.currentVersion || APP_VERSION)}</strong></p>
|
|
830
|
+
<p>${view.text("update.help")}</p>
|
|
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>
|
|
832
|
+
</section>
|
|
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>` : ""}
|
|
836
|
+
${check.supported && check.updateAvailable && !canStart && !active && !check.errorCode && status.currentVersion !== check.targetVersion ? `<p>${view.text("update.checkAgain")}</p>` : ""}
|
|
837
|
+
${canStart ? `<p>${view.text("update.available", { version: check.targetVersion })}</p><p>${view.text("update.backup")}</p>
|
|
838
|
+
<form class="editor-form" method="post" action="/admin/update/start">${localeInput(view.locale)}
|
|
839
|
+
<input type="hidden" name="confirmationId" value="${escapeAttribute(check.confirmationId)}">
|
|
840
|
+
<input type="hidden" name="targetVersion" value="${escapeAttribute(check.targetVersion)}">
|
|
841
|
+
<label><input type="checkbox" name="confirmUpdate" value="true" required class="form-check-input"> ${view.text("update.confirm")}</label>
|
|
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>` : ""}
|
|
843
|
+
<button type="submit" class="btn btn-primary">${view.text("update.start")}</button>
|
|
844
|
+
</form>` : ""}
|
|
845
|
+
</section>` : ""}
|
|
846
|
+
${run ? `<section class="meta-card" data-update-status data-active="${active}" data-messages="${escapeAttribute(JSON.stringify(messages))}"><h2>${view.text("update.progress")}</h2>
|
|
847
|
+
<p>${view.text("update.latest")}: ${escapeHtml(run.targetVersion || "—")}</p>
|
|
848
|
+
<p role="status" aria-live="polite" data-update-state>${escapeHtml(states[run.state] || states.running)}</p>
|
|
849
|
+
<p data-update-error>${escapeHtml(run.error || "")}</p>
|
|
850
|
+
<a href="${view.href("/admin/update")}">${view.text("update.refresh")}</a> <a data-update-login hidden href="${view.href("/login")}">${view.text("nav.signIn")}</a>
|
|
851
|
+
</section>` : ""}
|
|
852
|
+
</main>`,
|
|
853
|
+
afterBody: active ? '<script type="module" src="/assets/update.js"></script>' : ""
|
|
854
|
+
});
|
|
855
|
+
}
|
|
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
|
+
|
|
740
874
|
export function renderAdminAccessPage({ baseUrl, user, mode, teams = [], users = [], networkPolicy = { allowedCidrs: [] }, currentIp = "", preview = null, pendingNetworkPolicy = null, shareLinkPolicy = {}, locale = DEFAULT_LOCALE, currentPath = "/admin/access" }) {
|
|
741
875
|
const view = viewContext(locale, currentPath);
|
|
742
876
|
return pageShell({
|
|
743
877
|
title: view.text("access.title"),
|
|
744
878
|
locale: view.locale,
|
|
745
|
-
body:
|
|
746
|
-
<
|
|
747
|
-
|
|
879
|
+
body: `${workspaceHeader(user, view)}
|
|
880
|
+
<header class="topbar"><div><h1>${view.text("access.title")}</h1><p>${escapeHtml(baseUrl)}</p></div>
|
|
881
|
+
</header>
|
|
882
|
+
<main id="main-content" class="settings-page artifact-view">
|
|
748
883
|
<section class="meta-card"><h2>${view.text("access.mode")}</h2>
|
|
749
884
|
<form class="editor-form" method="post" action="/admin/access/preview">${localeInput(view.locale)}
|
|
750
|
-
<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>
|
|
751
886
|
<p class="muted">${view.text("access.publicHelp")}</p>
|
|
752
|
-
<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>
|
|
753
888
|
<p class="muted">${view.text("access.privateHelp")}</p>
|
|
754
|
-
<button type="submit">${view.text("access.preview")}</button>
|
|
889
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("access.preview")}</button>
|
|
755
890
|
</form>
|
|
756
891
|
</section>
|
|
757
892
|
${preview ? `<section class="meta-card"><h2>${view.text("access.preview")}</h2><p>${view.text("access.previewCount", { count: preview.anonymousArtifactCount, mode: preview.mode })}</p>
|
|
758
|
-
<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>` : ""}
|
|
759
|
-
<section class="meta-card"><h2>${view.text("sharing.links")}</h2><form method="post" action="/admin/access/share-links">${localeInput(view.locale)}
|
|
760
|
-
<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>
|
|
761
896
|
${pendingNetworkPolicy ? `<section class="meta-card"><h2>${view.text("access.pendingNetwork")}</h2><p>${view.text("access.pendingHelp", { expiresAt: pendingNetworkPolicy.expiresAt })}</p>
|
|
762
|
-
${["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>` : ""}
|
|
763
898
|
<section class="meta-card"><h2>${view.text("access.network")}</h2>
|
|
764
899
|
<p class="muted">${view.text("access.networkHelp")}</p>
|
|
765
900
|
<p>${view.text("access.currentIp")}: <code>${escapeHtml(currentIp)}</code></p>
|
|
766
901
|
<form class="editor-form" method="post" action="/admin/access/network">${localeInput(view.locale)}
|
|
767
|
-
<label class="field"><span>${view.text("access.allowedCidrs")}</span><textarea name="allowedCidrs" rows="5">${escapeHtml((networkPolicy.allowedCidrs || []).join("\n"))}</textarea></label>
|
|
768
|
-
<button type="submit">${view.text("access.saveNetwork")}</button>
|
|
902
|
+
<label class="field"><span>${view.text("access.allowedCidrs")}</span><textarea name="allowedCidrs" rows="5" class="form-control">${escapeHtml((networkPolicy.allowedCidrs || []).join("\n"))}</textarea></label>
|
|
903
|
+
<button type="submit" class="btn btn-primary">${view.text("access.saveNetwork")}</button>
|
|
769
904
|
</form>
|
|
770
905
|
</section>
|
|
771
|
-
<section class="meta-card"><h2>${view.text("access.samples")}</h2><p>${view.text("access.samplesHelp")}</p>
|
|
772
|
-
${["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>
|
|
773
906
|
<section class="meta-card"><h2>${view.text("access.teams")}</h2>
|
|
774
907
|
<form class="editor-form auth-form" method="post" action="/admin/teams">${localeInput(view.locale)}
|
|
775
|
-
<label class="field"><span>${view.text("access.teamName")}</span><input name="name" required></label>
|
|
776
|
-
<button type="submit">${view.text("access.createTeam")}</button>
|
|
908
|
+
<label class="field"><span>${view.text("access.teamName")}</span><input name="name" required class="form-control"></label>
|
|
909
|
+
<button type="submit" class="btn btn-primary">${view.text("access.createTeam")}</button>
|
|
777
910
|
</form>
|
|
778
911
|
${teams.length ? teams.map((team) => `<section><h3>${escapeHtml(team.name)}</h3>
|
|
779
912
|
<form class="editor-form" method="post" action="/admin/teams/${encodeURIComponent(team.id)}/members">${localeInput(view.locale)}
|
|
780
913
|
<fieldset><legend>${view.text("access.members")}</legend>${accessCheckboxes(users, "userIds", team.memberIds || [], view)}</fieldset>
|
|
781
|
-
<button type="submit">${view.text("access.saveMembers")}</button>
|
|
914
|
+
<button type="submit" class="btn btn-primary">${view.text("access.saveMembers")}</button>
|
|
782
915
|
</form></section>`).join("") : `<p class="muted">${view.text("access.noTeams")}</p>`}
|
|
783
916
|
</section>
|
|
917
|
+
<section class="meta-card"><h2>${view.text("access.samples")}</h2><p>${view.text("access.samplesHelp")}</p>
|
|
918
|
+
${["install", "remove"].map((action) => `<form method="post" action="/admin/samples/${action}">${localeInput(view.locale)}<button type="submit" class="btn btn-outline-secondary">${view.text(`access.samples.${action}`)}</button></form>`).join("")}</section>
|
|
784
919
|
</main>`
|
|
785
920
|
});
|
|
786
921
|
}
|
|
@@ -790,14 +925,15 @@ export function renderArtifactSharingPage({ baseUrl, user, artifact, sharing = {
|
|
|
790
925
|
return pageShell({
|
|
791
926
|
title: view.text("sharing.title"),
|
|
792
927
|
locale: view.locale,
|
|
793
|
-
body:
|
|
794
|
-
<
|
|
795
|
-
<
|
|
928
|
+
body: `${workspaceHeader(user, view)}
|
|
929
|
+
<header class="topbar"><div><h1>${view.text("sharing.title")}: ${escapeHtml(artifact.title)}</h1><p>${escapeHtml(baseUrl)}</p></div>
|
|
930
|
+
<nav><a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}`)}">${view.text("nav.artifact")}</a></nav></header>
|
|
931
|
+
<main id="main-content" class="settings-page artifact-view"><section class="meta-card">
|
|
796
932
|
<p>${view.text("sharing.help")}</p>
|
|
797
933
|
<form class="editor-form" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/sharing">${localeInput(view.locale)}
|
|
798
934
|
<fieldset><legend>${view.text("sharing.users")}</legend>${accessCheckboxes(users, "userIds", sharing.userIds || [], view, sharing.userPermissions || {})}</fieldset>
|
|
799
935
|
<fieldset><legend>${view.text("sharing.teams")}</legend>${accessCheckboxes(teams, "teamIds", sharing.teamIds || [], view, sharing.teamPermissions || {})}</fieldset>
|
|
800
|
-
<button type="submit">${view.text("sharing.save")}</button>
|
|
936
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("sharing.save")}</button>
|
|
801
937
|
</form>
|
|
802
938
|
</section>
|
|
803
939
|
${accessReport && (user?.role === "admin" || user?.id === artifact.ownerUserId) ? `<section class="meta-card"><h2>${view.text("sharing.effective")}</h2><p>${view.text("sharing.networkCondition")}: ${escapeHtml(networkPolicy.allowedCidrs?.length ? networkPolicy.allowedCidrs.join(", ") : view.text("sharing.unrestricted"))}</p><p>${view.text("sharing.anonymous")}: ${view.text(accessReport.anonymousRead ? "sharing.allowed" : "sharing.denied")}</p>
|
|
@@ -805,27 +941,28 @@ export function renderArtifactSharingPage({ baseUrl, user, artifact, sharing = {
|
|
|
805
941
|
<section class="meta-card"><h2>${view.text("sharing.links")}</h2><p>${view.text("sharing.linksHelp")}</p>
|
|
806
942
|
${createdShareLink?.token ? `<p>${view.text("sharing.copyNow")}</p><a href="${escapeAttribute(`${baseUrl.replace(/\/$/, "")}/s/${encodeURIComponent(createdShareLink.token)}`)}">${escapeHtml(`${baseUrl.replace(/\/$/, "")}/s/${encodeURIComponent(createdShareLink.token)}`)}</a>` : ""}
|
|
807
943
|
<form class="editor-form" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/share-links">${localeInput(view.locale)}
|
|
808
|
-
<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>
|
|
809
|
-
<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>
|
|
810
|
-
${shareLinks.map((link) => `<section><p>${escapeHtml(link.id)} · ${link.version == null ? view.text("sharing.latest") : `v${Number(link.version)}`} · ${view.text("sharing.expires")}: ${
|
|
811
|
-
${link.revokedAt ? `<p>${view.text("sharing.revoked")}</p>` : `<form method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/share-links/${encodeURIComponent(link.id)}/revoke">${localeInput(view.locale)}<button type="submit">${view.text("sharing.revoke")}</button></form>`}</section>`).join("")}
|
|
944
|
+
<label class="field"><span>${view.text("sharing.version")}</span><select name="version" class="form-select"><option value="latest">${view.text("sharing.latest")}</option>${(artifact.versions || []).map((item) => `<option value="${Number(item.version)}">v${Number(item.version)}</option>`).join("")}</select></label>
|
|
945
|
+
<label class="field"><span>${view.text("sharing.days")}</span><input type="number" name="days" min="1" max="365" value="7" required class="form-control"></label><button type="submit" class="btn btn-outline-secondary">${view.text("sharing.createLink")}</button></form>
|
|
946
|
+
${shareLinks.map((link) => `<section><p>${escapeHtml(link.id)} · ${link.version == null ? view.text("sharing.latest") : `v${Number(link.version)}`} · ${view.text("sharing.expires")}: ${renderDateTime(link.expiresAt, view)}</p>
|
|
947
|
+
${link.revokedAt ? `<p>${view.text("sharing.revoked")}</p>` : `<form method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/share-links/${encodeURIComponent(link.id)}/revoke">${localeInput(view.locale)}<button type="submit" class="btn btn-outline-danger">${view.text("sharing.revoke")}</button></form>`}</section>`).join("")}
|
|
812
948
|
</section></main>`
|
|
813
949
|
});
|
|
814
950
|
}
|
|
815
951
|
|
|
816
952
|
function accessCheckboxes(items, name, selectedIds, view, permissions = null) {
|
|
817
953
|
if (!items.length) return `<p class="muted">${view.text(name === "userIds" ? "access.noUsers" : "access.noTeams")}</p>`;
|
|
818
|
-
return items.filter((item) => item.active !== false).map((item) => `<label><input type="checkbox" name="${name}" value="${escapeAttribute(item.id)}"${selectedIds.includes(item.id) ? " checked" : ""}> ${escapeHtml(item.name || item.email || item.id)}${item.email && item.name ? ` (${escapeHtml(item.email)})` : ""}</label>${permissions ? `<label>${view.text("sharing.permission")}<select name="${name === "userIds" ? "userPermission" : "teamPermission"}:${escapeAttribute(item.id)}">${["read", "comment", "edit"].map((permission) => `<option value="${permission}"${(permissions[item.id] || "read") === permission ? " selected" : ""}>${view.text(`sharing.permission.${permission}`)}</option>`).join("")}</select></label>` : ""}`).join("");
|
|
954
|
+
return items.filter((item) => item.active !== false).map((item) => `<label><input type="checkbox" name="${name}" value="${escapeAttribute(item.id)}"${selectedIds.includes(item.id) ? " checked" : ""} class="form-check-input"> ${escapeHtml(item.name || item.email || item.id)}${item.email && item.name ? ` (${escapeHtml(item.email)})` : ""}</label>${permissions ? `<label>${view.text("sharing.permission")}<select name="${name === "userIds" ? "userPermission" : "teamPermission"}:${escapeAttribute(item.id)}" class="form-select">${["read", "comment", "edit"].map((permission) => `<option value="${permission}"${(permissions[item.id] || "read") === permission ? " selected" : ""}>${view.text(`sharing.permission.${permission}`)}</option>`).join("")}</select></label>` : ""}`).join("");
|
|
819
955
|
}
|
|
820
956
|
|
|
821
957
|
export function renderInboxPage({ baseUrl, user, notifications = [], unreadCount = 0, locale = DEFAULT_LOCALE, currentPath = "/inbox" }) {
|
|
822
958
|
const view = viewContext(locale, currentPath);
|
|
823
959
|
return pageShell({ title: view.text("inbox.title"), locale: view.locale,
|
|
824
|
-
body:
|
|
825
|
-
<
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
960
|
+
body: `${workspaceHeader(user, view)}
|
|
961
|
+
<header class="topbar"><div><h1>${view.text("inbox.title")}</h1><p>${escapeHtml(baseUrl)}</p></div></header>
|
|
962
|
+
<main id="main-content" class="artifact-view"><section class="meta-card"><p>${view.text("inbox.unread", { count: unreadCount })}</p>
|
|
963
|
+
<form method="post" action="/inbox/read-all">${localeInput(view.locale)}<button type="submit" class="btn btn-outline-secondary">${view.text("inbox.readAll")}</button></form>
|
|
964
|
+
${notifications.length ? notifications.map((item) => `<section><h2><a href="${view.href(`/artifacts/${encodeURIComponent(item.artifactId)}`)}">${escapeHtml(item.title)}</a></h2><p>${["shared-with-me", "comment", "new-version", "review-requested"].includes(item.type) ? view.text(`inbox.type.${item.type}`) : escapeHtml(item.type)} · ${renderDateTime(item.createdAt, view)}</p>
|
|
965
|
+
${item.readAt ? `<p>${view.text("inbox.read")}</p>` : `<form method="post" action="/inbox/${encodeURIComponent(item.id)}/read">${localeInput(view.locale)}<button type="submit" class="btn btn-outline-secondary">${view.text("inbox.markRead")}</button></form>`}</section>`).join("") : `<p>${view.text("inbox.empty")}</p>`}
|
|
829
966
|
</section></main>` });
|
|
830
967
|
}
|
|
831
968
|
|
|
@@ -842,33 +979,40 @@ export function renderSharedArtifactPage({ artifact, version, content, locale =
|
|
|
842
979
|
});
|
|
843
980
|
const needsViewer = ["code", "markdown", "notebook", "sarif", "csv"].includes(version.format);
|
|
844
981
|
return pageShell({ title: artifact.title, locale: view.locale,
|
|
845
|
-
body: `<header class="topbar"><div><h1>${escapeHtml(artifact.title)}</h1><p>${view.text("sharing.readOnly")} · v${Number(version.version)}</p></div></header><main class="artifact-view">${rendered}</main>`,
|
|
982
|
+
body: `<header class="topbar"><div><h1>${escapeHtml(artifact.title)}</h1><p>${view.text("sharing.readOnly")} · v${Number(version.version)}</p></div></header><main id="main-content" class="artifact-view">${rendered}</main>`,
|
|
846
983
|
head: needsViewer ? editorHead() : "",
|
|
847
|
-
afterBody: `${frameResizeScript()}${needsViewer ? viewerScript() : ""}` });
|
|
984
|
+
afterBody: `${frameResizeScript()}${needsViewer ? viewerScript(view.locale) : ""}` });
|
|
848
985
|
}
|
|
849
986
|
|
|
850
|
-
export function renderAdminUsersPage({ baseUrl, user, users = [], importResult = null, importError = "", locale = DEFAULT_LOCALE, currentPath = "/admin/users" }) {
|
|
987
|
+
export function renderAdminUsersPage({ baseUrl, user, users = [], importResult = null, importError = "", roleError = "", resetResult = null, resetError = "", locale = DEFAULT_LOCALE, currentPath = "/admin/users" }) {
|
|
851
988
|
const view = viewContext(locale, currentPath);
|
|
852
989
|
const rows = users.map((item) => `
|
|
853
990
|
<tr>
|
|
854
991
|
<td>${escapeHtml(item.email)}</td>
|
|
855
992
|
<td>${escapeHtml(item.name)}</td>
|
|
856
|
-
<td
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
993
|
+
<td><form class="user-role-form" method="post" action="/admin/users/${encodeURIComponent(item.id)}/role">${localeInput(view.locale)}
|
|
994
|
+
<select name="role" aria-label="${view.text("users.roleFor", { email: item.email })}" class="form-select">
|
|
995
|
+
${["user", "admin"].map((role) => `<option value="${role}"${item.role === role ? " selected" : ""}>${view.text(`ui.role.${role}`)}</option>`).join("")}
|
|
996
|
+
</select><button type="submit" aria-label="${view.text("users.saveRoleFor", { email: item.email })}" class="btn btn-outline-secondary">${view.text("users.saveRole")}</button>
|
|
997
|
+
</form></td>
|
|
998
|
+
<td>${item.active ? view.text("ui.active") : view.text("ui.disabled")}${item.passwordResetRequired ? ` · ${view.text("users.resetRequired")}` : ""}</td>
|
|
999
|
+
<td>${renderDateTime(item.createdAt, view)}</td>
|
|
1000
|
+
<td><div class="user-account-actions">
|
|
1001
|
+
<form method="post" action="/admin/users/${encodeURIComponent(item.id)}/reset-password">${localeInput(view.locale)}
|
|
1002
|
+
<button type="submit" aria-label="${view.text("users.resetFor", { email: item.email })}" class="btn btn-ghost">${view.text("users.resetPassword")}</button>
|
|
1003
|
+
</form>
|
|
1004
|
+
${item.id === user.id ? "" : `<form method="post" action="/admin/users/${encodeURIComponent(item.id)}/${item.active ? "disable" : "enable"}">${localeInput(view.locale)}
|
|
1005
|
+
<button type="submit" class="btn btn-outline-danger">${item.active ? view.text("ui.disable") : view.text("ui.enable")}</button>
|
|
862
1006
|
</form>`}
|
|
863
|
-
</td>
|
|
1007
|
+
</div></td>
|
|
864
1008
|
</tr>
|
|
865
1009
|
`).join("");
|
|
866
1010
|
const createdRows = (importResult?.created || []).map((item) => `
|
|
867
1011
|
<tr>
|
|
868
1012
|
<td>${escapeHtml(item.user.email)}</td>
|
|
869
|
-
<td>${
|
|
870
|
-
<td>${item.user.passwordResetRequired ? "
|
|
871
|
-
<td>${item.passwordGenerated ? `<code>${escapeHtml(item.temporaryPassword)}</code>` : "
|
|
1013
|
+
<td>${view.text(`ui.role.${item.user.role}`)}</td>
|
|
1014
|
+
<td>${item.user.passwordResetRequired ? view.text("ui.yes") : view.text("ui.no")}</td>
|
|
1015
|
+
<td>${item.passwordGenerated ? `<code>${escapeHtml(item.temporaryPassword)}</code>` : view.text("users.provided")}</td>
|
|
872
1016
|
</tr>
|
|
873
1017
|
`).join("");
|
|
874
1018
|
const skippedRows = (importResult?.skipped || []).map((item) => `
|
|
@@ -879,72 +1023,76 @@ export function renderAdminUsersPage({ baseUrl, user, users = [], importResult =
|
|
|
879
1023
|
`).join("");
|
|
880
1024
|
const importSummary = importResult ? `
|
|
881
1025
|
<section class="meta-card">
|
|
882
|
-
<h2
|
|
883
|
-
<p class="muted">${importResult.created.length
|
|
1026
|
+
<h2>${view.text("users.importResults")}</h2>
|
|
1027
|
+
<p class="muted">${view.text("users.summary", { created: importResult.created.length, skipped: importResult.skipped.length, failed: importResult.failed.length })}</p>
|
|
884
1028
|
${createdRows ? `<table class="data-table">
|
|
885
|
-
<thead><tr><th
|
|
1029
|
+
<thead><tr><th>${view.text("ui.email")}</th><th>${view.text("ui.role")}</th><th>${view.text("users.resetRequired")}</th><th>${view.text("users.temporaryPassword")}</th></tr></thead>
|
|
886
1030
|
<tbody>${createdRows}</tbody>
|
|
887
1031
|
</table>` : ""}
|
|
888
|
-
${skippedRows ? `<h3
|
|
889
|
-
<thead><tr><th
|
|
1032
|
+
${skippedRows ? `<h3>${view.text("users.skipped")}</h3><table class="data-table">
|
|
1033
|
+
<thead><tr><th>${view.text("ui.row")}</th><th>${view.text("ui.email")}</th><th>${view.text("ui.reason")}</th></tr></thead>
|
|
890
1034
|
<tbody>${skippedRows}</tbody>
|
|
891
1035
|
</table>` : ""}
|
|
892
|
-
${failedRows ? `<h3
|
|
893
|
-
<thead><tr><th
|
|
1036
|
+
${failedRows ? `<h3>${view.text("users.failed")}</h3><table class="data-table">
|
|
1037
|
+
<thead><tr><th>${view.text("ui.row")}</th><th>${view.text("ui.email")}</th><th>${view.text("ui.error")}</th></tr></thead>
|
|
894
1038
|
<tbody>${failedRows}</tbody>
|
|
895
1039
|
</table>` : ""}
|
|
896
1040
|
</section>
|
|
897
1041
|
` : "";
|
|
898
1042
|
|
|
899
1043
|
return pageShell({
|
|
900
|
-
title: "
|
|
1044
|
+
title: view.raw("nav.users"),
|
|
901
1045
|
body: `
|
|
1046
|
+
${workspaceHeader(user, view)}
|
|
902
1047
|
<header class="topbar">
|
|
903
1048
|
<div>
|
|
904
|
-
<h1
|
|
1049
|
+
<h1>${view.text("nav.users")}</h1>
|
|
905
1050
|
<p>${escapeHtml(baseUrl)}</p>
|
|
906
1051
|
</div>
|
|
907
|
-
|
|
908
|
-
<a href="${view.href("/")}">${view.text("nav.index")}</a>
|
|
909
|
-
<a href="/admin/backup">Backup</a><a href="${view.href("/admin/access")}">${view.text("access.title")}</a>
|
|
910
|
-
<a href="/admin/retention">${view.text("retention.navLabel")}</a>
|
|
911
|
-
<a href="/account">Account</a>
|
|
912
|
-
${languageSwitcher(view)}
|
|
913
|
-
</nav>
|
|
1052
|
+
|
|
914
1053
|
</header>
|
|
915
|
-
<main class="artifact-view">
|
|
1054
|
+
<main id="main-content" class="settings-page artifact-view">
|
|
1055
|
+
${resetError ? `<p class="auth-error" role="alert">${escapeHtml(resetError)}</p>` : ""}
|
|
1056
|
+
${resetResult ? `<section class="token-once" role="status"><h2>${view.text("users.resetReady")}</h2>
|
|
1057
|
+
<p>${view.text("users.resetAccount", { email: resetResult.user.email })}</p>
|
|
1058
|
+
<pre class="artifact-code"><code>${escapeHtml(resetResult.temporaryPassword)}</code></pre>
|
|
1059
|
+
<p>${view.text("users.resetHelp")}</p>
|
|
1060
|
+
<p>${view.text(`smtp.mailStatus.${["sent", "failed", "not-configured"].includes(resetResult.mailStatus) ? resetResult.mailStatus : "not-configured"}`)}</p>
|
|
1061
|
+
</section>` : ""}
|
|
916
1062
|
<section class="meta-card">
|
|
917
|
-
<h2
|
|
1063
|
+
<h2>${view.text("users.create")}</h2>
|
|
918
1064
|
<form class="editor-form auth-form" method="post" action="/admin/users">
|
|
919
1065
|
<section class="editor-fields">
|
|
920
|
-
<label class="field"><span
|
|
921
|
-
<label class="field"><span
|
|
922
|
-
<label class="field"><span
|
|
923
|
-
<label class="field"><span
|
|
924
|
-
<label class="check-field"><input type="checkbox" name="passwordResetRequired">
|
|
1066
|
+
<label class="field"><span>${view.text("ui.email")}</span><input type="email" name="email" required class="form-control"></label>
|
|
1067
|
+
<label class="field"><span>${view.text("ui.name")}</span><input name="name" class="form-control"></label>
|
|
1068
|
+
<label class="field"><span>${view.text("ui.role")}</span><select name="role" class="form-select"><option value="user">${view.text("ui.role.user")}</option><option value="admin">${view.text("ui.role.admin")}</option></select></label>
|
|
1069
|
+
<label class="field"><span>${view.text("ui.password")}</span><input type="password" name="password" minlength="8" required class="form-control"></label>
|
|
1070
|
+
<label class="check-field"><input type="checkbox" name="passwordResetRequired" class="form-check-input"> ${view.text("users.requirePassword")}</label>
|
|
925
1071
|
</section>
|
|
926
|
-
<footer class="editor-actions"><button type="submit"
|
|
1072
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-primary">${view.text("users.create")}</button></footer>
|
|
927
1073
|
</form>
|
|
928
1074
|
</section>
|
|
929
1075
|
<section class="meta-card">
|
|
930
|
-
<h2
|
|
931
|
-
<p class="muted"
|
|
1076
|
+
<h2>${view.text("users.importTitle")}</h2>
|
|
1077
|
+
<p class="muted">${view.text("users.csvHelp")}</p>
|
|
932
1078
|
${importError ? `<p class="auth-error">${escapeHtml(importError)}</p>` : ""}
|
|
933
1079
|
<form class="editor-form" method="post" action="/admin/users/import">
|
|
934
1080
|
<label class="field content-field">
|
|
935
1081
|
<span>CSV</span>
|
|
936
|
-
<textarea class="compact-textarea" name="csv" spellcheck="false" placeholder="email,name,role user@example.com,User,user" required></textarea>
|
|
1082
|
+
<textarea class="compact-textarea form-control" name="csv" spellcheck="false" placeholder="email,name,role user@example.com,User,user" required></textarea>
|
|
937
1083
|
</label>
|
|
938
|
-
<footer class="editor-actions"><button type="submit"
|
|
1084
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-outline-secondary">${view.text("users.import")}</button></footer>
|
|
939
1085
|
</form>
|
|
940
1086
|
</section>
|
|
941
1087
|
${importSummary}
|
|
942
1088
|
<section class="meta-card">
|
|
943
|
-
<h2
|
|
944
|
-
|
|
945
|
-
|
|
1089
|
+
<h2>${view.text("users.existing")}</h2>
|
|
1090
|
+
${roleError ? `<p class="auth-error" role="alert">${escapeHtml(roleError)}</p>` : ""}
|
|
1091
|
+
<p class="muted">${view.text("users.lastAdmin")}</p>
|
|
1092
|
+
<div class="artifact-table-scroll"><table class="data-table">
|
|
1093
|
+
<thead><tr><th>${view.text("ui.email")}</th><th>${view.text("ui.name")}</th><th>${view.text("ui.role")}</th><th>${view.text("ui.status")}</th><th>${view.text("ui.created")}</th><th></th></tr></thead>
|
|
946
1094
|
<tbody>${rows}</tbody>
|
|
947
|
-
</table>
|
|
1095
|
+
</table></div>
|
|
948
1096
|
</section>
|
|
949
1097
|
</main>
|
|
950
1098
|
`,
|
|
@@ -952,7 +1100,7 @@ export function renderAdminUsersPage({ baseUrl, user, users = [], importResult =
|
|
|
952
1100
|
});
|
|
953
1101
|
}
|
|
954
1102
|
|
|
955
|
-
export function renderAdminWebhooksPage({ baseUrl, webhooks = [], createdSecret = null, createdWebhookId = null, locale = DEFAULT_LOCALE, currentPath = "/admin/webhooks" }) {
|
|
1103
|
+
export function renderAdminWebhooksPage({ user, baseUrl, webhooks = [], createdSecret = null, createdWebhookId = null, locale = DEFAULT_LOCALE, currentPath = "/admin/webhooks" }) {
|
|
956
1104
|
const view = viewContext(locale, currentPath);
|
|
957
1105
|
const rows = webhooks.map((webhook) => `
|
|
958
1106
|
<tr>
|
|
@@ -963,7 +1111,7 @@ export function renderAdminWebhooksPage({ baseUrl, webhooks = [], createdSecret
|
|
|
963
1111
|
<td>${webhook.failureCount}</td>
|
|
964
1112
|
<td>
|
|
965
1113
|
<form method="post" action="/admin/webhooks/${encodeURIComponent(webhook.id)}/delete" onsubmit="return confirm('${escapeAttribute(view.text("webhooks.confirmDelete"))}')">
|
|
966
|
-
<button type="submit">${view.text("webhooks.delete")}</button>
|
|
1114
|
+
<button type="submit" class="btn btn-outline-danger">${view.text("webhooks.delete")}</button>
|
|
967
1115
|
</form>
|
|
968
1116
|
</td>
|
|
969
1117
|
</tr>
|
|
@@ -981,30 +1129,24 @@ export function renderAdminWebhooksPage({ baseUrl, webhooks = [], createdSecret
|
|
|
981
1129
|
return pageShell({
|
|
982
1130
|
title: view.text("webhooks.title"),
|
|
983
1131
|
body: `
|
|
1132
|
+
${workspaceHeader(user, view)}
|
|
984
1133
|
<header class="topbar">
|
|
985
1134
|
<div>
|
|
986
1135
|
<h1>${view.text("webhooks.title")}</h1>
|
|
987
1136
|
<p>${escapeHtml(baseUrl)}</p>
|
|
988
1137
|
</div>
|
|
989
|
-
|
|
990
|
-
<a href="${view.href("/")}">${view.text("nav.index")}</a>
|
|
991
|
-
<a href="/admin/backup">Backup</a><a href="${view.href("/admin/access")}">${view.text("access.title")}</a>
|
|
992
|
-
<a href="/admin/users">Users</a>
|
|
993
|
-
<a href="/admin/retention">${view.text("retention.navLabel")}</a>
|
|
994
|
-
<a href="/account">Account</a>
|
|
995
|
-
${languageSwitcher(view)}
|
|
996
|
-
</nav>
|
|
1138
|
+
|
|
997
1139
|
</header>
|
|
998
|
-
<main class="artifact-view">
|
|
1140
|
+
<main id="main-content" class="settings-page artifact-view">
|
|
999
1141
|
${secretBanner}
|
|
1000
1142
|
<section class="meta-card">
|
|
1001
1143
|
<h2>${view.text("webhooks.createTitle")}</h2>
|
|
1002
1144
|
<form class="editor-form" method="post" action="/admin/webhooks">
|
|
1003
1145
|
<section class="editor-fields">
|
|
1004
|
-
<label class="field"><span>${view.text("webhooks.url")}</span><input type="url" name="url" placeholder="https://example.com/hooks/artifacty" required></label>
|
|
1005
|
-
<label class="field"><span>${view.text("webhooks.eventTypes")}</span><input name="eventTypes" placeholder="${view.text("webhooks.eventTypesPlaceholder")}"></label>
|
|
1146
|
+
<label class="field"><span>${view.text("webhooks.url")}</span><input type="url" name="url" placeholder="https://example.com/hooks/artifacty" required class="form-control"></label>
|
|
1147
|
+
<label class="field"><span>${view.text("webhooks.eventTypes")}</span><input name="eventTypes" placeholder="${view.text("webhooks.eventTypesPlaceholder")}" class="form-control"></label>
|
|
1006
1148
|
</section>
|
|
1007
|
-
<footer class="editor-actions"><button type="submit">${view.text("webhooks.create")}</button></footer>
|
|
1149
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-outline-secondary">${view.text("webhooks.create")}</button></footer>
|
|
1008
1150
|
</form>
|
|
1009
1151
|
</section>
|
|
1010
1152
|
<section class="meta-card">
|
|
@@ -1020,7 +1162,7 @@ export function renderAdminWebhooksPage({ baseUrl, webhooks = [], createdSecret
|
|
|
1020
1162
|
});
|
|
1021
1163
|
}
|
|
1022
1164
|
|
|
1023
|
-
export function renderAdminRetentionPage({ baseUrl, policy, lastSweep = null, report = null, locale = DEFAULT_LOCALE, currentPath = "/admin/retention" }) {
|
|
1165
|
+
export function renderAdminRetentionPage({ user, baseUrl, policy, lastSweep = null, report = null, locale = DEFAULT_LOCALE, currentPath = "/admin/retention" }) {
|
|
1024
1166
|
const view = viewContext(locale, currentPath);
|
|
1025
1167
|
const byTypeText = Object.entries(policy.archiveAfterDays.byType || {})
|
|
1026
1168
|
.map(([type, days]) => `${type}=${days}`)
|
|
@@ -1031,7 +1173,7 @@ export function renderAdminRetentionPage({ baseUrl, policy, lastSweep = null, re
|
|
|
1031
1173
|
<h2>${view.text("retention.lastSweepTitle")}</h2>
|
|
1032
1174
|
<table class="data-table">
|
|
1033
1175
|
<tbody>
|
|
1034
|
-
<tr><th>${view.text("retention.sweepAt")}</th><td>${
|
|
1176
|
+
<tr><th>${view.text("retention.sweepAt")}</th><td>${renderDateTime(lastSweep.createdAt, view)}</td></tr>
|
|
1035
1177
|
<tr><th>${view.text("retention.archived")}</th><td>${escapeHtml(lastSweep.metadata?.archived ?? 0)}</td></tr>
|
|
1036
1178
|
<tr><th>${view.text("retention.purged")}</th><td>${escapeHtml(lastSweep.metadata?.purged ?? 0)}</td></tr>
|
|
1037
1179
|
<tr><th>${view.text("retention.purgeSkipped")}</th><td>${lastSweep.metadata?.purgeSkipped ? view.text("retention.yes") : view.text("retention.no")}</td></tr>
|
|
@@ -1070,32 +1212,35 @@ export function renderAdminRetentionPage({ baseUrl, policy, lastSweep = null, re
|
|
|
1070
1212
|
return pageShell({
|
|
1071
1213
|
title: view.text("retention.title"),
|
|
1072
1214
|
body: `
|
|
1215
|
+
${workspaceHeader(user, view)}
|
|
1073
1216
|
<header class="topbar">
|
|
1074
1217
|
<div>
|
|
1075
1218
|
<h1>${view.text("retention.title")}</h1>
|
|
1076
1219
|
<p>${escapeHtml(baseUrl)}</p>
|
|
1077
1220
|
</div>
|
|
1078
|
-
|
|
1079
|
-
<a href="${view.href("/")}">${view.text("nav.index")}</a>
|
|
1080
|
-
<a href="/admin/backup">Backup</a><a href="${view.href("/admin/access")}">${view.text("access.title")}</a>
|
|
1081
|
-
<a href="/admin/users">Users</a><a href="${view.href("/admin/access")}">${view.text("access.title")}</a>
|
|
1082
|
-
<a href="/account">Account</a>
|
|
1083
|
-
${languageSwitcher(view)}
|
|
1084
|
-
</nav>
|
|
1221
|
+
|
|
1085
1222
|
</header>
|
|
1086
|
-
<main class="artifact-view">
|
|
1223
|
+
<main id="main-content" class="settings-page artifact-view">
|
|
1087
1224
|
<section class="meta-card">
|
|
1088
1225
|
<h2>${view.text("retention.policyTitle")}</h2>
|
|
1089
|
-
<form class="editor-form" method="post" action="/admin/retention"
|
|
1090
|
-
<
|
|
1091
|
-
<
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
<label class="field"><span>${view.text("retention.
|
|
1096
|
-
|
|
1097
|
-
</
|
|
1098
|
-
|
|
1226
|
+
<form class="editor-form retention-policy-form" method="post" action="/admin/retention">${localeInput(view.locale)}
|
|
1227
|
+
<fieldset class="retention-section"><legend>${view.text("retention.artifactsSection")}</legend>
|
|
1228
|
+
<div class="retention-field-grid">
|
|
1229
|
+
<label class="field"><span>${view.text("retention.archiveAfterDaysDefault")}</span><input type="number" min="1" name="archiveAfterDaysDefault" value="${policy.archiveAfterDays.default ?? ""}" class="form-control"></label>
|
|
1230
|
+
<label class="field"><span>${view.text("retention.purgeArchivedAfterDays")}</span><input type="number" min="1" name="purgeArchivedAfterDays" value="${policy.purgeArchivedAfterDays ?? ""}" class="form-control"></label>
|
|
1231
|
+
</div>
|
|
1232
|
+
<label class="field"><span>${view.text("retention.keepTags")}</span><input name="keepTags" value="${escapeAttribute((policy.keepTags || []).join(", "))}" placeholder="pinned, release" class="form-control"></label>
|
|
1233
|
+
</fieldset>
|
|
1234
|
+
<fieldset class="retention-section"><legend>${view.text("retention.typeRulesSection")}</legend>
|
|
1235
|
+
<label class="field"><span>${view.text("retention.archiveAfterDaysByType")}</span><textarea class="compact-textarea form-control" name="archiveAfterDaysByType" placeholder="test-report=30">${escapeHtml(byTypeText)}</textarea></label>
|
|
1236
|
+
</fieldset>
|
|
1237
|
+
<fieldset class="retention-section"><legend>${view.text("retention.historySection")}</legend>
|
|
1238
|
+
<div class="retention-field-grid">
|
|
1239
|
+
<label class="field"><span>${view.text("retention.auditRetentionDays")}</span><input type="number" min="1" name="auditRetentionDays" value="${policy.auditRetentionDays ?? ""}" class="form-control"></label>
|
|
1240
|
+
<label class="field"><span>${view.text("retention.eventRetentionRows")}</span><input type="number" min="1" name="eventRetentionRows" value="${policy.eventRetentionRows ?? ""}" class="form-control"></label>
|
|
1241
|
+
</div>
|
|
1242
|
+
</fieldset>
|
|
1243
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-primary">${view.text("retention.saveButton")}</button></footer>
|
|
1099
1244
|
</form>
|
|
1100
1245
|
</section>
|
|
1101
1246
|
${lastSweepSection}
|
|
@@ -1103,9 +1248,9 @@ export function renderAdminRetentionPage({ baseUrl, policy, lastSweep = null, re
|
|
|
1103
1248
|
<h2>${view.text("retention.runTitle")}</h2>
|
|
1104
1249
|
<p class="auth-warning">${view.text("retention.purgeGateNote")}</p>
|
|
1105
1250
|
<form class="editor-form" method="post" action="/admin/retention/run">
|
|
1106
|
-
<label class="checkbox-field"><input type="checkbox" name="dryRun" value="1" checked> ${view.text("retention.dryRunLabel")}</label>
|
|
1107
|
-
<label class="checkbox-field"><input type="checkbox" name="allowPurge" value="1"> ${view.text("retention.allowPurgeLabel")}</label>
|
|
1108
|
-
<footer class="editor-actions"><button type="submit">${view.text("retention.runButton")}</button></footer>
|
|
1251
|
+
<label class="checkbox-field"><input type="checkbox" name="dryRun" value="1" checked class="form-check-input"> ${view.text("retention.dryRunLabel")}</label>
|
|
1252
|
+
<label class="checkbox-field"><input type="checkbox" name="allowPurge" value="1" class="form-check-input"> ${view.text("retention.allowPurgeLabel")}</label>
|
|
1253
|
+
<footer class="editor-actions"><button type="submit" class="btn btn-outline-secondary">${view.text("retention.runButton")}</button></footer>
|
|
1109
1254
|
</form>
|
|
1110
1255
|
</section>
|
|
1111
1256
|
${reportSection}
|
|
@@ -1161,45 +1306,48 @@ export function renderArtifactPage({ artifact, version, content, comments = [],
|
|
|
1161
1306
|
<span>${view.text("artifact.visibility.label")}</span>
|
|
1162
1307
|
${visibilitySelect(artifact.visibility || "team", view)}
|
|
1163
1308
|
</label>
|
|
1164
|
-
<button type="submit">${view.text("artifact.visibility.change")}</button>
|
|
1165
|
-
</form>`
|
|
1309
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("artifact.visibility.change")}</button>
|
|
1310
|
+
</form><p class="muted">${view.text("artifact.visibility.help")}</p>`
|
|
1166
1311
|
: "";
|
|
1167
1312
|
|
|
1168
1313
|
return pageShell({
|
|
1169
1314
|
title: artifact.title,
|
|
1170
1315
|
body: `
|
|
1316
|
+
${workspaceHeader(user, view)}
|
|
1171
1317
|
<header class="topbar">
|
|
1172
1318
|
<div>
|
|
1173
1319
|
<h1>${escapeHtml(artifact.title)}</h1>
|
|
1174
1320
|
<p>${escapeHtml(artifact.id)} · ${escapeHtml(artifact.sourceAgent)} · ${escapeHtml(baseUrl)}</p>
|
|
1175
1321
|
</div>
|
|
1176
1322
|
<nav>
|
|
1177
|
-
|
|
1323
|
+
|
|
1178
1324
|
${canEdit ? `<a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}/edit`)}">${view.text("nav.edit")}</a>` : ""}
|
|
1179
1325
|
<a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}/diff`)}">${view.text("nav.diff")}</a>
|
|
1180
|
-
${user?.role === "admin" ? `<a href="${view.href(`/admin/artifacts/${encodeURIComponent(artifact.id)}/versions`)}"
|
|
1326
|
+
${user?.role === "admin" ? `<a href="${view.href(`/admin/artifacts/${encodeURIComponent(artifact.id)}/versions`)}">${view.text("nav.versions")}</a>` : ""}
|
|
1181
1327
|
<a href="${view.href(rawUrl)}">${view.text("nav.raw")}</a>
|
|
1182
1328
|
${user && canManageVisibility ? `<a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}/sharing`)}">${view.text("sharing.title")}</a>` : ""}
|
|
1183
|
-
|
|
1184
|
-
|
|
1329
|
+
|
|
1330
|
+
|
|
1185
1331
|
</nav>
|
|
1186
1332
|
</header>
|
|
1187
|
-
<main class="${viewClass}">
|
|
1188
|
-
<section class="meta-strip">
|
|
1189
|
-
|
|
1190
|
-
${typeBadge(artifact.artifactType || "document")}
|
|
1191
|
-
<span>${view.text("artifact.schema", { version: artifact.schemaVersion || 1 })}</span>
|
|
1333
|
+
<main id="main-content" class="${viewClass}">
|
|
1334
|
+
<section class="meta-strip artifact-overview">
|
|
1335
|
+
<span>${escapeHtml(version.format)} · v${version.version}</span>
|
|
1192
1336
|
${artifact.archivedAt ? statusBadge(view.text("artifact.archived", { date: artifact.archivedAt })) : ""}
|
|
1193
1337
|
${visibilityBadge(artifact, view)}
|
|
1194
|
-
<
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1338
|
+
<details class="artifact-details"><summary>${view.text("artifact.details")}</summary><div class="meta-strip">
|
|
1339
|
+
${typeBadge(artifact.artifactType || "document")}
|
|
1340
|
+
<span>${view.text("artifact.schema", { version: artifact.schemaVersion || 1 })}</span>
|
|
1341
|
+
<span title="${escapeAttribute(publisherTitle(artifact, view))}">${view.text("artifact.publisher", { publisher })}</span>
|
|
1342
|
+
<span>${view.text("artifact.bytes", { size: version.sizeBytes })}</span>
|
|
1343
|
+
<span>${renderDateTime(version.createdAt, view)}</span>
|
|
1344
|
+
<span>${escapeHtml(version.sha256.slice(0, 12))}</span>
|
|
1345
|
+
</div></details>
|
|
1198
1346
|
</section>
|
|
1199
1347
|
${canArchive ? `<form class="inline-action" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/${archiveAction}">
|
|
1200
1348
|
${hiddenToken(authToken)}
|
|
1201
1349
|
${localeInput(view.locale)}
|
|
1202
|
-
<button type="submit">${archiveLabel}</button>
|
|
1350
|
+
<button type="submit" class="btn btn-outline-secondary">${archiveLabel}</button>
|
|
1203
1351
|
</form>` : ""}
|
|
1204
1352
|
${visibilityForm}
|
|
1205
1353
|
<section class="version-strip">${versionLinks}</section>
|
|
@@ -1209,7 +1357,7 @@ export function renderArtifactPage({ artifact, version, content, comments = [],
|
|
|
1209
1357
|
</main>
|
|
1210
1358
|
`,
|
|
1211
1359
|
head: needsViewerScript ? editorHead() : "",
|
|
1212
|
-
afterBody: `${frameResizeScript()}${needsViewerScript ? viewerScript() : ""}`,
|
|
1360
|
+
afterBody: `${frameResizeScript()}${needsViewerScript ? viewerScript(view.locale) : ""}`,
|
|
1213
1361
|
locale: view.locale
|
|
1214
1362
|
});
|
|
1215
1363
|
}
|
|
@@ -1298,14 +1446,14 @@ function renderCommentEntry(artifact, comment, view, { authToken, canModerate, c
|
|
|
1298
1446
|
? `<form class="inline-action" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/comments/${encodeURIComponent(comment.id)}/resolve">
|
|
1299
1447
|
${hiddenToken(authToken)}
|
|
1300
1448
|
${localeInput(view.locale)}
|
|
1301
|
-
<button type="submit">${view.text("comments.resolve")}</button>
|
|
1449
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("comments.resolve")}</button>
|
|
1302
1450
|
</form>`
|
|
1303
1451
|
: "";
|
|
1304
1452
|
const deleteForm = canModerate
|
|
1305
1453
|
? `<form class="inline-action" method="post" action="/artifacts/${encodeURIComponent(artifact.id)}/comments/${encodeURIComponent(comment.id)}/delete">
|
|
1306
1454
|
${hiddenToken(authToken)}
|
|
1307
1455
|
${localeInput(view.locale)}
|
|
1308
|
-
<button type="submit">${view.text("comments.delete")}</button>
|
|
1456
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("comments.delete")}</button>
|
|
1309
1457
|
</form>`
|
|
1310
1458
|
: "";
|
|
1311
1459
|
|
|
@@ -1313,7 +1461,7 @@ function renderCommentEntry(artifact, comment, view, { authToken, canModerate, c
|
|
|
1313
1461
|
<li class="comment-item${isReply ? " comment-reply" : ""}${resolved ? " comment-resolved" : ""}">
|
|
1314
1462
|
<div class="comment-header">
|
|
1315
1463
|
<span class="comment-author">${escapeHtml(comment.authorLabel)}</span>
|
|
1316
|
-
<span class="comment-meta">${
|
|
1464
|
+
<span class="comment-meta">${renderDateTime(comment.createdAt, view)}</span>
|
|
1317
1465
|
${anchorLabel ? `<span class="comment-anchor">${anchorLabel}</span>` : ""}
|
|
1318
1466
|
${resolved ? `<span class="badge s-archived">${view.text("comments.resolved")}</span>` : ""}
|
|
1319
1467
|
</div>
|
|
@@ -1351,13 +1499,13 @@ function renderCommentForm(artifact, view, { version, authToken, parentId, compa
|
|
|
1351
1499
|
${parentId ? `<input type="hidden" name="parentId" value="${escapeAttribute(parentId)}">` : ""}
|
|
1352
1500
|
<label>
|
|
1353
1501
|
<span>${view.text(compact ? "comments.replyLabel" : "comments.bodyLabel")}</span>
|
|
1354
|
-
<textarea name="body" required maxlength="16384"></textarea>
|
|
1502
|
+
<textarea name="body" required maxlength="16384" class="form-control"></textarea>
|
|
1355
1503
|
</label>
|
|
1356
1504
|
${compact ? "" : `<label class="comment-line-label">
|
|
1357
1505
|
<span>${view.text("comments.lineLabel")}</span>
|
|
1358
|
-
<input type="number" name="line" min="1">
|
|
1506
|
+
<input type="number" name="line" min="1" class="form-control">
|
|
1359
1507
|
</label>`}
|
|
1360
|
-
<button type="submit">${view.text(compact ? "comments.reply" : "comments.submit")}</button>
|
|
1508
|
+
<button type="submit" class="btn btn-primary">${view.text(compact ? "comments.reply" : "comments.submit")}</button>
|
|
1361
1509
|
</form>
|
|
1362
1510
|
`;
|
|
1363
1511
|
}
|
|
@@ -1370,11 +1518,11 @@ function renderReviewStatusForm(artifact, view, authToken) {
|
|
|
1370
1518
|
${localeInput(view.locale)}
|
|
1371
1519
|
<label>
|
|
1372
1520
|
<span>${view.text("comments.reviewStatus.label")}</span>
|
|
1373
|
-
<select name="status">
|
|
1521
|
+
<select name="status" class="form-select">
|
|
1374
1522
|
${REVIEW_STATUSES.map((status) => `<option value="${status}"${status === current ? " selected" : ""}>${view.text(`comments.reviewStatus.${status}`)}</option>`).join("")}
|
|
1375
1523
|
</select>
|
|
1376
1524
|
</label>
|
|
1377
|
-
<button type="submit">${view.text("comments.reviewStatus.change")}</button>
|
|
1525
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("comments.reviewStatus.change")}</button>
|
|
1378
1526
|
</form>
|
|
1379
1527
|
`;
|
|
1380
1528
|
}
|
|
@@ -1456,7 +1604,7 @@ function artifactViewClass({ artifact, version }) {
|
|
|
1456
1604
|
return classes.join(" ");
|
|
1457
1605
|
}
|
|
1458
1606
|
|
|
1459
|
-
export function renderDiffPage({ artifact, fromVersion, toVersion, fromContent, toContent, diffRows, view: diffView = "lines", structuredDiff, baseUrl, locale = DEFAULT_LOCALE, currentPath = "/" }) {
|
|
1607
|
+
export function renderDiffPage({ user, artifact, fromVersion, toVersion, fromContent, toContent, diffRows, view: diffView = "lines", structuredDiff, baseUrl, locale = DEFAULT_LOCALE, currentPath = "/" }) {
|
|
1460
1608
|
const view = viewContext(locale, currentPath);
|
|
1461
1609
|
const title = view.raw("diff.title", { title: artifact.title });
|
|
1462
1610
|
const versionOptions = artifact.versions
|
|
@@ -1481,8 +1629,8 @@ export function renderDiffPage({ artifact, fromVersion, toVersion, fromContent,
|
|
|
1481
1629
|
|
|
1482
1630
|
const preview = toVersion.format === "html"
|
|
1483
1631
|
? `<section class="split-preview">
|
|
1484
|
-
<div>${renderContent(fromVersion.format, fromContent)}</div>
|
|
1485
|
-
<div>${renderContent(toVersion.format, toContent)}</div>
|
|
1632
|
+
<div>${renderContent(fromVersion.format, fromContent, {}, { view })}</div>
|
|
1633
|
+
<div>${renderContent(toVersion.format, toContent, {}, { view })}</div>
|
|
1486
1634
|
</section>`
|
|
1487
1635
|
: "";
|
|
1488
1636
|
|
|
@@ -1504,6 +1652,7 @@ export function renderDiffPage({ artifact, fromVersion, toVersion, fromContent,
|
|
|
1504
1652
|
return pageShell({
|
|
1505
1653
|
title,
|
|
1506
1654
|
body: `
|
|
1655
|
+
${workspaceHeader(user, view)}
|
|
1507
1656
|
<header class="topbar">
|
|
1508
1657
|
<div>
|
|
1509
1658
|
<h1>${escapeHtml(title)}</h1>
|
|
@@ -1511,22 +1660,22 @@ export function renderDiffPage({ artifact, fromVersion, toVersion, fromContent,
|
|
|
1511
1660
|
</div>
|
|
1512
1661
|
<nav>
|
|
1513
1662
|
<a href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}`)}">${view.text("nav.artifact")}</a>
|
|
1514
|
-
|
|
1663
|
+
|
|
1515
1664
|
</nav>
|
|
1516
1665
|
</header>
|
|
1517
|
-
<main class="artifact-view">
|
|
1666
|
+
<main id="main-content" class="artifact-view">
|
|
1518
1667
|
<form class="diff-form" method="get" action="/artifacts/${encodeURIComponent(artifact.id)}/diff">
|
|
1519
1668
|
${localeInput(view.locale)}
|
|
1520
1669
|
<input type="hidden" name="view" value="${escapeAttribute(resolvedView)}">
|
|
1521
1670
|
<label class="field">
|
|
1522
1671
|
<span>${view.text("diff.from")}</span>
|
|
1523
|
-
<select name="from">${versionOptions}</select>
|
|
1672
|
+
<select name="from" class="form-select">${versionOptions}</select>
|
|
1524
1673
|
</label>
|
|
1525
1674
|
<label class="field">
|
|
1526
1675
|
<span>${view.text("diff.to")}</span>
|
|
1527
|
-
<select name="to">${versionOptions}</select>
|
|
1676
|
+
<select name="to" class="form-select">${versionOptions}</select>
|
|
1528
1677
|
</label>
|
|
1529
|
-
<button type="submit">${view.text("diff.compare")}</button>
|
|
1678
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("diff.compare")}</button>
|
|
1530
1679
|
</form>
|
|
1531
1680
|
<script>
|
|
1532
1681
|
document.querySelector('select[name="from"]').value = ${JSON.stringify(String(fromVersion.version))};
|
|
@@ -1755,6 +1904,7 @@ function renderBundleFiles(content, options = {}) {
|
|
|
1755
1904
|
}
|
|
1756
1905
|
|
|
1757
1906
|
export function renderContent(format, content, metadata = {}, options = {}) {
|
|
1907
|
+
const view = options.view || viewContext(options.locale || DEFAULT_LOCALE, "/");
|
|
1758
1908
|
if (format === "html") {
|
|
1759
1909
|
return `<iframe class="artifact-frame" sandbox="allow-scripts allow-forms allow-popups" srcdoc="${escapeAttribute(htmlFrameContent(content))}"></iframe>`;
|
|
1760
1910
|
}
|
|
@@ -1782,15 +1932,15 @@ export function renderContent(format, content, metadata = {}, options = {}) {
|
|
|
1782
1932
|
}
|
|
1783
1933
|
|
|
1784
1934
|
if (format === "notebook") {
|
|
1785
|
-
return renderNotebook(content);
|
|
1935
|
+
return renderNotebook(content, view);
|
|
1786
1936
|
}
|
|
1787
1937
|
|
|
1788
1938
|
if (format === "sarif") {
|
|
1789
|
-
return renderSarif(content);
|
|
1939
|
+
return renderSarif(content, view);
|
|
1790
1940
|
}
|
|
1791
1941
|
|
|
1792
1942
|
if (format === "csv") {
|
|
1793
|
-
return renderCsv(content);
|
|
1943
|
+
return renderCsv(content, view);
|
|
1794
1944
|
}
|
|
1795
1945
|
|
|
1796
1946
|
if (format === "image") {
|
|
@@ -1804,7 +1954,7 @@ export function renderContent(format, content, metadata = {}, options = {}) {
|
|
|
1804
1954
|
if (format === "code") {
|
|
1805
1955
|
const language = metadata.language || metadata.artifactyImport?.language || "";
|
|
1806
1956
|
return `<section class="artifact-code-viewer" data-artifacty-code-viewer data-language="${escapeAttribute(language)}">
|
|
1807
|
-
<textarea hidden>${escapeHtml(content)}</textarea>
|
|
1957
|
+
<textarea hidden class="form-control">${escapeHtml(content)}</textarea>
|
|
1808
1958
|
<pre class="artifact-code artifact-code-fallback"><code>${escapeHtml(content)}</code></pre>
|
|
1809
1959
|
</section>`;
|
|
1810
1960
|
}
|
|
@@ -1814,7 +1964,7 @@ export function renderContent(format, content, metadata = {}, options = {}) {
|
|
|
1814
1964
|
return `<iframe class="artifact-frame artifact-react-frame" sandbox="allow-scripts" src="${escapeAttribute(options.reactFrameUrl)}"></iframe>`;
|
|
1815
1965
|
}
|
|
1816
1966
|
return `<section class="artifact-react-disabled">
|
|
1817
|
-
<p
|
|
1967
|
+
<p>${view.text("react.disabled")}</p>
|
|
1818
1968
|
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>
|
|
1819
1969
|
</section>`;
|
|
1820
1970
|
}
|
|
@@ -1905,53 +2055,62 @@ export function renderReactFramePage({ title, content }) {
|
|
|
1905
2055
|
}
|
|
1906
2056
|
|
|
1907
2057
|
export function pageShell({ title, body, head = "", afterBody = "", locale = DEFAULT_LOCALE }) {
|
|
2058
|
+
const darkThemeVariables = ` --bg: #15171b;
|
|
2059
|
+
--grid: rgba(120, 160, 200, 0.06);
|
|
2060
|
+
--panel: #1c1f24;
|
|
2061
|
+
--panel-2: #25292f;
|
|
2062
|
+
--text: #e6edf3;
|
|
2063
|
+
--muted: #adb5c0;
|
|
2064
|
+
--faint: #9aa5b2;
|
|
2065
|
+
--line: #343b44;
|
|
2066
|
+
--line-2: #596575;
|
|
2067
|
+
--accent: #9cbcf5;
|
|
2068
|
+
--accent-2: #bbd1f8;
|
|
2069
|
+
--accent-ink: #18253d;
|
|
2070
|
+
--accent-soft: #29374e;
|
|
2071
|
+
--ring: #9cbcf5;
|
|
2072
|
+
--code: #080c12;
|
|
2073
|
+
--code-text: #e6edf3;
|
|
2074
|
+
`;
|
|
1908
2075
|
return `<!doctype html>
|
|
1909
2076
|
<html lang="${escapeAttribute(locale)}">
|
|
1910
2077
|
<head>
|
|
1911
2078
|
<meta charset="utf-8">
|
|
2079
|
+
<script src="/assets/theme.js"></script><script type="module" src="/assets/auto-apply.js"></script>
|
|
2080
|
+
<script src="/assets/sidebar.js"></script>
|
|
1912
2081
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1913
2082
|
<title>${escapeHtml(title)}</title>
|
|
1914
2083
|
${head}
|
|
1915
2084
|
<style>
|
|
1916
2085
|
:root {
|
|
1917
|
-
color-scheme: light
|
|
1918
|
-
--bg: #
|
|
2086
|
+
color-scheme: light;
|
|
2087
|
+
--bg: #f5f6f8;
|
|
1919
2088
|
--grid: rgba(31, 58, 99, 0.05);
|
|
1920
2089
|
--panel: #ffffff;
|
|
1921
|
-
--panel-2: #
|
|
1922
|
-
--text: #
|
|
1923
|
-
--muted: #
|
|
1924
|
-
--faint: #
|
|
1925
|
-
--line: #
|
|
1926
|
-
--line-2: #
|
|
1927
|
-
--accent: #
|
|
1928
|
-
--accent-2: #
|
|
2090
|
+
--panel-2: #eef0f3;
|
|
2091
|
+
--text: #20242b;
|
|
2092
|
+
--muted: #58616e;
|
|
2093
|
+
--faint: #626c79;
|
|
2094
|
+
--line: #dfe3e8;
|
|
2095
|
+
--line-2: #b9c1cc;
|
|
2096
|
+
--accent: #315ca8;
|
|
2097
|
+
--accent-2: #264a89;
|
|
1929
2098
|
--accent-ink: #ffffff;
|
|
1930
|
-
--accent-soft:
|
|
1931
|
-
--ring:
|
|
2099
|
+
--accent-soft: #eaf0fb;
|
|
2100
|
+
--ring: #315ca8;
|
|
1932
2101
|
--code: #0e1320;
|
|
1933
2102
|
--code-text: #e6edf3;
|
|
1934
2103
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans KR", sans-serif;
|
|
1935
2104
|
--mono: ui-monospace, "SFMono-Regular", "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
|
1936
2105
|
}
|
|
2106
|
+
:root[data-theme="dark"] {
|
|
2107
|
+
color-scheme: dark;
|
|
2108
|
+
${darkThemeVariables}
|
|
2109
|
+
}
|
|
1937
2110
|
@media (prefers-color-scheme: dark) {
|
|
1938
|
-
:root {
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
--panel: #12171f;
|
|
1942
|
-
--panel-2: #171d27;
|
|
1943
|
-
--text: #e6edf3;
|
|
1944
|
-
--muted: #9aa6b3;
|
|
1945
|
-
--faint: #6b7686;
|
|
1946
|
-
--line: #222a35;
|
|
1947
|
-
--line-2: #2d3744;
|
|
1948
|
-
--accent: #22b8cf;
|
|
1949
|
-
--accent-2: #3ccbdd;
|
|
1950
|
-
--accent-ink: #04161c;
|
|
1951
|
-
--accent-soft: rgba(34, 184, 207, 0.14);
|
|
1952
|
-
--ring: rgba(34, 184, 207, 0.40);
|
|
1953
|
-
--code: #080c12;
|
|
1954
|
-
--code-text: #e6edf3;
|
|
2111
|
+
:root:not([data-theme="light"]) {
|
|
2112
|
+
color-scheme: dark;
|
|
2113
|
+
${darkThemeVariables}
|
|
1955
2114
|
}
|
|
1956
2115
|
}
|
|
1957
2116
|
* { box-sizing: border-box; }
|
|
@@ -1959,13 +2118,10 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
1959
2118
|
margin: 0;
|
|
1960
2119
|
min-height: 100vh;
|
|
1961
2120
|
background-color: var(--bg);
|
|
1962
|
-
|
|
1963
|
-
linear-gradient(var(--grid) 1px, transparent 1px),
|
|
1964
|
-
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
|
|
1965
|
-
background-size: 28px 28px;
|
|
1966
|
-
background-position: center top;
|
|
2121
|
+
|
|
1967
2122
|
color: var(--text);
|
|
1968
2123
|
font-family: var(--sans);
|
|
2124
|
+
font-size: 14px;
|
|
1969
2125
|
line-height: 1.55;
|
|
1970
2126
|
letter-spacing: 0;
|
|
1971
2127
|
-webkit-font-smoothing: antialiased;
|
|
@@ -1973,28 +2129,28 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
1973
2129
|
a { color: var(--accent); text-decoration: none; }
|
|
1974
2130
|
a:hover { color: var(--accent-2); text-decoration: underline; }
|
|
1975
2131
|
.topbar {
|
|
1976
|
-
position:
|
|
1977
|
-
|
|
1978
|
-
|
|
2132
|
+
position: relative;
|
|
2133
|
+
width: min(1440px, calc(100% - 64px));
|
|
2134
|
+
margin: 28px auto 20px;
|
|
1979
2135
|
display: flex;
|
|
1980
2136
|
align-items: center;
|
|
1981
2137
|
justify-content: space-between;
|
|
1982
2138
|
gap: 24px;
|
|
1983
|
-
padding:
|
|
2139
|
+
padding: 0 0 20px;
|
|
1984
2140
|
border-bottom: 1px solid var(--line);
|
|
1985
|
-
background:
|
|
1986
|
-
backdrop-filter: saturate(150%) blur(10px);
|
|
2141
|
+
background: transparent;
|
|
1987
2142
|
}
|
|
1988
2143
|
.topbar h1 {
|
|
1989
2144
|
margin: 0;
|
|
1990
|
-
font-size:
|
|
1991
|
-
|
|
2145
|
+
font-size: 25px;
|
|
2146
|
+
font-weight: 650;
|
|
2147
|
+
line-height: 1.3;
|
|
1992
2148
|
letter-spacing: -0.01em;
|
|
1993
2149
|
}
|
|
1994
2150
|
.topbar p {
|
|
1995
2151
|
margin: 5px 0 0;
|
|
1996
2152
|
color: var(--muted);
|
|
1997
|
-
font-family: var(--
|
|
2153
|
+
font-family: var(--sans);
|
|
1998
2154
|
font-size: 12px;
|
|
1999
2155
|
overflow-wrap: anywhere;
|
|
2000
2156
|
}
|
|
@@ -2008,14 +2164,14 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2008
2164
|
.brand-mark { display: inline-flex; color: var(--accent); }
|
|
2009
2165
|
.brand-text { display: grid; gap: 2px; }
|
|
2010
2166
|
.brand-name {
|
|
2011
|
-
font-family: var(--
|
|
2167
|
+
font-family: var(--sans);
|
|
2012
2168
|
font-size: 19px;
|
|
2013
2169
|
font-weight: 600;
|
|
2014
2170
|
letter-spacing: -0.01em;
|
|
2015
2171
|
}
|
|
2016
2172
|
.brand-tag {
|
|
2017
2173
|
color: var(--muted);
|
|
2018
|
-
font-family: var(--
|
|
2174
|
+
font-family: var(--sans);
|
|
2019
2175
|
font-size: 12px;
|
|
2020
2176
|
overflow-wrap: anywhere;
|
|
2021
2177
|
}
|
|
@@ -2024,7 +2180,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2024
2180
|
align-items: center;
|
|
2025
2181
|
gap: 3px;
|
|
2026
2182
|
flex-wrap: wrap;
|
|
2027
|
-
font-family: var(--
|
|
2183
|
+
font-family: var(--sans);
|
|
2028
2184
|
font-size: 13px;
|
|
2029
2185
|
}
|
|
2030
2186
|
.topbar nav > a {
|
|
@@ -2037,38 +2193,24 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2037
2193
|
color: var(--text);
|
|
2038
2194
|
text-decoration: none;
|
|
2039
2195
|
}
|
|
2040
|
-
.language-switcher {
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
padding-left: 9px;
|
|
2045
|
-
border-left: 1px solid var(--line);
|
|
2046
|
-
}
|
|
2047
|
-
.language-switcher a,
|
|
2048
|
-
.language-switcher .active {
|
|
2049
|
-
padding: 5px 8px;
|
|
2050
|
-
border-radius: 7px;
|
|
2051
|
-
}
|
|
2052
|
-
.language-switcher .active {
|
|
2053
|
-
color: var(--text);
|
|
2054
|
-
font-weight: 700;
|
|
2055
|
-
text-decoration: none;
|
|
2056
|
-
cursor: default;
|
|
2057
|
-
}
|
|
2196
|
+
.language-switcher { display: grid; gap: 8px; min-width: 0; }
|
|
2197
|
+
.language-switcher label { display: grid; gap: 6px; min-width: 0; }
|
|
2198
|
+
.language-switcher select { width: 100%; min-width: 0; max-width: 100%; }
|
|
2199
|
+
.language-switcher button { justify-self: start; }
|
|
2058
2200
|
.dashboard,
|
|
2059
2201
|
.artifact-view,
|
|
2060
2202
|
.artifact-editor {
|
|
2061
|
-
width: min(
|
|
2203
|
+
width: min(1440px, calc(100% - 64px));
|
|
2062
2204
|
margin: 28px auto 72px;
|
|
2063
2205
|
}
|
|
2064
2206
|
.artifact-view-wide {
|
|
2065
|
-
width: min(1760px, calc(
|
|
2207
|
+
width: min(1760px, calc(100% - 64px));
|
|
2066
2208
|
}
|
|
2067
2209
|
.toolbar {
|
|
2068
|
-
font-family: var(--
|
|
2210
|
+
font-family: var(--sans);
|
|
2069
2211
|
font-size: 12px;
|
|
2070
|
-
letter-spacing: 0
|
|
2071
|
-
text-transform:
|
|
2212
|
+
letter-spacing: 0;
|
|
2213
|
+
text-transform: none;
|
|
2072
2214
|
color: var(--faint);
|
|
2073
2215
|
}
|
|
2074
2216
|
.toolbar,
|
|
@@ -2096,34 +2238,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2096
2238
|
.filter-form select {
|
|
2097
2239
|
min-width: 120px;
|
|
2098
2240
|
}
|
|
2099
|
-
.dashboard-layout {
|
|
2100
|
-
|
|
2101
|
-
align-items: flex-start;
|
|
2102
|
-
gap: 24px;
|
|
2103
|
-
width: min(1180px, calc(100vw - 32px));
|
|
2104
|
-
margin: 28px auto 72px;
|
|
2105
|
-
}
|
|
2106
|
-
.dashboard-layout .dashboard {
|
|
2107
|
-
flex: 1;
|
|
2108
|
-
min-width: 0;
|
|
2109
|
-
width: auto;
|
|
2110
|
-
margin: 0;
|
|
2111
|
-
}
|
|
2112
|
-
.dashboard-sidebar {
|
|
2113
|
-
width: 240px;
|
|
2114
|
-
flex: none;
|
|
2115
|
-
border: 1px solid var(--line);
|
|
2116
|
-
border-radius: 12px;
|
|
2117
|
-
background: var(--panel);
|
|
2118
|
-
padding: 14px;
|
|
2119
|
-
}
|
|
2120
|
-
.dashboard-sidebar h2 {
|
|
2121
|
-
margin: 0 0 10px;
|
|
2122
|
-
font-size: 13px;
|
|
2123
|
-
text-transform: uppercase;
|
|
2124
|
-
letter-spacing: 0.06em;
|
|
2125
|
-
color: var(--faint);
|
|
2126
|
-
}
|
|
2241
|
+
.dashboard-layout { width: min(1440px, calc(100% - 64px)); margin: 28px auto 48px; }
|
|
2242
|
+
.dashboard-layout .dashboard { min-width: 0; width: 100%; margin: 0; }
|
|
2127
2243
|
.saved-view-list {
|
|
2128
2244
|
list-style: none;
|
|
2129
2245
|
margin: 0 0 14px;
|
|
@@ -2160,8 +2276,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2160
2276
|
margin: 0;
|
|
2161
2277
|
padding: 10px 14px;
|
|
2162
2278
|
font-size: 12px;
|
|
2163
|
-
text-transform:
|
|
2164
|
-
letter-spacing: 0
|
|
2279
|
+
text-transform: none;
|
|
2280
|
+
letter-spacing: 0;
|
|
2165
2281
|
color: var(--faint);
|
|
2166
2282
|
background: var(--panel-2);
|
|
2167
2283
|
border-bottom: 1px solid var(--line);
|
|
@@ -2226,17 +2342,24 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2226
2342
|
}
|
|
2227
2343
|
.artifact-list {
|
|
2228
2344
|
border: 1px solid var(--line);
|
|
2229
|
-
border-radius:
|
|
2345
|
+
border-radius: 8px;
|
|
2230
2346
|
overflow: hidden;
|
|
2231
2347
|
background: var(--panel);
|
|
2232
|
-
box-shadow:
|
|
2233
|
-
}
|
|
2348
|
+
box-shadow: none;
|
|
2349
|
+
}
|
|
2350
|
+
.dashboard-pagination { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; margin-top: 16px; }
|
|
2351
|
+
.page-size-form, .page-size-form label { display: flex; align-items: center; gap: 8px; }
|
|
2352
|
+
.page-size-form { margin: 0; }
|
|
2353
|
+
.page-size-form select { min-width: 76px; width: auto; min-height: 44px; }
|
|
2354
|
+
.page-size-form button { min-height: 44px; }
|
|
2355
|
+
.pager-position { align-self: center; white-space: nowrap; }
|
|
2234
2356
|
.pager {
|
|
2235
2357
|
display: flex;
|
|
2236
|
-
|
|
2358
|
+
flex-wrap: wrap;
|
|
2359
|
+
justify-content: flex-start;
|
|
2237
2360
|
gap: 8px;
|
|
2238
|
-
margin-top:
|
|
2239
|
-
font-family: var(--
|
|
2361
|
+
margin-top: 0;
|
|
2362
|
+
font-family: var(--sans);
|
|
2240
2363
|
font-size: 12.5px;
|
|
2241
2364
|
}
|
|
2242
2365
|
.pager a,
|
|
@@ -2342,8 +2465,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2342
2465
|
color: var(--faint);
|
|
2343
2466
|
font-family: var(--mono);
|
|
2344
2467
|
font-size: 11.5px;
|
|
2345
|
-
letter-spacing: 0
|
|
2346
|
-
text-transform:
|
|
2468
|
+
letter-spacing: 0;
|
|
2469
|
+
text-transform: none;
|
|
2347
2470
|
}
|
|
2348
2471
|
.data-table form {
|
|
2349
2472
|
margin: 0;
|
|
@@ -2361,11 +2484,12 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2361
2484
|
font-size: 13px;
|
|
2362
2485
|
}
|
|
2363
2486
|
.field > span {
|
|
2364
|
-
font-family: var(--
|
|
2365
|
-
font-size:
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2487
|
+
font-family: var(--sans);
|
|
2488
|
+
font-size: 12px;
|
|
2489
|
+
font-weight: 500;
|
|
2490
|
+
letter-spacing: 0;
|
|
2491
|
+
text-transform: none;
|
|
2492
|
+
color: var(--muted);
|
|
2369
2493
|
}
|
|
2370
2494
|
.check-field {
|
|
2371
2495
|
display: flex;
|
|
@@ -2380,8 +2504,6 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2380
2504
|
min-height: 0;
|
|
2381
2505
|
padding: 0;
|
|
2382
2506
|
}
|
|
2383
|
-
input,
|
|
2384
|
-
select,
|
|
2385
2507
|
textarea {
|
|
2386
2508
|
width: 100%;
|
|
2387
2509
|
border: 1px solid var(--line-2);
|
|
@@ -2393,13 +2515,13 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2393
2515
|
}
|
|
2394
2516
|
input::placeholder,
|
|
2395
2517
|
textarea::placeholder { color: var(--faint); }
|
|
2396
|
-
input,
|
|
2518
|
+
input:not([type="checkbox"]):not([type="radio"]),
|
|
2397
2519
|
select {
|
|
2398
2520
|
min-height: 38px;
|
|
2399
2521
|
padding: 7px 10px;
|
|
2400
2522
|
}
|
|
2401
2523
|
textarea {
|
|
2402
|
-
min-height:
|
|
2524
|
+
min-height: 120px;
|
|
2403
2525
|
padding: 14px;
|
|
2404
2526
|
resize: vertical;
|
|
2405
2527
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
@@ -2408,6 +2530,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2408
2530
|
white-space: pre;
|
|
2409
2531
|
overflow: auto;
|
|
2410
2532
|
}
|
|
2533
|
+
textarea[data-artifacty-editor] { min-height: 52vh; }
|
|
2534
|
+
.comments textarea { min-height: 96px; white-space: pre-wrap; }
|
|
2411
2535
|
.compact-textarea {
|
|
2412
2536
|
min-height: 160px;
|
|
2413
2537
|
}
|
|
@@ -2465,60 +2589,13 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2465
2589
|
border: 0;
|
|
2466
2590
|
background: var(--panel);
|
|
2467
2591
|
}
|
|
2468
|
-
|
|
2469
|
-
select:focus,
|
|
2470
|
-
textarea:focus {
|
|
2471
|
-
outline: 3px solid var(--ring);
|
|
2472
|
-
outline-offset: 0;
|
|
2473
|
-
border-color: var(--accent);
|
|
2474
|
-
}
|
|
2592
|
+
button:not(.btn) { font: inherit; color: var(--text); background: var(--panel); border: 1px solid var(--line-2); border-radius: 6px; padding: 6px 10px; cursor: pointer; }
|
|
2475
2593
|
.editor-actions {
|
|
2476
2594
|
display: flex;
|
|
2477
2595
|
align-items: center;
|
|
2478
2596
|
justify-content: flex-end;
|
|
2479
2597
|
gap: 12px;
|
|
2480
2598
|
}
|
|
2481
|
-
button {
|
|
2482
|
-
min-height: 38px;
|
|
2483
|
-
border: 1px solid var(--accent);
|
|
2484
|
-
border-radius: 8px;
|
|
2485
|
-
padding: 7px 15px;
|
|
2486
|
-
background: var(--accent);
|
|
2487
|
-
color: var(--accent-ink);
|
|
2488
|
-
font: inherit;
|
|
2489
|
-
font-weight: 600;
|
|
2490
|
-
cursor: pointer;
|
|
2491
|
-
transition: background 0.15s ease, border-color 0.15s ease;
|
|
2492
|
-
}
|
|
2493
|
-
button:hover { background: var(--accent-2); border-color: var(--accent-2); }
|
|
2494
|
-
.button-link {
|
|
2495
|
-
display: inline-flex;
|
|
2496
|
-
min-height: 38px;
|
|
2497
|
-
align-items: center;
|
|
2498
|
-
justify-content: center;
|
|
2499
|
-
padding: 7px 15px;
|
|
2500
|
-
border: 1px solid var(--accent);
|
|
2501
|
-
border-radius: 8px;
|
|
2502
|
-
background: var(--accent);
|
|
2503
|
-
color: var(--accent-ink);
|
|
2504
|
-
font-weight: 600;
|
|
2505
|
-
}
|
|
2506
|
-
.button-link:hover {
|
|
2507
|
-
border-color: var(--accent-2);
|
|
2508
|
-
background: var(--accent-2);
|
|
2509
|
-
color: var(--accent-ink);
|
|
2510
|
-
text-decoration: none;
|
|
2511
|
-
}
|
|
2512
|
-
.inline-action button {
|
|
2513
|
-
min-height: 32px;
|
|
2514
|
-
padding: 5px 12px;
|
|
2515
|
-
font-family: var(--mono);
|
|
2516
|
-
font-size: 12px;
|
|
2517
|
-
font-weight: 500;
|
|
2518
|
-
border-color: var(--line-2);
|
|
2519
|
-
background: var(--panel);
|
|
2520
|
-
color: var(--muted);
|
|
2521
|
-
}
|
|
2522
2599
|
.inline-action button:hover {
|
|
2523
2600
|
background: var(--panel-2);
|
|
2524
2601
|
border-color: var(--accent);
|
|
@@ -2570,7 +2647,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2570
2647
|
overflow-wrap: anywhere;
|
|
2571
2648
|
}
|
|
2572
2649
|
.artifact-row > span:not(.row-main):not(.tags):not(.badge) {
|
|
2573
|
-
font-family: var(--
|
|
2650
|
+
font-family: var(--sans);
|
|
2574
2651
|
font-size: 12.5px;
|
|
2575
2652
|
color: var(--muted);
|
|
2576
2653
|
}
|
|
@@ -2596,7 +2673,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2596
2673
|
border-radius: 999px;
|
|
2597
2674
|
background: var(--panel-2);
|
|
2598
2675
|
color: var(--muted);
|
|
2599
|
-
font-family: var(--
|
|
2676
|
+
font-family: var(--sans);
|
|
2600
2677
|
font-size: 11.5px;
|
|
2601
2678
|
}
|
|
2602
2679
|
.badge {
|
|
@@ -2604,10 +2681,10 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2604
2681
|
align-items: center;
|
|
2605
2682
|
padding: 3px 9px;
|
|
2606
2683
|
border-radius: 999px;
|
|
2607
|
-
border: 1px solid
|
|
2608
|
-
background:
|
|
2609
|
-
color:
|
|
2610
|
-
font-family: var(--
|
|
2684
|
+
border: 1px solid var(--line);
|
|
2685
|
+
background: var(--panel-2);
|
|
2686
|
+
color: var(--muted);
|
|
2687
|
+
font-family: var(--sans);
|
|
2611
2688
|
font-size: 11.5px;
|
|
2612
2689
|
line-height: 1.45;
|
|
2613
2690
|
white-space: nowrap;
|
|
@@ -2694,9 +2771,9 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2694
2771
|
width: 100%;
|
|
2695
2772
|
min-height: 320px;
|
|
2696
2773
|
border: 1px solid var(--line);
|
|
2697
|
-
border-radius:
|
|
2774
|
+
border-radius: 8px;
|
|
2698
2775
|
background: var(--panel);
|
|
2699
|
-
box-shadow:
|
|
2776
|
+
box-shadow: none;
|
|
2700
2777
|
}
|
|
2701
2778
|
.split-preview {
|
|
2702
2779
|
display: grid;
|
|
@@ -2790,12 +2867,12 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2790
2867
|
.artifact-code {
|
|
2791
2868
|
margin: 0;
|
|
2792
2869
|
border: 1px solid var(--line);
|
|
2793
|
-
border-radius:
|
|
2870
|
+
border-radius: 8px;
|
|
2794
2871
|
background: var(--panel);
|
|
2795
2872
|
}
|
|
2796
2873
|
.artifact-doc {
|
|
2797
2874
|
padding: 32px;
|
|
2798
|
-
box-shadow:
|
|
2875
|
+
box-shadow: none;
|
|
2799
2876
|
}
|
|
2800
2877
|
.artifact-doc h1,
|
|
2801
2878
|
.artifact-doc h2,
|
|
@@ -2831,10 +2908,16 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2831
2908
|
border-radius: 10px;
|
|
2832
2909
|
overflow: hidden;
|
|
2833
2910
|
}
|
|
2834
|
-
.artifact-doc-codemirror .cm-editor
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
}
|
|
2911
|
+
.artifact-doc-codemirror .cm-editor,
|
|
2912
|
+
.artifact-code-viewer .cm-editor { min-height: 0; background: #f8f9fb; color: #20242b; }
|
|
2913
|
+
.artifact-doc-codemirror .cm-editor .cm-content,
|
|
2914
|
+
.artifact-code-viewer .cm-editor .cm-content { min-height: 0; }
|
|
2915
|
+
.artifact-doc-codemirror .cm-editor .cm-gutters,
|
|
2916
|
+
.artifact-code-viewer .cm-editor .cm-gutters { background: #eef0f3; color: #626c79; border-color: #dfe3e8; }
|
|
2917
|
+
.artifact-doc-codemirror .cm-editor .cm-activeLine,
|
|
2918
|
+
.artifact-doc-codemirror .cm-editor .cm-activeLineGutter,
|
|
2919
|
+
.artifact-code-viewer .cm-editor .cm-activeLine,
|
|
2920
|
+
.artifact-code-viewer .cm-editor .cm-activeLineGutter { background: transparent; }
|
|
2838
2921
|
.artifact-doc-codemirror .cm-scroller {
|
|
2839
2922
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
2840
2923
|
font-size: 13px;
|
|
@@ -2887,7 +2970,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2887
2970
|
.artifact-notebook-cell-code {
|
|
2888
2971
|
padding: 14px;
|
|
2889
2972
|
border: 1px solid var(--line);
|
|
2890
|
-
border-radius:
|
|
2973
|
+
border-radius: 8px;
|
|
2891
2974
|
background: var(--panel);
|
|
2892
2975
|
}
|
|
2893
2976
|
.artifact-notebook-execcount {
|
|
@@ -2976,8 +3059,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
2976
3059
|
color: var(--muted);
|
|
2977
3060
|
font-family: var(--mono);
|
|
2978
3061
|
font-size: 11.5px;
|
|
2979
|
-
text-transform:
|
|
2980
|
-
letter-spacing: 0
|
|
3062
|
+
text-transform: none;
|
|
3063
|
+
letter-spacing: 0;
|
|
2981
3064
|
}
|
|
2982
3065
|
.summary-card strong {
|
|
2983
3066
|
font-size: 20px;
|
|
@@ -3070,7 +3153,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3070
3153
|
}
|
|
3071
3154
|
.artifact-raw-details {
|
|
3072
3155
|
border: 1px solid var(--line);
|
|
3073
|
-
border-radius:
|
|
3156
|
+
border-radius: 8px;
|
|
3074
3157
|
background: var(--panel);
|
|
3075
3158
|
overflow: hidden;
|
|
3076
3159
|
}
|
|
@@ -3092,9 +3175,9 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3092
3175
|
margin: 0;
|
|
3093
3176
|
padding: 14px;
|
|
3094
3177
|
border: 1px solid var(--line);
|
|
3095
|
-
border-radius:
|
|
3178
|
+
border-radius: 8px;
|
|
3096
3179
|
background: var(--panel);
|
|
3097
|
-
box-shadow:
|
|
3180
|
+
box-shadow: none;
|
|
3098
3181
|
}
|
|
3099
3182
|
.artifact-media img,
|
|
3100
3183
|
.artifact-media video {
|
|
@@ -3123,25 +3206,17 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3123
3206
|
}
|
|
3124
3207
|
.artifact-code-viewer {
|
|
3125
3208
|
border: 1px solid var(--line);
|
|
3126
|
-
border-radius:
|
|
3209
|
+
border-radius: 8px;
|
|
3127
3210
|
overflow: hidden;
|
|
3128
3211
|
background: var(--code);
|
|
3129
3212
|
}
|
|
3130
|
-
|
|
3131
|
-
min-height: 50vh;
|
|
3132
|
-
background: var(--code);
|
|
3133
|
-
color: var(--code-text);
|
|
3134
|
-
}
|
|
3213
|
+
|
|
3135
3214
|
.artifact-code-viewer .cm-scroller {
|
|
3136
3215
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
3137
3216
|
font-size: 13px;
|
|
3138
3217
|
line-height: 1.55;
|
|
3139
3218
|
}
|
|
3140
|
-
|
|
3141
|
-
background: color-mix(in srgb, var(--code) 84%, white);
|
|
3142
|
-
color: var(--muted);
|
|
3143
|
-
border-color: rgba(255, 255, 255, 0.08);
|
|
3144
|
-
}
|
|
3219
|
+
|
|
3145
3220
|
.artifact-react-disabled {
|
|
3146
3221
|
display: grid;
|
|
3147
3222
|
gap: 12px;
|
|
@@ -3160,11 +3235,9 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3160
3235
|
font-size: 13px;
|
|
3161
3236
|
}
|
|
3162
3237
|
@media (max-width: 760px) {
|
|
3163
|
-
.topbar {
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
padding: 20px;
|
|
3167
|
-
}
|
|
3238
|
+
.topbar { align-items: flex-start; flex-direction: column; gap: 12px; }
|
|
3239
|
+
.topbar, .dashboard-layout, .artifact-view, .artifact-editor, .artifact-view-wide { width: calc(100% - 32px); margin-top: 20px; }
|
|
3240
|
+
input:not([type="checkbox"]):not([type="radio"]), select, button { min-height: 44px; }
|
|
3168
3241
|
.artifact-row {
|
|
3169
3242
|
grid-template-columns: 1fr;
|
|
3170
3243
|
gap: 6px;
|
|
@@ -3186,7 +3259,163 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3186
3259
|
padding: 18px;
|
|
3187
3260
|
}
|
|
3188
3261
|
}
|
|
3262
|
+
.artifact-editor { max-width: 1000px; }
|
|
3263
|
+
.auth-panel { max-width: 520px; }
|
|
3264
|
+
.meta-card { padding: 24px; }
|
|
3265
|
+
.meta-card h2 { font-size: 16px; font-weight: 650; }
|
|
3266
|
+
.meta-card table { display: block; max-width: 100%; overflow-x: auto; }
|
|
3267
|
+
.artifact-doc { font-size: 15px; line-height: 1.7; }
|
|
3268
|
+
.workspace-header { background: var(--panel); border-bottom: 1px solid var(--line); padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
3269
|
+
.workspace-brand-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
3270
|
+
.workspace-brand { display: flex; align-items: center; gap: 10px; color: var(--text); font-size: 19px; font-weight: 700; letter-spacing: -.04em; }
|
|
3271
|
+
.workspace-brand svg { width: 27px; height: 27px; color: var(--accent); }
|
|
3272
|
+
.workspace-desktop { display: none; }
|
|
3273
|
+
.workspace-mobile > summary { cursor: pointer; padding: 8px 12px; border: 1px solid var(--line-2); border-radius: 6px; }
|
|
3274
|
+
.workspace-mobile[open] { flex-basis: 100%; }
|
|
3275
|
+
.workspace-header:has(.workspace-mobile[open]) { flex-wrap: wrap; }
|
|
3276
|
+
.workspace-mobile nav { padding-top: 16px; }
|
|
3277
|
+
.workspace-primary, .workspace-actions { display: grid; gap: 4px; }
|
|
3278
|
+
.workspace-primary { padding-bottom: 16px; border-bottom: 1px solid var(--line); margin-bottom: 16px; }
|
|
3279
|
+
.workspace-header nav a, .workspace-dropdown summary, .workspace-dropdown .nav-form button { display: flex; align-items: center; gap: 10px; min-height: 38px; padding: 8px 10px; color: var(--muted); border-radius: 6px; font-size: 14px; text-align: left; }
|
|
3280
|
+
.workspace-header nav a:hover, .workspace-dropdown summary:hover { background: var(--panel-2); color: var(--text); text-decoration: none; }
|
|
3281
|
+
.workspace-header [aria-current="page"], .workspace-header .is-current { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
3282
|
+
.workspace-dropdown summary { cursor: pointer; list-style: none; }
|
|
3283
|
+
.workspace-dropdown summary::-webkit-details-marker { display: none; }
|
|
3284
|
+
.workspace-chevron { margin-left: auto; display: inline-flex; }
|
|
3285
|
+
.workspace-dropdown[open] > summary .workspace-chevron { transform: rotate(180deg); }
|
|
3286
|
+
.workspace-version { color: var(--faint); font-size: 12px; padding: 10px; }
|
|
3287
|
+
.settings-page { max-width: 1100px; }
|
|
3288
|
+
.settings-page .meta-card { gap: 16px; }
|
|
3289
|
+
.settings-page .meta-card p { margin: 0; }
|
|
3290
|
+
.settings-page form > button { justify-self: start; width: fit-content; }
|
|
3291
|
+
.settings-page form:not(.editor-form) { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
3292
|
+
.settings-page .field { min-width: 0; }
|
|
3293
|
+
.settings-page textarea[name="allowedCidrs"] { min-height: 120px; height: 140px; }
|
|
3294
|
+
.settings-page .editor-form { gap: 12px; }
|
|
3295
|
+
.settings-page .user-role-form { display: flex; flex-wrap: nowrap; gap: 8px; align-items: center; }
|
|
3296
|
+
.user-account-actions { display: grid; gap: 8px; justify-items: start; }
|
|
3297
|
+
|
|
3298
|
+
.user-role-form select { width: auto; min-width: 105px; }
|
|
3299
|
+
.user-role-form select, .user-role-form button { margin: 0; }
|
|
3300
|
+
|
|
3301
|
+
.meta-card:has(> form[action="/admin/retention/run"]) { min-width: 0; overflow-wrap: anywhere; }
|
|
3302
|
+
form[action="/admin/retention/run"], form[action="/admin/retention/run"] > * { min-width: 0; }
|
|
3303
|
+
form[action="/admin/retention/run"] .checkbox-field { display: block; white-space: normal; overflow-wrap: anywhere; line-height: 1.6; }
|
|
3304
|
+
form[action="/admin/retention/run"] .checkbox-field input { margin-right: 6px; vertical-align: middle; }
|
|
3305
|
+
.retention-policy-form .retention-section { display: grid; gap: 16px; min-width: 0; margin: 0; padding: 16px; border: 1px solid var(--line); border-radius: 6px; }
|
|
3306
|
+
.retention-policy-form legend { padding: 0 6px; font-weight: 600; }
|
|
3307
|
+
.smtp-field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; align-items: start; }
|
|
3308
|
+
.smtp-settings-form input, .smtp-settings-form select { min-width: 0; }
|
|
3309
|
+
.smtp-settings-form .field { align-content: start; }
|
|
3310
|
+
@media (max-width: 640px) { .smtp-field-grid { grid-template-columns: minmax(0, 1fr); } }
|
|
3311
|
+
.retention-field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 16px; }
|
|
3312
|
+
.retention-policy-form .field { display: grid; gap: 8px; align-content: start; }
|
|
3313
|
+
.retention-policy-form input, .retention-policy-form textarea { width: 100%; min-width: 0; box-sizing: border-box; }
|
|
3314
|
+
.retention-policy-form textarea { min-height: 112px; height: 112px; }
|
|
3315
|
+
@media (max-width: 640px) { .retention-field-grid { grid-template-columns: minmax(0, 1fr); } }
|
|
3316
|
+
|
|
3317
|
+
.workspace-dropdown-content { display: grid; padding-left: 10px; border-left: 1px solid var(--line); margin: 4px 0 8px 10px; }
|
|
3318
|
+
.workspace-dropdown-content .nav-form { display: block; }
|
|
3319
|
+
.workspace-identity { overflow-wrap: anywhere; font-size: 12px; color: var(--faint); padding: 8px 10px; }
|
|
3320
|
+
.theme-controls { border: 0; min-width: 0; margin: 16px 10px 0; padding: 0; }
|
|
3321
|
+
.theme-controls legend { color: var(--muted); font-size: 12px; margin-bottom: 6px; }
|
|
3322
|
+
.theme-choices { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); padding: 3px; gap: 2px; background: var(--panel-2); border-radius: 8px; }
|
|
3323
|
+
.theme-choices .btn { padding: 6px; min-width: 0; border: 0; display: inline-flex; justify-content: center; align-items: center; background: transparent; color: var(--muted); }
|
|
3324
|
+
.theme-choices .btn[aria-pressed="true"] { background: var(--panel); color: var(--accent); box-shadow: 0 1px 3px #0002; }
|
|
3325
|
+
.workspace-header svg { flex-shrink: 0; }
|
|
3326
|
+
.workspace-header .sidebar-toggle { display: none; }
|
|
3327
|
+
.sidebar-toggle > span:not([hidden]) { display: flex; align-items: center; }
|
|
3328
|
+
.workspace-brand-row svg { display: block; }
|
|
3329
|
+
@media (min-width: 1100px) {
|
|
3330
|
+
.sidebar-toggle:not([hidden]) { display: inline-flex; position: static; width: 36px; height: 36px; padding: 0; flex-shrink: 0; }
|
|
3331
|
+
.workspace-desktop { padding-top: 0; }
|
|
3332
|
+
:root[data-sidebar="collapsed"] body:has(> .workspace-header) { padding-left: 68px; }
|
|
3333
|
+
:root[data-sidebar="collapsed"] .workspace-header { width: 68px; padding: 24px 10px; }
|
|
3334
|
+
:root[data-sidebar="collapsed"] .workspace-brand { display: none; }
|
|
3335
|
+
:root[data-sidebar="collapsed"] .workspace-brand-row { justify-content: center; }
|
|
3336
|
+
|
|
3337
|
+
:root[data-sidebar="collapsed"] .workspace-nav-label, :root[data-sidebar="collapsed"] .workspace-brand-name, :root[data-sidebar="collapsed"] .workspace-chevron, :root[data-sidebar="collapsed"] .workspace-version, :root[data-sidebar="collapsed"] .workspace-identity, :root[data-sidebar="collapsed"] .language-switcher, :root[data-sidebar="collapsed"] .theme-controls { display: none; }
|
|
3338
|
+
:root[data-sidebar="collapsed"] .workspace-dropdown-content { padding-left: 0; border: 0; margin-left: 0; }
|
|
3339
|
+
:root[data-sidebar="collapsed"] .workspace-header nav a, :root[data-sidebar="collapsed"] .workspace-dropdown summary, :root[data-sidebar="collapsed"] .nav-form button { justify-content: center; padding: 10px; }
|
|
3340
|
+
:root[data-sidebar="collapsed"] .workspace-header [aria-current="page"] { box-shadow: inset 3px 0 var(--accent); }
|
|
3341
|
+
:root[data-sidebar="collapsed"] .workspace-header nav a:is(:hover, :focus-visible) .workspace-nav-label, :root[data-sidebar="collapsed"] .workspace-dropdown > summary:is(:hover, :focus-visible) .workspace-nav-label, :root[data-sidebar="collapsed"] .nav-form button:is(:hover, :focus-visible) .workspace-nav-label { display: block; position: fixed; left: 76px; z-index: 50; padding: 6px 10px; max-width: 240px; background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 6px; box-shadow: 0 3px 12px #0002; pointer-events: none; }
|
|
3342
|
+
}
|
|
3343
|
+
.workspace-header .language-switcher { margin: 12px 0 0; padding: 0 10px; font-size: 12px; }
|
|
3344
|
+
|
|
3345
|
+
.workspace-skip { position: absolute; left: 16px; top: -100px; z-index: 100; background: var(--panel); padding: 8px 16px; }
|
|
3346
|
+
.workspace-skip:focus { top: 8px; }
|
|
3347
|
+
:focus-visible { outline: 2px solid var(--ring); outline-offset: 3px; }
|
|
3348
|
+
.dashboard-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
|
|
3349
|
+
.dashboard-heading h1 { font-size: 25px; font-weight: 650; line-height: 1.3; margin: 0; letter-spacing: -.025em; }
|
|
3350
|
+
.dashboard-heading nav { display: flex; align-items: center; gap: 16px; }
|
|
3351
|
+
.dashboard-heading .button-link { padding: 8px 14px; background: var(--accent); color: var(--accent-ink); border-radius: 6px; }
|
|
3352
|
+
.dashboard .filter-form { display: block; width: 100%; margin-bottom: 0; }
|
|
3353
|
+
.dashboard-search-row { display: flex; align-items: center; gap: 12px; }
|
|
3354
|
+
.dashboard-search { flex: 1; min-width: 0; }
|
|
3355
|
+
.dashboard-search input { width: 100%; min-width: 0; background: var(--panel); }
|
|
3356
|
+
.dashboard-clear { white-space: nowrap; padding: 8px 0; font-size: 13px; }
|
|
3357
|
+
.dashboard-sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
3358
|
+
.dashboard-advanced, .dashboard-saved-views { margin: 0; border-bottom: 1px solid var(--line); }
|
|
3359
|
+
.dashboard-advanced > summary, .dashboard-saved-views > summary { padding: 10px 0; cursor: pointer; color: var(--muted); font-size: 13px; }
|
|
3360
|
+
.dashboard-filter-count { color: var(--faint); margin-left: 8px; font-size: 12px; }
|
|
3361
|
+
.dashboard-filter-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; padding: 12px 0 20px; }
|
|
3362
|
+
.dashboard-field { display: grid; gap: 6px; min-width: 0; color: var(--muted); font-size: 12px; }
|
|
3363
|
+
.dashboard-field input, .dashboard-field select { min-width: 0; width: 100%; font-size: 14px; }
|
|
3364
|
+
.dashboard-filter-actions { grid-column: 1 / -1; }
|
|
3365
|
+
.dashboard-active-filters { display: flex; gap: 4px 16px; flex-wrap: wrap; font-size: 12px; color: var(--muted); margin: 10px 0; overflow-wrap: anywhere; }
|
|
3366
|
+
.dashboard-saved-content { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; padding: 8px 0 20px; }
|
|
3367
|
+
.dashboard-saved-content .empty { padding: 0; }
|
|
3368
|
+
.dashboard .saved-view-form { align-content: start; }
|
|
3369
|
+
.dashboard .saved-view-list { margin: 0; }
|
|
3370
|
+
.dashboard .saved-view-item { min-width: 0; }
|
|
3371
|
+
.artifact-overview { align-items: flex-start; }
|
|
3372
|
+
.artifact-details { min-width: 0; }
|
|
3373
|
+
.artifact-details > summary { cursor: pointer; color: var(--muted); font-size: 13px; }
|
|
3374
|
+
.artifact-details > .meta-strip { padding: 12px 0 0; max-width: 100%; overflow-wrap: anywhere; }
|
|
3375
|
+
.dashboard-results { margin: 18px 0 10px; display: flex; justify-content: space-between; }
|
|
3376
|
+
.dashboard-results > span + span { color: var(--faint); font-size: 12px; }
|
|
3377
|
+
.dashboard .row-main strong { white-space: normal; overflow-wrap: anywhere; font-size: 15px; line-height: 1.45; font-weight: 600; }
|
|
3378
|
+
.dashboard .row-agent, .dashboard .row-formats { display: grid; gap: 2px; min-width: 0; font-size: 12px; color: var(--muted); }
|
|
3379
|
+
.dashboard .row-formats { display: flex; flex-wrap: wrap; gap: 4px 8px; }
|
|
3380
|
+
.dashboard .row-formats .badge { border: 0; padding: 0; background: transparent; color: var(--muted); font: inherit; border-radius: 0; }
|
|
3381
|
+
.dashboard .artifact-row .tag { border: 0; background: transparent; padding: 0; border-radius: 0; font-family: var(--sans); font-size: 12px; }
|
|
3382
|
+
.dashboard .artifact-row .tag + .tag::before { content: "·"; margin-right: 6px; color: var(--faint); }
|
|
3383
|
+
.dashboard .tags { min-width: 0; overflow-wrap: anywhere; }
|
|
3384
|
+
.dashboard .row-agent .publisher-cell { font-size: 12px; color: var(--faint); }
|
|
3385
|
+
.dashboard .artifact-row { grid-template-columns: minmax(180px, 1fr) minmax(100px, 150px) minmax(100px, 145px) minmax(80px, 150px); min-height: 76px; padding: 14px 16px; gap: 16px; }
|
|
3386
|
+
@media (min-width: 1100px) {
|
|
3387
|
+
body:has(> .workspace-header) { padding-left: 196px; }
|
|
3388
|
+
.workspace-header { position: fixed; inset: 0 auto 0 0; width: 196px; padding: 24px 12px; border-bottom: 0; border-right: 1px solid var(--line); display: block; overflow-y: auto; z-index: 20; }
|
|
3389
|
+
.workspace-brand-row { min-height: 36px; margin-bottom: 18px; }
|
|
3390
|
+
.workspace-brand { padding: 0 0 0 10px; }
|
|
3391
|
+
.workspace-desktop { display: block; }
|
|
3392
|
+
.workspace-mobile { display: none; }
|
|
3393
|
+
}
|
|
3394
|
+
@media (max-width: 1099px) {
|
|
3395
|
+
.dashboard-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
3396
|
+
}
|
|
3397
|
+
@media (max-width: 899px) {
|
|
3398
|
+
.dashboard .artifact-row { grid-template-columns: auto minmax(0, 1fr); gap: 8px 12px; }
|
|
3399
|
+
.dashboard .row-main, .dashboard .tags { grid-column: 1 / -1; }
|
|
3400
|
+
.dashboard .row-agent { max-width: 42vw; }
|
|
3401
|
+
.dashboard .row-agent, .dashboard .row-formats { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
3402
|
+
.dashboard .row-agent .publisher-cell { max-width: 100%; }
|
|
3403
|
+
.dashboard .tags { gap: 6px; }
|
|
3404
|
+
}
|
|
3405
|
+
@media (max-width: 640px) {
|
|
3406
|
+
.workspace-header { padding: 12px 16px; }
|
|
3407
|
+
.workspace-header nav a, .workspace-dropdown summary, .workspace-dropdown .nav-form button { min-height: 44px; }
|
|
3408
|
+
.dashboard-filter-grid, .dashboard-saved-content { grid-template-columns: 1fr; }
|
|
3409
|
+
.dashboard-heading h1 { font-size: 22px; }
|
|
3410
|
+
.dashboard-heading nav { gap: 12px; font-size: 13px; }
|
|
3411
|
+
.dashboard-search-row { gap: 8px; }
|
|
3412
|
+
.dashboard-search-row button { padding: 8px 12px; }
|
|
3413
|
+
.dashboard-advanced > summary, .dashboard-saved-views > summary { min-height: 44px; }
|
|
3414
|
+
.dashboard .artifact-row { padding: 14px; }
|
|
3415
|
+
.dashboard .row-main strong { font-size: 14px; }
|
|
3416
|
+
}
|
|
3189
3417
|
</style>
|
|
3418
|
+
<link rel="stylesheet" href="/assets/ui-controls.css">
|
|
3190
3419
|
</head>
|
|
3191
3420
|
<body>
|
|
3192
3421
|
${body}
|
|
@@ -3196,7 +3425,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
3196
3425
|
}
|
|
3197
3426
|
|
|
3198
3427
|
function formatSelect(selected) {
|
|
3199
|
-
return `<select name="format">
|
|
3428
|
+
return `<select name="format" class="form-select">
|
|
3200
3429
|
${ARTIFACT_FORMATS.map((format) => {
|
|
3201
3430
|
const label = format[0].toUpperCase() + format.slice(1);
|
|
3202
3431
|
return `<option value="${format}"${format === selected ? " selected" : ""}>${label}</option>`;
|
|
@@ -3205,13 +3434,13 @@ function formatSelect(selected) {
|
|
|
3205
3434
|
}
|
|
3206
3435
|
|
|
3207
3436
|
function artifactTypeSelect(selected) {
|
|
3208
|
-
return `<select name="artifactType">
|
|
3437
|
+
return `<select name="artifactType" class="form-select">
|
|
3209
3438
|
${artifactTypeOptions(selected)}
|
|
3210
3439
|
</select>`;
|
|
3211
3440
|
}
|
|
3212
3441
|
|
|
3213
3442
|
function visibilitySelect(selected, view) {
|
|
3214
|
-
return `<select name="visibility">
|
|
3443
|
+
return `<select name="visibility" class="form-select">
|
|
3215
3444
|
${VISIBILITY_VALUES.map((value) => `<option value="${value}"${value === selected ? " selected" : ""}>${view.text(`artifact.visibility.${value}`)}</option>`).join("")}
|
|
3216
3445
|
</select>`;
|
|
3217
3446
|
}
|
|
@@ -3239,6 +3468,13 @@ function editorScript(locale) {
|
|
|
3239
3468
|
return `<script>window.ARTIFACTY_I18N=${config};</script><script type="module" src="${EDITOR_CLIENT_PATH}"></script>`;
|
|
3240
3469
|
}
|
|
3241
3470
|
|
|
3471
|
+
function renderDateTime(value, view) {
|
|
3472
|
+
const date = new Date(value);
|
|
3473
|
+
if (!Number.isFinite(date.getTime())) return escapeHtml(value);
|
|
3474
|
+
const label = new Intl.DateTimeFormat(view.locale, { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" }).format(date);
|
|
3475
|
+
return `<time datetime="${escapeAttribute(value)}" title="${escapeAttribute(value)}">${escapeHtml(label)} UTC</time>`;
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3242
3478
|
function viewContext(locale, currentPath) {
|
|
3243
3479
|
const i18n = createI18n(locale);
|
|
3244
3480
|
return {
|
|
@@ -3300,13 +3536,11 @@ function statusBadge(label) {
|
|
|
3300
3536
|
return `<span class="badge s-archived">${label}</span>`;
|
|
3301
3537
|
}
|
|
3302
3538
|
|
|
3303
|
-
//
|
|
3304
|
-
// common case doesn't add visual noise to every row.
|
|
3539
|
+
// Highlight explicit public/private visibility while keeping default team rows compact.
|
|
3305
3540
|
function visibilityBadge(artifact, view) {
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
}
|
|
3309
|
-
return `<span class="badge v-private">${view.text("artifact.visibility.private")}</span>`;
|
|
3541
|
+
const visibility = artifact.visibility || "team";
|
|
3542
|
+
if (!["private", "public"].includes(visibility)) return "";
|
|
3543
|
+
return `<span class="badge v-${visibility}">${view.text(`artifact.visibility.${visibility}`)}</span>`;
|
|
3310
3544
|
}
|
|
3311
3545
|
|
|
3312
3546
|
function htmlFrameContent(content) {
|
|
@@ -3400,31 +3634,56 @@ function frameResizeScript() {
|
|
|
3400
3634
|
return `<script>(function(){window.addEventListener("message",function(event){var data=event.data;if(!data||typeof data.__artifactyHeight!=="number"){return;}var frames=document.querySelectorAll(".artifact-frame");for(var i=0;i<frames.length;i+=1){if(frames[i].contentWindow===event.source){var height=Math.min(Math.max(Math.ceil(data.__artifactyHeight),200),200000);frames[i].style.height=height+"px";break;}}});})();</script>`;
|
|
3401
3635
|
}
|
|
3402
3636
|
|
|
3403
|
-
function viewerScript() {
|
|
3404
|
-
|
|
3637
|
+
function viewerScript(locale = DEFAULT_LOCALE) {
|
|
3638
|
+
const view = viewContext(locale, "/");
|
|
3639
|
+
const messages = Object.fromEntries(["filter", "filterColumn", "ruleFilter", "rowsShown", "resultsShown"].map((key) => [key, view.raw(`viewer.${key}`, { column: "{column}", visible: "{visible}", total: "{total}" })]));
|
|
3640
|
+
const config = JSON.stringify(messages).replaceAll("<", "\\u003c");
|
|
3641
|
+
return `<script type="application/json" id="artifacty-viewer-i18n">${config}</script><script type="module" src="${VIEWER_CLIENT_PATH}"></script>`;
|
|
3405
3642
|
}
|
|
3406
3643
|
|
|
3407
3644
|
function reactRendererEnabled() {
|
|
3408
3645
|
return process.env.ARTIFACTY_ENABLE_REACT_RENDERER === "true";
|
|
3409
3646
|
}
|
|
3410
3647
|
|
|
3648
|
+
function themeControls(view) {
|
|
3649
|
+
return `<fieldset class="theme-controls" data-theme-controls hidden><legend>${view.text("theme.label")}</legend>
|
|
3650
|
+
<div class="theme-choices">${["system", "light", "dark"].map((theme) => `<button type="button" data-theme-choice="${theme}" aria-pressed="${theme === "system"}" aria-label="${view.attr(`theme.${theme}`)}" title="${view.attr(`theme.${theme}`)}" class="btn btn-ghost">${uiIcon({ system: "device-desktop", light: "sun", dark: "moon" }[theme])}<span class="dashboard-sr-only">${view.text(`theme.${theme}`)}</span></button>`).join("")}</div>
|
|
3651
|
+
</fieldset>`;
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3411
3654
|
function languageSwitcher(view) {
|
|
3412
|
-
const
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
const
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
return `<
|
|
3655
|
+
const current = new URL(view.currentPath || "/", "http://artifacty.local");
|
|
3656
|
+
const preserved = [...current.searchParams.entries()].filter(([name]) => name !== "lang")
|
|
3657
|
+
.map(([name, value]) => `<input type="hidden" name="${escapeAttribute(name)}" value="${escapeAttribute(value)}">`).join("");
|
|
3658
|
+
const options = availableLanguages().filter((language) => language.enabled).map((language) =>
|
|
3659
|
+
`<option value="${escapeAttribute(language.locale)}"${language.locale === view.locale ? " selected" : ""}>${escapeHtml(language.nativeName)}</option>`
|
|
3660
|
+
).join("");
|
|
3661
|
+
return `<form class="language-switcher" data-auto-apply="all" method="get" action="${escapeAttribute(`${current.pathname}${current.hash}`)}">
|
|
3662
|
+
${preserved}<label><span>${view.text("language.label")}</span><select name="lang" class="form-select">${options}</select></label>
|
|
3663
|
+
<button type="submit" class="btn btn-outline-secondary">${view.text("language.apply")}</button>
|
|
3664
|
+
</form>`;
|
|
3419
3665
|
}
|
|
3420
3666
|
|
|
3421
|
-
function
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
}
|
|
3425
|
-
|
|
3667
|
+
function workspaceHeader(user, view) {
|
|
3668
|
+
const pathname = new URL(view.currentPath, "http://artifacty.local").pathname;
|
|
3669
|
+
const icons = { "/": "files", "/inbox": "inbox", "/new": "plus", "/import": "upload", "/admin/access": "settings", "/admin/users": "users", "/admin/backup": "database-export", "/admin/retention": "archive", "/admin/webhooks": "webhook", "/admin/update": "refresh", "/admin/languages": "language", "/admin/email": "mail", "/account": "user-circle", "/account/password": "key", "/login": "login", "/docs/api": "code" };
|
|
3670
|
+
const link = (href, key, active = pathname === href, className = "") => `<a href="${view.href(href)}" title="${view.attr(key)}" aria-label="${view.attr(key)}"${active ? ' aria-current="page"' : ""}${className ? ` class="${className}"` : ""}>${uiIcon(icons[href] || "files")}<span class="workspace-nav-label">${view.text(key)}</span></a>`;
|
|
3671
|
+
const artifactActive = pathname === "/" || pathname.startsWith("/artifacts/");
|
|
3672
|
+
const adminLinks = user?.role === "admin" ? `<details class="workspace-dropdown"${pathname.startsWith("/admin/") ? " open" : ""}><summary${pathname.startsWith("/admin/") ? ' class="is-current"' : ""} title="${view.attr("nav.administration")}" aria-label="${view.attr("nav.administration")}">${uiIcon("settings")}<span class="workspace-nav-label">${view.text("nav.administration")}</span><span class="workspace-chevron">${uiIcon("chevron-down", { size: 14 })}</span></summary><div class="workspace-dropdown-content">
|
|
3673
|
+
${link("/admin/access", "access.title")}${link("/admin/users", "nav.users")}${link("/admin/backup", "nav.backup")}${link("/admin/retention", "retention.navLabel")}${link("/admin/webhooks", "nav.webhooks")}${link("/admin/update", "update.title")}${link("/admin/languages", "languageAdmin.title")}${link("/admin/email", "smtp.title")}<span class="workspace-version">Artifacty v${escapeHtml(APP_VERSION)}</span>
|
|
3674
|
+
</div></details>` : "";
|
|
3675
|
+
const account = user ? `<details class="workspace-dropdown"${pathname.startsWith("/account") ? " open" : ""}><summary${pathname.startsWith("/account") ? ' class="is-current"' : ""} title="${view.attr("nav.account")}" aria-label="${view.attr("nav.account")}">${uiIcon("user-circle")}<span class="workspace-nav-label">${view.text("nav.account")}</span><span class="workspace-chevron">${uiIcon("chevron-down", { size: 14 })}</span></summary><div class="workspace-dropdown-content">
|
|
3676
|
+
<span class="workspace-identity">${escapeHtml(user.name || user.email || "")}</span>${link("/account", "nav.accountTokens")}${link("/account/password", "nav.password")}
|
|
3677
|
+
<form method="post" action="/logout" class="nav-form">${localeInput(view.locale)}<button type="submit" title="${view.attr("nav.signOut")}" aria-label="${view.attr("nav.signOut")}" class="btn btn-ghost">${uiIcon("logout")}<span class="workspace-nav-label">${view.text("nav.signOut")}</span></button></form>
|
|
3678
|
+
</div></details>` : user === null ? link("/login", "nav.signIn") : "";
|
|
3679
|
+
const links = `<div class="workspace-primary">${link("/", "nav.artifacts", artifactActive)}${user ? link("/inbox", "inbox.title") : ""}</div><div class="workspace-actions">${link("/new", "nav.new", pathname === "/new", "workspace-create")}${link("/import", "nav.import")}${adminLinks}${account}${link("/docs/api", "nav.apiDocs")}${languageSwitcher(view)}${themeControls(view)}</div>`;
|
|
3680
|
+
return `<a class="workspace-skip" href="#main-content">${view.text("nav.skip")}</a><header class="workspace-header"><div class="workspace-brand-row"><a class="workspace-brand" aria-label="Artifacty" href="${view.href("/")}">${brandMark()}<span class="workspace-brand-name">artifacty</span></a><button type="button" class="sidebar-toggle btn btn-ghost" data-sidebar-toggle hidden aria-expanded="true" aria-label="${view.attr("nav.collapseSidebar")}" title="${view.attr("nav.collapseSidebar")}" data-collapse-label="${view.attr("nav.collapseSidebar")}" data-expand-label="${view.attr("nav.expandSidebar")}"><span data-sidebar-collapse-icon>${uiIcon("layout-sidebar-left-collapse")}</span><span data-sidebar-expand-icon hidden>${uiIcon("layout-sidebar-left-expand")}</span></button></div>
|
|
3681
|
+
<nav class="workspace-desktop" aria-label="${view.attr("dashboard.navigation")}">${links}</nav>
|
|
3682
|
+
<details class="workspace-mobile"><summary>${view.text("dashboard.more")}</summary><nav aria-label="${view.attr("dashboard.navigation")}">${links}</nav></details>
|
|
3683
|
+
</header>`;
|
|
3426
3684
|
}
|
|
3427
3685
|
|
|
3686
|
+
|
|
3428
3687
|
export function escapeHtml(value) {
|
|
3429
3688
|
return String(value)
|
|
3430
3689
|
.replaceAll("&", "&")
|
|
@@ -3528,7 +3787,7 @@ function markdownToHtml(markdown, diagramState = createMermaidDiagramState()) {
|
|
|
3528
3787
|
inList = true;
|
|
3529
3788
|
}
|
|
3530
3789
|
const checked = taskItem[1].toLowerCase() === "x";
|
|
3531
|
-
html.push(`<li class="task-list-item"><label><input type="checkbox" disabled${checked ? " checked" : ""}> ${inlineMarkdown(taskItem[2])}</label></li>`);
|
|
3790
|
+
html.push(`<li class="task-list-item"><label><input type="checkbox" disabled${checked ? " checked" : ""} class="form-check-input"> ${inlineMarkdown(taskItem[2])}</label></li>`);
|
|
3532
3791
|
continue;
|
|
3533
3792
|
}
|
|
3534
3793
|
|
|
@@ -3663,19 +3922,20 @@ const CSV_RENDER_COLUMN_LIMIT = 80;
|
|
|
3663
3922
|
const SARIF_RENDER_RESULT_LIMIT = 500;
|
|
3664
3923
|
|
|
3665
3924
|
function renderMediaArtifact(format, content, metadata = {}, options = {}) {
|
|
3925
|
+
const view = options.view || viewContext(options.locale || DEFAULT_LOCALE, "/");
|
|
3666
3926
|
const mimeType = mediaMimeType(metadata, format);
|
|
3667
3927
|
const source = options.rawUrl || mediaDataUrl(content, mimeType);
|
|
3668
3928
|
if (!source) {
|
|
3669
3929
|
return `<section class="artifact-media-note">
|
|
3670
|
-
|
|
3930
|
+
${view.text("media.unavailable")}
|
|
3671
3931
|
</section>
|
|
3672
3932
|
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>`;
|
|
3673
3933
|
}
|
|
3674
3934
|
|
|
3675
3935
|
const label = [
|
|
3676
3936
|
mimeType || format,
|
|
3677
|
-
metadata.encoding ?
|
|
3678
|
-
metadata.originalEncoding ?
|
|
3937
|
+
metadata.encoding ? view.raw("media.encoding", { encoding: metadata.encoding }) : "",
|
|
3938
|
+
metadata.originalEncoding ? view.raw("media.source", { encoding: metadata.originalEncoding }) : ""
|
|
3679
3939
|
].filter(Boolean).join(" · ");
|
|
3680
3940
|
|
|
3681
3941
|
if (format === "video") {
|
|
@@ -3686,7 +3946,7 @@ function renderMediaArtifact(format, content, metadata = {}, options = {}) {
|
|
|
3686
3946
|
}
|
|
3687
3947
|
|
|
3688
3948
|
return `<figure class="artifact-media artifact-image">
|
|
3689
|
-
<img src="${escapeAttribute(source)}" alt="
|
|
3949
|
+
<img src="${escapeAttribute(source)}" alt="${view.text("media.image")}">
|
|
3690
3950
|
<figcaption>${escapeHtml(label || "image artifact")}</figcaption>
|
|
3691
3951
|
</figure>`;
|
|
3692
3952
|
}
|
|
@@ -3711,17 +3971,17 @@ function mediaDataUrl(content, mimeType) {
|
|
|
3711
3971
|
return `data:${mimeType};base64,${base64}`;
|
|
3712
3972
|
}
|
|
3713
3973
|
|
|
3714
|
-
function renderCsv(content) {
|
|
3974
|
+
function renderCsv(content, view) {
|
|
3715
3975
|
const parsed = parseCsv(content);
|
|
3716
3976
|
if (!parsed.ok) {
|
|
3717
3977
|
return `<section class="artifact-csv">
|
|
3718
|
-
<p class="artifact-csv-note"
|
|
3978
|
+
<p class="artifact-csv-note">${view.text("csv.error", { error: parsed.error })}</p>
|
|
3719
3979
|
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>
|
|
3720
3980
|
</section>`;
|
|
3721
3981
|
}
|
|
3722
3982
|
|
|
3723
3983
|
if (parsed.rows.length === 0) {
|
|
3724
|
-
return `<section class="artifact-csv"><p class="artifact-csv-note"
|
|
3984
|
+
return `<section class="artifact-csv"><p class="artifact-csv-note">${view.text("csv.empty")}</p></section>`;
|
|
3725
3985
|
}
|
|
3726
3986
|
|
|
3727
3987
|
const columnCount = Math.max(...parsed.rows.map((row) => row.length));
|
|
@@ -3731,7 +3991,7 @@ function renderCsv(content) {
|
|
|
3731
3991
|
const rowTruncated = parsed.rows.length - 1 > CSV_RENDER_ROW_LIMIT;
|
|
3732
3992
|
const columnTruncated = columnCount > CSV_RENDER_COLUMN_LIMIT;
|
|
3733
3993
|
const headerHtml = Array.from({ length: visibleColumns }, (_, index) =>
|
|
3734
|
-
`<th data-csv-col="${index}">${escapeHtml(header[index] ||
|
|
3994
|
+
`<th data-csv-col="${index}">${escapeHtml(header[index] || view.raw("csv.column", { column: index + 1 }))}</th>`
|
|
3735
3995
|
).join("");
|
|
3736
3996
|
const bodyHtml = bodyRows.map((row) =>
|
|
3737
3997
|
`<tr data-csv-row>${Array.from({ length: visibleColumns }, (_, index) =>
|
|
@@ -3739,10 +3999,10 @@ function renderCsv(content) {
|
|
|
3739
3999
|
).join("")}</tr>`
|
|
3740
4000
|
).join("\n");
|
|
3741
4001
|
const notes = [
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
rowTruncated ?
|
|
3745
|
-
columnTruncated ?
|
|
4002
|
+
view.raw("csv.rows", { count: parsed.rows.length - 1 }),
|
|
4003
|
+
view.raw("csv.columns", { count: columnCount }),
|
|
4004
|
+
rowTruncated ? view.raw("csv.firstRows", { count: CSV_RENDER_ROW_LIMIT }) : "",
|
|
4005
|
+
columnTruncated ? view.raw("csv.firstColumns", { count: CSV_RENDER_COLUMN_LIMIT }) : ""
|
|
3746
4006
|
].filter(Boolean).join(" · ");
|
|
3747
4007
|
|
|
3748
4008
|
return `<section class="artifact-csv" data-artifact-csv>
|
|
@@ -3756,20 +4016,20 @@ function renderCsv(content) {
|
|
|
3756
4016
|
</section>`;
|
|
3757
4017
|
}
|
|
3758
4018
|
|
|
3759
|
-
function renderSarif(content) {
|
|
4019
|
+
function renderSarif(content, view) {
|
|
3760
4020
|
let sarif;
|
|
3761
4021
|
try {
|
|
3762
4022
|
sarif = JSON.parse(content);
|
|
3763
4023
|
} catch {
|
|
3764
4024
|
return `<section class="artifact-sarif">
|
|
3765
|
-
<p class="artifact-sarif-note"
|
|
4025
|
+
<p class="artifact-sarif-note">${view.text("sarif.invalid")}</p>
|
|
3766
4026
|
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>
|
|
3767
4027
|
</section>`;
|
|
3768
4028
|
}
|
|
3769
4029
|
|
|
3770
4030
|
if (!isSarifDocument(sarif)) {
|
|
3771
4031
|
return `<section class="artifact-sarif">
|
|
3772
|
-
<p class="artifact-sarif-note"
|
|
4032
|
+
<p class="artifact-sarif-note">${view.text("sarif.shape")}</p>
|
|
3773
4033
|
<pre class="artifact-code"><code>${escapeHtml(formatJson(content))}</code></pre>
|
|
3774
4034
|
</section>`;
|
|
3775
4035
|
}
|
|
@@ -3785,26 +4045,26 @@ function renderSarif(content) {
|
|
|
3785
4045
|
<td>${escapeHtml(result.tool)}</td>
|
|
3786
4046
|
</tr>`).join("\n");
|
|
3787
4047
|
const note = results.length > SARIF_RENDER_RESULT_LIMIT
|
|
3788
|
-
?
|
|
3789
|
-
:
|
|
4048
|
+
? view.raw("sarif.first", { count: SARIF_RENDER_RESULT_LIMIT, total: results.length })
|
|
4049
|
+
: view.raw("sarif.results", { count: results.length });
|
|
3790
4050
|
|
|
3791
4051
|
return `<section class="artifact-sarif" data-artifact-sarif>
|
|
3792
4052
|
<div class="artifact-summary-grid">
|
|
3793
|
-
${summaryCard("
|
|
3794
|
-
${summaryCard("
|
|
3795
|
-
${summaryCard("
|
|
3796
|
-
${summaryCard("
|
|
3797
|
-
${summaryCard("
|
|
4053
|
+
${summaryCard(view.raw("sarif.runs"), sarif.runs.length)}
|
|
4054
|
+
${summaryCard(view.raw("sarif.resultsTitle"), results.length)}
|
|
4055
|
+
${summaryCard(view.raw("sarif.errors"), counts.error || 0)}
|
|
4056
|
+
${summaryCard(view.raw("sarif.warnings"), counts.warning || 0)}
|
|
4057
|
+
${summaryCard(view.raw("sarif.notes"), counts.note || 0)}
|
|
3798
4058
|
</div>
|
|
3799
4059
|
<p class="artifact-sarif-note">${escapeHtml(note)}</p>
|
|
3800
4060
|
<div class="artifact-table-scroll">
|
|
3801
4061
|
<table class="artifact-table" data-sarif-table>
|
|
3802
|
-
<thead><tr><th data-sarif-sort="level"
|
|
4062
|
+
<thead><tr><th data-sarif-sort="level">${view.text("sarif.level")}</th><th data-sarif-sort="rule">${view.text("sarif.rule")}</th><th>${view.text("sarif.message")}</th><th data-sarif-sort="location">${view.text("sarif.location")}</th><th>${view.text("sarif.tool")}</th></tr></thead>
|
|
3803
4063
|
<tbody>${rowHtml}</tbody>
|
|
3804
4064
|
</table>
|
|
3805
4065
|
</div>
|
|
3806
4066
|
<details class="artifact-raw-details">
|
|
3807
|
-
<summary
|
|
4067
|
+
<summary>${view.text("sarif.raw")}</summary>
|
|
3808
4068
|
<pre class="artifact-code"><code>${escapeHtml(formatJson(content))}</code></pre>
|
|
3809
4069
|
</details>
|
|
3810
4070
|
</section>`;
|
|
@@ -3885,7 +4145,7 @@ const NOTEBOOK_OUTPUT_MIME_PRIORITY = [
|
|
|
3885
4145
|
"text/plain"
|
|
3886
4146
|
];
|
|
3887
4147
|
|
|
3888
|
-
function renderNotebook(content) {
|
|
4148
|
+
function renderNotebook(content, view) {
|
|
3889
4149
|
let notebook;
|
|
3890
4150
|
try {
|
|
3891
4151
|
notebook = JSON.parse(content);
|
|
@@ -3900,16 +4160,16 @@ function renderNotebook(content) {
|
|
|
3900
4160
|
const diagramState = createMermaidDiagramState();
|
|
3901
4161
|
const cells = notebook.cells;
|
|
3902
4162
|
const rendered = cells.slice(0, NOTEBOOK_MAX_RENDERED_CELLS)
|
|
3903
|
-
.map((cell) => renderNotebookCell(cell, language, diagramState))
|
|
4163
|
+
.map((cell) => renderNotebookCell(cell, language, diagramState, view))
|
|
3904
4164
|
.join("");
|
|
3905
4165
|
const notice = cells.length > NOTEBOOK_MAX_RENDERED_CELLS
|
|
3906
|
-
? `<p class="artifact-notebook-notice"
|
|
4166
|
+
? `<p class="artifact-notebook-notice">${view.text("notebook.cells", { count: NOTEBOOK_MAX_RENDERED_CELLS, total: cells.length })}</p>`
|
|
3907
4167
|
: "";
|
|
3908
4168
|
|
|
3909
4169
|
return `<article class="artifact-doc artifact-notebook">${notice}${rendered}</article>`;
|
|
3910
4170
|
}
|
|
3911
4171
|
|
|
3912
|
-
function renderNotebookCell(cell, language, diagramState) {
|
|
4172
|
+
function renderNotebookCell(cell, language, diagramState, view) {
|
|
3913
4173
|
const cellType = cell?.cell_type;
|
|
3914
4174
|
const source = notebookText(cell?.source);
|
|
3915
4175
|
|
|
@@ -3928,9 +4188,9 @@ function renderNotebookCell(cell, language, diagramState) {
|
|
|
3928
4188
|
const cellOutputs = Array.isArray(cell?.outputs) ? cell.outputs : [];
|
|
3929
4189
|
const visibleOutputs = cellOutputs.slice(0, NOTEBOOK_MAX_OUTPUTS_PER_CELL);
|
|
3930
4190
|
const outputsNotice = cellOutputs.length > NOTEBOOK_MAX_OUTPUTS_PER_CELL
|
|
3931
|
-
? `<p class="artifact-notebook-notice"
|
|
4191
|
+
? `<p class="artifact-notebook-notice">${view.text("notebook.outputs", { count: NOTEBOOK_MAX_OUTPUTS_PER_CELL, total: cellOutputs.length })}</p>`
|
|
3932
4192
|
: "";
|
|
3933
|
-
const outputs = visibleOutputs.map((output) => renderNotebookOutput(output, diagramState)).join("") + outputsNotice;
|
|
4193
|
+
const outputs = visibleOutputs.map((output) => renderNotebookOutput(output, diagramState, view)).join("") + outputsNotice;
|
|
3934
4194
|
return `<section class="artifact-notebook-cell artifact-notebook-cell-code">
|
|
3935
4195
|
<div class="artifact-notebook-execcount">${escapeHtml(executionLabel)}</div>
|
|
3936
4196
|
<pre><code${classAttribute}>${escapeHtml(source)}</code></pre>
|
|
@@ -3949,11 +4209,11 @@ function stripAnsiEscapes(value) {
|
|
|
3949
4209
|
return String(value ?? "").replace(ANSI_ESCAPE_PATTERN, "");
|
|
3950
4210
|
}
|
|
3951
4211
|
|
|
3952
|
-
function renderNotebookOutput(output, diagramState) {
|
|
4212
|
+
function renderNotebookOutput(output, diagramState, view) {
|
|
3953
4213
|
const outputType = output?.output_type;
|
|
3954
4214
|
|
|
3955
4215
|
if (outputType === "stream") {
|
|
3956
|
-
return renderNotebookTextOutput(notebookText(output.text));
|
|
4216
|
+
return renderNotebookTextOutput(notebookText(output.text), "", view);
|
|
3957
4217
|
}
|
|
3958
4218
|
|
|
3959
4219
|
if (outputType === "error") {
|
|
@@ -3967,18 +4227,18 @@ function renderNotebookOutput(output, diagramState) {
|
|
|
3967
4227
|
: notebookText(output.traceback);
|
|
3968
4228
|
const traceback = stripAnsiEscapes(rawTraceback) ||
|
|
3969
4229
|
`${notebookText(output.ename)}: ${notebookText(output.evalue)}`;
|
|
3970
|
-
return renderNotebookTextOutput(traceback, "artifact-notebook-error");
|
|
4230
|
+
return renderNotebookTextOutput(traceback, "artifact-notebook-error", view);
|
|
3971
4231
|
}
|
|
3972
4232
|
|
|
3973
4233
|
if (outputType === "execute_result" || outputType === "display_data") {
|
|
3974
|
-
return renderNotebookMimeBundle(output.data, diagramState);
|
|
4234
|
+
return renderNotebookMimeBundle(output.data, diagramState, view);
|
|
3975
4235
|
}
|
|
3976
4236
|
|
|
3977
4237
|
return "";
|
|
3978
4238
|
}
|
|
3979
4239
|
|
|
3980
|
-
function renderNotebookTextOutput(text, extraClass = "") {
|
|
3981
|
-
const truncated = notebookTruncateIfOversized(text);
|
|
4240
|
+
function renderNotebookTextOutput(text, extraClass = "", view) {
|
|
4241
|
+
const truncated = notebookTruncateIfOversized(text, view);
|
|
3982
4242
|
if (truncated) {
|
|
3983
4243
|
return truncated;
|
|
3984
4244
|
}
|
|
@@ -3986,7 +4246,7 @@ function renderNotebookTextOutput(text, extraClass = "") {
|
|
|
3986
4246
|
return `<pre class="artifact-notebook-output artifact-code${classAttribute}"><code>${escapeHtml(text)}</code></pre>`;
|
|
3987
4247
|
}
|
|
3988
4248
|
|
|
3989
|
-
function renderNotebookMimeBundle(data, diagramState) {
|
|
4249
|
+
function renderNotebookMimeBundle(data, diagramState, view) {
|
|
3990
4250
|
const bundle = data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
3991
4251
|
const keys = Object.keys(bundle);
|
|
3992
4252
|
if (keys.length === 0) {
|
|
@@ -3995,11 +4255,11 @@ function renderNotebookMimeBundle(data, diagramState) {
|
|
|
3995
4255
|
|
|
3996
4256
|
const mime = NOTEBOOK_OUTPUT_MIME_PRIORITY.find((candidate) => keys.includes(candidate));
|
|
3997
4257
|
if (!mime) {
|
|
3998
|
-
return `<p class="artifact-notebook-output-placeholder"
|
|
4258
|
+
return `<p class="artifact-notebook-output-placeholder">${view.text("notebook.unsupported", { type: keys[0] })}</p>`;
|
|
3999
4259
|
}
|
|
4000
4260
|
|
|
4001
4261
|
const value = notebookText(bundle[mime]);
|
|
4002
|
-
const truncated = notebookTruncateIfOversized(value);
|
|
4262
|
+
const truncated = notebookTruncateIfOversized(value, view);
|
|
4003
4263
|
if (truncated) {
|
|
4004
4264
|
return truncated;
|
|
4005
4265
|
}
|
|
@@ -4013,9 +4273,9 @@ function renderNotebookMimeBundle(data, diagramState) {
|
|
|
4013
4273
|
if (mime === "image/png" || mime === "image/jpeg") {
|
|
4014
4274
|
const dataUrl = mediaDataUrl(value, mime);
|
|
4015
4275
|
if (!dataUrl) {
|
|
4016
|
-
return `<p class="artifact-notebook-output-placeholder"
|
|
4276
|
+
return `<p class="artifact-notebook-output-placeholder">${view.text("notebook.unsupported", { type: mime })}</p>`;
|
|
4017
4277
|
}
|
|
4018
|
-
return `<figure class="artifact-media artifact-image artifact-notebook-output"><img src="${escapeAttribute(dataUrl)}" alt="
|
|
4278
|
+
return `<figure class="artifact-media artifact-image artifact-notebook-output"><img src="${escapeAttribute(dataUrl)}" alt="${view.text("notebook.image")}"></figure>`;
|
|
4019
4279
|
}
|
|
4020
4280
|
if (mime === "image/svg+xml") {
|
|
4021
4281
|
return `<iframe class="artifact-frame artifact-svg-frame artifact-notebook-output" sandbox srcdoc="${escapeAttribute(svgFrameContent(value))}"></iframe>`;
|
|
@@ -4026,12 +4286,12 @@ function renderNotebookMimeBundle(data, diagramState) {
|
|
|
4026
4286
|
return "";
|
|
4027
4287
|
}
|
|
4028
4288
|
|
|
4029
|
-
function notebookTruncateIfOversized(value) {
|
|
4289
|
+
function notebookTruncateIfOversized(value, view) {
|
|
4030
4290
|
const size = Buffer.byteLength(String(value ?? ""), "utf8");
|
|
4031
4291
|
if (size <= NOTEBOOK_MAX_OUTPUT_BYTES) {
|
|
4032
4292
|
return null;
|
|
4033
4293
|
}
|
|
4034
|
-
return `<p class="artifact-notebook-output-truncated"
|
|
4294
|
+
return `<p class="artifact-notebook-output-truncated">${view.text("notebook.truncated", { size })}</p>`;
|
|
4035
4295
|
}
|
|
4036
4296
|
|
|
4037
4297
|
function notebookText(value) {
|