@lmzhen/dsh-evolution-ui 0.0.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +6 -1
  2. package/lib/client.js +3656 -0
  3. package/lib/index.js +323 -0
  4. package/lib/types/client/index.d.ts +9 -0
  5. package/lib/types/index.d.ts +12 -0
  6. package/lib/types/panels/history/client/Panel.d.ts +27 -0
  7. package/lib/types/panels/history/client/api.d.ts +142 -0
  8. package/lib/types/panels/history/client/atoms.d.ts +73 -0
  9. package/lib/types/panels/history/client/chrome.d.ts +76 -0
  10. package/lib/types/panels/history/client/document.d.ts +71 -0
  11. package/lib/types/panels/history/client/guards.d.ts +13 -0
  12. package/lib/types/panels/history/client/ids.d.ts +3 -0
  13. package/lib/types/panels/history/client/index.d.ts +11 -0
  14. package/lib/types/panels/history/client/markdown.d.ts +56 -0
  15. package/lib/types/panels/history/client/messages.d.ts +30 -0
  16. package/lib/types/panels/history/client/styles.d.ts +5 -0
  17. package/lib/types/panels/history/host.d.ts +25 -0
  18. package/lib/types/panels/history/routes.d.ts +50 -0
  19. package/lib/types/panels/history/unit-undoable.d.ts +18 -0
  20. package/lib/types/panels/settings/DiagnosticsCard.d.ts +29 -0
  21. package/lib/types/panels/settings/ParamCard.d.ts +107 -0
  22. package/lib/types/panels/settings/PendingCard.d.ts +45 -0
  23. package/lib/types/panels/settings/RowConfigCard.d.ts +30 -0
  24. package/lib/types/panels/settings/SettingsSection.d.ts +19 -0
  25. package/lib/types/panels/settings/api.d.ts +64 -0
  26. package/lib/types/panels/settings/diagnostics-state.d.ts +28 -0
  27. package/lib/types/panels/settings/generated-about.d.ts +11 -0
  28. package/lib/types/panels/settings/generated-params.d.ts +51 -0
  29. package/lib/types/panels/settings/guards.d.ts +13 -0
  30. package/lib/types/panels/settings/index.d.ts +50 -0
  31. package/lib/types/panels/settings/messages.d.ts +134 -0
  32. package/lib/types/panels/settings/pending-state.d.ts +74 -0
  33. package/lib/types/panels/settings/seam.d.ts +178 -0
  34. package/lib/types/panels/settings/source.d.ts +88 -0
  35. package/lib/types/panels/settings/styles.d.ts +32 -0
  36. package/package.json +51 -4
package/lib/index.js ADDED
@@ -0,0 +1,323 @@
1
+ import { apply as apply$1 } from "@lmzhen/dsh-evolution-merge-ui";
2
+ import { HOST_ROUTES, json, methodNotAllowed, readJsonObject, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
3
+ import { contentHash, displayBodyOf, elapsedSince, errorText, isLandmark, latestVersionAt, partitionVersions, skillRowCells, textDiffFacts, versionActionKind, versionRowCells } from "@lmzhen/dsh-evolution-core";
4
+ //#region lib/types/panels/history/unit-undoable.js
5
+ /**
6
+ * G3b: undoability is a property of the UNIT, not of one entry.
7
+ *
8
+ * Every entry of a unit shares the same live body, so "is this undoable" must answer the same for all
9
+ * of them; computing it per entry made the body row say `false` (already current) while a support row
10
+ * of the SAME transaction said `true` — two entrances to one unit in one panel.
11
+ *
12
+ * Entries without a unit id keep the per-entry answer (they are their own singleton unit).
13
+ */
14
+ /** @param members - every entry that shares the unit (content and support chains together).
15
+ * @param liveHash - hash of the live body, or null when the skill is not readable.
16
+ * @returns one flag per unit id; unknown ids fall back to the per-entry rule. */
17
+ function unitUndoableFlags(members, liveHash) {
18
+ const flags = /* @__PURE__ */ new Map();
19
+ for (const entry of members) {
20
+ if (entry.unitId === void 0) continue;
21
+ if (isLandmark(entry)) continue;
22
+ const differs = entry.hash !== liveHash;
23
+ flags.set(entry.unitId, (flags.get(entry.unitId) ?? false) || differs);
24
+ }
25
+ return flags;
26
+ }
27
+ /** @param entry - the entry being rendered. @param flags - the unit flags from {@link unitUndoableFlags}.
28
+ * @param liveHash - hash of the live body. @returns the row's `undoable` value. */
29
+ function undoableFor(entry, flags, liveHash) {
30
+ if (isLandmark(entry)) return void 0;
31
+ const unit = entry.unitId;
32
+ if (unit !== void 0) {
33
+ const flagged = flags.get(unit);
34
+ if (flagged !== void 0) return flagged;
35
+ }
36
+ return entry.hash !== liveHash;
37
+ }
38
+ //#endregion
39
+ //#region lib/types/panels/history/routes.js
40
+ /**
41
+ * The skill-history HTTP surface: six exact Fetch routes over the curator's read/write seam (five
42
+ * reads plus the one write).
43
+ *
44
+ * W6/G4: the carrier is the platform's Host connection data plane — the fence, browser auth and
45
+ * owner-only admission are applied by the platform BEFORE `fetch(request)` runs, so this module has
46
+ * no 403/401 branch. The wire envelope the browser half reads is unchanged.
47
+ *
48
+ * Layering: this module maps a route to a USE CASE and does nothing else. "Which artifact does this
49
+ * version hold" and "how do the two chains split" live in evolution-core; the only write is
50
+ * curator.undo. The panel never writes the skill tree itself.
51
+ * @module @lmzhen/dsh-evolution-ui/panels/history/routes
52
+ */
53
+ /** Route paths: the ONE declaration lives in core (W1); the client constants are generated from it. */
54
+ const SKILL_HISTORY_ROUTES = HOST_ROUTES.skillHistory;
55
+ /**
56
+ * How much of one version's body the read route hands back. A bound on the ANSWER, not on what the
57
+ * deployment may store: a long skill arrives as its head plus `truncated`, so the reader is told.
58
+ */
59
+ const MAX_BODY_CHARS = 2e4;
60
+ /**
61
+ * Build the six routes over the curator seam.
62
+ * @param services - the host services the handlers read.
63
+ * @param _audit - the registration audit (read routes record nothing; undo's decision stays the
64
+ * curator's own — W6 keeps the ledger to the approval surface).
65
+ * @returns the route list for `registerHostRoutes`.
66
+ */
67
+ function makeSkillHistoryRoutes(services, _audit) {
68
+ /** Run one handler body with the curator, or answer the family's E-302 sentence when it is absent. */
69
+ const withCurator = async (run) => {
70
+ const curator = services.getCurator();
71
+ if (curator === void 0) return json(200, {
72
+ ok: false,
73
+ code: "curator-not-mounted",
74
+ message: errorText("e-302-curator-service-not-mounted")
75
+ });
76
+ return await run(curator);
77
+ };
78
+ return [
79
+ {
80
+ path: SKILL_HISTORY_ROUTES.skills,
81
+ methods: ["GET"],
82
+ requestBody: "buffered",
83
+ fetch: (request) => {
84
+ if (request.method !== "GET") return Promise.resolve(methodNotAllowed(["GET"]));
85
+ return withCurator(async (curator) => {
86
+ const listed = await curator.skills.list();
87
+ const withHistory = [];
88
+ const now = Date.now();
89
+ for (const skill of listed) {
90
+ const versions = await curator.skills.listVersions(skill.name);
91
+ if (versions.length === 0) continue;
92
+ const lastAt = latestVersionAt(versions);
93
+ const row = {
94
+ name: skill.name,
95
+ versions: versions.length,
96
+ description: skill.description,
97
+ managed: skill.managed,
98
+ protectedBy: skill.protectedBy,
99
+ protectionUnknown: skill.protectionUnknown,
100
+ lastAt,
101
+ age: lastAt === null ? null : elapsedSince(lastAt, now)
102
+ };
103
+ withHistory.push({
104
+ ...row,
105
+ cells: skillRowCells(row)
106
+ });
107
+ }
108
+ return json(200, {
109
+ ok: true,
110
+ data: withHistory
111
+ });
112
+ });
113
+ }
114
+ },
115
+ {
116
+ path: SKILL_HISTORY_ROUTES.versions,
117
+ methods: ["GET"],
118
+ requestBody: "buffered",
119
+ fetch: (request) => {
120
+ if (request.method !== "GET") return Promise.resolve(methodNotAllowed(["GET"]));
121
+ const name = new URL(request.url).searchParams.get("name")?.trim() ?? "";
122
+ if (name === "") return Promise.resolve(json(400, {
123
+ ok: false,
124
+ code: "bad-request",
125
+ message: "缺少 name"
126
+ }));
127
+ return withCurator(async (curator) => {
128
+ const groups = partitionVersions(await curator.history(name));
129
+ const live = await curator.skills.read(name);
130
+ const liveHash = live === null ? null : contentHash(live);
131
+ const now = Date.now();
132
+ const decorate = (entry, index, chain, withDelta) => {
133
+ const previous = index === 0 ? void 0 : chain[index - 1];
134
+ return {
135
+ ...entry,
136
+ actionKind: versionActionKind(entry.action),
137
+ age: elapsedSince(entry.at, now),
138
+ ...!withDelta || previous === void 0 ? {} : { charsDelta: entry.chars - previous.chars }
139
+ };
140
+ };
141
+ const unitFlags = unitUndoableFlags([...groups.content, ...groups.support], liveHash);
142
+ return json(200, {
143
+ ok: true,
144
+ data: {
145
+ content: groups.content.map((entry, index) => {
146
+ const undoable = undoableFor(entry, unitFlags, liveHash);
147
+ const row = {
148
+ ...decorate(entry, index, groups.content, true),
149
+ ...undoable === void 0 ? {} : { undoable }
150
+ };
151
+ return {
152
+ ...row,
153
+ cells: versionRowCells(row)
154
+ };
155
+ }),
156
+ support: groups.support.map((entry, index) => {
157
+ const row = decorate(entry, index, groups.support, false);
158
+ return {
159
+ ...row,
160
+ cells: versionRowCells(row)
161
+ };
162
+ }),
163
+ liveHash
164
+ }
165
+ });
166
+ });
167
+ }
168
+ },
169
+ {
170
+ path: SKILL_HISTORY_ROUTES.diff,
171
+ methods: ["GET"],
172
+ requestBody: "buffered",
173
+ fetch: (request) => {
174
+ if (request.method !== "GET") return Promise.resolve(methodNotAllowed(["GET"]));
175
+ const params = new URL(request.url).searchParams;
176
+ const name = params.get("name")?.trim() ?? "";
177
+ const rawV = params.get("v")?.trim() ?? "";
178
+ const v = Number.parseInt(rawV, 10);
179
+ if (name === "" || !/^\d+$/.test(rawV) || !Number.isInteger(v) || v < 1) return Promise.resolve(json(400, {
180
+ ok: false,
181
+ code: "bad-request",
182
+ message: "需要 name 与正整数 v"
183
+ }));
184
+ return withCurator(async (curator) => {
185
+ const groups = partitionVersions(await curator.history(name));
186
+ const index = groups.content.findIndex((entry) => entry.v === v);
187
+ if (index === -1) return json(404, {
188
+ ok: false,
189
+ code: "not-found",
190
+ message: "该版本不在「" + name + "」的记录历史里"
191
+ });
192
+ const entry = groups.content[index];
193
+ const previous = (entry === void 0 || entry.beforeHash === void 0 ? void 0 : groups.content.find((candidate) => candidate.hash === entry.beforeHash)) ?? (index === 0 ? void 0 : groups.content[index - 1]);
194
+ const after = entry === void 0 ? null : await curator.skills.readVersion(name, v);
195
+ const before = previous === void 0 ? "" : await curator.skills.readVersion(name, previous.v);
196
+ if (after === null || before === null) return json(404, {
197
+ ok: false,
198
+ code: "not-found",
199
+ message: "该版本的存储内容读不出来"
200
+ });
201
+ const facts = textDiffFacts(before, after, "SKILL.md");
202
+ return json(200, {
203
+ ok: true,
204
+ data: {
205
+ v,
206
+ against: previous?.v ?? null,
207
+ ...facts
208
+ }
209
+ });
210
+ });
211
+ }
212
+ },
213
+ {
214
+ path: SKILL_HISTORY_ROUTES.body,
215
+ methods: ["GET"],
216
+ requestBody: "buffered",
217
+ fetch: (request) => {
218
+ if (request.method !== "GET") return Promise.resolve(methodNotAllowed(["GET"]));
219
+ const params = new URL(request.url).searchParams;
220
+ const name = params.get("name")?.trim() ?? "";
221
+ const rawV = params.get("v")?.trim() ?? "";
222
+ const v = Number.parseInt(rawV, 10);
223
+ if (name === "" || !/^\d+$/.test(rawV) || !Number.isInteger(v) || v < 1) return Promise.resolve(json(400, {
224
+ ok: false,
225
+ code: "bad-request",
226
+ message: "需要 name 与正整数 v"
227
+ }));
228
+ return withCurator(async (curator) => {
229
+ const text = await curator.skills.readVersion(name, v);
230
+ if (text === null) return json(404, {
231
+ ok: false,
232
+ code: "not-found",
233
+ message: "该版本的存储内容读不出来"
234
+ });
235
+ const truncated = text.length > MAX_BODY_CHARS;
236
+ return json(200, {
237
+ ok: true,
238
+ data: {
239
+ v,
240
+ display: displayBodyOf(truncated ? text.slice(0, MAX_BODY_CHARS) : text),
241
+ chars: text.length,
242
+ truncated
243
+ }
244
+ });
245
+ });
246
+ }
247
+ },
248
+ {
249
+ path: SKILL_HISTORY_ROUTES.undo,
250
+ methods: ["POST"],
251
+ requestBody: "buffered",
252
+ fetch: (request) => {
253
+ if (request.method !== "POST") return Promise.resolve(methodNotAllowed(["POST"]));
254
+ return (async () => {
255
+ const payload = await readJsonObject(request);
256
+ const name = typeof payload?.name === "string" ? payload.name.trim() : "";
257
+ if (name === "") return json(400, {
258
+ ok: false,
259
+ code: "bad-request",
260
+ message: "缺少 name"
261
+ });
262
+ const raw = payload?.v;
263
+ if (raw !== void 0 && (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1)) return json(400, {
264
+ ok: false,
265
+ code: "bad-request",
266
+ message: "v 必须是正整数"
267
+ });
268
+ const unitId = typeof payload?.unitId === "string" ? payload.unitId.trim() : "";
269
+ if (raw !== void 0 && unitId !== "") return json(400, {
270
+ ok: false,
271
+ code: "bad-request",
272
+ message: "v 与 unitId 不能同时使用"
273
+ });
274
+ return withCurator(async (curator) => {
275
+ const result = unitId !== "" ? await curator.undoUnit(name, unitId) : raw === void 0 ? await curator.undo(name) : await curator.undo(name, raw);
276
+ return json(200, result.ok ? {
277
+ ok: true,
278
+ data: result
279
+ } : {
280
+ ok: false,
281
+ code: "undo-refused",
282
+ message: result.message
283
+ });
284
+ });
285
+ })();
286
+ }
287
+ },
288
+ {
289
+ path: SKILL_HISTORY_ROUTES.health,
290
+ methods: ["GET"],
291
+ requestBody: "buffered",
292
+ fetch: (request) => {
293
+ if (request.method !== "GET") return Promise.resolve(methodNotAllowed(["GET"]));
294
+ return Promise.resolve(json(200, {
295
+ ok: true,
296
+ data: { surface: "skill-history" }
297
+ }));
298
+ }
299
+ }
300
+ ];
301
+ }
302
+ //#endregion
303
+ //#region lib/types/panels/history/host.js
304
+ /**
305
+ * Mount the routes as ONE owner effect (disposers run on unload and on HMR). A profile whose host has
306
+ * no connection service simply never runs the callback — inert, never pending.
307
+ * @param ctx - the host context; the connection and curator arrive through it.
308
+ */
309
+ function apply$2(ctx) {
310
+ registerHostRoutes(ctx, (audit) => makeSkillHistoryRoutes({ getCurator: () => ctx.get("evolutionCurator") }, audit));
311
+ }
312
+ //#endregion
313
+ //#region lib/types/index.js
314
+ const name = "evolution-ui";
315
+ /** No required service: every nested row resolves its own optional seats. */
316
+ const inject = [];
317
+ /** Mount the four panel host halves through their existing owners. */
318
+ function apply(ctx) {
319
+ apply$1();
320
+ apply$2(ctx);
321
+ }
322
+ //#endregion
323
+ export { apply, inject, name };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Browser umbrella: one client bundle registers the four existing panel rows.
3
+ * @module @lmzhen/dsh-evolution-ui/client
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ export declare const inject: string[];
7
+ /** Register every panel through the shared client-kit lifecycle. */
8
+ export declare function apply(ctx: Context): void;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Host umbrella for the evolution UI panels: one row mounts all four existing host halves.
3
+ * The panel packages stay separate until their client code moves here.
4
+ * @module @lmzhen/dsh-evolution-ui
5
+ */
6
+ import type { Context } from '@deepseek-ai/cordis';
7
+ export declare const name = "evolution-ui";
8
+ /** No required service: every nested row resolves its own optional seats. */
9
+ export declare const inject: string[];
10
+ /** Mount the four panel host halves through their existing owners. */
11
+ export declare function apply(ctx: Context): void;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The panel: one read, one open skill, one open row — and the faces that draw them.
3
+ *
4
+ * Everything a PERSON reads is decided elsewhere: what a row says is core's (`skill-row-cells.ts`,
5
+ * shipped with the row), the words are the dictionary's, the tool face is `chrome.ts` and the reading
6
+ * face is `document.ts`. What is left here is the STATE — which skill is open, which row is expanded,
7
+ * which reads are in flight — and the two rules that only a state machine can hold: a read takes a
8
+ * ticket so a slow reply cannot land on another skill, and a write reports its own result instead of
9
+ * being erased by the read that follows it.
10
+ * @module @lmzhen/dsh-evolution-ui/panels/history/client
11
+ */
12
+ import { type ReactNode } from 'react';
13
+ import { type Copy } from './chrome.ts';
14
+ import { type SkillRow, type VersionBodyRow, type VersionDiffRow, type VersionsPayload } from './api.ts';
15
+ import type { MarkdownWords } from './markdown.ts';
16
+ /** What the panel is handed: the dictionary seat, the renderer's words, and the host's callbacks. */
17
+ export interface PanelFace extends Copy {
18
+ /** The two words the platform Markdown renderer needs; the dictionary owns them. */
19
+ readonly markdownWords: MarkdownWords;
20
+ readonly loadSkills: () => Promise<readonly SkillRow[]>;
21
+ readonly loadVersions: (name: string) => Promise<VersionsPayload>;
22
+ readonly loadDiff: (name: string, v: number) => Promise<VersionDiffRow>;
23
+ readonly loadBody: (name: string, v: number) => Promise<VersionBodyRow>;
24
+ readonly undo: (name: string, v?: number, unitId?: string) => Promise<string>;
25
+ }
26
+ export declare function SkillHistoryPanel(face: PanelFace): ReactNode;
27
+ //# sourceMappingURL=Panel.d.ts.map
@@ -0,0 +1,142 @@
1
+ import type { RouteFailure } from '@lmzhen/dsh-evolution-client-kit/generated/route-client';
2
+ /** Route paths: the generated mirror of the host's HOST_ROUTES (one declaration in core). */
3
+ export declare const HOST_ROUTES: {
4
+ readonly skills: "/api/dsh-evolution/skill-history/skills";
5
+ readonly versions: "/api/dsh-evolution/skill-history/versions";
6
+ readonly diff: "/api/dsh-evolution/skill-history/versions/diff";
7
+ readonly body: "/api/dsh-evolution/skill-history/versions/body";
8
+ readonly undo: "/api/dsh-evolution/skill-history/undo";
9
+ readonly health: "/api/dsh-evolution/skill-history/health";
10
+ };
11
+ /** One changed region, as the host reports it (the platform diff card's own shape). */
12
+ export interface DiffHunkRow {
13
+ readonly path: string;
14
+ readonly oldText: string;
15
+ readonly newText: string;
16
+ }
17
+ /** One version's diff, as the host computes it. */
18
+ export interface VersionDiffRow {
19
+ readonly v: number;
20
+ /** The version this one replaced, or null for the first recorded version. */
21
+ readonly against: number | null;
22
+ readonly linesAdded: number;
23
+ readonly linesRemoved: number;
24
+ readonly hunks: readonly DiffHunkRow[];
25
+ readonly truncated: boolean;
26
+ }
27
+ /** Where a cell sits in its row: `aside` is right-aligned and is never truncated. */
28
+ export type CellSlot = 'lead' | 'meta' | 'aside';
29
+ /** The visual register a cell speaks in; the stylesheet maps it to a scale token. */
30
+ export type CellTone = 'primary' | 'secondary' | 'tertiary';
31
+ /** One cell of a row: a locale key with the values its template needs, or a verbatim fact. */
32
+ export type RowCell = {
33
+ readonly key: string;
34
+ readonly slot: CellSlot;
35
+ readonly tone: CellTone;
36
+ readonly note?: string;
37
+ readonly copy: string;
38
+ readonly values?: Readonly<Record<string, string | number>>;
39
+ } | {
40
+ readonly key: string;
41
+ readonly slot: CellSlot;
42
+ readonly tone: CellTone;
43
+ readonly note?: string;
44
+ readonly literal: string;
45
+ };
46
+ /** The lines one row shows: a title line, and a meta line under it. */
47
+ export interface RowCells {
48
+ readonly title: readonly RowCell[];
49
+ readonly meta: readonly RowCell[];
50
+ }
51
+ /** One recorded version, as the host reports it. */
52
+ export interface VersionRow {
53
+ readonly v: number;
54
+ readonly at: string;
55
+ readonly action: string;
56
+ readonly hash: string;
57
+ readonly chars: number;
58
+ /** Present on body rows: false when this version already IS the live content. */
59
+ readonly undoable?: boolean;
60
+ /** Characters this version added to (or removed from) the one before it; absent on the first. */
61
+ readonly charsDelta?: number;
62
+ /** Which kind of write produced it (the vocabulary key the panel turns into words). */
63
+ readonly actionKind: string;
64
+ /** How long ago the host recorded it, as a bucket plus a count — never a sentence. */
65
+ readonly age: {
66
+ readonly unit: string;
67
+ readonly n: number;
68
+ };
69
+ /** What this row says and where each fact sits, decided in core (`skill-row-cells.ts`). */
70
+ readonly cells: RowCells;
71
+ /** The one line the optional summarizer wrote, when the deployment turned it on. */
72
+ readonly summary?: string;
73
+ /**
74
+ * Which support file these bytes came from, relative to the skill directory. Absent on body rows,
75
+ * and absent on every support row recorded before the field existed (the bytes carry no name).
76
+ */
77
+ readonly path?: string;
78
+ /** Present when this row belongs to a cross-skill write unit; undo then restores the whole unit. */
79
+ readonly unitId?: string;
80
+ }
81
+ /** One skill that has recorded versions, with what the listing already knows about it. */
82
+ export interface SkillRow {
83
+ readonly name: string;
84
+ readonly versions: number;
85
+ readonly description: string;
86
+ readonly managed: boolean;
87
+ readonly protectedBy: string | null;
88
+ readonly protectionUnknown: boolean;
89
+ /** When this skill's content last changed (either chain), or null when the index is empty. */
90
+ readonly lastAt: string | null;
91
+ /** That moment as a bucket plus a count; null exactly when `lastAt` is. */
92
+ readonly age: {
93
+ readonly unit: string;
94
+ readonly n: number;
95
+ } | null;
96
+ /** What this row says and where each fact sits, decided in core (`skill-row-cells.ts`). */
97
+ readonly cells: RowCells;
98
+ }
99
+ /** One version's whole body, as the read route hands it over. */
100
+ export interface VersionBodyRow {
101
+ readonly v: number;
102
+ /**
103
+ * The text a READER sees: the body without its frontmatter block, or its head when `truncated` —
104
+ * the reader is told rather than silently cut. The exact bytes are the diff route's answer.
105
+ */
106
+ readonly display: string;
107
+ /** How long the stored body is, whatever came back. */
108
+ readonly chars: number;
109
+ readonly truncated: boolean;
110
+ }
111
+ /**
112
+ * The two chains. G4.2 whitelist audit: the payload's `liveHash` is DELETED here — the host route already
113
+ * compares it (`undoable: entry.hash !== liveHash`, routes.ts) and delivers the verdict per row, so the
114
+ * "current" marker has one source. A declared field nobody reads is drift waiting to happen.
115
+ */
116
+ export interface VersionsPayload {
117
+ readonly content: readonly VersionRow[];
118
+ readonly support: readonly VersionRow[];
119
+ }
120
+ /** The panel's data face: plain callbacks, no service handle. */
121
+ export interface SkillHistoryApi {
122
+ readonly skills: () => Promise<readonly SkillRow[]>;
123
+ readonly versions: (name: string) => Promise<VersionsPayload>;
124
+ readonly undo: (name: string, v?: number, unitId?: string) => Promise<string>;
125
+ readonly diff: (name: string, v: number) => Promise<VersionDiffRow>;
126
+ readonly body: (name: string, v: number) => Promise<VersionBodyRow>;
127
+ }
128
+ /**
129
+ * A refusal the host reported, carrying its own sentence (the curator's, not ours) and the classified
130
+ * four-state failure so the panel can render the shared failure block (G5/W7 S6b).
131
+ */
132
+ export declare class SkillHistoryRefusal extends Error {
133
+ readonly failure: RouteFailure;
134
+ constructor(failure: RouteFailure);
135
+ }
136
+ /**
137
+ * Build the panel's face over one fetch implementation.
138
+ * @param fetchImpl - the fetch to use; a spec passes a stub.
139
+ * @returns the three callbacks the component receives through its inject face.
140
+ */
141
+ export declare function createSkillHistoryApi(fetchImpl?: typeof fetch): SkillHistoryApi;
142
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The four atoms the panel's tool face is built from (G3, §15.2 L2a atoms).
3
+ *
4
+ * Why atoms at all: the row used to be assembled from five different elements that each wrote their
5
+ * own classes, so "what a control looks like" was spread over the stylesheet and a click target, a
6
+ * state chip, a refusal and a glyph had no vocabulary at all (W7/W9/W13/W14). Every interactive
7
+ * element now says what it IS — a control that acts, a control that only reveals more, a state, a
8
+ * notice — and the stylesheet answers once per kind.
9
+ *
10
+ * No words live here: an atom takes the copy its caller already resolved.
11
+ * @module @lmzhen/dsh-evolution-ui/panels/history/client
12
+ */
13
+ import { type ReactNode } from 'react';
14
+ /** The register one control speaks in. `primary` is reserved for the click that writes. */
15
+ export type Tone = 'quiet' | 'primary';
16
+ /**
17
+ * What a control DOES, which is what makes them distinguishable at a glance: one acts, one reveals
18
+ * more (and shows whether it is open), one chooses between two views of the same thing.
19
+ */
20
+ export type ControlKind = 'action' | 'toggle' | 'switch';
21
+ /** The glyphs the panel draws. The platform's own arrows are text characters, which render in the
22
+ * reader's fallback font at whatever weight it has (W7); a drawn path cannot. */
23
+ export type IconName = 'chevron-right' | 'chevron-down' | 'alert';
24
+ /**
25
+ * One drawn glyph.
26
+ * @param name - which glyph.
27
+ * @returns an inline SVG that takes the surrounding colour and type size.
28
+ */
29
+ export declare function icon(name: IconName): ReactNode;
30
+ /** What every atom takes. */
31
+ interface BaseProps {
32
+ /** React key, for the callers that build lists. */
33
+ readonly key?: string;
34
+ /** The copy, already resolved by the dictionary seat. */
35
+ readonly children: ReactNode;
36
+ }
37
+ /** One control. `kind` says whether it acts or only reveals; `tone` says whether it writes. */
38
+ export interface ButtonProps extends BaseProps {
39
+ readonly kind?: ControlKind;
40
+ readonly tone?: Tone;
41
+ /** Hover text, when the label alone is ambiguous. */
42
+ readonly title?: string;
43
+ /** Whether the thing this control reveals is currently open (`aria-expanded`). */
44
+ readonly expanded?: boolean;
45
+ /** Whether this control is the side of a two-way choice that is showing. */
46
+ readonly pressed?: boolean;
47
+ readonly onClick: () => void;
48
+ }
49
+ /**
50
+ * One control.
51
+ * @param props - what it is, how it speaks, and what it does.
52
+ * @returns the button element.
53
+ */
54
+ export declare function button(props: ButtonProps): ReactNode;
55
+ /**
56
+ * One state, spelled as a chip: never clickable, never in the primary colour (W9).
57
+ * @param props - the copy.
58
+ * @returns the chip element.
59
+ */
60
+ export declare function chip(props: BaseProps): ReactNode;
61
+ /** One notice: a sentence about what just happened, or why nothing did. */
62
+ export interface NoteProps extends BaseProps {
63
+ /** Whether this is a refusal or a failure, which colours it and adds the glyph (W13). */
64
+ readonly error?: boolean;
65
+ }
66
+ /**
67
+ * One notice.
68
+ * @param props - the copy, and whether it reports a failure.
69
+ * @returns the note element.
70
+ */
71
+ export declare function note(props: NoteProps): ReactNode;
72
+ export {};
73
+ //# sourceMappingURL=atoms.d.ts.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The TOOL face: the skill list, the version list, and the controls that act on a version.
3
+ *
4
+ * This file renders FACTS. What a row says, in which order and in which slot is decided in core
5
+ * (`skill-row-cells.ts`) and travels with the row, because a browser half cannot import core at
6
+ * runtime; the words come from the locale dictionary through the `Copy` seat. The renderer's only
7
+ * judgements are layout: a cell's slot decides its alignment, and the `aside` slot is the one the
8
+ * browser's ellipsis may never eat (W1).
9
+ *
10
+ * The reading face — a version rendered as a document, and the rendered diff — lives in
11
+ * `document.ts`; the two faces share the scale in `tokens.ts` and nothing else (§15.2 L2b).
12
+ * @module @lmzhen/dsh-evolution-ui/panels/history/client
13
+ */
14
+ import { type ReactNode } from 'react';
15
+ import type { SkillRow, VersionRow } from './api.ts';
16
+ /** What a chrome builder needs: the dictionary, and nothing else. */
17
+ export interface Copy {
18
+ readonly t: (key: string) => string;
19
+ readonly format: (key: string, values: Record<string, string | number>) => string;
20
+ }
21
+ /** One kind of expansion a row can show. */
22
+ export type OpenKind = 'diff' | 'preview';
23
+ /** The local time an ISO timestamp names, as the operator's own clock shows it. */
24
+ export declare function localTime(at: string): string;
25
+ /** The search field above the list. */
26
+ export declare function searchField(copy: Copy, query: string, onQuery: (value: string) => void): ReactNode;
27
+ /**
28
+ * One skill in the left column: its name and size, then what it is and when it last changed.
29
+ * @param copy - the dictionary seat.
30
+ * @param row - the skill's facts and cells.
31
+ * @param selected - whether this is the open skill.
32
+ * @param onOpen - what a click does.
33
+ * @returns the row.
34
+ */
35
+ export declare function skillLine(copy: Copy, row: SkillRow, selected: boolean, onOpen: () => void): ReactNode;
36
+ /** What one version row needs from the panel's state machine. */
37
+ export interface VersionRowProps {
38
+ readonly copy: Copy;
39
+ readonly row: VersionRow;
40
+ /** Whether this row is the one waiting for a confirmation click. */
41
+ readonly confirming: boolean;
42
+ /** Which expansion this row currently shows, if any. */
43
+ readonly open: OpenKind | undefined;
44
+ /** The expanded content, already built by the caller. */
45
+ readonly expanded: ReactNode;
46
+ readonly onRestore: () => void;
47
+ readonly onCancel: () => void;
48
+ readonly onToggle: (kind: OpenKind) => void;
49
+ }
50
+ /**
51
+ * One version row: what happened, when, how big — then the controls, then whatever is expanded.
52
+ *
53
+ * The write is the only control that can change anything, and it is the only one that ever takes the
54
+ * primary tone; a toggle that merely reveals more is borderless and carries a drawn chevron, so the
55
+ * two kinds cannot be mistaken for each other (W7/W14).
56
+ * @param props - the row, the copy, and the panel's callbacks.
57
+ * @returns the row.
58
+ */
59
+ export declare function versionRow(props: VersionRowProps): ReactNode;
60
+ /**
61
+ * One group's heading, and the note that explains the group.
62
+ * @param title - the group's name, already resolved.
63
+ * @param noteText - the group's note, when it has one.
64
+ * @returns the heading and the note, for the caller to place in its own section.
65
+ */
66
+ export declare function groupHeading(title: string, noteText: string | undefined): ReactNode[];
67
+ /** What the list shows when it has nothing to show.
68
+ * S5-07: the sites that render it as a SIBLING in an array must pass a key - a keyless sibling made
69
+ * React warn on every render of the default view (measured: one warning per render, captured by spy).
70
+ */
71
+ export declare function emptyState(text: string, key?: string): ReactNode;
72
+ /** One notice about what just happened. */
73
+ export declare function notice(text: string | undefined, error: boolean): ReactNode;
74
+ /** The pane's heading and its one control. */
75
+ export declare function panelHead(copy: Copy, onRefresh: () => void): ReactNode;
76
+ //# sourceMappingURL=chrome.d.ts.map