@dogsbay/ui 0.2.0-beta.17 → 0.2.0-beta.171
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 +37 -12
- package/src/alert/CollapsibleAlert.astro +33 -7
- package/src/alert/icon.ts +8 -15
- package/src/api-layout/ApiLayout.astro +28 -4
- package/src/api-layout/ExampleBlock.astro +3 -2
- package/src/api-params/ApiParams.astro +1 -1
- package/src/api-symbol/ApiSymbol.astro +1 -1
- package/src/api-symbol/api-symbol.css +14 -11
- package/src/avatar/Avatar.astro +6 -2
- package/src/badge/Badge.astro +3 -10
- package/src/badge/variants.ts +39 -0
- package/src/code-block/CodeBlock.astro +52 -11
- package/src/code-block/ansi.ts +31 -0
- package/src/code-rich/CodeRich.astro +156 -15
- package/src/code-rich/code-rich.css +54 -2
- package/src/code-rich/markers.ts +66 -0
- package/src/content-renderer/ContentRenderer.astro +47 -6
- package/src/content-renderer/InlineRenderer.astro +24 -1
- package/src/details-sheet/DetailsSheet.astro +28 -0
- package/src/details-sheet/index.ts +1 -0
- package/src/docs-layout/DocsLayout.astro +36 -4
- package/src/endpoint-card/EndpointCard.astro +59 -1
- package/src/files/FileTree.astro +16 -0
- package/src/files/index.ts +2 -0
- package/src/files/markup.ts +34 -0
- package/src/image/Image.astro +23 -2
- package/src/kbd/Kbd.astro +2 -3
- package/src/kbd/variants.ts +3 -0
- package/src/link-card/LinkCard.astro +10 -3
- package/src/property-list/PropertyList.astro +3 -2
- package/src/sidebar/Sidebar.astro +16 -6
- package/src/sidebar/SidebarNavMark.astro +85 -0
- package/src/sidebar/SidebarNavTree.astro +111 -7
- package/src/sidebar/sidebar.ts +2 -2
- package/src/steps/Steps.astro +5 -0
- package/src/steps/SubSteps.astro +5 -0
- package/src/table/Table.astro +18 -3
- package/src/table/TableCell.astro +1 -1
- package/src/table/TableHead.astro +1 -1
- package/src/toc/TableOfContents.astro +10 -10
- package/src/youtube/YouTube.astro +2 -2
package/package.json
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/ui",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.171",
|
|
4
4
|
"description": "Accessible UI components for Astro, inspired by Base UI and shadcn",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./*": "./src/*"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"tailwind-variants": "^0.3.
|
|
10
|
+
"tailwind-variants": "^0.3.1"
|
|
11
11
|
},
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"@
|
|
15
|
-
"
|
|
16
|
-
"
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"vitest": "^4.1.10",
|
|
14
|
+
"@shikijs/transformers": "^4.3.1",
|
|
15
|
+
"katex": "^0.16.47",
|
|
16
|
+
"maplibre-gl": "^5.24.0",
|
|
17
|
+
"shiki": "^4.3.1"
|
|
17
18
|
},
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"@shikijs/transformers": "^4.
|
|
21
|
-
"
|
|
22
|
-
"
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"astro": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
21
|
+
"@shikijs/transformers": "^4.3.1",
|
|
22
|
+
"katex": "^0.16.47",
|
|
23
|
+
"maplibre-gl": "^5.24.0",
|
|
24
|
+
"shiki": "^4.3.1",
|
|
25
|
+
"@dogsbay/elements": "0.2.0-beta.171",
|
|
26
|
+
"@dogsbay/icons": "0.2.0-beta.171",
|
|
27
|
+
"@dogsbay/primitives": "0.2.0-beta.171"
|
|
23
28
|
},
|
|
24
29
|
"files": [
|
|
25
30
|
"src",
|
|
@@ -34,5 +39,25 @@
|
|
|
34
39
|
"homepage": "https://github.com/dogsbay/dogsbay/tree/main/packages/ui",
|
|
35
40
|
"bugs": {
|
|
36
41
|
"url": "https://github.com/dogsbay/dogsbay/issues"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"astro": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"@shikijs/transformers": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"katex": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"maplibre-gl": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"shiki": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"test": "vitest run"
|
|
37
62
|
}
|
|
38
63
|
}
|
|
@@ -6,18 +6,26 @@
|
|
|
6
6
|
* Composes with the same variant tokens as Alert.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
-
* <CollapsibleAlert variant="warning" title="Warning" open>
|
|
9
|
+
* <CollapsibleAlert variant="warning" title="Warning" icon="lucide:rocket" open>
|
|
10
10
|
* <p>Body content here.</p>
|
|
11
11
|
* </CollapsibleAlert>
|
|
12
|
+
*
|
|
13
|
+
* A title with markup goes in the `title` slot: `<Fragment slot="title">Run <code>init</code></Fragment>`.
|
|
12
14
|
*/
|
|
13
15
|
import { tv, type VariantProps } from "tailwind-variants";
|
|
14
16
|
import { contentVariants, icons, chevronSvg } from "./variants.js";
|
|
17
|
+
import { resolveIcon } from "./icon.js";
|
|
15
18
|
|
|
16
19
|
const wrapper = tv({
|
|
17
20
|
base: "relative w-full rounded-lg border overflow-hidden [&>summary]:list-none [&>summary::-webkit-details-marker]:hidden",
|
|
18
21
|
variants: {
|
|
19
22
|
variant: {
|
|
20
23
|
default: "border-border",
|
|
24
|
+
// `plain` = a disclosure that is CHROME, not an alert: no tint, no
|
|
25
|
+
// icon, no ARIA role. Used for collapsed context (e.g. the
|
|
26
|
+
// comparison view's unchanged-section folds), where announcing a
|
|
27
|
+
// "note" or a live-region "status" would be a lie.
|
|
28
|
+
plain: "border-border",
|
|
21
29
|
destructive: "border-destructive/50",
|
|
22
30
|
note: "border-note/50",
|
|
23
31
|
abstract: "border-abstract/50",
|
|
@@ -41,6 +49,7 @@ type Variant = VariantProps<typeof wrapper>["variant"];
|
|
|
41
49
|
/** Title bar: stronger tint (~10%) */
|
|
42
50
|
const titleBg: Record<string, string> = {
|
|
43
51
|
default: "bg-muted",
|
|
52
|
+
plain: "bg-transparent",
|
|
44
53
|
destructive: "bg-destructive/10 text-destructive",
|
|
45
54
|
note: "bg-note/10", abstract: "bg-abstract/10", info: "bg-info/10",
|
|
46
55
|
tip: "bg-tip/10", success: "bg-success/10", question: "bg-question/10",
|
|
@@ -51,6 +60,7 @@ const titleBg: Record<string, string> = {
|
|
|
51
60
|
/** Body: subtle tint (~3%) */
|
|
52
61
|
const bodyBg: Record<string, string> = {
|
|
53
62
|
default: "bg-background",
|
|
63
|
+
plain: "bg-transparent",
|
|
54
64
|
destructive: "bg-destructive/3",
|
|
55
65
|
note: "bg-note/3", abstract: "bg-abstract/3", info: "bg-info/3",
|
|
56
66
|
tip: "bg-tip/3", success: "bg-success/3", question: "bg-question/3",
|
|
@@ -73,31 +83,47 @@ interface Props {
|
|
|
73
83
|
title?: string;
|
|
74
84
|
/** Whether the details starts open */
|
|
75
85
|
open?: boolean;
|
|
86
|
+
/** An icon name in place of the variant's (`lucide:rocket`, `compass`, an emoji shortcode); unknown names keep the variant's. */
|
|
87
|
+
icon?: string;
|
|
76
88
|
class?: string;
|
|
89
|
+
/** id, data-*, aria-*: passed to the <details> (an `id` is a link target) */
|
|
90
|
+
[key: string]: unknown;
|
|
77
91
|
}
|
|
78
92
|
|
|
79
|
-
const { variant = "note", title, open = false, class: className } = Astro.props;
|
|
93
|
+
const { variant = "note", title, open = false, class: className, icon: iconName, ...rest } = Astro.props;
|
|
80
94
|
const v = variant || "note";
|
|
81
95
|
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
// `plain` is chrome — a disclosure, nothing more. No role at all:
|
|
97
|
+
// role="note"/"status" on collapsed context misinforms assistive tech
|
|
98
|
+
// (and "status" would make it a live region).
|
|
99
|
+
const role = v === "plain" ? undefined : contentVariants.has(v) ? "note" : "status";
|
|
100
|
+
// The author's icon, as Alert draws it; else the variant's (`plain` has none)
|
|
101
|
+
const icon = (iconName ? resolveIcon(iconName) : null) ?? (contentVariants.has(v) ? icons[v] : null);
|
|
102
|
+
const fallbackTitle = title || v.charAt(0).toUpperCase() + v.slice(1);
|
|
84
103
|
---
|
|
85
104
|
|
|
105
|
+
{/*
|
|
106
|
+
The role belongs on the CONTENT, not on <details>. A <details> element
|
|
107
|
+
has an implicit `group` role, and ARIA does not permit overriding it
|
|
108
|
+
with `note`/`status` — axe flags it (aria-allowed-role), which is
|
|
109
|
+
exactly what an accessibility pass found on the comparison site's
|
|
110
|
+
collapsed sections. Moving it to the body keeps the semantics legal.
|
|
111
|
+
*/}
|
|
86
112
|
<details
|
|
87
113
|
class:list={[wrapper({ variant, class: className }), "group/collapsible"]}
|
|
88
114
|
data-variant={variant}
|
|
89
|
-
role={role}
|
|
90
115
|
open={open}
|
|
116
|
+
{...rest}
|
|
91
117
|
>
|
|
92
118
|
<summary class:list={[
|
|
93
119
|
"flex cursor-pointer items-center gap-2 px-4 py-3 text-sm font-semibold select-none",
|
|
94
120
|
titleBg[v],
|
|
95
121
|
]}>
|
|
96
122
|
{icon && <span class:list={["shrink-0 [&>svg]:size-4", iconColor[v]]} set:html={icon} />}
|
|
97
|
-
<span class="flex-1 text-foreground" data-part="title">{
|
|
123
|
+
<span class="flex-1 text-foreground" data-part="title"><slot name="title">{fallbackTitle}</slot></span>
|
|
98
124
|
<span class="shrink-0 text-muted-foreground group-open/collapsible:rotate-180 transition-transform duration-200" set:html={chevronSvg} />
|
|
99
125
|
</summary>
|
|
100
|
-
<div class:list={["px-4 py-3 text-sm [&_p]:leading-relaxed", bodyBg[v]]}>
|
|
126
|
+
<div class:list={["px-4 py-3 text-sm [&_p]:leading-relaxed", bodyBg[v]]} role={role}>
|
|
101
127
|
<slot />
|
|
102
128
|
</div>
|
|
103
129
|
</details>
|
package/src/alert/icon.ts
CHANGED
|
@@ -1,30 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resolve an icon
|
|
2
|
+
* Resolve an icon name for Alert and CollapsibleAlert.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - `
|
|
6
|
-
* - bare
|
|
4
|
+
* The same names as Icon.astro and dogsbay-ssg (`@dogsbay/icons` resolveIcon):
|
|
5
|
+
* - `pack:name` — e.g. "lucide:rocket", "material:check-circle", "simple:github"
|
|
6
|
+
* - a bare name — an emoji shortcode first ("rocket" → 🚀), then a Lucide icon ("compass")
|
|
7
7
|
*
|
|
8
8
|
* Returns an inline SVG string or a span with the emoji character. Returns
|
|
9
|
-
* null when the
|
|
9
|
+
* null when the name can't be resolved (caller should fall back to the
|
|
10
10
|
* variant's default icon).
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
const registry = createIconRegistry();
|
|
12
|
+
import { resolveIcon as resolveName } from "@dogsbay/icons";
|
|
15
13
|
|
|
16
14
|
export function resolveIcon(shortcode: string): string | null {
|
|
17
|
-
const
|
|
18
|
-
const [library, name] = colonIdx >= 0
|
|
19
|
-
? [shortcode.slice(0, colonIdx), shortcode.slice(colonIdx + 1)]
|
|
20
|
-
: ["emoji", shortcode];
|
|
21
|
-
|
|
22
|
-
const resolved = registry.resolve(library, name);
|
|
15
|
+
const resolved = resolveName(shortcode);
|
|
23
16
|
if (!resolved) return null;
|
|
24
17
|
|
|
25
18
|
// Emoji resolve to Unicode characters; wrap for consistent sizing/positioning
|
|
26
19
|
// with the SVG icons in the Alert layout.
|
|
27
|
-
if (
|
|
20
|
+
if (!resolved.startsWith("<")) {
|
|
28
21
|
return `<span aria-hidden="true" style="font-size:1em;line-height:1;display:inline-block;width:1em;text-align:center;">${resolved}</span>`;
|
|
29
22
|
}
|
|
30
23
|
|
|
@@ -12,17 +12,41 @@
|
|
|
12
12
|
*/
|
|
13
13
|
interface Props {
|
|
14
14
|
class?: string;
|
|
15
|
+
/**
|
|
16
|
+
* "page" (default): a dedicated API operation page — full-viewport
|
|
17
|
+
* min-height, sticky code rail.
|
|
18
|
+
* "embedded": the layout sits inline among prose (imported MDX docs
|
|
19
|
+
* with several endpoints per page) — natural height, no sticky.
|
|
20
|
+
*/
|
|
21
|
+
variant?: "page" | "embedded";
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
const { class: className } = Astro.props;
|
|
24
|
+
const { class: className, variant = "page" } = Astro.props;
|
|
25
|
+
const isPage = variant === "page";
|
|
18
26
|
---
|
|
19
27
|
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
{/* dba-api marks an API-reference region: the generated .docs-prose
|
|
29
|
+
typography rules skip everything inside it via :not(.dba-api *), so
|
|
30
|
+
endpoint cards keep their own utility-class typography even when the
|
|
31
|
+
page body is wrapped in the prose article. */}
|
|
32
|
+
<div
|
|
33
|
+
class:list={[
|
|
34
|
+
"dba-api grid grid-cols-1 lg:grid-cols-2",
|
|
35
|
+
isPage ? "lg:min-h-[calc(100vh-3rem)]" : "my-6 rounded-lg border overflow-hidden",
|
|
36
|
+
className,
|
|
37
|
+
]}
|
|
38
|
+
>
|
|
39
|
+
<div class:list={[isPage ? "px-6 py-10" : "p-6", "lg:border-r"]}>
|
|
22
40
|
<slot name="description" />
|
|
23
41
|
</div>
|
|
24
42
|
<div class="bg-[var(--api-panel-bg)] text-[var(--api-panel-fg)]">
|
|
25
|
-
<div
|
|
43
|
+
<div
|
|
44
|
+
class:list={[
|
|
45
|
+
isPage
|
|
46
|
+
? "px-6 py-10 lg:sticky lg:top-12 lg:max-h-[calc(100vh-3rem)] lg:overflow-y-auto"
|
|
47
|
+
: "p-6",
|
|
48
|
+
]}
|
|
49
|
+
>
|
|
26
50
|
<slot name="code" />
|
|
27
51
|
</div>
|
|
28
52
|
</div>
|
|
@@ -55,9 +55,10 @@ const content = code || (json !== undefined && json !== null ? JSON.stringify(js
|
|
|
55
55
|
|
|
56
56
|
{content && (
|
|
57
57
|
<div class={className}>
|
|
58
|
-
|
|
58
|
+
{/* A label, not a heading: on an operation page it followed the h1 (axe heading-order). */}
|
|
59
|
+
<p class="mb-3 text-xs font-semibold uppercase tracking-wider text-white/60">
|
|
59
60
|
{title}
|
|
60
|
-
</
|
|
61
|
+
</p>
|
|
61
62
|
<CodeBlock code={content} lang={resolvedLang} copy={true} />
|
|
62
63
|
</div>
|
|
63
64
|
)}
|
|
@@ -57,7 +57,7 @@ function splitCodeBlocks(html: string): Segment[] {
|
|
|
57
57
|
}
|
|
58
58
|
---
|
|
59
59
|
|
|
60
|
-
<table class="api-params w-full text-sm border-collapse
|
|
60
|
+
<table class="api-params w-full text-sm border-collapse">
|
|
61
61
|
<thead>
|
|
62
62
|
<tr class="border-b border-border">
|
|
63
63
|
<th class="py-3 px-4 text-left font-semibold text-muted-foreground uppercase tracking-wider text-xs w-[140px]">Parameter</th>
|
|
@@ -18,7 +18,7 @@ const slug = name.toLowerCase().replace(/[^\w]/g, "-").replace(/-+/g, "-");
|
|
|
18
18
|
const HeadingTag = `h${level}` as any;
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
<div class="api-symbol
|
|
21
|
+
<div class="api-symbol" data-kind={kind}>
|
|
22
22
|
<div class="api-symbol-heading">
|
|
23
23
|
<span class={`api-badge api-badge-${badgeKind}`}>{badgeKind}</span>
|
|
24
24
|
<HeadingTag id={slug} class="!mt-0 !mb-0 !border-0">
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
/* API Symbol badges
|
|
1
|
+
/* API Symbol badges, coloured by kind. Text on its own 20% tint must reach
|
|
2
|
+
WCAG AA (4.5:1) on the page background: the light colours were L 0.5–0.6
|
|
3
|
+
(3.2–4.0:1) and are L 0.44 (5.0:1 or more); render-html's
|
|
4
|
+
tests/contrast.test.ts measures every kind in both modes. */
|
|
2
5
|
.api-badge {
|
|
3
6
|
display: inline-flex;
|
|
4
7
|
align-items: center;
|
|
@@ -13,8 +16,8 @@
|
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
.api-badge-class {
|
|
16
|
-
background-color: oklch(0.
|
|
17
|
-
color: oklch(0.
|
|
19
|
+
background-color: oklch(0.44 0.18 265 / 0.2);
|
|
20
|
+
color: oklch(0.44 0.18 265);
|
|
18
21
|
}
|
|
19
22
|
.dark .api-badge-class {
|
|
20
23
|
background-color: oklch(0.7 0.15 265 / 0.2);
|
|
@@ -22,8 +25,8 @@
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
.api-badge-meth {
|
|
25
|
-
background-color: oklch(0.
|
|
26
|
-
color: oklch(0.
|
|
28
|
+
background-color: oklch(0.44 0.15 155 / 0.2);
|
|
29
|
+
color: oklch(0.44 0.15 155);
|
|
27
30
|
}
|
|
28
31
|
.dark .api-badge-meth {
|
|
29
32
|
background-color: oklch(0.7 0.15 155 / 0.2);
|
|
@@ -31,8 +34,8 @@
|
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
.api-badge-attr {
|
|
34
|
-
background-color: oklch(0.
|
|
35
|
-
color: oklch(0.
|
|
37
|
+
background-color: oklch(0.44 0.15 55 / 0.2);
|
|
38
|
+
color: oklch(0.44 0.15 55);
|
|
36
39
|
}
|
|
37
40
|
.dark .api-badge-attr {
|
|
38
41
|
background-color: oklch(0.75 0.15 55 / 0.2);
|
|
@@ -40,8 +43,8 @@
|
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
.api-badge-func {
|
|
43
|
-
background-color: oklch(0.
|
|
44
|
-
color: oklch(0.
|
|
46
|
+
background-color: oklch(0.44 0.18 330 / 0.2);
|
|
47
|
+
color: oklch(0.44 0.18 330);
|
|
45
48
|
}
|
|
46
49
|
.dark .api-badge-func {
|
|
47
50
|
background-color: oklch(0.7 0.15 330 / 0.2);
|
|
@@ -49,8 +52,8 @@
|
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
.api-badge-module {
|
|
52
|
-
background-color: oklch(0.
|
|
53
|
-
color: oklch(0.
|
|
55
|
+
background-color: oklch(0.44 0.12 200 / 0.2);
|
|
56
|
+
color: oklch(0.44 0.12 200);
|
|
54
57
|
}
|
|
55
58
|
.dark .api-badge-module {
|
|
56
59
|
background-color: oklch(0.65 0.12 200 / 0.2);
|
package/src/avatar/Avatar.astro
CHANGED
|
@@ -30,16 +30,20 @@ const initials =
|
|
|
30
30
|
.join("")
|
|
31
31
|
.slice(0, 2)
|
|
32
32
|
.toUpperCase();
|
|
33
|
+
// The name is on the avatar itself, so it is read whether the image loads, fails or was never given:
|
|
34
|
+
// the initials stand in visually only, and a failed image is hidden with its alt.
|
|
35
|
+
// An author's own aria-label wins (an empty one does not); the role is ours, as in render-html.
|
|
36
|
+
const label = alt ? { role: "img", "aria-label": (rest["aria-label"] as string | undefined) || alt } : {};
|
|
33
37
|
---
|
|
34
38
|
|
|
35
|
-
<span class={avatar({ class: className })} {...rest}>
|
|
39
|
+
<span class={avatar({ class: className })} {...rest} {...label}>
|
|
36
40
|
{
|
|
37
41
|
src ? (
|
|
38
42
|
<>
|
|
39
43
|
<img
|
|
40
44
|
class={avatarImage()}
|
|
41
45
|
src={src}
|
|
42
|
-
alt=
|
|
46
|
+
alt=""
|
|
43
47
|
width="40"
|
|
44
48
|
height="40"
|
|
45
49
|
onerror="this.hidden=true;this.nextElementSibling.hidden=false"
|
package/src/badge/Badge.astro
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { tv, type VariantProps } from "tailwind-variants";
|
|
3
|
+
import { BADGE_BASE, BADGE_VARIANTS } from "./variants.js";
|
|
3
4
|
|
|
4
5
|
const badge = tv({
|
|
5
|
-
base:
|
|
6
|
-
variants: {
|
|
7
|
-
variant: {
|
|
8
|
-
default: "border-transparent bg-primary text-primary-foreground",
|
|
9
|
-
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
|
10
|
-
destructive:
|
|
11
|
-
"border-transparent bg-destructive text-destructive-foreground",
|
|
12
|
-
outline: "text-foreground",
|
|
13
|
-
},
|
|
14
|
-
},
|
|
6
|
+
base: BADGE_BASE,
|
|
7
|
+
variants: { variant: BADGE_VARIANTS },
|
|
15
8
|
defaultVariants: {
|
|
16
9
|
variant: "default",
|
|
17
10
|
},
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Badge classes, shared by Badge.astro, ContentRenderer and @dogsbay/render-html
|
|
3
|
+
* (which copies this file with scripts/sync-sources.mjs), so a badge
|
|
4
|
+
* looks the same however a page is produced.
|
|
5
|
+
*/
|
|
6
|
+
export const BADGE_BASE =
|
|
7
|
+
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors";
|
|
8
|
+
|
|
9
|
+
export const BADGE_VARIANTS = {
|
|
10
|
+
default: "border-transparent bg-primary text-primary-foreground",
|
|
11
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
|
12
|
+
destructive: "border-transparent bg-destructive text-destructive-foreground",
|
|
13
|
+
outline: "text-foreground",
|
|
14
|
+
// Status colours, from the callout palette (`:badge[Beta]{variant="warning"}`).
|
|
15
|
+
// The TEXT is the accent mixed 60% toward --foreground; the tint keeps the
|
|
16
|
+
// accent at full strength. The accent used raw measured 1.85-2.6:1 in light
|
|
17
|
+
// mode (danger 3.3-3.9:1 in dark too), from painted pixels. --foreground is
|
|
18
|
+
// near-black in light and near-white in dark, so one rule serves both themes
|
|
19
|
+
// (worst case 4.95:1); a fixed dark colour would fail dark mode. The same
|
|
20
|
+
// rule as the in-cell admonitions in format-astro's theme-css.ts.
|
|
21
|
+
// The `color:` hint is required: tailwind-merge 2.x (Badge.astro's tv())
|
|
22
|
+
// reads a bare text-[... 60% ...] as a FONT SIZE and drops text-xs.
|
|
23
|
+
// Checked per preset by @dogsbay/render-html tests/contrast.test.ts, and
|
|
24
|
+
// through tv() by ui tests/badge.test.ts.
|
|
25
|
+
info: "border-transparent bg-info/15 text-[color:color-mix(in_oklab,var(--info)_60%,var(--foreground))]",
|
|
26
|
+
success: "border-transparent bg-success/15 text-[color:color-mix(in_oklab,var(--success)_60%,var(--foreground))]",
|
|
27
|
+
warning: "border-transparent bg-warning/15 text-[color:color-mix(in_oklab,var(--warning)_60%,var(--foreground))]",
|
|
28
|
+
danger: "border-transparent bg-danger/15 text-[color:color-mix(in_oklab,var(--danger)_60%,var(--foreground))]",
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
export type BadgeVariant = keyof typeof BADGE_VARIANTS;
|
|
32
|
+
|
|
33
|
+
/** The class list for a variant; an unknown variant gets the base classes only. */
|
|
34
|
+
export function badgeClass(variant: string | undefined): string {
|
|
35
|
+
// Own keys only: `toString` or `__proto__` must not read Object.prototype
|
|
36
|
+
const key = variant ?? "default";
|
|
37
|
+
const v = Object.prototype.hasOwnProperty.call(BADGE_VARIANTS, key) ? BADGE_VARIANTS[key as BadgeVariant] : undefined;
|
|
38
|
+
return v ? `${BADGE_BASE} ${v}` : BADGE_BASE;
|
|
39
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { Code } from "astro/components";
|
|
3
|
+
import { ansiColoursOnly, stripAnsi } from "./ansi";
|
|
3
4
|
import { tv } from "tailwind-variants";
|
|
4
5
|
|
|
5
6
|
const codeBlockWrapper = tv({
|
|
@@ -21,6 +22,10 @@ interface Props {
|
|
|
21
22
|
lang?: string;
|
|
22
23
|
/** Optional title displayed in the header bar */
|
|
23
24
|
title?: string;
|
|
25
|
+
/*
|
|
26
|
+
* A `caption` slot makes the block a figure: the header bar is its
|
|
27
|
+
* <figcaption>, showing the caption with the language beside it.
|
|
28
|
+
*/
|
|
24
29
|
/** Show copy button (default: true) */
|
|
25
30
|
copy?: boolean;
|
|
26
31
|
/** Show the header title bar. true = always, false = never, "auto" = only when title prop is set (default: true) */
|
|
@@ -42,31 +47,66 @@ const {
|
|
|
42
47
|
...rest
|
|
43
48
|
} = Astro.props;
|
|
44
49
|
|
|
50
|
+
// AsciiDoc/docs language tokens that are not Shiki grammars → the nearest real
|
|
51
|
+
// one, so the block actually highlights (and data-language is meaningful)
|
|
52
|
+
// instead of silently falling back to plaintext. The header still shows the
|
|
53
|
+
// original label (`lang` below), only the highlighter input is aliased. #009.
|
|
54
|
+
const LANG_ALIASES: Record<string, string> = {
|
|
55
|
+
terminal: "bash",
|
|
56
|
+
};
|
|
57
|
+
const shikiLang = LANG_ALIASES[lang.toLowerCase()] ?? lang.toLowerCase();
|
|
58
|
+
|
|
59
|
+
// Tags that mean "no language": no label, as render-html (its PLAIN set, @dogsbay/render-html/lines).
|
|
60
|
+
// Only `plaintext` was hidden, so `txt` and `output` blocks showed "txt" and "output".
|
|
61
|
+
const PLAIN = new Set(["", "text", "txt", "plain", "plaintext", "none", "output", "log", "code", "codeblock"]);
|
|
62
|
+
const plain = PLAIN.has(lang.toLowerCase().trim());
|
|
63
|
+
// An `ansi` block shows its colours, not its escape codes: copy the text as shown
|
|
64
|
+
const isAnsi = shikiLang === "ansi";
|
|
65
|
+
const copyText = isAnsi ? stripAnsi(code) : code;
|
|
66
|
+
// Shiki draws colours only; any other sequence would garble the text (./ansi.ts)
|
|
67
|
+
const shownCode = isAnsi ? ansiColoursOnly(code) : code;
|
|
68
|
+
|
|
45
69
|
// Show header bar based on showTitle prop:
|
|
46
70
|
// - true: show when there's a title or a declared language (default, original behavior)
|
|
47
71
|
// - false: never show
|
|
48
72
|
// - "auto": only show when an explicit title is provided (not lang-only)
|
|
73
|
+
const hasCaption = Astro.slots.has("caption");
|
|
74
|
+
const Wrapper = hasCaption ? "figure" : "div";
|
|
75
|
+
const Header = hasCaption ? "figcaption" : "div";
|
|
49
76
|
const showHeader =
|
|
50
|
-
|
|
77
|
+
hasCaption ? true
|
|
78
|
+
: showTitle === false ? false
|
|
51
79
|
: showTitle === "auto" ? !!title
|
|
52
|
-
: !!title ||
|
|
80
|
+
: !!title || !plain;
|
|
53
81
|
// When no header, copy button floats inside the code body
|
|
54
82
|
const floatingCopy = copy && !showHeader;
|
|
55
83
|
---
|
|
56
84
|
|
|
57
|
-
<
|
|
58
|
-
class={codeBlockWrapper({ class: className })}
|
|
85
|
+
<Wrapper
|
|
86
|
+
class={codeBlockWrapper({ class: [hasCaption && "m-0", className] })}
|
|
59
87
|
data-component="code-block"
|
|
60
88
|
{...rest}
|
|
61
89
|
>
|
|
62
90
|
{showHeader && (
|
|
63
|
-
<
|
|
64
|
-
|
|
91
|
+
<Header class={codeBlockHeader()}>
|
|
92
|
+
{/*
|
|
93
|
+
Left: what the code is (caption or filename title), or the language when that is
|
|
94
|
+
all there is. Right: the language beside the copy button, as secondary detail
|
|
95
|
+
*/}
|
|
96
|
+
{hasCaption ? (
|
|
97
|
+
<span class="min-w-0 font-medium text-code-fg" data-part="caption"><slot name="caption" /></span>
|
|
98
|
+
) : title ? (
|
|
99
|
+
<span class="min-w-0 font-medium text-code-fg" data-part="title">{title}</span>
|
|
100
|
+
) : (
|
|
101
|
+
<span class="select-none">{lang}</span>
|
|
102
|
+
)}
|
|
103
|
+
<span class="flex shrink-0 items-center gap-2">
|
|
104
|
+
{(hasCaption || title) && !plain && <span class="select-none" data-part="language">{lang}</span>}
|
|
65
105
|
{copy && (
|
|
66
106
|
<button
|
|
67
107
|
class={codeBlockCopy()}
|
|
68
108
|
data-part="copy"
|
|
69
|
-
data-copy-text={
|
|
109
|
+
data-copy-text={copyText}
|
|
70
110
|
type="button"
|
|
71
111
|
aria-label="Copy code"
|
|
72
112
|
>
|
|
@@ -104,15 +144,16 @@ const floatingCopy = copy && !showHeader;
|
|
|
104
144
|
</svg>
|
|
105
145
|
</button>
|
|
106
146
|
)}
|
|
107
|
-
|
|
147
|
+
</span>
|
|
148
|
+
</Header>
|
|
108
149
|
)}
|
|
109
150
|
<div class="relative overflow-x-auto [&_pre]:!m-0 [&_pre]:!rounded-none [&_pre]:!border-0 [&_pre]:px-4 [&_pre]:py-3">
|
|
110
|
-
<Code code={
|
|
151
|
+
<Code code={shownCode} lang={shikiLang} themes={{ light: "github-light-default", dark: theme }} defaultColor={false} />
|
|
111
152
|
{floatingCopy && (
|
|
112
153
|
<button
|
|
113
154
|
class:list={[codeBlockCopy(), "absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity"]}
|
|
114
155
|
data-part="copy"
|
|
115
|
-
data-copy-text={
|
|
156
|
+
data-copy-text={copyText}
|
|
116
157
|
type="button"
|
|
117
158
|
aria-label="Copy code"
|
|
118
159
|
>
|
|
@@ -126,7 +167,7 @@ const floatingCopy = copy && !showHeader;
|
|
|
126
167
|
</button>
|
|
127
168
|
)}
|
|
128
169
|
</div>
|
|
129
|
-
</
|
|
170
|
+
</Wrapper>
|
|
130
171
|
|
|
131
172
|
<script>
|
|
132
173
|
function setupCodeBlockCopy() {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A terminal's escape sequences in an `ansi` code block (dogsbay-ssg, 2026-11-04). Shared with
|
|
3
|
+
* @dogsbay/render-html (copied by its sync-sources as `code-ansi.ts`), so both pipelines show and
|
|
4
|
+
* copy the same text.
|
|
5
|
+
*
|
|
6
|
+
* One pattern, linear (each alternative is a run of one character class with a fixed end):
|
|
7
|
+
* - CSI: `ESC [` or the 8-bit `\x9b`, parameters, intermediates, a final byte — or cut off at the
|
|
8
|
+
* end of the text;
|
|
9
|
+
* - OSC (titles, `ESC ] 8` hyperlinks): to BEL or `ESC \`, or the end of the text;
|
|
10
|
+
* - `ESC` + intermediates + a final byte (`ESC ( B`, which `tput sgr0` writes), and any other
|
|
11
|
+
* two-byte escape.
|
|
12
|
+
*/
|
|
13
|
+
const SEQUENCE = /(?:\x1b\[|\x9b)[0-?]*[ -/]*(?:[@-~]|$)|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\|$)|\x1b[ -/]*[0-~]?/g;
|
|
14
|
+
|
|
15
|
+
/** Is this sequence a colour or style (SGR, `ESC [ … m`), the only kind a highlighter draws? */
|
|
16
|
+
const SGR = /^(?:\x1b\[|\x9b)[0-9;:]*m$/;
|
|
17
|
+
|
|
18
|
+
/** The text as a terminal shows it, every escape sequence left out (plain rendering, copy). */
|
|
19
|
+
export function stripAnsi(s: string): string {
|
|
20
|
+
return s.replace(SEQUENCE, "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Only the colour and style sequences kept, for Shiki's `ansi`: it reads `ESC [` as running to the
|
|
25
|
+
* next `m`, so `ESC [2K` (erase line) ate the next letters (`Compiling` → `piling`), and it shows
|
|
26
|
+
* an OSC hyperlink's bytes as text. What is left is what it draws correctly. 8-bit CSI is written
|
|
27
|
+
* in its 7-bit form.
|
|
28
|
+
*/
|
|
29
|
+
export function ansiColoursOnly(s: string): string {
|
|
30
|
+
return s.replace(SEQUENCE, (m) => (SGR.test(m) ? m.replace(/^\x9b/, "\x1b[") : ""));
|
|
31
|
+
}
|