hd-garden 0.5.8 → 0.5.10
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/app/components/SiteHeader.tsx +18 -10
- package/app/components/garden/blocks.tsx +1 -1
- package/app/globals.css +191 -36
- package/app/styles/a2.css +7 -5
- package/app/styles/shell.css +1 -30
- package/package.json +1 -1
- package/ui/garden/search-index.mjs +32 -10
- package/ui/garden/search-match.mjs +7 -4
|
@@ -276,11 +276,17 @@ export function SiteHeader() {
|
|
|
276
276
|
|
|
277
277
|
// Excerpts first, full text once it is here. Typos fall back to Fuse's fuzzy match.
|
|
278
278
|
const matcher = useMemo(() => (engine ? createSearch(engine.records, fullText) : null), [engine, fullText]);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (!
|
|
282
|
-
|
|
283
|
-
|
|
279
|
+
// The list shows the best few; the label counts every match.
|
|
280
|
+
const { hits: results, total: resultTotal } = useMemo((): { hits: SearchHit[]; total: number } => {
|
|
281
|
+
if (!engine || !matcher) return { hits: [], total: 0 };
|
|
282
|
+
if (!query.trim()) {
|
|
283
|
+
const recent = engine.records.slice(0, 5).map((item) => ({ item, matches: [] }));
|
|
284
|
+
return { hits: recent, total: recent.length };
|
|
285
|
+
}
|
|
286
|
+
const exact = matcher.find(query, 6) as unknown as { hits: SearchHit[]; total: number };
|
|
287
|
+
if (exact.total) return exact;
|
|
288
|
+
const fuzzy = engine.fuse.search(query);
|
|
289
|
+
return { hits: fuzzy.slice(0, 6), total: fuzzy.length };
|
|
284
290
|
}, [engine, matcher, query]);
|
|
285
291
|
|
|
286
292
|
useEffect(() => {
|
|
@@ -459,7 +465,7 @@ export function SiteHeader() {
|
|
|
459
465
|
aria-label="搜索结果"
|
|
460
466
|
>
|
|
461
467
|
<p className="search-label">
|
|
462
|
-
{query ? `${
|
|
468
|
+
{query ? `${resultTotal} 篇相关文章` : "最近内容"}
|
|
463
469
|
</p>
|
|
464
470
|
{results.map((result, index) => {
|
|
465
471
|
const preview = searchPreview(result.item, result.matches);
|
|
@@ -477,9 +483,11 @@ export function SiteHeader() {
|
|
|
477
483
|
aria-selected={activeResult === index}
|
|
478
484
|
className={`search-result${activeResult === index ? " active" : ""}`}
|
|
479
485
|
>
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
486
|
+
{result.item.section ? (
|
|
487
|
+
<span className="search-chip">
|
|
488
|
+
{result.item.section}
|
|
489
|
+
</span>
|
|
490
|
+
) : null}
|
|
483
491
|
<strong>
|
|
484
492
|
<HighlightedText
|
|
485
493
|
text={result.item.title}
|
|
@@ -500,7 +508,7 @@ export function SiteHeader() {
|
|
|
500
508
|
</div>
|
|
501
509
|
<footer className="search-footer">
|
|
502
510
|
<span>支持模糊匹配</span>
|
|
503
|
-
<span>ESC 关闭</span>
|
|
511
|
+
<span><kbd>ESC</kbd> 关闭</span>
|
|
504
512
|
</footer>
|
|
505
513
|
</section>
|
|
506
514
|
</div>
|
|
@@ -111,7 +111,7 @@ function CompareLine({ line }: { line: string }) {
|
|
|
111
111
|
if (!split) return <p><Rich text={line} /></p>;
|
|
112
112
|
return (
|
|
113
113
|
<p className="g-cmp-line">
|
|
114
|
-
<span className="g-cmp-t"><Rich text={split.title}
|
|
114
|
+
<span className="g-cmp-h"><span className="g-cmp-t"><Rich text={split.title} /></span><span className="g-cmp-sep">{split.separator}</span></span>
|
|
115
115
|
<Rich text={split.text} />
|
|
116
116
|
</p>
|
|
117
117
|
);
|
package/app/globals.css
CHANGED
|
@@ -1521,6 +1521,11 @@ button {
|
|
|
1521
1521
|
border: 1px solid var(--line);
|
|
1522
1522
|
}
|
|
1523
1523
|
|
|
1524
|
+
/* A thought opened from a link: only the border changes, in the ink accent. */
|
|
1525
|
+
.thought-card:target {
|
|
1526
|
+
border-color: var(--accent);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1524
1529
|
.thought-card > header {
|
|
1525
1530
|
display: flex;
|
|
1526
1531
|
justify-content: space-between;
|
|
@@ -3530,6 +3535,8 @@ html[data-theme="dark"] .article-references {
|
|
|
3530
3535
|
font-size: 12px;
|
|
3531
3536
|
}
|
|
3532
3537
|
|
|
3538
|
+
/* Search dialog (A2): a paper sheet with soft depth. No hard rules inside:
|
|
3539
|
+
rows are separated by space, the active row by a warm tint. */
|
|
3533
3540
|
.search-backdrop {
|
|
3534
3541
|
position: fixed;
|
|
3535
3542
|
inset: 0;
|
|
@@ -3538,92 +3545,173 @@ html[data-theme="dark"] .article-references {
|
|
|
3538
3545
|
display: flex;
|
|
3539
3546
|
justify-content: center;
|
|
3540
3547
|
align-items: flex-start;
|
|
3541
|
-
background: rgb(20
|
|
3542
|
-
|
|
3548
|
+
background: rgb(20 19 18 / 32%);
|
|
3549
|
+
animation: search-fade 0.14s ease-out;
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
html[data-theme="dark"] .search-backdrop {
|
|
3553
|
+
background: rgb(0 0 0 / 55%);
|
|
3543
3554
|
}
|
|
3544
3555
|
|
|
3545
3556
|
.search-panel {
|
|
3546
3557
|
width: min(640px, 100%);
|
|
3547
|
-
max-height:
|
|
3558
|
+
max-height: min(600px, 80vh);
|
|
3559
|
+
display: flex;
|
|
3560
|
+
flex-direction: column;
|
|
3548
3561
|
overflow: hidden;
|
|
3562
|
+
background: var(--paper);
|
|
3563
|
+
border: 1px solid var(--rule);
|
|
3564
|
+
border-radius: 16px;
|
|
3565
|
+
box-shadow: 0 18px 48px rgb(27 26 24 / 16%), 0 2px 8px rgb(27 26 24 / 6%);
|
|
3566
|
+
color: var(--text);
|
|
3567
|
+
animation: search-fade 0.14s ease-out;
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
html[data-theme="dark"] .search-panel {
|
|
3549
3571
|
background: var(--card);
|
|
3550
|
-
|
|
3551
|
-
box-shadow: 0 24px 80px rgb(0 0 0 / 25%);
|
|
3572
|
+
box-shadow: 0 18px 48px rgb(0 0 0 / 45%), 0 2px 8px rgb(0 0 0 / 25%);
|
|
3552
3573
|
}
|
|
3553
3574
|
|
|
3554
3575
|
.search-input-wrap {
|
|
3555
|
-
|
|
3556
|
-
padding:
|
|
3576
|
+
flex: none;
|
|
3577
|
+
padding: 10px 10px 4px 20px;
|
|
3557
3578
|
display: grid;
|
|
3558
|
-
grid-template-columns:
|
|
3579
|
+
grid-template-columns: 20px 1fr 36px;
|
|
3559
3580
|
align-items: center;
|
|
3560
|
-
gap:
|
|
3561
|
-
|
|
3581
|
+
gap: 12px;
|
|
3582
|
+
color: var(--muted);
|
|
3562
3583
|
}
|
|
3563
3584
|
|
|
3564
3585
|
.search-input-wrap input {
|
|
3565
3586
|
width: 100%;
|
|
3566
|
-
|
|
3587
|
+
min-width: 0;
|
|
3588
|
+
height: 48px;
|
|
3589
|
+
padding: 0;
|
|
3567
3590
|
border: 0;
|
|
3568
3591
|
background: transparent;
|
|
3569
3592
|
outline: 0;
|
|
3570
|
-
|
|
3593
|
+
color: var(--ink);
|
|
3594
|
+
caret-color: var(--ink);
|
|
3595
|
+
font: 400 17px / 1.4 var(--sans);
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
.search-input-wrap input::placeholder {
|
|
3599
|
+
color: var(--muted);
|
|
3571
3600
|
}
|
|
3572
3601
|
|
|
3573
3602
|
.search-input-wrap button {
|
|
3574
|
-
|
|
3603
|
+
width: 36px;
|
|
3604
|
+
height: 36px;
|
|
3605
|
+
padding: 0;
|
|
3575
3606
|
display: grid;
|
|
3576
3607
|
place-items: center;
|
|
3577
|
-
background: transparent;
|
|
3578
3608
|
border: 0;
|
|
3609
|
+
border-radius: 10px;
|
|
3610
|
+
background: transparent;
|
|
3611
|
+
color: var(--muted);
|
|
3612
|
+
cursor: pointer;
|
|
3613
|
+
transition: background-color 0.15s ease, color 0.15s ease;
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
.search-input-wrap button:hover {
|
|
3617
|
+
background: var(--tint);
|
|
3618
|
+
color: var(--ink);
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
.search-input-wrap button:focus-visible {
|
|
3622
|
+
border-radius: 10px;
|
|
3579
3623
|
}
|
|
3580
3624
|
|
|
3581
3625
|
.search-results {
|
|
3582
|
-
|
|
3583
|
-
|
|
3626
|
+
flex: 1 1 auto;
|
|
3627
|
+
min-height: 0;
|
|
3628
|
+
padding: 2px 10px 10px;
|
|
3629
|
+
display: grid;
|
|
3630
|
+
align-content: start;
|
|
3631
|
+
gap: 2px;
|
|
3584
3632
|
overflow-y: auto;
|
|
3633
|
+
overscroll-behavior: contain;
|
|
3585
3634
|
}
|
|
3586
3635
|
|
|
3587
3636
|
.search-label {
|
|
3588
|
-
margin:
|
|
3589
|
-
color: var(--
|
|
3590
|
-
font
|
|
3637
|
+
margin: 6px 10px 6px;
|
|
3638
|
+
color: var(--muted);
|
|
3639
|
+
font: 400 var(--fs-meta) / 1.4 var(--sans);
|
|
3591
3640
|
}
|
|
3592
3641
|
|
|
3593
3642
|
.search-result {
|
|
3594
|
-
padding: 12px;
|
|
3643
|
+
padding: 10px 12px;
|
|
3595
3644
|
display: grid;
|
|
3596
|
-
grid-template-columns:
|
|
3597
|
-
|
|
3598
|
-
|
|
3645
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
3646
|
+
align-items: center;
|
|
3647
|
+
gap: 4px 8px;
|
|
3648
|
+
border-radius: 10px;
|
|
3649
|
+
color: var(--text);
|
|
3650
|
+
text-decoration: none;
|
|
3651
|
+
scroll-margin: 8px;
|
|
3652
|
+
transition: background-color 0.12s ease, box-shadow 0.12s ease;
|
|
3599
3653
|
}
|
|
3600
3654
|
|
|
3601
3655
|
.search-result:hover,
|
|
3602
|
-
.search-result:focus-visible,
|
|
3603
3656
|
.search-result.active {
|
|
3604
|
-
background: var(--
|
|
3657
|
+
background: var(--card-hover);
|
|
3658
|
+
box-shadow: var(--card-shadow);
|
|
3659
|
+
text-decoration: none;
|
|
3605
3660
|
}
|
|
3606
3661
|
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3662
|
+
/* Keyboard focus keeps the row's radius and adds a faint ink ring. */
|
|
3663
|
+
.search-result:focus-visible {
|
|
3664
|
+
outline: none;
|
|
3665
|
+
border-radius: 10px;
|
|
3666
|
+
background: var(--card-hover);
|
|
3667
|
+
box-shadow: var(--card-shadow), inset 0 0 0 1px var(--blue-line);
|
|
3611
3668
|
}
|
|
3612
3669
|
|
|
3613
3670
|
.search-result strong {
|
|
3614
|
-
|
|
3671
|
+
grid-column: 1 / -1;
|
|
3672
|
+
grid-row: 1;
|
|
3673
|
+
color: var(--ink);
|
|
3674
|
+
font: 600 15px / 1.5 var(--sans);
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
.search-result > .search-chip {
|
|
3678
|
+
grid-column: 1;
|
|
3679
|
+
grid-row: 2;
|
|
3680
|
+
max-width: 12em;
|
|
3681
|
+
padding: 1px 8px;
|
|
3682
|
+
overflow: hidden;
|
|
3683
|
+
border-radius: 999px;
|
|
3684
|
+
background: var(--chip);
|
|
3685
|
+
color: var(--soft);
|
|
3686
|
+
font: 400 12px / 1.6 var(--sans);
|
|
3687
|
+
text-overflow: ellipsis;
|
|
3688
|
+
white-space: nowrap;
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
.search-result:hover > .search-chip,
|
|
3692
|
+
.search-result.active > .search-chip {
|
|
3693
|
+
background: var(--chip-hover);
|
|
3615
3694
|
}
|
|
3616
3695
|
|
|
3617
3696
|
.search-result small {
|
|
3697
|
+
grid-column: 2;
|
|
3698
|
+
grid-row: 2;
|
|
3699
|
+
min-width: 0;
|
|
3618
3700
|
color: var(--muted);
|
|
3619
3701
|
overflow: hidden;
|
|
3620
|
-
font
|
|
3702
|
+
font: 400 var(--fs-meta) / 1.6 var(--sans);
|
|
3621
3703
|
text-overflow: ellipsis;
|
|
3622
3704
|
white-space: nowrap;
|
|
3623
3705
|
}
|
|
3624
3706
|
|
|
3707
|
+
/* A post in no column has no chip: its excerpt takes the whole line. */
|
|
3708
|
+
.search-result:not(:has(> .search-chip)) small {
|
|
3709
|
+
grid-column: 1 / -1;
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3625
3712
|
.search-match {
|
|
3626
3713
|
padding: 0 1px;
|
|
3714
|
+
border-radius: 3px;
|
|
3627
3715
|
background: var(--selection);
|
|
3628
3716
|
color: var(--ink);
|
|
3629
3717
|
box-decoration-break: clone;
|
|
@@ -3631,19 +3719,86 @@ html[data-theme="dark"] .article-references {
|
|
|
3631
3719
|
}
|
|
3632
3720
|
|
|
3633
3721
|
.empty-state {
|
|
3634
|
-
padding:
|
|
3722
|
+
padding: 32px 12px;
|
|
3635
3723
|
color: var(--muted);
|
|
3636
|
-
font-size:
|
|
3724
|
+
font-size: var(--fs-small);
|
|
3637
3725
|
text-align: center;
|
|
3638
3726
|
}
|
|
3639
3727
|
|
|
3640
3728
|
.search-footer {
|
|
3641
|
-
|
|
3729
|
+
flex: none;
|
|
3730
|
+
padding: 10px 20px;
|
|
3642
3731
|
display: flex;
|
|
3643
3732
|
justify-content: space-between;
|
|
3733
|
+
gap: 16px;
|
|
3734
|
+
border-radius: 0 0 15px 15px;
|
|
3735
|
+
background: var(--card-hover);
|
|
3736
|
+
color: var(--muted);
|
|
3737
|
+
font: 400 12px / 1.5 var(--sans);
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
html[data-theme="dark"] .search-footer {
|
|
3644
3741
|
background: var(--paper-deep);
|
|
3645
|
-
|
|
3646
|
-
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
.search-footer kbd {
|
|
3745
|
+
padding: 1px 6px;
|
|
3746
|
+
border-radius: 6px;
|
|
3747
|
+
background: var(--chip);
|
|
3748
|
+
color: var(--soft);
|
|
3749
|
+
font: inherit;
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
@keyframes search-fade {
|
|
3753
|
+
from {
|
|
3754
|
+
opacity: 0;
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
/* Phones: a sheet from the bottom, nearly full height, rounded on top. */
|
|
3759
|
+
@media (max-width: 640px) {
|
|
3760
|
+
.search-backdrop {
|
|
3761
|
+
padding: 0;
|
|
3762
|
+
align-items: flex-end;
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
.search-panel {
|
|
3766
|
+
width: 100%;
|
|
3767
|
+
height: calc(100dvh - 16px);
|
|
3768
|
+
max-height: none;
|
|
3769
|
+
border-bottom: 0;
|
|
3770
|
+
border-radius: 16px 16px 0 0;
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
.search-input-wrap {
|
|
3774
|
+
padding: 8px 8px 2px 18px;
|
|
3775
|
+
grid-template-columns: 20px 1fr 44px;
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
.search-input-wrap button {
|
|
3779
|
+
width: 44px;
|
|
3780
|
+
height: 44px;
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3783
|
+
.search-results {
|
|
3784
|
+
padding: 2px 8px calc(12px + env(safe-area-inset-bottom));
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
.search-footer {
|
|
3788
|
+
display: none;
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3793
|
+
.search-backdrop,
|
|
3794
|
+
.search-panel {
|
|
3795
|
+
animation: none;
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
.search-result,
|
|
3799
|
+
.search-input-wrap button {
|
|
3800
|
+
transition: none;
|
|
3801
|
+
}
|
|
3647
3802
|
}
|
|
3648
3803
|
|
|
3649
3804
|
.site-footer {
|
package/app/styles/a2.css
CHANGED
|
@@ -62,7 +62,6 @@ body {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
.icon-button,
|
|
65
|
-
.search-panel,
|
|
66
65
|
.column-feature,
|
|
67
66
|
.archive-card,
|
|
68
67
|
.thought-card,
|
|
@@ -806,8 +805,10 @@ html[data-theme="dark"] .markdown-body .markdown-alert code {
|
|
|
806
805
|
}
|
|
807
806
|
|
|
808
807
|
/* A hidden TOC slides just past the right edge; clip it there so it does not
|
|
809
|
-
widen the page. clip (unlike hidden) keeps the sticky TOC working.
|
|
810
|
-
|
|
808
|
+
widen the page. clip (unlike hidden) keeps the sticky TOC working. Not tied
|
|
809
|
+
to data-toc-yield: the attribute goes as the TOC starts sliding back, and
|
|
810
|
+
the page would widen for that 0.3s. */
|
|
811
|
+
#main-content:has(.toc) {
|
|
811
812
|
overflow-x: clip;
|
|
812
813
|
}
|
|
813
814
|
}
|
|
@@ -997,8 +998,9 @@ html[data-theme="dark"] .markdown-body .markdown-alert code {
|
|
|
997
998
|
margin-bottom: 0;
|
|
998
999
|
}
|
|
999
1000
|
|
|
1000
|
-
/* One-line cell 「X:Y」: X sits on its own line like a title, colon kept.
|
|
1001
|
-
.g-cmp .g-cmp-
|
|
1001
|
+
/* One-line cell 「X:Y」: X sits on its own line like a title, colon kept.
|
|
1002
|
+
.g-cmp-t holds the title only; the colon and its spaces are .g-cmp-sep. */
|
|
1003
|
+
.g-cmp .g-cmp-h {
|
|
1002
1004
|
display: block;
|
|
1003
1005
|
margin: 0 0 8px;
|
|
1004
1006
|
font: 600 16px / 1.4 var(--sans);
|
package/app/styles/shell.css
CHANGED
|
@@ -199,7 +199,6 @@ html[data-theme="dark"] .sh-rail{color:var(--muted)}
|
|
|
199
199
|
.sh-prose ul{margin:0;padding-left:1.2em}
|
|
200
200
|
.sh-prose li{margin:.35em 0}
|
|
201
201
|
.sh-prose a{color:var(--blue);text-underline-offset:4px;text-decoration-thickness:1px}
|
|
202
|
-
.sh-note{font-size:var(--fs-meta);color:var(--muted);margin-top:12px}
|
|
203
202
|
|
|
204
203
|
/* ---------- 404 ---------- */
|
|
205
204
|
.sh-404{padding:96px 0 24px;max-width:var(--list)}
|
|
@@ -325,38 +324,10 @@ html[data-theme="dark"] .sh-pal-scrim{background:rgba(0,0,0,.55)}
|
|
|
325
324
|
.sh-notice strong{color:var(--ink)}
|
|
326
325
|
.sh-month > h2{font:700 var(--fs-lead)/1.3 var(--sans);color:var(--ink);margin:40px 0 0;padding:0 0 12px;border-bottom:1px solid var(--rule-strong);display:flex;align-items:baseline;gap:10px}
|
|
327
326
|
.sh-month > h2 .n{font:400 var(--fs-meta)/1 var(--sans);color:var(--muted)}
|
|
328
|
-
.sh-note{padding:28px 0 26px;border-bottom:1px solid var(--rule)}
|
|
329
|
-
.sh-note:target{background:linear-gradient(var(--blue-tint),var(--blue-tint)) no-repeat;background-size:100% 100%;box-shadow:-16px 0 0 var(--blue-tint),16px 0 0 var(--blue-tint)}
|
|
330
|
-
.sh-note h3{margin:0 0 6px;font:700 var(--fs-body)/1.55 var(--sans);color:var(--ink)}
|
|
331
|
-
.sh-note-body p{margin:0 0 .7em;font-size:var(--fs-body);line-height:1.8;color:var(--text)}
|
|
332
|
-
.sh-note-body p:last-child{margin-bottom:0}
|
|
333
|
-
.sh-note-imgs{display:grid;gap:6px;margin:16px 0 0;border-radius:var(--r);overflow:hidden}
|
|
334
|
-
.sh-note-imgs img{display:block;width:100%;height:100%;object-fit:cover;background:var(--well)}
|
|
335
|
-
.sh-note-imgs.one img{aspect-ratio:3/2}
|
|
336
|
-
.sh-note-imgs.two{grid-template-columns:1fr 1fr}.sh-note-imgs.two img{aspect-ratio:1/1}
|
|
337
|
-
.sh-note-imgs.three{grid-template-columns:2fr 1fr;grid-template-rows:1fr 1fr;aspect-ratio:3/2}
|
|
338
|
-
.sh-note-imgs.three img:first-child{grid-row:1 / 3}
|
|
339
|
-
.sh-note-video{display:block;width:100%;aspect-ratio:16/9;margin:16px 0 0;border-radius:var(--r);background:#000;object-fit:cover}
|
|
340
|
-
.sh-note-audio{margin:16px 0 0;padding:12px 14px;border:1px solid var(--rule);border-radius:var(--r);background:var(--card)}
|
|
341
|
-
.sh-note-audio figcaption{font-size:var(--fs-meta);color:var(--muted);margin:0 0 8px}
|
|
342
|
-
.sh-note-audio audio{display:block;width:100%;height:36px}
|
|
343
|
-
.sh-note-link{display:grid;gap:2px;margin:16px 0 0;padding:14px 16px;border:1px solid var(--rule);border-radius:var(--r);background:var(--card);text-decoration:none;transition:background-color .15s ease,box-shadow .15s ease}
|
|
344
|
-
@media (hover:hover){.sh-note-link:hover{background:var(--card-hover);box-shadow:var(--card-shadow)}}
|
|
345
|
-
.sh-note-link .h{font-size:var(--fs-meta);color:var(--muted)}
|
|
346
|
-
.sh-note-link .t{font:600 15px/1.5 var(--sans);color:var(--ink)}
|
|
347
|
-
.sh-note-link .t::after{content:" ↗";color:var(--muted);font-weight:400}
|
|
348
|
-
.sh-note-link .d{font-size:var(--fs-small);line-height:1.6;color:var(--soft)}
|
|
349
|
-
.sh-note footer{display:flex;flex-wrap:wrap;align-items:center;gap:4px 14px;margin-top:14px;font-size:var(--fs-meta);color:var(--muted)}
|
|
350
|
-
.sh-note .when{color:var(--muted);text-decoration:none;font-variant-numeric:tabular-nums;padding:4px 0}
|
|
351
|
-
.sh-note .when:hover{color:var(--blue);text-decoration:underline;text-underline-offset:3px}
|
|
352
|
-
.sh-note .tag{color:var(--soft)}
|
|
353
|
-
.sh-note .ex{font-size:12px;line-height:1;color:var(--muted);border:1px dashed var(--rule-strong);border-radius:6px;padding:3px 6px}
|
|
354
|
-
html[data-theme="dark"] .sh-note-imgs img,html[data-theme="dark"] .sh-note-video{filter:brightness(.92)}
|
|
355
327
|
|
|
356
328
|
@media (max-width:820px){
|
|
357
329
|
.sh-subs{flex-wrap:nowrap;overflow-x:auto;margin:6px -20px 4px;padding:12px 20px 0;scrollbar-width:none}
|
|
358
330
|
.sh-subs::-webkit-scrollbar{display:none}
|
|
359
|
-
.sh-note:target{box-shadow:-20px 0 0 var(--blue-tint),20px 0 0 var(--blue-tint)}
|
|
360
331
|
}
|
|
361
332
|
|
|
362
333
|
|
|
@@ -437,5 +408,5 @@ html[data-theme="dark"] .sh-repos > li.sk::after{background:linear-gradient(90de
|
|
|
437
408
|
|
|
438
409
|
/* Cards answer hover with a warm fill and a faint shadow; nothing moves. */
|
|
439
410
|
@media (prefers-reduced-motion:reduce){
|
|
440
|
-
.sh-col,.sh-
|
|
411
|
+
.sh-col,.sh-repos > li,.sh-tagchip{transition:none}
|
|
441
412
|
}
|
package/package.json
CHANGED
|
@@ -58,8 +58,18 @@ const excerpt = (text, length = EXCERPT_LENGTH) => {
|
|
|
58
58
|
const columnEntryPath = (entry) =>
|
|
59
59
|
entry.column && entry.path.startsWith(`${entry.column}/`) ? entry.path.slice(entry.column.length + 1) : entry.path;
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const entryKey = (item) => item.sourcePath ?? item.path;
|
|
62
|
+
const columnKey = (item) => item.sourcePath ?? columnEntryPath(item);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* One search entry per piece of writing, as `{ item, record }`, in the order
|
|
66
|
+
* the header lists them: posts, then column entries that are not also posts,
|
|
67
|
+
* then thoughts. A post that sits in a column is listed once, at its /blog/
|
|
68
|
+
* URL, with the column's title as its section; a post in no column has an
|
|
69
|
+
* empty section. A column entry is only listed on its own when no post has
|
|
70
|
+
* the same source (a column-only page).
|
|
71
|
+
*/
|
|
72
|
+
function searchEntries({ posts = [], columns = [], thoughts = [] }, { length = EXCERPT_LENGTH } = {}) {
|
|
63
73
|
const record = (item, extra) => {
|
|
64
74
|
const text = searchableText(item.content ?? "");
|
|
65
75
|
return {
|
|
@@ -71,25 +81,37 @@ export function buildSearchIndex({ posts = [], columns = [], thoughts = [] }, {
|
|
|
71
81
|
...extra(text),
|
|
72
82
|
};
|
|
73
83
|
};
|
|
84
|
+
const postKeys = new Set(posts.map(entryKey));
|
|
85
|
+
const columnOf = new Map();
|
|
86
|
+
for (const item of columns) {
|
|
87
|
+
const key = columnKey(item);
|
|
88
|
+
if (postKeys.has(key) && !columnOf.has(key)) columnOf.set(key, item.columnTitle ?? "专栏");
|
|
89
|
+
}
|
|
74
90
|
return [
|
|
75
|
-
...posts.map((item) => record(item, () => ({ section: "
|
|
76
|
-
...columns
|
|
77
|
-
|
|
91
|
+
...posts.map((item) => ({ item, record: record(item, () => ({ section: columnOf.get(entryKey(item)) ?? "", path: `/blog/${item.path}` })) })),
|
|
92
|
+
...columns
|
|
93
|
+
.filter((item) => !postKeys.has(columnKey(item)))
|
|
94
|
+
.map((item) => ({ item, record: record(item, () => ({ section: item.columnTitle ?? "专栏", path: `/columns/${item.column}/${columnEntryPath(item)}` })) })),
|
|
95
|
+
...thoughts.map((item) => ({ item, record: record(item, (text) => ({ section: "随想", path: `/thoughts/${item.slug}`, description: excerpt(text, length) })) })),
|
|
78
96
|
];
|
|
79
97
|
}
|
|
80
98
|
|
|
99
|
+
/** The small index: one record per piece of writing (see searchEntries). */
|
|
100
|
+
export function buildSearchIndex(content, options) {
|
|
101
|
+
return searchEntries(content, options).map(({ record }) => record);
|
|
102
|
+
}
|
|
103
|
+
|
|
81
104
|
/**
|
|
82
105
|
* The full-text file: `{ hash, texts, docs }`, where docs is
|
|
83
|
-
* `[[path, textIndex], ...]` in the small index's order.
|
|
84
|
-
*
|
|
106
|
+
* `[[path, textIndex], ...]` in the small index's order. Identical bodies
|
|
107
|
+
* are stored once.
|
|
85
108
|
* The hash is of the contents, so the file name changes exactly when they do.
|
|
86
109
|
*/
|
|
87
110
|
export function buildFullIndex({ posts = [], columns = [], thoughts = [] }) {
|
|
88
|
-
const items = [...posts, ...columns, ...thoughts];
|
|
89
111
|
const texts = [];
|
|
90
112
|
const seen = new Map();
|
|
91
|
-
const docs =
|
|
92
|
-
const text = fullText(
|
|
113
|
+
const docs = searchEntries({ posts, columns, thoughts }).map(({ item, record }) => {
|
|
114
|
+
const text = fullText(item.content ?? "");
|
|
93
115
|
if (!seen.has(text)) {
|
|
94
116
|
seen.set(text, texts.length);
|
|
95
117
|
texts.push(text);
|
|
@@ -55,9 +55,10 @@ export function createSearch(records, bodies = null) {
|
|
|
55
55
|
};
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
/** The best `limit` hits, plus how many records match in all. */
|
|
59
|
+
function find(query, limit = 6) {
|
|
59
60
|
const terms = queryTerms(query);
|
|
60
|
-
if (!terms.length) return [];
|
|
61
|
+
if (!terms.length) return { hits: [], total: 0 };
|
|
61
62
|
const hits = [];
|
|
62
63
|
for (const doc of docs) {
|
|
63
64
|
let score = 0;
|
|
@@ -89,8 +90,10 @@ export function createSearch(records, bodies = null) {
|
|
|
89
90
|
hits.push({ score, order: doc.order, item: doc.item, matches });
|
|
90
91
|
}
|
|
91
92
|
hits.sort((a, b) => b.score - a.score || a.order - b.order);
|
|
92
|
-
return hits.slice(0, limit).map(({ item, matches }) => ({ item, matches }));
|
|
93
|
+
return { hits: hits.slice(0, limit).map(({ item, matches }) => ({ item, matches })), total: hits.length };
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
const search = (query, limit = 6) => find(query, limit).hits;
|
|
97
|
+
|
|
98
|
+
return { full: Boolean(bodies), find, search };
|
|
96
99
|
}
|