@dogsbay/ui 0.2.0-beta.115 → 0.2.0-beta.116
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 +4 -4
- package/src/badge/Badge.astro +3 -10
- package/src/badge/variants.ts +37 -0
- package/src/code-block/CodeBlock.astro +28 -7
- package/src/code-rich/CodeRich.astro +27 -7
- package/src/code-rich/code-rich.css +3 -1
- package/src/code-rich/markers.ts +2 -0
- package/src/content-renderer/ContentRenderer.astro +8 -1
- package/src/content-renderer/InlineRenderer.astro +5 -0
- package/src/files/FileTree.astro +16 -0
- package/src/files/index.ts +2 -0
- package/src/files/markup.ts +34 -0
- package/src/kbd/Kbd.astro +2 -3
- package/src/kbd/variants.ts +3 -0
- package/src/link-card/LinkCard.astro +8 -2
- package/src/sidebar/sidebar.ts +2 -2
- package/src/table/Table.astro +14 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/ui",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.116",
|
|
4
4
|
"description": "Accessible UI components for Astro, inspired by Base UI and shadcn",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
16
|
"astro": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
17
|
-
"@dogsbay/
|
|
18
|
-
"@dogsbay/
|
|
19
|
-
"@dogsbay/
|
|
17
|
+
"@dogsbay/elements": "0.2.0-beta.116",
|
|
18
|
+
"@dogsbay/icons": "0.2.0-beta.116",
|
|
19
|
+
"@dogsbay/primitives": "0.2.0-beta.116"
|
|
20
20
|
},
|
|
21
21
|
"optionalDependencies": {
|
|
22
22
|
"@shikijs/transformers": "^4.3.1",
|
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,37 @@
|
|
|
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
|
+
const v = BADGE_VARIANTS[(variant ?? "default") as BadgeVariant];
|
|
36
|
+
return v ? `${BADGE_BASE} ${v}` : BADGE_BASE;
|
|
37
|
+
}
|
|
@@ -21,6 +21,10 @@ interface Props {
|
|
|
21
21
|
lang?: string;
|
|
22
22
|
/** Optional title displayed in the header bar */
|
|
23
23
|
title?: string;
|
|
24
|
+
/*
|
|
25
|
+
* A `caption` slot makes the block a figure: the header bar is its
|
|
26
|
+
* <figcaption>, showing the caption with the language beside it.
|
|
27
|
+
*/
|
|
24
28
|
/** Show copy button (default: true) */
|
|
25
29
|
copy?: boolean;
|
|
26
30
|
/** Show the header title bar. true = always, false = never, "auto" = only when title prop is set (default: true) */
|
|
@@ -55,22 +59,38 @@ const shikiLang = LANG_ALIASES[lang.toLowerCase()] ?? lang.toLowerCase();
|
|
|
55
59
|
// - true: show when there's a title or a declared language (default, original behavior)
|
|
56
60
|
// - false: never show
|
|
57
61
|
// - "auto": only show when an explicit title is provided (not lang-only)
|
|
62
|
+
const hasCaption = Astro.slots.has("caption");
|
|
63
|
+
const Wrapper = hasCaption ? "figure" : "div";
|
|
64
|
+
const Header = hasCaption ? "figcaption" : "div";
|
|
58
65
|
const showHeader =
|
|
59
|
-
|
|
66
|
+
hasCaption ? true
|
|
67
|
+
: showTitle === false ? false
|
|
60
68
|
: showTitle === "auto" ? !!title
|
|
61
69
|
: !!title || lang !== "plaintext";
|
|
62
70
|
// When no header, copy button floats inside the code body
|
|
63
71
|
const floatingCopy = copy && !showHeader;
|
|
64
72
|
---
|
|
65
73
|
|
|
66
|
-
<
|
|
67
|
-
class={codeBlockWrapper({ class: className })}
|
|
74
|
+
<Wrapper
|
|
75
|
+
class={codeBlockWrapper({ class: [hasCaption && "m-0", className] })}
|
|
68
76
|
data-component="code-block"
|
|
69
77
|
{...rest}
|
|
70
78
|
>
|
|
71
79
|
{showHeader && (
|
|
72
|
-
<
|
|
73
|
-
|
|
80
|
+
<Header class={codeBlockHeader()}>
|
|
81
|
+
{/*
|
|
82
|
+
Left: what the code is (caption or filename title), or the language when that is
|
|
83
|
+
all there is. Right: the language beside the copy button, as secondary detail
|
|
84
|
+
*/}
|
|
85
|
+
{hasCaption ? (
|
|
86
|
+
<span class="min-w-0 font-medium text-code-fg" data-part="caption"><slot name="caption" /></span>
|
|
87
|
+
) : title ? (
|
|
88
|
+
<span class="min-w-0 font-medium text-code-fg" data-part="title">{title}</span>
|
|
89
|
+
) : (
|
|
90
|
+
<span class="select-none">{lang}</span>
|
|
91
|
+
)}
|
|
92
|
+
<span class="flex shrink-0 items-center gap-2">
|
|
93
|
+
{(hasCaption || title) && lang !== "plaintext" && <span class="select-none" data-part="language">{lang}</span>}
|
|
74
94
|
{copy && (
|
|
75
95
|
<button
|
|
76
96
|
class={codeBlockCopy()}
|
|
@@ -113,7 +133,8 @@ const floatingCopy = copy && !showHeader;
|
|
|
113
133
|
</svg>
|
|
114
134
|
</button>
|
|
115
135
|
)}
|
|
116
|
-
|
|
136
|
+
</span>
|
|
137
|
+
</Header>
|
|
117
138
|
)}
|
|
118
139
|
<div class="relative overflow-x-auto [&_pre]:!m-0 [&_pre]:!rounded-none [&_pre]:!border-0 [&_pre]:px-4 [&_pre]:py-3">
|
|
119
140
|
<Code code={code} lang={shikiLang} themes={{ light: "github-light-default", dark: theme }} defaultColor={false} />
|
|
@@ -135,7 +156,7 @@ const floatingCopy = copy && !showHeader;
|
|
|
135
156
|
</button>
|
|
136
157
|
)}
|
|
137
158
|
</div>
|
|
138
|
-
</
|
|
159
|
+
</Wrapper>
|
|
139
160
|
|
|
140
161
|
<script>
|
|
141
162
|
function setupCodeBlockCopy() {
|
|
@@ -33,6 +33,10 @@ interface Props {
|
|
|
33
33
|
lang?: string;
|
|
34
34
|
/** Optional title/filename displayed in the header bar */
|
|
35
35
|
title?: string;
|
|
36
|
+
/*
|
|
37
|
+
* A `caption` slot makes the block a figure: the header bar is its
|
|
38
|
+
* <figcaption>, showing the caption with the language beside it.
|
|
39
|
+
*/
|
|
36
40
|
/** Show copy button (default: true) */
|
|
37
41
|
copy?: boolean;
|
|
38
42
|
/** Show line numbers (default: false) */
|
|
@@ -212,21 +216,36 @@ const html = await codeToHtml(highlightCode, {
|
|
|
212
216
|
});
|
|
213
217
|
|
|
214
218
|
// Show header bar only when there's a title or a declared language
|
|
215
|
-
const
|
|
219
|
+
const hasCaption = Astro.slots.has("caption");
|
|
220
|
+
const Wrapper = hasCaption ? "figure" : "div";
|
|
221
|
+
const Header = hasCaption ? "figcaption" : "div";
|
|
222
|
+
const hasLabel = hasCaption || !!title || (lang !== "plaintext");
|
|
216
223
|
const showHeader = hasLabel;
|
|
217
224
|
// When no header, copy button floats inside the code body
|
|
218
225
|
const floatingCopy = copy && !showHeader;
|
|
219
226
|
---
|
|
220
227
|
|
|
221
|
-
<
|
|
222
|
-
class={codeRichWrapper({ class: className })}
|
|
228
|
+
<Wrapper
|
|
229
|
+
class={codeRichWrapper({ class: [hasCaption && "m-0", className] })}
|
|
223
230
|
data-component="code-rich"
|
|
224
231
|
data-line-numbers={lineNumbers || undefined}
|
|
225
232
|
{...rest}
|
|
226
233
|
>
|
|
227
234
|
{showHeader && (
|
|
228
|
-
<
|
|
229
|
-
|
|
235
|
+
<Header class={codeRichHeader()}>
|
|
236
|
+
{/*
|
|
237
|
+
Left: what the code is (caption or filename title), or the language when that is
|
|
238
|
+
all there is. Right: the language beside the copy button, as secondary detail
|
|
239
|
+
*/}
|
|
240
|
+
{hasCaption ? (
|
|
241
|
+
<span class="min-w-0 font-medium text-code-fg" data-part="caption"><slot name="caption" /></span>
|
|
242
|
+
) : title ? (
|
|
243
|
+
<span class="min-w-0 font-medium text-code-fg" data-part="title">{title}</span>
|
|
244
|
+
) : (
|
|
245
|
+
<span class="select-none">{lang}</span>
|
|
246
|
+
)}
|
|
247
|
+
<span class="flex shrink-0 items-center gap-2">
|
|
248
|
+
{(hasCaption || title) && lang !== "plaintext" && <span class="select-none" data-part="language">{lang}</span>}
|
|
230
249
|
{copy && (
|
|
231
250
|
<button
|
|
232
251
|
class={codeRichCopy()}
|
|
@@ -269,7 +288,8 @@ const floatingCopy = copy && !showHeader;
|
|
|
269
288
|
</svg>
|
|
270
289
|
</button>
|
|
271
290
|
)}
|
|
272
|
-
|
|
291
|
+
</span>
|
|
292
|
+
</Header>
|
|
273
293
|
)}
|
|
274
294
|
<div class="code-rich-body relative overflow-x-auto [&_pre]:!m-0 [&_pre]:!rounded-none [&_pre]:!border-0 [&_pre]:!bg-transparent [&_pre]:px-4 [&_pre]:py-3">
|
|
275
295
|
<Fragment set:html={html} />
|
|
@@ -291,7 +311,7 @@ const floatingCopy = copy && !showHeader;
|
|
|
291
311
|
</button>
|
|
292
312
|
)}
|
|
293
313
|
</div>
|
|
294
|
-
</
|
|
314
|
+
</Wrapper>
|
|
295
315
|
|
|
296
316
|
<script>
|
|
297
317
|
function setupCodeRichCopy() {
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
/* ── All lines ────────────────────────────────────────── */
|
|
4
4
|
.code-rich-body .line {
|
|
5
|
-
|
|
5
|
+
/* inline-block, not inline-flex: a flex line makes each token span a flex item, and
|
|
6
|
+
browsers then copy a hand-made selection with a line break between tokens. */
|
|
7
|
+
display: inline-block;
|
|
6
8
|
width: 100%;
|
|
7
9
|
}
|
|
8
10
|
|
package/src/code-rich/markers.ts
CHANGED
|
@@ -36,6 +36,8 @@ const BARE_MARKER = /[ \t]*\[!code[^\]]*\]/g;
|
|
|
36
36
|
* @param isPlainGrammar true when highlighting falls back to plaintext
|
|
37
37
|
*/
|
|
38
38
|
export function stripUnprocessableMarkers(code: string, isPlainGrammar: boolean): string {
|
|
39
|
+
// Every marker has "[!code"; without one there is nothing to strip, and MARKER's leading [ \t]* is quadratic on a long run of spaces.
|
|
40
|
+
if (!code.includes("[!code")) return code;
|
|
39
41
|
return code
|
|
40
42
|
.split("\n")
|
|
41
43
|
.map((line) => {
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
* api-params, api-symbol, api-doc, api-source, youtube, image, grid
|
|
30
30
|
*/
|
|
31
31
|
import Alert from "../alert/Alert.astro";
|
|
32
|
+
import { badgeClass } from "../badge/variants.js";
|
|
33
|
+
|
|
34
|
+
/** Attribute-safe text (also safe as element text). */
|
|
35
|
+
const escAttr = (s: string) => s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
32
36
|
import AlertTitle from "../alert/AlertTitle.astro";
|
|
33
37
|
import AlertDescription from "../alert/AlertDescription.astro";
|
|
34
38
|
import CollapsibleAlert from "../alert/CollapsibleAlert.astro";
|
|
@@ -128,6 +132,7 @@ function inlineToHtml(nodes: InlineNode[]): string {
|
|
|
128
132
|
case "link": return `<a href="${n.href || ""}">${inlineToHtml((n.children || []) as InlineNode[])}</a>`;
|
|
129
133
|
case "image": return `<img src="${withBaseUrl(n.src as string | undefined)}" alt="${n.alt || ""}">`;
|
|
130
134
|
case "kbd": return (n.keys as string[] || []).map((k: string) => `<kbd>${k}</kbd>`).join("+");
|
|
135
|
+
case "badge": return `<span class="${badgeClass(n.variant as string | undefined)}" data-variant="${escAttr(String(n.variant || "default"))}">${escAttr(String(n.text ?? ""))}</span>`;
|
|
131
136
|
case "html-inline": return String(n.html || "");
|
|
132
137
|
case "break": return "<br>";
|
|
133
138
|
default: return "";
|
|
@@ -387,7 +392,9 @@ function withBaseUrl(src: string | undefined): string {
|
|
|
387
392
|
<li id={(node.props?.id as string) || ""} class="text-sm text-muted-foreground">
|
|
388
393
|
{node.inline
|
|
389
394
|
? <span>{node.inline.map(span => <InlineRenderer node={span} icons={icons} images={images} />)}</span>
|
|
390
|
-
:
|
|
395
|
+
: children.length > 0
|
|
396
|
+
? children.map(child => <Astro.self node={child} images={images} icons={icons} />)
|
|
397
|
+
: <Fragment set:html={node.html} />
|
|
391
398
|
}
|
|
392
399
|
{" "}<a href={`#${node.props?.backrefId || ""}`} class="text-primary hover:underline" aria-label={`Back to reference ${node.props?.label}`}>↩</a>
|
|
393
400
|
</li>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* InlineRenderer — renders structured InlineNode[] to HTML elements.
|
|
4
4
|
*/
|
|
5
5
|
import MathKatex from "../math/MathKatex.astro";
|
|
6
|
+
import Badge from "../badge/Badge.astro";
|
|
6
7
|
|
|
7
8
|
interface InlineNode {
|
|
8
9
|
type: string;
|
|
@@ -135,6 +136,10 @@ const isEmojiIcon = node.type === "icon" && (node.library === "emoji" || (iconSv
|
|
|
135
136
|
</a>
|
|
136
137
|
)}
|
|
137
138
|
|
|
139
|
+
{node.type === "badge" && (
|
|
140
|
+
<Badge variant={(node as { variant?: string }).variant ?? "default"}>{node.text}</Badge>
|
|
141
|
+
)}
|
|
142
|
+
|
|
138
143
|
{node.type === "kbd" && (
|
|
139
144
|
<span class="inline-flex items-center gap-0.5">
|
|
140
145
|
{(node.keys || []).map((key, i) => (
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* FileTree — the `:::files` directive: a nested list shown as folders and files.
|
|
4
|
+
* The markup is fileTreeHtml (markup.ts), shared with dogsbay-ssg.
|
|
5
|
+
*/
|
|
6
|
+
import { fileTreeHtml, type FileTreeItem } from "./markup.js";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
items: FileTreeItem[];
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { items, class: className } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<Fragment set:html={fileTreeHtml(items, className ?? "")} />
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `:::files` tree, written once for both pipelines: FileTree.astro renders it,
|
|
3
|
+
* and dogsbay-ssg copies this file (scripts/sync-astro-styles.mjs) and calls it.
|
|
4
|
+
* Each pipeline turns an entry's inline Markdown into `label` HTML itself.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface FileTreeItem {
|
|
8
|
+
/** The entry's label as HTML (already escaped). */
|
|
9
|
+
label: string;
|
|
10
|
+
/** A folder: it has entries, or its name ends in "/". */
|
|
11
|
+
folder: boolean;
|
|
12
|
+
children: FileTreeItem[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const svg = (body: string) =>
|
|
16
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0 text-muted-foreground" aria-hidden="true" focusable="false">${body}</svg>`;
|
|
17
|
+
// Lucide folder and file (ISC licence).
|
|
18
|
+
const FOLDER = svg('<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>');
|
|
19
|
+
const FILE = svg('<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>');
|
|
20
|
+
|
|
21
|
+
function list(items: FileTreeItem[], nested: boolean): string {
|
|
22
|
+
const cls = nested ? "m-0 ml-2 list-none border-l border-border pl-4" : "m-0 list-none p-0";
|
|
23
|
+
return `<ul class="${cls}">${items
|
|
24
|
+
.map(
|
|
25
|
+
(it) =>
|
|
26
|
+
`<li class="my-0.5" data-kind="${it.folder ? "folder" : "file"}"><span class="inline-flex items-center gap-1.5">${it.folder ? FOLDER : FILE}<span>${it.label}</span></span>${it.children.length ? list(it.children, true) : ""}</li>`,
|
|
27
|
+
)
|
|
28
|
+
.join("")}</ul>`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The whole tree, in a bordered, monospaced box. */
|
|
32
|
+
export function fileTreeHtml(items: FileTreeItem[], extraClass = ""): string {
|
|
33
|
+
return `<div class="not-prose my-4 overflow-x-auto rounded-lg border bg-card p-3 font-mono text-sm${extraClass ? ` ${extraClass}` : ""}" data-component="files">${list(items, false)}</div>`;
|
|
34
|
+
}
|
package/src/kbd/Kbd.astro
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { tv } from "tailwind-variants";
|
|
3
|
+
import { KBD_CLASS } from "./variants.js";
|
|
3
4
|
|
|
4
|
-
const kbd = tv({
|
|
5
|
-
base: "pointer-events-none inline-flex h-5 items-center justify-center gap-1 rounded border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground shadow-[0_1px_0_1px] shadow-muted",
|
|
6
|
-
});
|
|
5
|
+
const kbd = tv({ base: KBD_CLASS });
|
|
7
6
|
|
|
8
7
|
interface Props {
|
|
9
8
|
class?: string;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Kbd.astro's classes, shared with dogsbay-ssg (copied by scripts/sync-astro-styles.mjs). */
|
|
2
|
+
export const KBD_CLASS =
|
|
3
|
+
"pointer-events-none inline-flex h-5 items-center justify-center gap-1 rounded border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground shadow-[0_1px_0_1px] shadow-muted";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { tv } from "tailwind-variants";
|
|
3
|
+
import Icon from "../icon/Icon.astro";
|
|
3
4
|
|
|
4
5
|
const linkCard = tv({
|
|
5
6
|
base: "group flex flex-col gap-1 rounded-lg border bg-card p-4 text-card-foreground shadow-sm transition-colors hover:bg-accent/50",
|
|
@@ -17,16 +18,21 @@ interface Props {
|
|
|
17
18
|
title: string;
|
|
18
19
|
description?: string;
|
|
19
20
|
href: string;
|
|
21
|
+
/** An icon before the title: a name (`rocket`, `mdi:home`) as the Icon component takes. */
|
|
22
|
+
icon?: string;
|
|
20
23
|
class?: string;
|
|
21
24
|
[key: string]: unknown;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
const { title, description, href, class: className, ...rest } = Astro.props;
|
|
27
|
+
const { title, description, href, icon, class: className, ...rest } = Astro.props;
|
|
25
28
|
---
|
|
26
29
|
|
|
27
30
|
<a href={href} class={linkCard({ class: className })} {...rest}>
|
|
28
31
|
<div class="flex items-center justify-between">
|
|
29
|
-
<span class=
|
|
32
|
+
<span class="flex min-w-0 items-center gap-2">
|
|
33
|
+
{icon && <Icon name={icon} class="size-4 shrink-0 text-muted-foreground" />}
|
|
34
|
+
<span class={linkCardTitle()}>{title}</span>
|
|
35
|
+
</span>
|
|
30
36
|
<svg
|
|
31
37
|
xmlns="http://www.w3.org/2000/svg"
|
|
32
38
|
width="16"
|
package/src/sidebar/sidebar.ts
CHANGED
|
@@ -202,7 +202,7 @@ function openMobile(provider: HTMLElement) {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
// Lock scroll
|
|
205
|
-
ScrollLock.
|
|
205
|
+
ScrollLock.activate();
|
|
206
206
|
|
|
207
207
|
// Focus first focusable element in sidebar
|
|
208
208
|
requestAnimationFrame(() => {
|
|
@@ -237,7 +237,7 @@ function closeMobile(provider: HTMLElement) {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
// Unlock scroll
|
|
240
|
-
ScrollLock.
|
|
240
|
+
ScrollLock.deactivate();
|
|
241
241
|
|
|
242
242
|
// Return focus to trigger
|
|
243
243
|
const trigger = instance.provider.querySelector(
|
package/src/table/Table.astro
CHANGED
|
@@ -6,22 +6,33 @@ const tableContainer = tv({
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
const table = tv({
|
|
9
|
-
base: "w-full caption-
|
|
9
|
+
base: "w-full caption-top text-sm",
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
class?: string;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Optional table caption, rendered as a `<caption>`: the table's title,
|
|
16
|
+
* above it and left-aligned.
|
|
17
|
+
* For a caption with markup, use the `caption` slot instead.
|
|
18
|
+
*/
|
|
15
19
|
caption?: string;
|
|
16
20
|
[key: string]: unknown;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
const { class: className, caption, ...rest } = Astro.props;
|
|
24
|
+
|
|
25
|
+
// A table's caption is its title: above the table, left-aligned, in the body color
|
|
26
|
+
const captionClass = "mb-2 text-left text-sm font-medium text-foreground";
|
|
20
27
|
---
|
|
21
28
|
|
|
22
29
|
<div class={tableContainer()}>
|
|
23
30
|
<table class={table({ class: className })} {...rest}>
|
|
24
|
-
{caption
|
|
31
|
+
{Astro.slots.has("caption") ? (
|
|
32
|
+
<caption class={captionClass}><slot name="caption" /></caption>
|
|
33
|
+
) : (
|
|
34
|
+
caption && <caption class={captionClass}>{caption}</caption>
|
|
35
|
+
)}
|
|
25
36
|
<slot />
|
|
26
37
|
</table>
|
|
27
38
|
</div>
|