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.
- package/README.md +40 -21
- package/index.js +51 -0
- package/package.json +11 -89
- package/starter/.claude/settings.local.json +23 -0
- package/starter/.env.example +1 -0
- package/starter/README.md +26 -0
- package/starter/components.json +24 -0
- package/starter/package.json +42 -0
- package/starter/src/flows/list-pokemons.flow.ts +129 -0
- package/starter/src/server.ts +169 -0
- package/starter/src/web/PokemonList.tsx +126 -0
- package/starter/src/web/components/blog-post-card.tsx +288 -0
- package/starter/src/web/components/blog-post-list.tsx +291 -0
- package/starter/src/web/components/payment-methods.tsx +201 -0
- package/starter/src/web/components/table.tsx +478 -0
- package/starter/src/web/components/ui/.gitkeep +0 -0
- package/starter/src/web/components/ui/button.tsx +62 -0
- package/starter/src/web/components/ui/checkbox.tsx +30 -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 +25 -0
- package/starter/tsconfig.web.json +24 -0
- package/starter/vite.config.ts +37 -0
- package/assets/monorepo/README.md +0 -52
- package/assets/monorepo/api-package.json +0 -9
- package/assets/monorepo/api-readme.md +0 -50
- 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 -50
- 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,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
|