dsh-plugin-lookatstudy 0.20.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -817,6 +817,11 @@ window.__ModuleLoader__.load({
817
817
  .lks-ui[data-lks-theme='light'] .lks14-threadmenu{background:rgb(255 255 255/0.98);border-color:var(--border-faint);box-shadow:0 12px 32px -8px rgb(0 0 0/0.18)}
818
818
  .lks-ui[data-lks-theme='light'] .lks14-ctxpanel{background:rgb(255 255 255/0.98);box-shadow:0 8px 24px rgb(0 0 0/0.12)}
819
819
  /* ambient glyph motion sleeps under reduced-motion (A7 never had a guard) */
820
+ /* v0.29 pane-resize: the grip line lights the upstream accent under the skin
821
+ (base sheet's host-token color is the no-skin fallback; the handle itself
822
+ is structural lks14- and lives in styles.ts) */
823
+ .lks-ui .lks14-panehandle:hover::after,.lks-ui .lks14-panehandle[data-dragging='true']::after{background:color-mix(in srgb,var(--accent) 55%,transparent)}
824
+
820
825
  @media (prefers-reduced-motion: reduce){
821
826
  .lks-ui .lks-lessorow.st-mastered .lks-lessorow-glyph svg,.lks-ui .lks-hdr-xp .lks-hdr-glyph,.lks-ui .lks-hdr-streak .lks-hdr-glyph{animation:none}
822
827
  }
@@ -887,7 +892,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
887
892
  .lks14-col{display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden}
888
893
 
889
894
  /* 左 rail: course picker, tree, review, import */
890
- .lks14-rail{flex:0 0 300px;border-right:none;padding:0;overflow:hidden;position:relative}
895
+ .lks14-rail{flex:0 0 var(--lks-rail-w,300px);border-right:none;padding:0;overflow:hidden;position:relative;transition:flex-basis 150ms ease}
891
896
  .lks14-railhead{display:flex;align-items:center;gap:6px;margin-top:10px}
892
897
  .lks14-railtitle{font-weight:600;font-size:13.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
893
898
  .lks14-railsub{font-size:12px;color:var(--dsw-alias-label-secondary);margin:6px 0}
@@ -902,7 +907,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
902
907
  .lks14-hint{font-size:11.5px;color:var(--dsw-alias-label-tertiary);margin-top:6px;line-height:1.6}
903
908
 
904
909
  /* 中 chat: the tutor stream + its own composer (upstream ChatStream/ChatComposer) */
905
- .lks14-chat{flex:0 0 auto;width:clamp(480px,45%,800px);min-width:0;border-right:none}
910
+ .lks14-chat{flex:0 0 auto;width:var(--lks-chat-w,clamp(480px,45%,800px));min-width:0;border-right:none;transition:width 150ms ease}
906
911
  .lks14-chatlesson{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:400;color:var(--dsw-alias-label-tertiary)}
907
912
  .lks14-pills{display:inline-flex;gap:2px}
908
913
  .lks14-pill{border:none;background:none;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;padding:2px 8px;border-radius:7px;cursor:pointer}
@@ -933,6 +938,22 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
933
938
  .lks14-composertext{flex:1;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:7px 10px;resize:none;box-sizing:border-box}
934
939
  .lks14-composertext:focus{outline:none;border-color:var(--dsw-alias-state-business-primary)}
935
940
 
941
+ /* ============================================================
942
+ * v0.29 pane-resize 拖拽手柄(PaneResizeHandle)——
943
+ * 栏间 6px 竖条,视觉握线默认隐形、hover/拖拽亮 accent;
944
+ * 命中区触屏 44px(触屏密度红线),细指针收窄 12px(不吃栏内容点击)。
945
+ * pane 分隔语汇 = surface 色阶不加描边:握线是 hover 才现身的交互
946
+ * affordance,不是常驻分隔线,不违反 v0.6「pane 间无 border」。
947
+ * ============================================================ */
948
+ .lks14-panehandle{position:relative;flex:none;width:6px;cursor:col-resize;touch-action:none;z-index:5}
949
+ .lks14-panehandle::before{content:"";position:absolute;top:0;bottom:0;left:50%;width:44px;transform:translateX(-50%)}
950
+ @media (pointer:fine){.lks14-panehandle::before{width:12px}}
951
+ .lks14-panehandle::after{content:"";position:absolute;top:0;bottom:0;left:50%;width:2px;transform:translateX(-50%);border-radius:1px;background:transparent;transition:background 120ms}
952
+ .lks14-panehandle:hover::after,.lks14-panehandle[data-dragging='true']::after{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 55%,transparent)}
953
+ /* 拖拽中:全局列宽光标 + 禁文本选择(宽度直写不经过 React) */
954
+ body.pane-dragging{cursor:col-resize;user-select:none}
955
+ body.pane-dragging *{cursor:col-resize}
956
+
936
957
  /* 右 notebook: 讲解/概念图/笔记 (upstream NotebookPanel) */
937
958
  .lks14-note{flex:1 1 auto;min-width:440px;padding:0;overflow-y:auto}
938
959
  .lks14-lessonhead{margin:10px 0 6px}
@@ -1001,6 +1022,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
1001
1022
  .lks14-body[data-pane='rail'] .lks14-righthalf{display:none}
1002
1023
  .lks14-body[data-pane='chat'] .lks14-rail,.lks14-body[data-pane='chat'] .lks14-note,
1003
1024
  .lks14-body[data-pane='note'] .lks14-rail,.lks14-body[data-pane='note'] .lks14-chat{display:none}
1025
+ .lks14-panehandle{display:none}
1004
1026
  .lks14-body[data-pane='rail'] .lks14-rail{display:flex;max-height:none;flex:1 1 auto}
1005
1027
  .lks14-body[data-pane='note'] .lks14-note{display:flex}
1006
1028
  }
@@ -2129,6 +2151,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
2129
2151
  "note.retryRender": "重试渲染",
2130
2152
  "zoom.in": "放大面板字号",
2131
2153
  "zoom.out": "缩小面板字号",
2154
+ "pane.rail.aria": "调整课程栏宽度",
2155
+ "pane.chat.aria": "调整对话栏宽度",
2156
+ "pane.tip": "拖拽调宽,双击重置",
2132
2157
  "palette.label": "命令面板",
2133
2158
  "palette.placeholder": "搜索课时/课程,或选择动作…",
2134
2159
  "palette.lessons": "课时",
@@ -2472,6 +2497,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
2472
2497
  "note.retryRender": "Retry render",
2473
2498
  "zoom.in": "Increase panel font size",
2474
2499
  "zoom.out": "Decrease panel font size",
2500
+ "pane.rail.aria": "Resize the course pane width",
2501
+ "pane.chat.aria": "Resize the chat pane width",
2502
+ "pane.tip": "Drag to resize, double-click to reset",
2475
2503
  "palette.label": "Command palette",
2476
2504
  "palette.placeholder": "Search lessons/courses or pick an action…",
2477
2505
  "palette.lessons": "Lesson",
@@ -3327,6 +3355,149 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
3327
3355
  }
3328
3356
  return out.join("\n");
3329
3357
  }
3358
+ const CHAT_MAX = 1100;
3359
+ const clampNum = (v, min, max) => Math.min(max, Math.max(min, v));
3360
+ /** 对话栏未定制时的默认宽数值形式(= clamp(480px,45%,800px) 在 rowW 下;rowW = 容器宽 − rail 有效宽)。 */
3361
+ function defaultChatWidth(rowW) {
3362
+ return clampNum(rowW * .45, 480, 800);
3363
+ }
3364
+ /** 课程栏拖拽候选钳制:reserved = 对话栏有效宽 + NOTE_MIN + 手柄。 */
3365
+ function clampRailCandidate(next, containerW, reserved) {
3366
+ return clampNum(next, 240, Math.min(480, Math.max(240, containerW - reserved)));
3367
+ }
3368
+ /** 对话栏拖拽候选钳制:reserved = 课程栏有效宽 + NOTE_MIN;极端挤压允许压到硬底。 */
3369
+ function clampChatCandidate(next, rowW, reserved) {
3370
+ return clampNum(next, 480, Math.min(CHAT_MAX, Math.max(320, rowW - reserved)));
3371
+ }
3372
+ /** 存储值解析:空/非数/超区间 → null(回默认;历史脏值不硬吃)。 */
3373
+ function parseStoredWidth(v, min, max) {
3374
+ if (v == null || v.trim() === "") return null;
3375
+ const n = Number(v);
3376
+ if (!Number.isFinite(n) || n < min || n > max) return null;
3377
+ return Math.round(n);
3378
+ }
3379
+ /**
3380
+ * The panel root's font-zoom factor (E7 zoom scales the px-fixed layout) —
3381
+ * getBoundingClientRect returns VISUAL px under zoom while flex-basis/width
3382
+ * apply in LAYOUT px, so every drag measurement divides by this (upstream had
3383
+ * no zoom; this is the one place our port must normalize).
3384
+ */
3385
+ function panelZoomOf(el) {
3386
+ const z = el?.closest(".lks14")?.style.zoom;
3387
+ const n = z === void 0 || z === "" ? NaN : Number(z);
3388
+ return Number.isFinite(n) && n > 0 ? n : 1;
3389
+ }
3390
+ /**
3391
+ * 渲染时有效宽度求解(持久化值 × 当前容器宽):拖拽钳制保证提交时刻不溢出,
3392
+ * 但宿主列后来变窄会破坏旧组合 —— 存储值按当前容器重新压回预算。
3393
+ * 返回 null = 未定制(挂 CSS 默认)。求解序 acyclic:rail 基准(默认 300 是
3394
+ * 常数)→ row 宽 → chat 默认(% 对 row)→ rail 预算按 chat 有效宽。
3395
+ */
3396
+ function solvePaneWidths(railStored, chatStored, containerW) {
3397
+ const rowW = Math.max(0, containerW - (railStored ?? 300) - 12);
3398
+ const chat = chatStored == null ? null : clampChatCandidate(chatStored, rowW, 440);
3399
+ const chatBasis = chat ?? defaultChatWidth(rowW);
3400
+ return {
3401
+ rail: railStored == null ? null : clampRailCandidate(railStored, containerW, chatBasis + 440 + 12),
3402
+ chat
3403
+ };
3404
+ }
3405
+ //#endregion
3406
+ //#region src/client/paneresize.tsx
3407
+ /**
3408
+ * PaneResizeHandle —— 栏间拖拽手柄(ported from upstream LookatStudy v0.29
3409
+ * issue #14, src/renderer/components/PaneResizeHandle.tsx @ v0.33.2, MIT —
3410
+ * adapted 2026-09-12; structural deviations recorded here):
3411
+ * - the drag target is the handle's OWN previousElementSibling (the column it
3412
+ * follows in the DOM), not a document.querySelector testid anchor;
3413
+ * - rail widths apply through flex-basis (its class is flex:0 0 basis), chat
3414
+ * widths through width (flex-basis:auto) — the side prop picks the channel;
3415
+ * - on commit the parent's layout effect clears the inline width once the
3416
+ * container-level CSS vars carry the truth (same paint, no flash), so this
3417
+ * component only writes during the drag and hands over on finish.
3418
+ *
3419
+ * 命中区触屏 44px / 细指针 12px(styles.ts ::before);拖拽期宽度 imperative
3420
+ * 直写目标元素(对话栏是消息流整树,逐帧 setState 不可接受),100ms 节流,
3421
+ * 松手终值必达;双击 = 重置回响应式默认(清 inline + onCommit(null),类/clamp
3422
+ * 接管)。a11y:role=separator + aria-label;键盘调整不在本期范围。
3423
+ */
3424
+ /** 拖拽中宽度应用节流(松手必应用终值,不受节流影响)。 */
3425
+ const DRAG_APPLY_MIN_MS = 100;
3426
+ /** Inline width ← px;null clears it (class/var/CSS default takes over). */
3427
+ function applyWidth(el, side, px) {
3428
+ if (side === "rail") el.style.flexBasis = px === null ? "" : `${String(px)}px`;
3429
+ else el.style.width = px === null ? "" : `${String(px)}px`;
3430
+ }
3431
+ function PaneResizeHandle(props) {
3432
+ const [dragging, setDragging] = (0, react.useState)(false);
3433
+ const dragRef = (0, react.useRef)(null);
3434
+ /** props 进 effect 闭包会滞后:拖拽全程用 ref 取最新。 */
3435
+ const propsRef = (0, react.useRef)(props);
3436
+ propsRef.current = props;
3437
+ (0, react.useEffect)(() => {
3438
+ if (!dragging) return;
3439
+ document.body.classList.add("pane-dragging");
3440
+ const onMove = (e) => {
3441
+ const d = dragRef.current;
3442
+ if (d == null) return;
3443
+ const now = performance.now();
3444
+ if (now - d.lastAppliedAt < DRAG_APPLY_MIN_MS) return;
3445
+ d.lastAppliedAt = now;
3446
+ applyWidth(d.targetEl, propsRef.current.side, propsRef.current.clampLive(d.startW + (e.clientX - d.startX)));
3447
+ };
3448
+ const finish = (e) => {
3449
+ const d = dragRef.current;
3450
+ dragRef.current = null;
3451
+ setDragging(false);
3452
+ if (d == null) return;
3453
+ d.targetEl.style.transition = "";
3454
+ const finalW = propsRef.current.clampLive(d.startW + (e.clientX - d.startX));
3455
+ applyWidth(d.targetEl, propsRef.current.side, finalW);
3456
+ propsRef.current.onCommit(finalW);
3457
+ };
3458
+ window.addEventListener("pointermove", onMove);
3459
+ window.addEventListener("pointerup", finish);
3460
+ window.addEventListener("pointercancel", finish);
3461
+ return () => {
3462
+ document.body.classList.remove("pane-dragging");
3463
+ window.removeEventListener("pointermove", onMove);
3464
+ window.removeEventListener("pointerup", finish);
3465
+ window.removeEventListener("pointercancel", finish);
3466
+ };
3467
+ }, [dragging]);
3468
+ const startDrag = (e) => {
3469
+ if (e.button !== 0) return;
3470
+ const target = e.currentTarget.previousElementSibling;
3471
+ if (!(target instanceof HTMLElement)) return;
3472
+ e.preventDefault();
3473
+ dragRef.current = {
3474
+ startX: e.clientX,
3475
+ startW: target.getBoundingClientRect().width / panelZoomOf(target),
3476
+ lastAppliedAt: 0,
3477
+ targetEl: target
3478
+ };
3479
+ target.style.transition = "none";
3480
+ setDragging(true);
3481
+ try {
3482
+ e.currentTarget.setPointerCapture(e.pointerId);
3483
+ } catch {}
3484
+ };
3485
+ return (0, react.createElement)("div", {
3486
+ role: "separator",
3487
+ "aria-orientation": "vertical",
3488
+ "aria-label": props.side === "rail" ? tr("pane.rail.aria") : tr("pane.chat.aria"),
3489
+ "data-tooltip": tr("pane.tip"),
3490
+ "data-testid": `pane-handle-${props.side}`,
3491
+ "data-dragging": dragging ? "true" : void 0,
3492
+ className: "lks14-panehandle",
3493
+ onPointerDown: startDrag,
3494
+ onDoubleClick: (e) => {
3495
+ const target = e.currentTarget.previousElementSibling;
3496
+ if (target instanceof HTMLElement) applyWidth(target, props.side, null);
3497
+ props.onCommit(null);
3498
+ }
3499
+ });
3500
+ }
3330
3501
  //#endregion
3331
3502
  //#region src/client/views.tsx
3332
3503
  /**
@@ -6948,6 +7119,56 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
6948
7119
  localStorage.setItem("dsh-plugin-lookatstudy:zoom", String(clamped));
6949
7120
  } catch {}
6950
7121
  };
7122
+ const [railW, setRailW] = (0, react.useState)(() => parseStoredWidth(localStorage.getItem("dsh-plugin-lookatstudy:pane-rail"), 240, 480));
7123
+ const [chatW, setChatW] = (0, react.useState)(() => parseStoredWidth(localStorage.getItem("dsh-plugin-lookatstudy:pane-chat"), 480, CHAT_MAX));
7124
+ const [paneBoxW, setPaneBoxW] = (0, react.useState)(0);
7125
+ const bodyEl = (0, react.useRef)(null);
7126
+ const rowEl = (0, react.useRef)(null);
7127
+ const panesCustom = railW !== null || chatW !== null;
7128
+ (0, react.useEffect)(() => {
7129
+ const el = bodyEl.current;
7130
+ if (el === null || !panesCustom) return;
7131
+ const ro = new ResizeObserver((entries) => {
7132
+ for (const entry of entries) setPaneBoxW(Math.round(entry.contentRect.width));
7133
+ });
7134
+ ro.observe(el);
7135
+ return () => {
7136
+ ro.disconnect();
7137
+ };
7138
+ }, [panesCustom]);
7139
+ const solvedPanes = solvePaneWidths(railW, chatW, paneBoxW > 0 ? paneBoxW : bodyEl.current?.clientWidth ?? 0);
7140
+ (0, react.useLayoutEffect)(() => {
7141
+ const el = bodyEl.current;
7142
+ if (el === null) return;
7143
+ if (solvedPanes.rail !== null) el.style.setProperty("--lks-rail-w", `${String(solvedPanes.rail)}px`);
7144
+ else el.style.removeProperty("--lks-rail-w");
7145
+ if (solvedPanes.chat !== null) el.style.setProperty("--lks-chat-w", `${String(solvedPanes.chat)}px`);
7146
+ else el.style.removeProperty("--lks-chat-w");
7147
+ for (const col of el.querySelectorAll(".lks14-rail, .lks14-chat")) {
7148
+ col.style.flexBasis = "";
7149
+ col.style.width = "";
7150
+ }
7151
+ }, [solvedPanes.rail, solvedPanes.chat]);
7152
+ const commitPaneWidth = (which, px) => {
7153
+ const key = which === "rail" ? "dsh-plugin-lookatstudy:pane-rail" : "dsh-plugin-lookatstudy:pane-chat";
7154
+ const value = px === null ? null : Math.round(px);
7155
+ if (which === "rail") setRailW(value);
7156
+ else setChatW(value);
7157
+ try {
7158
+ if (value === null) localStorage.removeItem(key);
7159
+ else localStorage.setItem(key, String(value));
7160
+ } catch {}
7161
+ };
7162
+ const railClampLive = (px) => {
7163
+ const el = bodyEl.current;
7164
+ const containerW = (el?.getBoundingClientRect().width ?? 1280) / panelZoomOf(el);
7165
+ const rowW = Math.max(0, containerW - 300 - 12);
7166
+ return clampRailCandidate(px, containerW, (solvedPanes.chat ?? defaultChatWidth(rowW)) + 440 + 12);
7167
+ };
7168
+ const chatClampLive = (px) => {
7169
+ const el = rowEl.current;
7170
+ return clampChatCandidate(px, (el?.getBoundingClientRect().width ?? 900) / panelZoomOf(el), 440);
7171
+ };
6951
7172
  const paletteBus = (0, react.useRef)({});
6952
7173
  const [paletteOpen, setPaletteOpen] = (0, react.useState)(false);
6953
7174
  (0, react.useEffect)(() => {
@@ -7279,6 +7500,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
7279
7500
  const progress = data?.progress ?? null;
7280
7501
  const body = (0, react.createElement)("div", {
7281
7502
  className: "lks14-body",
7503
+ ref: bodyEl,
7282
7504
  "data-pane": narrowPane,
7283
7505
  onTouchStart: (e) => {
7284
7506
  const t = e.touches[0];
@@ -7310,6 +7532,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
7310
7532
  importProgress,
7311
7533
  stop,
7312
7534
  paletteBus
7535
+ }), (0, react.createElement)(PaneResizeHandle, {
7536
+ side: "rail",
7537
+ clampLive: railClampLive,
7538
+ onCommit: (px) => commitPaneWidth("rail", px)
7313
7539
  }), (0, react.createElement)("div", { className: "lks14-righthalf" }, (0, react.createElement)("div", { className: "lks14-appheader" }, (0, react.createElement)("span", { className: "lks-hdr-title" }, lesson?.courseTitle ?? tr("tab.label")), (0, react.createElement)("span", {
7314
7540
  className: "lks-hdr-xp",
7315
7541
  "data-tooltip": tr("header.xp", { xp: progress?.totalXp ?? 0 })
@@ -7339,7 +7565,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
7339
7565
  onClick: () => {
7340
7566
  setZoom(zoomTier + .1);
7341
7567
  }
7342
- }, "A+"))), (0, react.createElement)("div", { className: "lks14-row" }, examLessonActive ? (0, react.createElement)("div", { className: "lks14-col lks14-chat" }, (0, react.createElement)(ExamView, {
7568
+ }, "A+"))), (0, react.createElement)("div", {
7569
+ className: "lks14-row",
7570
+ ref: rowEl
7571
+ }, examLessonActive ? (0, react.createElement)("div", { className: "lks14-col lks14-chat" }, (0, react.createElement)(ExamView, {
7343
7572
  lessonId: lesson.lessonId,
7344
7573
  sectionTitle: lesson.sectionTitle,
7345
7574
  paused: examLeave !== null,
@@ -7365,6 +7594,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
7365
7594
  contextMeter,
7366
7595
  contextBreakdown: breakdown,
7367
7596
  uploadAttachment
7597
+ }), (0, react.createElement)(PaneResizeHandle, {
7598
+ side: "chat",
7599
+ clampLive: chatClampLive,
7600
+ onCommit: (px) => commitPaneWidth("chat", px)
7368
7601
  }), (0, react.createElement)(NotebookPane, {
7369
7602
  data,
7370
7603
  deleteNote,