@lmzhen/dsh-evolution-skill-history 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/client.js +488 -297
- package/lib/index.js +24 -8
- package/lib/types/client/Panel.d.ts +13 -19
- package/lib/types/client/api.d.ts +33 -2
- package/lib/types/client/atoms.d.ts +73 -0
- package/lib/types/client/chrome.d.ts +73 -0
- package/lib/types/client/document.d.ts +71 -0
- package/lib/types/client/markdown.d.ts +33 -0
- package/lib/types/client/styles.d.ts +0 -21
- package/lib/types/client/tokens.d.ts +25 -0
- package/package.json +7 -7
package/lib/client.js
CHANGED
|
@@ -85,7 +85,8 @@ window.__ModuleLoader__.load({
|
|
|
85
85
|
"undo.confirm": "确认回退",
|
|
86
86
|
"undo.no": "取消",
|
|
87
87
|
current: "当前",
|
|
88
|
-
"versions.count": "个版本",
|
|
88
|
+
"versions.count": "{n} 个版本",
|
|
89
|
+
"row.version": "v{n}",
|
|
89
90
|
"row.first": "初始版本",
|
|
90
91
|
"row.summary.missing": "(未生成摘要)",
|
|
91
92
|
"row.delta.up": "(+{n} 字符)",
|
|
@@ -94,7 +95,7 @@ window.__ModuleLoader__.load({
|
|
|
94
95
|
"diff.source": "源码",
|
|
95
96
|
"preview.show": "预览",
|
|
96
97
|
"preview.hide": "收起预览",
|
|
97
|
-
"preview.truncated": "
|
|
98
|
+
"preview.truncated": "正文过长:只显示前 {shown} 个字符(共 {total} 个)",
|
|
98
99
|
"path.unrecorded": "(路径未记录)",
|
|
99
100
|
"diff.rendered": "渲染",
|
|
100
101
|
"markdown.copy": "复制",
|
|
@@ -158,7 +159,8 @@ window.__ModuleLoader__.load({
|
|
|
158
159
|
"undo.confirm": "Confirm restore",
|
|
159
160
|
"undo.no": "Cancel",
|
|
160
161
|
current: "Current",
|
|
161
|
-
"versions.count": "versions",
|
|
162
|
+
"versions.count": "{n} versions",
|
|
163
|
+
"row.version": "v{n}",
|
|
162
164
|
"row.first": "First version",
|
|
163
165
|
"row.summary.missing": "(no summary generated)",
|
|
164
166
|
"row.delta.up": "(+{n} chars)",
|
|
@@ -167,7 +169,7 @@ window.__ModuleLoader__.load({
|
|
|
167
169
|
"diff.source": "Source",
|
|
168
170
|
"preview.show": "Preview",
|
|
169
171
|
"preview.hide": "Hide preview",
|
|
170
|
-
"preview.truncated": "the body is long
|
|
172
|
+
"preview.truncated": "the body is long: showing the first {shown} of {total} characters",
|
|
171
173
|
"path.unrecorded": "(path not recorded)",
|
|
172
174
|
"diff.rendered": "Rendered",
|
|
173
175
|
"markdown.copy": "Copy",
|
|
@@ -226,81 +228,355 @@ window.__ModuleLoader__.load({
|
|
|
226
228
|
});
|
|
227
229
|
}
|
|
228
230
|
//#endregion
|
|
231
|
+
//#region src/client/tokens.ts
|
|
232
|
+
/**
|
|
233
|
+
* The scale, declared on one element so it never leaks into the host shell.
|
|
234
|
+
* @param scope - the selector the custom properties are declared on.
|
|
235
|
+
* @returns the declaration block to put in the stylesheet.
|
|
236
|
+
*/
|
|
237
|
+
function tokenVars(scope) {
|
|
238
|
+
return scope + "{--evo-type-lg:calc(1em + 2px);--evo-type-body:1em;--evo-type-sm:calc(1em - 1px);--evo-type-xs:calc(1em - 2px);--evo-space-2:2px;--evo-space-4:4px;--evo-space-6:6px;--evo-space-8:8px;--evo-space-10:10px;--evo-space-12:12px;--evo-space-14:14px;--evo-space-16:16px;--evo-space-20:20px;--evo-radius-chip:4px;--evo-radius-control:8px;--evo-radius-surface:12px;--evo-radius-pill:999px;--evo-hairline-width:.5px;--evo-hairline-marker:2px;--evo-measure-read:80ch;--evo-leading-base:1.5;--evo-tone-quiet:var(--dsw-alias-label-secondary);--evo-tone-primary:var(--dsw-alias-brand-primary);--evo-tone-danger:var(--dsw-alias-state-error-primary);--evo-tone-success:var(--dsw-alias-state-success-primary);--evo-focus-ring:2px solid var(--dsw-alias-label-primary);--evo-focus-offset:-2px;--evo-cap-aside:280px;--evo-cap-block:260px}";
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
229
241
|
//#region src/client/styles.ts
|
|
230
242
|
/**
|
|
231
243
|
* The panel’s stylesheet, injected once behind a `<style data-plugin-css=…>` tag.
|
|
232
244
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
245
|
+
* TWO FACES, ONE SCALE. The TOOL face (`.evo-hist-*`) is the compact list and its controls; the
|
|
246
|
+
* READING face (`.evo-doc-*`, at the end) is a version rendered as a document. Each face owns its
|
|
247
|
+
* rules and they never overlap: the list may be dense and full of controls, a document may not. That
|
|
248
|
+
* is the whole reason for two prefixes — a rule that serves one face can no longer fight the other’s
|
|
249
|
+
* comfort, and a third face would be another block rather than an edit here (§15.2 L2b, W22).
|
|
236
250
|
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
251
|
+
* Every colour is a platform design token; every type step and every geometry value is a family scale
|
|
252
|
+
* token (`./tokens.ts`, generated from `packages/scripts/client-tokens.json`), so the two client
|
|
253
|
+
* halves of this family cannot drift apart.
|
|
254
|
+
*
|
|
255
|
+
* The metrics themselves are COPIED from the platform’s own components rather than invented:
|
|
256
|
+
*
|
|
257
|
+
* - the code block (`--dsw-font-markdown-code-block-small`, `0.5px` border, `12px` padding, radius
|
|
258
|
+
* `12px`, `max-height: 260px`) is the platform command card’s `<pre>` (client/ui-chat/…/GenericCommandCard.module.css);
|
|
239
259
|
* - hover / focus (`--dsw-alias-interactive-bg-hover`, a 2px inset focus ring) follow the platform
|
|
240
260
|
* sidebar row (client/ui-sidebar/…/SidebarRoot.module.css);
|
|
241
|
-
* - the
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* platform’s
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
* own components write geometry the same way; what must never be a literal is TYPE and COLOUR, and
|
|
248
|
-
* every size and colour below is a token or a `calc()` over one.
|
|
261
|
+
* - the field radius (`8px`) is the platform’s own Input primitive (client/ui-primitives/Input.module.css);
|
|
262
|
+
* - the panel’s type steps come from the family scale, which is relative to the shell’s chrome size —
|
|
263
|
+
* NOT the setting behind `--dsh-content-font-size*` (that one says it affects conversation content
|
|
264
|
+
* only, and the platform’s own surfaces never read it). The step is relative, so the panel scales
|
|
265
|
+
* with the shell, not with the conversation: the dense list needs it — at the raw chrome size
|
|
266
|
+
* `dsh-evolution-maintenance` truncates in 264px.
|
|
249
267
|
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
250
268
|
*/
|
|
251
269
|
/** Tag id that makes the injection idempotent. */
|
|
252
270
|
const CSS_TAG_ID = "@lmzhen/dsh-evolution-skill-history/panel.css";
|
|
253
271
|
/** The stylesheet the panel injects once. */
|
|
254
272
|
const CSS = [
|
|
255
|
-
".evo-hist-root
|
|
256
|
-
".evo-hist-
|
|
257
|
-
".evo-hist-
|
|
258
|
-
".evo-hist-search
|
|
259
|
-
".evo-hist-search input:
|
|
260
|
-
".evo-hist-
|
|
261
|
-
".evo-hist-
|
|
262
|
-
".evo-hist-
|
|
273
|
+
tokenVars(".evo-hist-root"),
|
|
274
|
+
".evo-hist-root{display:flex;height:100%;min-height:0;min-width:0;font-size:var(--evo-type-sm);color:var(--dsw-alias-label-primary)}",
|
|
275
|
+
".evo-hist-aside{display:flex;flex-direction:column;flex:0 0 var(--evo-cap-aside);width:var(--evo-cap-aside);min-height:0;border-right:var(--evo-hairline-width) solid var(--dsw-alias-border-l2)}",
|
|
276
|
+
".evo-hist-search{padding:var(--evo-space-12) var(--evo-space-12) var(--evo-space-4)}",
|
|
277
|
+
".evo-hist-search input{width:100%;box-sizing:border-box;padding:var(--evo-space-6) var(--evo-space-10);border:var(--evo-hairline-width) solid var(--dsw-alias-border-l2);border-radius:var(--evo-radius-control);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit}",
|
|
278
|
+
".evo-hist-search input:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
|
|
279
|
+
".evo-hist-list{flex:1 1 auto;min-height:0;overflow-y:auto;padding:var(--evo-space-4) var(--evo-space-12) var(--evo-space-10)}",
|
|
280
|
+
".evo-hist-empty{padding:var(--evo-space-6) var(--evo-space-8);margin:0;color:var(--dsw-alias-label-tertiary)}",
|
|
281
|
+
".evo-hist-skill{display:block;width:100%;box-sizing:border-box;padding:var(--evo-space-6) var(--evo-space-8);border:0;border-radius:var(--evo-radius-control);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;text-align:left;cursor:pointer}",
|
|
263
282
|
".evo-hist-skill:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
264
|
-
".evo-hist-skill:focus-visible{outline:
|
|
283
|
+
".evo-hist-skill:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
|
|
265
284
|
".evo-hist-skill[aria-current=\"true\"]{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
|
|
266
|
-
".evo-hist-
|
|
267
|
-
".evo-hist-
|
|
268
|
-
".evo-hist-
|
|
269
|
-
".evo-hist-
|
|
270
|
-
".evo-hist-
|
|
271
|
-
".evo-hist-
|
|
272
|
-
".evo-hist-
|
|
273
|
-
".evo-hist-
|
|
274
|
-
".evo-hist-
|
|
275
|
-
".evo-hist-
|
|
276
|
-
".evo-hist-
|
|
277
|
-
".evo-hist-
|
|
278
|
-
".evo-hist-
|
|
285
|
+
".evo-hist-cell-line{display:flex;align-items:baseline;gap:var(--evo-space-6);min-width:0}",
|
|
286
|
+
".evo-hist-cell-meta{margin-top:var(--evo-space-2)}",
|
|
287
|
+
".evo-hist-cell{min-width:0}",
|
|
288
|
+
".evo-hist-slot-lead{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
289
|
+
".evo-hist-slot-meta{flex:none;white-space:nowrap}",
|
|
290
|
+
".evo-hist-slot-aside{flex:none;margin-left:auto;white-space:nowrap}",
|
|
291
|
+
".evo-hist-tone-primary{color:inherit}",
|
|
292
|
+
".evo-hist-tone-secondary{color:var(--dsw-alias-label-secondary)}",
|
|
293
|
+
".evo-hist-tone-tertiary{color:var(--dsw-alias-label-tertiary)}",
|
|
294
|
+
".evo-hist-main{flex:1 1 auto;min-width:0;min-height:0;overflow-y:auto;padding:var(--evo-space-12) var(--evo-space-12)}",
|
|
295
|
+
".evo-hist-head{display:flex;align-items:center;gap:var(--evo-space-10);margin:0 0 var(--evo-space-4)}",
|
|
296
|
+
".evo-hist-title{flex:1 1 auto;margin:0;font-size:var(--evo-type-lg);font-weight:600}",
|
|
297
|
+
".evo-hist-hint{margin:0 0 var(--evo-space-10);color:var(--dsw-alias-label-secondary);line-height:var(--evo-leading-base)}",
|
|
298
|
+
".evo-hist-note{display:flex;align-items:flex-start;gap:var(--evo-space-6);margin:0 0 var(--evo-space-10);color:var(--dsw-alias-label-secondary);line-height:var(--evo-leading-base)}",
|
|
299
|
+
".evo-hist-note-text{min-width:0;white-space:pre-wrap}",
|
|
300
|
+
".evo-hist-note[data-error=\"true\"]{color:var(--evo-tone-danger);background:var(--dsw-alias-bg-layer-2);border:var(--evo-hairline-width) solid var(--evo-tone-danger);border-radius:var(--evo-radius-control);padding:var(--evo-space-8) var(--evo-space-10)}",
|
|
301
|
+
".evo-hist-icon{flex:none;margin-top:.15em}",
|
|
302
|
+
".evo-hist-group{margin:var(--evo-space-10) 0 var(--evo-space-2);font-size:var(--evo-type-sm);font-weight:600;color:var(--dsw-alias-label-secondary)}",
|
|
303
|
+
".evo-hist-row{display:flex;align-items:flex-start;gap:var(--evo-space-8);padding:var(--evo-space-8) 0;border-top:var(--evo-hairline-width) solid var(--dsw-alias-border-l2);max-width:calc(var(--evo-measure-read) * 1.5);color:var(--dsw-alias-label-primary)}",
|
|
279
304
|
".evo-hist-row:first-of-type{border-top:0}",
|
|
280
305
|
".evo-hist-row-body{flex:1 1 auto;min-width:0}",
|
|
281
|
-
".evo-hist-row-title{display:flex;align-items:baseline;gap:
|
|
282
|
-
".evo-hist-row-
|
|
283
|
-
".evo-hist-
|
|
284
|
-
".evo-hist-
|
|
285
|
-
".evo-hist-
|
|
286
|
-
".evo-hist-button{
|
|
306
|
+
".evo-hist-row-title{display:flex;align-items:baseline;gap:var(--evo-space-8);min-width:0}",
|
|
307
|
+
".evo-hist-row-summary{margin-top:var(--evo-space-2);line-height:var(--evo-leading-base)}",
|
|
308
|
+
".evo-hist-actions{display:flex;flex:none;align-items:center;gap:var(--evo-space-6)}",
|
|
309
|
+
".evo-hist-chip{flex:none;padding:var(--evo-space-2) var(--evo-space-8);border-radius:var(--evo-radius-pill);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}",
|
|
310
|
+
".evo-hist-button{display:inline-flex;flex:none;align-items:center;gap:var(--evo-space-2);font:inherit;padding:var(--evo-space-4) var(--evo-space-10);border:var(--evo-hairline-width) solid var(--dsw-alias-border-l2);border-radius:var(--evo-radius-control);background:transparent;color:var(--evo-tone-quiet);cursor:pointer}",
|
|
311
|
+
".evo-hist-button[data-kind=\"toggle\"],.evo-hist-button[data-kind=\"switch\"]{padding:var(--evo-space-2) var(--evo-space-4);border-color:transparent;color:var(--evo-tone-quiet)}",
|
|
312
|
+
".evo-hist-button[data-kind=\"switch\"][aria-pressed=\"true\"]{color:var(--dsw-alias-label-primary)}",
|
|
287
313
|
".evo-hist-button:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
288
|
-
".evo-hist-button:focus-visible{outline:
|
|
289
|
-
".evo-hist-button[data-tone=\"primary\"]{border-color:var(--
|
|
290
|
-
".evo-
|
|
291
|
-
".evo-
|
|
292
|
-
".evo-
|
|
293
|
-
".evo-hist-
|
|
294
|
-
".evo-
|
|
295
|
-
".evo-
|
|
296
|
-
".evo-
|
|
297
|
-
".evo-
|
|
298
|
-
".evo-
|
|
299
|
-
".evo-
|
|
300
|
-
".evo-
|
|
301
|
-
".evo-
|
|
314
|
+
".evo-hist-button:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
|
|
315
|
+
".evo-hist-button[data-tone=\"primary\"]{border-color:var(--evo-tone-primary);color:var(--evo-tone-primary)}",
|
|
316
|
+
".evo-doc-preview{margin:var(--evo-space-6) 0 var(--evo-space-2);max-width:var(--evo-measure-read);line-height:var(--evo-leading-base)}",
|
|
317
|
+
".evo-doc-diff{display:flex;flex-direction:column;gap:var(--evo-space-8)}",
|
|
318
|
+
".evo-doc-head{display:flex;align-items:center;gap:var(--evo-space-8);max-width:var(--evo-measure-read)}",
|
|
319
|
+
".evo-doc-head .evo-hist-note{flex:1 1 auto;margin:0}",
|
|
320
|
+
".evo-doc-toggle{display:flex;flex:none;align-items:center;gap:var(--evo-space-6)}",
|
|
321
|
+
".evo-doc-source{margin:var(--evo-space-6) 0 var(--evo-space-2);padding:var(--evo-space-12) var(--evo-space-16);max-height:var(--evo-cap-block);overflow:auto;border:var(--evo-hairline-width) solid var(--dsw-alias-border-l1);border-radius:var(--evo-radius-surface);background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-primary);font:var(--dsw-font-markdown-code-block-small);white-space:pre-wrap}",
|
|
322
|
+
".evo-doc-render{display:flex;flex-direction:column;gap:var(--evo-space-8);margin:var(--evo-space-6) 0 var(--evo-space-2);max-width:var(--evo-measure-read)}",
|
|
323
|
+
".evo-doc-block{position:relative;padding:var(--evo-space-8) var(--evo-space-12) var(--evo-space-8) var(--evo-space-20);border:var(--evo-hairline-width) solid var(--dsw-alias-border-l2);border-left-width:var(--evo-hairline-marker);border-radius:var(--evo-radius-surface);background:var(--dsw-alias-markdown-code-block);overflow-x:auto}",
|
|
324
|
+
".evo-doc-del{border-left-color:var(--evo-tone-danger)}",
|
|
325
|
+
".evo-doc-add{border-left-color:var(--evo-tone-success)}",
|
|
326
|
+
".evo-doc-tag{position:absolute;left:var(--evo-space-8);top:var(--evo-space-8);color:var(--dsw-alias-label-tertiary)}",
|
|
327
|
+
".evo-doc-line-add{color:var(--evo-tone-success)}",
|
|
328
|
+
".evo-doc-line-del{color:var(--evo-tone-danger)}"
|
|
302
329
|
].join("\n");
|
|
303
330
|
//#endregion
|
|
331
|
+
//#region src/client/atoms.ts
|
|
332
|
+
/**
|
|
333
|
+
* The four atoms the panel's tool face is built from (G3, §15.2 L2a atoms).
|
|
334
|
+
*
|
|
335
|
+
* Why atoms at all: the row used to be assembled from five different elements that each wrote their
|
|
336
|
+
* own classes, so "what a control looks like" was spread over the stylesheet and a click target, a
|
|
337
|
+
* state chip, a refusal and a glyph had no vocabulary at all (W7/W9/W13/W14). Every interactive
|
|
338
|
+
* element now says what it IS — a control that acts, a control that only reveals more, a state, a
|
|
339
|
+
* notice — and the stylesheet answers once per kind.
|
|
340
|
+
*
|
|
341
|
+
* No words live here: an atom takes the copy its caller already resolved.
|
|
342
|
+
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
343
|
+
*/
|
|
344
|
+
/** The stroked path of one glyph, in a 16x16 box. */
|
|
345
|
+
const PATHS = {
|
|
346
|
+
"chevron-right": ["M6.5 3.5L11 8l-4.5 4.5"],
|
|
347
|
+
"chevron-down": ["M3.5 6.5L8 11l4.5-4.5"],
|
|
348
|
+
alert: [
|
|
349
|
+
"M8 2.5l5.5 10.5h-11z",
|
|
350
|
+
"M8 6.6v3.1",
|
|
351
|
+
"M8 11.4h.01"
|
|
352
|
+
]
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* One drawn glyph.
|
|
356
|
+
* @param name - which glyph.
|
|
357
|
+
* @returns an inline SVG that takes the surrounding colour and type size.
|
|
358
|
+
*/
|
|
359
|
+
function icon(name) {
|
|
360
|
+
return (0, react.createElement)("svg", {
|
|
361
|
+
className: "evo-hist-icon",
|
|
362
|
+
viewBox: "0 0 16 16",
|
|
363
|
+
width: "1em",
|
|
364
|
+
height: "1em",
|
|
365
|
+
fill: "none",
|
|
366
|
+
stroke: "currentColor",
|
|
367
|
+
strokeWidth: 1.5,
|
|
368
|
+
strokeLinecap: "round",
|
|
369
|
+
strokeLinejoin: "round",
|
|
370
|
+
"aria-hidden": "true",
|
|
371
|
+
focusable: "false"
|
|
372
|
+
}, PATHS[name].map((d, index) => (0, react.createElement)("path", {
|
|
373
|
+
key: index,
|
|
374
|
+
d
|
|
375
|
+
})));
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* One control.
|
|
379
|
+
* @param props - what it is, how it speaks, and what it does.
|
|
380
|
+
* @returns the button element.
|
|
381
|
+
*/
|
|
382
|
+
function button(props) {
|
|
383
|
+
const { children, kind = "action", tone = "quiet", title, expanded, pressed, onClick } = props;
|
|
384
|
+
return (0, react.createElement)("button", {
|
|
385
|
+
key: props.key,
|
|
386
|
+
type: "button",
|
|
387
|
+
className: "evo-hist-button",
|
|
388
|
+
"data-kind": kind,
|
|
389
|
+
"data-tone": tone,
|
|
390
|
+
title,
|
|
391
|
+
"aria-expanded": expanded,
|
|
392
|
+
"aria-pressed": pressed,
|
|
393
|
+
onClick
|
|
394
|
+
}, kind === "toggle" ? icon(expanded === true ? "chevron-down" : "chevron-right") : null, children);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* One state, spelled as a chip: never clickable, never in the primary colour (W9).
|
|
398
|
+
* @param props - the copy.
|
|
399
|
+
* @returns the chip element.
|
|
400
|
+
*/
|
|
401
|
+
function chip(props) {
|
|
402
|
+
return (0, react.createElement)("span", {
|
|
403
|
+
key: props.key,
|
|
404
|
+
className: "evo-hist-chip"
|
|
405
|
+
}, props.children);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* One notice.
|
|
409
|
+
* @param props - the copy, and whether it reports a failure.
|
|
410
|
+
* @returns the note element.
|
|
411
|
+
*/
|
|
412
|
+
function note(props) {
|
|
413
|
+
return (0, react.createElement)("p", {
|
|
414
|
+
key: props.key,
|
|
415
|
+
className: "evo-hist-note",
|
|
416
|
+
"data-error": props.error === true ? "true" : void 0
|
|
417
|
+
}, props.error === true ? icon("alert") : null, (0, react.createElement)("span", { className: "evo-hist-note-text" }, props.children));
|
|
418
|
+
}
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/client/chrome.ts
|
|
421
|
+
/**
|
|
422
|
+
* The TOOL face: the skill list, the version list, and the controls that act on a version.
|
|
423
|
+
*
|
|
424
|
+
* This file renders FACTS. What a row says, in which order and in which slot is decided in core
|
|
425
|
+
* (`skill-row-cells.ts`) and travels with the row, because a browser half cannot import core at
|
|
426
|
+
* runtime; the words come from the locale dictionary through the `Copy` seat. The renderer's only
|
|
427
|
+
* judgements are layout: a cell's slot decides its alignment, and the `aside` slot is the one the
|
|
428
|
+
* browser's ellipsis may never eat (W1).
|
|
429
|
+
*
|
|
430
|
+
* The reading face — a version rendered as a document, and the rendered diff — lives in
|
|
431
|
+
* `document.ts`; the two faces share the scale in `tokens.ts` and nothing else (§15.2 L2b).
|
|
432
|
+
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
433
|
+
*/
|
|
434
|
+
/** The local time an ISO timestamp names, as the operator's own clock shows it. */
|
|
435
|
+
function localTime(at) {
|
|
436
|
+
const parsed = Date.parse(at);
|
|
437
|
+
if (Number.isNaN(parsed)) return at;
|
|
438
|
+
const date = new Date(parsed);
|
|
439
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
440
|
+
return String(date.getFullYear()) + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes());
|
|
441
|
+
}
|
|
442
|
+
/** The text one cell shows, resolved through the seat. */
|
|
443
|
+
function cellText(copy, cell) {
|
|
444
|
+
if (!("copy" in cell)) return cell.literal;
|
|
445
|
+
return cell.values === void 0 ? copy.t(cell.copy) : copy.format(cell.copy, cell.values);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* One line of cells. The slot decides placement; the tone only changes the register.
|
|
449
|
+
* @param cells - the cells of this line, in reading order.
|
|
450
|
+
* @param copy - the dictionary seat.
|
|
451
|
+
* @param className - the line's own class.
|
|
452
|
+
* @param hover - hover text the host cannot express (the local clock), by cell key.
|
|
453
|
+
* @returns the line element.
|
|
454
|
+
*/
|
|
455
|
+
function cellLine(cells, copy, className, hover = {}) {
|
|
456
|
+
return (0, react.createElement)("div", { className }, cells.map((cell) => (0, react.createElement)("span", {
|
|
457
|
+
key: cell.key,
|
|
458
|
+
className: "evo-hist-cell evo-hist-slot-" + cell.slot + " evo-hist-tone-" + cell.tone,
|
|
459
|
+
title: hover[cell.key] ?? cell.note
|
|
460
|
+
}, cellText(copy, cell))));
|
|
461
|
+
}
|
|
462
|
+
/** The search field above the list. */
|
|
463
|
+
function searchField(copy, query, onQuery) {
|
|
464
|
+
return (0, react.createElement)("div", { className: "evo-hist-search" }, (0, react.createElement)("input", {
|
|
465
|
+
type: "search",
|
|
466
|
+
value: query,
|
|
467
|
+
placeholder: copy.t("search"),
|
|
468
|
+
"aria-label": copy.t("search"),
|
|
469
|
+
onChange: (event) => {
|
|
470
|
+
onQuery(event.target.value);
|
|
471
|
+
}
|
|
472
|
+
}));
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* One skill in the left column: its name and size, then what it is and when it last changed.
|
|
476
|
+
* @param copy - the dictionary seat.
|
|
477
|
+
* @param row - the skill's facts and cells.
|
|
478
|
+
* @param selected - whether this is the open skill.
|
|
479
|
+
* @param onOpen - what a click does.
|
|
480
|
+
* @returns the row.
|
|
481
|
+
*/
|
|
482
|
+
function skillLine(copy, row, selected, onOpen) {
|
|
483
|
+
return (0, react.createElement)("button", {
|
|
484
|
+
key: row.name,
|
|
485
|
+
type: "button",
|
|
486
|
+
className: "evo-hist-skill",
|
|
487
|
+
"aria-current": selected ? "true" : void 0,
|
|
488
|
+
onClick: onOpen
|
|
489
|
+
}, cellLine(row.cells.title, copy, "evo-hist-cell-line"), cellLine(row.cells.meta, copy, "evo-hist-cell-line evo-hist-cell-meta"));
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* One version row: what happened, when, how big — then the controls, then whatever is expanded.
|
|
493
|
+
*
|
|
494
|
+
* The write is the only control that can change anything, and it is the only one that ever takes the
|
|
495
|
+
* primary tone; a toggle that merely reveals more is borderless and carries a drawn chevron, so the
|
|
496
|
+
* two kinds cannot be mistaken for each other (W7/W14).
|
|
497
|
+
* @param props - the row, the copy, and the panel's callbacks.
|
|
498
|
+
* @returns the row.
|
|
499
|
+
*/
|
|
500
|
+
function versionRow(props) {
|
|
501
|
+
const { copy, row, confirming, open, expanded, onRestore, onCancel, onToggle } = props;
|
|
502
|
+
const actions = [];
|
|
503
|
+
if (row.undoable === false) actions.push(chip({
|
|
504
|
+
key: "current",
|
|
505
|
+
children: copy.t("current")
|
|
506
|
+
}));
|
|
507
|
+
if (row.undoable === true) {
|
|
508
|
+
if (confirming) actions.push(button({
|
|
509
|
+
key: "cancel",
|
|
510
|
+
onClick: onCancel,
|
|
511
|
+
children: copy.t("undo.no")
|
|
512
|
+
}));
|
|
513
|
+
actions.push(button({
|
|
514
|
+
key: "undo",
|
|
515
|
+
tone: confirming ? "primary" : "quiet",
|
|
516
|
+
onClick: onRestore,
|
|
517
|
+
children: confirming ? copy.t("undo.confirm") : copy.t("undo")
|
|
518
|
+
}));
|
|
519
|
+
}
|
|
520
|
+
if (row.undoable === true || row.charsDelta !== void 0) actions.push(button({
|
|
521
|
+
key: "diff",
|
|
522
|
+
kind: "toggle",
|
|
523
|
+
expanded: open === "diff",
|
|
524
|
+
onClick: () => {
|
|
525
|
+
onToggle("diff");
|
|
526
|
+
},
|
|
527
|
+
children: open === "diff" ? copy.t("diff.hide") : copy.t("diff.show")
|
|
528
|
+
}));
|
|
529
|
+
actions.push(button({
|
|
530
|
+
key: "preview",
|
|
531
|
+
kind: "toggle",
|
|
532
|
+
expanded: open === "preview",
|
|
533
|
+
onClick: () => {
|
|
534
|
+
onToggle("preview");
|
|
535
|
+
},
|
|
536
|
+
children: open === "preview" ? copy.t("preview.hide") : copy.t("preview.show")
|
|
537
|
+
}));
|
|
538
|
+
return (0, react.createElement)("div", {
|
|
539
|
+
key: String(row.v),
|
|
540
|
+
className: "evo-hist-row"
|
|
541
|
+
}, (0, react.createElement)("div", { className: "evo-hist-row-body" }, cellLine(row.cells.title, copy, "evo-hist-row-title", { time: localTime(row.at) }), row.cells.meta.length === 0 ? null : cellLine(row.cells.meta, copy, "evo-hist-row-summary"), expanded), (0, react.createElement)("div", { className: "evo-hist-actions" }, actions));
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* One group's heading, and the note that explains the group.
|
|
545
|
+
* @param title - the group's name, already resolved.
|
|
546
|
+
* @param noteText - the group's note, when it has one.
|
|
547
|
+
* @returns the heading and the note, for the caller to place in its own section.
|
|
548
|
+
*/
|
|
549
|
+
function groupHeading(title, noteText) {
|
|
550
|
+
return [(0, react.createElement)("h3", {
|
|
551
|
+
key: "title",
|
|
552
|
+
className: "evo-hist-group"
|
|
553
|
+
}, title), noteText === void 0 ? null : (0, react.createElement)("p", {
|
|
554
|
+
key: "note",
|
|
555
|
+
className: "evo-hist-hint"
|
|
556
|
+
}, noteText)];
|
|
557
|
+
}
|
|
558
|
+
/** What the list shows when it has nothing to show. */
|
|
559
|
+
function emptyState(text) {
|
|
560
|
+
return (0, react.createElement)("p", { className: "evo-hist-empty" }, text);
|
|
561
|
+
}
|
|
562
|
+
/** One notice about what just happened. */
|
|
563
|
+
function notice(text, error) {
|
|
564
|
+
if (text === void 0) return null;
|
|
565
|
+
return note({
|
|
566
|
+
error,
|
|
567
|
+
children: text
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
/** The pane's heading and its one control. */
|
|
571
|
+
function panelHead(copy, onRefresh) {
|
|
572
|
+
return (0, react.createElement)("div", { className: "evo-hist-head" }, (0, react.createElement)("h2", { className: "evo-hist-title" }, copy.t("title")), button({
|
|
573
|
+
key: "refresh",
|
|
574
|
+
title: copy.t("refresh"),
|
|
575
|
+
onClick: onRefresh,
|
|
576
|
+
children: copy.t("refresh")
|
|
577
|
+
}));
|
|
578
|
+
}
|
|
579
|
+
//#endregion
|
|
304
580
|
//#region src/client/markdown.ts
|
|
305
581
|
/**
|
|
306
582
|
* The one seam onto the platform's Markdown renderer.
|
|
@@ -318,16 +594,32 @@ window.__ModuleLoader__.load({
|
|
|
318
594
|
/** The module-table specifier; kept in one place so the seam is the only reader. */
|
|
319
595
|
const PLATFORM_UI_MODULE = "@deepseek-ai/dsh-client-ui-primitives";
|
|
320
596
|
/**
|
|
597
|
+
* The renderer a module row carries, when it carries one.
|
|
598
|
+
*
|
|
599
|
+
* A React component type is NOT always a function: the platform's own `MarkdownText` is
|
|
600
|
+
* `memo(function MarkdownText …)`, and `memo` returns an OBJECT. A `typeof === 'function'` test
|
|
601
|
+
* therefore rejected the real renderer and silently degraded every view to its source — exactly
|
|
602
|
+
* what 0.14.0 shipped (measured on the installed panel 2026-09-28: `[预览]` and the rendered diff
|
|
603
|
+
* both fell back). The accepted domain is React's element-type domain: a function, or a non-null
|
|
604
|
+
* object it can mount. `tests/markdown.client.spec.ts` pins all four cases.
|
|
605
|
+
* @param row - what the client module table handed over.
|
|
606
|
+
* @returns the component, or undefined when the row carries none.
|
|
607
|
+
*/
|
|
608
|
+
function rendererOf(row) {
|
|
609
|
+
const candidate = row.MarkdownText;
|
|
610
|
+
if (typeof candidate === "function") return candidate;
|
|
611
|
+
if (typeof candidate === "object" && candidate !== null) return candidate;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
321
614
|
* The platform component, when this deployment's module table carries one.
|
|
322
615
|
*
|
|
323
|
-
* A
|
|
324
|
-
*
|
|
616
|
+
* A row that IS reachable without a renderer is not a render: reporting one would move the failure
|
|
617
|
+
* into the render itself, where only the slot's error boundary would catch it.
|
|
325
618
|
* @returns the component, or undefined when it cannot be reached.
|
|
326
619
|
*/
|
|
327
620
|
function markdownText() {
|
|
328
621
|
try {
|
|
329
|
-
|
|
330
|
-
return typeof row.MarkdownText === "function" ? row.MarkdownText : void 0;
|
|
622
|
+
return rendererOf(require(PLATFORM_UI_MODULE));
|
|
331
623
|
} catch {
|
|
332
624
|
return;
|
|
333
625
|
}
|
|
@@ -379,105 +671,58 @@ window.__ModuleLoader__.load({
|
|
|
379
671
|
};
|
|
380
672
|
}
|
|
381
673
|
//#endregion
|
|
382
|
-
//#region src/client/
|
|
674
|
+
//#region src/client/document.ts
|
|
383
675
|
/**
|
|
384
|
-
* The
|
|
385
|
-
* and one action per body version (restore, behind an inline confirmation).
|
|
676
|
+
* The READING face: one version rendered as a document, and the rendered diff.
|
|
386
677
|
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
678
|
+
* This file is the only place that decides how a document reads (G4). The tool face (`chrome.ts`)
|
|
679
|
+
* picks versions and acts on them; here the rules are the reader's, and they follow the positioning
|
|
680
|
+
* ruling of §14.0: a document is read, not scanned — one measure, page-like spacing, no inner scroll
|
|
681
|
+
* (W18/W20/W21/W22). Nothing here is a control except the source/rendered choice, which belongs to
|
|
682
|
+
* the reader rather than to the panel's actions.
|
|
391
683
|
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
* discarded by its ticket; and the diff is fetched only when a row is expanded.
|
|
396
|
-
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
397
|
-
*/
|
|
398
|
-
/** Cut a description to one line's worth of characters (the CSS ellipsises the rest). */
|
|
399
|
-
function oneLine(text) {
|
|
400
|
-
const firstLine = text.split("\n")[0] ?? "";
|
|
401
|
-
return firstLine.length > 120 ? firstLine.slice(0, 119) + "…" : firstLine;
|
|
402
|
-
}
|
|
403
|
-
/** The local time an ISO timestamp names, as the operator's own clock shows it. */
|
|
404
|
-
function localTime(at) {
|
|
405
|
-
const parsed = Date.parse(at);
|
|
406
|
-
if (Number.isNaN(parsed)) return at;
|
|
407
|
-
const date = new Date(parsed);
|
|
408
|
-
const pad = (value) => String(value).padStart(2, "0");
|
|
409
|
-
return String(date.getFullYear()) + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes());
|
|
410
|
-
}
|
|
411
|
-
/** The state word one skill row carries, from the facts the listing reported. */
|
|
412
|
-
function stateKey(skill) {
|
|
413
|
-
if (skill.protectionUnknown) return "state.unknown";
|
|
414
|
-
if (skill.protectedBy !== null) return "state.protected";
|
|
415
|
-
return skill.managed ? "state.managed" : "state.foreign";
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* The one clause an action word needs: what the recorded artifact IS. It rides the action word's
|
|
419
|
-
* `title` instead of widening the row (a parenthetical in the sentence pushed time and size aside).
|
|
420
|
-
* @param face - copy plus the data callbacks.
|
|
421
|
-
* @param row - the version row whose action is being named.
|
|
422
|
-
* @returns the clause, or undefined for the actions that need none.
|
|
423
|
-
*/
|
|
424
|
-
function actionNote(face, row) {
|
|
425
|
-
if (row.actionKind === "delete") return face.t("action.delete.note");
|
|
426
|
-
if (row.actionKind === "archive") return face.t("action.archive.note");
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* The lazy-read cache without one key.
|
|
684
|
+
* The text arrives already prepared: the host strips the frontmatter block through core's
|
|
685
|
+
* `displayBodyOf`, because a browser half cannot import core. The RENDERED DIFF keeps the whole text
|
|
686
|
+
* on purpose — a diff is about what CHANGED, and a metadata change is a change.
|
|
430
687
|
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
* @param current - the cache.
|
|
435
|
-
* @param key - the version whose entry is dropped.
|
|
436
|
-
* @returns a new cache without that key.
|
|
688
|
+
* It degrades rather than blanking: when the platform's renderer cannot be reached, the exact text is
|
|
689
|
+
* shown instead, and the reader can still read the version.
|
|
690
|
+
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
437
691
|
*/
|
|
438
|
-
function without(current, key) {
|
|
439
|
-
const next = new Map(current);
|
|
440
|
-
next.delete(key);
|
|
441
|
-
return next;
|
|
442
|
-
}
|
|
443
|
-
/** The relative-time sentence for one row's bucket. */
|
|
444
|
-
function ageText(face, age) {
|
|
445
|
-
if (age.unit === "now") return face.t("time.now");
|
|
446
|
-
return age.n === 1 ? face.t("time." + age.unit + ".one") : face.format("time." + age.unit, { n: age.n });
|
|
447
|
-
}
|
|
448
|
-
/** The character delta of one row, or null when it is the first version. */
|
|
449
|
-
function deltaText(face, row) {
|
|
450
|
-
if (row.charsDelta === void 0 || row.charsDelta === 0) return null;
|
|
451
|
-
return row.charsDelta > 0 ? face.format("row.delta.up", { n: row.charsDelta }) : face.format("row.delta.down", { n: -row.charsDelta });
|
|
452
|
-
}
|
|
453
692
|
/** One `<pre>` rendering of a diff: added lines marked `+`, removed ones `-`. */
|
|
454
|
-
function
|
|
693
|
+
function sourceBlock(diff) {
|
|
455
694
|
const rows = [];
|
|
456
695
|
for (const hunk of diff.hunks) {
|
|
457
696
|
for (const [index, line] of hunk.oldText.split("\n").entries()) {
|
|
458
697
|
if (hunk.oldText === "" && line === "") continue;
|
|
459
698
|
rows.push((0, react.createElement)("div", {
|
|
460
699
|
key: "old-" + String(index),
|
|
461
|
-
className: "evo-
|
|
700
|
+
className: "evo-doc-line-del"
|
|
462
701
|
}, "- " + line));
|
|
463
702
|
}
|
|
464
703
|
for (const [index, line] of hunk.newText.split("\n").entries()) {
|
|
465
704
|
if (hunk.newText === "" && line === "") continue;
|
|
466
705
|
rows.push((0, react.createElement)("div", {
|
|
467
706
|
key: "new-" + String(index),
|
|
468
|
-
className: "evo-
|
|
707
|
+
className: "evo-doc-line-add"
|
|
469
708
|
}, "+ " + line));
|
|
470
709
|
}
|
|
471
710
|
}
|
|
472
|
-
return (0, react.createElement)("pre", { className: "evo-
|
|
711
|
+
return (0, react.createElement)("pre", { className: "evo-doc-source" }, rows.length === 0 ? "±" : rows);
|
|
712
|
+
}
|
|
713
|
+
/** One rendered side of a diff: an \u201cold\u201d or \u201cnew\u201d block under a tinted edge. */
|
|
714
|
+
function renderedSide(face, mark, side, text) {
|
|
715
|
+
const rendered = renderMarkdown(text, face.markdownWords);
|
|
716
|
+
if (!rendered.ok) return null;
|
|
717
|
+
return (0, react.createElement)("div", { className: "evo-doc-block evo-doc-" + side }, (0, react.createElement)("span", { className: "evo-doc-tag" }, mark), rendered.node);
|
|
473
718
|
}
|
|
474
719
|
/**
|
|
475
|
-
* The same diff, both sides rendered as Markdown and stacked: the removed text first, then the
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
* @param face -
|
|
720
|
+
* The same diff, both sides rendered as Markdown and stacked: the removed text first, then the added
|
|
721
|
+
* one. Block level on purpose — line-level interleaving of two rendered documents is a different (and
|
|
722
|
+
* much larger) problem, and the source view stays one click away for exact bytes.
|
|
723
|
+
* @param face - the dictionary seat and the renderer's words.
|
|
479
724
|
* @param diff - the windowed change the host reported.
|
|
480
|
-
* @returns the stacked blocks.
|
|
725
|
+
* @returns the stacked blocks, or null when the renderer is out.
|
|
481
726
|
*/
|
|
482
727
|
function renderedBlocks(face, diff) {
|
|
483
728
|
const blocks = [];
|
|
@@ -497,7 +742,7 @@ window.__ModuleLoader__.load({
|
|
|
497
742
|
}
|
|
498
743
|
/**
|
|
499
744
|
* One diff in the mode the reader asked for, degrading to the source view when the renderer is out.
|
|
500
|
-
* @param face -
|
|
745
|
+
* @param face - the dictionary seat and the renderer's words.
|
|
501
746
|
* @param diff - the windowed change the host reported.
|
|
502
747
|
* @param mode - source or rendered.
|
|
503
748
|
* @returns the element to draw.
|
|
@@ -505,119 +750,36 @@ window.__ModuleLoader__.load({
|
|
|
505
750
|
function diffView(face, diff, mode) {
|
|
506
751
|
if (mode === "rendered") {
|
|
507
752
|
const blocks = renderedBlocks(face, diff);
|
|
508
|
-
if (blocks !== null) return (0, react.createElement)("div", { className: "evo-
|
|
753
|
+
if (blocks !== null) return (0, react.createElement)("div", { className: "evo-doc-render" }, blocks);
|
|
509
754
|
}
|
|
510
|
-
return
|
|
511
|
-
}
|
|
512
|
-
/** One version row: what happened, when, how big, and what it is against. */
|
|
513
|
-
function versionRow(face, row, confirming, expanded, diff, body, restore, cancelRestore, toggle) {
|
|
514
|
-
const showing = (kind) => expanded !== void 0 && expanded.v === row.v && expanded.kind === kind;
|
|
515
|
-
const parts = [
|
|
516
|
-
(0, react.createElement)("span", {
|
|
517
|
-
key: "v",
|
|
518
|
-
className: "evo-hist-row-meta"
|
|
519
|
-
}, "v" + String(row.v)),
|
|
520
|
-
(0, react.createElement)("span", {
|
|
521
|
-
key: "action",
|
|
522
|
-
title: actionNote(face, row)
|
|
523
|
-
}, face.t("action." + row.actionKind)),
|
|
524
|
-
(0, react.createElement)("span", {
|
|
525
|
-
key: "time",
|
|
526
|
-
className: "evo-hist-row-meta",
|
|
527
|
-
title: localTime(row.at)
|
|
528
|
-
}, ageText(face, row.age))
|
|
529
|
-
];
|
|
530
|
-
if (row.path !== void 0) parts.push((0, react.createElement)("span", {
|
|
531
|
-
key: "path",
|
|
532
|
-
className: "evo-hist-row-meta",
|
|
533
|
-
title: row.path
|
|
534
|
-
}, row.path));
|
|
535
|
-
else if (row.actionKind === "support-write" || row.actionKind === "support-remove") parts.push((0, react.createElement)("span", {
|
|
536
|
-
key: "path",
|
|
537
|
-
className: "evo-hist-row-meta"
|
|
538
|
-
}, face.t("path.unrecorded")));
|
|
539
|
-
const delta = deltaText(face, row);
|
|
540
|
-
if (delta !== null) parts.push((0, react.createElement)("span", {
|
|
541
|
-
key: "delta",
|
|
542
|
-
className: "evo-hist-row-meta"
|
|
543
|
-
}, delta));
|
|
544
|
-
const actions = [];
|
|
545
|
-
if (row.undoable === false) actions.push((0, react.createElement)("span", {
|
|
546
|
-
key: "current",
|
|
547
|
-
className: "evo-hist-capsule"
|
|
548
|
-
}, face.t("current")));
|
|
549
|
-
if (row.undoable === true) {
|
|
550
|
-
if (confirming === row.v) actions.push((0, react.createElement)("button", {
|
|
551
|
-
key: "cancel",
|
|
552
|
-
type: "button",
|
|
553
|
-
className: "evo-hist-button",
|
|
554
|
-
onClick: () => {
|
|
555
|
-
cancelRestore();
|
|
556
|
-
}
|
|
557
|
-
}, face.t("undo.no")));
|
|
558
|
-
actions.push((0, react.createElement)("button", {
|
|
559
|
-
key: "undo",
|
|
560
|
-
type: "button",
|
|
561
|
-
className: "evo-hist-button",
|
|
562
|
-
"data-tone": confirming === row.v ? "primary" : void 0,
|
|
563
|
-
onClick: () => {
|
|
564
|
-
restore(row.v);
|
|
565
|
-
}
|
|
566
|
-
}, confirming === row.v ? face.t("undo.confirm") : face.t("undo")));
|
|
567
|
-
}
|
|
568
|
-
if (row.undoable === true || row.charsDelta !== void 0) actions.push((0, react.createElement)("button", {
|
|
569
|
-
key: "diff",
|
|
570
|
-
type: "button",
|
|
571
|
-
className: "evo-hist-button",
|
|
572
|
-
onClick: () => {
|
|
573
|
-
toggle(row.v, "diff");
|
|
574
|
-
}
|
|
575
|
-
}, showing("diff") ? face.t("diff.hide") : face.t("diff.show")));
|
|
576
|
-
actions.push((0, react.createElement)("button", {
|
|
577
|
-
key: "preview",
|
|
578
|
-
type: "button",
|
|
579
|
-
className: "evo-hist-button",
|
|
580
|
-
onClick: () => {
|
|
581
|
-
toggle(row.v, "preview");
|
|
582
|
-
}
|
|
583
|
-
}, showing("preview") ? face.t("preview.hide") : face.t("preview.show")));
|
|
584
|
-
return (0, react.createElement)("div", {
|
|
585
|
-
key: String(row.v),
|
|
586
|
-
className: "evo-hist-row"
|
|
587
|
-
}, (0, react.createElement)("div", { className: "evo-hist-row-body" }, (0, react.createElement)("div", { className: "evo-hist-row-title" }, parts), row.summary === void 0 || row.summary === "" ? null : (0, react.createElement)("div", { className: "evo-hist-row-summary" }, row.summary), showing("diff") && diff !== void 0 ? (0, react.createElement)(DiffBody, {
|
|
588
|
-
face,
|
|
589
|
-
diff
|
|
590
|
-
}) : null, showing("preview") && body !== void 0 ? (0, react.createElement)(BodyView, {
|
|
591
|
-
face,
|
|
592
|
-
body
|
|
593
|
-
}) : null), (0, react.createElement)("div", { className: "evo-hist-actions" }, actions));
|
|
594
|
-
}
|
|
595
|
-
/** One rendered side of a diff: an "old" or "new" block under a tinted edge. */
|
|
596
|
-
function renderedSide(face, mark, side, text) {
|
|
597
|
-
const rendered = renderMarkdown(text, face.markdownWords);
|
|
598
|
-
if (!rendered.ok) return null;
|
|
599
|
-
return (0, react.createElement)("div", { className: "evo-hist-render-block evo-hist-render-" + side }, (0, react.createElement)("span", { className: "evo-hist-render-tag" }, mark), rendered.node);
|
|
755
|
+
return sourceBlock(diff);
|
|
600
756
|
}
|
|
601
757
|
/**
|
|
602
758
|
* One version read as a document: the platform renderer when it is there, plain text when it is not.
|
|
603
|
-
*
|
|
759
|
+
*
|
|
760
|
+
* The document is the page, not a card inside it: no border, no surface of its own and no inner
|
|
761
|
+
* scroll, so the pane's own scrollbar is the only one a reader meets (W20/W21). A body the host
|
|
762
|
+
* bounded says how much it is not showing, with both numbers (W23).
|
|
604
763
|
* @param props - the face and the body state to draw.
|
|
605
764
|
* @returns the element.
|
|
606
765
|
*/
|
|
607
|
-
function
|
|
766
|
+
function PreviewBody(props) {
|
|
608
767
|
const { face, body } = props;
|
|
609
|
-
if (body.kind === "loading") return (
|
|
610
|
-
if (body.kind === "failed") return (
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
}
|
|
614
|
-
const rendered = renderMarkdown(body.body.
|
|
615
|
-
return (0, react.createElement)("div", { className: "evo-
|
|
768
|
+
if (body.kind === "loading") return note({ children: face.t("diff.loading") });
|
|
769
|
+
if (body.kind === "failed") return note({
|
|
770
|
+
error: true,
|
|
771
|
+
children: body.message
|
|
772
|
+
});
|
|
773
|
+
const rendered = renderMarkdown(body.body.display, face.markdownWords);
|
|
774
|
+
return (0, react.createElement)("div", { className: "evo-doc-preview" }, body.body.truncated ? note({ children: face.format("preview.truncated", {
|
|
775
|
+
shown: body.body.display.length,
|
|
776
|
+
total: body.body.chars
|
|
777
|
+
}) }) : null, rendered.ok ? rendered.node : (0, react.createElement)("pre", { className: "evo-doc-source" }, body.body.display));
|
|
616
778
|
}
|
|
617
779
|
/**
|
|
618
780
|
* The expanded body of one row: the diff, a loading line, or the refusal.
|
|
619
781
|
*
|
|
620
|
-
* The source/rendered
|
|
782
|
+
* The source/rendered choice is LOCAL to this view: which row is open is the panel's business, but
|
|
621
783
|
* how one open diff is displayed is not, so the state does not climb into the parent.
|
|
622
784
|
* @param props - the face and the diff state to draw.
|
|
623
785
|
* @returns the element.
|
|
@@ -625,36 +787,44 @@ window.__ModuleLoader__.load({
|
|
|
625
787
|
function DiffBody(props) {
|
|
626
788
|
const { face, diff } = props;
|
|
627
789
|
const [mode, setMode] = (0, react.useState)("source");
|
|
628
|
-
if (diff.kind === "loading") return (
|
|
629
|
-
if (diff.kind === "failed") return (
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}
|
|
790
|
+
if (diff.kind === "loading") return note({ children: face.t("diff.loading") });
|
|
791
|
+
if (diff.kind === "failed") return note({
|
|
792
|
+
error: true,
|
|
793
|
+
children: diff.message
|
|
794
|
+
});
|
|
633
795
|
const against = diff.diff.against === null ? face.t("diff.first") : face.format("diff.against", { n: diff.diff.against });
|
|
634
|
-
const toggle = (0, react.createElement)("div", { className: "evo-
|
|
796
|
+
const toggle = (0, react.createElement)("div", { className: "evo-doc-toggle" }, button({
|
|
635
797
|
key: "source",
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
"data-tone": mode === "source" ? "primary" : void 0,
|
|
798
|
+
kind: "switch",
|
|
799
|
+
pressed: mode === "source",
|
|
639
800
|
onClick: () => {
|
|
640
801
|
setMode("source");
|
|
641
|
-
}
|
|
642
|
-
|
|
802
|
+
},
|
|
803
|
+
children: face.t("diff.source")
|
|
804
|
+
}), button({
|
|
643
805
|
key: "rendered",
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
"data-tone": mode === "rendered" ? "primary" : void 0,
|
|
806
|
+
kind: "switch",
|
|
807
|
+
pressed: mode === "rendered",
|
|
647
808
|
onClick: () => {
|
|
648
809
|
setMode("rendered");
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
/** One group: a heading, an optional note, and its rows. */
|
|
654
|
-
function group(face, title, entries, noteText, state) {
|
|
655
|
-
if (entries.length === 0) return null;
|
|
656
|
-
return (0, react.createElement)("section", { key: title }, (0, react.createElement)("h3", { className: "evo-hist-group" }, title), noteText === void 0 ? null : (0, react.createElement)("p", { className: "evo-hist-hint" }, noteText), ...entries.map((entry) => versionRow(face, entry, state.confirming, state.expanded, state.diffs.get(entry.v), state.bodies.get(entry.v), state.restore, state.cancelRestore, state.toggle)));
|
|
810
|
+
},
|
|
811
|
+
children: face.t("diff.rendered")
|
|
812
|
+
}));
|
|
813
|
+
return (0, react.createElement)("div", { className: "evo-doc-diff" }, (0, react.createElement)("div", { className: "evo-doc-head" }, note({ children: against + " · " + face.format("diff.added", { n: diff.diff.linesAdded }) + " · " + face.format("diff.removed", { n: diff.diff.linesRemoved }) + (diff.diff.truncated ? " · " + face.t("diff.truncated") : "") }), toggle), diffView(face, diff.diff, mode));
|
|
657
814
|
}
|
|
815
|
+
//#endregion
|
|
816
|
+
//#region src/client/Panel.ts
|
|
817
|
+
/**
|
|
818
|
+
* The panel: one read, one open skill, one open row — and the faces that draw them.
|
|
819
|
+
*
|
|
820
|
+
* Everything a PERSON reads is decided elsewhere: what a row says is core's (`skill-row-cells.ts`,
|
|
821
|
+
* shipped with the row), the words are the dictionary's, the tool face is `chrome.ts` and the reading
|
|
822
|
+
* face is `document.ts`. What is left here is the STATE — which skill is open, which row is expanded,
|
|
823
|
+
* which reads are in flight — and the two rules that only a state machine can hold: a read takes a
|
|
824
|
+
* ticket so a slow reply cannot land on another skill, and a write reports its own result instead of
|
|
825
|
+
* being erased by the read that follows it.
|
|
826
|
+
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
827
|
+
*/
|
|
658
828
|
/**
|
|
659
829
|
* The panel body.
|
|
660
830
|
* @param face - copy plus the data callbacks.
|
|
@@ -713,7 +883,7 @@ window.__ModuleLoader__.load({
|
|
|
713
883
|
}
|
|
714
884
|
showVersions(name);
|
|
715
885
|
};
|
|
716
|
-
/** What the refresh
|
|
886
|
+
/** What the refresh control does: both reads again, and the sentence stays. */
|
|
717
887
|
const refresh = () => {
|
|
718
888
|
reloadSkills();
|
|
719
889
|
if (selected !== void 0) showVersions(selected);
|
|
@@ -802,44 +972,65 @@ window.__ModuleLoader__.load({
|
|
|
802
972
|
const needle = query.trim().toLowerCase();
|
|
803
973
|
const visible = (0, react.useMemo)(() => needle === "" ? skills : skills.filter((skill) => skill.name.toLowerCase().includes(needle) || skill.description.toLowerCase().includes(needle)), [skills, needle]);
|
|
804
974
|
const loaded = payload !== void 0;
|
|
805
|
-
/** Drop the pending confirmation: the
|
|
975
|
+
/** Drop the pending confirmation: the control that opened it must not be the only way out. */
|
|
806
976
|
const cancelRestore = () => {
|
|
807
977
|
setPending(void 0);
|
|
808
978
|
};
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
expanded
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
return (0, react.createElement)("div", { className: "evo-hist-root" }, (0, react.createElement)("aside", { className: "evo-hist-aside" }, (0, react.createElement)("div", { className: "evo-hist-search" }, (0, react.createElement)("input", {
|
|
819
|
-
type: "search",
|
|
820
|
-
value: query,
|
|
821
|
-
placeholder: face.t("search"),
|
|
822
|
-
"aria-label": face.t("search"),
|
|
823
|
-
onChange: (event) => {
|
|
824
|
-
setQuery(event.target.value);
|
|
825
|
-
}
|
|
826
|
-
})), (0, react.createElement)("div", { className: "evo-hist-list" }, skills.length === 0 ? (0, react.createElement)("p", { className: "evo-hist-empty" }, face.t("empty.skills")) : visible.length === 0 ? (0, react.createElement)("p", { className: "evo-hist-empty" }, face.t("search.none")) : visible.map((skill) => (0, react.createElement)("button", {
|
|
827
|
-
key: skill.name,
|
|
828
|
-
type: "button",
|
|
829
|
-
className: "evo-hist-skill",
|
|
830
|
-
title: skill.name,
|
|
831
|
-
"aria-current": skill.name === selected ? "true" : void 0,
|
|
832
|
-
onClick: () => {
|
|
833
|
-
open(skill.name);
|
|
979
|
+
/** What one row shows when it is the open one. */
|
|
980
|
+
const expandedOf = (row) => {
|
|
981
|
+
if (expanded === void 0 || expanded.v !== row.v) return null;
|
|
982
|
+
if (expanded.kind === "diff") {
|
|
983
|
+
const state = diffs.get(row.v);
|
|
984
|
+
return state === void 0 ? null : (0, react.createElement)(DiffBody, {
|
|
985
|
+
face,
|
|
986
|
+
diff: state
|
|
987
|
+
});
|
|
834
988
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
989
|
+
const state = bodies.get(row.v);
|
|
990
|
+
return state === void 0 ? null : (0, react.createElement)(PreviewBody, {
|
|
991
|
+
face,
|
|
992
|
+
body: state
|
|
993
|
+
});
|
|
994
|
+
};
|
|
995
|
+
/** One group: a heading, an optional note, and its rows. */
|
|
996
|
+
const group = (title, entries, noteText2) => {
|
|
997
|
+
if (entries.length === 0) return null;
|
|
998
|
+
return (0, react.createElement)("section", { key: title }, ...groupHeading(title, noteText2), ...entries.map((row) => versionRow({
|
|
999
|
+
copy: face,
|
|
1000
|
+
row,
|
|
1001
|
+
confirming: pending === row.v,
|
|
1002
|
+
open: expanded !== void 0 && expanded.v === row.v ? expanded.kind : void 0,
|
|
1003
|
+
expanded: expandedOf(row),
|
|
1004
|
+
onRestore: () => {
|
|
1005
|
+
restore(row.v);
|
|
1006
|
+
},
|
|
1007
|
+
onCancel: cancelRestore,
|
|
1008
|
+
onToggle: (kind) => {
|
|
1009
|
+
toggle(row.v, kind);
|
|
1010
|
+
}
|
|
1011
|
+
})));
|
|
1012
|
+
};
|
|
1013
|
+
return (0, react.createElement)("div", { className: "evo-hist-root" }, (0, react.createElement)("aside", { className: "evo-hist-aside" }, searchField(face, query, setQuery), (0, react.createElement)("div", { className: "evo-hist-list" }, skills.length === 0 ? emptyState(face.t("empty.skills")) : visible.length === 0 ? emptyState(face.t("search.none")) : visible.map((skill) => skillLine(face, skill, skill.name === selected, () => {
|
|
1014
|
+
open(skill.name);
|
|
1015
|
+
})))), (0, react.createElement)("div", { className: "evo-hist-main" }, panelHead(face, refresh), notice(noteText, noteError), selected === void 0 ? [emptyState(face.t(skills.length === 0 ? "empty.skills" : "empty.pick")), (0, react.createElement)("p", {
|
|
1016
|
+
key: "hint",
|
|
1017
|
+
className: "evo-hist-hint"
|
|
1018
|
+
}, face.t("hint"))] : loaded ? null : emptyState(face.t("loading")), loaded ? (0, react.createElement)("div", null, group(face.t("group.content"), payload.content, void 0), group(face.t("group.support"), payload.support, face.t("group.support.note")), payload.content.length === 0 && payload.support.length === 0 ? (0, react.createElement)("p", { className: "evo-hist-hint" }, face.t("empty.versions")) : null) : null));
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* The lazy-read cache without one key.
|
|
1022
|
+
*
|
|
1023
|
+
* A read that a later toggle superseded must leave nothing behind: `loading` is not an answer, and
|
|
1024
|
+
* the cache also serves as the \u201cdo not read twice\u201d guard, so an abandoned entry would refuse every
|
|
1025
|
+
* later read of that row and leave it saying \u201cloading\u201d until the page is reloaded.
|
|
1026
|
+
* @param current - the cache.
|
|
1027
|
+
* @param key - the version whose entry is dropped.
|
|
1028
|
+
* @returns a new cache without that key.
|
|
1029
|
+
*/
|
|
1030
|
+
function without(current, key) {
|
|
1031
|
+
const next = new Map(current);
|
|
1032
|
+
next.delete(key);
|
|
1033
|
+
return next;
|
|
843
1034
|
}
|
|
844
1035
|
//#endregion
|
|
845
1036
|
//#region src/client/seam.ts
|