@dashforge/tw 0.3.0-beta → 0.5.0-beta

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 (81) hide show
  1. package/CHANGELOG.md +246 -0
  2. package/PARITY.md +483 -0
  3. package/PERFORMANCE.md +169 -0
  4. package/dist/index.esm.js +1055 -2
  5. package/dist/src/components/Accordion/Accordion.d.ts +21 -0
  6. package/dist/src/components/Accordion/Accordion.d.ts.map +1 -0
  7. package/dist/src/components/Accordion/accordion.types.d.ts +74 -0
  8. package/dist/src/components/Accordion/accordion.types.d.ts.map +1 -0
  9. package/dist/src/components/Accordion/accordion.variants.d.ts +58 -0
  10. package/dist/src/components/Accordion/accordion.variants.d.ts.map +1 -0
  11. package/dist/src/components/Dialog/Dialog.d.ts +17 -0
  12. package/dist/src/components/Dialog/Dialog.d.ts.map +1 -0
  13. package/dist/src/components/Dialog/dialog.types.d.ts +75 -0
  14. package/dist/src/components/Dialog/dialog.types.d.ts.map +1 -0
  15. package/dist/src/components/Dialog/dialog.variants.d.ts +80 -0
  16. package/dist/src/components/Dialog/dialog.variants.d.ts.map +1 -0
  17. package/dist/src/components/Pagination/Pagination.d.ts +32 -0
  18. package/dist/src/components/Pagination/Pagination.d.ts.map +1 -0
  19. package/dist/src/components/Pagination/pagination.helpers.d.ts +23 -0
  20. package/dist/src/components/Pagination/pagination.helpers.d.ts.map +1 -0
  21. package/dist/src/components/Pagination/pagination.types.d.ts +120 -0
  22. package/dist/src/components/Pagination/pagination.types.d.ts.map +1 -0
  23. package/dist/src/components/Pagination/pagination.variants.d.ts +150 -0
  24. package/dist/src/components/Pagination/pagination.variants.d.ts.map +1 -0
  25. package/dist/src/components/Popover/Popover.d.ts +15 -0
  26. package/dist/src/components/Popover/Popover.d.ts.map +1 -0
  27. package/dist/src/components/Popover/popover.types.d.ts +45 -0
  28. package/dist/src/components/Popover/popover.types.d.ts.map +1 -0
  29. package/dist/src/components/Popover/popover.variants.d.ts +34 -0
  30. package/dist/src/components/Popover/popover.variants.d.ts.map +1 -0
  31. package/dist/src/components/Skeleton/Skeleton.d.ts +23 -0
  32. package/dist/src/components/Skeleton/Skeleton.d.ts.map +1 -0
  33. package/dist/src/components/Skeleton/skeleton.types.d.ts +56 -0
  34. package/dist/src/components/Skeleton/skeleton.types.d.ts.map +1 -0
  35. package/dist/src/components/Skeleton/skeleton.variants.d.ts +110 -0
  36. package/dist/src/components/Skeleton/skeleton.variants.d.ts.map +1 -0
  37. package/dist/src/components/Tabs/Tabs.d.ts +15 -0
  38. package/dist/src/components/Tabs/Tabs.d.ts.map +1 -0
  39. package/dist/src/components/Tabs/tabs.types.d.ts +55 -0
  40. package/dist/src/components/Tabs/tabs.types.d.ts.map +1 -0
  41. package/dist/src/components/Tabs/tabs.variants.d.ts +82 -0
  42. package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -0
  43. package/dist/src/components/Tooltip/Tooltip.d.ts +29 -0
  44. package/dist/src/components/Tooltip/Tooltip.d.ts.map +1 -0
  45. package/dist/src/components/Tooltip/tooltip.types.d.ts +47 -0
  46. package/dist/src/components/Tooltip/tooltip.types.d.ts.map +1 -0
  47. package/dist/src/components/Tooltip/tooltip.variants.d.ts +34 -0
  48. package/dist/src/components/Tooltip/tooltip.variants.d.ts.map +1 -0
  49. package/dist/src/index.d.ts +22 -1
  50. package/dist/src/index.d.ts.map +1 -1
  51. package/package.json +8 -3
  52. package/src/components/Accordion/Accordion.test.tsx +95 -0
  53. package/src/components/Accordion/Accordion.tsx +97 -0
  54. package/src/components/Accordion/accordion.types.ts +66 -0
  55. package/src/components/Accordion/accordion.variants.ts +30 -0
  56. package/src/components/Dialog/Dialog.test.tsx +96 -0
  57. package/src/components/Dialog/Dialog.tsx +93 -0
  58. package/src/components/Dialog/dialog.types.ts +62 -0
  59. package/src/components/Dialog/dialog.variants.ts +62 -0
  60. package/src/components/Pagination/Pagination.test.tsx +314 -0
  61. package/src/components/Pagination/Pagination.tsx +275 -0
  62. package/src/components/Pagination/pagination.helpers.ts +102 -0
  63. package/src/components/Pagination/pagination.types.ts +117 -0
  64. package/src/components/Pagination/pagination.variants.ts +111 -0
  65. package/src/components/Popover/Popover.test.tsx +72 -0
  66. package/src/components/Popover/Popover.tsx +55 -0
  67. package/src/components/Popover/popover.types.ts +42 -0
  68. package/src/components/Popover/popover.variants.ts +18 -0
  69. package/src/components/Skeleton/Skeleton.test.tsx +127 -0
  70. package/src/components/Skeleton/Skeleton.tsx +49 -0
  71. package/src/components/Skeleton/skeleton.types.ts +59 -0
  72. package/src/components/Skeleton/skeleton.variants.ts +86 -0
  73. package/src/components/Tabs/Tabs.test.tsx +74 -0
  74. package/src/components/Tabs/Tabs.tsx +66 -0
  75. package/src/components/Tabs/tabs.types.ts +49 -0
  76. package/src/components/Tabs/tabs.variants.ts +58 -0
  77. package/src/components/Tooltip/Tooltip.test.tsx +73 -0
  78. package/src/components/Tooltip/Tooltip.tsx +70 -0
  79. package/src/components/Tooltip/tooltip.types.ts +44 -0
  80. package/src/components/Tooltip/tooltip.variants.ts +18 -0
  81. package/src/index.ts +57 -1
@@ -0,0 +1,62 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { DialogVariants } from './dialog.variants.js';
3
+
4
+ /**
5
+ * Per-slot className overrides for `<Dialog>`. Mirrors the
6
+ * canonical TW slotProps shape: `{ slot?: { className?: string } }`.
7
+ */
8
+ export interface DialogSlotProps {
9
+ overlay?: { className?: string };
10
+ content?: { className?: string };
11
+ title?: { className?: string };
12
+ description?: { className?: string };
13
+ closeButton?: { className?: string };
14
+ body?: { className?: string };
15
+ actions?: { className?: string };
16
+ }
17
+
18
+ /**
19
+ * Props for the declarative `<Dialog>` component.
20
+ *
21
+ * Built on `@radix-ui/react-dialog`. Use this when:
22
+ * - You want full control over the dialog's open state (controlled
23
+ * from a parent component).
24
+ * - You want to mount a custom-styled dialog body / trigger that
25
+ * composes with the rest of your component tree.
26
+ * - You don't need an imperative `confirm()`-style return value
27
+ * (use `useDialog()` or `useConfirm()` for those).
28
+ *
29
+ * Three size variants (`sm` / `md` / `lg`) with sensible defaults.
30
+ * Backdrop + content animations respect `prefers-reduced-motion`.
31
+ */
32
+ export interface DialogProps extends Pick<DialogVariants, 'size'> {
33
+ /** Controlled open state. */
34
+ open: boolean;
35
+ /** Called when the dialog requests to change open state. */
36
+ onOpenChange: (open: boolean) => void;
37
+ /** Dialog body — usually a `<DialogContent>` subtree. */
38
+ children: ReactNode;
39
+ /** Dialog title (screen-reader announced via `aria-labelledby`). */
40
+ title?: ReactNode;
41
+ /** Dialog description (announced via `aria-describedby`). */
42
+ description?: ReactNode;
43
+ /**
44
+ * Show the `×` close button in the top-right corner.
45
+ * @default true
46
+ */
47
+ showCloseButton?: boolean;
48
+ /**
49
+ * Disable closing on backdrop click.
50
+ * @default false
51
+ */
52
+ disableBackdropClose?: boolean;
53
+ /**
54
+ * Disable closing on Escape key.
55
+ * @default false
56
+ */
57
+ disableEscapeClose?: boolean;
58
+ /** Root className shortcut on the content element. */
59
+ sx?: string;
60
+ /** Per-slot overrides. */
61
+ slotProps?: DialogSlotProps;
62
+ }
@@ -0,0 +1,62 @@
1
+ import { tv, type VariantProps } from 'tailwind-variants';
2
+
3
+ /**
4
+ * `<Dialog>` variant recipe.
5
+ *
6
+ * Slots:
7
+ * - `overlay` — backdrop (Radix's `DialogOverlay`)
8
+ * - `content` — the dialog box itself
9
+ * - `title` — heading at the top
10
+ * - `description` — secondary text under the title
11
+ * - `closeButton` — top-right `×` button
12
+ * - `body` — main scrollable area
13
+ * - `actions` — footer action bar
14
+ *
15
+ * Three sizes (`sm` / `md` / `lg`) drive the content max-width.
16
+ * Motion is gated by `prefers-reduced-motion` so the slide-in
17
+ * animation collapses to an instant appear on users that opt out.
18
+ */
19
+ export const dialogVariants = tv({
20
+ slots: {
21
+ overlay: [
22
+ 'fixed inset-0 z-50 bg-neutral-950/60 backdrop-blur-sm',
23
+ 'data-[state=open]:animate-in data-[state=open]:fade-in-0',
24
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0',
25
+ 'motion-reduce:transition-none motion-reduce:duration-0',
26
+ ],
27
+ content: [
28
+ 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',
29
+ 'w-full bg-white dark:bg-neutral-900 rounded-lg shadow-xl',
30
+ 'border border-neutral-200 dark:border-neutral-800',
31
+ 'p-6 flex flex-col gap-4 max-h-[90vh]',
32
+ 'focus:outline-none',
33
+ 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
34
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
35
+ 'motion-reduce:transition-none motion-reduce:duration-0',
36
+ ],
37
+ title: 'text-lg font-semibold leading-tight text-neutral-900 dark:text-neutral-50',
38
+ description: 'text-sm text-neutral-600 dark:text-neutral-400',
39
+ closeButton: [
40
+ 'absolute top-4 right-4 inline-flex items-center justify-center',
41
+ 'h-8 w-8 rounded-md text-neutral-500 hover:text-neutral-900',
42
+ 'dark:text-neutral-400 dark:hover:text-neutral-50',
43
+ 'hover:bg-neutral-100 dark:hover:bg-neutral-800',
44
+ 'focus:outline-none focus:ring-2 focus:ring-primary-500',
45
+ 'transition-colors',
46
+ ],
47
+ body: 'flex-1 overflow-y-auto',
48
+ actions: 'flex justify-end gap-2 pt-2',
49
+ },
50
+ variants: {
51
+ size: {
52
+ sm: { content: 'max-w-sm' },
53
+ md: { content: 'max-w-md' },
54
+ lg: { content: 'max-w-2xl' },
55
+ },
56
+ },
57
+ defaultVariants: {
58
+ size: 'md',
59
+ },
60
+ });
61
+
62
+ export type DialogVariants = VariantProps<typeof dialogVariants>;
@@ -0,0 +1,314 @@
1
+ // @vitest-environment jsdom
2
+ import * as React from 'react';
3
+ import { describe, it, expect, afterEach } from 'vitest';
4
+ import { render, screen, fireEvent, cleanup } from '@testing-library/react';
5
+ import { Pagination } from './Pagination.js';
6
+ import { computePageRange } from './pagination.helpers.js';
7
+
8
+ void React;
9
+ afterEach(() => cleanup());
10
+
11
+ describe('computePageRange — helper', () => {
12
+ it('returns empty for totalPages=0', () => {
13
+ expect(computePageRange(1, 0, 1, 1)).toEqual([]);
14
+ });
15
+
16
+ it('returns [1] for totalPages=1', () => {
17
+ expect(computePageRange(1, 1, 1, 1)).toEqual([1]);
18
+ });
19
+
20
+ it('lists all pages when totalPages <= window size', () => {
21
+ expect(computePageRange(1, 5, 1, 1)).toEqual([1, 2, 3, 4, 5]);
22
+ });
23
+
24
+ it('inserts ellipsis on the right when current is near start', () => {
25
+ const r = computePageRange(2, 20, 1, 1);
26
+ expect(r).toContain(1);
27
+ expect(r).toContain(2);
28
+ expect(r).toContain(3);
29
+ expect(r).toContain('ellipsis');
30
+ expect(r).toContain(20);
31
+ expect(r).not.toContain(10);
32
+ });
33
+
34
+ it('inserts ellipsis on both sides when current is in the middle', () => {
35
+ const r = computePageRange(10, 20, 1, 1);
36
+ expect(r.filter((x) => x === 'ellipsis').length).toBeGreaterThanOrEqual(1);
37
+ expect(r).toContain(1);
38
+ expect(r).toContain(9);
39
+ expect(r).toContain(10);
40
+ expect(r).toContain(11);
41
+ expect(r).toContain(20);
42
+ });
43
+
44
+ it('inserts ellipsis on the left when current is near end', () => {
45
+ const r = computePageRange(19, 20, 1, 1);
46
+ expect(r).toContain(1);
47
+ expect(r).toContain('ellipsis');
48
+ expect(r).toContain(18);
49
+ expect(r).toContain(19);
50
+ expect(r).toContain(20);
51
+ });
52
+
53
+ it('respects siblingCount=2', () => {
54
+ const r = computePageRange(10, 20, 2, 1);
55
+ expect(r).toContain(8);
56
+ expect(r).toContain(9);
57
+ expect(r).toContain(10);
58
+ expect(r).toContain(11);
59
+ expect(r).toContain(12);
60
+ });
61
+
62
+ it('respects boundaryCount=2', () => {
63
+ const r = computePageRange(10, 20, 1, 2);
64
+ expect(r).toContain(1);
65
+ expect(r).toContain(2);
66
+ expect(r).toContain(19);
67
+ expect(r).toContain(20);
68
+ });
69
+ });
70
+
71
+ describe('Pagination — rendering (default variant)', () => {
72
+ it('renders nav landmark with aria-label', () => {
73
+ render(<Pagination page={1} pageSize={10} totalCount={50} onPageChange={() => {}} />);
74
+ expect(screen.getByRole('navigation', { name: 'Pagination' })).not.toBeNull();
75
+ });
76
+
77
+ it('renders the "Showing X-Y of Z" summary', () => {
78
+ render(<Pagination page={1} pageSize={10} totalCount={50} onPageChange={() => {}} />);
79
+ expect(screen.getByText(/Showing 1-10 of 50/)).not.toBeNull();
80
+ });
81
+
82
+ it('renders correct summary on last page (partial fill)', () => {
83
+ render(<Pagination page={5} pageSize={10} totalCount={47} onPageChange={() => {}} />);
84
+ expect(screen.getByText(/Showing 41-47 of 47/)).not.toBeNull();
85
+ });
86
+
87
+ it('marks active page with aria-current=page', () => {
88
+ render(<Pagination page={3} pageSize={10} totalCount={50} onPageChange={() => {}} />);
89
+ const active = screen.getByRole('button', { name: 'Page 3' });
90
+ expect(active.getAttribute('aria-current')).toBe('page');
91
+ });
92
+
93
+ it('disables Prev + First on page 1', () => {
94
+ render(<Pagination page={1} pageSize={10} totalCount={50} onPageChange={() => {}} />);
95
+ expect((screen.getByRole('button', { name: 'Previous' }) as HTMLButtonElement).disabled).toBe(true);
96
+ expect((screen.getByRole('button', { name: 'First' }) as HTMLButtonElement).disabled).toBe(true);
97
+ });
98
+
99
+ it('disables Next + Last on last page', () => {
100
+ render(<Pagination page={5} pageSize={10} totalCount={50} onPageChange={() => {}} />);
101
+ expect((screen.getByRole('button', { name: 'Next' }) as HTMLButtonElement).disabled).toBe(true);
102
+ expect((screen.getByRole('button', { name: 'Last' }) as HTMLButtonElement).disabled).toBe(true);
103
+ });
104
+ });
105
+
106
+ describe('Pagination — interactions', () => {
107
+ it('emits onPageChange when a page button is clicked', () => {
108
+ let last = 0;
109
+ render(
110
+ <Pagination
111
+ page={1}
112
+ pageSize={10}
113
+ totalCount={50}
114
+ onPageChange={(p) => (last = p)}
115
+ />,
116
+ );
117
+ fireEvent.click(screen.getByRole('button', { name: 'Page 3' }));
118
+ expect(last).toBe(3);
119
+ });
120
+
121
+ it('Prev decrements page', () => {
122
+ let last = 0;
123
+ render(
124
+ <Pagination
125
+ page={3}
126
+ pageSize={10}
127
+ totalCount={50}
128
+ onPageChange={(p) => (last = p)}
129
+ />,
130
+ );
131
+ fireEvent.click(screen.getByRole('button', { name: 'Previous' }));
132
+ expect(last).toBe(2);
133
+ });
134
+
135
+ it('Next increments page', () => {
136
+ let last = 0;
137
+ render(
138
+ <Pagination
139
+ page={3}
140
+ pageSize={10}
141
+ totalCount={50}
142
+ onPageChange={(p) => (last = p)}
143
+ />,
144
+ );
145
+ fireEvent.click(screen.getByRole('button', { name: 'Next' }));
146
+ expect(last).toBe(4);
147
+ });
148
+
149
+ it('First/Last jump to extremes', () => {
150
+ let last = 0;
151
+ const { rerender } = render(
152
+ <Pagination
153
+ page={3}
154
+ pageSize={10}
155
+ totalCount={50}
156
+ onPageChange={(p) => (last = p)}
157
+ />,
158
+ );
159
+ fireEvent.click(screen.getByRole('button', { name: 'First' }));
160
+ expect(last).toBe(1);
161
+ rerender(
162
+ <Pagination
163
+ page={3}
164
+ pageSize={10}
165
+ totalCount={50}
166
+ onPageChange={(p) => (last = p)}
167
+ />,
168
+ );
169
+ fireEvent.click(screen.getByRole('button', { name: 'Last' }));
170
+ expect(last).toBe(5);
171
+ });
172
+
173
+ it('emits onPageSizeChange when selector changes', () => {
174
+ let lastSize = 0;
175
+ render(
176
+ <Pagination
177
+ page={1}
178
+ pageSize={10}
179
+ totalCount={100}
180
+ onPageChange={() => {}}
181
+ onPageSizeChange={(s) => (lastSize = s)}
182
+ />,
183
+ );
184
+ const selector = screen.getByLabelText('per page') as HTMLSelectElement;
185
+ fireEvent.change(selector, { target: { value: '50' } });
186
+ expect(lastSize).toBe(50);
187
+ });
188
+
189
+ it('hides page-size selector if onPageSizeChange not provided', () => {
190
+ render(<Pagination page={1} pageSize={10} totalCount={50} onPageChange={() => {}} />);
191
+ expect(screen.queryByLabelText('per page')).toBeNull();
192
+ });
193
+ });
194
+
195
+ describe('Pagination — variants', () => {
196
+ it('compact variant hides summary + selector', () => {
197
+ render(
198
+ <Pagination
199
+ variant="compact"
200
+ page={1}
201
+ pageSize={10}
202
+ totalCount={50}
203
+ onPageChange={() => {}}
204
+ onPageSizeChange={() => {}}
205
+ />,
206
+ );
207
+ expect(screen.queryByText(/Showing/)).toBeNull();
208
+ expect(screen.queryByLabelText('per page')).toBeNull();
209
+ });
210
+
211
+ it('minimal variant shows "Page X of Y" + only Prev/Next', () => {
212
+ render(
213
+ <Pagination
214
+ variant="minimal"
215
+ page={3}
216
+ pageSize={10}
217
+ totalCount={50}
218
+ onPageChange={() => {}}
219
+ />,
220
+ );
221
+ expect(screen.getByText(/Page 3 of 5/)).not.toBeNull();
222
+ // Page-number buttons should NOT exist
223
+ expect(screen.queryByRole('button', { name: 'Page 1' })).toBeNull();
224
+ // Prev / Next remain
225
+ expect(screen.queryByRole('button', { name: 'Previous' })).not.toBeNull();
226
+ expect(screen.queryByRole('button', { name: 'Next' })).not.toBeNull();
227
+ });
228
+ });
229
+
230
+ describe('Pagination — i18n', () => {
231
+ it('honors custom labels', () => {
232
+ render(
233
+ <Pagination
234
+ page={1}
235
+ pageSize={10}
236
+ totalCount={50}
237
+ onPageChange={() => {}}
238
+ labels={{ showing: 'Mostro', of: 'di', prev: 'Indietro', next: 'Avanti' }}
239
+ />,
240
+ );
241
+ expect(screen.getByText(/Mostro 1-10 di 50/)).not.toBeNull();
242
+ expect(screen.queryByRole('button', { name: 'Indietro' })).not.toBeNull();
243
+ expect(screen.queryByRole('button', { name: 'Avanti' })).not.toBeNull();
244
+ });
245
+ });
246
+
247
+ describe('Pagination — disabled', () => {
248
+ it('disables all buttons when disabled=true', () => {
249
+ render(
250
+ <Pagination
251
+ page={3}
252
+ pageSize={10}
253
+ totalCount={50}
254
+ onPageChange={() => {}}
255
+ disabled
256
+ />,
257
+ );
258
+ expect((screen.getByRole('button', { name: 'Previous' }) as HTMLButtonElement).disabled).toBe(true);
259
+ expect((screen.getByRole('button', { name: 'Next' }) as HTMLButtonElement).disabled).toBe(true);
260
+ expect((screen.getByRole('button', { name: 'Page 2' }) as HTMLButtonElement).disabled).toBe(true);
261
+ });
262
+ });
263
+
264
+ describe('Pagination — sx + slotProps', () => {
265
+ it('applies sx to root', () => {
266
+ const { container } = render(
267
+ <Pagination
268
+ page={1}
269
+ pageSize={10}
270
+ totalCount={50}
271
+ onPageChange={() => {}}
272
+ sx="my-custom-root"
273
+ />,
274
+ );
275
+ expect(container.querySelector('nav')!.className).toContain('my-custom-root');
276
+ });
277
+
278
+ it('applies slotProps.pageButton.className', () => {
279
+ render(
280
+ <Pagination
281
+ page={1}
282
+ pageSize={10}
283
+ totalCount={50}
284
+ onPageChange={() => {}}
285
+ slotProps={{ pageButton: { className: 'slot-page' } }}
286
+ />,
287
+ );
288
+ expect(screen.getByRole('button', { name: 'Page 2' }).className).toContain('slot-page');
289
+ });
290
+ });
291
+
292
+ describe('Pagination — edge cases', () => {
293
+ it('renders correctly with totalCount=0', () => {
294
+ render(<Pagination page={1} pageSize={10} totalCount={0} onPageChange={() => {}} />);
295
+ expect(screen.getByText(/Showing 0-0 of 0/)).not.toBeNull();
296
+ expect((screen.getByRole('button', { name: 'Previous' }) as HTMLButtonElement).disabled).toBe(true);
297
+ expect((screen.getByRole('button', { name: 'Next' }) as HTMLButtonElement).disabled).toBe(true);
298
+ });
299
+
300
+ it('clamps page > totalPages internally without crashing', () => {
301
+ let last = 0;
302
+ render(
303
+ <Pagination
304
+ page={99}
305
+ pageSize={10}
306
+ totalCount={50}
307
+ onPageChange={(p) => (last = p)}
308
+ />,
309
+ );
310
+ // Component computes safePage=5 (last valid). User clicks Prev → goes to 4.
311
+ fireEvent.click(screen.getByRole('button', { name: 'Previous' }));
312
+ expect(last).toBe(4);
313
+ });
314
+ });