@f0rbit/ui 0.1.2 → 0.1.6
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/dist/components.css +4 -0
- package/dist/index.d.ts +80 -5
- package/dist/index.js +103 -80
- package/dist/index.jsx +80 -61
- package/dist/server.js +41 -61
- package/dist/server.jsx +80 -61
- package/dist/styles.css +4 -0
- package/package.json +3 -2
- package/src/components/Stepper.tsx +11 -3
- package/src/components/Tabs.tsx +155 -69
- package/src/styles/components.css +4 -0
package/src/components/Tabs.tsx
CHANGED
|
@@ -1,62 +1,146 @@
|
|
|
1
|
-
import { type JSX, splitProps,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { type JSX, splitProps, onMount, onCleanup, createSignal } from "solid-js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A tab container component that manages tab selection and panel visibility.
|
|
5
|
+
*
|
|
6
|
+
* Uses DOM-based discovery via data attributes, making it compatible with
|
|
7
|
+
* Astro's island architecture where each child component hydrates independently.
|
|
8
|
+
*
|
|
9
|
+
* @example Basic usage in SolidJS
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <Tabs defaultValue="tab1">
|
|
12
|
+
* <TabList>
|
|
13
|
+
* <Tab value="tab1">First Tab</Tab>
|
|
14
|
+
* <Tab value="tab2">Second Tab</Tab>
|
|
15
|
+
* </TabList>
|
|
16
|
+
* <TabPanel value="tab1">First panel content</TabPanel>
|
|
17
|
+
* <TabPanel value="tab2">Second panel content</TabPanel>
|
|
18
|
+
* </Tabs>
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @example Usage in Astro MDX (each component needs client:load)
|
|
22
|
+
* ```mdx
|
|
23
|
+
* <Tabs defaultValue="tab1" client:load>
|
|
24
|
+
* <TabList client:load>
|
|
25
|
+
* <Tab value="tab1" client:load>First Tab</Tab>
|
|
26
|
+
* <Tab value="tab2" client:load>Second Tab</Tab>
|
|
27
|
+
* </TabList>
|
|
28
|
+
* <TabPanel value="tab1" client:load>First panel content</TabPanel>
|
|
29
|
+
* <TabPanel value="tab2" client:load>Second panel content</TabPanel>
|
|
30
|
+
* </Tabs>
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example Creating a wrapper to avoid multiple client:load directives
|
|
34
|
+
* ```tsx
|
|
35
|
+
* // MyTabs.tsx - your custom wrapper
|
|
36
|
+
* import { Tabs, TabList, Tab, TabPanel } from '@f0rbit/ui';
|
|
37
|
+
*
|
|
38
|
+
* export function MyTabs() {
|
|
39
|
+
* return (
|
|
40
|
+
* <Tabs defaultValue="tab1">
|
|
41
|
+
* <TabList>
|
|
42
|
+
* <Tab value="tab1">First</Tab>
|
|
43
|
+
* <Tab value="tab2">Second</Tab>
|
|
44
|
+
* </TabList>
|
|
45
|
+
* <TabPanel value="tab1">Content 1</TabPanel>
|
|
46
|
+
* <TabPanel value="tab2">Content 2</TabPanel>
|
|
47
|
+
* </Tabs>
|
|
48
|
+
* );
|
|
49
|
+
* }
|
|
50
|
+
*
|
|
51
|
+
* // Then in Astro: <MyTabs client:load />
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
10
54
|
export interface TabsProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
55
|
+
/** The value of the initially selected tab */
|
|
11
56
|
defaultValue?: string;
|
|
12
|
-
value?: string;
|
|
13
|
-
onValueChange?: (value: string) => void;
|
|
14
57
|
children: JSX.Element;
|
|
15
58
|
}
|
|
16
59
|
|
|
17
|
-
export
|
|
18
|
-
children
|
|
19
|
-
|
|
60
|
+
export function Tabs(props: TabsProps) {
|
|
61
|
+
const [local, rest] = splitProps(props, ["defaultValue", "children", "class"]);
|
|
62
|
+
let containerRef: HTMLDivElement | undefined;
|
|
63
|
+
const [activeTab, setActiveTab] = createSignal(local.defaultValue ?? "");
|
|
64
|
+
|
|
65
|
+
const updateTabs = (value: string) => {
|
|
66
|
+
if (!containerRef) return;
|
|
67
|
+
|
|
68
|
+
const tabs = containerRef.querySelectorAll<HTMLButtonElement>("[data-tab-value]");
|
|
69
|
+
tabs.forEach((tab) => {
|
|
70
|
+
const isActive = tab.dataset.tabValue === value;
|
|
71
|
+
tab.setAttribute("aria-selected", String(isActive));
|
|
72
|
+
tab.setAttribute("tabindex", isActive ? "0" : "-1");
|
|
73
|
+
tab.classList.toggle("active", isActive);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const panels = containerRef.querySelectorAll<HTMLDivElement>("[data-panel-value]");
|
|
77
|
+
panels.forEach((panel) => {
|
|
78
|
+
const isActive = panel.dataset.panelValue === value;
|
|
79
|
+
panel.hidden = !isActive;
|
|
80
|
+
});
|
|
81
|
+
};
|
|
20
82
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
children: JSX.Element;
|
|
24
|
-
}
|
|
83
|
+
const initializeTabs = () => {
|
|
84
|
+
if (!containerRef) return;
|
|
25
85
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
86
|
+
let value = activeTab();
|
|
87
|
+
if (!value) {
|
|
88
|
+
const firstTab = containerRef.querySelector<HTMLButtonElement>("[data-tab-value]");
|
|
89
|
+
value = firstTab?.dataset.tabValue ?? "";
|
|
90
|
+
if (value) setActiveTab(value);
|
|
91
|
+
}
|
|
30
92
|
|
|
31
|
-
|
|
32
|
-
|
|
93
|
+
if (value) updateTabs(value);
|
|
94
|
+
};
|
|
33
95
|
|
|
34
|
-
|
|
96
|
+
onMount(() => {
|
|
97
|
+
if (!containerRef) return;
|
|
35
98
|
|
|
36
|
-
|
|
37
|
-
|
|
99
|
+
// Use event delegation for clicks - handles dynamically added children
|
|
100
|
+
containerRef.addEventListener("click", (e) => {
|
|
101
|
+
const tab = (e.target as HTMLElement).closest<HTMLButtonElement>("[data-tab-value]");
|
|
102
|
+
if (tab?.dataset.tabValue) {
|
|
103
|
+
setActiveTab(tab.dataset.tabValue);
|
|
104
|
+
updateTabs(tab.dataset.tabValue);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
38
107
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
108
|
+
// Initialize with any children already present
|
|
109
|
+
initializeTabs();
|
|
110
|
+
|
|
111
|
+
// Watch for new children being added (e.g., Astro islands hydrating)
|
|
112
|
+
const observer = new MutationObserver(() => {
|
|
113
|
+
initializeTabs();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
observer.observe(containerRef, {
|
|
117
|
+
childList: true,
|
|
118
|
+
subtree: true,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
onCleanup(() => observer.disconnect());
|
|
122
|
+
});
|
|
45
123
|
|
|
46
124
|
const classes = () => `tabs ${local.class ?? ""}`.trim();
|
|
47
125
|
|
|
48
126
|
return (
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
</div>
|
|
53
|
-
</TabsContext.Provider>
|
|
127
|
+
<div ref={containerRef} class={classes()} data-default-value={local.defaultValue} {...rest}>
|
|
128
|
+
{local.children}
|
|
129
|
+
</div>
|
|
54
130
|
);
|
|
55
131
|
}
|
|
56
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Container for Tab buttons. Provides the tablist role for accessibility.
|
|
135
|
+
*
|
|
136
|
+
* In Astro, requires `client:load` directive.
|
|
137
|
+
*/
|
|
138
|
+
export interface TabListProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
139
|
+
children: JSX.Element;
|
|
140
|
+
}
|
|
141
|
+
|
|
57
142
|
export function TabList(props: TabListProps) {
|
|
58
143
|
const [local, rest] = splitProps(props, ["children", "class"]);
|
|
59
|
-
|
|
60
144
|
const classes = () => `tab-list ${local.class ?? ""}`.trim();
|
|
61
145
|
|
|
62
146
|
return (
|
|
@@ -66,35 +150,30 @@ export function TabList(props: TabListProps) {
|
|
|
66
150
|
);
|
|
67
151
|
}
|
|
68
152
|
|
|
153
|
+
/**
|
|
154
|
+
* A tab button that switches the active panel when clicked.
|
|
155
|
+
*
|
|
156
|
+
* The `value` prop must match a corresponding `TabPanel` value.
|
|
157
|
+
* In Astro, requires `client:load` directive.
|
|
158
|
+
*/
|
|
159
|
+
export interface TabProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
160
|
+
/** Unique identifier that links this tab to its panel */
|
|
161
|
+
value: string;
|
|
162
|
+
children: JSX.Element;
|
|
163
|
+
}
|
|
164
|
+
|
|
69
165
|
export function Tab(props: TabProps) {
|
|
70
166
|
const [local, rest] = splitProps(props, ["value", "children", "class"]);
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
const isActive = () => ctx?.activeTab() === local.value;
|
|
74
|
-
|
|
75
|
-
const handleClick = () => {
|
|
76
|
-
ctx?.setActiveTab(local.value);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const classes = () => {
|
|
80
|
-
const parts = ["tab"];
|
|
81
|
-
if (isActive()) {
|
|
82
|
-
parts.push("active");
|
|
83
|
-
}
|
|
84
|
-
if (local.class) {
|
|
85
|
-
parts.push(local.class);
|
|
86
|
-
}
|
|
87
|
-
return parts.join(" ");
|
|
88
|
-
};
|
|
167
|
+
const classes = () => `tab ${local.class ?? ""}`.trim();
|
|
89
168
|
|
|
90
169
|
return (
|
|
91
170
|
<button
|
|
92
171
|
type="button"
|
|
93
172
|
role="tab"
|
|
94
|
-
aria-selected=
|
|
95
|
-
tabIndex={
|
|
173
|
+
aria-selected="false"
|
|
174
|
+
tabIndex={-1}
|
|
96
175
|
class={classes()}
|
|
97
|
-
|
|
176
|
+
data-tab-value={local.value}
|
|
98
177
|
{...rest}
|
|
99
178
|
>
|
|
100
179
|
{local.children}
|
|
@@ -102,19 +181,26 @@ export function Tab(props: TabProps) {
|
|
|
102
181
|
);
|
|
103
182
|
}
|
|
104
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Content panel that is shown when its corresponding Tab is active.
|
|
186
|
+
*
|
|
187
|
+
* The `value` prop must match a corresponding `Tab` value.
|
|
188
|
+
* Panels are hidden by default and shown when their tab is selected.
|
|
189
|
+
* In Astro, requires `client:load` directive.
|
|
190
|
+
*/
|
|
191
|
+
export interface TabPanelProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
192
|
+
/** Unique identifier that links this panel to its tab */
|
|
193
|
+
value: string;
|
|
194
|
+
children: JSX.Element;
|
|
195
|
+
}
|
|
196
|
+
|
|
105
197
|
export function TabPanel(props: TabPanelProps) {
|
|
106
198
|
const [local, rest] = splitProps(props, ["value", "children", "class"]);
|
|
107
|
-
const ctx = useContext(TabsContext);
|
|
108
|
-
|
|
109
|
-
const isActive = () => ctx?.activeTab() === local.value;
|
|
110
|
-
|
|
111
199
|
const classes = () => `tab-panel ${local.class ?? ""}`.trim();
|
|
112
200
|
|
|
113
201
|
return (
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
</div>
|
|
118
|
-
</Show>
|
|
202
|
+
<div class={classes()} role="tabpanel" data-panel-value={local.value} hidden {...rest}>
|
|
203
|
+
{local.children}
|
|
204
|
+
</div>
|
|
119
205
|
);
|
|
120
206
|
}
|