@deksden-com/cpa-kit-react 0.0.0-stage → 0.1.4

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/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # @deksden-com/cpa-kit-react
2
+
3
+ ## 0.1.4
4
+
5
+ ### Patch Changes
6
+
7
+ - ede8760: Include generated package changelogs in npm distributions. Release versions and the exact core peer dependency are maintained together through Changesets.
8
+ - Skip irrelevant archive entries without decompressing them, allow larger uploads, and show clear import progress and limit errors.
9
+ - Updated dependencies [ede8760]
10
+ - Updated dependencies
11
+ - @deksden-com/cpa-kit@0.1.4
package/NOTICE ADDED
@@ -0,0 +1,7 @@
1
+ CPA Kit React
2
+ Copyright 2026 deksden-com. All rights reserved.
3
+
4
+ Original UI implementation; no third-party UI source is
5
+ vendored. React and React DOM are external peer dependencies under their
6
+ respective MIT license. Their original copyright and license terms remain
7
+ applicable when a host redistributes those dependencies.
package/README.md CHANGED
@@ -1,3 +1,33 @@
1
- # Temporary Holding Version
1
+ # CPA Kit React
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Portable, host-authenticated CPA console. No login page, router, Next.js, Tailwind scanner or toast provider is required.
4
+
5
+ Available on [npm](https://www.npmjs.com/package/@deksden-com/cpa-kit-react):
6
+ `pnpm add @deksden-com/cpa-kit-react @deksden-com/cpa-kit`. No install token required.
7
+
8
+ ```tsx
9
+ import { useMemo } from 'react';
10
+ import { createCpaAdminClient } from '@deksden-com/cpa-kit/client';
11
+ import { CpaConsole } from '@deksden-com/cpa-kit-react';
12
+ import '@deksden-com/cpa-kit-react/styles.css';
13
+
14
+ export function Administration({ csrfToken }: { csrfToken: string }) {
15
+ const client = useMemo(() => createCpaAdminClient({
16
+ baseUrl: '/api/cpa', instanceId: 'primary',
17
+ headers: () => ({ 'X-CSRF-Token': csrfToken }),
18
+ }), [csrfToken]);
19
+ return <CpaConsole client={client} instanceId="primary" locale="ru" theme="dark" />;
20
+ }
21
+ ```
22
+
23
+ [Documentation index](https://github.com/deksden-com/dd-cpa-kit/blob/main/docs/index.md): installation, host API and safe runtime updates. React and React DOM 19.2.x are required; use the matching core peer version.
24
+
25
+ Memoize the scoped client for the lifetime of the instance. `initialPanel` provides internal navigation; `panel` + `onNavigate` allows a host router to control navigation. `initialOperationId` resumes a durable job after reload; `onOperationChange` lets the host put its safe operation ID in the URL. The package never stores credentials, files or management/inference keys in browser storage. The host must authenticate and authorize every read/mutation, enforce same-origin/CSRF and execute the accepted operation in a durable worker.
26
+
27
+ `initialCandidateId` can reopen an owner-authorized import preview, including an alias resolved by the host API. Apply/cancel always use the canonical `candidateId` from the validated preview response, never the URL alias. Capability `modes` and `overwrite` govern the available import controls; absent policy fails closed. PG merge-only pools cannot select full replacement or overwrite existing identities.
28
+
29
+ Individual `CpaOverviewPanel`, `CpaCredentialsPanel`, `CpaModelsPanel`, `CpaVersionsPanel`, and `CpaOperationsPanel` use the same props; when controlled the host must handle `onNavigate`. `locale` supports Russian and English; `labels` overrides phrasing without a mandatory i18n dependency. `probeProtocols` and `reasoningProfiles` must match host-approved probe policies. A probe always requires explicit confirmation. Missing capabilities show disabled actions with reason codes; management unavailability does not imply inference failure.
30
+
31
+ Import is one ZIP/JSON control, with an optional native directory picker inside a disclosure. Unsupported directory pickers fall back to ZIP. Preview is explicit, and deletions must be confirmed. Polling is 1 second active/5 seconds hidden, transient backoff capped at 30 seconds, stopped at terminal or recovery state and aborted on unmount. The redacted operation outcome is displayed inline, not only as a transient toast.
32
+
33
+ All styles are scoped under `.cpa-kit`, with overridable `--cpa-*` CSS custom properties. No fonts or global reset are injected. Light/dark, reduced-motion-compatible static UI, 320px layout and keyboard focus are supported. Keys and upstream error bodies must not be passed as props.
@@ -0,0 +1,27 @@
1
+ import type { CpaAdminClient } from "@deksden-com/cpa-kit/client";
2
+ import { type CpaLabels, type CpaLocale } from "./labels.js";
3
+ export type { CpaLocale, CpaLabels } from "./labels.js";
4
+ export type CpaPanel = "overview" | "credentials" | "models" | "versions" | "operations";
5
+ export interface CpaConsoleProps {
6
+ client: CpaAdminClient;
7
+ instanceId: string;
8
+ locale?: CpaLocale;
9
+ labels?: Partial<CpaLabels>;
10
+ initialPanel?: CpaPanel;
11
+ panel?: CpaPanel;
12
+ initialOperationId?: string;
13
+ initialCandidateId?: string;
14
+ onOperationChange?: (id: string) => void;
15
+ onNavigate?: (panel: CpaPanel) => void;
16
+ onBack?: () => void;
17
+ className?: string;
18
+ theme?: "light" | "dark";
19
+ probeProtocols?: string[];
20
+ reasoningProfiles?: (string | null)[];
21
+ }
22
+ export declare function CpaConsole(props: CpaConsoleProps): import("react").JSX.Element;
23
+ export declare const CpaOverviewPanel: (props: CpaConsoleProps) => import("react").JSX.Element;
24
+ export declare const CpaCredentialsPanel: (props: CpaConsoleProps) => import("react").JSX.Element;
25
+ export declare const CpaModelsPanel: (props: CpaConsoleProps) => import("react").JSX.Element;
26
+ export declare const CpaVersionsPanel: (props: CpaConsoleProps) => import("react").JSX.Element;
27
+ export declare const CpaOperationsPanel: (props: CpaConsoleProps) => import("react").JSX.Element;
package/dist/index.js ADDED
@@ -0,0 +1,402 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useId, useRef, useState, } from "react";
4
+ import { labels } from "./labels.js";
5
+ const panels = [
6
+ "overview",
7
+ "credentials",
8
+ "models",
9
+ "versions",
10
+ "operations",
11
+ ];
12
+ const active = (o) => o.state === "queued" || o.state === "running";
13
+ const key = () => `ui-${globalThis.crypto.randomUUID()}`;
14
+ const errorCode = (error) => typeof error === "object" && error !== null && "detail" in error
15
+ ? String(error.detail.code ?? "INTERNAL")
16
+ : "INTERNAL";
17
+ function Button({ children, onClick, disabled = false, reason, className = "", type = "button", }) {
18
+ return (_jsxs("span", { children: [_jsx("button", { type: type, className: `cpa-button ${className}`, disabled: disabled, onClick: onClick, title: reason, children: children }), disabled && reason ? (_jsxs("span", { className: "cpa-disabled", children: [" \u00B7 ", reason] })) : null] }));
19
+ }
20
+ export function CpaConsole(props) {
21
+ const { client, instanceId } = props;
22
+ const l = { ...labels[props.locale ?? "ru"], ...props.labels };
23
+ const readable = (value) => {
24
+ const label = value === "running"
25
+ ? "stateRunning"
26
+ : value === "usable"
27
+ ? "usableState"
28
+ : value === "preview"
29
+ ? "previewPhase"
30
+ : value;
31
+ return l[label] ?? value;
32
+ };
33
+ const [localPanel, setPanel] = useState(props.initialPanel ?? "overview");
34
+ const panel = props.panel ?? localPanel;
35
+ const [status, setStatus] = useState(null), [credentials, setCredentials] = useState([]), [models, setModels] = useState(null), [releases, setReleases] = useState([]), [operations, setOperations] = useState([]);
36
+ const [nextCursor, setNextCursor] = useState(null), [operationId, setOperationId] = useState(props.initialOperationId ?? ""), [operation, setOperation] = useState(null), [error, setError] = useState(null), [loading, setLoading] = useState(false), [pending, setPending] = useState(false);
37
+ const [draftOperationId, setDraftOperationId] = useState(props.initialOperationId ?? "");
38
+ const [files, setFiles] = useState([]), [candidate, setCandidate] = useState(props.initialCandidateId ?? null), [preview, setPreview] = useState(null), [mode, setMode] = useState("merge"), [overwrite, setOverwrite] = useState([]), [confirmedRemovals, setConfirmedRemovals] = useState(false);
39
+ const [confirmation, setConfirmation] = useState(null);
40
+ const [protocol, setProtocol] = useState(props.probeProtocols?.[0] ?? "chat-completions"), [reasoning, setReasoning] = useState(props.reasoningProfiles?.[0] ?? "");
41
+ const mounted = useRef(true), mutation = useRef(null), loadController = useRef(null), inflight = useRef(false), dialog = useRef(null), retryCommand = useRef(null);
42
+ const fieldId = useId();
43
+ useEffect(() => {
44
+ mounted.current = true;
45
+ return () => {
46
+ mounted.current = false;
47
+ mutation.current?.abort();
48
+ loadController.current?.abort();
49
+ };
50
+ }, [client]);
51
+ const fail = useCallback((e) => {
52
+ if (mounted.current && !(e?.name === "AbortError"))
53
+ setError(errorCode(e));
54
+ }, []);
55
+ const navigate = (p) => {
56
+ setPanel(p);
57
+ props.onNavigate?.(p);
58
+ setConfirmation(null);
59
+ setError(null);
60
+ };
61
+ const capability = (kind) => status?.capabilities[kind];
62
+ const reason = (kind) => capability(kind)?.reasonCode ?? l.unsupported;
63
+ const can = (kind) => capability(kind)?.enabled === true &&
64
+ !pending &&
65
+ (!(operation && active(operation)) || kind === "operation.cancel");
66
+ const importCapability = capability("import.apply");
67
+ const allowedModes = importCapability?.modes ?? [];
68
+ const overwriteAllowed = importCapability?.overwrite === true;
69
+ const importModeAllowed = allowedModes.includes(mode);
70
+ useEffect(() => {
71
+ if (allowedModes.length && !allowedModes.includes(mode))
72
+ setMode(allowedModes[0]);
73
+ if (!overwriteAllowed && overwrite.length)
74
+ setOverwrite([]);
75
+ }, [allowedModes, mode, overwriteAllowed, overwrite]);
76
+ useEffect(() => {
77
+ if (confirmation) {
78
+ const previous = document.activeElement instanceof HTMLElement
79
+ ? document.activeElement
80
+ : null;
81
+ dialog.current?.focus();
82
+ return () => previous?.focus();
83
+ }
84
+ }, [confirmation]);
85
+ const load = useCallback(async () => {
86
+ loadController.current?.abort();
87
+ const controller = new AbortController();
88
+ loadController.current = controller;
89
+ setLoading(true);
90
+ try {
91
+ const next = await client.status(controller.signal);
92
+ if (controller.signal.aborted)
93
+ return;
94
+ setStatus(next);
95
+ if (panel === "credentials" &&
96
+ next.capabilities["credentials.list"]?.enabled) {
97
+ const list = await client.credentials(controller.signal);
98
+ if (!controller.signal.aborted)
99
+ setCredentials(list.items);
100
+ }
101
+ if (panel === "models" && next.capabilities["models.get"]?.enabled) {
102
+ const data = await client.models(controller.signal);
103
+ if (!controller.signal.aborted)
104
+ setModels(data);
105
+ }
106
+ if (panel === "versions" && next.capabilities["releases.list"]?.enabled) {
107
+ const data = await client.releases(controller.signal);
108
+ if (!controller.signal.aborted)
109
+ setReleases(data.items);
110
+ }
111
+ if (panel === "operations") {
112
+ const data = await client.operations({ signal: controller.signal });
113
+ if (!controller.signal.aborted) {
114
+ setOperations(data.items);
115
+ setNextCursor(data.nextCursor);
116
+ }
117
+ }
118
+ }
119
+ catch (e) {
120
+ fail(e);
121
+ }
122
+ finally {
123
+ if (!controller.signal.aborted && mounted.current)
124
+ setLoading(false);
125
+ }
126
+ }, [client, panel, fail]);
127
+ useEffect(() => {
128
+ setError(null);
129
+ void load();
130
+ return () => loadController.current?.abort();
131
+ }, [load]);
132
+ const track = (id) => {
133
+ setOperationId(id);
134
+ setDraftOperationId(id);
135
+ setOperation(null);
136
+ props.onOperationChange?.(id);
137
+ };
138
+ const command = async (kind, payload, revision) => {
139
+ if (inflight.current)
140
+ return;
141
+ inflight.current = true;
142
+ setPending(true);
143
+ setError(null);
144
+ const controller = new AbortController();
145
+ mutation.current = controller;
146
+ try {
147
+ const signature = JSON.stringify({ instanceId, kind, payload });
148
+ if (retryCommand.current?.signature !== signature)
149
+ retryCommand.current = {
150
+ signature,
151
+ key: key(),
152
+ revision: revision === undefined ? (status?.revision ?? null) : revision,
153
+ };
154
+ const envelope = {
155
+ apiVersion: 1,
156
+ instanceId,
157
+ kind,
158
+ payload,
159
+ idempotencyKey: retryCommand.current.key,
160
+ expectedRevision: retryCommand.current.revision,
161
+ };
162
+ const result = await client.command(envelope, controller.signal);
163
+ retryCommand.current = null;
164
+ if (mounted.current)
165
+ track(result.operationId);
166
+ }
167
+ catch (e) {
168
+ if (errorCode(e) !== "INTERNAL")
169
+ retryCommand.current = null;
170
+ fail(e);
171
+ }
172
+ finally {
173
+ inflight.current = false;
174
+ if (mounted.current)
175
+ setPending(false);
176
+ }
177
+ };
178
+ useEffect(() => {
179
+ if (!operationId)
180
+ return;
181
+ const controller = new AbortController();
182
+ let timer;
183
+ let failures = 0;
184
+ const poll = async () => {
185
+ try {
186
+ const data = await client.operation(operationId, controller.signal);
187
+ if (controller.signal.aborted)
188
+ return;
189
+ setOperation(data);
190
+ failures = 0;
191
+ if (active(data)) {
192
+ timer = setTimeout(poll, document.visibilityState === "hidden" ? 5000 : 1000);
193
+ }
194
+ else {
195
+ if (data.kind === "import.stage" &&
196
+ data.state === "succeeded" &&
197
+ typeof data.result?.candidateId === "string")
198
+ setCandidate(data.result.candidateId);
199
+ if (data.kind === "import.apply" && data.state === "succeeded") {
200
+ setCandidate(null);
201
+ setPreview(null);
202
+ setOverwrite([]);
203
+ }
204
+ void load();
205
+ }
206
+ }
207
+ catch (e) {
208
+ if (controller.signal.aborted)
209
+ return;
210
+ fail(e);
211
+ if (errorCode(e) === "NOT_AUTHORIZED")
212
+ return;
213
+ timer = setTimeout(poll, Math.min(30000, 1000 * 2 ** ++failures));
214
+ }
215
+ };
216
+ void poll();
217
+ return () => {
218
+ controller.abort();
219
+ if (timer)
220
+ clearTimeout(timer);
221
+ };
222
+ }, [client, operationId, fail, load]);
223
+ useEffect(() => {
224
+ if (!candidate)
225
+ return;
226
+ const controller = new AbortController();
227
+ setPreview(null);
228
+ setConfirmedRemovals(false);
229
+ void client
230
+ .preview(candidate, {
231
+ mode,
232
+ overwriteIds: overwrite,
233
+ signal: controller.signal,
234
+ })
235
+ .then((data) => {
236
+ if (!controller.signal.aborted)
237
+ setPreview(data);
238
+ })
239
+ .catch((e) => {
240
+ if (!controller.signal.aborted)
241
+ fail(e);
242
+ });
243
+ return () => controller.abort();
244
+ }, [client, candidate, mode, overwrite, fail]);
245
+ const upload = async () => {
246
+ if (!files.length) {
247
+ setError("INVALID_INPUT");
248
+ return;
249
+ }
250
+ if (inflight.current)
251
+ return;
252
+ inflight.current = true;
253
+ setPending(true);
254
+ setError(null);
255
+ const controller = new AbortController();
256
+ mutation.current = controller;
257
+ try {
258
+ const data = await client.upload(files, controller.signal);
259
+ setFiles([]);
260
+ const result = await client.command({
261
+ apiVersion: 1,
262
+ instanceId,
263
+ kind: "import.stage",
264
+ payload: { uploadId: data.uploadId, format: "auto" },
265
+ idempotencyKey: key(),
266
+ expectedRevision: null,
267
+ }, controller.signal);
268
+ if (mounted.current)
269
+ track(result.operationId);
270
+ }
271
+ catch (e) {
272
+ fail(e);
273
+ }
274
+ finally {
275
+ inflight.current = false;
276
+ if (mounted.current)
277
+ setPending(false);
278
+ }
279
+ };
280
+ const request = (kind, payload, text) => setConfirmation({
281
+ kind,
282
+ payload,
283
+ text,
284
+ revision: status?.revision ?? null,
285
+ });
286
+ const renderOperation = () => operation ? (_jsxs("section", { className: "cpa-card cpa-operation", "aria-label": l.progress, "aria-live": "polite", children: [_jsxs("div", { className: "cpa-row", children: [_jsx("h3", { children: l[operation.state] }), _jsx("span", { className: "cpa-badge", children: operation.kind })] }), _jsxs("p", { className: "cpa-muted", children: [l.phase, ": ", readable(operation.phase), " \u00B7", " ", _jsx("span", { className: "cpa-mono", children: operation.id })] }), operation.progress.total !== null ? (_jsxs(_Fragment, { children: [_jsx("progress", { value: operation.progress.completed, max: operation.progress.total || 1 }), _jsxs("span", { children: [operation.progress.completed, " / ", operation.progress.total] })] })) : null, operation.error ? (_jsxs("p", { role: "alert", children: [operation.error.code === "UPLOAD_LIMIT" && operation.kind === "import.stage"
287
+ ? (props.locale === "en" ? "The archive exceeded a safety limit. Remove logs, databases and temporary files, then retry." : "Архив превысил ограничение безопасности. Удалите журналы, базы и временные файлы, затем повторите загрузку.")
288
+ : `${operation.error.code}: ${operation.error.message}`, operation.error.retryAt ? ` · ${operation.error.retryAt}` : ""] })) : null, operation.state === "partially_applied" ? _jsx("p", { children: l.partial }) : null, operation.state === "recovery_required" ? (_jsxs("p", { children: [l.recovery, " \u00B7 ", operation.recoveryDisposition] })) : null, active(operation) && !operation.cancelRequested ? (_jsx(Button, { disabled: !can("operation.cancel"), reason: !can("operation.cancel") ? reason("operation.cancel") : undefined, onClick: () => void command("operation.cancel", { operationId: operation.id }), children: l.cancel })) : null, operation.cancelRequested && active(operation) ? (_jsxs("p", { children: [l.cancel, "\u2026"] })) : null] })) : operationId ? (_jsxs("p", { role: "status", children: [l.loading, " \u00B7 ", _jsx("span", { className: "cpa-mono", children: operationId })] })) : null;
289
+ return (_jsxs("div", { className: `cpa-kit ${props.className ?? ""}`, "data-theme": props.theme, children: [_jsxs("nav", { className: "cpa-crumbs", "aria-label": props.locale === "en" ? "Breadcrumb" : "Хлебные крошки", children: [props.onBack ? (_jsxs(Button, { onClick: props.onBack, children: ["\u2190 ", l.back] })) : null, _jsx(Button, { onClick: () => navigate("overview"), children: "CPA" }), panel !== "overview" ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", children: "\u203A" }), _jsx("span", { "aria-current": "page", children: l[panel] })] })) : null] }), _jsxs("header", { className: "cpa-head", children: [_jsx("h2", { children: l[panel] }), _jsx(Button, { disabled: loading || pending, onClick: () => void load(), children: loading ? l.loading : l.refresh })] }), _jsx("nav", { className: "cpa-nav", "aria-label": props.locale === "en" ? "CPA sections" : "Разделы CPA", children: panels.map((p) => (_jsx("button", { className: "cpa-button", "aria-current": panel === p ? "page" : undefined, onClick: () => navigate(p), children: l[p] }, p))) }), error ? (_jsxs("div", { role: "alert", className: "cpa-notice", children: [error === "NOT_AUTHORIZED" ? l.unauthorized : l.inputError, " ", _jsx("span", { className: "cpa-mono", children: error }), _jsx(Button, { onClick: () => {
290
+ setError(null);
291
+ void load();
292
+ }, children: l.retry })] })) : null, confirmation ? (_jsx("div", { className: "cpa-modal", children: _jsxs("div", { ref: dialog, tabIndex: -1, className: "cpa-dialog", role: "alertdialog", "aria-modal": "true", "aria-label": l.confirm, "aria-describedby": `${fieldId}-confirm`, onKeyDown: (e) => {
293
+ if (e.key === "Escape") {
294
+ setConfirmation(null);
295
+ return;
296
+ }
297
+ if (e.key === "Tab") {
298
+ const nodes = dialog.current?.querySelectorAll("button:not(:disabled)");
299
+ if (!nodes?.length)
300
+ return;
301
+ const first = nodes[0], last = nodes[nodes.length - 1];
302
+ if (e.shiftKey &&
303
+ (document.activeElement === first ||
304
+ document.activeElement === dialog.current)) {
305
+ e.preventDefault();
306
+ last.focus();
307
+ }
308
+ else if (!e.shiftKey &&
309
+ (document.activeElement === last ||
310
+ document.activeElement === dialog.current)) {
311
+ e.preventDefault();
312
+ first.focus();
313
+ }
314
+ }
315
+ }, children: [_jsx("p", { id: `${fieldId}-confirm`, children: confirmation.text }), _jsxs("div", { className: "cpa-actions", children: [_jsx(Button, { className: "cpa-danger", disabled: pending, onClick: () => {
316
+ const value = confirmation;
317
+ setConfirmation(null);
318
+ void command(value.kind, value.payload, value.revision);
319
+ }, children: l.confirm }), _jsx(Button, { onClick: () => setConfirmation(null), children: l.cancel })] })] }) })) : null, _jsxs("div", { className: "cpa-panel", "aria-busy": loading || pending, children: [panel === "overview" ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "cpa-muted", children: l.safe }), _jsx("div", { className: "cpa-grid", children: [
320
+ [l.service, status?.runtime.state ?? "unknown"],
321
+ [l.management, status?.management.availability ?? "unknown"],
322
+ [
323
+ l.inference,
324
+ status?.inference.reachable === true
325
+ ? "reachable"
326
+ : status?.inference.reachable === false
327
+ ? "offline"
328
+ : "unknown",
329
+ ],
330
+ ].map(([title, value]) => (_jsxs("section", { className: "cpa-card", children: [_jsx("h3", { children: title }), _jsx("p", { children: readable(value) })] }, title))) }), _jsxs("section", { className: "cpa-card", children: [_jsx("h3", { children: l.version }), _jsx("p", { className: "cpa-mono", children: status?.runtime.version ?? "—" }), _jsxs("p", { className: "cpa-muted", children: [readable(status?.runtime.mode ?? "—"), " \u00B7", " ", status?.management.protocol ?? "—"] }), _jsxs(Button, { onClick: () => navigate("versions"), children: [l.versions, " \u2192"] })] }), status?.busyOperationId ? (_jsxs(Button, { onClick: () => track(status.busyOperationId), children: [l.progress, " \u2192"] })) : null] })) : null, panel === "credentials" ? (_jsxs(_Fragment, { children: [_jsxs("section", { className: "cpa-card", children: [_jsx("h3", { children: l.import }), _jsx("p", { className: "cpa-muted", children: l.uploadHint }), _jsxs("div", { className: "cpa-form", children: [_jsxs("label", { className: "cpa-field", children: [l.choose, _jsx("input", { "aria-label": l.choose, type: "file", multiple: true, accept: ".zip,.json,application/zip,application/json", disabled: !can("import.stage"), onChange: (e) => setFiles(Array.from(e.currentTarget.files ?? [])) })] }), _jsxs("details", { children: [_jsx("summary", { children: l.directory }), _jsx("input", { "aria-label": l.directory, type: "file", multiple: true, webkitdirectory: "", disabled: !can("import.stage"), onChange: (e) => setFiles(Array.from(e.currentTarget.files ?? [])) })] }), pending ? _jsx("p", { role: "status", "aria-live": "polite", children: props.locale === "en" ? "Uploading files and preparing verification…" : "Загружаем файлы и подготавливаем проверку…" }) : null, _jsx(Button, { disabled: !can("import.stage") || !files.length, reason: !can("import.stage") ? reason("import.stage") : undefined, onClick: () => void upload(), children: pending ? l.loading : l.preview })] })] }), candidate ? (_jsxs("section", { className: "cpa-card", children: [_jsx("h3", { children: l.preview }), _jsxs("label", { className: "cpa-field", children: [l.import, _jsxs("select", { value: mode, disabled: !can("import.apply"), "aria-describedby": allowedModes.includes("replace")
331
+ ? undefined
332
+ : `${fieldId}-mode-policy`, onChange: (e) => {
333
+ const next = e.target.value;
334
+ if (allowedModes.includes(next))
335
+ setMode(next);
336
+ }, children: [_jsx("option", { value: "merge", disabled: !allowedModes.includes("merge"), children: l.merge }), _jsx("option", { value: "replace", disabled: !allowedModes.includes("replace"), children: l.replace })] })] }), !allowedModes.includes("replace") ? (_jsx("p", { id: `${fieldId}-mode-policy`, className: "cpa-disabled", children: l.modeUnavailable })) : null, preview ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "cpa-stats", children: [[
337
+ "additions",
338
+ "preserved",
339
+ "replacements",
340
+ "removals",
341
+ ].map((k) => (_jsxs("div", { className: "cpa-stat", children: [_jsx("strong", { children: preview[k].length }), l[k]] }, k))), _jsxs("div", { className: "cpa-stat", children: [_jsx("strong", { children: preview.skipped }), l.skipped] })] }), Object.entries(preview.reasons).map(([r, n]) => (_jsxs("p", { className: "cpa-muted", children: [r, ": ", n] }, r))), preview.preserved.length ? (_jsxs("fieldset", { children: [_jsx("legend", { children: l.overwrite }), !overwriteAllowed ? (_jsx("p", { className: "cpa-disabled", children: l.overwriteUnavailable })) : null, preview.preserved.map((id) => (_jsxs("label", { className: "cpa-check", children: [_jsx("input", { type: "checkbox", checked: overwrite.includes(id), disabled: !overwriteAllowed || !can("import.apply"), onChange: (e) => overwriteAllowed &&
342
+ setOverwrite((old) => e.target.checked
343
+ ? [...old, id]
344
+ : old.filter((x) => x !== id)) }), _jsx("span", { className: "cpa-mono", children: id })] }, id)))] })) : null, preview.removals.length ? (_jsxs(_Fragment, { children: [_jsx("ul", { children: preview.removals.map((id) => (_jsx("li", { className: "cpa-mono", children: id }, id))) }), _jsxs("label", { className: "cpa-check", children: [_jsx("input", { type: "checkbox", checked: confirmedRemovals, onChange: (e) => setConfirmedRemovals(e.target.checked) }), l.checkRemovals] })] })) : null, _jsxs("div", { className: "cpa-actions", children: [_jsx(Button, { className: "cpa-primary", disabled: !can("import.apply") ||
345
+ !importModeAllowed ||
346
+ (!overwriteAllowed && overwrite.length > 0) ||
347
+ (preview.removals.length > 0 && !confirmedRemovals), reason: !importModeAllowed
348
+ ? l.modeUnavailable
349
+ : !can("import.apply")
350
+ ? reason("import.apply")
351
+ : undefined, onClick: () => void command("import.apply", {
352
+ candidateId: preview.candidateId,
353
+ previewRevision: preview.revision,
354
+ mode,
355
+ overwriteIds: overwrite,
356
+ confirmedRemovals: preview.removals,
357
+ }, preview.revision), children: l.apply }), _jsx(Button, { disabled: !can("import.cancel"), onClick: () => {
358
+ void command("import.cancel", {
359
+ candidateId: preview.candidateId,
360
+ });
361
+ setCandidate(null);
362
+ setPreview(null);
363
+ }, children: l.cancel })] })] })) : (_jsx("p", { children: l.loading }))] })) : null, _jsxs("section", { className: "cpa-card", children: [_jsx("h3", { children: l.credentials }), !capability("credentials.list")?.enabled ? (_jsx("p", { children: reason("credentials.list") })) : credentials.length ? (credentials.map((c) => (_jsxs("div", { className: "cpa-row", children: [_jsxs("div", { children: [_jsx("strong", { children: c.provider }), _jsx("p", { className: "cpa-mono", children: c.fingerprint }), _jsx("span", { className: "cpa-badge", children: readable(c.usability) })] }), _jsx(Button, { className: "cpa-danger", disabled: !can("credential.delete") || credentials.length <= 1, reason: credentials.length <= 1
364
+ ? l.last
365
+ : !can("credential.delete")
366
+ ? reason("credential.delete")
367
+ : undefined, onClick: () => request("credential.delete", { credentialId: c.id, confirmed: true }, l.confirmHint), children: l.remove })] }, c.id)))) : (_jsx("p", { className: "cpa-muted", children: l.empty }))] })] })) : null, panel === "models" ? (_jsxs("section", { className: "cpa-card", children: [_jsxs("div", { className: "cpa-row", children: [_jsx("h3", { children: l.models }), _jsx(Button, { disabled: !can("catalog.refresh"), reason: !can("catalog.refresh")
368
+ ? reason("catalog.refresh")
369
+ : undefined, onClick: () => void command("catalog.refresh", {}), children: l.refresh })] }), models?.stale ? _jsx("p", { className: "cpa-notice", children: l.stale }) : null, _jsx("p", { className: "cpa-muted", children: l.probeHint }), _jsxs("div", { className: "cpa-actions", children: [_jsxs("label", { className: "cpa-field", children: [l.protocol, _jsx("select", { value: protocol, onChange: (e) => setProtocol(e.target.value), children: (props.probeProtocols ?? ["chat-completions"]).map((p) => (_jsx("option", { children: p }, p))) })] }), _jsxs("label", { className: "cpa-field", children: [l.reasoning, _jsx("select", { value: reasoning, onChange: (e) => setReasoning(e.target.value), children: (props.reasoningProfiles ?? [null]).map((r) => (_jsx("option", { value: r ?? "", children: r ?? "—" }, r ?? ""))) })] })] }), !capability("models.get")?.enabled ? (_jsx("p", { children: reason("models.get") })) : models?.models.length ? (models.models.map((m) => (_jsxs("div", { className: "cpa-row", children: [_jsxs("div", { children: [_jsx("strong", { className: "cpa-mono", children: m.id }), _jsx("p", { className: "cpa-muted", children: m.capabilities.join(" · ") })] }), _jsx(Button, { disabled: !can("model.probe"), reason: !can("model.probe") ? reason("model.probe") : undefined, onClick: () => request("model.probe", {
370
+ modelId: m.id,
371
+ protocol,
372
+ reasoningProfile: reasoning || null,
373
+ }, l.probeHint), children: l.probe })] }, m.id)))) : (_jsx("p", { children: l.empty }))] })) : null, panel === "versions" ? (_jsxs("section", { className: "cpa-card", children: [_jsxs("div", { className: "cpa-row", children: [_jsx("h3", { children: l.versions }), _jsx(Button, { disabled: !can("release.discover"), reason: !can("release.discover")
374
+ ? reason("release.discover")
375
+ : undefined, onClick: () => void command("release.discover", {}), children: l.discover })] }), !capability("releases.list")?.enabled ? (_jsx("p", { children: reason("releases.list") })) : releases.length ? (releases.map((r) => (_jsxs("div", { className: "cpa-row", children: [_jsxs("div", { children: [_jsxs("strong", { className: "cpa-mono", children: ["v", r.version] }), " ", r.active ? (_jsx("span", { className: "cpa-badge", children: l.active })) : r.installed ? (_jsx("span", { className: "cpa-badge", children: l.installed })) : null] }), _jsx("div", { className: "cpa-actions", children: !r.installed ? (_jsx(Button, { disabled: !can("release.install"), reason: !can("release.install")
376
+ ? reason("release.install")
377
+ : undefined, onClick: () => void command("release.install", { releaseId: r.id }), children: l.install })) : !r.active ? (_jsxs(_Fragment, { children: [_jsx(Button, { disabled: !can("release.activate"), reason: !can("release.activate")
378
+ ? reason("release.activate")
379
+ : undefined, onClick: () => request("release.activate", { installedReleaseId: r.id, confirmed: true }, l.confirmHint), children: l.activate }), _jsx(Button, { className: "cpa-danger", disabled: !can("release.delete") || r.lastKnownGood, reason: r.lastKnownGood
380
+ ? l.recovery
381
+ : !can("release.delete")
382
+ ? reason("release.delete")
383
+ : undefined, onClick: () => request("release.delete", { installedReleaseId: r.id, confirmed: true }, l.confirmHint), children: l.remove }), r.lastKnownGood ? (_jsx(Button, { disabled: !can("release.rollback"), onClick: () => request("release.rollback", {
384
+ lastKnownGoodReleaseId: r.id,
385
+ confirmed: true,
386
+ }, l.confirmHint), children: l.rollback })) : null] })) : null })] }, r.id)))) : (_jsx("p", { children: l.empty }))] })) : null, panel === "operations" ? (_jsxs("section", { className: "cpa-card", children: [_jsx("h3", { children: l.operations }), _jsxs("label", { className: "cpa-field", children: [l.operationId, _jsx("input", { value: draftOperationId, onChange: (e) => setDraftOperationId(e.target.value), maxLength: 128, pattern: "[A-Za-z0-9_.:-]+" })] }), _jsx(Button, { disabled: !/^[A-Za-z0-9_.:-]{1,128}$/.test(draftOperationId), onClick: () => track(draftOperationId), children: l.resume }), operations.length ? (operations.map((o) => (_jsxs("div", { className: "cpa-row", children: [_jsxs("div", { children: [_jsx("strong", { children: o.kind }), _jsxs("p", { className: "cpa-muted", children: [l[o.state], " \u00B7 ", o.createdAt] })] }), _jsx(Button, { onClick: () => track(o.id), children: l.resume })] }, o.id)))) : (_jsx("p", { children: l.empty })), nextCursor ? (_jsxs(Button, { disabled: pending, onClick: () => {
387
+ setPending(true);
388
+ void client
389
+ .operations({ cursor: nextCursor })
390
+ .then((data) => {
391
+ setOperations((old) => [...old, ...data.items]);
392
+ setNextCursor(data.nextCursor);
393
+ })
394
+ .catch(fail)
395
+ .finally(() => setPending(false));
396
+ }, children: [l.operations, " \u2192"] })) : null] })) : null, renderOperation()] })] }));
397
+ }
398
+ export const CpaOverviewPanel = (props) => (_jsx(CpaConsole, { ...props, panel: "overview" }));
399
+ export const CpaCredentialsPanel = (props) => (_jsx(CpaConsole, { ...props, panel: "credentials" }));
400
+ export const CpaModelsPanel = (props) => (_jsx(CpaConsole, { ...props, panel: "models" }));
401
+ export const CpaVersionsPanel = (props) => (_jsx(CpaConsole, { ...props, panel: "versions" }));
402
+ export const CpaOperationsPanel = (props) => (_jsx(CpaConsole, { ...props, panel: "operations" }));
@@ -0,0 +1,188 @@
1
+ export declare const labels: {
2
+ readonly ru: {
3
+ readonly overview: "Обзор";
4
+ readonly credentials: "Авторизации";
5
+ readonly models: "Модели";
6
+ readonly versions: "Версии ядра";
7
+ readonly operations: "Операции";
8
+ readonly back: "Назад";
9
+ readonly refresh: "Обновить";
10
+ readonly loading: "Загрузка…";
11
+ readonly retry: "Повторить";
12
+ readonly empty: "Пока нет данных.";
13
+ readonly offline: "Недоступно";
14
+ readonly service: "Сервис";
15
+ readonly management: "Управление";
16
+ readonly inference: "Генерация";
17
+ readonly version: "Версия";
18
+ readonly status: "Состояние";
19
+ readonly import: "Импорт авторизаций";
20
+ readonly choose: "Выбрать ZIP или JSON";
21
+ readonly directory: "Выбрать папку";
22
+ readonly uploadHint: "Формат определим по содержимому. Служебные файлы пропустим; временную загрузку удалим после разбора.";
23
+ readonly preview: "Проверить загрузку";
24
+ readonly merge: "Дополнить пул";
25
+ readonly replace: "Заменить весь пул";
26
+ readonly additions: "Добавить";
27
+ readonly preserved: "Сохранить";
28
+ readonly replacements: "Заменить";
29
+ readonly removals: "Удалить";
30
+ readonly skipped: "Пропустить";
31
+ readonly apply: "Подтвердить и применить";
32
+ readonly cancel: "Отменить";
33
+ readonly remove: "Удалить";
34
+ readonly confirm: "Подтвердить";
35
+ readonly confirmHint: "Действие изменит активную конфигурацию. Продолжить?";
36
+ readonly active: "Активна";
37
+ readonly installed: "Установлена";
38
+ readonly install: "Установить";
39
+ readonly activate: "Активировать";
40
+ readonly rollback: "Вернуть предыдущую";
41
+ readonly discover: "Проверить обновления";
42
+ readonly probe: "Проверить модель";
43
+ readonly probeHint: "Проверка делает короткий запрос и может расходовать квоту.";
44
+ readonly protocol: "Протокол";
45
+ readonly reasoning: "Рассуждение";
46
+ readonly stale: "Устаревший каталог — показаны последние успешные данные.";
47
+ readonly fingerprint: "Отпечаток";
48
+ readonly provider: "Провайдер";
49
+ readonly usable: "Доступность";
50
+ readonly last: "Последнюю рабочую авторизацию удалить нельзя.";
51
+ readonly safe: "Здесь только безопасные статусы и отпечатки. Ключи и токены не отображаются.";
52
+ readonly progress: "Ход операции";
53
+ readonly resume: "Открыть операцию";
54
+ readonly terminal: "Операция завершена";
55
+ readonly phase: "Этап";
56
+ readonly recovery: "Требуется восстановление";
57
+ readonly partial: "Часть изменений применена. Проверьте фактическое состояние перед новой попыткой.";
58
+ readonly overwrite: "Заменить совпадающие авторизации";
59
+ readonly modeUnavailable: "Замена всего пула недоступна для текущего хранилища.";
60
+ readonly overwriteUnavailable: "Совпадающие серверные авторизации сохраняются: перезапись недоступна для текущего хранилища.";
61
+ readonly checkRemovals: "Я подтверждаю удаление перечисленных авторизаций";
62
+ readonly noFiles: "Выберите файлы.";
63
+ readonly unauthorized: "Нет доступа или сеанс завершён. Войдите в приложение повторно.";
64
+ readonly unsupported: "Действие недоступно";
65
+ readonly operationId: "ID операции";
66
+ readonly inputError: "Не удалось выполнить действие. Повторите загрузку состояния.";
67
+ readonly succeeded: "Завершено";
68
+ readonly queued: "В очереди";
69
+ readonly running: "Выполняется";
70
+ readonly failed: "Ошибка";
71
+ readonly cancelled: "Отменено";
72
+ readonly partially_applied: "Частично применено";
73
+ readonly recovery_required: "Требуется восстановление";
74
+ readonly stateRunning: "Запущен";
75
+ readonly stopped: "Остановлен";
76
+ readonly unknown: "Не проверено";
77
+ readonly available: "Доступно";
78
+ readonly unavailable: "Недоступно";
79
+ readonly reachable: "Доступна";
80
+ readonly managed: "Управляемый сервис";
81
+ readonly remote: "Внешний сервис";
82
+ readonly usableState: "Рабочая";
83
+ readonly disabled: "Отключена";
84
+ readonly validate: "Проверка данных";
85
+ readonly previewPhase: "Предварительный просмотр";
86
+ readonly drain: "Ожидание текущих запросов";
87
+ readonly prepare: "Подготовка";
88
+ readonly switch: "Переключение";
89
+ readonly verify: "Проверка результата";
90
+ readonly commit: "Сохранение результата";
91
+ readonly cleanup: "Очистка";
92
+ };
93
+ readonly en: {
94
+ readonly overview: "Overview";
95
+ readonly credentials: "Authorizations";
96
+ readonly models: "Models";
97
+ readonly versions: "Core versions";
98
+ readonly operations: "Operations";
99
+ readonly back: "Back";
100
+ readonly refresh: "Refresh";
101
+ readonly loading: "Loading…";
102
+ readonly retry: "Retry";
103
+ readonly empty: "No data yet.";
104
+ readonly offline: "Unavailable";
105
+ readonly service: "Service";
106
+ readonly management: "Management";
107
+ readonly inference: "Generation";
108
+ readonly version: "Version";
109
+ readonly status: "State";
110
+ readonly import: "Import authorizations";
111
+ readonly choose: "Choose ZIP or JSON";
112
+ readonly directory: "Choose folder";
113
+ readonly uploadHint: "The content determines the format. Unrelated files are skipped; temporary uploads are removed after parsing.";
114
+ readonly preview: "Validate upload";
115
+ readonly merge: "Merge pool";
116
+ readonly replace: "Replace entire pool";
117
+ readonly additions: "Add";
118
+ readonly preserved: "Preserve";
119
+ readonly replacements: "Replace";
120
+ readonly removals: "Remove";
121
+ readonly skipped: "Skip";
122
+ readonly apply: "Confirm and apply";
123
+ readonly cancel: "Cancel";
124
+ readonly remove: "Delete";
125
+ readonly confirm: "Confirm";
126
+ readonly confirmHint: "This action changes the active configuration. Continue?";
127
+ readonly active: "Active";
128
+ readonly installed: "Installed";
129
+ readonly install: "Install";
130
+ readonly activate: "Activate";
131
+ readonly rollback: "Restore previous";
132
+ readonly discover: "Check for updates";
133
+ readonly probe: "Probe model";
134
+ readonly probeHint: "A probe makes a short request and may consume quota.";
135
+ readonly protocol: "Protocol";
136
+ readonly reasoning: "Reasoning";
137
+ readonly stale: "Stale catalog — showing the last successful data.";
138
+ readonly fingerprint: "Fingerprint";
139
+ readonly provider: "Provider";
140
+ readonly usable: "Usability";
141
+ readonly last: "The last usable authorization cannot be deleted.";
142
+ readonly safe: "Only safe statuses and fingerprints are shown. Keys and tokens are never displayed.";
143
+ readonly progress: "Operation progress";
144
+ readonly resume: "Open operation";
145
+ readonly terminal: "Operation finished";
146
+ readonly phase: "Phase";
147
+ readonly recovery: "Recovery required";
148
+ readonly partial: "Some changes were applied. Check the actual state before retrying.";
149
+ readonly overwrite: "Replace matching authorizations";
150
+ readonly modeUnavailable: "Pool replacement is unavailable for this storage.";
151
+ readonly overwriteUnavailable: "Matching server authorizations are preserved: this storage does not support overwriting them.";
152
+ readonly checkRemovals: "I confirm deletion of the listed authorizations";
153
+ readonly noFiles: "Choose files.";
154
+ readonly unauthorized: "Access denied or session expired. Sign in to the application again.";
155
+ readonly unsupported: "Action unavailable";
156
+ readonly operationId: "Operation ID";
157
+ readonly inputError: "The action could not be completed. Reload the current state.";
158
+ readonly succeeded: "Completed";
159
+ readonly queued: "Queued";
160
+ readonly running: "Running";
161
+ readonly failed: "Failed";
162
+ readonly cancelled: "Cancelled";
163
+ readonly partially_applied: "Partially applied";
164
+ readonly recovery_required: "Recovery required";
165
+ readonly stateRunning: "running";
166
+ readonly stopped: "stopped";
167
+ readonly unknown: "unknown";
168
+ readonly available: "available";
169
+ readonly unavailable: "unavailable";
170
+ readonly reachable: "reachable";
171
+ readonly managed: "Managed service";
172
+ readonly remote: "Remote service";
173
+ readonly usableState: "Usable";
174
+ readonly disabled: "Disabled";
175
+ readonly validate: "Validate data";
176
+ readonly previewPhase: "Preview";
177
+ readonly drain: "Drain active requests";
178
+ readonly prepare: "Prepare";
179
+ readonly switch: "Switch";
180
+ readonly verify: "Verify result";
181
+ readonly commit: "Save result";
182
+ readonly cleanup: "Clean up";
183
+ };
184
+ };
185
+ export type CpaLocale = keyof typeof labels;
186
+ export type CpaLabels = {
187
+ [K in keyof typeof labels.en]: string;
188
+ };
package/dist/labels.js ADDED
@@ -0,0 +1,184 @@
1
+ export const labels = {
2
+ ru: {
3
+ overview: "Обзор",
4
+ credentials: "Авторизации",
5
+ models: "Модели",
6
+ versions: "Версии ядра",
7
+ operations: "Операции",
8
+ back: "Назад",
9
+ refresh: "Обновить",
10
+ loading: "Загрузка…",
11
+ retry: "Повторить",
12
+ empty: "Пока нет данных.",
13
+ offline: "Недоступно",
14
+ service: "Сервис",
15
+ management: "Управление",
16
+ inference: "Генерация",
17
+ version: "Версия",
18
+ status: "Состояние",
19
+ import: "Импорт авторизаций",
20
+ choose: "Выбрать ZIP или JSON",
21
+ directory: "Выбрать папку",
22
+ uploadHint: "Формат определим по содержимому. Служебные файлы пропустим; временную загрузку удалим после разбора.",
23
+ preview: "Проверить загрузку",
24
+ merge: "Дополнить пул",
25
+ replace: "Заменить весь пул",
26
+ additions: "Добавить",
27
+ preserved: "Сохранить",
28
+ replacements: "Заменить",
29
+ removals: "Удалить",
30
+ skipped: "Пропустить",
31
+ apply: "Подтвердить и применить",
32
+ cancel: "Отменить",
33
+ remove: "Удалить",
34
+ confirm: "Подтвердить",
35
+ confirmHint: "Действие изменит активную конфигурацию. Продолжить?",
36
+ active: "Активна",
37
+ installed: "Установлена",
38
+ install: "Установить",
39
+ activate: "Активировать",
40
+ rollback: "Вернуть предыдущую",
41
+ discover: "Проверить обновления",
42
+ probe: "Проверить модель",
43
+ probeHint: "Проверка делает короткий запрос и может расходовать квоту.",
44
+ protocol: "Протокол",
45
+ reasoning: "Рассуждение",
46
+ stale: "Устаревший каталог — показаны последние успешные данные.",
47
+ fingerprint: "Отпечаток",
48
+ provider: "Провайдер",
49
+ usable: "Доступность",
50
+ last: "Последнюю рабочую авторизацию удалить нельзя.",
51
+ safe: "Здесь только безопасные статусы и отпечатки. Ключи и токены не отображаются.",
52
+ progress: "Ход операции",
53
+ resume: "Открыть операцию",
54
+ terminal: "Операция завершена",
55
+ phase: "Этап",
56
+ recovery: "Требуется восстановление",
57
+ partial: "Часть изменений применена. Проверьте фактическое состояние перед новой попыткой.",
58
+ overwrite: "Заменить совпадающие авторизации",
59
+ modeUnavailable: "Замена всего пула недоступна для текущего хранилища.",
60
+ overwriteUnavailable: "Совпадающие серверные авторизации сохраняются: перезапись недоступна для текущего хранилища.",
61
+ checkRemovals: "Я подтверждаю удаление перечисленных авторизаций",
62
+ noFiles: "Выберите файлы.",
63
+ unauthorized: "Нет доступа или сеанс завершён. Войдите в приложение повторно.",
64
+ unsupported: "Действие недоступно",
65
+ operationId: "ID операции",
66
+ inputError: "Не удалось выполнить действие. Повторите загрузку состояния.",
67
+ succeeded: "Завершено",
68
+ queued: "В очереди",
69
+ running: "Выполняется",
70
+ failed: "Ошибка",
71
+ cancelled: "Отменено",
72
+ partially_applied: "Частично применено",
73
+ recovery_required: "Требуется восстановление",
74
+ stateRunning: "Запущен",
75
+ stopped: "Остановлен",
76
+ unknown: "Не проверено",
77
+ available: "Доступно",
78
+ unavailable: "Недоступно",
79
+ reachable: "Доступна",
80
+ managed: "Управляемый сервис",
81
+ remote: "Внешний сервис",
82
+ usableState: "Рабочая",
83
+ disabled: "Отключена",
84
+ validate: "Проверка данных",
85
+ previewPhase: "Предварительный просмотр",
86
+ drain: "Ожидание текущих запросов",
87
+ prepare: "Подготовка",
88
+ switch: "Переключение",
89
+ verify: "Проверка результата",
90
+ commit: "Сохранение результата",
91
+ cleanup: "Очистка",
92
+ },
93
+ en: {
94
+ overview: "Overview",
95
+ credentials: "Authorizations",
96
+ models: "Models",
97
+ versions: "Core versions",
98
+ operations: "Operations",
99
+ back: "Back",
100
+ refresh: "Refresh",
101
+ loading: "Loading…",
102
+ retry: "Retry",
103
+ empty: "No data yet.",
104
+ offline: "Unavailable",
105
+ service: "Service",
106
+ management: "Management",
107
+ inference: "Generation",
108
+ version: "Version",
109
+ status: "State",
110
+ import: "Import authorizations",
111
+ choose: "Choose ZIP or JSON",
112
+ directory: "Choose folder",
113
+ uploadHint: "The content determines the format. Unrelated files are skipped; temporary uploads are removed after parsing.",
114
+ preview: "Validate upload",
115
+ merge: "Merge pool",
116
+ replace: "Replace entire pool",
117
+ additions: "Add",
118
+ preserved: "Preserve",
119
+ replacements: "Replace",
120
+ removals: "Remove",
121
+ skipped: "Skip",
122
+ apply: "Confirm and apply",
123
+ cancel: "Cancel",
124
+ remove: "Delete",
125
+ confirm: "Confirm",
126
+ confirmHint: "This action changes the active configuration. Continue?",
127
+ active: "Active",
128
+ installed: "Installed",
129
+ install: "Install",
130
+ activate: "Activate",
131
+ rollback: "Restore previous",
132
+ discover: "Check for updates",
133
+ probe: "Probe model",
134
+ probeHint: "A probe makes a short request and may consume quota.",
135
+ protocol: "Protocol",
136
+ reasoning: "Reasoning",
137
+ stale: "Stale catalog — showing the last successful data.",
138
+ fingerprint: "Fingerprint",
139
+ provider: "Provider",
140
+ usable: "Usability",
141
+ last: "The last usable authorization cannot be deleted.",
142
+ safe: "Only safe statuses and fingerprints are shown. Keys and tokens are never displayed.",
143
+ progress: "Operation progress",
144
+ resume: "Open operation",
145
+ terminal: "Operation finished",
146
+ phase: "Phase",
147
+ recovery: "Recovery required",
148
+ partial: "Some changes were applied. Check the actual state before retrying.",
149
+ overwrite: "Replace matching authorizations",
150
+ modeUnavailable: "Pool replacement is unavailable for this storage.",
151
+ overwriteUnavailable: "Matching server authorizations are preserved: this storage does not support overwriting them.",
152
+ checkRemovals: "I confirm deletion of the listed authorizations",
153
+ noFiles: "Choose files.",
154
+ unauthorized: "Access denied or session expired. Sign in to the application again.",
155
+ unsupported: "Action unavailable",
156
+ operationId: "Operation ID",
157
+ inputError: "The action could not be completed. Reload the current state.",
158
+ succeeded: "Completed",
159
+ queued: "Queued",
160
+ running: "Running",
161
+ failed: "Failed",
162
+ cancelled: "Cancelled",
163
+ partially_applied: "Partially applied",
164
+ recovery_required: "Recovery required",
165
+ stateRunning: "running",
166
+ stopped: "stopped",
167
+ unknown: "unknown",
168
+ available: "available",
169
+ unavailable: "unavailable",
170
+ reachable: "reachable",
171
+ managed: "Managed service",
172
+ remote: "Remote service",
173
+ usableState: "Usable",
174
+ disabled: "Disabled",
175
+ validate: "Validate data",
176
+ previewPhase: "Preview",
177
+ drain: "Drain active requests",
178
+ prepare: "Prepare",
179
+ switch: "Switch",
180
+ verify: "Verify result",
181
+ commit: "Save result",
182
+ cleanup: "Clean up",
183
+ },
184
+ };
@@ -0,0 +1,251 @@
1
+ .cpa-kit {
2
+ --cpa-bg: #fff;
3
+ --cpa-surface: #f6f8fb;
4
+ --cpa-text: #172337;
5
+ --cpa-muted: #617089;
6
+ --cpa-border: #dbe2ec;
7
+ --cpa-accent: #3566a8;
8
+ --cpa-danger: #b53543;
9
+ --cpa-focus: #4285d4;
10
+ box-sizing: border-box;
11
+ color: var(--cpa-text);
12
+ background: var(--cpa-bg);
13
+ font: inherit;
14
+ line-height: 1.5;
15
+ max-width: 100%;
16
+ color-scheme: light;
17
+ }
18
+ .cpa-kit[data-theme="dark"] {
19
+ --cpa-bg: #151c28;
20
+ --cpa-surface: #1e2939;
21
+ --cpa-text: #e9eef7;
22
+ --cpa-muted: #a0afc4;
23
+ --cpa-border: #34435a;
24
+ --cpa-accent: #91bafb;
25
+ --cpa-danger: #ff9aa4;
26
+ --cpa-focus: #91bafb;
27
+ color-scheme: dark;
28
+ }
29
+ @media (prefers-color-scheme: dark) {
30
+ .cpa-kit:not([data-theme="light"]) {
31
+ --cpa-bg: #151c28;
32
+ --cpa-surface: #1e2939;
33
+ --cpa-text: #e9eef7;
34
+ --cpa-muted: #a0afc4;
35
+ --cpa-border: #34435a;
36
+ --cpa-accent: #91bafb;
37
+ --cpa-danger: #ff9aa4;
38
+ --cpa-focus: #91bafb;
39
+ color-scheme: dark;
40
+ }
41
+ }
42
+ .cpa-kit * {
43
+ box-sizing: border-box;
44
+ }
45
+ .cpa-kit h2,
46
+ .cpa-kit h3,
47
+ .cpa-kit p {
48
+ margin: 0;
49
+ }
50
+ .cpa-kit h2 {
51
+ font-size: 1.35rem;
52
+ font-weight: 650;
53
+ letter-spacing: -0.025em;
54
+ }
55
+ .cpa-kit h3 {
56
+ font-size: 1rem;
57
+ font-weight: 650;
58
+ }
59
+ .cpa-kit .cpa-head,
60
+ .cpa-kit .cpa-row,
61
+ .cpa-kit .cpa-actions,
62
+ .cpa-kit .cpa-nav {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 0.65rem;
66
+ flex-wrap: wrap;
67
+ }
68
+ .cpa-kit .cpa-head {
69
+ justify-content: space-between;
70
+ margin-bottom: 1rem;
71
+ }
72
+ .cpa-kit .cpa-nav {
73
+ margin-bottom: 1rem;
74
+ }
75
+ .cpa-kit .cpa-nav button[aria-current="page"] {
76
+ color: var(--cpa-accent);
77
+ background: var(--cpa-surface);
78
+ border-color: var(--cpa-border);
79
+ }
80
+ .cpa-kit .cpa-panel {
81
+ display: grid;
82
+ gap: 1rem;
83
+ }
84
+ .cpa-kit .cpa-card {
85
+ padding: 1.1rem;
86
+ border: 1px solid var(--cpa-border);
87
+ border-radius: 0.65rem;
88
+ background: var(--cpa-bg);
89
+ display: grid;
90
+ gap: 0.8rem;
91
+ }
92
+ .cpa-kit .cpa-grid {
93
+ display: grid;
94
+ grid-template-columns: repeat(3, minmax(0, 1fr));
95
+ gap: 0.85rem;
96
+ }
97
+ .cpa-kit .cpa-muted {
98
+ font-size: 0.875rem;
99
+ color: var(--cpa-muted);
100
+ }
101
+ .cpa-kit .cpa-mono {
102
+ font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
103
+ font-size: 0.875rem;
104
+ overflow-wrap: anywhere;
105
+ }
106
+ .cpa-kit .cpa-row {
107
+ justify-content: space-between;
108
+ padding: 0.8rem 0;
109
+ border-top: 1px solid var(--cpa-border);
110
+ }
111
+ .cpa-kit .cpa-row:first-child {
112
+ border-top: 0;
113
+ }
114
+ .cpa-kit .cpa-button {
115
+ font: inherit;
116
+ font-size: 0.875rem;
117
+ font-weight: 550;
118
+ border: 1px solid var(--cpa-border);
119
+ border-radius: 0.45rem;
120
+ background: var(--cpa-bg);
121
+ color: var(--cpa-text);
122
+ padding: 0.42rem 0.75rem;
123
+ cursor: pointer;
124
+ min-height: 2.2rem;
125
+ }
126
+ .cpa-kit .cpa-button:hover:not(:disabled) {
127
+ background: var(--cpa-surface);
128
+ }
129
+ .cpa-kit .cpa-button:disabled {
130
+ opacity: 0.55;
131
+ cursor: not-allowed;
132
+ }
133
+ .cpa-kit .cpa-danger {
134
+ color: var(--cpa-danger);
135
+ }
136
+ .cpa-kit .cpa-primary {
137
+ color: var(--cpa-accent);
138
+ border-color: var(--cpa-accent);
139
+ }
140
+ .cpa-kit .cpa-badge {
141
+ padding: 0.12rem 0.45rem;
142
+ border: 1px solid var(--cpa-border);
143
+ border-radius: 1rem;
144
+ font-size: 0.75rem;
145
+ color: var(--cpa-muted);
146
+ }
147
+ .cpa-kit .cpa-notice {
148
+ padding: 0.8rem 1rem;
149
+ border: 1px solid var(--cpa-border);
150
+ border-left: 3px solid var(--cpa-accent);
151
+ border-radius: 0.4rem;
152
+ background: var(--cpa-surface);
153
+ overflow-wrap: anywhere;
154
+ }
155
+ .cpa-kit .cpa-notice[role="alert"] {
156
+ border-left-color: var(--cpa-danger);
157
+ }
158
+ .cpa-kit .cpa-form {
159
+ display: grid;
160
+ gap: 0.85rem;
161
+ }
162
+ .cpa-kit .cpa-field {
163
+ display: grid;
164
+ gap: 0.35rem;
165
+ font-size: 0.875rem;
166
+ }
167
+ .cpa-kit input:not([type="checkbox"]),
168
+ .cpa-kit select {
169
+ font: inherit;
170
+ max-width: 100%;
171
+ color: var(--cpa-text);
172
+ background: var(--cpa-bg);
173
+ padding: 0.45rem;
174
+ border: 1px solid var(--cpa-border);
175
+ border-radius: 0.4rem;
176
+ }
177
+ .cpa-kit input[type="file"] {
178
+ width: 100%;
179
+ }
180
+ .cpa-kit .cpa-check {
181
+ display: flex;
182
+ gap: 0.45rem;
183
+ align-items: flex-start;
184
+ font-size: 0.875rem;
185
+ }
186
+ .cpa-kit :is(button, input, select):focus-visible {
187
+ outline: 3px solid var(--cpa-focus);
188
+ outline-offset: 3px;
189
+ }
190
+ .cpa-kit .cpa-crumbs {
191
+ display: flex;
192
+ gap: 0.4rem;
193
+ align-items: center;
194
+ margin-bottom: 0.7rem;
195
+ font-size: 0.875rem;
196
+ }
197
+ .cpa-kit .cpa-stats {
198
+ display: flex;
199
+ gap: 1.1rem;
200
+ flex-wrap: wrap;
201
+ }
202
+ .cpa-kit .cpa-stat strong {
203
+ font-size: 1.25rem;
204
+ display: block;
205
+ }
206
+ .cpa-kit .cpa-disabled {
207
+ font-size: 0.8rem;
208
+ color: var(--cpa-muted);
209
+ }
210
+ .cpa-kit .cpa-operation {
211
+ border-left: 3px solid var(--cpa-accent);
212
+ }
213
+ .cpa-kit progress {
214
+ width: 100%;
215
+ accent-color: var(--cpa-accent);
216
+ }
217
+ .cpa-kit .cpa-dialog {
218
+ padding: 1rem;
219
+ border: 1px solid var(--cpa-border);
220
+ border-radius: 0.6rem;
221
+ background: var(--cpa-surface);
222
+ display: grid;
223
+ gap: 0.7rem;
224
+ width: min(100%, 32rem);
225
+ max-height: 85vh;
226
+ overflow: auto;
227
+ }
228
+ .cpa-kit .cpa-modal {
229
+ position: fixed;
230
+ inset: 0;
231
+ z-index: 1000;
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ padding: 1rem;
236
+ background: rgb(8 18 32 / 55%);
237
+ }
238
+ @media (max-width: 640px) {
239
+ .cpa-kit .cpa-grid {
240
+ grid-template-columns: 1fr;
241
+ }
242
+ .cpa-kit .cpa-card {
243
+ padding: 0.85rem;
244
+ }
245
+ .cpa-kit .cpa-row {
246
+ align-items: flex-start;
247
+ }
248
+ .cpa-kit .cpa-actions {
249
+ width: 100%;
250
+ }
251
+ }
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "@deksden-com/cpa-kit-react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.4",
4
+ "private": false,
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/deksden-com/dd-cpa-kit.git",
10
+ "directory": "packages/cpa-kit-react"
11
+ },
12
+ "homepage": "https://github.com/deksden-com/dd-cpa-kit/blob/main/docs/index.md",
13
+ "files": [
14
+ "dist",
15
+ "README.md",
16
+ "CHANGELOG.md",
17
+ "NOTICE"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ },
24
+ "./styles.css": "./dist/styles.css"
25
+ },
26
+ "peerDependencies": {
27
+ "react": ">=19.2.0 <20",
28
+ "react-dom": ">=19.2.0 <20",
29
+ "@deksden-com/cpa-kit": "0.1.4"
30
+ },
31
+ "devDependencies": {
32
+ "@types/react": "^19.2.0",
33
+ "@types/react-dom": "^19.2.0",
34
+ "react": "^19.2.0",
35
+ "react-dom": "^19.2.0",
36
+ "typescript": "^5.8.3",
37
+ "vitest": "^4.0.15",
38
+ "@testing-library/react": "^16.3.0",
39
+ "jsdom": "^26.1.0",
40
+ "@deksden-com/cpa-kit": "0.1.4"
41
+ },
42
+ "publishConfig": {
43
+ "registry": "https://registry.npmjs.org/",
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc -p tsconfig.json && cp src/styles.css dist/styles.css",
48
+ "typecheck": "tsc -p tsconfig.json --noEmit",
49
+ "test": "vitest run"
50
+ }
6
51
  }