@danielthurau/atlas-labs-codex 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 (42) hide show
  1. package/README.md +138 -0
  2. package/components/react/Badge/Badge.module.css +38 -0
  3. package/components/react/Badge/Badge.stories.tsx +79 -0
  4. package/components/react/Badge/Badge.tsx +42 -0
  5. package/components/react/Badge/index.ts +2 -0
  6. package/components/react/Button/Button.module.css +118 -0
  7. package/components/react/Button/Button.stories.tsx +108 -0
  8. package/components/react/Button/Button.tsx +73 -0
  9. package/components/react/Button/index.ts +2 -0
  10. package/components/react/Card/Card.module.css +59 -0
  11. package/components/react/Card/Card.stories.tsx +114 -0
  12. package/components/react/Card/Card.tsx +79 -0
  13. package/components/react/Card/index.ts +11 -0
  14. package/components/react/Input/Input.module.css +63 -0
  15. package/components/react/Input/Input.stories.tsx +88 -0
  16. package/components/react/Input/Input.tsx +50 -0
  17. package/components/react/Input/index.ts +2 -0
  18. package/components/react/Modal/Modal.module.css +103 -0
  19. package/components/react/Modal/Modal.stories.tsx +119 -0
  20. package/components/react/Modal/Modal.tsx +75 -0
  21. package/components/react/Modal/index.ts +2 -0
  22. package/components/react/RefreshButton/RefreshButton.module.css +202 -0
  23. package/components/react/RefreshButton/RefreshButton.stories.tsx +43 -0
  24. package/components/react/RefreshButton/RefreshButton.tsx +222 -0
  25. package/components/react/RefreshButton/index.ts +2 -0
  26. package/components/react/Tabs/Tabs.module.css +58 -0
  27. package/components/react/Tabs/Tabs.stories.tsx +101 -0
  28. package/components/react/Tabs/Tabs.tsx +62 -0
  29. package/components/react/Tabs/index.ts +2 -0
  30. package/components/react/Toast/Toast.module.css +145 -0
  31. package/components/react/Toast/Toast.stories.tsx +143 -0
  32. package/components/react/Toast/Toast.tsx +123 -0
  33. package/components/react/Toast/index.ts +2 -0
  34. package/components/react/index.ts +31 -0
  35. package/lib/index.ts +7 -0
  36. package/lib/utils.ts +32 -0
  37. package/package.json +95 -0
  38. package/public/fonts/MartianMono-OFL.txt +93 -0
  39. package/public/fonts/MartianMono-VariableFont_wdth,wght.ttf +0 -0
  40. package/themes/css/base.css +142 -0
  41. package/themes/css/theme-dark.css +48 -0
  42. package/themes/css/theme-light.css +49 -0
@@ -0,0 +1,2 @@
1
+ export { Modal, type ModalProps } from "./Modal";
2
+
@@ -0,0 +1,202 @@
1
+ .container {
2
+ position: relative;
3
+ display: inline-flex;
4
+ flex-direction: column;
5
+ }
6
+
7
+ .buttonGroup {
8
+ display: flex;
9
+ position: relative;
10
+ flex-direction: column;
11
+ }
12
+
13
+ .buttons {
14
+ display: flex;
15
+ }
16
+
17
+ /* Main refresh button */
18
+ .refreshButton {
19
+ display: flex;
20
+ align-items: center;
21
+ gap: var(--space-2);
22
+ padding: var(--space-2) var(--space-3);
23
+ background: var(--color-bg-subtle);
24
+ border: 1px solid var(--color-border-default);
25
+ border-right: none;
26
+ border-radius: var(--radius-md) 0 0 var(--radius-md);
27
+ font-family: inherit;
28
+ font-size: var(--text-sm);
29
+ color: var(--color-text-primary);
30
+ cursor: pointer;
31
+ transition: background 150ms, color 150ms;
32
+ }
33
+
34
+ /* When progress bar is shown, remove bottom radius */
35
+ .buttonGroup[data-has-progress="true"] .refreshButton {
36
+ border-radius: var(--radius-md) 0 0 0;
37
+ }
38
+
39
+ .refreshButton:hover:not(:disabled) {
40
+ background: var(--color-bg-muted);
41
+ }
42
+
43
+ .refreshButton:disabled {
44
+ opacity: 0.6;
45
+ cursor: not-allowed;
46
+ }
47
+
48
+ .icon {
49
+ display: inline-block;
50
+ font-size: var(--text-base);
51
+ }
52
+
53
+ .spinning {
54
+ animation: spin 1s linear infinite;
55
+ }
56
+
57
+ @keyframes spin {
58
+ from { transform: rotate(0deg); }
59
+ to { transform: rotate(360deg); }
60
+ }
61
+
62
+ /* Dropdown trigger */
63
+ .dropdownTrigger {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: var(--space-1);
67
+ padding: var(--space-2) var(--space-2);
68
+ background: var(--color-bg-subtle);
69
+ border: 1px solid var(--color-border-default);
70
+ border-radius: 0 var(--radius-md) var(--radius-md) 0;
71
+ font-family: inherit;
72
+ font-size: var(--text-xs);
73
+ color: var(--color-text-secondary);
74
+ cursor: pointer;
75
+ transition: background 150ms;
76
+ min-width: 60px;
77
+ justify-content: center;
78
+ }
79
+
80
+ /* When progress bar is shown, remove bottom radius */
81
+ .buttonGroup[data-has-progress="true"] .dropdownTrigger {
82
+ border-radius: 0 var(--radius-md) 0 0;
83
+ }
84
+
85
+ .dropdownTrigger:hover:not(:disabled) {
86
+ background: var(--color-bg-muted);
87
+ }
88
+
89
+ .dropdownTrigger:disabled {
90
+ opacity: 0.6;
91
+ cursor: not-allowed;
92
+ }
93
+
94
+ .intervalLabel {
95
+ font-weight: var(--font-medium);
96
+ }
97
+
98
+ .chevron {
99
+ font-size: 8px;
100
+ color: var(--color-text-muted);
101
+ }
102
+
103
+ /* Progress bar */
104
+ .progressBar {
105
+ width: 100%;
106
+ height: 6px;
107
+ background: var(--color-bg-muted);
108
+ border-radius: 0 0 var(--radius-sm) var(--radius-sm);
109
+ overflow: hidden;
110
+ border: 1px solid var(--color-border-default);
111
+ border-top: none;
112
+ }
113
+
114
+ .progressFill {
115
+ height: 100%;
116
+ background: linear-gradient(90deg, var(--color-intent-primary), var(--color-indigo-400));
117
+ transition: width 100ms linear;
118
+ min-width: 2px; /* Ensure always visible */
119
+ }
120
+
121
+ /* Dropdown menu */
122
+ .dropdown {
123
+ position: absolute;
124
+ top: calc(100% + var(--space-1));
125
+ right: 0;
126
+ min-width: 120px;
127
+ background: var(--color-bg-elevated);
128
+ border: 1px solid var(--color-border-default);
129
+ border-radius: var(--radius-md);
130
+ box-shadow: var(--shadow-lg);
131
+ z-index: 100;
132
+ overflow: hidden;
133
+ }
134
+
135
+ .dropdownItem {
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: space-between;
139
+ width: 100%;
140
+ padding: var(--space-2) var(--space-3);
141
+ background: transparent;
142
+ border: none;
143
+ font-family: inherit;
144
+ font-size: var(--text-sm);
145
+ color: var(--color-text-primary);
146
+ cursor: pointer;
147
+ transition: background 150ms;
148
+ text-align: left;
149
+ }
150
+
151
+ .dropdownItem:hover {
152
+ background: var(--color-bg-subtle);
153
+ }
154
+
155
+ .dropdownItem.selected {
156
+ background: var(--color-intent-primary-bg);
157
+ color: var(--color-intent-primary);
158
+ }
159
+
160
+ .checkmark {
161
+ font-size: var(--text-xs);
162
+ color: var(--color-intent-primary);
163
+ }
164
+
165
+ /* Dropdown divider */
166
+ .dropdownDivider {
167
+ height: 1px;
168
+ background: var(--color-border-default);
169
+ margin: var(--space-1) 0;
170
+ }
171
+
172
+ /* Check Now button in dropdown */
173
+ .checkNow {
174
+ color: var(--color-text-secondary);
175
+ font-weight: var(--font-medium);
176
+ }
177
+
178
+ .checkNow:hover {
179
+ background: var(--color-bg-muted);
180
+ color: var(--color-text-primary);
181
+ }
182
+
183
+ .checkNow:disabled {
184
+ opacity: 0.6;
185
+ cursor: not-allowed;
186
+ }
187
+
188
+ /* Responsive */
189
+ @media (max-width: 640px) {
190
+ .refreshButton span:not(.icon) {
191
+ display: none;
192
+ }
193
+
194
+ .refreshButton {
195
+ padding: var(--space-2);
196
+ }
197
+
198
+ .dropdownTrigger {
199
+ min-width: 50px;
200
+ }
201
+ }
202
+
@@ -0,0 +1,43 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { RefreshButton } from "./RefreshButton";
3
+
4
+ const meta: Meta<typeof RefreshButton> = {
5
+ title: "Components/RefreshButton",
6
+ component: RefreshButton,
7
+ parameters: {
8
+ layout: "centered",
9
+ },
10
+ tags: ["autodocs"],
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof RefreshButton>;
15
+
16
+ const mockRefresh = async () => {
17
+ await new Promise((resolve) => setTimeout(resolve, 1000));
18
+ };
19
+
20
+ const mockForceCheck = async () => {
21
+ await new Promise((resolve) => setTimeout(resolve, 2000));
22
+ };
23
+
24
+ export const Default: Story = {
25
+ args: {
26
+ onRefresh: mockRefresh,
27
+ },
28
+ };
29
+
30
+ export const WithForceCheck: Story = {
31
+ args: {
32
+ onRefresh: mockRefresh,
33
+ onForceCheck: mockForceCheck,
34
+ },
35
+ };
36
+
37
+ export const Disabled: Story = {
38
+ args: {
39
+ onRefresh: mockRefresh,
40
+ disabled: true,
41
+ },
42
+ };
43
+
@@ -0,0 +1,222 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useRef, useCallback } from "react";
4
+ import styles from "./RefreshButton.module.css";
5
+
6
+ interface RefreshButtonProps {
7
+ onRefresh: () => Promise<void>;
8
+ onForceCheck?: () => Promise<void>;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ type IntervalOption = {
13
+ label: string;
14
+ value: number | null; // null means manual only
15
+ };
16
+
17
+ const INTERVAL_OPTIONS: IntervalOption[] = [
18
+ { label: "Manual", value: null },
19
+ { label: "10s", value: 10000 },
20
+ { label: "30s", value: 30000 },
21
+ { label: "1m", value: 60000 },
22
+ { label: "5m", value: 300000 },
23
+ ];
24
+
25
+ export function RefreshButton({ onRefresh, onForceCheck, disabled }: RefreshButtonProps) {
26
+ const [isOpen, setIsOpen] = useState(false);
27
+ const [isRefreshing, setIsRefreshing] = useState(false);
28
+ const [isChecking, setIsChecking] = useState(false);
29
+ const [selectedInterval, setSelectedInterval] = useState<IntervalOption>(INTERVAL_OPTIONS[0]);
30
+ const [progress, setProgress] = useState(0);
31
+ const [timeLeft, setTimeLeft] = useState<number | null>(null);
32
+
33
+ const intervalRef = useRef<NodeJS.Timeout | null>(null);
34
+ const progressRef = useRef<NodeJS.Timeout | null>(null);
35
+ const dropdownRef = useRef<HTMLDivElement>(null);
36
+ const onRefreshRef = useRef(onRefresh);
37
+ const onForceCheckRef = useRef(onForceCheck);
38
+
39
+ // Keep refs updated
40
+ useEffect(() => {
41
+ onRefreshRef.current = onRefresh;
42
+ onForceCheckRef.current = onForceCheck;
43
+ }, [onRefresh, onForceCheck]);
44
+
45
+ // Handle manual refresh
46
+ const handleRefresh = useCallback(async () => {
47
+ if (isRefreshing || disabled) return;
48
+
49
+ setIsRefreshing(true);
50
+ setProgress(0);
51
+
52
+ try {
53
+ await onRefreshRef.current();
54
+ } finally {
55
+ setIsRefreshing(false);
56
+ // Reset progress timer if auto-refresh is active
57
+ if (selectedInterval.value) {
58
+ setProgress(0);
59
+ setTimeLeft(selectedInterval.value);
60
+ }
61
+ }
62
+ }, [isRefreshing, disabled, selectedInterval.value]);
63
+
64
+ // Handle interval selection
65
+ const handleSelectInterval = (option: IntervalOption) => {
66
+ setSelectedInterval(option);
67
+ setIsOpen(false);
68
+ setProgress(0);
69
+ setTimeLeft(option.value);
70
+ };
71
+
72
+ // Handle force check (runs actual health checks)
73
+ const handleForceCheck = useCallback(async () => {
74
+ if (isChecking || disabled || !onForceCheckRef.current) return;
75
+
76
+ setIsChecking(true);
77
+ setIsOpen(false);
78
+
79
+ try {
80
+ await onForceCheckRef.current();
81
+ } finally {
82
+ setIsChecking(false);
83
+ }
84
+ }, [isChecking, disabled]);
85
+
86
+ // Auto-refresh logic
87
+ useEffect(() => {
88
+ // Clear existing intervals
89
+ if (intervalRef.current) clearInterval(intervalRef.current);
90
+ if (progressRef.current) clearInterval(progressRef.current);
91
+
92
+ if (!selectedInterval.value) {
93
+ setProgress(0);
94
+ setTimeLeft(null);
95
+ return;
96
+ }
97
+
98
+ const intervalMs = selectedInterval.value;
99
+ setProgress(0);
100
+ setTimeLeft(intervalMs);
101
+
102
+ // Progress update every 100ms
103
+ const progressStep = 100 / (intervalMs / 100);
104
+ progressRef.current = setInterval(() => {
105
+ setProgress((prev) => {
106
+ const next = prev + progressStep;
107
+ return next >= 100 ? 0 : next; // Reset to 0 when reaching 100
108
+ });
109
+ setTimeLeft((prev) => {
110
+ if (prev === null) return intervalMs;
111
+ const next = prev - 100;
112
+ return next <= 0 ? intervalMs : next; // Reset to full when reaching 0
113
+ });
114
+ }, 100);
115
+
116
+ // Trigger refresh at interval
117
+ intervalRef.current = setInterval(async () => {
118
+ setIsRefreshing(true);
119
+ try {
120
+ await onRefreshRef.current();
121
+ } finally {
122
+ setIsRefreshing(false);
123
+ }
124
+ }, intervalMs);
125
+
126
+ return () => {
127
+ if (intervalRef.current) clearInterval(intervalRef.current);
128
+ if (progressRef.current) clearInterval(progressRef.current);
129
+ };
130
+ }, [selectedInterval.value]);
131
+
132
+ // Close dropdown on outside click
133
+ useEffect(() => {
134
+ const handleClickOutside = (event: MouseEvent) => {
135
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
136
+ setIsOpen(false);
137
+ }
138
+ };
139
+
140
+ document.addEventListener("mousedown", handleClickOutside);
141
+ return () => document.removeEventListener("mousedown", handleClickOutside);
142
+ }, []);
143
+
144
+ const isAutoRefresh = selectedInterval.value !== null;
145
+
146
+ return (
147
+ <div className={styles.container} ref={dropdownRef}>
148
+ <div className={styles.buttonGroup} data-has-progress={isAutoRefresh}>
149
+ <div className={styles.buttons}>
150
+ {/* Main refresh button */}
151
+ <button
152
+ className={styles.refreshButton}
153
+ onClick={handleRefresh}
154
+ disabled={isRefreshing || disabled}
155
+ title="Refresh now"
156
+ >
157
+ <span className={`${styles.icon} ${isRefreshing ? styles.spinning : ""}`}>
158
+
159
+ </span>
160
+ {isRefreshing ? "Refreshing..." : "Refresh"}
161
+ </button>
162
+
163
+ {/* Dropdown trigger */}
164
+ <button
165
+ className={styles.dropdownTrigger}
166
+ onClick={() => setIsOpen(!isOpen)}
167
+ disabled={disabled}
168
+ title="Set auto-refresh interval"
169
+ >
170
+ <span className={styles.intervalLabel}>
171
+ {selectedInterval.label}
172
+ </span>
173
+ <span className={styles.chevron}>{isOpen ? "▲" : "▼"}</span>
174
+ </button>
175
+ </div>
176
+
177
+ {/* Progress bar - only shows when auto-refresh is active */}
178
+ {isAutoRefresh && (
179
+ <div className={styles.progressBar}>
180
+ <div
181
+ className={styles.progressFill}
182
+ style={{ width: `${progress}%` }}
183
+ />
184
+ </div>
185
+ )}
186
+ </div>
187
+
188
+ {/* Dropdown menu */}
189
+ {isOpen && (
190
+ <div className={styles.dropdown}>
191
+ {INTERVAL_OPTIONS.map((option) => (
192
+ <button
193
+ key={option.label}
194
+ className={`${styles.dropdownItem} ${
195
+ selectedInterval.label === option.label ? styles.selected : ""
196
+ }`}
197
+ onClick={() => handleSelectInterval(option)}
198
+ >
199
+ {option.label}
200
+ {selectedInterval.label === option.label && (
201
+ <span className={styles.checkmark}>✓</span>
202
+ )}
203
+ </button>
204
+ ))}
205
+ {onForceCheck && (
206
+ <>
207
+ <div className={styles.dropdownDivider} />
208
+ <button
209
+ className={`${styles.dropdownItem} ${styles.checkNow}`}
210
+ onClick={handleForceCheck}
211
+ disabled={isChecking}
212
+ >
213
+ {isChecking ? "Checking..." : "Check Now"}
214
+ </button>
215
+ </>
216
+ )}
217
+ </div>
218
+ )}
219
+ </div>
220
+ );
221
+ }
222
+
@@ -0,0 +1,2 @@
1
+ export { RefreshButton } from "./RefreshButton";
2
+
@@ -0,0 +1,58 @@
1
+ .root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ }
5
+
6
+ .list {
7
+ display: flex;
8
+ gap: var(--space-1);
9
+ border-bottom: 1px solid var(--color-border-default);
10
+ margin-bottom: var(--space-4);
11
+ }
12
+
13
+ .trigger {
14
+ padding: var(--space-2) var(--space-4);
15
+ font-size: var(--text-sm);
16
+ font-weight: var(--font-medium);
17
+ color: var(--color-text-secondary);
18
+ background: transparent;
19
+ border: none;
20
+ border-bottom: 2px solid transparent;
21
+ margin-bottom: -1px;
22
+ cursor: pointer;
23
+ transition: color 150ms ease, border-color 150ms ease;
24
+ }
25
+
26
+ .trigger:hover:not(:disabled) {
27
+ color: var(--color-text-primary);
28
+ }
29
+
30
+ .trigger:focus-visible {
31
+ outline: none;
32
+ box-shadow: var(--focus-ring);
33
+ border-radius: var(--radius-sm);
34
+ }
35
+
36
+ .trigger[data-state="active"] {
37
+ color: var(--color-intent-primary);
38
+ border-bottom-color: var(--color-intent-primary);
39
+ }
40
+
41
+ .trigger:disabled {
42
+ color: var(--color-text-muted);
43
+ cursor: not-allowed;
44
+ }
45
+
46
+ .content {
47
+ outline: none;
48
+ }
49
+
50
+ .content:focus-visible {
51
+ box-shadow: var(--focus-ring);
52
+ border-radius: var(--radius-sm);
53
+ }
54
+
55
+ .content[data-state="inactive"] {
56
+ display: none;
57
+ }
58
+
@@ -0,0 +1,101 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Tabs } from "./Tabs";
3
+ import { Card } from "../Card";
4
+
5
+ const meta: Meta<typeof Tabs> = {
6
+ title: "Components/Tabs",
7
+ component: Tabs,
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ tags: ["autodocs"],
12
+ decorators: [
13
+ (Story) => (
14
+ <div style={{ width: "500px" }}>
15
+ <Story />
16
+ </div>
17
+ ),
18
+ ],
19
+ };
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ export const Default: Story = {
25
+ args: {
26
+ tabs: [
27
+ {
28
+ id: "account",
29
+ label: "Account",
30
+ content: (
31
+ <Card>
32
+ <h3>Account Settings</h3>
33
+ <p style={{ marginTop: "8px" }}>
34
+ Manage your account settings and preferences.
35
+ </p>
36
+ </Card>
37
+ ),
38
+ },
39
+ {
40
+ id: "password",
41
+ label: "Password",
42
+ content: (
43
+ <Card>
44
+ <h3>Password Settings</h3>
45
+ <p style={{ marginTop: "8px" }}>
46
+ Update your password and security options.
47
+ </p>
48
+ </Card>
49
+ ),
50
+ },
51
+ {
52
+ id: "notifications",
53
+ label: "Notifications",
54
+ content: (
55
+ <Card>
56
+ <h3>Notification Preferences</h3>
57
+ <p style={{ marginTop: "8px" }}>
58
+ Configure how you receive notifications.
59
+ </p>
60
+ </Card>
61
+ ),
62
+ },
63
+ ],
64
+ },
65
+ };
66
+
67
+ export const WithDisabledTab: Story = {
68
+ args: {
69
+ tabs: [
70
+ {
71
+ id: "active",
72
+ label: "Active",
73
+ content: <Card>This tab is active and clickable.</Card>,
74
+ },
75
+ {
76
+ id: "disabled",
77
+ label: "Disabled",
78
+ content: <Card>You shouldn't see this.</Card>,
79
+ disabled: true,
80
+ },
81
+ {
82
+ id: "another",
83
+ label: "Another",
84
+ content: <Card>This tab is also active.</Card>,
85
+ },
86
+ ],
87
+ },
88
+ };
89
+
90
+ export const ManyTabs: Story = {
91
+ args: {
92
+ tabs: [
93
+ { id: "1", label: "Overview", content: <Card>Overview content</Card> },
94
+ { id: "2", label: "Analytics", content: <Card>Analytics content</Card> },
95
+ { id: "3", label: "Reports", content: <Card>Reports content</Card> },
96
+ { id: "4", label: "Settings", content: <Card>Settings content</Card> },
97
+ { id: "5", label: "Users", content: <Card>Users content</Card> },
98
+ ],
99
+ },
100
+ };
101
+
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type ReactNode } from "react";
4
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
5
+ import { clsx } from "clsx";
6
+ import styles from "./Tabs.module.css";
7
+
8
+ export interface TabItem {
9
+ id: string;
10
+ label: ReactNode;
11
+ content: ReactNode;
12
+ disabled?: boolean;
13
+ }
14
+
15
+ export interface TabsProps {
16
+ tabs: TabItem[];
17
+ defaultValue?: string;
18
+ value?: string;
19
+ onValueChange?: (value: string) => void;
20
+ className?: string;
21
+ }
22
+
23
+ export const Tabs = forwardRef<HTMLDivElement, TabsProps>(
24
+ ({ tabs, defaultValue, value, onValueChange, className }, ref) => {
25
+ const defaultTab = defaultValue || tabs[0]?.id;
26
+
27
+ return (
28
+ <TabsPrimitive.Root
29
+ ref={ref}
30
+ defaultValue={defaultTab}
31
+ value={value}
32
+ onValueChange={onValueChange}
33
+ className={clsx(styles.root, className)}
34
+ >
35
+ <TabsPrimitive.List className={styles.list}>
36
+ {tabs.map((tab) => (
37
+ <TabsPrimitive.Trigger
38
+ key={tab.id}
39
+ value={tab.id}
40
+ disabled={tab.disabled}
41
+ className={styles.trigger}
42
+ >
43
+ {tab.label}
44
+ </TabsPrimitive.Trigger>
45
+ ))}
46
+ </TabsPrimitive.List>
47
+ {tabs.map((tab) => (
48
+ <TabsPrimitive.Content
49
+ key={tab.id}
50
+ value={tab.id}
51
+ className={styles.content}
52
+ >
53
+ {tab.content}
54
+ </TabsPrimitive.Content>
55
+ ))}
56
+ </TabsPrimitive.Root>
57
+ );
58
+ }
59
+ );
60
+
61
+ Tabs.displayName = "Tabs";
62
+
@@ -0,0 +1,2 @@
1
+ export { Tabs, type TabsProps, type TabItem } from "./Tabs";
2
+