@papernote/ui 1.2.0 → 1.3.1
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/Spreadsheet.d.ts +5 -1
- package/dist/components/Spreadsheet.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 +115 -49
- package/dist/index.esm.js +187 -8563
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +186 -8563
- package/dist/index.js.map +1 -1
- package/dist/styles.css +8 -51
- package/package.json +4 -4
- 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/Spreadsheet.tsx +8 -57
- 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;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React, { useState, useCallback } from 'react';
|
|
2
2
|
import BaseSpreadsheet, { CellBase, Matrix } from 'react-spreadsheet';
|
|
3
|
-
import {
|
|
3
|
+
import { utils, writeFile } from 'xlsx';
|
|
4
4
|
import Button from './Button';
|
|
5
5
|
import Card, { CardHeader, CardTitle, CardContent } from './Card';
|
|
6
6
|
import Stack from './Stack';
|
|
7
|
-
import { Download,
|
|
7
|
+
import { Download, Save } from 'lucide-react';
|
|
8
8
|
import { addSuccessMessage, addErrorMessage } from './StatusBar';
|
|
9
9
|
import './Spreadsheet.css';
|
|
10
10
|
|
|
@@ -39,7 +39,11 @@ export interface SpreadsheetProps {
|
|
|
39
39
|
rowLabels?: string[];
|
|
40
40
|
/** Show toolbar with actions */
|
|
41
41
|
showToolbar?: boolean;
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Enable Excel import
|
|
44
|
+
* @deprecated Excel import has been disabled due to security vulnerabilities in the xlsx library.
|
|
45
|
+
* This prop is kept for API compatibility but has no effect.
|
|
46
|
+
*/
|
|
43
47
|
enableImport?: boolean;
|
|
44
48
|
/** Enable Excel export */
|
|
45
49
|
enableExport?: boolean;
|
|
@@ -123,7 +127,7 @@ export const Spreadsheet: React.FC<SpreadsheetProps> = ({
|
|
|
123
127
|
columnLabels,
|
|
124
128
|
rowLabels,
|
|
125
129
|
showToolbar = false,
|
|
126
|
-
enableImport = false,
|
|
130
|
+
enableImport: _enableImport = false, // Deprecated - kept for API compatibility
|
|
127
131
|
enableExport = false,
|
|
128
132
|
enableSave = false,
|
|
129
133
|
onSave,
|
|
@@ -153,44 +157,6 @@ export const Spreadsheet: React.FC<SpreadsheetProps> = ({
|
|
|
153
157
|
[onChange]
|
|
154
158
|
);
|
|
155
159
|
|
|
156
|
-
// Handle Excel import
|
|
157
|
-
const handleImport = useCallback(
|
|
158
|
-
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
159
|
-
const file = event.target.files?.[0];
|
|
160
|
-
if (!file) return;
|
|
161
|
-
|
|
162
|
-
const reader = new FileReader();
|
|
163
|
-
reader.onload = (e) => {
|
|
164
|
-
try {
|
|
165
|
-
const workbook: WorkBook = read(e.target?.result, { type: 'binary' });
|
|
166
|
-
const sheetName = workbook.SheetNames[0];
|
|
167
|
-
const worksheet = workbook.Sheets[sheetName];
|
|
168
|
-
|
|
169
|
-
// Convert to array of arrays
|
|
170
|
-
const jsonData: any[][] = utils.sheet_to_json(worksheet, { header: 1 });
|
|
171
|
-
|
|
172
|
-
// Convert to spreadsheet format
|
|
173
|
-
const spreadsheetData: Matrix<SpreadsheetCell> = jsonData.map(row =>
|
|
174
|
-
row.map(cell => ({
|
|
175
|
-
value: cell,
|
|
176
|
-
}))
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
handleChange(spreadsheetData);
|
|
180
|
-
addSuccessMessage('Excel file imported successfully');
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.error('Error importing Excel file:', error);
|
|
183
|
-
addErrorMessage('Failed to import Excel file');
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
reader.readAsBinaryString(file);
|
|
187
|
-
|
|
188
|
-
// Reset input
|
|
189
|
-
event.target.value = '';
|
|
190
|
-
},
|
|
191
|
-
[handleChange]
|
|
192
|
-
);
|
|
193
|
-
|
|
194
160
|
// Handle Excel export
|
|
195
161
|
const handleExport = useCallback(() => {
|
|
196
162
|
try {
|
|
@@ -238,20 +204,6 @@ export const Spreadsheet: React.FC<SpreadsheetProps> = ({
|
|
|
238
204
|
<Stack direction="horizontal" spacing="md" align="center" className="mb-4">
|
|
239
205
|
{title && <div className="text-lg font-medium text-ink-900 flex-1">{title}</div>}
|
|
240
206
|
|
|
241
|
-
{enableImport && (
|
|
242
|
-
<label>
|
|
243
|
-
<input
|
|
244
|
-
type="file"
|
|
245
|
-
accept=".xlsx,.xls,.csv"
|
|
246
|
-
onChange={handleImport}
|
|
247
|
-
className="hidden"
|
|
248
|
-
/>
|
|
249
|
-
<Button variant="ghost" size="sm" icon={<Upload className="h-4 w-4" />}>
|
|
250
|
-
Import
|
|
251
|
-
</Button>
|
|
252
|
-
</label>
|
|
253
|
-
)}
|
|
254
|
-
|
|
255
207
|
{enableExport && (
|
|
256
208
|
<Button
|
|
257
209
|
variant="ghost"
|
|
@@ -342,7 +294,6 @@ export const SpreadsheetReport: React.FC<
|
|
|
342
294
|
<Spreadsheet
|
|
343
295
|
{...props}
|
|
344
296
|
showToolbar
|
|
345
|
-
enableImport
|
|
346
297
|
enableExport
|
|
347
298
|
enableSave
|
|
348
299
|
wrapInCard
|
|
@@ -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
|
+
};
|