@papernote/ui 1.3.1 → 1.5.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 (100) hide show
  1. package/dist/components/BottomNavigation.d.ts +98 -0
  2. package/dist/components/BottomNavigation.d.ts.map +1 -0
  3. package/dist/components/Checkbox.d.ts +2 -0
  4. package/dist/components/Checkbox.d.ts.map +1 -1
  5. package/dist/components/CheckboxList.d.ts +81 -0
  6. package/dist/components/CheckboxList.d.ts.map +1 -0
  7. package/dist/components/Chip.d.ts +92 -1
  8. package/dist/components/Chip.d.ts.map +1 -1
  9. package/dist/components/ConfirmDialog.d.ts +43 -1
  10. package/dist/components/ConfirmDialog.d.ts.map +1 -1
  11. package/dist/components/DataTable.d.ts +10 -1
  12. package/dist/components/DataTable.d.ts.map +1 -1
  13. package/dist/components/DataTableCardView.d.ts +99 -0
  14. package/dist/components/DataTableCardView.d.ts.map +1 -0
  15. package/dist/components/ExpandablePanel.d.ts +142 -0
  16. package/dist/components/ExpandablePanel.d.ts.map +1 -0
  17. package/dist/components/FloatingActionButton.d.ts +98 -0
  18. package/dist/components/FloatingActionButton.d.ts.map +1 -0
  19. package/dist/components/Input.d.ts +45 -1
  20. package/dist/components/Input.d.ts.map +1 -1
  21. package/dist/components/MobileHeader.d.ts +98 -0
  22. package/dist/components/MobileHeader.d.ts.map +1 -0
  23. package/dist/components/MobileLayout.d.ts +121 -0
  24. package/dist/components/MobileLayout.d.ts.map +1 -0
  25. package/dist/components/Modal.d.ts +50 -1
  26. package/dist/components/Modal.d.ts.map +1 -1
  27. package/dist/components/PullToRefresh.d.ts +87 -0
  28. package/dist/components/PullToRefresh.d.ts.map +1 -0
  29. package/dist/components/QueryTransparency.d.ts +1 -1
  30. package/dist/components/QueryTransparency.d.ts.map +1 -1
  31. package/dist/components/SearchableList.d.ts +83 -0
  32. package/dist/components/SearchableList.d.ts.map +1 -0
  33. package/dist/components/Select.d.ts +16 -2
  34. package/dist/components/Select.d.ts.map +1 -1
  35. package/dist/components/Sidebar.d.ts +40 -1
  36. package/dist/components/Sidebar.d.ts.map +1 -1
  37. package/dist/components/SwipeActions.d.ts +93 -0
  38. package/dist/components/SwipeActions.d.ts.map +1 -0
  39. package/dist/components/Switch.d.ts +1 -0
  40. package/dist/components/Switch.d.ts.map +1 -1
  41. package/dist/components/Textarea.d.ts +13 -0
  42. package/dist/components/Textarea.d.ts.map +1 -1
  43. package/dist/components/index.d.ts +27 -3
  44. package/dist/components/index.d.ts.map +1 -1
  45. package/dist/context/MobileContext.d.ts +168 -0
  46. package/dist/context/MobileContext.d.ts.map +1 -0
  47. package/dist/hooks/useResponsive.d.ts +158 -0
  48. package/dist/hooks/useResponsive.d.ts.map +1 -0
  49. package/dist/index.d.ts +1653 -56
  50. package/dist/index.esm.js +2832 -194
  51. package/dist/index.esm.js.map +1 -1
  52. package/dist/index.js +2865 -192
  53. package/dist/index.js.map +1 -1
  54. package/dist/styles.css +404 -1
  55. package/dist/types/index.d.ts +2 -0
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/package.json +1 -1
  58. package/src/components/BottomNavigation.stories.tsx +142 -0
  59. package/src/components/BottomNavigation.tsx +225 -0
  60. package/src/components/Checkbox.stories.tsx +162 -0
  61. package/src/components/Checkbox.tsx +22 -6
  62. package/src/components/CheckboxList.stories.tsx +311 -0
  63. package/src/components/CheckboxList.tsx +433 -0
  64. package/src/components/Chip.stories.tsx +389 -0
  65. package/src/components/Chip.tsx +182 -3
  66. package/src/components/ConfirmDialog.tsx +56 -4
  67. package/src/components/DataTable.tsx +60 -1
  68. package/src/components/DataTableCardView.stories.tsx +307 -0
  69. package/src/components/DataTableCardView.tsx +419 -0
  70. package/src/components/ExpandablePanel.stories.tsx +620 -0
  71. package/src/components/ExpandablePanel.tsx +383 -0
  72. package/src/components/FloatingActionButton.stories.tsx +197 -0
  73. package/src/components/FloatingActionButton.tsx +301 -0
  74. package/src/components/Grid.stories.tsx +16 -16
  75. package/src/components/Input.stories.tsx +214 -0
  76. package/src/components/Input.tsx +81 -4
  77. package/src/components/MobileHeader.stories.tsx +205 -0
  78. package/src/components/MobileHeader.tsx +233 -0
  79. package/src/components/MobileLayout.stories.tsx +338 -0
  80. package/src/components/MobileLayout.tsx +313 -0
  81. package/src/components/Modal.stories.tsx +183 -0
  82. package/src/components/Modal.tsx +84 -3
  83. package/src/components/PullToRefresh.stories.tsx +321 -0
  84. package/src/components/PullToRefresh.tsx +294 -0
  85. package/src/components/QueryTransparency.tsx +1 -1
  86. package/src/components/SearchableList.stories.tsx +437 -0
  87. package/src/components/SearchableList.tsx +326 -0
  88. package/src/components/Select.stories.tsx +190 -0
  89. package/src/components/Select.tsx +353 -137
  90. package/src/components/Sidebar.tsx +191 -8
  91. package/src/components/SwipeActions.stories.tsx +327 -0
  92. package/src/components/SwipeActions.tsx +387 -0
  93. package/src/components/Switch.stories.tsx +158 -0
  94. package/src/components/Switch.tsx +12 -3
  95. package/src/components/Textarea.tsx +31 -1
  96. package/src/components/index.ts +63 -3
  97. package/src/context/MobileContext.tsx +296 -0
  98. package/src/hooks/useResponsive.ts +360 -0
  99. package/src/types/index.ts +4 -0
  100. package/tailwind.config.js +56 -1
@@ -0,0 +1,389 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import Chip, { ChipGroup } from './Chip';
4
+ import { Star, Tag, User, X, Check } from 'lucide-react';
5
+
6
+ const meta = {
7
+ title: 'Components/Chip',
8
+ component: Chip,
9
+ parameters: {
10
+ layout: 'centered',
11
+ docs: {
12
+ description: {
13
+ component: `
14
+ Compact element for displaying values with optional remove functionality. Commonly used for tags, selected items, and filters.
15
+
16
+ ## Features
17
+ - **Variants**: primary, secondary, success, warning, error, info
18
+ - **Sizes**: sm, md, lg
19
+ - **Removable**: Optional close button with onClose callback
20
+ - **Clickable**: Optional onClick for interactive chips
21
+ - **Selected State**: Visual highlight for selected chips
22
+ - **Icons**: Optional leading icon
23
+ - **Max Width**: Truncate long text with ellipsis
24
+
25
+ ## ChipGroup
26
+ Container component for multiple chips with layout and selection support.
27
+
28
+ ## Usage
29
+
30
+ \`\`\`tsx
31
+ import { Chip, ChipGroup } from 'notebook-ui';
32
+
33
+ // Basic chip
34
+ <Chip>Tag Name</Chip>
35
+
36
+ // Removable chip
37
+ <Chip onClose={() => removeTag(tag)}>{tag.name}</Chip>
38
+
39
+ // Chip group with selection
40
+ <ChipGroup
41
+ selectionMode="multiple"
42
+ selectedKeys={selected}
43
+ onSelectionChange={setSelected}
44
+ >
45
+ <Chip chipKey="a">Option A</Chip>
46
+ <Chip chipKey="b">Option B</Chip>
47
+ </ChipGroup>
48
+ \`\`\`
49
+ `,
50
+ },
51
+ },
52
+ },
53
+ tags: ['autodocs'],
54
+ argTypes: {
55
+ variant: {
56
+ control: 'select',
57
+ options: ['primary', 'secondary', 'success', 'warning', 'error', 'info'],
58
+ description: 'Color variant',
59
+ },
60
+ size: {
61
+ control: 'select',
62
+ options: ['sm', 'md', 'lg'],
63
+ description: 'Size variant',
64
+ },
65
+ disabled: {
66
+ control: 'boolean',
67
+ description: 'Disable the chip',
68
+ },
69
+ selected: {
70
+ control: 'boolean',
71
+ description: 'Selected state',
72
+ },
73
+ },
74
+ } satisfies Meta<typeof Chip>;
75
+
76
+ export default meta;
77
+ type Story = StoryObj<typeof meta>;
78
+
79
+ export const Default: Story = {
80
+ args: {
81
+ children: 'Default Chip',
82
+ },
83
+ };
84
+
85
+ export const Variants: Story = {
86
+ render: () => (
87
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
88
+ <Chip variant="primary">Primary</Chip>
89
+ <Chip variant="secondary">Secondary</Chip>
90
+ <Chip variant="success">Success</Chip>
91
+ <Chip variant="warning">Warning</Chip>
92
+ <Chip variant="error">Error</Chip>
93
+ <Chip variant="info">Info</Chip>
94
+ </div>
95
+ ),
96
+ };
97
+
98
+ export const Sizes: Story = {
99
+ render: () => (
100
+ <div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
101
+ <Chip size="sm">Small</Chip>
102
+ <Chip size="md">Medium</Chip>
103
+ <Chip size="lg">Large</Chip>
104
+ </div>
105
+ ),
106
+ };
107
+
108
+ export const WithIcon: Story = {
109
+ render: () => (
110
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
111
+ <Chip icon={<Star className="h-3 w-3" />} variant="warning">Featured</Chip>
112
+ <Chip icon={<Tag className="h-3 w-3" />} variant="info">Tagged</Chip>
113
+ <Chip icon={<User className="h-3 w-3" />} variant="primary">Admin</Chip>
114
+ </div>
115
+ ),
116
+ };
117
+
118
+ export const Removable: Story = {
119
+ render: () => {
120
+ const [tags, setTags] = useState(['React', 'TypeScript', 'Tailwind', 'Storybook']);
121
+
122
+ return (
123
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
124
+ {tags.map(tag => (
125
+ <Chip
126
+ key={tag}
127
+ onClose={() => setTags(tags.filter(t => t !== tag))}
128
+ >
129
+ {tag}
130
+ </Chip>
131
+ ))}
132
+ {tags.length === 0 && (
133
+ <span style={{ color: '#666', fontSize: '0.875rem' }}>All tags removed</span>
134
+ )}
135
+ </div>
136
+ );
137
+ },
138
+ };
139
+
140
+ export const Clickable: Story = {
141
+ render: () => {
142
+ const [clicked, setClicked] = useState<string | null>(null);
143
+
144
+ return (
145
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
146
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
147
+ <Chip onClick={() => setClicked('Option A')}>Option A</Chip>
148
+ <Chip onClick={() => setClicked('Option B')}>Option B</Chip>
149
+ <Chip onClick={() => setClicked('Option C')}>Option C</Chip>
150
+ </div>
151
+ <div style={{ fontSize: '0.875rem', color: '#666' }}>
152
+ Last clicked: {clicked || 'None'}
153
+ </div>
154
+ </div>
155
+ );
156
+ },
157
+ };
158
+
159
+ export const Selected: Story = {
160
+ render: () => (
161
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
162
+ <Chip selected variant="primary">Selected Primary</Chip>
163
+ <Chip selected variant="success">Selected Success</Chip>
164
+ <Chip selected variant="info">Selected Info</Chip>
165
+ <Chip variant="primary">Not Selected</Chip>
166
+ </div>
167
+ ),
168
+ };
169
+
170
+ export const Disabled: Story = {
171
+ render: () => (
172
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
173
+ <Chip disabled>Disabled</Chip>
174
+ <Chip disabled onClose={() => {}}>Disabled Removable</Chip>
175
+ <Chip disabled onClick={() => {}}>Disabled Clickable</Chip>
176
+ </div>
177
+ ),
178
+ };
179
+
180
+ export const WithMaxWidth: Story = {
181
+ render: () => (
182
+ <div style={{ display: 'flex', gap: '0.5rem', maxWidth: '300px' }}>
183
+ <Chip maxWidth={100}>Short</Chip>
184
+ <Chip maxWidth={100}>This is a very long chip label that will truncate</Chip>
185
+ <Chip maxWidth={150} onClose={() => {}}>Another long label here</Chip>
186
+ </div>
187
+ ),
188
+ };
189
+
190
+ // ChipGroup Stories
191
+ export const BasicGroup: Story = {
192
+ render: () => (
193
+ <ChipGroup gap="sm">
194
+ <Chip>Tag 1</Chip>
195
+ <Chip>Tag 2</Chip>
196
+ <Chip>Tag 3</Chip>
197
+ <Chip>Tag 4</Chip>
198
+ </ChipGroup>
199
+ ),
200
+ };
201
+
202
+ export const WrappingGroup: Story = {
203
+ render: () => (
204
+ <div style={{ maxWidth: '300px' }}>
205
+ <ChipGroup wrap gap="xs">
206
+ <Chip size="sm">JavaScript</Chip>
207
+ <Chip size="sm">TypeScript</Chip>
208
+ <Chip size="sm">React</Chip>
209
+ <Chip size="sm">Vue</Chip>
210
+ <Chip size="sm">Angular</Chip>
211
+ <Chip size="sm">Svelte</Chip>
212
+ <Chip size="sm">Node.js</Chip>
213
+ <Chip size="sm">Python</Chip>
214
+ </ChipGroup>
215
+ </div>
216
+ ),
217
+ };
218
+
219
+ export const VerticalGroup: Story = {
220
+ render: () => (
221
+ <ChipGroup direction="vertical" gap="sm">
222
+ <Chip icon={<Check className="h-3 w-3" />} variant="success">Completed</Chip>
223
+ <Chip icon={<Star className="h-3 w-3" />} variant="warning">In Progress</Chip>
224
+ <Chip icon={<X className="h-3 w-3" />} variant="error">Cancelled</Chip>
225
+ </ChipGroup>
226
+ ),
227
+ };
228
+
229
+ export const SingleSelection: Story = {
230
+ render: () => {
231
+ const [selected, setSelected] = useState<string[]>(['all']);
232
+
233
+ return (
234
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
235
+ <ChipGroup
236
+ selectionMode="single"
237
+ selectedKeys={selected}
238
+ onSelectionChange={setSelected}
239
+ gap="sm"
240
+ >
241
+ <Chip chipKey="all">All</Chip>
242
+ <Chip chipKey="active">Active</Chip>
243
+ <Chip chipKey="pending">Pending</Chip>
244
+ <Chip chipKey="archived">Archived</Chip>
245
+ </ChipGroup>
246
+ <div style={{ fontSize: '0.875rem', color: '#666' }}>
247
+ Selected: {selected.join(', ') || 'None'}
248
+ </div>
249
+ </div>
250
+ );
251
+ },
252
+ };
253
+
254
+ export const MultipleSelection: Story = {
255
+ render: () => {
256
+ const [selected, setSelected] = useState<string[]>(['react', 'typescript']);
257
+
258
+ return (
259
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
260
+ <ChipGroup
261
+ selectionMode="multiple"
262
+ selectedKeys={selected}
263
+ onSelectionChange={setSelected}
264
+ wrap
265
+ gap="sm"
266
+ >
267
+ <Chip chipKey="react" variant="info">React</Chip>
268
+ <Chip chipKey="vue" variant="info">Vue</Chip>
269
+ <Chip chipKey="angular" variant="info">Angular</Chip>
270
+ <Chip chipKey="typescript" variant="info">TypeScript</Chip>
271
+ <Chip chipKey="javascript" variant="info">JavaScript</Chip>
272
+ </ChipGroup>
273
+ <div style={{ fontSize: '0.875rem', color: '#666' }}>
274
+ Selected: {selected.join(', ') || 'None'}
275
+ </div>
276
+ </div>
277
+ );
278
+ },
279
+ };
280
+
281
+ export const RemovableGroup: Story = {
282
+ render: () => {
283
+ const [tags, setTags] = useState([
284
+ { key: '1', label: 'Design' },
285
+ { key: '2', label: 'Development' },
286
+ { key: '3', label: 'Marketing' },
287
+ { key: '4', label: 'Sales' },
288
+ ]);
289
+
290
+ return (
291
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
292
+ <ChipGroup wrap gap="xs">
293
+ {tags.map(tag => (
294
+ <Chip
295
+ key={tag.key}
296
+ onClose={() => setTags(tags.filter(t => t.key !== tag.key))}
297
+ size="sm"
298
+ >
299
+ {tag.label}
300
+ </Chip>
301
+ ))}
302
+ </ChipGroup>
303
+ {tags.length === 0 && (
304
+ <span style={{ fontSize: '0.875rem', color: '#666' }}>No tags remaining</span>
305
+ )}
306
+ </div>
307
+ );
308
+ },
309
+ };
310
+
311
+ export const FilterChips: Story = {
312
+ parameters: {
313
+ docs: {
314
+ description: {
315
+ story: 'Example of using chips as filter toggles.',
316
+ },
317
+ },
318
+ },
319
+ render: () => {
320
+ const [filters, setFilters] = useState<string[]>(['in-stock']);
321
+
322
+ const categories = [
323
+ { key: 'in-stock', label: 'In Stock', variant: 'success' as const },
324
+ { key: 'on-sale', label: 'On Sale', variant: 'error' as const },
325
+ { key: 'new', label: 'New Arrival', variant: 'info' as const },
326
+ { key: 'featured', label: 'Featured', variant: 'warning' as const },
327
+ ];
328
+
329
+ return (
330
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
331
+ <div style={{ fontSize: '0.875rem', fontWeight: 500 }}>Filter Products:</div>
332
+ <ChipGroup
333
+ selectionMode="multiple"
334
+ selectedKeys={filters}
335
+ onSelectionChange={setFilters}
336
+ gap="sm"
337
+ >
338
+ {categories.map(cat => (
339
+ <Chip key={cat.key} chipKey={cat.key} variant={cat.variant}>
340
+ {cat.label}
341
+ </Chip>
342
+ ))}
343
+ </ChipGroup>
344
+ <div style={{ fontSize: '0.75rem', color: '#666' }}>
345
+ Active filters: {filters.length > 0 ? filters.join(', ') : 'None'}
346
+ </div>
347
+ </div>
348
+ );
349
+ },
350
+ };
351
+
352
+ export const SelectedFields: Story = {
353
+ parameters: {
354
+ docs: {
355
+ description: {
356
+ story: 'Example from a report builder showing selected fields as removable chips.',
357
+ },
358
+ },
359
+ },
360
+ render: () => {
361
+ const [fields, setFields] = useState([
362
+ { key: 'users.name', label: 'Name', table: 'users' },
363
+ { key: 'users.email', label: 'Email', table: 'users' },
364
+ { key: 'orders.total', label: 'Total', table: 'orders' },
365
+ { key: 'orders.date', label: 'Date', table: 'orders' },
366
+ ]);
367
+
368
+ return (
369
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
370
+ <div style={{ fontSize: '0.875rem', fontWeight: 500 }}>Selected Fields:</div>
371
+ <ChipGroup wrap gap="xs">
372
+ {fields.map(field => (
373
+ <Chip
374
+ key={field.key}
375
+ size="sm"
376
+ variant="info"
377
+ onClose={() => setFields(fields.filter(f => f.key !== field.key))}
378
+ >
379
+ {field.table}.{field.label}
380
+ </Chip>
381
+ ))}
382
+ </ChipGroup>
383
+ {fields.length === 0 && (
384
+ <span style={{ fontSize: '0.875rem', color: '#666' }}>No fields selected</span>
385
+ )}
386
+ </div>
387
+ );
388
+ },
389
+ };
@@ -1,4 +1,4 @@
1
- import { ReactNode } from 'react';
1
+ import { ReactNode, Children, isValidElement, cloneElement } from 'react';
2
2
  import { X } from 'lucide-react';
3
3
 
4
4
  export interface ChipProps {
@@ -10,6 +10,30 @@ export interface ChipProps {
10
10
  disabled?: boolean;
11
11
  className?: string;
12
12
  onClick?: () => void;
13
+ /** Whether the chip is in a selected state */
14
+ selected?: boolean;
15
+ /** Maximum width for text truncation */
16
+ maxWidth?: string | number;
17
+ /** Unique key for use in ChipGroup selection */
18
+ chipKey?: string;
19
+ }
20
+
21
+ export interface ChipGroupProps {
22
+ children: ReactNode;
23
+ /** Layout direction */
24
+ direction?: 'horizontal' | 'vertical';
25
+ /** Whether chips should wrap to next line */
26
+ wrap?: boolean;
27
+ /** Gap between chips */
28
+ gap?: 'xs' | 'sm' | 'md' | 'lg';
29
+ /** Selection mode */
30
+ selectionMode?: 'none' | 'single' | 'multiple';
31
+ /** Selected chip keys (controlled) */
32
+ selectedKeys?: string[];
33
+ /** Callback when selection changes */
34
+ onSelectionChange?: (keys: string[]) => void;
35
+ /** Additional CSS classes */
36
+ className?: string;
13
37
  }
14
38
 
15
39
  const variantClasses = {
@@ -17,31 +41,37 @@ const variantClasses = {
17
41
  default: 'bg-primary-100 text-primary-700 border-primary-200',
18
42
  hover: 'hover:bg-primary-200',
19
43
  close: 'hover:bg-primary-300 text-primary-600',
44
+ selected: 'bg-primary-200 border-primary-400 ring-2 ring-primary-300',
20
45
  },
21
46
  secondary: {
22
47
  default: 'bg-ink-100 text-ink-700 border-ink-200',
23
48
  hover: 'hover:bg-ink-200',
24
49
  close: 'hover:bg-ink-300 text-ink-600',
50
+ selected: 'bg-ink-200 border-ink-400 ring-2 ring-ink-300',
25
51
  },
26
52
  success: {
27
53
  default: 'bg-success-100 text-success-700 border-success-200',
28
54
  hover: 'hover:bg-success-200',
29
55
  close: 'hover:bg-success-300 text-success-600',
56
+ selected: 'bg-success-200 border-success-400 ring-2 ring-success-300',
30
57
  },
31
58
  warning: {
32
59
  default: 'bg-warning-100 text-warning-700 border-warning-200',
33
60
  hover: 'hover:bg-warning-200',
34
61
  close: 'hover:bg-warning-300 text-warning-600',
62
+ selected: 'bg-warning-200 border-warning-400 ring-2 ring-warning-300',
35
63
  },
36
64
  error: {
37
65
  default: 'bg-error-100 text-error-700 border-error-200',
38
66
  hover: 'hover:bg-error-200',
39
67
  close: 'hover:bg-error-300 text-error-600',
68
+ selected: 'bg-error-200 border-error-400 ring-2 ring-error-300',
40
69
  },
41
70
  info: {
42
71
  default: 'bg-accent-100 text-accent-700 border-accent-200',
43
72
  hover: 'hover:bg-accent-200',
44
73
  close: 'hover:bg-accent-300 text-accent-600',
74
+ selected: 'bg-accent-200 border-accent-400 ring-2 ring-accent-300',
45
75
  },
46
76
  };
47
77
 
@@ -63,6 +93,39 @@ const sizeClasses = {
63
93
  },
64
94
  };
65
95
 
96
+ const gapClasses = {
97
+ xs: 'gap-1',
98
+ sm: 'gap-1.5',
99
+ md: 'gap-2',
100
+ lg: 'gap-3',
101
+ };
102
+
103
+ /**
104
+ * Chip - Compact element for displaying values with optional remove functionality
105
+ *
106
+ * @example Basic chip
107
+ * ```tsx
108
+ * <Chip>Tag Name</Chip>
109
+ * ```
110
+ *
111
+ * @example Removable chip
112
+ * ```tsx
113
+ * <Chip onClose={() => removeTag(tag)}>
114
+ * {tag.name}
115
+ * </Chip>
116
+ * ```
117
+ *
118
+ * @example With icon and selected state
119
+ * ```tsx
120
+ * <Chip
121
+ * icon={<Star className="h-3 w-3" />}
122
+ * selected={isSelected}
123
+ * onClick={() => toggleSelection()}
124
+ * >
125
+ * Favorite
126
+ * </Chip>
127
+ * ```
128
+ */
66
129
  export default function Chip({
67
130
  children,
68
131
  variant = 'secondary',
@@ -72,6 +135,9 @@ export default function Chip({
72
135
  disabled = false,
73
136
  className = '',
74
137
  onClick,
138
+ selected = false,
139
+ maxWidth,
140
+ chipKey,
75
141
  }: ChipProps) {
76
142
  const variantStyle = variantClasses[variant];
77
143
  const sizeStyle = sizeClasses[size];
@@ -83,8 +149,8 @@ export default function Chip({
83
149
  className={`
84
150
  inline-flex items-center rounded-full border font-medium
85
151
  transition-colors
86
- ${variantStyle.default}
87
- ${isClickable && !disabled ? variantStyle.hover : ''}
152
+ ${selected ? variantStyle.selected : variantStyle.default}
153
+ ${isClickable && !disabled && !selected ? variantStyle.hover : ''}
88
154
  ${sizeStyle.container}
89
155
  ${disabled ? 'opacity-50 cursor-not-allowed' : ''}
90
156
  ${onClick && !disabled ? 'cursor-pointer' : ''}
@@ -93,6 +159,9 @@ export default function Chip({
93
159
  onClick={onClick && !disabled ? onClick : undefined}
94
160
  role={onClick ? 'button' : undefined}
95
161
  aria-disabled={disabled}
162
+ aria-pressed={onClick ? selected : undefined}
163
+ data-chip-key={chipKey}
164
+ style={{ maxWidth: maxWidth || undefined }}
96
165
  >
97
166
  {icon && (
98
167
  <span className={`flex-shrink-0 ${sizeStyle.icon}`}>
@@ -124,3 +193,113 @@ export default function Chip({
124
193
  </div>
125
194
  );
126
195
  }
196
+
197
+ /**
198
+ * ChipGroup - Container for multiple chips with layout and selection support
199
+ *
200
+ * @example Basic group
201
+ * ```tsx
202
+ * <ChipGroup wrap gap="sm">
203
+ * {tags.map(tag => (
204
+ * <Chip key={tag.id} onClose={() => removeTag(tag)}>
205
+ * {tag.name}
206
+ * </Chip>
207
+ * ))}
208
+ * </ChipGroup>
209
+ * ```
210
+ *
211
+ * @example Selectable group (single)
212
+ * ```tsx
213
+ * <ChipGroup
214
+ * selectionMode="single"
215
+ * selectedKeys={[selectedCategory]}
216
+ * onSelectionChange={(keys) => setSelectedCategory(keys[0])}
217
+ * >
218
+ * <Chip chipKey="all">All</Chip>
219
+ * <Chip chipKey="active">Active</Chip>
220
+ * <Chip chipKey="archived">Archived</Chip>
221
+ * </ChipGroup>
222
+ * ```
223
+ *
224
+ * @example Multi-select group
225
+ * ```tsx
226
+ * <ChipGroup
227
+ * selectionMode="multiple"
228
+ * selectedKeys={selectedTags}
229
+ * onSelectionChange={setSelectedTags}
230
+ * wrap
231
+ * >
232
+ * {availableTags.map(tag => (
233
+ * <Chip key={tag} chipKey={tag}>{tag}</Chip>
234
+ * ))}
235
+ * </ChipGroup>
236
+ * ```
237
+ */
238
+ export function ChipGroup({
239
+ children,
240
+ direction = 'horizontal',
241
+ wrap = false,
242
+ gap = 'sm',
243
+ selectionMode = 'none',
244
+ selectedKeys = [],
245
+ onSelectionChange,
246
+ className = '',
247
+ }: ChipGroupProps) {
248
+ const handleChipClick = (chipKey: string) => {
249
+ if (selectionMode === 'none' || !onSelectionChange) return;
250
+
251
+ if (selectionMode === 'single') {
252
+ // Toggle single selection
253
+ if (selectedKeys.includes(chipKey)) {
254
+ onSelectionChange([]);
255
+ } else {
256
+ onSelectionChange([chipKey]);
257
+ }
258
+ } else if (selectionMode === 'multiple') {
259
+ // Toggle in array
260
+ if (selectedKeys.includes(chipKey)) {
261
+ onSelectionChange(selectedKeys.filter(k => k !== chipKey));
262
+ } else {
263
+ onSelectionChange([...selectedKeys, chipKey]);
264
+ }
265
+ }
266
+ };
267
+
268
+ // Clone children to inject selection props
269
+ const enhancedChildren = Children.map(children, (child) => {
270
+ if (!isValidElement<ChipProps>(child)) return child;
271
+
272
+ const chipKey = child.props.chipKey;
273
+ if (!chipKey || selectionMode === 'none') return child;
274
+
275
+ const isSelected = selectedKeys.includes(chipKey);
276
+
277
+ return cloneElement(child, {
278
+ ...child.props,
279
+ selected: isSelected,
280
+ onClick: () => {
281
+ // Call original onClick if exists
282
+ if (child.props.onClick) {
283
+ child.props.onClick();
284
+ }
285
+ handleChipClick(chipKey);
286
+ },
287
+ } as ChipProps);
288
+ });
289
+
290
+ return (
291
+ <div
292
+ className={`
293
+ flex
294
+ ${direction === 'vertical' ? 'flex-col' : 'flex-row'}
295
+ ${wrap ? 'flex-wrap' : ''}
296
+ ${gapClasses[gap]}
297
+ ${className}
298
+ `}
299
+ role={selectionMode !== 'none' ? 'group' : undefined}
300
+ aria-label={selectionMode !== 'none' ? 'Chip selection group' : undefined}
301
+ >
302
+ {enhancedChildren}
303
+ </div>
304
+ );
305
+ }