@papernote/ui 1.1.0 → 1.3.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 (114) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +455 -455
  3. package/dist/components/Box.d.ts +2 -1
  4. package/dist/components/Box.d.ts.map +1 -1
  5. package/dist/components/Button.d.ts +10 -1
  6. package/dist/components/Button.d.ts.map +1 -1
  7. package/dist/components/Card.d.ts +11 -2
  8. package/dist/components/Card.d.ts.map +1 -1
  9. package/dist/components/CurrencyInput.d.ts +52 -0
  10. package/dist/components/CurrencyInput.d.ts.map +1 -0
  11. package/dist/components/DataTable.d.ts +19 -3
  12. package/dist/components/DataTable.d.ts.map +1 -1
  13. package/dist/components/EmptyState.d.ts +3 -1
  14. package/dist/components/EmptyState.d.ts.map +1 -1
  15. package/dist/components/Grid.d.ts +4 -2
  16. package/dist/components/Grid.d.ts.map +1 -1
  17. package/dist/components/Input.d.ts +2 -0
  18. package/dist/components/Input.d.ts.map +1 -1
  19. package/dist/components/Modal.d.ts.map +1 -1
  20. package/dist/components/MultiSelect.d.ts +13 -1
  21. package/dist/components/MultiSelect.d.ts.map +1 -1
  22. package/dist/components/Page.d.ts +2 -0
  23. package/dist/components/Page.d.ts.map +1 -1
  24. package/dist/components/PageLayout.d.ts +5 -1
  25. package/dist/components/PageLayout.d.ts.map +1 -1
  26. package/dist/components/Stack.d.ts +25 -5
  27. package/dist/components/Stack.d.ts.map +1 -1
  28. package/dist/components/Text.d.ts +20 -4
  29. package/dist/components/Text.d.ts.map +1 -1
  30. package/dist/components/Textarea.d.ts +2 -0
  31. package/dist/components/Textarea.d.ts.map +1 -1
  32. package/dist/components/index.d.ts +5 -3
  33. package/dist/components/index.d.ts.map +1 -1
  34. package/dist/index.d.ts +311 -49
  35. package/dist/index.esm.js +557 -224
  36. package/dist/index.esm.js.map +1 -1
  37. package/dist/index.js +555 -219
  38. package/dist/index.js.map +1 -1
  39. package/dist/styles.css +2838 -2679
  40. package/dist/utils/excelExport.d.ts +143 -0
  41. package/dist/utils/excelExport.d.ts.map +1 -0
  42. package/dist/utils/index.d.ts +2 -0
  43. package/dist/utils/index.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/components/AdminModal.css +49 -49
  46. package/src/components/Box.stories.tsx +377 -0
  47. package/src/components/Box.tsx +8 -4
  48. package/src/components/Button.tsx +23 -10
  49. package/src/components/Card.tsx +20 -5
  50. package/src/components/CurrencyInput.stories.tsx +290 -0
  51. package/src/components/CurrencyInput.tsx +193 -0
  52. package/src/components/DataTable.stories.tsx +36 -25
  53. package/src/components/DataTable.tsx +170 -16
  54. package/src/components/EmptyState.stories.tsx +124 -72
  55. package/src/components/EmptyState.tsx +10 -0
  56. package/src/components/Grid.stories.tsx +348 -0
  57. package/src/components/Grid.tsx +12 -5
  58. package/src/components/Input.tsx +12 -2
  59. package/src/components/Modal.stories.tsx +64 -0
  60. package/src/components/Modal.tsx +15 -2
  61. package/src/components/MultiSelect.tsx +41 -10
  62. package/src/components/Page.stories.tsx +76 -0
  63. package/src/components/Page.tsx +35 -3
  64. package/src/components/PageLayout.stories.tsx +75 -0
  65. package/src/components/PageLayout.tsx +28 -9
  66. package/src/components/RoleManager.css +10 -10
  67. package/src/components/Spreadsheet.css +216 -216
  68. package/src/components/Spreadsheet.stories.tsx +362 -362
  69. package/src/components/Spreadsheet.tsx +351 -351
  70. package/src/components/SpreadsheetSimple.stories.tsx +27 -27
  71. package/src/components/Stack.stories.tsx +24 -1
  72. package/src/components/Stack.tsx +40 -10
  73. package/src/components/Tabs.tsx +152 -152
  74. package/src/components/Text.stories.tsx +273 -0
  75. package/src/components/Text.tsx +33 -8
  76. package/src/components/Textarea.tsx +32 -21
  77. package/src/components/index.ts +6 -4
  78. package/src/styles/index.css +41 -4
  79. package/src/utils/excelExport.stories.tsx +535 -0
  80. package/src/utils/excelExport.ts +225 -0
  81. package/src/utils/index.ts +3 -0
  82. package/tailwind.config.js +253 -253
  83. package/dist/components/Button.stories.d.ts +0 -51
  84. package/dist/components/Button.stories.d.ts.map +0 -1
  85. package/dist/components/ChartVisualizationUI.d.ts +0 -21
  86. package/dist/components/ChartVisualizationUI.d.ts.map +0 -1
  87. package/dist/components/ChatUI.d.ts +0 -23
  88. package/dist/components/ChatUI.d.ts.map +0 -1
  89. package/dist/components/CommissionDashboardUI.d.ts +0 -25
  90. package/dist/components/CommissionDashboardUI.d.ts.map +0 -1
  91. package/dist/components/DataTable.stories.d.ts +0 -23
  92. package/dist/components/DataTable.stories.d.ts.map +0 -1
  93. package/dist/components/FormField.d.ts +0 -35
  94. package/dist/components/FormField.d.ts.map +0 -1
  95. package/dist/components/Input.stories.d.ts +0 -366
  96. package/dist/components/Input.stories.d.ts.map +0 -1
  97. package/dist/components/InsightsPanelUI.d.ts +0 -21
  98. package/dist/components/InsightsPanelUI.d.ts.map +0 -1
  99. package/dist/components/PaymentHistoryTimeline.d.ts +0 -34
  100. package/dist/components/PaymentHistoryTimeline.d.ts.map +0 -1
  101. package/dist/components/RelationshipManagerUI.d.ts +0 -60
  102. package/dist/components/RelationshipManagerUI.d.ts.map +0 -1
  103. package/dist/components/RoleManager.d.ts +0 -19
  104. package/dist/components/RoleManager.d.ts.map +0 -1
  105. package/dist/components/SplitCommissionBadge.d.ts +0 -18
  106. package/dist/components/SplitCommissionBadge.d.ts.map +0 -1
  107. package/dist/components/Spreadsheet.css +0 -216
  108. package/dist/components/Table.d.ts +0 -26
  109. package/dist/components/Table.d.ts.map +0 -1
  110. package/dist/components/__tests__/Button.test.d.ts +0 -2
  111. package/dist/components/__tests__/Button.test.d.ts.map +0 -1
  112. package/dist/components/__tests__/Input.test.d.ts +0 -2
  113. package/dist/components/__tests__/Input.test.d.ts.map +0 -1
  114. package/src/components/Table.tsx +0 -239
@@ -1,27 +1,27 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
2
- import { Spreadsheet } from './Spreadsheet';
3
-
4
- const meta: Meta<typeof Spreadsheet> = {
5
- title: 'Components/Spreadsheet/Simple Test',
6
- component: Spreadsheet,
7
- parameters: {
8
- docs: {
9
- description: {
10
- component: 'Simple test story for Spreadsheet component debugging.',
11
- },
12
- },
13
- },
14
- };
15
-
16
- export default meta;
17
- type Story = StoryObj<typeof Spreadsheet>;
18
-
19
- /**
20
- * Minimal test - just render with defaults
21
- */
22
- export const MinimalTest: Story = {
23
- args: {
24
- rows: 5,
25
- columns: 3,
26
- },
27
- };
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Spreadsheet } from './Spreadsheet';
3
+
4
+ const meta: Meta<typeof Spreadsheet> = {
5
+ title: 'Components/Spreadsheet/Simple Test',
6
+ component: Spreadsheet,
7
+ parameters: {
8
+ docs: {
9
+ description: {
10
+ component: 'Simple test story for Spreadsheet component debugging.',
11
+ },
12
+ },
13
+ },
14
+ };
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof Spreadsheet>;
18
+
19
+ /**
20
+ * Minimal test - just render with defaults
21
+ */
22
+ export const MinimalTest: Story = {
23
+ args: {
24
+ rows: 5,
25
+ columns: 3,
26
+ },
27
+ };
@@ -49,7 +49,16 @@ import { Stack, Button } from 'notebook-ui';
49
49
  spacing: {
50
50
  control: 'select',
51
51
  options: ['none', 'xs', 'sm', 'md', 'lg', 'xl'],
52
- description: 'Gap spacing between children (none: 0, xs: 0.25rem, sm: 0.5rem, md: 1rem, lg: 1.5rem, xl: 2rem)',
52
+ description: 'Gap spacing between children (alias: gap)',
53
+ table: {
54
+ type: { summary: 'none | xs | sm | md | lg | xl' },
55
+ defaultValue: { summary: 'md' },
56
+ },
57
+ },
58
+ gap: {
59
+ control: 'select',
60
+ options: ['none', 'xs', 'sm', 'md', 'lg', 'xl'],
61
+ description: 'Gap spacing between children (alias for spacing)',
53
62
  table: {
54
63
  type: { summary: 'none | xs | sm | md | lg | xl' },
55
64
  defaultValue: { summary: 'md' },
@@ -339,3 +348,17 @@ export const NestedStacks: Story = {
339
348
  </Stack>
340
349
  ),
341
350
  };
351
+
352
+ /**
353
+ * The `gap` prop is an alias for `spacing` - they are interchangeable.
354
+ * This provides flexibility for developers who prefer the `gap` terminology.
355
+ */
356
+ export const GapAlias: Story = {
357
+ render: () => (
358
+ <Stack direction="horizontal" gap="md">
359
+ <Box>Using</Box>
360
+ <Box color="#8b5cf6">gap</Box>
361
+ <Box color="#10b981">prop</Box>
362
+ </Stack>
363
+ ),
364
+ };
@@ -1,15 +1,19 @@
1
1
  // Stack Component - Vertical or horizontal stacking layout
2
2
  // Provides consistent spacing between child elements
3
3
 
4
- import React from 'react';
4
+ import React, { forwardRef } from 'react';
5
5
 
6
- export interface StackProps {
6
+ type SpacingValue = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
7
+
8
+ export interface StackProps extends React.HTMLAttributes<HTMLDivElement> {
7
9
  /** Content to stack */
8
10
  children: React.ReactNode;
9
11
  /** Direction of stack */
10
12
  direction?: 'vertical' | 'horizontal';
11
- /** Spacing between items */
12
- spacing?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
13
+ /** Spacing between items (alias: gap) */
14
+ spacing?: SpacingValue;
15
+ /** Spacing between items (alias for spacing - for developer convenience) */
16
+ gap?: SpacingValue;
13
17
  /** Alignment of items */
14
18
  align?: 'start' | 'center' | 'end' | 'stretch';
15
19
  /** Justify content */
@@ -23,23 +27,45 @@ export interface StackProps {
23
27
  /**
24
28
  * Stack component for arranging children vertically or horizontally with consistent spacing.
25
29
  *
26
- * Spacing scale:
30
+ * Supports ref forwarding for DOM access.
31
+ *
32
+ * Spacing scale (use either `spacing` or `gap` prop - they're aliases):
27
33
  * - none: 0
28
34
  * - xs: 0.5rem (2)
29
35
  * - sm: 0.75rem (3)
30
36
  * - md: 1.5rem (6)
31
37
  * - lg: 2rem (8)
32
38
  * - xl: 3rem (12)
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * // Using spacing prop
43
+ * <Stack spacing="md">
44
+ * <Card>Item 1</Card>
45
+ * <Card>Item 2</Card>
46
+ * </Stack>
47
+ *
48
+ * // Using gap prop (alias)
49
+ * <Stack gap="md">
50
+ * <Card>Item 1</Card>
51
+ * <Card>Item 2</Card>
52
+ * </Stack>
53
+ * ```
33
54
  */
34
- export const Stack: React.FC<StackProps> = ({
55
+ export const Stack = forwardRef<HTMLDivElement, StackProps>(({
35
56
  children,
36
57
  direction = 'vertical',
37
- spacing = 'md',
58
+ spacing,
59
+ gap,
38
60
  align = 'stretch',
39
61
  justify = 'start',
40
62
  wrap = false,
41
63
  className = '',
42
- }) => {
64
+ ...htmlProps
65
+ }, ref) => {
66
+ // Use gap as alias for spacing (spacing takes precedence if both provided)
67
+ const effectiveSpacing = spacing ?? gap ?? 'md';
68
+
43
69
  const spacingClasses = {
44
70
  vertical: {
45
71
  none: '',
@@ -76,11 +102,13 @@ export const Stack: React.FC<StackProps> = ({
76
102
 
77
103
  return (
78
104
  <div
105
+ ref={ref}
106
+ {...htmlProps}
79
107
  className={`
80
108
  flex
81
109
  ${direction === 'vertical' ? 'flex-col' : 'flex-row'}
82
110
  ${wrap ? 'flex-wrap' : ''}
83
- ${spacingClasses[direction][spacing]}
111
+ ${spacingClasses[direction][effectiveSpacing]}
84
112
  ${alignClasses[align]}
85
113
  ${justifyClasses[justify]}
86
114
  ${className}
@@ -89,6 +117,8 @@ export const Stack: React.FC<StackProps> = ({
89
117
  {children}
90
118
  </div>
91
119
  );
92
- };
120
+ });
121
+
122
+ Stack.displayName = 'Stack';
93
123
 
94
124
  export default Stack;
@@ -1,152 +1,152 @@
1
- import React, { useState, useEffect } from 'react';
2
-
3
- export interface Tab {
4
- id: string;
5
- label: string;
6
- icon?: React.ReactNode;
7
- content: React.ReactNode;
8
- disabled?: boolean;
9
- }
10
-
11
- export interface TabsProps {
12
- tabs: Tab[];
13
- /** Controlled mode: Currently active tab ID */
14
- activeTab?: string;
15
- /** Uncontrolled mode: Initial tab ID (ignored if activeTab is provided) */
16
- defaultTab?: string;
17
- variant?: 'underline' | 'pill';
18
- /** Orientation of tabs (default: 'horizontal') */
19
- orientation?: 'horizontal' | 'vertical';
20
- /** Size of tabs (default: 'md') */
21
- size?: 'sm' | 'md' | 'lg';
22
- /** Called when tab changes (required for controlled mode) */
23
- onChange?: (tabId: string) => void;
24
- }
25
-
26
- export default function Tabs({ tabs, activeTab: controlledActiveTab, defaultTab, variant = 'underline', orientation = 'horizontal', size = 'md', onChange }: TabsProps) {
27
- const [internalActiveTab, setInternalActiveTab] = useState(defaultTab || tabs[0]?.id);
28
-
29
- // Controlled mode: use activeTab prop, Uncontrolled mode: use internal state
30
- const isControlled = controlledActiveTab !== undefined;
31
- const activeTab = isControlled ? controlledActiveTab : internalActiveTab;
32
-
33
- // Ensure the activeTab exists in the current tabs array
34
- // This handles the case where tabs array reference changes at the same time as activeTab
35
- useEffect(() => {
36
- const tabExists = tabs.some(tab => tab.id === activeTab);
37
- if (!tabExists && tabs.length > 0) {
38
- // If the activeTab doesn't exist in the new tabs array, use the first tab
39
- if (isControlled) {
40
- onChange?.(tabs[0].id);
41
- } else {
42
- setInternalActiveTab(tabs[0].id);
43
- }
44
- }
45
- }, [tabs, activeTab, isControlled, onChange]);
46
-
47
- const handleTabChange = (tabId: string) => {
48
- if (!isControlled) {
49
- setInternalActiveTab(tabId);
50
- }
51
- onChange?.(tabId);
52
- };
53
-
54
- // Size-specific classes
55
- const sizeClasses = {
56
- sm: {
57
- padding: 'px-3 py-1.5',
58
- text: 'text-xs',
59
- icon: 'h-3.5 w-3.5',
60
- gap: orientation === 'vertical' ? 'gap-1.5' : 'gap-4',
61
- minWidth: orientation === 'vertical' ? 'min-w-[150px]' : '',
62
- spacing: orientation === 'vertical' ? 'mt-4' : 'mt-4',
63
- },
64
- md: {
65
- padding: 'px-4 py-2.5',
66
- text: 'text-sm',
67
- icon: 'h-4 w-4',
68
- gap: orientation === 'vertical' ? 'gap-2' : 'gap-6',
69
- minWidth: orientation === 'vertical' ? 'min-w-[200px]' : '',
70
- spacing: orientation === 'vertical' ? 'mt-6' : 'mt-6',
71
- },
72
- lg: {
73
- padding: 'px-5 py-3',
74
- text: 'text-base',
75
- icon: 'h-5 w-5',
76
- gap: orientation === 'vertical' ? 'gap-3' : 'gap-8',
77
- minWidth: orientation === 'vertical' ? 'min-w-[250px]' : '',
78
- spacing: orientation === 'vertical' ? 'mt-8' : 'mt-8',
79
- },
80
- };
81
-
82
- return (
83
- <div className={`w-full ${orientation === 'vertical' ? `flex ${sizeClasses[size].gap}` : ''}`}>
84
- {/* Tab Headers */}
85
- <div
86
- className={`
87
- flex ${orientation === 'vertical' ? 'flex-col' : ''}
88
- ${variant === 'underline'
89
- ? orientation === 'vertical'
90
- ? `border-r border-paper-200 ${sizeClasses[size].gap} pr-6`
91
- : `border-b border-paper-200 ${sizeClasses[size].gap}`
92
- : `${sizeClasses[size].gap} p-1 bg-paper-50 rounded-lg`
93
- }
94
- ${sizeClasses[size].minWidth}
95
- `}
96
- role="tablist"
97
- >
98
- {tabs.map((tab) => {
99
- const isActive = activeTab === tab.id;
100
-
101
- return (
102
- <button
103
- key={tab.id}
104
- role="tab"
105
- aria-selected={isActive}
106
- aria-controls={`panel-${tab.id}`}
107
- disabled={tab.disabled}
108
- onClick={() => !tab.disabled && handleTabChange(tab.id)}
109
- className={`
110
- flex items-center gap-2 ${sizeClasses[size].padding} ${sizeClasses[size].text} font-medium transition-all duration-200
111
- ${orientation === 'vertical' ? 'w-full justify-start' : ''}
112
- ${
113
- variant === 'underline'
114
- ? isActive
115
- ? orientation === 'vertical'
116
- ? 'text-accent-900 border-r-2 border-accent-500 -mr-[1px]'
117
- : 'text-accent-900 border-b-2 border-accent-500 -mb-[1px]'
118
- : orientation === 'vertical'
119
- ? 'text-ink-600 hover:text-ink-900 border-r-2 border-transparent'
120
- : 'text-ink-600 hover:text-ink-900 border-b-2 border-transparent'
121
- : isActive
122
- ? 'bg-white text-accent-900 rounded-md shadow-xs'
123
- : 'text-ink-600 hover:text-ink-900 hover:bg-white/50 rounded-md'
124
- }
125
- ${tab.disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}
126
- `}
127
- >
128
- {tab.icon && <span className={`flex-shrink-0 ${sizeClasses[size].icon}`}>{tab.icon}</span>}
129
- <span>{tab.label}</span>
130
- </button>
131
- );
132
- })}
133
- </div>
134
-
135
- {/* Tab Content */}
136
- <div className={`${orientation === 'vertical' ? 'flex-1' : sizeClasses[size].spacing}`}>
137
- {tabs.map((tab) => (
138
- <div
139
- key={tab.id}
140
- id={`panel-${tab.id}`}
141
- role="tabpanel"
142
- aria-labelledby={tab.id}
143
- hidden={activeTab !== tab.id}
144
- className={activeTab === tab.id ? 'animate-fade-in' : ''}
145
- >
146
- {activeTab === tab.id && tab.content}
147
- </div>
148
- ))}
149
- </div>
150
- </div>
151
- );
152
- }
1
+ import React, { useState, useEffect } from 'react';
2
+
3
+ export interface Tab {
4
+ id: string;
5
+ label: string;
6
+ icon?: React.ReactNode;
7
+ content: React.ReactNode;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ export interface TabsProps {
12
+ tabs: Tab[];
13
+ /** Controlled mode: Currently active tab ID */
14
+ activeTab?: string;
15
+ /** Uncontrolled mode: Initial tab ID (ignored if activeTab is provided) */
16
+ defaultTab?: string;
17
+ variant?: 'underline' | 'pill';
18
+ /** Orientation of tabs (default: 'horizontal') */
19
+ orientation?: 'horizontal' | 'vertical';
20
+ /** Size of tabs (default: 'md') */
21
+ size?: 'sm' | 'md' | 'lg';
22
+ /** Called when tab changes (required for controlled mode) */
23
+ onChange?: (tabId: string) => void;
24
+ }
25
+
26
+ export default function Tabs({ tabs, activeTab: controlledActiveTab, defaultTab, variant = 'underline', orientation = 'horizontal', size = 'md', onChange }: TabsProps) {
27
+ const [internalActiveTab, setInternalActiveTab] = useState(defaultTab || tabs[0]?.id);
28
+
29
+ // Controlled mode: use activeTab prop, Uncontrolled mode: use internal state
30
+ const isControlled = controlledActiveTab !== undefined;
31
+ const activeTab = isControlled ? controlledActiveTab : internalActiveTab;
32
+
33
+ // Ensure the activeTab exists in the current tabs array
34
+ // This handles the case where tabs array reference changes at the same time as activeTab
35
+ useEffect(() => {
36
+ const tabExists = tabs.some(tab => tab.id === activeTab);
37
+ if (!tabExists && tabs.length > 0) {
38
+ // If the activeTab doesn't exist in the new tabs array, use the first tab
39
+ if (isControlled) {
40
+ onChange?.(tabs[0].id);
41
+ } else {
42
+ setInternalActiveTab(tabs[0].id);
43
+ }
44
+ }
45
+ }, [tabs, activeTab, isControlled, onChange]);
46
+
47
+ const handleTabChange = (tabId: string) => {
48
+ if (!isControlled) {
49
+ setInternalActiveTab(tabId);
50
+ }
51
+ onChange?.(tabId);
52
+ };
53
+
54
+ // Size-specific classes
55
+ const sizeClasses = {
56
+ sm: {
57
+ padding: 'px-3 py-1.5',
58
+ text: 'text-xs',
59
+ icon: 'h-3.5 w-3.5',
60
+ gap: orientation === 'vertical' ? 'gap-1.5' : 'gap-4',
61
+ minWidth: orientation === 'vertical' ? 'min-w-[150px]' : '',
62
+ spacing: orientation === 'vertical' ? 'mt-4' : 'mt-4',
63
+ },
64
+ md: {
65
+ padding: 'px-4 py-2.5',
66
+ text: 'text-sm',
67
+ icon: 'h-4 w-4',
68
+ gap: orientation === 'vertical' ? 'gap-2' : 'gap-6',
69
+ minWidth: orientation === 'vertical' ? 'min-w-[200px]' : '',
70
+ spacing: orientation === 'vertical' ? 'mt-6' : 'mt-6',
71
+ },
72
+ lg: {
73
+ padding: 'px-5 py-3',
74
+ text: 'text-base',
75
+ icon: 'h-5 w-5',
76
+ gap: orientation === 'vertical' ? 'gap-3' : 'gap-8',
77
+ minWidth: orientation === 'vertical' ? 'min-w-[250px]' : '',
78
+ spacing: orientation === 'vertical' ? 'mt-8' : 'mt-8',
79
+ },
80
+ };
81
+
82
+ return (
83
+ <div className={`w-full ${orientation === 'vertical' ? `flex ${sizeClasses[size].gap}` : ''}`}>
84
+ {/* Tab Headers */}
85
+ <div
86
+ className={`
87
+ flex ${orientation === 'vertical' ? 'flex-col' : ''}
88
+ ${variant === 'underline'
89
+ ? orientation === 'vertical'
90
+ ? `border-r border-paper-200 ${sizeClasses[size].gap} pr-6`
91
+ : `border-b border-paper-200 ${sizeClasses[size].gap}`
92
+ : `${sizeClasses[size].gap} p-1 bg-paper-50 rounded-lg`
93
+ }
94
+ ${sizeClasses[size].minWidth}
95
+ `}
96
+ role="tablist"
97
+ >
98
+ {tabs.map((tab) => {
99
+ const isActive = activeTab === tab.id;
100
+
101
+ return (
102
+ <button
103
+ key={tab.id}
104
+ role="tab"
105
+ aria-selected={isActive}
106
+ aria-controls={`panel-${tab.id}`}
107
+ disabled={tab.disabled}
108
+ onClick={() => !tab.disabled && handleTabChange(tab.id)}
109
+ className={`
110
+ flex items-center gap-2 ${sizeClasses[size].padding} ${sizeClasses[size].text} font-medium transition-all duration-200
111
+ ${orientation === 'vertical' ? 'w-full justify-start' : ''}
112
+ ${
113
+ variant === 'underline'
114
+ ? isActive
115
+ ? orientation === 'vertical'
116
+ ? 'text-accent-900 border-r-2 border-accent-500 -mr-[1px]'
117
+ : 'text-accent-900 border-b-2 border-accent-500 -mb-[1px]'
118
+ : orientation === 'vertical'
119
+ ? 'text-ink-600 hover:text-ink-900 border-r-2 border-transparent'
120
+ : 'text-ink-600 hover:text-ink-900 border-b-2 border-transparent'
121
+ : isActive
122
+ ? 'bg-white text-accent-900 rounded-md shadow-xs'
123
+ : 'text-ink-600 hover:text-ink-900 hover:bg-white/50 rounded-md'
124
+ }
125
+ ${tab.disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}
126
+ `}
127
+ >
128
+ {tab.icon && <span className={`flex-shrink-0 ${sizeClasses[size].icon}`}>{tab.icon}</span>}
129
+ <span>{tab.label}</span>
130
+ </button>
131
+ );
132
+ })}
133
+ </div>
134
+
135
+ {/* Tab Content */}
136
+ <div className={`${orientation === 'vertical' ? 'flex-1' : sizeClasses[size].spacing}`}>
137
+ {tabs.map((tab) => (
138
+ <div
139
+ key={tab.id}
140
+ id={`panel-${tab.id}`}
141
+ role="tabpanel"
142
+ aria-labelledby={tab.id}
143
+ hidden={activeTab !== tab.id}
144
+ className={activeTab === tab.id ? 'animate-fade-in' : ''}
145
+ >
146
+ {activeTab === tab.id && tab.content}
147
+ </div>
148
+ ))}
149
+ </div>
150
+ </div>
151
+ );
152
+ }