@songsid/agend 2.2.0 → 2.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-cli.js +248 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +25 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +30 -0
- package/dist/agent-file-read.js +129 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.js +13 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/atomic-write.js +9 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.js +17 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.js +3 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/launch-effort.js +1 -1
- package/dist/backend/launch-effort.js.map +1 -1
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +17 -2
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +8 -1
- package/dist/channel/adapters/telegram.js +74 -4
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +34 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +15 -0
- package/dist/config-validator.js +17 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.js +244 -20
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-manager.d.ts +89 -0
- package/dist/fleet-manager.js +401 -25
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
- package/dist/instance-avatar.js +3 -0
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +3 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +9 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/needs-you-hub.d.ts +4 -0
- package/dist/needs-you-hub.js +31 -6
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +1 -13
- package/dist/needs-you.js +3 -37
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +5 -0
- package/dist/outbound-handlers.js +30 -9
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +64 -0
- package/dist/outbound-schemas.js +50 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +14 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/repo-worktree-guard.d.ts +36 -0
- package/dist/repo-worktree-guard.js +143 -0
- package/dist/repo-worktree-guard.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +58 -1
- package/dist/scheduler/scheduler.js +176 -10
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/settings-api.js +74 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +8 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +16 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +5 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/tool-permissions.js +50 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +10 -6
- package/dist/topic-commands.js +21 -17
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.d.ts +128 -0
- package/dist/topic-link.js +269 -0
- package/dist/topic-link.js.map +1 -0
- package/dist/types.d.ts +13 -0
- package/dist/ui/app-effects.js +187 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +72 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +4 -4
- package/dist/ui/chat-store.js +9 -3
- package/dist/ui/chat-strings.js +15 -0
- package/dist/ui/chat-thread.js +47 -21
- package/dist/ui/chat-tour.js +1 -1
- package/dist/ui/effect-trial-controller.js +119 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +37 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +85 -17
- package/dist/ui/panel-details.js +18 -5
- package/dist/ui/panel-fleet.js +3 -3
- package/dist/ui/panel-log.js +6 -6
- package/dist/ui/panel-needs.js +3 -3
- package/dist/ui/panel-settings.js +8 -5
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/settings-appearance.js +325 -0
- package/dist/ui/settings-effects.js +201 -0
- package/dist/ui/settings-mods.js +257 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +245 -0
- package/dist/ui/shared/agend-auth.js +27 -3
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +2 -0
- package/dist/ui/shared/app-prefs.js +30 -10
- package/dist/ui/shared/app-route.js +23 -1
- package/dist/ui/shared/app-session.js +1 -1
- package/dist/ui/shared/app-shell.js +29 -16
- package/dist/ui/shared/app-store.js +2 -0
- package/dist/ui/shared/app-stream.js +12 -7
- package/dist/ui/shared/app-themes.js +188 -0
- package/dist/ui/shared/app.css +214 -1
- package/dist/ui/shared/app.js +6 -0
- package/dist/ui/shared/card-strings.js +28 -0
- package/dist/ui/shared/effect-app-runtime.js +248 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +154 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +127 -0
- package/dist/ui/shared/effect-geometry.js +122 -0
- package/dist/ui/shared/effect-host-style.js +10 -0
- package/dist/ui/shared/effect-owner-regions.js +74 -0
- package/dist/ui/shared/effect-owner-source.js +115 -0
- package/dist/ui/shared/effect-policy.js +137 -0
- package/dist/ui/shared/effect-poster-reader.js +98 -0
- package/dist/ui/shared/effect-posters.js +117 -0
- package/dist/ui/shared/effect-profiler.js +162 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +496 -0
- package/dist/ui/shared/effect-runtime-loader.js +234 -0
- package/dist/ui/shared/effect-safety.js +153 -0
- package/dist/ui/shared/effect-stock-snapshot.js +34 -0
- package/dist/ui/shared/effect-stock-trust.js +165 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +147 -0
- package/dist/ui/shared/header-tools.js +1 -1
- package/dist/ui/shared/identity-card.js +202 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +22 -14
- package/dist/ui/shared/owner-css.js +221 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +81 -8
- package/dist/ui/shared/signin.js +2 -0
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/ui-menu.js +1 -1
- package/dist/ui/shared/ui-toast.js +1 -1
- package/dist/ui/shared/view-strings.js +30 -0
- package/dist/ui/signin.html +1 -0
- package/dist/view-api.d.ts +29 -0
- package/dist/view-api.js +188 -8
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +42 -0
- package/dist/web-api.js +896 -36
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-limits.d.ts +82 -0
- package/dist/web-appearance-limits.js +83 -0
- package/dist/web-appearance-limits.js.map +1 -0
- package/dist/web-appearance-policy.d.ts +66 -0
- package/dist/web-appearance-policy.js +94 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +50 -0
- package/dist/web-appearance-tools.js +105 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +99 -0
- package/dist/web-appearance-view.js +163 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +79 -0
- package/dist/web-appearance.js +298 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +98 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +17 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +96 -0
- package/dist/web-card.js +232 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-chat-history.d.ts +13 -0
- package/dist/web-chat-history.js +23 -2
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +126 -0
- package/dist/web-effect-bundle.js.map +1 -0
- package/dist/web-effect-canonical.d.ts +8 -0
- package/dist/web-effect-canonical.js +27 -0
- package/dist/web-effect-canonical.js.map +1 -0
- package/dist/web-effect-library.d.ts +114 -0
- package/dist/web-effect-library.js +530 -0
- package/dist/web-effect-library.js.map +1 -0
- package/dist/web-effect-poster.d.ts +8 -0
- package/dist/web-effect-poster.js +52 -0
- package/dist/web-effect-poster.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +118 -0
- package/dist/web-effect-profiles.js +180 -0
- package/dist/web-effect-profiles.js.map +1 -0
- package/dist/web-effect-service.d.ts +86 -0
- package/dist/web-effect-service.js +312 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +47 -0
- package/dist/web-effect-tools.js +126 -0
- package/dist/web-effect-tools.js.map +1 -0
- package/dist/web-effect-trials.d.ts +86 -0
- package/dist/web-effect-trials.js +204 -0
- package/dist/web-effect-trials.js.map +1 -0
- package/dist/web-effect-upload.d.ts +29 -0
- package/dist/web-effect-upload.js +190 -0
- package/dist/web-effect-upload.js.map +1 -0
- package/dist/web-effect-worker.d.ts +8 -0
- package/dist/web-effect-worker.js +170 -0
- package/dist/web-effect-worker.js.map +1 -0
- package/dist/web-effect.d.ts +89 -0
- package/dist/web-effect.js +132 -0
- package/dist/web-effect.js.map +1 -0
- package/dist/web-identity-color.d.ts +87 -0
- package/dist/web-identity-color.js +209 -0
- package/dist/web-identity-color.js.map +1 -0
- package/dist/web-identity-palette.d.ts +113 -0
- package/dist/web-identity-palette.js +84 -0
- package/dist/web-identity-palette.js.map +1 -0
- package/dist/web-image-meta.d.ts +25 -0
- package/dist/web-image-meta.js +200 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +203 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +144 -0
- package/dist/web-mod-bundle.js +464 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +59 -0
- package/dist/web-mod-export.js +79 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +283 -0
- package/dist/web-mod-store.js +624 -0
- package/dist/web-mod-store.js.map +1 -0
- package/dist/web-org.d.ts +3 -3
- package/dist/web-org.js +11 -17
- package/dist/web-org.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +180 -0
- package/dist/web-owner-css-store.js +384 -0
- package/dist/web-owner-css-store.js.map +1 -0
- package/dist/web-owner-css.d.ts +58 -0
- package/dist/web-owner-css.js +552 -0
- package/dist/web-owner-css.js.map +1 -0
- package/dist/web-prefs.d.ts +48 -0
- package/dist/web-prefs.js +209 -33
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +9 -1
- package/dist/web-preview.js +24 -7
- package/dist/web-preview.js.map +1 -1
- package/dist/web-settings-patch.d.ts +18 -0
- package/dist/web-settings-patch.js +51 -0
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +218 -0
- package/dist/web-theme-store.js +516 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +121 -0
- package/dist/web-theme.js +407 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/package.json +1 -1
|
@@ -41,9 +41,10 @@ import { takeAgentRequest } from "./settings-request.js";
|
|
|
41
41
|
import { setPreviewOptIn } from "./panel-chat.js";
|
|
42
42
|
import { startTour } from "./chat-tour.js";
|
|
43
43
|
import "./chat-strings.js";
|
|
44
|
+
import { AppearanceSection } from "./settings-appearance.js";
|
|
44
45
|
|
|
45
46
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
46
|
-
const ICONS = { device: "user", agents: "bot", bots: "plug", classic: "room", general: "sliders", web: "globe", advanced: "code" };
|
|
47
|
+
const ICONS = { device: "user", appearance: "palette", agents: "bot", bots: "plug", classic: "room", general: "sliders", web: "globe", advanced: "code" };
|
|
47
48
|
const RELEASES = "https://github.com/songsid/AgEnD/releases";
|
|
48
49
|
|
|
49
50
|
/** GET under the panel's lease → { ok, status, body }. */
|
|
@@ -224,6 +225,8 @@ export function SettingsPanel({ route, navKey }) {
|
|
|
224
225
|
}, [!!ctx, navKey]);
|
|
225
226
|
// #1604: This device needs no configuration: shown at once, whatever the configuration read does.
|
|
226
227
|
const sectionBody = section === "device" ? html`<${DeviceSection} />`
|
|
228
|
+
// #1629 r4 §3: Appearance is the person's own (no configuration read): shown at once, like Preferences.
|
|
229
|
+
: section === "appearance" ? html`<${AppearanceSection} />`
|
|
227
230
|
: !data ? html`<${Skeleton} lines=${6} />`
|
|
228
231
|
: data.error ? html`<${ErrorState} message=${tn("configLoadFailed")} onRetry=${reload} />`
|
|
229
232
|
: section === "agents" ? html`<${Agents} ctx=${ctx} search=${search} openDialog=${(d) => setDialog({ ...d, key: navKey })} />`
|
|
@@ -236,14 +239,14 @@ export function SettingsPanel({ route, navKey }) {
|
|
|
236
239
|
const open = dialog && dialog.key === navKey ? dialog : null;
|
|
237
240
|
const close = () => setDialog(null);
|
|
238
241
|
const searchable = section === "agents" || section === "bots" || section === "classic";
|
|
239
|
-
return html`<div class="panel p-settings">
|
|
242
|
+
return html`<div data-effect-panel="settings" class="panel p-settings">
|
|
240
243
|
<${PanelHeader} title=${tn("title")} sub=${data && data.version ? html`<a class="pill ver" href=${data.version.includes("-") ? `${RELEASES}/latest` : `${RELEASES}/tag/v${data.version}`} target="_blank" rel="noopener">v${data.version}</a>` : null}>
|
|
241
244
|
<button type="button" class="btn btn-ghost btn-sm" disabled=${!ctx} onClick=${() => setDialog({ kind: "wizard", key: navKey })}><${Icon} name="wand" size=${16} /><span class="hide-narrow">${tn("wizardButton")}</span></button>
|
|
242
245
|
<button type="button" class="icon-btn" aria-label=${tn("helpButton")} title=${tn("helpButton")} onClick=${() => setDialog({ kind: "help", key: navKey })}><${Icon} name="info" /></button>
|
|
243
246
|
</${PanelHeader}>
|
|
244
|
-
<nav class="seg" aria-label=${tn("title")}>${SETTINGS_SECTIONS.map((k) => html`<a key=${k} href=${settingsPath(k)} class=${`seg-item${k === section ? " active" : ""}`}
|
|
247
|
+
<nav data-trust="panel-navigation" class="seg" aria-label=${tn("title")}>${SETTINGS_SECTIONS.map((k) => html`<a key=${k} href=${settingsPath(k)} class=${`seg-item${k === section ? " active" : ""}`}
|
|
245
248
|
aria-current=${k === section ? "page" : undefined}><${Icon} name=${ICONS[k]} size=${16} /><span>${tn(`sec_${k}`)}</span></a>`)}</nav>
|
|
246
|
-
<div class="panel-body"><div class="col">
|
|
249
|
+
<div data-trust="panel-content" class="panel-body"><div class="col">
|
|
247
250
|
${searchable ? html`<div class="s-toolbar"><label class="s-search"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("searchAll")}</span>
|
|
248
251
|
<input type="search" value=${search} placeholder=${tn("searchAll")} onInput=${(e) => setSearch(e.target.value)} /></label></div>` : null}
|
|
249
252
|
${op ? html`<${OperationCard} op=${op} schema=${data && data.schema} />` : null}
|
|
@@ -915,7 +918,7 @@ function Developer({ ctx }) {
|
|
|
915
918
|
/** Staged, not applied: the count, what it will cost, Discard and Apply. */
|
|
916
919
|
function PendingBar({ staged, busy, onApply, onDiscard }) {
|
|
917
920
|
const kinds = [...new Set([...staged.values()].map((c) => c.impact))];
|
|
918
|
-
return html`<div class="s-pending" role="region" aria-label=${tn("pendingRegion")}>
|
|
921
|
+
return html`<div data-trust="panel-controls" class="s-pending" role="region" aria-label=${tn("pendingRegion")}>
|
|
919
922
|
<strong>${tn("changesPending", staged.size)}</strong>
|
|
920
923
|
<span class="note">${kinds.map(impactText).join(" · ")}</span>
|
|
921
924
|
<span class="grow"></span>
|
package/dist/ui/preview-panel.js
CHANGED
|
@@ -16,6 +16,7 @@ import { Icon } from "/assets/ui-icons.js";
|
|
|
16
16
|
import "./chat-render.js";
|
|
17
17
|
import "./preview.js";
|
|
18
18
|
import { loadHtmlAttachment } from "./html-attachment.js";
|
|
19
|
+
import { EffectTrialPanel } from "./effect-trial-panel.js";
|
|
19
20
|
import { chatTime, onDayChange } from "./chat-time.js";
|
|
20
21
|
|
|
21
22
|
const R = () => globalThis.AgendChatRender;
|
|
@@ -43,14 +44,25 @@ export const panelClaims = () => claims;
|
|
|
43
44
|
/** The frame's key in preview.js: its own, so the card that opened it keeps its own state. */
|
|
44
45
|
export const frameKey = (key) => `panel:${key}`;
|
|
45
46
|
/** Show `spec` in the panel; `run`: start it there (the card's "Open in panel" click). The block shown before stops. */
|
|
47
|
+
function retire(open) {
|
|
48
|
+
if (!open) return;
|
|
49
|
+
if (open.kind === "trial") { try { open.handle.stop(); } catch { /* host controller owns cleanup */ } }
|
|
50
|
+
else if (P()) P().stop(frameKey(open.key), "stopped", "");
|
|
51
|
+
}
|
|
46
52
|
export function openPanel(spec, { run = false } = {}) {
|
|
47
|
-
|
|
48
|
-
set({ open: { ...spec }, run: run ? ++runs : 0 });
|
|
53
|
+
const old = state.open;
|
|
54
|
+
set({ open: { ...spec, kind: "html" }, run: run ? ++runs : 0 });
|
|
55
|
+
retire(old); // State changes first: cleanup reentry cannot close a newer panel.
|
|
56
|
+
}
|
|
57
|
+
/** A checked controller handle enters the existing component slot; no HTML run token is issued. */
|
|
58
|
+
export function openTrialPanel(spec) {
|
|
59
|
+
const old = state.open;
|
|
60
|
+
set({ open: { ...spec, kind: "trial" }, run: 0 }); retire(old);
|
|
49
61
|
}
|
|
50
|
-
/** Close the
|
|
62
|
+
/** Close publishes the empty slot before old-owner callbacks can reenter. */
|
|
51
63
|
export function closePanel() {
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
const old = state.open; if (!old) return;
|
|
65
|
+
set({ open: null, run: 0 }); retire(old);
|
|
54
66
|
}
|
|
55
67
|
/** The card key the panel shows, or null. */
|
|
56
68
|
export const shownKey = () => (state.open ? state.open.key : null);
|
|
@@ -67,7 +79,7 @@ export function htmlTitle(code) {
|
|
|
67
79
|
* without one, the newest block does. `msgs` in order; `keyOf` makes a message's key.
|
|
68
80
|
*/
|
|
69
81
|
export function newerVersion(open, msgs, keyOf) {
|
|
70
|
-
if (!open || !Array.isArray(msgs)) return null;
|
|
82
|
+
if (!open || open.kind === "trial" || !Array.isArray(msgs)) return null;
|
|
71
83
|
const at = msgs.findIndex(x => keyOf(x) === open.msgKey);
|
|
72
84
|
if (at < 0) return null;
|
|
73
85
|
// An attached file: a later agent reply attaching a file of the same name is its newer version. Its HTML is read
|
|
@@ -132,7 +144,8 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
132
144
|
const pending = useRef(0); // #1704: the card's click, waiting for this device's Allow (the st.run it came with)
|
|
133
145
|
const ownAllow = useRef(0); // while this panel's own Allow commits its choice: the click it answers (not cancelled by it)
|
|
134
146
|
const holder = useRef(null), divider = useRef(null), panel = useRef(null);
|
|
135
|
-
const
|
|
147
|
+
const trial = st.open?.kind === "trial" && st.open.instance === name ? st.open : null;
|
|
148
|
+
const open = st.open?.kind !== "trial" && st.open?.instance === name ? st.open : null;
|
|
136
149
|
|
|
137
150
|
useEffect(() => panelStore.subscribe(setSt), []);
|
|
138
151
|
// This device's choice (here or in another tab) decides whether Preview is offered: re-read it when it changes.
|
|
@@ -231,6 +244,9 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
231
244
|
// Esc in the panel closes it (and is not the chat's "stop the reply").
|
|
232
245
|
const onPanelKey = (e) => { if (e.key === "Escape" && !e.isComposing) { e.preventDefault(); closePanel(); } };
|
|
233
246
|
|
|
247
|
+
if (trial) return html`<${EffectTrialPanel} binding=${trial.binding} title=${trial.handle.reply.offer.title}
|
|
248
|
+
current=${trial.handle.current} ownerCss=${trial.ownerCss} onStage=${trial.onStage}
|
|
249
|
+
onStop=${trial.handle.stop} onClose=${() => { if (panelStore.get().open === trial) closePanel(); }} />`;
|
|
234
250
|
if (!open) return null;
|
|
235
251
|
const going = run.state === "starting" || run.state === "running";
|
|
236
252
|
const a = P().availability();
|
|
@@ -250,9 +266,9 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
250
266
|
if (want && pending.current === want && panelStore.get().run === want) start();
|
|
251
267
|
};
|
|
252
268
|
const source = t("chat.pvSource", open.sender || name, timeOf(open.ts));
|
|
253
|
-
return html`<div class="pv-divider" ref=${divider} role="separator" aria-orientation="vertical" tabindex="0" aria-label=${t("chat.pvResize")}
|
|
269
|
+
return html`<div data-trust="preview" class="pv-divider" ref=${divider} role="separator" aria-orientation="vertical" tabindex="0" aria-label=${t("chat.pvResize")}
|
|
254
270
|
aria-valuemin=${String(MIN_PANEL)} onKeyDown=${onKey} onPointerDown=${onDown}></div>
|
|
255
|
-
<aside class="pv-panel" ref=${panel} aria-label=${t("chat.pvPanel")} onKeyDown=${onPanelKey}>
|
|
271
|
+
<aside data-trust="preview" class="pv-panel" ref=${panel} aria-label=${t("chat.pvPanel")} onKeyDown=${onPanelKey}>
|
|
256
272
|
<div class="pv-panel-head">
|
|
257
273
|
<button type="button" class="icon-btn only-narrow pv-panel-close" title=${t("chat.pvBack")} aria-label=${t("chat.pvBack")} onClick=${closePanel}><${Icon} name="back" size=${18} /></button>
|
|
258
274
|
<div class="pv-panel-title"><strong>${t("chat.pvPanel")}</strong>
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// #1629 r4 §3: Settings → Appearance — the person's own custom CSS (owner CSS): the editor, keep-or-revert, earlier
|
|
2
|
+
// versions, the pictures and fonts it may name, and this device's off switch. Settings itself is always drawn stock
|
|
3
|
+
// (owner-css.js removes the link here), so a stylesheet that hides things can always be fixed from this page.
|
|
4
|
+
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
5
|
+
import { t } from "/assets/app-i18n.js";
|
|
6
|
+
import { appStore, useStore } from "/assets/app-store.js";
|
|
7
|
+
import { ModsCard } from "./settings-mods.js";
|
|
8
|
+
import { EffectsCard } from "./settings-effects.js";
|
|
9
|
+
import { Icon } from "/assets/ui-icons.js";
|
|
10
|
+
import { toast } from "/assets/ui-toast.js";
|
|
11
|
+
import { ownerCssOffHere, ownerCssTrial, safeMode, setOwnerCssActive, setOwnerCssOffHere, startOwnerCssTrial } from "/assets/owner-css.js";
|
|
12
|
+
import { setThemesShown, themesStore } from "/assets/app-themes.js";
|
|
13
|
+
import { CARD_SIZES, cardSizeDefault, cardSizeStore, setCardSizeDefault } from "/assets/identity-card.js";
|
|
14
|
+
import { AgentWidget, checkWidget, fleetBinds, setSidebarWidget, sidebarWidgetRaw, sidebarWidgetStore, widgetProblem } from "/assets/agent-widget.js";
|
|
15
|
+
import { confirmedWrite } from "./settings-confirm.js";
|
|
16
|
+
import { savePref } from "/assets/app-prefs.js";
|
|
17
|
+
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
18
|
+
import { colorsOff, defaultPlaces, PLACEMENTS, setColorsOff, setDefaultPlaces, useIdentityColors } from "/assets/identity-color.js";
|
|
19
|
+
import "./settings-strings.js";
|
|
20
|
+
|
|
21
|
+
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
22
|
+
const short = (sha) => (sha ? sha.slice(0, 8) : "");
|
|
23
|
+
const kib = (n) => `${Math.max(1, Math.round(n / 1024))} KiB`;
|
|
24
|
+
|
|
25
|
+
async function call(path, init) {
|
|
26
|
+
let res;
|
|
27
|
+
try { res = await fetch(path, { credentials: "same-origin", ...init }); } catch { return { ok: false, status: 0, body: null }; }
|
|
28
|
+
let body = null;
|
|
29
|
+
try { body = await res.json(); } catch { /* not JSON */ }
|
|
30
|
+
return { ok: res.ok, status: res.status, body };
|
|
31
|
+
}
|
|
32
|
+
const postJson = (path, body) => call(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
33
|
+
|
|
34
|
+
/** Why the host refused a text, in the person's words. */
|
|
35
|
+
function refusal(body) {
|
|
36
|
+
const problem = body && body.error, detail = body && body.detail;
|
|
37
|
+
const known = ["at_rule", "url", "unknown_asset", "function", "property", "script_scheme", "too_large", "malformed", "nul", "unserialisable",
|
|
38
|
+
"asset_type", "asset_unreadable", "asset_quota", "asset_in_use", "owner_css_read_only", "owner_css_needs_a_signed_in_person"];
|
|
39
|
+
return known.includes(problem) ? tn(`cssError_${problem}`, detail || "") : tn("cssError_other", problem || "?");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function AppearanceSection() {
|
|
43
|
+
const publicLink = typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1";
|
|
44
|
+
if (publicLink) return html`<section class="card s-appearance"><h3>${tn("cssTitle")}</h3><p class="sub">${tn("cssPublicLink")}</p></section>`;
|
|
45
|
+
return html`<${ThemesCard} /><${IdentityColorCard} /><${CardSizeCard} /><${SidebarWidgetCard} /><${OwnerCssCard} /><${ModsCard} /><${EffectsCard} /><${DeviceCard} /><${CardLimitsCard} /><${RestoreDefaultsCard} />`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* #1958: identity colours on this host — where they show for an instance nobody chose for (ring and bar unless
|
|
50
|
+
* changed), and a switch that turns them off everywhere while every pick is kept.
|
|
51
|
+
*/
|
|
52
|
+
function IdentityColorCard() {
|
|
53
|
+
useIdentityColors();
|
|
54
|
+
const places = defaultPlaces(), off = colorsOff();
|
|
55
|
+
const toggle = (p) => setDefaultPlaces(places.includes(p) ? places.filter((x) => x !== p) : [...places, p]);
|
|
56
|
+
return html`<section class="card s-appearance s-synced s-colors"><h3>${tn("colorTitle")}</h3>
|
|
57
|
+
<p class="sub">${tn("colorLead")}</p>
|
|
58
|
+
<fieldset class="idc-places" disabled=${off}><legend>${tn("colorPlaces")}</legend>
|
|
59
|
+
${PLACEMENTS.map((p) => html`<label key=${p} class="check"><input type="checkbox" checked=${places.includes(p)} onChange=${() => toggle(p)} /> ${t(`color.place_${p}`)}</label>`)}
|
|
60
|
+
</fieldset>
|
|
61
|
+
<label class="check"><input type="checkbox" class="s-colors-off" checked=${off} onChange=${(e) => setColorsOff(e.target.checked)} /> <span>${tn("colorOff")}</span></label>
|
|
62
|
+
<p class="sub">${tn("colorOffLead")}</p>
|
|
63
|
+
</section>`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* #1958 decision 5: Restore defaults — the host's appearance settings back to their defaults after a confirm that says
|
|
68
|
+
* what it resets; this person's custom CSS turned off (its versions stay). Not settings, so never touched: the colours
|
|
69
|
+
* people picked per instance, installed mods, uploaded effects and the fleet's card limits.
|
|
70
|
+
*/
|
|
71
|
+
export async function restoreAppearanceDefaults() {
|
|
72
|
+
setThemesShown(true);
|
|
73
|
+
setCardSizeDefault("m");
|
|
74
|
+
savePref("card.sizeByInstance", null);
|
|
75
|
+
setSidebarWidget(null);
|
|
76
|
+
setDefaultPlaces(null);
|
|
77
|
+
setColorsOff(false);
|
|
78
|
+
setOwnerCssOffHere(false);
|
|
79
|
+
const r = await postJson("/ui/owner-css/restore", { sha: null });
|
|
80
|
+
return r && r.error ? { ok: false, error: r.error } : { ok: true };
|
|
81
|
+
}
|
|
82
|
+
function RestoreDefaultsCard() {
|
|
83
|
+
const [busy, setBusy] = useState(false);
|
|
84
|
+
async function go() {
|
|
85
|
+
if (!(await confirmDialog({ title: tn("restoreTitle"), message: tn("restoreConfirm"), confirmLabel: tn("restoreGo"), danger: true }))) return;
|
|
86
|
+
setBusy(true);
|
|
87
|
+
const r = await restoreAppearanceDefaults();
|
|
88
|
+
setBusy(false);
|
|
89
|
+
if (r.ok) toast(tn("restoreDone")); else toast(tn("restoreCssFailed", r.error), false);
|
|
90
|
+
}
|
|
91
|
+
return html`<section class="card s-appearance s-restore"><h3>${tn("restoreTitle")}</h3>
|
|
92
|
+
<p class="sub">${tn("restoreLead")}</p>
|
|
93
|
+
<button type="button" class="btn" disabled=${busy} onClick=${go}>${tn("restoreTitle")}</button>
|
|
94
|
+
</section>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** #1629 r4.1 Q3 (a): every identity card's size on this person's screens (a card's own control sets it apart). */
|
|
98
|
+
function CardSizeCard() {
|
|
99
|
+
useStore(cardSizeStore);
|
|
100
|
+
const size = cardSizeDefault();
|
|
101
|
+
return html`<section class="card s-appearance s-synced"><h3>${tn("cardSizeTitle")}</h3>
|
|
102
|
+
<div class="card-size" role="group" aria-label=${tn("cardSizeTitle")}>
|
|
103
|
+
${CARD_SIZES.map((s) => html`<button key=${s} type="button" class=${`seg-item${s === size ? " active" : ""}`} aria-pressed=${s === size ? "true" : "false"}
|
|
104
|
+
onClick=${() => setCardSizeDefault(s)}>${t(`card.size${s.toUpperCase()}Name`)}</button>`)}
|
|
105
|
+
</div>
|
|
106
|
+
<p class="sub">${tn("cardSizeLead")}</p>
|
|
107
|
+
</section>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const LIMIT_FIELDS = [["portrait", "max_width"], ["portrait", "max_height"], ["portrait", "max_kib"], ["banner", "max_width"], ["banner", "max_height"], ["banner", "max_kib"],
|
|
111
|
+
["instance_quota", "files"], ["instance_quota", "mib"]];
|
|
112
|
+
/**
|
|
113
|
+
* #1629 r4.1 Q3 (b): the identity card's upload limits (fleet.yaml web.appearance). Each field shows what is in force
|
|
114
|
+
* when it differs from what is written (a value beyond its bound is clamped). A lower limit applies at once; a higher
|
|
115
|
+
* one waits for a fleet admin's confirmation (the server decides). Stored pictures are never touched.
|
|
116
|
+
*/
|
|
117
|
+
function CardLimitsCard() {
|
|
118
|
+
const [view, setView] = useState(null);
|
|
119
|
+
const [draft, setDraft] = useState({});
|
|
120
|
+
const [busy, setBusy] = useState(false);
|
|
121
|
+
const [msg, setMsg] = useState(null);
|
|
122
|
+
const load = async () => {
|
|
123
|
+
const r = await call("/api/settings/fleet/appearance");
|
|
124
|
+
// Only an answer of the expected shape is drawn: every group's in-force values, defaults and bounds.
|
|
125
|
+
const ok = r.ok && r.body && ["portrait", "banner", "instance_quota"].every((g) => r.body.effective && r.body.effective[g] && r.body.defaults && r.body.defaults[g] && r.body.bounds && r.body.bounds[g]);
|
|
126
|
+
if (!ok) { setView({ error: true }); return; }
|
|
127
|
+
setView(r.body);
|
|
128
|
+
const d = {};
|
|
129
|
+
for (const [g, k] of LIMIT_FIELDS) { const v = r.body.configured && r.body.configured[g] && r.body.configured[g][k]; d[`${g}.${k}`] = typeof v === "number" ? String(v) : ""; }
|
|
130
|
+
setDraft(d);
|
|
131
|
+
};
|
|
132
|
+
useEffect(() => { load(); }, []);
|
|
133
|
+
if (!view) return null;
|
|
134
|
+
if (view.error) return html`<section class="card s-appearance"><h3>${tn("cardLimitsTitle")}</h3><p class="sub">${tn("cardLimitsUnavailable")}</p></section>`;
|
|
135
|
+
const configured = (g, k) => (view.configured && view.configured[g] && typeof view.configured[g][k] === "number" ? view.configured[g][k] : null);
|
|
136
|
+
const save = async () => {
|
|
137
|
+
const body = {};
|
|
138
|
+
for (const [g, k] of LIMIT_FIELDS) {
|
|
139
|
+
const raw = (draft[`${g}.${k}`] || "").trim();
|
|
140
|
+
const next = raw === "" ? null : Number(raw);
|
|
141
|
+
if (next !== null && !(Number.isInteger(next) && next > 0)) { setMsg(tn("cardLimitsWhole")); return; }
|
|
142
|
+
if (next !== configured(g, k)) (body[g] ??= {})[k] = next;
|
|
143
|
+
}
|
|
144
|
+
if (!Object.keys(body).length) { setMsg(tn("cardLimitsNoChange")); return; }
|
|
145
|
+
setBusy(true); setMsg(null);
|
|
146
|
+
const r = await confirmedWrite("/api/settings/fleet/appearance", { method: "PUT", body, label: tn("cardLimitsTitle") });
|
|
147
|
+
setBusy(false);
|
|
148
|
+
if (r.ok) { setMsg(tn("cardLimitsSaved")); load(); } else setMsg(tn("cardLimitsFailed", (r.body && r.body.error) || r.status));
|
|
149
|
+
};
|
|
150
|
+
const warnings = Array.isArray(view.warnings) ? view.warnings : [];
|
|
151
|
+
return html`<section class="card s-appearance"><h3>${tn("cardLimitsTitle")}</h3>
|
|
152
|
+
<p class="sub">${tn("cardLimitsLead")}</p>
|
|
153
|
+
<fieldset class="form" disabled=${busy}>
|
|
154
|
+
${["portrait", "banner", "instance_quota"].map((g) => html`<div key=${g} class="grid3">
|
|
155
|
+
${LIMIT_FIELDS.filter(([gg]) => gg === g).map(([, k]) => {
|
|
156
|
+
const id = `cardLimit-${g}-${k}`, eff = view.effective[g][k], conf = configured(g, k);
|
|
157
|
+
return html`<div key=${k} class="field"><label for=${id}>${tn(`cardLimit_${g}_${k}`)}</label>
|
|
158
|
+
<input id=${id} type="number" min="1" step="1" placeholder=${String(view.defaults[g][k])} value=${draft[`${g}.${k}`] || ""}
|
|
159
|
+
onInput=${(e) => setDraft({ ...draft, [`${g}.${k}`]: e.target.value })} />
|
|
160
|
+
${conf !== null && conf !== eff ? html`<span class="note">${tn("cardLimitInForce", eff, view.bounds[g][k][0], view.bounds[g][k][1])}</span>` : null}</div>`;
|
|
161
|
+
})}
|
|
162
|
+
</div>`)}
|
|
163
|
+
</fieldset>
|
|
164
|
+
${warnings.length ? html`<ul class="note s-limit-warnings">${warnings.map((w, i) => html`<li key=${i}><code>${w.path}</code>: ${w.message}</li>`)}</ul>` : null}
|
|
165
|
+
<div class="save-row"><button type="button" class="btn btn-primary" disabled=${busy} onClick=${save}>${tn("cardLimitsSave")}</button>
|
|
166
|
+
${msg ? html`<span class="note" role="status">${msg}</span>` : null}</div>
|
|
167
|
+
</section>`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** #1629 r2 (d): whether this person's screens show agents' themes (and, as they come, effects) — synced. */
|
|
171
|
+
function ThemesCard() {
|
|
172
|
+
const s = useStore(themesStore);
|
|
173
|
+
return html`<section class="card s-appearance s-synced"><h3>${tn("themesTitle")}</h3>
|
|
174
|
+
<label class="check"><input type="checkbox" checked=${s.shown} disabled=${safeMode()} onChange=${(e) => setThemesShown(e.target.checked)} />
|
|
175
|
+
<span>${tn("themesShow")}</span></label>
|
|
176
|
+
<p class="sub">${safeMode() ? tn("themesSafeMode") : tn("themesShowLead")}</p>
|
|
177
|
+
</section>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** An example to start from: the fleet's counts, as a person's sidebar widget may show them. */
|
|
181
|
+
export const SIDEBAR_WIDGET_EXAMPLE = {
|
|
182
|
+
spec: { v: 1, title: "Fleet", items: [
|
|
183
|
+
{ type: "stat", label: "Working", bind: "fleet.working" },
|
|
184
|
+
{ type: "stat", label: "Needs you", bind: "fleet.needs_you" },
|
|
185
|
+
{ type: "stat", label: "Idle", bind: "fleet.idle" },
|
|
186
|
+
{ type: "stat", label: "Stopped", bind: "fleet.stopped" },
|
|
187
|
+
] },
|
|
188
|
+
data: {},
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* #1629 r4-4: the person's own sidebar widget — a declarative spec and its values, as JSON; drawn under the instance
|
|
192
|
+
* list by the page's own components, with the fleet's counts as the only live values. Synced like the other choices.
|
|
193
|
+
*/
|
|
194
|
+
function SidebarWidgetCard() {
|
|
195
|
+
useStore(sidebarWidgetStore);
|
|
196
|
+
const app = useStore(appStore);
|
|
197
|
+
const saved = sidebarWidgetRaw();
|
|
198
|
+
const [text, setText] = useState(() => (saved ? JSON.stringify(saved, null, 2) : ""));
|
|
199
|
+
const [error, setError] = useState("");
|
|
200
|
+
let parsed = null, problem = "";
|
|
201
|
+
if (text.trim()) {
|
|
202
|
+
try { parsed = JSON.parse(text); problem = widgetProblem("sidebar", parsed) || ""; } catch { problem = tn("widgetNotJson"); }
|
|
203
|
+
}
|
|
204
|
+
const preview = parsed && !problem ? checkWidget("sidebar", parsed) : null;
|
|
205
|
+
const save = () => {
|
|
206
|
+
if (!parsed || problem) { setError(problem || tn("widgetNotJson")); return; }
|
|
207
|
+
setError("");
|
|
208
|
+
setSidebarWidget(parsed);
|
|
209
|
+
toast(tn("widgetSaved"));
|
|
210
|
+
};
|
|
211
|
+
const remove = () => { setSidebarWidget(null); setText(""); setError(""); toast(tn("widgetRemoved")); };
|
|
212
|
+
return html`<section class="card s-appearance s-synced s-widget"><h3>${tn("widgetTitle")}</h3>
|
|
213
|
+
<p class="sub">${tn("widgetLead")}</p>
|
|
214
|
+
<label class="field"><span class="sr-only">${tn("widgetTitle")}</span>
|
|
215
|
+
<textarea class="s-widget-text" rows="10" spellcheck=${false} autocapitalize="off" autocomplete="off" value=${text}
|
|
216
|
+
onInput=${(e) => { setText(e.target.value); setError(""); }}></textarea></label>
|
|
217
|
+
${error || problem ? html`<p class="err" role="alert">${error || problem}</p>` : null}
|
|
218
|
+
${preview ? html`<div class="s-widget-preview"><${AgentWidget} slot="sidebar" widget=${preview} binds=${fleetBinds(app.instances, app.exec, app.awaiting)} /></div>` : null}
|
|
219
|
+
<div class="s-widget-actions">
|
|
220
|
+
<button type="button" class="btn btn-primary" disabled=${!parsed || !!problem} onClick=${save}><${Icon} name="check" size=${16} />${tn("widgetSave")}</button>
|
|
221
|
+
${!text.trim() ? html`<button type="button" class="btn" onClick=${() => setText(JSON.stringify(SIDEBAR_WIDGET_EXAMPLE, null, 2))}>${tn("widgetExample")}</button>` : null}
|
|
222
|
+
${saved ? html`<button type="button" class="btn" onClick=${remove}>${tn("widgetRemove")}</button>` : null}
|
|
223
|
+
</div>
|
|
224
|
+
</section>`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function OwnerCssCard() {
|
|
228
|
+
const [state, setState] = useState(null);
|
|
229
|
+
const [text, setText] = useState("");
|
|
230
|
+
const [busy, setBusy] = useState(false);
|
|
231
|
+
const [error, setError] = useState("");
|
|
232
|
+
const trial = useStore(ownerCssTrial);
|
|
233
|
+
const file = useRef(null);
|
|
234
|
+
const load = async (opts = {}) => {
|
|
235
|
+
const r = await call("/ui/owner-css/state");
|
|
236
|
+
if (!r.ok || !r.body) { setState({ failed: true }); return; }
|
|
237
|
+
setState(r.body);
|
|
238
|
+
if (opts.text !== false) setText(r.body.text || "");
|
|
239
|
+
};
|
|
240
|
+
useEffect(() => { load(); }, []);
|
|
241
|
+
// A trial answered (kept or reverted) changes what is in force: read it again, keeping the editor's text.
|
|
242
|
+
useEffect(() => { if (state) load({ text: false }); }, [trial && trial.sha]);
|
|
243
|
+
if (!state) return html`<section class="card s-appearance"><h3>${tn("cssTitle")}</h3><p class="sub">…</p></section>`;
|
|
244
|
+
if (state.failed) return html`<section class="card s-appearance"><h3>${tn("cssTitle")}</h3><p class="err">${tn("cssLoadFailed")}</p></section>`;
|
|
245
|
+
|
|
246
|
+
const save = async () => {
|
|
247
|
+
setBusy(true); setError("");
|
|
248
|
+
const r = await postJson("/ui/owner-css", { text });
|
|
249
|
+
setBusy(false);
|
|
250
|
+
if (!r.ok) { setError(refusal(r.body)); return; }
|
|
251
|
+
startOwnerCssTrial(r.body.trial, r.body.sha, r.body.previous);
|
|
252
|
+
toast(tn("cssTrying"));
|
|
253
|
+
load({ text: false });
|
|
254
|
+
};
|
|
255
|
+
const restore = async (sha) => {
|
|
256
|
+
setBusy(true); setError("");
|
|
257
|
+
const r = await postJson("/ui/owner-css/restore", { sha });
|
|
258
|
+
setBusy(false);
|
|
259
|
+
if (!r.ok) { setError(refusal(r.body)); return; }
|
|
260
|
+
setOwnerCssActive(r.body.active);
|
|
261
|
+
load();
|
|
262
|
+
};
|
|
263
|
+
const addAsset = async () => {
|
|
264
|
+
const f = file.current && file.current.files && file.current.files[0];
|
|
265
|
+
if (!f) return;
|
|
266
|
+
setBusy(true); setError("");
|
|
267
|
+
const r = await call("/ui/owner-css/asset", { method: "POST", headers: { "Content-Type": "application/octet-stream" }, body: f });
|
|
268
|
+
setBusy(false);
|
|
269
|
+
if (file.current) file.current.value = "";
|
|
270
|
+
if (!r.ok) { setError(refusal(r.body)); return; }
|
|
271
|
+
load({ text: false });
|
|
272
|
+
};
|
|
273
|
+
const removeAsset = async (id) => {
|
|
274
|
+
setBusy(true); setError("");
|
|
275
|
+
const r = await call(`/ui/owner-css/asset/${id}`, { method: "DELETE" });
|
|
276
|
+
setBusy(false);
|
|
277
|
+
if (!r.ok) { setError(refusal(r.body)); return; }
|
|
278
|
+
load({ text: false });
|
|
279
|
+
};
|
|
280
|
+
const copyRef = (id) => {
|
|
281
|
+
const ref = `url("asset:${id}")`;
|
|
282
|
+
try { navigator.clipboard.writeText(ref).then(() => toast(tn("cssCopied", ref)), () => toast(ref)); } catch { toast(ref); }
|
|
283
|
+
};
|
|
284
|
+
const inForce = state.active
|
|
285
|
+
? (state.pending ? tn("cssOnTrial", short(state.active)) : tn("cssInForce", short(state.active)))
|
|
286
|
+
: tn("cssNone");
|
|
287
|
+
const assetBytes = state.assets.reduce((n, a) => n + a.bytes, 0);
|
|
288
|
+
return html`<section class="card s-appearance s-owner-css"><h3>${tn("cssTitle")}</h3>
|
|
289
|
+
<p class="sub">${tn("cssLead")}</p>
|
|
290
|
+
<p class="s-css-status" role="status">${inForce}</p>
|
|
291
|
+
<label class="field"><span class="sr-only">${tn("cssTitle")}</span>
|
|
292
|
+
<textarea class="s-css-text" rows="14" spellcheck=${false} autocapitalize="off" autocomplete="off" value=${text} disabled=${busy}
|
|
293
|
+
onInput=${(e) => setText(e.target.value)}></textarea></label>
|
|
294
|
+
${error ? html`<p class="err" role="alert">${error}</p>` : null}
|
|
295
|
+
<div class="s-css-actions">
|
|
296
|
+
<button type="button" class="btn btn-primary" disabled=${busy || !text.trim()} onClick=${save}><${Icon} name="check" size=${16} />${tn("cssSave")}</button>
|
|
297
|
+
${state.active ? html`<button type="button" class="btn" disabled=${busy} onClick=${() => restore(null)}>${tn("cssOff")}</button>` : null}
|
|
298
|
+
</div>
|
|
299
|
+
${state.versions.length ? html`<details class="s-css-versions"><summary>${tn("cssVersions", state.versions.length)}</summary>
|
|
300
|
+
<ul class="s-css-list">${state.versions.map((v) => html`<li key=${v.sha}>
|
|
301
|
+
<code>${short(v.sha)}</code><span class="muted">${new Date(v.at).toLocaleString()} · ${kib(v.bytes)}</span>
|
|
302
|
+
${v.sha === state.active ? html`<span class="pill">${tn("cssCurrent")}</span>`
|
|
303
|
+
: html`<button type="button" class="btn btn-sm" disabled=${busy} onClick=${() => restore(v.sha)}>${tn("cssRestore")}</button>`}
|
|
304
|
+
</li>`)}</ul></details>` : null}
|
|
305
|
+
<h4>${tn("cssAssets")}</h4>
|
|
306
|
+
<p class="sub">${tn("cssAssetsLead", state.assets.length, state.limits.maxAssets, kib(assetBytes))}</p>
|
|
307
|
+
${state.assets.length ? html`<ul class="s-css-list">${state.assets.map((a) => html`<li key=${a.id}>
|
|
308
|
+
<code>${a.id}</code><span class="muted">${a.kind} · ${kib(a.bytes)}</span>
|
|
309
|
+
<button type="button" class="btn btn-sm" onClick=${() => copyRef(a.id)}>${tn("cssAssetCopy")}</button>
|
|
310
|
+
<button type="button" class="btn btn-sm btn-ghost" disabled=${busy} onClick=${() => removeAsset(a.id)}>${tn("cssAssetRemove")}</button>
|
|
311
|
+
</li>`)}</ul>` : null}
|
|
312
|
+
<label class="field"><span>${tn("cssAssetAdd")}</span>
|
|
313
|
+
<input ref=${file} type="file" accept="image/png,image/webp,image/jpeg,image/gif,font/woff2,.woff2" disabled=${busy} onChange=${addAsset} /></label>
|
|
314
|
+
</section>`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function DeviceCard() {
|
|
318
|
+
const [off, setOff] = useState(ownerCssOffHere());
|
|
319
|
+
return html`<section class="card s-appearance s-local"><h3>${tn("cssDeviceTitle")}</h3>
|
|
320
|
+
<label class="check"><input type="checkbox" checked=${off} onChange=${(e) => { setOwnerCssOffHere(e.target.checked); setOff(ownerCssOffHere()); }} />
|
|
321
|
+
<span>${tn("cssDeviceOff")}</span></label>
|
|
322
|
+
<p class="sub">${tn("cssDeviceOffLead")}</p>
|
|
323
|
+
<p class="sub">${tn("cssSafe")}</p>
|
|
324
|
+
</section>`;
|
|
325
|
+
}
|