@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.
- package/dist/components/BottomNavigation.d.ts +98 -0
- package/dist/components/BottomNavigation.d.ts.map +1 -0
- package/dist/components/Checkbox.d.ts +2 -0
- package/dist/components/Checkbox.d.ts.map +1 -1
- package/dist/components/CheckboxList.d.ts +81 -0
- package/dist/components/CheckboxList.d.ts.map +1 -0
- package/dist/components/Chip.d.ts +92 -1
- package/dist/components/Chip.d.ts.map +1 -1
- package/dist/components/ConfirmDialog.d.ts +43 -1
- package/dist/components/ConfirmDialog.d.ts.map +1 -1
- package/dist/components/DataTable.d.ts +10 -1
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/DataTableCardView.d.ts +99 -0
- package/dist/components/DataTableCardView.d.ts.map +1 -0
- package/dist/components/ExpandablePanel.d.ts +142 -0
- package/dist/components/ExpandablePanel.d.ts.map +1 -0
- package/dist/components/FloatingActionButton.d.ts +98 -0
- package/dist/components/FloatingActionButton.d.ts.map +1 -0
- package/dist/components/Input.d.ts +45 -1
- package/dist/components/Input.d.ts.map +1 -1
- package/dist/components/MobileHeader.d.ts +98 -0
- package/dist/components/MobileHeader.d.ts.map +1 -0
- package/dist/components/MobileLayout.d.ts +121 -0
- package/dist/components/MobileLayout.d.ts.map +1 -0
- package/dist/components/Modal.d.ts +50 -1
- package/dist/components/Modal.d.ts.map +1 -1
- package/dist/components/PullToRefresh.d.ts +87 -0
- package/dist/components/PullToRefresh.d.ts.map +1 -0
- package/dist/components/QueryTransparency.d.ts +1 -1
- package/dist/components/QueryTransparency.d.ts.map +1 -1
- package/dist/components/SearchableList.d.ts +83 -0
- package/dist/components/SearchableList.d.ts.map +1 -0
- package/dist/components/Select.d.ts +16 -2
- package/dist/components/Select.d.ts.map +1 -1
- package/dist/components/Sidebar.d.ts +40 -1
- package/dist/components/Sidebar.d.ts.map +1 -1
- package/dist/components/SwipeActions.d.ts +93 -0
- package/dist/components/SwipeActions.d.ts.map +1 -0
- package/dist/components/Switch.d.ts +1 -0
- package/dist/components/Switch.d.ts.map +1 -1
- package/dist/components/Textarea.d.ts +13 -0
- package/dist/components/Textarea.d.ts.map +1 -1
- package/dist/components/index.d.ts +27 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/context/MobileContext.d.ts +168 -0
- package/dist/context/MobileContext.d.ts.map +1 -0
- package/dist/hooks/useResponsive.d.ts +158 -0
- package/dist/hooks/useResponsive.d.ts.map +1 -0
- package/dist/index.d.ts +1653 -56
- package/dist/index.esm.js +2832 -194
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2865 -192
- package/dist/index.js.map +1 -1
- package/dist/styles.css +404 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/BottomNavigation.stories.tsx +142 -0
- package/src/components/BottomNavigation.tsx +225 -0
- package/src/components/Checkbox.stories.tsx +162 -0
- package/src/components/Checkbox.tsx +22 -6
- package/src/components/CheckboxList.stories.tsx +311 -0
- package/src/components/CheckboxList.tsx +433 -0
- package/src/components/Chip.stories.tsx +389 -0
- package/src/components/Chip.tsx +182 -3
- package/src/components/ConfirmDialog.tsx +56 -4
- package/src/components/DataTable.tsx +60 -1
- package/src/components/DataTableCardView.stories.tsx +307 -0
- package/src/components/DataTableCardView.tsx +419 -0
- package/src/components/ExpandablePanel.stories.tsx +620 -0
- package/src/components/ExpandablePanel.tsx +383 -0
- package/src/components/FloatingActionButton.stories.tsx +197 -0
- package/src/components/FloatingActionButton.tsx +301 -0
- package/src/components/Grid.stories.tsx +16 -16
- package/src/components/Input.stories.tsx +214 -0
- package/src/components/Input.tsx +81 -4
- package/src/components/MobileHeader.stories.tsx +205 -0
- package/src/components/MobileHeader.tsx +233 -0
- package/src/components/MobileLayout.stories.tsx +338 -0
- package/src/components/MobileLayout.tsx +313 -0
- package/src/components/Modal.stories.tsx +183 -0
- package/src/components/Modal.tsx +84 -3
- package/src/components/PullToRefresh.stories.tsx +321 -0
- package/src/components/PullToRefresh.tsx +294 -0
- package/src/components/QueryTransparency.tsx +1 -1
- package/src/components/SearchableList.stories.tsx +437 -0
- package/src/components/SearchableList.tsx +326 -0
- package/src/components/Select.stories.tsx +190 -0
- package/src/components/Select.tsx +353 -137
- package/src/components/Sidebar.tsx +191 -8
- package/src/components/SwipeActions.stories.tsx +327 -0
- package/src/components/SwipeActions.tsx +387 -0
- package/src/components/Switch.stories.tsx +158 -0
- package/src/components/Switch.tsx +12 -3
- package/src/components/Textarea.tsx +31 -1
- package/src/components/index.ts +63 -3
- package/src/context/MobileContext.tsx +296 -0
- package/src/hooks/useResponsive.ts +360 -0
- package/src/types/index.ts +4 -0
- 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
|
+
};
|
package/src/components/Chip.tsx
CHANGED
|
@@ -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
|
+
}
|