@tpzdsp/next-toolkit 1.7.0 → 1.9.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.
@@ -1,40 +1,58 @@
1
1
  /* eslint-disable storybook/no-renderer-packages */
2
- import type { Meta, StoryFn } from '@storybook/react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
3
  import { fn } from '@storybook/test';
4
4
 
5
- import { Hint, type HintProps } from './Hint';
5
+ import { Hint } from './Hint';
6
6
  import { Button } from '../Button/Button';
7
7
 
8
- export default {
9
- children: 'Hint',
8
+ const meta: Meta<typeof Hint> = {
9
+ title: 'Components/Hint',
10
10
  component: Hint,
11
- } as Meta;
11
+ parameters: {
12
+ layout: 'padded',
13
+ },
14
+ tags: ['autodocs'],
15
+ argTypes: {
16
+ children: {
17
+ control: false,
18
+ description: 'The content to display for the hint',
19
+ },
20
+ className: {
21
+ control: 'text',
22
+ description: 'Additional TailwindCSS classes to apply to the hint container',
23
+ },
24
+ },
25
+ args: {
26
+ children: 'Hint message',
27
+ },
28
+ };
12
29
 
13
- const Template: StoryFn<HintProps> = (args) => <Hint {...args} />;
30
+ export default meta;
14
31
 
15
- export const DefaultHint = Template.bind({});
16
- DefaultHint.args = {
17
- children: 'Hint message',
18
- };
32
+ type Story = StoryObj<typeof Hint>;
33
+
34
+ export const Default: Story = {};
19
35
 
20
- export const CustomStyling = Template.bind({});
21
- CustomStyling.args = {
22
- className: 'text-3xl',
23
- children: 'Error message with extra large text',
36
+ export const CustomStyling: Story = {
37
+ args: {
38
+ className: 'text-3xl',
39
+ children: 'Hint message with extra large text',
40
+ },
24
41
  };
25
42
 
26
- export const ComplexChildren = Template.bind({});
27
- ComplexChildren.args = {
28
- children: (
29
- <div>
30
- Hint message with link and button{' '}
31
- <a className="underline text-link" href="/">
32
- Link
33
- </a>
34
- {/* // eslint-disable-next-line custom/padding-between-jsx-elements */}
35
- <Button variant="primary" onClick={fn()}>
36
- Click
37
- </Button>
38
- </div>
39
- ),
43
+ export const ComplexChildren: Story = {
44
+ args: {
45
+ children: (
46
+ <div>
47
+ Hint message with link and button{' '}
48
+ <a className="underline text-link" href="/">
49
+ Link
50
+ </a>
51
+ {}
52
+ <Button variant="primary" onClick={fn()}>
53
+ Click
54
+ </Button>
55
+ </div>
56
+ ),
57
+ },
40
58
  };
@@ -1,30 +1,74 @@
1
1
  /* eslint-disable storybook/no-renderer-packages */
2
- import type { Meta, StoryFn } from '@storybook/react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
3
 
4
- import { Paragraph, type ParagraphProps } from './Paragraph';
4
+ import { Paragraph } from './Paragraph';
5
+ import { ExternalLink } from '../link/ExternalLink';
5
6
 
6
- export default {
7
- children: 'Paragraph',
7
+ const meta: Meta<typeof Paragraph> = {
8
+ title: 'Components/Paragraph',
8
9
  component: Paragraph,
9
- } as Meta;
10
+ parameters: {
11
+ layout: 'padded',
12
+ },
13
+ tags: ['autodocs'],
14
+ argTypes: {
15
+ children: {
16
+ description: 'Content of the paragraph',
17
+ control: false,
18
+ },
19
+ className: {
20
+ description: 'Additional TailwindCSS classes to apply to the paragraph',
21
+ control: 'text',
22
+ },
23
+ },
24
+ args: {
25
+ children: 'Hello, this is some simple text',
26
+ },
27
+ };
28
+
29
+ export default meta;
30
+
31
+ type Story = StoryObj<typeof Paragraph>;
32
+
33
+ export const Default: Story = {};
10
34
 
11
- const Template: StoryFn<ParagraphProps> = (args) => <Paragraph {...args} />;
35
+ export const CustomStyling: Story = {
36
+ args: {
37
+ className: 'text-yellow-800 p-2',
38
+ children: 'Hello, this is some simple text with custom styling',
39
+ },
40
+ };
41
+
42
+ export const LongText: Story = {
43
+ args: {
44
+ children:
45
+ 'This is a very long paragraph. '.repeat(20) +
46
+ 'It is used to test how the Paragraph component handles large amounts of text.',
47
+ },
48
+ };
12
49
 
13
- export const JustText = Template.bind({});
14
- JustText.args = {
15
- children: 'Hello, this is some simple text',
50
+ export const WithLink: Story = {
51
+ args: {
52
+ children: (
53
+ <span>
54
+ This paragraph contains a <ExternalLink href="https://storybook.js.org/">link</ExternalLink>
55
+ .
56
+ </span>
57
+ ),
58
+ },
16
59
  };
17
60
 
18
- export const ImageAndText = Template.bind({});
19
- ImageAndText.args = {
20
- children: (
21
- <div>
22
- <img
23
- src="https://images.unsplash.com/photo-1563991655280-cb95c90ca2fb?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8bm8lMjBjb3B5cmlnaHR8ZW58MHx8MHx8fDA%3D"
24
- alt="Card"
25
- />
26
-
27
- <p>This is text about the image</p>
28
- </div>
29
- ),
61
+ export const ImageAndText: Story = {
62
+ args: {
63
+ children: (
64
+ <div>
65
+ <img
66
+ src="https://images.unsplash.com/photo-1563991655280-cb95c90ca2fb?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8bm8lMjBjb3B5cmlnaHR8ZW58MHx8MHx8fDA%3D"
67
+ alt="Card"
68
+ />
69
+
70
+ <p>This is text about the image</p>
71
+ </div>
72
+ ),
73
+ },
30
74
  };
@@ -1,10 +1,12 @@
1
+ import { twMerge } from 'tailwind-merge';
2
+
1
3
  import type { ExtendProps } from '../../types/utils';
2
4
 
3
5
  export type ParagraphProps = ExtendProps<'p'>;
4
6
 
5
7
  export const Paragraph = ({ className, children, ...props }: ParagraphProps) => {
6
8
  return (
7
- <p className={`pb-4 text-sm text-text-primary ${className}`} {...props}>
9
+ <p className={twMerge('pb-4 text-sm text-text-primary', className)} {...props}>
8
10
  {children}
9
11
  </p>
10
12
  );
@@ -29,3 +29,102 @@ export const AllSlidingPanels: StoryObj<typeof SlidingPanel> = {
29
29
  </div>
30
30
  ),
31
31
  };
32
+
33
+ export const WithAutoFocus: StoryObj<typeof SlidingPanel> = {
34
+ render: () => (
35
+ <SlidingPanel tabLabel="Auto Focus Demo" position="center-left">
36
+ <div>
37
+ <h3>Panel with Auto Focus</h3>
38
+
39
+ <p>The input below should be focused when the panel opens:</p>
40
+
41
+ <input data-autofocus placeholder="I will be focused automatically" />
42
+
43
+ <button type="button">Button</button>
44
+ </div>
45
+ </SlidingPanel>
46
+ ),
47
+ };
48
+
49
+ export const DefaultOpen: StoryObj<typeof SlidingPanel> = {
50
+ render: () => (
51
+ <SlidingPanel tabLabel="Always Open" position="center-right" defaultOpen>
52
+ <div>
53
+ <h3>Default Open Panel</h3>
54
+
55
+ <p>This panel starts open by default.</p>
56
+
57
+ <button type="button">Button 1</button>
58
+
59
+ <button type="button">Button 2</button>
60
+
61
+ <input placeholder="Input field" />
62
+ </div>
63
+ </SlidingPanel>
64
+ ),
65
+ };
66
+
67
+ export const FormExample: StoryObj<typeof SlidingPanel> = {
68
+ render: () => (
69
+ <SlidingPanel tabLabel="Contact Form" position="center-top">
70
+ <div>
71
+ <h3>Contact Form</h3>
72
+
73
+ <form>
74
+ <div style={{ marginBottom: '1rem' }}>
75
+ <label htmlFor="name">Name:</label>
76
+
77
+ <input id="name" type="text" placeholder="Your name" data-autofocus />
78
+ </div>
79
+
80
+ <div style={{ marginBottom: '1rem' }}>
81
+ <label htmlFor="email">Email:</label>
82
+
83
+ <input id="email" type="email" placeholder="your@email.com" />
84
+ </div>
85
+
86
+ <div style={{ marginBottom: '1rem' }}>
87
+ <label htmlFor="message">Message:</label>
88
+
89
+ <textarea id="message" placeholder="Your message"></textarea>
90
+ </div>
91
+
92
+ <button type="submit">Send Message</button>
93
+
94
+ <button type="button">Cancel</button>
95
+ </form>
96
+ </div>
97
+ </SlidingPanel>
98
+ ),
99
+ };
100
+
101
+ export const NoFocusableElements: StoryObj<typeof SlidingPanel> = {
102
+ render: () => (
103
+ <SlidingPanel tabLabel="No Focus Elements" position="center-left">
104
+ <div>
105
+ <h3>Panel Without Focusable Elements</h3>
106
+
107
+ <p>This panel only contains text and headings.</p>
108
+
109
+ <p>
110
+ It should still work properly with focus trapping, even though there are no buttons,
111
+ inputs, or other interactive elements.
112
+ </p>
113
+
114
+ <div>
115
+ <strong>Key Features:</strong>
116
+ </div>
117
+
118
+ <ul>
119
+ <li>Opens and closes properly</li>
120
+
121
+ <li>Focus trap works correctly</li>
122
+
123
+ <li>Escape key closes the panel</li>
124
+
125
+ <li>Focus returns to trigger button</li>
126
+ </ul>
127
+ </div>
128
+ </SlidingPanel>
129
+ ),
130
+ };
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, type ReactNode, useRef, useMemo, useCallback } from 'react';
3
+ import { useState, useEffect, type ReactNode, useRef, useMemo, useId } from 'react';
4
4
 
5
- import { KeyboardKeys } from '../../utils';
5
+ import { FocusTrap } from 'focus-trap-react';
6
6
 
7
7
  type Position = 'center-left' | 'center-right' | 'center-top' | 'center-bottom';
8
8
 
@@ -13,134 +13,69 @@ export type SlidingPanelProps = {
13
13
  defaultOpen?: boolean;
14
14
  };
15
15
 
16
- const TRAP_SELECTORS =
17
- 'a[href]:not([aria-hidden="true"]):not([hidden]), button:not([disabled]):not([aria-hidden="true"]):not([hidden]), input:not([disabled]):not([aria-hidden="true"]):not([hidden]), textarea:not([disabled]):not([aria-hidden="true"]):not([hidden]), select:not([disabled]):not([aria-hidden="true"]):not([hidden]), [tabindex]:not([tabindex="-1"]):not([aria-hidden="true"]):not([hidden])';
18
-
19
16
  export const SlidingPanel = ({
20
17
  children,
21
18
  tabLabel = 'Open',
22
19
  position = 'center-left',
23
20
  defaultOpen = false,
24
21
  }: SlidingPanelProps) => {
22
+ const id = useId();
23
+
25
24
  const [isVisible, setIsVisible] = useState(defaultOpen);
25
+ const [isTrapActive, setIsTrapActive] = useState(defaultOpen);
26
26
  const [panelDimensions, setPanelDimensions] = useState({ width: 0, height: 0 });
27
- const panelRef = useRef<HTMLDivElement>(null);
28
- const triggerRef = useRef<HTMLElement | null>(null); // store previously focused element
29
27
 
30
- const openPanel = () => {
31
- triggerRef.current = document.activeElement as HTMLElement;
32
- setIsVisible(true);
33
- };
28
+ const panelRef = useRef<HTMLDivElement>(null);
29
+ const buttonRef = useRef<HTMLButtonElement | null>(null);
34
30
 
35
- const closePanel = useCallback(() => {
36
- setIsVisible(false);
37
- }, []);
31
+ // Required for the `escapeDeactivates` function as these options are an object and only
32
+ // capture their environment once, so without a ref, `isVisible` would only ever be
33
+ // `false` inside the callback, even if it's change. A `useCallback` doesn't help because
34
+ // it creates a new function each time, so the event would still only capture the first created function.
35
+ const isVisibleRef = useRef(isVisible);
36
+ // tracks if the focus trap was deactivated by clicking outside the trap
37
+ const deactivatedByClick = useRef(false);
38
38
 
39
- // Measure panel dimensions when visible
40
39
  useEffect(() => {
41
- if (isVisible && panelRef.current) {
42
- const updateDimensions = () => {
43
- if (panelRef.current) {
44
- const rect = panelRef.current.getBoundingClientRect();
45
-
46
- const newDimensions = { width: rect.width, height: rect.height };
47
-
48
- if (
49
- newDimensions.width !== panelDimensions.width ||
50
- newDimensions.height !== panelDimensions.height
51
- ) {
52
- setPanelDimensions(newDimensions);
53
- }
54
- }
55
- };
56
-
57
- // Initial measurement
58
- updateDimensions();
40
+ isVisibleRef.current = isVisible;
59
41
 
60
- // Use ResizeObserver to detect changes
61
- const resizeObserver = new ResizeObserver(updateDimensions);
42
+ deactivatedByClick.current = false;
62
43
 
63
- resizeObserver.observe(panelRef.current);
64
-
65
- return () => resizeObserver.disconnect();
66
- }
67
- }, [isVisible, panelDimensions.height, panelDimensions.width]);
68
-
69
- // move focus into panel when opened, restore focus to correct element when closed
70
- useEffect(() => {
71
- if (isVisible && panelRef.current) {
72
- const els = Array.from(panelRef.current.querySelectorAll<HTMLElement>(TRAP_SELECTORS));
73
-
74
- if (els.length > 0) {
75
- els[0].focus();
76
- } else {
77
- panelRef.current.focus();
78
- }
79
- } else if (!isVisible && triggerRef.current) {
80
- triggerRef.current.focus();
81
- }
44
+ setIsTrapActive(isVisible);
82
45
  }, [isVisible]);
83
46
 
84
- // trap focus while open
47
+ // Measure panel dimensions when visible or when panel content changes
85
48
  useEffect(() => {
86
- if (!isVisible || !panelRef.current) {
49
+ if (!panelRef.current) {
87
50
  return;
88
51
  }
89
52
 
90
- const panel = panelRef.current;
91
-
92
- const moveFocus = (direction: 'next' | 'prev') => {
93
- const els = Array.from(panel.querySelectorAll<HTMLElement>(TRAP_SELECTORS));
94
-
95
- if (!els.length) {
96
- panel.focus();
97
-
98
- return true;
99
- }
100
-
101
- const first = els[0];
102
- const last = els[els.length - 1];
103
- const active = document.activeElement;
104
-
105
- if (direction === 'next' && active === last) {
106
- first.focus();
107
-
108
- return true;
109
- }
110
-
111
- if (direction === 'prev' && active === first) {
112
- last.focus();
53
+ const updateDimensions = () => {
54
+ if (panelRef.current) {
55
+ const rect = panelRef.current.getBoundingClientRect();
56
+ const newDimensions = { width: rect.width, height: rect.height };
113
57
 
114
- return true;
58
+ if (
59
+ newDimensions.width !== panelDimensions.width ||
60
+ newDimensions.height !== panelDimensions.height
61
+ ) {
62
+ setPanelDimensions(newDimensions);
63
+ }
115
64
  }
116
-
117
- return false;
118
65
  };
119
66
 
120
- const handleKey = (event: KeyboardEvent) => {
121
- if (event.key === KeyboardKeys.Escape) {
122
- event.preventDefault();
123
- closePanel();
124
-
125
- return;
126
- }
127
-
128
- if (event.key !== KeyboardKeys.Tab) {
129
- return;
130
- }
67
+ // Initial measurement
68
+ updateDimensions();
131
69
 
132
- // only if focus was moved do we prevent the default behaviour
133
- if (moveFocus(event.shiftKey ? 'prev' : 'next')) {
134
- event.preventDefault();
135
- }
136
- };
70
+ // Use ResizeObserver to detect content changes
71
+ const resizeObserver = new ResizeObserver(updateDimensions);
137
72
 
138
- panel.addEventListener('keydown', handleKey);
73
+ resizeObserver.observe(panelRef.current);
139
74
 
140
75
  return () => {
141
- panel.removeEventListener('keydown', handleKey);
76
+ resizeObserver.disconnect();
142
77
  };
143
- }, [closePanel, isVisible]);
78
+ }, [isVisible, panelDimensions.height, panelDimensions.width]);
144
79
 
145
80
  const panelBase =
146
81
  'absolute bg-white shadow-lg p-4 flex flex-col transition-transform duration-300 ease-in-out overflow-auto z-30';
@@ -201,6 +136,14 @@ export const SlidingPanel = ({
201
136
  return {};
202
137
  }, [isVisible, panelDimensions.height, panelDimensions.width, position]);
203
138
 
139
+ const openPanel = () => {
140
+ setIsVisible(true);
141
+ };
142
+
143
+ const closePanel = () => {
144
+ setIsVisible(false);
145
+ };
146
+
204
147
  return (
205
148
  <div className="absolute inset-0 z-30 overflow-hidden pointer-events-none sliding-panel">
206
149
  <button
@@ -208,21 +151,87 @@ export const SlidingPanel = ({
208
151
  style={getButtonStyle}
209
152
  onClick={() => (isVisible ? closePanel() : openPanel())}
210
153
  aria-expanded={isVisible}
211
- aria-controls="sliding-panel"
154
+ aria-controls={id}
155
+ ref={buttonRef}
156
+ type="button"
212
157
  >
213
158
  {isVisible ? `Close ${tabLabel}` : `Open ${tabLabel}`}
214
159
  </button>
215
160
 
216
- <div
217
- ref={panelRef}
218
- tabIndex={-1}
219
- id="sliding-panel"
220
- className={`${panelBase} ${panelLayout} pointer-events-auto`}
221
- aria-hidden={!isVisible}
222
- inert={!isVisible}
161
+ <FocusTrap
162
+ active={isTrapActive}
163
+ focusTrapOptions={{
164
+ escapeDeactivates: () => {
165
+ deactivatedByClick.current = false;
166
+
167
+ if (isVisibleRef.current) {
168
+ closePanel();
169
+ }
170
+
171
+ return false;
172
+ },
173
+ clickOutsideDeactivates: () => {
174
+ deactivatedByClick.current = true; // mark as mouse-driven
175
+
176
+ return true; // still allow it to deactivate
177
+ },
178
+ // eslint-disable-next-line sonarjs/function-return-type
179
+ setReturnFocus: (target) => {
180
+ // if the user has clicked outside the focus trap we shouldn't return focus as it will steal focus from the element they clicked on
181
+ return deactivatedByClick.current ? false : target;
182
+ },
183
+ onDeactivate: () => {
184
+ setIsTrapActive(false);
185
+ },
186
+ onActivate: () => {
187
+ setIsTrapActive(true);
188
+ },
189
+ /**
190
+ * Emergency fallback focus function for FocusTrap.
191
+ *
192
+ * This function is automatically called by FocusTrap whenever it cannot find
193
+ * any tabbable elements within the panel. This commonly occurs in scenarios like:
194
+ *
195
+ * - Panel opens but async content hasn't loaded yet
196
+ * - Panel is closing and content is being unmounted
197
+ * - User presses Escape and TableFilters content disappears
198
+ * - Dynamic content changes temporarily remove all focusable elements
199
+ *
200
+ * Without this fallback, FocusTrap throws the error:
201
+ * "Your focus-trap must have at least one container with at least one tabbable node in it at all times"
202
+ *
203
+ * The function ensures there's always a focusable element available by:
204
+ * 1. Looking for our guaranteed hidden fallback element with [data-fallback-focus]
205
+ * 2. Making it temporarily focusable (tabIndex = 0)
206
+ * 3. Returning it to FocusTrap as a safe focus target
207
+ * 4. Using document.body as ultimate fallback if something goes wrong
208
+ *
209
+ * This is especially crucial for complex content like ObservationTableManager
210
+ * where TableFilters has async operations that can cause content to disappear
211
+ * during panel lifecycle events.
212
+ *
213
+ * @returns HTMLElement - The element FocusTrap should focus on as a fallback
214
+ */
215
+ fallbackFocus: () => {
216
+ return panelRef.current ?? buttonRef.current ?? document.body;
217
+ },
218
+ }}
223
219
  >
224
- <div className="mt-4">{children}</div>
225
- </div>
220
+ <div
221
+ ref={panelRef}
222
+ tabIndex={-1}
223
+ id={id}
224
+ role="dialog"
225
+ aria-modal="true"
226
+ aria-label={`Sliding panel for ${tabLabel}`}
227
+ className={`${panelBase} ${panelLayout} pointer-events-auto`}
228
+ aria-hidden={!isVisible}
229
+ inert={!isVisible}
230
+ onPointerDown={() => (!isTrapActive ? setIsTrapActive(true) : void 0)}
231
+ >
232
+ <div className="mt-4">{children}</div>
233
+ </div>
234
+ </FocusTrap>
226
235
  </div>
227
236
  );
228
237
  };
@@ -1,40 +1,62 @@
1
+ /* eslint-disable storybook/no-renderer-packages */
1
2
  import { AiFillChrome } from 'react-icons/ai';
2
3
 
3
- /* eslint-disable storybook/no-renderer-packages */
4
- import type { Meta, StoryFn } from '@storybook/react';
4
+ import type { Meta, StoryObj } from '@storybook/react';
5
5
 
6
- import { Chip, type ChipProps } from './Chip';
6
+ import { Chip } from './Chip';
7
7
  import { Paragraph } from '../Paragraph/Paragraph';
8
8
 
9
- export default {
10
- children: 'Chip',
9
+ const meta: Meta<typeof Chip> = {
10
+ title: 'Components/Chip',
11
11
  component: Chip,
12
- } as Meta;
12
+ parameters: {
13
+ layout: 'padded',
14
+ },
15
+ tags: ['autodocs'],
16
+ argTypes: {
17
+ children: {
18
+ description: 'Content of the chip',
19
+ control: false,
20
+ },
21
+ className: {
22
+ description: 'Additional TailwindCSS classes to apply',
23
+ control: 'text',
24
+ },
25
+ },
26
+ args: {
27
+ children: 'Hello, this is some simple Chip',
28
+ },
29
+ };
13
30
 
14
- const Template: StoryFn<ChipProps> = (args) => <Chip {...args} />;
31
+ export default meta;
15
32
 
16
- export const Default = Template.bind({});
17
- Default.args = {
18
- children: 'Chip',
19
- };
33
+ type Story = StoryObj<typeof Chip>;
20
34
 
21
- export const JustText = Template.bind({});
22
- JustText.args = {
23
- children: 'Hello, this is some simple text',
24
- };
35
+ export const Default: Story = {};
25
36
 
26
- export const ParagraphOfText = Template.bind({});
27
- ParagraphOfText.args = {
28
- children: <Paragraph className="pb-0">Hello, this is a paragraph of text</Paragraph>,
37
+ export const ParagraphOfText: Story = {
38
+ args: {
39
+ children: <Paragraph className="pb-0">Hello, this is a paragraph of text</Paragraph>,
40
+ },
29
41
  };
30
42
 
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" />
43
+ export const TextWithIcon: Story = {
44
+ args: {
45
+ children: (
46
+ <div className="flex items-center justify-center gap-2">
47
+ <AiFillChrome className="text-base" />
48
+
49
+ <Paragraph className="pb-0">
50
+ Hello, this is a Chip container with a paragraph of text
51
+ </Paragraph>
52
+ </div>
53
+ ),
54
+ },
55
+ };
36
56
 
37
- <Paragraph className="pb-0">Hello, this is a paragraph of text</Paragraph>
38
- </div>
39
- ),
57
+ export const CustomStyling: Story = {
58
+ args: {
59
+ className: 'bg-blue-100 text-blue-800 px-4 py-2 rounded-full',
60
+ children: 'Custom styled chip',
61
+ },
40
62
  };