@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
@@ -0,0 +1,91 @@
1
+ ---
2
+ import { renderFragmentLight } from "@xtyle/core/elements/ssr";
3
+
4
+ import type { FullTone, ButtonAlign } from "@xtyle/core";
5
+
6
+ type ButtonVariant = "solid" | "outline" | "ghost" | "subtle" | "link";
7
+ type Size = "xs" | "sm" | "md" | "lg";
8
+
9
+ interface Props {
10
+ variant?: ButtonVariant;
11
+ tone?: FullTone;
12
+ size?: Size;
13
+ align?: ButtonAlign;
14
+ type?: "button" | "submit" | "reset";
15
+ href?: string;
16
+ disabled?: boolean;
17
+ loading?: boolean;
18
+ block?: boolean;
19
+ iconOnly?: boolean;
20
+ pressed?: boolean;
21
+ selected?: boolean;
22
+ /** Render zero-JS: emit the resolved light-DOM markup but never load the runtime to hydrate it. */
23
+ static?: boolean;
24
+ class?: string;
25
+ /** Any other attribute (`id`, `title`, `data-*`, `aria-*`, …) passes through to the element. */
26
+ [key: string]: unknown;
27
+ }
28
+
29
+ const {
30
+ variant = "solid",
31
+ tone = "accent",
32
+ size = "md",
33
+ align = "center",
34
+ type = "button",
35
+ href,
36
+ disabled = false,
37
+ loading = false,
38
+ block = false,
39
+ iconOnly = false,
40
+ pressed,
41
+ selected,
42
+ static: isStatic = false,
43
+ class: extra,
44
+ ...rest
45
+ } = Astro.props;
46
+
47
+ const bindings = {
48
+ variant,
49
+ tone,
50
+ size,
51
+ align,
52
+ type,
53
+ href: href ?? null,
54
+ disabled,
55
+ loading,
56
+ block,
57
+ iconOnly,
58
+ pressed: pressed ?? null,
59
+ selected: selected ?? null,
60
+ ariaLabel: (rest as Record<string, unknown>)["aria-label"] ?? null,
61
+ ariaLabelledby: (rest as Record<string, unknown>)["aria-labelledby"] ?? null,
62
+ };
63
+ const chrome = await renderFragmentLight("button", bindings);
64
+ const iconStartHtml = Astro.slots.has("icon-start") ? await Astro.slots.render("icon-start") : "";
65
+ const labelHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
66
+ const iconEndHtml = Astro.slots.has("icon-end") ? await Astro.slots.render("icon-end") : "";
67
+ const composed = chrome
68
+ .replace('<slot name="icon-start"></slot>', () => iconStartHtml)
69
+ .replace('<slot name="icon-end"></slot>', () => iconEndHtml)
70
+ .replace("<slot></slot>", () => labelHtml);
71
+ ---
72
+
73
+ <xtyle-button
74
+ variant={variant}
75
+ tone={tone}
76
+ size={size}
77
+ align={align}
78
+ type={type}
79
+ href={href}
80
+ disabled={disabled || undefined}
81
+ loading={loading || undefined}
82
+ block={block || undefined}
83
+ icon-only={iconOnly || undefined}
84
+ pressed={pressed === undefined ? undefined : String(pressed)}
85
+ selected={selected === undefined ? undefined : String(selected)}
86
+ class={extra}
87
+ set:html={composed}
88
+ {...rest}
89
+ />
90
+
91
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
package/src/Card.astro ADDED
@@ -0,0 +1,44 @@
1
+ ---
2
+ import { renderFragmentLight } from "@xtyle/core/elements/ssr";
3
+
4
+ import type { FullTone as Tone } from "@xtyle/core";
5
+
6
+ interface Props {
7
+ overlay?: boolean;
8
+ interactive?: boolean;
9
+ action?: boolean;
10
+ compact?: boolean;
11
+ tone?: Tone;
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 { overlay = false, interactive = false, action = false, compact = false, tone, static: isStatic = false, class: extra, ...rest } =
20
+ Astro.props;
21
+
22
+ const bindings = { overlay, interactive, action, compact, tone: tone ?? null };
23
+ const chrome = await renderFragmentLight("card", bindings);
24
+ const headerHtml = Astro.slots.has("header") ? await Astro.slots.render("header") : "";
25
+ const bodyHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
26
+ const footerHtml = Astro.slots.has("footer") ? await Astro.slots.render("footer") : "";
27
+ const composed = chrome
28
+ .replace('<slot name="header"></slot>', () => headerHtml)
29
+ .replace('<slot name="footer"></slot>', () => footerHtml)
30
+ .replace("<slot></slot>", () => bodyHtml);
31
+ ---
32
+
33
+ <xtyle-card
34
+ overlay={overlay || undefined}
35
+ interactive={interactive || undefined}
36
+ action={action || undefined}
37
+ compact={compact || undefined}
38
+ tone={tone}
39
+ class={extra}
40
+ set:html={composed}
41
+ {...rest}
42
+ />
43
+
44
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
@@ -0,0 +1,60 @@
1
+ ---
2
+ import { renderFragmentLight } from "@xtyle/core/elements/ssr";
3
+
4
+ interface Props {
5
+ href: string;
6
+ target?: string;
7
+ rel?: string;
8
+ interactive?: boolean;
9
+ overlay?: boolean;
10
+ compact?: boolean;
11
+ /** Render zero-JS: emit the resolved light-DOM markup but never load the runtime to hydrate it. */
12
+ static?: boolean;
13
+ class?: string;
14
+ /** Any other attribute (`id`, `title`, `data-*`, `aria-*`, …) passes through to the element. */
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ const {
19
+ href,
20
+ target,
21
+ rel,
22
+ interactive = true,
23
+ overlay = false,
24
+ compact = false,
25
+ static: isStatic = false,
26
+ class: extra,
27
+ ...rest
28
+ } = Astro.props;
29
+
30
+ const bindings = {
31
+ href,
32
+ target: target ?? null,
33
+ rel: rel ?? null,
34
+ interactive,
35
+ overlay,
36
+ compact,
37
+ };
38
+ const chrome = await renderFragmentLight("card-link", bindings);
39
+ const headerHtml = Astro.slots.has("header") ? await Astro.slots.render("header") : "";
40
+ const bodyHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
41
+ const footerHtml = Astro.slots.has("footer") ? await Astro.slots.render("footer") : "";
42
+ const composed = chrome
43
+ .replace('<slot name="header"></slot>', () => headerHtml)
44
+ .replace('<slot name="footer"></slot>', () => footerHtml)
45
+ .replace("<slot></slot>", () => bodyHtml);
46
+ ---
47
+
48
+ <xtyle-card-link
49
+ href={href}
50
+ target={target}
51
+ rel={rel}
52
+ interactive={interactive ? undefined : "false"}
53
+ overlay={overlay || undefined}
54
+ compact={compact || undefined}
55
+ class={extra}
56
+ set:html={composed}
57
+ {...rest}
58
+ />
59
+
60
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
@@ -0,0 +1,39 @@
1
+ ---
2
+ interface Props {
3
+ label?: string;
4
+ autoplay?: boolean;
5
+ interval?: number;
6
+ loop?: boolean;
7
+ controls?: boolean;
8
+ dots?: boolean;
9
+ class?: string;
10
+ /** Any other attribute (`id`, `title`, `data-*`, `aria-*`, …) passes through to the element. */
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ const {
15
+ label,
16
+ autoplay = false,
17
+ interval,
18
+ loop = false,
19
+ controls = true,
20
+ dots = true,
21
+ class: extra,
22
+ ...rest
23
+ } = Astro.props;
24
+ ---
25
+
26
+ <xtyle-carousel
27
+ label={label}
28
+ interval={interval}
29
+ autoplay={autoplay ? "" : undefined}
30
+ loop={loop ? "" : undefined}
31
+ controls={controls ? undefined : "false"}
32
+ dots={dots ? undefined : "false"}
33
+ class={extra}
34
+ {...rest}
35
+ >
36
+ <slot />
37
+ </xtyle-carousel>
38
+
39
+ <script>import "@xtyle/core/elements";</script>
@@ -0,0 +1,68 @@
1
+ ---
2
+ import { renderFragmentLight } from "@xtyle/core/elements/ssr";
3
+
4
+ import type { FullTone as Tone } from "@xtyle/core";
5
+ type Size = "sm" | "md" | "lg";
6
+
7
+ interface Props {
8
+ checked?: boolean;
9
+ indeterminate?: boolean;
10
+ disabled?: boolean;
11
+ size?: Size;
12
+ tone?: Tone;
13
+ name?: string;
14
+ value?: string;
15
+ label?: string;
16
+ labelledby?: string;
17
+ /** Render zero-JS: emit the declarative shadow 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
+ checked = false,
26
+ indeterminate = false,
27
+ disabled = false,
28
+ size = "md",
29
+ tone = "accent",
30
+ name,
31
+ value,
32
+ label,
33
+ labelledby,
34
+ static: isStatic = false,
35
+ class: extra,
36
+ ...rest
37
+ } = Astro.props;
38
+
39
+ const bindings = {
40
+ checked,
41
+ indeterminate,
42
+ disabled,
43
+ size,
44
+ tone,
45
+ label: label ?? null,
46
+ labelledby: labelledby ?? null,
47
+ };
48
+ const light = await renderFragmentLight("checkbox", bindings);
49
+ const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
50
+ const composed = light.replace("<slot></slot>", () => slotHtml);
51
+ ---
52
+
53
+ <xtyle-checkbox
54
+ checked={checked || undefined}
55
+ indeterminate={indeterminate || undefined}
56
+ disabled={disabled || undefined}
57
+ size={size}
58
+ tone={tone}
59
+ name={name}
60
+ value={value}
61
+ label={label}
62
+ labelledby={labelledby}
63
+ class={extra}
64
+ set:html={composed}
65
+ {...rest}
66
+ />
67
+
68
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
@@ -0,0 +1,54 @@
1
+ ---
2
+ import { checkboxGroupMarkup, checkboxGroupHostCss } from "@xtyle/core/markup";
3
+ import { checkboxCss } from "@xtyle/core/css";
4
+
5
+ import type { FullTone as Tone } from "@xtyle/core";
6
+ type Size = "sm" | "md" | "lg";
7
+
8
+ interface Props {
9
+ label?: string;
10
+ labelledby?: string;
11
+ tone?: Tone;
12
+ size?: Size;
13
+ disabled?: boolean;
14
+ /** Keep the heading checkbox but stop the automatic all/some/none roll-up. */
15
+ manual?: boolean;
16
+ /** Render zero-JS: emit the declarative shadow 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
+ label,
25
+ labelledby,
26
+ tone = "accent",
27
+ size = "md",
28
+ disabled = false,
29
+ manual = false,
30
+ static: isStatic = false,
31
+ class: extra,
32
+ ...rest
33
+ } = Astro.props;
34
+
35
+ const labelId = `xtyle-checkbox-group-${Math.random().toString(36).slice(2, 8)}-label`;
36
+
37
+ const shadow = `<style>${checkboxGroupHostCss}${checkboxCss}</style>${checkboxGroupMarkup({ label, labelledby, tone, size, disabled, labelId })}`;
38
+ ---
39
+
40
+ <xtyle-checkbox-group
41
+ label={label}
42
+ labelledby={labelledby}
43
+ tone={tone}
44
+ size={size}
45
+ disabled={disabled || undefined}
46
+ manual={manual || undefined}
47
+ class={extra}
48
+ {...rest}
49
+ >
50
+ <template shadowrootmode="open" set:html={shadow}></template>
51
+ <slot />
52
+ </xtyle-checkbox-group>
53
+
54
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
@@ -0,0 +1,46 @@
1
+ ---
2
+ import { renderFragmentLight } from "@xtyle/core/elements/ssr";
3
+
4
+ type ClusterAlign = "start" | "center" | "end" | "stretch" | "baseline";
5
+ type ClusterJustify = "start" | "center" | "end" | "between" | "around" | "evenly";
6
+
7
+ interface Props {
8
+ gap?: number;
9
+ align?: ClusterAlign;
10
+ justify?: ClusterJustify;
11
+ nowrap?: boolean;
12
+ inline?: boolean;
13
+ /** Render zero-JS: emit the resolved light-DOM markup but never load the runtime to hydrate it. */
14
+ static?: boolean;
15
+ class?: string;
16
+ /** Any other attribute (`id`, `title`, `data-*`, `aria-*`, …) passes through to the element. */
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ const { gap = 2, align, justify, nowrap = false, inline = false, static: isStatic = false, class: extra, ...rest } =
21
+ Astro.props;
22
+
23
+ const bindings = {
24
+ gap,
25
+ align: align ?? null,
26
+ justify: justify ?? null,
27
+ nowrap,
28
+ inline,
29
+ };
30
+ const chrome = await renderFragmentLight("cluster", bindings);
31
+ const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
32
+ const composed = chrome.replace("<slot></slot>", () => slotHtml);
33
+ ---
34
+
35
+ <xtyle-cluster
36
+ gap={gap}
37
+ align={align}
38
+ justify={justify}
39
+ nowrap={nowrap || undefined}
40
+ inline={inline || undefined}
41
+ class={extra}
42
+ set:html={composed}
43
+ {...rest}
44
+ />
45
+
46
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
package/src/Code.astro ADDED
@@ -0,0 +1,42 @@
1
+ ---
2
+ import { plainCodeHtml, splitCodeLines, codeGutterWidth, parseLineSpec } from "@xtyle/core/markup";
3
+ import { highlight } from "@xtyle/core/elements/code-highlight";
4
+ import { renderFragmentLight } from "@xtyle/core/elements/ssr";
5
+
6
+ interface Props {
7
+ lang?: string;
8
+ code?: string;
9
+ /** Warm this block's grammar eagerly in the browser and emit a `modulepreload` hint. */
10
+ preload?: boolean;
11
+ /** Render zero-JS: emit the pre-colored light-DOM markup but never load the runtime. */
12
+ static?: boolean;
13
+ /** The copy-to-clipboard button; on by default, set `false` to drop it. Inert on the `static` (zero-JS) path. */
14
+ copy?: boolean;
15
+ /** Soft-wrap long lines instead of scrolling them horizontally. Works on the zero-JS path. */
16
+ wrap?: boolean;
17
+ /** Number each line in a counter gutter. Works on the zero-JS path. */
18
+ lineNumbers?: boolean;
19
+ /** Tint chosen lines; a 1-based spec like `2`, `2,4`, or `4-6`. Works on the zero-JS path. */
20
+ highlight?: string;
21
+ /** A caption header above the block, e.g. a filename. Works on the zero-JS path. */
22
+ caption?: string;
23
+ class?: string;
24
+ /** Any other attribute (`id`, `title`, `data-*`, `aria-*`, …) passes through to the element. */
25
+ [key: string]: unknown;
26
+ }
27
+
28
+ const { lang, code = "", preload = false, static: isStatic = false, copy = true, wrap = false, lineNumbers = false, highlight: highlightSpec, caption, class: extra, ...rest } = Astro.props;
29
+
30
+ const result = await highlight(code, lang ?? null);
31
+ const base = result.language === null ? plainCodeHtml(code) : result.html;
32
+ const litSet = highlightSpec ? parseLineSpec(highlightSpec) : undefined;
33
+ const split = lineNumbers || litSet ? splitCodeLines(base, litSet) : null;
34
+ const inner = split ? split.html : base;
35
+ const gutter = split && lineNumbers ? `--xtyle-code-gutter: ${codeGutterWidth(split.lines)}` : undefined;
36
+ const bindings = { html: inner, language: result.language, caption: caption ?? null };
37
+ const light = await renderFragmentLight("code", bindings);
38
+ ---
39
+
40
+ <xtyle-code language={lang ?? undefined} code={code} preload={preload || undefined} copy={copy ? undefined : "false"} wrap={wrap || undefined} line-numbers={lineNumbers || undefined} highlight={highlightSpec || undefined} caption={caption || undefined} style={gutter} class={extra} set:html={light} {...rest} />
41
+
42
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
@@ -0,0 +1,184 @@
1
+ ---
2
+ import { formatChannelValue, namedSnapLabel, PLANE_H, PLANE_W } from "@xtyle/core/markup";
3
+ import { renderFragmentLight } from "@xtyle/core/elements/ssr";
4
+ import {
5
+ channelModels,
6
+ channelsOf,
7
+ colorFormats,
8
+ colorToChannels,
9
+ formatColor,
10
+ harmonySchemes,
11
+ hsvToRgb,
12
+ nearestNamedColor,
13
+ parseColor,
14
+ rgbToHsv,
15
+ type ChannelModel,
16
+ type ColorFormat,
17
+ type HarmonyScheme,
18
+ } from "@xtyle/core";
19
+
20
+ interface Props {
21
+ value?: string;
22
+ format?: ColorFormat;
23
+ modes?: string;
24
+ channels?: string;
25
+ snap?: string;
26
+ plane?: boolean;
27
+ trigger?: boolean;
28
+ swatches?: string;
29
+ harmony?: string;
30
+ contrastAgainst?: string;
31
+ alpha?: boolean;
32
+ disabled?: boolean;
33
+ label?: string;
34
+ labelledby?: string;
35
+ name?: string;
36
+ /** Render zero-JS: emit the resolved light-DOM markup but never load the runtime to hydrate it. */
37
+ static?: boolean;
38
+ class?: string;
39
+ /** Any other attribute (`id`, `title`, `data-*`, `aria-*`, …) passes through to the element. */
40
+ [key: string]: unknown;
41
+ }
42
+
43
+ const {
44
+ value = "#5b8cff",
45
+ format,
46
+ modes,
47
+ channels,
48
+ snap,
49
+ plane = false,
50
+ trigger = false,
51
+ swatches,
52
+ harmony,
53
+ contrastAgainst,
54
+ alpha = false,
55
+ disabled = false,
56
+ label,
57
+ labelledby,
58
+ name,
59
+ static: isStatic = false,
60
+ class: extra,
61
+ ...rest
62
+ } = Astro.props;
63
+
64
+ const elementId = `xtyle-color-picker-${Math.random().toString(36).slice(2, 8)}`;
65
+
66
+ // Resolve the markup props exactly as the custom element's getters do, so the SSR
67
+ // declarative shadow is byte-identical to the post-hydration client re-render.
68
+ const resolvedModes: ColorFormat[] = modes
69
+ ? (() => {
70
+ const picked = modes
71
+ .split(",")
72
+ .map((entry) => entry.trim())
73
+ .filter((entry): entry is ColorFormat => (colorFormats as string[]).includes(entry));
74
+ return picked.length ? picked : colorFormats;
75
+ })()
76
+ : colorFormats;
77
+ const resolvedFormat: ColorFormat = format && resolvedModes.includes(format) ? format : (resolvedModes[0] ?? "hex");
78
+
79
+ const channelModel: ChannelModel | null =
80
+ channels === undefined ? null : channels.trim() && (channelModels as string[]).includes(channels.trim()) ? (channels.trim() as ChannelModel) : "rgb";
81
+
82
+ const SNAP_TARGETS = ["web-safe", "named"] as const;
83
+ const snapTargets: string[] =
84
+ snap === undefined
85
+ ? []
86
+ : (() => {
87
+ const raw = snap.trim();
88
+ if (!raw) return [...SNAP_TARGETS];
89
+ const picked = raw
90
+ .split(",")
91
+ .map((entry) => entry.trim())
92
+ .filter((entry) => (SNAP_TARGETS as readonly string[]).includes(entry));
93
+ return picked.length ? picked : [...SNAP_TARGETS];
94
+ })();
95
+
96
+ const swatchList = swatches ? swatches.split(",").map((entry) => entry.trim()).filter(Boolean) : [];
97
+
98
+ const harmonyScheme: HarmonyScheme | null = harmony && (harmonySchemes as string[]).includes(harmony) ? (harmony as HarmonyScheme) : null;
99
+
100
+ const parsed = parseColor(value);
101
+ const baseHsv = parsed ? rgbToHsv(parsed) : { h: 217, s: 0.64, v: 1 };
102
+ const hsv = { h: baseHsv.h, s: baseHsv.s, v: baseHsv.v };
103
+ const alphaValue = parsed && alpha ? parsed.alpha : 1;
104
+ const currentAlpha = alpha ? alphaValue : 1;
105
+
106
+ const currentHex = formatColor(hsvToRgb({ ...hsv, alpha: alphaValue }), "hex");
107
+ const resolvedSwatches = swatchList
108
+ .map((entry) => {
109
+ const swatchColor = parseColor(entry);
110
+ if (!swatchColor) return null;
111
+ const hex = formatColor(swatchColor, "hex");
112
+ return { entry, hex, pressed: hex.toLowerCase() === currentHex.toLowerCase() };
113
+ })
114
+ .filter((s): s is { entry: string; hex: string; pressed: boolean } => s !== null);
115
+
116
+ const resolvedChannels = channelModel
117
+ ? (() => {
118
+ const defs = channelsOf(channelModel);
119
+ const values = colorToChannels(hsvToRgb({ ...hsv, alpha: currentAlpha }), channelModel);
120
+ return defs.map((def, i) => {
121
+ const channelValue = values[i] ?? def.min;
122
+ return {
123
+ label: def.label,
124
+ name: `${def.label}${def.unit ? ` (${def.unit})` : ""}`,
125
+ min: def.min,
126
+ max: def.max,
127
+ step: def.step,
128
+ value: channelValue,
129
+ formatted: formatChannelValue(channelValue, def),
130
+ };
131
+ });
132
+ })()
133
+ : [];
134
+
135
+ const namedSnap = snapTargets.includes("named")
136
+ ? namedSnapLabel(nearestNamedColor(hsvToRgb({ ...hsv, alpha: currentAlpha })).name)
137
+ : null;
138
+
139
+ const bindings = {
140
+ disabled,
141
+ label: label ?? null,
142
+ labelledby: labelledby ?? null,
143
+ elementId,
144
+ formatLabel: resolvedFormat.toUpperCase(),
145
+ planeEnabled: plane,
146
+ triggerEnabled: trigger,
147
+ alphaEnabled: alpha,
148
+ channelModelLabel: channelModel ? channelModel.toUpperCase() : null,
149
+ harmonyScheme,
150
+ hasContrast: !!(contrastAgainst && parseColor(contrastAgainst)),
151
+ hasEyeDropper: false,
152
+ planeWidth: PLANE_W,
153
+ planeHeight: PLANE_H,
154
+ swatches: resolvedSwatches,
155
+ channels: resolvedChannels,
156
+ snapTargets,
157
+ namedSnap,
158
+ };
159
+
160
+ const light = await renderFragmentLight("color-picker", bindings);
161
+ ---
162
+
163
+ <xtyle-color-picker
164
+ value={value}
165
+ format={format}
166
+ modes={modes}
167
+ channels={channels}
168
+ snap={snap}
169
+ plane={plane || undefined}
170
+ trigger={trigger || undefined}
171
+ swatches={swatches}
172
+ harmony={harmony}
173
+ contrast-against={contrastAgainst}
174
+ alpha={alpha || undefined}
175
+ disabled={disabled || undefined}
176
+ label={label}
177
+ labelledby={labelledby}
178
+ name={name}
179
+ class={extra}
180
+ {...rest}
181
+ set:html={light}
182
+ />
183
+
184
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}
@@ -0,0 +1,69 @@
1
+ ---
2
+ import { renderFragmentLight, composeFallbackSlot } from "@xtyle/core/elements/ssr";
3
+
4
+ type Size = "sm" | "md" | "lg";
5
+
6
+ interface Props {
7
+ open?: boolean;
8
+ size?: Size;
9
+ heading?: string;
10
+ label?: string;
11
+ labelledby?: string;
12
+ closeLabel?: string;
13
+ noCloseButton?: boolean;
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 {
22
+ open = false,
23
+ size = "md",
24
+ heading,
25
+ label,
26
+ labelledby,
27
+ closeLabel,
28
+ noCloseButton = false,
29
+ static: isStatic = false,
30
+ class: extra,
31
+ ...rest
32
+ } = Astro.props;
33
+
34
+ const hasHeader = Astro.slots.has("header");
35
+ const hasFooter = Astro.slots.has("footer");
36
+ const elementId = `xtyle-dialog-${Math.random().toString(36).slice(2, 8)}`;
37
+
38
+ const bindings = {
39
+ size,
40
+ heading: heading ?? null,
41
+ label: label ?? null,
42
+ labelledby: labelledby ?? null,
43
+ closeLabel: closeLabel ?? null,
44
+ noCloseButton,
45
+ elementId,
46
+ };
47
+ const light = await renderFragmentLight("dialog", bindings);
48
+ const headerHtml = hasHeader ? await Astro.slots.render("header") : null;
49
+ const bodyHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
50
+ const footerHtml = hasFooter ? await Astro.slots.render("footer") : "";
51
+ const composed = composeFallbackSlot(light, "header", headerHtml)
52
+ .replace("<slot></slot>", () => bodyHtml)
53
+ .replace('<slot name="footer"></slot>', () => footerHtml);
54
+ ---
55
+
56
+ <xtyle-dialog
57
+ open={open || undefined}
58
+ size={size}
59
+ heading={heading}
60
+ label={label}
61
+ labelledby={labelledby}
62
+ close-label={closeLabel}
63
+ no-close-button={noCloseButton || undefined}
64
+ class={extra}
65
+ set:html={composed}
66
+ {...rest}
67
+ />
68
+
69
+ {!isStatic && <script>import "@xtyle/core/elements";</script>}