@db-vincent/components 0.0.0-stage → 0.1.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.
Files changed (62) hide show
  1. package/package.json +28 -4
  2. package/src/astro/Accordion.astro +37 -0
  3. package/src/astro/Alert.astro +73 -0
  4. package/src/astro/Badge.astro +23 -0
  5. package/src/astro/Breadcrumbs.astro +45 -0
  6. package/src/astro/Button.astro +125 -0
  7. package/src/astro/Card.astro +63 -0
  8. package/src/astro/Checkbox.astro +61 -0
  9. package/src/astro/Chip.astro +116 -0
  10. package/src/astro/CodeBlock.astro +17 -0
  11. package/src/astro/EmptyState.astro +45 -0
  12. package/src/astro/Example.astro +37 -0
  13. package/src/astro/Grid.astro +50 -0
  14. package/src/astro/GridItem.astro +23 -0
  15. package/src/astro/Icon.astro +52 -0
  16. package/src/astro/Modal.astro +117 -0
  17. package/src/astro/Navbar.astro +756 -0
  18. package/src/astro/PageHeader.astro +34 -0
  19. package/src/astro/Pagination.astro +162 -0
  20. package/src/astro/Radio.astro +58 -0
  21. package/src/astro/RadioGroup.astro +25 -0
  22. package/src/astro/Section.astro +25 -0
  23. package/src/astro/Select.astro +403 -0
  24. package/src/astro/Sidebar.astro +215 -0
  25. package/src/astro/Skeleton.astro +39 -0
  26. package/src/astro/Spinner.astro +19 -0
  27. package/src/astro/Table.astro +254 -0
  28. package/src/astro/TextField.astro +287 -0
  29. package/src/astro/ThemeToggle.astro +66 -0
  30. package/src/astro/Toast.astro +181 -0
  31. package/src/astro/TopBar.astro +29 -0
  32. package/src/data/icons.ts +104 -0
  33. package/src/scripts/toast.ts +531 -0
  34. package/src/styles/accordion.css +77 -0
  35. package/src/styles/alert.css +106 -0
  36. package/src/styles/badge.css +46 -0
  37. package/src/styles/base.css +203 -0
  38. package/src/styles/breadcrumbs.css +52 -0
  39. package/src/styles/button.css +163 -0
  40. package/src/styles/card.css +108 -0
  41. package/src/styles/chip.css +104 -0
  42. package/src/styles/choice.css +137 -0
  43. package/src/styles/code.css +42 -0
  44. package/src/styles/empty.css +57 -0
  45. package/src/styles/example.css +55 -0
  46. package/src/styles/grid.css +62 -0
  47. package/src/styles/input.css +198 -0
  48. package/src/styles/modal.css +143 -0
  49. package/src/styles/motion.css +96 -0
  50. package/src/styles/nav.css +230 -0
  51. package/src/styles/navbar.css +656 -0
  52. package/src/styles/page-header.css +59 -0
  53. package/src/styles/pagination.css +77 -0
  54. package/src/styles/section.css +36 -0
  55. package/src/styles/select.css +235 -0
  56. package/src/styles/skeleton.css +64 -0
  57. package/src/styles/spinner.css +42 -0
  58. package/src/styles/table.css +175 -0
  59. package/src/styles/theme-toggle.css +29 -0
  60. package/src/styles/toast.css +207 -0
  61. package/src/styles/topbar.css +30 -0
  62. package/README.md +0 -3
package/package.json CHANGED
@@ -1,6 +1,30 @@
1
1
  {
2
2
  "name": "@db-vincent/components",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "license": "UNLICENSED",
5
+ "type": "module",
6
+ "description": "Minerva design-system components for Astro. Token-driven, dependency-free and accessibility-checked. (Placeholder scope `@ds`; see AGENTS.md.)",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org"
10
+ },
11
+ "files": [
12
+ "src"
13
+ ],
14
+ "sideEffects": [
15
+ "**/*.css",
16
+ "**/*.astro"
17
+ ],
18
+ "exports": {
19
+ "./astro/*.astro": "./src/astro/*.astro",
20
+ "./data/*": "./src/data/*.ts",
21
+ "./scripts/*": "./src/scripts/*.ts",
22
+ "./styles/*.css": "./src/styles/*.css"
23
+ },
24
+ "peerDependencies": {
25
+ "astro": ">=4"
26
+ },
27
+ "dependencies": {
28
+ "@db-vincent/tokens": "^0.1.0"
29
+ }
30
+ }
@@ -0,0 +1,37 @@
1
+ ---
2
+ import Icon from "./Icon.astro";
3
+ import "../styles/accordion.css";
4
+
5
+ interface Item {
6
+ title: string;
7
+ content: string;
8
+ open?: boolean;
9
+ }
10
+
11
+ /**
12
+ * Accordion built on <details>/<summary>: native keyboard behaviour, native
13
+ * expansion semantics, no script. Pass `name` for an exclusive accordion in
14
+ * browsers that support it.
15
+ */
16
+ interface Props {
17
+ items: Item[];
18
+ name?: string;
19
+ class?: string;
20
+ }
21
+
22
+ const { items, name, class: className } = Astro.props;
23
+ ---
24
+
25
+ <div class:list={["ds-accordion", className]}>
26
+ {items.map((item) => (
27
+ <details class="ds-accordion__item" open={item.open} name={name}>
28
+ <summary class="ds-accordion__trigger">
29
+ <span>{item.title}</span>
30
+ <Icon name="chevron-down" size={20} class="ds-accordion__icon" />
31
+ </summary>
32
+ <div class="ds-accordion__panel">
33
+ <p>{item.content}</p>
34
+ </div>
35
+ </details>
36
+ ))}
37
+ </div>
@@ -0,0 +1,73 @@
1
+ ---
2
+ import Icon from "./Icon.astro";
3
+ import "../styles/alert.css";
4
+
5
+ /**
6
+ * Inline alert. Not a live region by default: a static page-load message is
7
+ * read in document order. Use Toast for asynchronous feedback. `dismissible`
8
+ * adds a close control when the message can be cleared.
9
+ */
10
+ interface Props {
11
+ tone?: "info" | "success" | "warning" | "danger";
12
+ title: string;
13
+ message?: string;
14
+ dismissible?: boolean;
15
+ class?: string;
16
+ }
17
+
18
+ const {
19
+ tone = "info",
20
+ title,
21
+ message,
22
+ dismissible = false,
23
+ class: className,
24
+ } = Astro.props;
25
+ const icon = {
26
+ info: "info",
27
+ success: "check-circle",
28
+ warning: "alert-triangle",
29
+ danger: "alert-circle",
30
+ }[tone];
31
+ ---
32
+
33
+ <div
34
+ class:list={["ds-alert", tone !== "info" && `ds-alert--${tone}`, className]}
35
+ data-alert
36
+ >
37
+ <span class="ds-alert__icon" aria-hidden="true">
38
+ <Icon name={icon} size={20} />
39
+ </span>
40
+ <div class="ds-alert__content">
41
+ <p class="ds-alert__title">{title}</p>
42
+ {message && <p class="ds-alert__body">{message}</p>}
43
+ <slot />
44
+ </div>
45
+ {dismissible && (
46
+ <button
47
+ class="ds-alert__close"
48
+ type="button"
49
+ aria-label="Dismiss alert"
50
+ data-alert-close
51
+ >
52
+ <Icon name="x" size={16} />
53
+ </button>
54
+ )}
55
+ </div>
56
+
57
+ <script>
58
+ document.addEventListener("click", (event) => {
59
+ const target = event.target;
60
+ if (!(target instanceof Element)) return;
61
+ const close = target.closest("[data-alert-close]");
62
+ if (!close) return;
63
+ const alert = close.closest<HTMLElement>("[data-alert]");
64
+ if (!alert) return;
65
+ // Focus would fall to <body>; hand it to the content the alert sat on.
66
+ const host = alert.parentElement;
67
+ alert.remove();
68
+ if (host) {
69
+ if (!host.hasAttribute("tabindex")) host.tabIndex = -1;
70
+ host.focus({ preventScroll: true });
71
+ }
72
+ });
73
+ </script>
@@ -0,0 +1,23 @@
1
+ ---
2
+ import "../styles/badge.css";
3
+
4
+ /** Compact status label. Tone never carries meaning alone; the text says it. */
5
+ interface Props {
6
+ tone?: "neutral" | "accent" | "success" | "warning" | "danger";
7
+ dot?: boolean;
8
+ class?: string;
9
+ }
10
+
11
+ const { tone = "neutral", dot = false, class: className } = Astro.props;
12
+ ---
13
+
14
+ <span
15
+ class:list={[
16
+ "ds-badge",
17
+ tone !== "neutral" && `ds-badge--${tone}`,
18
+ className,
19
+ ]}
20
+ >
21
+ {dot && <span class="ds-badge__dot" aria-hidden="true"></span>}
22
+ <slot />
23
+ </span>
@@ -0,0 +1,45 @@
1
+ ---
2
+ import Icon from "./Icon.astro";
3
+ import "../styles/breadcrumbs.css";
4
+
5
+ interface Item {
6
+ label: string;
7
+ href?: string;
8
+ }
9
+
10
+ interface Props {
11
+ items: Item[];
12
+ label?: string;
13
+ class?: string;
14
+ }
15
+
16
+ const { items, label = "Breadcrumb", class: className } = Astro.props;
17
+ ---
18
+
19
+ <nav class:list={["ds-breadcrumbs", className]} aria-label={label}>
20
+ <ol class="ds-breadcrumbs__list">
21
+ {items.map((item, index) => (
22
+ <li class="ds-breadcrumbs__item">
23
+ {item.href && index < items.length - 1 ? (
24
+ <a class="ds-breadcrumbs__link" href={item.href}>
25
+ {item.label}
26
+ </a>
27
+ ) : (
28
+ <span
29
+ class="ds-breadcrumbs__current"
30
+ aria-current={index === items.length - 1 ? "page" : undefined}
31
+ >
32
+ {item.label}
33
+ </span>
34
+ )}
35
+ {index < items.length - 1 && (
36
+ <Icon
37
+ name="chevron-right"
38
+ size={12}
39
+ class="ds-breadcrumbs__separator"
40
+ />
41
+ )}
42
+ </li>
43
+ ))}
44
+ </ol>
45
+ </nav>
@@ -0,0 +1,125 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import Icon from "./Icon.astro";
4
+ import "../styles/button.css";
5
+
6
+ /**
7
+ * Button. One filled primary action per view; secondary and ghost for the rest.
8
+ * Renders <a> when `href` is set, <button> otherwise. Loading sets aria-busy
9
+ * and disables activation while keeping the label in the accessible name.
10
+ */
11
+ interface Props extends Omit<HTMLAttributes<"button">, "type"> {
12
+ variant?: "primary" | "secondary" | "ghost" | "danger";
13
+ size?: "sm" | "md" | "lg";
14
+ href?: string;
15
+ type?: "button" | "submit" | "reset";
16
+ disabled?: boolean;
17
+ loading?: boolean;
18
+ icon?: string;
19
+ iconPosition?: "start" | "end";
20
+ iconOnly?: boolean;
21
+ block?: boolean;
22
+ ariaLabel?: string;
23
+ class?: string;
24
+ }
25
+
26
+ const {
27
+ variant = "secondary",
28
+ size = "md",
29
+ href,
30
+ type = "button",
31
+ disabled = false,
32
+ loading = false,
33
+ icon,
34
+ iconPosition = "start",
35
+ iconOnly = false,
36
+ block = false,
37
+ ariaLabel,
38
+ class: className,
39
+ // Astro assigns named slots at the call site; forwarding the attribute
40
+ // would leak an inert `slot` onto the rendered element.
41
+ slot: _slot,
42
+ ...rest
43
+ } = Astro.props;
44
+
45
+ const Tag = href ? "a" : "button";
46
+ const inactive = disabled || loading;
47
+ const iconSize = size === "lg" ? 20 : 16;
48
+
49
+ const classes = [
50
+ "ds-button",
51
+ `ds-button--${variant}`,
52
+ size !== "md" && `ds-button--${size}`,
53
+ iconOnly && "ds-button--icon",
54
+ block && "ds-button--block",
55
+ className,
56
+ ]
57
+ .filter(Boolean)
58
+ .join(" ");
59
+
60
+ const tagProps = href
61
+ ? {
62
+ href: disabled ? undefined : href,
63
+ role: disabled ? "link" : undefined,
64
+ "aria-disabled": inactive ? "true" : undefined,
65
+ }
66
+ : {
67
+ type,
68
+ // Loading keeps the button focusable (aria-disabled only) so focus is
69
+ // not dropped mid-submit; the click is blocked in CSS and script.
70
+ disabled: disabled,
71
+ "aria-disabled": inactive ? "true" : undefined,
72
+ };
73
+
74
+ const showLeadingIcon = icon && iconPosition === "start";
75
+ const showTrailingIcon = icon && iconPosition === "end";
76
+ ---
77
+
78
+ <Tag
79
+ class={classes}
80
+ aria-label={ariaLabel}
81
+ aria-busy={loading ? "true" : undefined}
82
+ data-loading={loading ? "true" : undefined}
83
+ {...tagProps}
84
+ {...rest}
85
+ >
86
+ {loading && <span class="ds-button__spinner" aria-hidden="true"></span>}
87
+ {showLeadingIcon && !loading && (
88
+ <span class="ds-button__icon" aria-hidden="true">
89
+ <Icon name={icon} size={iconSize} />
90
+ </span>
91
+ )}
92
+ {!iconOnly && (
93
+ <span class="ds-button__label">
94
+ <slot />
95
+ </span>
96
+ )}
97
+ {iconOnly && !icon && (
98
+ <span class="ds-button__icon">
99
+ <slot />
100
+ </span>
101
+ )}
102
+ {showTrailingIcon && !loading && (
103
+ <span class="ds-button__icon" aria-hidden="true">
104
+ <Icon name={icon} size={iconSize} />
105
+ </span>
106
+ )}
107
+ </Tag>
108
+
109
+ <script>
110
+ // A loading button stays focusable (aria-disabled, not disabled), so its
111
+ // activation is blocked here; pointer events are already off in CSS.
112
+ document.addEventListener(
113
+ "click",
114
+ (event) => {
115
+ const button = (event.target as Element | null)?.closest?.(
116
+ '.ds-button[aria-disabled="true"]',
117
+ );
118
+ if (button) {
119
+ event.preventDefault();
120
+ event.stopImmediatePropagation();
121
+ }
122
+ },
123
+ true,
124
+ );
125
+ </script>
@@ -0,0 +1,63 @@
1
+ ---
2
+ import "../styles/card.css";
3
+
4
+ /**
5
+ * Surface container with distinct header, body and footer. `accent` marks the
6
+ * one card that needs attention with a border on every edge; a linked card is
7
+ * one clickable target and exposes its hover/focus states on the root.
8
+ */
9
+ interface Props {
10
+ variant?: "default" | "accent" | "raised" | "flat";
11
+ href?: string;
12
+ eyebrow?: string;
13
+ title?: string;
14
+ meta?: string;
15
+ class?: string;
16
+ }
17
+
18
+ const {
19
+ variant = "default",
20
+ href,
21
+ eyebrow,
22
+ title,
23
+ meta,
24
+ class: className,
25
+ } = Astro.props;
26
+ const Tag = href ? "a" : "div";
27
+ const classes = [
28
+ "ds-card",
29
+ variant !== "default" && `ds-card--${variant}`,
30
+ href && "ds-card--interactive",
31
+ className,
32
+ ]
33
+ .filter(Boolean)
34
+ .join(" ");
35
+
36
+ const hasHeader =
37
+ Boolean(eyebrow || title || meta) || Astro.slots.has("header");
38
+ const hasFooter = Astro.slots.has("footer");
39
+ ---
40
+
41
+ <Tag class={classes} href={href}>
42
+ {hasHeader && (
43
+ <header class="ds-card__header">
44
+ {Astro.slots.has("header") ? (
45
+ <slot name="header" />
46
+ ) : (
47
+ <>
48
+ {eyebrow && <div class="ds-card__eyebrow">{eyebrow}</div>}
49
+ {title && <div class="ds-card__title">{title}</div>}
50
+ {meta && <div class="ds-card__meta">{meta}</div>}
51
+ </>
52
+ )}
53
+ </header>
54
+ )}
55
+ <div class="ds-card__body">
56
+ <slot />
57
+ </div>
58
+ {hasFooter && (
59
+ <footer class="ds-card__footer">
60
+ <slot name="footer" />
61
+ </footer>
62
+ )}
63
+ </Tag>
@@ -0,0 +1,61 @@
1
+ ---
2
+ import Icon from "./Icon.astro";
3
+ import "../styles/choice.css";
4
+
5
+ /** Checkbox with an optional description. Native input, custom visual box. */
6
+ interface Props {
7
+ label: string;
8
+ description?: string;
9
+ id?: string;
10
+ name?: string;
11
+ value?: string;
12
+ checked?: boolean;
13
+ disabled?: boolean;
14
+ required?: boolean;
15
+ class?: string;
16
+ }
17
+
18
+ const {
19
+ label,
20
+ description,
21
+ id,
22
+ name,
23
+ value,
24
+ checked = false,
25
+ disabled = false,
26
+ required = false,
27
+ class: className,
28
+ } = Astro.props;
29
+
30
+ const inputId = id ?? `checkbox-${Math.random().toString(36).slice(2, 9)}`;
31
+ const descriptionId = description ? `${inputId}-description` : undefined;
32
+ // The name comes from the label text only; the description is announced
33
+ // once, as a description, instead of twice (name + description).
34
+ const labelId = `${inputId}-label`;
35
+ ---
36
+
37
+ <label class:list={["ds-choice", className]}>
38
+ <input
39
+ class="ds-choice__input"
40
+ id={inputId}
41
+ type="checkbox"
42
+ name={name}
43
+ value={value}
44
+ checked={checked}
45
+ disabled={disabled}
46
+ required={required}
47
+ aria-labelledby={description ? labelId : undefined}
48
+ aria-describedby={descriptionId}
49
+ />
50
+ <span class="ds-choice__box" aria-hidden="true">
51
+ <Icon name="check" size={14} />
52
+ </span>
53
+ <span class="ds-choice__label">
54
+ <span id={labelId}>{label}</span>
55
+ {description && (
56
+ <span class="ds-choice__description" id={descriptionId}>
57
+ {description}
58
+ </span>
59
+ )}
60
+ </span>
61
+ </label>
@@ -0,0 +1,116 @@
1
+ ---
2
+ import Icon from "./Icon.astro";
3
+ import "../styles/chip.css";
4
+
5
+ /**
6
+ * Chip. Renders a span by default. `interactive` renders a toggle button with
7
+ * aria-pressed; `removable` adds a separate remove button with its own label.
8
+ */
9
+ interface Props {
10
+ interactive?: boolean;
11
+ selected?: boolean;
12
+ removable?: boolean;
13
+ disabled?: boolean;
14
+ removeLabel?: string;
15
+ href?: string;
16
+ size?: "sm" | "md";
17
+ class?: string;
18
+ }
19
+
20
+ const {
21
+ interactive = false,
22
+ selected = false,
23
+ removable = false,
24
+ disabled = false,
25
+ removeLabel,
26
+ href,
27
+ size = "md",
28
+ class: className,
29
+ } = Astro.props;
30
+
31
+ // The remove button names the item it removes ("Remove 4K"), not just "Remove".
32
+ const text = (await Astro.slots.render("default"))
33
+ .replace(/<[^>]+>/g, "")
34
+ .trim();
35
+ const removeName = removeLabel ?? (text ? `Remove ${text}` : "Remove");
36
+
37
+ const classes = [
38
+ "ds-chip",
39
+ size === "sm" && "ds-chip--sm",
40
+ selected && "is-selected",
41
+ disabled && "is-disabled",
42
+ className,
43
+ ]
44
+ .filter(Boolean)
45
+ .join(" ");
46
+ ---
47
+
48
+ {removable ? (
49
+ <span class={classes}>
50
+ <slot />
51
+ <button
52
+ class="ds-chip__remove"
53
+ type="button"
54
+ aria-label={removeName}
55
+ disabled={disabled}
56
+ data-chip-remove
57
+ >
58
+ <Icon name="x" size={14} />
59
+ </button>
60
+ </span>
61
+ ) : href ? (
62
+ <a class={classes} href={href}>
63
+ <slot />
64
+ </a>
65
+ ) : interactive ? (
66
+ <button
67
+ class={classes}
68
+ type="button"
69
+ aria-pressed={selected ? "true" : "false"}
70
+ disabled={disabled}
71
+ data-chip-toggle
72
+ >
73
+ <Icon name="check" size={14} class="ds-chip__check" />
74
+ <slot />
75
+ </button>
76
+ ) : (
77
+ <span class={classes}>
78
+ <slot />
79
+ </span>
80
+ )}
81
+
82
+ <script>
83
+ document.addEventListener("click", (event) => {
84
+ const target = event.target as HTMLElement;
85
+ const remove = target.closest<HTMLButtonElement>("[data-chip-remove]");
86
+ if (remove) {
87
+ const chip = remove.closest<HTMLElement>(".ds-chip");
88
+ if (!chip) return;
89
+ const focusable = (el: Element | null) =>
90
+ el?.matches("[data-chip-toggle]")
91
+ ? (el as HTMLElement)
92
+ : el?.querySelector<HTMLElement>(
93
+ "[data-chip-remove], [data-chip-toggle]",
94
+ );
95
+ const target =
96
+ focusable(chip.nextElementSibling) ??
97
+ focusable(chip.previousElementSibling);
98
+ const fallback = chip.parentElement;
99
+ chip.remove();
100
+ if (target) {
101
+ target.focus();
102
+ } else if (fallback) {
103
+ fallback.tabIndex = -1;
104
+ fallback.focus();
105
+ }
106
+ return;
107
+ }
108
+
109
+ const toggle = target.closest<HTMLButtonElement>("[data-chip-toggle]");
110
+ if (!toggle || toggle.disabled) return;
111
+ toggle.setAttribute(
112
+ "aria-pressed",
113
+ toggle.getAttribute("aria-pressed") === "true" ? "false" : "true",
114
+ );
115
+ });
116
+ </script>
@@ -0,0 +1,17 @@
1
+ ---
2
+ import "../styles/code.css";
3
+
4
+ /** Code sample. Renders the exact source passed in; no highlighting tricks. */
5
+ interface Props {
6
+ code?: string;
7
+ label?: string;
8
+ class?: string;
9
+ }
10
+
11
+ const { code, label, class: className } = Astro.props;
12
+ ---
13
+
14
+ <figure class:list={["ds-code", className]}>
15
+ {label && <figcaption class="ds-code__label">{label}</figcaption>}
16
+ <pre class="ds-code__pre"><code>{code ?? <slot />}</code></pre>
17
+ </figure>
@@ -0,0 +1,45 @@
1
+ ---
2
+ import Icon from "./Icon.astro";
3
+ import Skeleton from "./Skeleton.astro";
4
+ import "../styles/empty.css";
5
+
6
+ /**
7
+ * Empty state. Says what is missing, why it matters, and the next action.
8
+ * `dashed` frames the region; `skeleton` previews the shape of the content
9
+ * that will replace it.
10
+ */
11
+ interface Props {
12
+ icon?: string;
13
+ title: string;
14
+ message: string;
15
+ dashed?: boolean;
16
+ skeleton?: boolean;
17
+ class?: string;
18
+ }
19
+
20
+ const {
21
+ icon = "inbox",
22
+ title,
23
+ message,
24
+ dashed = false,
25
+ skeleton = false,
26
+ class: className,
27
+ } = Astro.props;
28
+ ---
29
+
30
+ <div class:list={["ds-empty", dashed && "ds-empty--dashed", className]}>
31
+ <span class="ds-empty__icon" aria-hidden="true">
32
+ <Icon name={icon} size={24} />
33
+ </span>
34
+ {skeleton && (
35
+ <div class="ds-empty__skeleton" aria-hidden="true">
36
+ <Skeleton variant="heading" />
37
+ <Skeleton lines={2} />
38
+ </div>
39
+ )}
40
+ <h3 class="ds-empty__title">{title}</h3>
41
+ <p class="ds-empty__body">{message}</p>
42
+ <div class="ds-empty__actions">
43
+ <slot />
44
+ </div>
45
+ </div>
@@ -0,0 +1,37 @@
1
+ ---
2
+ import CodeBlock from "./CodeBlock.astro";
3
+ import "../styles/example.css";
4
+
5
+ /**
6
+ * A live example and the exact source that produced it. Every specimen in the
7
+ * explorer goes through this component so no demo can drift from its code.
8
+ */
9
+ interface Props {
10
+ title: string;
11
+ code: string;
12
+ description?: string;
13
+ flush?: boolean;
14
+ class?: string;
15
+ }
16
+
17
+ const {
18
+ title,
19
+ code,
20
+ description,
21
+ flush = false,
22
+ class: className,
23
+ } = Astro.props;
24
+ ---
25
+
26
+ <figure class:list={["ds-example", className]}>
27
+ <figcaption class="ds-example__caption">
28
+ <span class="ds-example__title">{title}</span>
29
+ {description && <span class="ds-example__description">{description}</span>}
30
+ </figcaption>
31
+ <div
32
+ class:list={["ds-example__preview", flush && "ds-example__preview--flush"]}
33
+ >
34
+ <slot />
35
+ </div>
36
+ <CodeBlock label="Astro" code={code} />
37
+ </figure>