@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,354 @@
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 { Announcement } 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 getByText(container: HTMLElement, text: string) {
18
+ const el = Array.from(container.querySelectorAll('*')).find((n) => n.textContent?.includes(text))
19
+ if (!el) throw new Error(`Element with text "${text}" not found`)
20
+ return el as HTMLElement
21
+ }
22
+
23
+ function getByTestId(container: HTMLElement, testId: string) {
24
+ const el = container.querySelector(`[data-testid="${testId}"]`)
25
+ if (!el) throw new Error(`Element with data-testid="${testId}" not found`)
26
+ return el as HTMLElement
27
+ }
28
+
29
+ describe('Announcement', () => {
30
+ it('renders with default props', () => {
31
+ const { container } = render(
32
+ <Announcement title="Test title" description="Test description" />
33
+ )
34
+
35
+ expect(getByText(container, 'Test title')).toBeTruthy()
36
+ expect(getByText(container, 'Test description')).toBeTruthy()
37
+ })
38
+
39
+ it('renders title when provided', () => {
40
+ const { container } = render(
41
+ <Announcement title="Test Title" />
42
+ )
43
+
44
+ const title = getByTestId(container, 'title')
45
+ expect(title.textContent).toBe('Test Title')
46
+ })
47
+
48
+ it('renders description when provided', () => {
49
+ const { container } = render(
50
+ <Announcement description="Test Description" />
51
+ )
52
+
53
+ const description = getByTestId(container, 'description')
54
+ expect(description.textContent).toBe('Test Description')
55
+ })
56
+
57
+ it('does not render when visible is false', () => {
58
+ const { container } = render(
59
+ <Announcement title="Test" visible={false} />
60
+ )
61
+
62
+ expect(container.firstChild).toBeNull()
63
+ })
64
+
65
+ it('renders with neutral variant by default', () => {
66
+ const { container } = render(
67
+ <Announcement title="Test" />
68
+ )
69
+
70
+ const announcement = container.firstChild as HTMLElement
71
+ expect(announcement.className).toMatch(/bg-background/)
72
+ })
73
+
74
+ it('renders with attentive variant', () => {
75
+ const { container } = render(
76
+ <Announcement title="Test" variant="attentive" />
77
+ )
78
+
79
+ const announcement = container.firstChild as HTMLElement
80
+ expect(announcement.className).toMatch(/bg-amber-50/)
81
+ })
82
+
83
+ it('renders with informative variant', () => {
84
+ const { container } = render(
85
+ <Announcement title="Test" variant="informative" />
86
+ )
87
+
88
+ const announcement = container.firstChild as HTMLElement
89
+ expect(announcement.className).toMatch(/bg-blue-50/)
90
+ })
91
+
92
+ it('renders with negative variant', () => {
93
+ const { container } = render(
94
+ <Announcement title="Test" variant="negative" />
95
+ )
96
+
97
+ const announcement = container.firstChild as HTMLElement
98
+ expect(announcement.className).toMatch(/bg-destructive/)
99
+ })
100
+
101
+ it('renders with positive variant', () => {
102
+ const { container } = render(
103
+ <Announcement title="Test" variant="positive" />
104
+ )
105
+
106
+ const announcement = container.firstChild as HTMLElement
107
+ expect(announcement.className).toMatch(/bg-green-50/)
108
+ })
109
+
110
+ it('renders with large size by default', () => {
111
+ const { container } = render(
112
+ <Announcement title="Test" />
113
+ )
114
+
115
+ const announcement = container.firstChild as HTMLElement
116
+ expect(announcement.className).toMatch(/w-full max-w-full/)
117
+ })
118
+
119
+ it('renders with small size', () => {
120
+ const { container } = render(
121
+ <Announcement title="Test" size="small" />
122
+ )
123
+
124
+ const announcement = container.firstChild as HTMLElement
125
+ expect(announcement.className).toMatch(/max-w-\[343px\]/)
126
+ })
127
+
128
+ it('renders icon by default', () => {
129
+ const { container } = render(
130
+ <Announcement title="Test" />
131
+ )
132
+
133
+ const icon = container.querySelector('svg')
134
+ expect(icon).toBeTruthy()
135
+ })
136
+
137
+ it('does not render icon when withIcon is false', () => {
138
+ const { container } = render(
139
+ <Announcement title="Test" withIcon={false} />
140
+ )
141
+
142
+ const icon = container.querySelector('svg')
143
+ expect(icon).toBeFalsy()
144
+ })
145
+
146
+ it('renders custom icon when provided', () => {
147
+ const customIcon = <span data-testid="custom-icon">Custom</span>
148
+ const { container } = render(
149
+ <Announcement title="Test" iconName={customIcon} />
150
+ )
151
+
152
+ expect(getByTestId(container, 'custom-icon')).toBeTruthy()
153
+ })
154
+
155
+ it('renders link when linkText and link are provided', () => {
156
+ const { container } = render(
157
+ <Announcement
158
+ title="Test"
159
+ linkText="Click here"
160
+ link="https://example.com"
161
+ />
162
+ )
163
+
164
+ const link = getByTestId(container, 'linkButton')
165
+ expect(link.textContent).toBe('Click here')
166
+ })
167
+
168
+ it('renders link when linkText and onClickLink are provided', () => {
169
+ const handleClick = vi.fn()
170
+ const { container } = render(
171
+ <Announcement
172
+ title="Test"
173
+ linkText="Click here"
174
+ onClickLink={handleClick}
175
+ />
176
+ )
177
+
178
+ const link = getByTestId(container, 'linkButton')
179
+ expect(link.textContent).toBe('Click here')
180
+ })
181
+
182
+ it('does not render link when only linkText is provided', () => {
183
+ const { container } = render(
184
+ <Announcement title="Test" linkText="Click here" />
185
+ )
186
+
187
+ expect(() => getByTestId(container, 'linkButton')).toThrow()
188
+ })
189
+
190
+ it('renders remove button when isRemovable is true', () => {
191
+ const { container } = render(
192
+ <Announcement title="Test" isRemovable />
193
+ )
194
+
195
+ const removeButton = getByTestId(container, 'removeAnnouncement')
196
+ expect(removeButton).toBeTruthy()
197
+ })
198
+
199
+ it('does not render remove button when isRemovable is false', () => {
200
+ const { container } = render(
201
+ <Announcement title="Test" isRemovable={false} />
202
+ )
203
+
204
+ expect(() => getByTestId(container, 'removeAnnouncement')).toThrow()
205
+ })
206
+
207
+ it('calls onClose when remove button is clicked', () => {
208
+ const handleClose = vi.fn()
209
+ const { container } = render(
210
+ <Announcement title="Test" isRemovable onClose={handleClose} />
211
+ )
212
+
213
+ const removeButton = getByTestId(container, 'removeAnnouncement')
214
+ act(() => {
215
+ removeButton.click()
216
+ })
217
+
218
+ expect(handleClose).toHaveBeenCalledTimes(1)
219
+ })
220
+
221
+ it('hides announcement when remove button is clicked', () => {
222
+ const { container } = render(
223
+ <Announcement title="Test" isRemovable />
224
+ )
225
+
226
+ const removeButton = getByTestId(container, 'removeAnnouncement')
227
+ act(() => {
228
+ removeButton.click()
229
+ })
230
+
231
+ expect(container.firstChild).toBeNull()
232
+ })
233
+
234
+ it('forwards ref correctly', () => {
235
+ const ref = { current: null }
236
+ render(
237
+ <Announcement ref={ref} title="Test" />
238
+ )
239
+
240
+ expect(ref.current).toBeTruthy()
241
+ })
242
+
243
+ it('applies custom className', () => {
244
+ const { container } = render(
245
+ <Announcement title="Test" className="custom-class" />
246
+ )
247
+
248
+ const announcement = container.firstChild as HTMLElement
249
+ expect(announcement.className).toMatch(/custom-class/)
250
+ })
251
+
252
+ it('passes through HTML attributes', () => {
253
+ const { container } = render(
254
+ <Announcement title="Test" data-custom="value" />
255
+ )
256
+
257
+ const announcement = container.firstChild as HTMLElement
258
+ expect(announcement.getAttribute('data-custom')).toBe('value')
259
+ })
260
+
261
+ it('updates visibility when visible prop changes', () => {
262
+ const { container, root } = render(
263
+ <Announcement title="Test" visible={true} />
264
+ )
265
+
266
+ expect(container.firstChild).toBeTruthy()
267
+
268
+ act(() => {
269
+ root.render(<Announcement title="Test" visible={false} />)
270
+ })
271
+
272
+ expect(container.firstChild).toBeNull()
273
+ })
274
+
275
+ it('renders correct icon for attentive variant', () => {
276
+ const { container } = render(
277
+ <Announcement title="Test" variant="attentive" />
278
+ )
279
+
280
+ const icon = container.querySelector('svg')
281
+ expect(icon).toBeTruthy()
282
+ })
283
+
284
+ it('renders correct icon for informative variant', () => {
285
+ const { container } = render(
286
+ <Announcement title="Test" variant="informative" />
287
+ )
288
+
289
+ const icon = container.querySelector('svg')
290
+ expect(icon).toBeTruthy()
291
+ })
292
+
293
+ it('renders correct icon for negative variant', () => {
294
+ const { container } = render(
295
+ <Announcement title="Test" variant="negative" />
296
+ )
297
+
298
+ const icon = container.querySelector('svg')
299
+ expect(icon).toBeTruthy()
300
+ })
301
+
302
+ it('renders correct icon for positive variant', () => {
303
+ const { container } = render(
304
+ <Announcement title="Test" variant="positive" />
305
+ )
306
+
307
+ const icon = container.querySelector('svg')
308
+ expect(icon).toBeTruthy()
309
+ })
310
+
311
+ it('handles all variant and size combinations', () => {
312
+ const variants = ['neutral', 'attentive', 'informative', 'negative', 'positive'] as const
313
+ const sizes = ['large', 'small'] as const
314
+
315
+ variants.forEach(variant => {
316
+ sizes.forEach(size => {
317
+ const { container } = render(
318
+ <Announcement title="Test" variant={variant} size={size} />
319
+ )
320
+
321
+ expect(container.firstChild).toBeTruthy()
322
+ })
323
+ })
324
+ })
325
+
326
+ it('renders without title or description', () => {
327
+ const { container } = render(
328
+ <Announcement />
329
+ )
330
+
331
+ expect(container.firstChild).toBeTruthy()
332
+ })
333
+
334
+ it('handles complex content structure', () => {
335
+ const { container } = render(
336
+ <Announcement
337
+ title="Complex Title"
338
+ description="Complex Description"
339
+ linkText="Complex Link"
340
+ link="https://example.com"
341
+ withIcon
342
+ isRemovable
343
+ variant="informative"
344
+ size="large"
345
+ />
346
+ )
347
+
348
+ expect(getByText(container, 'Complex Title')).toBeTruthy()
349
+ expect(getByText(container, 'Complex Description')).toBeTruthy()
350
+ expect(getByText(container, 'Complex Link')).toBeTruthy()
351
+ expect(container.querySelector('svg')).toBeTruthy()
352
+ expect(getByTestId(container, 'removeAnnouncement')).toBeTruthy()
353
+ })
354
+ })
@@ -0,0 +1,200 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { AlertTriangle, Info, X } from "lucide-react"
4
+ import { cn } from "../../../lib/utils"
5
+ import { Link } from "../../atoms/link"
6
+
7
+ const announcementVariants = cva(
8
+ "flex gap-4 rounded-lg border p-4 transition-all",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ neutral: "bg-background border-border text-foreground",
13
+ attentive: "bg-amber-50 border-amber-200 text-amber-900 dark:bg-amber-950 dark:border-amber-800 dark:text-amber-100",
14
+ informative: "bg-blue-50 border-blue-200 text-blue-900 dark:bg-blue-950 dark:border-blue-800 dark:text-blue-100",
15
+ negative: "bg-destructive/10 border-destructive/20 text-destructive-foreground",
16
+ positive: "bg-green-50 border-green-200 text-green-900 dark:bg-green-950 dark:border-green-800 dark:text-green-100",
17
+ },
18
+ size: {
19
+ large: "w-full max-w-full",
20
+ small: "w-full max-w-[343px]",
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ variant: "neutral",
25
+ size: "large",
26
+ },
27
+ }
28
+ )
29
+
30
+ const announcementContentVariants = cva(
31
+ "flex flex-1 gap-4",
32
+ {
33
+ variants: {
34
+ size: {
35
+ large: "md:flex-row md:items-center flex-col items-start",
36
+ small: "flex-col items-start",
37
+ },
38
+ },
39
+ defaultVariants: {
40
+ size: "large",
41
+ },
42
+ }
43
+ )
44
+
45
+ const announcementIconVariants = cva(
46
+ "shrink-0",
47
+ {
48
+ variants: {
49
+ variant: {
50
+ neutral: "text-muted-foreground",
51
+ attentive: "text-amber-600 dark:text-amber-400",
52
+ informative: "text-blue-600 dark:text-blue-400",
53
+ negative: "text-destructive",
54
+ positive: "text-green-600 dark:text-green-400",
55
+ },
56
+ },
57
+ defaultVariants: {
58
+ variant: "neutral",
59
+ },
60
+ }
61
+ )
62
+
63
+ export interface AnnouncementProps
64
+ extends React.HTMLAttributes<HTMLDivElement>,
65
+ VariantProps<typeof announcementVariants> {
66
+ title?: string
67
+ description?: string
68
+ linkText?: string
69
+ link?: string
70
+ withIcon?: boolean
71
+ iconName?: React.ReactNode
72
+ isRemovable?: boolean
73
+ visible?: boolean
74
+ onClose?: () => void
75
+ onClickLink?: React.MouseEventHandler<HTMLAnchorElement>
76
+ }
77
+
78
+ const Announcement = React.forwardRef<HTMLDivElement, AnnouncementProps>(
79
+ (
80
+ {
81
+ className,
82
+ variant,
83
+ size,
84
+ title,
85
+ description,
86
+ linkText,
87
+ link,
88
+ withIcon = true,
89
+ iconName,
90
+ isRemovable = false,
91
+ visible = true,
92
+ onClose,
93
+ onClickLink,
94
+ ...props
95
+ },
96
+ ref
97
+ ) => {
98
+ const [isVisible, setIsVisible] = React.useState(visible)
99
+
100
+ const handleClose = React.useCallback(() => {
101
+ onClose?.()
102
+ setIsVisible(false)
103
+ }, [onClose])
104
+
105
+ const getDefaultIcon = () => {
106
+ switch (variant) {
107
+ case "attentive":
108
+ case "negative":
109
+ return <AlertTriangle className="size-6" />
110
+ case "informative":
111
+ case "positive":
112
+ return <Info className="size-6" />
113
+ default:
114
+ return <AlertTriangle className="size-6" />
115
+ }
116
+ }
117
+
118
+ React.useEffect(() => {
119
+ setIsVisible(visible)
120
+ }, [visible])
121
+
122
+ if (!isVisible) return null
123
+
124
+ return (
125
+ <div
126
+ ref={ref}
127
+ className={cn(announcementVariants({ variant, size, className }))}
128
+ {...props}
129
+ >
130
+ {withIcon && (
131
+ <div className={cn(announcementIconVariants({ variant }))}>
132
+ {iconName || getDefaultIcon()}
133
+ </div>
134
+ )}
135
+
136
+ <div className={cn(announcementContentVariants({ size }))}>
137
+ <div className="flex-1 space-y-1">
138
+ {title && (
139
+ <div
140
+ className="text-[var(--font-size-small)] font-medium leading-[var(--line-height-small)]"
141
+ data-testid="title"
142
+ >
143
+ {title}
144
+ </div>
145
+ )}
146
+ {description && (
147
+ <div
148
+ className="text-[var(--font-size-tiny)] leading-[var(--line-height-tiny)]"
149
+ data-testid="description"
150
+ >
151
+ {description}
152
+ </div>
153
+ )}
154
+ </div>
155
+
156
+ <div className="flex items-center gap-4">
157
+ {linkText && (link || onClickLink) && (
158
+ <Link
159
+ href={link}
160
+ onClick={onClickLink}
161
+ target={link ? "_blank" : undefined}
162
+ inline
163
+ size="small"
164
+ data-testid="linkButton"
165
+ >
166
+ {linkText}
167
+ </Link>
168
+ )}
169
+
170
+ {isRemovable && (size === "large" || size === undefined) && (
171
+ <button
172
+ onClick={handleClose}
173
+ className="inline-flex items-center justify-center size-6 rounded-sm opacity-70 hover:opacity-100 transition-opacity"
174
+ aria-label="Close announcement"
175
+ data-testid="removeAnnouncement"
176
+ >
177
+ <X className="size-4" />
178
+ </button>
179
+ )}
180
+ </div>
181
+ </div>
182
+
183
+ {isRemovable && size === "small" && (
184
+ <button
185
+ onClick={handleClose}
186
+ className="inline-flex items-center justify-center size-6 rounded-sm opacity-70 hover:opacity-100 transition-opacity"
187
+ aria-label="Close announcement"
188
+ data-testid="removeAnnouncement"
189
+ >
190
+ <X className="size-4" />
191
+ </button>
192
+ )}
193
+ </div>
194
+ )
195
+ }
196
+ )
197
+
198
+ Announcement.displayName = "Announcement"
199
+
200
+ export { Announcement }