@half-built/astro 0.1.0 → 0.2.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
@@ -14,3 +14,42 @@ is the design record for this package.
14
14
 
15
15
  Default icon glyphs are derived from Lucide (https://lucide.dev), ISC
16
16
  license. See `ICONS-LICENSE`.
17
+
18
+ ## EditorNote
19
+
20
+ `content/EditorNote.astro` is a reminder block for content that must
21
+ not ship: by default it renders only when the consuming build runs in
22
+ dev mode, and a deploy build emits nothing for it. A consumer with a
23
+ wider preview concept (the blog's SHOW_DRAFTS builds, for example)
24
+ passes its own gate through the `shown` prop; the component reads no
25
+ consumer config itself.
26
+
27
+ ## Live code colors
28
+
29
+ `shiki/code-theme` bakes its amber values into every highlighted span
30
+ at build time. `shiki/code-vars` is a Shiki transformer that rewrites
31
+ those baked values to the css package's `--code-token-*` and
32
+ `--code-*` custom properties, so highlighted code follows a runtime
33
+ palette override. The variables resolve to the same hexes the theme
34
+ bakes, so adopting the transformer changes no rendered pixel on its
35
+ own. Pass it beside the theme: the `transformers` prop of
36
+ `astro:components`' `Code`, or `markdown.shikiConfig.transformers` in
37
+ an Astro config.
38
+
39
+ ## Palette token entries
40
+
41
+ A `content/Palette.astro` entry may carry `token` (a custom property
42
+ name) instead of `hex`: the swatch then paints `var(token)` and
43
+ follows the live cascade with no script, and the hex cell renders
44
+ empty with a `data-token-hex` attribute for a consumer script to fill
45
+ from computed styles. Entries with `hex` render exactly as before.
46
+
47
+ ## Import notes
48
+
49
+ Wildcard subpath imports need explicit file extensions under
50
+ TypeScript's bundler mode: `@half-built/astro/lib/slug.ts` and
51
+ `@half-built/astro/components/Shell.astro`, not extensionless forms.
52
+ Vite resolves either; `tsc --noEmit` only accepts the explicit one.
53
+
54
+ The `Masthead.astro` export is an alias for `SiteHeader.astro`, the
55
+ same component under its public name.
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@half-built/astro",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Astro components, islands, and pure helpers for the half-built design system.",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/curthenrichs/half-built-ui.git",
9
+ "directory": "packages/astro"
10
+ },
6
11
  "type": "module",
7
12
  "files": ["src", "ICONS-LICENSE"],
8
13
  "exports": {
@@ -11,7 +16,8 @@
11
16
  "./content/*": "./src/components/content/*",
12
17
  "./scripts/*": "./src/scripts/*",
13
18
  "./lib/*": "./src/lib/*",
14
- "./shiki/code-theme": "./src/shiki/code-theme.mjs"
19
+ "./shiki/code-theme": "./src/shiki/code-theme.mjs",
20
+ "./shiki/code-vars": "./src/shiki/code-vars.mjs"
15
21
  },
16
22
  "peerDependencies": { "astro": "^5.0.0" },
17
23
  "publishConfig": { "access": "public" }
@@ -90,12 +90,18 @@ const heroStyle = heroPosition ? `object-position: ${heroPosition}` : undefined;
90
90
  z-index: 2;
91
91
  font-size: var(--font-size-xl);
92
92
  }
93
- /* Chip look comes from the .chip pattern; only placement is contextual. */
93
+ /* Chip look comes from the .chip pattern; only placement is
94
+ contextual. display: flex, not a plain block: a block wrapper
95
+ gives the inline-flex chip a text line box whose descender gap
96
+ floats it off the true bottom, so this chip and the corner badges
97
+ (already a flex container) sat at different offsets (owner catch
98
+ 2026-09-06). */
94
99
  .read-time-comment {
95
100
  position: absolute;
96
101
  bottom: 10px;
97
102
  left: 10px;
98
103
  z-index: 1;
104
+ display: flex;
99
105
  }
100
106
  .post-item-content { padding: 15px 0 0; padding-top: 10px; }
101
107
  .entry-cat .post-categories {
@@ -0,0 +1,57 @@
1
+ ---
2
+ /* Editor note: a reminder in content that renders on the dev server
3
+ and stays out of deploy builds. Moved up from the blog's
4
+ content/EditorNote.astro (owner call 2026-09-06: it is generic and
5
+ should have crossed in the 11.3 extraction; the blog swaps onto this
6
+ copy at its next pin bump). The blog's gate read its own SHOW_DRAFTS
7
+ config, which a package component must not do, so visibility is the
8
+ `shown` prop instead: the default is the consumer build's dev mode,
9
+ and a consumer with a preview concept passes its own wider gate.
10
+ Anatomy follows Callout (box, label straddling the top rule), but the
11
+ rule is dashed and amber: nothing else in the system is dashed, so a
12
+ note cannot be mistaken for content or for a callout. */
13
+
14
+ /* The label always reads "Editor's Note", with the title after a colon
15
+ when one is given (owner call 2026-08-27). */
16
+ interface Props { title?: string; shown?: boolean; class?: string }
17
+ const { title, shown = import.meta.env.MODE === "development", class: className } = Astro.props;
18
+ const label = title ? `Editor's Note: ${title}` : "Editor's Note";
19
+ ---
20
+ {shown && (
21
+ <div class:list={["editor-note", "rule-box", className]} role="note">
22
+ <strong class="editor-note-label boxed-label">{label}</strong>
23
+ <div class="editor-note-body"><slot /></div>
24
+ </div>
25
+ )}
26
+
27
+ <style>
28
+ /* Box from .rule-box; the dashed amber stroke and the margin are here. */
29
+ .editor-note {
30
+ position: relative;
31
+ border-style: dashed;
32
+ border-color: var(--accent-1);
33
+ margin: 30px 0;
34
+ }
35
+ .editor-note-label {
36
+ position: absolute;
37
+ top: 0;
38
+ left: 20px;
39
+ /* Center the label on the 2px top rule, not on the padding-box edge. */
40
+ transform: translateY(calc(-50% - 2px));
41
+ padding: 4px 10px;
42
+ font-size: var(--font-size-xs);
43
+ font-weight: 700;
44
+ text-transform: uppercase;
45
+ letter-spacing: 0.5px;
46
+ line-height: 1;
47
+ color: var(--accent-1-ink);
48
+ border-color: var(--accent-1);
49
+ }
50
+ /* Reads as an annotation, not a paragraph: a step smaller than prose. */
51
+ .editor-note-body {
52
+ position: relative;
53
+ font-size: var(--font-size-sm);
54
+ color: var(--accent-1-ink);
55
+ }
56
+ .editor-note-body > :global(:last-child) { margin-bottom: 0; }
57
+ </style>
@@ -3,8 +3,13 @@
3
3
  page: bordered swatch, role, hex. Hand-authored-page component (the
4
4
  migrator never emits it). Chip color is per-entry data, so it rides an
5
5
  inline background-color, which the html-validate gate whitelists for
6
- exactly this dynamic-value case. */
7
- interface Entry { role: string; hex: string; note?: string }
6
+ exactly this dynamic-value case.
7
+ An entry may carry `token` instead of `hex` (owner ask 2026-09-06,
8
+ the reference site's live token sheet): the chip then paints
9
+ var(token), following the live cascade with no script, and the hex
10
+ cell renders empty with data-token-hex for a consumer script to
11
+ fill from computed styles; the component itself stays inert. */
12
+ interface Entry { role: string; hex?: string; token?: string; note?: string }
8
13
  interface Props { entries: Entry[]; class?: string }
9
14
  const { entries, class: className } = Astro.props;
10
15
  ---
@@ -21,9 +26,9 @@ const { entries, class: className } = Astro.props;
21
26
  <tbody>
22
27
  {entries.map((e) => (
23
28
  <tr>
24
- <td><span class="palette-chip" aria-hidden="true" style={`background-color:${e.hex}`}></span></td>
29
+ <td><span class="palette-chip" aria-hidden="true" style={`background-color:${e.token ? `var(${e.token})` : e.hex}`}></span></td>
25
30
  <td>{e.role}</td>
26
- <td><code>{e.hex}</code></td>
31
+ <td>{e.token ? <code data-token-hex={e.token}></code> : <code>{e.hex}</code>}</td>
27
32
  <td>{e.note}</td>
28
33
  </tr>
29
34
  ))}
@@ -0,0 +1,79 @@
1
+ /* The ecosystem island: the footer's Ecosystem column, fetched at
2
+ runtime from a shared document so adding a property to the family
3
+ never means rebuilding every site (design record
4
+ 2026-09-06-ecosystem-endpoint-design.md in this repo's docs).
5
+
6
+ The component keeps rendering its typed props, which are the static
7
+ baseline. This island replaces that list only on a validated,
8
+ non-empty document that contains the site's own key. Every other
9
+ path leaves the baseline standing, so a reader with JavaScript off,
10
+ a dead endpoint or a malformed payload sees a slightly stale footer
11
+ rather than a blank one.
12
+
13
+ The endpoint is a parameter. This package hardcodes no consumer URL. */
14
+
15
+ export interface EcosystemDocEntry {
16
+ key: string;
17
+ label: string;
18
+ /* null: the property exists but is not deployed. Renders unlinked. */
19
+ href: string | null;
20
+ /* Ascending rank, 0 highest. */
21
+ priority: number;
22
+ family: string;
23
+ }
24
+
25
+ export interface EcosystemDocument {
26
+ version: number;
27
+ entries: EcosystemDocEntry[];
28
+ }
29
+
30
+ const SCHEMA_VERSION = 1;
31
+
32
+ function isEntry(value: unknown): value is EcosystemDocEntry {
33
+ if (typeof value !== "object" || value === null) return false;
34
+ const entry = value as Record<string, unknown>;
35
+ return (
36
+ typeof entry.key === "string" && entry.key !== "" &&
37
+ typeof entry.label === "string" && entry.label !== "" &&
38
+ (entry.href === null || typeof entry.href === "string") &&
39
+ typeof entry.priority === "number" && Number.isFinite(entry.priority) &&
40
+ typeof entry.family === "string" && entry.family !== ""
41
+ );
42
+ }
43
+
44
+ /** The document, or null when anything about it is unusable. */
45
+ export function validateDocument(raw: unknown): EcosystemDocument | null {
46
+ if (typeof raw !== "object" || raw === null) return null;
47
+ const doc = raw as Record<string, unknown>;
48
+ if (doc.version !== SCHEMA_VERSION) return null;
49
+ if (!Array.isArray(doc.entries) || doc.entries.length === 0) return null;
50
+ /* filter, not every: a type predicate narrows the array through
51
+ filter and does not through every, so this is the form that hands
52
+ back EcosystemDocEntry[] instead of any[]. Comparing lengths is
53
+ what makes it a refusal rather than a silent drop of bad rows. */
54
+ const entries = doc.entries.filter(isEntry);
55
+ if (entries.length !== doc.entries.length) return null;
56
+ return { version: SCHEMA_VERSION, entries };
57
+ }
58
+
59
+ /** The self entry's family first, each group by ascending priority and
60
+ then label, capped at limit. Null when selfKey is absent, which is
61
+ the refusal that keeps a site out of a list missing itself. */
62
+ export function sortEntries(
63
+ entries: EcosystemDocEntry[],
64
+ selfKey: string,
65
+ limit: number,
66
+ ): EcosystemDocEntry[] | null {
67
+ const self = entries.find((entry) => entry.key === selfKey);
68
+ if (!self) return null;
69
+ const own = self.family;
70
+ return [...entries]
71
+ .sort((a, b) => {
72
+ const aOwn = a.family === own ? 0 : 1;
73
+ const bOwn = b.family === own ? 0 : 1;
74
+ if (aOwn !== bOwn) return aOwn - bOwn;
75
+ if (a.priority !== b.priority) return a.priority - b.priority;
76
+ return a.label.localeCompare(b.label);
77
+ })
78
+ .slice(0, limit);
79
+ }
@@ -0,0 +1,33 @@
1
+ // Shiki transformer: rewrite the code theme's baked hex values to the
2
+ // css package's custom properties, so highlighted code follows a live
3
+ // palette override (spec 2026-09-06, live code colors). Shiki inlines
4
+ // literal colors on every span at build; with this transformer the
5
+ // build emits var() instead, and the variables resolve to the exact
6
+ // same hexes until something overrides them, so adopting it changes
7
+ // no rendered pixel. Pass it wherever shiki options go: the transformers
8
+ // prop of astro:components' Code, or markdown.shikiConfig.transformers
9
+ // in a consumer's astro config.
10
+ const HEX_TO_VAR = {
11
+ "#ffaa3c": "var(--code-token-keyword)",
12
+ "#ffd18a": "var(--code-token-function)",
13
+ "#e07c14": "var(--code-token-string)",
14
+ "#8a7a63": "var(--code-token-comment)",
15
+ "#e8d9c3": "var(--code-fg)",
16
+ "#1b140c": "var(--code-bg)",
17
+ };
18
+
19
+ function swap(node) {
20
+ const style = node.properties?.style;
21
+ if (typeof style !== "string") return;
22
+ node.properties.style = style.replace(/#[0-9a-fA-F]{6}/g, (hex) => HEX_TO_VAR[hex.toLowerCase()] ?? hex);
23
+ }
24
+
25
+ export default {
26
+ name: "half-built-code-vars",
27
+ pre(node) {
28
+ swap(node);
29
+ },
30
+ span(node) {
31
+ swap(node);
32
+ },
33
+ };