@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.
Files changed (52) 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/Stack.d.ts +25 -5
  18. package/dist/components/Stack.d.ts.map +1 -1
  19. package/dist/components/Text.d.ts +20 -4
  20. package/dist/components/Text.d.ts.map +1 -1
  21. package/dist/components/Textarea.d.ts +2 -0
  22. package/dist/components/Textarea.d.ts.map +1 -1
  23. package/dist/components/index.d.ts +1 -3
  24. package/dist/components/index.d.ts.map +1 -1
  25. package/dist/index.d.ts +110 -48
  26. package/dist/index.esm.js +144 -138
  27. package/dist/index.esm.js.map +1 -1
  28. package/dist/index.js +143 -138
  29. package/dist/index.js.map +1 -1
  30. package/dist/styles.css +8 -51
  31. package/package.json +1 -1
  32. package/src/components/Box.stories.tsx +377 -0
  33. package/src/components/Box.tsx +8 -4
  34. package/src/components/Button.tsx +23 -10
  35. package/src/components/Card.tsx +20 -5
  36. package/src/components/DataTable.stories.tsx +36 -25
  37. package/src/components/DataTable.tsx +95 -5
  38. package/src/components/EmptyState.stories.tsx +124 -72
  39. package/src/components/EmptyState.tsx +10 -0
  40. package/src/components/Grid.stories.tsx +348 -0
  41. package/src/components/Grid.tsx +12 -5
  42. package/src/components/Input.tsx +12 -2
  43. package/src/components/MultiSelect.tsx +41 -10
  44. package/src/components/Stack.stories.tsx +24 -1
  45. package/src/components/Stack.tsx +40 -10
  46. package/src/components/Text.stories.tsx +273 -0
  47. package/src/components/Text.tsx +33 -8
  48. package/src/components/Textarea.tsx +32 -21
  49. package/src/components/index.ts +1 -4
  50. package/dist/components/Table.d.ts +0 -26
  51. package/dist/components/Table.d.ts.map +0 -1
  52. 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;
@@ -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
+ };
@@ -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
- export interface StackProps {
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?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
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
- * Spacing scale:
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: React.FC<StackProps> = ({
55
+ export const Stack = forwardRef<HTMLDivElement, StackProps>(({
35
56
  children,
36
57
  direction = 'vertical',
37
- spacing = 'md',
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][spacing]}
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;