create-manifest 1.3.3 → 2.0.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.
- package/README.md +39 -24
- package/index.js +51 -0
- package/package.json +11 -89
- package/starter/.claude/settings.local.json +21 -0
- package/starter/.env.example +1 -0
- package/starter/@/components/table.tsx +478 -0
- package/starter/@/components/ui/button.tsx +62 -0
- package/starter/@/components/ui/checkbox.tsx +30 -0
- package/starter/README-DEV.md +167 -0
- package/starter/components.json +24 -0
- package/starter/package.json +42 -0
- package/starter/src/flows/list-pokemons.flow.ts +131 -0
- package/starter/src/server.ts +165 -0
- package/starter/src/web/PokemonList.tsx +125 -0
- package/starter/src/web/components/blog-post-card.tsx +286 -0
- package/starter/src/web/components/blog-post-list.tsx +291 -0
- package/starter/src/web/components/ui/.gitkeep +0 -0
- package/starter/src/web/components/ui/button.tsx +62 -0
- package/starter/src/web/globals.css +98 -0
- package/starter/src/web/hooks/.gitkeep +0 -0
- package/starter/src/web/lib/utils.ts +6 -0
- package/starter/src/web/root.tsx +36 -0
- package/starter/src/web/tsconfig.json +3 -0
- package/starter/tsconfig.json +21 -0
- package/starter/tsconfig.web.json +24 -0
- package/starter/vite.config.ts +36 -0
- package/assets/monorepo/README.md +0 -51
- package/assets/monorepo/api-package.json +0 -9
- package/assets/monorepo/api-readme.md +0 -49
- package/assets/monorepo/manifest.yml +0 -34
- package/assets/monorepo/root-package.json +0 -15
- package/assets/monorepo/web-package.json +0 -10
- package/assets/monorepo/web-readme.md +0 -9
- package/assets/standalone/README.md +0 -49
- package/assets/standalone/api-package.json +0 -9
- package/assets/standalone/manifest.yml +0 -34
- package/bin/dev.cmd +0 -3
- package/bin/dev.js +0 -5
- package/bin/run.cmd +0 -3
- package/bin/run.js +0 -5
- package/dist/commands/index.d.ts +0 -65
- package/dist/commands/index.js +0 -480
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/utils/GetBackendFileContent.d.ts +0 -1
- package/dist/utils/GetBackendFileContent.js +0 -21
- package/dist/utils/GetLatestPackageVersion.d.ts +0 -1
- package/dist/utils/GetLatestPackageVersion.js +0 -5
- package/dist/utils/UpdateExtensionJsonFile.d.ts +0 -6
- package/dist/utils/UpdateExtensionJsonFile.js +0 -8
- package/dist/utils/UpdatePackageJsonFile.d.ts +0 -18
- package/dist/utils/UpdatePackageJsonFile.js +0 -21
- package/dist/utils/UpdateSettingsJsonFile.d.ts +0 -4
- package/dist/utils/UpdateSettingsJsonFile.js +0 -6
- package/dist/utils/helpers.d.ts +0 -1
- package/dist/utils/helpers.js +0 -11
- package/oclif.manifest.json +0 -47
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
import { Check, ChevronDown, ChevronUp, Download, Minus, Send } from 'lucide-react'
|
|
6
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
7
|
+
|
|
8
|
+
export interface TableColumn<T = Record<string, unknown>> {
|
|
9
|
+
header: string
|
|
10
|
+
accessor: keyof T | string
|
|
11
|
+
sortable?: boolean
|
|
12
|
+
width?: string
|
|
13
|
+
align?: 'left' | 'center' | 'right'
|
|
14
|
+
render?: (value: unknown, row: T, index: number) => React.ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TableProps<T = Record<string, unknown>> {
|
|
18
|
+
columns?: TableColumn<T>[]
|
|
19
|
+
data?: T[]
|
|
20
|
+
selectable?: 'none' | 'single' | 'multi'
|
|
21
|
+
onSelectionChange?: (selectedRows: T[]) => void
|
|
22
|
+
loading?: boolean
|
|
23
|
+
emptyMessage?: string
|
|
24
|
+
stickyHeader?: boolean
|
|
25
|
+
compact?: boolean
|
|
26
|
+
selectedRows?: T[]
|
|
27
|
+
showActions?: boolean
|
|
28
|
+
onDownload?: (selectedRows: T[]) => void
|
|
29
|
+
onSend?: (selectedRows: T[]) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Default demo data for the table
|
|
33
|
+
const defaultColumns: TableColumn[] = [
|
|
34
|
+
{ header: 'Model', accessor: 'model', sortable: true },
|
|
35
|
+
{
|
|
36
|
+
header: 'Input (w/ Cache)',
|
|
37
|
+
accessor: 'inputCache',
|
|
38
|
+
sortable: true,
|
|
39
|
+
align: 'right'
|
|
40
|
+
},
|
|
41
|
+
{ header: 'Output', accessor: 'output', sortable: true, align: 'right' },
|
|
42
|
+
{
|
|
43
|
+
header: 'Total Tokens',
|
|
44
|
+
accessor: 'totalTokens',
|
|
45
|
+
sortable: true,
|
|
46
|
+
align: 'right'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
header: 'API Cost',
|
|
50
|
+
accessor: 'apiCost',
|
|
51
|
+
sortable: true,
|
|
52
|
+
align: 'right',
|
|
53
|
+
render: (value) => `$${(value as number).toFixed(2)}`
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const defaultData = [
|
|
58
|
+
{
|
|
59
|
+
model: 'gpt-5',
|
|
60
|
+
inputCache: 0,
|
|
61
|
+
output: 103271,
|
|
62
|
+
totalTokens: 2267482,
|
|
63
|
+
apiCost: 0.0
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
model: 'claude-3.5-sonnet',
|
|
67
|
+
inputCache: 176177,
|
|
68
|
+
output: 8326,
|
|
69
|
+
totalTokens: 647528,
|
|
70
|
+
apiCost: 1.0
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
model: 'gemini-2.0-flash-exp',
|
|
74
|
+
inputCache: 176100,
|
|
75
|
+
output: 8326,
|
|
76
|
+
totalTokens: 647528,
|
|
77
|
+
apiCost: 0.0
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
model: 'gemini-2.5-pro',
|
|
81
|
+
inputCache: 176177,
|
|
82
|
+
output: 7000,
|
|
83
|
+
totalTokens: 647528,
|
|
84
|
+
apiCost: 0.0
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
model: 'claude-4-sonnet',
|
|
88
|
+
inputCache: 68415,
|
|
89
|
+
output: 12769,
|
|
90
|
+
totalTokens: 946536,
|
|
91
|
+
apiCost: 0.71
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
function SkeletonRow({
|
|
96
|
+
columns,
|
|
97
|
+
compact
|
|
98
|
+
}: {
|
|
99
|
+
columns: number
|
|
100
|
+
compact?: boolean
|
|
101
|
+
}) {
|
|
102
|
+
return (
|
|
103
|
+
<tr className="border-b border-border">
|
|
104
|
+
{Array.from({ length: columns }).map((_, i) => (
|
|
105
|
+
<td key={i} className={cn('px-3', compact ? 'py-2' : 'py-3')}>
|
|
106
|
+
<div className="h-4 bg-muted animate-pulse rounded" />
|
|
107
|
+
</td>
|
|
108
|
+
))}
|
|
109
|
+
</tr>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function Table<T extends Record<string, unknown>>({
|
|
114
|
+
columns = defaultColumns as unknown as TableColumn<T>[],
|
|
115
|
+
data = defaultData as unknown as T[],
|
|
116
|
+
selectable = 'none',
|
|
117
|
+
onSelectionChange,
|
|
118
|
+
loading = false,
|
|
119
|
+
emptyMessage = 'No data available',
|
|
120
|
+
stickyHeader = false,
|
|
121
|
+
compact = false,
|
|
122
|
+
selectedRows: controlledSelectedRows,
|
|
123
|
+
showActions = false,
|
|
124
|
+
onDownload,
|
|
125
|
+
onSend
|
|
126
|
+
}: TableProps<T>) {
|
|
127
|
+
const [sortConfig, setSortConfig] = useState<{
|
|
128
|
+
key: string
|
|
129
|
+
direction: 'asc' | 'desc'
|
|
130
|
+
} | null>(null)
|
|
131
|
+
const [internalSelectedRows, setInternalSelectedRows] = useState<Set<number>>(
|
|
132
|
+
new Set()
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const selectedRowsSet = controlledSelectedRows
|
|
136
|
+
? new Set(controlledSelectedRows.map((row) => data.indexOf(row)))
|
|
137
|
+
: internalSelectedRows
|
|
138
|
+
|
|
139
|
+
const handleSort = useCallback((accessor: string) => {
|
|
140
|
+
setSortConfig((current) => {
|
|
141
|
+
if (current?.key === accessor) {
|
|
142
|
+
if (current.direction === 'asc') {
|
|
143
|
+
return { key: accessor, direction: 'desc' }
|
|
144
|
+
}
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
return { key: accessor, direction: 'asc' }
|
|
148
|
+
})
|
|
149
|
+
}, [])
|
|
150
|
+
|
|
151
|
+
const sortedData = useMemo(() => {
|
|
152
|
+
if (!sortConfig) return data
|
|
153
|
+
|
|
154
|
+
return [...data].sort((a, b) => {
|
|
155
|
+
const aValue = a[sortConfig.key as keyof T]
|
|
156
|
+
const bValue = b[sortConfig.key as keyof T]
|
|
157
|
+
|
|
158
|
+
if (aValue === bValue) return 0
|
|
159
|
+
|
|
160
|
+
let comparison = 0
|
|
161
|
+
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
|
162
|
+
comparison = aValue - bValue
|
|
163
|
+
} else {
|
|
164
|
+
comparison = String(aValue).localeCompare(String(bValue))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return sortConfig.direction === 'asc' ? comparison : -comparison
|
|
168
|
+
})
|
|
169
|
+
}, [data, sortConfig])
|
|
170
|
+
|
|
171
|
+
const handleRowSelect = useCallback(
|
|
172
|
+
(index: number) => {
|
|
173
|
+
if (selectable === 'none') return
|
|
174
|
+
|
|
175
|
+
const newSelected = new Set(selectedRowsSet)
|
|
176
|
+
|
|
177
|
+
if (selectable === 'single') {
|
|
178
|
+
if (newSelected.has(index)) {
|
|
179
|
+
newSelected.clear()
|
|
180
|
+
} else {
|
|
181
|
+
newSelected.clear()
|
|
182
|
+
newSelected.add(index)
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
if (newSelected.has(index)) {
|
|
186
|
+
newSelected.delete(index)
|
|
187
|
+
} else {
|
|
188
|
+
newSelected.add(index)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
setInternalSelectedRows(newSelected)
|
|
193
|
+
onSelectionChange?.(sortedData.filter((_, i) => newSelected.has(i)))
|
|
194
|
+
},
|
|
195
|
+
[selectable, selectedRowsSet, sortedData, onSelectionChange]
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
const handleSelectAll = useCallback(() => {
|
|
199
|
+
if (selectable !== 'multi') return
|
|
200
|
+
|
|
201
|
+
const allSelected = selectedRowsSet.size === sortedData.length
|
|
202
|
+
const newSelected = allSelected
|
|
203
|
+
? new Set<number>()
|
|
204
|
+
: new Set(sortedData.map((_, i) => i))
|
|
205
|
+
|
|
206
|
+
setInternalSelectedRows(newSelected)
|
|
207
|
+
onSelectionChange?.(allSelected ? [] : sortedData)
|
|
208
|
+
}, [selectable, selectedRowsSet.size, sortedData, onSelectionChange])
|
|
209
|
+
|
|
210
|
+
const getValue = (row: T, accessor: string): unknown => {
|
|
211
|
+
const keys = accessor.split('.')
|
|
212
|
+
let value: unknown = row
|
|
213
|
+
for (const key of keys) {
|
|
214
|
+
value = (value as Record<string, unknown>)?.[key]
|
|
215
|
+
}
|
|
216
|
+
return value
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const formatNumber = (value: unknown): string => {
|
|
220
|
+
if (typeof value === 'number') {
|
|
221
|
+
return new Intl.NumberFormat('en-US').format(value)
|
|
222
|
+
}
|
|
223
|
+
return String(value ?? '')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const getSortIcon = (accessor: string) => {
|
|
227
|
+
if (sortConfig?.key !== accessor) {
|
|
228
|
+
return <Minus className="h-3 w-3 opacity-0 group-hover:opacity-30" />
|
|
229
|
+
}
|
|
230
|
+
return sortConfig.direction === 'asc' ? (
|
|
231
|
+
<ChevronUp className="h-3 w-3" />
|
|
232
|
+
) : (
|
|
233
|
+
<ChevronDown className="h-3 w-3" />
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="w-full">
|
|
239
|
+
{/* Mobile: Card view */}
|
|
240
|
+
<div className="sm:hidden space-y-2">
|
|
241
|
+
{loading ? (
|
|
242
|
+
Array.from({ length: 3 }).map((_, i) => (
|
|
243
|
+
<div key={i} className="rounded-md sm:rounded-lg border bg-card p-3 space-y-2">
|
|
244
|
+
{columns.slice(0, 4).map((_, j) => (
|
|
245
|
+
<div
|
|
246
|
+
key={j}
|
|
247
|
+
className="h-4 bg-muted animate-pulse rounded w-3/4"
|
|
248
|
+
/>
|
|
249
|
+
))}
|
|
250
|
+
</div>
|
|
251
|
+
))
|
|
252
|
+
) : sortedData.length === 0 ? (
|
|
253
|
+
<div className="rounded-md sm:rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
|
|
254
|
+
{emptyMessage}
|
|
255
|
+
</div>
|
|
256
|
+
) : (
|
|
257
|
+
sortedData.map((row, rowIndex) => (
|
|
258
|
+
<button
|
|
259
|
+
key={rowIndex}
|
|
260
|
+
type="button"
|
|
261
|
+
onClick={() => handleRowSelect(rowIndex)}
|
|
262
|
+
disabled={selectable === 'none'}
|
|
263
|
+
className={cn(
|
|
264
|
+
'w-full rounded-md sm:rounded-lg border bg-card p-3 text-left transition-all',
|
|
265
|
+
selectable !== 'none' &&
|
|
266
|
+
'cursor-pointer hover:border-foreground/30',
|
|
267
|
+
selectedRowsSet.has(rowIndex) &&
|
|
268
|
+
'border-foreground ring-1 ring-foreground'
|
|
269
|
+
)}
|
|
270
|
+
>
|
|
271
|
+
<div className="space-y-1.5">
|
|
272
|
+
{columns.map((column, colIndex) => {
|
|
273
|
+
const value = getValue(row, column.accessor as string)
|
|
274
|
+
const displayValue = column.render
|
|
275
|
+
? column.render(value, row, rowIndex)
|
|
276
|
+
: formatNumber(value)
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<div
|
|
280
|
+
key={colIndex}
|
|
281
|
+
className="flex justify-between items-center"
|
|
282
|
+
>
|
|
283
|
+
<span className="text-xs text-muted-foreground">
|
|
284
|
+
{column.header}
|
|
285
|
+
</span>
|
|
286
|
+
<span
|
|
287
|
+
className={cn(
|
|
288
|
+
'text-xs font-medium',
|
|
289
|
+
colIndex === 0 && 'font-semibold'
|
|
290
|
+
)}
|
|
291
|
+
>
|
|
292
|
+
{displayValue}
|
|
293
|
+
</span>
|
|
294
|
+
</div>
|
|
295
|
+
)
|
|
296
|
+
})}
|
|
297
|
+
</div>
|
|
298
|
+
</button>
|
|
299
|
+
))
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
{/* Desktop: Table view */}
|
|
304
|
+
<div className="hidden sm:block overflow-x-auto rounded-md sm:rounded-lg">
|
|
305
|
+
<table className="w-full text-sm" role="grid">
|
|
306
|
+
<thead
|
|
307
|
+
className={cn(
|
|
308
|
+
'border-b bg-muted/50',
|
|
309
|
+
stickyHeader && 'sticky top-0 z-10'
|
|
310
|
+
)}
|
|
311
|
+
>
|
|
312
|
+
<tr>
|
|
313
|
+
{selectable === 'multi' && (
|
|
314
|
+
<th className={cn('w-10 px-3', compact ? 'py-2' : 'py-3')}>
|
|
315
|
+
<button
|
|
316
|
+
type="button"
|
|
317
|
+
onClick={handleSelectAll}
|
|
318
|
+
className={cn(
|
|
319
|
+
'flex h-4 w-4 items-center justify-center rounded border transition-colors',
|
|
320
|
+
selectedRowsSet.size === sortedData.length &&
|
|
321
|
+
sortedData.length > 0
|
|
322
|
+
? 'bg-foreground border-foreground text-background'
|
|
323
|
+
: 'border-border hover:border-foreground/50'
|
|
324
|
+
)}
|
|
325
|
+
aria-label="Select all rows"
|
|
326
|
+
>
|
|
327
|
+
{selectedRowsSet.size === sortedData.length &&
|
|
328
|
+
sortedData.length > 0 && <Check className="h-3 w-3" />}
|
|
329
|
+
</button>
|
|
330
|
+
</th>
|
|
331
|
+
)}
|
|
332
|
+
{selectable === 'single' && (
|
|
333
|
+
<th className={cn('w-10 px-3', compact ? 'py-2' : 'py-3')} />
|
|
334
|
+
)}
|
|
335
|
+
{columns.map((column, index) => (
|
|
336
|
+
<th
|
|
337
|
+
key={index}
|
|
338
|
+
className={cn(
|
|
339
|
+
'px-3 font-medium text-muted-foreground group text-left',
|
|
340
|
+
compact ? 'py-2' : 'py-3',
|
|
341
|
+
column.align === 'right' && 'text-right',
|
|
342
|
+
column.sortable &&
|
|
343
|
+
'cursor-pointer select-none hover:text-foreground'
|
|
344
|
+
)}
|
|
345
|
+
style={{ width: column.width }}
|
|
346
|
+
onClick={() =>
|
|
347
|
+
column.sortable && handleSort(column.accessor as string)
|
|
348
|
+
}
|
|
349
|
+
role={
|
|
350
|
+
column.sortable ? 'columnheader button' : 'columnheader'
|
|
351
|
+
}
|
|
352
|
+
aria-sort={
|
|
353
|
+
sortConfig?.key === column.accessor
|
|
354
|
+
? sortConfig.direction === 'asc'
|
|
355
|
+
? 'ascending'
|
|
356
|
+
: 'descending'
|
|
357
|
+
: undefined
|
|
358
|
+
}
|
|
359
|
+
>
|
|
360
|
+
<span
|
|
361
|
+
className={cn(
|
|
362
|
+
'inline-flex items-center gap-1',
|
|
363
|
+
column.align === 'right' && 'justify-end'
|
|
364
|
+
)}
|
|
365
|
+
>
|
|
366
|
+
{column.header}
|
|
367
|
+
{column.sortable && getSortIcon(column.accessor as string)}
|
|
368
|
+
</span>
|
|
369
|
+
</th>
|
|
370
|
+
))}
|
|
371
|
+
</tr>
|
|
372
|
+
</thead>
|
|
373
|
+
<tbody>
|
|
374
|
+
{loading ? (
|
|
375
|
+
Array.from({ length: 5 }).map((_, i) => (
|
|
376
|
+
<SkeletonRow
|
|
377
|
+
key={i}
|
|
378
|
+
columns={columns.length + (selectable !== 'none' ? 1 : 0)}
|
|
379
|
+
compact={compact}
|
|
380
|
+
/>
|
|
381
|
+
))
|
|
382
|
+
) : sortedData.length === 0 ? (
|
|
383
|
+
<tr>
|
|
384
|
+
<td
|
|
385
|
+
colSpan={columns.length + (selectable !== 'none' ? 1 : 0)}
|
|
386
|
+
className="px-3 py-8 text-center text-muted-foreground"
|
|
387
|
+
>
|
|
388
|
+
{emptyMessage}
|
|
389
|
+
</td>
|
|
390
|
+
</tr>
|
|
391
|
+
) : (
|
|
392
|
+
sortedData.map((row, rowIndex) => (
|
|
393
|
+
<tr
|
|
394
|
+
key={rowIndex}
|
|
395
|
+
onClick={() => handleRowSelect(rowIndex)}
|
|
396
|
+
className={cn(
|
|
397
|
+
'border-b border-border last:border-0 transition-colors',
|
|
398
|
+
selectable !== 'none' && 'cursor-pointer hover:bg-muted/30'
|
|
399
|
+
)}
|
|
400
|
+
role="row"
|
|
401
|
+
aria-selected={selectedRowsSet.has(rowIndex)}
|
|
402
|
+
>
|
|
403
|
+
{selectable !== 'none' && (
|
|
404
|
+
<td className={cn('px-3', compact ? 'py-2' : 'py-3')}>
|
|
405
|
+
<div
|
|
406
|
+
className={cn(
|
|
407
|
+
'flex h-4 w-4 items-center justify-center rounded border transition-colors',
|
|
408
|
+
selectedRowsSet.has(rowIndex)
|
|
409
|
+
? 'bg-foreground border-foreground text-background'
|
|
410
|
+
: 'border-border'
|
|
411
|
+
)}
|
|
412
|
+
>
|
|
413
|
+
{selectedRowsSet.has(rowIndex) && (
|
|
414
|
+
<Check className="h-3 w-3" />
|
|
415
|
+
)}
|
|
416
|
+
</div>
|
|
417
|
+
</td>
|
|
418
|
+
)}
|
|
419
|
+
{columns.map((column, colIndex) => {
|
|
420
|
+
const value = getValue(row, column.accessor as string)
|
|
421
|
+
const displayValue = column.render
|
|
422
|
+
? column.render(value, row, rowIndex)
|
|
423
|
+
: formatNumber(value)
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<td
|
|
427
|
+
key={colIndex}
|
|
428
|
+
className={cn(
|
|
429
|
+
'px-3',
|
|
430
|
+
compact ? 'py-2' : 'py-3',
|
|
431
|
+
column.align === 'center' && 'text-center',
|
|
432
|
+
column.align === 'right' && 'text-right',
|
|
433
|
+
colIndex === 0 && 'font-medium'
|
|
434
|
+
)}
|
|
435
|
+
>
|
|
436
|
+
{displayValue}
|
|
437
|
+
</td>
|
|
438
|
+
)
|
|
439
|
+
})}
|
|
440
|
+
</tr>
|
|
441
|
+
))
|
|
442
|
+
)}
|
|
443
|
+
</tbody>
|
|
444
|
+
</table>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
{/* Action buttons for multi-select */}
|
|
448
|
+
{showActions && selectable === 'multi' && (
|
|
449
|
+
<div className="mt-3 flex items-center justify-between border-t pt-3">
|
|
450
|
+
<span className="text-sm text-muted-foreground">
|
|
451
|
+
{selectedRowsSet.size > 0
|
|
452
|
+
? `${selectedRowsSet.size} item${selectedRowsSet.size > 1 ? 's' : ''} selected`
|
|
453
|
+
: 'Select items'}
|
|
454
|
+
</span>
|
|
455
|
+
<div className="flex gap-2">
|
|
456
|
+
<Button
|
|
457
|
+
variant="outline"
|
|
458
|
+
size="sm"
|
|
459
|
+
disabled={selectedRowsSet.size === 0}
|
|
460
|
+
onClick={() => onDownload?.(sortedData.filter((_, i) => selectedRowsSet.has(i)))}
|
|
461
|
+
>
|
|
462
|
+
<Download className="mr-1.5 h-3.5 w-3.5" />
|
|
463
|
+
Download
|
|
464
|
+
</Button>
|
|
465
|
+
<Button
|
|
466
|
+
size="sm"
|
|
467
|
+
disabled={selectedRowsSet.size === 0}
|
|
468
|
+
onClick={() => onSend?.(sortedData.filter((_, i) => selectedRowsSet.has(i)))}
|
|
469
|
+
>
|
|
470
|
+
<Send className="mr-1.5 h-3.5 w-3.5" />
|
|
471
|
+
Send
|
|
472
|
+
</Button>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
)}
|
|
476
|
+
</div>
|
|
477
|
+
)
|
|
478
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
|
+
destructive:
|
|
14
|
+
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
15
|
+
outline:
|
|
16
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
17
|
+
secondary:
|
|
18
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
|
+
ghost:
|
|
20
|
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
25
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
26
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
27
|
+
icon: "size-9",
|
|
28
|
+
"icon-sm": "size-8",
|
|
29
|
+
"icon-lg": "size-10",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: "default",
|
|
34
|
+
size: "default",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
function Button({
|
|
40
|
+
className,
|
|
41
|
+
variant = "default",
|
|
42
|
+
size = "default",
|
|
43
|
+
asChild = false,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<"button"> &
|
|
46
|
+
VariantProps<typeof buttonVariants> & {
|
|
47
|
+
asChild?: boolean
|
|
48
|
+
}) {
|
|
49
|
+
const Comp = asChild ? Slot : "button"
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Comp
|
|
53
|
+
data-slot="button"
|
|
54
|
+
data-variant={variant}
|
|
55
|
+
data-size={size}
|
|
56
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
3
|
+
import { CheckIcon } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
function Checkbox({
|
|
8
|
+
className,
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
|
11
|
+
return (
|
|
12
|
+
<CheckboxPrimitive.Root
|
|
13
|
+
data-slot="checkbox"
|
|
14
|
+
className={cn(
|
|
15
|
+
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<CheckboxPrimitive.Indicator
|
|
21
|
+
data-slot="checkbox-indicator"
|
|
22
|
+
className="grid place-content-center text-current transition-none"
|
|
23
|
+
>
|
|
24
|
+
<CheckIcon className="size-3.5" />
|
|
25
|
+
</CheckboxPrimitive.Indicator>
|
|
26
|
+
</CheckboxPrimitive.Root>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { Checkbox }
|