hd-garden 0.9.0 → 0.9.2

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.
@@ -6,7 +6,10 @@
6
6
  .bookshelf-demo .cover.is-extracting:hover,
7
7
  .bookshelf-demo .spine.is-extracting,
8
8
  .bookshelf-demo .spine.is-extracting:hover,
9
- .bookshelf-demo .spine.is-extracting.is-expanded {
9
+ .bookshelf-demo .spine.is-extracting.is-expanded,
10
+ .bookshelf-demo .spine.is-seat,
11
+ .bookshelf-demo .spine.is-seat:hover,
12
+ .bookshelf-demo .spine.is-seat:focus-visible {
10
13
  transform: none !important;
11
14
  box-shadow: none !important;
12
15
  transition: none !important;
@@ -232,18 +235,22 @@
232
235
  z-index: 2;
233
236
  }
234
237
  }
235
- .bookshelf-demo .cover.is-placeholder {
236
- /* ux13+ux24: invisible seat — no gray/wood/bk plate while flyer is out */
238
+ .bookshelf-demo .cover.is-placeholder,
239
+ .bookshelf-demo .spine.is-placeholder {
240
+ /* Empty seat while the flyer owns the motion: shelf surface only.
241
+ A bk-* fill here is the gray/lavender plate behind the real cover. */
237
242
  opacity: 0 !important;
238
243
  visibility: hidden !important;
239
244
  background-color: transparent !important;
240
245
  background-image: none !important;
241
- filter: none;
246
+ filter: none !important;
242
247
  pointer-events: none;
243
- box-shadow: none;
248
+ box-shadow: none !important;
249
+ color: transparent !important;
244
250
  transform: none !important;
245
251
  }
246
- .bookshelf-demo .cover.is-placeholder > .cover-art {
252
+ .bookshelf-demo .cover.is-placeholder > .cover-art,
253
+ .bookshelf-demo .spine.is-placeholder > .cover-art {
247
254
  visibility: hidden !important;
248
255
  opacity: 0 !important;
249
256
  }
@@ -339,15 +346,7 @@
339
346
  z-index: 2;
340
347
  }
341
348
  }
342
- .bookshelf-demo .spine.is-placeholder {
343
- /* ux13: invisible seat — ban gray ghost plate */
344
- opacity: 0;
345
- filter: none;
346
- pointer-events: none;
347
- box-shadow: none;
348
- transform: none !important;
349
- }
350
- .bookshelf-demo .spine.is-placeholder span { opacity: 0; }
349
+ .bookshelf-demo .spine.is-placeholder span { opacity: 0 !important; visibility: hidden !important; }
351
350
  .bookshelf-demo .cover.is-placeholder.is-landing,
352
351
  .bookshelf-demo .spine.is-placeholder.is-landing {
353
352
  opacity: 0;
@@ -356,6 +355,25 @@
356
355
 
357
356
  /* Mode C: temporary expand on spine wall */
358
357
  /* resting spines: solid hue only; cover image only when expanded */
358
+ /* Expanded / extracting face must not fall back to bk-* (#513688 etc.).
359
+ The jacket is .cover-art. A missing image stays cream shelf, not a shell pad. */
360
+ .bookshelf-demo .spine.is-expanded,
361
+ .bookshelf-demo .spine.is-extracting {
362
+ background-color: transparent !important;
363
+ }
364
+ .bookshelf-demo .spine.is-expanded > .cover-art,
365
+ .bookshelf-demo .spine.is-extracting > .cover-art {
366
+ position: absolute;
367
+ inset: 0;
368
+ width: 100%;
369
+ height: 100%;
370
+ object-fit: contain;
371
+ object-position: center top;
372
+ display: block;
373
+ pointer-events: none;
374
+ z-index: 1;
375
+ border-radius: inherit;
376
+ }
359
377
  .bookshelf-demo .spine[class*="img-"] {
360
378
  background-image: none !important;
361
379
  }
@@ -556,15 +574,32 @@
556
574
  align-items: flex-start;
557
575
  margin-bottom: 36px;
558
576
  }
577
+ .bookshelf-demo .note-meta {
578
+ min-width: 0;
579
+ flex: 1 1 auto;
580
+ }
581
+ /* Settled note header: size the box to the picture. A fixed 2:3 frame with
582
+ background-size:cover center-crops landscape heroes (Obsidian → "sid"). */
559
583
  .bookshelf-demo .note-cover {
560
- width: 120px;
561
- height: 180px;
562
- flex-shrink: 0;
584
+ width: max-content;
585
+ max-width: min(420px, 46%);
586
+ height: auto;
587
+ flex: 0 0 auto;
563
588
  border-radius: 2px 3px 0 0;
564
- background-size: cover;
565
- background-position: center top;
566
- background-color: #2a2a2a;
589
+ background-color: transparent;
590
+ background-image: none;
567
591
  box-shadow: 0 8px 20px rgba(40,30,20,0.12);
592
+ line-height: 0;
593
+ }
594
+ .bookshelf-demo .note-cover-img {
595
+ display: block;
596
+ width: auto;
597
+ height: auto;
598
+ max-width: 100%;
599
+ max-height: 180px;
600
+ object-fit: contain;
601
+ object-position: center;
602
+ border-radius: inherit;
568
603
  }
569
604
  .bookshelf-demo .note-cat {
570
605
  margin: 0 0 8px;
@@ -674,7 +709,7 @@
674
709
  contain: layout style;
675
710
  }
676
711
  .bookshelf-demo .flyer.is-ready {
677
- will-change: transform, opacity;
712
+ will-change: translate, scale, opacity;
678
713
  }
679
714
 
680
715
  /* Soft oval foot shadow — no filter:blur (blur+anim = jitter) */
@@ -710,21 +745,16 @@
710
745
  backface-visibility: hidden;
711
746
  }
712
747
 
713
- /* Thin cover board peeking under page block (thickness cue) */
748
+ /* ux27 plate ban: this used to be a full-spread shell (fallback #6a7a98 / #5a6a88,
749
+ and shell-hi of dark covers reads the same gray-lavender). It sat wider than
750
+ the jacket during lift and put-back. Fore-edge stays on .flyer-thickness. */
714
751
  .bookshelf-demo .flyer-board {
715
- position: absolute;
716
- inset: -2% -1.2% -3.5% -1.2%;
717
- border-radius: 3px 4px 2px 2px;
718
- /* ux26: shell = this book's cover dominant (via --shell); ban site-wide brown */
719
- background: linear-gradient(
720
- 180deg,
721
- var(--shell-hi, #6a7a98) 0%,
722
- var(--shell, #5a6a88) 55%,
723
- var(--shell-lo, #3a4a68) 100%
724
- );
725
- box-shadow: 0 1px 0 rgba(255,255,255,0.06) inset;
726
- transform: translateZ(-8px);
727
- z-index: 0;
752
+ display: none !important;
753
+ background: none !important;
754
+ background-color: transparent !important;
755
+ box-shadow: none !important;
756
+ opacity: 0 !important;
757
+ visibility: hidden !important;
728
758
  }
729
759
 
730
760
  .bookshelf-demo .flyer-leaf {
@@ -756,7 +786,7 @@
756
786
  /* Cover on BACK of left leaf — camera-facing when leaf folded (180°) */
757
787
  .bookshelf-demo .flyer-face-cover {
758
788
  transform: rotateY(180deg);
759
- background-color: transparent; /* ux24: no preset wood plate */
789
+ background-color: transparent !important; /* ux24/ux27: bk-* must not fill behind the img */
760
790
  /* No background-image here — .img-* / inline applyCoverImage owns the photo.
761
791
  Gradients-only used to override img classes → flat brown/gray close flash. */
762
792
  box-shadow:
@@ -779,17 +809,18 @@
779
809
  }
780
810
  .bookshelf-demo .flyer-spine-label { pointer-events: none; }
781
811
 
782
- /* ux15 plate kept inert; ux16 uses full-height .flyer-spine-slot instead */
812
+ /* ux15 plate kept inert; ux16 uses full-height .flyer-spine-slot instead.
813
+ Never paint the lavender shell fallback if a leftover plate node is mounted. */
783
814
  .bookshelf-demo .flyer-spine-land-plate {
784
815
  position: absolute;
785
816
  inset: 0;
786
817
  pointer-events: none;
787
818
  border-radius: inherit;
788
819
  z-index: 1;
789
- display: none;
790
- }
791
- .bookshelf-demo .flyer-spine-land-plate:not([class*="bk-"]) {
792
- background: linear-gradient(180deg, var(--shell-hi, #6a7a98) 0%, var(--shell, #5a6a88) 100%);
820
+ display: none !important;
821
+ background: none !important;
822
+ background-color: transparent !important;
823
+ opacity: 0 !important;
793
824
  }
794
825
 
795
826
  /* ux16: exact homeRect standing spine — full height, spine width, slides out/in */
@@ -999,6 +1030,18 @@
999
1030
  overflow: auto;
1000
1031
  }
1001
1032
 
1033
+ /* Close: flyer owns the motion. The note used to stay at z-index 75, so the
1034
+ article sat on top of the return for the whole fade and the shrink was hidden. */
1035
+ .bookshelf-demo.is-return .stage { z-index: 80; }
1036
+ .bookshelf-demo.is-return .note-view,
1037
+ .bookshelf-demo.is-return .note-view.is-preparing,
1038
+ .bookshelf-demo.is-return .note-view.is-open { z-index: 60; }
1039
+ .bookshelf-demo.is-site.is-return .stage { z-index: 36; }
1040
+ .bookshelf-demo.is-site.is-return .open-backdrop { z-index: 30; }
1041
+ .bookshelf-demo.is-site.is-return .note-view,
1042
+ .bookshelf-demo.is-site.is-return .note-view.is-preparing,
1043
+ .bookshelf-demo.is-site.is-return .note-view.is-open { z-index: 31; }
1044
+
1002
1045
  .bookshelf-demo .shelf-view.is-dim {
1003
1046
  /* Backdrop covers shelf — dim is unused during open; keep inert */
1004
1047
  opacity: 1;
@@ -1056,7 +1099,7 @@
1056
1099
  inset: 0;
1057
1100
  border-radius: inherit;
1058
1101
  overflow: hidden;
1059
- background-color: transparent; /* ux24: no preset wood plate */
1102
+ background-color: transparent !important; /* ux24/ux27: no solid shell behind the img */
1060
1103
  background-size: cover;
1061
1104
  background-position: center top;
1062
1105
  background-repeat: no-repeat;
@@ -1159,9 +1202,133 @@
1159
1202
  .bookshelf-demo .spine.is-expanded.is-collapsing.s-mid { width: 24px; height: 134px; }
1160
1203
  .bookshelf-demo .spine.is-expanded.is-collapsing.s-short { width: 20px; height: 120px; }
1161
1204
  .bookshelf-demo .note-hero { flex-direction: column; gap: 16px; }
1162
- .bookshelf-demo .note-cover { width: 100px; height: 150px; }
1205
+ .bookshelf-demo .note-cover { width: max-content; max-width: 100%; }
1206
+ .bookshelf-demo .note-cover-img {
1207
+ width: auto;
1208
+ max-width: 100%;
1209
+ max-height: 180px;
1210
+ height: auto;
1211
+ }
1163
1212
  .bookshelf-demo .qa-hint { width: 100%; margin-left: 0; margin-top: 4px; }
1164
1213
  }
1165
1214
 
1166
1215
  /* Reading path: hide demo/QA chrome unless ?debug=1 */
1167
1216
  body:not(.debug-on) .bookshelf-demo .is-debug { display: none !important; }
1217
+
1218
+ /* Site shelf: same column, type, and header as the blog. Motion rules above stay. */
1219
+ .bookshelf-page.sh-main {
1220
+ display: flex;
1221
+ flex-direction: column;
1222
+ align-items: center;
1223
+ }
1224
+ .bookshelf-page .sh-head {
1225
+ width: 100%;
1226
+ max-width: var(--col);
1227
+ margin-inline: auto;
1228
+ }
1229
+ .bookshelf-page .sh-kicker {
1230
+ margin: 0 0 12px;
1231
+ font-size: var(--fs-meta);
1232
+ letter-spacing: 0.08em;
1233
+ color: var(--muted);
1234
+ }
1235
+ .bookshelf-demo.is-site {
1236
+ --site-sans: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Segoe UI", system-ui, sans-serif;
1237
+ width: 100%;
1238
+ max-width: var(--col);
1239
+ margin-inline: auto;
1240
+ background: transparent;
1241
+ min-height: 0;
1242
+ color: inherit;
1243
+ font-family: var(--site-sans);
1244
+ font-size: inherit;
1245
+ line-height: inherit;
1246
+ }
1247
+ .bookshelf-demo.is-site .page {
1248
+ width: 100%;
1249
+ max-width: none;
1250
+ margin: 0;
1251
+ padding: 0 0 8px;
1252
+ }
1253
+ .bookshelf-demo.is-site .top { display: none; }
1254
+ .bookshelf-page .sh-head h1,
1255
+ .bookshelf-page .sh-deck,
1256
+ .bookshelf-page .sh-kicker,
1257
+ body:has(.bookshelf-demo.is-site) .sh-header,
1258
+ body:has(.bookshelf-demo.is-site) .sh-footer {
1259
+ font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Segoe UI", system-ui, sans-serif;
1260
+ }
1261
+ body:has(.bookshelf-demo.is-site) .sh-header .sh-word {
1262
+ font-family: "Songti SC", "STSong", "Source Serif 4", Georgia, serif;
1263
+ }
1264
+ /* Overlays stay under the sticky header so open/close does not cover it. */
1265
+ .bookshelf-demo.is-site .open-backdrop {
1266
+ top: var(--hdr);
1267
+ z-index: 30;
1268
+ background: var(--paper);
1269
+ }
1270
+ .bookshelf-demo.is-site .stage { z-index: 32; }
1271
+ .bookshelf-demo.is-site .note-view {
1272
+ top: var(--hdr);
1273
+ background: var(--paper);
1274
+ color: var(--text);
1275
+ }
1276
+ .bookshelf-demo.is-site .note-view.is-preparing,
1277
+ .bookshelf-demo.is-site .note-view.is-open { z-index: 34; }
1278
+ .bookshelf-demo.is-site .open-veil { z-index: 36; }
1279
+ .bookshelf-demo.is-site .note {
1280
+ width: min(100%, var(--col));
1281
+ margin: 0 auto;
1282
+ padding: 28px 0 80px;
1283
+ }
1284
+ .bookshelf-demo.is-site .back-btn,
1285
+ .bookshelf-demo.is-site .note-hero,
1286
+ .bookshelf-demo.is-site .note-body,
1287
+ .bookshelf-demo.is-site .note-read {
1288
+ max-width: none;
1289
+ font-family: var(--site-sans);
1290
+ }
1291
+ .bookshelf-demo.is-site .note-body {
1292
+ font-size: var(--fs-body);
1293
+ line-height: 1.75;
1294
+ color: var(--text);
1295
+ }
1296
+ .bookshelf-demo.is-site .note-body h2 {
1297
+ font-family: inherit;
1298
+ font-size: var(--fs-h2);
1299
+ font-weight: 700;
1300
+ line-height: 1.3;
1301
+ color: var(--ink);
1302
+ }
1303
+ .bookshelf-demo.is-site .note-meta h1 {
1304
+ font-family: inherit;
1305
+ font-weight: 700;
1306
+ font-size: var(--fs-display);
1307
+ line-height: 1.2;
1308
+ color: var(--ink);
1309
+ }
1310
+ .bookshelf-demo .flyer-page-more {
1311
+ pointer-events: auto;
1312
+ align-self: flex-start;
1313
+ margin-top: 8px;
1314
+ font-family: system-ui, sans-serif;
1315
+ font-size: 9px;
1316
+ font-weight: 600;
1317
+ letter-spacing: 0.04em;
1318
+ color: var(--ink, #2a261e);
1319
+ text-decoration: underline;
1320
+ text-underline-offset: 2px;
1321
+ }
1322
+ .bookshelf-demo .note-read {
1323
+ display: inline-block;
1324
+ margin: 12px 0 0;
1325
+ font-size: 14px;
1326
+ color: var(--ink);
1327
+ text-decoration: underline;
1328
+ text-underline-offset: 3px;
1329
+ }
1330
+ .bookshelf-demo .note-read[hidden] { display: none; }
1331
+ .bookshelf-demo.is-site .note-read { color: var(--blue); }
1332
+ @media (max-width: 820px) {
1333
+ .bookshelf-demo.is-site .note { padding: 16px 20px 64px; }
1334
+ }
@@ -0,0 +1,116 @@
1
+ import { marked } from "marked";
2
+ import { BookshelfConfigError } from "./schema.mjs";
3
+
4
+ /**
5
+ * Site shelf for `<Bookshelf catalog notes />`.
6
+ * Built only from `content/bookshelf.yaml` and the posts it names.
7
+ * The package demo is a separate path: this module never reads it.
8
+ *
9
+ * `post` is a `content/posts` path without `.md`. Its front matter description
10
+ * leads the note HTML so the open book's right page is that summary.
11
+ * A cover file name is published at `/media/covers/<file>` (same stem cleanup
12
+ * as content sync). A full URL or a path starting with `/` is kept.
13
+ */
14
+
15
+ const WIDGET_FENCE = /```(?:garden|ui-error|ui|intent|widget)(?:[ \t]+[^\n]*)?\r?\n[\s\S]*?```/gu;
16
+
17
+ export function resolveBookshelfCover(cover, where = "cover") {
18
+ if (!cover?.trim()) return undefined;
19
+ const value = cover.trim();
20
+ if (/^(?:[a-z][a-z0-9+.-]*:)?\/\//iu.test(value) || value.startsWith("/") || value.startsWith("data:")) {
21
+ return value;
22
+ }
23
+ let relative = value.replace(/^\.\//u, "");
24
+ if (relative.startsWith("covers/")) relative = relative.slice("covers/".length);
25
+ const parts = relative.split("/");
26
+ if (!relative || parts.some((segment) => segment === "" || segment === "." || segment === "..")) {
27
+ throw new BookshelfConfigError("cover", `${where} 的 cover 必须是文件名或完整 URL。`);
28
+ }
29
+ return `/media/covers/${parts.map(publishCoverSegment).join("/")}`;
30
+ }
31
+
32
+ /** `content/posts` path, without `.md`. Rejects anything that leaves that directory. */
33
+ export function normalizeBookshelfPostPath(value) {
34
+ const relative = value
35
+ .trim()
36
+ .replaceAll("\\", "/")
37
+ .replace(/^(?:content\/)?posts\//u, "")
38
+ .replace(/\.md$/u, "");
39
+ if (
40
+ !relative
41
+ || relative.startsWith("/")
42
+ || relative.split("/").some((segment) => segment === "" || segment === "." || segment === "..")
43
+ ) {
44
+ throw new BookshelfConfigError("post", `post「${value}」必须是 content/posts 下的路径,不带 .md。`);
45
+ }
46
+ return relative;
47
+ }
48
+
49
+ export function bookshelfMountFromFile(file, posts) {
50
+ const byPath = new Map(posts.map((post) => [normalizeBookshelfPostPath(post.path), post]));
51
+ const notes = {};
52
+ const catalog = file.shelves.map((shelf) => ({
53
+ id: shelf.id,
54
+ name: shelf.name,
55
+ books: shelf.books.map((book) => toCatalogBook(book, byPath, notes)),
56
+ }));
57
+ return {
58
+ title: file.title,
59
+ description: file.description,
60
+ eyebrow: file.eyebrow,
61
+ catalog,
62
+ notes,
63
+ };
64
+ }
65
+
66
+ function toCatalogBook(book, byPath, notes) {
67
+ const post = book.post ? byPath.get(normalizeBookshelfPostPath(book.post)) : undefined;
68
+ if (book.post && !post) {
69
+ throw new BookshelfConfigError("post", `书架引用的博文 ${book.post} 没有读到。`);
70
+ }
71
+ const cover = resolveBookshelfCover(book.cover, book.id) ?? (post ? resolveBookshelfCover(post.cover, book.id) : undefined);
72
+ const body = post
73
+ ? renderNoteHtml(post.content, post.description)
74
+ : book.note
75
+ ? renderNoteHtml(book.note)
76
+ : "";
77
+ const note = body ? book.id : null;
78
+ if (note) notes[book.id] = { title: book.title, body, summary: post?.description };
79
+ return {
80
+ id: book.id,
81
+ title: book.title,
82
+ author: book.author ?? "",
83
+ spine: book.spine ?? book.title,
84
+ size: book.size ?? (book.filler ? "thin" : "s-mid"),
85
+ cover: cover ?? false,
86
+ color: book.color,
87
+ filler: book.filler,
88
+ post: book.post ? normalizeBookshelfPostPath(book.post) : undefined,
89
+ note,
90
+ };
91
+ }
92
+
93
+ function renderNoteHtml(markdown, description) {
94
+ const prose = markdown.replace(WIDGET_FENCE, "\n").trim();
95
+ const parsed = prose ? marked.parse(prose, { async: false }).trim() : "";
96
+ const summary = description?.trim();
97
+ if (!summary || prose.includes(summary)) return parsed;
98
+ return `<p>${escapeHtml(summary)}</p>${parsed ? `\n${parsed}` : ""}`;
99
+ }
100
+
101
+ function publishCoverSegment(segment, index, all) {
102
+ const isFile = index === all.length - 1;
103
+ if (!isFile) return encodeURIComponent(segment.replace(/[^\w\u3400-\u9fff.-]+/gu, "-"));
104
+ const dot = segment.lastIndexOf(".");
105
+ const ext = dot >= 0 ? segment.slice(dot).toLowerCase() : "";
106
+ const stem = dot >= 0 ? segment.slice(0, dot) : segment;
107
+ return encodeURIComponent(`${stem.replace(/[^\w\u3400-\u9fff.-]+/gu, "-")}${ext}`);
108
+ }
109
+
110
+ function escapeHtml(value) {
111
+ return value
112
+ .replaceAll("&", "&amp;")
113
+ .replaceAll("<", "&lt;")
114
+ .replaceAll(">", "&gt;")
115
+ .replaceAll('"', "&quot;");
116
+ }
@@ -0,0 +1,23 @@
1
+ import type { BookshelfNote, BookshelfShelf } from "./types";
2
+
3
+ /**
4
+ * Site shelf ready for `<Bookshelf catalog notes />`.
5
+ * The runtime lives in `catalog.mjs` (node tests and the page share it).
6
+ * The package demo is not an input.
7
+ */
8
+ export type BookshelfMount = {
9
+ title: string;
10
+ description?: string;
11
+ eyebrow?: string;
12
+ catalog: BookshelfShelf[];
13
+ notes: Record<string, BookshelfNote>;
14
+ };
15
+
16
+ /** A post the shelf can turn into a note. `path` is relative to `content/posts`, without `.md`. */
17
+ export type BookshelfPostSource = {
18
+ path: string;
19
+ title?: string;
20
+ description?: string;
21
+ cover?: string;
22
+ content: string;
23
+ };