@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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
};
|