@papernote/ui 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Box.d.ts +2 -1
- package/dist/components/Box.d.ts.map +1 -1
- package/dist/components/Button.d.ts +10 -1
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Card.d.ts +11 -2
- package/dist/components/Card.d.ts.map +1 -1
- package/dist/components/DataTable.d.ts +17 -3
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/EmptyState.d.ts +3 -1
- package/dist/components/EmptyState.d.ts.map +1 -1
- package/dist/components/Grid.d.ts +4 -2
- package/dist/components/Grid.d.ts.map +1 -1
- package/dist/components/Input.d.ts +2 -0
- package/dist/components/Input.d.ts.map +1 -1
- package/dist/components/MultiSelect.d.ts +13 -1
- package/dist/components/MultiSelect.d.ts.map +1 -1
- package/dist/components/Stack.d.ts +25 -5
- package/dist/components/Stack.d.ts.map +1 -1
- package/dist/components/Text.d.ts +20 -4
- package/dist/components/Text.d.ts.map +1 -1
- package/dist/components/Textarea.d.ts +2 -0
- package/dist/components/Textarea.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +110 -48
- package/dist/index.esm.js +144 -138
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +143 -138
- package/dist/index.js.map +1 -1
- package/dist/styles.css +8 -51
- package/package.json +1 -1
- package/src/components/Box.stories.tsx +377 -0
- package/src/components/Box.tsx +8 -4
- package/src/components/Button.tsx +23 -10
- package/src/components/Card.tsx +20 -5
- package/src/components/DataTable.stories.tsx +36 -25
- package/src/components/DataTable.tsx +95 -5
- package/src/components/EmptyState.stories.tsx +124 -72
- package/src/components/EmptyState.tsx +10 -0
- package/src/components/Grid.stories.tsx +348 -0
- package/src/components/Grid.tsx +12 -5
- package/src/components/Input.tsx +12 -2
- package/src/components/MultiSelect.tsx +41 -10
- package/src/components/Stack.stories.tsx +24 -1
- package/src/components/Stack.tsx +40 -10
- package/src/components/Text.stories.tsx +273 -0
- package/src/components/Text.tsx +33 -8
- package/src/components/Textarea.tsx +32 -21
- package/src/components/index.ts +1 -4
- package/dist/components/Table.d.ts +0 -26
- package/dist/components/Table.d.ts.map +0 -1
- package/src/components/Table.tsx +0 -239
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Grid } from './Grid';
|
|
3
|
+
import Box from './Box';
|
|
4
|
+
import Text from './Text';
|
|
5
|
+
import Card from './Card';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Layout/Grid',
|
|
9
|
+
component: Grid,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'padded',
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component: `
|
|
15
|
+
Grid component for arranging children in a CSS grid layout with responsive breakpoints.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
- **Column options**: 1, 2, 3, 4, 6, 12 columns
|
|
19
|
+
- **Responsive breakpoints**: columns (base), sm (640px+), md (768px+), lg (1024px+), xl (1280px+)
|
|
20
|
+
- **Gap spacing**: none, xs, sm, md, lg, xl
|
|
21
|
+
- **Mobile-first**: Set base columns and override at larger breakpoints
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
\`\`\`tsx
|
|
26
|
+
import { Grid } from 'notebook-ui';
|
|
27
|
+
|
|
28
|
+
// Simple 3-column grid
|
|
29
|
+
<Grid columns={3} gap="md">
|
|
30
|
+
<Card>Item 1</Card>
|
|
31
|
+
<Card>Item 2</Card>
|
|
32
|
+
<Card>Item 3</Card>
|
|
33
|
+
</Grid>
|
|
34
|
+
|
|
35
|
+
// Responsive grid: 1 column mobile, 2 tablet, 4 desktop
|
|
36
|
+
<Grid columns={1} md={2} lg={4} gap="md">
|
|
37
|
+
{items.map(item => <Card key={item.id}>{item.name}</Card>)}
|
|
38
|
+
</Grid>
|
|
39
|
+
\`\`\`
|
|
40
|
+
`,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
tags: ['autodocs'],
|
|
45
|
+
argTypes: {
|
|
46
|
+
columns: {
|
|
47
|
+
control: 'select',
|
|
48
|
+
options: [1, 2, 3, 4, 6, 12],
|
|
49
|
+
description: 'Base number of columns',
|
|
50
|
+
},
|
|
51
|
+
sm: {
|
|
52
|
+
control: 'select',
|
|
53
|
+
options: [undefined, 1, 2, 3, 4, 6, 12],
|
|
54
|
+
description: 'Columns at sm breakpoint (640px+)',
|
|
55
|
+
},
|
|
56
|
+
md: {
|
|
57
|
+
control: 'select',
|
|
58
|
+
options: [undefined, 1, 2, 3, 4, 6, 12],
|
|
59
|
+
description: 'Columns at md breakpoint (768px+)',
|
|
60
|
+
},
|
|
61
|
+
lg: {
|
|
62
|
+
control: 'select',
|
|
63
|
+
options: [undefined, 1, 2, 3, 4, 6, 12],
|
|
64
|
+
description: 'Columns at lg breakpoint (1024px+)',
|
|
65
|
+
},
|
|
66
|
+
xl: {
|
|
67
|
+
control: 'select',
|
|
68
|
+
options: [undefined, 1, 2, 3, 4, 6, 12],
|
|
69
|
+
description: 'Columns at xl breakpoint (1280px+)',
|
|
70
|
+
},
|
|
71
|
+
gap: {
|
|
72
|
+
control: 'select',
|
|
73
|
+
options: ['none', 'xs', 'sm', 'md', 'lg', 'xl'],
|
|
74
|
+
description: 'Gap between grid items',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
} satisfies Meta<typeof Grid>;
|
|
78
|
+
|
|
79
|
+
export default meta;
|
|
80
|
+
type Story = StoryObj<typeof meta>;
|
|
81
|
+
|
|
82
|
+
// Helper component for demo items
|
|
83
|
+
const DemoItem = ({ children }: { children: React.ReactNode }) => (
|
|
84
|
+
<Box padding="md" border="all" rounded="md" className="bg-paper-50">
|
|
85
|
+
<Text align="center">{children}</Text>
|
|
86
|
+
</Box>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
export const Default: Story = {
|
|
90
|
+
args: {
|
|
91
|
+
columns: 3,
|
|
92
|
+
gap: 'md',
|
|
93
|
+
children: (
|
|
94
|
+
<>
|
|
95
|
+
<DemoItem>Item 1</DemoItem>
|
|
96
|
+
<DemoItem>Item 2</DemoItem>
|
|
97
|
+
<DemoItem>Item 3</DemoItem>
|
|
98
|
+
<DemoItem>Item 4</DemoItem>
|
|
99
|
+
<DemoItem>Item 5</DemoItem>
|
|
100
|
+
<DemoItem>Item 6</DemoItem>
|
|
101
|
+
</>
|
|
102
|
+
),
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const TwoColumns: Story = {
|
|
107
|
+
args: {
|
|
108
|
+
columns: 2,
|
|
109
|
+
gap: 'md',
|
|
110
|
+
children: (
|
|
111
|
+
<>
|
|
112
|
+
<DemoItem>Left</DemoItem>
|
|
113
|
+
<DemoItem>Right</DemoItem>
|
|
114
|
+
<DemoItem>Left</DemoItem>
|
|
115
|
+
<DemoItem>Right</DemoItem>
|
|
116
|
+
</>
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const FourColumns: Story = {
|
|
122
|
+
args: {
|
|
123
|
+
columns: 4,
|
|
124
|
+
gap: 'md',
|
|
125
|
+
children: (
|
|
126
|
+
<>
|
|
127
|
+
<DemoItem>1</DemoItem>
|
|
128
|
+
<DemoItem>2</DemoItem>
|
|
129
|
+
<DemoItem>3</DemoItem>
|
|
130
|
+
<DemoItem>4</DemoItem>
|
|
131
|
+
<DemoItem>5</DemoItem>
|
|
132
|
+
<DemoItem>6</DemoItem>
|
|
133
|
+
<DemoItem>7</DemoItem>
|
|
134
|
+
<DemoItem>8</DemoItem>
|
|
135
|
+
</>
|
|
136
|
+
),
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const SixColumns: Story = {
|
|
141
|
+
args: {
|
|
142
|
+
columns: 6,
|
|
143
|
+
gap: 'sm',
|
|
144
|
+
children: (
|
|
145
|
+
<>
|
|
146
|
+
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((n) => (
|
|
147
|
+
<DemoItem key={n}>{n}</DemoItem>
|
|
148
|
+
))}
|
|
149
|
+
</>
|
|
150
|
+
),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const TwelveColumns: Story = {
|
|
155
|
+
args: {
|
|
156
|
+
columns: 12,
|
|
157
|
+
gap: 'xs',
|
|
158
|
+
children: (
|
|
159
|
+
<>
|
|
160
|
+
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((n) => (
|
|
161
|
+
<DemoItem key={n}>{n}</DemoItem>
|
|
162
|
+
))}
|
|
163
|
+
</>
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Responsive grid that changes columns based on screen size.
|
|
170
|
+
* Resize the browser to see it adapt.
|
|
171
|
+
*/
|
|
172
|
+
export const Responsive: Story = {
|
|
173
|
+
args: {
|
|
174
|
+
columns: 1,
|
|
175
|
+
sm: 2,
|
|
176
|
+
md: 3,
|
|
177
|
+
lg: 4,
|
|
178
|
+
gap: 'md',
|
|
179
|
+
children: (
|
|
180
|
+
<>
|
|
181
|
+
{[1, 2, 3, 4, 5, 6, 7, 8].map((n) => (
|
|
182
|
+
<DemoItem key={n}>Item {n}</DemoItem>
|
|
183
|
+
))}
|
|
184
|
+
</>
|
|
185
|
+
),
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const GapSizes: Story = {
|
|
190
|
+
render: () => (
|
|
191
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
|
192
|
+
<div>
|
|
193
|
+
<Text weight="medium" size="sm" color="muted">gap="none"</Text>
|
|
194
|
+
<Grid columns={4} gap="none">
|
|
195
|
+
<DemoItem>1</DemoItem>
|
|
196
|
+
<DemoItem>2</DemoItem>
|
|
197
|
+
<DemoItem>3</DemoItem>
|
|
198
|
+
<DemoItem>4</DemoItem>
|
|
199
|
+
</Grid>
|
|
200
|
+
</div>
|
|
201
|
+
<div>
|
|
202
|
+
<Text weight="medium" size="sm" color="muted">gap="xs"</Text>
|
|
203
|
+
<Grid columns={4} gap="xs">
|
|
204
|
+
<DemoItem>1</DemoItem>
|
|
205
|
+
<DemoItem>2</DemoItem>
|
|
206
|
+
<DemoItem>3</DemoItem>
|
|
207
|
+
<DemoItem>4</DemoItem>
|
|
208
|
+
</Grid>
|
|
209
|
+
</div>
|
|
210
|
+
<div>
|
|
211
|
+
<Text weight="medium" size="sm" color="muted">gap="sm"</Text>
|
|
212
|
+
<Grid columns={4} gap="sm">
|
|
213
|
+
<DemoItem>1</DemoItem>
|
|
214
|
+
<DemoItem>2</DemoItem>
|
|
215
|
+
<DemoItem>3</DemoItem>
|
|
216
|
+
<DemoItem>4</DemoItem>
|
|
217
|
+
</Grid>
|
|
218
|
+
</div>
|
|
219
|
+
<div>
|
|
220
|
+
<Text weight="medium" size="sm" color="muted">gap="md" (default)</Text>
|
|
221
|
+
<Grid columns={4} gap="md">
|
|
222
|
+
<DemoItem>1</DemoItem>
|
|
223
|
+
<DemoItem>2</DemoItem>
|
|
224
|
+
<DemoItem>3</DemoItem>
|
|
225
|
+
<DemoItem>4</DemoItem>
|
|
226
|
+
</Grid>
|
|
227
|
+
</div>
|
|
228
|
+
<div>
|
|
229
|
+
<Text weight="medium" size="sm" color="muted">gap="lg"</Text>
|
|
230
|
+
<Grid columns={4} gap="lg">
|
|
231
|
+
<DemoItem>1</DemoItem>
|
|
232
|
+
<DemoItem>2</DemoItem>
|
|
233
|
+
<DemoItem>3</DemoItem>
|
|
234
|
+
<DemoItem>4</DemoItem>
|
|
235
|
+
</Grid>
|
|
236
|
+
</div>
|
|
237
|
+
<div>
|
|
238
|
+
<Text weight="medium" size="sm" color="muted">gap="xl"</Text>
|
|
239
|
+
<Grid columns={4} gap="xl">
|
|
240
|
+
<DemoItem>1</DemoItem>
|
|
241
|
+
<DemoItem>2</DemoItem>
|
|
242
|
+
<DemoItem>3</DemoItem>
|
|
243
|
+
<DemoItem>4</DemoItem>
|
|
244
|
+
</Grid>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
),
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export const WithCards: Story = {
|
|
251
|
+
render: () => (
|
|
252
|
+
<Grid columns={1} md={2} lg={3} gap="md">
|
|
253
|
+
<Card>
|
|
254
|
+
<Card.Header>
|
|
255
|
+
<Card.Title>Card 1</Card.Title>
|
|
256
|
+
</Card.Header>
|
|
257
|
+
<Card.Content>
|
|
258
|
+
<Text color="secondary">Content for the first card.</Text>
|
|
259
|
+
</Card.Content>
|
|
260
|
+
</Card>
|
|
261
|
+
<Card>
|
|
262
|
+
<Card.Header>
|
|
263
|
+
<Card.Title>Card 2</Card.Title>
|
|
264
|
+
</Card.Header>
|
|
265
|
+
<Card.Content>
|
|
266
|
+
<Text color="secondary">Content for the second card.</Text>
|
|
267
|
+
</Card.Content>
|
|
268
|
+
</Card>
|
|
269
|
+
<Card>
|
|
270
|
+
<Card.Header>
|
|
271
|
+
<Card.Title>Card 3</Card.Title>
|
|
272
|
+
</Card.Header>
|
|
273
|
+
<Card.Content>
|
|
274
|
+
<Text color="secondary">Content for the third card.</Text>
|
|
275
|
+
</Card.Content>
|
|
276
|
+
</Card>
|
|
277
|
+
</Grid>
|
|
278
|
+
),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const DashboardLayout: Story = {
|
|
282
|
+
render: () => (
|
|
283
|
+
<Grid columns={1} md={2} lg={4} gap="md">
|
|
284
|
+
<Box padding="lg" border="all" rounded="lg" className="bg-success-50">
|
|
285
|
+
<Text size="sm" color="muted">Revenue</Text>
|
|
286
|
+
<Text size="2xl" weight="bold">$45,231</Text>
|
|
287
|
+
<Text size="sm" color="success">+20.1% from last month</Text>
|
|
288
|
+
</Box>
|
|
289
|
+
<Box padding="lg" border="all" rounded="lg" className="bg-primary-50">
|
|
290
|
+
<Text size="sm" color="muted">Users</Text>
|
|
291
|
+
<Text size="2xl" weight="bold">2,350</Text>
|
|
292
|
+
<Text size="sm" color="accent">+180 new this week</Text>
|
|
293
|
+
</Box>
|
|
294
|
+
<Box padding="lg" border="all" rounded="lg" className="bg-warning-50">
|
|
295
|
+
<Text size="sm" color="muted">Pending</Text>
|
|
296
|
+
<Text size="2xl" weight="bold">12</Text>
|
|
297
|
+
<Text size="sm" color="warning">Requires attention</Text>
|
|
298
|
+
</Box>
|
|
299
|
+
<Box padding="lg" border="all" rounded="lg" className="bg-error-50">
|
|
300
|
+
<Text size="sm" color="muted">Errors</Text>
|
|
301
|
+
<Text size="2xl" weight="bold">3</Text>
|
|
302
|
+
<Text size="sm" color="error">Critical issues</Text>
|
|
303
|
+
</Box>
|
|
304
|
+
</Grid>
|
|
305
|
+
),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
export const FormLayout: Story = {
|
|
309
|
+
render: () => (
|
|
310
|
+
<Box padding="lg" border="all" rounded="lg" style={{ maxWidth: '600px' }}>
|
|
311
|
+
<Text as="h3" size="lg" weight="semibold">User Details</Text>
|
|
312
|
+
<Box marginTop="md">
|
|
313
|
+
<Grid columns={1} md={2} gap="md">
|
|
314
|
+
<Box padding="sm" border="all" rounded="sm" className="bg-paper-50">
|
|
315
|
+
<Text size="sm" color="muted">First Name</Text>
|
|
316
|
+
</Box>
|
|
317
|
+
<Box padding="sm" border="all" rounded="sm" className="bg-paper-50">
|
|
318
|
+
<Text size="sm" color="muted">Last Name</Text>
|
|
319
|
+
</Box>
|
|
320
|
+
<Box padding="sm" border="all" rounded="sm" className="bg-paper-50">
|
|
321
|
+
<Text size="sm" color="muted">Email</Text>
|
|
322
|
+
</Box>
|
|
323
|
+
<Box padding="sm" border="all" rounded="sm" className="bg-paper-50">
|
|
324
|
+
<Text size="sm" color="muted">Phone</Text>
|
|
325
|
+
</Box>
|
|
326
|
+
</Grid>
|
|
327
|
+
</Box>
|
|
328
|
+
</Box>
|
|
329
|
+
),
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
export const ImageGallery: Story = {
|
|
333
|
+
render: () => (
|
|
334
|
+
<Grid columns={2} md={3} lg={4} gap="sm">
|
|
335
|
+
{[1, 2, 3, 4, 5, 6, 7, 8].map((n) => (
|
|
336
|
+
<Box
|
|
337
|
+
key={n}
|
|
338
|
+
padding="none"
|
|
339
|
+
rounded="md"
|
|
340
|
+
className="bg-paper-200 aspect-square flex items-center justify-center"
|
|
341
|
+
style={{ aspectRatio: '1/1', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
|
342
|
+
>
|
|
343
|
+
<Text color="muted">Image {n}</Text>
|
|
344
|
+
</Box>
|
|
345
|
+
))}
|
|
346
|
+
</Grid>
|
|
347
|
+
),
|
|
348
|
+
};
|
package/src/components/Grid.tsx
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Grid Component - CSS Grid layout system
|
|
2
2
|
// Provides flexible grid layouts with consistent spacing
|
|
3
3
|
|
|
4
|
-
import React from 'react';
|
|
4
|
+
import React, { forwardRef } from 'react';
|
|
5
5
|
|
|
6
6
|
type ColumnCount = 1 | 2 | 3 | 4 | 6 | 12;
|
|
7
7
|
|
|
8
|
-
export interface GridProps {
|
|
8
|
+
export interface GridProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
9
9
|
/** Content to arrange in grid */
|
|
10
10
|
children: React.ReactNode;
|
|
11
11
|
/** Number of columns (default, or mobile-first base) */
|
|
@@ -26,6 +26,8 @@ export interface GridProps {
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Grid component for arranging children in a CSS grid layout.
|
|
29
|
+
*
|
|
30
|
+
* Supports ref forwarding for DOM access.
|
|
29
31
|
*
|
|
30
32
|
* Column options: 1, 2, 3, 4, 6, 12
|
|
31
33
|
*
|
|
@@ -51,7 +53,7 @@ export interface GridProps {
|
|
|
51
53
|
* <Card>Item 2</Card>
|
|
52
54
|
* </Grid>
|
|
53
55
|
*/
|
|
54
|
-
export const Grid
|
|
56
|
+
export const Grid = forwardRef<HTMLDivElement, GridProps>(({
|
|
55
57
|
children,
|
|
56
58
|
columns = 1,
|
|
57
59
|
sm,
|
|
@@ -60,7 +62,8 @@ export const Grid: React.FC<GridProps> = ({
|
|
|
60
62
|
xl,
|
|
61
63
|
gap = 'md',
|
|
62
64
|
className = '',
|
|
63
|
-
|
|
65
|
+
...htmlProps
|
|
66
|
+
}, ref) => {
|
|
64
67
|
// Base column classes
|
|
65
68
|
const baseColumnClasses: Record<ColumnCount, string> = {
|
|
66
69
|
1: 'grid-cols-1',
|
|
@@ -128,6 +131,8 @@ export const Grid: React.FC<GridProps> = ({
|
|
|
128
131
|
|
|
129
132
|
return (
|
|
130
133
|
<div
|
|
134
|
+
ref={ref}
|
|
135
|
+
{...htmlProps}
|
|
131
136
|
className={`
|
|
132
137
|
grid
|
|
133
138
|
${columnClassList}
|
|
@@ -138,6 +143,8 @@ export const Grid: React.FC<GridProps> = ({
|
|
|
138
143
|
{children}
|
|
139
144
|
</div>
|
|
140
145
|
);
|
|
141
|
-
};
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
Grid.displayName = 'Grid';
|
|
142
149
|
|
|
143
150
|
export default Grid;
|
package/src/components/Input.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { forwardRef, useState } from 'react';
|
|
2
|
-
import { AlertCircle, CheckCircle, AlertTriangle, Eye, EyeOff, X } from 'lucide-react';
|
|
2
|
+
import { AlertCircle, CheckCircle, AlertTriangle, Eye, EyeOff, X, Loader2 } from 'lucide-react';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Validation state for input components
|
|
@@ -38,6 +38,8 @@ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>
|
|
|
38
38
|
clearable?: boolean;
|
|
39
39
|
/** Callback when clear button is clicked */
|
|
40
40
|
onClear?: () => void;
|
|
41
|
+
/** Show loading spinner in input */
|
|
42
|
+
loading?: boolean;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/**
|
|
@@ -105,6 +107,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
105
107
|
showPasswordToggle = false,
|
|
106
108
|
clearable = false,
|
|
107
109
|
onClear,
|
|
110
|
+
loading = false,
|
|
108
111
|
className = '',
|
|
109
112
|
id,
|
|
110
113
|
type = 'text',
|
|
@@ -245,8 +248,15 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
245
248
|
|
|
246
249
|
{/* Right Icon, Validation Icon, Clear Button, or Password Toggle */}
|
|
247
250
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center gap-2">
|
|
251
|
+
{/* Loading Spinner */}
|
|
252
|
+
{loading && (
|
|
253
|
+
<div className="pointer-events-none text-ink-400">
|
|
254
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
|
|
248
258
|
{/* Suffix Icon */}
|
|
249
|
-
{suffixIcon && !suffix && !validationState && !showPasswordToggle && !showClearButton && (
|
|
259
|
+
{suffixIcon && !suffix && !validationState && !showPasswordToggle && !showClearButton && !loading && (
|
|
250
260
|
<div className="pointer-events-none text-ink-400">
|
|
251
261
|
{suffixIcon}
|
|
252
262
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
-
import { Check, ChevronDown, Search, X } from 'lucide-react';
|
|
1
|
+
import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
|
|
2
|
+
import { Check, ChevronDown, Search, X, Loader2 } from 'lucide-react';
|
|
3
3
|
|
|
4
4
|
export interface MultiSelectOption {
|
|
5
5
|
value: string;
|
|
@@ -21,10 +21,22 @@ export interface MultiSelectProps {
|
|
|
21
21
|
maxHeight?: number;
|
|
22
22
|
/** Maximum number of selections allowed */
|
|
23
23
|
maxSelections?: number;
|
|
24
|
+
/** Show loading spinner (for async options loading) */
|
|
25
|
+
loading?: boolean;
|
|
24
26
|
'aria-label'?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
/** Handle for imperative methods */
|
|
30
|
+
export interface MultiSelectHandle {
|
|
31
|
+
/** Focus the select trigger button */
|
|
32
|
+
focus: () => void;
|
|
33
|
+
/** Open the dropdown */
|
|
34
|
+
open: () => void;
|
|
35
|
+
/** Close the dropdown */
|
|
36
|
+
close: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(({
|
|
28
40
|
options,
|
|
29
41
|
value = [],
|
|
30
42
|
onChange,
|
|
@@ -36,12 +48,21 @@ export default function MultiSelect({
|
|
|
36
48
|
error,
|
|
37
49
|
maxHeight = 240,
|
|
38
50
|
maxSelections,
|
|
51
|
+
loading = false,
|
|
39
52
|
'aria-label': ariaLabel,
|
|
40
|
-
}
|
|
53
|
+
}, ref) => {
|
|
41
54
|
const [isOpen, setIsOpen] = useState(false);
|
|
42
55
|
const [searchQuery, setSearchQuery] = useState('');
|
|
43
56
|
const selectRef = useRef<HTMLDivElement>(null);
|
|
44
57
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
58
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
59
|
+
|
|
60
|
+
// Expose imperative methods
|
|
61
|
+
useImperativeHandle(ref, () => ({
|
|
62
|
+
focus: () => triggerRef.current?.focus(),
|
|
63
|
+
open: () => !disabled && setIsOpen(true),
|
|
64
|
+
close: () => setIsOpen(false),
|
|
65
|
+
}));
|
|
45
66
|
|
|
46
67
|
const selectedOptions = options.filter(opt => value.includes(opt.value));
|
|
47
68
|
const hasReachedMax = maxSelections ? value.length >= maxSelections : false;
|
|
@@ -112,13 +133,14 @@ export default function MultiSelect({
|
|
|
112
133
|
<div ref={selectRef} className="relative">
|
|
113
134
|
{/* Trigger Button */}
|
|
114
135
|
<button
|
|
136
|
+
ref={triggerRef}
|
|
115
137
|
type="button"
|
|
116
|
-
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
117
|
-
disabled={disabled}
|
|
138
|
+
onClick={() => !disabled && !loading && setIsOpen(!isOpen)}
|
|
139
|
+
disabled={disabled || loading}
|
|
118
140
|
className={`
|
|
119
141
|
input w-full flex items-center justify-between min-h-[42px]
|
|
120
142
|
${error ? 'border-error-400 focus:border-error-400 focus:ring-error-400' : ''}
|
|
121
|
-
${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}
|
|
143
|
+
${disabled || loading ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}
|
|
122
144
|
`}
|
|
123
145
|
aria-haspopup="listbox"
|
|
124
146
|
aria-expanded={isOpen}
|
|
@@ -148,7 +170,10 @@ export default function MultiSelect({
|
|
|
148
170
|
)}
|
|
149
171
|
</div>
|
|
150
172
|
<div className="flex items-center gap-2 ml-2">
|
|
151
|
-
{
|
|
173
|
+
{loading && (
|
|
174
|
+
<Loader2 className="h-4 w-4 text-ink-400 animate-spin" />
|
|
175
|
+
)}
|
|
176
|
+
{!loading && selectedOptions.length > 0 && !disabled && (
|
|
152
177
|
<button
|
|
153
178
|
type="button"
|
|
154
179
|
onClick={handleClearAll}
|
|
@@ -158,7 +183,9 @@ export default function MultiSelect({
|
|
|
158
183
|
<X className="h-4 w-4" />
|
|
159
184
|
</button>
|
|
160
185
|
)}
|
|
161
|
-
|
|
186
|
+
{!loading && (
|
|
187
|
+
<ChevronDown className={`h-4 w-4 text-ink-500 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
|
188
|
+
)}
|
|
162
189
|
</div>
|
|
163
190
|
</button>
|
|
164
191
|
|
|
@@ -244,4 +271,8 @@ export default function MultiSelect({
|
|
|
244
271
|
</div>
|
|
245
272
|
</div>
|
|
246
273
|
);
|
|
247
|
-
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
MultiSelect.displayName = 'MultiSelect';
|
|
277
|
+
|
|
278
|
+
export default MultiSelect;
|
|
@@ -49,7 +49,16 @@ import { Stack, Button } from 'notebook-ui';
|
|
|
49
49
|
spacing: {
|
|
50
50
|
control: 'select',
|
|
51
51
|
options: ['none', 'xs', 'sm', 'md', 'lg', 'xl'],
|
|
52
|
-
description: 'Gap spacing between children (
|
|
52
|
+
description: 'Gap spacing between children (alias: gap)',
|
|
53
|
+
table: {
|
|
54
|
+
type: { summary: 'none | xs | sm | md | lg | xl' },
|
|
55
|
+
defaultValue: { summary: 'md' },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
gap: {
|
|
59
|
+
control: 'select',
|
|
60
|
+
options: ['none', 'xs', 'sm', 'md', 'lg', 'xl'],
|
|
61
|
+
description: 'Gap spacing between children (alias for spacing)',
|
|
53
62
|
table: {
|
|
54
63
|
type: { summary: 'none | xs | sm | md | lg | xl' },
|
|
55
64
|
defaultValue: { summary: 'md' },
|
|
@@ -339,3 +348,17 @@ export const NestedStacks: Story = {
|
|
|
339
348
|
</Stack>
|
|
340
349
|
),
|
|
341
350
|
};
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The `gap` prop is an alias for `spacing` - they are interchangeable.
|
|
354
|
+
* This provides flexibility for developers who prefer the `gap` terminology.
|
|
355
|
+
*/
|
|
356
|
+
export const GapAlias: Story = {
|
|
357
|
+
render: () => (
|
|
358
|
+
<Stack direction="horizontal" gap="md">
|
|
359
|
+
<Box>Using</Box>
|
|
360
|
+
<Box color="#8b5cf6">gap</Box>
|
|
361
|
+
<Box color="#10b981">prop</Box>
|
|
362
|
+
</Stack>
|
|
363
|
+
),
|
|
364
|
+
};
|
package/src/components/Stack.tsx
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// Stack Component - Vertical or horizontal stacking layout
|
|
2
2
|
// Provides consistent spacing between child elements
|
|
3
3
|
|
|
4
|
-
import React from 'react';
|
|
4
|
+
import React, { forwardRef } from 'react';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
type SpacingValue = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
7
|
+
|
|
8
|
+
export interface StackProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
9
|
/** Content to stack */
|
|
8
10
|
children: React.ReactNode;
|
|
9
11
|
/** Direction of stack */
|
|
10
12
|
direction?: 'vertical' | 'horizontal';
|
|
11
|
-
/** Spacing between items */
|
|
12
|
-
spacing?:
|
|
13
|
+
/** Spacing between items (alias: gap) */
|
|
14
|
+
spacing?: SpacingValue;
|
|
15
|
+
/** Spacing between items (alias for spacing - for developer convenience) */
|
|
16
|
+
gap?: SpacingValue;
|
|
13
17
|
/** Alignment of items */
|
|
14
18
|
align?: 'start' | 'center' | 'end' | 'stretch';
|
|
15
19
|
/** Justify content */
|
|
@@ -23,23 +27,45 @@ export interface StackProps {
|
|
|
23
27
|
/**
|
|
24
28
|
* Stack component for arranging children vertically or horizontally with consistent spacing.
|
|
25
29
|
*
|
|
26
|
-
*
|
|
30
|
+
* Supports ref forwarding for DOM access.
|
|
31
|
+
*
|
|
32
|
+
* Spacing scale (use either `spacing` or `gap` prop - they're aliases):
|
|
27
33
|
* - none: 0
|
|
28
34
|
* - xs: 0.5rem (2)
|
|
29
35
|
* - sm: 0.75rem (3)
|
|
30
36
|
* - md: 1.5rem (6)
|
|
31
37
|
* - lg: 2rem (8)
|
|
32
38
|
* - xl: 3rem (12)
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* // Using spacing prop
|
|
43
|
+
* <Stack spacing="md">
|
|
44
|
+
* <Card>Item 1</Card>
|
|
45
|
+
* <Card>Item 2</Card>
|
|
46
|
+
* </Stack>
|
|
47
|
+
*
|
|
48
|
+
* // Using gap prop (alias)
|
|
49
|
+
* <Stack gap="md">
|
|
50
|
+
* <Card>Item 1</Card>
|
|
51
|
+
* <Card>Item 2</Card>
|
|
52
|
+
* </Stack>
|
|
53
|
+
* ```
|
|
33
54
|
*/
|
|
34
|
-
export const Stack
|
|
55
|
+
export const Stack = forwardRef<HTMLDivElement, StackProps>(({
|
|
35
56
|
children,
|
|
36
57
|
direction = 'vertical',
|
|
37
|
-
spacing
|
|
58
|
+
spacing,
|
|
59
|
+
gap,
|
|
38
60
|
align = 'stretch',
|
|
39
61
|
justify = 'start',
|
|
40
62
|
wrap = false,
|
|
41
63
|
className = '',
|
|
42
|
-
|
|
64
|
+
...htmlProps
|
|
65
|
+
}, ref) => {
|
|
66
|
+
// Use gap as alias for spacing (spacing takes precedence if both provided)
|
|
67
|
+
const effectiveSpacing = spacing ?? gap ?? 'md';
|
|
68
|
+
|
|
43
69
|
const spacingClasses = {
|
|
44
70
|
vertical: {
|
|
45
71
|
none: '',
|
|
@@ -76,11 +102,13 @@ export const Stack: React.FC<StackProps> = ({
|
|
|
76
102
|
|
|
77
103
|
return (
|
|
78
104
|
<div
|
|
105
|
+
ref={ref}
|
|
106
|
+
{...htmlProps}
|
|
79
107
|
className={`
|
|
80
108
|
flex
|
|
81
109
|
${direction === 'vertical' ? 'flex-col' : 'flex-row'}
|
|
82
110
|
${wrap ? 'flex-wrap' : ''}
|
|
83
|
-
${spacingClasses[direction][
|
|
111
|
+
${spacingClasses[direction][effectiveSpacing]}
|
|
84
112
|
${alignClasses[align]}
|
|
85
113
|
${justifyClasses[justify]}
|
|
86
114
|
${className}
|
|
@@ -89,6 +117,8 @@ export const Stack: React.FC<StackProps> = ({
|
|
|
89
117
|
{children}
|
|
90
118
|
</div>
|
|
91
119
|
);
|
|
92
|
-
};
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
Stack.displayName = 'Stack';
|
|
93
123
|
|
|
94
124
|
export default Stack;
|