@lmzhen/dsh-evolution-skill-history 0.13.1 → 0.13.3

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 (3) hide show
  1. package/README.md +5 -3
  2. package/lib/client.js +101 -45
  3. package/package.json +7 -7
package/README.md CHANGED
@@ -33,13 +33,15 @@ The row is **inert without a web server** (`ctx.get('webServer')`, not a declare
33
33
 
34
34
  ## What the panel shows
35
35
 
36
- **The left column** lists the skills that recorded versions, one row per skill: its name, how many versions it has, its own one-line description (the listing already carried it) and the management state the marker probe reported. A **search box** filters that list by name or description — client state only, no route behind it.
36
+ **The left column** lists the skills that recorded versions, one row per skill: its name, how many versions it has, its own one-line description, clipped with an ellipsis (the listing already carried it) and the management state the marker probe reported. A **search box** filters that list by name or description — client state only, no route behind it.
37
37
 
38
- **The right pane** shows the selected skill's versions as two groups: **Body versions** (restorable) and **File versions** (history only, because a restore rewrites `SKILL.md`). A body row reads like a sentence rather than a dump — `v3 targeted edit 2026-09-27 18:20 · 3 hours ago (+1363 chars)` — with a *current* capsule on the live content, **Restore this version** (an inline two-click confirmation) on the others, and **Diff** expanding that version against its predecessor (lines added and removed, plus the changed region, marked truncated when it is long). After a restore the rows are read again — the restore itself is a new version — while the curator's result sentence stays on screen. **Refresh** re-reads the skill list and the open skill.
38
+ **The right pane** shows the selected skill's versions as two groups: **Body versions** (restorable) and **File versions** (history only, because a restore rewrites `SKILL.md`). A body row reads like a sentence rather than a dump — `v3 targeted edit 3 hours ago (+1363 chars)`, where the absolute clock is the row's hover text — with a *current* capsule on the live content, **Restore this version** (an inline two-click confirmation, with a **Cancel** beside it once it is armed) on the others, and **Diff** expanding that version against its predecessor (lines added and removed, plus the changed region, marked truncated when it is long). After a restore the rows are read again — the restore itself is a new version — while the curator's result sentence stays on screen. **Refresh** re-reads the skill list and the open skill.
39
+
40
+ While nothing is selected the pane says so — a line pointing at the left column, or the empty listing when no skill has a history yet.
39
41
 
40
42
  **The panel owns no rule and no arithmetic.** The host reports every row with its whole verdict — `undoable` (evolution-core's artifact classification against the live bytes), `actionKind` (a closed vocabulary key), `age` (a bucket plus a count), `charsDelta`, `summary` — and this half substitutes words from its locale dictionary and renders. The time, the action words and the sentences are therefore localized; the facts are computed once, host-side, so the panel and the slash commands cannot drift.
41
43
 
42
- **Appearance follows the platform's tokens, not its modules.** The panel carries its own stylesheet (`src/client/styles.ts`, injected once behind `data-plugin-css`) and copies its metrics from the platform's own components — the command card's `<pre>` for the diff block, the sidebar row's hover and focus ring for the controls — while every size is a `--dsh-content-font-size*` token or a `calc()` over one, so the panel follows the deployment's content size the way the platform's panels do. Importing the platform's control package is NOT possible from here without splitting this package's compiler faces: the specifier is path-mapped to source, and `tsc -b` then refuses the foreign project (host/client face split, `react` devDependencies and a CI graph change would all be needed). The roll call of what the panel deliberately does not do is in `## Known limitations and deferred work`.
44
+ **Appearance follows the platform's tokens, not its modules.** The panel carries its own stylesheet (`src/client/styles.ts`, injected once behind `data-plugin-css`) and copies its metrics from the platform's own components — the command card's `<pre>` for the diff block, the sidebar row's hover and focus ring for the controls, and the theme's semantic state pair (`--dsw-alias-state-success-primary` / `--dsw-alias-state-error-primary`) for the diff's added and removed lines — while type follows the shell rather than the conversation: the panel declares no `font-size` of its own (the way the platform's own surfaces declare none) and steps with `calc(1em + 2px)` for the title and `calc(1em - 1px)` for a group label. Only the diff block takes the platform's code-block token — it renders content, not chrome. The setting behind `--dsh-content-font-size*` is documented as affecting conversation content only, so the panel deliberately does not follow it. Importing the platform's control package is NOT possible from here without splitting this package's compiler faces: the specifier is path-mapped to source, and `tsc -b` then refuses the foreign project (host/client face split, `react` devDependencies and a CI graph change would all be needed). The roll call of what the panel deliberately does not do is in `## Known limitations and deferred work`.
43
45
 
44
46
  ## Build
45
47
 
package/lib/client.js CHANGED
@@ -68,8 +68,7 @@ window.__ModuleLoader__.load({
68
68
  const zh = {
69
69
  "entry.label": "技能历史",
70
70
  title: "技能历史",
71
- hint: "每次技能写入留下的内容版本。回退只改 SKILL.md 正文,标记、使用计数与策展状态不变。",
72
- "hint.current": "内容相同的版本会同时标「当前」。",
71
+ hint: "每次技能写入留下的内容版本。回退只改 SKILL.md 正文,标记、使用计数与策展状态不变。内容相同的版本会同时标「当前」。",
73
72
  search: "按名字或描述筛选",
74
73
  "search.none": "没有匹配的技能。",
75
74
  "group.content": "正文版本",
@@ -77,6 +76,7 @@ window.__ModuleLoader__.load({
77
76
  "group.support.note": "附带文件的字节与正文共用一份索引;回退不恢复它们。",
78
77
  "empty.skills": "还没有技能留下过内容版本。",
79
78
  "empty.versions": "这个技能还没有记录版本。",
79
+ "empty.pick": "从左栏选一个技能,看它记录过的版本。",
80
80
  loading: "读取中…",
81
81
  refresh: "刷新",
82
82
  undo: "回退到此版",
@@ -105,8 +105,10 @@ window.__ModuleLoader__.load({
105
105
  "action.patch": "定点修改",
106
106
  "action.update": "整篇更新",
107
107
  "action.restore": "回退",
108
- "action.delete": "删除(存的是被删的正文)",
109
- "action.archive": "归档(存的是被归档的正文)",
108
+ "action.delete": "删除",
109
+ "action.delete.note": "这一版存的是被删掉的正文",
110
+ "action.archive": "归档",
111
+ "action.archive.note": "这一版存的是被归档的正文",
110
112
  "action.consolidate": "合并",
111
113
  "action.restructure": "结构调整",
112
114
  "action.support-write": "写入附带文件",
@@ -130,8 +132,7 @@ window.__ModuleLoader__.load({
130
132
  const en = {
131
133
  "entry.label": "Skill history",
132
134
  title: "Skill history",
133
- hint: "The content versions every skill write leaves behind. Restoring changes the SKILL.md body only: markers, usage counts and curation state stay as they are.",
134
- "hint.current": "Versions holding the same content are all marked “current”.",
135
+ hint: "The content versions every skill write leaves behind. Restoring changes the SKILL.md body only: markers, usage counts and curation state stay as they are. Versions holding the same content are all marked “current”.",
135
136
  search: "Filter by name or description",
136
137
  "search.none": "No skill matches.",
137
138
  "group.content": "Body versions",
@@ -139,6 +140,7 @@ window.__ModuleLoader__.load({
139
140
  "group.support.note": "Support-file bytes share this index with the body; restoring the body does not bring them back.",
140
141
  "empty.skills": "No skill has recorded a content version yet.",
141
142
  "empty.versions": "This skill has no recorded versions.",
143
+ "empty.pick": "Pick a skill on the left to see the versions it recorded.",
142
144
  loading: "Loading…",
143
145
  refresh: "Refresh",
144
146
  undo: "Restore this version",
@@ -167,8 +169,10 @@ window.__ModuleLoader__.load({
167
169
  "action.patch": "targeted edit",
168
170
  "action.update": "whole-body update",
169
171
  "action.restore": "restored",
170
- "action.delete": "deleted (holds the removed body)",
171
- "action.archive": "archived (holds the archived body)",
172
+ "action.delete": "deleted",
173
+ "action.delete.note": "this version holds the removed body",
174
+ "action.archive": "archived",
175
+ "action.archive.note": "this version holds the archived body",
172
176
  "action.consolidate": "merged",
173
177
  "action.restructure": "restructured",
174
178
  "action.support-write": "support file written",
@@ -228,12 +232,12 @@ window.__ModuleLoader__.load({
228
232
  const CSS_TAG_ID = "@lmzhen/dsh-evolution-skill-history/panel.css";
229
233
  /** The stylesheet the panel injects once. */
230
234
  const CSS = [
231
- ".evo-hist-root{display:flex;height:100%;min-height:0;min-width:0;font-size:var(--dsh-content-font-size-secondary,13px);color:var(--dsw-alias-label-primary)}",
235
+ ".evo-hist-root{display:flex;height:100%;min-height:0;min-width:0;color:var(--dsw-alias-label-primary)}",
232
236
  ".evo-hist-aside{display:flex;flex-direction:column;flex:0 0 264px;width:264px;min-height:0;border-right:.5px solid var(--dsw-alias-border-l2)}",
233
- ".evo-hist-search{padding:10px 12px 4px}",
237
+ ".evo-hist-search{padding:12px 12px 4px}",
234
238
  ".evo-hist-search input{width:100%;box-sizing:border-box;padding:6px 10px;border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit}",
235
239
  ".evo-hist-search input:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:-2px}",
236
- ".evo-hist-list{flex:1 1 auto;min-height:0;overflow-y:auto;padding:4px 6px 10px}",
240
+ ".evo-hist-list{flex:1 1 auto;min-height:0;overflow-y:auto;padding:4px 12px 10px}",
237
241
  ".evo-hist-empty{padding:8px 12px;color:var(--dsw-alias-label-tertiary)}",
238
242
  ".evo-hist-skill{display:block;width:100%;box-sizing:border-box;padding:7px 8px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;text-align:left;cursor:pointer}",
239
243
  ".evo-hist-skill:hover{background:var(--dsw-alias-interactive-bg-hover)}",
@@ -242,27 +246,29 @@ window.__ModuleLoader__.load({
242
246
  ".evo-hist-skill-line{display:flex;align-items:baseline;gap:6px}",
243
247
  ".evo-hist-skill-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
244
248
  ".evo-hist-skill-count{flex:none;margin-left:auto;color:var(--dsw-alias-label-tertiary)}",
245
- ".evo-hist-skill-desc{margin-top:2px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
246
- ".evo-hist-main{flex:1 1 auto;min-width:0;min-height:0;overflow-y:auto;padding:12px 14px}",
249
+ ".evo-hist-skill-desc{display:block;margin-top:2px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
250
+ ".evo-hist-main{flex:1 1 auto;min-width:0;min-height:0;overflow-y:auto;padding:12px 12px}",
247
251
  ".evo-hist-head{display:flex;align-items:center;gap:10px;margin:0 0 4px}",
248
- ".evo-hist-title{flex:1 1 auto;margin:0;font-size:inherit;font-weight:600}",
252
+ ".evo-hist-title{flex:1 1 auto;margin:0;font-size:calc(1em + 2px);font-weight:600}",
249
253
  ".evo-hist-hint{margin:0 0 10px;color:var(--dsw-alias-label-secondary);line-height:1.5}",
254
+ ".evo-hist-pane-empty{margin:2px 0 0;color:var(--dsw-alias-label-tertiary)}",
250
255
  ".evo-hist-note{margin:0 0 10px;color:var(--dsw-alias-label-secondary);line-height:1.5;white-space:pre-wrap}",
251
256
  ".evo-hist-note[data-error=\"true\"]{color:var(--dsw-alias-state-error-primary)}",
252
- ".evo-hist-group{margin:10px 0 2px;font-size:inherit;font-weight:600}",
253
- ".evo-hist-row{display:flex;align-items:flex-start;gap:8px;padding:8px 0;border-top:.5px solid var(--dsw-alias-border-l3)}",
257
+ ".evo-hist-group{margin:10px 0 2px;font-size:calc(1em - 1px);font-weight:600;color:var(--dsw-alias-label-secondary)}",
258
+ ".evo-hist-row{display:flex;align-items:flex-start;gap:8px;padding:8px 0;border-top:.5px solid var(--dsw-alias-border-l2)}",
259
+ ".evo-hist-row:first-of-type{border-top:0}",
254
260
  ".evo-hist-row-body{flex:1 1 auto;min-width:0}",
255
261
  ".evo-hist-row-title{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}",
256
262
  ".evo-hist-row-meta{color:var(--dsw-alias-label-tertiary)}",
257
263
  ".evo-hist-row-summary{margin-top:2px;color:var(--dsw-alias-label-secondary);line-height:1.5}",
258
264
  ".evo-hist-actions{display:flex;flex:none;align-items:center;gap:6px}",
259
- ".evo-hist-capsule{flex:none;padding:1px 6px;border:.5px solid var(--dsw-alias-border-l2);border-radius:999px;color:var(--dsw-alias-label-secondary)}",
265
+ ".evo-hist-capsule{flex:none;padding:1px 8px;border:0;border-radius:999px;background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-secondary)}",
260
266
  ".evo-hist-button{flex:none;font:inherit;padding:3px 10px;border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;background:transparent;color:inherit;cursor:pointer}",
261
267
  ".evo-hist-button:hover{background:var(--dsw-alias-interactive-bg-hover)}",
262
268
  ".evo-hist-button:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:-2px}",
263
269
  ".evo-hist-button[data-tone=\"primary\"]{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}",
264
270
  ".evo-hist-pre{margin:6px 0 2px;padding:12px 16px;max-height:260px;overflow:auto;border:.5px solid var(--dsw-alias-border-l1);border-radius:12px;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}",
265
- ".evo-hist-pre-add{color:var(--dsw-alias-brand-primary)}",
271
+ ".evo-hist-pre-add{color:var(--dsw-alias-state-success-primary)}",
266
272
  ".evo-hist-pre-del{color:var(--dsw-alias-state-error-primary)}"
267
273
  ].join("\n");
268
274
  //#endregion
@@ -301,6 +307,17 @@ window.__ModuleLoader__.load({
301
307
  if (skill.protectedBy !== null) return "state.protected";
302
308
  return skill.managed ? "state.managed" : "state.foreign";
303
309
  }
310
+ /**
311
+ * The one clause an action word needs: what the recorded artifact IS. It rides the action word's
312
+ * `title` instead of widening the row (a parenthetical in the sentence pushed time and size aside).
313
+ * @param face - copy plus the data callbacks.
314
+ * @param row - the version row whose action is being named.
315
+ * @returns the clause, or undefined for the actions that need none.
316
+ */
317
+ function actionNote(face, row) {
318
+ if (row.actionKind === "delete") return face.t("action.delete.note");
319
+ if (row.actionKind === "archive") return face.t("action.archive.note");
320
+ }
304
321
  /** The relative-time sentence for one row's bucket. */
305
322
  function ageText(face, age) {
306
323
  if (age.unit === "now") return face.t("time.now");
@@ -333,17 +350,21 @@ window.__ModuleLoader__.load({
333
350
  return (0, react.createElement)("pre", { className: "evo-hist-pre" }, rows.length === 0 ? "±" : rows);
334
351
  }
335
352
  /** One version row: what happened, when, how big, and what it is against. */
336
- function versionRow(face, row, confirming, openDiff, diff, restore, toggleDiff) {
353
+ function versionRow(face, row, confirming, openDiff, diff, restore, cancelRestore, toggleDiff) {
337
354
  const parts = [
338
355
  (0, react.createElement)("span", {
339
356
  key: "v",
340
357
  className: "evo-hist-row-meta"
341
358
  }, "v" + String(row.v)),
342
- (0, react.createElement)("span", { key: "action" }, face.t("action." + row.actionKind)),
359
+ (0, react.createElement)("span", {
360
+ key: "action",
361
+ title: actionNote(face, row)
362
+ }, face.t("action." + row.actionKind)),
343
363
  (0, react.createElement)("span", {
344
364
  key: "time",
345
- className: "evo-hist-row-meta"
346
- }, localTime(row.at) + " · " + ageText(face, row.age))
365
+ className: "evo-hist-row-meta",
366
+ title: localTime(row.at)
367
+ }, ageText(face, row.age))
347
368
  ];
348
369
  const delta = deltaText(face, row);
349
370
  if (delta !== null) parts.push((0, react.createElement)("span", {
@@ -355,15 +376,25 @@ window.__ModuleLoader__.load({
355
376
  key: "current",
356
377
  className: "evo-hist-capsule"
357
378
  }, face.t("current")));
358
- if (row.undoable === true) actions.push((0, react.createElement)("button", {
359
- key: "undo",
360
- type: "button",
361
- className: "evo-hist-button",
362
- "data-tone": confirming === row.v ? "primary" : void 0,
363
- onClick: () => {
364
- restore(row.v);
365
- }
366
- }, confirming === row.v ? face.t("undo.confirm") : face.t("undo")));
379
+ if (row.undoable === true) {
380
+ if (confirming === row.v) actions.push((0, react.createElement)("button", {
381
+ key: "cancel",
382
+ type: "button",
383
+ className: "evo-hist-button",
384
+ onClick: () => {
385
+ cancelRestore();
386
+ }
387
+ }, face.t("undo.no")));
388
+ actions.push((0, react.createElement)("button", {
389
+ key: "undo",
390
+ type: "button",
391
+ className: "evo-hist-button",
392
+ "data-tone": confirming === row.v ? "primary" : void 0,
393
+ onClick: () => {
394
+ restore(row.v);
395
+ }
396
+ }, confirming === row.v ? face.t("undo.confirm") : face.t("undo")));
397
+ }
367
398
  if (row.undoable === true || row.charsDelta !== void 0) actions.push((0, react.createElement)("button", {
368
399
  key: "diff",
369
400
  type: "button",
@@ -389,7 +420,7 @@ window.__ModuleLoader__.load({
389
420
  /** One group: a heading, an optional note, and its rows. */
390
421
  function group(face, title, entries, noteText, state) {
391
422
  if (entries.length === 0) return null;
392
- 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.openDiff, state.diffs.get(entry.v), state.restore, state.toggleDiff)));
423
+ 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.openDiff, state.diffs.get(entry.v), state.restore, state.cancelRestore, state.toggleDiff)));
393
424
  }
394
425
  /**
395
426
  * The panel body.
@@ -499,11 +530,16 @@ window.__ModuleLoader__.load({
499
530
  const needle = query.trim().toLowerCase();
500
531
  const visible = (0, react.useMemo)(() => needle === "" ? skills : skills.filter((skill) => skill.name.toLowerCase().includes(needle) || skill.description.toLowerCase().includes(needle)), [skills, needle]);
501
532
  const loaded = payload !== void 0;
533
+ /** Drop the pending confirmation: the button that opened it must not be the only way out. */
534
+ const cancelRestore = () => {
535
+ setPending(void 0);
536
+ };
502
537
  const rowState = {
503
538
  confirming: pending,
504
539
  openDiff,
505
540
  diffs,
506
541
  restore,
542
+ cancelRestore,
507
543
  toggleDiff
508
544
  };
509
545
  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", {
@@ -526,7 +562,7 @@ window.__ModuleLoader__.load({
526
562
  type: "button",
527
563
  className: "evo-hist-button",
528
564
  onClick: refresh
529
- }, face.t("refresh"))), (0, react.createElement)("p", { className: "evo-hist-hint" }, face.t("hint") + " " + face.t("hint.current")), noteText === void 0 ? null : (0, react.createElement)("p", {
565
+ }, face.t("refresh"))), (0, react.createElement)("p", { className: "evo-hist-hint" }, face.t("hint")), selected === void 0 ? (0, react.createElement)("p", { className: "evo-hist-pane-empty" }, face.t(skills.length === 0 ? "empty.skills" : "empty.pick")) : null, noteText === void 0 ? null : (0, react.createElement)("p", {
530
566
  className: "evo-hist-note",
531
567
  "data-error": noteError ? "true" : void 0
532
568
  }, noteText), selected === void 0 ? null : loaded ? null : (0, react.createElement)("p", { className: "evo-hist-hint" }, face.t("loading")), loaded ? (0, react.createElement)("div", null, group(face, face.t("group.content"), payload.content, void 0, rowState), group(face, face.t("group.support"), payload.support, face.t("group.support.note"), rowState), payload.content.length === 0 && payload.support.length === 0 ? (0, react.createElement)("p", { className: "evo-hist-hint" }, face.t("empty.versions")) : null) : null));
@@ -551,24 +587,44 @@ window.__ModuleLoader__.load({
551
587
  /** Row order inside the global panel list (beside the platform's own rows). */
552
588
  const PANEL_ORDER = 35;
553
589
  /**
554
- * The row's icon: a platform-style line glyph, sized by the SIDEBAR's own contract.
590
+ * The row's icon: a platform-style line glyph, whose SIZE comes from the sidebar's contract and whose
591
+ * glyph BOX is the one the neighbouring rows draw at.
555
592
  *
556
- * The platform hands every `sidebar.panellist` occupant `{ size, active }` (`SidebarRoot.tsx`
557
- * `renderSlot('sidebar.panellist', { size: wide ? 16 : 18, active }, …)`) and its own icons render at
558
- * that size (`IconGlobeOutline14 size={size}` in the sidebar's own spec). The neighbouring rows in a
559
- * shipped GUI take the OTHER route — they inject their entry into the sidebar's DOM with their own
560
- * `svg { width: 18px; height: 18px }` CSS — so 18px is the size a row glyph is expected to occupy.
561
- * A glyph that sizes itself from CSS (`width: 100%`) resolves against an unsized flex box and fills
562
- * the column: the size must come from the prop, with the neighbours' 18 as the fallback. The label
563
- * carries the accessible name, so the glyph stays decoration.
593
+ * Size: the platform hands every `sidebar.panellist` occupant `{ size, active }` (`SidebarRoot.tsx`
594
+ * `renderSlot('sidebar.panellist', { size: wide ? 16 : 18, active }, …)`), so the glyph takes its size
595
+ * from that prop; a glyph that sizes itself from CSS (`width: 100%`) resolves against an unsized flex
596
+ * box and fills the whole column.
597
+ *
598
+ * Box: the three rows above (task board / SSH / skill centre) are NOT slot occupants — they inject
599
+ * plain DOM into the sidebar with their own CSS (`.entry{padding:0 10px;gap:8px}` plus a 24px icon box
600
+ * holding an 18px glyph, `shared/client/sidebar-entry-core.ts`), so their glyph ink starts 13px into
601
+ * the column and their label 42px. Inside the platform panel row (8px padding, 8px gap) those two
602
+ * lines are reached by a 26px box: `8 + (26 - 16) / 2 = 13` and `8 + 26 + 8 = 42`.
603
+ * Matching the neighbours is a CHOICE (the platform's own rows below — the workspace folders — sit at
604
+ * 8px and 32px); it keeps the four plugin rows reading as one column. The label carries the accessible
605
+ * name, so the glyph stays decoration.
606
+ */
607
+ /** The glyph box in px: the neighbours' 24px icon box, placed to land on their two content lines. */
608
+ const GLYPH_BOX = 26;
609
+ /** The glyph's own coordinate system: every path below is drawn inside 16 units. */
610
+ const GLYPH_UNITS = 16;
611
+ /**
612
+ * Trim a computed viewBox number: three decimals stay readable and are far below one pixel.
613
+ * @param value - the raw number.
614
+ * @returns the value rounded to three decimals.
564
615
  */
616
+ function round3(value) {
617
+ return Math.round(value * 1e3) / 1e3;
618
+ }
565
619
  function PanelIcon(props = {}) {
566
620
  const size = typeof props.size === "number" && props.size > 0 ? props.size : 18;
621
+ const units = round3(GLYPH_UNITS * GLYPH_BOX / size);
622
+ const inset = round3((units - GLYPH_UNITS) / 2);
567
623
  return (0, react.createElement)("svg", {
568
624
  "aria-hidden": "true",
569
- width: String(size),
570
- height: String(size),
571
- viewBox: "0 0 16 16",
625
+ width: String(GLYPH_BOX),
626
+ height: String(GLYPH_BOX),
627
+ viewBox: String(-inset) + " " + String(-inset) + " " + String(units) + " " + String(units),
572
628
  fill: "none",
573
629
  stroke: "currentColor",
574
630
  strokeWidth: "1.5",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-skill-history",
3
3
  "description": "Web surface for skill content history: loopback host routes plus the sidebar panel (community build)",
4
- "version": "0.13.1",
4
+ "version": "0.13.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -30,21 +30,21 @@
30
30
  ],
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "@lmzhen/dsh-evolution-core": "^0.13.1"
33
+ "@lmzhen/dsh-evolution-core": "^0.13.3"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@deepseek-ai/cordis": "^4.0.1",
37
37
  "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
38
38
  "react": "^18.2.0",
39
- "@lmzhen/dsh-evolution-curator": "^0.13.1"
39
+ "@lmzhen/dsh-evolution-curator": "^0.13.3"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@deepseek-ai/cordis": "^4.0.1",
43
43
  "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
44
- "@lmzhen/dsh-evolution-core": "^0.13.1",
45
- "@lmzhen/dsh-evolution-curator": "^0.13.1",
46
- "@lmzhen/dsh-evolution-io": "^0.13.1",
47
- "@lmzhen/dsh-evolution-io-node": "^0.13.1"
44
+ "@lmzhen/dsh-evolution-core": "^0.13.3",
45
+ "@lmzhen/dsh-evolution-curator": "^0.13.3",
46
+ "@lmzhen/dsh-evolution-io": "^0.13.3",
47
+ "@lmzhen/dsh-evolution-io-node": "^0.13.3"
48
48
  },
49
49
  "dsh": {
50
50
  "client": {