@xtyle/astro 0.5.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 (68) hide show
  1. package/README.md +42 -0
  2. package/package.json +25 -0
  3. package/src/Accordion.astro +79 -0
  4. package/src/Alert.astro +57 -0
  5. package/src/AppShell.astro +97 -0
  6. package/src/Avatar.astro +71 -0
  7. package/src/AvatarGroup.astro +44 -0
  8. package/src/Badge.astro +73 -0
  9. package/src/Bar.astro +83 -0
  10. package/src/Breadcrumb.astro +54 -0
  11. package/src/Button.astro +91 -0
  12. package/src/Card.astro +44 -0
  13. package/src/CardLink.astro +60 -0
  14. package/src/Carousel.astro +39 -0
  15. package/src/Checkbox.astro +68 -0
  16. package/src/CheckboxGroup.astro +54 -0
  17. package/src/Cluster.astro +46 -0
  18. package/src/Code.astro +42 -0
  19. package/src/ColorPicker.astro +184 -0
  20. package/src/Dialog.astro +69 -0
  21. package/src/Dock.astro +79 -0
  22. package/src/DockZone.astro +17 -0
  23. package/src/Eyebrow.astro +30 -0
  24. package/src/Field.astro +117 -0
  25. package/src/FormGroup.astro +71 -0
  26. package/src/Grid.astro +57 -0
  27. package/src/Heading.astro +34 -0
  28. package/src/Heatmap.astro +116 -0
  29. package/src/Hero.astro +19 -0
  30. package/src/Icon.astro +53 -0
  31. package/src/Image.astro +56 -0
  32. package/src/Kbd.astro +27 -0
  33. package/src/Link.astro +56 -0
  34. package/src/Menu.astro +38 -0
  35. package/src/NumberInput.astro +86 -0
  36. package/src/Pagination.astro +53 -0
  37. package/src/Panel.astro +68 -0
  38. package/src/Parallax.astro +18 -0
  39. package/src/Pie.astro +65 -0
  40. package/src/Progress.astro +96 -0
  41. package/src/Radio.astro +71 -0
  42. package/src/RadioGroup.astro +44 -0
  43. package/src/Section.astro +61 -0
  44. package/src/Segment.astro +15 -0
  45. package/src/Segmented.astro +95 -0
  46. package/src/Select.astro +79 -0
  47. package/src/Separator.astro +36 -0
  48. package/src/Skeleton.astro +25 -0
  49. package/src/Slider.astro +93 -0
  50. package/src/Sparkline.astro +79 -0
  51. package/src/Spinner.astro +37 -0
  52. package/src/Splitter.astro +87 -0
  53. package/src/Stack.astro +42 -0
  54. package/src/Stat.astro +70 -0
  55. package/src/Statusbar.astro +56 -0
  56. package/src/Swatch.astro +69 -0
  57. package/src/Switch.astro +89 -0
  58. package/src/Table.astro +46 -0
  59. package/src/Tabs.astro +94 -0
  60. package/src/Text.astro +54 -0
  61. package/src/Textarea.astro +81 -0
  62. package/src/Toast.astro +62 -0
  63. package/src/ToastRegion.astro +39 -0
  64. package/src/Toc.astro +35 -0
  65. package/src/Toolbar.astro +68 -0
  66. package/src/Tooltip.astro +68 -0
  67. package/src/Tree.astro +78 -0
  68. package/src/internal/palette.ts +92 -0
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @xtyle/astro
2
+
3
+ [![npm](https://img.shields.io/npm/v/@xtyle/astro?label=npm)](https://www.npmjs.com/package/@xtyle/astro)
4
+ [![docs](https://img.shields.io/badge/docs-xtyle.dev-blue)](https://xtyle.dev)
5
+ [![license](https://img.shields.io/badge/license-MIT-green)](https://opensource.org/license/mit)
6
+
7
+ Zero-JS Astro 6 components that emit semantic HTML against the xtyle component classes,
8
+ the SSR binding of the [`@xtyle/core`](https://www.npmjs.com/package/@xtyle/core) component
9
+ contract. Each component server-renders the matching markup and ships **no client
10
+ JavaScript** unless that component genuinely needs it.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm install @xtyle/astro @xtyle/core
16
+ ```
17
+
18
+ `astro@^6` is a peer dependency; `@xtyle/core` (pulled in automatically) provides the
19
+ markup contract and the derivation engine.
20
+
21
+ ## Usage
22
+
23
+ ```astro
24
+ ---
25
+ import Button from "@xtyle/astro/Button.astro";
26
+ import Card from "@xtyle/astro/Card.astro";
27
+ ---
28
+
29
+ <Card>
30
+ <Button tone="accent">Save</Button>
31
+ </Card>
32
+ ```
33
+
34
+ Components are styled entirely by **design tokens**, the CSS custom properties a xtyle
35
+ *algorithm* derives. Emit a theme to CSS (via the `xtyle` CLI in `@xtyle/core`) and drop it
36
+ on `:root`; every component themes with it, with nothing running at runtime. See
37
+ [`@xtyle/core`](https://www.npmjs.com/package/@xtyle/core) for deriving a theme, and
38
+ [xtyle.dev](https://xtyle.dev) for the full component reference.
39
+
40
+ ## License
41
+
42
+ MIT
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@xtyle/astro",
3
+ "version": "0.5.0",
4
+ "description": "Zero-JS Astro components that emit semantic HTML against the xtyle component classes; the SSR binding of the component contract.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "exports": {
8
+ "./*": "./src/*"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "dependencies": {
15
+ "@xtyle/core": "^0.5.0"
16
+ },
17
+ "peerDependencies": {
18
+ "astro": "^6.0.0"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/nekoyoubi/xtyle.git",
23
+ "directory": "packages/astro"
24
+ }
25
+ }
@@ -0,0 +1,79 @@
1
+ ---
2
+ import { type AccordionSection } from "@xtyle/core/markup";
3
+ import { renderFragmentLight } from "@xtyle/core/elements/ssr";
4
+
5
+ type Size = "sm" | "md" | "lg";
6
+
7
+ interface Props {
8
+ items?: AccordionSection[];
9
+ multiple?: boolean;
10
+ size?: Size;
11
+ headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
12
+ /** Render zero-JS: emit the declarative shadow but never load the runtime to hydrate it. */
13
+ static?: boolean;
14
+ class?: string;
15
+ /** Any other attribute (`id`, `title`, `data-*`, `aria-*`, …) passes through to the element. */
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ const {
20
+ items,
21
+ multiple = false,
22
+ size = "md",
23
+ headingLevel,
24
+ static: isStatic = false,
25
+ class: extra,
26
+ ...rest
27
+ } = Astro.props;
28
+
29
+ const sections = items ?? [];
30
+ const uid = `xtyle-accordion-${Math.random().toString(36).slice(2, 9)}`;
31
+
32
+ const openKeys: string[] = [];
33
+ sections.forEach((section, i) => {
34
+ if (!section.open) return;
35
+ const key = section.value ?? String(i);
36
+ if (multiple) openKeys.push(key);
37
+ else if (openKeys.length === 0) openKeys.push(key);
38
+ });
39
+
40
+ const bindings = {
41
+ sections: sections.map((section, i) => ({
42
+ header: section.header,
43
+ panel: section.panel,
44
+ panelSlot: section.panelSlot,
45
+ value: section.value ?? String(i),
46
+ disabled: section.disabled,
47
+ })),
48
+ openKeys,
49
+ size,
50
+ headingLevel,
51
+ uid,
52
+ };
53
+ // Two authoring modes. items-mode renders the whole structure light from `bindings` and ships no
54
+ // children. The advanced slotted mode pairs the consumer's `slot="header"`/`slot="panel"` children
55
+ // by order, so they must survive *individually* (with their slot attributes) — Astro's `<slot
56
+ // name>` passthrough keeps them intact, and the element builds the structure from them on connect.
57
+ const useItems = sections.length > 0;
58
+ const light = useItems ? await renderFragmentLight("accordion", bindings) : "";
59
+ ---
60
+
61
+ {useItems ? (
62
+ <xtyle-accordion
63
+ multiple={multiple || undefined}
64
+ size={size}
65
+ heading-level={headingLevel}
66
+ items={JSON.stringify(items)}
67
+ class={extra}
68
+ set:html={light}
69
+ {...rest}
70
+ />
71
+ ) : (
72
+ <xtyle-accordion multiple={multiple || undefined} size={size} heading-level={headingLevel} class={extra} {...rest}>
73
+ <slot name="header" />
74
+ <slot name="panel" />
75
+ <slot />
76
+ </xtyle-accordion>
77
+ )}
78
+
79
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
@@ -0,0 +1,57 @@
1
+ ---
2
+ import { renderFragmentLight, composeFallbackSlot } from "@xtyle/core/elements/ssr";
3
+ import type { FullTone } from "@xtyle/core";
4
+
5
+ type AlertSeverity = "success" | "warn" | "danger" | "info";
6
+ type AlertVariant = "soft" | "solid";
7
+
8
+ interface Props {
9
+ /** Color — any semantic role, accent variant, or named hue. Defaults to the severity color. */
10
+ tone?: FullTone;
11
+ /** Meaning — drives the glyph + politeness, independent of color. Omit for a color-only notice. */
12
+ severity?: AlertSeverity;
13
+ variant?: AlertVariant;
14
+ dismissible?: boolean;
15
+ dismissLabel?: string;
16
+ /** Render zero-JS: emit the resolved light-DOM markup but never load the runtime to hydrate it. */
17
+ static?: boolean;
18
+ class?: string;
19
+ /** Any other attribute (`id`, `title`, `data-*`, `aria-*`, …) passes through to the element. */
20
+ [key: string]: unknown;
21
+ }
22
+
23
+ const {
24
+ tone,
25
+ severity,
26
+ variant = "soft",
27
+ dismissible = false,
28
+ dismissLabel = "Dismiss",
29
+ static: isStatic = false,
30
+ class: extra,
31
+ ...rest
32
+ } = Astro.props;
33
+
34
+ const bindings = { tone, severity, variant, dismissible, dismissLabel };
35
+ const light = await renderFragmentLight("alert", bindings);
36
+ const iconHtml = Astro.slots.has("icon") ? await Astro.slots.render("icon") : null;
37
+ const titleHtml = Astro.slots.has("title") ? await Astro.slots.render("title") : "";
38
+ const messageHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
39
+ const actionsHtml = Astro.slots.has("actions") ? await Astro.slots.render("actions") : "";
40
+ const composed = composeFallbackSlot(light, "icon", iconHtml)
41
+ .replace('<slot name="title"></slot>', () => titleHtml)
42
+ .replace('<slot name="actions"></slot>', () => actionsHtml)
43
+ .replace("<slot></slot>", () => messageHtml);
44
+ ---
45
+
46
+ <xtyle-alert
47
+ tone={tone}
48
+ severity={severity}
49
+ variant={variant}
50
+ dismissible={dismissible || undefined}
51
+ dismiss-label={dismissLabel}
52
+ class={extra}
53
+ set:html={composed}
54
+ {...rest}
55
+ />
56
+
57
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
@@ -0,0 +1,97 @@
1
+ ---
2
+ import { renderFragmentLight } from "@xtyle/core/elements/ssr";
3
+
4
+ interface Props {
5
+ skipLink?: string | boolean;
6
+ /** Initial width of the left rail; sets `--xtyle-app-left` server-side so the grid doesn't reflow on hydration. Number → px. */
7
+ leftSize?: number | string;
8
+ /** Initial width of the right rail; sets `--xtyle-app-right` server-side so the grid doesn't reflow on hydration. Number → px. */
9
+ rightSize?: number | string;
10
+ /** Make the left / right rail user-resizable: a drag handle, arrow-key nudges, and a double-click reset to `leftSize` / `rightSize`. Needs the hydrated runtime. */
11
+ leftResizable?: boolean;
12
+ rightResizable?: boolean;
13
+ /** Clamp for the resizable rails, in px. */
14
+ leftMin?: number;
15
+ leftMax?: number;
16
+ rightMin?: number;
17
+ rightMax?: number;
18
+ /** The `id` of the `<main>` region and the skip link's target. Override when more than one shell renders on a page so the ids stay unique. */
19
+ mainId?: string;
20
+ /** Render zero-JS: emit the declarative shadow but never load the runtime to hydrate it. */
21
+ static?: boolean;
22
+ class?: string;
23
+ /** Any other attribute (`id`, `title`, `data-*`, `aria-*`, …) passes through to the element. */
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ const {
28
+ skipLink,
29
+ leftSize,
30
+ rightSize,
31
+ leftResizable,
32
+ rightResizable,
33
+ leftMin,
34
+ leftMax,
35
+ rightMin,
36
+ rightMax,
37
+ mainId = "main",
38
+ static: isStatic = false,
39
+ class: extra,
40
+ ...rest
41
+ } = Astro.props;
42
+
43
+ const hasLeft = Astro.slots.has("left");
44
+ const hasRight = Astro.slots.has("right");
45
+ const hasToolbar = Astro.slots.has("toolbar");
46
+ const hasStatusbar = Astro.slots.has("statusbar");
47
+ const showSkip = skipLink !== undefined;
48
+ const skipText = typeof skipLink === "string" && skipLink.length > 0 ? skipLink : "Skip to main content";
49
+
50
+ const railSize = (v: number | string): string => (typeof v === "number" ? `${v}px` : v);
51
+ const bodyStyle =
52
+ [
53
+ leftSize !== undefined ? `--xtyle-app-left: ${railSize(leftSize)}` : null,
54
+ rightSize !== undefined ? `--xtyle-app-right: ${railSize(rightSize)}` : null,
55
+ ]
56
+ .filter(Boolean)
57
+ .join("; ") || null;
58
+
59
+ const bindings = {
60
+ skipLink: showSkip,
61
+ skipLinkText: skipText,
62
+ bodyStyle,
63
+ mainId,
64
+ leftResizable: Boolean(leftResizable),
65
+ rightResizable: Boolean(rightResizable),
66
+ };
67
+ const light = await renderFragmentLight("app-shell", bindings);
68
+ const toolbarHtml = hasToolbar ? await Astro.slots.render("toolbar") : "";
69
+ const leftHtml = hasLeft ? await Astro.slots.render("left") : "";
70
+ const rightHtml = hasRight ? await Astro.slots.render("right") : "";
71
+ const statusbarHtml = hasStatusbar ? await Astro.slots.render("statusbar") : "";
72
+ const defaultHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
73
+ const composed = light
74
+ .replace('<slot name="toolbar"></slot>', () => toolbarHtml)
75
+ .replace('<slot name="left"></slot>', () => leftHtml)
76
+ .replace('<slot name="right"></slot>', () => rightHtml)
77
+ .replace('<slot name="statusbar"></slot>', () => statusbarHtml)
78
+ .replace("<slot></slot>", () => defaultHtml);
79
+ ---
80
+
81
+ <xtyle-app-shell
82
+ skip-link={typeof skipLink === "string" ? skipLink : showSkip ? "" : undefined}
83
+ left-size={leftSize === undefined ? undefined : String(leftSize)}
84
+ right-size={rightSize === undefined ? undefined : String(rightSize)}
85
+ left-resizable={leftResizable ? "" : undefined}
86
+ right-resizable={rightResizable ? "" : undefined}
87
+ left-min={leftMin === undefined ? undefined : String(leftMin)}
88
+ left-max={leftMax === undefined ? undefined : String(leftMax)}
89
+ right-min={rightMin === undefined ? undefined : String(rightMin)}
90
+ right-max={rightMax === undefined ? undefined : String(rightMax)}
91
+ main-id={mainId === "main" ? undefined : mainId}
92
+ class={extra}
93
+ set:html={composed}
94
+ {...rest}
95
+ />
96
+
97
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
@@ -0,0 +1,71 @@
1
+ ---
2
+ import { renderFragmentLight } from "@xtyle/core/elements/ssr";
3
+
4
+ import type { Tone, FullTone as BadgeTone } from "@xtyle/core";
5
+ type AvatarSize = "sm" | "md" | "lg" | "xl";
6
+ type AvatarShape = "circle" | "square";
7
+
8
+ interface Props {
9
+ src?: string;
10
+ alt?: string;
11
+ tone?: BadgeTone;
12
+ size?: AvatarSize;
13
+ shape?: AvatarShape;
14
+ status?: Tone;
15
+ statusLabel?: string;
16
+ /** Breathe the status dot for a live / online presence: a bare `true` pulses slow, `"fast"` quick. */
17
+ pulse?: boolean | "slow" | "fast";
18
+ /** Render zero-JS: emit the resolved light-DOM markup but never load the runtime to hydrate it. */
19
+ static?: boolean;
20
+ class?: string;
21
+ /** Any other attribute (`id`, `title`, `data-*`, `aria-*`, …) passes through to the element. */
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ const {
26
+ src,
27
+ alt = "",
28
+ tone = "neutral",
29
+ size = "md",
30
+ shape = "circle",
31
+ status,
32
+ statusLabel,
33
+ pulse,
34
+ static: isStatic = false,
35
+ class: extra,
36
+ ...rest
37
+ } = Astro.props;
38
+
39
+ const bindings = {
40
+ src: src ?? null,
41
+ alt,
42
+ tone,
43
+ size,
44
+ shape,
45
+ status: status ?? null,
46
+ statusLabel: statusLabel ?? null,
47
+ pulse: pulse ?? null,
48
+ };
49
+ const chrome = await renderFragmentLight("avatar", bindings);
50
+ const iconHtml = Astro.slots.has("icon") ? await Astro.slots.render("icon") : "";
51
+ const bodyHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
52
+ const composed = chrome
53
+ .replace('<slot name="icon"></slot>', () => iconHtml)
54
+ .replace("<slot></slot>", () => bodyHtml);
55
+ ---
56
+
57
+ <xtyle-avatar
58
+ src={src ?? undefined}
59
+ alt={alt}
60
+ tone={tone}
61
+ size={size}
62
+ shape={shape}
63
+ status={status ?? undefined}
64
+ status-label={statusLabel ?? undefined}
65
+ pulse={pulse === true ? "" : pulse || undefined}
66
+ class={extra}
67
+ set:html={composed}
68
+ {...rest}
69
+ />
70
+
71
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
@@ -0,0 +1,44 @@
1
+ ---
2
+ import { renderFragmentLight } from "@xtyle/core/elements/ssr";
3
+
4
+ type AvatarGroupSize = "sm" | "md" | "lg" | "xl";
5
+ type AvatarGroupSpacing = "snug" | "normal" | "loose";
6
+
7
+ interface Props {
8
+ size?: AvatarGroupSize;
9
+ spacing?: AvatarGroupSpacing;
10
+ /** How many avatars beyond the shown ones to summarize as a trailing `+N` chip. */
11
+ overflow?: number;
12
+ /** Accessible name for the group (a list of people). */
13
+ label?: string;
14
+ /** Render zero-JS: emit the resolved light-DOM markup but never load the runtime to hydrate it. */
15
+ static?: boolean;
16
+ class?: string;
17
+ /** Any other attribute (`id`, `title`, `data-*`, `aria-*`, …) passes through to the element. */
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ const { size, spacing, overflow, label, static: isStatic = false, class: extra, ...rest } = Astro.props;
22
+
23
+ const bindings = {
24
+ size: size ?? null,
25
+ spacing: spacing ?? null,
26
+ overflow: overflow ?? 0,
27
+ label: label ?? null,
28
+ };
29
+ const chrome = await renderFragmentLight("avatar-group", bindings);
30
+ const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
31
+ const composed = chrome.replace("<slot></slot>", () => slotHtml);
32
+ ---
33
+
34
+ <xtyle-avatar-group
35
+ size={size}
36
+ spacing={spacing}
37
+ overflow={overflow ?? undefined}
38
+ label={label}
39
+ class={extra}
40
+ set:html={composed}
41
+ {...rest}
42
+ />
43
+
44
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
@@ -0,0 +1,73 @@
1
+ ---
2
+ import { renderFragmentLight } from "@xtyle/core/elements/ssr";
3
+
4
+ import type { FullTone as BadgeTone } from "@xtyle/core";
5
+ type Size = "sm" | "md" | "lg";
6
+ type BadgeVariant = "solid" | "soft" | "outline";
7
+
8
+ interface Props {
9
+ variant?: BadgeVariant;
10
+ tone?: BadgeTone;
11
+ size?: Size;
12
+ dot?: boolean;
13
+ pulse?: boolean | "slow" | "fast";
14
+ count?: string | number;
15
+ removable?: boolean;
16
+ removeLabel?: string;
17
+ /** Render zero-JS: emit the resolved light-DOM markup but never load the runtime to hydrate it. */
18
+ static?: boolean;
19
+ class?: string;
20
+ /** Any other attribute (`id`, `title`, `data-*`, `aria-*`, …) passes through to the element. */
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ const {
25
+ variant = "soft",
26
+ tone = "neutral",
27
+ size = "md",
28
+ dot = false,
29
+ pulse,
30
+ count,
31
+ removable = false,
32
+ removeLabel,
33
+ static: isStatic = false,
34
+ class: extra,
35
+ ...rest
36
+ } = Astro.props;
37
+
38
+ const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
39
+ // Compose the dismiss button's accessible name from the label text when the consumer didn't name
40
+ // it — so a zero-JS (`static`) removable badge still announces "Remove <text>" instead of a bare
41
+ // "Remove". The runtime composes this client-side from the slot, but `static` never loads it.
42
+ const slotText = slotHtml.replace(/<[^>]*>/g, "").trim();
43
+ const resolvedRemoveLabel = removeLabel ?? (removable && slotText ? `Remove ${slotText}` : undefined);
44
+
45
+ const bindings = {
46
+ variant,
47
+ tone,
48
+ size,
49
+ dot,
50
+ pulse: pulse === true ? "slow" : pulse || null,
51
+ count: count ?? null,
52
+ removable,
53
+ removeLabel: resolvedRemoveLabel,
54
+ };
55
+ const chrome = await renderFragmentLight("badge", bindings);
56
+ const composed = chrome.replace("<slot></slot>", () => slotHtml);
57
+ ---
58
+
59
+ <xtyle-badge
60
+ variant={variant}
61
+ tone={tone}
62
+ size={size}
63
+ dot={dot || undefined}
64
+ pulse={pulse === true ? "slow" : pulse || undefined}
65
+ count={count !== undefined && count !== null && count !== "" ? String(count) : undefined}
66
+ removable={removable || undefined}
67
+ remove-label={resolvedRemoveLabel}
68
+ class={extra}
69
+ set:html={composed}
70
+ {...rest}
71
+ />
72
+
73
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
package/src/Bar.astro ADDED
@@ -0,0 +1,83 @@
1
+ ---
2
+ import { renderFragmentLight } from "@xtyle/core/elements/ssr";
3
+ import { resolveSeriesColorsFor } from "./internal/palette.ts";
4
+ import type { BarSeries, BarScheme } from "@xtyle/core";
5
+
6
+ interface Props {
7
+ series?: BarSeries[];
8
+ categories?: string[];
9
+ scheme?: BarScheme;
10
+ reverse?: boolean;
11
+ colorBy?: "series" | "category";
12
+ orientation?: "vertical" | "horizontal";
13
+ stacked?: boolean;
14
+ showValues?: boolean;
15
+ legend?: boolean;
16
+ height?: number;
17
+ label?: string;
18
+ /** Make bars actionable: each becomes a `role="button"` that fires `select` on click or Enter/Space. */
19
+ selectable?: boolean;
20
+ /** Render zero-JS: emit the resolved light-DOM markup but never load the runtime to hydrate it. */
21
+ static?: boolean;
22
+ class?: string;
23
+ /** Any other attribute (`id`, `title`, `data-*`, `aria-*`, …) passes through to the element. */
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ const {
28
+ series = [],
29
+ categories = [],
30
+ scheme = "accents",
31
+ reverse = false,
32
+ colorBy,
33
+ orientation = "vertical",
34
+ stacked = false,
35
+ showValues = false,
36
+ legend = true,
37
+ height = 320,
38
+ label,
39
+ selectable = false,
40
+ static: isStatic = false,
41
+ class: extra,
42
+ ...rest
43
+ } = Astro.props;
44
+
45
+ const resolvedColorBy = colorBy ?? (series.length <= 1 ? "category" : "series");
46
+ const colorItems = resolvedColorBy === "category" ? categories.map(() => ({})) : series;
47
+ const colors = await resolveSeriesColorsFor(scheme, colorItems, reverse);
48
+ const bindings = {
49
+ series,
50
+ categories,
51
+ colors,
52
+ colorBy: resolvedColorBy,
53
+ orientation,
54
+ stacked,
55
+ showValues,
56
+ legend,
57
+ height,
58
+ title: label ?? null,
59
+ ariaLabel: label ?? null,
60
+ selectable,
61
+ };
62
+ const chrome = await renderFragmentLight("bar", bindings);
63
+ ---
64
+
65
+ <xtyle-bar
66
+ series={JSON.stringify(series)}
67
+ categories={JSON.stringify(categories)}
68
+ scheme={Array.isArray(scheme) ? JSON.stringify(scheme) : scheme}
69
+ reverse={reverse || undefined}
70
+ color-by={colorBy}
71
+ orientation={orientation === "horizontal" ? "horizontal" : undefined}
72
+ stacked={stacked || undefined}
73
+ show-values={showValues || undefined}
74
+ legend={legend ? undefined : "false"}
75
+ selectable={selectable || undefined}
76
+ height={height}
77
+ label={label}
78
+ class={extra}
79
+ set:html={chrome}
80
+ {...rest}
81
+ />
82
+
83
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
@@ -0,0 +1,54 @@
1
+ ---
2
+ import { type BreadcrumbItem } from "@xtyle/core/markup";
3
+ import { renderFragmentLight } from "@xtyle/core/elements/ssr";
4
+ import type { Size, FullTone } from "@xtyle/core";
5
+
6
+ interface Props {
7
+ items?: BreadcrumbItem[];
8
+ separator?: string;
9
+ tone?: FullTone;
10
+ size?: Size;
11
+ label?: string;
12
+ /** Render zero-JS: emit the resolved light-DOM markup but never load the runtime to hydrate it. */
13
+ static?: boolean;
14
+ class?: string;
15
+ /** Any other attribute (`id`, `title`, `data-*`, `aria-*`, …) passes through to the element. */
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ const {
20
+ items,
21
+ separator = "/",
22
+ tone = "accent",
23
+ size = "md",
24
+ label = "Breadcrumb",
25
+ static: isStatic = false,
26
+ class: extra,
27
+ ...rest
28
+ } = Astro.props;
29
+
30
+ const bindings = {
31
+ items: items ?? [],
32
+ separator,
33
+ tone,
34
+ size,
35
+ label,
36
+ };
37
+ const light = await renderFragmentLight("breadcrumb", bindings);
38
+ // only the no-items (consumer-slotted) render carries a marker; the items render fills the list
39
+ const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
40
+ const composed = light.replace("<slot></slot>", () => slotHtml);
41
+ ---
42
+
43
+ <xtyle-breadcrumb
44
+ items={items ? JSON.stringify(items) : undefined}
45
+ separator={separator}
46
+ tone={tone}
47
+ size={size}
48
+ label={label}
49
+ class={extra}
50
+ set:html={composed}
51
+ {...rest}
52
+ />
53
+
54
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}