@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.
Files changed (55) hide show
  1. package/dist/components/Box.d.ts +2 -1
  2. package/dist/components/Box.d.ts.map +1 -1
  3. package/dist/components/Button.d.ts +10 -1
  4. package/dist/components/Button.d.ts.map +1 -1
  5. package/dist/components/Card.d.ts +11 -2
  6. package/dist/components/Card.d.ts.map +1 -1
  7. package/dist/components/DataTable.d.ts +17 -3
  8. package/dist/components/DataTable.d.ts.map +1 -1
  9. package/dist/components/EmptyState.d.ts +3 -1
  10. package/dist/components/EmptyState.d.ts.map +1 -1
  11. package/dist/components/Grid.d.ts +4 -2
  12. package/dist/components/Grid.d.ts.map +1 -1
  13. package/dist/components/Input.d.ts +2 -0
  14. package/dist/components/Input.d.ts.map +1 -1
  15. package/dist/components/MultiSelect.d.ts +13 -1
  16. package/dist/components/MultiSelect.d.ts.map +1 -1
  17. package/dist/components/Spreadsheet.d.ts +5 -1
  18. package/dist/components/Spreadsheet.d.ts.map +1 -1
  19. package/dist/components/Stack.d.ts +25 -5
  20. package/dist/components/Stack.d.ts.map +1 -1
  21. package/dist/components/Text.d.ts +20 -4
  22. package/dist/components/Text.d.ts.map +1 -1
  23. package/dist/components/Textarea.d.ts +2 -0
  24. package/dist/components/Textarea.d.ts.map +1 -1
  25. package/dist/components/index.d.ts +1 -3
  26. package/dist/components/index.d.ts.map +1 -1
  27. package/dist/index.d.ts +115 -49
  28. package/dist/index.esm.js +187 -8563
  29. package/dist/index.esm.js.map +1 -1
  30. package/dist/index.js +186 -8563
  31. package/dist/index.js.map +1 -1
  32. package/dist/styles.css +8 -51
  33. package/package.json +4 -4
  34. package/src/components/Box.stories.tsx +377 -0
  35. package/src/components/Box.tsx +8 -4
  36. package/src/components/Button.tsx +23 -10
  37. package/src/components/Card.tsx +20 -5
  38. package/src/components/DataTable.stories.tsx +36 -25
  39. package/src/components/DataTable.tsx +95 -5
  40. package/src/components/EmptyState.stories.tsx +124 -72
  41. package/src/components/EmptyState.tsx +10 -0
  42. package/src/components/Grid.stories.tsx +348 -0
  43. package/src/components/Grid.tsx +12 -5
  44. package/src/components/Input.tsx +12 -2
  45. package/src/components/MultiSelect.tsx +41 -10
  46. package/src/components/Spreadsheet.tsx +8 -57
  47. package/src/components/Stack.stories.tsx +24 -1
  48. package/src/components/Stack.tsx +40 -10
  49. package/src/components/Text.stories.tsx +273 -0
  50. package/src/components/Text.tsx +33 -8
  51. package/src/components/Textarea.tsx +32 -21
  52. package/src/components/index.ts +1 -4
  53. package/dist/components/Table.d.ts +0 -26
  54. package/dist/components/Table.d.ts.map +0 -1
  55. 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
+ };
@@ -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: React.FC<GridProps> = ({
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;
@@ -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
- export default function MultiSelect({
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
- }: MultiSelectProps) {
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
- {selectedOptions.length > 0 && !disabled && (
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
- <ChevronDown className={`h-4 w-4 text-ink-500 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
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 { read, utils, writeFile, WorkBook } from 'xlsx';
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, Upload, Save } from 'lucide-react';
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
- /** Enable Excel import */
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 (none: 0, xs: 0.25rem, sm: 0.5rem, md: 1rem, lg: 1.5rem, xl: 2rem)',
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
+ };