@elysn/astro 0.0.3 → 0.0.4
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/index.ts +46 -0
- package/package.json +21 -5
- package/src/AccessibleIcon.astro +14 -0
- package/src/AspectRatio.astro +14 -0
- package/src/Avatar.astro +54 -0
- package/src/Badge.astro +34 -0
- package/src/Blockquote.astro +21 -0
- package/src/Box.astro +13 -0
- package/src/Button.astro +2 -2
- package/src/Callout.astro +34 -0
- package/src/CalloutIcon.astro +12 -0
- package/src/CalloutText.astro +12 -0
- package/src/Card.astro +30 -0
- package/src/Checkbox.astro +212 -0
- package/src/Code.astro +21 -0
- package/src/Container.astro +26 -0
- package/src/DecoratedBox.astro +22 -0
- package/src/Em.astro +10 -0
- package/src/Flex.astro +25 -0
- package/src/Grid.astro +21 -0
- package/src/Heading.astro +43 -0
- package/src/InputControl.astro +40 -0
- package/src/InputField.astro +10 -0
- package/src/InputSlot.astro +41 -0
- package/src/Kbd.astro +11 -0
- package/src/Link.astro +33 -0
- package/src/Panel.astro +12 -0
- package/src/Progress.astro +63 -0
- package/src/Quote.astro +10 -0
- package/src/RadioCards.astro +71 -0
- package/src/RadioCardsItem.astro +41 -0
- package/src/RadioGroup.astro +70 -0
- package/src/RadioGroupItem.astro +53 -0
- package/src/Section.astro +23 -0
- package/src/SegmentedControl.astro +189 -0
- package/src/Separator.astro +25 -0
- package/src/Skeleton.astro +30 -0
- package/src/Spinner.astro +31 -0
- package/src/Strong.astro +10 -0
- package/src/Switch.astro +164 -0
- package/src/Tabs.astro +179 -0
- package/src/TabsContent.astro +20 -0
- package/src/TabsIndicator.astro +17 -0
- package/src/TabsList.astro +12 -0
- package/src/TabsTrigger.astro +23 -0
- package/src/Text.astro +43 -0
- package/src/TextArea.astro +30 -0
- package/src/TextField.astro +70 -0
- package/src/VisuallyHidden.astro +10 -0
- package/src/internal/radio-group.ts +82 -0
- package/src/internal/ssr.ts +18 -0
- package/src/internal/zag.ts +24 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
type Size = "xs" | "sm" | "md" | "lg";
|
|
5
|
+
|
|
6
|
+
interface Item {
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props extends Omit<HTMLAttributes<"div">, "color" | "dir" | "name"> {
|
|
13
|
+
className?: string;
|
|
14
|
+
color?: string;
|
|
15
|
+
defaultValue?: string;
|
|
16
|
+
dir?: "ltr" | "rtl";
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
items: Item[];
|
|
19
|
+
name?: string;
|
|
20
|
+
radius?: "none" | "small" | "medium" | "large" | "full";
|
|
21
|
+
size?: Size;
|
|
22
|
+
}
|
|
23
|
+
const {
|
|
24
|
+
className,
|
|
25
|
+
color,
|
|
26
|
+
defaultValue,
|
|
27
|
+
dir,
|
|
28
|
+
disabled,
|
|
29
|
+
items,
|
|
30
|
+
name,
|
|
31
|
+
radius,
|
|
32
|
+
size = "md",
|
|
33
|
+
...rest
|
|
34
|
+
} = Astro.props as Props;
|
|
35
|
+
|
|
36
|
+
const sizes: Record<Size, string> = {
|
|
37
|
+
xs: "el-SegmentedControl-size-xs",
|
|
38
|
+
sm: "el-SegmentedControl-size-sm",
|
|
39
|
+
md: "el-SegmentedControl-size-md",
|
|
40
|
+
lg: "el-SegmentedControl-size-lg",
|
|
41
|
+
};
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
<el-segmented-control
|
|
45
|
+
{...rest}
|
|
46
|
+
role="radiogroup"
|
|
47
|
+
data-scope="segment-group"
|
|
48
|
+
data-part="root"
|
|
49
|
+
data-accent-color={color}
|
|
50
|
+
data-default-value={defaultValue}
|
|
51
|
+
data-dir={dir}
|
|
52
|
+
data-disabled={disabled ? "" : undefined}
|
|
53
|
+
data-name={name}
|
|
54
|
+
data-orientation="horizontal"
|
|
55
|
+
data-radius={radius}
|
|
56
|
+
class:list={["el-SegmentedControl", sizes[size], className]}
|
|
57
|
+
>
|
|
58
|
+
{
|
|
59
|
+
items.map((item) => {
|
|
60
|
+
const state = item.value === defaultValue ? "checked" : "unchecked";
|
|
61
|
+
return (
|
|
62
|
+
<label
|
|
63
|
+
data-scope="segment-group"
|
|
64
|
+
data-part="item"
|
|
65
|
+
data-state={state}
|
|
66
|
+
data-item-value={item.value}
|
|
67
|
+
data-item-disabled={item.disabled ? "" : undefined}
|
|
68
|
+
data-disabled={item.disabled || disabled ? "" : undefined}
|
|
69
|
+
class="el-SegmentedControl-item"
|
|
70
|
+
>
|
|
71
|
+
<span
|
|
72
|
+
data-scope="segment-group"
|
|
73
|
+
data-part="item-text"
|
|
74
|
+
data-state={state}
|
|
75
|
+
data-text={item.label}
|
|
76
|
+
class="el-SegmentedControl-itemText"
|
|
77
|
+
>
|
|
78
|
+
{item.label}
|
|
79
|
+
</span>
|
|
80
|
+
<input
|
|
81
|
+
type="radio"
|
|
82
|
+
data-part="hidden-input"
|
|
83
|
+
data-item-value={item.value}
|
|
84
|
+
value={item.value}
|
|
85
|
+
checked={state === "checked"}
|
|
86
|
+
disabled={item.disabled || disabled}
|
|
87
|
+
style="border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;white-space:nowrap;word-wrap:normal"
|
|
88
|
+
/>
|
|
89
|
+
</label>
|
|
90
|
+
);
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
<div
|
|
94
|
+
data-scope="segment-group"
|
|
95
|
+
data-part="indicator"
|
|
96
|
+
data-orientation="horizontal"
|
|
97
|
+
class="el-SegmentedControl-indicator"
|
|
98
|
+
>
|
|
99
|
+
</div>
|
|
100
|
+
</el-segmented-control>
|
|
101
|
+
|
|
102
|
+
<script>
|
|
103
|
+
import * as zradio from "@zag-js/radio-group";
|
|
104
|
+
import { VanillaMachine, normalizeProps } from "@zag-js/vanilla";
|
|
105
|
+
import { createSpreader, nextId, textDirection } from "./internal/zag";
|
|
106
|
+
|
|
107
|
+
type Machine = ReturnType<typeof createMachine>;
|
|
108
|
+
const createMachine = (props: zradio.Props) => new VanillaMachine(zradio.machine, props);
|
|
109
|
+
|
|
110
|
+
class ElSegmentedControl extends HTMLElement {
|
|
111
|
+
#machine?: Machine;
|
|
112
|
+
#unsubscribe?: () => void;
|
|
113
|
+
#spread = createSpreader("");
|
|
114
|
+
|
|
115
|
+
connectedCallback() {
|
|
116
|
+
if (this.#machine) return;
|
|
117
|
+
const id = nextId("el-segmented-control");
|
|
118
|
+
this.#spread = createSpreader(id);
|
|
119
|
+
const machine = createMachine({
|
|
120
|
+
id,
|
|
121
|
+
defaultValue: this.dataset["defaultValue"] ?? null,
|
|
122
|
+
name: this.dataset["name"],
|
|
123
|
+
disabled: this.hasAttribute("data-disabled"),
|
|
124
|
+
orientation: "horizontal",
|
|
125
|
+
dir: textDirection(this),
|
|
126
|
+
getRootNode: () => this.getRootNode(),
|
|
127
|
+
onValueChange: ({ value }: { value: string | null }) =>
|
|
128
|
+
this.dispatchEvent(
|
|
129
|
+
new CustomEvent("el-segmented-control:value-change", {
|
|
130
|
+
detail: { value },
|
|
131
|
+
bubbles: true,
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
134
|
+
});
|
|
135
|
+
this.#machine = machine;
|
|
136
|
+
// The indicator is positioned from the item rects, which need the ids on the DOM first
|
|
137
|
+
this.#render();
|
|
138
|
+
machine.start();
|
|
139
|
+
this.#render();
|
|
140
|
+
this.#unsubscribe = machine.subscribe(() => this.#render());
|
|
141
|
+
this.dataset["ready"] = "";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
disconnectedCallback() {
|
|
145
|
+
this.#unsubscribe?.();
|
|
146
|
+
this.#spread.clear();
|
|
147
|
+
this.#machine?.stop();
|
|
148
|
+
this.#machine = undefined;
|
|
149
|
+
delete this.dataset["ready"];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Programmatic API: `el.value` / `el.value = "b"`. */
|
|
153
|
+
get value() {
|
|
154
|
+
return this.#api()?.value ?? null;
|
|
155
|
+
}
|
|
156
|
+
set value(value: string | null) {
|
|
157
|
+
this.#api()?.setValue(value as string);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#api() {
|
|
161
|
+
return this.#machine && zradio.connect(this.#machine.service, normalizeProps);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#render() {
|
|
165
|
+
const api = this.#api();
|
|
166
|
+
if (!api) return;
|
|
167
|
+
const { id: _id, ...rootProps } = api.getRootProps();
|
|
168
|
+
this.#spread.apply(this, rootProps);
|
|
169
|
+
for (const item of this.querySelectorAll<HTMLElement>("[data-part='item']")) {
|
|
170
|
+
const props = {
|
|
171
|
+
value: item.dataset["itemValue"] ?? "",
|
|
172
|
+
disabled: item.hasAttribute("data-item-disabled"),
|
|
173
|
+
};
|
|
174
|
+
const part = (name: string) => item.querySelector<HTMLElement>(`[data-part='${name}']`);
|
|
175
|
+
this.#spread.apply(item, api.getItemProps(props));
|
|
176
|
+
this.#spread.apply(part("item-text"), api.getItemTextProps(props));
|
|
177
|
+
this.#spread.apply(part("hidden-input"), api.getItemHiddenInputProps(props));
|
|
178
|
+
}
|
|
179
|
+
this.#spread.apply(
|
|
180
|
+
this.querySelector("[data-part='indicator']"),
|
|
181
|
+
api.getIndicatorProps(),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!customElements.get("el-segmented-control")) {
|
|
187
|
+
customElements.define("el-segmented-control", ElSegmentedControl);
|
|
188
|
+
}
|
|
189
|
+
</script>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
interface Props extends Omit<HTMLAttributes<"div">, "color"> {
|
|
5
|
+
className?: string;
|
|
6
|
+
color?: string;
|
|
7
|
+
decorative?: boolean;
|
|
8
|
+
orientation?: "horizontal" | "vertical";
|
|
9
|
+
size?: "1" | "2" | "3" | "4";
|
|
10
|
+
}
|
|
11
|
+
const { className, color, decorative, orientation = "horizontal", size = "1", ...rest } =
|
|
12
|
+
Astro.props as Props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
{...rest}
|
|
17
|
+
role={decorative ? undefined : "separator"}
|
|
18
|
+
aria-hidden={decorative ? "true" : undefined}
|
|
19
|
+
aria-orientation={decorative ? undefined : orientation}
|
|
20
|
+
data-accent-color={color}
|
|
21
|
+
data-orientation={orientation}
|
|
22
|
+
data-size={size}
|
|
23
|
+
class:list={["el-Separator", className]}
|
|
24
|
+
>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
interface Props extends Omit<HTMLAttributes<"span">, "style"> {
|
|
5
|
+
className?: string;
|
|
6
|
+
height?: string;
|
|
7
|
+
loading?: boolean;
|
|
8
|
+
style?: Record<string, string | number>;
|
|
9
|
+
width?: string;
|
|
10
|
+
}
|
|
11
|
+
const { className, height, loading = true, style, width, ...rest } = Astro.props as Props;
|
|
12
|
+
// Like Radix: when not loading, render the bare slot (no wrapper element).
|
|
13
|
+
// Whitespace-only slots are emptied so the `:empty` block placeholder rule still applies.
|
|
14
|
+
const html = loading && Astro.slots.has("default") ? await Astro.slots.render("default") : "";
|
|
15
|
+
const content = html.trim() === "" ? "" : html;
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
loading ? (
|
|
20
|
+
<span
|
|
21
|
+
{...rest}
|
|
22
|
+
data-loading
|
|
23
|
+
class:list={["el-Skeleton", className]}
|
|
24
|
+
style={{ width, height, ...style }}
|
|
25
|
+
set:html={content}
|
|
26
|
+
/>
|
|
27
|
+
) : (
|
|
28
|
+
<slot />
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
type Size = "xs" | "sm" | "md" | "lg" | "xl";
|
|
5
|
+
|
|
6
|
+
interface Props extends HTMLAttributes<"span"> {
|
|
7
|
+
className?: string;
|
|
8
|
+
loading?: boolean;
|
|
9
|
+
size?: Size;
|
|
10
|
+
}
|
|
11
|
+
const { className, loading = true, size = "md", ...rest } = Astro.props as Props;
|
|
12
|
+
|
|
13
|
+
const sizes: Record<Size, string> = {
|
|
14
|
+
xs: "el-Spinner-size-xs",
|
|
15
|
+
sm: "el-Spinner-size-sm",
|
|
16
|
+
md: "el-Spinner-size-md",
|
|
17
|
+
lg: "el-Spinner-size-lg",
|
|
18
|
+
xl: "el-Spinner-size-xl",
|
|
19
|
+
};
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
loading && (
|
|
24
|
+
<span
|
|
25
|
+
{...rest}
|
|
26
|
+
role="status"
|
|
27
|
+
aria-label="Loading"
|
|
28
|
+
class:list={["el-Spinner", sizes[size], className]}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
package/src/Strong.astro
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
interface Props extends HTMLAttributes<"strong"> {
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
const { className, ...rest } = Astro.props as Props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<strong {...rest} class:list={["el-Strong", className]}><slot /></strong>
|
package/src/Switch.astro
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
type Size = "xs" | "sm" | "md" | "lg";
|
|
5
|
+
|
|
6
|
+
interface Props extends Omit<HTMLAttributes<"label">, "color" | "dir" | "name"> {
|
|
7
|
+
className?: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
defaultChecked?: boolean;
|
|
10
|
+
dir?: "ltr" | "rtl";
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
highContrast?: boolean;
|
|
13
|
+
name?: string;
|
|
14
|
+
radius?: "none" | "small" | "medium" | "large" | "full";
|
|
15
|
+
required?: boolean;
|
|
16
|
+
size?: Size;
|
|
17
|
+
value?: string;
|
|
18
|
+
variant?: "surface" | "soft";
|
|
19
|
+
}
|
|
20
|
+
const {
|
|
21
|
+
className,
|
|
22
|
+
color,
|
|
23
|
+
defaultChecked,
|
|
24
|
+
dir,
|
|
25
|
+
disabled,
|
|
26
|
+
highContrast,
|
|
27
|
+
name,
|
|
28
|
+
radius,
|
|
29
|
+
required,
|
|
30
|
+
size = "md",
|
|
31
|
+
value = "on",
|
|
32
|
+
variant = "surface",
|
|
33
|
+
...rest
|
|
34
|
+
} = Astro.props as Props;
|
|
35
|
+
|
|
36
|
+
const sizes: Record<Size, string> = {
|
|
37
|
+
xs: "el-Switch-size-xs",
|
|
38
|
+
sm: "el-Switch-size-sm",
|
|
39
|
+
md: "el-Switch-size-md",
|
|
40
|
+
lg: "el-Switch-size-lg",
|
|
41
|
+
};
|
|
42
|
+
const state = defaultChecked ? "checked" : "unchecked";
|
|
43
|
+
const hasLabel = Astro.slots.has("default");
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<!-- display: contents keeps the Zag root (a <label>) as the visual box -->
|
|
47
|
+
<el-switch
|
|
48
|
+
style="display: contents"
|
|
49
|
+
data-default-checked={defaultChecked ? "true" : undefined}
|
|
50
|
+
data-dir={dir}
|
|
51
|
+
>
|
|
52
|
+
<label
|
|
53
|
+
{...rest}
|
|
54
|
+
data-scope="switch"
|
|
55
|
+
data-part="root"
|
|
56
|
+
data-state={state}
|
|
57
|
+
data-disabled={disabled ? "" : undefined}
|
|
58
|
+
data-accent-color={color}
|
|
59
|
+
data-high-contrast={highContrast ? "true" : undefined}
|
|
60
|
+
data-radius={radius}
|
|
61
|
+
data-variant={variant}
|
|
62
|
+
class:list={["el-Switch", sizes[size], className]}
|
|
63
|
+
>
|
|
64
|
+
<span data-scope="switch" data-part="control" data-state={state} class="el-Switch-control">
|
|
65
|
+
<span data-scope="switch" data-part="thumb" data-state={state} class="el-Switch-thumb"></span>
|
|
66
|
+
</span>
|
|
67
|
+
{
|
|
68
|
+
hasLabel && (
|
|
69
|
+
<span data-scope="switch" data-part="label" data-state={state} class="el-Switch-label">
|
|
70
|
+
<slot />
|
|
71
|
+
</span>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
<input
|
|
75
|
+
type="checkbox"
|
|
76
|
+
data-part="hidden-input"
|
|
77
|
+
name={name}
|
|
78
|
+
value={value}
|
|
79
|
+
checked={defaultChecked}
|
|
80
|
+
disabled={disabled}
|
|
81
|
+
required={required}
|
|
82
|
+
style="border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;white-space:nowrap;word-wrap:normal"
|
|
83
|
+
/>
|
|
84
|
+
</label>
|
|
85
|
+
</el-switch>
|
|
86
|
+
|
|
87
|
+
<script>
|
|
88
|
+
import * as zswitch from "@zag-js/switch";
|
|
89
|
+
import { VanillaMachine, normalizeProps } from "@zag-js/vanilla";
|
|
90
|
+
import { createSpreader, nextId, textDirection } from "./internal/zag";
|
|
91
|
+
|
|
92
|
+
type Machine = ReturnType<typeof createMachine>;
|
|
93
|
+
const createMachine = (props: zswitch.Props) => new VanillaMachine(zswitch.machine, props);
|
|
94
|
+
|
|
95
|
+
class ElSwitch extends HTMLElement {
|
|
96
|
+
#machine?: Machine;
|
|
97
|
+
#unsubscribe?: () => void;
|
|
98
|
+
#spread = createSpreader("");
|
|
99
|
+
|
|
100
|
+
connectedCallback() {
|
|
101
|
+
if (this.#machine) return;
|
|
102
|
+
const id = nextId("el-switch");
|
|
103
|
+
this.#spread = createSpreader(id);
|
|
104
|
+
const input = this.#part("hidden-input") as HTMLInputElement | undefined;
|
|
105
|
+
const machine = createMachine({
|
|
106
|
+
id,
|
|
107
|
+
defaultChecked: this.dataset["defaultChecked"] === "true",
|
|
108
|
+
disabled: input?.disabled,
|
|
109
|
+
required: input?.required,
|
|
110
|
+
name: input?.name || undefined,
|
|
111
|
+
value: input?.value,
|
|
112
|
+
dir: textDirection(this),
|
|
113
|
+
getRootNode: () => this.getRootNode(),
|
|
114
|
+
onCheckedChange: ({ checked }: { checked: boolean }) =>
|
|
115
|
+
this.dispatchEvent(
|
|
116
|
+
new CustomEvent("el-switch:checked-change", { detail: { checked }, bubbles: true }),
|
|
117
|
+
),
|
|
118
|
+
});
|
|
119
|
+
this.#machine = machine;
|
|
120
|
+
machine.start();
|
|
121
|
+
this.#render();
|
|
122
|
+
this.#unsubscribe = machine.subscribe(() => this.#render());
|
|
123
|
+
this.dataset["ready"] = "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
disconnectedCallback() {
|
|
127
|
+
this.#unsubscribe?.();
|
|
128
|
+
this.#spread.clear();
|
|
129
|
+
this.#machine?.stop();
|
|
130
|
+
this.#machine = undefined;
|
|
131
|
+
delete this.dataset["ready"];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Programmatic API: `el.checked` / `el.checked = true`. */
|
|
135
|
+
get checked() {
|
|
136
|
+
return this.#api()?.checked ?? false;
|
|
137
|
+
}
|
|
138
|
+
set checked(checked: boolean) {
|
|
139
|
+
this.#api()?.setChecked(checked);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#api() {
|
|
143
|
+
return this.#machine && zswitch.connect(this.#machine.service, normalizeProps);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#part(name: string) {
|
|
147
|
+
return this.querySelector<HTMLElement>(`[data-part='${name}']`) ?? undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#render() {
|
|
151
|
+
const api = this.#api();
|
|
152
|
+
if (!api) return;
|
|
153
|
+
// Zag never looks the root up by id, so keep the author's `id` instead of its generated one
|
|
154
|
+
const { id: _id, ...rootProps } = api.getRootProps();
|
|
155
|
+
this.#spread.apply(this.#part("root"), rootProps);
|
|
156
|
+
this.#spread.apply(this.#part("control"), api.getControlProps());
|
|
157
|
+
this.#spread.apply(this.#part("thumb"), api.getThumbProps());
|
|
158
|
+
this.#spread.apply(this.#part("label"), api.getLabelProps());
|
|
159
|
+
this.#spread.apply(this.#part("hidden-input"), api.getHiddenInputProps());
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!customElements.get("el-switch")) customElements.define("el-switch", ElSwitch);
|
|
164
|
+
</script>
|
package/src/Tabs.astro
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
type Size = "xs" | "sm" | "md" | "lg";
|
|
5
|
+
|
|
6
|
+
interface Props extends Omit<HTMLAttributes<"div">, "color" | "dir"> {
|
|
7
|
+
activationMode?: "automatic" | "manual";
|
|
8
|
+
className?: string;
|
|
9
|
+
color?: string;
|
|
10
|
+
defaultValue?: string;
|
|
11
|
+
dir?: "ltr" | "rtl";
|
|
12
|
+
highContrast?: boolean;
|
|
13
|
+
loopFocus?: boolean;
|
|
14
|
+
orientation?: "horizontal" | "vertical";
|
|
15
|
+
size?: Size;
|
|
16
|
+
}
|
|
17
|
+
const {
|
|
18
|
+
activationMode = "automatic",
|
|
19
|
+
className,
|
|
20
|
+
color,
|
|
21
|
+
defaultValue,
|
|
22
|
+
dir,
|
|
23
|
+
highContrast,
|
|
24
|
+
loopFocus = true,
|
|
25
|
+
orientation = "horizontal",
|
|
26
|
+
size = "md",
|
|
27
|
+
...rest
|
|
28
|
+
} = Astro.props as Props;
|
|
29
|
+
|
|
30
|
+
const sizes: Record<Size, string> = {
|
|
31
|
+
xs: "el-Tabs-size-xs",
|
|
32
|
+
sm: "el-Tabs-size-sm",
|
|
33
|
+
md: "el-Tabs-size-md",
|
|
34
|
+
lg: "el-Tabs-size-lg",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Pre-hydration state, CSS only. Astro has no parent -> child context, so TabsTrigger/TabsContent
|
|
38
|
+
// cannot know the default value at render time; instead each instance emits a tiny scoped
|
|
39
|
+
// <style> that hides every panel but the default one and colours the default trigger as
|
|
40
|
+
// selected. `data-ready` (set by <el-tabs> once Zag has started) switches it off.
|
|
41
|
+
// Caveats:
|
|
42
|
+
// - Needs `defaultValue`; without it all panels are visible until the script runs.
|
|
43
|
+
// - ARIA/ids/roving tabindex exist only after hydration (Zag applies them).
|
|
44
|
+
// - The inline <style> needs a CSP that allows inline styles.
|
|
45
|
+
// - Nested tabs are safe: selectors skip anything inside a nested <el-tabs>.
|
|
46
|
+
const uid = crypto.randomUUID().slice(0, 8);
|
|
47
|
+
const cssValue = defaultValue?.replaceAll(/["\\<]/gu, (c) => (c === "<" ? "\\3c " : `\\${c}`));
|
|
48
|
+
const scope = `el-tabs[data-uid="${uid}"]:not([data-ready])`;
|
|
49
|
+
const ssrCss =
|
|
50
|
+
cssValue === undefined
|
|
51
|
+
? undefined
|
|
52
|
+
: `${scope} .el-Tabs-content:not([data-value="${cssValue}"]):not(el-tabs[data-uid="${uid}"] el-tabs .el-Tabs-content){display:none}` +
|
|
53
|
+
`${scope} .el-Tabs-trigger[data-value="${cssValue}"]:not(el-tabs[data-uid="${uid}"] el-tabs .el-Tabs-trigger){color:var(--gray-12)}`;
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
<el-tabs
|
|
57
|
+
{...rest}
|
|
58
|
+
data-uid={uid}
|
|
59
|
+
data-accent-color={color}
|
|
60
|
+
data-activation-mode={activationMode}
|
|
61
|
+
data-default-value={defaultValue}
|
|
62
|
+
data-dir={dir}
|
|
63
|
+
data-high-contrast={highContrast ? "true" : undefined}
|
|
64
|
+
data-loop-focus={loopFocus ? "true" : "false"}
|
|
65
|
+
data-orientation={orientation}
|
|
66
|
+
class:list={["el-Tabs", sizes[size], className]}
|
|
67
|
+
>
|
|
68
|
+
<slot />
|
|
69
|
+
</el-tabs>
|
|
70
|
+
{ssrCss && <style is:inline set:html={ssrCss} />}
|
|
71
|
+
|
|
72
|
+
<script>
|
|
73
|
+
import * as tabs from "@zag-js/tabs";
|
|
74
|
+
import { VanillaMachine, normalizeProps, spreadProps } from "@zag-js/vanilla";
|
|
75
|
+
|
|
76
|
+
type Machine = ReturnType<typeof createMachine>;
|
|
77
|
+
const createMachine = (props: tabs.Props) =>
|
|
78
|
+
new VanillaMachine(tabs.machine, props);
|
|
79
|
+
|
|
80
|
+
let instances = 0;
|
|
81
|
+
|
|
82
|
+
class ElTabs extends HTMLElement {
|
|
83
|
+
#machine?: Machine;
|
|
84
|
+
#unsubscribe?: () => void;
|
|
85
|
+
#cleanups = new Map<Element, () => void>();
|
|
86
|
+
// Zag ids are derived from this; unique per connection so cloned markup never collides.
|
|
87
|
+
#id = "";
|
|
88
|
+
|
|
89
|
+
connectedCallback() {
|
|
90
|
+
// Guard against double init (the browser can call this again only after a disconnect)
|
|
91
|
+
if (this.#machine) return;
|
|
92
|
+
this.#id = `el-tabs-${++instances}`;
|
|
93
|
+
const firstEnabled = this.#parts("[data-part='trigger']:not([disabled])")[0];
|
|
94
|
+
const machine = createMachine({
|
|
95
|
+
id: this.#id,
|
|
96
|
+
defaultValue: this.dataset["defaultValue"] ?? firstEnabled?.dataset["value"],
|
|
97
|
+
orientation: this.dataset["orientation"] === "vertical" ? "vertical" : "horizontal",
|
|
98
|
+
activationMode: this.dataset["activationMode"] === "manual" ? "manual" : "automatic",
|
|
99
|
+
loopFocus: this.dataset["loopFocus"] !== "false",
|
|
100
|
+
dir:
|
|
101
|
+
this.dataset["dir"] === "rtl" || getComputedStyle(this).direction === "rtl"
|
|
102
|
+
? "rtl"
|
|
103
|
+
: "ltr",
|
|
104
|
+
getRootNode: () => this.getRootNode(),
|
|
105
|
+
onValueChange: ({ value }: { value: string }) =>
|
|
106
|
+
this.dispatchEvent(
|
|
107
|
+
new CustomEvent("el-tabs:value-change", { detail: { value }, bubbles: true }),
|
|
108
|
+
),
|
|
109
|
+
});
|
|
110
|
+
this.#machine = machine;
|
|
111
|
+
// Ids/attrs must be on the DOM before start(): the entry action measures the trigger
|
|
112
|
+
// (looked up by id) for the indicator.
|
|
113
|
+
this.#render();
|
|
114
|
+
machine.start();
|
|
115
|
+
this.#render();
|
|
116
|
+
this.#unsubscribe = machine.subscribe(() => this.#render());
|
|
117
|
+
this.dataset["ready"] = "";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
disconnectedCallback() {
|
|
121
|
+
this.#unsubscribe?.();
|
|
122
|
+
this.#cleanups.forEach((fn) => fn());
|
|
123
|
+
this.#cleanups.clear();
|
|
124
|
+
this.#machine?.stop();
|
|
125
|
+
this.#machine = undefined;
|
|
126
|
+
delete this.dataset["ready"];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Programmatic API: `el.value` / `el.value = "b"`. */
|
|
130
|
+
get value() {
|
|
131
|
+
return this.#api()?.value ?? null;
|
|
132
|
+
}
|
|
133
|
+
set value(value: string | null) {
|
|
134
|
+
this.#api()?.setValue(value as string);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#api() {
|
|
138
|
+
return this.#machine && tabs.connect(this.#machine.service, normalizeProps);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Own parts only: skips parts of nested el-tabs. */
|
|
142
|
+
#parts(selector: string) {
|
|
143
|
+
return Array.from(this.querySelectorAll<HTMLElement>(selector)).filter(
|
|
144
|
+
(el) => el.closest("el-tabs") === this,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#render() {
|
|
149
|
+
const api = this.#api();
|
|
150
|
+
if (!api) return;
|
|
151
|
+
const apply = (el: Element | undefined, props: object) => {
|
|
152
|
+
if (!el) return;
|
|
153
|
+
this.#cleanups.set(
|
|
154
|
+
el,
|
|
155
|
+
spreadProps(el as HTMLElement, props as Record<string, unknown>, this.#id),
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
// Zag never looks the root up by id, so keep the author's `id` instead of its generated one
|
|
159
|
+
const { id: _id, ...rootProps } = api.getRootProps();
|
|
160
|
+
apply(this, rootProps);
|
|
161
|
+
apply(this.#parts("[data-part='list']")[0], api.getListProps());
|
|
162
|
+
for (const el of this.#parts("[data-part='trigger']")) {
|
|
163
|
+
apply(
|
|
164
|
+
el,
|
|
165
|
+
api.getTriggerProps({
|
|
166
|
+
value: el.dataset["value"] ?? "",
|
|
167
|
+
disabled: el.hasAttribute("disabled"),
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
for (const el of this.#parts("[data-part='content']")) {
|
|
172
|
+
apply(el, api.getContentProps({ value: el.dataset["value"] ?? "" }));
|
|
173
|
+
}
|
|
174
|
+
apply(this.#parts("[data-part='indicator']")[0], api.getIndicatorProps());
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!customElements.get("el-tabs")) customElements.define("el-tabs", ElTabs);
|
|
179
|
+
</script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
interface Props extends HTMLAttributes<"div"> {
|
|
5
|
+
className?: string;
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
const { className, value, ...rest } = Astro.props as Props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div
|
|
12
|
+
{...rest}
|
|
13
|
+
role="tabpanel"
|
|
14
|
+
data-scope="tabs"
|
|
15
|
+
data-part="content"
|
|
16
|
+
data-value={value}
|
|
17
|
+
class:list={["el-Tabs-content", className]}
|
|
18
|
+
>
|
|
19
|
+
<slot />
|
|
20
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
interface Props extends HTMLAttributes<"div"> {
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
const { className, ...rest } = Astro.props as Props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<div
|
|
11
|
+
{...rest}
|
|
12
|
+
data-scope="tabs"
|
|
13
|
+
data-part="indicator"
|
|
14
|
+
hidden
|
|
15
|
+
class:list={["el-Tabs-indicator", className]}
|
|
16
|
+
>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
interface Props extends HTMLAttributes<"div"> {
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
const { className, ...rest } = Astro.props as Props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<div {...rest} role="tablist" data-scope="tabs" data-part="list" class:list={["el-Tabs-list", className]}>
|
|
11
|
+
<slot />
|
|
12
|
+
</div>
|