daily-question 1.1.0 → 2.0.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/client.js CHANGED
@@ -133,7 +133,7 @@ window.__ModuleLoader__.load({
133
133
  window.MathJax = {
134
134
  startup: { typeset: false },
135
135
  // 每条公式自带 <defs>,注入 innerHTML 后不依赖全局字体缓存
136
- svg: { fontCache: 'local' },
136
+ svg: { fontCache: 'local', scale: 1, matchFontHeight: false },
137
137
  }
138
138
  const script = document.createElement('script')
139
139
  script.src = MATHJAX_SRC
@@ -543,239 +543,273 @@ window.__ModuleLoader__.load({
543
543
  /** 由 apply 用 styles.insert 注入时置位;否则由组件渲染 <style> 兜底。 */
544
544
  let styleInjected = false
545
545
  const CSS = `
546
- .kd-page { height:100%; min-height:0; display:flex; flex-direction:column; position:relative;
547
- background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary);
546
+ .kd-page {
547
+ --kd-bg-base:var(--dsw-alias-bg-base, #ffffff);
548
+ --kd-bg-layer-1:var(--dsw-alias-bg-layer-1, #ffffff);
549
+ --kd-bg-layer-2:color-mix(in srgb, var(--kd-ink) 5%, var(--kd-surface));
550
+ --kd-bg-overlay:var(--dsw-alias-bg-overlay, #ffffff);
551
+ --kd-label-primary:var(--dsw-alias-label-primary, #24332f);
552
+ --kd-label-secondary:var(--dsw-alias-label-secondary, #66736e);
553
+ --kd-border-l1:color-mix(in srgb, var(--kd-ink) 9%, var(--kd-surface));
554
+ --kd-border-l2:color-mix(in srgb, var(--kd-ink) 17%, var(--kd-surface));
555
+ --kd-brand-primary:#4d6bfe;
556
+ --kd-state-success-primary:#078443;
557
+ --kd-state-error-primary:#db3144;
558
+ --kd-state-warn-primary:#95702b;
559
+ --kd-state-idle-primary:#85918b;
560
+ --kd-ink:var(--kd-label-primary); --kd-surface:var(--kd-bg-base);
561
+ font-family:inherit; isolation:isolate; height:100%; min-height:0; display:flex; flex-direction:column; position:relative;
562
+ background:color-mix(in srgb, var(--kd-brand-primary) 2%, var(--kd-surface)); color: var(--kd-label-primary);
548
563
  font-size:14px; line-height:1.65; -webkit-font-smoothing:antialiased; box-sizing:border-box; }
549
564
  .kd-page *, .kd-page *::before, .kd-page *::after { box-sizing:border-box; }
550
- .kd-page :focus-visible { outline:2px solid var(--dsw-alias-brand-primary); outline-offset:2px; }
551
- .kd-header { padding:18px 24px 14px; border-bottom:1px solid var(--dsw-alias-border-l1);
565
+ .kd-page :focus-visible { outline:2px solid var(--kd-brand-primary); outline-offset:2px; }
566
+ .kd-header { padding:20px 28px 12px; background:var(--kd-surface); border-bottom:1px solid var(--kd-border-l1);
552
567
  display:flex; flex-direction:column; gap:12px; }
553
- .kd-header-top { display:flex; gap:24px; justify-content:space-between; align-items:flex-start; flex-wrap:wrap; }
554
- .kd-header-left { min-width:0; flex:1 1 320px; display:flex; gap:12px; align-items:flex-start; }
555
- .kd-brand { flex:0 0 34px; width:34px; height:34px; border-radius:10px; display:flex; align-items:center;
556
- justify-content:center; border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-1);
557
- color:var(--dsw-alias-brand-primary); }
558
- .kd-title { margin:0; font-size:18px; font-weight:650; letter-spacing:.01em; line-height:1.4; }
559
- .kd-sub { margin-top:3px; font-size:12px; color:var(--dsw-alias-label-secondary); line-height:1.6; }
568
+ .kd-header-top { display:flex; gap:20px; justify-content:space-between; align-items:center; flex-wrap:wrap; }
569
+ .kd-header-left { min-width:0; flex:1 1 320px; display:flex; gap:12px; align-items:center; }
570
+ .kd-brand { flex:0 0 40px; width:40px; height:40px; border-radius:12px; display:flex; align-items:center;
571
+ justify-content:center; border:0; background:color-mix(in srgb, var(--kd-brand-primary) 10%, var(--kd-surface));
572
+ color:var(--kd-brand-primary); }
573
+ .kd-title { margin:0; font-size:19px; font-weight:650; letter-spacing:.01em; line-height:1.4; }
574
+ .kd-sub { margin-top:3px; font-size:12px; color:var(--kd-label-secondary); line-height:1.6; }
560
575
  .kd-select { height:30px; padding:0 10px; border-radius:8px; font-size:13px; font-family:inherit;
561
- border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-1);
562
- color:var(--dsw-alias-label-primary); transition:border-color .15s; }
563
- .kd-select:hover { border-color:var(--dsw-alias-brand-primary); }
576
+ border:1px solid var(--kd-border-l2); background:var(--kd-bg-layer-1);
577
+ color:var(--kd-label-primary); transition:border-color .15s; }
578
+ .kd-select:hover { border-color:var(--kd-brand-primary); }
564
579
  .kd-select-sm { height:26px; font-size:12px; padding:0 6px; }
565
580
  .kd-cd-lines { min-width:0; flex:0 1 auto; display:flex; flex-direction:column; align-items:flex-end;
566
581
  gap:2px; text-align:right; }
567
- .kd-cd-primary { font-size:24px; font-weight:700; letter-spacing:.01em; line-height:1.25; }
568
- .kd-cd-secondary { font-size:12px; color:var(--dsw-alias-label-secondary); }
569
- .kd-cd-target { font-size:13.5px; font-weight:500; }
570
- .kd-exam-strip { display:flex; align-items:center; gap:6px 14px; flex-wrap:wrap; padding:8px 12px;
571
- border:1px solid var(--dsw-alias-border-l1); border-radius:10px; background:var(--dsw-alias-bg-layer-1);
572
- font-size:11.5px; color:var(--dsw-alias-label-secondary); }
582
+ .kd-cd-primary { font-size:21px; font-weight:650; color:var(--kd-brand-primary); font-variant-numeric:tabular-nums; letter-spacing:.01em; line-height:1.25; }
583
+ .kd-cd-secondary { font-size:12px; color:var(--kd-label-secondary); }
584
+ .kd-cd-target { font-size:11px; font-weight:400; color:var(--kd-label-secondary); }
585
+ .kd-exam-strip { display:flex; align-items:center; gap:6px 14px; flex-wrap:wrap; padding:8px 0 0;
586
+ border:0; border-top:1px solid var(--kd-border-l1); background:transparent;
587
+ font-size:11.5px; color:var(--kd-label-secondary); }
573
588
  .kd-strip-item { display:inline-flex; align-items:center; gap:6px; white-space:nowrap; }
574
- .kd-strip-warn { color:var(--dsw-alias-state-warn-primary); white-space:normal; text-align:left; }
589
+ .kd-strip-warn { color:var(--kd-state-warn-primary); white-space:normal; text-align:left; }
575
590
  .kd-shell { flex:1 1 auto; min-height:0; display:flex; }
576
- .kd-nav { width:212px; flex:0 0 212px; border-right:1px solid var(--dsw-alias-border-l1);
577
- padding:16px 12px; overflow:auto; display:flex; flex-direction:column; gap:18px; }
591
+ .kd-nav { width:204px; flex:0 0 204px; background:var(--kd-surface); border-right:1px solid var(--kd-border-l1);
592
+ padding:24px 14px; overflow:auto; display:flex; flex-direction:column; gap:18px; }
578
593
  .kd-nav-group { display:flex; flex-direction:column; gap:4px; }
579
- .kd-nav-group-label { font-size:10.5px; letter-spacing:.1em; color:var(--dsw-alias-label-secondary);
594
+ .kd-nav-group-label { font-size:10.5px; letter-spacing:.1em; color:var(--kd-label-secondary);
580
595
  padding:0 8px 4px; }
581
- .kd-nav-item { position:relative; display:flex; align-items:center; justify-content:space-between; gap:8px;
582
- width:100%; padding:8px 10px; border-radius:9px; border:1px solid transparent; background:transparent;
583
- cursor:pointer; color:var(--dsw-alias-label-primary); font:inherit; font-size:13px; text-align:left;
596
+ .kd-nav-item { position:relative; display:flex; align-items:center; justify-content:space-between; gap:7px; flex-direction:column; align-items:flex-start;
597
+ width:100%; padding:12px; border-radius:9px; border:1px solid transparent; background:transparent;
598
+ cursor:pointer; color:var(--kd-label-primary); font:inherit; font-size:13px; text-align:left;
584
599
  transition:background-color .15s, border-color .15s, color .15s; }
585
- .kd-nav-item:hover { background:var(--dsw-alias-bg-layer-2); }
586
- .kd-nav-item > span:first-child { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
587
- .kd-nav-item.is-active { background:var(--dsw-alias-bg-layer-2); border-color:var(--dsw-alias-border-l2);
588
- font-weight:600; box-shadow:inset 2px 0 0 var(--dsw-alias-brand-primary); }
600
+ .kd-nav-item:hover { background:var(--kd-bg-layer-2); }
601
+ .kd-nav-item > span:first-child { min-width:0; overflow-wrap:anywhere; line-height:1.6; }
602
+ .kd-nav-item.is-active { background:color-mix(in srgb, var(--kd-brand-primary) 9%, var(--kd-surface)); border-color:transparent;
603
+ color:var(--kd-brand-primary); font-weight:600; }
589
604
  .kd-nav-item .kd-nav-count { flex:0 0 auto; white-space:nowrap; font-size:11px;
590
- color:var(--dsw-alias-label-secondary); }
605
+ color:var(--kd-label-secondary); }
591
606
  .kd-nav-status { flex:0 0 auto; display:inline-flex; align-items:center; gap:4px; white-space:nowrap;
592
- font-size:10.5px; color:var(--dsw-alias-label-secondary); }
593
- .kd-dot { width:6px; height:6px; border-radius:50%; background:var(--dsw-alias-state-success-primary); }
594
- .kd-dot-idle { background:var(--dsw-alias-state-idle-primary); }
607
+ font-size:10.5px; color:var(--kd-label-secondary); }
608
+ .kd-dot { width:6px; height:6px; border-radius:50%; background:var(--kd-state-success-primary); }
609
+ .kd-dot-idle { background:var(--kd-state-idle-primary); }
595
610
  .kd-nav-spacer { flex:1 1 auto; }
596
- .kd-main { flex:1 1 auto; min-width:0; overflow:auto; padding:20px 24px 48px; }
597
- .kd-main-inner { max-width:900px; margin:0 auto; }
598
- .kd-thread { width:340px; flex:0 0 340px; border-left:1px solid var(--dsw-alias-border-l1);
599
- display:flex; flex-direction:column; min-height:0; background:var(--dsw-alias-bg-layer-1); }
600
- .kd-thread-head { padding:13px 16px; border-bottom:1px solid var(--dsw-alias-border-l1);
611
+ .kd-main { flex:1 1 auto; min-width:0; overflow:hidden; display:flex; flex-direction:column; }
612
+ .kd-main-toolbar { flex:none; padding:24px 28px 20px; display:flex; align-items:center; gap:12px; }
613
+ .kd-main-toolbar > .kd-btn { flex:none; align-self:stretch; aspect-ratio:1; padding:0; min-height:0; white-space:nowrap; border-radius:12px; }
614
+ .kd-main-scroll { flex:1; min-height:0; overflow:auto; padding:0 28px 48px; scrollbar-gutter:stable; }
615
+ .kd-main-inner { width:100%; margin:0; }
616
+ .kd-thread { position:relative; width:clamp(280px,calc((100% - 204px) * var(--kd-thread-ratio, .33)),55%); flex:0 0 auto; border-left:1px solid var(--kd-border-l1);
617
+ display:flex; flex-direction:column; min-height:0; background:var(--kd-bg-layer-1); }
618
+ .kd-thread-resize { position:absolute; left:-5px; top:0; bottom:0; width:10px; cursor:col-resize; touch-action:none; z-index:2; }
619
+ .kd-thread-resize:hover, .kd-thread-resize:focus-visible { background:color-mix(in srgb,var(--kd-brand-primary) 20%,transparent); }
620
+ .kd-thread-head { padding:20px; border-bottom:1px solid var(--kd-border-l1);
601
621
  display:flex; align-items:center; justify-content:space-between; gap:8px; }
602
622
  .kd-thread-title { font-size:13px; font-weight:600; }
603
- .kd-thread-hint { padding:10px 16px 0; font-size:11px; line-height:1.6;
604
- color:var(--dsw-alias-label-secondary); }
605
- .kd-thread-scroll { flex:1 1 auto; min-height:0; overflow:auto; padding:14px 16px; display:flex;
623
+ .kd-thread-hint { padding:12px 20px 0; font-size:11px; line-height:1.6;
624
+ color:var(--kd-label-secondary); }
625
+ .kd-thread-scroll { flex:1 1 auto; min-height:0; overflow:auto; padding:18px 20px; display:flex;
606
626
  flex-direction:column; gap:12px; }
607
627
  .kd-msg { max-width:100%; border-radius:12px; padding:10px 12px; font-size:13px; line-height:1.72; }
608
- .kd-msg-user { align-self:flex-end; background:color-mix(in srgb, var(--dsw-alias-brand-primary) 9%, transparent);
609
- border:1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 26%, transparent); }
610
- .kd-msg-assistant { align-self:stretch; border:1px solid var(--dsw-alias-border-l1);
611
- background:var(--dsw-alias-bg-base); }
612
- .kd-msg-meta { font-size:10px; color:var(--dsw-alias-label-secondary); margin-top:6px; }
613
- .kd-msg-failed { border-color:var(--dsw-alias-state-error-primary); }
614
- .kd-thread-foot { border-top:1px solid var(--dsw-alias-border-l1); padding:12px 16px; }
615
- .kd-composer { border:1px solid var(--dsw-alias-border-l2); border-radius:12px;
616
- background:var(--dsw-alias-bg-base); padding:6px; transition:border-color .15s, box-shadow .15s; }
617
- .kd-composer:focus-within { border-color:var(--dsw-alias-brand-primary);
618
- box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent); }
628
+ .kd-msg-user { align-self:flex-end; background:color-mix(in srgb, var(--kd-brand-primary) 9%, transparent);
629
+ border:1px solid color-mix(in srgb, var(--kd-brand-primary) 26%, transparent); }
630
+ .kd-msg-assistant { align-self:stretch; padding:0; border:0; background:transparent; }
631
+ .kd-msg-meta { font-size:10px; color:var(--kd-label-secondary); margin-top:6px; }
632
+ .kd-msg-failed { border-color:var(--kd-state-error-primary); }
633
+ .kd-thread-foot { border-top:1px solid var(--kd-border-l1); padding:12px 16px; }
634
+ .kd-composer { border:1px solid var(--kd-border-l2); border-radius:12px;
635
+ background:var(--kd-bg-base); padding:6px; transition:border-color .15s, box-shadow .15s; }
636
+ .kd-composer:focus-within { border-color:var(--kd-brand-primary);
637
+ box-shadow:0 0 0 3px color-mix(in srgb, var(--kd-brand-primary) 14%, transparent); }
619
638
  .kd-textarea { width:100%; min-height:58px; max-height:180px; resize:vertical; padding:6px 8px;
620
- border:0; background:transparent; color:var(--dsw-alias-label-primary); font:inherit; font-size:13px; }
639
+ border:0; background:transparent; color:var(--kd-label-primary); font:inherit; font-size:13px; }
621
640
  .kd-textarea:focus { outline:none; }
622
- .kd-tabs { display:flex; gap:2px; margin-bottom:16px; border-bottom:1px solid var(--dsw-alias-border-l1); }
623
- .kd-tab { position:relative; display:inline-flex; align-items:center; gap:6px; padding:8px 12px 11px;
624
- border:0; background:transparent; color:var(--dsw-alias-label-secondary); font:inherit; font-size:13px;
625
- cursor:pointer; border-radius:8px 8px 0 0; transition:color .15s, background-color .15s; }
626
- .kd-tab:hover { color:var(--dsw-alias-label-primary); background:var(--dsw-alias-bg-layer-2); }
627
- .kd-tab.is-active { color:var(--dsw-alias-label-primary); font-weight:600; }
628
- .kd-tab.is-active::after { content:''; position:absolute; left:10px; right:10px; bottom:-1px; height:2px;
629
- border-radius:2px; background:var(--dsw-alias-brand-primary); }
641
+ .kd-tabs { display:flex; gap:4px; width:fit-content; max-width:100%; min-width:0; overflow-x:auto; padding:4px; border:1px solid var(--kd-border-l1); border-radius:12px; background:var(--kd-surface); }
642
+ .kd-tab { position:relative; display:inline-flex; align-items:center; gap:6px; padding:7px 14px; white-space:nowrap;
643
+ border:0; background:transparent; color:var(--kd-label-secondary); font:inherit; font-size:13px;
644
+ cursor:pointer; border-radius:8px; transition:color .15s, background-color .15s; }
645
+ .kd-tab:hover { color:var(--kd-label-primary); background:var(--kd-bg-layer-2); }
646
+ .kd-tab.is-active { color:var(--kd-brand-primary); font-weight:600; background:color-mix(in srgb, var(--kd-brand-primary) 10%, var(--kd-surface)); }
647
+
630
648
  .kd-tab-count { min-width:17px; padding:0 5px; border-radius:999px; font-size:10.5px; line-height:17px;
631
- text-align:center; background:var(--dsw-alias-bg-layer-2); color:var(--dsw-alias-label-secondary); }
632
- .kd-tab.is-active .kd-tab-count { color:var(--dsw-alias-brand-primary); }
633
- .kd-card { border:1px solid var(--dsw-alias-border-l1); border-radius:14px;
634
- background:var(--dsw-alias-bg-layer-1); padding:20px 22px; }
649
+ text-align:center; background:var(--kd-bg-layer-2); color:var(--kd-label-secondary); }
650
+ .kd-tab.is-active .kd-tab-count { color:var(--kd-brand-primary); }
651
+ .kd-card { border:1px solid var(--kd-border-l1); border-radius:14px;
652
+ background:var(--kd-bg-layer-1); padding:28px; box-shadow:0 4px 24px rgb(20 45 35 / 3%); }
635
653
  .kd-card + .kd-card { margin-top:14px; }
636
654
  .kd-meta { display:flex; gap:8px; flex-wrap:wrap; align-items:center; font-size:12px;
637
- color:var(--dsw-alias-label-secondary); }
655
+ color:var(--kd-label-secondary); }
638
656
  .kd-meta-dot::before { content:'·'; margin-right:8px; }
639
657
  .kd-chips { display:flex; gap:6px; flex-wrap:wrap; align-items:center; }
640
658
  .kd-tag { display:inline-flex; align-items:center; gap:5px; padding:2px 8px; border-radius:6px;
641
659
  font-size:11.5px; line-height:1.7; white-space:nowrap;
642
- color:var(--dsw-alias-label-secondary); background:var(--dsw-alias-bg-layer-2); }
643
- .kd-tag-brand { color:var(--dsw-alias-brand-primary);
644
- background:color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent); }
660
+ color:var(--kd-label-secondary); background:var(--kd-bg-layer-2); }
661
+ .kd-tag-brand { color:var(--kd-brand-primary);
662
+ background:color-mix(in srgb, var(--kd-brand-primary) 10%, transparent); }
645
663
  .kd-badge { display:inline-flex; align-items:center; gap:4px; padding:2px 8px; border-radius:999px;
646
- font-size:11px; line-height:1.6; border:1px solid var(--dsw-alias-border-l2);
647
- color:var(--dsw-alias-label-secondary); background:var(--dsw-alias-bg-layer-1); white-space:nowrap; }
648
- .kd-badge-ok { color:var(--dsw-alias-state-success-primary);
649
- border-color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 34%, transparent);
650
- background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent); }
651
- .kd-badge-bad { color:var(--dsw-alias-state-error-primary);
652
- border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 34%, transparent);
653
- background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent); }
654
- .kd-badge-warn { color:var(--dsw-alias-state-warn-primary);
655
- border-color:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 34%, transparent);
656
- background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent); }
657
- .kd-badge-brand { color:var(--dsw-alias-brand-primary);
658
- border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 34%, transparent);
659
- background:color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent); }
660
- .kd-stem { margin:14px 0 18px; font-size:15px; line-height:1.85; }
664
+ font-size:11px; line-height:1.6; border:1px solid var(--kd-border-l2);
665
+ color:var(--kd-label-secondary); background:var(--kd-bg-layer-1); white-space:nowrap; }
666
+ .kd-badge-ok { color:var(--kd-state-success-primary);
667
+ border-color:color-mix(in srgb, var(--kd-state-success-primary) 34%, transparent);
668
+ background:color-mix(in srgb, var(--kd-state-success-primary) 10%, transparent); }
669
+ .kd-badge-bad { color:var(--kd-state-error-primary);
670
+ border-color:color-mix(in srgb, var(--kd-state-error-primary) 34%, transparent);
671
+ background:color-mix(in srgb, var(--kd-state-error-primary) 10%, transparent); }
672
+ .kd-badge-warn { color:var(--kd-state-warn-primary);
673
+ border-color:color-mix(in srgb, var(--kd-state-warn-primary) 34%, transparent);
674
+ background:color-mix(in srgb, var(--kd-state-warn-primary) 12%, transparent); }
675
+ .kd-badge-brand { color:var(--kd-brand-primary);
676
+ border-color:color-mix(in srgb, var(--kd-brand-primary) 34%, transparent);
677
+ background:color-mix(in srgb, var(--kd-brand-primary) 10%, transparent); }
678
+ .kd-stem { margin:24px 0; font-size:16px; line-height:1.85; }
661
679
  .kd-options { display:flex; flex-direction:column; gap:8px; margin:0 0 4px; }
662
- .kd-option { display:flex; gap:12px; align-items:flex-start; padding:12px 14px; border-radius:10px;
663
- border:1px solid var(--dsw-alias-border-l1); background:var(--dsw-alias-bg-base); cursor:pointer;
664
- text-align:left; color:var(--dsw-alias-label-primary); font:inherit; font-size:14px;
680
+ .kd-option { display:flex; gap:12px; align-items:flex-start; padding:14px 16px; border-radius:12px;
681
+ border:1px solid var(--kd-border-l1); background:var(--kd-bg-base); cursor:pointer;
682
+ text-align:left; color:var(--kd-label-primary); font:inherit; font-size:14px;
665
683
  transition:background-color .15s, border-color .15s; }
666
- .kd-option:hover:not(:disabled) { border-color:var(--dsw-alias-border-l2);
667
- background:var(--dsw-alias-bg-layer-2); }
684
+ .kd-option:hover:not(:disabled) { border-color:var(--kd-border-l2);
685
+ background:var(--kd-bg-layer-2); }
668
686
  .kd-option:disabled { cursor:default; }
669
- .kd-option.is-selected { border-color:var(--dsw-alias-brand-primary);
670
- background:color-mix(in srgb, var(--dsw-alias-brand-primary) 7%, transparent); }
671
- .kd-option.is-correct { border-color:var(--dsw-alias-state-success-primary);
672
- background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent); }
673
- .kd-option.is-wrong { border-color:var(--dsw-alias-state-error-primary);
674
- background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent); }
675
- .kd-option-key { flex:0 0 auto; width:24px; height:24px; border-radius:999px; display:inline-flex;
687
+ .kd-option.is-selected { border-color:var(--kd-brand-primary);
688
+ background:color-mix(in srgb, var(--kd-brand-primary) 7%, transparent); }
689
+ .kd-option.is-correct { border-color:color-mix(in srgb, var(--kd-state-success-primary) 45%, var(--kd-surface));
690
+ background:color-mix(in srgb, var(--kd-state-success-primary) 8%, transparent); }
691
+ .kd-option.is-wrong { border-color:var(--kd-state-error-primary);
692
+ background:color-mix(in srgb, var(--kd-state-error-primary) 8%, transparent); }
693
+ .kd-option-key { flex:0 0 auto; width:24px; height:24px; border-radius:8px; display:inline-flex;
676
694
  align-items:center; justify-content:center; font-size:12px; font-weight:600;
677
- border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-base); }
678
- .kd-option.is-selected .kd-option-key { color:var(--dsw-alias-brand-primary); border-color:var(--dsw-alias-brand-primary); }
679
- .kd-option.is-correct .kd-option-key { color:var(--dsw-alias-state-success-primary); border-color:var(--dsw-alias-state-success-primary); }
680
- .kd-option.is-wrong .kd-option-key { color:var(--dsw-alias-state-error-primary); border-color:var(--dsw-alias-state-error-primary); }
695
+ border:1px solid var(--kd-border-l2); background:var(--kd-bg-base); }
696
+ .kd-option.is-selected .kd-option-key { color:var(--kd-brand-primary); border-color:var(--kd-brand-primary); }
697
+ .kd-option.is-correct .kd-option-key { color:var(--kd-state-success-primary); border-color:var(--kd-state-success-primary); }
698
+ .kd-option.is-wrong .kd-option-key { color:var(--kd-state-error-primary); border-color:var(--kd-state-error-primary); }
681
699
  .kd-option-text { min-width:0; flex:1 1 auto; }
682
700
  .kd-actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-top:16px; }
683
- .kd-btn { display:inline-flex; align-items:center; justify-content:center; gap:6px; min-height:32px;
701
+ .kd-btn { display:inline-flex; align-items:center; justify-content:center; gap:6px; min-height:36px;
684
702
  padding:0 14px; border-radius:8px; font-family:inherit; font-size:13px; font-weight:500; cursor:pointer;
685
- border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-1);
686
- color:var(--dsw-alias-label-primary);
703
+ border:1px solid var(--kd-border-l2); background:var(--kd-bg-layer-1);
704
+ color:var(--kd-label-primary);
687
705
  transition:background-color .15s, border-color .15s, color .15s; }
688
- .kd-btn:hover:not(:disabled) { background:var(--dsw-alias-bg-layer-2); }
706
+ .kd-btn:hover:not(:disabled) { background:var(--kd-bg-layer-2); }
689
707
  .kd-btn:disabled { opacity:.5; cursor:not-allowed; }
690
- .kd-btn-primary { background:var(--dsw-alias-brand-primary); border-color:var(--dsw-alias-brand-primary); color:#fff; }
691
- .kd-btn-primary:hover:not(:disabled) { background:var(--dsw-alias-brand-primary); filter:brightness(1.07); }
692
- .kd-btn-ghost { border-color:transparent; background:transparent; color:var(--dsw-alias-label-secondary); }
693
- .kd-btn-ghost:hover:not(:disabled) { background:var(--dsw-alias-bg-layer-2); color:var(--dsw-alias-label-primary); }
694
- .kd-btn-danger { color:var(--dsw-alias-state-error-primary); background:transparent;
695
- border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent); }
696
- .kd-btn-danger:hover:not(:disabled) { background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent); }
708
+ .kd-btn-primary { background:var(--kd-brand-primary); border-color:var(--kd-brand-primary); color:#fff; }
709
+ .kd-btn-primary:hover:not(:disabled) { background:var(--kd-brand-primary); filter:brightness(1.07); }
710
+ .kd-btn-ghost { border-color:transparent; background:transparent; color:var(--kd-label-secondary); }
711
+ .kd-btn-ghost:hover:not(:disabled) { background:var(--kd-bg-layer-2); color:var(--kd-label-primary); }
712
+ .kd-btn-danger { color:var(--kd-state-error-primary); background:transparent;
713
+ border-color:color-mix(in srgb, var(--kd-state-error-primary) 40%, transparent); }
714
+ .kd-btn-danger:hover:not(:disabled) { background:color-mix(in srgb, var(--kd-state-error-primary) 10%, transparent); }
697
715
  .kd-btn-sm { min-height:26px; padding:0 9px; font-size:12px; }
698
716
  .kd-iconbtn { display:inline-flex; align-items:center; justify-content:center; width:26px; height:26px;
699
- border-radius:6px; border:0; background:transparent; color:var(--dsw-alias-label-secondary); cursor:pointer;
717
+ border-radius:6px; border:0; background:transparent; color:var(--kd-label-secondary); cursor:pointer;
700
718
  transition:background-color .15s, color .15s; }
701
- .kd-iconbtn:hover { background:var(--dsw-alias-bg-layer-2); color:var(--dsw-alias-label-primary); }
719
+ .kd-iconbtn:hover { background:var(--kd-bg-layer-2); color:var(--kd-label-primary); }
702
720
  .kd-answer { margin-top:6px; }
703
- .kd-answer-panel { margin-top:18px; border:1px solid var(--dsw-alias-border-l1); border-radius:14px;
704
- background:var(--dsw-alias-bg-layer-1); overflow:hidden; }
721
+ .kd-answer-panel { margin-top:18px; border:1px solid var(--kd-border-l1); border-radius:14px;
722
+ background:var(--kd-bg-layer-1); overflow:hidden; }
705
723
  .kd-answer-panel-head { display:flex; align-items:center; justify-content:space-between; gap:10px;
706
- flex-wrap:wrap; padding:11px 18px; border-bottom:1px solid var(--dsw-alias-border-l1);
707
- background:var(--dsw-alias-bg-layer-2); font-size:13px; font-weight:600; }
708
- .kd-answer-panel-head .kd-tag { background:var(--dsw-alias-bg-layer-1); font-weight:400; }
724
+ flex-wrap:wrap; padding:11px 18px; border-bottom:1px solid var(--kd-border-l1);
725
+ background:color-mix(in srgb, var(--kd-brand-primary) 5%, var(--kd-surface)); font-size:13px; font-weight:600; }
726
+ .kd-answer-panel-head .kd-tag { background:var(--kd-bg-layer-1); font-weight:400; }
709
727
  .kd-answer-panel-body { padding:18px; }
710
728
  .kd-answer-block + .kd-answer-block { margin-top:18px; padding-top:16px;
711
- border-top:1px solid var(--dsw-alias-border-l1); }
712
- .kd-answer-label { font-size:10.5px; letter-spacing:.1em; color:var(--dsw-alias-label-secondary);
729
+ border-top:1px solid var(--kd-border-l1); }
730
+ .kd-answer-label { font-size:10.5px; letter-spacing:.1em; color:var(--kd-label-secondary);
713
731
  margin-bottom:6px; }
714
732
  .kd-verify { margin-top:14px; padding:11px 13px; border-radius:10px; font-size:12px; line-height:1.7;
715
- border:1px solid var(--dsw-alias-border-l1); background:var(--dsw-alias-bg-layer-2);
716
- color:var(--dsw-alias-label-secondary); }
733
+ border:1px solid var(--kd-border-l1); background:var(--kd-bg-layer-2);
734
+ color:var(--kd-label-secondary); }
717
735
  .kd-verify ul { margin:6px 0 0; padding-left:18px; }
718
736
  .kd-list-row { display:flex; gap:14px; align-items:flex-start; width:100%; text-align:left;
719
- padding:14px 16px; border-radius:12px; border:1px solid var(--dsw-alias-border-l1);
720
- background:var(--dsw-alias-bg-layer-1); cursor:pointer; color:var(--dsw-alias-label-primary);
737
+ padding:14px 16px; border-radius:12px; border:1px solid var(--kd-border-l1);
738
+ background:var(--kd-bg-layer-1); color:var(--kd-label-primary);
721
739
  font:inherit; transition:background-color .15s, border-color .15s; }
740
+ .kd-list-row > .kd-btn { flex:none; white-space:nowrap; }
741
+ .kd-list-row[role=button] { cursor:pointer; }
742
+ .kd-list-open { flex:none; display:flex; align-items:center; gap:6px; font-size:12px; color:var(--kd-label-secondary); white-space:nowrap; }
722
743
  .kd-list-row + .kd-list-row { margin-top:8px; }
723
- .kd-list-row:hover { border-color:var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-2); }
744
+ .kd-list-row:hover { border-color:var(--kd-border-l2); background:var(--kd-bg-layer-2); }
724
745
  .kd-list-body { min-width:0; flex:1 1 auto; }
725
- .kd-list-summary { font-size:13.5px; line-height:1.7; overflow:hidden; display:-webkit-box;
726
- -webkit-line-clamp:2; -webkit-box-orient:vertical; }
746
+ .kd-list-summary { --kd-preview-line-height:28px; font-size:14px; line-height:var(--kd-preview-line-height); min-width:0;
747
+ max-height:calc(2 * var(--kd-preview-line-height)); overflow:hidden; }
748
+ .kd-list-summary .kd-md { line-height:inherit; }
749
+ .kd-list-summary .kd-md > * { display:inline; }
750
+ .kd-list-summary .kd-p, .kd-list-summary .kd-h, .kd-list-summary .kd-mathblock { margin:0; }
751
+ .kd-list-summary .kd-p::after { content:' '; }
752
+ .kd-list-summary .kd-mathblock { display:inline; padding:0; text-align:left; vertical-align:baseline; }
753
+ .kd-list-summary mjx-container[display=true] { margin:0; }
754
+ .kd-list-summary .kd-code-wrap { margin:0; border:0; }
755
+ .kd-list-summary .kd-code-lang { display:none; }
756
+ .kd-list-summary .kd-code { display:inline; padding:0; line-height:inherit; white-space:pre-wrap; }
757
+ .kd-list-summary.is-truncated { mask-image:linear-gradient(#000,#000),linear-gradient(to right,#000 var(--kd-preview-fade-start),transparent var(--kd-preview-fade-end));
758
+ mask-size:100% var(--kd-preview-fade-top),100% calc(100% - var(--kd-preview-fade-top)); mask-position:top,bottom; mask-repeat:no-repeat; }
727
759
  .kd-list-meta { margin-top:7px; display:flex; gap:6px; flex-wrap:wrap; font-size:11px;
728
- color:var(--dsw-alias-label-secondary); align-items:center; }
729
- .kd-empty { padding:56px 24px; text-align:center; color:var(--dsw-alias-label-secondary); }
760
+ color:var(--kd-label-secondary); align-items:center; }
761
+ .kd-empty { padding:56px 24px; text-align:center; color:var(--kd-label-secondary); }
730
762
  .kd-empty-icon { width:44px; height:44px; margin:0 auto 14px; border-radius:50%; display:flex;
731
- align-items:center; justify-content:center; border:1px solid var(--dsw-alias-border-l1);
732
- background:var(--dsw-alias-bg-layer-1); color:var(--dsw-alias-label-secondary); }
733
- .kd-empty-title { font-size:15px; font-weight:600; color:var(--dsw-alias-label-primary); margin-bottom:6px; }
763
+ align-items:center; justify-content:center; border:1px solid var(--kd-border-l1);
764
+ background:var(--kd-bg-layer-1); color:var(--kd-label-secondary); }
765
+ .kd-empty-title { font-size:15px; font-weight:600; color:var(--kd-label-primary); margin-bottom:6px; }
734
766
  .kd-empty-body { font-size:13px; max-width:460px; margin:0 auto; line-height:1.8; }
735
767
  .kd-empty .kd-actions { justify-content:center; }
736
768
  .kd-overlay { position:absolute; inset:0; background:rgba(0,0,0,.38); display:flex; align-items:center;
737
769
  justify-content:center; padding:24px; z-index:40; backdrop-filter:blur(2px); }
738
770
  .kd-modal { width:min(580px,100%); max-height:100%; display:flex; flex-direction:column;
739
- background:var(--dsw-alias-bg-overlay); color:var(--dsw-alias-label-primary);
740
- border:1px solid var(--dsw-alias-border-l2); border-radius:16px; overflow:hidden;
771
+ background:var(--kd-bg-overlay); color:var(--kd-label-primary);
772
+ border:1px solid var(--kd-border-l2); border-radius:16px; overflow:hidden;
741
773
  box-shadow:0 16px 48px rgba(0,0,0,.24); }
742
774
  .kd-modal-wide { width:min(880px,100%); }
743
- .kd-modal-head { padding:16px 20px; border-bottom:1px solid var(--dsw-alias-border-l1);
775
+ .kd-modal-head { padding:16px 20px; border-bottom:1px solid var(--kd-border-l1);
744
776
  display:flex; align-items:center; justify-content:space-between; gap:10px; }
745
777
  .kd-modal-title { font-size:15px; font-weight:650; letter-spacing:.01em; }
746
778
  .kd-modal-body { padding:18px 20px; overflow:auto; min-height:0; }
747
- .kd-modal-foot { padding:14px 20px; border-top:1px solid var(--dsw-alias-border-l1);
779
+ .kd-modal-foot { padding:14px 20px; border-top:1px solid var(--kd-border-l1);
748
780
  display:flex; gap:8px; justify-content:flex-end; flex-wrap:wrap; }
749
781
  .kd-field { display:flex; flex-direction:column; gap:6px; margin-bottom:14px; }
750
- .kd-field-label { font-size:12px; font-weight:500; color:var(--dsw-alias-label-secondary); }
751
- .kd-input { height:34px; padding:0 11px; border-radius:8px; border:1px solid var(--dsw-alias-border-l2);
752
- background:var(--dsw-alias-bg-base); color:var(--dsw-alias-label-primary); font:inherit; font-size:13px;
782
+ .kd-field-label { font-size:12px; font-weight:500; color:var(--kd-label-secondary); }
783
+ .kd-input { height:34px; padding:0 11px; border-radius:8px; border:1px solid var(--kd-border-l2);
784
+ background:var(--kd-bg-base); color:var(--kd-label-primary); font:inherit; font-size:13px;
753
785
  transition:border-color .15s, box-shadow .15s; }
754
- .kd-input:focus { outline:none; border-color:var(--dsw-alias-brand-primary);
755
- box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent); }
756
- .kd-input::placeholder { color:var(--dsw-alias-label-secondary); }
786
+ .kd-input:focus { outline:none; border-color:var(--kd-brand-primary);
787
+ box-shadow:0 0 0 3px color-mix(in srgb, var(--kd-brand-primary) 14%, transparent); }
788
+ .kd-input::placeholder { color:var(--kd-label-secondary); }
757
789
  .kd-row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
758
790
  .kd-row-between { display:flex; gap:10px; align-items:center; justify-content:space-between; flex-wrap:wrap; }
759
- .kd-divider { height:1px; background:var(--dsw-alias-border-l1); margin:16px 0; }
760
- .kd-preview-item { border:1px solid var(--dsw-alias-border-l1); border-radius:12px; padding:12px 14px;
761
- background:var(--dsw-alias-bg-base); }
791
+ .kd-divider { height:1px; background:var(--kd-border-l1); margin:16px 0; }
792
+ .kd-preview-item { border:1px solid var(--kd-border-l1); border-radius:12px; padding:12px 14px;
793
+ background:var(--kd-bg-base); }
762
794
  .kd-preview-item + .kd-preview-item { margin-top:8px; }
763
- .kd-warn-text { color:var(--dsw-alias-state-warn-primary); font-size:12px; line-height:1.7; }
764
- .kd-error-text { color:var(--dsw-alias-state-error-primary); font-size:12px; line-height:1.7; }
795
+ .kd-warn-text { color:var(--kd-state-warn-primary); font-size:12px; line-height:1.7; }
796
+ .kd-error-text { color:var(--kd-state-error-primary); font-size:12px; line-height:1.7; }
765
797
  .kd-toast { position:absolute; left:50%; bottom:24px; transform:translateX(-50%); z-index:60;
766
798
  padding:9px 18px; border-radius:999px; font-size:13px; max-width:80%;
767
- background:var(--dsw-alias-bg-overlay); border:1px solid var(--dsw-alias-border-l2);
799
+ background:var(--kd-bg-overlay); border:1px solid var(--kd-border-l2);
768
800
  box-shadow:0 8px 24px rgba(0,0,0,.18); }
769
- .kd-toast-bad { color:var(--dsw-alias-state-error-primary); }
770
- .kd-toast-ok { color:var(--dsw-alias-state-success-primary); }
771
- .kd-toast-warn { color:var(--dsw-alias-state-warn-primary); }
772
- .kd-loading { padding:48px 0; text-align:center; color:var(--dsw-alias-label-secondary); font-size:13px; }
801
+ .kd-toast-bad { color:var(--kd-state-error-primary); }
802
+ .kd-toast-ok { color:var(--kd-state-success-primary); }
803
+ .kd-toast-warn { color:var(--kd-state-warn-primary); }
804
+ .kd-loading { padding:48px 0; text-align:center; color:var(--kd-label-secondary); font-size:13px; }
773
805
  .kd-spinner { display:inline-block; width:14px; height:14px; border-radius:50%; vertical-align:-2px;
774
- border:2px solid var(--dsw-alias-border-l2); border-top-color:var(--dsw-alias-brand-primary);
806
+ border:2px solid var(--kd-border-l2); border-top-color:var(--kd-brand-primary);
775
807
  animation:kd-spin .8s linear infinite; margin-right:8px; }
776
808
  @keyframes kd-spin { to { transform:rotate(360deg); } }
777
- .kd-mobilebar { display:none; gap:8px; padding:10px 14px; border-bottom:1px solid var(--dsw-alias-border-l1);
809
+ .kd-mobilebar { display:none; gap:8px; padding:10px 14px; border-bottom:1px solid var(--kd-border-l1);
778
810
  overflow:auto; }
811
+ .kd-mobilebar > * { flex:0 0 auto; white-space:nowrap; }
812
+ .kd-mobilebar .kd-select { max-width:180px; }
779
813
  .kd-md { line-height:1.78; overflow-wrap:anywhere; }
780
814
  .kd-md > *:first-child { margin-top:0; }
781
815
  .kd-md > *:last-child { margin-bottom:0; }
@@ -785,57 +819,105 @@ window.__ModuleLoader__.load({
785
819
  .kd-h4, .kd-h5, .kd-h6 { font-size:14px; }
786
820
  .kd-list { margin:8px 0; padding-left:22px; }
787
821
  .kd-list li { margin:3px 0; }
788
- .kd-quote { margin:10px 0; padding:6px 12px; border-left:3px solid var(--dsw-alias-border-l2);
789
- color:var(--dsw-alias-label-secondary); }
790
- .kd-hr { border:0; border-top:1px solid var(--dsw-alias-border-l1); margin:14px 0; }
791
- .kd-link { color:var(--dsw-alias-brand-primary); text-decoration:underline; text-underline-offset:2px; }
822
+ .kd-quote { margin:10px 0; padding:6px 12px; border-left:3px solid var(--kd-border-l2);
823
+ color:var(--kd-label-secondary); }
824
+ .kd-hr { border:0; border-top:1px solid var(--kd-border-l1); margin:14px 0; }
825
+ .kd-link { color:var(--kd-brand-primary); text-decoration:underline; text-underline-offset:2px; }
792
826
  .kd-inline-code { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:.9em;
793
- padding:1px 5px; border-radius:5px; background:var(--dsw-alias-bg-layer-2);
794
- border:1px solid var(--dsw-alias-border-l1); }
795
- .kd-code-wrap { margin:10px 0; border-radius:10px; overflow:hidden; border:1px solid var(--dsw-alias-border-l1);
796
- background:var(--dsw-alias-bg-layer-2); }
797
- .kd-code-lang { padding:3px 10px; font-size:11px; color:var(--dsw-alias-label-secondary);
798
- border-bottom:1px solid var(--dsw-alias-border-l1); }
827
+ padding:1px 5px; border-radius:5px; background:var(--kd-bg-layer-2);
828
+ border:1px solid var(--kd-border-l1); }
829
+ .kd-code-wrap { margin:10px 0; border-radius:10px; overflow:hidden; border:1px solid var(--kd-border-l1);
830
+ background:var(--kd-bg-layer-2); }
831
+ .kd-code-lang { padding:3px 10px; font-size:11px; color:var(--kd-label-secondary);
832
+ border-bottom:1px solid var(--kd-border-l1); }
799
833
  .kd-code { margin:0; padding:10px 12px; overflow:auto; font-family:ui-monospace,SFMono-Regular,Consolas,monospace;
800
834
  font-size:12.5px; line-height:1.6; white-space:pre; tab-size:2; }
801
835
  .kd-code code { font:inherit; }
802
836
  .kd-table-wrap { overflow:auto; margin:10px 0; }
803
837
  .kd-table { border-collapse:collapse; font-size:13px; min-width:0; }
804
- .kd-table th, .kd-table td { border:1px solid var(--dsw-alias-border-l1); padding:5px 10px; text-align:left; }
805
- .kd-table th { background:var(--dsw-alias-bg-layer-2); font-weight:600; }
838
+ .kd-table th, .kd-table td { border:1px solid var(--kd-border-l1); padding:5px 10px; text-align:left; }
839
+ .kd-table th { background:var(--kd-bg-layer-2); font-weight:600; }
806
840
  .kd-image { max-width:100%; height:auto; border-radius:8px; display:block; margin:8px 0; }
807
841
  .kd-image-fallback { display:inline-block; font-family:ui-monospace,monospace; font-size:12px;
808
- color:var(--dsw-alias-state-warn-primary); border:1px dashed var(--dsw-alias-state-warn-primary);
842
+ color:var(--kd-state-warn-primary); border:1px dashed var(--kd-state-warn-primary);
809
843
  border-radius:6px; padding:2px 6px; }
810
- .kd-math { display:inline-block; vertical-align:middle; max-width:100%; }
811
- .kd-math mjx-container { color:currentColor; }
812
- .kd-mathblock { display:block; margin:10px 0; padding:.35em .2em; overflow-x:auto; overflow-y:hidden;
844
+ .kd-math, .kd-mathblock { font-size:17px; }
845
+ .kd-math { display:inline-flex; vertical-align:baseline; align-items:baseline; max-width:100%; padding:.25em .1em;
846
+ overflow-x:auto; overflow-y:hidden; }
847
+ .kd-math mjx-container { color:currentColor; flex:none; }
848
+ .kd-mathblock { display:block; max-width:100%; min-width:0; margin:10px 0; padding:.35em .2em; overflow-x:auto; overflow-y:hidden;
813
849
  text-align:center; }
814
- .kd-mathblock mjx-container { display:inline-block; max-width:100%; }
850
+ .kd-page .kd-math mjx-container, .kd-page .kd-mathblock mjx-container {
851
+ display:inline-block; font-size:100% !important; max-width:none; }
852
+ .kd-page .kd-math svg, .kd-page .kd-mathblock svg { max-width:none !important; }
815
853
  .kd-math-pending { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:.9em;
816
854
  opacity:.55; white-space:pre-wrap; }
817
- .kd-math-error { color:var(--dsw-alias-state-error-primary);
855
+ .kd-math-error { color:var(--kd-state-error-primary);
818
856
  font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:.9em; }
819
857
  .kd-checkline { display:flex; gap:8px; align-items:flex-start; padding:5px 0; font-size:13px; }
820
858
  .kd-chip { display:inline-flex; align-items:center; gap:2px; padding:0 2px 0 8px; border-radius:999px;
821
- border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-1); font-size:12px; }
859
+ border:1px solid var(--kd-border-l2); background:var(--kd-bg-layer-1); font-size:12px; }
822
860
  .kd-chip .kd-checkline { padding:0; }
823
861
  .kd-chip .kd-iconbtn { width:20px; height:20px; }
862
+ .kd-option.is-correct { border-color:#22c55e; }
863
+ .kd-option.is-wrong { border-color:#f35464; }
864
+ .kd-page input[type=checkbox] { accent-color:var(--kd-brand-primary); width:16px; height:16px; flex:none; }
865
+ .kd-btn, .kd-iconbtn, .kd-tab, .kd-nav-item, .kd-option, .kd-list-row {
866
+ transition:background-color .16s, border-color .16s, color .16s, box-shadow .16s, transform .16s; }
867
+ .kd-btn:active:not(:disabled), .kd-iconbtn:active, .kd-tab:active, .kd-nav-item:active { transform:scale(.97); }
868
+ .kd-option:active:not(:disabled) { transform:translateY(1px); }
869
+ .kd-view { animation:kd-enter .2s ease-out; }
870
+ .kd-presence { display:contents; }
871
+ .kd-overlay { animation:kd-fade .16s ease-out; background:rgb(18 27 52 / 28%); backdrop-filter:blur(5px); }
872
+ .kd-modal { animation:kd-enter .18s ease-out; box-shadow:0 24px 80px rgb(12 25 60 / 18%); border-radius:20px; }
873
+ .kd-presence.is-leaving .kd-overlay { animation:kd-fade .16s ease-in reverse both; pointer-events:none; }
874
+ .kd-presence.is-leaving .kd-modal { animation:kd-enter .16s ease-in reverse both; }
875
+ .kd-thread { animation:kd-drawer .2s ease-out; }
876
+ .kd-presence.is-leaving .kd-thread { animation:kd-drawer .16s ease-in reverse both; pointer-events:none; }
877
+ .kd-modal-head { padding:22px 24px; }
878
+ .kd-modal-title { font-size:18px; }
879
+ .kd-modal-body { padding:20px 24px; background:color-mix(in srgb, var(--kd-brand-primary) 2%, var(--kd-surface)); }
880
+ .kd-modal-foot { padding:16px 24px; }
881
+ .kd-manage-card { padding:16px; border:1px solid var(--kd-border-l1); border-radius:14px; background:var(--kd-bg-base); margin-bottom:10px; }
882
+ .kd-manage-card.is-enabled { border-color:color-mix(in srgb, var(--kd-brand-primary) 25%, var(--kd-surface)); }
883
+ .kd-manage-card .kd-checkline { font-weight:550; }
884
+ .kd-manage-card .kd-msg-meta { font-weight:400; line-height:1.7; }
885
+ .kd-custom-subject { padding:20px; border-radius:14px; border:1px solid var(--kd-border-l1); background:var(--kd-bg-base); }
886
+ .kd-field-label { font-size:13px; }
887
+ .kd-input { height:38px; }
888
+ .kd-bulkbar { position:sticky; top:0; z-index:5; display:flex; flex-direction:column; gap:10px; padding:14px;
889
+ margin-bottom:14px; border-radius:14px; border:1px solid color-mix(in srgb, var(--kd-brand-primary) 25%, var(--kd-surface));
890
+ background:var(--kd-bg-base); box-shadow:0 5px 20px rgb(20 35 70 / 5%); animation:kd-enter .18s ease-out; }
891
+ .kd-list-row.is-selected { border-color:var(--kd-brand-primary); background:color-mix(in srgb, var(--kd-brand-primary) 5%, var(--kd-surface)); }
892
+ .kd-list-row input { margin-top:5px; }
893
+ @keyframes kd-fade { from { opacity:0; } to { opacity:1; } }
894
+ @keyframes kd-enter { from { opacity:0; transform:translateY(8px) scale(.99); } to { opacity:1; transform:none; } }
895
+ @keyframes kd-drawer { from { opacity:0; transform:translateX(16px); } to { opacity:1; transform:none; } }
824
896
  @media (max-width: 1180px) {
825
- .kd-thread { position:absolute; right:0; top:0; bottom:0; width:min(370px,92vw); z-index:30;
897
+ .kd-thread { position:absolute; right:0; top:0; bottom:0; width:min(370px,92vw); flex-basis:auto; z-index:30;
826
898
  box-shadow:-12px 0 32px rgba(0,0,0,.22); }
899
+ .kd-thread-resize { display:none; }
827
900
  }
828
901
  @media (max-width: 860px) {
829
902
  .kd-nav { display:none; }
830
903
  .kd-mobilebar { display:flex; }
831
- .kd-main { padding:16px 14px 40px; }
904
+ .kd-main-toolbar { padding:16px 14px; }
905
+ .kd-main-scroll { padding:0 14px 40px; }
832
906
  .kd-header { padding:14px 14px 12px; gap:10px; }
907
+ .kd-header-left { flex-basis:100%; }
833
908
  .kd-cd-lines { align-items:flex-start; text-align:left; }
909
+ .kd-cd-primary { font-size:19px; }
910
+ .kd-tabs { flex:1; }
911
+ .kd-tab { flex:1 0 auto; justify-content:center; padding:7px 10px; }
834
912
  .kd-strip-item { white-space:normal; }
835
913
  .kd-card { padding:16px 14px; }
836
914
  .kd-answer-panel-body { padding:14px; }
837
915
  .kd-exam-strip { padding:8px 10px; }
838
916
  }
917
+ @media (prefers-reduced-motion: reduce) {
918
+ .kd-page *, .kd-page *::before, .kd-page *::after { transition:none !important; animation:none !important; }
919
+ }
920
+
839
921
  `
840
922
 
841
923
  /* ============================== 接口 ============================== */
@@ -896,6 +978,7 @@ window.__ModuleLoader__.load({
896
978
  check: 'M4 12.5l5 5L20 6.5',
897
979
  x: 'M6 6l12 12M18 6L6 18',
898
980
  chevron: 'M9 6l6 6-6 6',
981
+ 'chevron-left': 'M15 6l-6 6 6 6',
899
982
  send: 'M4 12l16-8-6 8 6 8z',
900
983
  book: 'M4 5.5A2.5 2.5 0 0 1 6.5 3H20v16H6.5A2.5 2.5 0 0 0 4 21.5z',
901
984
  plus: 'M12 5v14M5 12h14',
@@ -914,13 +997,14 @@ window.__ModuleLoader__.load({
914
997
  }, h('path', { d: path }))
915
998
  }
916
999
 
917
- function Btn({ children, onClick, variant = 'default', disabled, title, small, type = 'button' }) {
1000
+ function Btn({ children, onClick, variant = 'default', disabled, title, small, type = 'button', ariaLabel }) {
918
1001
  return h('button', {
919
1002
  type,
920
1003
  className: `kd-btn kd-btn-${variant}${small ? ' kd-btn-sm' : ''}`,
921
1004
  onClick,
922
1005
  disabled,
923
1006
  title,
1007
+ 'aria-label': ariaLabel,
924
1008
  }, children)
925
1009
  }
926
1010
 
@@ -937,6 +1021,17 @@ window.__ModuleLoader__.load({
937
1021
  actions ? h('div', { className: 'kd-actions' }, actions) : null)
938
1022
  }
939
1023
 
1024
+ function Presence({ children }) {
1025
+ const active = Array.isArray(children) ? children.some(Boolean) : Boolean(children)
1026
+ const [retained, setRetained] = useState(children)
1027
+ useEffect(() => {
1028
+ if (active) { setRetained(children); return undefined }
1029
+ const timer = setTimeout(() => setRetained([]), 160)
1030
+ return () => clearTimeout(timer)
1031
+ }, [children, active])
1032
+ return h('div', { className: 'kd-presence' + (active ? '' : ' is-leaving') }, active ? children : retained)
1033
+ }
1034
+
940
1035
  function Modal({ title, children, footer, onClose, wide }) {
941
1036
  useEffect(() => {
942
1037
  const onKey = (event) => {
@@ -976,12 +1071,12 @@ window.__ModuleLoader__.load({
976
1071
  return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
977
1072
  }
978
1073
 
979
- /** 严格的三行倒计时。 */
1074
+ /** 剩余不超过 30 天时省略中间的时长行。 */
980
1075
  function CountdownLines({ countdown }) {
981
1076
  const lines = (countdown || {}).lines || {}
982
1077
  return h('div', { className: 'kd-cd-lines' },
983
1078
  h('div', { className: 'kd-cd-primary' }, lines.primary || ''),
984
- h('div', { className: 'kd-cd-secondary' }, lines.secondary || ''),
1079
+ lines.secondary ? h('div', { className: 'kd-cd-secondary' }, lines.secondary) : null,
985
1080
  h('div', { className: 'kd-cd-target' }, lines.target || ''))
986
1081
  }
987
1082
 
@@ -1077,7 +1172,7 @@ window.__ModuleLoader__.load({
1077
1172
  }, label, count ? h('span', { className: 'kd-tab-count' }, String(count)) : null)))
1078
1173
  }
1079
1174
 
1080
- function MobileBar({ boot, subjectId, kind, onPick, onTab, onManage, onBanks }) {
1175
+ function MobileBar({ boot, subjectId, onPick, onManage, onBanks }) {
1081
1176
  const enabled = (boot.subjects || []).filter((subject) => subject.enabled)
1082
1177
  return h('div', { className: 'kd-mobilebar' },
1083
1178
  h('select', {
@@ -1086,13 +1181,6 @@ window.__ModuleLoader__.load({
1086
1181
  'aria-label': '切换科目',
1087
1182
  onChange: (event) => onPick(event.target.value),
1088
1183
  }, enabled.map((subject) => h('option', { key: subject.id, value: subject.id }, subject.name))),
1089
- ['today', 'all', 'favorite', 'wrong'].map((value) => h('button', {
1090
- key: value,
1091
- type: 'button',
1092
- className: `kd-nav-item${kind === value ? ' is-active' : ''}`,
1093
- style: { width: 'auto' },
1094
- onClick: () => onTab(value),
1095
- }, T.tabs[value])),
1096
1184
  // 窄屏隐藏了左侧栏,科目的增删与题库入口必须在这里保留。
1097
1185
  h(Btn, { small: true, variant: 'ghost', onClick: onManage }, h(Icon, { name: 'plus', size: 13 }), T.manageSubjects),
1098
1186
  h(Btn, { small: true, variant: 'ghost', onClick: onBanks }, h(Icon, { name: 'book', size: 13 }), T.manageBanks))
@@ -1184,7 +1272,7 @@ window.__ModuleLoader__.load({
1184
1272
 
1185
1273
  function QuestionPane({
1186
1274
  q, busy, choice, onSelect, onReveal, onJudge, onFavorite, onRedo, localRedo,
1187
- onRevise, revising, onToggleThread, threadOpen, narrow, onRemoveWrong,
1275
+ onRevise, revising, onToggleThread, threadOpen, onRemoveWrong,
1188
1276
  }) {
1189
1277
  if (!q) return null
1190
1278
  const revealed = !localRedo && q.answerState === 'revealed' && q.attemptCount > 0
@@ -1255,9 +1343,7 @@ window.__ModuleLoader__.load({
1255
1343
  revealed
1256
1344
  ? h(Btn, { variant: 'ghost', onClick: onRedo, disabled: busy === 'reveal' }, h(Icon, { name: 'refresh', size: 13 }), T.redo)
1257
1345
  : null,
1258
- narrow
1259
- ? h(Btn, { variant: 'ghost', onClick: onToggleThread }, h(Icon, { name: 'book' }), threadOpen ? '收起追问' : T.followup)
1260
- : null),
1346
+ revealed ? h(Btn, { variant: 'ghost', onClick: onToggleThread }, h(Icon, { name: 'book' }), threadOpen ? '收起追问' : T.followup) : null),
1261
1347
  revealed ? h(AnswerBlock, { question: q, onRevise, revising, onRemoveWrong }) : null,
1262
1348
  revealed && last?.result === 'pending'
1263
1349
  ? h('div', { className: 'kd-actions', style: { marginTop: '12px' } },
@@ -1270,7 +1356,7 @@ window.__ModuleLoader__.load({
1270
1356
  h(Badge, { tone: q.publishVerification.status === 'passed' ? 'ok' : 'warn' },
1271
1357
  q.publishVerification.status === 'passed' ? '试题发布前已通过独立校验' : '试题发布状态:待核验'),
1272
1358
  // 校验结论里通常写着答案与关键推导,提交答案前不能展示,否则等于泄题。
1273
- q.answerState === 'revealed'
1359
+ revealed
1274
1360
  ? ` ${q.publishVerification.summary}`
1275
1361
  : ' 结论在提交答案后显示',
1276
1362
  `(${methodLabel(q.publishVerification.method)}${q.publishVerification.at ? `,${formatDateTime(q.publishVerification.at)}` : ''})`)
@@ -1278,7 +1364,59 @@ window.__ModuleLoader__.load({
1278
1364
  }
1279
1365
 
1280
1366
  /* ============================== 列表 ============================== */
1281
- function ListPane({ list, kind, subjectName, onOpen }) {
1367
+ function previewFadePosition(element, lineHeight) {
1368
+ const bounds = element.getBoundingClientRect()
1369
+ const rects = []
1370
+ const document = element.ownerDocument
1371
+ const walker = document.createTreeWalker(element, 4) // 只测量正文文字,公式由 SVG 的外框参与定位。
1372
+ const range = document.createRange()
1373
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
1374
+ if (!node.textContent.trim() || node.parentElement.closest('mjx-container,.kd-code-lang')) continue
1375
+ range.setStart(node, 0)
1376
+ range.setEnd(node, node.textContent.trimEnd().length)
1377
+ rects.push(...range.getClientRects())
1378
+ }
1379
+ for (const node of element.querySelectorAll('svg,img')) rects.push(node.getBoundingClientRect())
1380
+ const visible = rects.filter((rect) => rect.width > 0 && rect.height > 0
1381
+ && rect.top < bounds.bottom - 1 && rect.bottom > bounds.top
1382
+ && rect.left < bounds.right && rect.right > bounds.left)
1383
+ if (!visible.length) return null
1384
+ const lineOf = (rect) => Math.max(0, Math.min(1, Math.floor(((rect.top + rect.bottom) / 2 - bounds.top) / lineHeight)))
1385
+ const lastLine = Math.max(...visible.map(lineOf))
1386
+ const end = Math.min(bounds.width, Math.max(...visible.filter((rect) => lineOf(rect) === lastLine).map((rect) => rect.right - bounds.left)))
1387
+ return { top: lastLine * lineHeight, start: Math.max(0, end - 72), end }
1388
+ }
1389
+
1390
+ function QuestionPreview({ text }) {
1391
+ const ref = useRef(null)
1392
+ const [truncated, setTruncated] = useState(false)
1393
+ const mathStatus = useMathJaxStatus()
1394
+ useEffect(() => {
1395
+ const element = ref.current
1396
+ if (!element) return undefined
1397
+ const measure = () => {
1398
+ let lineHeight = 28
1399
+ for (const svg of element.querySelectorAll('svg')) lineHeight = Math.max(lineHeight, Math.ceil(svg.getBoundingClientRect().height + 16))
1400
+ element.style.setProperty('--kd-preview-line-height', `${lineHeight}px`)
1401
+ const content = element.firstElementChild
1402
+ const clipped = element.scrollHeight > element.clientHeight + 1 || element.scrollWidth > element.clientWidth + 1
1403
+ || (content && content.scrollHeight > content.clientHeight + 1)
1404
+ const fade = clipped && previewFadePosition(element, lineHeight)
1405
+ if (fade) {
1406
+ for (const [key, value] of Object.entries(fade)) element.style.setProperty(`--kd-preview-fade-${key}`, `${value}px`)
1407
+ }
1408
+ setTruncated(Boolean(fade))
1409
+ }
1410
+ measure()
1411
+ const observer = new ResizeObserver(measure)
1412
+ observer.observe(element)
1413
+ if (element.firstElementChild) observer.observe(element.firstElementChild)
1414
+ return () => observer.disconnect()
1415
+ }, [text, mathStatus])
1416
+ return h('div', { ref, className: 'kd-list-summary' + (truncated ? ' is-truncated' : '') }, h(Markdown, { text }))
1417
+ }
1418
+
1419
+ function ListPane({ list, kind, subjectName, onOpen, managing, selected, onManage, onToggle, onSelectAll, onBulk, busy }) {
1282
1420
  if (!list) return h('div', { className: 'kd-loading' }, h('span', { className: 'kd-spinner' }), '加载中…')
1283
1421
  if (!list.items.length) {
1284
1422
  const icon = kind === 'favorite' ? 'star' : kind === 'wrong' ? 'check' : kind === 'all' ? 'book' : 'spark'
@@ -1290,33 +1428,49 @@ window.__ModuleLoader__.load({
1290
1428
  }
1291
1429
  return h('div', null,
1292
1430
  h('div', { className: 'kd-meta', style: { marginBottom: '10px' } },
1293
- h('span', null, `${subjectName} · ${T.tabs[kind]} · 共 ${list.items.length} 道`)),
1294
- list.items.map((item) => h('button', {
1431
+ h('span', null, `${subjectName} · ${T.tabs[kind]} · 共 ${list.items.length} 道`),
1432
+ h(Btn, { small: true, onClick: onManage, disabled: busy }, managing ? '完成管理' : '批量管理')),
1433
+ managing ? h('div', { className: 'kd-bulkbar', 'aria-label': '批量管理' },
1434
+ h('div', { className: 'kd-row' },
1435
+ h('strong', { 'aria-live': 'polite' }, `已选 ${selected.length} 道`),
1436
+ h(Btn, { small: true, disabled: busy, onClick: () => onSelectAll(true) }, '全选'),
1437
+ h(Btn, { small: true, disabled: busy || !selected.length, onClick: () => onSelectAll(false) }, '全不选')),
1438
+ h('div', { className: 'kd-row' },
1439
+ ...[['favorite', '收藏'], ['unfavorite', '取消收藏'], ['wrong', '加入错题本'], ['unwrong', '移出错题本'], ['delete', '删除题目']].map(([action, label]) =>
1440
+ h(Btn, { key: action, small: true, variant: action === 'delete' ? 'danger' : 'default', disabled: busy || !selected.length, onClick: () => onBulk(action) }, label)))) : null,
1441
+ list.items.map((item) => h('div', {
1295
1442
  key: item.id,
1296
- type: 'button',
1297
- className: 'kd-list-row',
1298
- onClick: () => onOpen(item.id),
1443
+ className: 'kd-list-row' + (managing && selected.includes(item.id) ? ' is-selected' : ''),
1444
+ role: managing ? undefined : 'button',
1445
+ tabIndex: managing ? undefined : 0,
1446
+ onClick: managing ? undefined : (event) => { if (!event.target.closest('a,input')) onOpen(item.id) },
1447
+ onKeyDown: managing ? undefined : (event) => {
1448
+ if (event.target === event.currentTarget && ['Enter', ' '].includes(event.key)) { event.preventDefault(); onOpen(item.id) }
1449
+ },
1299
1450
  },
1451
+ managing ? h('input', { type: 'checkbox', checked: selected.includes(item.id), disabled: busy,
1452
+ 'aria-label': '选择题目:' + (item.summary || item.id), onChange: () => onToggle(item.id) }) : null,
1300
1453
  h('div', { className: 'kd-list-body' },
1301
- h('div', { className: 'kd-list-summary' }, item.summary || '(无题干)'),
1454
+ h(QuestionPreview, { text: item.stem || item.summary || '(无题干)' }),
1302
1455
  h('div', { className: 'kd-list-meta' },
1303
1456
  h(Badge, null, item.typeLabel),
1304
1457
  h('span', null, formatDate(item.assignedDate)),
1305
1458
  h('span', null, `作答 ${item.attemptCount} 次`),
1306
1459
  item.favorite ? h(Badge, { tone: 'brand' }, '收藏') : null,
1307
- item.wrongEver ? h(Badge, { tone: 'bad' }, item.mastered ? '曾答错 · 已掌握' : '错题') : null,
1460
+ item.wrongEver ? h(Badge, { tone: 'bad' }, item.mastered ? '错题 · 已掌握' : '错题') : null,
1308
1461
  item.answerStatus !== 'verified' ? h(Badge, { tone: 'warn' }, T.unverified) : null,
1309
1462
  h('span', null, `难度 ${item.difficulty}/5`))),
1310
- h('span', { className: 'kd-iconbtn' }, h(Icon, { name: 'chevron' })))))
1463
+ !managing ? h('span', { className: 'kd-list-open', 'aria-hidden': true }, '查看题目', h(Icon, { name: 'chevron' })) : null)))
1311
1464
  }
1312
1465
 
1313
1466
  /* ============================== 追问侧栏 ============================== */
1314
- function ThreadPane({ thread, questionId, busy, onSend, onClear, onClose, narrow }) {
1467
+ function ThreadPane({ thread, questionId, busy, onSend, onClear, onClose, narrow, ratio = 33, onRatioChange }) {
1315
1468
  const [draft, setDraft] = useState('')
1316
1469
  const [sending, setSending] = useState(false)
1317
1470
  const [localError, setLocalError] = useState('')
1318
1471
  const [pending, setPending] = useState(null)
1319
1472
  const scrollRef = useRef(null)
1473
+ const sendingRef = useRef(false)
1320
1474
  const messages = thread?.messages || []
1321
1475
 
1322
1476
  useEffect(() => {
@@ -1326,7 +1480,8 @@ window.__ModuleLoader__.load({
1326
1480
 
1327
1481
  const send = useCallback(async (text, messageId) => {
1328
1482
  const value = String(text || '').trim()
1329
- if (!value) return
1483
+ if (!value || sendingRef.current) return
1484
+ sendingRef.current = true
1330
1485
  setSending(true)
1331
1486
  setLocalError('')
1332
1487
  try {
@@ -1337,6 +1492,7 @@ window.__ModuleLoader__.load({
1337
1492
  setLocalError(error.message)
1338
1493
  setPending({ id: messageId, text: value })
1339
1494
  } finally {
1495
+ sendingRef.current = false
1340
1496
  setSending(false)
1341
1497
  }
1342
1498
  }, [onSend])
@@ -1372,11 +1528,30 @@ window.__ModuleLoader__.load({
1372
1528
  h('span', { className: 'kd-spinner' }), T.sending))
1373
1529
  }
1374
1530
 
1375
- return h('aside', { className: 'kd-thread', 'aria-label': T.followup },
1531
+ const resize = (value) => onRatioChange(Math.max(20, Math.min(55, value)))
1532
+ return h('aside', { className: 'kd-thread', 'aria-label': T.followup, style: { '--kd-thread-ratio': ratio / 100 } },
1533
+ !narrow ? h('div', { className: 'kd-thread-resize', role: 'separator', tabIndex: 0,
1534
+ 'aria-label': '调整题目与追问的宽度比例', 'aria-orientation': 'vertical',
1535
+ 'aria-valuemin': 20, 'aria-valuemax': 55, 'aria-valuenow': Math.round(ratio),
1536
+ onPointerDown: (event) => { event.preventDefault(); event.currentTarget.setPointerCapture(event.pointerId) },
1537
+ onPointerMove: (event) => {
1538
+ if (!event.currentTarget.hasPointerCapture(event.pointerId)) return
1539
+ const pane = event.currentTarget.parentElement
1540
+ const left = pane.closest('.kd-shell').querySelector('.kd-main').getBoundingClientRect().left
1541
+ const right = pane.getBoundingClientRect().right
1542
+ resize((right - event.clientX) / (right - left) * 100)
1543
+ },
1544
+ onPointerUp: (event) => event.currentTarget.releasePointerCapture(event.pointerId),
1545
+ onPointerCancel: (event) => event.currentTarget.releasePointerCapture(event.pointerId),
1546
+ onKeyDown: (event) => {
1547
+ const value = { ArrowLeft: ratio + 2, ArrowRight: ratio - 2, Home: 20, End: 55 }[event.key]
1548
+ if (value !== undefined) { event.preventDefault(); resize(value) }
1549
+ },
1550
+ }) : null,
1376
1551
  h('div', { className: 'kd-thread-head' },
1377
1552
  h('div', null,
1378
1553
  h('div', { className: 'kd-thread-title' }, T.followup),
1379
- h('div', { className: 'kd-msg-meta' }, `题目 ${String(questionId || '').slice(0, 14)}`)),
1554
+ h('div', { className: 'kd-msg-meta' }, '围绕当前题目,一起理清思路')),
1380
1555
  h('div', { className: 'kd-row' },
1381
1556
  h(Btn, {
1382
1557
  small: true,
@@ -1385,9 +1560,7 @@ window.__ModuleLoader__.load({
1385
1560
  onClick: onClear,
1386
1561
  title: '只清空本题的追问记录',
1387
1562
  }, T.clearThread),
1388
- narrow
1389
- ? h('button', { className: 'kd-iconbtn', onClick: onClose, 'aria-label': T.close }, h(Icon, { name: 'close' }))
1390
- : null)),
1563
+ h('button', { className: 'kd-iconbtn', onClick: onClose, 'aria-label': T.close }, h(Icon, { name: 'close' })))),
1391
1564
  h('div', { className: 'kd-thread-hint' }, T.followupHint),
1392
1565
  h('div', { className: 'kd-thread-scroll', ref: scrollRef },
1393
1566
  rows.length ? rows : h('div', { className: 'kd-empty-body' }, '还没有追问。可以问某一步推导为什么成立、某个知识点怎么理解,或有没有别的解法。')),
@@ -1397,13 +1570,13 @@ window.__ModuleLoader__.load({
1397
1570
  h('textarea', {
1398
1571
  className: 'kd-textarea',
1399
1572
  value: draft,
1400
- placeholder: '围绕这道题继续追问…(Ctrl+Enter 发送)',
1573
+ placeholder: '围绕这道题继续追问…(Enter 发送,Shift+Enter 换行)',
1401
1574
  disabled: sending,
1402
1575
  onChange: (event) => setDraft(event.target.value),
1403
1576
  onKeyDown: (event) => {
1404
- if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
1577
+ if (event.key === 'Enter' && !event.shiftKey && !event.isComposing && !event.nativeEvent?.isComposing && event.keyCode !== 229) {
1405
1578
  event.preventDefault()
1406
- send(draft, newAttemptId())
1579
+ if (!event.repeat) send(draft, newAttemptId())
1407
1580
  }
1408
1581
  },
1409
1582
  }),
@@ -1417,12 +1590,22 @@ window.__ModuleLoader__.load({
1417
1590
  }
1418
1591
 
1419
1592
  /* ============================== 弹窗 ============================== */
1420
- function SubjectsModal({ boot, onClose, onAdd, onRemove, onSource, onPick }) {
1593
+ function SubjectsModal({ boot, onClose, onDone }) {
1594
+ const [draft, setDraft] = useState(() => ({ subjects: [...boot.settings.subjects],
1595
+ subjectSource: { ...boot.settings.subjectSource }, customSubjects: structuredClone(boot.customSubjects || []) }))
1596
+ const [saving, setSaving] = useState(false)
1597
+ const [saveError, setSaveError] = useState('')
1421
1598
  const [name, setName] = useState('')
1422
1599
  const [types, setTypes] = useState([])
1423
1600
  const [customTypes, setCustomTypes] = useState([])
1424
1601
  const [typeDraft, setTypeDraft] = useState('')
1425
- const enabled = new Set(boot.settings.subjects)
1602
+ const enabled = new Set(draft.subjects)
1603
+ const subjects = [...(boot.subjects || []), ...draft.customSubjects.filter((item) => !(boot.subjects || []).some((s) => s.id === item.id)).map((item) => ({
1604
+ ...item, typeLabels: item.types.map((type) => ({ single: '单项选择题', multiple: '多项选择题', fill: '填空题', solution: '解答题' })[type] || type), bankItemCount: 0,
1605
+ }))]
1606
+ const close = () => { if (!saving) onClose() }
1607
+ const toggleSubject = (id) => setDraft((current) => ({ ...current,
1608
+ subjects: current.subjects.includes(id) ? current.subjects.filter((value) => value !== id) : [...current.subjects, id] }))
1426
1609
  const toggleType = (type) => setTypes((current) => current.includes(type)
1427
1610
  ? current.filter((item) => item !== type)
1428
1611
  : [...current, type])
@@ -1437,10 +1620,28 @@ window.__ModuleLoader__.load({
1437
1620
  setCustomTypes((current) => current.filter((item) => item !== value))
1438
1621
  setTypes((current) => current.filter((item) => item !== value))
1439
1622
  }
1440
- const canAdd = Boolean(name.trim()) && types.length > 0
1623
+ const selectedTypes = [...new Set([...types, ...(typeDraft.trim() ? [typeDraft.trim().slice(0, 24)] : [])])]
1624
+ const canAdd = Boolean(name.trim()) && selectedTypes.length > 0
1625
+ const withCustomSubject = (current) => {
1626
+ const subject = { id: 'custom_' + newAttemptId(), name: name.trim(), types: selectedTypes }
1627
+ return { ...current, subjects: [...current.subjects, subject.id], customSubjects: [...current.customSubjects, subject] }
1628
+ }
1629
+ const finish = async () => {
1630
+ if (saving) return
1631
+ if ((name.trim() || types.length || typeDraft.trim()) && !canAdd) {
1632
+ setSaveError('请填写自定义科目名称并至少选择一个题型,或清空未完成的输入。')
1633
+ return
1634
+ }
1635
+ const next = canAdd ? withCustomSubject(draft) : draft
1636
+ setSaving(true)
1637
+ setSaveError('')
1638
+ try { await onDone(next) }
1639
+ catch (error) { setSaveError(error.message || '保存失败,请重试') }
1640
+ finally { setSaving(false) }
1641
+ }
1441
1642
  const submit = () => {
1442
1643
  if (!canAdd) return
1443
- onAdd({ name: name.trim(), types })
1644
+ setDraft(withCustomSubject)
1444
1645
  setName('')
1445
1646
  setTypes([])
1446
1647
  setCustomTypes([])
@@ -1448,24 +1649,26 @@ window.__ModuleLoader__.load({
1448
1649
  }
1449
1650
  return h(Modal, {
1450
1651
  title: T.manageSubjects,
1451
- onClose,
1652
+ onClose: close,
1452
1653
  wide: true,
1453
1654
  footer: [
1454
- h(Btn, { key: 'close', onClick: onClose }, T.close),
1655
+ h(Btn, { key: 'cancel', onClick: close, disabled: saving }, T.cancel),
1656
+ h(Btn, { key: 'done', variant: 'primary', onClick: finish, disabled: saving }, saving ? '保存中…' : '完成'),
1455
1657
  ],
1456
1658
  },
1659
+ saveError ? h('div', { className: 'kd-error-text', role: 'alert' }, saveError) : null,
1660
+ h('fieldset', { disabled: saving, style: { border: 0, padding: 0, margin: 0, minWidth: 0 } },
1457
1661
  h('div', { className: 'kd-meta', style: { marginBottom: '12px' } },
1458
- '勾选要每天出题的科目。取消勾选只会移出每日出题,该科目已出过的题目、收藏、错题与作答记录都会保留。'),
1459
- (boot.subjects || []).map((subject) => h('div', {
1662
+ '修改后点击右下角「完成」统一保存;取消或关闭会放弃修改。停用科目不会删除历史题目、作答、收藏和追问。'),
1663
+ subjects.map((subject) => h('div', {
1460
1664
  key: subject.id,
1461
- className: 'kd-row-between',
1462
- style: { padding: '8px 0', borderBottom: '1px solid var(--dsw-alias-border-l1)' },
1665
+ className: 'kd-row-between kd-manage-card' + (enabled.has(subject.id) ? ' is-enabled' : ''),
1463
1666
  },
1464
1667
  h('label', { className: 'kd-checkline', style: { flex: '1 1 240px', cursor: 'pointer' } },
1465
1668
  h('input', {
1466
1669
  type: 'checkbox',
1467
1670
  checked: enabled.has(subject.id),
1468
- onChange: () => (enabled.has(subject.id) ? onRemove(subject) : onAdd(subject)),
1671
+ onChange: () => toggleSubject(subject.id),
1469
1672
  }),
1470
1673
  h('span', null,
1471
1674
  h('span', null, subject.name),
@@ -1475,24 +1678,19 @@ window.__ModuleLoader__.load({
1475
1678
  ? h('div', { className: 'kd-row' },
1476
1679
  h('select', {
1477
1680
  className: 'kd-select',
1478
- value: subject.source,
1681
+ value: draft.subjectSource[subject.id] || 'auto',
1479
1682
  'aria-label': `${subject.name} 出题来源`,
1480
- onChange: (event) => onSource(subject.id, event.target.value),
1683
+ onChange: (event) => { const source = event.target.value; setDraft((current) => ({ ...current, subjectSource: { ...current.subjectSource, [subject.id]: source } })) },
1481
1684
  },
1482
1685
  h('option', { value: 'auto' }, '自动(优先题库)'),
1483
1686
  h('option', { value: 'online' }, '在线出题'),
1484
1687
  h('option', { value: 'bank' }, '题库出题')),
1485
1688
  subject.bankItemCount
1486
1689
  ? h('span', { className: 'kd-msg-meta' }, `题库 ${subject.bankItemCount} 题`)
1487
- : h('span', { className: 'kd-msg-meta' }, '题库为空'),
1488
- h(Btn, {
1489
- small: true,
1490
- variant: 'ghost',
1491
- onClick: () => { onClose(); onPick(subject.id) },
1492
- }, '查看'))
1690
+ : h('span', { className: 'kd-msg-meta' }, '题库为空'))
1493
1691
  : null)),
1494
1692
  h('div', { className: 'kd-divider' }),
1495
- h('div', { className: 'kd-field' },
1693
+ h('div', { className: 'kd-field kd-custom-subject' },
1496
1694
  h('div', { className: 'kd-field-label' }, '添加自定义科目'),
1497
1695
  h('div', { className: 'kd-row' },
1498
1696
  h('input', {
@@ -1543,7 +1741,7 @@ window.__ModuleLoader__.load({
1543
1741
  }),
1544
1742
  h(Btn, { small: true, disabled: !typeDraft.trim(), onClick: addCustomType }, '添加题型')),
1545
1743
  h('div', { className: 'kd-msg-meta', style: { marginTop: '6px' } },
1546
- canAdd ? `将添加:${name.trim()}(${types.join(' / ')})` : '填写科目名称并至少选择一个题型后即可添加')))
1744
+ canAdd ? `将添加:${name.trim()}(${types.join(' / ')})` : '填写科目名称并至少选择一个题型后即可添加'))))
1547
1745
  }
1548
1746
 
1549
1747
  function BanksModal({ boot, importResult, onClose, onPickFile, onDelete, onCommit, onCancelImport }) {
@@ -1601,8 +1799,7 @@ window.__ModuleLoader__.load({
1601
1799
  (boot.banks || []).length
1602
1800
  ? boot.banks.map((bank) => h('div', {
1603
1801
  key: bank.id,
1604
- className: 'kd-row-between',
1605
- style: { padding: '9px 0', borderBottom: '1px solid var(--dsw-alias-border-l1)' },
1802
+ className: 'kd-row-between kd-manage-card',
1606
1803
  },
1607
1804
  h('div', { style: { minWidth: 0 } },
1608
1805
  h('div', null, bank.name),
@@ -1647,13 +1844,13 @@ window.__ModuleLoader__.load({
1647
1844
  h('div', { className: 'kd-msg-meta' }, '确认后才会替换正式版本;旧版本会保留在版本历史中。'))
1648
1845
  }
1649
1846
 
1650
- function ConfirmModal({ title, message, confirmLabel, onConfirm, onClose, danger }) {
1847
+ function ConfirmModal({ title, message, confirmLabel, onConfirm, onClose, danger, busy }) {
1651
1848
  return h(Modal, {
1652
1849
  title,
1653
1850
  onClose,
1654
1851
  footer: [
1655
- h(Btn, { key: 'cancel', onClick: onClose }, T.cancel),
1656
- h(Btn, { key: 'ok', variant: danger ? 'danger' : 'primary', onClick: onConfirm }, confirmLabel || T.confirm),
1852
+ h(Btn, { key: 'cancel', onClick: onClose, disabled: busy }, T.cancel),
1853
+ h(Btn, { key: 'ok', variant: danger ? 'danger' : 'primary', onClick: onConfirm, disabled: busy }, busy ? '正在处理…' : (confirmLabel || T.confirm)),
1657
1854
  ],
1658
1855
  }, h('div', { className: 'kd-md' }, message))
1659
1856
  }
@@ -1670,15 +1867,27 @@ window.__ModuleLoader__.load({
1670
1867
  const [thread, setThread] = useState(null)
1671
1868
  const [list, setList] = useState(null)
1672
1869
  const [busy, setBusy] = useState('')
1870
+ const [managing, setManaging] = useState(false)
1871
+ const [selected, setSelected] = useState([])
1872
+ const bulkLock = useRef(false)
1673
1873
  const [fail, setFail] = useState('')
1674
1874
  const [toast, setToast] = useState(null)
1675
1875
  const [modal, setModal] = useState(null)
1676
1876
  const [choice, setChoice] = useState('')
1677
1877
  const [localRedo, setLocalRedo] = useState(false)
1678
1878
  const [threadOpen, setThreadOpen] = useState(false)
1879
+ const [threadRatio, setThreadRatio] = useState(33)
1679
1880
  const fileRef = useRef(null)
1680
1881
  const attemptRef = useRef(newAttemptId())
1681
1882
  const toastTimer = useRef(null)
1883
+ const mainScroll = useRef(null)
1884
+ const listScroll = useRef(0)
1885
+ const redoQuestions = useRef(new Set())
1886
+
1887
+ useEffect(() => {
1888
+ if (view === 'list' && list && mainScroll.current) mainScroll.current.scrollTop = listScroll.current
1889
+ if (view === 'detail' && mainScroll.current) mainScroll.current.scrollTop = 0
1890
+ }, [view, list])
1682
1891
 
1683
1892
  const notify = useCallback((message, tone = 'ok') => {
1684
1893
  setToast({ message, tone })
@@ -1702,6 +1911,7 @@ window.__ModuleLoader__.load({
1702
1911
  if (!sid) return
1703
1912
  // 高亮必须跟着视图走:从「全部 / 收藏 / 错题本」切回今日题目时要一起复位
1704
1913
  setKind('today')
1914
+ setThreadOpen(false)
1705
1915
  setView('detail')
1706
1916
  setBusy('daily')
1707
1917
  setFail('')
@@ -1713,6 +1923,7 @@ window.__ModuleLoader__.load({
1713
1923
  try {
1714
1924
  const result = await rpc('ensureDaily', { subjectId: sid })
1715
1925
  setQuestion(result.question)
1926
+ setLocalRedo(redoQuestions.current.has(result.question.id))
1716
1927
  setThread(result.thread || null)
1717
1928
  } catch (issue) {
1718
1929
  setFail(issue.message)
@@ -1723,7 +1934,9 @@ window.__ModuleLoader__.load({
1723
1934
 
1724
1935
  const loadList = useCallback(async (sid, nextKind) => {
1725
1936
  if (!sid) return
1937
+ listScroll.current = 0
1726
1938
  setKind(nextKind)
1939
+ setThreadOpen(false)
1727
1940
  setView('list')
1728
1941
  setBusy('list')
1729
1942
  setList(null)
@@ -1737,6 +1950,8 @@ window.__ModuleLoader__.load({
1737
1950
  }
1738
1951
  }, [notify])
1739
1952
 
1953
+ useEffect(() => { setSelected([]); setManaging(false) }, [subjectId, kind, view])
1954
+
1740
1955
  const pickSubject = useCallback((sid) => {
1741
1956
  if (!sid || sid === subjectId) return
1742
1957
  setSubjectId(sid)
@@ -1746,13 +1961,17 @@ window.__ModuleLoader__.load({
1746
1961
  }, [subjectId, loadToday])
1747
1962
 
1748
1963
  const openQuestion = useCallback(async (qid) => {
1964
+ listScroll.current = mainScroll.current?.scrollTop || 0
1749
1965
  setBusy('question')
1750
1966
  try {
1751
1967
  const result = await rpc('getQuestion', { qid })
1968
+ const canResume = result.isToday && result.question.answerState === 'revealed'
1969
+ && result.question.attemptCount > 0 && !redoQuestions.current.has(qid)
1970
+ if (!canResume) redoQuestions.current.add(qid)
1752
1971
  setQuestion(result.question)
1753
1972
  setThread(result.thread || null)
1754
1973
  setView('detail')
1755
- setLocalRedo(false)
1974
+ setLocalRedo(!canResume)
1756
1975
  setChoice('')
1757
1976
  setThreadOpen(false)
1758
1977
  attemptRef.current = newAttemptId()
@@ -1763,6 +1982,15 @@ window.__ModuleLoader__.load({
1763
1982
  }
1764
1983
  }, [notify])
1765
1984
 
1985
+ const backToList = async () => {
1986
+ setThreadOpen(false)
1987
+ setView('list')
1988
+ try {
1989
+ const result = await rpc('listQuestions', { subjectId, kind })
1990
+ setList(result)
1991
+ } catch (issue) { notify(issue.message, 'bad') }
1992
+ }
1993
+
1766
1994
  useEffect(() => {
1767
1995
  let cancelled = false
1768
1996
  rpc('bootstrap').then((result) => {
@@ -1798,6 +2026,7 @@ window.__ModuleLoader__.load({
1798
2026
  attemptId: attemptRef.current,
1799
2027
  })
1800
2028
  setQuestion(result.question)
2029
+ redoQuestions.current.delete(question.id)
1801
2030
  setLocalRedo(false)
1802
2031
  refreshBoot()
1803
2032
  } catch (issue) {
@@ -1938,67 +2167,18 @@ window.__ModuleLoader__.load({
1938
2167
  }
1939
2168
  }, [boot, notify, refreshBoot])
1940
2169
 
1941
- const addSubject = useCallback(async (subject) => {
1942
- try {
1943
- let targetId = subject.id
1944
- if (!targetId) {
1945
- targetId = `custom_${Date.now().toString(36)}`
1946
- await rpc('saveSettings', {
1947
- customSubjects: [...(boot?.customSubjects || []), {
1948
- id: targetId,
1949
- name: subject.name,
1950
- types: subject.types,
1951
- }],
1952
- })
1953
- }
1954
- const result = await rpc('addSubject', { subjectId: targetId })
1955
- const fresh = await refreshBoot()
1956
- notify(`${subject.name} 已启用,系统会为它分配今天的题目`)
1957
- if (fresh && result) {
1958
- setSubjectId(targetId)
1959
- loadToday(targetId)
1960
- }
1961
- setModal(null)
1962
- } catch (issue) {
1963
- notify(issue.message, 'bad')
1964
- }
1965
- }, [boot, notify, refreshBoot, loadToday])
1966
-
1967
- const removeSubject = useCallback((subject) => {
1968
- setModal({
1969
- kind: 'confirm',
1970
- title: `${T.deleteSubject}:${subject.name}`,
1971
- message: `确认把「${subject.name}」移出每日出题吗?\n\n- 该科目已出过的题目、收藏、错题与作答记录都会保留在“全部”里。\n- 今天已经分配的题目不会丢失,重新启用后当天仍会复用同一道题。`,
1972
- confirmLabel: T.deleteSubject,
1973
- danger: true,
1974
- onConfirm: async () => {
1975
- setModal(null)
1976
- try {
1977
- const result = await rpc('removeSubject', { subjectId: subject.id })
1978
- const fresh = await refreshBoot()
1979
- notify(result.message || '已移出每日出题')
1980
- if (fresh) {
1981
- const next = fresh.settings.subjects[0] || null
1982
- setSubjectId(next)
1983
- if (next) loadToday(next)
1984
- else { setQuestion(null); setView('empty') }
1985
- }
1986
- } catch (issue) {
1987
- notify(issue.message, 'bad')
1988
- }
1989
- },
1990
- })
1991
- }, [notify, refreshBoot, loadToday])
1992
-
1993
- const changeSource = useCallback(async (id, source) => {
1994
- try {
1995
- await rpc('saveSettings', { subjectSource: { [id]: source } })
1996
- await refreshBoot()
1997
- notify('出题来源已更新')
1998
- } catch (issue) {
1999
- notify(issue.message, 'bad')
2170
+ const saveSubjects = async (draft) => {
2171
+ const next = draft.subjects.includes(subjectId) ? subjectId : (draft.subjects[0] || null)
2172
+ const result = await rpc('saveSettings', { ...draft, activeSubject: next })
2173
+ setBoot((current) => ({ ...current, ...result, customSubjects: draft.customSubjects }))
2174
+ setModal(null)
2175
+ notify('科目设置已保存')
2176
+ if (next !== subjectId) {
2177
+ setSubjectId(next)
2178
+ if (next) loadToday(next)
2179
+ else { setQuestion(null); setThread(null); setThreadOpen(false); setView('empty') }
2000
2180
  }
2001
- }, [notify, refreshBoot])
2181
+ }
2002
2182
 
2003
2183
  const pickFile = useCallback(() => {
2004
2184
  if (fileRef.current) {
@@ -2063,6 +2243,38 @@ window.__ModuleLoader__.load({
2063
2243
  })
2064
2244
  }, [notify, refreshBoot])
2065
2245
 
2246
+ const bulkQuestions = async (action) => {
2247
+ if (bulkLock.current || !selected.length) return
2248
+ const qids = [...selected]
2249
+ const execute = async (confirmProtected = false) => {
2250
+ if (bulkLock.current) return
2251
+ bulkLock.current = true
2252
+ setBusy('bulk')
2253
+ try {
2254
+ const result = await rpc('bulkQuestions', { qids, action, confirmProtected })
2255
+ if (result.requiresConfirmation) {
2256
+ setModal({ kind: 'confirm', title: '再次确认删除收藏或错题', danger: true,
2257
+ message: `选中的题目中有 ${result.protectedCount} 道已收藏或在错题本中。继续将永久删除所选 ${qids.length} 道题及其作答和追问,无法恢复。确定仍要删除吗?`,
2258
+ confirmLabel: '仍要永久删除', onConfirm: () => execute(true) })
2259
+ return
2260
+ }
2261
+ setModal(null)
2262
+ setSelected([])
2263
+ await loadList(subjectId, kind)
2264
+ await refreshBoot()
2265
+ notify(`已处理 ${result.affected} 道题目`)
2266
+ } catch (issue) {
2267
+ setModal(null)
2268
+ notify(issue.message, 'bad')
2269
+ } finally { bulkLock.current = false; setBusy('') }
2270
+ }
2271
+ if (action === 'delete') {
2272
+ setModal({ kind: 'confirm', title: '删除所选题目', danger: true,
2273
+ message: `将永久删除 ${qids.length} 道题及其作答、收藏、错题标记和追问,无法恢复。题库原文件仍会保留,未来可能重新抽到其中的题目。`,
2274
+ confirmLabel: '确认删除', onConfirm: () => execute() })
2275
+ } else await execute()
2276
+ }
2277
+
2066
2278
  const counts = subjectId ? boot?.counts?.[subjectId] : null
2067
2279
  const narrow = useMediaQuery('(max-width: 1180px)')
2068
2280
 
@@ -2127,6 +2339,11 @@ window.__ModuleLoader__.load({
2127
2339
  kind,
2128
2340
  subjectName: currentSubject?.name || '',
2129
2341
  onOpen: openQuestion,
2342
+ managing, selected, busy: busy === 'bulk',
2343
+ onManage: () => { setManaging(!managing); setSelected([]) },
2344
+ onToggle: (id) => setSelected((ids) => ids.includes(id) ? ids.filter((value) => value !== id) : [...ids, id]),
2345
+ onSelectAll: (value) => setSelected(value ? list.items.map((item) => item.id) : []),
2346
+ onBulk: bulkQuestions,
2130
2347
  })
2131
2348
  }
2132
2349
  if (view === 'detail' && question) {
@@ -2135,7 +2352,6 @@ window.__ModuleLoader__.load({
2135
2352
  busy,
2136
2353
  choice,
2137
2354
  localRedo,
2138
- narrow,
2139
2355
  threadOpen,
2140
2356
  onSelect: (key) => {
2141
2357
  setChoice((current) => {
@@ -2150,7 +2366,9 @@ window.__ModuleLoader__.load({
2150
2366
  onJudge: judge,
2151
2367
  onFavorite: favorite,
2152
2368
  onRedo: () => {
2369
+ redoQuestions.current.add(question.id)
2153
2370
  setLocalRedo(true)
2371
+ setThreadOpen(false)
2154
2372
  setChoice('')
2155
2373
  attemptRef.current = newAttemptId()
2156
2374
  },
@@ -2177,7 +2395,13 @@ window.__ModuleLoader__.load({
2177
2395
  })
2178
2396
  })()
2179
2397
 
2180
- const showThread = view === 'detail' && question && (!narrow || threadOpen)
2398
+ const pickTab = (value) => {
2399
+ if (value === kind && (value === 'today' || view === 'list')) return
2400
+ if (value === 'today') loadToday(subjectId)
2401
+ else loadList(subjectId, value)
2402
+ }
2403
+ const showThread = view === 'detail' && question && threadOpen && !localRedo
2404
+ && question.answerState === 'revealed' && question.attemptCount > 0
2181
2405
 
2182
2406
  return h('div', { className: 'kd-page' },
2183
2407
  styleInjected ? null : h('style', null, CSS),
@@ -2187,10 +2411,10 @@ window.__ModuleLoader__.load({
2187
2411
  h('div', { className: 'kd-brand' }, h(SidebarIcon, { size: 18 })),
2188
2412
  h('div', { style: { minWidth: 0 } },
2189
2413
  h('h1', { className: 'kd-title' }, T.title),
2190
- h('div', { className: 'kd-sub' },
2414
+ h('div', { className: 'kd-sub', title: T.subtitle },
2191
2415
  boot?.today ? `${formatDate(boot.today)} · ` : '',
2192
2416
  boot ? `已启用 ${boot.settings.subjects.length} 个科目 · ` : '',
2193
- T.subtitle))),
2417
+ '每天一道,稳步向前'))),
2194
2418
  boot ? h(CountdownLines, { countdown: boot.countdown }) : null),
2195
2419
  boot ? h(ExamStrip, {
2196
2420
  boot,
@@ -2201,14 +2425,9 @@ window.__ModuleLoader__.load({
2201
2425
  boot ? h(MobileBar, {
2202
2426
  boot,
2203
2427
  subjectId,
2204
- kind,
2205
2428
  onPick: pickSubject,
2206
2429
  onManage: () => setModal({ kind: 'subjects' }),
2207
2430
  onBanks: () => setModal({ kind: 'banks' }),
2208
- onTab: (value) => {
2209
- if (value === 'today') loadToday(subjectId)
2210
- else loadList(subjectId, value)
2211
- },
2212
2431
  }) : null,
2213
2432
  h('div', { className: 'kd-shell' },
2214
2433
  boot ? h(SubjectNav, {
@@ -2219,27 +2438,30 @@ window.__ModuleLoader__.load({
2219
2438
  onBanks: () => setModal({ kind: 'banks' }),
2220
2439
  }) : null,
2221
2440
  h('main', { className: 'kd-main' },
2222
- h('div', { className: 'kd-main-inner' },
2441
+ h('div', { className: 'kd-main-toolbar' },
2223
2442
  boot ? h(TabBar, {
2224
2443
  kind,
2225
2444
  counts,
2226
- onPick: (value) => {
2227
- if (value === 'today') loadToday(subjectId)
2228
- else loadList(subjectId, value)
2229
- },
2445
+ onPick: pickTab,
2230
2446
  }) : null,
2231
- mainContent)),
2232
- showThread
2447
+ kind !== 'today' && view === 'detail'
2448
+ ? h(Btn, { small: true, onClick: backToList, title: '返回列表', ariaLabel: '返回列表' }, h(Icon, { name: 'chevron-left', size: 16 })) : null),
2449
+ h('div', { className: 'kd-main-scroll', ref: mainScroll },
2450
+ h('div', { className: 'kd-main-inner' },
2451
+ h('div', { className: 'kd-view', key: `${subjectId}|${kind}|${view}|${question?.id || ''}` }, mainContent)))),
2452
+ h(Presence, null, showThread
2233
2453
  ? h(ThreadPane, {
2234
2454
  thread,
2235
2455
  questionId: question.id,
2236
2456
  busy,
2237
2457
  narrow,
2458
+ ratio: threadRatio,
2459
+ onRatioChange: setThreadRatio,
2238
2460
  onSend: sendFollowup,
2239
2461
  onClear: clearThread,
2240
2462
  onClose: () => setThreadOpen(false),
2241
2463
  })
2242
- : null),
2464
+ : null)),
2243
2465
  h('input', {
2244
2466
  ref: fileRef,
2245
2467
  type: 'file',
@@ -2247,14 +2469,12 @@ window.__ModuleLoader__.load({
2247
2469
  style: { display: 'none' },
2248
2470
  onChange: onFile,
2249
2471
  }),
2472
+ h(Presence, null,
2250
2473
  modal?.kind === 'subjects' && boot
2251
2474
  ? h(SubjectsModal, {
2252
2475
  boot,
2253
2476
  onClose: () => setModal(null),
2254
- onAdd: addSubject,
2255
- onRemove: removeSubject,
2256
- onSource: changeSource,
2257
- onPick: (id) => { setModal(null); pickSubject(id) },
2477
+ onDone: saveSubjects,
2258
2478
  })
2259
2479
  : null,
2260
2480
  modal?.kind === 'banks' && boot
@@ -2276,13 +2496,16 @@ window.__ModuleLoader__.load({
2276
2496
  onDiscard: discardRevision,
2277
2497
  }) : null,
2278
2498
  modal?.kind === 'confirm' ? h(ConfirmModal, {
2499
+ key: modal.title,
2279
2500
  title: modal.title,
2280
2501
  message: modal.message,
2281
2502
  confirmLabel: modal.confirmLabel,
2282
2503
  danger: modal.danger,
2504
+ busy: busy === 'bulk',
2283
2505
  onConfirm: modal.onConfirm,
2284
- onClose: () => setModal(null),
2506
+ onClose: () => { if (!bulkLock.current) setModal(null) },
2285
2507
  }) : null,
2508
+ ),
2286
2509
  toast ? h('div', { className: `kd-toast kd-toast-${toast.tone}` }, toast.message) : null,
2287
2510
  busy === 'import' || busy === 'commit'
2288
2511
  ? h('div', { className: 'kd-loading', style: { position: 'absolute', inset: 'auto 0 60px 0' } },