@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 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; // 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) => ReactNode; // Custom render function for cell content
240
- width?: string; // CSS width value (e.g., '100px', '20%', 'auto')
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
- { key: 'id', header: 'ID', width: '80px' },
269
- { key: 'name', header: 'Name', sortable: true },
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: (item) => <Badge variant={item.status === 'active' ? 'success' : 'default'}>{item.status}</Badge>
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**:
package/dist/Table.d.ts CHANGED
@@ -5,6 +5,7 @@ export interface Column<T> {
5
5
  sortable?: boolean;
6
6
  render?: (item: T) => React.ReactNode;
7
7
  width?: string;
8
+ truncate?: boolean;
8
9
  }
9
10
  export interface TableProps<T> {
10
11
  data: T[];