create-manifest 1.3.4 → 2.0.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 (57) hide show
  1. package/README.md +40 -21
  2. package/index.js +51 -0
  3. package/package.json +11 -89
  4. package/starter/.claude/settings.local.json +23 -0
  5. package/starter/.env.example +1 -0
  6. package/starter/README.md +26 -0
  7. package/starter/components.json +24 -0
  8. package/starter/package.json +42 -0
  9. package/starter/src/flows/list-pokemons.flow.ts +129 -0
  10. package/starter/src/server.ts +169 -0
  11. package/starter/src/web/PokemonList.tsx +126 -0
  12. package/starter/src/web/components/blog-post-card.tsx +288 -0
  13. package/starter/src/web/components/blog-post-list.tsx +291 -0
  14. package/starter/src/web/components/payment-methods.tsx +201 -0
  15. package/starter/src/web/components/table.tsx +478 -0
  16. package/starter/src/web/components/ui/.gitkeep +0 -0
  17. package/starter/src/web/components/ui/button.tsx +62 -0
  18. package/starter/src/web/components/ui/checkbox.tsx +30 -0
  19. package/starter/src/web/globals.css +98 -0
  20. package/starter/src/web/hooks/.gitkeep +0 -0
  21. package/starter/src/web/lib/utils.ts +6 -0
  22. package/starter/src/web/root.tsx +36 -0
  23. package/starter/src/web/tsconfig.json +3 -0
  24. package/starter/tsconfig.json +25 -0
  25. package/starter/tsconfig.web.json +24 -0
  26. package/starter/vite.config.ts +37 -0
  27. package/assets/monorepo/README.md +0 -52
  28. package/assets/monorepo/api-package.json +0 -9
  29. package/assets/monorepo/api-readme.md +0 -50
  30. package/assets/monorepo/manifest.yml +0 -34
  31. package/assets/monorepo/root-package.json +0 -15
  32. package/assets/monorepo/web-package.json +0 -10
  33. package/assets/monorepo/web-readme.md +0 -9
  34. package/assets/standalone/README.md +0 -50
  35. package/assets/standalone/api-package.json +0 -9
  36. package/assets/standalone/manifest.yml +0 -34
  37. package/bin/dev.cmd +0 -3
  38. package/bin/dev.js +0 -5
  39. package/bin/run.cmd +0 -3
  40. package/bin/run.js +0 -5
  41. package/dist/commands/index.d.ts +0 -65
  42. package/dist/commands/index.js +0 -480
  43. package/dist/index.d.ts +0 -1
  44. package/dist/index.js +0 -1
  45. package/dist/utils/GetBackendFileContent.d.ts +0 -1
  46. package/dist/utils/GetBackendFileContent.js +0 -21
  47. package/dist/utils/GetLatestPackageVersion.d.ts +0 -1
  48. package/dist/utils/GetLatestPackageVersion.js +0 -5
  49. package/dist/utils/UpdateExtensionJsonFile.d.ts +0 -6
  50. package/dist/utils/UpdateExtensionJsonFile.js +0 -8
  51. package/dist/utils/UpdatePackageJsonFile.d.ts +0 -18
  52. package/dist/utils/UpdatePackageJsonFile.js +0 -21
  53. package/dist/utils/UpdateSettingsJsonFile.d.ts +0 -4
  54. package/dist/utils/UpdateSettingsJsonFile.js +0 -6
  55. package/dist/utils/helpers.d.ts +0 -1
  56. package/dist/utils/helpers.js +0 -11
  57. package/oclif.manifest.json +0 -47
@@ -0,0 +1,201 @@
1
+ import { Button } from '@/components/ui/button'
2
+ import { cn } from '@/lib/utils'
3
+ import { CreditCard, Lock, Plus } from 'lucide-react'
4
+ import { useState } from 'react'
5
+
6
+ export interface PaymentMethod {
7
+ id: string
8
+ type: 'card' | 'apple_pay' | 'google_pay' | 'paypal'
9
+ brand?: 'visa' | 'mastercard' | 'amex' | 'cb'
10
+ last4?: string
11
+ isDefault?: boolean
12
+ }
13
+
14
+ export interface PaymentMethodsProps {
15
+ methods?: PaymentMethod[]
16
+ amount?: number
17
+ currency?: string
18
+ selectedMethodId?: string
19
+ onSelectMethod?: (methodId: string) => void
20
+ onAddCard?: () => void
21
+ onPay?: (methodId: string) => void
22
+ isLoading?: boolean
23
+ }
24
+
25
+ const defaultMethods: PaymentMethod[] = [
26
+ { id: '1', type: 'card', brand: 'visa', last4: '4242' },
27
+ {
28
+ id: '2',
29
+ type: 'card',
30
+ brand: 'mastercard',
31
+ last4: '8888',
32
+ isDefault: true
33
+ },
34
+ { id: '3', type: 'apple_pay' }
35
+ ]
36
+
37
+ const BrandLogo = ({ brand }: { brand?: string }) => {
38
+ switch (brand) {
39
+ case 'visa':
40
+ return (
41
+ <svg viewBox="0 0 48 32" className="h-5 w-auto">
42
+ <rect width="48" height="32" rx="4" fill="#fff" stroke="#e5e5e5" />
43
+ <g transform="translate(5, 10) scale(0.15)">
44
+ <polygon points="116.145,95.719 97.858,95.719 109.296,24.995 127.582,24.995" fill="#00579f" />
45
+ <path d="M182.437,26.724c-3.607-1.431-9.328-3.011-16.402-3.011c-18.059,0-30.776,9.63-30.854,23.398c-0.15,10.158,9.105,15.8,16.027,19.187c7.075,3.461,9.48,5.72,9.48,8.805c-0.072,4.738-5.717,6.922-10.982,6.922c-7.301,0-11.213-1.126-17.158-3.762l-2.408-1.13l-2.559,15.876c4.289,1.954,12.191,3.688,20.395,3.764c19.188,0,31.68-9.481,31.828-24.153c0.073-8.051-4.814-14.22-15.35-19.261c-6.396-3.236-10.313-5.418-10.313-8.729c0.075-3.01,3.313-6.093,10.533-6.093c5.945-0.151,10.313,1.278,13.622,2.708l1.654,0.751l2.487-15.272z" fill="#00579f" />
46
+ <path d="M206.742,70.664c1.506-4.063,7.301-19.788,7.301-19.788c-0.076,0.151,1.503-4.138,2.406-6.771l1.278,6.094c0,0,3.463,16.929,4.215,20.465c-2.858,0-11.588,0-15.2,0zm22.573-45.669l-14.145,0c-4.362,0-7.676,1.278-9.558,5.868l-27.163,64.855l19.188,0c0,0,3.159-8.729,3.838-10.609c2.105,0,20.771,0,23.479,0c0.525,2.483,2.182,10.609,2.182,10.609l16.932,0l-14.753-70.723z" fill="#00579f" />
47
+ <path d="M82.584,24.995l-17.909,48.227l-1.957-9.781c-3.311-11.286-13.695-23.548-25.283-29.645l16.404,61.848l19.338,0l28.744-70.649l-19.337,0z" fill="#00579f" />
48
+ <path d="M48.045,24.995l-29.422,0l-0.301,1.429c22.951,5.869,38.151,20.016,44.396,37.02l-6.396-32.523c-1.053-4.517-4.289-5.796-8.277-5.926z" fill="#faa61a" />
49
+ </g>
50
+ </svg>
51
+ )
52
+ case 'mastercard':
53
+ return (
54
+ <svg viewBox="0 0 48 32" className="h-5 w-auto">
55
+ <rect width="48" height="32" rx="4" fill="#fff" stroke="#e5e5e5" />
56
+ <g transform="translate(7, 5) scale(0.22)">
57
+ <rect x="60.4" y="25.7" width="31.5" height="56.6" fill="#FF5F00" />
58
+ <path d="M62.4,54c0-11,5.1-21.5,13.7-28.3c-15.6-12.3-38.3-9.6-50.6,6.1C13.3,47.4,16,70,31.7,82.3c13.1,10.3,31.4,10.3,44.5,0C67.5,75.5,62.4,65,62.4,54z" fill="#EB001B" />
59
+ <path d="M134.4,54c0,19.9-16.1,36-36,36c-8.1,0-15.9-2.7-22.2-7.7c15.6-12.3,18.3-34.9,6-50.6c-1.8-2.2-3.8-4.3-6-6c15.6-12.3,38.3-9.6,50.5,6.1C131.7,38.1,134.4,45.9,134.4,54z" fill="#F79E1B" />
60
+ </g>
61
+ </svg>
62
+ )
63
+ case 'amex':
64
+ return (
65
+ <svg viewBox="0 0 48 32" className="h-5 w-auto">
66
+ <rect width="48" height="32" rx="4" fill="#006FCF" />
67
+ <path
68
+ d="M10 12h4l.8 2 .8-2h4v8h-3v-5l-1.3 3h-2l-1.3-3v5h-2v-8zm14 0h6v2h-3v1h3v2h-3v1h3v2h-6v-8zm8 0h3l2 3 2-3h3l-3.5 4 3.5 4h-3l-2-3-2 3h-3l3.5-4-3.5-4z"
69
+ fill="white"
70
+ />
71
+ </svg>
72
+ )
73
+ case 'cb':
74
+ return (
75
+ <svg viewBox="0 0 48 32" className="h-5 w-auto">
76
+ <rect width="48" height="32" rx="4" fill="#1E4B9E" />
77
+ <rect x="4" y="10" width="18" height="12" rx="2" fill="#49A942" />
78
+ <text x="28" y="20" fill="white" fontSize="10" fontWeight="bold">
79
+ CB
80
+ </text>
81
+ </svg>
82
+ )
83
+ default:
84
+ return <CreditCard className="h-5 w-5 text-muted-foreground" />
85
+ }
86
+ }
87
+
88
+ const MethodIcon = ({ method }: { method: PaymentMethod }) => {
89
+ if (method.type === 'apple_pay') {
90
+ return (
91
+ <div className="h-5 w-8 rounded bg-black flex items-center justify-center">
92
+ <img src="/images/apple-pay.svg" alt="Apple Pay" className="h-2 w-auto invert" />
93
+ </div>
94
+ )
95
+ }
96
+ if (method.type === 'google_pay') {
97
+ return (
98
+ <svg viewBox="0 0 48 32" className="h-5 w-auto">
99
+ <rect width="48" height="32" rx="4" fill="#fff" stroke="#ddd" />
100
+ <text x="8" y="20" fontSize="10" fontWeight="500" fill="#5F6368">
101
+ G Pay
102
+ </text>
103
+ </svg>
104
+ )
105
+ }
106
+ if (method.type === 'paypal') {
107
+ return (
108
+ <svg viewBox="0 0 48 32" className="h-5 w-auto">
109
+ <rect width="48" height="32" rx="4" fill="#003087" />
110
+ <text x="8" y="20" fontSize="9" fontWeight="bold" fill="#fff">
111
+ PayPal
112
+ </text>
113
+ </svg>
114
+ )
115
+ }
116
+ return <BrandLogo brand={method.brand} />
117
+ }
118
+
119
+ export function PaymentMethods({
120
+ methods = defaultMethods,
121
+ amount = 279.0,
122
+ currency = 'EUR',
123
+ selectedMethodId,
124
+ onSelectMethod,
125
+ onAddCard,
126
+ onPay,
127
+ isLoading = false
128
+ }: PaymentMethodsProps) {
129
+ const [selected, setSelected] = useState(
130
+ selectedMethodId || methods.find((m) => m.isDefault)?.id || methods[0]?.id
131
+ )
132
+
133
+ const handleSelect = (methodId: string) => {
134
+ setSelected(methodId)
135
+ onSelectMethod?.(methodId)
136
+ }
137
+
138
+ const formatCurrency = (value: number) => {
139
+ return new Intl.NumberFormat('en-US', {
140
+ style: 'currency',
141
+ currency
142
+ }).format(value)
143
+ }
144
+
145
+ const getMethodLabel = (method: PaymentMethod) => {
146
+ if (method.type === 'apple_pay') return 'Apple Pay'
147
+ if (method.type === 'google_pay') return 'Google Pay'
148
+ if (method.type === 'paypal') return 'PayPal'
149
+ return `•••• ${method.last4}`
150
+ }
151
+
152
+ return (
153
+ <div className="w-full rounded-md sm:rounded-lg bg-card p-2 space-y-4">
154
+ <div className="flex flex-wrap items-center gap-2">
155
+ {methods.map((method) => (
156
+ <button
157
+ key={method.id}
158
+ onClick={() => handleSelect(method.id)}
159
+ className={cn(
160
+ 'inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm transition-colors',
161
+ selected === method.id
162
+ ? 'border-foreground ring-1 ring-foreground'
163
+ : 'border-border hover:border-foreground/50'
164
+ )}
165
+ >
166
+ <MethodIcon method={method} />
167
+ <span>{getMethodLabel(method)}</span>
168
+ {method.isDefault && (
169
+ <span className="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
170
+ Default
171
+ </span>
172
+ )}
173
+ </button>
174
+ ))}
175
+ <button
176
+ onClick={onAddCard}
177
+ className="inline-flex items-center gap-1.5 rounded-full border border-dashed border-border px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
178
+ >
179
+ <Plus className="h-4 w-4" />
180
+ Add
181
+ </button>
182
+ </div>
183
+
184
+ <div className="flex flex-col-reverse sm:flex-row sm:items-center sm:justify-between gap-2">
185
+ <span className="flex items-center justify-center sm:justify-start gap-1.5 text-xs text-muted-foreground">
186
+ <Lock className="h-3 w-3" />
187
+ Secure encrypted transaction
188
+ </span>
189
+ <Button
190
+ size="sm"
191
+ className="w-full sm:w-auto"
192
+ onClick={() => selected && onPay?.(selected)}
193
+ disabled={!selected || isLoading}
194
+ >
195
+ <Lock className="mr-1.5 h-3.5 w-3.5" />
196
+ {isLoading ? 'Processing...' : `Pay ${formatCurrency(amount)}`}
197
+ </Button>
198
+ </div>
199
+ </div>
200
+ )
201
+ }
@@ -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
+ }
File without changes