sage-mem 0.9.13 → 0.9.14

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 (2) hide show
  1. package/lib/client.js +155 -92
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -112,13 +112,55 @@ const StarmapRuntime = (function (require, _react) {
112
112
  // 配色:五个色若取高饱和高亮度(#ffd27d / #6fe3ff / #b18cff / #7dffb0),
113
113
  // 配上星芒与脉冲环,整体读起来像贴纸。现在统一降饱和、压亮度 ——
114
114
  // 深空画布上够辨认,又不抢眼。
115
+ //
116
+ // 两档取值:`color` = 深色档(夜空画布),`light` = 浅色档(纸本设色)。
117
+ // 浅色档不用「深色档降亮度」那种做法 —— 高饱和冷色压在暖纸上会发脏;
118
+ // 换成明度接近的矿物颜料色(赭石/石青/紫墨/石绿/墨),色相错开才分得清类型。
115
119
  const KIND_META = {
116
- user: { label: "用户", color: "#e0c07a" },
117
- feedback: { label: "反馈", color: "#7ec8da" },
118
- project: { label: "项目", color: "#ac9edc" },
119
- reference: { label: "参考", color: "#92ceaa" },
120
- special: { label: "特殊", color: "#bcc3d0" },
120
+ user: { label: "用户", color: "#e0c07a", light: "#a4652c" },
121
+ feedback: { label: "反馈", color: "#7ec8da", light: "#3a6f8f" },
122
+ project: { label: "项目", color: "#ac9edc", light: "#6a5b8f" },
123
+ reference: { label: "参考", color: "#92ceaa", light: "#4a7c63" },
124
+ special: { label: "特殊", color: "#bcc3d0", light: "#5b5750" },
121
125
  };
126
+ /**
127
+ * 宿主主题挂钩 —— 与 ui-theme 同一处:body[data-ds-dark-theme] 存在即深色档。
128
+ */
129
+ function isDarkTheme() {
130
+ return typeof document !== "undefined" && !!document.body
131
+ && document.body.hasAttribute("data-ds-dark-theme");
132
+ }
133
+
134
+ /** 类型色按当前主题取(meta.color / meta.light)。 */
135
+ function kindColor(meta) {
136
+ const m = meta || KIND_META.special;
137
+ return (isDarkTheme() ? m.color : m.light) || m.color;
138
+ }
139
+
140
+ /**
141
+ * 画布调色板:深色档 = 夜空,浅色档 = 暖纸 + 晕染。
142
+ * 两套渲染器(WebGL / canvas2d)都从这里取色,别各写一份。
143
+ * bgGl 是 WebGL shader 用的 0-1 分量(与 bg2d 是同一色,写法不同而已)。
144
+ */
145
+ function starmapPalette() {
146
+ if (isDarkTheme()) {
147
+ return {
148
+ bg2d: ["#0b1026", "#101731"],
149
+ bgGl: [[0.105, 0.117, 0.145], [0.043, 0.050, 0.065]],
150
+ dust: "#9fb3dd",
151
+ archive: [0.46, 0.50, 0.58],
152
+ vignette: null,
153
+ };
154
+ }
155
+ return {
156
+ bg2d: ["#faf6ed", "#e9e0cd"],
157
+ bgGl: [[0.980, 0.965, 0.929], [0.914, 0.878, 0.804]],
158
+ dust: "#c9bfa6",
159
+ archive: [0.62, 0.60, 0.55],
160
+ vignette: { color: "#7a6a48", a: 0.10 },
161
+ };
162
+ }
163
+
122
164
  const KIND_ORDER = ["user", "feedback", "project", "reference", "special"];
123
165
  const FRESH_MS = 7 * 86400000;
124
166
 
@@ -370,79 +412,71 @@ const StarmapRuntime = (function (require, _react) {
370
412
  // ── 样式(data-plugin-css 约定,幂等注入)──
371
413
  const CSS = [
372
414
  ".smap-footer-btn{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--dsw-alias-border-l1,transparent);background:transparent;color:var(--dsw-alias-label-secondary,inherit);border-radius:8px;padding:4px 10px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap;}",
373
- ".smap-fab{position:fixed;right:0;top:15%;transform:translateY(-50%);z-index:900;width:36px;height:64px;border-radius:12px 0 0 12px;border:1px solid rgba(150,170,230,.35);border-right:none;background:rgba(13,18,38,.78);color:#ffd27d;font-size:17px;cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:.5;pointer-events:auto;transition:opacity .15s,background .15s,width .15s;box-shadow:-4px 0 18px rgba(0,0,0,.35);font-family:inherit;}",
374
- ".smap-fab:hover{opacity:1;width:46px;background:rgba(20,28,56,.95);}",
415
+ ".smap-fab{position:fixed;right:0;top:15%;transform:translateY(-50%);z-index:900;width:36px;height:64px;border-radius:12px 0 0 12px;border:1px solid var(--dsw-alias-border-l1);border-right:none;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-brand-primary);font-size:17px;cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:.5;pointer-events:auto;transition:opacity .15s,background .15s,width .15s;box-shadow:-4px 0 18px rgba(0,0,0,.35);font-family:inherit;}",
416
+ ".smap-fab:hover{opacity:1;width:46px;background:var(--dsw-alias-bg-layer-1);}",
375
417
  ".smap-footer-btn:hover{color:var(--dsw-alias-label-primary,inherit);border-color:var(--dsw-alias-border-l2,#8886);}",
376
418
  ".smap-footer-icon{font-size:13px;line-height:1;}",
377
- ".smap-veil{position:fixed;inset:0;z-index:1000;background:rgba(4,6,16,.72);backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;pointer-events:auto;animation:smap-fade-in .18s ease-out;}",
378
- ".smap-modal{width:min(980px,94vw);height:min(680px,92vh);background:linear-gradient(160deg,#0d1226,#111a33);border:1px solid rgba(140,160,220,.22);border-radius:16px;box-shadow:0 24px 80px rgba(0,0,0,.55);padding:16px 18px;display:flex;flex-direction:column;overflow:hidden;pointer-events:auto;animation:smap-pop-in .22s cubic-bezier(.2,.9,.3,1.15);}",
419
+ ".smap-veil{position:fixed;inset:0;z-index:1000;background:var(--dsw-alias-bg-mask-1,rgba(0,0,0,.45));backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;pointer-events:auto;animation:smap-fade-in .18s ease-out;}",
420
+ ".smap-modal{width:min(980px,94vw);height:min(680px,92vh);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:16px;box-shadow:0 24px 80px rgba(0,0,0,.55);padding:16px 18px;display:flex;flex-direction:column;overflow:hidden;pointer-events:auto;animation:smap-pop-in .22s cubic-bezier(.2,.9,.3,1.15);}",
379
421
  "@keyframes smap-fade-in{from{opacity:0}to{opacity:1}}",
380
422
  "@keyframes smap-pop-in{from{opacity:0;transform:scale(.96) translateY(10px)}to{opacity:1;transform:none}}",
381
- // 星图整块含 header 与图例,同属一张深色画布。
382
- //
383
- // 不这么做的下场:宿主切到**浅色主题**后,这个组件里硬编码的
384
- // 浅色文字(#dbe4f5 系)落在白色卡片上,标题、搜索框、图例几乎看不见 ——
385
- // 一片「重影」。星图本来就不打算跟随宿主主题(它是一张夜空),
386
- // 那就把边界画清楚:整块深色,而不是只有中间那半块。
387
- ".smap-card{background:#0c0e12;border-radius:10px;padding:12px 14px;color:#e3e7ef;display:flex;flex-direction:column;flex:1;min-height:0;}",
423
+ ".smap-card{background:var(--dsw-alias-bg-layer-1);border-radius:10px;padding:12px 14px;color:var(--dsw-alias-label-primary);display:flex;flex-direction:column;flex:1;min-height:0;}",
388
424
  ".smap-head{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap;}",
389
425
  ".smap-title{font-size:15px;font-weight:600;letter-spacing:.06em;}",
390
- ".smap-sub{font-size:11px;opacity:.55;}",
426
+ ".smap-sub{font-size:11px;color:var(--dsw-alias-label-secondary);}",
391
427
  ".smap-spacer{flex:1;}",
392
- ".smap-search{background:rgba(255,255,255,.06);border:1px solid rgba(150,170,230,.25);border-radius:8px;color:#dbe4f5;font-size:12px;padding:4px 9px;outline:none;width:160px;font-family:inherit;}",
393
- ".smap-search:focus{border-color:rgba(150,170,230,.55);}",
394
- ".smap-search::placeholder{color:rgba(219,228,245,.35);}",
395
- ".smap-btn{background:rgba(255,255,255,.07);border:1px solid rgba(150,170,230,.25);border-radius:8px;color:#dbe4f5;font-size:12px;padding:4px 10px;cursor:pointer;font-family:inherit;}",
396
- ".smap-btn:hover{background:rgba(255,255,255,.13);}",
397
- // 开态(档案馆开关)与主按钮(恢复):沿用同一套夜色调,不新增色板。
398
- // 「显示档案馆」的开/关要一眼看出来:开 = 亮边 + 蓝调底 + 亮字,关 = 整体压暗。
399
- ".smap-btn.on{border-color:rgba(150,170,230,.78);background:rgba(122,158,255,.20);color:#eef3ff;}",
400
- '.smap-ar-toggle[aria-pressed="false"]{opacity:.60;}',
428
+ ".smap-search{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;color:var(--dsw-alias-label-primary);font-size:12px;padding:4px 9px;outline:none;width:160px;font-family:inherit;}",
429
+ ".smap-search:focus{border-color:var(--dsw-alias-border-l2);}",
430
+ ".smap-search::placeholder{color:var(--dsw-alias-label-tertiary);}",
431
+ ".smap-btn{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;color:var(--dsw-alias-label-primary);font-size:12px;padding:4px 10px;cursor:pointer;font-family:inherit;}",
432
+ ".smap-btn:hover{background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));}",
433
+ ".smap-btn.on{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-primary);}",
434
+ ".smap-ar-toggle[aria-pressed=\"false\"]{opacity:.60;}",
401
435
  ".smap-btn:disabled{opacity:.5;cursor:default;}",
402
- ".smap-btn-primary{border-color:rgba(150,170,230,.55);background:rgba(122,158,255,.20);}",
436
+ ".smap-btn-primary{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));}",
403
437
  ".smap-wrap{position:relative;flex:1;min-height:0;}",
404
438
  ".smap-canvas{position:absolute;inset:0;width:100%;height:100%;display:block;border-radius:10px;cursor:crosshair;}",
405
- ".smap-tip{position:absolute;z-index:5;pointer-events:none;max-width:280px;background:rgba(10,14,30,.95);border:1px solid rgba(150,170,230,.35);border-radius:8px;padding:8px 10px;font-size:12px;line-height:1.55;box-shadow:0 6px 24px rgba(0,0,0,.45);}",
439
+ ".smap-tip{position:absolute;z-index:5;pointer-events:none;max-width:280px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:8px 10px;font-size:12px;line-height:1.55;box-shadow:0 6px 24px rgba(0,0,0,.45);}",
406
440
  ".smap-tip-title{font-weight:600;margin-bottom:2px;}",
407
- ".smap-tip-kind{opacity:.6;font-size:11px;margin-bottom:2px;}",
408
- ".smap-tip-desc{opacity:.85;}",
409
- ".smap-panel{position:absolute;top:10px;right:10px;bottom:10px;width:min(400px,54%);background:rgba(9,13,28,.97);border:1px solid rgba(150,170,230,.32);border-radius:10px;padding:12px 14px;overflow-y:auto;z-index:6;font-size:13px;line-height:1.7;box-shadow:-8px 0 30px rgba(0,0,0,.35);}",
441
+ ".smap-tip-kind{color:var(--dsw-alias-label-secondary);font-size:11px;margin-bottom:2px;}",
442
+ ".smap-tip-desc{color:var(--dsw-alias-label-secondary);}",
443
+ ".smap-panel{position:absolute;top:10px;right:10px;bottom:10px;width:min(400px,54%);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:12px 14px;overflow-y:auto;z-index:6;font-size:13px;line-height:1.7;box-shadow:-8px 0 30px rgba(0,0,0,.35);}",
410
444
  ".smap-panel-head{display:flex;align-items:flex-start;gap:8px;margin-bottom:4px;}",
411
445
  ".smap-panel-title{font-weight:600;font-size:14px;flex:1;}",
412
- ".smap-panel-meta{font-size:11px;opacity:.6;margin-bottom:8px;}",
413
- ".smap-panel-arch{display:flex;flex-direction:column;gap:6px;border:1px solid rgba(150,170,230,.28);border-radius:8px;padding:8px 10px;background:rgba(255,255,255,.03);}",
446
+ ".smap-panel-meta{font-size:11px;color:var(--dsw-alias-label-secondary);margin-bottom:8px;}",
447
+ ".smap-panel-arch{display:flex;flex-direction:column;gap:6px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2);}",
414
448
  ".smap-meta-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:4px;vertical-align:-1px;}",
415
449
  ".smap-legend{display:flex;flex-wrap:wrap;gap:14px;margin-top:10px;font-size:12px;align-items:center;}",
416
450
  ".smap-lg-item{cursor:pointer;user-select:none;}",
417
451
  ".smap-lg-item.off{opacity:.32;}",
418
452
  ".smap-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:-1px;}",
419
- ".smap-count{margin-left:auto;opacity:.55;font-size:11px;}",
420
- ".smap-status{padding:28px;text-align:center;opacity:.6;font-size:13px;}",
421
- ".smap-empty{position:absolute;left:0;right:0;top:46%;text-align:center;color:rgba(219,228,245,.42);font-size:13px;pointer-events:none;z-index:4;}",
422
- ".smap-replay-on{border-color:rgba(255,210,125,.55)!important;color:#ffd27d!important;}",
423
- ".smap-replay-date{font-size:11px;opacity:.75;color:#ffd27d;font-variant-numeric:tabular-nums;}",
453
+ ".smap-count{margin-left:auto;color:var(--dsw-alias-label-secondary);font-size:11px;}",
454
+ ".smap-status{padding:28px;text-align:center;color:var(--dsw-alias-label-secondary);font-size:13px;}",
455
+ ".smap-empty{position:absolute;left:0;right:0;top:46%;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:13px;pointer-events:none;z-index:4;}",
456
+ ".smap-replay-on{border-color:var(--dsw-alias-brand-primary)!important;color:var(--dsw-alias-brand-primary)!important;}",
457
+ ".smap-replay-date{font-size:11px;color:var(--dsw-alias-brand-primary);font-variant-numeric:tabular-nums;}",
424
458
  ".smap-md-h{font-weight:600;margin:12px 0 4px;}",
425
459
  ".smap-md-h1{font-size:16px;}",
426
460
  ".smap-md-h2{font-size:14.5px;}",
427
461
  ".smap-md-h3,.smap-md-h4{font-size:13.5px;opacity:.92;}",
428
462
  ".smap-md-p{margin:4px 0;}",
429
463
  ".smap-md-list{margin:4px 0;padding-left:20px;}",
430
- ".smap-md-code{background:rgba(255,255,255,.09);padding:1px 5px;border-radius:4px;font-size:12px;}",
431
- ".smap-md-pre{background:rgba(255,255,255,.06);padding:8px 10px;border-radius:8px;font-size:12px;overflow-x:auto;white-space:pre-wrap;margin:6px 0;}",
432
- ".smap-md-quote{border-left:3px solid rgba(150,170,230,.4);margin:6px 0;padding:2px 10px;opacity:.88;}",
433
- ".smap-md-wiki{color:#8ab4ff;}",
434
- ".smap-md-hr{border:none;border-top:1px solid rgba(150,170,230,.22);margin:10px 0;}",
435
- ".smap-viewtoggle{display:inline-flex;border:1px solid rgba(150,170,230,.25);border-radius:8px;overflow:hidden;}",
436
- ".smap-vt-btn{background:transparent;border:none;color:rgba(219,228,245,.55);font-size:12px;padding:4px 10px;cursor:pointer;font-family:inherit;}",
437
- ".smap-vt-btn.on{background:rgba(255,255,255,.12);color:#dbe4f5;}",
464
+ ".smap-md-code{background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));padding:1px 5px;border-radius:4px;font-size:12px;}",
465
+ ".smap-md-pre{background:var(--dsw-alias-bg-layer-2);padding:8px 10px;border-radius:8px;font-size:12px;overflow-x:auto;white-space:pre-wrap;margin:6px 0;}",
466
+ ".smap-md-quote{border-left:3px solid var(--dsw-alias-border-l2);margin:6px 0;padding:2px 10px;color:var(--dsw-alias-label-secondary);}",
467
+ ".smap-md-wiki{color:var(--dsw-alias-brand-primary);}",
468
+ ".smap-md-hr{border:none;border-top:1px solid var(--dsw-alias-border-l1);margin:10px 0;}",
469
+ ".smap-viewtoggle{display:inline-flex;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;overflow:hidden;}",
470
+ ".smap-vt-btn{background:transparent;border:none;color:var(--dsw-alias-label-secondary);font-size:12px;padding:4px 10px;cursor:pointer;font-family:inherit;}",
471
+ ".smap-vt-btn.on{background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-primary);}",
438
472
  ".smap-timeline{position:absolute;inset:0;overflow-y:auto;display:flex;flex-direction:column;gap:6px;padding:4px 6px 4px 2px;}",
439
- ".smap-tl-item{display:flex;align-items:baseline;gap:8px;background:rgba(255,255,255,.035);border:1px solid rgba(150,170,230,.16);border-radius:8px;padding:7px 10px;cursor:pointer;flex:none;}",
440
- ".smap-tl-item:hover{background:rgba(255,255,255,.08);border-color:rgba(150,170,230,.35);}",
473
+ ".smap-tl-item{display:flex;align-items:baseline;gap:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:7px 10px;cursor:pointer;flex:none;}",
474
+ ".smap-tl-item:hover{background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-border-l1);}",
441
475
  ".smap-tl-item--arch{opacity:.75;}",
442
- ".smap-ar-badge{font-size:10.5px;opacity:.65;border:1px solid rgba(150,170,230,.3);border-radius:5px;padding:0 5px;flex:none;align-self:center;}",
443
- ".smap-tl-date{font-size:11px;opacity:.5;font-variant-numeric:tabular-nums;flex:none;}",
476
+ ".smap-ar-badge{font-size:10.5px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:5px;padding:0 5px;flex:none;align-self:center;}",
477
+ ".smap-tl-date{font-size:11px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none;}",
444
478
  ".smap-tl-title{font-size:13px;font-weight:600;flex:none;max-width:40%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
445
- ".smap-tl-desc{font-size:12px;opacity:.55;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;}",
479
+ ".smap-tl-desc{font-size:12px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;}",
446
480
  ].join("\n");
447
481
 
448
482
  function injectCss() {
@@ -463,14 +497,15 @@ const StarmapRuntime = (function (require, _react) {
463
497
  }
464
498
 
465
499
  // 归档星(暗星)的用色:把类型色往石板灰上压 —— 它得「还在图上、一眼数得出来」,
466
- // 又要一眼看出不是活记忆。亮度靠 shader 那一档(0.52),这里负责色相:冷、灰、不饱和。
467
- const ARCHIVE_GREY = [0.46, 0.50, 0.58];
500
+ // 又要一眼看出不是活记忆。亮度靠 shader 那一档(0.52),这里负责色相:
501
+ // 深色档是冷石板灰;浅色档换成暖灰,否则灰点落在纸上看不见。
468
502
  function dimRgb(hex) {
469
503
  const c = hexToRgb(hex);
504
+ const g = starmapPalette().archive;
470
505
  return [
471
- c[0] * 0.26 + ARCHIVE_GREY[0] * 0.74,
472
- c[1] * 0.26 + ARCHIVE_GREY[1] * 0.74,
473
- c[2] * 0.26 + ARCHIVE_GREY[2] * 0.74,
506
+ c[0] * 0.26 + g[0] * 0.74,
507
+ c[1] * 0.26 + g[1] * 0.74,
508
+ c[2] * 0.26 + g[2] * 0.74,
474
509
  ];
475
510
  }
476
511
  function dimCss(hex) {
@@ -483,17 +518,21 @@ const StarmapRuntime = (function (require, _react) {
483
518
  // 「会呼吸的彩色底」正是廉价感的一大来源,而且每帧都要重算噪声。
484
519
  // 现在只剩一条静态径向渐变:中心略亮的中性深灰,边缘更暗。uTime 保留声明
485
520
  // 只为让 uniform 列表不变(用不到时 getUniformLocation 返回 null,调用是安全空操作)。
486
- const SMAP_BG_FS = [
487
- "precision highp float;",
488
- "uniform vec2 uRes;",
489
- "uniform float uTime;",
490
- "void main(){",
491
- " vec2 suv = gl_FragCoord.xy / uRes;",
492
- " float r = length((suv - vec2(0.5, 0.44)) * vec2(1.0, 1.28));",
493
- " vec3 col = mix(vec3(0.105, 0.117, 0.145), vec3(0.043, 0.050, 0.065), clamp(r * 1.30, 0.0, 1.0));",
494
- " gl_FragColor = vec4(col, 1.0);",
495
- "}",
496
- ].join("\n");
521
+ // 底色由调色板给(深色档=夜空深灰,浅色档=暖纸),所以是函数不是常量。
522
+ function smapBgFs(inner, outer) {
523
+ const v = (c) => c.map((x) => x.toFixed(3)).join(", ");
524
+ return [
525
+ "precision highp float;",
526
+ "uniform vec2 uRes;",
527
+ "uniform float uTime;",
528
+ "void main(){",
529
+ " vec2 suv = gl_FragCoord.xy / uRes;",
530
+ " float r = length((suv - vec2(0.5, 0.44)) * vec2(1.0, 1.28));",
531
+ " vec3 col = mix(vec3(" + v(inner) + "), vec3(" + v(outer) + "), clamp(r * 1.30, 0.0, 1.0));",
532
+ " gl_FragColor = vec4(col, 1.0);",
533
+ "}",
534
+ ].join("\n");
535
+ }
497
536
 
498
537
  const SMAP_STAR_VS = [
499
538
  "attribute vec2 aCorner;",
@@ -642,7 +681,8 @@ const StarmapRuntime = (function (require, _react) {
642
681
  canvasEl.width = W * 2;
643
682
  canvasEl.height = H * 2;
644
683
  gl.viewport(0, 0, canvasEl.width, canvasEl.height);
645
- const progBg = glProgram(gl, SMAP_BG_VS, SMAP_BG_FS, ["aPos"], ["uRes", "uTime"]);
684
+ const pal = starmapPalette();
685
+ const progBg = glProgram(gl, SMAP_BG_VS, smapBgFs(pal.bgGl[0], pal.bgGl[1]), ["aPos"], ["uRes", "uTime"]);
646
686
  const progStar = glProgram(gl, SMAP_STAR_VS, SMAP_STAR_FS, ["aCorner", "aMeta0", "aColor", "aMeta1", "aBirth"], ["uRes", "uTime", "uHoverIdx", "uRevealT"]);
647
687
  const progLine = glProgram(gl, SMAP_LINE_VS, SMAP_LINE_FS, ["aPos", "aColor", "aBirth"], ["uRes", "uRevealT"]);
648
688
 
@@ -660,7 +700,7 @@ const StarmapRuntime = (function (require, _react) {
660
700
  const x = rand01(i * 97 + 13) * W;
661
701
  const y = rand01(i * 31 + 7) * H;
662
702
  const r = 2.4 + rand01(i * 17 + 5) * 2.0;
663
- pushStarQuad(arr, x, y, r, [0.62, 0.70, 0.87], 0.10 + 0.30 * rand01(i * 7 + 3), rand01(i * 11 + 29), 0, -1, -1);
703
+ pushStarQuad(arr, x, y, r, hexToRgb(pal.dust), 0.10 + 0.30 * rand01(i * 7 + 3), rand01(i * 11 + 29), 0, -1, -1);
664
704
  }
665
705
  gl.bindBuffer(gl.ARRAY_BUFFER, dustBuf);
666
706
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arr), gl.STATIC_DRAW);
@@ -691,10 +731,10 @@ const StarmapRuntime = (function (require, _react) {
691
731
  // 负责「深空底上一眼数得出来」——只画一个小点是数不出来的;
692
732
  // ② 一颗小核心(走普通分支,fresh 传 -1 = 归档档 0.52),负责
693
733
  // 「看得出是颗星」,并且 hover 时整颗点亮 + 出环。
694
- pushStarQuad(arr, p.x, p.y, p.r * 6.0, dimRgb(meta.color), 0.26, p.tw, 0, -1, birth);
695
- pushStarQuad(arr, p.x, p.y, p.r * 4.4, dimRgb(meta.color), 1, p.tw, -1, i, birth);
734
+ pushStarQuad(arr, p.x, p.y, p.r * 6.0, dimRgb(kindColor(meta)), 0.26, p.tw, 0, -1, birth);
735
+ pushStarQuad(arr, p.x, p.y, p.r * 4.4, dimRgb(kindColor(meta)), 1, p.tw, -1, i, birth);
696
736
  } else {
697
- pushStarQuad(arr, p.x, p.y, p.r * 5.4, hexToRgb(meta.color), 1, p.tw, fresh, i, birth);
737
+ pushStarQuad(arr, p.x, p.y, p.r * 5.4, hexToRgb(kindColor(meta)), 1, p.tw, fresh, i, birth);
698
738
  }
699
739
  }
700
740
  gl.bindBuffer(gl.ARRAY_BUFFER, starBuf);
@@ -709,7 +749,7 @@ const StarmapRuntime = (function (require, _react) {
709
749
  const ln = visBox.lines[li];
710
750
  const a = layout.pts[ln[0]];
711
751
  const b = layout.pts[ln[1]];
712
- const c = hexToRgb((KIND_META[ln[2]] || KIND_META.special).color);
752
+ const c = hexToRgb(kindColor(KIND_META[ln[2]] || KIND_META.special));
713
753
  const hot = hoverLineIdx >= 0 && (ln[0] === hoverLineIdx || ln[1] === hoverLineIdx);
714
754
  const mul = hot ? 1 : 0.72;
715
755
  const al = hot ? 0.6 : 0.22;
@@ -801,16 +841,25 @@ const StarmapRuntime = (function (require, _react) {
801
841
  const c2d = canvasEl.getContext("2d");
802
842
  if (!c2d) return;
803
843
  c2d.setTransform(SCALE, 0, 0, SCALE, 0, 0);
844
+ const pal = starmapPalette();
804
845
  const bg = c2d.createLinearGradient(0, 0, 0, H);
805
- bg.addColorStop(0, "#0b1026");
806
- bg.addColorStop(1, "#101731");
846
+ bg.addColorStop(0, pal.bg2d[0]);
847
+ bg.addColorStop(1, pal.bg2d[1]);
807
848
  c2d.fillStyle = bg;
808
849
  c2d.fillRect(0, 0, W, H);
850
+ // 晕染:中心透、边缘略深(浅色档才要,深色档的夜空渐变已经够了)
851
+ if (pal.vignette) {
852
+ const vg = c2d.createRadialGradient(W * 0.5, H * 0.46, 0, W * 0.5, H * 0.46, Math.max(W, H) * 0.72);
853
+ vg.addColorStop(0, hexA(pal.vignette.color, 0));
854
+ vg.addColorStop(1, hexA(pal.vignette.color, pal.vignette.a));
855
+ c2d.fillStyle = vg;
856
+ c2d.fillRect(0, 0, W, H);
857
+ }
809
858
  for (let i = 0; i < 150; i++) {
810
859
  const x = rand01(i * 97 + 13) * W;
811
860
  const y = rand01(i * 31 + 7) * H;
812
861
  c2d.globalAlpha = 0.08 + 0.22 * rand01(i * 7 + 3);
813
- c2d.fillStyle = "#9fb3dd";
862
+ c2d.fillStyle = pal.dust;
814
863
  c2d.fillRect(x, y, 1, 1);
815
864
  }
816
865
  c2d.globalAlpha = 1;
@@ -827,7 +876,7 @@ const StarmapRuntime = (function (require, _react) {
827
876
  lrv = lt * lt * (3 - 2 * lt);
828
877
  if (lrv <= 0) continue;
829
878
  }
830
- c2d.strokeStyle = hexA(meta.color, (hot ? 0.6 : 0.22) * lrv);
879
+ c2d.strokeStyle = hexA(kindColor(meta), (hot ? 0.6 : 0.22) * lrv);
831
880
  c2d.lineWidth = 1;
832
881
  c2d.beginPath();
833
882
  c2d.moveTo(a.x, a.y);
@@ -849,7 +898,7 @@ const StarmapRuntime = (function (require, _react) {
849
898
  // 暗星(canvas2d 兜底路径):与 webgl 同一档 —— 核心小而暗、外面一圈很淡的冷辉光。
850
899
  // 有辉光才「数得出来」;hover 时整颗点亮 + 描边环,这是它能被发现的关键。
851
900
  const hot = hoverBox.idx === i;
852
- const dc = dimRgb(meta.color);
901
+ const dc = dimRgb(kindColor(meta));
853
902
  const rgba = (a) => "rgba(" + Math.round(dc[0] * 255) + "," + Math.round(dc[1] * 255) + "," + Math.round(dc[2] * 255) + "," + a.toFixed(3) + ")";
854
903
  const haloR = p.r * 3.4;
855
904
  const hg = c2d.createRadialGradient(p.x, p.y, 0, p.x, p.y, haloR);
@@ -860,13 +909,13 @@ const StarmapRuntime = (function (require, _react) {
860
909
  c2d.arc(p.x, p.y, haloR, 0, Math.PI * 2);
861
910
  c2d.fill();
862
911
  c2d.globalAlpha = (hot ? 0.95 : 0.52) * rv;
863
- c2d.fillStyle = dimCss(meta.color);
912
+ c2d.fillStyle = dimCss(kindColor(meta));
864
913
  c2d.beginPath();
865
914
  c2d.arc(p.x, p.y, Math.max(1.2, p.r * 0.7), 0, Math.PI * 2);
866
915
  c2d.fill();
867
916
  c2d.globalAlpha = 1;
868
917
  if (hot) {
869
- c2d.strokeStyle = dimCss(meta.color);
918
+ c2d.strokeStyle = dimCss(kindColor(meta));
870
919
  c2d.lineWidth = 1;
871
920
  c2d.beginPath();
872
921
  c2d.arc(p.x, p.y, p.r + 5, 0, Math.PI * 2);
@@ -876,7 +925,7 @@ const StarmapRuntime = (function (require, _react) {
876
925
  }
877
926
  if (p.star.mtimeMs > 0 && nowMs - p.star.mtimeMs < FRESH_MS) {
878
927
  const pr = p.r + 5 + 3 * (0.5 + 0.5 * Math.sin(phase * 1.4 + p.tw * 6.28));
879
- c2d.strokeStyle = hexA(meta.color, 0.4 * rv);
928
+ c2d.strokeStyle = hexA(kindColor(meta), 0.4 * rv);
880
929
  c2d.lineWidth = 1;
881
930
  c2d.beginPath();
882
931
  c2d.arc(p.x, p.y, pr, 0, Math.PI * 2);
@@ -884,20 +933,20 @@ const StarmapRuntime = (function (require, _react) {
884
933
  }
885
934
  const glowR = p.r * 4.2;
886
935
  const g = c2d.createRadialGradient(p.x, p.y, 0, p.x, p.y, glowR);
887
- g.addColorStop(0, hexA(meta.color, 0.5 * tw * rv));
888
- g.addColorStop(1, hexA(meta.color, 0));
936
+ g.addColorStop(0, hexA(kindColor(meta), 0.5 * tw * rv));
937
+ g.addColorStop(1, hexA(kindColor(meta), 0));
889
938
  c2d.fillStyle = g;
890
939
  c2d.beginPath();
891
940
  c2d.arc(p.x, p.y, glowR, 0, Math.PI * 2);
892
941
  c2d.fill();
893
942
  c2d.globalAlpha = (0.62 + 0.38 * tw) * rv;
894
- c2d.fillStyle = meta.color;
943
+ c2d.fillStyle = kindColor(meta);
895
944
  c2d.beginPath();
896
945
  c2d.arc(p.x, p.y, p.r, 0, Math.PI * 2);
897
946
  c2d.fill();
898
947
  c2d.globalAlpha = 1;
899
948
  if (hoverBox.idx === i) {
900
- c2d.strokeStyle = hexA(meta.color, 0.9 * rv);
949
+ c2d.strokeStyle = hexA(kindColor(meta), 0.9 * rv);
901
950
  c2d.lineWidth = 1;
902
951
  c2d.beginPath();
903
952
  c2d.arc(p.x, p.y, p.r + 5, 0, Math.PI * 2);
@@ -978,6 +1027,20 @@ const StarmapRuntime = (function (require, _react) {
978
1027
  const drawBox = react.useRef({ renderer: null, t0: 0, markDirty: null }).current;
979
1028
  const replayBox = react.useRef({ raf: 0, active: false }).current;
980
1029
 
1030
+ // 宿主主题:切主题要把画布重建一遍(底色/星点色都变了),所以它是个 state,
1031
+ // 并进渲染 effect 的依赖。挂 observer 而不是每次 render 读属性 —— 属性变了要主动重画。
1032
+ const themeKeyS = react.useState(isDarkTheme() ? "dark" : "light");
1033
+ const themeKey = themeKeyS[0];
1034
+ const setThemeKey = themeKeyS[1];
1035
+ react.useEffect(() => {
1036
+ if (typeof document === "undefined" || !document.body) return undefined;
1037
+ const sync = () => setThemeKey(isDarkTheme() ? "dark" : "light");
1038
+ sync();
1039
+ const mo = new MutationObserver(sync);
1040
+ mo.observe(document.body, { attributes: true, attributeFilter: ["data-ds-dark-theme"] });
1041
+ return () => mo.disconnect();
1042
+ }, []);
1043
+
981
1044
  // Esc 分层:详情面板开着时先关面板(capture 阶段拦截),再按才落到 overlay 关星空。
982
1045
  react.useEffect(() => {
983
1046
  if (!selected) return undefined;
@@ -1077,7 +1140,7 @@ const StarmapRuntime = (function (require, _react) {
1077
1140
  window.cancelAnimationFrame(replayBox.raf);
1078
1141
  if (renderer.destroy) renderer.destroy();
1079
1142
  };
1080
- }, [data, layout, canvasEl]);
1143
+ }, [data, layout, canvasEl, themeKey]);
1081
1144
 
1082
1145
  react.useEffect(() => {
1083
1146
  if (!data || !layout || !kindsOn) return;
@@ -1285,7 +1348,7 @@ const StarmapRuntime = (function (require, _react) {
1285
1348
  onClick: () => toggleKind(k),
1286
1349
  title: "点击显示/隐藏这一类",
1287
1350
  },
1288
- h("span", { className: "smap-dot", style: { background: KIND_META[k].color } }),
1351
+ h("span", { className: "smap-dot", style: { background: kindColor(KIND_META[k]) } }),
1289
1352
  KIND_META[k].label + " " + n));
1290
1353
  }
1291
1354
  let totalBytes = 0;
@@ -1327,7 +1390,7 @@ const StarmapRuntime = (function (require, _react) {
1327
1390
  h("span", { className: "smap-panel-title" }, selected.star.title),
1328
1391
  h("button", { className: "smap-btn", onClick: () => setSelected(null), title: "关闭" }, "×")),
1329
1392
  h("div", { key: "meta", className: "smap-panel-meta" },
1330
- h("span", { className: "smap-meta-dot", style: { background: meta.color } }),
1393
+ h("span", { className: "smap-meta-dot", style: { background: kindColor(meta) } }),
1331
1394
  meta.label + " · " + selected.star.file + " · " + fmtBytes(selected.star.bytes) +
1332
1395
  // 归档星的 mtime 是「当年写下」的时间,跟「什么时候收起来的」不是一回事 ——
1333
1396
  // 这里只留「已归档」,具体时间在下面的归档块里给。
@@ -1370,7 +1433,7 @@ const StarmapRuntime = (function (require, _react) {
1370
1433
  onClick: () => openStar(s),
1371
1434
  title: s.archived === true ? "归档案:点开看归档信息并恢复" : "展开全文",
1372
1435
  },
1373
- h("span", { className: "smap-meta-dot", style: { background: s.archived === true ? dimCss((KIND_META[s.kind] || KIND_META.special).color) : (KIND_META[s.kind] || KIND_META.special).color, flex: "none", alignSelf: "center" } }),
1436
+ h("span", { className: "smap-meta-dot", style: { background: s.archived === true ? dimCss(kindColor(KIND_META[s.kind] || KIND_META.special)) : kindColor(KIND_META[s.kind] || KIND_META.special), flex: "none", alignSelf: "center" } }),
1374
1437
  s.archived === true ? h("span", { className: "smap-ar-badge" }, "已归档") : null,
1375
1438
  h("span", { className: "smap-tl-date", title: s.mtimeMs ? fmtDate(s.mtimeMs) : "" }, s.mtimeMs ? fmtRel(s.mtimeMs) : "—"),
1376
1439
  h("span", { className: "smap-tl-title" }, s.title),
@@ -2016,7 +2079,7 @@ const StarmapRuntime = (function (require, _react) {
2016
2079
  // 而独立成屏后如果父容器不是 column flex,`align-items:stretch` 就不会发生。
2017
2080
  ".smem-tool-panel--full{width:100%;align-self:stretch;box-sizing:border-box;}",
2018
2081
  // 已锁定的记忆:卡片上那个按钮要一眼看出「这条现在拒绝归档」,用警示色
2019
- ".smem-btn-locked{border-color:var(--dsw-alias-state-warning-primary,#d97706);color:var(--dsw-alias-state-warning-primary,#d97706);}",
2082
+ ".smem-btn-locked{border-color:var(--dsw-alias-state-warn-primary,#d97706);color:var(--dsw-alias-state-warn-primary,#d97706);}",
2020
2083
  // 「自动归档」页签里的天数输入框:窄一点,别让三个数字各占一整行
2021
2084
  ".smem-num{width:88px;}",
2022
2085
  // 体检 / 候选是「翻清单」的面板:定高 + 内部滚动,列表不被顶出首屏。
@@ -2105,7 +2168,7 @@ const StarmapRuntime = (function (require, _react) {
2105
2168
  ".smem-ad-main{flex:1;min-width:140px;overflow-wrap:anywhere;}",
2106
2169
  ".smem-ad-sub{color:var(--dsw-alias-label-secondary);font-size:11.5px;overflow-wrap:anywhere;}",
2107
2170
  ".smem-ad-badge{display:inline-flex;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:0 5px;white-space:nowrap;}",
2108
- ".smem-ad-badge--warn{color:var(--dsw-alias-state-warning-primary,#d97706);border-color:var(--dsw-alias-state-warning-primary,#d97706);}",
2171
+ ".smem-ad-badge--warn{color:var(--dsw-alias-state-warn-primary,#d97706);border-color:var(--dsw-alias-state-warn-primary,#d97706);}",
2109
2172
  ".smem-ad-badge--legacy{opacity:.78;}",
2110
2173
  ".smem-ad-confirm{border:1px solid var(--dsw-alias-state-error-primary,#ef4444);border-radius:8px;padding:7px 9px;font-size:12px;line-height:1.55;display:flex;flex-direction:column;gap:6px;overflow-wrap:anywhere;}",
2111
2174
  ".smem-ad-rb{font-size:12px;line-height:1.5;overflow-wrap:anywhere;}",
@@ -2114,7 +2177,7 @@ const StarmapRuntime = (function (require, _react) {
2114
2177
  '.smem-ad-run[data-on="1"]{border-color:var(--dsw-alias-brand-primary,#4d76e6);}',
2115
2178
  ".smem-ad-md{max-height:360px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2);font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;line-height:1.6;}",
2116
2179
  // ── 注入参数 / 保留名名单(自动做梦 tab)──
2117
- ".smem-ad-warn{color:var(--dsw-alias-state-warning-primary,#d97706);}",
2180
+ ".smem-ad-warn{color:var(--dsw-alias-state-warn-primary,#d97706);}",
2118
2181
  ".smem-chip--fixed{opacity:.72;cursor:default;}",
2119
2182
  ".smem-chip-x{border:0;background:transparent;color:inherit;cursor:pointer;font-size:11px;font-family:inherit;padding:0 0 0 5px;}",
2120
2183
  // ── 更新提示(有新版本才出现的一条窄横幅)──
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sage-mem",
3
3
  "description": "File-based memory plugin for DeepSeek Harness (DSH) — cross-session memory in plain markdown files, with a memory star-map view, type/tag filtering, retrieval aliases, and an optional autodream pass that consolidates memories on demand or on a schedule — with per-run rollback points, a per-file change declaration (what changed and why), and a separately selectable model for the consolidation pass",
4
- "version": "0.9.13",
4
+ "version": "0.9.14",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {