@yuno-payments/dashboard-design-system 0.0.1

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 (71) hide show
  1. package/.storybook/main.ts +20 -0
  2. package/.storybook/preview.ts +18 -0
  3. package/.storybook/vitest.setup.ts +7 -0
  4. package/README.md +69 -0
  5. package/components.json +21 -0
  6. package/eslint.config.js +26 -0
  7. package/index.html +13 -0
  8. package/package.json +57 -0
  9. package/public/vite.svg +1 -0
  10. package/src/App.css +42 -0
  11. package/src/App.tsx +11 -0
  12. package/src/assets/react.svg +1 -0
  13. package/src/components/atoms/button/button.stories.tsx +222 -0
  14. package/src/components/atoms/button/button.test.tsx +78 -0
  15. package/src/components/atoms/button/index.tsx +80 -0
  16. package/src/components/atoms/checkbox/checkbox.stories.tsx +314 -0
  17. package/src/components/atoms/checkbox/checkbox.test.tsx +278 -0
  18. package/src/components/atoms/checkbox/index.tsx +103 -0
  19. package/src/components/atoms/chip/chip.stories.tsx +317 -0
  20. package/src/components/atoms/chip/chip.test.tsx +300 -0
  21. package/src/components/atoms/chip/index.tsx +114 -0
  22. package/src/components/atoms/input/index.tsx +27 -0
  23. package/src/components/atoms/link/index.tsx +79 -0
  24. package/src/components/atoms/link/link.stories.tsx +159 -0
  25. package/src/components/atoms/link/link.test.tsx +176 -0
  26. package/src/components/atoms/radiobutton/index.tsx +103 -0
  27. package/src/components/atoms/radiobutton/radiobutton.stories.tsx +314 -0
  28. package/src/components/atoms/radiobutton/radiobutton.test.tsx +245 -0
  29. package/src/components/atoms/tag/index.tsx +196 -0
  30. package/src/components/atoms/tag/tag.stories.tsx +281 -0
  31. package/src/components/atoms/tag/tag.test.tsx +282 -0
  32. package/src/components/atoms/typography/index.tsx +62 -0
  33. package/src/components/atoms/typography/typography.stories.tsx +214 -0
  34. package/src/components/atoms/typography/typography.test.tsx +187 -0
  35. package/src/components/index.tsx +17 -0
  36. package/src/components/molecules/announcement/announcement.stories.tsx +277 -0
  37. package/src/components/molecules/announcement/announcement.test.tsx +354 -0
  38. package/src/components/molecules/announcement/index.tsx +200 -0
  39. package/src/components/molecules/notification-alert/index.tsx +293 -0
  40. package/src/components/molecules/notification-alert/notification-alert.stories.tsx +418 -0
  41. package/src/components/molecules/notification-alert/notification-alert.test.tsx +454 -0
  42. package/src/components/molecules/popover/index.tsx +175 -0
  43. package/src/components/molecules/popover/popover.stories.tsx +241 -0
  44. package/src/components/molecules/popover/popover.test.tsx +191 -0
  45. package/src/components/molecules/textfield/index.tsx +154 -0
  46. package/src/components/molecules/textfield/textfield.stories.tsx +168 -0
  47. package/src/components/molecules/textfield/textfield.test.tsx +157 -0
  48. package/src/components/molecules/tooltip/index.tsx +263 -0
  49. package/src/components/molecules/tooltip/tooltip.stories.tsx +363 -0
  50. package/src/components/molecules/tooltip/tooltip.test.tsx +468 -0
  51. package/src/components/organisms/dialog/dialog.stories.tsx +522 -0
  52. package/src/components/organisms/dialog/dialog.test.tsx +525 -0
  53. package/src/components/organisms/dialog/index.tsx +233 -0
  54. package/src/components/organisms/dropdown/dropdown.stories.tsx +529 -0
  55. package/src/components/organisms/dropdown/dropdown.test.tsx +390 -0
  56. package/src/components/organisms/dropdown/index.tsx +624 -0
  57. package/src/index.css +184 -0
  58. package/src/lib/color-utils.ts +94 -0
  59. package/src/lib/utils.ts +6 -0
  60. package/src/main.tsx +10 -0
  61. package/src/stories/Colors.stories.tsx +107 -0
  62. package/src/stories/Shadows.stories.tsx +110 -0
  63. package/src/stories/Spacing.stories.tsx +121 -0
  64. package/src/stories/Typography.stories.tsx +197 -0
  65. package/src/vite-env.d.ts +1 -0
  66. package/tsconfig.app.json +33 -0
  67. package/tsconfig.json +13 -0
  68. package/tsconfig.node.json +25 -0
  69. package/vite.config.ts +43 -0
  70. package/vitest.config.ts +15 -0
  71. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,281 @@
1
+ /* eslint-disable storybook/no-renderer-packages */
2
+ import type { Meta, StoryObj } from '@storybook/react'
3
+ import { Tag } from './index'
4
+ import { Flag, Clock, Ban, CheckCircle, Minus, Circle, ArrowLeft } from 'lucide-react'
5
+
6
+ const meta: Meta<typeof Tag> = {
7
+ title: 'Atoms/Tag',
8
+ component: Tag,
9
+ parameters: {
10
+ layout: 'centered',
11
+ },
12
+ tags: ['autodocs'],
13
+ argTypes: {
14
+ variant: {
15
+ control: 'select',
16
+ options: ['primary', 'secondary', 'outlined'],
17
+ description: 'The visual style variant of the tag',
18
+ },
19
+ color: {
20
+ control: 'select',
21
+ options: ['neutral', 'positive', 'negative', 'attentive', 'informative', 'warning'],
22
+ description: 'The semantic color of the tag',
23
+ },
24
+ label: {
25
+ control: 'text',
26
+ description: 'The text content of the tag',
27
+ },
28
+ startIcon: {
29
+ control: false,
30
+ description: 'Icon to display at the start of the tag',
31
+ },
32
+ endIcon: {
33
+ control: false,
34
+ description: 'Icon to display at the end of the tag',
35
+ },
36
+ onDelete: {
37
+ control: false,
38
+ description: 'Callback function when delete button is clicked. When provided, shows delete button.',
39
+ },
40
+ disabled: {
41
+ control: 'boolean',
42
+ description: 'Whether the tag is disabled',
43
+ },
44
+ },
45
+ }
46
+
47
+ export default meta
48
+ type Story = StoryObj<typeof meta>
49
+
50
+ export const Basic: Story = {
51
+ args: {
52
+ label: 'Basic Tag',
53
+ variant: 'primary',
54
+ color: 'neutral',
55
+ },
56
+ }
57
+
58
+ export const WithStartIcon: Story = {
59
+ args: {
60
+ label: 'With Icon',
61
+ variant: 'primary',
62
+ color: 'informative',
63
+ startIcon: <Flag />,
64
+ },
65
+ }
66
+
67
+ export const WithEndIcon: Story = {
68
+ args: {
69
+ label: 'With End Icon',
70
+ variant: 'secondary',
71
+ color: 'positive',
72
+ endIcon: <CheckCircle />,
73
+ },
74
+ }
75
+
76
+ export const WithDelete: Story = {
77
+ args: {
78
+ label: 'Deletable Tag',
79
+ variant: 'outlined',
80
+ color: 'negative',
81
+ onDelete: () => console.log('Tag deleted'),
82
+ },
83
+ }
84
+
85
+ export const Disabled: Story = {
86
+ args: {
87
+ label: 'Disabled Tag',
88
+ variant: 'primary',
89
+ color: 'neutral',
90
+ disabled: true,
91
+ onDelete: () => console.log('Tag deleted'),
92
+ },
93
+ }
94
+
95
+ export const StatusExamples: Story = {
96
+ render: () => (
97
+ <div className="flex flex-wrap gap-2">
98
+ <Tag label="Created" variant="secondary" color="positive" startIcon={<CheckCircle />} />
99
+ <Tag label="Pending" variant="secondary" color="attentive" startIcon={<Clock />} />
100
+ <Tag label="Expired" variant="secondary" color="negative" startIcon={<Ban />} />
101
+ <Tag label="Processing" variant="primary" color="informative" startIcon={<Circle />} />
102
+ <Tag label="Cancelled" variant="outlined" color="negative" startIcon={<Minus />} />
103
+ <Tag label="Returned" variant="outlined" color="warning" startIcon={<ArrowLeft />} />
104
+ </div>
105
+ ),
106
+ parameters: {
107
+ docs: {
108
+ description: {
109
+ story: 'Examples of tags used for different status indicators with appropriate colors and icons.',
110
+ },
111
+ },
112
+ },
113
+ }
114
+
115
+ export const AllVariants: Story = {
116
+ render: () => (
117
+ <div className="space-y-4">
118
+ <div>
119
+ <h3 className="text-sm font-medium mb-2">Primary Variants</h3>
120
+ <div className="flex flex-wrap gap-2">
121
+ <Tag label="Neutral" variant="primary" color="neutral" />
122
+ <Tag label="Positive" variant="primary" color="positive" />
123
+ <Tag label="Negative" variant="primary" color="negative" />
124
+ <Tag label="Attentive" variant="primary" color="attentive" />
125
+ <Tag label="Informative" variant="primary" color="informative" />
126
+ <Tag label="Warning" variant="primary" color="warning" />
127
+ </div>
128
+ </div>
129
+
130
+ <div>
131
+ <h3 className="text-sm font-medium mb-2">Secondary Variants</h3>
132
+ <div className="flex flex-wrap gap-2">
133
+ <Tag label="Neutral" variant="secondary" color="neutral" />
134
+ <Tag label="Positive" variant="secondary" color="positive" />
135
+ <Tag label="Negative" variant="secondary" color="negative" />
136
+ <Tag label="Attentive" variant="secondary" color="attentive" />
137
+ <Tag label="Informative" variant="secondary" color="informative" />
138
+ <Tag label="Warning" variant="secondary" color="warning" />
139
+ </div>
140
+ </div>
141
+
142
+ <div>
143
+ <h3 className="text-sm font-medium mb-2">Outlined Variants</h3>
144
+ <div className="flex flex-wrap gap-2">
145
+ <Tag label="Neutral" variant="outlined" color="neutral" />
146
+ <Tag label="Positive" variant="outlined" color="positive" />
147
+ <Tag label="Negative" variant="outlined" color="negative" />
148
+ <Tag label="Attentive" variant="outlined" color="attentive" />
149
+ <Tag label="Informative" variant="outlined" color="informative" />
150
+ <Tag label="Warning" variant="outlined" color="warning" />
151
+ </div>
152
+ </div>
153
+
154
+ <div>
155
+ <h3 className="text-sm font-medium mb-2">With Icons and Delete</h3>
156
+ <div className="flex flex-wrap gap-2">
157
+ <Tag
158
+ label="With Start Icon"
159
+ variant="primary"
160
+ color="informative"
161
+ startIcon={<Flag />}
162
+ />
163
+ <Tag
164
+ label="With End Icon"
165
+ variant="secondary"
166
+ color="positive"
167
+ endIcon={<CheckCircle />}
168
+ />
169
+ <Tag
170
+ label="Deletable"
171
+ variant="outlined"
172
+ color="negative"
173
+ onDelete={() => console.log('Deleted')}
174
+ />
175
+ <Tag
176
+ label="Full Featured"
177
+ variant="primary"
178
+ color="attentive"
179
+ startIcon={<Clock />}
180
+ onDelete={() => console.log('Deleted')}
181
+ />
182
+ </div>
183
+ </div>
184
+ </div>
185
+ ),
186
+ parameters: {
187
+ docs: {
188
+ description: {
189
+ story: 'Comprehensive overview of all tag variants, colors, and features including icons and delete functionality.',
190
+ },
191
+ },
192
+ },
193
+ }
194
+
195
+ export const Docs: Story = {
196
+ render: () => (
197
+ <div className="space-y-6">
198
+ <div>
199
+ <h2 className="text-lg font-semibold mb-3">Tag Component</h2>
200
+ <p className="text-sm text-muted-foreground mb-4">
201
+ Tags are compact elements that represent an input, attribute, or action. They can be used to display status,
202
+ categories, or any other metadata. The Tag component supports multiple variants, semantic colors, icons,
203
+ and delete functionality.
204
+ </p>
205
+ </div>
206
+
207
+ <div>
208
+ <h3 className="text-base font-medium mb-2">Variants</h3>
209
+ <div className="space-y-2">
210
+ <div className="flex items-center gap-2">
211
+ <Tag label="Primary" variant="primary" color="neutral" />
212
+ <span className="text-sm text-muted-foreground">Solid background, high emphasis</span>
213
+ </div>
214
+ <div className="flex items-center gap-2">
215
+ <Tag label="Secondary" variant="secondary" color="neutral" />
216
+ <span className="text-sm text-muted-foreground">Light background, medium emphasis</span>
217
+ </div>
218
+ <div className="flex items-center gap-2">
219
+ <Tag label="Outlined" variant="outlined" color="neutral" />
220
+ <span className="text-sm text-muted-foreground">Border only, low emphasis</span>
221
+ </div>
222
+ </div>
223
+ </div>
224
+
225
+ <div>
226
+ <h3 className="text-base font-medium mb-2">Semantic Colors</h3>
227
+ <div className="grid grid-cols-2 gap-2">
228
+ <div className="flex items-center gap-2">
229
+ <Tag label="Neutral" variant="secondary" color="neutral" />
230
+ <span className="text-xs text-muted-foreground">Default state</span>
231
+ </div>
232
+ <div className="flex items-center gap-2">
233
+ <Tag label="Positive" variant="secondary" color="positive" />
234
+ <span className="text-xs text-muted-foreground">Success, approved</span>
235
+ </div>
236
+ <div className="flex items-center gap-2">
237
+ <Tag label="Negative" variant="secondary" color="negative" />
238
+ <span className="text-xs text-muted-foreground">Error, rejected</span>
239
+ </div>
240
+ <div className="flex items-center gap-2">
241
+ <Tag label="Attentive" variant="secondary" color="attentive" />
242
+ <span className="text-xs text-muted-foreground">Warning, attention needed</span>
243
+ </div>
244
+ <div className="flex items-center gap-2">
245
+ <Tag label="Informative" variant="secondary" color="informative" />
246
+ <span className="text-xs text-muted-foreground">Information, neutral</span>
247
+ </div>
248
+ <div className="flex items-center gap-2">
249
+ <Tag label="Warning" variant="secondary" color="warning" />
250
+ <span className="text-xs text-muted-foreground">Caution, important</span>
251
+ </div>
252
+ </div>
253
+ </div>
254
+
255
+ <div>
256
+ <h3 className="text-base font-medium mb-2">Features</h3>
257
+ <div className="space-y-2">
258
+ <div className="flex items-center gap-2">
259
+ <Tag label="With Icon" variant="primary" color="informative" startIcon={<Flag />} />
260
+ <span className="text-sm text-muted-foreground">Start and end icons supported</span>
261
+ </div>
262
+ <div className="flex items-center gap-2">
263
+ <Tag label="Deletable" variant="outlined" color="negative" onDelete={() => {}} />
264
+ <span className="text-sm text-muted-foreground">Delete functionality with X button</span>
265
+ </div>
266
+ <div className="flex items-center gap-2">
267
+ <Tag label="Disabled" variant="primary" color="neutral" disabled />
268
+ <span className="text-sm text-muted-foreground">Disabled state with reduced opacity</span>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ </div>
273
+ ),
274
+ parameters: {
275
+ docs: {
276
+ description: {
277
+ story: 'Complete documentation and usage examples for the Tag component.',
278
+ },
279
+ },
280
+ },
281
+ }
@@ -0,0 +1,282 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import * as React from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { act } from 'react-dom/test-utils'
5
+ import { Tag } from './index'
6
+
7
+ function render(ui: React.ReactElement) {
8
+ const container = document.createElement('div')
9
+ document.body.appendChild(container)
10
+ const root = createRoot(container)
11
+ act(() => {
12
+ root.render(ui)
13
+ })
14
+ return { container, root }
15
+ }
16
+
17
+ function getByTestId(container: HTMLElement, testId: string) {
18
+ const el = container.querySelector(`[data-testid="${testId}"]`)
19
+ if (!el) throw new Error(`Element with data-testid="${testId}" not found`)
20
+ return el as HTMLElement
21
+ }
22
+
23
+ function getByText(container: HTMLElement, text: string) {
24
+ const el = Array.from(container.querySelectorAll('*')).find((n) => n.textContent?.includes(text))
25
+ if (!el) throw new Error(`Element with text "${text}" not found`)
26
+ return el as HTMLElement
27
+ }
28
+
29
+ describe('Tag', () => {
30
+ it('renders with label', () => {
31
+ const { container } = render(<Tag label="Test Tag" />)
32
+ expect(getByText(container, 'Test Tag')).toBeTruthy()
33
+ })
34
+
35
+ it('renders with default variant and color', () => {
36
+ const { container } = render(<Tag label="Test" />)
37
+ const tag = getByTestId(container, 'tag')
38
+ expect(tag.className).toMatch(/bg-primary/)
39
+ })
40
+
41
+ it('renders with primary variant', () => {
42
+ const { container } = render(<Tag label="Test" variant="primary" color="neutral" />)
43
+ const tag = getByTestId(container, 'tag')
44
+ expect(tag.className).toMatch(/bg-primary/)
45
+ })
46
+
47
+ it('renders with secondary variant', () => {
48
+ const { container } = render(<Tag label="Test" variant="secondary" color="neutral" />)
49
+ const tag = getByTestId(container, 'tag')
50
+ expect(tag.className).toMatch(/bg-secondary/)
51
+ })
52
+
53
+ it('renders with outlined variant', () => {
54
+ const { container } = render(<Tag label="Test" variant="outlined" color="neutral" />)
55
+ const tag = getByTestId(container, 'tag')
56
+ expect(tag.className).toMatch(/border-border/)
57
+ })
58
+
59
+ it('renders with positive color', () => {
60
+ const { container } = render(<Tag label="Test" variant="secondary" color="positive" />)
61
+ const tag = getByTestId(container, 'tag')
62
+ expect(tag.className).toMatch(/bg-green-50/)
63
+ })
64
+
65
+ it('renders with negative color', () => {
66
+ const { container } = render(<Tag label="Test" variant="secondary" color="negative" />)
67
+ const tag = getByTestId(container, 'tag')
68
+ expect(tag.className).toMatch(/bg-destructive/)
69
+ })
70
+
71
+ it('renders with attentive color', () => {
72
+ const { container } = render(<Tag label="Test" variant="secondary" color="attentive" />)
73
+ const tag = getByTestId(container, 'tag')
74
+ expect(tag.className).toMatch(/bg-amber-50/)
75
+ })
76
+
77
+ it('renders with informative color', () => {
78
+ const { container } = render(<Tag label="Test" variant="secondary" color="informative" />)
79
+ const tag = getByTestId(container, 'tag')
80
+ expect(tag.className).toMatch(/bg-blue-50/)
81
+ })
82
+
83
+ it('renders with warning color', () => {
84
+ const { container } = render(<Tag label="Test" variant="secondary" color="warning" />)
85
+ const tag = getByTestId(container, 'tag')
86
+ expect(tag.className).toMatch(/bg-orange-50/)
87
+ })
88
+
89
+ it('renders start icon when provided', () => {
90
+ const Icon = () => <span data-testid="start-icon">icon</span>
91
+ const { container } = render(<Tag label="Test" startIcon={<Icon />} />)
92
+ expect(getByTestId(container, 'start-icon')).toBeTruthy()
93
+ })
94
+
95
+ it('renders end icon when provided', () => {
96
+ const Icon = () => <span data-testid="end-icon">icon</span>
97
+ const { container } = render(<Tag label="Test" endIcon={<Icon />} />)
98
+ expect(getByTestId(container, 'end-icon')).toBeTruthy()
99
+ })
100
+
101
+ it('renders delete button when onDelete is provided', () => {
102
+ const handleDelete = vi.fn()
103
+ const { container } = render(<Tag label="Test" onDelete={handleDelete} />)
104
+ expect(getByTestId(container, 'tag-delete')).toBeTruthy()
105
+ })
106
+
107
+ it('does not render delete button when onDelete is not provided', () => {
108
+ const { container } = render(<Tag label="Test" />)
109
+ expect(() => getByTestId(container, 'tag-delete')).toThrow()
110
+ })
111
+
112
+ it('calls onDelete when delete button is clicked', () => {
113
+ const handleDelete = vi.fn()
114
+ const { container } = render(<Tag label="Test" onDelete={handleDelete} />)
115
+ const deleteButton = getByTestId(container, 'tag-delete')
116
+
117
+ act(() => {
118
+ deleteButton.click()
119
+ })
120
+
121
+ expect(handleDelete).toHaveBeenCalledTimes(1)
122
+ })
123
+
124
+ it('prevents event propagation when delete button is clicked', () => {
125
+ const handleDelete = vi.fn()
126
+ const handleTagClick = vi.fn()
127
+ const { container } = render(
128
+ <Tag label="Test" onDelete={handleDelete} onClick={handleTagClick} />
129
+ )
130
+ const deleteButton = getByTestId(container, 'tag-delete')
131
+
132
+ act(() => {
133
+ deleteButton.click()
134
+ })
135
+
136
+ expect(handleDelete).toHaveBeenCalledTimes(1)
137
+ expect(handleTagClick).not.toHaveBeenCalled()
138
+ })
139
+
140
+ it('applies disabled state correctly', () => {
141
+ const { container } = render(<Tag label="Test" disabled />)
142
+ const tag = getByTestId(container, 'tag')
143
+ expect(tag.className).toMatch(/pointer-events-none/)
144
+ expect(tag.className).toMatch(/opacity-50/)
145
+ })
146
+
147
+ it('disables delete button when disabled', () => {
148
+ const handleDelete = vi.fn()
149
+ const { container } = render(<Tag label="Test" onDelete={handleDelete} disabled />)
150
+ const deleteButton = getByTestId(container, 'tag-delete') as HTMLButtonElement
151
+ expect(deleteButton.disabled).toBe(true)
152
+ })
153
+
154
+ it('forwards ref correctly', () => {
155
+ const ref = { current: null }
156
+ render(<Tag ref={ref} label="Test" />)
157
+ expect(ref.current).toBeTruthy()
158
+ })
159
+
160
+ it('applies custom className', () => {
161
+ const { container } = render(<Tag label="Test" className="custom-class" />)
162
+ const tag = getByTestId(container, 'tag')
163
+ expect(tag.className).toMatch(/custom-class/)
164
+ })
165
+
166
+ it('passes through HTML div attributes', () => {
167
+ const { container } = render(<Tag label="Test" data-custom="value" />)
168
+ const tag = getByTestId(container, 'tag')
169
+ expect(tag.getAttribute('data-custom')).toBe('value')
170
+ })
171
+
172
+ it('handles all variant and color combinations', () => {
173
+ const variants = ['primary', 'secondary', 'outlined'] as const
174
+ const colors = ['neutral', 'positive', 'negative', 'attentive', 'informative', 'warning'] as const
175
+
176
+ variants.forEach(variant => {
177
+ colors.forEach(color => {
178
+ const { container } = render(<Tag label="Test" variant={variant} color={color} />)
179
+ const tag = getByTestId(container, 'tag')
180
+ expect(tag).toBeTruthy()
181
+ })
182
+ })
183
+ })
184
+
185
+ it('renders with both start icon and end icon', () => {
186
+ const StartIcon = () => <span data-testid="start-icon">start</span>
187
+ const EndIcon = () => <span data-testid="end-icon">end</span>
188
+ const { container } = render(
189
+ <Tag label="Test" startIcon={<StartIcon />} endIcon={<EndIcon />} />
190
+ )
191
+
192
+ expect(getByTestId(container, 'start-icon')).toBeTruthy()
193
+ expect(getByTestId(container, 'end-icon')).toBeTruthy()
194
+ })
195
+
196
+ it('renders with start icon and delete button', () => {
197
+ const StartIcon = () => <span data-testid="start-icon">start</span>
198
+ const handleDelete = vi.fn()
199
+ const { container } = render(
200
+ <Tag label="Test" startIcon={<StartIcon />} onDelete={handleDelete} />
201
+ )
202
+
203
+ expect(getByTestId(container, 'start-icon')).toBeTruthy()
204
+ expect(getByTestId(container, 'tag-delete')).toBeTruthy()
205
+ })
206
+
207
+ it('renders with end icon and delete button', () => {
208
+ const EndIcon = () => <span data-testid="end-icon">end</span>
209
+ const handleDelete = vi.fn()
210
+ const { container } = render(
211
+ <Tag label="Test" endIcon={<EndIcon />} onDelete={handleDelete} />
212
+ )
213
+
214
+ expect(getByTestId(container, 'end-icon')).toBeTruthy()
215
+ expect(getByTestId(container, 'tag-delete')).toBeTruthy()
216
+ })
217
+
218
+ it('renders complex label content', () => {
219
+ const complexLabel = (
220
+ <span>
221
+ Complex <strong>Label</strong>
222
+ </span>
223
+ )
224
+ const { container } = render(<Tag label={complexLabel} />)
225
+ expect(getByText(container, 'Complex')).toBeTruthy()
226
+ expect(container.querySelector('strong')).toBeTruthy()
227
+ })
228
+
229
+ it('handles click events on the tag', () => {
230
+ const handleClick = vi.fn()
231
+ const { container } = render(<Tag label="Test" onClick={handleClick} />)
232
+ const tag = getByTestId(container, 'tag')
233
+
234
+ act(() => {
235
+ tag.click()
236
+ })
237
+
238
+ expect(handleClick).toHaveBeenCalledTimes(1)
239
+ })
240
+
241
+ it('does not call click handler when disabled', () => {
242
+ const handleClick = vi.fn()
243
+ const { container } = render(<Tag label="Test" onClick={handleClick} disabled />)
244
+ const tag = getByTestId(container, 'tag')
245
+
246
+ act(() => {
247
+ tag.click()
248
+ })
249
+
250
+ expect(handleClick).not.toHaveBeenCalled()
251
+ })
252
+
253
+ it('renders with all features combined', () => {
254
+ const StartIcon = () => <span data-testid="start-icon">start</span>
255
+ const EndIcon = () => <span data-testid="end-icon">end</span>
256
+ const handleDelete = vi.fn()
257
+ const handleClick = vi.fn()
258
+
259
+ const { container } = render(
260
+ <Tag
261
+ label="Full Featured Tag"
262
+ variant="primary"
263
+ color="informative"
264
+ startIcon={<StartIcon />}
265
+ endIcon={<EndIcon />}
266
+ onDelete={handleDelete}
267
+ onClick={handleClick}
268
+ className="custom-class"
269
+ data-custom="value"
270
+ />
271
+ )
272
+
273
+ expect(getByText(container, 'Full Featured Tag')).toBeTruthy()
274
+ expect(getByTestId(container, 'start-icon')).toBeTruthy()
275
+ expect(getByTestId(container, 'end-icon')).toBeTruthy()
276
+ expect(getByTestId(container, 'tag-delete')).toBeTruthy()
277
+
278
+ const tag = getByTestId(container, 'tag')
279
+ expect(tag.className).toMatch(/custom-class/)
280
+ expect(tag.getAttribute('data-custom')).toBe('value')
281
+ })
282
+ })
@@ -0,0 +1,62 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const typographyVariants = cva(
6
+ "font-[var(--font-family-inter)]",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ h1: "text-[var(--font-size-h1)] leading-[var(--line-height-h1)] tracking-[var(--letter-spacing-h1)]",
11
+ h2: "text-[var(--font-size-h2)] leading-[var(--line-height-h2)] tracking-[var(--letter-spacing-h2)]",
12
+ h3: "text-[var(--font-size-h3)] leading-[var(--line-height-h3)] tracking-[var(--letter-spacing-h3)]",
13
+ h4: "text-[var(--font-size-h4)] leading-[var(--line-height-h4)] tracking-[var(--letter-spacing-h4)]",
14
+ h5: "text-[var(--font-size-h5)] leading-[var(--line-height-h5)] tracking-[var(--letter-spacing-h5)]",
15
+ subtitle: "text-[var(--font-size-subtitle)] leading-[var(--line-height-subtitle)]",
16
+ body: "text-[var(--font-size-body)] leading-[var(--line-height-body)]",
17
+ small: "text-[var(--font-size-small)] leading-[var(--line-height-small)]",
18
+ tiny: "text-[var(--font-size-tiny)] leading-[var(--line-height-tiny)]",
19
+ "9xl": "text-[var(--font-size-9xl)] leading-[var(--line-height-9xl)] tracking-[var(--letter-spacing-9xl)]",
20
+ "8xl": "text-[var(--font-size-8xl)] leading-[var(--line-height-8xl)] tracking-[var(--letter-spacing-8xl)]",
21
+ "7xl": "text-[var(--font-size-7xl)] leading-[var(--line-height-7xl)] tracking-[var(--letter-spacing-7xl)]",
22
+ "6xl": "text-[var(--font-size-6xl)] leading-[var(--line-height-6xl)] tracking-[var(--letter-spacing-6xl)]",
23
+ "5xl": "text-[var(--font-size-5xl)] leading-[var(--line-height-5xl)] tracking-[var(--letter-spacing-5xl)]",
24
+ "4xl": "text-[var(--font-size-4xl)] leading-[var(--line-height-4xl)] tracking-[var(--letter-spacing-4xl)]",
25
+ "3xl": "text-[var(--font-size-3xl)] leading-[var(--line-height-3xl)]",
26
+ "2xl": "text-[var(--font-size-2xl)] leading-[var(--line-height-2xl)]",
27
+ xl: "text-[var(--font-size-xl)] leading-[var(--line-height-xl)]",
28
+ l: "text-[var(--font-size-l)] leading-[var(--line-height-l)]",
29
+ m: "text-[var(--font-size-m)] leading-[var(--line-height-m)]",
30
+ base: "text-[var(--font-size-base)] leading-[var(--line-height-base)]",
31
+ },
32
+ weight: {
33
+ regular: "font-normal",
34
+ bold: "font-bold",
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: "body",
39
+ weight: "regular",
40
+ },
41
+ }
42
+ )
43
+
44
+ export interface TypographyProps
45
+ extends React.HTMLAttributes<HTMLElement>,
46
+ VariantProps<typeof typographyVariants> {
47
+ as?: keyof React.JSX.IntrinsicElements
48
+ }
49
+
50
+ const Typography = React.forwardRef<HTMLElement, TypographyProps>(
51
+ ({ className, variant, weight, as: Component = "p", ...props }, ref) => {
52
+ return React.createElement(Component, {
53
+ className: cn(typographyVariants({ variant, weight, className })),
54
+ ref,
55
+ ...props,
56
+ })
57
+ }
58
+ )
59
+
60
+ Typography.displayName = "Typography"
61
+
62
+ export { Typography }