@tpzdsp/next-toolkit 1.2.9 → 1.4.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 (35) hide show
  1. package/package.json +14 -1
  2. package/src/components/Card/Card.stories.tsx +21 -0
  3. package/src/components/Card/Card.test.tsx +46 -7
  4. package/src/components/Card/Card.tsx +1 -1
  5. package/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +89 -0
  6. package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +75 -0
  7. package/src/components/ErrorBoundary/ErrorBoundary.tsx +28 -0
  8. package/src/components/ErrorBoundary/ErrorFallback.stories.tsx +70 -0
  9. package/src/components/ErrorBoundary/ErrorFallback.test.tsx +54 -0
  10. package/src/components/ErrorBoundary/ErrorFallback.tsx +30 -0
  11. package/src/components/Paragraph/Paragraph.tsx +9 -5
  12. package/src/components/backToTop/BackToTop.stories.tsx +409 -0
  13. package/src/components/backToTop/BackToTop.test.tsx +57 -0
  14. package/src/components/backToTop/BackToTop.tsx +131 -0
  15. package/src/components/chip/Chip.stories.tsx +40 -0
  16. package/src/components/chip/Chip.test.tsx +31 -0
  17. package/src/components/chip/Chip.tsx +20 -0
  18. package/src/components/cookieBanner/CookieBanner.stories.tsx +258 -0
  19. package/src/components/cookieBanner/CookieBanner.test.tsx +68 -0
  20. package/src/components/cookieBanner/CookieBanner.tsx +73 -0
  21. package/src/components/form/Input.stories.tsx +435 -0
  22. package/src/components/form/Input.test.tsx +214 -0
  23. package/src/components/form/Input.tsx +24 -0
  24. package/src/components/form/TextArea.stories.tsx +465 -0
  25. package/src/components/form/TextArea.test.tsx +236 -0
  26. package/src/components/form/TextArea.tsx +24 -0
  27. package/src/components/googleAnalytics/GlobalVars.tsx +31 -0
  28. package/src/components/googleAnalytics/GoogleAnalytics.tsx +15 -0
  29. package/src/components/index.ts +15 -0
  30. package/src/components/skipLink/SkipLink.stories.tsx +346 -0
  31. package/src/components/skipLink/SkipLink.test.tsx +22 -0
  32. package/src/components/skipLink/SkipLink.tsx +19 -0
  33. package/src/errors/ApiError.ts +71 -0
  34. package/src/errors/index.ts +1 -0
  35. package/src/utils/utils.ts +3 -0
@@ -0,0 +1,409 @@
1
+ /* eslint-disable @typescript-eslint/naming-convention */
2
+ /* eslint-disable storybook/no-renderer-packages */
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+
5
+ import { BackToTop } from './BackToTop';
6
+
7
+ const meta = {
8
+ title: 'Components/BackToTop',
9
+ component: BackToTop,
10
+ parameters: {
11
+ layout: 'fullscreen',
12
+ docs: {
13
+ description: {
14
+ component:
15
+ 'A floating "Back to Top" button that appears when the user scrolls down and allows quick navigation back to the top of the page.',
16
+ },
17
+ },
18
+ },
19
+ tags: ['autodocs'],
20
+ argTypes: {
21
+ threshold: {
22
+ control: { type: 'number', min: 0, max: 2000, step: 100 },
23
+ description: 'Scroll threshold in pixels before button appears',
24
+ defaultValue: 600,
25
+ },
26
+ bottom: {
27
+ control: { type: 'number', min: 0, max: 100, step: 4 },
28
+ description: 'Position from bottom in pixels',
29
+ defaultValue: 16,
30
+ },
31
+ left: {
32
+ control: { type: 'number', min: 0, max: 100, step: 4 },
33
+ description: 'Position from left in pixels',
34
+ defaultValue: 8,
35
+ },
36
+ className: {
37
+ control: 'text',
38
+ description: 'Custom className for additional styling',
39
+ },
40
+ },
41
+ } satisfies Meta<typeof BackToTop>;
42
+
43
+ export default meta;
44
+
45
+ type Story = StoryObj<typeof meta>;
46
+
47
+ export const Default: Story = {
48
+ parameters: {
49
+ docs: {
50
+ description: {
51
+ story: 'Default back to top button. Scroll down to see it appear at 600px threshold.',
52
+ },
53
+ },
54
+ },
55
+ render: (args) => (
56
+ <div>
57
+ <BackToTop {...args} />
58
+
59
+ <div className="space-y-8 p-8">
60
+ <h1 className="text-3xl font-bold">Scroll down to see the Back to Top button</h1>
61
+
62
+ <div className="space-y-6">
63
+ {Array.from({ length: 50 }, (_, i) => (
64
+ <div key={i} className="p-4 bg-gray-100 rounded">
65
+ <h2 className="text-xl font-semibold mb-2">Section {i + 1}</h2>
66
+
67
+ <p>
68
+ This is content section {i + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing
69
+ elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
70
+ minim veniam, quis nostrud exercitation ullamco laboris.
71
+ </p>
72
+ </div>
73
+ ))}
74
+ </div>
75
+ </div>
76
+ </div>
77
+ ),
78
+ };
79
+
80
+ export const LowThreshold: Story = {
81
+ args: {
82
+ threshold: 200,
83
+ },
84
+ parameters: {
85
+ docs: {
86
+ description: {
87
+ story: 'Back to top button with a low threshold (200px) - appears quickly when scrolling.',
88
+ },
89
+ },
90
+ },
91
+ render: (args) => (
92
+ <div>
93
+ <BackToTop {...args} />
94
+
95
+ <div className="space-y-8 p-8">
96
+ <h1 className="text-3xl font-bold">Low Threshold Example (200px)</h1>
97
+
98
+ <div className="bg-yellow-100 border border-yellow-400 rounded p-4 mb-6">
99
+ <p className="text-yellow-800">
100
+ This example has a low threshold of 200px. The button will appear much sooner when
101
+ scrolling.
102
+ </p>
103
+ </div>
104
+
105
+ <div className="space-y-6">
106
+ {Array.from({ length: 30 }, (_, i) => (
107
+ <div key={i} className="p-4 bg-blue-50 rounded">
108
+ <h2 className="text-xl font-semibold mb-2">Content Block {i + 1}</h2>
109
+
110
+ <p>
111
+ Content with low threshold setting. The back to top button should appear after
112
+ scrolling just 200 pixels down the page.
113
+ </p>
114
+ </div>
115
+ ))}
116
+ </div>
117
+ </div>
118
+ </div>
119
+ ),
120
+ };
121
+
122
+ export const CustomPosition: Story = {
123
+ args: {
124
+ bottom: 32,
125
+ left: 32,
126
+ },
127
+ parameters: {
128
+ docs: {
129
+ description: {
130
+ story: 'Back to top button with custom positioning (32px from bottom and left).',
131
+ },
132
+ },
133
+ },
134
+ render: (args) => (
135
+ <div>
136
+ <BackToTop {...args} />
137
+
138
+ <div className="space-y-8 p-8">
139
+ <h1 className="text-3xl font-bold">Custom Position Example</h1>
140
+
141
+ <div className="bg-green-100 border border-green-400 rounded p-4 mb-6">
142
+ <p className="text-green-800">
143
+ This button is positioned 32px from both bottom and left edges.
144
+ </p>
145
+ </div>
146
+
147
+ <div className="space-y-6">
148
+ {Array.from({ length: 40 }, (_, i) => (
149
+ <div key={i} className="p-4 bg-green-50 rounded">
150
+ <h2 className="text-xl font-semibold mb-2">Section {i + 1}</h2>
151
+
152
+ <p>Scroll down to see the back to top button in its custom position.</p>
153
+ </div>
154
+ ))}
155
+ </div>
156
+ </div>
157
+ </div>
158
+ ),
159
+ };
160
+
161
+ export const RightAligned: Story = {
162
+ args: {
163
+ left: undefined,
164
+ className: 'right-4',
165
+ },
166
+ parameters: {
167
+ docs: {
168
+ description: {
169
+ story: 'Back to top button positioned on the right side using custom className.',
170
+ },
171
+ },
172
+ },
173
+ render: (args) => (
174
+ <div>
175
+ <BackToTop {...args} />
176
+
177
+ <div className="space-y-8 p-8">
178
+ <h1 className="text-3xl font-bold">Right-Aligned Button</h1>
179
+
180
+ <div className="bg-purple-100 border border-purple-400 rounded p-4 mb-6">
181
+ <p className="text-purple-800">
182
+ This button is positioned on the right side of the screen using className override.
183
+ </p>
184
+ </div>
185
+
186
+ <div className="space-y-6">
187
+ {Array.from({ length: 35 }, (_, i) => (
188
+ <div key={i} className="p-4 bg-purple-50 rounded">
189
+ <h2 className="text-xl font-semibold mb-2">Content {i + 1}</h2>
190
+
191
+ <p>The back to top button appears on the right side instead of the default left.</p>
192
+ </div>
193
+ ))}
194
+ </div>
195
+ </div>
196
+ </div>
197
+ ),
198
+ };
199
+
200
+ export const CustomStyling: Story = {
201
+ args: {
202
+ className: 'bg-blue-600 text-white border-blue-700 hover:bg-blue-700 shadow-xl',
203
+ },
204
+ parameters: {
205
+ docs: {
206
+ description: {
207
+ story: 'Back to top button with custom styling using className prop.',
208
+ },
209
+ },
210
+ },
211
+ render: (args) => (
212
+ <div>
213
+ <BackToTop {...args} />
214
+
215
+ <div className="space-y-8 p-8">
216
+ <h1 className="text-3xl font-bold">Custom Styled Button</h1>
217
+
218
+ <div className="bg-blue-100 border border-blue-400 rounded p-4 mb-6">
219
+ <p className="text-blue-800">
220
+ This button has custom blue styling applied via the className prop.
221
+ </p>
222
+ </div>
223
+
224
+ <div className="space-y-6">
225
+ {Array.from({ length: 25 }, (_, i) => (
226
+ <div key={i} className="p-4 bg-blue-50 rounded">
227
+ <h2 className="text-xl font-semibold mb-2">Styled Section {i + 1}</h2>
228
+
229
+ <p>Custom styled back to top button with blue theme.</p>
230
+ </div>
231
+ ))}
232
+ </div>
233
+ </div>
234
+ </div>
235
+ ),
236
+ };
237
+
238
+ export const AlwaysVisible: Story = {
239
+ args: {
240
+ threshold: 0,
241
+ },
242
+ parameters: {
243
+ docs: {
244
+ description: {
245
+ story: 'Back to top button that is always visible (threshold set to 0).',
246
+ },
247
+ },
248
+ },
249
+ render: (args) => (
250
+ <div>
251
+ <BackToTop {...args} />
252
+
253
+ <div className="space-y-8 p-8">
254
+ <h1 className="text-3xl font-bold">Always Visible Button</h1>
255
+
256
+ <div className="bg-red-100 border border-red-400 rounded p-4 mb-6">
257
+ <p className="text-red-800">
258
+ This button is always visible because threshold is set to 0px.
259
+ </p>
260
+ </div>
261
+
262
+ <div className="space-y-6">
263
+ {Array.from({ length: 20 }, (_, i) => (
264
+ <div key={i} className="p-4 bg-red-50 rounded">
265
+ <h2 className="text-xl font-semibold mb-2">Always Visible {i + 1}</h2>
266
+
267
+ <p>The button is visible even at the top of the page.</p>
268
+ </div>
269
+ ))}
270
+ </div>
271
+ </div>
272
+ </div>
273
+ ),
274
+ };
275
+
276
+ export const LongPageExample: Story = {
277
+ parameters: {
278
+ docs: {
279
+ description: {
280
+ story: 'Comprehensive example with a very long page to test scroll behavior thoroughly.',
281
+ },
282
+ },
283
+ },
284
+ render: (args) => (
285
+ <div>
286
+ <BackToTop {...args} />
287
+
288
+ <div className="space-y-8 p-8">
289
+ <header className="mb-12">
290
+ <h1 className="text-4xl font-bold mb-4">Long Page Scroll Test</h1>
291
+
292
+ <p className="text-lg text-gray-600">
293
+ This page demonstrates the back to top functionality with extensive content. Scroll down
294
+ to see the button appear after 600px.
295
+ </p>
296
+ </header>
297
+
298
+ <section className="space-y-8">
299
+ <h2 className="text-2xl font-bold border-b pb-2">Article Content</h2>
300
+ {Array.from({ length: 100 }, (_, i) => (
301
+ <article key={i} className="p-6 bg-white border rounded-lg shadow-sm">
302
+ <h3 className="text-xl font-semibold mb-3">Article {i + 1}</h3>
303
+
304
+ <p className="mb-4">
305
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
306
+ incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
307
+ exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
308
+ </p>
309
+
310
+ <p>
311
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
312
+ fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa
313
+ qui officia deserunt mollit anim id est laborum.
314
+ </p>
315
+ {i % 10 === 9 && (
316
+ <div className="mt-4 p-3 bg-gray-100 rounded">
317
+ <p className="text-sm text-gray-600">
318
+ 📍 Checkpoint: You&apos;ve scrolled through {i + 1} articles. Use the back to
319
+ top button to return to the beginning!
320
+ </p>
321
+ </div>
322
+ )}
323
+ </article>
324
+ ))}
325
+ </section>
326
+
327
+ <footer className="mt-12 p-6 bg-gray-800 text-white rounded">
328
+ <p className="text-center">
329
+ 🎉 You&apos;ve reached the end! Use the back to top button to scroll back up.
330
+ </p>
331
+ </footer>
332
+ </div>
333
+ </div>
334
+ ),
335
+ };
336
+
337
+ export const AccessibilityDemo: Story = {
338
+ parameters: {
339
+ docs: {
340
+ description: {
341
+ story:
342
+ 'Demonstration of accessibility features including keyboard navigation and screen reader support.',
343
+ },
344
+ },
345
+ },
346
+ render: (args) => (
347
+ <div>
348
+ <BackToTop {...args} />
349
+
350
+ <div className="space-y-8 p-8">
351
+ <h1 className="text-3xl font-bold">Accessibility Features Demo</h1>
352
+
353
+ <div className="bg-blue-50 border border-blue-200 rounded p-6">
354
+ <h2 className="text-xl font-bold text-blue-800 mb-4">Accessibility Features:</h2>
355
+
356
+ <ul className="list-disc list-inside space-y-2 text-blue-700">
357
+ <li>
358
+ <strong>Keyboard Navigation:</strong> Tab to focus, Enter or Space to activate
359
+ </li>
360
+
361
+ <li>
362
+ <strong>Screen Reader:</strong> Proper aria-label and hidden decorative icon
363
+ </li>
364
+
365
+ <li>
366
+ <strong>Focus Management:</strong> Visible focus indicators with ring
367
+ </li>
368
+
369
+ <li>
370
+ <strong>Responsive Text:</strong> &ldquo;Back to top&rdquo; text hidden on mobile,
371
+ available to screen readers
372
+ </li>
373
+
374
+ <li>
375
+ <strong>Semantic HTML:</strong> Proper button element with type attribute
376
+ </li>
377
+ </ul>
378
+ </div>
379
+
380
+ <div className="bg-green-50 border border-green-200 rounded p-4">
381
+ <h3 className="font-bold text-green-800 mb-2">Testing Instructions:</h3>
382
+
383
+ <ol className="list-decimal list-inside space-y-1 text-green-700 text-sm">
384
+ <li>Scroll down until the back to top button appears</li>
385
+
386
+ <li>Use Tab key to navigate to the button</li>
387
+
388
+ <li>Press Enter or Space to activate it</li>
389
+
390
+ <li>Test with a screen reader for proper announcements</li>
391
+ </ol>
392
+ </div>
393
+
394
+ <div className="space-y-6">
395
+ {Array.from({ length: 30 }, (_, i) => (
396
+ <div key={i} className="p-4 bg-gray-50 rounded">
397
+ <h3 className="text-lg font-semibold mb-2">Accessibility Section {i + 1}</h3>
398
+
399
+ <p>
400
+ This content tests the accessibility of the back to top button. The button should be
401
+ properly focusable and operable via keyboard.
402
+ </p>
403
+ </div>
404
+ ))}
405
+ </div>
406
+ </div>
407
+ </div>
408
+ ),
409
+ };
@@ -0,0 +1,57 @@
1
+ import { BackToTop } from './BackToTop';
2
+ import { render, screen, userEvent } from '../../test/renderers';
3
+
4
+ describe('BackToTop component', () => {
5
+ const originalScrollTo = window.scrollTo;
6
+
7
+ beforeEach(() => {
8
+ Object.defineProperty(window, 'scrollTo', {
9
+ value: vi.fn(),
10
+ writable: true,
11
+ });
12
+
13
+ // Simulate scrollY (more modern than pageYOffset)
14
+ Object.defineProperty(window, 'pageYOffset', {
15
+ get: () => 700,
16
+ configurable: true,
17
+ });
18
+
19
+ Object.defineProperty(window, 'scrollY', {
20
+ get: () => 700,
21
+ configurable: true,
22
+ });
23
+ });
24
+
25
+ afterEach(() => {
26
+ vi.clearAllMocks();
27
+ window.scrollTo = originalScrollTo;
28
+ });
29
+
30
+ it('renders correctly when scrolled down', async () => {
31
+ render(<BackToTop />);
32
+
33
+ // Trigger the scroll event manually to call toggleVisibility
34
+ window.dispatchEvent(new Event('scroll'));
35
+
36
+ // Wait for the component to update
37
+ const button = await screen.findByRole('button', { name: /back to top/i });
38
+
39
+ expect(button).toBeInTheDocument();
40
+ });
41
+
42
+ it('clickable and triggers scrollToTop', async () => {
43
+ const user = userEvent.setup();
44
+
45
+ render(<BackToTop />);
46
+
47
+ // Trigger the scroll event manually to call toggleVisibility
48
+ window.dispatchEvent(new Event('scroll'));
49
+
50
+ // Wait for the component to update and find the button
51
+ const button = await screen.findByRole('button', { name: /back to top/i });
52
+
53
+ await user.click(button);
54
+
55
+ expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
56
+ });
57
+ });
@@ -0,0 +1,131 @@
1
+ 'use client';
2
+
3
+ import { type KeyboardEvent, useCallback, useEffect, useState } from 'react';
4
+
5
+ import { LuArrowUp } from 'react-icons/lu';
6
+
7
+ export type BackToTopProps = {
8
+ /** Scroll threshold in pixels before button appears */
9
+ threshold?: number;
10
+ /** Position from bottom in pixels */
11
+ bottom?: number;
12
+ /** Position from left in pixels */
13
+ left?: number;
14
+ /** Custom className for styling */
15
+ className?: string;
16
+ };
17
+
18
+ export const BackToTop = ({
19
+ threshold = 600,
20
+ bottom = 16,
21
+ left = 8,
22
+ className = '',
23
+ }: BackToTopProps) => {
24
+ const [isVisible, setIsVisible] = useState(false);
25
+
26
+ // Throttle scroll events for better performance
27
+ const throttle = useCallback((func: () => void, delay: number) => {
28
+ let timeoutId: number | null = null;
29
+ let lastExecTime = 0;
30
+
31
+ return () => {
32
+ const currentTime = Date.now();
33
+
34
+ if (currentTime - lastExecTime > delay) {
35
+ func();
36
+ lastExecTime = currentTime;
37
+ } else {
38
+ if (timeoutId) {
39
+ clearTimeout(timeoutId);
40
+ }
41
+
42
+ timeoutId = window.setTimeout(
43
+ () => {
44
+ func();
45
+ lastExecTime = Date.now();
46
+ },
47
+ delay - (currentTime - lastExecTime),
48
+ );
49
+ }
50
+ };
51
+ }, []);
52
+
53
+ // Show button when page is scrolled down
54
+ const toggleVisibility = useCallback(() => {
55
+ if (typeof window !== 'undefined') {
56
+ setIsVisible(window.pageYOffset > threshold);
57
+ }
58
+ }, [threshold]);
59
+
60
+ // Scroll to top smoothly
61
+ const scrollToTop = useCallback(() => {
62
+ if (typeof window !== 'undefined') {
63
+ window.scrollTo({
64
+ top: 0,
65
+ behavior: 'smooth',
66
+ });
67
+ }
68
+ }, []);
69
+
70
+ useEffect(() => {
71
+ if (typeof window === 'undefined') {
72
+ return;
73
+ }
74
+
75
+ const throttledToggleVisibility = throttle(toggleVisibility, 100);
76
+
77
+ // Check initial scroll position
78
+ toggleVisibility();
79
+
80
+ window.addEventListener('scroll', throttledToggleVisibility, { passive: true });
81
+
82
+ return () => {
83
+ window.removeEventListener('scroll', throttledToggleVisibility);
84
+ };
85
+ }, [toggleVisibility, throttle]);
86
+
87
+ // Handle keyboard interaction
88
+ const handleKeyDown = useCallback(
89
+ (event: KeyboardEvent<HTMLButtonElement>) => {
90
+ if (event.key === 'Enter' || event.key === ' ') {
91
+ event.preventDefault();
92
+ scrollToTop();
93
+ }
94
+ },
95
+ [scrollToTop],
96
+ );
97
+
98
+ if (!isVisible) {
99
+ return null;
100
+ }
101
+
102
+ return (
103
+ <button
104
+ type="button"
105
+ className={`
106
+ fixed z-50 inline-flex items-center gap-1
107
+ bg-white text-link border border-gray-300
108
+ rounded-md px-3 py-2 shadow-lg
109
+ hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
110
+ transition-all duration-200 ease-in-out
111
+ ${className}
112
+ `.trim()}
113
+ // className={`fixed bottom-4 left-2 text-link gap-1 inline-flex items-center bg-white
114
+ // ${className}`.trim()}
115
+ style={{
116
+ bottom: `${bottom}px`,
117
+ left: `${left}px`,
118
+ }}
119
+ onClick={scrollToTop}
120
+ onKeyDown={handleKeyDown}
121
+ aria-label="Scroll back to top of page"
122
+ title="Back to top"
123
+ >
124
+ <LuArrowUp size={20} aria-hidden="true" />
125
+
126
+ <span className="hidden sm:inline">Back to top</span>
127
+
128
+ <span className="sr-only sm:hidden">Back to top</span>
129
+ </button>
130
+ );
131
+ };
@@ -0,0 +1,40 @@
1
+ import { AiFillChrome } from 'react-icons/ai';
2
+
3
+ /* eslint-disable storybook/no-renderer-packages */
4
+ import type { Meta, StoryFn } from '@storybook/react';
5
+
6
+ import { Chip, type ChipProps } from './Chip';
7
+ import { Paragraph } from '../Paragraph/Paragraph';
8
+
9
+ export default {
10
+ children: 'Chip',
11
+ component: Chip,
12
+ } as Meta;
13
+
14
+ const Template: StoryFn<ChipProps> = (args) => <Chip {...args} />;
15
+
16
+ export const Default = Template.bind({});
17
+ Default.args = {
18
+ children: 'Chip',
19
+ };
20
+
21
+ export const JustText = Template.bind({});
22
+ JustText.args = {
23
+ children: 'Hello, this is some simple text',
24
+ };
25
+
26
+ export const ParagraphOfText = Template.bind({});
27
+ ParagraphOfText.args = {
28
+ children: <Paragraph className="pb-0">Hello, this is a paragraph of text</Paragraph>,
29
+ };
30
+
31
+ export const TextWithIcon = Template.bind({});
32
+ TextWithIcon.args = {
33
+ children: (
34
+ <div className="flex items-center justify-center gap-2">
35
+ <AiFillChrome className="text-base" />
36
+
37
+ <Paragraph className="pb-0">Hello, this is a paragraph of text</Paragraph>
38
+ </div>
39
+ ),
40
+ };
@@ -0,0 +1,31 @@
1
+ import { Chip } from './Chip';
2
+ import { render, screen } from '../../test/renderers';
3
+
4
+ describe('Chip component', () => {
5
+ it('should render children correctly', () => {
6
+ render(
7
+ <Chip>
8
+ <p>Hello, World!</p>
9
+ </Chip>,
10
+ );
11
+
12
+ expect(screen.getByText('Hello, World!')).toBeInTheDocument();
13
+ });
14
+
15
+ it('should merge custom className correctly', () => {
16
+ render(<Chip className="bg-red-500 rounded-lg">Custom</Chip>);
17
+ const element = screen.getByText('Custom');
18
+
19
+ expect(element).toHaveClass('bg-red-500');
20
+ expect(element).toHaveClass('rounded-lg');
21
+ });
22
+
23
+ it('should override conflicting className using twMerge', () => {
24
+ render(<Chip className="px-2">Custom</Chip>);
25
+ const element = screen.getByText('Custom');
26
+
27
+ // Should NOT have original 'pt-[12px]' due to twMerge override
28
+ expect(element?.className).not.toMatch(/px-3/);
29
+ expect(element).toHaveClass('px-2');
30
+ });
31
+ });
@@ -0,0 +1,20 @@
1
+ import { twMerge } from 'tailwind-merge';
2
+
3
+ export type ChipProps = {
4
+ children: React.ReactNode;
5
+ className?: string;
6
+ };
7
+
8
+ export const Chip = ({ className, children }: ChipProps) => {
9
+ return (
10
+ <span
11
+ className={twMerge(
12
+ `inline-flex items-center rounded-lg bg-gray-200 px-3 py-1 text-sm font-medium
13
+ text-gray-800`,
14
+ className,
15
+ )}
16
+ >
17
+ {children}
18
+ </span>
19
+ );
20
+ };