@geenius/adapters 0.1.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/.changeset/config.json +11 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/release.yml +29 -0
- package/.nvmrc +1 -0
- package/.project/ACCOUNT.yaml +4 -0
- package/.project/IDEAS.yaml +7 -0
- package/.project/PROJECT.yaml +11 -0
- package/.project/ROADMAP.yaml +15 -0
- package/CHANGELOG.md +11 -0
- package/CODE_OF_CONDUCT.md +16 -0
- package/CONTRIBUTING.md +26 -0
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/SECURITY.md +15 -0
- package/SUPPORT.md +8 -0
- package/package.json +51 -0
- package/packages/convex/README.md +64 -0
- package/packages/convex/package.json +42 -0
- package/packages/convex/src/adapter.ts +39 -0
- package/packages/convex/src/index.ts +19 -0
- package/packages/convex/src/mutations.ts +142 -0
- package/packages/convex/src/queries.ts +106 -0
- package/packages/convex/src/schema.ts +54 -0
- package/packages/convex/src/types.ts +20 -0
- package/packages/convex/tsconfig.json +11 -0
- package/packages/convex/tsup.config.ts +10 -0
- package/packages/react/README.md +1 -0
- package/packages/react/package.json +45 -0
- package/packages/react/src/components/AdapterCard.tsx +49 -0
- package/packages/react/src/components/AdapterConfigForm.tsx +118 -0
- package/packages/react/src/components/AdapterList.tsx +84 -0
- package/packages/react/src/components/AdapterStatusBadge.tsx +30 -0
- package/packages/react/src/components/index.ts +4 -0
- package/packages/react/src/hooks/index.ts +75 -0
- package/packages/react/src/index.tsx +44 -0
- package/packages/react/src/pages/AdapterDetailPage.tsx +133 -0
- package/packages/react/src/pages/AdaptersPage.tsx +111 -0
- package/packages/react/src/pages/index.ts +2 -0
- package/packages/react/src/provider/AdapterProvider.tsx +115 -0
- package/packages/react/src/provider/index.ts +2 -0
- package/packages/react/tsconfig.json +18 -0
- package/packages/react/tsup.config.ts +10 -0
- package/packages/react-css/README.md +1 -0
- package/packages/react-css/package.json +44 -0
- package/packages/react-css/src/adapters.css +1576 -0
- package/packages/react-css/src/components/AdapterCard.tsx +34 -0
- package/packages/react-css/src/components/AdapterConfigForm.tsx +63 -0
- package/packages/react-css/src/components/AdapterList.tsx +40 -0
- package/packages/react-css/src/components/AdapterStatusBadge.tsx +21 -0
- package/packages/react-css/src/components/index.ts +4 -0
- package/packages/react-css/src/hooks/index.ts +75 -0
- package/packages/react-css/src/index.tsx +25 -0
- package/packages/react-css/src/pages/AdapterDetailPage.tsx +133 -0
- package/packages/react-css/src/pages/AdaptersPage.tsx +111 -0
- package/packages/react-css/src/pages/index.ts +2 -0
- package/packages/react-css/src/provider/AdapterProvider.tsx +115 -0
- package/packages/react-css/src/provider/index.ts +2 -0
- package/packages/react-css/src/styles.css +494 -0
- package/packages/react-css/tsconfig.json +19 -0
- package/packages/react-css/tsup.config.ts +2 -0
- package/packages/shared/README.md +1 -0
- package/packages/shared/package.json +39 -0
- package/packages/shared/src/__tests__/adapters.test.ts +545 -0
- package/packages/shared/src/admin/index.ts +2 -0
- package/packages/shared/src/admin/interface.ts +34 -0
- package/packages/shared/src/admin/localStorage.ts +109 -0
- package/packages/shared/src/ai/anthropic.ts +123 -0
- package/packages/shared/src/ai/cloudflare-gateway.ts +130 -0
- package/packages/shared/src/ai/gemini.ts +181 -0
- package/packages/shared/src/ai/index.ts +14 -0
- package/packages/shared/src/ai/interface.ts +11 -0
- package/packages/shared/src/ai/localStorage.ts +78 -0
- package/packages/shared/src/ai/ollama.ts +143 -0
- package/packages/shared/src/ai/openai.ts +120 -0
- package/packages/shared/src/ai/vercel-ai.ts +101 -0
- package/packages/shared/src/auth/better-auth.ts +118 -0
- package/packages/shared/src/auth/clerk.ts +151 -0
- package/packages/shared/src/auth/convex-auth.ts +125 -0
- package/packages/shared/src/auth/index.ts +10 -0
- package/packages/shared/src/auth/interface.ts +17 -0
- package/packages/shared/src/auth/localStorage.ts +125 -0
- package/packages/shared/src/auth/supabase-auth.ts +136 -0
- package/packages/shared/src/config.ts +57 -0
- package/packages/shared/src/constants.ts +122 -0
- package/packages/shared/src/db/convex.ts +146 -0
- package/packages/shared/src/db/index.ts +10 -0
- package/packages/shared/src/db/interface.ts +13 -0
- package/packages/shared/src/db/localStorage.ts +91 -0
- package/packages/shared/src/db/mongodb.ts +125 -0
- package/packages/shared/src/db/neon.ts +171 -0
- package/packages/shared/src/db/supabase.ts +158 -0
- package/packages/shared/src/index.ts +117 -0
- package/packages/shared/src/payments/index.ts +4 -0
- package/packages/shared/src/payments/interface.ts +11 -0
- package/packages/shared/src/payments/localStorage.ts +81 -0
- package/packages/shared/src/payments/stripe.ts +177 -0
- package/packages/shared/src/storage/convex.ts +113 -0
- package/packages/shared/src/storage/index.ts +14 -0
- package/packages/shared/src/storage/interface.ts +11 -0
- package/packages/shared/src/storage/localStorage.ts +95 -0
- package/packages/shared/src/storage/minio.ts +47 -0
- package/packages/shared/src/storage/r2.ts +123 -0
- package/packages/shared/src/storage/s3.ts +128 -0
- package/packages/shared/src/storage/supabase-storage.ts +116 -0
- package/packages/shared/src/storage/uploadthing.ts +126 -0
- package/packages/shared/src/styles/adapters.css +494 -0
- package/packages/shared/src/tier-gate.ts +119 -0
- package/packages/shared/src/types.ts +162 -0
- package/packages/shared/tsconfig.json +18 -0
- package/packages/shared/tsup.config.ts +9 -0
- package/packages/shared/vitest.config.ts +14 -0
- package/packages/solidjs/README.md +1 -0
- package/packages/solidjs/package.json +44 -0
- package/packages/solidjs/src/components/AdapterCard.tsx +24 -0
- package/packages/solidjs/src/components/AdapterConfigForm.tsx +54 -0
- package/packages/solidjs/src/components/AdapterList.tsx +28 -0
- package/packages/solidjs/src/components/AdapterStatusBadge.tsx +20 -0
- package/packages/solidjs/src/components/index.ts +4 -0
- package/packages/solidjs/src/index.tsx +17 -0
- package/packages/solidjs/src/pages/AdapterDetailPage.tsx +38 -0
- package/packages/solidjs/src/pages/AdaptersPage.tsx +39 -0
- package/packages/solidjs/src/pages/index.ts +2 -0
- package/packages/solidjs/src/primitives/index.ts +78 -0
- package/packages/solidjs/src/provider/AdapterProvider.tsx +62 -0
- package/packages/solidjs/src/provider/index.ts +2 -0
- package/packages/solidjs/tsconfig.json +20 -0
- package/packages/solidjs/tsup.config.ts +10 -0
- package/packages/solidjs-css/README.md +1 -0
- package/packages/solidjs-css/package.json +43 -0
- package/packages/solidjs-css/src/adapters.css +1576 -0
- package/packages/solidjs-css/src/components/AdapterCard.tsx +43 -0
- package/packages/solidjs-css/src/components/AdapterConfigForm.tsx +119 -0
- package/packages/solidjs-css/src/components/AdapterList.tsx +68 -0
- package/packages/solidjs-css/src/components/AdapterStatusBadge.tsx +24 -0
- package/packages/solidjs-css/src/components/index.ts +8 -0
- package/packages/solidjs-css/src/index.tsx +30 -0
- package/packages/solidjs-css/src/pages/AdapterDetailPage.tsx +107 -0
- package/packages/solidjs-css/src/pages/AdaptersPage.tsx +94 -0
- package/packages/solidjs-css/src/pages/index.ts +4 -0
- package/packages/solidjs-css/src/primitives/index.ts +1 -0
- package/packages/solidjs-css/src/provider/AdapterProvider.tsx +61 -0
- package/packages/solidjs-css/src/provider/index.ts +2 -0
- package/packages/solidjs-css/tsconfig.json +20 -0
- package/packages/solidjs-css/tsup.config.ts +2 -0
- package/pnpm-workspace.yaml +2 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @geenius-adapters/react-css — AdapterCard
|
|
2
|
+
|
|
3
|
+
import type { AdapterDomain, AdapterStatusInfo } from '@geenius-adapters/shared'
|
|
4
|
+
import { DOMAIN_LABELS, DOMAIN_ICONS, DOMAIN_DESCRIPTIONS } from '@geenius-adapters/shared'
|
|
5
|
+
import { AdapterStatusBadge } from './AdapterStatusBadge'
|
|
6
|
+
|
|
7
|
+
export interface AdapterCardProps {
|
|
8
|
+
domain: AdapterDomain
|
|
9
|
+
status: AdapterStatusInfo
|
|
10
|
+
onClick?: () => void
|
|
11
|
+
className?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function AdapterCard({ domain, status, onClick, className = '' }: AdapterCardProps) {
|
|
15
|
+
return (
|
|
16
|
+
<button type="button" onClick={onClick} className={`adapter-card ${className}`} aria-label={`${DOMAIN_LABELS[domain]} adapter`}>
|
|
17
|
+
<div className="adapter-card__icon">{DOMAIN_ICONS[domain]}</div>
|
|
18
|
+
<div className="adapter-card__body">
|
|
19
|
+
<div className="adapter-card__header">
|
|
20
|
+
<h3 className="adapter-card__name">{DOMAIN_LABELS[domain]}</h3>
|
|
21
|
+
<AdapterStatusBadge status={status.status} />
|
|
22
|
+
</div>
|
|
23
|
+
<p className="adapter-card__desc">{DOMAIN_DESCRIPTIONS[domain]}</p>
|
|
24
|
+
{status.provider && status.provider !== 'none' && (
|
|
25
|
+
<div className="adapter-card__meta">
|
|
26
|
+
<span className="adapter-card__provider">{status.provider}</span>
|
|
27
|
+
{status.latency && <span className="adapter-card__latency">{status.latency}ms</span>}
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
{status.error && <p className="adapter-card__error">{status.error}</p>}
|
|
31
|
+
</div>
|
|
32
|
+
</button>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// @geenius-adapters/react-css — AdapterConfigForm
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import type { AdapterDomain, DomainAdapterConfig } from '@geenius-adapters/shared'
|
|
5
|
+
import { DOMAIN_LABELS, getProvidersForDomain } from '@geenius-adapters/shared'
|
|
6
|
+
|
|
7
|
+
export interface AdapterConfigFormProps {
|
|
8
|
+
domain: AdapterDomain
|
|
9
|
+
initialConfig?: DomainAdapterConfig
|
|
10
|
+
onSave: (config: DomainAdapterConfig) => Promise<void> | void
|
|
11
|
+
onCancel?: () => void
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function AdapterConfigForm({ domain, initialConfig, onSave, onCancel, className = '' }: AdapterConfigFormProps) {
|
|
16
|
+
const providers = getProvidersForDomain(domain)
|
|
17
|
+
const [provider, setProvider] = useState(initialConfig?.provider ?? providers[0]?.id ?? '')
|
|
18
|
+
const [apiKey, setApiKey] = useState(initialConfig?.apiKey ?? '')
|
|
19
|
+
const [baseUrl, setBaseUrl] = useState(initialConfig?.baseUrl ?? '')
|
|
20
|
+
const [isSaving, setIsSaving] = useState(false)
|
|
21
|
+
const [error, setError] = useState<string | null>(null)
|
|
22
|
+
const needsApiKey = !['localStorage', 'noop'].includes(provider)
|
|
23
|
+
const needsBaseUrl = ['ollama', 'minio', 'neon', 'supabase'].includes(provider)
|
|
24
|
+
|
|
25
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
26
|
+
e.preventDefault(); setError(null)
|
|
27
|
+
if (!provider) { setError('Select a provider'); return }
|
|
28
|
+
setIsSaving(true)
|
|
29
|
+
try { await onSave({ provider, apiKey: apiKey || undefined, baseUrl: baseUrl || undefined }) }
|
|
30
|
+
catch (e) { setError(e instanceof Error ? e.message : 'Failed') }
|
|
31
|
+
finally { setIsSaving(false) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<form onSubmit={handleSubmit} className={`adapter-form ${className}`} noValidate>
|
|
36
|
+
<h3 className="adapter-form__title">Configure {DOMAIN_LABELS[domain]}</h3>
|
|
37
|
+
<p className="adapter-form__subtitle">Select your preferred provider.</p>
|
|
38
|
+
{error && <div className="adapter-alert adapter-alert--error" role="alert" style={{marginTop: '1rem'}}>{error}</div>}
|
|
39
|
+
<div className="adapter-field">
|
|
40
|
+
<span className="adapter-field__label">Provider</span>
|
|
41
|
+
<div className="adapter-provider-grid">
|
|
42
|
+
{providers.map(p => (
|
|
43
|
+
<button key={p.id} type="button" onClick={() => setProvider(p.id)} className={`adapter-provider-option ${provider === p.id ? 'adapter-provider-option--selected' : ''}`}>
|
|
44
|
+
<span className="adapter-provider-option__name">{p.name}</span>
|
|
45
|
+
<span className="adapter-provider-option__desc">{p.description}</span>
|
|
46
|
+
<span className={`adapter-tier-badge adapter-tier-badge--${p.tier}`}>{p.tier}</span>
|
|
47
|
+
</button>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
{needsApiKey && (
|
|
52
|
+
<div className="adapter-field"><label className="adapter-field__label">API Key</label><input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} className="adapter-field__input" placeholder="Enter API key" /></div>
|
|
53
|
+
)}
|
|
54
|
+
{needsBaseUrl && (
|
|
55
|
+
<div className="adapter-field"><label className="adapter-field__label">Base URL</label><input type="url" value={baseUrl} onChange={e => setBaseUrl(e.target.value)} className="adapter-field__input" placeholder="https://..." /></div>
|
|
56
|
+
)}
|
|
57
|
+
<div style={{display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1.5rem'}}>
|
|
58
|
+
{onCancel && <button type="button" onClick={onCancel} className="adapter-btn adapter-btn--ghost">Cancel</button>}
|
|
59
|
+
<button type="submit" disabled={isSaving || !provider} className="adapter-btn adapter-btn--primary">{isSaving ? 'Saving…' : 'Save'}</button>
|
|
60
|
+
</div>
|
|
61
|
+
</form>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// @geenius-adapters/react-css — AdapterList
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react'
|
|
4
|
+
import { useAdapters } from '../hooks'
|
|
5
|
+
import { AdapterCard } from './AdapterCard'
|
|
6
|
+
import { ADAPTER_DOMAINS, DOMAIN_LABELS } from '@geenius-adapters/shared'
|
|
7
|
+
import type { AdapterDomain, AdapterStatusType } from '@geenius-adapters/shared'
|
|
8
|
+
|
|
9
|
+
export interface AdapterListProps {
|
|
10
|
+
onSelect?: (domain: AdapterDomain) => void
|
|
11
|
+
filterStatus?: AdapterStatusType
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function AdapterList({ onSelect, filterStatus, className = '' }: AdapterListProps) {
|
|
16
|
+
const { statuses, isLoading } = useAdapters()
|
|
17
|
+
const [search, setSearch] = useState('')
|
|
18
|
+
|
|
19
|
+
const filtered = useMemo(() => {
|
|
20
|
+
let domains = [...ADAPTER_DOMAINS]
|
|
21
|
+
if (search) { const q = search.toLowerCase(); domains = domains.filter(d => DOMAIN_LABELS[d].toLowerCase().includes(q) || d.includes(q)) }
|
|
22
|
+
if (filterStatus) { domains = domains.filter(d => statuses[d]?.status === filterStatus) }
|
|
23
|
+
return domains
|
|
24
|
+
}, [search, filterStatus, statuses])
|
|
25
|
+
|
|
26
|
+
if (isLoading) return (
|
|
27
|
+
<div className={className} role="status">{[1,2,3,4].map(i => <div key={i} className="adapter-skeleton" style={{height: '6rem', marginBottom: '0.75rem'}} />)}</div>
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={className}>
|
|
32
|
+
<input type="text" value={search} onChange={e => setSearch(e.target.value)} placeholder="Search adapters..." className="adapter-search" aria-label="Search adapters" />
|
|
33
|
+
{filtered.length === 0 ? (
|
|
34
|
+
<div className="adapter-empty"><p className="adapter-empty__text">{search ? 'No match' : 'No adapters configured'}</p></div>
|
|
35
|
+
) : (
|
|
36
|
+
<div className="adapter-grid" role="list">{filtered.map(d => <AdapterCard key={d} domain={d} status={statuses[d]} onClick={() => onSelect?.(d)} />)}</div>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @geenius-adapters/react-css — AdapterStatusBadge
|
|
2
|
+
|
|
3
|
+
import type { AdapterStatusType } from '@geenius-adapters/shared'
|
|
4
|
+
|
|
5
|
+
export interface AdapterStatusBadgeProps {
|
|
6
|
+
status: AdapterStatusType
|
|
7
|
+
showLabel?: boolean
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AdapterStatusBadge({ status, showLabel = true, className = '' }: AdapterStatusBadgeProps) {
|
|
12
|
+
const labels: Record<AdapterStatusType, string> = {
|
|
13
|
+
connected: 'Connected', disconnected: 'Disconnected', error: 'Error', initializing: 'Initializing',
|
|
14
|
+
}
|
|
15
|
+
return (
|
|
16
|
+
<span className={`adapter-badge adapter-badge--${status} ${className}`}>
|
|
17
|
+
<span className="adapter-badge__dot" />
|
|
18
|
+
{showLabel && <span>{labels[status]}</span>}
|
|
19
|
+
</span>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { AdapterStatusBadge, type AdapterStatusBadgeProps } from './AdapterStatusBadge'
|
|
2
|
+
export { AdapterCard, type AdapterCardProps } from './AdapterCard'
|
|
3
|
+
export { AdapterList, type AdapterListProps } from './AdapterList'
|
|
4
|
+
export { AdapterConfigForm, type AdapterConfigFormProps } from './AdapterConfigForm'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @geenius-adapters/react — Hooks
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react'
|
|
4
|
+
import { useAdapterContext } from '../provider/AdapterProvider'
|
|
5
|
+
import type {
|
|
6
|
+
AuthAdapter, DbAdapter, PaymentsAdapter, AiAdapter, FileStorageAdapter, AdminAdapter,
|
|
7
|
+
AdapterDomain, AdapterStatusInfo,
|
|
8
|
+
} from '@geenius-adapters/shared'
|
|
9
|
+
|
|
10
|
+
// ─── Domain-specific hooks ───────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/** Access the database adapter */
|
|
13
|
+
export function useDb(): DbAdapter {
|
|
14
|
+
return useAdapterContext().getAdapter('db')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Access the auth adapter */
|
|
18
|
+
export function useAuth(): AuthAdapter {
|
|
19
|
+
return useAdapterContext().getAdapter('auth')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Access the payments adapter */
|
|
23
|
+
export function usePayments(): PaymentsAdapter {
|
|
24
|
+
return useAdapterContext().getAdapter('payments')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Access the AI adapter */
|
|
28
|
+
export function useAi(): AiAdapter {
|
|
29
|
+
return useAdapterContext().getAdapter('ai')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Access the file storage adapter */
|
|
33
|
+
export function useStorage(): FileStorageAdapter {
|
|
34
|
+
return useAdapterContext().getAdapter('storage')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Access the admin adapter */
|
|
38
|
+
export function useAdmin(): AdminAdapter {
|
|
39
|
+
return useAdapterContext().getAdapter('admin')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Status hooks ────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/** Get all adapter statuses */
|
|
45
|
+
export function useAdapterStatuses(): Record<AdapterDomain, AdapterStatusInfo> {
|
|
46
|
+
return useAdapterContext().statuses
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Get status for a specific adapter domain */
|
|
50
|
+
export function useAdapterStatus(domain: AdapterDomain): AdapterStatusInfo {
|
|
51
|
+
return useAdapterContext().statuses[domain]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Check if adapter is ready */
|
|
55
|
+
export function useIsAdapterReady(domain: AdapterDomain): boolean {
|
|
56
|
+
return useAdapterContext().isReady(domain)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Full adapters hook ──────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/** Access all adapters at once */
|
|
62
|
+
export function useAdapters() {
|
|
63
|
+
const ctx = useAdapterContext()
|
|
64
|
+
return useMemo(() => ({
|
|
65
|
+
db: ctx.adapters.db,
|
|
66
|
+
auth: ctx.adapters.auth,
|
|
67
|
+
payments: ctx.adapters.payments,
|
|
68
|
+
ai: ctx.adapters.ai,
|
|
69
|
+
storage: ctx.adapters.storage,
|
|
70
|
+
admin: ctx.adapters.admin,
|
|
71
|
+
statuses: ctx.statuses,
|
|
72
|
+
isLoading: ctx.isLoading,
|
|
73
|
+
isReady: ctx.isReady,
|
|
74
|
+
}), [ctx])
|
|
75
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// @geenius-adapters/react-css — Barrel export
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @geenius-adapters/react-css
|
|
5
|
+
* React components using vanilla CSS (no Tailwind).
|
|
6
|
+
* Import: `import '@geenius-adapters/react-css/styles.css'`
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
AuthAdapter, DbAdapter, PaymentsAdapter, AiAdapter, FileStorageAdapter,
|
|
11
|
+
AdminAdapter, AdapterDomain, AdapterConfig, AdapterStatusType, AdapterStatusInfo,
|
|
12
|
+
} from '@geenius-adapters/shared'
|
|
13
|
+
|
|
14
|
+
export { ADAPTER_DOMAINS, DOMAIN_LABELS, DOMAIN_ICONS, DOMAIN_DESCRIPTIONS, configureAdapters, getAdapterConfig } from '@geenius-adapters/shared'
|
|
15
|
+
|
|
16
|
+
export { AdapterProvider, useAdapterContext } from './provider'
|
|
17
|
+
export type { AdapterProviderProps, AdapterSet, AdapterContextValue } from './provider'
|
|
18
|
+
|
|
19
|
+
export { useDb, useAuth, usePayments, useAi, useStorage, useAdmin, useAdapters, useAdapterStatuses, useAdapterStatus, useIsAdapterReady } from './hooks'
|
|
20
|
+
|
|
21
|
+
export { AdapterStatusBadge, AdapterCard, AdapterList, AdapterConfigForm } from './components'
|
|
22
|
+
export type { AdapterStatusBadgeProps, AdapterCardProps, AdapterListProps, AdapterConfigFormProps } from './components'
|
|
23
|
+
|
|
24
|
+
export { AdaptersPage, AdapterDetailPage } from './pages'
|
|
25
|
+
export type { AdaptersPageProps, AdapterDetailPageProps } from './pages'
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// @geenius-adapters/react — AdapterDetailPage
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useAdapters, useAdapterStatus } from '../hooks'
|
|
5
|
+
import { AdapterStatusBadge } from '../components/AdapterStatusBadge'
|
|
6
|
+
import { AdapterConfigForm } from '../components/AdapterConfigForm'
|
|
7
|
+
import type { AdapterDomain } from '@geenius-adapters/shared'
|
|
8
|
+
import { DOMAIN_LABELS, DOMAIN_ICONS, DOMAIN_DESCRIPTIONS, getProvidersForDomain } from '@geenius-adapters/shared'
|
|
9
|
+
|
|
10
|
+
export interface AdapterDetailPageProps {
|
|
11
|
+
domain: AdapterDomain
|
|
12
|
+
onBack?: () => void
|
|
13
|
+
className?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AdapterDetailPage({ domain, onBack, className = '' }: AdapterDetailPageProps) {
|
|
17
|
+
const { isLoading } = useAdapters()
|
|
18
|
+
const status = useAdapterStatus(domain)
|
|
19
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
20
|
+
const providers = getProvidersForDomain(domain)
|
|
21
|
+
|
|
22
|
+
const label = DOMAIN_LABELS[domain]
|
|
23
|
+
const icon = DOMAIN_ICONS[domain]
|
|
24
|
+
const desc = DOMAIN_DESCRIPTIONS[domain]
|
|
25
|
+
|
|
26
|
+
if (isLoading) {
|
|
27
|
+
return (
|
|
28
|
+
<div className={`max-w-3xl mx-auto space-y-6 ${className}`}>
|
|
29
|
+
<div className="animate-pulse space-y-4">
|
|
30
|
+
<div className="h-6 w-32 rounded bg-white/10" />
|
|
31
|
+
<div className="h-40 rounded-2xl bg-white/[0.03]" />
|
|
32
|
+
<div className="h-60 rounded-2xl bg-white/[0.03]" />
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={`max-w-3xl mx-auto space-y-8 ${className}`}>
|
|
40
|
+
{/* Back navigation */}
|
|
41
|
+
{onBack && (
|
|
42
|
+
<button onClick={onBack} className="flex items-center gap-1.5 text-sm text-white/30 hover:text-white/60 transition-colors">
|
|
43
|
+
<span>←</span> Back to Adapters
|
|
44
|
+
</button>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
{/* Header */}
|
|
48
|
+
<div className="flex items-start gap-5">
|
|
49
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-white/5 text-3xl">{icon}</div>
|
|
50
|
+
<div className="flex-1">
|
|
51
|
+
<div className="flex items-center gap-3">
|
|
52
|
+
<h1 className="text-2xl font-extrabold text-white/95 tracking-tight">{label}</h1>
|
|
53
|
+
<AdapterStatusBadge status={status.status} size="md" />
|
|
54
|
+
</div>
|
|
55
|
+
<p className="mt-1 text-sm text-white/40">{desc}</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* Status Card */}
|
|
60
|
+
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-6 backdrop-blur-sm">
|
|
61
|
+
<h2 className="text-xs font-semibold uppercase tracking-wider text-white/30 mb-4">Current Status</h2>
|
|
62
|
+
<div className="grid gap-4 sm:grid-cols-3">
|
|
63
|
+
<div>
|
|
64
|
+
<p className="text-[0.625rem] text-white/30 uppercase">Provider</p>
|
|
65
|
+
<p className="mt-0.5 text-sm font-semibold text-white/80">{status.provider !== 'none' ? status.provider : '—'}</p>
|
|
66
|
+
</div>
|
|
67
|
+
<div>
|
|
68
|
+
<p className="text-[0.625rem] text-white/30 uppercase">Status</p>
|
|
69
|
+
<p className="mt-0.5"><AdapterStatusBadge status={status.status} /></p>
|
|
70
|
+
</div>
|
|
71
|
+
<div>
|
|
72
|
+
<p className="text-[0.625rem] text-white/30 uppercase">Last Checked</p>
|
|
73
|
+
<p className="mt-0.5 text-sm text-white/60">{status.lastCheckedAt ? new Date(status.lastCheckedAt).toLocaleTimeString() : '—'}</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
{status.error && (
|
|
77
|
+
<div className="mt-4 rounded-lg border border-red-500/20 bg-red-500/10 px-4 py-3 text-sm text-red-400">
|
|
78
|
+
<span className="font-semibold">Error:</span> {status.error}
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
{status.latency && (
|
|
82
|
+
<div className="mt-4">
|
|
83
|
+
<p className="text-[0.625rem] text-white/30 uppercase mb-1">Latency</p>
|
|
84
|
+
<div className="h-2 w-full rounded-full bg-white/5 overflow-hidden">
|
|
85
|
+
<div className={`h-full rounded-full transition-all ${status.latency < 100 ? 'bg-emerald-400' : status.latency < 500 ? 'bg-amber-400' : 'bg-red-400'}`}
|
|
86
|
+
style={{ width: `${Math.min(100, (status.latency / 1000) * 100)}%` }} />
|
|
87
|
+
</div>
|
|
88
|
+
<p className="mt-1 text-xs text-white/40">{status.latency}ms</p>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Available Providers */}
|
|
94
|
+
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-6 backdrop-blur-sm">
|
|
95
|
+
<div className="flex items-center justify-between mb-4">
|
|
96
|
+
<h2 className="text-xs font-semibold uppercase tracking-wider text-white/30">Available Providers</h2>
|
|
97
|
+
<span className="text-xs text-white/20">{providers.length} providers</span>
|
|
98
|
+
</div>
|
|
99
|
+
<div className="divide-y divide-white/5">
|
|
100
|
+
{providers.map(p => (
|
|
101
|
+
<div key={p.id} className="flex items-center justify-between py-3 first:pt-0 last:pb-0">
|
|
102
|
+
<div>
|
|
103
|
+
<p className="text-sm font-semibold text-white/80">{p.name}</p>
|
|
104
|
+
<p className="text-xs text-white/30">{p.description}</p>
|
|
105
|
+
</div>
|
|
106
|
+
<span className={`self-start rounded px-2 py-0.5 text-[0.625rem] font-bold uppercase ${
|
|
107
|
+
p.tier === 'pronto' ? 'bg-emerald-500/10 text-emerald-400' : p.tier === 'mvp' ? 'bg-blue-500/10 text-blue-400' : 'bg-purple-500/10 text-purple-400'
|
|
108
|
+
}`}>{p.tier}</span>
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Configuration */}
|
|
115
|
+
<div>
|
|
116
|
+
{isEditing ? (
|
|
117
|
+
<AdapterConfigForm
|
|
118
|
+
domain={domain}
|
|
119
|
+
onSave={async () => setIsEditing(false)}
|
|
120
|
+
onCancel={() => setIsEditing(false)}
|
|
121
|
+
/>
|
|
122
|
+
) : (
|
|
123
|
+
<button
|
|
124
|
+
onClick={() => setIsEditing(true)}
|
|
125
|
+
className="w-full rounded-2xl border border-dashed border-white/10 bg-white/[0.01] p-6 text-center text-sm text-white/30 hover:border-white/20 hover:text-white/50 transition-all"
|
|
126
|
+
>
|
|
127
|
+
⚙️ Configure {label} Adapter
|
|
128
|
+
</button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @geenius-adapters/react — AdaptersPage
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useAdapters } from '../hooks'
|
|
5
|
+
import { AdapterList } from '../components/AdapterList'
|
|
6
|
+
import { AdapterConfigForm } from '../components/AdapterConfigForm'
|
|
7
|
+
import { AdapterStatusBadge } from '../components/AdapterStatusBadge'
|
|
8
|
+
import { ADAPTER_DOMAINS, ADAPTER_STATUSES, DOMAIN_LABELS } from '@geenius-adapters/shared'
|
|
9
|
+
import type { AdapterDomain, AdapterStatusType } from '@geenius-adapters/shared'
|
|
10
|
+
|
|
11
|
+
export interface AdaptersPageProps {
|
|
12
|
+
onNavigateDetail?: (domain: AdapterDomain) => void
|
|
13
|
+
className?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AdaptersPage({ onNavigateDetail, className = '' }: AdaptersPageProps) {
|
|
17
|
+
const { statuses, isLoading } = useAdapters()
|
|
18
|
+
const [selectedDomain, setSelectedDomain] = useState<AdapterDomain | null>(null)
|
|
19
|
+
const [filterStatus, setFilterStatus] = useState<AdapterStatusType | undefined>()
|
|
20
|
+
|
|
21
|
+
// Status summary counts
|
|
22
|
+
const counts = {
|
|
23
|
+
connected: ADAPTER_DOMAINS.filter(d => statuses[d]?.status === 'connected').length,
|
|
24
|
+
error: ADAPTER_DOMAINS.filter(d => statuses[d]?.status === 'error').length,
|
|
25
|
+
total: ADAPTER_DOMAINS.length,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (isLoading) {
|
|
29
|
+
return (
|
|
30
|
+
<div className={`max-w-4xl mx-auto space-y-6 ${className}`}>
|
|
31
|
+
<div className="animate-pulse space-y-4">
|
|
32
|
+
<div className="h-8 w-48 rounded bg-white/10" />
|
|
33
|
+
<div className="h-4 w-72 rounded bg-white/5" />
|
|
34
|
+
<div className="grid gap-3 sm:grid-cols-3">{[1,2,3].map(i => <div key={i} className="h-20 rounded-2xl bg-white/[0.03]" />)}</div>
|
|
35
|
+
<div className="grid gap-3 sm:grid-cols-2">{[1,2,3,4].map(i => <div key={i} className="h-28 rounded-2xl bg-white/[0.03]" />)}</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className={`max-w-4xl mx-auto space-y-8 ${className}`}>
|
|
43
|
+
{/* Header */}
|
|
44
|
+
<div>
|
|
45
|
+
<h1 className="text-2xl font-extrabold text-white/95 tracking-tight">Adapters</h1>
|
|
46
|
+
<p className="mt-1 text-sm text-white/40">Manage your infrastructure adapters — database, auth, AI, storage, payments, and admin.</p>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{/* Status Summary */}
|
|
50
|
+
<div className="grid gap-3 sm:grid-cols-3">
|
|
51
|
+
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4 backdrop-blur-sm">
|
|
52
|
+
<p className="text-xs font-semibold uppercase tracking-wider text-white/30">Total</p>
|
|
53
|
+
<p className="mt-1 text-2xl font-extrabold text-white/90">{counts.total}</p>
|
|
54
|
+
<p className="text-[0.625rem] text-white/30">adapter domains</p>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="rounded-2xl border border-emerald-500/10 bg-emerald-500/[0.03] p-4 backdrop-blur-sm">
|
|
57
|
+
<p className="text-xs font-semibold uppercase tracking-wider text-emerald-400/60">Connected</p>
|
|
58
|
+
<p className="mt-1 text-2xl font-extrabold text-emerald-400">{counts.connected}</p>
|
|
59
|
+
<p className="text-[0.625rem] text-emerald-400/40">active adapters</p>
|
|
60
|
+
</div>
|
|
61
|
+
{counts.error > 0 && (
|
|
62
|
+
<div className="rounded-2xl border border-red-500/10 bg-red-500/[0.03] p-4 backdrop-blur-sm">
|
|
63
|
+
<p className="text-xs font-semibold uppercase tracking-wider text-red-400/60">Errors</p>
|
|
64
|
+
<p className="mt-1 text-2xl font-extrabold text-red-400">{counts.error}</p>
|
|
65
|
+
<p className="text-[0.625rem] text-red-400/40">need attention</p>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Filters */}
|
|
71
|
+
<div className="flex items-center gap-2">
|
|
72
|
+
<button
|
|
73
|
+
onClick={() => setFilterStatus(undefined)}
|
|
74
|
+
className={`rounded-lg px-3 py-1.5 text-xs font-semibold transition-all ${!filterStatus ? 'bg-white/10 text-white/80' : 'text-white/30 hover:text-white/50'}`}
|
|
75
|
+
>All</button>
|
|
76
|
+
{ADAPTER_STATUSES.map(s => (
|
|
77
|
+
<button
|
|
78
|
+
key={s}
|
|
79
|
+
onClick={() => setFilterStatus(s)}
|
|
80
|
+
className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semibold transition-all ${filterStatus === s ? 'bg-white/10 text-white/80' : 'text-white/30 hover:text-white/50'}`}
|
|
81
|
+
>
|
|
82
|
+
<AdapterStatusBadge status={s} showLabel={false} size="sm" />
|
|
83
|
+
{s}
|
|
84
|
+
</button>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Adapter Grid */}
|
|
89
|
+
<AdapterList
|
|
90
|
+
onSelect={(domain) => {
|
|
91
|
+
if (onNavigateDetail) { onNavigateDetail(domain) }
|
|
92
|
+
else { setSelectedDomain(domain) }
|
|
93
|
+
}}
|
|
94
|
+
filterStatus={filterStatus}
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
{/* Inline Config Modal */}
|
|
98
|
+
{selectedDomain && (
|
|
99
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={() => setSelectedDomain(null)}>
|
|
100
|
+
<div className="max-w-lg w-full mx-4" onClick={e => e.stopPropagation()}>
|
|
101
|
+
<AdapterConfigForm
|
|
102
|
+
domain={selectedDomain}
|
|
103
|
+
onSave={async () => { setSelectedDomain(null) }}
|
|
104
|
+
onCancel={() => setSelectedDomain(null)}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// @geenius-adapters/react — Provider
|
|
2
|
+
|
|
3
|
+
'use client'
|
|
4
|
+
|
|
5
|
+
import { createContext, useContext, useState, useCallback, useEffect, useMemo, type ReactNode } from 'react'
|
|
6
|
+
import type {
|
|
7
|
+
AuthAdapter, DbAdapter, PaymentsAdapter, AiAdapter, FileStorageAdapter, AdminAdapter,
|
|
8
|
+
AdapterDomain, AdapterStatusType, AdapterStatusInfo,
|
|
9
|
+
} from '@geenius-adapters/shared'
|
|
10
|
+
import { ADAPTER_DOMAINS, DOMAIN_LABELS } from '@geenius-adapters/shared'
|
|
11
|
+
|
|
12
|
+
// ─── Adapter Set ─────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface AdapterSet {
|
|
15
|
+
db?: DbAdapter
|
|
16
|
+
auth?: AuthAdapter
|
|
17
|
+
payments?: PaymentsAdapter
|
|
18
|
+
ai?: AiAdapter
|
|
19
|
+
storage?: FileStorageAdapter
|
|
20
|
+
admin?: AdminAdapter
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── Context ─────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface AdapterContextValue {
|
|
26
|
+
adapters: AdapterSet
|
|
27
|
+
statuses: Record<AdapterDomain, AdapterStatusInfo>
|
|
28
|
+
getAdapter: <K extends keyof AdapterSet>(domain: K) => NonNullable<AdapterSet[K]>
|
|
29
|
+
isReady: (domain: AdapterDomain) => boolean
|
|
30
|
+
isLoading: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const AdapterContext = createContext<AdapterContextValue | null>(null)
|
|
34
|
+
|
|
35
|
+
// ─── Provider ────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export interface AdapterProviderProps {
|
|
38
|
+
adapters: AdapterSet
|
|
39
|
+
children: ReactNode
|
|
40
|
+
/** Check adapter health on mount */
|
|
41
|
+
healthCheck?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function AdapterProvider({ adapters, children, healthCheck = false }: AdapterProviderProps) {
|
|
45
|
+
const [isLoading, setIsLoading] = useState(healthCheck)
|
|
46
|
+
const [statuses, setStatuses] = useState<Record<AdapterDomain, AdapterStatusInfo>>(() => {
|
|
47
|
+
const initial = {} as Record<AdapterDomain, AdapterStatusInfo>
|
|
48
|
+
for (const domain of ADAPTER_DOMAINS) {
|
|
49
|
+
const adapter = adapters[domain as keyof AdapterSet]
|
|
50
|
+
initial[domain] = {
|
|
51
|
+
domain,
|
|
52
|
+
provider: adapter ? 'configured' : 'none',
|
|
53
|
+
status: adapter ? 'connected' : 'disconnected',
|
|
54
|
+
lastCheckedAt: Date.now(),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return initial
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!healthCheck) return
|
|
62
|
+
setIsLoading(true)
|
|
63
|
+
// Simple health check — verify adapters are callable
|
|
64
|
+
const checks = ADAPTER_DOMAINS.map(async (domain) => {
|
|
65
|
+
const adapter = adapters[domain as keyof AdapterSet]
|
|
66
|
+
if (!adapter) return
|
|
67
|
+
try {
|
|
68
|
+
setStatuses(prev => ({ ...prev, [domain]: { ...prev[domain], status: 'initializing' } }))
|
|
69
|
+
// Basic connectivity test for each domain
|
|
70
|
+
if (domain === 'auth' && (adapter as AuthAdapter).getSession) {
|
|
71
|
+
await (adapter as AuthAdapter).getSession()
|
|
72
|
+
}
|
|
73
|
+
setStatuses(prev => ({
|
|
74
|
+
...prev,
|
|
75
|
+
[domain]: { ...prev[domain], status: 'connected' as AdapterStatusType, lastCheckedAt: Date.now() },
|
|
76
|
+
}))
|
|
77
|
+
} catch (e) {
|
|
78
|
+
setStatuses(prev => ({
|
|
79
|
+
...prev,
|
|
80
|
+
[domain]: {
|
|
81
|
+
...prev[domain],
|
|
82
|
+
status: 'error' as AdapterStatusType,
|
|
83
|
+
error: e instanceof Error ? e.message : 'Health check failed',
|
|
84
|
+
lastCheckedAt: Date.now(),
|
|
85
|
+
},
|
|
86
|
+
}))
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
Promise.allSettled(checks).finally(() => setIsLoading(false))
|
|
90
|
+
}, [adapters, healthCheck])
|
|
91
|
+
|
|
92
|
+
const getAdapter = useCallback(<K extends keyof AdapterSet>(domain: K): NonNullable<AdapterSet[K]> => {
|
|
93
|
+
const adapter = adapters[domain]
|
|
94
|
+
if (!adapter) throw new Error(`${DOMAIN_LABELS[domain as AdapterDomain] ?? domain} adapter not configured in <AdapterProvider>`)
|
|
95
|
+
return adapter as NonNullable<AdapterSet[K]>
|
|
96
|
+
}, [adapters])
|
|
97
|
+
|
|
98
|
+
const isReady = useCallback((domain: AdapterDomain) => {
|
|
99
|
+
return !!adapters[domain as keyof AdapterSet] && statuses[domain]?.status === 'connected'
|
|
100
|
+
}, [adapters, statuses])
|
|
101
|
+
|
|
102
|
+
const value = useMemo<AdapterContextValue>(() => ({
|
|
103
|
+
adapters, statuses, getAdapter, isReady, isLoading,
|
|
104
|
+
}), [adapters, statuses, getAdapter, isReady, isLoading])
|
|
105
|
+
|
|
106
|
+
return <AdapterContext.Provider value={value}>{children}</AdapterContext.Provider>
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Context Hook ────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export function useAdapterContext(): AdapterContextValue {
|
|
112
|
+
const ctx = useContext(AdapterContext)
|
|
113
|
+
if (!ctx) throw new Error('useAdapterContext must be used within <AdapterProvider>')
|
|
114
|
+
return ctx
|
|
115
|
+
}
|