dsh-quick-toc 0.7.1 → 0.7.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.
package/lib/client.js CHANGED
@@ -159,6 +159,8 @@ window.__ModuleLoader__.load({
159
159
  "settings.levels": "显示的标题层级",
160
160
  "settings.zoom": "面板缩放",
161
161
  "settings.zoom.tip": "文字、图标与按钮按比例缩放,面板本身的尺寸不变",
162
+ "settings.sheetZoom": "幕布缩放",
163
+ "settings.sheetZoom.tip": "幕布里的文字、图标与按钮按比例缩放,幕布本身的尺寸不变",
162
164
  "settings.handle": "把手位置",
163
165
  "settings.handle.tip": "收起面板后,边缘把手在对话区里的上下位置(0% 最下、100% 最上)",
164
166
  "settings.on": "开启",
@@ -255,6 +257,8 @@ window.__ModuleLoader__.load({
255
257
  "settings.levels": "Heading levels shown",
256
258
  "settings.zoom": "Panel scale",
257
259
  "settings.zoom.tip": "Scales the text, icons and buttons; the panel's own size is unchanged",
260
+ "settings.sheetZoom": "Curtain scale",
261
+ "settings.sheetZoom.tip": "Scales the curtain's text, icons and buttons; the curtain's own size is unchanged",
258
262
  "settings.handle": "Handle position",
259
263
  "settings.handle.tip": "Where the collapsed-panel handle sits vertically (0% bottom, 100% top of the conversation area)",
260
264
  "settings.on": "On",
@@ -335,13 +339,13 @@ window.__ModuleLoader__.load({
335
339
  // purpose — they describe THIS screen, not the account.
336
340
  var NAMESPACE = "dsh-quick-toc";
337
341
  // Fields backed by the Host settings document; everything else is localStorage.
338
- var HOST_FIELDS = { dock: 1, lang: 1, levels: 1, zoom: 1, handle: 1, fuzzy: 1, hover: 1, debug: 1, remember: 1, autoLoad: 1 };
342
+ var HOST_FIELDS = { dock: 1, lang: 1, levels: 1, zoom: 1, sheetZoom: 1, handle: 1, fuzzy: 1, hover: 1, debug: 1, remember: 1, autoLoad: 1 };
339
343
  // The schema defaults (mirrored here for the card's per-field reset and for the
340
344
  // one-time import of a 0.5.x install's stored values).
341
345
  var DEFAULTS = {
342
346
  dock: "left", y: 0, w: 0, h: 0,
343
347
  levels: [1, 2, 3, 4, 5, 6],
344
- zoom: 1, handle: 0.5,
348
+ zoom: 1, sheetZoom: 1, handle: 0.5,
345
349
  fuzzy: false, hover: true, lang: "auto", debug: false,
346
350
  remember: true, autoLoad: true
347
351
  };
@@ -352,6 +356,7 @@ window.__ModuleLoader__.load({
352
356
  h: "dsh-quick-toc.panelH.v1",
353
357
  levels: "dsh-quick-toc.levels.v1",
354
358
  zoom: "dsh-quick-toc.zoom.v1",
359
+ sheetZoom: "dsh-quick-toc.sheetZoom.v1",
355
360
  handle: "dsh-quick-toc.handle.v1",
356
361
  fuzzy: "dsh-quick-toc.fuzzy.v1",
357
362
  hover: "dsh-quick-toc.hover.v1",
@@ -471,6 +476,7 @@ window.__ModuleLoader__.load({
471
476
  var hRaw = read(PREF_KEYS.h);
472
477
  var langRaw = read(PREF_KEYS.lang);
473
478
  var zoomRaw = read(PREF_KEYS.zoom); // NB: absent (null) means 1 — Number(null) is 0!
479
+ var sheetZoomRaw = read(PREF_KEYS.sheetZoom); // same trap
474
480
  var handleRaw = read(PREF_KEYS.handle); // absent (null) means 0.5 — same trap
475
481
  var clampRatio = function (raw, fallback) {
476
482
  return raw !== null && isFinite(Number(raw)) ? Math.min(1, Math.max(0, Math.round(Number(raw) * 100) / 100)) : fallback;
@@ -483,6 +489,7 @@ window.__ModuleLoader__.load({
483
489
  h: hRaw !== null && isFinite(Number(hRaw)) && Number(hRaw) >= 60 ? Number(hRaw) : 0,
484
490
  levels: parseLevels(rawLevels) || LEVELS_ALL.slice(),
485
491
  zoom: clampRatio(zoomRaw, 1),
492
+ sheetZoom: clampRatio(sheetZoomRaw, 1),
486
493
  handle: clampRatio(handleRaw, 0.5),
487
494
  fuzzy: read(PREF_KEYS.fuzzy) === "1",
488
495
  // absent means "on": the hover card predates this switch
@@ -503,6 +510,7 @@ window.__ModuleLoader__.load({
503
510
  h: function (v) { return String(v); },
504
511
  levels: function (v) { return JSON.stringify(v); },
505
512
  zoom: function (v) { return String(v); },
513
+ sheetZoom: function (v) { return String(v); },
506
514
  handle: function (v) { return String(v); },
507
515
  fuzzy: function (v) { return v ? "1" : "0"; },
508
516
  hover: function (v) { return v ? "1" : "0"; },
@@ -529,12 +537,14 @@ window.__ModuleLoader__.load({
529
537
  var v = snap.value;
530
538
  var levels = parseLevelList(v.levels);
531
539
  var zoomNum = Number(v.zoom);
540
+ var sheetZoomNum = Number(v.sheetZoom);
532
541
  var handleNum = Number(v.handle);
533
542
  return {
534
543
  dock: v.dock === "right" ? "right" : "left",
535
544
  lang: v.lang === "zh" || v.lang === "en" ? v.lang : "auto",
536
545
  levels: levels || LEVELS_ALL.slice(),
537
546
  zoom: isFinite(zoomNum) ? Math.min(2, Math.max(0.5, Math.round(zoomNum * 100) / 100)) : 1,
547
+ sheetZoom: isFinite(sheetZoomNum) ? Math.min(2, Math.max(0.5, Math.round(sheetZoomNum * 100) / 100)) : 1,
538
548
  handle: isFinite(handleNum) ? Math.min(1, Math.max(0, Math.round(handleNum * 100) / 100)) : 0.5,
539
549
  fuzzy: v.fuzzy === true,
540
550
  hover: v.hover !== false,
@@ -549,7 +559,7 @@ window.__ModuleLoader__.load({
549
559
  if (!over) return local;
550
560
  return {
551
561
  dock: over.dock, y: local.y, w: local.w, h: local.h,
552
- levels: over.levels, zoom: over.zoom, handle: over.handle, fuzzy: over.fuzzy, hover: over.hover,
562
+ levels: over.levels, zoom: over.zoom, sheetZoom: over.sheetZoom, handle: over.handle, fuzzy: over.fuzzy, hover: over.hover,
553
563
  lang: over.lang, remember: over.remember, debug: over.debug, autoLoad: over.autoLoad
554
564
  };
555
565
  };
@@ -1441,6 +1451,17 @@ window.__ModuleLoader__.load({
1441
1451
  return null;
1442
1452
  }
1443
1453
  };
1454
+ // { uiWorkspace() } bridge: the workspace-navigation service that switches sessions on
1455
+ // the alpha (`uiWorkspace.openSession(target)`); rc.2 has no such service and switches
1456
+ // through `sessions.open`. Same lookup discipline as the session bridge.
1457
+ var hostUiWorkspace = function () {
1458
+ if (!tocHost || typeof tocHost.uiWorkspace !== "function") return null;
1459
+ try {
1460
+ return tocHost.uiWorkspace();
1461
+ } catch (e) {
1462
+ return null;
1463
+ }
1464
+ };
1444
1465
  // Which stage of the bridge is actually ready — reported once at mount so a
1445
1466
  // missing jump loader can be diagnosed from the console instead of guessed.
1446
1467
  // Past "the loader exists" the PAGE STATE is what decides whether a jump can do
@@ -1827,7 +1848,7 @@ window.__ModuleLoader__.load({
1827
1848
  if (node) {
1828
1849
  // put that row back where it SAT (not necessarily at the top): a row the reader left
1829
1850
  // clipped by 120px goes home clipped by 120px, so the view lands exactly where it was
1830
- var d = (node.getBoundingClientRect().top - el.getBoundingClientRect().top) - tgt.off;
1851
+ var d = ((node.getBoundingClientRect().top - el.getBoundingClientRect().top) - tgt.off) / liveZoom();
1831
1852
  if (Math.abs(d) > 0.5) el.scrollTop = el.scrollTop + d;
1832
1853
  }
1833
1854
  syncAtBottom(el);
@@ -2510,14 +2531,17 @@ window.__ModuleLoader__.load({
2510
2531
  if (!node) { sampleAnchor(el); return false; }
2511
2532
  var top = node.getBoundingClientRect().top;
2512
2533
  var moved = el.scrollTop - a.scrollTop; // what the reader (or a jump) scrolled
2534
+ var zf = liveZoom();
2513
2535
  // Growth above the anchor moves it DOWN by exactly as much as the content grew; the
2514
2536
  // reader scrolling up moves it down by exactly as much as they scrolled. Both happen
2515
2537
  // at once when a page lands mid-gesture, so subtract the reader's part rather than
2516
2538
  // refusing to compensate — the old "scrollTop changed, so the reader moved, skip"
2517
2539
  // guard is exactly why a load landing during a scroll was still seen pushing the
2518
2540
  // list. (The list carries `overflow-anchor: none`, so the browser is not adjusting
2519
- // `scrollTop` behind our back and this sum stays honest.)
2520
- var grew = (top - a.top) + moved;
2541
+ // `scrollTop` behind our back and this sum stays honest.) The measured growth is in
2542
+ // visual pixels while `moved` is already in the scroller's own units, so the growth
2543
+ // term is divided by the live factor.
2544
+ var grew = (top - a.top) / zf + moved;
2521
2545
  if (Math.abs(grew) < 0.5 || Math.abs(grew) > (el.clientHeight || 0) * 3) {
2522
2546
  // nothing grew — or something absurd happened (a repaged list, a session swap):
2523
2547
  // re-baseline instead of chasing it
@@ -2532,7 +2556,7 @@ window.__ModuleLoader__.load({
2532
2556
  return false;
2533
2557
  }
2534
2558
  a.scrollTop = el.scrollTop;
2535
- a.top = top - grew; // the offset the anchor keeps from now on
2559
+ a.top = top - grew * zf; // the visual offset the anchor keeps from now on
2536
2560
  return true;
2537
2561
  };
2538
2562
  var holdPlace = function (el) { sampleAnchor(el); };
@@ -2880,6 +2904,28 @@ window.__ModuleLoader__.load({
2880
2904
  if (zoom === 1) return typeof v === "number" ? v + "px" : v;
2881
2905
  return typeof v === "number" ? (v / zoom) + "px" : "calc(" + v + " / " + zoom + ")";
2882
2906
  };
2907
+ // The curtain carries its OWN scale: the docked panel's factor does not apply to it,
2908
+ // and a reader who wants a dense outline in the dock and a larger one across the
2909
+ // curtain (or the other way round) gets two independent sliders.
2910
+ //
2911
+ // The slider's 100% is 0.9 of the size the curtain had before this setting existed: the
2912
+ // reader asked for that base once they could compare it with a scaled one, and 100% is
2913
+ // still what the card reads at the default — the mapping is re-based here instead of
2914
+ // shipping a slider whose default reads 90%, so a stored factor keeps meaning the same
2915
+ // fraction of this base.
2916
+ var SHEET_ZOOM_BASE = 0.9;
2917
+ var sheetZoom = (prefs.sheetZoom > 0 ? prefs.sheetZoom : 1) * SHEET_ZOOM_BASE;
2918
+ // Whichever surface is on screen owns the coordinate space the list lives in: the
2919
+ // scroller's scrollTop is in the zoomed space while getBoundingClientRect reports
2920
+ // visual pixels, so a measured delta has to be divided by THAT factor before it is
2921
+ // written back as a scroll offset (and a CSS length written inside the surface is
2922
+ // divided so it keeps its visual size).
2923
+ var liveZoom = function () { return sheetOpen ? sheetZoom : zoom; };
2924
+ var liveLen = function (v) {
2925
+ var f = liveZoom();
2926
+ if (f === 1) return typeof v === "number" ? v + "px" : v;
2927
+ return typeof v === "number" ? (v / f) + "px" : "calc(" + v + " / " + f + ")";
2928
+ };
2883
2929
 
2884
2930
  // ---- resize drags (right edge = width, bottom edge = height) ----
2885
2931
  // No upper bound and only a keep-it-usable floor: the reader decides how small
@@ -3397,7 +3443,8 @@ window.__ModuleLoader__.load({
3397
3443
  // why the follow stopped "a little short of the bottom": for the newest turn the
3398
3444
  // centre is above the end of the list, so the scroll landed half a viewport early.
3399
3445
  // Aligning to the top needs no clamp at all — the list's own end does the work.
3400
- var delta = node.getBoundingClientRect().top - el.getBoundingClientRect().top;
3446
+ // (Rects are visual; the factor turns the delta back into the scroller's units.)
3447
+ var delta = (node.getBoundingClientRect().top - el.getBoundingClientRect().top) / liveZoom();
3401
3448
  if (Math.abs(delta) > 1) el.scrollTop = el.scrollTop + delta;
3402
3449
  };
3403
3450
  followFrame = requestAnimationFrame(function () { followFrame = requestAnimationFrame(place); });
@@ -3557,9 +3604,33 @@ window.__ModuleLoader__.load({
3557
3604
  // clicking a cross-session hit: switch the session, then leave the keyword for
3558
3605
  // the panel that comes up (see pendingLocate) — the host hit has no seq, so the
3559
3606
  // jump happens on the other side, after the same search runs there.
3560
- var openCrossHit = function (hitSessionId) {
3607
+ //
3608
+ // Two dialects again: rc.2 switches sessions with `sessions.open(id)`, while the
3609
+ // alpha moved that decision to the workspace navigation service
3610
+ // (`uiWorkspace.openSession(target)`) and dropped `open` from the session service
3611
+ // altogether. Both are optional lookups, so whatever the host offers is used and a
3612
+ // host offering neither says so instead of doing nothing.
3613
+ var openSessionById = function (id) {
3614
+ var uiWorkspace = hostUiWorkspace();
3615
+ if (uiWorkspace && typeof uiWorkspace.openSession === "function") {
3616
+ uiWorkspace.openSession(id);
3617
+ return true;
3618
+ }
3561
3619
  var sessions = hostSessions();
3562
- if (!sessions || typeof sessions.open !== "function") {
3620
+ if (sessions && typeof sessions.open === "function") {
3621
+ sessions.open(id);
3622
+ return true;
3623
+ }
3624
+ return false;
3625
+ };
3626
+ var canOpenSession = function () {
3627
+ var uiWorkspace = hostUiWorkspace();
3628
+ if (uiWorkspace && typeof uiWorkspace.openSession === "function") return true;
3629
+ var sessions = hostSessions();
3630
+ return !!(sessions && typeof sessions.open === "function");
3631
+ };
3632
+ var openCrossHit = function (hitSessionId) {
3633
+ if (!canOpenSession()) {
3563
3634
  showBanner(T("search.cross.unavailable"));
3564
3635
  return;
3565
3636
  }
@@ -3570,7 +3641,7 @@ window.__ModuleLoader__.load({
3570
3641
  }
3571
3642
  pendingLocate = { sessionId: hitSessionId, query: query.trim(), at: Date.now() };
3572
3643
  try {
3573
- sessions.open(hitSessionId);
3644
+ openSessionById(hitSessionId);
3574
3645
  } catch (e) {
3575
3646
  pendingLocate = null;
3576
3647
  showBanner(T("search.cross.error") + String(e && e.message ? e.message : e));
@@ -4368,7 +4439,8 @@ window.__ModuleLoader__.load({
4368
4439
  if (!el) return false;
4369
4440
  var node = el.querySelector('[data-group-idx="' + gi + '"]');
4370
4441
  if (!node) return false;
4371
- var delta = node.getBoundingClientRect().top - el.getBoundingClientRect().top;
4442
+ // the rects are visual: divide by the live factor before writing a scroll offset
4443
+ var delta = (node.getBoundingClientRect().top - el.getBoundingClientRect().top) / liveZoom();
4372
4444
  if (Math.abs(delta) > 0.5) el.scrollTop = el.scrollTop + delta;
4373
4445
  return true;
4374
4446
  };
@@ -5429,9 +5501,9 @@ window.__ModuleLoader__.load({
5429
5501
  if (root && typeof root.focus === "function") root.focus();
5430
5502
  },
5431
5503
  style: sheetOpen ? {
5432
- // inside the sheet the panel IS the content: the wrapper owns position,
5433
- // size and the drop-in animation, and the content renders at 100% (the
5434
- // panel-scale preference is a docked-panel setting, not a sheet one)
5504
+ // inside the sheet the panel IS the content: the wrapper owns position, size and
5505
+ // the drop-in animation, while the curtain's own scale (a separate preference)
5506
+ // zooms what is inside it — the box the wrapper gives it stays as it is
5435
5507
  position: "relative",
5436
5508
  width: "100%",
5437
5509
  flex: "1 1 auto",
@@ -5445,6 +5517,8 @@ window.__ModuleLoader__.load({
5445
5517
  overflow: "hidden",
5446
5518
  color: C.text,
5447
5519
  opacity: 1,
5520
+ // the curtain's own scale (the docked panel's does not apply here)
5521
+ zoom: sheetZoom === 1 ? undefined : sheetZoom,
5448
5522
  pointerEvents: chatViewActive ? "auto" : "none"
5449
5523
  } : {
5450
5524
  position: "fixed",
@@ -5611,8 +5685,9 @@ window.__ModuleLoader__.load({
5611
5685
  style: {
5612
5686
  position: "absolute",
5613
5687
  right: (sheetOpen && sheetColW > 0 ? SHEET_SEARCH_W : 0) + 18,
5614
- // below whatever chrome this surface has (measured, see listTop)
5615
- top: listTop + 12,
5688
+ // below whatever chrome this surface has: `listTop` is measured in visual
5689
+ // pixels, so it is divided by the live factor to land where it was measured
5690
+ top: liveLen(listTop + 12),
5616
5691
  width: sheetOpen ? 34 : 26,
5617
5692
  height: sheetOpen ? 34 : 26,
5618
5693
  display: "flex",
@@ -6859,15 +6934,18 @@ window.__ModuleLoader__.load({
6859
6934
  var sliderAwaiting = react.useRef({});
6860
6935
  var commitSlider = function (field, pct) {
6861
6936
  sliderAwaiting.current[field] = pct;
6862
- commit(field === "zoom" ? { zoom: pct / 100 } : { handle: pct / 100 });
6937
+ commit(field === "zoom" ? { zoom: pct / 100 }
6938
+ : field === "sheetZoom" ? { sheetZoom: pct / 100 }
6939
+ : { handle: pct / 100 });
6863
6940
  };
6864
6941
  react.useEffect(function () {
6865
6942
  var a = sliderAwaiting.current;
6866
6943
  var settled = false;
6867
6944
  if (a.zoom !== undefined && Math.round(prefs.zoom * 100) === a.zoom) { delete a.zoom; settled = true; }
6945
+ if (a.sheetZoom !== undefined && Math.round(prefs.sheetZoom * 100) === a.sheetZoom) { delete a.sheetZoom; settled = true; }
6868
6946
  if (a.handle !== undefined && Math.round(prefs.handle * 100) === a.handle) { delete a.handle; settled = true; }
6869
6947
  if (settled) setSliderDrag(null);
6870
- }, [prefs.zoom, prefs.handle]);
6948
+ }, [prefs.zoom, prefs.sheetZoom, prefs.handle]);
6871
6949
  react.useEffect(function () {
6872
6950
  if (sliderDrag === null) return undefined;
6873
6951
  var release = function () {
@@ -6917,6 +6995,7 @@ window.__ModuleLoader__.load({
6917
6995
  });
6918
6996
  };
6919
6997
  var zoomControl = sliderCtl("zoom", "settings.zoom", 50, 200, 5, Math.round(prefs.zoom * 100));
6998
+ var sheetZoomControl = sliderCtl("sheetZoom", "settings.sheetZoom", 50, 200, 5, Math.round(prefs.sheetZoom * 100));
6920
6999
  var handleControl = sliderCtl("handle", "settings.handle", 0, 100, 1, Math.round(prefs.handle * 100));
6921
7000
  var field = function (key, label, hint, control, inline, last) {
6922
7001
  var overridden = Object.prototype.hasOwnProperty.call(user, key);
@@ -6978,6 +7057,7 @@ window.__ModuleLoader__.load({
6978
7057
  function (v) { commit({ dock: v }); }), true),
6979
7058
  field("levels", T("settings.levels"), "", levelsControl(true), true),
6980
7059
  field("zoom", T("settings.zoom"), T("settings.zoom.tip"), zoomControl, false),
7060
+ field("sheetZoom", T("settings.sheetZoom"), T("settings.sheetZoom.tip"), sheetZoomControl, false),
6981
7061
  field("handle", T("settings.handle"), T("settings.handle.tip"), handleControl, false),
6982
7062
  field("fuzzy", T("settings.fuzzy"), "", onoff(prefs.fuzzy, function (v) { commit({ fuzzy: v }); }), true),
6983
7063
  field("hover", T("settings.hover"), "", onoff(prefs.hover, function (v) { commit({ hover: v }); }), true),
@@ -7029,7 +7109,10 @@ window.__ModuleLoader__.load({
7029
7109
  });
7030
7110
  }
7031
7111
 
7032
- var inject = ["slots", "locale", "settingsScope"];
7112
+ // Declared client services only. `slots` and `locale` exist on every supported line;
7113
+ // the settings service is looked up with `ctx.get` instead (see `pickSettingsScope`),
7114
+ // because declaring a name a host does not provide parks the whole package.
7115
+ var inject = ["slots", "locale"];
7033
7116
 
7034
7117
  function apply(ctx) {
7035
7118
  ctx.effect(function () {
@@ -7094,6 +7177,16 @@ window.__ModuleLoader__.load({
7094
7177
  } catch (e) {
7095
7178
  return null;
7096
7179
  }
7180
+ },
7181
+ // The alpha's session navigation (uiWorkspace.openSession); the older line has no
7182
+ // such service and switches sessions through `sessions.open` instead. Optional by
7183
+ // the same rule: whichever exists is used, and neither existing is reported.
7184
+ uiWorkspace: function () {
7185
+ try {
7186
+ return typeof ctx.get === "function" ? (ctx.get("uiWorkspace") || null) : null;
7187
+ } catch (e) {
7188
+ return null;
7189
+ }
7097
7190
  }
7098
7191
  };
7099
7192
 
@@ -7114,15 +7207,86 @@ window.__ModuleLoader__.load({
7114
7207
  });
7115
7208
  });
7116
7209
 
7117
- // The client settings scope: the browser face of the namespace the host half
7118
- // registers. The store adopts its snapshot as the authoritative layer for the
7119
- // host-backed fields and routes writes to it; on a host without the service (or
7210
+ // The client settings scope: the browser face of the settings namespace the host
7211
+ // half serves. The store adopts its snapshot as the authoritative layer for the
7212
+ // host-backed fields and routes writes to it; on a host without either dialect (or
7120
7213
  // on a non-loopback page, where DSH keeps settings read-only) the localStorage
7121
7214
  // mirror keeps every preference working, exactly as 0.5.x did.
7122
- var boundScope = null;
7123
- if (ctx.settingsScope && typeof ctx.settingsScope.bind === "function") {
7215
+ //
7216
+ // TWO DIALECTS, ONE SHAPE. Up to DSH 0.1.5-rc.2 the host serves a
7217
+ // `settingsScope` service whose `bind({namespace})` returns a namespace scope; from
7218
+ // 0.1.7-alpha.1 on, that service is gone and the same object is handed out by the
7219
+ // `configForms` service per PROFILE ENTRY ID (`get(id)`). Both expose
7220
+ // `getSnapshot()` / `mutate(ops)` / `unset(field)` over the same snapshot fields, so
7221
+ // one adapter covers them.
7222
+ //
7223
+ // Neither is declared in `inject`: a name the running host does not provide parks
7224
+ // the whole package until it appears, and on 0.1.7-alpha.1 that turned into a web
7225
+ // boot that never finished (`1 entry did not activate: dsh-quick-toc: pending
7226
+ // (waiting for service: settingsScope)`), taking the entire GUI with it. Both
7227
+ // declaration-free routes are tried — `ctx.get(name)` first, then the service
7228
+ // property the older line has always offered — and a missing service simply leaves
7229
+ // the localStorage layer in charge.
7230
+ var getService = function (name) {
7231
+ try {
7232
+ if (typeof ctx.get === "function") {
7233
+ var found = ctx.get(name);
7234
+ if (found) return found;
7235
+ }
7236
+ } catch (e) { /* fall through to the property read */ }
7237
+ try {
7238
+ return ctx[name] || null;
7239
+ } catch (e) {
7240
+ return null;
7241
+ }
7242
+ };
7243
+ // Which namespace the `configForms` dialect should be asked for. Its key is the
7244
+ // profile entry id, which the installing profile's patch decides (this package's own
7245
+ // cordis.patch.yml inserts the row as `quick-toc`), so the served schema is what
7246
+ // identifies us: a namespace carrying this plugin's own fields is ours whatever it
7247
+ // is called.
7248
+ var formsNamespace = function (forms) {
7249
+ var candidates = ["quick-toc", "dsh-quick-toc"];
7250
+ var served = [];
7251
+ try {
7252
+ var view = forms.describe ? forms.describe().getSnapshot().view : null;
7253
+ served = (view && view.namespaces) || [];
7254
+ } catch (e) {
7255
+ served = [];
7256
+ }
7257
+ for (var i = 0; i < served.length; i++) {
7258
+ var ns = served[i] || {};
7259
+ var json = "";
7260
+ try { json = JSON.stringify(ns.schema || {}); } catch (e) { json = ""; }
7261
+ if (json.indexOf("sheetZoom") >= 0 && json.indexOf("autoLoad") >= 0) return ns.ns;
7262
+ }
7263
+ for (var j = 0; j < candidates.length; j++) {
7264
+ try {
7265
+ var form = forms.get(candidates[j]);
7266
+ if (form && form.getSnapshot && form.getSnapshot().status === "ready") return candidates[j];
7267
+ } catch (e) { /* try the next candidate */ }
7268
+ }
7269
+ return candidates[0];
7270
+ };
7271
+ var pickSettingsScope = function () {
7272
+ var scope = getService("settingsScope");
7273
+ if (scope && typeof scope.bind === "function") {
7274
+ try {
7275
+ var bound = scope.bind({ namespace: NAMESPACE });
7276
+ if (bound) return bound;
7277
+ } catch (e) { /* fall through to the newer dialect */ }
7278
+ }
7279
+ var forms = getService("configForms");
7280
+ if (forms && typeof forms.get === "function") {
7281
+ try {
7282
+ return forms.get(formsNamespace(forms));
7283
+ } catch (e) { /* leave the localStorage layer in charge */ }
7284
+ }
7285
+ return null;
7286
+ };
7287
+ var boundScope = pickSettingsScope();
7288
+ if (boundScope) {
7124
7289
  try {
7125
- boundScope = ctx.settingsScope.bind({ namespace: NAMESPACE });
7126
7290
  prefsStore.attachHost(boundScope);
7127
7291
  } catch (e) {
7128
7292
  boundScope = null;
@@ -7130,11 +7294,26 @@ window.__ModuleLoader__.load({
7130
7294
  }
7131
7295
  }
7132
7296
 
7133
- // The plugin-configuration card: Settings → Plugins → Plugin configuration, a
7134
- // keyed slot dispatched only for namespaces the Host actually serves. Registering
7135
- // it only when the scope bound keeps a half-configured host from rendering a card
7136
- // that could not read or write anything.
7297
+ // The plugin-configuration card, in whichever seat the running host offers:
7298
+ // * up to rc.2 it is a keyed `settings.plugin.item` cell under
7299
+ // Settings → Plugins → Plugin configuration, keyed by the settings namespace;
7300
+ // * on the alpha the plugin's own page in the sidebar's Plugins list carries it,
7301
+ // as the bundle's configuration keyed by the npm package name
7302
+ // (`plugins.bundle.config`). A row-level cell (`plugins.row.config`, keyed
7303
+ // `<package>#<row id>`) exists too, but a registration there did not reach that
7304
+ // page in 0.1.7-alpha.1 while the bundle cell did — and these preferences are the
7305
+ // plugin's, not one row's, so the bundle page is the right seat anyway.
7306
+ // Registering into a slot a given host never declares is inert (the registration
7307
+ // waits), so both are offered unconditionally, and only once a scope bound keeps a
7308
+ // half-configured host from rendering a card that could not read or write anything.
7137
7309
  if (boundScope) {
7310
+ ctx.slots.inject("plugins.bundle.config", function () {
7311
+ return ctx.slots.register({
7312
+ name: "plugins.bundle.config",
7313
+ key: "dsh-quick-toc",
7314
+ locale: "dsh-quick-toc"
7315
+ }, TocSettingsCard);
7316
+ });
7138
7317
  ctx.slots.inject("settings.plugin.item", function () {
7139
7318
  return ctx.slots.register({
7140
7319
  name: "settings.plugin.item",
package/lib/index.js CHANGED
@@ -16,70 +16,109 @@ import z from "@deepseek-ai/schemastery";
16
16
  /** Namespace the browser half binds and the card is keyed on. */
17
17
  export const NAMESPACE = "dsh-quick-toc";
18
18
 
19
+ /**
20
+ * Mark one preference field as a live value where the running schemastery knows
21
+ * the modifier, and hand the field back untouched where it does not; see
22
+ * `apply` for why the difference exists.
23
+ * @param {object} schema - the field's schema.
24
+ * @returns {object} the same schema, volatile on hosts that support it.
25
+ */
26
+ const volatileSchema = (schema) =>
27
+ typeof schema.volatile === "function" ? schema.volatile() : schema;
28
+
19
29
  /**
20
30
  * The durable settings section. Values are written by the browser half's card
21
31
  * (and by the panel itself), so the schema is the validation layer, not a form.
22
32
  */
23
33
  export const Config = z.object({
24
- lang: z
34
+ lang: volatileSchema(z
25
35
  .string()
26
36
  .pattern(/^(auto|zh|en)$/)
27
37
  .default("auto")
28
- .description("Panel language: auto follows DSH, zh or en forces one"),
29
- dock: z
38
+ .description("Panel language: auto follows DSH, zh or en forces one")),
39
+ dock: volatileSchema(z
30
40
  .string()
31
41
  .pattern(/^(left|right)$/)
32
42
  .default("left")
33
- .description("Which edge the panel docks to by default"),
34
- levels: z
43
+ .description("Which edge the panel docks to by default")),
44
+ levels: volatileSchema(z
35
45
  .array(z.number().min(1).max(6))
36
46
  .default([1, 2, 3, 4, 5, 6])
37
- .description("Heading levels the outline shows"),
38
- zoom: z
47
+ .description("Heading levels the outline shows")),
48
+ zoom: volatileSchema(z
49
+ .number()
50
+ .min(0.5)
51
+ .max(2)
52
+ .default(1)
53
+ .description("Scale factor for the panel's content (text, icons, buttons) from 50% to 200%; the panel's own size is unchanged")),
54
+ sheetZoom: volatileSchema(z
39
55
  .number()
40
56
  .min(0.5)
41
57
  .max(2)
42
58
  .default(1)
43
- .description("Scale factor for the panel's content (text, icons, buttons) from 50% to 200%; the panel's own size is unchanged"),
44
- handle: z
59
+ .description("Scale factor for the curtain's content (text, icons, buttons) from 50% to 200%; the curtain's own size is unchanged")),
60
+ handle: volatileSchema(z
45
61
  .number()
46
62
  .min(0)
47
63
  .max(1)
48
64
  .default(0.5)
49
- .description("Vertical position of the collapsed-panel handle: 0 = bottom, 1 = top of the conversation area"),
50
- fuzzy: z
65
+ .description("Vertical position of the collapsed-panel handle: 0 = bottom, 1 = top of the conversation area")),
66
+ fuzzy: volatileSchema(z
51
67
  .boolean()
52
68
  .default(false)
53
- .description("Start with fuzzy search on"),
54
- hover: z
69
+ .description("Start with fuzzy search on")),
70
+ hover: volatileSchema(z
55
71
  .boolean()
56
72
  .default(true)
57
- .description("Show the hover preview card"),
58
- remember: z
73
+ .description("Show the hover preview card")),
74
+ remember: volatileSchema(z
59
75
  .boolean()
60
76
  .default(true)
61
- .description("Remember where you were reading in each session and return there when it is reopened"),
62
- autoLoad: z
77
+ .description("Remember where you were reading in each session and return there when it is reopened")),
78
+ autoLoad: volatileSchema(z
63
79
  .boolean()
64
80
  .default(true)
65
- .description("Load history automatically when the outline is scrolled into turns that are not loaded yet; off means nothing loads until a row is clicked"),
66
- debug: z
81
+ .description("Load history automatically when the outline is scrolled into turns that are not loaded yet; off means nothing loads until a row is clicked")),
82
+ debug: volatileSchema(z
67
83
  .boolean()
68
84
  .default(false)
69
- .description("Print the mount diagnostic to the browser console"),
85
+ .description("Print the mount diagnostic to the browser console")),
70
86
  });
71
87
 
72
88
  /**
73
- * Register the settings section. The values themselves are read and written by
74
- * the browser half through the client settings scope.
89
+ * Register the settings namespace, in whichever dialect the running host speaks.
90
+ *
91
+ * Up to DSH 0.1.5-rc.2 the settings service registers a named section with a
92
+ * schema and reads and writes it by that name (`installSection`). From
93
+ * 0.1.7-alpha.1 on, the service addresses settings by PROFILE ENTRY ID, derives
94
+ * the entry's form from the schema this module exports, and only projects the
95
+ * fields marked `.volatile()` — an instance announces its page policy with
96
+ * `settings.configure` instead. Both are pure registrations that the browser
97
+ * half then reads through its own service, so the plugin does not care which
98
+ * one ran.
99
+ *
100
+ * `volatileSchema` marks a field only where the running schemastery knows the
101
+ * modifier: it arrived in schemastery 3.18.3 (the alpha), and calling it on
102
+ * 3.18.2 (the rc line) would throw while the module is being imported.
103
+ *
75
104
  * @param {object} ctx - host cordis context.
76
105
  * @param {object} [config] - the plugin entry's composition config (base layer).
77
106
  */
78
107
  export function apply(ctx, config) {
79
108
  ctx.inject(["settings"], (settingsCtx) => {
80
- settingsCtx.settings.installSection(ctx, NAMESPACE, Config, config ?? {}, {
81
- setSource: () => {},
82
- onChange: () => {},
83
- });
109
+ const settings = settingsCtx.settings;
110
+ if (settings && typeof settings.installSection === "function") {
111
+ settings.installSection(ctx, NAMESPACE, Config, config ?? {}, {
112
+ setSource: () => {},
113
+ onChange: () => {},
114
+ });
115
+ return;
116
+ }
117
+ if (settings && typeof settings.configure === "function") {
118
+ // `auto: true` keeps the host's generated page as the fallback wherever no
119
+ // custom page of ours is reachable (the browser half renders one where the
120
+ // host offers a configuration cell for this entry).
121
+ settingsCtx.effect(() => settings.configure({ auto: true }, ctx.fiber));
122
+ }
84
123
  });
85
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-quick-toc",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Conversation TOC for DeepSeek Harness (DSH): a whole-session, turn-grouped Markdown outline with hover previews, a level filter, a full-width curtain, jump-to-end buttons, title/full-text/cross-session search and a Chinese/English interface.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -14,7 +14,7 @@
14
14
  "LICENSE"
15
15
  ],
16
16
  "engines": {
17
- "dsh": ">=0.1.5-rc.1"
17
+ "dsh": ">=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0"
18
18
  },
19
19
  "exports": {
20
20
  ".": "./lib/index.js",
@@ -36,15 +36,18 @@
36
36
  "compatibility": {
37
37
  "dshReleases": {
38
38
  "0.1.5-rc.1": "compatible",
39
- "0.1.5-rc.2": "compatible"
39
+ "0.1.5-rc.2": "compatible",
40
+ "0.1.5-rc.3": "compatible",
41
+ "0.1.7-alpha.1": "compatible",
42
+ "0.1.7-alpha.2": "compatible"
40
43
  }
41
44
  }
42
45
  },
43
46
  "peerDependencies": {
44
47
  "react": "^18.2.0",
45
48
  "@deepseek-ai/schemastery": "^3.18.0",
46
- "@deepseek-ai/dsh-client-ui-chat": ">=0.1.5-rc.1",
47
- "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.5-rc.1"
49
+ "@deepseek-ai/dsh-client-ui-chat": ">=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0",
50
+ "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0"
48
51
  },
49
52
  "peerDependenciesMeta": {
50
53
  "@deepseek-ai/schemastery": {