@dogsbay/docs-layout 0.2.0-beta.94 → 0.2.0-beta.95

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/docs-layout",
3
- "version": "0.2.0-beta.94",
3
+ "version": "0.2.0-beta.95",
4
4
  "description": "Standard documentation layout components for Dogsbay",
5
5
  "type": "module",
6
6
  "exports": {
@@ -30,8 +30,8 @@
30
30
  "./json-ld": "./src/json-ld.ts"
31
31
  },
32
32
  "dependencies": {
33
- "@dogsbay/primitives": "0.2.0-beta.94",
34
- "@dogsbay/ui": "0.2.0-beta.94"
33
+ "@dogsbay/primitives": "0.2.0-beta.95",
34
+ "@dogsbay/ui": "0.2.0-beta.95"
35
35
  },
36
36
  "devDependencies": {
37
37
  "happy-dom": "^20.10.6",
@@ -14,6 +14,8 @@
14
14
  * docs, so there is no blog palette, no blog typography scale, and no
15
15
  * second set of card styles to keep in sync.
16
16
  */
17
+ import Card from "@dogsbay/ui/card/Card.astro";
18
+
17
19
  interface BlogPostRef {
18
20
  slug: string;
19
21
  title: string;
@@ -78,65 +80,68 @@ const hrefForPage = (n: number): string => (n === 1 ? `${base}/` : `${base}/page
78
80
  <p class="text-muted-foreground">No posts yet.</p>
79
81
  )}
80
82
 
81
- <ul class="not-prose flex flex-col gap-8" data-blog-index>
83
+ {/*
84
+ Three across at lg, two at sm, one on mobile. The card is one link, not
85
+ a card containing links: nesting interactive elements inside a clickable
86
+ card is the classic keyboard-and-screen-reader trap, so tags and other
87
+ links stay off the card and live on the post page instead.
88
+
89
+ `items-stretch` + `h-full` keeps a short post's card the same height as
90
+ a long one's — ragged card bottoms are the thing that makes a grid look
91
+ broken rather than sparse.
92
+ */}
93
+ <ul
94
+ class="not-prose grid list-none grid-cols-1 items-stretch gap-6 p-0 sm:grid-cols-2 lg:grid-cols-3"
95
+ data-blog-index
96
+ >
82
97
  {posts.map((post) => (
83
- <li class="group">
84
- <article class="flex flex-col gap-2">
85
- {post.heroImage && (
86
- <a href={post.url} tabindex="-1" aria-hidden="true">
87
- {/*
88
- aspect-boxed for the same reason as the post hero: an
89
- unsized image in a list shifts every card below it.
90
- */}
98
+ <li class="m-0 p-0">
99
+ <a
100
+ href={post.url}
101
+ class="group block h-full rounded-lg no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
102
+ >
103
+ <Card class="flex h-full flex-col gap-3 p-5 transition-colors group-hover:border-ring">
104
+ {post.heroImage && (
91
105
  <img
92
106
  src={post.heroImage}
93
107
  alt=""
94
- class="mb-2 w-full rounded-lg border border-border object-cover aspect-[2/1]"
108
+ class="aspect-[16/9] w-full rounded-md border border-border object-cover"
95
109
  loading="lazy"
96
110
  decoding="async"
97
111
  />
98
- </a>
99
- )}
112
+ )}
100
113
 
101
- <h2 class="text-xl font-semibold tracking-tight">
102
- <a
103
- href={post.url}
104
- class="text-foreground no-underline hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
105
- >
114
+ <h2 class="text-lg font-semibold leading-snug tracking-tight text-foreground group-hover:underline">
106
115
  {post.title}
107
- </a>
108
- </h2>
116
+ </h2>
109
117
 
110
- {/*
111
- Byline mirrors the post page's, minus the reading estimate's
112
- separator noise. data-pagefind-ignore because the same values
113
- are indexed as structured filters on the post itself; without
114
- it, every card's date leads its search excerpt.
115
- */}
116
- <div
117
- class="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-muted-foreground"
118
- data-pagefind-ignore
119
- >
120
- {post.author && post.author.length > 0 && (
121
- <span class="font-medium text-foreground">{post.author.join(", ")}</span>
122
- )}
123
- {post.author && post.author.length > 0 && post.date && (
124
- <span aria-hidden="true">&middot;</span>
125
- )}
126
- {post.date && <time datetime={post.date}>{formatDate(post.date)}</time>}
127
118
  {/*
128
- Guarded like the post page's byline. Unconditional, a post with
129
- neither author nor date opened with a stray 5 min read", and
130
- the card disagreed with the page it linked to.
119
+ data-pagefind-ignore: the same values are indexed as structured
120
+ filters on the post itself. Without it every card's date leads
121
+ its search excerpt.
131
122
  */}
132
- {(post.author?.length || post.date) && <span aria-hidden="true">&middot;</span>}
133
- <span>{post.readingMinutes} min read</span>
134
- </div>
123
+ <div
124
+ class="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground"
125
+ data-pagefind-ignore
126
+ >
127
+ {post.author && post.author.length > 0 && (
128
+ <span class="font-medium text-foreground">{post.author.join(", ")}</span>
129
+ )}
130
+ {post.author && post.author.length > 0 && post.date && (
131
+ <span aria-hidden="true">&middot;</span>
132
+ )}
133
+ {post.date && <time datetime={post.date}>{formatDate(post.date)}</time>}
134
+ {(post.author?.length || post.date) && <span aria-hidden="true">&middot;</span>}
135
+ <span>{post.readingMinutes} min read</span>
136
+ </div>
135
137
 
136
- {post.description && (
137
- <p class="text-muted-foreground">{post.description}</p>
138
- )}
139
- </article>
138
+ {post.description && (
139
+ <p class="m-0 text-sm leading-relaxed text-muted-foreground">
140
+ {post.description}
141
+ </p>
142
+ )}
143
+ </Card>
144
+ </a>
140
145
  </li>
141
146
  ))}
142
147
  </ul>
@@ -432,6 +432,18 @@ interface Props {
432
432
  readingMinutes?: number;
433
433
  /** Optional hero image URL, rendered above the prose. */
434
434
  heroImage?: string;
435
+ /**
436
+ * Series position, when the post belongs to one. Derived at emit time
437
+ * from publication order, so "Part 3 of 7" cannot disagree with reality
438
+ * the way a hand-typed line does.
439
+ */
440
+ series?: {
441
+ name: string;
442
+ part: number;
443
+ total: number;
444
+ /** URL of part 1, so a reader landing mid-series can start over. */
445
+ startHref?: string;
446
+ };
435
447
  /**
436
448
  * Table-of-contents placement. Default `"top"`.
437
449
  * - `"top"` — expandable "On this page" disclosure at the top of the
@@ -521,6 +533,7 @@ const {
521
533
  updatedDate,
522
534
  readingMinutes,
523
535
  heroImage,
536
+ series,
524
537
  linkIcons,
525
538
  class: className,
526
539
  } = Astro.props;
@@ -1155,6 +1168,34 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
1155
1168
  </div>
1156
1169
  )}
1157
1170
 
1171
+ {/*
1172
+ Series banner. Sits above the byline because it is the
1173
+ first thing a reader arriving from search needs: which
1174
+ series is this, where am I in it, and where does it start.
1175
+ A blog index is chronological, so someone can easily meet
1176
+ part 7 first.
1177
+ */}
1178
+ {chrome === "blog" && series && (
1179
+ <div
1180
+ class="not-prose mb-4 flex flex-wrap items-center gap-x-2 gap-y-1 rounded-md border border-border bg-muted/40 px-3 py-2 text-sm"
1181
+ data-post-series
1182
+ data-pagefind-ignore
1183
+ >
1184
+ <span class="font-medium text-foreground">
1185
+ Part {series.part} of {series.total}
1186
+ </span>
1187
+ <span class="text-muted-foreground">in {series.name}</span>
1188
+ {series.startHref && series.part !== 1 && (
1189
+ <a
1190
+ href={series.startHref}
1191
+ class="ml-auto text-foreground underline underline-offset-2"
1192
+ >
1193
+ Start at part 1
1194
+ </a>
1195
+ )}
1196
+ </div>
1197
+ )}
1198
+
1158
1199
  {autoLede && description && (
1159
1200
  <p class="text-lg text-muted-foreground mb-6">
1160
1201
  {description}