dsh-weread-gather 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/client.js +199 -51
  2. package/package.json +1 -1
package/client.js CHANGED
@@ -136,6 +136,10 @@ window.__ModuleLoader__.load({
136
136
  'note.search': '搜索划线原文、书名、章节…',
137
137
  'thought.title': '想法墙',
138
138
  'thought.search': '搜索想法内容、书名…',
139
+ 'thought.expand': '展开全文(还有 {count} 字)',
140
+ 'thought.collapse': '收起',
141
+ 'thought.contextToggle': '同章划线 {count}',
142
+ 'thought.contextTitle': '同章的划线(供参考)',
139
143
  'view.list': '列表',
140
144
  'view.wall': '墙',
141
145
  'filter.allBooks': '全部书籍',
@@ -321,6 +325,10 @@ window.__ModuleLoader__.load({
321
325
  'note.search': 'Search highlight text, book, chapter…',
322
326
  'thought.title': 'Thoughts',
323
327
  'thought.search': 'Search thoughts and books…',
328
+ 'thought.expand': 'Show all ({count} more characters)',
329
+ 'thought.collapse': 'Collapse',
330
+ 'thought.contextToggle': '{count} highlight(s) in the same chapter',
331
+ 'thought.contextTitle': 'Highlights in the same chapter (for context)',
324
332
  'view.list': 'List',
325
333
  'view.wall': 'Wall',
326
334
  'filter.allBooks': 'All books',
@@ -469,7 +477,9 @@ window.__ModuleLoader__.load({
469
477
 
470
478
  // ----------------------------------------------------------------- styles
471
479
  const CSS = `
472
- .wr-root{--wr-paper:#F6F8F7;--wr-paper-soft:#FFFFFF;--wr-wash:#F0F4F1;--wr-ink:#263C34;--wr-ink-soft:#6B7F74;--wr-ink-mute:#93A49B;--wr-sage:#A6CCB5;--wr-moss:#4F9274;--wr-clay:#A57F5B;--wr-progress:#73AD8A;--wr-track:#F0F4F1;--wr-line:#E7EEEA;--wr-line-2:#DCE6E0;--wr-heat-0:#EDF2EF;--wr-heat-1:#D5E9DD;--wr-heat-2:#A9D3BA;--wr-heat-3:#74B28E;--wr-heat-4:#438765;--dsw-alias-bg-base:var(--wr-paper);--dsw-alias-bg-layer-1:var(--wr-paper-soft);--dsw-alias-bg-layer-2:var(--wr-wash);--dsw-alias-label-primary:var(--wr-ink);--dsw-alias-label-secondary:var(--wr-ink-soft);--dsw-alias-border-l1:var(--wr-line);--dsw-alias-border-l2:var(--wr-line-2);--dsw-alias-brand-primary:var(--wr-moss);--dsw-alias-state-success-primary:#4F7A5B;--dsw-alias-state-warn-primary:#B98A57;--dsw-alias-state-error-primary:#A8574C;display:flex;flex-direction:column;height:100%;min-height:0;width:100%;box-sizing:border-box;background:radial-gradient(900px 420px at 50% -18%,#FFFFFF 0%,transparent 70%),var(--wr-paper);color:var(--wr-ink);font-family:inherit}
480
+ .wr-root{--wr-paper:#F6F8F7;--wr-paper-soft:#FFFFFF;--wr-wash:#F0F4F1;--wr-ink:#263C34;--wr-ink-soft:#6B7F74;--wr-ink-mute:#93A49B;--wr-sage:#A6CCB5;--wr-moss:#4F9274;--wr-clay:#A57F5B;--wr-progress:#73AD8A;--wr-track:#F0F4F1;--wr-line:#E7EEEA;--wr-line-2:#DCE6E0;--wr-card-line:#EDF1EE;--wr-divider:#EEF3F0;--wr-surface:#FBFCFB;--wr-heat-0:#F3F4F2;--wr-heat-1:#D5E9DD;--wr-heat-2:#A9D3BA;--wr-heat-3:#74B28E;--wr-heat-4:#438765;--dsw-alias-bg-base:var(--wr-paper);--dsw-alias-bg-layer-1:var(--wr-paper-soft);--dsw-alias-bg-layer-2:var(--wr-wash);--dsw-alias-label-primary:var(--wr-ink);--dsw-alias-label-secondary:var(--wr-ink-soft);--dsw-alias-border-l1:var(--wr-line);--dsw-alias-border-l2:var(--wr-line-2);--dsw-alias-brand-primary:var(--wr-moss);--dsw-alias-state-success-primary:#4F7A5B;--dsw-alias-state-warn-primary:#B98A57;--dsw-alias-state-error-primary:#A8574C;display:flex;flex-direction:column;height:100%;min-height:0;width:100%;box-sizing:border-box;background:radial-gradient(900px 420px at 50% -18%,#FFFFFF 0%,transparent 70%),var(--wr-paper);color:var(--wr-ink);font-family:inherit}
481
+ .wr-root[data-view=shelf]{--wr-paper:#EDF2EE;--wr-paper-soft:#FFFFFF}
482
+ .wr-root[data-view=notes],.wr-root[data-view=thoughts]{--wr-paper:#F8FAF8;--wr-paper-soft:#FFFFFF}
473
483
  .wr-head{flex:none;display:flex;align-items:center;gap:16px;padding:14px 26px;border-bottom:1px solid var(--dsw-alias-border-l1);background:rgba(255,255,255,.86);backdrop-filter:blur(10px);flex-wrap:wrap}
474
484
  .wr-brand{display:flex;flex-direction:column;gap:1px;min-width:0}
475
485
  .wr-brand-title{font-size:15.5px;font-weight:700;letter-spacing:.07em;color:var(--wr-ink)}
@@ -494,7 +504,7 @@ window.__ModuleLoader__.load({
494
504
  .wr-btn-ghost:hover{background:var(--wr-wash);border-color:var(--wr-sage)}
495
505
  .wr-body{flex:1;min-height:0;overflow:auto;padding:26px 30px 64px;scrollbar-width:thin;scrollbar-color:#CFD8C6 transparent}
496
506
  .wr-body-inner{max-width:1240px;margin:0 auto}
497
- .wr-card{background:var(--wr-paper-soft);border:1px solid var(--wr-line);border-radius:16px;padding:20px 22px;box-shadow:0 1px 2px rgba(38,60,52,.04)}
507
+ .wr-card{background:var(--wr-paper-soft);border:1px solid var(--wr-card-line);border-radius:16px;padding:20px 22px}
498
508
  .wr-section{margin-top:22px}
499
509
  .wr-section:first-child{margin-top:0}
500
510
  .wr-section-title{display:flex;align-items:baseline;gap:8px;margin:0 0 12px;font-size:13.5px;font-weight:700;letter-spacing:.03em}
@@ -503,7 +513,7 @@ window.__ModuleLoader__.load({
503
513
  .wr-empty{padding:36px 10px;text-align:center;color:var(--dsw-alias-label-secondary);font-size:13px}
504
514
  .wr-empty strong{display:block;color:var(--dsw-alias-label-primary);font-size:14px;margin-bottom:6px}
505
515
  .wr-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
506
- .wr-stat{background:var(--wr-paper-soft);border:1px solid var(--wr-line);border-radius:16px;padding:16px 18px;box-shadow:0 1px 2px rgba(38,60,52,.03)}
516
+ .wr-stat{background:var(--wr-paper-soft);border:1px solid var(--wr-card-line);border-radius:16px;padding:16px 18px}
507
517
  .wr-stat-label{font-size:11.5px;color:var(--dsw-alias-label-secondary);letter-spacing:.04em}
508
518
  .wr-stat-value{font-size:22px;font-weight:700;margin-top:6px;font-variant-numeric:tabular-nums;line-height:1.15}
509
519
  .wr-stat-hint{font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-top:6px;display:flex;gap:10px;flex-wrap:wrap}
@@ -526,10 +536,9 @@ window.__ModuleLoader__.load({
526
536
  @media (max-width:1240px){.wr-grid{grid-template-columns:repeat(8,minmax(0,1fr))}}
527
537
  @media (max-width:920px){.wr-grid{grid-template-columns:repeat(6,minmax(0,1fr))}}
528
538
  @media (max-width:640px){.wr-grid{grid-template-columns:repeat(4,minmax(0,1fr))}}
529
- .wr-book{appearance:none;border:0;background:transparent;padding:0;margin:0;text-align:left;cursor:pointer;display:flex;flex-direction:column;gap:6px;font:inherit;color:inherit}
530
- .wr-book-cover-wrap{position:relative;border-radius:8px;overflow:hidden;box-shadow:0 3px 8px rgba(38,60,52,.10),0 14px 26px -18px rgba(38,60,52,.5);transition:transform .2s ease,box-shadow .2s ease}
531
- .wr-book-cover-wrap{position:relative;border-radius:8px;overflow:hidden;box-shadow:0 3px 8px rgba(38,60,52,.10),0 14px 26px -18px rgba(38,60,52,.5);transition:transform .2s ease,box-shadow .2s ease}
532
- .wr-book:hover .wr-book-cover-wrap{transform:translateY(-3px);box-shadow:0 8px 16px rgba(38,60,52,.14),0 22px 34px -20px rgba(38,60,52,.55)}
539
+ .wr-book{appearance:none;border:0;background:transparent;padding:0;margin:0;text-align:left;cursor:pointer;display:flex;flex-direction:column;gap:6px;font:inherit;color:inherit;content-visibility:auto;contain-intrinsic-size:0 220px}
540
+ .wr-book-cover-wrap{position:relative;border-radius:8px;overflow:hidden;box-shadow:0 2px 6px rgba(38,60,52,.08);transition:transform .2s ease,box-shadow .2s ease}
541
+ .wr-book:hover .wr-book-cover-wrap{transform:translateY(-3px);box-shadow:0 6px 14px rgba(38,60,52,.12)}
533
542
  .wr-cover{aspect-ratio:3/4;width:100%;background:var(--dsw-alias-bg-layer-2);display:block;overflow:hidden;position:relative}
534
543
  .wr-cover img{width:100%;height:100%;object-fit:cover;display:block}
535
544
  .wr-cover-fallback{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:30px;font-weight:700;color:rgba(255,255,255,.94);background:linear-gradient(150deg,#A8B9A3,#5F7562)}
@@ -543,32 +552,34 @@ window.__ModuleLoader__.load({
543
552
  .wr-progress span{display:block;height:100%;background:var(--dsw-alias-brand-primary);border-radius:2px}
544
553
  .wr-hour{display:grid;grid-template-columns:repeat(24,minmax(0,1fr));gap:3px;align-items:end;height:120px}
545
554
  .wr-hour-col{display:flex;flex-direction:column;justify-content:flex-end;align-items:center;gap:4px;height:100%}
546
- .wr-hour-bar{width:100%;border-radius:3px 3px 0 0;background:var(--wr-sage);min-height:2px}
555
+ .wr-hour-bar{width:100%;border-radius:4px 4px 0 0;background:var(--wr-sage);min-height:2px}
556
+ .wr-hour-bar[data-future=true]{background:var(--wr-track)}
547
557
  .wr-hour-col:hover .wr-hour-bar,.wr-hour-bar[data-active=true]{background:var(--wr-moss)}
558
+ .wr-hour-bar[data-future=true],.wr-hour-col:hover .wr-hour-bar[data-future=true]{background:var(--wr-track)}
548
559
  .wr-hour-label[data-active=true]{color:var(--wr-ink);font-weight:600}
549
560
  .wr-hour-label{font-size:9px;color:var(--dsw-alias-label-secondary);white-space:nowrap}
550
561
  .wr-months{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:6px;align-items:end;height:132px}
551
562
  .wr-months .wr-hour-label{font-size:10px}
552
563
  .wr-heat-top{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:6px}
553
- .wr-heat-kpi{display:flex;gap:14px;flex-wrap:wrap;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:10px}
554
- .wr-heat-kpi b{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-weight:600}
555
564
  .wr-yearbar{display:inline-flex;align-items:center;gap:2px;border:1px solid var(--dsw-alias-border-l1);border-radius:999px;padding:2px;background:var(--dsw-alias-bg-layer-1)}
556
565
  .wr-yearbar button{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:1;padding:4px 7px;border-radius:999px;cursor:pointer}
557
566
  .wr-yearbar button:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
558
567
  .wr-yearbar button:disabled{opacity:.35;cursor:default}
559
568
  .wr-yearbar strong{font-size:12.5px;font-variant-numeric:tabular-nums;padding:0 4px;min-width:42px;text-align:center}
560
569
  .wr-heat-wrap{display:flex;gap:6px;overflow-x:auto;padding-bottom:4px}
561
- .wr-heat-days{display:grid;grid-template-rows:repeat(7,13px);gap:3px;flex:none;font-size:9.5px;color:var(--dsw-alias-label-secondary);align-items:center}
562
- .wr-heat-months{display:grid;grid-auto-flow:column;grid-auto-columns:13px;gap:3px;font-size:9.5px;color:var(--dsw-alias-label-secondary);overflow:hidden;margin:0 0 4px 0}
570
+ .wr-heat-days{display:grid;grid-template-rows:repeat(7,12px);gap:3px;flex:none;font-size:9.5px;color:var(--dsw-alias-label-secondary);align-items:center}
571
+ .wr-heat-months{display:grid;grid-auto-flow:column;grid-auto-columns:12px;gap:3px;font-size:9.5px;color:var(--dsw-alias-label-secondary);overflow:hidden;margin:0 0 4px 0}
563
572
  .wr-heat-months span{white-space:nowrap;overflow:visible}
564
- .wr-heat{display:grid;grid-auto-flow:column;grid-template-rows:repeat(7,13px);gap:3px;flex:none}
565
- .wr-heat-cell{width:13px;height:13px;border-radius:3px;background:var(--wr-heat-0);transition:transform .12s ease}
573
+ .wr-heat{display:grid;grid-auto-flow:column;grid-template-rows:repeat(7,12px);gap:3px;flex:none}
574
+ .wr-heat-cell{width:12px;height:12px;border-radius:2.5px;background:var(--wr-heat-0);transition:transform .12s ease}
566
575
  .wr-heat-cell[data-on=true]:hover{transform:scale(1.3)}
567
- .wr-heat-cell[data-future=true]{background:transparent;box-shadow:inset 0 0 0 1px var(--wr-line-2);opacity:.7}
576
+ .wr-heat-cell[data-future=true]{background:transparent}
577
+ .wr-heat-foot{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px}
578
+ .wr-heat-foot-stats{margin-left:auto;font-size:11px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap}
568
579
  .wr-banner-close{border:none;background:transparent;color:inherit;font-size:14px;line-height:1;cursor:pointer;padding:2px 4px;border-radius:6px;opacity:.55;flex:none;margin-left:auto}
569
580
  .wr-banner-close:hover{opacity:1;background:var(--wr-wash)}
570
581
  .wr-banner-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
571
- .wr-heat-legend{display:flex;align-items:center;gap:5px;margin-top:10px;font-size:11px;color:var(--dsw-alias-label-secondary)}
582
+ .wr-heat-legend{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--dsw-alias-label-secondary)}
572
583
  .wr-bars{display:flex;flex-direction:column;gap:8px}
573
584
  .wr-bar-row{display:grid;grid-template-columns:104px 1fr 54px;gap:10px;align-items:center;font-size:12px}
574
585
  .wr-bar-track{height:6px;border-radius:999px;background:var(--wr-track);overflow:hidden}
@@ -592,13 +603,29 @@ window.__ModuleLoader__.load({
592
603
  .wr-cols{column-count:2;column-gap:14px}
593
604
  @media (min-width:900px){.wr-cols{column-count:3}}
594
605
  @media (min-width:1300px){.wr-cols{column-count:4}}
595
- .wr-quote{break-inside:avoid;margin:0 0 14px;background:var(--wr-paper-soft);border:1px solid var(--wr-line);border-radius:14px;padding:14px 16px;position:relative;box-shadow:0 1px 2px rgba(38,60,52,.04)}
606
+ .wr-quote{break-inside:avoid;margin:0 0 14px;background:var(--wr-paper-soft);border:1px solid var(--wr-card-line);border-radius:14px;padding:14px 16px;position:relative}
596
607
  .wr-quote:before{content:'“';position:absolute;left:9px;top:3px;font-size:26px;color:#C3D2BC;font-family:Georgia,serif;line-height:1}
597
608
  .wr-quote-text{font-size:12.8px;line-height:1.62;white-space:pre-wrap;word-break:break-word;padding-left:12px;margin:0}
598
609
  .wr-quote-foot{display:flex;align-items:center;gap:6px;margin-top:9px;padding-left:12px;font-size:11px;color:var(--dsw-alias-label-secondary);min-width:0}
599
610
  .wr-quote-book{font-weight:600;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:60%}
600
611
  .wr-quote-tail{margin-left:auto;flex:none;display:flex;gap:6px;align-items:center}
601
- .wr-thought{break-inside:avoid;margin:0 0 14px;background:var(--wr-paper-soft);border:1px solid var(--wr-line);border-left:3px solid var(--wr-progress);border-radius:14px;padding:14px 16px;box-shadow:0 1px 2px rgba(38,60,52,.04)}
612
+ .wr-thought{break-inside:avoid;margin:0 0 14px;background:var(--wr-paper-soft);border:1px solid var(--wr-card-line);border-left:2px solid color-mix(in srgb, var(--wr-sage) 70%, transparent);border-radius:14px;padding:15px 17px}
613
+ .wr-cols .wr-thought{margin-bottom:20px;background:var(--wr-paper-soft);border:1px solid var(--wr-line);border-left:2px solid color-mix(in srgb, var(--wr-sage) 70%, transparent);box-shadow:0 1px 2px rgba(38,60,52,.05)}
614
+ .wr-cols .wr-thought[data-clickable=true]:hover{background:var(--wr-paper-soft);border-color:var(--wr-sage);box-shadow:0 4px 12px rgba(38,60,52,.09)}
615
+ .wr-cols .wr-thought[data-picked=true]{border-color:var(--dsw-alias-brand-primary)}
616
+ .wr-thought .wr-quote-text{font-size:13.5px;line-height:1.75;text-wrap:pretty}
617
+ .wr-thought .wr-quote-foot{flex-wrap:wrap;gap:4px 8px;margin-top:12px;padding-top:9px;border-top:1px solid var(--wr-divider)}
618
+ .wr-thought .wr-quote-book{white-space:normal;overflow:visible;max-width:none;text-overflow:clip}
619
+ .wr-clamp{display:-webkit-box;-webkit-line-clamp:9;-webkit-box-orient:vertical;overflow:hidden}
620
+ .wr-more{appearance:none;border:0;background:transparent;color:var(--wr-moss);font:inherit;font-size:11.5px;line-height:1.5;padding:6px 0 0;cursor:pointer;text-align:left}
621
+ .wr-more:hover{text-decoration:underline}
622
+ .wr-thread-toggle{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;line-height:1.5;padding:8px 0 0;cursor:pointer}
623
+ .wr-thread-toggle:hover{color:var(--wr-ink)}
624
+ .wr-thread{margin-top:10px;padding:10px 12px;border-radius:10px;background:var(--wr-surface);border:1px solid var(--wr-divider)}
625
+ .wr-thread-title{font-size:11px;color:var(--wr-ink-mute);letter-spacing:.03em;margin-bottom:4px}
626
+ .wr-thread-item{font-size:12.5px;line-height:1.7;padding:6px 0;border-top:1px dashed var(--wr-divider)}
627
+ .wr-thread-item:first-of-type{border-top:0;padding-top:0}
628
+ .wr-thread-date{display:block;font-size:10.5px;color:var(--wr-ink-mute);margin-top:2px}
602
629
  .wr-quote[data-clickable=true],.wr-thought[data-clickable=true]{cursor:pointer;transition:border-color .12s ease}
603
630
  .wr-quote[data-clickable=true]:hover,.wr-thought[data-clickable=true]:hover{border-color:var(--dsw-alias-border-l2)}
604
631
  .wr-copy{appearance:none;border:1px solid var(--wr-line);background:var(--wr-paper);color:var(--wr-ink-soft);font:inherit;font-size:10.5px;line-height:1;padding:4px 9px;border-radius:999px;cursor:pointer;flex:none;opacity:0;transition:opacity .14s ease,color .14s ease,border-color .14s ease}
@@ -618,7 +645,7 @@ window.__ModuleLoader__.load({
618
645
  .wr-export-body{flex:1 1 auto;min-height:120px;max-height:46vh;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 12px;background:var(--dsw-alias-bg-layer-1)}
619
646
  .wr-export-label{display:flex;justify-content:space-between;gap:10px;font-size:11px;color:var(--dsw-alias-label-secondary);margin-bottom:6px}
620
647
  .wr-export-preview{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:1.6;white-space:pre-wrap;word-break:break-word}
621
- .wr-stars{color:var(--dsw-alias-state-warn-primary);font-size:10.5px;letter-spacing:1px}
648
+ .wr-stars{color:var(--wr-clay);font-size:10.5px;letter-spacing:1px}
622
649
  .wr-detail-head{display:flex;gap:18px;align-items:flex-start;flex-wrap:wrap}
623
650
  .wr-detail-cover{width:120px;flex:none;border-radius:12px;overflow:hidden;box-shadow:0 10px 24px -12px rgba(38,60,52,.55)}
624
651
  .wr-detail-main{flex:1;min-width:220px;display:flex;flex-direction:column;gap:8px}
@@ -662,7 +689,7 @@ window.__ModuleLoader__.load({
662
689
  'div',
663
690
  { className: 'wr-cover' },
664
691
  book.cover && !failed
665
- ? h('img', { src: book.cover, alt: book.title || '', loading: 'lazy', referrerPolicy: 'no-referrer', onError: () => setFailed(true) })
692
+ ? h('img', { src: book.cover, alt: book.title || '', loading: 'lazy', decoding: 'async', referrerPolicy: 'no-referrer', onError: () => setFailed(true) })
666
693
  : h('div', { className: 'wr-cover-fallback' }, String(book.title || '书').slice(0, 1)),
667
694
  )
668
695
  }
@@ -860,9 +887,12 @@ window.__ModuleLoader__.load({
860
887
  }
861
888
  return 0
862
889
  }
890
+ // Monthly ticks would crowd the ~53 columns, so only quarter starts get a
891
+ // label — enough to orient without turning the axis into a wall of text.
892
+ const labelledMonths = [1, 4, 7, 10]
863
893
  const ticks = model.columns.map((column, index) => {
864
894
  const month = monthOf(column)
865
- if (month === 0) return ''
895
+ if (month === 0 || !labelledMonths.includes(month)) return ''
866
896
  for (let back = index - 1; back >= 0; back -= 1) {
867
897
  const previous = monthOf(model.columns[back])
868
898
  if (previous > 0) return previous === month ? '' : monthLabel(month - 1)
@@ -893,8 +923,8 @@ window.__ModuleLoader__.load({
893
923
  cells.push(h('div', { key: `e-${columnIndex}-${row}`, className: 'wr-heat-cell', style: { background: 'transparent' } }))
894
924
  continue
895
925
  }
896
- // Days after today are drawn as empty outlines, so "not yet" never
897
- // reads as "read nothing".
926
+ // Days after today are left blank: an outline still reads as a row of
927
+ // dots, and "not yet" needs no decoration at all.
898
928
  const future = cell.iso > todayIso
899
929
  cells.push(
900
930
  h('div', {
@@ -903,7 +933,7 @@ window.__ModuleLoader__.load({
903
933
  'data-on': cell.value > 0 ? 'true' : 'false',
904
934
  'data-future': future ? 'true' : 'false',
905
935
  title: future
906
- ? `${cell.iso} · ${t('dash.heat.future')}`
936
+ ? null
907
937
  : `${cell.iso} · ${cell.value > 0 ? fmtDuration(cell.value) : t('dash.heat.none')}`,
908
938
  style: { background: future ? 'transparent' : level(cell.value) },
909
939
  }),
@@ -914,14 +944,6 @@ window.__ModuleLoader__.load({
914
944
  return h(
915
945
  'div',
916
946
  null,
917
- h(
918
- 'div',
919
- { className: 'wr-heat-kpi' },
920
- h('span', null, t('dash.heat.active', { days: model.active })),
921
- h('span', null, t('dash.heat.streak', { days: model.streak })),
922
- h('span', null, t('dash.heat.total', { time: fmtDuration(model.total) })),
923
- model.bestDate === '' ? null : h('span', null, t('dash.heat.best', { date: model.bestDate, time: fmtDuration(model.best) })),
924
- ),
925
947
  h(
926
948
  'div',
927
949
  { className: 'wr-heat-wrap' },
@@ -930,18 +952,36 @@ window.__ModuleLoader__.load({
930
952
  'div',
931
953
  { style: { minWidth: 0 } },
932
954
  h('div', { className: 'wr-heat-months' }, ticks.map((label, index) => h('span', { key: index }, label))),
933
- h('div', { className: 'wr-heat' }, cells),
955
+ h(
956
+ 'div',
957
+ {
958
+ className: 'wr-heat',
959
+ title: model.bestDate === '' ? null : t('dash.heat.best', { date: model.bestDate, time: fmtDuration(model.best) }),
960
+ },
961
+ cells,
962
+ ),
934
963
  ),
935
964
  ),
965
+ // The numbers live under the grid and stay to one line: above it they
966
+ // pushed the calendar away from the card title.
936
967
  h(
937
968
  'div',
938
- { className: 'wr-heat-legend' },
939
- h('span', null, t('dash.heat.none')),
940
- h('span', { className: 'wr-heat-cell' }),
941
- h('span', { style: { marginLeft: '6px' } }, t('dash.less')),
942
- ['var(--wr-heat-1)', 'var(--wr-heat-2)', 'var(--wr-heat-3)', 'var(--wr-heat-4)'].map((color, index) =>
943
- h('span', { key: index, className: 'wr-heat-cell', style: { background: color } })),
944
- h('span', null, t('dash.more')),
969
+ { className: 'wr-heat-foot' },
970
+ h(
971
+ 'div',
972
+ { className: 'wr-heat-legend' },
973
+ h('span', null, t('dash.heat.none')),
974
+ h('span', { className: 'wr-heat-cell' }),
975
+ h('span', { style: { marginLeft: '6px' } }, t('dash.less')),
976
+ ['var(--wr-heat-1)', 'var(--wr-heat-2)', 'var(--wr-heat-3)', 'var(--wr-heat-4)'].map((color, index) =>
977
+ h('span', { key: index, className: 'wr-heat-cell', style: { background: color } })),
978
+ h('span', null, t('dash.more')),
979
+ ),
980
+ h(
981
+ 'div',
982
+ { className: 'wr-heat-foot-stats' },
983
+ `${t('dash.heat.active', { days: model.active })} · ${t('dash.heat.total', { time: fmtDuration(model.total) })}`,
984
+ ),
945
985
  ),
946
986
  )
947
987
  }
@@ -987,16 +1027,22 @@ window.__ModuleLoader__.load({
987
1027
  function MonthChart(props) {
988
1028
  const values = Array.isArray(props.values) ? props.values : []
989
1029
  const activeIndex = Number.isInteger(props.activeIndex) ? props.activeIndex : -1
1030
+ const year = Number(props.year) || new Date().getFullYear()
1031
+ const now = new Date()
1032
+ // Months that have not happened yet are drawn in the track colour instead
1033
+ // of vanishing, so a partial year reads as "not yet" rather than "no data".
1034
+ const futureFrom = year === now.getFullYear() ? now.getMonth() + 1 : year > now.getFullYear() ? 0 : 13
990
1035
  const max = Math.max.apply(null, values.concat([1]))
991
1036
  const cells = []
992
1037
  for (let index = 0; index < 12; index += 1) {
993
1038
  const value = values[index] || 0
994
- const height = max <= 0 ? 0 : Math.max(value > 0 ? 4 : 0, Math.round((value / max) * 100))
1039
+ const future = index >= futureFrom
1040
+ const height = max <= 0 || future ? 0 : Math.max(value > 0 ? 4 : 0, Math.round((value / max) * 100))
995
1041
  cells.push(
996
1042
  h(
997
1043
  'div',
998
- { className: 'wr-hour-col', key: index, title: `${index + 1} · ${fmtDuration(value)}` },
999
- h('div', { className: 'wr-hour-bar', 'data-active': index === activeIndex ? 'true' : 'false', style: { height: `${height}%` } }),
1044
+ { className: 'wr-hour-col', key: index, title: future ? `${index + 1} · ${t('dash.heat.future')}` : `${index + 1} · ${fmtDuration(value)}` },
1045
+ h('div', { className: 'wr-hour-bar', 'data-active': index === activeIndex ? 'true' : 'false', 'data-future': future ? 'true' : 'false', style: { height: future ? '6%' : `${height}%` } }),
1000
1046
  h('div', { className: 'wr-hour-label', 'data-active': index === activeIndex ? 'true' : 'false' }, String(index + 1)),
1001
1047
  ),
1002
1048
  )
@@ -1196,7 +1242,7 @@ window.__ModuleLoader__.load({
1196
1242
  h(
1197
1243
  'div',
1198
1244
  { className: 'wr-split wr-section' },
1199
- h('div', { className: 'wr-card' }, h('h3', { className: 'wr-section-title' }, t('dash.month')), h(MonthChart, { values: monthly, activeIndex: isCurrent ? new Date().getMonth() : -1 })),
1245
+ h('div', { className: 'wr-card' }, h('h3', { className: 'wr-section-title' }, t('dash.month')), h(MonthChart, { values: monthly, year, activeIndex: isCurrent ? new Date().getMonth() : -1 })),
1200
1246
  h('div', { className: 'wr-card' }, h('h3', { className: 'wr-section-title' }, t('dash.category')), h(BarList, { items: categories })),
1201
1247
  ),
1202
1248
  h(
@@ -1487,15 +1533,42 @@ window.__ModuleLoader__.load({
1487
1533
  )
1488
1534
  }
1489
1535
 
1536
+ // A thought is the reader's own voice, so the card gives the text room:
1537
+ // wider line height, no clamped source label, and the left rail is a quiet
1538
+ // sage hairline instead of an accent bar. In the wall view the card has no
1539
+ // border at all and relies on spacing.
1540
+ const THOUGHT_CLAMP_CHARS = 220
1541
+
1490
1542
  function ThoughtRow(props) {
1491
1543
  const thought = props.thought
1492
1544
  const open = typeof props.onOpen === 'function' ? props.onOpen : null
1493
1545
  const pick = typeof props.onToggle === 'function' ? h(PickButton, { on: Boolean(props.selected), onChange: props.onToggle }) : null
1546
+ const content = String(thought.content || '')
1547
+ const clampable = Boolean(props.wall) && content.length > THOUGHT_CLAMP_CHARS
1548
+ const [expanded, setExpanded] = useState(false)
1549
+ const [threadOpen, setThreadOpen] = useState(false)
1550
+ const context = Array.isArray(props.context) ? props.context : []
1551
+ const slice = context.slice(0, 6)
1552
+ const showClamped = clampable && !expanded
1494
1553
  return h(
1495
1554
  'div',
1496
1555
  { className: 'wr-thought', 'data-clickable': open ? 'true' : 'false', 'data-picked': props.selected ? 'true' : 'false', title: open ? t('action.openBook') : undefined, onClick: open || undefined },
1497
1556
  pick,
1498
- h(MarkedText, { className: 'wr-quote-text', text: thought.content, needle: props.needle }),
1557
+ h(MarkedText, { className: showClamped ? 'wr-quote-text wr-clamp' : 'wr-quote-text', text: content, needle: props.needle }),
1558
+ clampable
1559
+ ? h(
1560
+ 'button',
1561
+ {
1562
+ className: 'wr-more',
1563
+ type: 'button',
1564
+ onClick: (event) => {
1565
+ if (event && typeof event.stopPropagation === 'function') event.stopPropagation()
1566
+ setExpanded(!expanded)
1567
+ },
1568
+ },
1569
+ expanded ? t('thought.collapse') : t('thought.expand', { count: String(content.length - THOUGHT_CLAMP_CHARS) }),
1570
+ )
1571
+ : null,
1499
1572
  h(
1500
1573
  'div',
1501
1574
  { className: 'wr-quote-foot' },
@@ -1506,9 +1579,38 @@ window.__ModuleLoader__.load({
1506
1579
  { className: 'wr-quote-tail' },
1507
1580
  thought.star >= 0 ? h('span', { className: 'wr-stars' }, '★'.repeat(Math.min(5, Math.max(0, thought.star)))) : null,
1508
1581
  h('span', null, fmtDate(thought.createTime)),
1509
- h(CopyButton, { text: thought.content }),
1582
+ h(CopyButton, { text: content }),
1510
1583
  ),
1511
1584
  ),
1585
+ // WeRead does not link a thought to the exact highlight it answers, so
1586
+ // this is chapter-level context only, labelled as such.
1587
+ slice.length === 0
1588
+ ? null
1589
+ : threadOpen
1590
+ ? h(
1591
+ 'div',
1592
+ { className: 'wr-thread' },
1593
+ h('div', { className: 'wr-thread-title' }, t('thought.contextTitle')),
1594
+ slice.map((note) =>
1595
+ h(
1596
+ 'div',
1597
+ { className: 'wr-thread-item', key: note.id },
1598
+ `> ${String(note.text || '').slice(0, 300)}`,
1599
+ h('span', { className: 'wr-thread-date' }, fmtDate(note.createTime)),
1600
+ )),
1601
+ )
1602
+ : h(
1603
+ 'button',
1604
+ {
1605
+ className: 'wr-thread-toggle',
1606
+ type: 'button',
1607
+ onClick: (event) => {
1608
+ if (event && typeof event.stopPropagation === 'function') event.stopPropagation()
1609
+ setThreadOpen(true)
1610
+ },
1611
+ },
1612
+ t('thought.contextToggle', { count: String(context.length) }),
1613
+ ),
1512
1614
  )
1513
1615
  }
1514
1616
 
@@ -1711,6 +1813,20 @@ window.__ModuleLoader__.load({
1711
1813
  })
1712
1814
  }, [source, query, bookFilter, kind])
1713
1815
 
1816
+ // Thoughts carry a chapter name, highlights a chapter title, and WeRead
1817
+ // does not link them; chapter-level context is the only honest pairing.
1818
+ const chapterIndex = useMemo(() => {
1819
+ const map = new Map()
1820
+ for (const note of data.notes || []) {
1821
+ const key = `${note.bookId}|${note.chapterTitle || ''}`
1822
+ const bucket = map.get(key)
1823
+ if (bucket) bucket.push(note)
1824
+ else map.set(key, [note])
1825
+ }
1826
+ return map
1827
+ }, [data.notes])
1828
+ const contextOf = (thought) => (thought.chapterName ? chapterIndex.get(`${thought.bookId}|${thought.chapterName}`) || [] : [])
1829
+
1714
1830
  useEffect(() => {
1715
1831
  setPage(0)
1716
1832
  setSelected(new Set())
@@ -1814,7 +1930,16 @@ window.__ModuleLoader__.load({
1814
1930
  })
1815
1931
  return kind === 'note'
1816
1932
  ? h(NoteRow, { key: item.id, note: item, needle: query.trim(), selected: selected.has(item.id), onToggle: pick, onOpen: props.onOpenBook ? () => props.onOpenBook(item.bookId, kind) : null })
1817
- : h(ThoughtRow, { key: item.id, thought: item, needle: query.trim(), selected: selected.has(item.id), onToggle: pick, onOpen: props.onOpenBook ? () => props.onOpenBook(item.bookId, kind) : null })
1933
+ : h(ThoughtRow, {
1934
+ key: item.id,
1935
+ thought: item,
1936
+ needle: query.trim(),
1937
+ wall: mode === 'wall',
1938
+ context: contextOf(item),
1939
+ selected: selected.has(item.id),
1940
+ onToggle: pick,
1941
+ onOpen: props.onOpenBook ? () => props.onOpenBook(item.bookId, kind) : null,
1942
+ })
1818
1943
  }),
1819
1944
  ),
1820
1945
  filtered.length > pageSize
@@ -1840,6 +1965,19 @@ window.__ModuleLoader__.load({
1840
1965
 
1841
1966
  const notes = useMemo(() => (data.notes || []).filter((note) => note.bookId === book.bookId), [data.notes, book.bookId])
1842
1967
  const thoughts = useMemo(() => (data.thoughts || []).filter((thought) => thought.bookId === book.bookId), [data.thoughts, book.bookId])
1968
+ // Highlights grouped by chapter: the only honest context a thought can
1969
+ // show, because WeRead never links a thought to one specific highlight.
1970
+ const chapterIndex = useMemo(() => {
1971
+ const map = new Map()
1972
+ for (const note of notes) {
1973
+ const key = note.chapterTitle || ''
1974
+ if (key === '') continue
1975
+ const bucket = map.get(key)
1976
+ if (bucket) bucket.push(note)
1977
+ else map.set(key, [note])
1978
+ }
1979
+ return map
1980
+ }, [notes])
1843
1981
  const needle = query.trim().toLowerCase()
1844
1982
  const visibleNotes = needle === '' ? notes : notes.filter((note) => contains(note.text, needle) || contains(note.chapterTitle, needle))
1845
1983
  const visibleThoughts = needle === '' ? thoughts : thoughts.filter((thought) => contains(thought.content, needle) || contains(thought.chapterName, needle))
@@ -1911,7 +2049,9 @@ window.__ModuleLoader__.load({
1911
2049
  'div',
1912
2050
  { className: 'wr-cols' },
1913
2051
  pageItems.map((item) =>
1914
- tab === 'notes' ? h(NoteRow, { key: item.id, note: item, needle: needle }) : h(ThoughtRow, { key: item.id, thought: item, needle: needle }),
2052
+ tab === 'notes'
2053
+ ? h(NoteRow, { key: item.id, note: item, needle: needle })
2054
+ : h(ThoughtRow, { key: item.id, thought: item, needle: needle, wall: true, context: chapterIndex.get(item.chapterName || '') || [] }),
1915
2055
  ),
1916
2056
  ),
1917
2057
  activeList.length > pageSize
@@ -1998,7 +2138,7 @@ window.__ModuleLoader__.load({
1998
2138
  'div',
1999
2139
  { className: 'wr-section' },
2000
2140
  h('h3', { className: 'wr-section-title' }, t('search.thoughts'), h('span', { className: 'wr-section-hint' }, String(groups.thoughts.length))),
2001
- h('div', { className: 'wr-cols' }, groups.thoughts.map((thought) => h(ThoughtRow, { key: thought.id, thought, needle, onOpen: () => openBookOf(thought.bookId, 'thoughts') }))),
2141
+ h('div', { className: 'wr-cols' }, groups.thoughts.map((thought) => h(ThoughtRow, { key: thought.id, thought, needle, wall: true, onOpen: () => openBookOf(thought.bookId, 'thoughts') }))),
2002
2142
  )
2003
2143
  : null,
2004
2144
  )
@@ -2489,9 +2629,17 @@ window.__ModuleLoader__.load({
2489
2629
  body = h(Overview, { data: data || {} })
2490
2630
  }
2491
2631
 
2632
+ // Each room gets its own paper tint, and the tint follows the tab you are
2633
+ // standing in — a book's thought tab keeps the thought-room warmth.
2634
+ const room = query.trim() !== ''
2635
+ ? 'search'
2636
+ : view === 'book'
2637
+ ? (bookTab === 'thoughts' ? 'thoughts' : 'notes')
2638
+ : view
2639
+
2492
2640
  return h(
2493
2641
  'div',
2494
- { className: 'wr-root', 'data-dsh-weread-gather': '' },
2642
+ { className: 'wr-root', 'data-view': room, 'data-dsh-weread-gather': '' },
2495
2643
  h('style', { dangerouslySetInnerHTML: { __html: CSS } }),
2496
2644
  head,
2497
2645
  h(
@@ -2595,7 +2743,7 @@ window.__ModuleLoader__.load({
2595
2743
  },
2596
2744
  // Test-only handles: the render smoke test mounts these directly, because
2597
2745
  // the export panel normally only appears after a click. Nothing else reads it.
2598
- __smoke: { ExportPanel, MarkedText, PickButton, SyncBanner, clip },
2746
+ __smoke: { ExportPanel, MarkedText, PickButton, SyncBanner, clip, ThoughtRow, THOUGHT_CLAMP_CHARS },
2599
2747
  }
2600
2748
  },
2601
2749
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-weread-gather",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "数字书房 — 微信读书书架、笔记与想法的私人书房:Markdown 导出、备份恢复、增量同步。",
5
5
  "type": "module",
6
6
  "license": "MIT",