@basictech/react 0.8.0-beta.4 → 0.9.0-beta.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.
@@ -1,665 +0,0 @@
1
- 'use client'
2
-
3
- import React, { useCallback, useMemo, useState } from 'react'
4
- import { useBasic, DBStatus } from '../context'
5
- import type { BasicSchemaDevInfo } from '../context'
6
- import { version as sdkVersion } from '../../package.json'
7
- import { isDevelopment } from '../utils/network'
8
-
9
- const INDEXED_DB_NAME = 'basicdb'
10
- const PANEL_PAD_X = 12
11
-
12
- type ChipTone = 'ok' | 'warn' | 'bad' | 'muted'
13
-
14
- function toneForAuth(isReady: boolean, isSignedIn: boolean): ChipTone {
15
- if (!isReady) return 'muted'
16
- if (isSignedIn) return 'ok'
17
- return 'warn'
18
- }
19
-
20
- function toneForDb(dbMode: string, dbStatus: DBStatus): ChipTone {
21
- if (dbMode === 'remote') return dbStatus === DBStatus.ONLINE ? 'ok' : 'warn'
22
- if (dbStatus === DBStatus.ONLINE || dbStatus === DBStatus.SYNCING) return 'ok'
23
- if (dbStatus === DBStatus.CONNECTING || dbStatus === DBStatus.LOADING) return 'warn'
24
- if (dbStatus === DBStatus.OFFLINE) return 'muted'
25
- return 'bad'
26
- }
27
-
28
- function toneForSchema(info: BasicSchemaDevInfo | null): ChipTone {
29
- if (!info) return 'muted'
30
- if (info.valid && info.status === 'current') return 'ok'
31
- if (info.status === 'unpublished') return 'warn'
32
- if (info.status === 'no_schema') return 'muted'
33
- return 'bad'
34
- }
35
-
36
- function dbStatusLabel(status: DBStatus): string {
37
- switch (status) {
38
- case DBStatus.LOADING:
39
- return 'Initializing'
40
- case DBStatus.OFFLINE:
41
- return 'Offline'
42
- case DBStatus.CONNECTING:
43
- return 'Connecting'
44
- case DBStatus.ONLINE:
45
- return 'Connected'
46
- case DBStatus.SYNCING:
47
- return 'Syncing'
48
- case DBStatus.ERROR:
49
- return 'Error'
50
- case DBStatus.ERROR_WILL_RETRY:
51
- return 'Retrying'
52
- case DBStatus.ERROR_TOKEN_EXPIRED:
53
- return 'Token refresh'
54
- default:
55
- return String(status)
56
- }
57
- }
58
-
59
- function chipColor(tone: ChipTone): string {
60
- switch (tone) {
61
- case 'ok':
62
- return '#22c55e'
63
- case 'warn':
64
- return '#eab308'
65
- case 'bad':
66
- return '#ef4444'
67
- default:
68
- return '#71717a'
69
- }
70
- }
71
-
72
- /** Full value for display in dev panel (wraps; no truncation). */
73
- function displayDid(did: string | null): string {
74
- return did || '—'
75
- }
76
-
77
- function displayUserLine(user: { sub?: string; email?: string; name?: string; picture?: string }): string {
78
- const parts: string[] = []
79
- if (user.sub) parts.push(`sub: ${user.sub}`)
80
- if (user.email) parts.push(`email: ${user.email}`)
81
- if (user.name) parts.push(`name: ${user.name}`)
82
- return parts.length ? parts.join(' · ') : '—'
83
- }
84
-
85
- function ClipboardIcon() {
86
- return (
87
- <svg
88
- width="14"
89
- height="14"
90
- viewBox="0 0 24 24"
91
- fill="none"
92
- stroke="currentColor"
93
- strokeWidth="2"
94
- strokeLinecap="round"
95
- strokeLinejoin="round"
96
- aria-hidden
97
- >
98
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
99
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
100
- </svg>
101
- )
102
- }
103
-
104
- export type BasicDevToolbarProps = {
105
- /** When false, toolbar does not render. Defaults to true when used standalone. */
106
- enabled?: boolean
107
- /** Same as BasicProvider `debug` — when true, toolbar shows even off localhost. */
108
- debug?: boolean
109
- }
110
-
111
- type CopyableRowProps = {
112
- rowKey: string
113
- label: string
114
- copyText: string
115
- copiedKey: string | null
116
- onCopied: (key: string) => void
117
- children: React.ReactNode
118
- }
119
-
120
- function SectionHeader({ children }: { children: React.ReactNode }) {
121
- return (
122
- <div
123
- style={{
124
- fontSize: 10,
125
- fontWeight: 700,
126
- color: '#e4e4e7',
127
- letterSpacing: '0.07em',
128
- textTransform: 'uppercase',
129
- marginBottom: 8,
130
- }}
131
- >
132
- {children}
133
- </div>
134
- )
135
- }
136
-
137
- /** Full-bleed horizontal rule: only above a section (not under the header). */
138
- function SectionRule() {
139
- const bleed = PANEL_PAD_X
140
- return (
141
- <div
142
- role="separator"
143
- style={{
144
- height: 1,
145
- background: 'rgba(255, 255, 255, 0.055)',
146
- marginLeft: -bleed,
147
- marginRight: -bleed,
148
- marginTop: 14,
149
- marginBottom: 10,
150
- width: `calc(100% + ${bleed * 2}px)`,
151
- }}
152
- />
153
- )
154
- }
155
-
156
- function CopyableRow({
157
- rowKey,
158
- label,
159
- copyText,
160
- copiedKey,
161
- onCopied,
162
- children,
163
- }: CopyableRowProps) {
164
- const [hover, setHover] = useState(false)
165
- const canCopy = copyText.length > 0
166
-
167
- const handleClick = useCallback(
168
- (e: React.MouseEvent) => {
169
- e.stopPropagation()
170
- if (!canCopy) return
171
- void navigator.clipboard.writeText(copyText).then(() => onCopied(rowKey))
172
- },
173
- [canCopy, copyText, onCopied, rowKey],
174
- )
175
-
176
- return (
177
- <div
178
- role={canCopy ? 'button' : undefined}
179
- tabIndex={canCopy ? 0 : undefined}
180
- onClick={canCopy ? handleClick : undefined}
181
- onKeyDown={
182
- canCopy
183
- ? (e) => {
184
- if (e.key === 'Enter' || e.key === ' ') {
185
- e.preventDefault()
186
- handleClick(e as unknown as React.MouseEvent)
187
- }
188
- }
189
- : undefined
190
- }
191
- onMouseEnter={() => setHover(true)}
192
- onMouseLeave={() => setHover(false)}
193
- style={{
194
- display: 'flex',
195
- gap: 8,
196
- marginBottom: 6,
197
- alignItems: 'flex-start',
198
- borderRadius: 6,
199
- padding: '4px 6px',
200
- marginLeft: -6,
201
- marginRight: -6,
202
- cursor: canCopy ? 'pointer' : 'default',
203
- background: hover && canCopy ? 'rgba(255,255,255,0.06)' : 'transparent',
204
- transition: 'background 0.12s ease',
205
- }}
206
- >
207
- <span style={{ color: '#a1a1aa', minWidth: 88, flexShrink: 0, paddingTop: 2 }}>{label}</span>
208
- <span
209
- style={{
210
- flex: 1,
211
- minWidth: 0,
212
- wordBreak: 'break-all',
213
- paddingTop: 2,
214
- lineHeight: 1.35,
215
- }}
216
- >
217
- {children}
218
- </span>
219
- {canCopy && (
220
- <span
221
- style={{
222
- flexShrink: 0,
223
- color: copiedKey === rowKey ? '#22c55e' : '#71717a',
224
- opacity: hover || copiedKey === rowKey ? 1 : 0,
225
- transition: 'opacity 0.12s ease, color 0.12s ease',
226
- paddingTop: 2,
227
- display: 'flex',
228
- alignItems: 'flex-start',
229
- }}
230
- title="Copy value"
231
- >
232
- {copiedKey === rowKey ? (
233
- <span style={{ fontSize: 10 }}>✓</span>
234
- ) : (
235
- <ClipboardIcon />
236
- )}
237
- </span>
238
- )}
239
- </div>
240
- )
241
- }
242
-
243
- /**
244
- * Floating dev-only toolbar: auth, DB/sync, and schema status. Requires `BasicProvider` with `debug` or localhost / NODE_ENV=development for visibility unless `enabled` is forced.
245
- */
246
- export function BasicDevToolbar({ enabled = true, debug }: BasicDevToolbarProps) {
247
- const {
248
- isReady,
249
- isSignedIn,
250
- user,
251
- did,
252
- scope,
253
- missingScopes,
254
- dbMode,
255
- dbStatus,
256
- devInfo,
257
- refreshSchemaStatus,
258
- } = useBasic()
259
-
260
- const [open, setOpen] = useState(false)
261
- const [refreshing, setRefreshing] = useState(false)
262
- const [copied, setCopied] = useState(false)
263
- const [rowCopied, setRowCopied] = useState<string | null>(null)
264
-
265
- const show =
266
- enabled && typeof window !== 'undefined' && isDevelopment(debug)
267
-
268
- const authTone = toneForAuth(isReady, isSignedIn)
269
- const dbTone = toneForDb(dbMode, dbStatus)
270
- const schemaTone = toneForSchema(devInfo)
271
-
272
- const syncTone: ChipTone =
273
- dbMode === 'remote'
274
- ? 'muted'
275
- : dbTone === 'ok' || dbStatus === DBStatus.SYNCING
276
- ? 'ok'
277
- : dbTone === 'warn'
278
- ? 'warn'
279
- : dbTone === 'bad'
280
- ? 'bad'
281
- : 'muted'
282
-
283
- const handleRefreshSchema = useCallback(async () => {
284
- setRefreshing(true)
285
- try {
286
- await refreshSchemaStatus()
287
- } finally {
288
- setRefreshing(false)
289
- }
290
- }, [refreshSchemaStatus])
291
-
292
- const missingList = missingScopes()
293
-
294
- const debugPayload = useMemo(() => {
295
- return {
296
- sdkVersion,
297
- isReady,
298
- isSignedIn,
299
- did: did ?? null,
300
- user: user
301
- ? {
302
- sub: user.sub,
303
- email: user.email,
304
- name: user.name,
305
- picture: user.picture,
306
- }
307
- : null,
308
- scope,
309
- missingScopes: missingList,
310
- dbMode,
311
- dbStatus,
312
- indexedDbName: dbMode === 'sync' ? INDEXED_DB_NAME : null,
313
- schema: devInfo,
314
- }
315
- }, [isReady, isSignedIn, did, user, scope, dbMode, dbStatus, devInfo, missingList])
316
-
317
- const handleCopy = useCallback(async () => {
318
- try {
319
- await navigator.clipboard.writeText(JSON.stringify(debugPayload, null, 2))
320
- setCopied(true)
321
- setTimeout(() => setCopied(false), 2000)
322
- } catch {
323
- /* ignore */
324
- }
325
- }, [debugPayload])
326
-
327
- const onRowCopied = useCallback((key: string) => {
328
- setRowCopied(key)
329
- setTimeout(() => setRowCopied((k) => (k === key ? null : k)), 1500)
330
- }, [])
331
-
332
- if (!show) return null
333
-
334
- const shell: React.CSSProperties = {
335
- position: 'fixed',
336
- bottom: 12,
337
- left: '50%',
338
- transform: 'translateX(-50%)',
339
- zIndex: 99999,
340
- fontFamily:
341
- 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
342
- fontSize: 11,
343
- color: '#e4e4e7',
344
- pointerEvents: 'auto',
345
- }
346
-
347
- const bar: React.CSSProperties = {
348
- display: 'flex',
349
- alignItems: 'center',
350
- gap: 8,
351
- padding: '8px 12px',
352
- borderRadius: 999,
353
- background: 'rgba(24, 24, 27, 0.92)',
354
- border: '1px solid rgba(63, 63, 70, 0.9)',
355
- boxShadow: '0 8px 32px rgba(0,0,0,0.35)',
356
- cursor: 'pointer',
357
- userSelect: 'none',
358
- }
359
-
360
- /** Fixed box so flex line-height / strut cannot stretch dots when the pill width changes. */
361
- const dot = (tone: ChipTone) => (
362
- <span
363
- style={{
364
- display: 'block',
365
- boxSizing: 'border-box',
366
- width: 6,
367
- height: 6,
368
- minWidth: 6,
369
- minHeight: 6,
370
- maxWidth: 6,
371
- maxHeight: 6,
372
- borderRadius: '50%',
373
- background: chipColor(tone),
374
- flexShrink: 0,
375
- }}
376
- />
377
- )
378
-
379
- const dotSlot = (title: string, tone: ChipTone) => (
380
- <span
381
- title={title}
382
- style={{
383
- display: 'inline-flex',
384
- alignItems: 'center',
385
- justifyContent: 'center',
386
- width: 6,
387
- height: 6,
388
- flexShrink: 0,
389
- lineHeight: 0,
390
- }}
391
- >
392
- {dot(tone)}
393
- </span>
394
- )
395
-
396
- const panel: React.CSSProperties = {
397
- marginBottom: 8,
398
- maxHeight: '50vh',
399
- overflow: 'auto',
400
- padding: PANEL_PAD_X,
401
- borderRadius: 10,
402
- background: 'rgba(24, 24, 27, 0.96)',
403
- border: '1px solid rgba(63, 63, 70, 0.9)',
404
- boxShadow: '0 8px 32px rgba(0,0,0,0.35)',
405
- minWidth: 300,
406
- maxWidth: 'min(560px, calc(100vw - 24px))',
407
- }
408
-
409
- const syncStatusText = dbStatusLabel(dbStatus)
410
-
411
- return (
412
- <div style={shell}>
413
- {open && (
414
- <div style={panel}>
415
- <div style={{ marginBottom: 12 }}>
416
- <div style={{ fontWeight: 600, fontSize: 12 }}>Basic SDK</div>
417
- <div style={{ color: '#71717a', fontSize: 10, marginTop: 2 }}>v{sdkVersion}</div>
418
- </div>
419
-
420
- <SectionHeader>Auth</SectionHeader>
421
- <CopyableRow
422
- rowKey="ready"
423
- label="Ready"
424
- copyText={String(isReady)}
425
- copiedKey={rowCopied}
426
- onCopied={onRowCopied}
427
- >
428
- {String(isReady)}
429
- </CopyableRow>
430
- <CopyableRow
431
- rowKey="signedIn"
432
- label="Signed in"
433
- copyText={String(isSignedIn)}
434
- copiedKey={rowCopied}
435
- onCopied={onRowCopied}
436
- >
437
- {String(isSignedIn)}
438
- </CopyableRow>
439
- <CopyableRow
440
- rowKey="did"
441
- label="DID"
442
- copyText={did || ''}
443
- copiedKey={rowCopied}
444
- onCopied={onRowCopied}
445
- >
446
- {displayDid(did)}
447
- </CopyableRow>
448
- <CopyableRow
449
- rowKey="user"
450
- label="User"
451
- copyText={user ? displayUserLine(user) : ''}
452
- copiedKey={rowCopied}
453
- onCopied={onRowCopied}
454
- >
455
- {user ? displayUserLine(user) : '—'}
456
- </CopyableRow>
457
- <CopyableRow
458
- rowKey="scopes"
459
- label="Scopes"
460
- copyText={scope || ''}
461
- copiedKey={rowCopied}
462
- onCopied={onRowCopied}
463
- >
464
- {scope || '—'}
465
- </CopyableRow>
466
- <CopyableRow
467
- rowKey="missingScopes"
468
- label="Missing scopes"
469
- copyText={missingList.length ? missingList.join(', ') : ''}
470
- copiedKey={rowCopied}
471
- onCopied={onRowCopied}
472
- >
473
- {missingList.length ? missingList.join(', ') : '—'}
474
- </CopyableRow>
475
-
476
- <SectionRule />
477
- <SectionHeader>Database</SectionHeader>
478
- <CopyableRow
479
- rowKey="dbMode"
480
- label="Mode"
481
- copyText={dbMode}
482
- copiedKey={rowCopied}
483
- onCopied={onRowCopied}
484
- >
485
- {dbMode}
486
- </CopyableRow>
487
- <CopyableRow
488
- rowKey="indexedDb"
489
- label="IndexedDB"
490
- copyText={dbMode === 'sync' ? INDEXED_DB_NAME : ''}
491
- copiedKey={rowCopied}
492
- onCopied={onRowCopied}
493
- >
494
- {dbMode === 'sync' ? INDEXED_DB_NAME : '—'}
495
- </CopyableRow>
496
- <CopyableRow
497
- rowKey="syncStatus"
498
- label="Sync / status"
499
- copyText={syncStatusText}
500
- copiedKey={rowCopied}
501
- onCopied={onRowCopied}
502
- >
503
- {syncStatusText}
504
- </CopyableRow>
505
-
506
- <SectionRule />
507
- <SectionHeader>Schema</SectionHeader>
508
- {devInfo ? (
509
- <>
510
- <CopyableRow
511
- rowKey="schemaProject"
512
- label="Project"
513
- copyText={devInfo.projectId ?? ''}
514
- copiedKey={rowCopied}
515
- onCopied={onRowCopied}
516
- >
517
- {devInfo.projectId ?? '—'}
518
- </CopyableRow>
519
- <CopyableRow
520
- rowKey="schemaLocalVer"
521
- label="Local version"
522
- copyText={
523
- devInfo.localVersion !== undefined && devInfo.localVersion !== null
524
- ? String(devInfo.localVersion)
525
- : ''
526
- }
527
- copiedKey={rowCopied}
528
- onCopied={onRowCopied}
529
- >
530
- {devInfo.localVersion ?? '—'}
531
- </CopyableRow>
532
- <CopyableRow
533
- rowKey="schemaRemote"
534
- label="Remote check"
535
- copyText={devInfo.status}
536
- copiedKey={rowCopied}
537
- onCopied={onRowCopied}
538
- >
539
- {devInfo.status}
540
- </CopyableRow>
541
- <CopyableRow
542
- rowKey="schemaValid"
543
- label="Valid"
544
- copyText={String(devInfo.valid)}
545
- copiedKey={rowCopied}
546
- onCopied={onRowCopied}
547
- >
548
- {String(devInfo.valid)}
549
- </CopyableRow>
550
- <CopyableRow
551
- rowKey="schemaChecked"
552
- label="Checked"
553
- copyText={
554
- devInfo.lastCheckedAt
555
- ? new Date(devInfo.lastCheckedAt).toISOString()
556
- : ''
557
- }
558
- copiedKey={rowCopied}
559
- onCopied={onRowCopied}
560
- >
561
- {devInfo.lastCheckedAt
562
- ? new Date(devInfo.lastCheckedAt).toLocaleString()
563
- : '—'}
564
- </CopyableRow>
565
- {devInfo.error ? (
566
- <CopyableRow
567
- rowKey="schemaError"
568
- label="Error"
569
- copyText={devInfo.error}
570
- copiedKey={rowCopied}
571
- onCopied={onRowCopied}
572
- >
573
- {devInfo.error}
574
- </CopyableRow>
575
- ) : null}
576
- </>
577
- ) : (
578
- <CopyableRow
579
- rowKey="schemaStatus"
580
- label="Status"
581
- copyText="No schema on provider"
582
- copiedKey={rowCopied}
583
- onCopied={onRowCopied}
584
- >
585
- No schema on provider
586
- </CopyableRow>
587
- )}
588
-
589
- <div style={{ display: 'flex', gap: 8, marginTop: 12, flexWrap: 'wrap' }}>
590
- <button
591
- type="button"
592
- onClick={(e) => {
593
- e.stopPropagation()
594
- void handleRefreshSchema()
595
- }}
596
- disabled={refreshing}
597
- style={{
598
- padding: '6px 10px',
599
- borderRadius: 6,
600
- border: '1px solid #3f3f46',
601
- background: '#27272a',
602
- color: '#e4e4e7',
603
- cursor: refreshing ? 'wait' : 'pointer',
604
- fontSize: 11,
605
- fontFamily: 'inherit',
606
- }}
607
- >
608
- {refreshing ? 'Refreshing…' : 'Refresh schema'}
609
- </button>
610
- <button
611
- type="button"
612
- onClick={(e) => {
613
- e.stopPropagation()
614
- void handleCopy()
615
- }}
616
- style={{
617
- padding: '6px 10px',
618
- borderRadius: 6,
619
- border: '1px solid #3f3f46',
620
- background: '#27272a',
621
- color: '#e4e4e7',
622
- cursor: 'pointer',
623
- fontSize: 11,
624
- fontFamily: 'inherit',
625
- }}
626
- >
627
- {copied ? 'Copied' : 'Copy debug info'}
628
- </button>
629
- </div>
630
- </div>
631
- )}
632
-
633
- <button
634
- type="button"
635
- aria-expanded={open}
636
- onClick={() => setOpen((o) => !o)}
637
- style={{
638
- ...bar,
639
- border: 'none',
640
- width: '100%',
641
- cursor: 'pointer',
642
- }}
643
- >
644
- <span style={{ fontWeight: 600, letterSpacing: 0.02 }}>Basic</span>
645
- <span
646
- style={{
647
- display: 'inline-flex',
648
- alignItems: 'center',
649
- gap: 6,
650
- marginLeft: 8,
651
- height: 6,
652
- flexShrink: 0,
653
- lineHeight: 0,
654
- }}
655
- >
656
- {dotSlot('Auth', authTone)}
657
- {dotSlot('DB', dbTone)}
658
- {dotSlot('Sync', syncTone)}
659
- {dotSlot('Schema', schemaTone)}
660
- </span>
661
- <span style={{ color: '#71717a', marginLeft: 4 }}>{open ? '▾' : '▴'}</span>
662
- </button>
663
- </div>
664
- )
665
- }
package/src/index.ts DELETED
@@ -1,36 +0,0 @@
1
- import { useBasic, BasicProvider } from "./AuthContext";
2
- import { useLiveQuery as useQuery } from "dexie-react-hooks";
3
-
4
- export { useBasic, BasicProvider, useQuery }
5
- export { BasicDevToolbar } from "./dev/BasicDevToolbar"
6
- export type { BasicDevToolbarProps } from "./dev/BasicDevToolbar"
7
-
8
- export type {
9
- AuthConfig,
10
- BasicStorage,
11
- LocalStorageAdapter,
12
- BasicProviderProps,
13
- BasicContextType,
14
- BasicSchemaDevInfo,
15
- AuthResult
16
- } from "./AuthContext"
17
- export { DBStatus } from "./AuthContext"
18
-
19
- // Core DB exports
20
- export type {
21
- DBMode,
22
- BasicDB,
23
- Collection,
24
- RemoteDBConfig,
25
- GetTokenOptions,
26
- AuthError
27
- } from "./core/db"
28
-
29
- export { RemoteDB, RemoteCollection, RemoteDBError, NotAuthenticatedError } from "./core/db"
30
-
31
- // Storage utilities
32
- export { STORAGE_KEYS } from "./utils/storage"
33
-
34
- // DID resolution
35
- export { resolveDid, resolveHandle, resolveDidWebUrl } from "./utils/resolveDid"
36
- export type { ResolvedDid } from "./utils/resolveDid"