@fragments-sdk/ui 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 (73) hide show
  1. package/package.json +44 -0
  2. package/src/brand.ts +15 -0
  3. package/src/components/Alert/Alert.fragment.tsx +163 -0
  4. package/src/components/Alert/Alert.module.scss +116 -0
  5. package/src/components/Alert/index.tsx +95 -0
  6. package/src/components/Avatar/Avatar.fragment.tsx +147 -0
  7. package/src/components/Avatar/Avatar.module.scss +136 -0
  8. package/src/components/Avatar/index.tsx +177 -0
  9. package/src/components/Badge/Badge.fragment.tsx +151 -0
  10. package/src/components/Badge/Badge.module.scss +87 -0
  11. package/src/components/Badge/index.tsx +55 -0
  12. package/src/components/Button/Button.fragment.tsx +159 -0
  13. package/src/components/Button/Button.module.scss +97 -0
  14. package/src/components/Button/index.tsx +51 -0
  15. package/src/components/Card/Card.fragment.tsx +156 -0
  16. package/src/components/Card/Card.module.scss +86 -0
  17. package/src/components/Card/index.tsx +79 -0
  18. package/src/components/Checkbox/Checkbox.fragment.tsx +166 -0
  19. package/src/components/Checkbox/Checkbox.module.scss +144 -0
  20. package/src/components/Checkbox/index.tsx +166 -0
  21. package/src/components/Dialog/Dialog.fragment.tsx +179 -0
  22. package/src/components/Dialog/Dialog.module.scss +158 -0
  23. package/src/components/Dialog/index.tsx +230 -0
  24. package/src/components/EmptyState/EmptyState.fragment.tsx +222 -0
  25. package/src/components/EmptyState/EmptyState.module.scss +120 -0
  26. package/src/components/EmptyState/index.tsx +80 -0
  27. package/src/components/Input/Input.fragment.tsx +174 -0
  28. package/src/components/Input/Input.module.scss +64 -0
  29. package/src/components/Input/index.tsx +76 -0
  30. package/src/components/Menu/Menu.fragment.tsx +168 -0
  31. package/src/components/Menu/Menu.module.scss +190 -0
  32. package/src/components/Menu/index.tsx +318 -0
  33. package/src/components/Popover/Popover.fragment.tsx +178 -0
  34. package/src/components/Popover/Popover.module.scss +165 -0
  35. package/src/components/Popover/index.tsx +229 -0
  36. package/src/components/Progress/Progress.fragment.tsx +142 -0
  37. package/src/components/Progress/Progress.module.scss +185 -0
  38. package/src/components/Progress/index.tsx +196 -0
  39. package/src/components/RadioGroup/RadioGroup.fragment.tsx +188 -0
  40. package/src/components/RadioGroup/RadioGroup.module.scss +155 -0
  41. package/src/components/RadioGroup/index.tsx +166 -0
  42. package/src/components/Select/Select.fragment.tsx +173 -0
  43. package/src/components/Select/Select.module.scss +187 -0
  44. package/src/components/Select/index.tsx +233 -0
  45. package/src/components/Separator/Separator.fragment.tsx +148 -0
  46. package/src/components/Separator/Separator.module.scss +92 -0
  47. package/src/components/Separator/index.tsx +89 -0
  48. package/src/components/Skeleton/Skeleton.fragment.tsx +147 -0
  49. package/src/components/Skeleton/Skeleton.module.scss +166 -0
  50. package/src/components/Skeleton/index.tsx +185 -0
  51. package/src/components/Table/Table.fragment.tsx +193 -0
  52. package/src/components/Table/Table.module.scss +152 -0
  53. package/src/components/Table/index.tsx +266 -0
  54. package/src/components/Tabs/Tabs.fragment.tsx +155 -0
  55. package/src/components/Tabs/Tabs.module.scss +142 -0
  56. package/src/components/Tabs/index.tsx +142 -0
  57. package/src/components/Textarea/Textarea.fragment.tsx +171 -0
  58. package/src/components/Textarea/Textarea.module.scss +89 -0
  59. package/src/components/Textarea/index.tsx +128 -0
  60. package/src/components/Toast/Toast.fragment.tsx +210 -0
  61. package/src/components/Toast/Toast.module.scss +227 -0
  62. package/src/components/Toast/index.tsx +315 -0
  63. package/src/components/Toggle/Toggle.fragment.tsx +174 -0
  64. package/src/components/Toggle/Toggle.module.scss +103 -0
  65. package/src/components/Toggle/index.tsx +80 -0
  66. package/src/components/Tooltip/Tooltip.fragment.tsx +158 -0
  67. package/src/components/Tooltip/Tooltip.module.scss +82 -0
  68. package/src/components/Tooltip/index.tsx +135 -0
  69. package/src/index.ts +151 -0
  70. package/src/scss.d.ts +4 -0
  71. package/src/styles/globals.scss +17 -0
  72. package/src/tokens/_mixins.scss +93 -0
  73. package/src/tokens/_variables.scss +276 -0
@@ -0,0 +1,266 @@
1
+ import * as React from 'react';
2
+ import {
3
+ useReactTable,
4
+ getCoreRowModel,
5
+ getSortedRowModel,
6
+ flexRender,
7
+ type ColumnDef,
8
+ type SortingState,
9
+ type RowSelectionState,
10
+ type OnChangeFn,
11
+ } from '@tanstack/react-table';
12
+ // Import globals to ensure CSS variables are defined
13
+ import '../../styles/globals.scss';
14
+ import styles from './Table.module.scss';
15
+
16
+ // Column definition helper type
17
+ export type TableColumn<T> = ColumnDef<T, unknown>;
18
+
19
+ export interface TableProps<T> {
20
+ /** Column definitions */
21
+ columns: TableColumn<T>[];
22
+ /** Data array */
23
+ data: T[];
24
+ /** Unique key extractor for each row */
25
+ getRowId?: (row: T) => string;
26
+ /** Enable sorting */
27
+ sortable?: boolean;
28
+ /** Controlled sorting state */
29
+ sorting?: SortingState;
30
+ /** Sorting change handler */
31
+ onSortingChange?: OnChangeFn<SortingState>;
32
+ /** Enable row selection */
33
+ selectable?: boolean;
34
+ /** Controlled selection state */
35
+ rowSelection?: RowSelectionState;
36
+ /** Selection change handler */
37
+ onRowSelectionChange?: OnChangeFn<RowSelectionState>;
38
+ /** Row click handler */
39
+ onRowClick?: (row: T) => void;
40
+ /** Empty state message */
41
+ emptyMessage?: string;
42
+ /** Size variant */
43
+ size?: 'sm' | 'md';
44
+ /** Additional class name */
45
+ className?: string;
46
+ }
47
+
48
+ export function Table<T>({
49
+ columns,
50
+ data,
51
+ getRowId,
52
+ sortable = false,
53
+ sorting: controlledSorting,
54
+ onSortingChange,
55
+ selectable = false,
56
+ rowSelection: controlledRowSelection,
57
+ onRowSelectionChange,
58
+ onRowClick,
59
+ emptyMessage = 'No data available',
60
+ size = 'md',
61
+ className,
62
+ }: TableProps<T>) {
63
+ // Internal sorting state when uncontrolled
64
+ const [internalSorting, setInternalSorting] = React.useState<SortingState>([]);
65
+ const sorting = controlledSorting ?? internalSorting;
66
+ const handleSortingChange = onSortingChange ?? setInternalSorting;
67
+
68
+ // Internal selection state when uncontrolled
69
+ const [internalRowSelection, setInternalRowSelection] = React.useState<RowSelectionState>({});
70
+ const rowSelection = controlledRowSelection ?? internalRowSelection;
71
+ const handleRowSelectionChange = onRowSelectionChange ?? setInternalRowSelection;
72
+
73
+ const table = useReactTable({
74
+ data,
75
+ columns,
76
+ getRowId,
77
+ getCoreRowModel: getCoreRowModel(),
78
+ getSortedRowModel: sortable ? getSortedRowModel() : undefined,
79
+ state: {
80
+ sorting: sortable ? sorting : undefined,
81
+ rowSelection: selectable ? rowSelection : undefined,
82
+ },
83
+ onSortingChange: sortable ? handleSortingChange : undefined,
84
+ onRowSelectionChange: selectable ? handleRowSelectionChange : undefined,
85
+ enableRowSelection: selectable,
86
+ enableSorting: sortable,
87
+ });
88
+
89
+ const isEmpty = data.length === 0;
90
+
91
+ const rootClasses = [styles.table, styles[size], className]
92
+ .filter(Boolean)
93
+ .join(' ');
94
+
95
+ if (isEmpty) {
96
+ return (
97
+ <div className={styles.emptyState}>
98
+ <span className={styles.emptyMessage}>{emptyMessage}</span>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ return (
104
+ <div className={styles.wrapper}>
105
+ <table className={rootClasses}>
106
+ <thead className={styles.thead}>
107
+ {table.getHeaderGroups().map((headerGroup) => (
108
+ <tr key={headerGroup.id} className={styles.headerRow}>
109
+ {headerGroup.headers.map((header) => {
110
+ const canSort = sortable && header.column.getCanSort();
111
+ const sortDirection = header.column.getIsSorted();
112
+
113
+ return (
114
+ <th
115
+ key={header.id}
116
+ className={styles.th}
117
+ style={{
118
+ width: header.getSize() !== 150 ? header.getSize() : undefined,
119
+ }}
120
+ onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
121
+ aria-sort={
122
+ sortDirection
123
+ ? sortDirection === 'asc'
124
+ ? 'ascending'
125
+ : 'descending'
126
+ : undefined
127
+ }
128
+ >
129
+ <div
130
+ className={[
131
+ styles.headerContent,
132
+ canSort && styles.sortable,
133
+ ]
134
+ .filter(Boolean)
135
+ .join(' ')}
136
+ >
137
+ {header.isPlaceholder
138
+ ? null
139
+ : flexRender(
140
+ header.column.columnDef.header,
141
+ header.getContext()
142
+ )}
143
+ {canSort && (
144
+ <span className={styles.sortIndicator} aria-hidden="true">
145
+ {sortDirection === 'asc' ? (
146
+ <SortAscIcon />
147
+ ) : sortDirection === 'desc' ? (
148
+ <SortDescIcon />
149
+ ) : (
150
+ <SortIcon />
151
+ )}
152
+ </span>
153
+ )}
154
+ </div>
155
+ </th>
156
+ );
157
+ })}
158
+ </tr>
159
+ ))}
160
+ </thead>
161
+ <tbody className={styles.tbody}>
162
+ {table.getRowModel().rows.map((row) => {
163
+ const isClickable = !!onRowClick;
164
+ const isSelected = selectable ? row.getIsSelected() : false;
165
+
166
+ return (
167
+ <tr
168
+ key={row.id}
169
+ className={[
170
+ styles.row,
171
+ isClickable && styles.clickable,
172
+ isSelected && styles.selected,
173
+ ]
174
+ .filter(Boolean)
175
+ .join(' ')}
176
+ onClick={isClickable ? () => onRowClick(row.original) : undefined}
177
+ data-selected={isSelected || undefined}
178
+ >
179
+ {row.getVisibleCells().map((cell) => (
180
+ <td key={cell.id} className={styles.td}>
181
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
182
+ </td>
183
+ ))}
184
+ </tr>
185
+ );
186
+ })}
187
+ </tbody>
188
+ </table>
189
+ </div>
190
+ );
191
+ }
192
+
193
+ // Sort icons - minimal and functional
194
+ function SortIcon() {
195
+ return (
196
+ <svg
197
+ width="12"
198
+ height="12"
199
+ viewBox="0 0 12 12"
200
+ fill="none"
201
+ xmlns="http://www.w3.org/2000/svg"
202
+ >
203
+ <path
204
+ d="M6 2L8.5 5H3.5L6 2Z"
205
+ fill="currentColor"
206
+ opacity="0.3"
207
+ />
208
+ <path
209
+ d="M6 10L3.5 7H8.5L6 10Z"
210
+ fill="currentColor"
211
+ opacity="0.3"
212
+ />
213
+ </svg>
214
+ );
215
+ }
216
+
217
+ function SortAscIcon() {
218
+ return (
219
+ <svg
220
+ width="12"
221
+ height="12"
222
+ viewBox="0 0 12 12"
223
+ fill="none"
224
+ xmlns="http://www.w3.org/2000/svg"
225
+ >
226
+ <path d="M6 2L8.5 5H3.5L6 2Z" fill="currentColor" />
227
+ </svg>
228
+ );
229
+ }
230
+
231
+ function SortDescIcon() {
232
+ return (
233
+ <svg
234
+ width="12"
235
+ height="12"
236
+ viewBox="0 0 12 12"
237
+ fill="none"
238
+ xmlns="http://www.w3.org/2000/svg"
239
+ >
240
+ <path d="M6 10L3.5 7H8.5L6 10Z" fill="currentColor" />
241
+ </svg>
242
+ );
243
+ }
244
+
245
+ // Helper to create simple columns without TanStack's createColumnHelper
246
+ export function createColumns<T>(
247
+ columns: Array<{
248
+ key: string;
249
+ header: string;
250
+ width?: number;
251
+ cell?: (row: T) => React.ReactNode;
252
+ }>
253
+ ): TableColumn<T>[] {
254
+ return columns.map((col) => ({
255
+ id: col.key,
256
+ accessorKey: col.key,
257
+ header: col.header,
258
+ size: col.width,
259
+ cell: col.cell
260
+ ? ({ row }) => col.cell!(row.original)
261
+ : ({ getValue }) => getValue() ?? '--',
262
+ }));
263
+ }
264
+
265
+ // Re-export useful types
266
+ export type { ColumnDef, SortingState, RowSelectionState };
@@ -0,0 +1,155 @@
1
+ import React from 'react';
2
+ import { defineSegment } from '@fragments/core';
3
+ import { Tabs } from './index.js';
4
+
5
+ export default defineSegment({
6
+ component: Tabs,
7
+
8
+ meta: {
9
+ name: 'Tabs',
10
+ description: 'Organize content into switchable panels. Use for related content that benefits from a compact, navigable layout.',
11
+ category: 'navigation',
12
+ status: 'stable',
13
+ tags: ['tabs', 'navigation', 'panels', 'content-switcher'],
14
+ since: '0.1.0',
15
+ },
16
+
17
+ usage: {
18
+ when: [
19
+ 'Organizing related content into sections',
20
+ 'Reducing page scrolling by grouping content',
21
+ 'Settings pages with multiple categories',
22
+ 'Dashboard views with different data perspectives',
23
+ ],
24
+ whenNot: [
25
+ 'Primary navigation (use sidebar or header nav)',
26
+ 'Sequential steps (use Stepper or wizard)',
27
+ 'Comparing content side-by-side',
28
+ 'Very long lists of options (use Select or Menu)',
29
+ ],
30
+ guidelines: [
31
+ 'Keep tab labels short (1-2 words)',
32
+ 'Order tabs by usage frequency or logical sequence',
33
+ 'Avoid more than 5-6 tabs; consider sub-navigation for more',
34
+ 'Tab content should be roughly equivalent in scope',
35
+ 'Use pills variant for contained sections, underline for page-level tabs',
36
+ ],
37
+ accessibility: [
38
+ 'Keyboard navigation with arrow keys',
39
+ 'Tab panels are properly labeled',
40
+ 'Focus management follows WAI-ARIA tabs pattern',
41
+ ],
42
+ },
43
+
44
+ props: {
45
+ children: {
46
+ type: 'node',
47
+ description: 'Tab list and panels (use Tabs.List, Tabs.Tab, Tabs.Panel)',
48
+ required: true,
49
+ },
50
+ defaultValue: {
51
+ type: 'string',
52
+ description: 'Initially active tab (uncontrolled)',
53
+ },
54
+ value: {
55
+ type: 'string',
56
+ description: 'Controlled active tab value',
57
+ },
58
+ onValueChange: {
59
+ type: 'function',
60
+ description: 'Called when active tab changes',
61
+ },
62
+ orientation: {
63
+ type: 'enum',
64
+ description: 'Tab list orientation',
65
+ values: ['horizontal', 'vertical'],
66
+ default: 'horizontal',
67
+ },
68
+ },
69
+
70
+ relations: [
71
+ { component: 'Select', relationship: 'alternative', note: 'Use Select for many options in compact space' },
72
+ { component: 'Menu', relationship: 'alternative', note: 'Use Menu for action-based navigation' },
73
+ ],
74
+
75
+ contract: {
76
+ propsSummary: [
77
+ 'value: string - controlled active tab',
78
+ 'defaultValue: string - initial active tab',
79
+ 'onValueChange: (value) => void - tab change handler',
80
+ 'Tabs.List variant: underline|pills - visual style',
81
+ ],
82
+ scenarioTags: [
83
+ 'navigation.tabs',
84
+ 'layout.panels',
85
+ 'content.sections',
86
+ ],
87
+ a11yRules: ['A11Y_TABS_KEYBOARD', 'A11Y_TABS_LABELS'],
88
+ },
89
+
90
+ variants: [
91
+ {
92
+ name: 'Underline',
93
+ description: 'Default underline style tabs',
94
+ render: () => (
95
+ <Tabs defaultValue="overview">
96
+ <Tabs.List variant="underline">
97
+ <Tabs.Tab value="overview">Overview</Tabs.Tab>
98
+ <Tabs.Tab value="analytics">Analytics</Tabs.Tab>
99
+ <Tabs.Tab value="settings">Settings</Tabs.Tab>
100
+ </Tabs.List>
101
+ <Tabs.Panel value="overview">
102
+ <p>Overview content goes here.</p>
103
+ </Tabs.Panel>
104
+ <Tabs.Panel value="analytics">
105
+ <p>Analytics content goes here.</p>
106
+ </Tabs.Panel>
107
+ <Tabs.Panel value="settings">
108
+ <p>Settings content goes here.</p>
109
+ </Tabs.Panel>
110
+ </Tabs>
111
+ ),
112
+ },
113
+ {
114
+ name: 'Pills',
115
+ description: 'Pill-style tabs for contained sections',
116
+ render: () => (
117
+ <Tabs defaultValue="all">
118
+ <Tabs.List variant="pills">
119
+ <Tabs.Tab value="all">All</Tabs.Tab>
120
+ <Tabs.Tab value="active">Active</Tabs.Tab>
121
+ <Tabs.Tab value="archived">Archived</Tabs.Tab>
122
+ </Tabs.List>
123
+ <Tabs.Panel value="all">
124
+ <p>Showing all items.</p>
125
+ </Tabs.Panel>
126
+ <Tabs.Panel value="active">
127
+ <p>Showing active items only.</p>
128
+ </Tabs.Panel>
129
+ <Tabs.Panel value="archived">
130
+ <p>Showing archived items.</p>
131
+ </Tabs.Panel>
132
+ </Tabs>
133
+ ),
134
+ },
135
+ {
136
+ name: 'With Disabled',
137
+ description: 'Tabs with a disabled option',
138
+ render: () => (
139
+ <Tabs defaultValue="general">
140
+ <Tabs.List variant="underline">
141
+ <Tabs.Tab value="general">General</Tabs.Tab>
142
+ <Tabs.Tab value="security">Security</Tabs.Tab>
143
+ <Tabs.Tab value="billing" disabled>Billing</Tabs.Tab>
144
+ </Tabs.List>
145
+ <Tabs.Panel value="general">
146
+ <p>General settings panel.</p>
147
+ </Tabs.Panel>
148
+ <Tabs.Panel value="security">
149
+ <p>Security settings panel.</p>
150
+ </Tabs.Panel>
151
+ </Tabs>
152
+ ),
153
+ },
154
+ ],
155
+ });
@@ -0,0 +1,142 @@
1
+ @use '../../tokens/variables' as *;
2
+ @use '../../tokens/mixins' as *;
3
+
4
+ // Root container
5
+ .root {
6
+ display: flex;
7
+ flex-direction: column;
8
+ font-family: var(--fui-font-sans, $fui-font-sans);
9
+
10
+ &[data-orientation='vertical'] {
11
+ flex-direction: row;
12
+ }
13
+ }
14
+
15
+ // Tab list container
16
+ .list {
17
+ position: relative;
18
+ display: flex;
19
+ align-items: center;
20
+ gap: var(--fui-space-1, $fui-space-1);
21
+ border-bottom: 1px solid var(--fui-border, $fui-border);
22
+ padding: 0 var(--fui-space-1, $fui-space-1);
23
+
24
+ &[data-orientation='vertical'] {
25
+ flex-direction: column;
26
+ align-items: stretch;
27
+ border-bottom: none;
28
+ border-right: 1px solid var(--fui-border, $fui-border);
29
+ padding: var(--fui-space-1, $fui-space-1) 0;
30
+ }
31
+ }
32
+
33
+ // Variant: pills (no border, background tabs)
34
+ .listPills {
35
+ border-bottom: none;
36
+ background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
37
+ border-radius: var(--fui-radius-md, $fui-radius-md);
38
+ padding: var(--fui-space-1, $fui-space-1);
39
+ }
40
+
41
+ // Variant: underline (default, with indicator line)
42
+ .listUnderline {
43
+ // Default styles already applied to .list
44
+ }
45
+
46
+ // Individual tab button
47
+ .tab {
48
+ @include button-reset;
49
+ @include interactive-base;
50
+
51
+ position: relative;
52
+ display: inline-flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ gap: var(--fui-space-2, $fui-space-2);
56
+ padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
57
+ font-size: var(--fui-font-size-sm, $fui-font-size-sm);
58
+ font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
59
+ color: var(--fui-text-secondary, $fui-text-secondary);
60
+ white-space: nowrap;
61
+ border-radius: var(--fui-radius-sm, $fui-radius-sm);
62
+
63
+ &:hover:not([data-disabled]) {
64
+ color: var(--fui-text-primary, $fui-text-primary);
65
+ background-color: var(--fui-bg-hover, $fui-bg-hover);
66
+ }
67
+
68
+ &[data-active] {
69
+ color: var(--fui-text-primary, $fui-text-primary);
70
+ }
71
+
72
+ &[data-disabled] {
73
+ color: var(--fui-text-tertiary, $fui-text-tertiary);
74
+ }
75
+ }
76
+
77
+ // Tab in underline variant
78
+ .tabUnderline {
79
+ border-radius: 0;
80
+ margin-bottom: -1px;
81
+ padding-bottom: calc(var(--fui-space-2, $fui-space-2) + 1px);
82
+
83
+ &:hover:not([data-disabled]) {
84
+ background-color: transparent;
85
+ }
86
+ }
87
+
88
+ // Tab in pills variant
89
+ .tabPills {
90
+ &[data-active] {
91
+ background-color: var(--fui-bg-elevated, $fui-bg-elevated);
92
+ box-shadow: var(--fui-shadow-sm, $fui-shadow-sm);
93
+ }
94
+ }
95
+
96
+ // Animated indicator (underline variant)
97
+ .indicator {
98
+ position: absolute;
99
+ bottom: 0;
100
+ left: 0;
101
+ height: 2px;
102
+ background-color: var(--fui-color-accent, $fui-color-accent);
103
+ border-radius: var(--fui-radius-full, $fui-radius-full);
104
+
105
+ // Use CSS variables from Base UI for position
106
+ width: var(--active-tab-width);
107
+ transform: translateX(var(--active-tab-left));
108
+ transition:
109
+ width var(--fui-transition-fast, $fui-transition-fast),
110
+ transform var(--fui-transition-fast, $fui-transition-fast);
111
+
112
+ [data-orientation='vertical'] & {
113
+ bottom: auto;
114
+ left: auto;
115
+ right: 0;
116
+ top: 0;
117
+ width: 2px;
118
+ height: var(--active-tab-height);
119
+ transform: translateY(var(--active-tab-top));
120
+ }
121
+ }
122
+
123
+ // Tab panel content
124
+ .panel {
125
+ padding: var(--fui-space-4, $fui-space-4) 0;
126
+ outline: none;
127
+
128
+ &[data-hidden] {
129
+ display: none;
130
+ }
131
+
132
+ // Focus visible for keyboard navigation
133
+ &:focus-visible {
134
+ @include focus-ring;
135
+ border-radius: var(--fui-radius-md, $fui-radius-md);
136
+ }
137
+ }
138
+
139
+ // Panel with no padding (for custom layouts)
140
+ .panelFlush {
141
+ padding: 0;
142
+ }
@@ -0,0 +1,142 @@
1
+ import * as React from 'react';
2
+ import { Tabs as BaseTabs } from '@base-ui/react/tabs';
3
+ import styles from './Tabs.module.scss';
4
+ // Import globals to ensure CSS variables are defined
5
+ import '../../styles/globals.scss';
6
+
7
+ // ============================================
8
+ // Types
9
+ // ============================================
10
+
11
+ export type TabValue = string | number;
12
+
13
+ export interface TabsProps {
14
+ children: React.ReactNode;
15
+ defaultValue?: TabValue;
16
+ value?: TabValue;
17
+ onValueChange?: (value: TabValue) => void;
18
+ orientation?: 'horizontal' | 'vertical';
19
+ className?: string;
20
+ }
21
+
22
+ export interface TabsListProps {
23
+ children: React.ReactNode;
24
+ variant?: 'underline' | 'pills';
25
+ className?: string;
26
+ }
27
+
28
+ export interface TabProps {
29
+ children: React.ReactNode;
30
+ value: TabValue;
31
+ disabled?: boolean;
32
+ className?: string;
33
+ }
34
+
35
+ export interface TabsPanelProps {
36
+ children: React.ReactNode;
37
+ value: TabValue;
38
+ keepMounted?: boolean;
39
+ flush?: boolean;
40
+ className?: string;
41
+ }
42
+
43
+ // ============================================
44
+ // Context for variant
45
+ // ============================================
46
+
47
+ const TabsVariantContext = React.createContext<'underline' | 'pills'>('underline');
48
+
49
+ // ============================================
50
+ // Components
51
+ // ============================================
52
+
53
+ function TabsRoot({
54
+ children,
55
+ defaultValue,
56
+ value,
57
+ onValueChange,
58
+ orientation = 'horizontal',
59
+ className,
60
+ }: TabsProps) {
61
+ const classes = [styles.root, className].filter(Boolean).join(' ');
62
+
63
+ return (
64
+ <BaseTabs.Root
65
+ defaultValue={defaultValue}
66
+ value={value}
67
+ onValueChange={onValueChange}
68
+ orientation={orientation}
69
+ className={classes}
70
+ >
71
+ {children}
72
+ </BaseTabs.Root>
73
+ );
74
+ }
75
+
76
+ function TabsList({
77
+ children,
78
+ variant = 'underline',
79
+ className,
80
+ }: TabsListProps) {
81
+ const variantClass = variant === 'pills' ? styles.listPills : styles.listUnderline;
82
+ const classes = [styles.list, variantClass, className].filter(Boolean).join(' ');
83
+
84
+ return (
85
+ <TabsVariantContext.Provider value={variant}>
86
+ <BaseTabs.List className={classes}>
87
+ {children}
88
+ {variant === 'underline' && <BaseTabs.Indicator className={styles.indicator} />}
89
+ </BaseTabs.List>
90
+ </TabsVariantContext.Provider>
91
+ );
92
+ }
93
+
94
+ function Tab({
95
+ children,
96
+ value,
97
+ disabled,
98
+ className,
99
+ }: TabProps) {
100
+ const variant = React.useContext(TabsVariantContext);
101
+ const variantClass = variant === 'pills' ? styles.tabPills : styles.tabUnderline;
102
+ const classes = [styles.tab, variantClass, className].filter(Boolean).join(' ');
103
+
104
+ return (
105
+ <BaseTabs.Tab value={value} disabled={disabled} className={classes}>
106
+ {children}
107
+ </BaseTabs.Tab>
108
+ );
109
+ }
110
+
111
+ function TabsPanel({
112
+ children,
113
+ value,
114
+ keepMounted = false,
115
+ flush = false,
116
+ className,
117
+ }: TabsPanelProps) {
118
+ const classes = [
119
+ styles.panel,
120
+ flush && styles.panelFlush,
121
+ className,
122
+ ].filter(Boolean).join(' ');
123
+
124
+ return (
125
+ <BaseTabs.Panel value={value} keepMounted={keepMounted} className={classes}>
126
+ {children}
127
+ </BaseTabs.Panel>
128
+ );
129
+ }
130
+
131
+ // ============================================
132
+ // Export compound component
133
+ // ============================================
134
+
135
+ export const Tabs = Object.assign(TabsRoot, {
136
+ List: TabsList,
137
+ Tab: Tab,
138
+ Panel: TabsPanel,
139
+ });
140
+
141
+ // Re-export individual components
142
+ export { TabsRoot, TabsList, Tab, TabsPanel };