@half-built/astro 0.8.0 → 0.10.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/README.md CHANGED
@@ -57,20 +57,37 @@ consumer config itself.
57
57
 
58
58
  ## WhenPublished and PostLink
59
59
 
60
- `content/WhenPublished.astro` renders its children only when the post
61
- at `slug` is visible, so a published post can tease one still in
62
- draft and the passage appears on its own when the target ships.
63
60
  `content/PostLink.astro` links to a post by slug and resolves the
64
61
  href at build time through `postPath`, so a re-dated post does not
65
- strand the links pointing at it. Both take the consumer's collection
66
- as the `posts` prop (the full collection, drafts included, so an
67
- unknown slug can throw instead of hiding as "still a draft");
68
- `WhenPublished` also takes the consumer's draft gate as `showDrafts`,
69
- and `PostLink` accepts an optional `tip` carried as `data-tooltip` for
70
- the link-tip island. A consumer wraps each in a one-line site
71
- component that injects `getCollection` and its own gate, the way the
72
- blog does. The resolvers live in `lib/drafts.ts` for consumers that
73
- want the logic without the components.
62
+ strand the links pointing at it. While the target is hidden (a draft, with the
63
+ draft gate off) it renders its text alone, with no element and no
64
+ styling, and becomes a link on its own when the post ships. That makes
65
+ it the right tool for a draft's name in published prose.
66
+
67
+ `content/WhenPublished.astro` renders its children only when the post
68
+ at `slug` is visible. Use it when a whole phrase only makes sense once
69
+ the post exists ("its own post", "see it here"). An optional `fallback`
70
+ slot renders while the target is hidden:
71
+
72
+ ```mdx
73
+ <WhenPublished slug="my-draft">
74
+ It has <PostLink slug="my-draft">its own post</PostLink> now.
75
+ <Fragment slot="fallback">A post on it is coming.</Fragment>
76
+ </WhenPublished>
77
+ ```
78
+
79
+ Both take the consumer's collection as the `posts` prop (the full
80
+ collection, drafts included, so an unknown slug can throw instead of
81
+ hiding as "still a draft") and the consumer's draft gate as
82
+ `showDrafts`. `showDrafts` defaults to false, so a preview build must
83
+ pass its own draft gate through to `PostLink` to keep draft links
84
+ live. `PostLink` also accepts an optional `tip` carried as
85
+ `data-tooltip` for the link-tip island; a hidden `PostLink` drops it. A
86
+ consumer wraps each in a one-line site component that injects
87
+ `getCollection` and its own gate, the way the blog does. The resolvers
88
+ (`postLinkHref`, `resolvePostHref`, `forwardLinkVisible`) live in
89
+ `lib/drafts.ts` for consumers that want the logic without the
90
+ components.
74
91
 
75
92
  ## Derived excerpts
76
93
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@half-built/astro",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Astro components, islands, and pure helpers for the half-built design system.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -3,7 +3,6 @@ import type { SitemapGroup, EcosystemEntry } from "./models";
3
3
  import { BP_PHONE_MAX } from "../scripts/core/breakpoints";
4
4
 
5
5
  interface Props {
6
- siteName: string;
7
6
  legalHolder: string;
8
7
  sitemap: SitemapGroup[];
9
8
  ecosystem: EcosystemEntry[];
@@ -12,7 +11,6 @@ interface Props {
12
11
  }
13
12
 
14
13
  const {
15
- siteName,
16
14
  legalHolder,
17
15
  sitemap,
18
16
  ecosystem,
@@ -28,17 +26,15 @@ const year = new Date().getFullYear();
28
26
  <div class="shell">
29
27
  <div class="bottom-footer-info">
30
28
  <div class="site-info">
31
- {/* Two lines at every width (owner call 2026-09-13): the
32
- copyright over the site name, a real break rather than the
33
- old pipe that retired on phones and left the wrap to do
34
- the separating. The copyright's segments stay unbreakable
35
- so a narrow screen wraps that line only after the year. */}
29
+ {/* The copyright line alone (owner call 2026-09-14). The
30
+ site-name home link that used to sit under it retired.
31
+ The masthead and the Ecosystem group's bold entry already
32
+ say where you are, and the notice is complete without it.
33
+ The segments stay unbreakable so a narrow screen wraps
34
+ only after the year. */}
36
35
  <slot name="site-info">
37
- <span class="site-info-line">
38
- <span class="site-info-seg">Copyright © {year}</span>{" "}
39
- <span class="site-info-seg">{legalHolder}</span>
40
- </span>
41
- <span class="site-info-line"><a href="/">{siteName}</a></span>
36
+ <span class="site-info-seg">Copyright © {year}</span>{" "}
37
+ <span class="site-info-seg">{legalHolder}</span>
42
38
  </slot>
43
39
  </div>
44
40
  </div>
@@ -128,18 +124,9 @@ const year = new Date().getFullYear();
128
124
  font-size: var(--font-size-sm);
129
125
  }
130
126
 
131
- /* The two lines center under each other like the sitemap below
132
- (owner report 2026-08-25). */
127
+ /* Centered like the sitemap below (owner report 2026-08-25). */
133
128
  .site-info { text-align: center; }
134
- .site-info-line { display: block; }
135
-
136
- /* Underlined, unlike the sitemap links below: this one sits inside
137
- running text (the copyright block), where the same ink at 0.8
138
- opacity is not a visible link mark (WCAG 1.4.1, axe
139
- link-in-text-block; the line break is all that frames it). */
140
- .site-info a { text-decoration: underline; opacity: 0.8; transition: var(--transition); color: var(--ink); }
141
129
  .site-info-seg { white-space: nowrap; }
142
- .site-info a:hover { opacity: 1; }
143
130
 
144
131
  /* Sitemap columns under the copyright line, centered like it (owner
145
132
  feedback 2026-08-01). position: relative lifts the content above the
@@ -1,15 +1,22 @@
1
1
  ---
2
- import { resolvePostHref, type Linkable } from "../../lib/drafts";
2
+ import { postLinkHref, type Linkable } from "../../lib/drafts";
3
3
 
4
4
  interface Props {
5
5
  slug: string;
6
6
  posts: Linkable[];
7
+ showDrafts?: boolean;
7
8
  tip?: string;
8
9
  class?: string;
9
10
  }
10
11
 
11
- const { slug, posts, tip, class: className } = Astro.props;
12
- const href = resolvePostHref(slug, posts);
12
+ const { slug, posts, showDrafts = false, tip, class: className } = Astro.props;
13
+ const href = postLinkHref(slug, posts, showDrafts);
13
14
  ---
14
15
 
15
- <a href={href} data-tooltip={tip} class={className}><slot /></a>
16
+ {href ? (
17
+ <a href={href} data-tooltip={tip} class={className}>
18
+ <slot />
19
+ </a>
20
+ ) : (
21
+ <slot />
22
+ )}
@@ -11,4 +11,4 @@ const { slug, posts, showDrafts = false } = Astro.props;
11
11
  const visible = forwardLinkVisible(slug, posts, showDrafts);
12
12
  ---
13
13
 
14
- {visible && <slot />}
14
+ {visible ? <slot /> : <slot name="fallback" />}
package/src/lib/drafts.ts CHANGED
@@ -43,17 +43,21 @@ export function forwardLinkVisible(
43
43
  /* Post links by slug. A hand-typed post URL bakes in the target's date,
44
44
  so a re-dated draft silently breaks every earlier link to it. The
45
45
  PostLink content component resolves the slug through postPath() at
46
- build time instead. It resolves drafts too, and must: Astro renders
47
- MDX slot children eagerly, so a PostLink inside a hidden WhenPublished
48
- still runs (learned the hard way 2026-08-24). The deploy-build guard
49
- against a bare link to a draft is therefore a test, not a throw here:
50
- test/post-link.test.ts asserts no draft slug appears in any built
51
- page. Unknown slugs throw for the same reason forwardLinkVisible's do. */
46
+ build time instead. postLinkHref() is its policy: a visible target
47
+ gets its href, a hidden one gets null and the component renders its
48
+ text with no link (owner decision 2026-09-26), so a draft's name can
49
+ sit in published prose and becomes a link on its own when the draft
50
+ ships. resolvePostHref() still resolves drafts: consumers use it for
51
+ lookups keyed by href, and Astro renders MDX slot children eagerly,
52
+ so a PostLink inside a hidden WhenPublished still runs. The
53
+ deploy-build guard against a link to a draft stays a consumer test
54
+ (no draft slug in any built page). Unknown slugs throw for the same
55
+ reason forwardLinkVisible's do. */
52
56
 
53
57
  import { postPath } from "./slug";
54
58
 
55
59
  export interface Linkable {
56
- data: { slug: string; date: Date };
60
+ data: { slug: string; date: Date; draft?: boolean };
57
61
  }
58
62
 
59
63
  export function resolvePostHref(slug: string, posts: Linkable[]): string {
@@ -61,3 +65,14 @@ export function resolvePostHref(slug: string, posts: Linkable[]): string {
61
65
  if (!target) throw new Error(`PostLink: no post has slug "${slug}"`);
62
66
  return postPath(target);
63
67
  }
68
+
69
+ export function postLinkHref(
70
+ slug: string,
71
+ posts: Linkable[],
72
+ showDrafts: boolean,
73
+ ): string | null {
74
+ const target = posts.find((p) => p.data.slug === slug);
75
+ if (!target) throw new Error(`PostLink: no post has slug "${slug}"`);
76
+ if (target.data.draft && !showDrafts) return null;
77
+ return resolvePostHref(slug, posts);
78
+ }