@tpzdsp/next-toolkit 1.2.9 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpzdsp/next-toolkit",
3
- "version": "1.2.9",
3
+ "version": "1.3.0",
4
4
  "description": "A reusable React component library for Next.js applications",
5
5
  "type": "module",
6
6
  "private": false,
@@ -111,6 +111,7 @@
111
111
  "@storybook/react": "8.6.14",
112
112
  "@storybook/react-vite": "8.6.14",
113
113
  "@storybook/test": "^8.6.14",
114
+ "@storybook/types": "8.6.14",
114
115
  "@tailwindcss/typography": "^0.5.16",
115
116
  "@testing-library/dom": "^10.4.0",
116
117
  "@testing-library/jest-dom": "^6.6.3",
@@ -129,6 +130,8 @@
129
130
  "@vitest/coverage-v8": "^3.2.4",
130
131
  "@vitest/eslint-plugin": "^1.3.4",
131
132
  "autoprefixer": "^10.4.21",
133
+ "buffer": "^6.0.3",
134
+ "crypto-browserify": "^3.12.1",
132
135
  "eslint": "^9.30.1",
133
136
  "eslint-config-prettier": "^10.1.8",
134
137
  "eslint-import-resolver-alias": "^1.1.2",
@@ -155,6 +158,7 @@
155
158
  "prettier": "^3.6.2",
156
159
  "prettier-plugin-classnames": "^0.8.1",
157
160
  "prettier-plugin-tailwindcss": "^0.6.14",
161
+ "process": "^0.11.10",
158
162
  "proj4": "^2.19.10",
159
163
  "react": "^19.1.0",
160
164
  "react-dom": "^19.1.0",
@@ -164,10 +168,12 @@
164
168
  "rollup-plugin-peer-deps-external": "^2.2.4",
165
169
  "semantic-release": "^24.2.7",
166
170
  "storybook": "8.6.14",
171
+ "stream-browserify": "^3.0.0",
167
172
  "tailwind-merge": "^3.3.1",
168
173
  "tailwindcss": "^3.4.16",
169
174
  "typescript": "~5.8.3",
170
175
  "typescript-eslint": "^8.35.1",
176
+ "util": "^0.12.5",
171
177
  "vite": "^7.0.4",
172
178
  "vite-plugin-dts": "^4.5.4",
173
179
  "vitest": "^3.2.4"
@@ -1,7 +1,10 @@
1
+ import { FaChevronRight } from 'react-icons/fa6';
2
+
1
3
  /* eslint-disable storybook/no-renderer-packages */
2
4
  import type { Meta, StoryFn } from '@storybook/react';
3
5
 
4
6
  import { Card, type CardProps } from './Card';
7
+ import { Paragraph } from '../Paragraph/Paragraph';
5
8
 
6
9
  export default {
7
10
  children: 'Card',
@@ -33,3 +36,21 @@ ImageAndText.args = {
33
36
  </div>
34
37
  ),
35
38
  };
39
+
40
+ export const WithLink = Template.bind({});
41
+ WithLink.args = {
42
+ children: (
43
+ <>
44
+ <a
45
+ href="https://google.co.uk"
46
+ className="mb-4 flex flex-nowrap items-center gap-2 justify-between font-bold"
47
+ >
48
+ <strong>title</strong>
49
+
50
+ <FaChevronRight className="text-base" />
51
+ </a>
52
+
53
+ <Paragraph>Some descriptive text</Paragraph>
54
+ </>
55
+ ),
56
+ };
@@ -1,12 +1,51 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
1
  import { Card } from './Card';
4
- import { render, screen } from '../../test/renderers';
2
+ import { render, screen, userEvent, within } from '../../test/renderers';
3
+
4
+ describe('Card Component', () => {
5
+ it('renders children correctly', () => {
6
+ render(
7
+ <Card>
8
+ <p>Hello, World!</p>
9
+ </Card>,
10
+ );
11
+
12
+ expect(within(screen.getByRole('article')).getByText('Hello, World!')).toBeInTheDocument();
13
+ });
14
+
15
+ it('renders a link inside the card and handles click', async () => {
16
+ const user = userEvent.setup();
17
+ const handleClick = vi.fn();
18
+
19
+ render(
20
+ <Card>
21
+ <a href="/some-path" onClick={handleClick}>
22
+ Go to page
23
+ </a>
24
+ </Card>,
25
+ );
26
+
27
+ const link = screen.getByRole('link', { name: /go to page/i });
28
+
29
+ expect(link).toBeInTheDocument();
30
+
31
+ await user.click(link);
32
+ expect(handleClick).toHaveBeenCalled();
33
+ });
34
+
35
+ it('merges custom className correctly', () => {
36
+ render(<Card className="bg-red-500 rounded-lg">Custom</Card>);
37
+ const article = screen.getByRole('article');
38
+
39
+ expect(article).toHaveClass('bg-red-500');
40
+ expect(article).toHaveClass('rounded-lg');
41
+ });
5
42
 
6
- describe('Card', () => {
7
- it('renders with default color blue', () => {
8
- render(<Card>Some test text</Card>);
43
+ it('overrides conflicting className using twMerge', () => {
44
+ render(<Card className="pt-4">Override Padding</Card>);
45
+ const article = screen.getByRole('article');
9
46
 
10
- expect(screen.getByRole('article')).toBeInTheDocument();
47
+ // Should NOT have original 'pt-[12px]' due to twMerge override
48
+ expect(article?.className).not.toMatch(/pt-\[12px\]/);
49
+ expect(article).toHaveClass('pt-4');
11
50
  });
12
51
  });
@@ -9,7 +9,7 @@ export const Card = ({ className, children }: CardProps) => {
9
9
  return (
10
10
  <article
11
11
  className={twMerge(
12
- 'relative my-6 flex w-96 flex-col rounded-lg border border-slate-200 bg-white p-2 shadow-sm',
12
+ 'h-full flex flex-col pt-[12px] px-[4px] mx-3 border-t border-slate-500',
13
13
  className,
14
14
  )}
15
15
  >
@@ -1,7 +1,11 @@
1
- export type ParagraphProps = {
2
- children: React.ReactNode;
3
- };
1
+ import type { ExtendProps } from '../../types/utils';
2
+
3
+ export type ParagraphProps = ExtendProps<'p'>;
4
4
 
5
- export const Paragraph = ({ children }: ParagraphProps) => {
6
- return <p className="pb-4 text-sm text-text-primary">{children}</p>;
5
+ export const Paragraph = ({ className, children, ...props }: ParagraphProps) => {
6
+ return (
7
+ <p className={`pb-4 text-sm text-text-primary ${className}`} {...props}>
8
+ {children}
9
+ </p>
10
+ );
7
11
  };
@@ -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
+ });