@qrkit/react 0.0.1 → 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/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
1
+ {"version":3,"sources":["../src/context.tsx","../src/components/ConnectModal.tsx","../src/components/Modal.tsx","../src/hooks/useQRScanner.ts","../src/hooks/useURDecoder.ts","../src/components/QRScanner.tsx","../src/components/SignModal.tsx","../src/hooks/useQRDisplay.ts","../src/hooks/useQRParts.ts","../src/components/QRDisplay.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport { createPortal } from \"react-dom\";\n\nimport type { Account } from \"@qrkit/core\";\n\nimport { ConnectModal } from \"./components/ConnectModal.js\";\nimport { SignModal } from \"./components/SignModal.js\";\nimport type { QRKitContextValue, QRKitProviderProps, QRKitTheme, SignRequest } from \"./types.js\";\n\nconst QRKitContext = createContext<QRKitContextValue | null>(null);\n\nfunction buildThemeStyle(theme: QRKitTheme): string {\n const vars: Record<string, string | undefined> = {\n \"--qrkit-accent\": theme.accent,\n \"--qrkit-bg\": theme.background,\n \"--qrkit-backdrop\": theme.backdrop,\n \"--qrkit-text\": theme.text,\n \"--qrkit-text-muted\": theme.textMuted,\n \"--qrkit-radius\": theme.radius,\n \"--qrkit-font\": theme.fontFamily,\n };\n\n const declarations = Object.entries(vars)\n .filter(([, v]) => v !== undefined)\n .map(([k, v]) => ` ${k}: ${v};`)\n .join(\"\\n\");\n\n return declarations ? `.qrkit {\\n${declarations}\\n}` : \"\";\n}\n\ninterface PendingSign {\n request: SignRequest;\n resolve: (sig: string) => void;\n reject: (err: Error) => void;\n}\n\nexport function QRKitProvider({ children, theme = {}, appName = \"qrkit\" }: QRKitProviderProps) {\n const [account, setAccount] = useState<Account | null>(null);\n const [connectOpen, setConnectOpen] = useState(false);\n const [pendingSign, setPendingSign] = useState<PendingSign | null>(null);\n const pendingSignRef = useRef<PendingSign | null>(null);\n\n const themeStyle = useMemo(() => buildThemeStyle(theme), [theme]);\n\n useEffect(() => {\n if (!themeStyle) return;\n const el = document.createElement(\"style\");\n el.setAttribute(\"data-qrkit-theme\", \"\");\n el.textContent = themeStyle;\n document.head.appendChild(el);\n return () => el.remove();\n }, [themeStyle]);\n\n const connect = useCallback(() => setConnectOpen(true), []);\n const disconnect = useCallback(() => setAccount(null), []);\n\n const handleConnect = useCallback((acc: Account) => {\n setAccount(acc);\n setConnectOpen(false);\n }, []);\n\n const sign = useCallback((request: SignRequest): Promise<string> => {\n return new Promise((resolve, reject) => {\n const pending: PendingSign = { request, resolve, reject };\n pendingSignRef.current = pending;\n setPendingSign(pending);\n });\n }, []);\n\n const handleSign = useCallback((sig: string) => {\n pendingSignRef.current?.resolve(sig);\n pendingSignRef.current = null;\n setPendingSign(null);\n }, []);\n\n const handleReject = useCallback(() => {\n pendingSignRef.current?.reject(new Error(\"User rejected the sign request\"));\n pendingSignRef.current = null;\n setPendingSign(null);\n }, []);\n\n const value = useMemo<QRKitContextValue>(\n () => ({ account, connect, disconnect, sign }),\n [account, connect, disconnect, sign],\n );\n\n return (\n <QRKitContext.Provider value={value}>\n {children}\n {connectOpen &&\n createPortal(\n <ConnectModal onConnect={handleConnect} onClose={() => setConnectOpen(false)} />,\n document.body,\n )}\n {pendingSign &&\n createPortal(\n <SignModal\n request={pendingSign.request}\n appName={appName}\n onSign={handleSign}\n onReject={handleReject}\n />,\n document.body,\n )}\n </QRKitContext.Provider>\n );\n}\n\nexport function useQRKit(): QRKitContextValue {\n const ctx = useContext(QRKitContext);\n if (!ctx) throw new Error(\"useQRKit must be used within a QRKitProvider\");\n return ctx;\n}\n","import { useCallback } from \"react\";\n\nimport { parseConnection } from \"@qrkit/core\";\nimport type { Account, ScannedUR } from \"@qrkit/core\";\n\nimport { Modal } from \"./Modal.js\";\nimport { QRScanner } from \"./QRScanner.js\";\n\nexport interface ConnectModalProps {\n onConnect: (account: Account) => void;\n onClose: () => void;\n}\n\nexport function ConnectModal({ onConnect, onClose }: ConnectModalProps) {\n const handleScan = useCallback(\n (data: ScannedUR | string): boolean | void => {\n try {\n const accounts = parseConnection(data as ScannedUR, { chains: [\"evm\"] });\n const account = accounts[0];\n if (!account) return false;\n onConnect(account);\n } catch {\n return false;\n }\n },\n [onConnect],\n );\n\n return (\n <Modal title=\"Connect Wallet\" onClose={onClose}>\n <p className=\"qrkit-step\">\n On your hardware wallet, go to <strong>Connect software wallet</strong> and point the\n screen at this camera.\n </p>\n <QRScanner onScan={handleScan} hint=\"Scan the wallet's connection QR code\" />\n </Modal>\n );\n}\n","import { useEffect, useRef } from \"react\";\n\nimport { createFocusTrap } from \"focus-trap\";\n\nexport interface ModalProps {\n title: string;\n onClose: () => void;\n children: React.ReactNode;\n className?: string;\n}\n\nexport function Modal({ title, onClose, children, className }: ModalProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const el = containerRef.current;\n const trap = el\n ? createFocusTrap(el, {\n escapeDeactivates: true,\n onDeactivate: onClose,\n allowOutsideClick: true,\n })\n : null;\n\n trap?.activate();\n return () => { trap?.deactivate(); };\n }, [onClose]);\n\n useEffect(() => {\n const handleKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", handleKey);\n return () => document.removeEventListener(\"keydown\", handleKey);\n }, [onClose]);\n\n return (\n <div className=\"qrkit qrkit-backdrop\" onClick={onClose} role=\"dialog\" aria-modal=\"true\">\n <div\n ref={containerRef}\n className={`qrkit-modal${className ? ` ${className}` : \"\"}`}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"qrkit-modal-header\">\n <h2 className=\"qrkit-modal-title\">{title}</h2>\n <button className=\"qrkit-close-btn\" onClick={onClose} aria-label=\"Close\">\n ✕\n </button>\n </div>\n {children}\n </div>\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport QrScanner from \"qr-scanner\";\n\nimport type { ScannedUR } from \"@qrkit/core\";\n\nimport { useURDecoder } from \"./useURDecoder.js\";\n\nexport interface UseQRScannerOptions {\n /**\n * Called when a QR code is decoded.\n * Return false to keep scanning (e.g. on parse error), void/true to stop.\n */\n onScan: (result: ScannedUR | string) => boolean | void;\n enabled?: boolean;\n}\n\nexport interface UseQRScannerResult {\n videoRef: React.RefObject<HTMLVideoElement | null>;\n /** 0–100 while scanning an animated UR, null otherwise */\n progress: number | null;\n error: string | null;\n}\n\nexport function useQRScanner({\n onScan,\n enabled = true,\n}: UseQRScannerOptions): UseQRScannerResult {\n const videoRef = useRef<HTMLVideoElement>(null);\n const scannerRef = useRef<QrScanner | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const { receivePart, progress } = useURDecoder({ onScan });\n\n const processResult = useCallback(\n (data: string, scanner: QrScanner): void => {\n const done = receivePart(data);\n if (done) scanner.stop();\n },\n [receivePart],\n );\n\n useEffect(() => {\n if (!enabled || !videoRef.current) return;\n\n const scanner = new QrScanner(\n videoRef.current,\n (result) => processResult(result.data, scanner),\n {\n preferredCamera: \"environment\",\n highlightScanRegion: false,\n highlightCodeOutline: false,\n },\n );\n\n scannerRef.current = scanner;\n\n scanner.start().catch(() => {\n setError(\"Camera access denied. Please allow camera permissions.\");\n });\n\n return () => {\n scanner.stop();\n scanner.destroy();\n scannerRef.current = null;\n };\n }, [enabled, processResult]);\n\n return { videoRef, progress, error };\n}\n","import { useCallback, useRef, useState } from \"react\";\n\nimport { URDecoder } from \"@ngraveio/bc-ur\";\n\nimport type { ScannedUR } from \"@qrkit/core\";\n\nexport interface UseURDecoderOptions {\n /**\n * Called when a QR string is decoded.\n * Return false to keep scanning (e.g. on parse error), void/true to stop.\n */\n onScan: (result: ScannedUR | string) => boolean | void;\n}\n\nexport interface UseURDecoderResult {\n /** Feed a raw QR string into the decoder. Returns true when scanning is done. */\n receivePart: (data: string) => boolean;\n /** 0–100 while assembling an animated UR, null otherwise */\n progress: number | null;\n /** Reset decoder state (e.g. to start a new scan) */\n reset: () => void;\n}\n\nexport function useURDecoder({ onScan }: UseURDecoderOptions): UseURDecoderResult {\n const decoderRef = useRef<URDecoder>(new URDecoder());\n const onScanRef = useRef(onScan);\n const [progress, setProgress] = useState<number | null>(null);\n\n onScanRef.current = onScan;\n\n const reset = useCallback(() => {\n decoderRef.current = new URDecoder();\n setProgress(null);\n }, []);\n\n const receivePart = useCallback(\n (data: string): boolean => {\n if (!data.toLowerCase().startsWith(\"ur:\")) {\n return onScanRef.current(data) !== false;\n }\n\n decoderRef.current.receivePart(data.toLowerCase());\n setProgress(Math.round(decoderRef.current.estimatedPercentComplete() * 100));\n\n if (!decoderRef.current.isComplete()) return false;\n\n const ur = decoderRef.current.resultUR();\n const scanned: ScannedUR = { type: ur.type, cbor: new Uint8Array(ur.cbor) };\n if (onScanRef.current(scanned) !== false) return true;\n\n reset();\n return false;\n },\n [reset],\n );\n\n return { receivePart, progress, reset };\n}\n","import type { ScannedUR } from \"@qrkit/core\";\n\nimport { useQRScanner } from \"../hooks/useQRScanner.js\";\n\nexport interface QRScannerProps {\n onScan: (result: ScannedUR | string) => boolean | void;\n hint?: string;\n enabled?: boolean;\n className?: string;\n}\n\nexport function QRScanner({ onScan, hint, enabled = true, className }: QRScannerProps) {\n const { videoRef, progress, error } = useQRScanner({ onScan, enabled });\n\n if (error) {\n return <div className={`qrkit-scanner-error${className ? ` ${className}` : \"\"}`}>{error}</div>;\n }\n\n return (\n <div className={`qrkit-scanner-wrap${className ? ` ${className}` : \"\"}`}>\n <video ref={videoRef} autoPlay playsInline muted className=\"qrkit-scanner-video\" />\n <div className=\"qrkit-scanner-overlay\">\n <div className=\"qrkit-scanner-corner tl\" />\n <div className=\"qrkit-scanner-corner tr\" />\n <div className=\"qrkit-scanner-corner bl\" />\n <div className=\"qrkit-scanner-corner br\" />\n </div>\n {progress !== null && progress < 100 && (\n <div className=\"qrkit-scanner-progress\">{progress}%</div>\n )}\n <p className=\"qrkit-hint\" style={{ position: \"absolute\", bottom: 8, left: 0, right: 0 }}>\n {progress !== null && progress < 100\n ? \"Keep scanning — animated QR in progress…\"\n : (hint ?? \"Point camera at the QR code\")}\n </p>\n </div>\n );\n}\n","import { useCallback, useState } from \"react\";\n\nimport { buildEthSignRequestURParts, parseEthSignature } from \"@qrkit/core\";\nimport type { ScannedUR } from \"@qrkit/core\";\n\nimport type { SignRequest } from \"../types.js\";\nimport { Modal } from \"./Modal.js\";\nimport { QRDisplay } from \"./QRDisplay.js\";\nimport { QRScanner } from \"./QRScanner.js\";\n\ntype Step = \"display\" | \"scan\";\n\nexport interface SignModalProps {\n request: SignRequest;\n appName: string;\n onSign: (signature: string) => void;\n onReject: () => void;\n}\n\nexport function SignModal({ request, appName, onSign, onReject }: SignModalProps) {\n const [step, setStep] = useState<Step>(\"display\");\n\n const parts = buildEthSignRequestURParts(\n request.message,\n request.address,\n request.sourceFingerprint,\n appName,\n );\n\n const handleScan = useCallback(\n (data: ScannedUR | string): boolean | void => {\n try {\n const sig = parseEthSignature(data as ScannedUR);\n onSign(sig);\n } catch {\n return false;\n }\n },\n [onSign],\n );\n\n return (\n <Modal\n title={step === \"display\" ? \"Sign Request\" : \"Scan Signature\"}\n onClose={onReject}\n >\n {step === \"display\" && (\n <>\n <p className=\"qrkit-step\">\n Point your hardware wallet camera at this QR code to approve the sign request.\n </p>\n <QRDisplay parts={parts} />\n <button className=\"qrkit-btn qrkit-btn-primary\" onClick={() => setStep(\"scan\")}>\n Wallet signed — scan response\n </button>\n <button className=\"qrkit-btn qrkit-btn-ghost\" onClick={onReject}>\n Cancel\n </button>\n </>\n )}\n\n {step === \"scan\" && (\n <>\n <p className=\"qrkit-step\">\n On your hardware wallet, show the signature QR and point it at this camera.\n </p>\n <QRScanner onScan={handleScan} hint=\"Scan the wallet's signature QR code\" />\n <button className=\"qrkit-btn qrkit-btn-ghost\" onClick={() => setStep(\"display\")}>\n ← Back\n </button>\n </>\n )}\n </Modal>\n );\n}\n","import { useEffect, useRef } from \"react\";\n\nimport QRCode from \"qrcode\";\n\nimport { useQRParts } from \"./useQRParts.js\";\n\nexport interface UseQRDisplayOptions {\n /** QR parts to cycle through. Single-frame: pass an array with one element. */\n parts: string[];\n /** Interval between frames in ms. Default: 200 */\n interval?: number;\n /** Canvas size in pixels. Default: 300 */\n size?: number;\n}\n\nexport interface UseQRDisplayResult {\n canvasRef: React.RefObject<HTMLCanvasElement | null>;\n /** Current frame index */\n frame: number;\n total: number;\n}\n\nexport function useQRDisplay({\n parts,\n interval,\n size = 300,\n}: UseQRDisplayOptions): UseQRDisplayResult {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const { part, frame, total } = useQRParts({ parts, interval });\n\n useEffect(() => {\n if (!part || !canvasRef.current) return;\n\n QRCode.toCanvas(canvasRef.current, part, {\n width: size,\n margin: 2,\n errorCorrectionLevel: \"M\",\n }).catch(() => {\n // ignore render errors\n });\n }, [part, size]);\n\n return { canvasRef, frame, total };\n}\n","import { useEffect, useRef, useState } from \"react\";\n\nexport interface UseQRPartsOptions {\n /** QR parts to cycle through. Single-frame: pass an array with one element. */\n parts: string[];\n /** Interval between frames in ms. Default: 200 */\n interval?: number;\n}\n\nexport interface UseQRPartsResult {\n /** The current QR string to render */\n part: string;\n /** Current frame index (0-based) */\n frame: number;\n total: number;\n}\n\nexport function useQRParts({\n parts,\n interval = 200,\n}: UseQRPartsOptions): UseQRPartsResult {\n const [frame, setFrame] = useState(0);\n const frameRef = useRef(0);\n\n useEffect(() => {\n frameRef.current = 0;\n setFrame(0);\n }, [parts]);\n\n useEffect(() => {\n if (parts.length <= 1) return;\n\n const id = setInterval(() => {\n frameRef.current = (frameRef.current + 1) % parts.length;\n setFrame(frameRef.current);\n }, interval);\n\n return () => clearInterval(id);\n }, [parts, interval]);\n\n return {\n part: parts[frameRef.current % Math.max(parts.length, 1)] ?? \"\",\n frame,\n total: parts.length,\n };\n}\n","import { useQRDisplay } from \"../hooks/useQRDisplay.js\";\n\nexport interface QRDisplayProps {\n parts: string[];\n interval?: number;\n size?: number;\n className?: string;\n}\n\nexport function QRDisplay({ parts, interval, size = 300, className }: QRDisplayProps) {\n const { canvasRef, frame, total } = useQRDisplay({ parts, interval, size });\n\n return (\n <div className={`qrkit-qr-wrap${className ? ` ${className}` : \"\"}`}>\n <canvas ref={canvasRef} className=\"qrkit-qr-canvas\" width={size} height={size} />\n {total > 1 && (\n <p className=\"qrkit-hint\">\n Frame {frame + 1} / {total} — keep Shell pointed at the screen\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["useCallback","useEffect","useRef","useState","useCallback","useCallback","useEffect","useRef","useState","useRef","useRef","useState","useCallback","useEffect","jsx","jsxs","jsx","jsxs","useCallback","useCallback","useState","useEffect","useRef","useEffect","useRef","useState","useRef","useEffect","jsx","jsxs","jsx","jsxs","useState","useCallback","jsx","jsxs","useState","useRef","useEffect","useCallback"]}
@@ -0,0 +1,183 @@
1
+ /* src/styles.css */
2
+ .qrkit {
3
+ --qrkit-accent: #6750a4;
4
+ --qrkit-bg: #fffbfe;
5
+ --qrkit-backdrop: rgba(0, 0, 0, 0.6);
6
+ --qrkit-text: #1c1b1f;
7
+ --qrkit-text-muted: #49454f;
8
+ --qrkit-radius: 12px;
9
+ --qrkit-font: inherit;
10
+ }
11
+ @media (prefers-color-scheme: dark) {
12
+ .qrkit {
13
+ --qrkit-accent: #d0bcff;
14
+ --qrkit-bg: #1c1b1f;
15
+ --qrkit-backdrop: rgba(0, 0, 0, 0.75);
16
+ --qrkit-text: #e6e1e5;
17
+ --qrkit-text-muted: #cac4d0;
18
+ }
19
+ }
20
+ .qrkit-backdrop {
21
+ position: fixed;
22
+ inset: 0;
23
+ background: var(--qrkit-backdrop);
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ z-index: 9999;
28
+ font-family: var(--qrkit-font);
29
+ }
30
+ .qrkit-modal {
31
+ background: var(--qrkit-bg);
32
+ color: var(--qrkit-text);
33
+ border-radius: var(--qrkit-radius);
34
+ padding: 28px 24px 24px;
35
+ width: 100%;
36
+ max-width: 400px;
37
+ max-height: 90vh;
38
+ overflow-y: auto;
39
+ box-shadow: 0 8px 40px rgba(0, 0, 0, 0.18);
40
+ display: flex;
41
+ flex-direction: column;
42
+ gap: 20px;
43
+ }
44
+ .qrkit-modal-header {
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: space-between;
48
+ }
49
+ .qrkit-modal-title {
50
+ margin: 0;
51
+ font-size: 18px;
52
+ font-weight: 600;
53
+ color: var(--qrkit-text);
54
+ }
55
+ .qrkit-close-btn {
56
+ background: none;
57
+ border: none;
58
+ cursor: pointer;
59
+ font-size: 20px;
60
+ line-height: 1;
61
+ color: var(--qrkit-text-muted);
62
+ padding: 4px;
63
+ border-radius: 4px;
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ }
68
+ .qrkit-close-btn:hover {
69
+ color: var(--qrkit-text);
70
+ }
71
+ .qrkit-scanner-wrap {
72
+ position: relative;
73
+ width: 100%;
74
+ aspect-ratio: 1;
75
+ border-radius: calc(var(--qrkit-radius) - 4px);
76
+ overflow: hidden;
77
+ background: #000;
78
+ }
79
+ .qrkit-scanner-video {
80
+ width: 100%;
81
+ height: 100%;
82
+ object-fit: cover;
83
+ display: block;
84
+ }
85
+ .qrkit-scanner-overlay {
86
+ position: absolute;
87
+ inset: 0;
88
+ pointer-events: none;
89
+ }
90
+ .qrkit-scanner-corner {
91
+ position: absolute;
92
+ width: 24px;
93
+ height: 24px;
94
+ border-color: var(--qrkit-accent);
95
+ border-style: solid;
96
+ }
97
+ .qrkit-scanner-corner.tl {
98
+ top: 12px;
99
+ left: 12px;
100
+ border-width: 3px 0 0 3px;
101
+ border-radius: 4px 0 0 0;
102
+ }
103
+ .qrkit-scanner-corner.tr {
104
+ top: 12px;
105
+ right: 12px;
106
+ border-width: 3px 3px 0 0;
107
+ border-radius: 0 4px 0 0;
108
+ }
109
+ .qrkit-scanner-corner.bl {
110
+ bottom: 12px;
111
+ left: 12px;
112
+ border-width: 0 0 3px 3px;
113
+ border-radius: 0 0 0 4px;
114
+ }
115
+ .qrkit-scanner-corner.br {
116
+ bottom: 12px;
117
+ right: 12px;
118
+ border-width: 0 3px 3px 0;
119
+ border-radius: 0 0 4px 0;
120
+ }
121
+ .qrkit-scanner-progress {
122
+ position: absolute;
123
+ bottom: 10px;
124
+ left: 50%;
125
+ transform: translateX(-50%);
126
+ background: rgba(0, 0, 0, 0.6);
127
+ color: #fff;
128
+ font-size: 12px;
129
+ padding: 3px 10px;
130
+ border-radius: 20px;
131
+ }
132
+ .qrkit-scanner-error {
133
+ color: #c00;
134
+ font-size: 14px;
135
+ text-align: center;
136
+ }
137
+ .qrkit-hint {
138
+ margin: 0;
139
+ font-size: 13px;
140
+ color: var(--qrkit-text-muted);
141
+ text-align: center;
142
+ }
143
+ .qrkit-qr-wrap {
144
+ display: flex;
145
+ flex-direction: column;
146
+ align-items: center;
147
+ gap: 12px;
148
+ }
149
+ .qrkit-qr-canvas {
150
+ border-radius: calc(var(--qrkit-radius) - 4px);
151
+ display: block;
152
+ }
153
+ .qrkit-btn {
154
+ display: inline-flex;
155
+ align-items: center;
156
+ justify-content: center;
157
+ padding: 10px 20px;
158
+ border-radius: calc(var(--qrkit-radius) - 4px);
159
+ font-size: 15px;
160
+ font-weight: 500;
161
+ cursor: pointer;
162
+ border: none;
163
+ transition: opacity 0.15s;
164
+ width: 100%;
165
+ }
166
+ .qrkit-btn:hover {
167
+ opacity: 0.85;
168
+ }
169
+ .qrkit-btn-primary {
170
+ background: var(--qrkit-accent);
171
+ color: #fff;
172
+ }
173
+ .qrkit-btn-ghost {
174
+ background: transparent;
175
+ color: var(--qrkit-text-muted);
176
+ border: 1px solid currentColor;
177
+ }
178
+ .qrkit-step {
179
+ font-size: 13px;
180
+ color: var(--qrkit-text-muted);
181
+ text-align: center;
182
+ margin: 0;
183
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qrkit/react",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "React context, hooks, and drop-in components for QR-based airgapped wallet flows.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -9,7 +9,8 @@
9
9
  "types": "./dist/index.d.ts",
10
10
  "import": "./dist/index.js",
11
11
  "require": "./dist/index.cjs"
12
- }
12
+ },
13
+ "./styles.css": "./dist/styles.css"
13
14
  },
14
15
  "main": "./dist/index.cjs",
15
16
  "module": "./dist/index.js",
@@ -18,7 +19,9 @@
18
19
  "dist"
19
20
  ],
20
21
  "dependencies": {
21
- "jsqr": "^1.4.0",
22
+ "@ngraveio/bc-ur": "^1.1.11",
23
+ "focus-trap": "^8.0.1",
24
+ "qr-scanner": "^1.4.2",
22
25
  "qrcode": "^1.5.4",
23
26
  "@qrkit/core": "0.1.0"
24
27
  },
@@ -27,9 +30,12 @@
27
30
  "react-dom": ">=18"
28
31
  },
29
32
  "devDependencies": {
33
+ "@testing-library/jest-dom": "^6.9.1",
34
+ "@testing-library/react": "^16.3.2",
30
35
  "@types/qrcode": "^1.5.6",
31
36
  "@types/react": "^19.2.14",
32
37
  "@types/react-dom": "^19.2.3",
38
+ "jsdom": "^29.0.1",
33
39
  "react": "^19.2.4",
34
40
  "react-dom": "^19.2.4",
35
41
  "tsup": "^8.4.0",