@stackloop/ui 1.0.7 → 1.0.9
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/README.md +163 -24
- package/dist/Table.d.ts +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +234 -222
- package/dist/index.js.map +1 -1
- package/dist/ui.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -233,14 +233,132 @@ You can add dark mode variants:
|
|
|
233
233
|
- **Column Interface:**
|
|
234
234
|
```typescript
|
|
235
235
|
interface Column<T> {
|
|
236
|
-
key: string;
|
|
237
|
-
header: string;
|
|
238
|
-
sortable?: boolean;
|
|
239
|
-
render?: (item: T) => ReactNode; // Custom render function
|
|
240
|
-
width?: string;
|
|
236
|
+
key: string; // Unique column identifier and default accessor key
|
|
237
|
+
header: string; // Column header text displayed in table header
|
|
238
|
+
sortable?: boolean; // Enable sorting for this column (default: false)
|
|
239
|
+
render?: (item: T) => React.ReactNode; // Custom render function - can return any valid React element
|
|
240
|
+
width?: string; // CSS width value (e.g., '100px', '20%', 'auto')
|
|
241
|
+
truncate?: boolean; // Enable text truncation with ellipsis for long content (default: false)
|
|
241
242
|
}
|
|
242
243
|
```
|
|
243
244
|
|
|
245
|
+
- **Custom Rendering with `render`:**
|
|
246
|
+
|
|
247
|
+
The `render` function accepts the current row item and can return **any React node**, including:
|
|
248
|
+
- JSX elements (buttons, badges, icons)
|
|
249
|
+
- Formatted strings or numbers
|
|
250
|
+
- Complex components with conditional logic
|
|
251
|
+
- Nested elements with multiple components
|
|
252
|
+
|
|
253
|
+
**Examples:**
|
|
254
|
+
|
|
255
|
+
```jsx
|
|
256
|
+
// Render Badge components
|
|
257
|
+
{
|
|
258
|
+
key: 'status',
|
|
259
|
+
header: 'Status',
|
|
260
|
+
render: (item) => (
|
|
261
|
+
<Badge variant={item.status === 'active' ? 'success' : 'danger'}>
|
|
262
|
+
{item.status}
|
|
263
|
+
</Badge>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Render action buttons
|
|
268
|
+
{
|
|
269
|
+
key: 'actions',
|
|
270
|
+
header: 'Actions',
|
|
271
|
+
render: (item) => (
|
|
272
|
+
<div className="flex gap-2">
|
|
273
|
+
<Button size="sm" onClick={() => handleEdit(item)}>Edit</Button>
|
|
274
|
+
<Button size="sm" variant="danger" onClick={() => handleDelete(item)}>Delete</Button>
|
|
275
|
+
</div>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Render images with fallback
|
|
280
|
+
{
|
|
281
|
+
key: 'avatar',
|
|
282
|
+
header: 'Avatar',
|
|
283
|
+
render: (user) => (
|
|
284
|
+
<img
|
|
285
|
+
src={user.avatar || '/default-avatar.png'}
|
|
286
|
+
alt={user.name}
|
|
287
|
+
className="w-10 h-10 rounded-full"
|
|
288
|
+
/>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Render formatted dates
|
|
293
|
+
{
|
|
294
|
+
key: 'createdAt',
|
|
295
|
+
header: 'Created',
|
|
296
|
+
render: (item) => new Date(item.createdAt).toLocaleDateString('en-US', {
|
|
297
|
+
year: 'numeric',
|
|
298
|
+
month: 'short',
|
|
299
|
+
day: 'numeric'
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Render icons with conditional colors
|
|
304
|
+
{
|
|
305
|
+
key: 'verified',
|
|
306
|
+
header: 'Verified',
|
|
307
|
+
render: (user) => user.verified ? (
|
|
308
|
+
<Check className="w-5 h-5 text-success" />
|
|
309
|
+
) : (
|
|
310
|
+
<X className="w-5 h-5 text-error" />
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Render multiple values combined
|
|
315
|
+
{
|
|
316
|
+
key: 'fullName',
|
|
317
|
+
header: 'User',
|
|
318
|
+
render: (user) => (
|
|
319
|
+
<div>
|
|
320
|
+
<div className="font-semibold">{user.firstName} {user.lastName}</div>
|
|
321
|
+
<div className="text-sm text-primary/60">{user.email}</div>
|
|
322
|
+
</div>
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Enable text truncation for long content
|
|
327
|
+
{
|
|
328
|
+
key: 'description',
|
|
329
|
+
header: 'Description',
|
|
330
|
+
truncate: true, // Adds ellipsis when text exceeds cell width
|
|
331
|
+
width: '300px' // Set explicit width to control truncation point
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
- **Text Truncation:**
|
|
336
|
+
- Set `truncate: true` on any column to automatically truncate long text with ellipsis (`...`) when content exceeds the cell width.
|
|
337
|
+
- Hovering over truncated cells shows a browser tooltip with the full text.
|
|
338
|
+
- **Must specify `width`** property (e.g., `'200px'`, `'30%'`, `'20rem'`) to define when truncation occurs.
|
|
339
|
+
- Without `width`, the cell will expand to fit content and truncation won't activate.
|
|
340
|
+
- Only applies to default cell rendering; custom `render` functions handle their own truncation.
|
|
341
|
+
|
|
342
|
+
**Example:**
|
|
343
|
+
```jsx
|
|
344
|
+
const columns = [
|
|
345
|
+
{ key: 'title', header: 'Title', sortable: true },
|
|
346
|
+
{
|
|
347
|
+
key: 'description',
|
|
348
|
+
header: 'Description',
|
|
349
|
+
truncate: true, // Enable ellipsis
|
|
350
|
+
width: '250px' // Required: defines max width before truncation
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
key: 'email',
|
|
354
|
+
header: 'Email',
|
|
355
|
+
truncate: true,
|
|
356
|
+
width: '200px'
|
|
357
|
+
},
|
|
358
|
+
{ key: 'author', header: 'Author' }
|
|
359
|
+
]
|
|
360
|
+
```
|
|
361
|
+
|
|
244
362
|
- **Sorting Behavior:**
|
|
245
363
|
- Click sortable column headers to toggle between ascending → descending → no sort.
|
|
246
364
|
- Sort icons: `ChevronUp` (ascending), `ChevronDown` (descending), `ChevronsUpDown` (sortable but not active).
|
|
@@ -258,19 +376,53 @@ You can add dark mode variants:
|
|
|
258
376
|
- Colors: Uses semantic color tokens (`border`, `border-dark`, `background`, `foreground-color`, `primary`).
|
|
259
377
|
- Animations: Powered by Framer Motion for smooth row entry and sorting transitions.
|
|
260
378
|
|
|
261
|
-
- **Usage:**
|
|
379
|
+
- **Complete Usage Example:**
|
|
262
380
|
|
|
263
381
|
```jsx
|
|
264
|
-
import { Table } from '@stackloop/ui'
|
|
382
|
+
import { Table, Badge, Button } from '@stackloop/ui'
|
|
383
|
+
import { Check, X, Edit, Trash2 } from 'lucide-react'
|
|
265
384
|
|
|
266
|
-
// Basic example
|
|
267
385
|
const columns = [
|
|
268
|
-
{
|
|
269
|
-
|
|
386
|
+
{
|
|
387
|
+
key: 'id',
|
|
388
|
+
header: 'ID',
|
|
389
|
+
width: '80px',
|
|
390
|
+
sortable: true
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
key: 'name',
|
|
394
|
+
header: 'Name',
|
|
395
|
+
sortable: true
|
|
396
|
+
},
|
|
270
397
|
{
|
|
271
398
|
key: 'status',
|
|
272
399
|
header: 'Status',
|
|
273
|
-
render: (
|
|
400
|
+
render: (user) => (
|
|
401
|
+
<Badge variant={user.status === 'active' ? 'success' : 'default'}>
|
|
402
|
+
{user.status}
|
|
403
|
+
</Badge>
|
|
404
|
+
)
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
key: 'verified',
|
|
408
|
+
header: 'Verified',
|
|
409
|
+
render: (user) => user.verified ?
|
|
410
|
+
<Check className="w-5 h-5 text-success" /> :
|
|
411
|
+
<X className="w-5 h-5 text-error" />
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
key: 'actions',
|
|
415
|
+
header: 'Actions',
|
|
416
|
+
render: (user) => (
|
|
417
|
+
<div className="flex gap-2">
|
|
418
|
+
<Button size="sm" icon={<Edit />} onClick={() => handleEdit(user)}>
|
|
419
|
+
Edit
|
|
420
|
+
</Button>
|
|
421
|
+
<Button size="sm" variant="danger" icon={<Trash2 />}>
|
|
422
|
+
Delete
|
|
423
|
+
</Button>
|
|
424
|
+
</div>
|
|
425
|
+
)
|
|
274
426
|
}
|
|
275
427
|
]
|
|
276
428
|
|
|
@@ -281,19 +433,6 @@ You can add dark mode variants:
|
|
|
281
433
|
onRowClick={(user) => navigate(`/users/${user.id}`)}
|
|
282
434
|
loading={isLoading}
|
|
283
435
|
/>
|
|
284
|
-
|
|
285
|
-
// Advanced example with custom rendering
|
|
286
|
-
const columns = [
|
|
287
|
-
{ key: 'avatar', header: '', width: '50px', render: (u) => <img src={u.avatar} /> },
|
|
288
|
-
{ key: 'name', header: 'Full Name', sortable: true },
|
|
289
|
-
{ key: 'email', header: 'Email Address', sortable: true },
|
|
290
|
-
{
|
|
291
|
-
key: 'createdAt',
|
|
292
|
-
header: 'Joined',
|
|
293
|
-
sortable: true,
|
|
294
|
-
render: (u) => new Date(u.createdAt).toLocaleDateString()
|
|
295
|
-
}
|
|
296
|
-
]
|
|
297
436
|
```
|
|
298
437
|
|
|
299
438
|
**Dropdown**:
|