+
{filename ?? lang}
-
-
+
+
{children.trim()}
@@ -54,7 +54,7 @@ export function Table({ headers, rows }: TableProps) {
{headers.map((h) => (
{h}
|
@@ -67,7 +67,7 @@ export function Table({ headers, rows }: TableProps) {
{row.map((cell, j) => (
{cell}
|
diff --git a/apps/docs/aura-ui.d.ts b/apps/docs/aura-ui.d.ts
index 4f233ab..475ca60 100644
--- a/apps/docs/aura-ui.d.ts
+++ b/apps/docs/aura-ui.d.ts
@@ -1,9 +1,40 @@
import "react";
+type AuraElement> = React.DetailedHTMLProps<
+ React.HTMLAttributes,
+ HTMLElement
+> &
+ T & { class?: string };
+
declare module "react" {
namespace JSX {
interface IntrinsicElements {
- "a-theme-provider": React.HTMLAttributes;
+ "a-theme-provider": AuraElement;
+ "a-button": AuraElement<{
+ variant?: string;
+ size?: string;
+ color?: string;
+ type?: string;
+ disabled?: boolean;
+ selected?: boolean;
+ }>;
+ "a-badge": AuraElement<{
+ variant?: string;
+ size?: string;
+ rounded?: boolean;
+ removable?: boolean;
+ }>;
+ "a-card": AuraElement<{ variant?: string; interactive?: boolean }>;
+ "a-input": AuraElement<{
+ type?: string;
+ label?: string;
+ name?: string;
+ placeholder?: string;
+ value?: string;
+ disabled?: boolean;
+ }>;
+ "a-text": AuraElement<{ variant?: string; size?: string }>;
+ "a-separator": AuraElement<{ orientation?: string }>;
}
}
}
diff --git a/apps/interface/api/projects/[id]/verify.ts b/apps/interface/api/projects/[id]/verify.ts
index 9bb6406..768d405 100644
--- a/apps/interface/api/projects/[id]/verify.ts
+++ b/apps/interface/api/projects/[id]/verify.ts
@@ -1,5 +1,6 @@
import { and, eq } from 'drizzle-orm'
import { z } from 'zod'
+import withCors from '../../lib/cors.js'
import { db } from '../../lib/db.js'
import { projectsTable, verificationsTable } from '../../lib/schema.js'
@@ -10,7 +11,7 @@ const verifySchema = z.object({
userId: z.string()
})
-export async function POST(req: Request, { params }: { params: { id: string } }) {
+async function handler(req: Request, { params }: { params: { id: string } }) {
try {
const body = verifySchema.parse(await req.json())
const projectId = Number(params.id)
@@ -104,3 +105,5 @@ export async function POST(req: Request, { params }: { params: { id: string } })
return Response.json({ error: 'Invalid request' }, { status: 400 })
}
}
+
+export default withCors(handler)
diff --git a/apps/interface/src/app-routes.ts b/apps/interface/src/app-routes.ts
index 20ef936..1cd88b4 100644
--- a/apps/interface/src/app-routes.ts
+++ b/apps/interface/src/app-routes.ts
@@ -127,7 +127,9 @@ export const createRouter = (classThis: ReactiveControllerHost & HTMLElement) =>
return true
},
render: ({ id }) =>
- html` `
+ html``
},
{
path: '*',
diff --git a/apps/interface/src/utils/apis/index.ts b/apps/interface/src/utils/apis/index.ts
index 8bd393f..53b4f10 100644
--- a/apps/interface/src/utils/apis/index.ts
+++ b/apps/interface/src/utils/apis/index.ts
@@ -32,3 +32,39 @@ export const getProjects = async () => {
return (res.data! ?? []) as Project[]
}
+
+export interface VerificationSignature {
+ r: string
+ s: string
+ v: number
+}
+
+export interface VerifyProjectResult {
+ userId: string
+ projectId: number
+ client: string
+ signature: VerificationSignature
+ auraScore?: number
+ auraLevel?: number
+ verifiedAt: string
+}
+
+/**
+ * Calls the interface app's verify endpoint to generate a verification
+ * signature for a verified user. Returns the signature payload on success.
+ */
+export const verifyProject = async (
+ projectId: number,
+ payload: { userId: string; client: string; auraScore?: number; auraLevel?: number }
+) => {
+ const res = await clientAPI.POST('/projects/{id}/verify' as never, {
+ params: { path: { id: String(projectId) } },
+ body: payload
+ } as never)
+
+ if ((res as { error?: unknown }).error) {
+ throw new Error('Failed to generate verification signature')
+ }
+
+ return (res as { data?: { data?: VerifyProjectResult } }).data?.data
+}
diff --git a/apps/interface/vite.config.ts b/apps/interface/vite.config.ts
index 9297d5a..4a735ad 100644
--- a/apps/interface/vite.config.ts
+++ b/apps/interface/vite.config.ts
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'
import { litHMRPlugin } from './vite-plugin-lit-hmr'
+
const dirname =
typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url))
@@ -17,6 +18,8 @@ export default defineConfig({
},
plugins: [tsconfigPaths(), litHMRPlugin()],
server: {
+ host: true,
+ allowedHosts: ['localhost', '.localhost'],
port: 3000,
proxy: {
'^/auranode(/.*)?$': {
diff --git a/bun.lock b/bun.lock
index 8d80cc8..7b39149 100644
--- a/bun.lock
+++ b/bun.lock
@@ -47,8 +47,10 @@
"@sentry/react": "^7.70.0",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18",
+ "@tanstack/query-sync-storage-persister": "5.99.0",
"@tanstack/react-query": "^5.99.0",
"@tanstack/react-query-devtools": "^5.99.0",
+ "@tanstack/react-query-persist-client": "5.99.0",
"apisauce": "2.0.1",
"axios": "^1.5.0",
"base64-js": "^1.5.1",
@@ -160,6 +162,35 @@
"vitest-canvas-mock": "^0.3.3",
},
},
+ "apps/core": {
+ "name": "@aura/core",
+ "version": "2.1.0",
+ "dependencies": {
+ "@aura/domain": "workspace:*",
+ "@aura/ui": "workspace:*",
+ "@solid-primitives/storage": "^4.3.4",
+ "@solidjs/router": "^0.15.3",
+ "@tanstack/query-async-storage-persister": "^5.100.14",
+ "@tanstack/solid-query": "^5.100.14",
+ "@tanstack/solid-query-persist-client": "^5.100.14",
+ "bcryptjs": "^3.0.3",
+ "libphonenumber-js": "^1.13.3",
+ "localforage": "^1.10.0",
+ "qrcode": "^1.5.4",
+ "solid-js": "^1.9.5",
+ "solid-motionone": "^1.0.4",
+ "tw-animate-css": "^1.4.0",
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.1.18",
+ "@types/bcryptjs": "^3.0.0",
+ "@types/qrcode": "^1.5.6",
+ "tailwindcss": "^4.1.18",
+ "typescript": "^5.9.2",
+ "vite": "^6.1.0",
+ "vite-plugin-solid": "^2.11.6",
+ },
+ },
"apps/dashboard": {
"name": "aura-dashboard",
"version": "0.0.1",
@@ -242,6 +273,22 @@
"vite-tsconfig-paths": "^5.1.4",
},
},
+ "apps/demo-integration": {
+ "name": "@aura/demo-integration",
+ "version": "0.0.0",
+ "dependencies": {
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ },
+ "devDependencies": {
+ "@aura/typescript-config": "workspace:*",
+ "@types/react": "^19.2.2",
+ "@types/react-dom": "^19.2.2",
+ "@vitejs/plugin-react": "^4.3.4",
+ "typescript": "5.9.2",
+ "vite": "^6.3.5",
+ },
+ },
"apps/docs": {
"name": "docs",
"version": "0.1.0",
@@ -325,6 +372,20 @@
"vite-tsconfig-paths": "^5.1.4",
},
},
+ "packages/domain": {
+ "name": "@aura/domain",
+ "version": "0.1.0",
+ "dependencies": {
+ "base64-js": "^1.5.1",
+ "crypto-js": "^4.2.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "tweetnacl": "^1.0.3",
+ },
+ "devDependencies": {
+ "@types/crypto-js": "^4.2.2",
+ "typescript": "~5.9.3",
+ },
+ },
"packages/eslint-config": {
"name": "@aura/eslint-config",
"version": "0.0.0",
@@ -357,6 +418,7 @@
"name": "@aura/sdk",
"version": "0.0.14-beta.1",
"dependencies": {
+ "@aura/domain": "workspace:*",
"@lit-app/state": "^1.0.0",
"@lit-labs/router": "^0.1.4",
"@lit-labs/signals": "^0.1.2",
@@ -364,7 +426,6 @@
"@zerodev/ecdsa-validator": "^5.4.9",
"@zerodev/wagmi": "^4.6.7",
"axios": "^1.13.5",
- "base64-js": "^1.5.1",
"lit": "^3.3.0",
"tweetnacl": "^1.0.3",
"viem": "^2.45.3",
@@ -393,7 +454,7 @@
},
"packages/ui": {
"name": "@aura/ui",
- "version": "0.0.1",
+ "version": "0.0.2",
"dependencies": {
"@lit/react": "^1.0.7",
"lit": "^3.3.2",
@@ -457,6 +518,12 @@
"@aura/aura": ["@aura/aura@workspace:apps/aura"],
+ "@aura/core": ["@aura/core@workspace:apps/core"],
+
+ "@aura/demo-integration": ["@aura/demo-integration@workspace:apps/demo-integration"],
+
+ "@aura/domain": ["@aura/domain@workspace:packages/domain"],
+
"@aura/eslint-config": ["@aura/eslint-config@workspace:packages/eslint-config"],
"@aura/interface": ["@aura/interface@workspace:apps/interface"],
@@ -1219,6 +1286,18 @@
"@module-federation/webpack-bundler-runtime": ["@module-federation/webpack-bundler-runtime@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA=="],
+ "@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
+
+ "@motionone/dom": ["@motionone/dom@10.18.0", "", { "dependencies": { "@motionone/animation": "^10.18.0", "@motionone/generators": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-bKLP7E0eyO4B2UaHBBN55tnppwRnaE3KFfh3Ps9HhnAkar3Cb69kUCJY9as8LrccVYKgHA+JY5dOQqJLOPhF5A=="],
+
+ "@motionone/easing": ["@motionone/easing@10.18.0", "", { "dependencies": { "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg=="],
+
+ "@motionone/generators": ["@motionone/generators@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg=="],
+
+ "@motionone/types": ["@motionone/types@10.17.1", "", {}, "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A=="],
+
+ "@motionone/utils": ["@motionone/utils@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw=="],
+
"@msgpack/msgpack": ["@msgpack/msgpack@3.1.2", "", {}, "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ=="],
"@mswjs/interceptors": ["@mswjs/interceptors@0.41.0", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-edAo9bW53BLYeSK+UPRr2Iz1Fj9DeGMjytvVM0HXRoo750ElWUgPsZPAOTQa12EUiwgDErH2PsFNTLvk1jBxjQ=="],
@@ -1831,6 +1910,18 @@
"@solana/web3.js": ["@solana/web3.js@1.98.4", "", { "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", "@noble/hashes": "^1.4.0", "@solana/buffer-layout": "^4.0.1", "@solana/codecs-numbers": "^2.1.0", "agentkeepalive": "^4.5.0", "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.1", "node-fetch": "^2.7.0", "rpc-websockets": "^9.0.2", "superstruct": "^2.0.2" } }, "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw=="],
+ "@solid-primitives/props": ["@solid-primitives/props@3.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-XzG6en9gSFwmvbKcATm2BxL63HegZ+BAG5fmHi8jyBppQHcaths7ffz+6vYvwYy3nlgLa20ufJLj7tst+PcHFA=="],
+
+ "@solid-primitives/refs": ["@solid-primitives/refs@1.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA=="],
+
+ "@solid-primitives/storage": ["@solid-primitives/storage@4.3.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12" }, "optionalPeers": ["@tauri-apps/plugin-store"] }, "sha512-GSxPAIuyxhJWOcv7n10iv3aid5oHN3KUgyA9IV0GYWlPpgyGs43aS9E85b0VXDLoH+D4ThNK8+2WEJ8B/S6Ccg=="],
+
+ "@solid-primitives/transition-group": ["@solid-primitives/transition-group@1.1.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-gnHS0OmcdjeoHN9n7Khu8KNrOlRc8a2weETDt2YT6o1zeW/XtUC6Db3Q9pkMU/9cCKdEmN4b0a/41MKAHRhzWA=="],
+
+ "@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="],
+
+ "@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="],
+
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
@@ -1903,18 +1994,30 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
+ "@tanstack/query-async-storage-persister": ["@tanstack/query-async-storage-persister@5.100.14", "", { "dependencies": { "@tanstack/query-core": "5.100.14", "@tanstack/query-persist-client-core": "5.100.14" } }, "sha512-otwoi4OsYoECIZBqDZ/crzyJFDXvzwCMg+T/Z+mforv9/GTqkYShddaItNC7SkFRq9VMIO3R3T71rfZfmb0NOA=="],
+
"@tanstack/query-core": ["@tanstack/query-core@5.99.0", "", {}, "sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ=="],
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.99.0", "", {}, "sha512-m4ufXaJ8FjWXw7xDtyzE/6fkZAyQFg9WrbMrUpt8ZecRJx58jiFOZ2lxZMphZdIpAnIeto/S8stbwLKLusyckQ=="],
+ "@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.99.0", "", { "dependencies": { "@tanstack/query-core": "5.99.0" } }, "sha512-FzXoxqGYa+ozGNGBIPHJT06rJD4geiHGbVXNS8K7cwN34te64LXlO5Ep+roQYvZjXJFNrLa4W48DIYITpMxzuA=="],
+
+ "@tanstack/query-sync-storage-persister": ["@tanstack/query-sync-storage-persister@5.99.0", "", { "dependencies": { "@tanstack/query-core": "5.99.0", "@tanstack/query-persist-client-core": "5.99.0" } }, "sha512-Q7xJ+AiesEEB8ZKGrsv7qJfhyr0eT1Z+84TZxQspGZZnJziOalcLUOLGgHOxPaEQGGcxqM1NbrgGYO/WpnTjeQ=="],
+
"@tanstack/react-query": ["@tanstack/react-query@5.99.0", "", { "dependencies": { "@tanstack/query-core": "5.99.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw=="],
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.99.0", "", { "dependencies": { "@tanstack/query-devtools": "5.99.0" }, "peerDependencies": { "@tanstack/react-query": "^5.99.0", "react": "^18 || ^19" } }, "sha512-CqqX7LCU9yOfCY/vBURSx2YSD83ryfX+QkfkaKionTfg1s2Hdm572Ro99gW3QPoJjzvsj1HM4pnN4nbDy3MXKA=="],
+ "@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.99.0", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.99.0" }, "peerDependencies": { "@tanstack/react-query": "^5.99.0", "react": "^18 || ^19" } }, "sha512-A3TTaaYZOy6dFUNJj3r10rwmUBWaXqc2N71a7jzpFXilYAYWLKJf9ivT5n5bhXsJnSkdvv1RLifUg/GdvDGaDw=="],
+
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.18", "", { "dependencies": { "@tanstack/virtual-core": "3.13.18" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A=="],
+ "@tanstack/solid-query": ["@tanstack/solid-query@5.100.14", "", { "dependencies": { "@tanstack/query-core": "5.100.14" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-eKW5fPWuNjGBjK9To/DNNS2b3HwTvD58T6CZbN6H0HyCjDrBOlH8q4qyJQS9gR9EXflSiivgQK+DUzg3KIHNDw=="],
+
+ "@tanstack/solid-query-persist-client": ["@tanstack/solid-query-persist-client@5.100.14", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.100.14" }, "peerDependencies": { "@tanstack/solid-query": "^5.100.14", "solid-js": "^1.6.0" } }, "sha512-CdQaaOTl9MEcfG7nJ//5rfwu0dA7W5FxIpXhRjcNYR86tKRmgKkfne98HZWpw8EGdiXYstPHClw071oNrTFgPw=="],
+
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.18", "", {}, "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg=="],
@@ -2003,6 +2106,8 @@
"@types/bcrypt": ["@types/bcrypt@6.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ=="],
+ "@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
+
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
"@types/caseless": ["@types/caseless@0.12.5", "", {}, "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg=="],
@@ -2461,12 +2566,16 @@
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
+ "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="],
+
"babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.15", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-define-polyfill-provider": "^0.6.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw=="],
"babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.14.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.6", "core-js-compat": "^3.48.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ=="],
"babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.6", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.6" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A=="],
+ "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="],
+
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
@@ -3315,6 +3424,8 @@
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
+ "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
+
"hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="],
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
@@ -3511,6 +3622,8 @@
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
+ "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="],
+
"is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="],
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
@@ -3635,7 +3748,7 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
- "libphonenumber-js": ["libphonenumber-js@1.12.41", "", {}, "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA=="],
+ "libphonenumber-js": ["libphonenumber-js@1.13.1", "", {}, "sha512-GEw0GLL7YUUA6nv21IsCvVjtI5Ejn84sjbdfQ9KxdbqEVOk1PZh7xejn01EEiniKw+dBeCfim+8MGeuvVuE2BA=="],
"lie": ["lie@3.1.1", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw=="],
@@ -3795,6 +3908,8 @@
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
+ "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
+
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
@@ -4501,6 +4616,10 @@
"serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="],
+ "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="],
+
+ "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="],
+
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
@@ -4561,6 +4680,12 @@
"socket.io-parser": ["socket.io-parser@4.2.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="],
+ "solid-js": ["solid-js@1.9.13", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ=="],
+
+ "solid-motionone": ["solid-motionone@1.0.4", "", { "dependencies": { "@motionone/dom": "^10.17.0", "@motionone/utils": "^10.17.0", "@solid-primitives/props": "^3.1.11", "@solid-primitives/refs": "^1.0.8", "@solid-primitives/transition-group": "^1.0.5", "csstype": "^3.1.3" }, "peerDependencies": { "solid-js": "^1.8.0" } }, "sha512-aqEjgecoO9raDFznu/dEci7ORSmA26Kjj9J4Cn1Gyr0GZuOVdvsNxdxClTL9J40Aq/uYFx4GLwC8n70fMLHiuA=="],
+
+ "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
+
"sonic-boom": ["sonic-boom@3.8.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg=="],
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
@@ -4969,8 +5094,12 @@
"vite-plugin-pwa": ["vite-plugin-pwa@0.21.2", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.3.0", "workbox-window": "^7.3.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^0.2.6", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-vFhH6Waw8itNu37hWUJxL50q+CBbNcMVzsKaYHQVrfxTt3ihk3PeLO22SbiP1UNWzcEPaTQv+YVxe4G0KOjAkg=="],
+ "vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="],
+
"vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="],
+ "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
+
"vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
"vitest-canvas-mock": ["vitest-canvas-mock@0.3.3", "", { "dependencies": { "jest-canvas-mock": "~2.5.2" }, "peerDependencies": { "vitest": "*" } }, "sha512-3P968tYBpqYyzzOaVtqnmYjqbe13576/fkjbDEJSfQAkHtC5/UjuRHOhFEN/ZV5HVZIkaROBUWgazDKJ+Ibw+Q=="],
@@ -5119,6 +5248,12 @@
"@aura/aura/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+ "@aura/core/libphonenumber-js": ["libphonenumber-js@1.13.3", "", {}, "sha512-xMkdAMqcyG7iN2WZZmGIfWbYxW4orRkny+0/AXIbwL0xll2zkDX0Vzo/BXFa6+7mh2UvJl9MbcTtHk0YXkFtBA=="],
+
+ "@aura/core/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "@aura/domain/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
"@aura/eslint-config/eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
"@aura/eslint-config/eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
@@ -5129,6 +5264,8 @@
"@aura/interface/@types/ramda": ["@types/ramda@0.31.1", "", { "dependencies": { "types-ramda": "^0.31.0" } }, "sha512-Vt6sFXnuRpzaEj+yeutA0q3bcAsK7wdPuASIzR9LXqL4gJPyFw8im9qchlbp4ltuf3kDEIRmPJTD/Fkg60dn7g=="],
+ "@aura/interface/libphonenumber-js": ["libphonenumber-js@1.12.41", "", {}, "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA=="],
+
"@aura/interface/ramda": ["ramda@0.31.3", "", {}, "sha512-xKADKRNnqmDdX59PPKLm3gGmk1ZgNnj3k7DryqWwkamp4TJ6B36DdpyKEQ0EoEYmH2R62bV4Q+S0ym2z8N2f3Q=="],
"@aura/interface/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
@@ -5147,8 +5284,6 @@
"@aura/ui/vite-tsconfig-paths": ["vite-tsconfig-paths@6.0.5", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-f/WvY6ekHykUF1rWJUAbCU7iS/5QYDIugwpqJA+ttwKbxSbzNlqlE8vZSrsnxNQciUW+z6lvhlXMaEyZn9MSig=="],
- "@aura/widgets/libphonenumber-js": ["libphonenumber-js@1.13.1", "", {}, "sha512-GEw0GLL7YUUA6nv21IsCvVjtI5Ejn84sjbdfQ9KxdbqEVOk1PZh7xejn01EEiniKw+dBeCfim+8MGeuvVuE2BA=="],
-
"@aura/widgets/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
@@ -5405,6 +5540,16 @@
"@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+ "@motionone/animation/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@motionone/dom/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@motionone/easing/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@motionone/generators/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@motionone/utils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
"@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
"@npmcli/fs/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
@@ -5665,6 +5810,14 @@
"@tailwindcss/vite/tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
+ "@tanstack/query-async-storage-persister/@tanstack/query-core": ["@tanstack/query-core@5.100.14", "", {}, "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew=="],
+
+ "@tanstack/query-async-storage-persister/@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.100.14", "", { "dependencies": { "@tanstack/query-core": "5.100.14" } }, "sha512-mn60cqoQO/xB6aHxp/hxlSj5mcdcTO4tjj4SXSz5MKzkaMZnvcEGySz3+cGQOT8McREN56fL41L0eR//v5RwNw=="],
+
+ "@tanstack/solid-query/@tanstack/query-core": ["@tanstack/query-core@5.100.14", "", {}, "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew=="],
+
+ "@tanstack/solid-query-persist-client/@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.100.14", "", { "dependencies": { "@tanstack/query-core": "5.100.14" } }, "sha512-mn60cqoQO/xB6aHxp/hxlSj5mcdcTO4tjj4SXSz5MKzkaMZnvcEGySz3+cGQOT8McREN56fL41L0eR//v5RwNw=="],
+
"@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
"@toruslabs/base-controllers/@metamask/rpc-errors": ["@metamask/rpc-errors@6.4.0", "", { "dependencies": { "@metamask/utils": "^9.0.0", "fast-safe-stringify": "^2.0.6" } }, "sha512-1ugFO1UoirU2esS3juZanS/Fo8C8XYocCuBpfZI5N7ECtoG+zu0wF+uWZASik6CkO6w9n/Iebt4iI4pT0vptpg=="],
@@ -5943,6 +6096,10 @@
"aura-dashboard/uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
+ "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
+
+ "babel-plugin-jsx-dom-expressions/html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
+
"basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
@@ -7155,6 +7312,8 @@
"@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="],
+ "@tanstack/solid-query-persist-client/@tanstack/query-persist-client-core/@tanstack/query-core": ["@tanstack/query-core@5.100.14", "", {}, "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew=="],
+
"@toruslabs/base-controllers/@metamask/rpc-errors/@metamask/utils": ["@metamask/utils@9.3.0", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.1.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", "@types/debug": "^4.1.7", "debug": "^4.3.4", "pony-cause": "^2.1.10", "semver": "^7.5.4", "uuid": "^9.0.1" } }, "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g=="],
"@toruslabs/base-controllers/@toruslabs/broadcast-channel/oblivious-set": ["oblivious-set@1.1.1", "", {}, "sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w=="],
diff --git a/packages/domain/package.json b/packages/domain/package.json
new file mode 100644
index 0000000..4a676d7
--- /dev/null
+++ b/packages/domain/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "@aura/domain",
+ "version": "0.1.0",
+ "type": "module",
+ "description": "Framework-agnostic Aura/BrightID domain logic: crypto, recovery channels, scoring, verifications and shared types",
+ "license": "MIT",
+ "files": ["dist", "src", "package.json"],
+ "main": "./src/index.ts",
+ "module": "./src/index.ts",
+ "types": "./src/index.ts",
+ "exports": {
+ ".": "./src/index.ts",
+ "./*": "./src/*.ts"
+ },
+ "scripts": {
+ "build": "tsc --noEmit",
+ "check-types": "tsc --noEmit"
+ },
+ "dependencies": {
+ "base64-js": "^1.5.1",
+ "crypto-js": "^4.2.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "tweetnacl": "^1.0.3"
+ },
+ "devDependencies": {
+ "@types/crypto-js": "^4.2.2",
+ "typescript": "~5.9.3"
+ }
+}
diff --git a/packages/domain/src/channel.ts b/packages/domain/src/channel.ts
new file mode 100644
index 0000000..24d9813
--- /dev/null
+++ b/packages/domain/src/channel.ts
@@ -0,0 +1,100 @@
+/*
+ * Channel service client.
+ *
+ * Replaces the old `ChannelAPI` class + apisauce instance. These are plain
+ * fetch helpers — the recovery flow drives them through TanStack Query
+ * (mutation for upload, polling query for list/download) instead of calling a
+ * stateful class.
+ *
+ * POST /profile/upload/{channelId}
+ * GET /profile/list/{channelId}
+ * GET /profile/download/{channelId}/{dataId}
+ */
+
+const NO_CACHE = { 'Cache-Control': 'no-cache' };
+
+async function errorFrom(res: Response): Promise {
+ try {
+ const body = await res.json();
+ if (body?.error) return body.error as string;
+ } catch {
+ // not json
+ }
+ return `Request failed with status ${res.status}`;
+}
+
+export interface ChannelTarget {
+ /** Channel base url, e.g. `/auranode-test/profile` (proxied) */
+ channelUrl: string;
+ channelId: string;
+}
+
+export async function uploadToChannel({
+ channelUrl,
+ channelId,
+ data,
+ dataId,
+ requestedTtl,
+}: ChannelTarget & {
+ data: string;
+ dataId: string;
+ requestedTtl?: number;
+}): Promise {
+ const body = JSON.stringify({
+ data,
+ uuid: dataId,
+ // backend expects seconds
+ requestedTtl: requestedTtl ? Math.floor(requestedTtl / 1000) : undefined,
+ });
+ const res = await fetch(`${channelUrl}/upload/${channelId}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', ...NO_CACHE },
+ body,
+ });
+ if (!res.ok) throw new Error(await errorFrom(res));
+}
+
+export async function listChannel({
+ channelUrl,
+ channelId,
+}: ChannelTarget): Promise {
+ const res = await fetch(`${channelUrl}/list/${channelId}`, {
+ headers: NO_CACHE,
+ });
+ if (!res.ok) throw new Error(await errorFrom(res));
+ const json = (await res.json()) as { profileIds?: string[] };
+ if (!json?.profileIds) {
+ throw new Error(
+ `list for channel ${channelId}: unexpected response format`,
+ );
+ }
+ return json.profileIds;
+}
+
+export async function downloadFromChannel({
+ channelUrl,
+ channelId,
+ dataId,
+ deleteAfterDownload,
+}: ChannelTarget & {
+ dataId: string;
+ deleteAfterDownload?: boolean;
+}): Promise {
+ const res = await fetch(`${channelUrl}/download/${channelId}/${dataId}`, {
+ headers: NO_CACHE,
+ });
+ if (!res.ok) throw new Error(await errorFrom(res));
+ const json = (await res.json()) as { data?: string };
+ if (deleteAfterDownload) {
+ // best-effort cleanup, ignore failures
+ fetch(`${channelUrl}/${channelId}/${dataId}`, { method: 'DELETE' }).catch(
+ () => {},
+ );
+ }
+ if (!json?.data) {
+ throw new Error(
+ `download ${dataId} from channel ${channelId}: unexpected response format`,
+ );
+ }
+ return json.data;
+}
diff --git a/packages/domain/src/crypto.ts b/packages/domain/src/crypto.ts
new file mode 100644
index 0000000..6fb3200
--- /dev/null
+++ b/packages/domain/src/crypto.ts
@@ -0,0 +1,48 @@
+import { fromByteArray, toByteArray } from 'base64-js';
+import CryptoJS from 'crypto-js';
+import nacl from 'tweetnacl';
+
+export function encryptData(data: string, password: string) {
+ return CryptoJS.AES.encrypt(data, password).toString();
+}
+
+export function decryptData(data: string, password: string) {
+ return CryptoJS.AES.decrypt(data, password).toString(CryptoJS.enc.Utf8);
+}
+
+const URL_SAFE_MAP: Record = { '/': '_', '+': '-', '=': '' };
+export const b64ToUrlSafeB64 = (s: string) =>
+ s.replace(/[/+=]/g, (c) => URL_SAFE_MAP[c] ?? c);
+
+export const hash = (data: string) => {
+ const b = CryptoJS.SHA256(data).toString(CryptoJS.enc.Base64);
+ return b64ToUrlSafeB64(b);
+};
+
+export const randomWordArray = (size: number) =>
+ CryptoJS.lib.WordArray.random(size);
+
+/** Random url-safe base64 key of `bytes` length — used as the channel AES key. */
+export const urlSafeRandomKey = (bytes = 16): string =>
+ b64ToUrlSafeB64(wordArrayToB64(randomWordArray(bytes)));
+
+export const uInt8ArrayToB64 = (array: Uint8Array): string =>
+ fromByteArray(array);
+
+/** Decode a standard (non-url-safe) base64 string into bytes. */
+export const b64ToUint8Array = (str: string): Uint8Array => toByteArray(str);
+
+/** UTF-8 encode a string into bytes (matches the old app's `strToUint8Array`). */
+export const strToUint8Array = (str: string): Uint8Array =>
+ new TextEncoder().encode(str);
+
+export const wordArrayToB64 = (wa: CryptoJS.lib.WordArray) =>
+ CryptoJS.enc.Base64.stringify(wa);
+
+export const generateB64Keypair = () => {
+ const { publicKey, secretKey } = nacl.sign.keyPair();
+ return {
+ privateKey: fromByteArray(secretKey),
+ publicKey: fromByteArray(publicKey),
+ };
+};
diff --git a/packages/domain/src/globals.d.ts b/packages/domain/src/globals.d.ts
new file mode 100644
index 0000000..ce74e25
--- /dev/null
+++ b/packages/domain/src/globals.d.ts
@@ -0,0 +1,4 @@
+declare module "fast-json-stable-stringify" {
+ /** Deterministic JSON serialization with sorted object keys. */
+ export default function stringify(value: unknown): string
+}
diff --git a/packages/domain/src/http.ts b/packages/domain/src/http.ts
new file mode 100644
index 0000000..77b4708
--- /dev/null
+++ b/packages/domain/src/http.ts
@@ -0,0 +1,71 @@
+/** Default per-request ceiling so a hung server can't wedge a query forever. */
+const REQUEST_TIMEOUT_MS = 30_000
+
+/** Non-2xx response — carries the status so callers can branch on it. */
+export class HttpError extends Error {
+ readonly status: number
+
+ constructor(message: string, status: number) {
+ super(message)
+ this.name = "HttpError"
+ this.status = status
+ }
+}
+
+/**
+ * Whether an error is a 404 — e.g. a brightid profile or connections lookup
+ * for a user the aura node hasn't seen yet (profiles are created by their
+ * first received evaluation).
+ */
+export const isNotFound = (e: unknown): boolean =>
+ e instanceof HttpError && e.status === 404
+
+/**
+ * Fetch with a timeout, honoring an optional caller signal (e.g. query
+ * cancellation on unmount). Aborts on whichever fires first.
+ */
+async function fetchWithTimeout(
+ url: string,
+ init?: RequestInit,
+ signal?: AbortSignal,
+): Promise {
+ const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS)
+ const composed = signal ? AbortSignal.any([signal, timeout]) : timeout
+ return fetch(url, { ...init, signal: composed })
+}
+
+/** GET + parse JSON, throwing on non-2xx. */
+export async function getJson(url: string, signal?: AbortSignal): Promise {
+ const res = await fetchWithTimeout(url, undefined, signal)
+ if (!res.ok)
+ throw new HttpError(`GET ${url} failed with status ${res.status}`, res.status)
+ return (await res.json()) as T
+}
+
+/** GET raw text, throwing on non-2xx (encrypted backup blobs). */
+export async function getText(url: string, signal?: AbortSignal): Promise {
+ const res = await fetchWithTimeout(url, undefined, signal)
+ if (!res.ok)
+ throw new HttpError(`GET ${url} failed with status ${res.status}`, res.status)
+ return await res.text()
+}
+
+/** POST a JSON body + parse the JSON response, throwing on non-2xx. */
+export async function postJson(
+ url: string,
+ body: unknown,
+ signal?: AbortSignal,
+): Promise {
+ const res = await fetchWithTimeout(
+ url,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ },
+ signal,
+ )
+ if (!res.ok)
+ throw new HttpError(`POST ${url} failed with status ${res.status}`, res.status)
+ return (await res.json()) as T
+}
diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts
new file mode 100644
index 0000000..b51b041
--- /dev/null
+++ b/packages/domain/src/index.ts
@@ -0,0 +1,15 @@
+export * from "./channel"
+export * from "./crypto"
+export * from "./http"
+export * from "./labels"
+export * from "./levels"
+export * from "./notifications"
+export * from "./operations"
+export * from "./passkeys"
+export * from "./recovery"
+export * from "./score"
+export * from "./types/aura"
+export * from "./types/dashboard"
+export * from "./types/evaluations"
+export * from "./verifications"
+export * from "./view-mode"
diff --git a/packages/domain/src/labels.ts b/packages/domain/src/labels.ts
new file mode 100644
index 0000000..a42c348
--- /dev/null
+++ b/packages/domain/src/labels.ts
@@ -0,0 +1,28 @@
+import { EvaluationCategory } from "./types/evaluations"
+
+/** Human label for a confidence value (1–4). */
+export const CONFIDENCE_LABELS: Record = {
+ 1: "Low",
+ 2: "Medium",
+ 3: "High",
+ 4: "Very High",
+}
+
+export const confidenceLabel = (value: number): string =>
+ CONFIDENCE_LABELS[Math.abs(value)] ?? String(Math.abs(value))
+
+/** Display label for an evaluation category. */
+export const categoryLabel: Record = {
+ [EvaluationCategory.SUBJECT]: "Subject",
+ [EvaluationCategory.PLAYER]: "Player",
+ [EvaluationCategory.TRAINER]: "Trainer",
+ [EvaluationCategory.MANAGER]: "Manager",
+}
+
+/** Who evaluates a category: subjects ← players ← trainers ← managers. */
+export const categoryEvaluatedBy: Record = {
+ [EvaluationCategory.SUBJECT]: EvaluationCategory.PLAYER,
+ [EvaluationCategory.PLAYER]: EvaluationCategory.TRAINER,
+ [EvaluationCategory.TRAINER]: EvaluationCategory.MANAGER,
+ [EvaluationCategory.MANAGER]: EvaluationCategory.MANAGER,
+}
diff --git a/packages/domain/src/levels.ts b/packages/domain/src/levels.ts
new file mode 100644
index 0000000..fe27cfe
--- /dev/null
+++ b/packages/domain/src/levels.ts
@@ -0,0 +1,15 @@
+import { EvaluationCategory } from "./types/evaluations"
+
+export const subjectLevelPoints = [
+ 0, 1_000_000, 5_000_000, 10_000_000, 150_000_000,
+]
+export const playerLevelPoints = [0, 1_000_000, 2_000_000, 3_000_000]
+export const trainerLevelPoints = [0, 500_000, 1_000_000]
+export const managerLevelPoints = [0, 1_000, 200_000]
+
+export const userLevelPoints = {
+ [EvaluationCategory.MANAGER]: managerLevelPoints,
+ [EvaluationCategory.PLAYER]: playerLevelPoints,
+ [EvaluationCategory.SUBJECT]: subjectLevelPoints,
+ [EvaluationCategory.TRAINER]: trainerLevelPoints,
+}
diff --git a/packages/domain/src/notifications.ts b/packages/domain/src/notifications.ts
new file mode 100644
index 0000000..dda4791
--- /dev/null
+++ b/packages/domain/src/notifications.ts
@@ -0,0 +1,234 @@
+import type { AuraNodeBrightIdConnection, Verifications } from "./types/aura"
+import { EvaluationCategory, EvaluationValue } from "./types/evaluations"
+import { getAuraVerification } from "./verifications"
+
+/** Thresholds from the old app's notifications store. */
+export const ALERT_THRESHOLDS = {
+ LEVEL_CHANGE: 1,
+ SCORE_CHANGE_PERCENT: 35,
+}
+
+export type NotificationKind =
+ | "evaluation"
+ | "score-increase"
+ | "score-decrease"
+ | "level-increase"
+ | "level-decrease"
+
+export interface NotificationAlert {
+ id: string
+ kind: NotificationKind
+ category: EvaluationCategory
+ /** Who the alert is about — links to their subject page. */
+ about: string
+ /** Optional second party (e.g. the subject an evaluator rated). */
+ to: string | null
+ previous: number | null
+ next: number | null
+ timestamp: number
+ viewed: boolean
+}
+
+export interface TrackedProfile {
+ level: number | null
+ score: number | null
+ /** evaluator → confidence; only tracked for outbound subjects. */
+ evaluators?: Record
+}
+
+/** Keyed by `${id}:${category}`. */
+export type TrackedProfiles = Record
+
+export const trackedKey = (id: string, category: EvaluationCategory) =>
+ `${id}:${category}`
+
+export const NOTIFICATION_CATEGORIES = [
+ EvaluationCategory.SUBJECT,
+ EvaluationCategory.PLAYER,
+ EvaluationCategory.TRAINER,
+ EvaluationCategory.MANAGER,
+]
+
+const alertId = (
+ kind: NotificationKind,
+ about: string,
+ category: EvaluationCategory,
+ timestamp: number,
+) => `${kind}:${about}:${category}:${timestamp}`
+
+function levelScoreAlerts(
+ id: string,
+ category: EvaluationCategory,
+ prev: TrackedProfile | undefined,
+ level: number | null,
+ score: number | null,
+ now: number,
+): NotificationAlert[] {
+ if (!prev) return []
+ const alerts: NotificationAlert[] = []
+
+ if (
+ level !== null &&
+ prev.level !== null &&
+ Math.abs(prev.level - level) >= ALERT_THRESHOLDS.LEVEL_CHANGE
+ ) {
+ alerts.push({
+ id: alertId(level > prev.level ? "level-increase" : "level-decrease", id, category, now),
+ kind: level > prev.level ? "level-increase" : "level-decrease",
+ category,
+ about: id,
+ to: null,
+ previous: prev.level,
+ next: level,
+ timestamp: now,
+ viewed: false,
+ })
+ }
+
+ if (
+ score !== null &&
+ prev.score !== null &&
+ prev.score !== 0 &&
+ Math.abs((score / prev.score) * 100 - 100) >=
+ ALERT_THRESHOLDS.SCORE_CHANGE_PERCENT
+ ) {
+ alerts.push({
+ id: alertId(score > prev.score ? "score-increase" : "score-decrease", id, category, now),
+ kind: score > prev.score ? "score-increase" : "score-decrease",
+ category,
+ about: id,
+ to: null,
+ previous: prev.score,
+ next: score,
+ timestamp: now,
+ viewed: false,
+ })
+ }
+
+ return alerts
+}
+
+/**
+ * Pure diff of fresh node data against the previously tracked snapshot.
+ * Produces alerts for: own level/score changes per category, new inbound
+ * evaluations of the user, and changes on subjects the user has evaluated
+ * (their level/score plus other evaluators' new or changed ratings).
+ *
+ * The first run (`lastFetch === null`) only seeds the tracked snapshot and
+ * yields no alerts — otherwise every existing evaluation would fire at once.
+ * No I/O and no clock access: the caller passes `now`.
+ */
+export function diffNotifications(params: {
+ subjectId: string
+ /** The user's own verifications (from their profile). */
+ verifications: Verifications | undefined
+ /** The user's inbound connections (evaluations of them ride along). */
+ inbound: AuraNodeBrightIdConnection[]
+ /** The user's outbound connections (subjects they evaluated). */
+ outbound: AuraNodeBrightIdConnection[]
+ prevTracked: TrackedProfiles
+ lastFetch: number | null
+ now: number
+}): { alerts: NotificationAlert[]; tracked: TrackedProfiles } {
+ const { subjectId, verifications, inbound, outbound, prevTracked, lastFetch, now } = params
+
+ const seedOnly = lastFetch === null
+ const tracked: TrackedProfiles = { ...prevTracked }
+ const alerts: NotificationAlert[] = []
+
+ // ── Own level/score per category ─────────────────────────
+ for (const category of NOTIFICATION_CATEGORIES) {
+ const v = getAuraVerification(verifications, category)
+ const key = trackedKey(subjectId, category)
+ if (!seedOnly) {
+ alerts.push(
+ ...levelScoreAlerts(
+ subjectId,
+ category,
+ tracked[key],
+ v?.level ?? null,
+ v?.score ?? null,
+ now,
+ ),
+ )
+ }
+ tracked[key] = { level: v?.level ?? null, score: v?.score ?? null }
+ }
+
+ // ── New inbound evaluations of the user ──────────────────
+ if (!seedOnly) {
+ for (const connection of inbound) {
+ for (const e of connection.auraEvaluations ?? []) {
+ const modified = e.modified || connection.timestamp
+ if (modified <= (lastFetch ?? 0)) continue
+ alerts.push({
+ id: alertId("evaluation", connection.id, e.category, modified),
+ kind: "evaluation",
+ category: e.category,
+ about: connection.id,
+ to: subjectId,
+ previous: null,
+ next:
+ (e.evaluation === EvaluationValue.POSITIVE ? 1 : -1) * e.confidence,
+ timestamp: modified,
+ viewed: false,
+ })
+ }
+ }
+ }
+
+ // ── Subjects the user evaluated ──────────────────────────
+ for (const subject of outbound) {
+ const categories = new Set(
+ (subject.auraEvaluations ?? []).map((e) => e.category),
+ )
+ for (const category of categories) {
+ const v = getAuraVerification(subject.verifications, category)
+ const key = trackedKey(subject.id, category)
+ const prev = tracked[key]
+
+ const evaluators: Record = {}
+ for (const impact of v?.impacts ?? []) {
+ evaluators[impact.evaluator] = impact.confidence
+ }
+
+ if (prev && !seedOnly) {
+ alerts.push(
+ ...levelScoreAlerts(
+ subject.id,
+ category,
+ prev,
+ v?.level ?? null,
+ v?.score ?? null,
+ now,
+ ),
+ )
+
+ // Other evaluators newly rating (or re-rating) this subject.
+ for (const impact of v?.impacts ?? []) {
+ if (impact.evaluator === subjectId) continue
+ if (prev.evaluators?.[impact.evaluator] === impact.confidence) continue
+ alerts.push({
+ id: alertId("evaluation", impact.evaluator, category, impact.modified),
+ kind: "evaluation",
+ category,
+ about: impact.evaluator,
+ to: subject.id,
+ previous: prev.evaluators?.[impact.evaluator] ?? null,
+ next: impact.confidence,
+ timestamp: impact.modified,
+ viewed: false,
+ })
+ }
+ }
+
+ tracked[key] = {
+ level: v?.level ?? null,
+ score: v?.score ?? null,
+ evaluators,
+ }
+ }
+ }
+
+ return { alerts, tracked }
+}
diff --git a/packages/domain/src/operations.ts b/packages/domain/src/operations.ts
new file mode 100644
index 0000000..7f2b7ff
--- /dev/null
+++ b/packages/domain/src/operations.ts
@@ -0,0 +1,143 @@
+import stringify from "fast-json-stable-stringify"
+import nacl from "tweetnacl"
+import { hash, strToUint8Array, uInt8ArrayToB64 } from "./crypto"
+import { getJson, postJson } from "./http"
+import type {
+ EvaluateOperation,
+ EvaluationCategory,
+ EvaluationValue,
+ OperationState,
+} from "./types/evaluations"
+
+/** BrightID operation protocol version (constant in the old app). */
+const OP_VERSION = 6
+
+/**
+ * The signed "Evaluate" operation exactly as the node canonicalizes and hashes
+ * it. Key set and value types must match the server's expectation byte-for-byte
+ * — `fast-json-stable-stringify` sorts keys, so the serialized message is
+ * identical regardless of insertion order here.
+ */
+export interface SignedEvaluateOp {
+ name: "Evaluate"
+ evaluator: string
+ evaluated: string
+ evaluation: EvaluationValue
+ confidence: number
+ domain: "BrightID"
+ category: EvaluationCategory
+ timestamp: number
+ v: number
+ sig: string
+}
+
+export interface BuildEvaluateOperationParams {
+ evaluator: string
+ evaluated: string
+ evaluation: EvaluationValue
+ confidence: number
+ category: EvaluationCategory
+ timestamp: number
+ /** Ed25519 secret key bytes (caller decodes the stored b64 key). */
+ secretKey: Uint8Array
+}
+
+/**
+ * Build and sign an "Evaluate" operation. Pure: no I/O. Returns the signed op
+ * plus the canonical `message` it was signed over (also what the node hashes),
+ * so the caller can verify the returned hash without re-serializing.
+ */
+export function buildEvaluateOperation(params: BuildEvaluateOperationParams): {
+ op: SignedEvaluateOp
+ message: string
+} {
+ const unsigned = {
+ name: "Evaluate" as const,
+ evaluator: params.evaluator,
+ evaluated: params.evaluated,
+ evaluation: params.evaluation,
+ confidence: params.confidence,
+ domain: "BrightID" as const,
+ category: params.category,
+ timestamp: params.timestamp,
+ v: OP_VERSION,
+ }
+
+ const message = stringify(unsigned)
+ const sig = uInt8ArrayToB64(
+ nacl.sign.detached(strToUint8Array(message), params.secretKey),
+ )
+
+ return { op: { ...unsigned, sig }, message }
+}
+
+/**
+ * Build, sign, and submit an "Evaluate" operation to the node, returning an
+ * `EvaluateOperation` the operations store can track. The node independently
+ * canonicalizes the op and returns its hash; we verify it equals `hash(message)`
+ * (SHA256 -> url-safe b64) and throw on mismatch so a rejected op never lands in
+ * the store as if accepted.
+ */
+export async function submitEvaluateOperation(
+ nodeUrl: string,
+ params: BuildEvaluateOperationParams,
+ signal?: AbortSignal,
+): Promise {
+ const { op, message } = buildEvaluateOperation(params)
+
+ const res = await postJson<{ data: { hash: string } }>(
+ `${nodeUrl}/operations`,
+ op,
+ signal,
+ )
+
+ const returnedHash = res.data?.hash
+ if (returnedHash !== hash(message)) {
+ throw new Error("Invalid operation hash returned from server")
+ }
+
+ return {
+ hash: returnedHash,
+ evaluator: op.evaluator,
+ evaluated: op.evaluated,
+ category: op.category,
+ confidence: op.confidence,
+ evaluation: op.evaluation,
+ timestamp: op.timestamp,
+ postTimestamp: Date.now(),
+ state: "INIT",
+ }
+}
+
+/**
+ * The node reports operation states in lowercase; the domain `OperationState`
+ * is uppercase. Map known values, defaulting unexpected strings to `"UNKNOWN"`.
+ */
+const NODE_STATE_TO_OPERATION_STATE: Record = {
+ unknown: "UNKNOWN",
+ init: "INIT",
+ sent: "SENT",
+ applied: "APPLIED",
+ failed: "FAILED",
+ expired: "EXPIRED",
+}
+
+/**
+ * Fetch the current state of a submitted operation from the node. GETs
+ * `${nodeUrl}/operations/${hash}` (response shape `{ data: { state, result } }`,
+ * matching the old `getOperationState`) and normalizes the lowercase node state
+ * to the uppercase domain `OperationState`. Pure aside from the GET; unknown or
+ * missing states resolve to `"UNKNOWN"`. Throws only on transport errors.
+ */
+export async function fetchOperationState(
+ nodeUrl: string,
+ hash: string,
+ signal?: AbortSignal,
+): Promise {
+ const res = await getJson<{ data?: { state?: string } }>(
+ `${nodeUrl}/operations/${hash}`,
+ signal,
+ )
+ const nodeState = res.data?.state ?? ""
+ return NODE_STATE_TO_OPERATION_STATE[nodeState] ?? "UNKNOWN"
+}
diff --git a/packages/domain/src/passkeys.ts b/packages/domain/src/passkeys.ts
new file mode 100644
index 0000000..746f06e
--- /dev/null
+++ b/packages/domain/src/passkeys.ts
@@ -0,0 +1,176 @@
+import nacl from "tweetnacl"
+import {
+ b64ToUint8Array,
+ b64ToUrlSafeB64,
+ strToUint8Array,
+ uInt8ArrayToB64,
+} from "./crypto"
+
+/**
+ * Passkey-derived identity. The Ed25519 keypair is derived deterministically
+ * from the passkey's PRF output, so the same passkey always yields the same
+ * identity — no server-side account storage is needed.
+ */
+export interface PasskeyIdentity {
+ /** Url-safe b64 of the public key — used as the user's id on the node. */
+ id: string
+ /** Standard b64 (same shape as `generateB64Keypair`). */
+ publicKey: string
+ privateKey: string
+}
+
+interface PRFExtensionResult {
+ prf?: { results?: { first?: ArrayBuffer } }
+}
+
+const LS_CRED_ID = "aura_passkey_cred_id"
+const LS_PUB_KEY = "aura_passkey_pub_key"
+
+// Derivation constants — must never change, or existing passkeys would map to
+// different identities. Shared with the interface app's passkey login.
+const PRF_SALT = "BrightID"
+const HKDF_INFO = "BrightID Ed25519 Identity v1"
+
+const PRF_UNSUPPORTED_MESSAGE =
+ "This device's passkeys don't support key derivation (PRF). " +
+ "Try Chrome on desktop, or Safari 17.5+ on iOS."
+
+async function hkdf(inputKeyMaterial: Uint8Array): Promise {
+ const baseKey = await crypto.subtle.importKey(
+ "raw",
+ inputKeyMaterial as BufferSource,
+ { name: "HKDF" },
+ false,
+ ["deriveBits"],
+ )
+ const derived = await crypto.subtle.deriveBits(
+ {
+ name: "HKDF",
+ hash: "SHA-256",
+ salt: strToUint8Array(PRF_SALT) as BufferSource,
+ info: strToUint8Array(HKDF_INFO) as BufferSource,
+ },
+ baseKey,
+ 32 * 8,
+ )
+ return new Uint8Array(derived)
+}
+
+function identityFromSeed(seed: Uint8Array): PasskeyIdentity {
+ const keypair = nacl.sign.keyPair.fromSeed(seed)
+ // nacl copies the seed; zero our copy so it doesn't linger in memory
+ seed.fill(0)
+ const publicKey = uInt8ArrayToB64(keypair.publicKey)
+ return {
+ id: b64ToUrlSafeB64(publicKey),
+ publicKey,
+ privateKey: uInt8ArrayToB64(keypair.secretKey),
+ }
+}
+
+function rememberCredential(rawId: ArrayBuffer, publicKey: string): void {
+ localStorage.setItem(LS_CRED_ID, uInt8ArrayToB64(new Uint8Array(rawId)))
+ localStorage.setItem(LS_PUB_KEY, publicKey)
+}
+
+/** Whether a passkey was already registered from this device/browser. */
+export function hasPasskeyCredential(): boolean {
+ return !!localStorage.getItem(LS_CRED_ID)
+}
+
+/** Forget the locally remembered credential (does not delete the passkey). */
+export function clearPasskeyCredential(): void {
+ localStorage.removeItem(LS_CRED_ID)
+ localStorage.removeItem(LS_PUB_KEY)
+}
+
+/**
+ * Create a brand-new passkey and derive an identity from it.
+ *
+ * Some authenticators return the PRF output during creation itself (one tap);
+ * others only evaluate PRF during authentication, so we fall back to an
+ * immediate assertion against the freshly created credential.
+ */
+export async function createPasskeyIdentity(
+ username: string,
+): Promise {
+ const credential = (await navigator.credentials.create({
+ publicKey: {
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
+ rp: { name: "Aura" },
+ user: {
+ id: crypto.getRandomValues(new Uint8Array(16)),
+ name: username,
+ displayName: username,
+ },
+ pubKeyCredParams: [
+ { type: "public-key", alg: -7 }, // ES256
+ { type: "public-key", alg: -257 }, // RS256
+ ],
+ authenticatorSelection: {
+ userVerification: "required",
+ residentKey: "required",
+ },
+ extensions: {
+ prf: { eval: { first: strToUint8Array(PRF_SALT) } },
+ } as AuthenticationExtensionsClientInputs,
+ },
+ })) as PublicKeyCredential | null
+
+ if (!credential) throw new Error("Passkey creation was cancelled")
+
+ // Persist the credential id so the fallback assertion targets it
+ localStorage.setItem(
+ LS_CRED_ID,
+ uInt8ArrayToB64(new Uint8Array(credential.rawId)),
+ )
+
+ const extensions = credential.getClientExtensionResults() as PRFExtensionResult
+ const prfFromCreate = extensions.prf?.results?.first
+ if (!prfFromCreate) return getPasskeyIdentity()
+
+ const identity = identityFromSeed(await hkdf(new Uint8Array(prfFromCreate)))
+ rememberCredential(credential.rawId, identity.publicKey)
+ return identity
+}
+
+/**
+ * Re-derive the identity from an existing passkey (one tap). Works without a
+ * locally remembered credential too — the browser then offers any resident
+ * passkey for this origin, which lets a user sign in on a new browser profile.
+ */
+export async function getPasskeyIdentity(): Promise {
+ const savedId = localStorage.getItem(LS_CRED_ID)
+ const savedPubKey = localStorage.getItem(LS_PUB_KEY)
+
+ const assertion = (await navigator.credentials.get({
+ publicKey: {
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
+ allowCredentials: savedId
+ ? [{ id: b64ToUint8Array(savedId) as BufferSource, type: "public-key" }]
+ : [],
+ userVerification: "required",
+ extensions: {
+ prf: { eval: { first: strToUint8Array(PRF_SALT) } },
+ } as AuthenticationExtensionsClientInputs,
+ },
+ })) as PublicKeyCredential | null
+
+ if (!assertion) throw new Error("Passkey prompt was cancelled")
+
+ const extensions = assertion.getClientExtensionResults() as PRFExtensionResult
+ const prfBytes = extensions.prf?.results?.first
+ if (!prfBytes) throw new Error(PRF_UNSUPPORTED_MESSAGE)
+
+ const identity = identityFromSeed(await hkdf(new Uint8Array(prfBytes)))
+
+ if (savedPubKey && savedPubKey !== identity.publicKey) {
+ throw new Error(
+ "Wrong passkey selected — this would produce a different identity. " +
+ "Please use the passkey you originally registered with.",
+ )
+ }
+
+ rememberCredential(assertion.rawId, identity.publicKey)
+ return identity
+}
diff --git a/packages/domain/src/recovery.ts b/packages/domain/src/recovery.ts
new file mode 100644
index 0000000..5d8f7cd
--- /dev/null
+++ b/packages/domain/src/recovery.ts
@@ -0,0 +1,94 @@
+import {
+ downloadFromChannel,
+ listChannel,
+ uploadToChannel,
+} from "./channel"
+import { b64ToUrlSafeB64, decryptData, hash } from "./crypto"
+
+export const RECOVERY_CHANNEL_TTL = 24 * 60 * 60 * 1000 // 1 day
+export const CHANNEL_POLL_INTERVAL = 3000
+
+const IMPORT_PREFIX = "sig_"
+const QR_TYPE_ADD_SUPER_USER_APP = "5"
+
+export interface RecoveredUser {
+ id: string
+ name?: string
+ password: string
+}
+
+/** Build the channel url embedded in the QR code. */
+export function buildRecoveryChannelQrUrl({
+ aesKey,
+ href,
+ name,
+}: {
+ aesKey: string
+ href: string
+ name?: string
+}): string {
+ const url = new URL(href)
+ url.searchParams.append("aes", aesKey)
+ url.searchParams.append("t", QR_TYPE_ADD_SUPER_USER_APP)
+ if (name) url.searchParams.append("n", name)
+ url.searchParams.append("p", "false")
+ return url.href
+}
+
+/** Upload our recovery signing key to the freshly created channel. */
+export async function uploadRecoveryData({
+ channelUrl,
+ aesKey,
+ publicKey,
+ timestamp,
+}: {
+ channelUrl: string
+ aesKey: string
+ publicKey: string
+ timestamp: number
+}): Promise {
+ const channelId = hash(aesKey)
+ const data = JSON.stringify({ signingKey: publicKey, timestamp })
+ await uploadToChannel({
+ channelUrl,
+ channelId,
+ data,
+ dataId: "data",
+ requestedTtl: RECOVERY_CHANNEL_TTL,
+ })
+}
+
+/**
+ * Poll the channel once: look for the encrypted user-info the scanner uploaded
+ * and, if present, decrypt and return it. Returns null until the phone scans.
+ */
+export async function pollRecoveredUser({
+ channelUrl,
+ channelId,
+ aesKey,
+ signingKey,
+}: {
+ channelUrl: string
+ channelId: string
+ aesKey: string
+ signingKey: string
+}): Promise {
+ const dataIds = await listChannel({ channelUrl, channelId })
+
+ const prefix = `${IMPORT_PREFIX}userinfo_`
+ const self = b64ToUrlSafeB64(signingKey)
+ const dataId = dataIds.find(
+ (id) =>
+ id.startsWith(prefix) && id.replace(prefix, "").split(":")[1] !== self,
+ )
+ if (!dataId) return null
+
+ const encrypted = await downloadFromChannel({
+ channelUrl,
+ channelId,
+ dataId,
+ deleteAfterDownload: true,
+ })
+ const info = JSON.parse(decryptData(encrypted, aesKey)) as RecoveredUser
+ return info
+}
diff --git a/packages/domain/src/score.ts b/packages/domain/src/score.ts
new file mode 100644
index 0000000..b1d6dd4
--- /dev/null
+++ b/packages/domain/src/score.ts
@@ -0,0 +1,75 @@
+import { userLevelPoints } from "./levels"
+import type { EvaluationCategory } from "./types/evaluations"
+import type { AuraImpactRaw } from "./types/aura"
+
+export const calculateImpact = (score: number, rating: number) =>
+ rating > 0 ? score * rating : rating * score * 4
+
+export const calculateImpactPercent = (
+ impacts: AuraImpactRaw[],
+ score: number,
+) => {
+ const sumImpacts = impacts.reduce((p, c) => Math.abs(c.impact) + p, 0)
+ return sumImpacts ? score / sumImpacts : 0
+}
+
+export const calculateRemainingScoreToNextLevel = (
+ view: EvaluationCategory,
+ score: number,
+) => {
+ const levels = userLevelPoints[view]
+ const nextLevelStart = levels.find((item) => item > score) ?? levels.at(-1)!
+ return nextLevelStart - score
+}
+
+export const maximumScoreTobeReached = 4_000_000_000
+
+export const progressSections = [
+ 0.00000875, 0.000125, 0.025, 0.075, 0.2, 0.375, 0.625, 1,
+]
+
+/** 0–100 progress of a score across the non-linear level sections. */
+export const calculateUserScorePercentage = (
+ _view: EvaluationCategory,
+ score: number,
+) => {
+ if (score < 0) return -1
+ if (score === 0) return 0
+ if (score > maximumScoreTobeReached) return 100
+
+ const sectionsPassed = progressSections.filter(
+ (pct) => pct * maximumScoreTobeReached < score,
+ )
+ const currentSection =
+ (progressSections[sectionsPassed.length] ?? 1) * maximumScoreTobeReached
+
+ return (
+ (sectionsPassed.length / progressSections.length) * 100 +
+ (score / currentSection) * progressSections.length
+ )
+}
+
+/** Level index for a category given its impact ratings. */
+export const calculateSubjectScore = (
+ category: EvaluationCategory,
+ ratings: AuraImpactRaw[],
+) => {
+ const levels = userLevelPoints[category]
+ const score = ratings.reduce((p, item) => (item.score ?? 0) + p, 0)
+ return levels.findIndex((item) => item > score)
+}
+
+/**
+ * One evaluator's share of a subject's total absolute impact, as a rounded
+ * percentage — null when the evaluator has no impact or there is none at all.
+ */
+export const impactShare = (
+ impacts: AuraImpactRaw[] | null | undefined,
+ evaluator: string | undefined,
+): number | null => {
+ if (!impacts?.length || !evaluator) return null
+ const total = impacts.reduce((sum, i) => sum + Math.abs(i.impact), 0)
+ const mine = impacts.find((i) => i.evaluator === evaluator)?.impact
+ if (!mine || !total) return null
+ return Math.round((Math.abs(mine) / total) * 100)
+}
diff --git a/packages/domain/src/types/aura.ts b/packages/domain/src/types/aura.ts
new file mode 100644
index 0000000..37d91fa
--- /dev/null
+++ b/packages/domain/src/types/aura.ts
@@ -0,0 +1,116 @@
+import type { EvaluationCategory, EvaluationValue } from "./evaluations"
+
+/** BrightID verification / domain types (aura node `?withVerifications=true`). */
+export interface AuraImpactRaw {
+ evaluator: string
+ level?: number | null
+ score: number | null
+ confidence: number
+ impact: number
+ modified: number
+}
+
+export type Domain = {
+ name: "BrightID"
+ categories: {
+ name: EvaluationCategory
+ score: number
+ level: number
+ impacts: AuraImpactRaw[]
+ }[]
+}
+
+export type Verifications = {
+ name: string
+ block: number
+ timestamp: number
+ domains?: Domain[]
+}[]
+
+export interface BrightIdProfile {
+ createdAt: number
+ verifications: Verifications
+}
+
+/** Connections + evaluations from the aura node. */
+export type ConnectionLevel =
+ | "reported"
+ | "suspicious"
+ | "just met"
+ | "already known"
+ | "recovery"
+ | "aura only"
+
+export type BrightIdConnection = {
+ id: string
+ level: ConnectionLevel
+ reportReason: string | null
+ timestamp: number
+}
+
+export type AuraEvaluation = {
+ evaluation: EvaluationValue
+ confidence: number
+ domain: "BrightID"
+ category: EvaluationCategory
+ modified: number
+ timestamp: number
+}
+
+export type AuraNodeBrightIdConnection = BrightIdConnection & {
+ auraEvaluations?: AuraEvaluation[]
+ verifications: Verifications
+}
+
+export type AuraNodeConnectionsResponse = {
+ data: {
+ connections: AuraNodeBrightIdConnection[]
+ }
+}
+
+/** A rating derived from a connection's aura evaluations (or a pending op). */
+export type AuraRating = {
+ id?: number
+ toBrightId: string
+ fromBrightId: string
+ rating: string
+ timestamp: number
+ createdAt: string
+ updatedAt: string
+ category: EvaluationCategory
+ isPending: boolean
+ verifications?: Verifications
+}
+
+/** Connections as stored in the (decrypted) BrightID backup. */
+export type BrightIdBackupConnection = BrightIdConnection &
+ Partial<{
+ name: string
+ connectionDate: number
+ photo: { filename: string }
+ status: "verified"
+ notificationToken: string
+ socialMedia: unknown[]
+ verifications: Verifications
+ incomingLevel: ConnectionLevel
+ }>
+
+export type AuraNodeBrightIdConnectionWithBackupData =
+ AuraNodeBrightIdConnection & BrightIdBackupConnection
+
+export type BrightIdBackup = {
+ userData: {
+ id: string
+ name: string
+ photo: { filename: string }
+ }
+ connections: BrightIdBackupConnection[]
+ groups: unknown[]
+}
+
+export type BrightIdBackupWithAuraConnectionData = Omit<
+ BrightIdBackup,
+ "connections"
+> & {
+ connections: AuraNodeBrightIdConnectionWithBackupData[]
+}
diff --git a/packages/domain/src/types/dashboard.ts b/packages/domain/src/types/dashboard.ts
new file mode 100644
index 0000000..d3c120b
--- /dev/null
+++ b/packages/domain/src/types/dashboard.ts
@@ -0,0 +1,10 @@
+/** Which role lens the user is viewing the app through. */
+export enum PreferredView {
+ PLAYER,
+ TRAINER,
+ MANAGER_EVALUATING_TRAINER,
+ MANAGER_EVALUATING_MANAGER,
+}
+
+
+
diff --git a/packages/domain/src/types/evaluations.ts b/packages/domain/src/types/evaluations.ts
new file mode 100644
index 0000000..43c6ef7
--- /dev/null
+++ b/packages/domain/src/types/evaluations.ts
@@ -0,0 +1,31 @@
+export enum EvaluationCategory {
+ SUBJECT = "subject",
+ PLAYER = "player",
+ TRAINER = "trainer",
+ MANAGER = "manager",
+}
+
+export enum EvaluationValue {
+ POSITIVE = "positive",
+ NEGATIVE = "negative",
+}
+
+export type OperationState =
+ | "UNKNOWN"
+ | "INIT"
+ | "SENT"
+ | "APPLIED"
+ | "FAILED"
+ | "EXPIRED"
+
+export interface EvaluateOperation {
+ hash: string
+ evaluator: string
+ evaluated: string
+ category: EvaluationCategory
+ confidence: number
+ evaluation: EvaluationValue
+ timestamp: number
+ postTimestamp?: number
+ state: OperationState
+}
diff --git a/packages/domain/src/verifications.ts b/packages/domain/src/verifications.ts
new file mode 100644
index 0000000..bde48a3
--- /dev/null
+++ b/packages/domain/src/verifications.ts
@@ -0,0 +1,34 @@
+import type { EvaluationCategory } from "./types/evaluations"
+import type { Verifications } from "./types/aura"
+
+export const getUserHasRecovery = (verifications?: Verifications) =>
+ verifications
+ ? !!verifications.find((v) => v.name === "SocialRecoverySetup")
+ : null
+
+/** The Aura → BrightID domain category entry for a given evaluation category. */
+export const getAuraVerification = (
+ verifications: Verifications | undefined,
+ category: EvaluationCategory,
+) => {
+ if (!verifications) return null
+ return verifications
+ .find((v) => v.name === "Aura")
+ ?.domains?.find((d) => d.name === "BrightID")
+ ?.categories.find((c) => c.name === category)
+}
+
+/** Extract level/score for a subject in a category from their verifications. */
+export function parseVerifications(
+ verifications: Verifications | undefined,
+ category: EvaluationCategory,
+) {
+ const auraVerification = getAuraVerification(verifications, category)
+ return {
+ userHasRecovery: getUserHasRecovery(verifications),
+ auraVerification,
+ auraLevel: auraVerification?.level ?? null,
+ auraScore: auraVerification?.score ?? null,
+ auraImpacts: auraVerification?.impacts ?? null,
+ }
+}
diff --git a/packages/domain/src/view-mode.ts b/packages/domain/src/view-mode.ts
new file mode 100644
index 0000000..efa617b
--- /dev/null
+++ b/packages/domain/src/view-mode.ts
@@ -0,0 +1,57 @@
+import { EvaluationCategory } from "./types/evaluations"
+import { PreferredView } from "./types/dashboard"
+
+/** The role we *evaluate* when in a given view. */
+export const viewModeToViewAs: Record = {
+ [PreferredView.PLAYER]: EvaluationCategory.SUBJECT,
+ [PreferredView.TRAINER]: EvaluationCategory.PLAYER,
+ [PreferredView.MANAGER_EVALUATING_TRAINER]: EvaluationCategory.TRAINER,
+ [PreferredView.MANAGER_EVALUATING_MANAGER]: EvaluationCategory.MANAGER,
+}
+
+/** The view that evaluates the current view's role (one level up). */
+export const viewModeToEvaluatorViewMode: Record =
+ {
+ [PreferredView.PLAYER]: PreferredView.TRAINER,
+ [PreferredView.TRAINER]: PreferredView.MANAGER_EVALUATING_TRAINER,
+ [PreferredView.MANAGER_EVALUATING_TRAINER]:
+ PreferredView.MANAGER_EVALUATING_MANAGER,
+ [PreferredView.MANAGER_EVALUATING_MANAGER]:
+ PreferredView.MANAGER_EVALUATING_MANAGER,
+ }
+
+/** `?viewas=` query value → view. */
+export const viewAsToViewMode: Record = {
+ [EvaluationCategory.SUBJECT]: PreferredView.PLAYER,
+ [EvaluationCategory.PLAYER]: PreferredView.TRAINER,
+ [EvaluationCategory.TRAINER]: PreferredView.MANAGER_EVALUATING_TRAINER,
+ [EvaluationCategory.MANAGER]: PreferredView.MANAGER_EVALUATING_MANAGER,
+}
+
+export const viewModeToString: Record = {
+ [PreferredView.PLAYER]: "Player",
+ [PreferredView.TRAINER]: "Trainer",
+ [PreferredView.MANAGER_EVALUATING_TRAINER]: "Manager",
+ [PreferredView.MANAGER_EVALUATING_MANAGER]: "Manager",
+}
+
+/** URL slug (`/home/:view`) ↔ view. Manager collapses to one slug. */
+export const viewSlugToViewMode: Record = {
+ player: PreferredView.PLAYER,
+ trainer: PreferredView.TRAINER,
+ manager: PreferredView.MANAGER_EVALUATING_TRAINER,
+}
+
+export const viewModeToSlug: Record = {
+ [PreferredView.PLAYER]: "player",
+ [PreferredView.TRAINER]: "trainer",
+ [PreferredView.MANAGER_EVALUATING_TRAINER]: "manager",
+ [PreferredView.MANAGER_EVALUATING_MANAGER]: "manager",
+}
+
+export const viewModeSubjectString: Record = {
+ [PreferredView.PLAYER]: "Subject",
+ [PreferredView.TRAINER]: "Player",
+ [PreferredView.MANAGER_EVALUATING_TRAINER]: "Trainer",
+ [PreferredView.MANAGER_EVALUATING_MANAGER]: "Manager",
+}
diff --git a/packages/domain/tsconfig.json b/packages/domain/tsconfig.json
new file mode 100644
index 0000000..7dfa1c4
--- /dev/null
+++ b/packages/domain/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "../typescript-config/base.json",
+ "compilerOptions": {
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "target": "ES2022",
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "include": ["src"]
+}
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index 377b8c6..e9349db 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -55,6 +55,7 @@
}
},
"dependencies": {
+ "@aura/domain": "workspace:*",
"@lit-app/state": "^1.0.0",
"@lit-labs/router": "^0.1.4",
"@lit-labs/signals": "^0.1.2",
@@ -62,7 +63,6 @@
"@zerodev/ecdsa-validator": "^5.4.9",
"@zerodev/wagmi": "^4.6.7",
"axios": "^1.13.5",
- "base64-js": "^1.5.1",
"lit": "^3.3.0",
"tweetnacl": "^1.0.3",
"viem": "^2.45.3",
diff --git a/packages/sdk/src/apis/dashboard.ts b/packages/sdk/src/apis/dashboard.ts
new file mode 100644
index 0000000..94c3b5b
--- /dev/null
+++ b/packages/sdk/src/apis/dashboard.ts
@@ -0,0 +1,62 @@
+import axios from "axios"
+
+// Dashboard API base URL - using environment variable for flexibility
+const DASHBOARD_API_BASE = import.meta.env.VITE_SOME_AURA_DASHBOARD_API_URL || 'https://dashboard.aura.brightid.org'
+
+const $http = axios.create({
+ baseURL: DASHBOARD_API_BASE,
+ headers: {
+ "Cache-Control": "no-cache",
+ "Content-Type": "application/json"
+ },
+ timeout: 60 * 1000,
+})
+
+export const dashboardApi = {
+ // Project endpoints
+ getProjectsList: async () => {
+ return $http.get('/api/projects/list')
+ },
+
+ updateProject: async (projectId: string, projectData: any) => {
+ return $http.post(`/api/projects/update-project`, {
+ projectId,
+ ...projectData
+ })
+ },
+
+ createProject: async (projectData: any) => {
+ return $http.post('/api/projects/create-project', projectData)
+ },
+
+ upgradeProject: async (projectId: string, planId: string) => {
+ return $http.post(`/api/projects/upgrade-project`, {
+ projectId,
+ planId
+ })
+ },
+
+ updateProjectBrightid: async (projectId: string, brightId: string) => {
+ return $http.post(`/api/projects/update-project-brightid`, {
+ projectId,
+ brightId
+ })
+ },
+
+ // Payment endpoints
+ getPaymentsHistory: async () => {
+ return $http.get('/api/payments/history')
+ },
+
+ getPaymentStatus: async (orderId: string) => {
+ return $http.get(`/api/payments/status/${orderId}`)
+ },
+
+ createInvoice: async (invoiceData: any) => {
+ return $http.post('/api/payments/create-invoice', invoiceData)
+ },
+
+ createPayment: async (paymentData: any) => {
+ return $http.post('/api/payments/create-payment', paymentData)
+ }
+}
\ No newline at end of file
diff --git a/packages/sdk/src/apis/index.ts b/packages/sdk/src/apis/index.ts
new file mode 100644
index 0000000..8f2bb30
--- /dev/null
+++ b/packages/sdk/src/apis/index.ts
@@ -0,0 +1,5 @@
+export * from "./brightid"
+export * from "./dashboard"
+export * from "./get-verified"
+export * from "./node"
+export * from "./recovery"
diff --git a/packages/sdk/src/utils/crypto.ts b/packages/sdk/src/utils/crypto.ts
index aed036e..9806fe9 100644
--- a/packages/sdk/src/utils/crypto.ts
+++ b/packages/sdk/src/utils/crypto.ts
@@ -1,55 +1,24 @@
-import { fromByteArray } from "base64-js"
-import CryptoJS from "crypto-js"
-import nacl from "tweetnacl"
import { type BrightIdBackup } from "types"
-
-export function encryptData(data: string, password: string) {
- return CryptoJS.AES.encrypt(data, password).toString()
-}
+import { decryptData, encryptData } from "@aura/domain/crypto"
+
+// Shared crypto primitives live in @aura/domain — re-exported here to keep the
+// sdk's public API unchanged.
+export {
+ b64ToUrlSafeB64,
+ decryptData,
+ encryptData,
+ generateB64Keypair,
+ hash,
+ randomWordArray,
+ uInt8ArrayToB64,
+ urlSafeRandomKey,
+ wordArrayToB64,
+} from "@aura/domain/crypto"
export function encryptUserData(userData: BrightIdBackup, password: string) {
return encryptData(JSON.stringify(userData), password)
}
-export function decryptData(data: string, password: string) {
- return CryptoJS.AES.decrypt(data, password).toString(CryptoJS.enc.Utf8)
-}
-
export function decryptUserData(encryptedUserData: string, password: string) {
return JSON.parse(decryptData(encryptedUserData, password))
}
-
-export const b64ToUrlSafeB64 = (s: string) => {
- const alts: {
- [key: string]: string
- } = {
- "/": "_",
- "+": "-",
- "=": "",
- }
- return s.replace(/[/+=]/g, (c) => alts[c]!)
-}
-
-export const hash = (data: string) => {
- const h = CryptoJS.SHA256(data)
- const b = h.toString(CryptoJS.enc.Base64)
- return b64ToUrlSafeB64(b)
-}
-
-export const randomWordArray = (size: number) =>
- CryptoJS.lib.WordArray.random(size)
-
-export const wordArrayToB64 = (WordArray: CryptoJS.lib.WordArray) =>
- CryptoJS.enc.Base64.stringify(WordArray)
-
-export const generateB64Keypair = () => {
- const { publicKey, secretKey } = nacl.sign.keyPair()
- const b64PublicKey = fromByteArray(publicKey)
-
- const b64SecretKey = fromByteArray(secretKey)
-
- return {
- privateKey: b64SecretKey,
- publicKey: b64PublicKey,
- }
-}
diff --git a/packages/ui/custom-elements.json b/packages/ui/custom-elements.json
index 36f759c..26863d0 100644
--- a/packages/ui/custom-elements.json
+++ b/packages/ui/custom-elements.json
@@ -193,6 +193,17 @@
"attribute": "disabled",
"reflects": true
},
+ {
+ "kind": "field",
+ "name": "selected",
+ "type": {
+ "text": "boolean"
+ },
+ "description": "Toggle/pill state: a selected button renders filled in its palette color\nregardless of variant, so call sites don't juggle variant/color pairs.",
+ "default": "false",
+ "attribute": "selected",
+ "reflects": true
+ },
{
"kind": "field",
"name": "class",
@@ -243,6 +254,15 @@
"default": "false",
"fieldName": "disabled"
},
+ {
+ "name": "selected",
+ "type": {
+ "text": "boolean"
+ },
+ "description": "Toggle/pill state: a selected button renders filled in its palette color\nregardless of variant, so call sites don't juggle variant/color pairs.",
+ "default": "false",
+ "fieldName": "selected"
+ },
{
"name": "class",
"type": {
@@ -293,9 +313,21 @@
"type": {
"text": "\"default\" | \"glass\""
},
- "default": "\"default\"",
+ "description": "Glass is the design default; use `variant=\"default\"` for a solid card.",
+ "default": "\"glass\"",
"attribute": "variant",
"reflects": true
+ },
+ {
+ "kind": "field",
+ "name": "interactive",
+ "type": {
+ "text": "boolean"
+ },
+ "description": "Clickable card: pointer cursor, hover lift, press feedback.",
+ "default": "false",
+ "attribute": "interactive",
+ "reflects": true
}
],
"attributes": [
@@ -304,8 +336,18 @@
"type": {
"text": "\"default\" | \"glass\""
},
- "default": "\"default\"",
+ "description": "Glass is the design default; use `variant=\"default\"` for a solid card.",
+ "default": "\"glass\"",
"fieldName": "variant"
+ },
+ {
+ "name": "interactive",
+ "type": {
+ "text": "boolean"
+ },
+ "description": "Clickable card: pointer cursor, hover lift, press feedback.",
+ "default": "false",
+ "fieldName": "interactive"
}
],
"superclass": {
@@ -470,6 +512,228 @@
}
]
},
+ {
+ "kind": "javascript-module",
+ "path": "src/components/color-picker.ts",
+ "declarations": [
+ {
+ "kind": "class",
+ "description": "",
+ "name": "ColorPickerElement",
+ "members": [
+ {
+ "kind": "field",
+ "name": "value",
+ "type": {
+ "text": "string"
+ },
+ "default": "\"#000000\"",
+ "attribute": "value",
+ "reflects": true
+ },
+ {
+ "kind": "field",
+ "name": "label",
+ "type": {
+ "text": "string | undefined"
+ },
+ "default": "undefined",
+ "attribute": "label"
+ },
+ {
+ "kind": "field",
+ "name": "disabled",
+ "type": {
+ "text": "boolean"
+ },
+ "default": "false",
+ "attribute": "disabled"
+ },
+ {
+ "kind": "field",
+ "name": "_h",
+ "type": {
+ "text": "number"
+ },
+ "privacy": "private",
+ "default": "0"
+ },
+ {
+ "kind": "field",
+ "name": "_s",
+ "type": {
+ "text": "number"
+ },
+ "privacy": "private",
+ "default": "0"
+ },
+ {
+ "kind": "field",
+ "name": "_v",
+ "type": {
+ "text": "number"
+ },
+ "privacy": "private",
+ "default": "0"
+ },
+ {
+ "kind": "field",
+ "name": "_text",
+ "type": {
+ "text": "string"
+ },
+ "privacy": "private"
+ },
+ {
+ "kind": "field",
+ "name": "_suppressSync",
+ "type": {
+ "text": "boolean"
+ },
+ "privacy": "private",
+ "default": "false"
+ },
+ {
+ "kind": "method",
+ "name": "_syncFromValue",
+ "privacy": "private",
+ "parameters": [
+ {
+ "name": "value",
+ "type": {
+ "text": "string"
+ }
+ }
+ ]
+ },
+ {
+ "kind": "field",
+ "name": "_onSvPointerDown",
+ "privacy": "private"
+ },
+ {
+ "kind": "method",
+ "name": "_updateSv",
+ "privacy": "private",
+ "parameters": [
+ {
+ "name": "e",
+ "type": {
+ "text": "PointerEvent"
+ }
+ },
+ {
+ "name": "el",
+ "type": {
+ "text": "HTMLElement"
+ }
+ }
+ ]
+ },
+ {
+ "kind": "field",
+ "name": "_onHuePointerDown",
+ "privacy": "private"
+ },
+ {
+ "kind": "method",
+ "name": "_updateHue",
+ "privacy": "private",
+ "parameters": [
+ {
+ "name": "e",
+ "type": {
+ "text": "PointerEvent"
+ }
+ },
+ {
+ "name": "el",
+ "type": {
+ "text": "HTMLElement"
+ }
+ }
+ ]
+ },
+ {
+ "kind": "method",
+ "name": "_emit",
+ "privacy": "private"
+ },
+ {
+ "kind": "method",
+ "name": "_onTextChange",
+ "privacy": "private",
+ "parameters": [
+ {
+ "name": "e",
+ "type": {
+ "text": "Event"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "change",
+ "type": {
+ "text": "CustomEvent"
+ }
+ }
+ ],
+ "attributes": [
+ {
+ "name": "value",
+ "type": {
+ "text": "string"
+ },
+ "default": "\"#000000\"",
+ "fieldName": "value"
+ },
+ {
+ "name": "label",
+ "type": {
+ "text": "string | undefined"
+ },
+ "default": "undefined",
+ "fieldName": "label"
+ },
+ {
+ "name": "disabled",
+ "type": {
+ "text": "boolean"
+ },
+ "default": "false",
+ "fieldName": "disabled"
+ }
+ ],
+ "superclass": {
+ "name": "LitElement",
+ "package": "lit"
+ },
+ "tagName": "a-color-picker",
+ "customElement": true
+ }
+ ],
+ "exports": [
+ {
+ "kind": "js",
+ "name": "ColorPickerElement",
+ "declaration": {
+ "name": "ColorPickerElement",
+ "module": "src/components/color-picker.ts"
+ }
+ },
+ {
+ "kind": "custom-element-definition",
+ "name": "a-color-picker",
+ "declaration": {
+ "name": "ColorPickerElement",
+ "module": "src/components/color-picker.ts"
+ }
+ }
+ ]
+ },
{
"kind": "javascript-module",
"path": "src/components/container.ts",
@@ -533,6 +797,14 @@
"privacy": "private",
"default": "false"
},
+ {
+ "kind": "field",
+ "name": "_hideTimer",
+ "type": {
+ "text": "ReturnType | undefined"
+ },
+ "privacy": "private"
+ },
{
"kind": "method",
"name": "_onTriggerClick",
@@ -579,6 +851,12 @@
"type": {
"text": "CustomEvent"
}
+ },
+ {
+ "name": "after-hide",
+ "type": {
+ "text": "CustomEvent"
+ }
}
],
"attributes": [
@@ -1349,6 +1627,14 @@
"module": "src/components/collapse"
}
},
+ {
+ "kind": "js",
+ "name": "*",
+ "declaration": {
+ "name": "*",
+ "module": "src/components/color-picker"
+ }
+ },
{
"kind": "js",
"name": "*",
@@ -1429,6 +1715,14 @@
"module": "src/components/scroll-area"
}
},
+ {
+ "kind": "js",
+ "name": "*",
+ "declaration": {
+ "name": "*",
+ "module": "src/components/select"
+ }
+ },
{
"kind": "js",
"name": "*",
@@ -1437,6 +1731,14 @@
"module": "src/components/separator"
}
},
+ {
+ "kind": "js",
+ "name": "*",
+ "declaration": {
+ "name": "*",
+ "module": "src/components/switch"
+ }
+ },
{
"kind": "js",
"name": "*",
@@ -1931,7 +2233,7 @@
"kind": "field",
"name": "_thumbY",
"type": {
- "text": "object"
+ "text": "{ top: number; height: number }"
},
"privacy": "private",
"default": "{ top: 2, height: 0 }"
@@ -1940,7 +2242,7 @@
"kind": "field",
"name": "_thumbX",
"type": {
- "text": "object"
+ "text": "{ left: number; width: number }"
},
"privacy": "private",
"default": "{ left: 2, width: 0 }"
@@ -2133,12 +2435,175 @@
},
{
"kind": "javascript-module",
- "path": "src/components/separator.ts",
+ "path": "src/components/select.ts",
"declarations": [
{
"kind": "class",
"description": "",
- "name": "SeparatorElement",
+ "name": "SelectElement",
+ "members": [
+ {
+ "kind": "field",
+ "name": "label",
+ "type": {
+ "text": "string | undefined"
+ },
+ "default": "undefined",
+ "attribute": "label"
+ },
+ {
+ "kind": "field",
+ "name": "name",
+ "type": {
+ "text": "string"
+ },
+ "default": "\"select\"",
+ "attribute": "name"
+ },
+ {
+ "kind": "field",
+ "name": "value",
+ "type": {
+ "text": "string"
+ },
+ "default": "\"\"",
+ "attribute": "value",
+ "reflects": true
+ },
+ {
+ "kind": "field",
+ "name": "disabled",
+ "type": {
+ "text": "boolean"
+ },
+ "default": "false",
+ "attribute": "disabled"
+ },
+ {
+ "kind": "field",
+ "name": "options",
+ "type": {
+ "text": "SelectOption[]"
+ },
+ "default": "[]",
+ "attribute": "options"
+ },
+ {
+ "kind": "field",
+ "name": "placeholder",
+ "type": {
+ "text": "string"
+ },
+ "default": "\"\"",
+ "attribute": "placeholder"
+ },
+ {
+ "kind": "method",
+ "name": "_onChange",
+ "privacy": "private",
+ "parameters": [
+ {
+ "name": "e",
+ "type": {
+ "text": "Event"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "change",
+ "type": {
+ "text": "CustomEvent"
+ }
+ }
+ ],
+ "attributes": [
+ {
+ "name": "label",
+ "type": {
+ "text": "string | undefined"
+ },
+ "default": "undefined",
+ "fieldName": "label"
+ },
+ {
+ "name": "name",
+ "type": {
+ "text": "string"
+ },
+ "default": "\"select\"",
+ "fieldName": "name"
+ },
+ {
+ "name": "value",
+ "type": {
+ "text": "string"
+ },
+ "default": "\"\"",
+ "fieldName": "value"
+ },
+ {
+ "name": "disabled",
+ "type": {
+ "text": "boolean"
+ },
+ "default": "false",
+ "fieldName": "disabled"
+ },
+ {
+ "name": "options",
+ "type": {
+ "text": "SelectOption[]"
+ },
+ "default": "[]",
+ "fieldName": "options"
+ },
+ {
+ "name": "placeholder",
+ "type": {
+ "text": "string"
+ },
+ "default": "\"\"",
+ "fieldName": "placeholder"
+ }
+ ],
+ "superclass": {
+ "name": "LitElement",
+ "package": "lit"
+ },
+ "tagName": "a-select",
+ "customElement": true
+ }
+ ],
+ "exports": [
+ {
+ "kind": "js",
+ "name": "SelectElement",
+ "declaration": {
+ "name": "SelectElement",
+ "module": "src/components/select.ts"
+ }
+ },
+ {
+ "kind": "custom-element-definition",
+ "name": "a-select",
+ "declaration": {
+ "name": "SelectElement",
+ "module": "src/components/select.ts"
+ }
+ }
+ ]
+ },
+ {
+ "kind": "javascript-module",
+ "path": "src/components/separator.ts",
+ "declarations": [
+ {
+ "kind": "class",
+ "description": "",
+ "name": "SeparatorElement",
"members": [
{
"kind": "field",
@@ -2193,6 +2658,128 @@
}
]
},
+ {
+ "kind": "javascript-module",
+ "path": "src/components/switch.ts",
+ "declarations": [
+ {
+ "kind": "class",
+ "description": "",
+ "name": "SwitchElement",
+ "members": [
+ {
+ "kind": "field",
+ "name": "checked",
+ "type": {
+ "text": "boolean"
+ },
+ "default": "false",
+ "attribute": "checked",
+ "reflects": true
+ },
+ {
+ "kind": "field",
+ "name": "disabled",
+ "type": {
+ "text": "boolean"
+ },
+ "default": "false",
+ "attribute": "disabled",
+ "reflects": true
+ },
+ {
+ "kind": "field",
+ "name": "name",
+ "type": {
+ "text": "string"
+ },
+ "default": "\"switch\"",
+ "attribute": "name"
+ },
+ {
+ "kind": "field",
+ "name": "label",
+ "type": {
+ "text": "string | undefined"
+ },
+ "default": "undefined",
+ "attribute": "label"
+ },
+ {
+ "kind": "method",
+ "name": "_toggle",
+ "privacy": "private"
+ }
+ ],
+ "events": [
+ {
+ "name": "change",
+ "type": {
+ "text": "CustomEvent"
+ }
+ }
+ ],
+ "attributes": [
+ {
+ "name": "checked",
+ "type": {
+ "text": "boolean"
+ },
+ "default": "false",
+ "fieldName": "checked"
+ },
+ {
+ "name": "disabled",
+ "type": {
+ "text": "boolean"
+ },
+ "default": "false",
+ "fieldName": "disabled"
+ },
+ {
+ "name": "name",
+ "type": {
+ "text": "string"
+ },
+ "default": "\"switch\"",
+ "fieldName": "name"
+ },
+ {
+ "name": "label",
+ "type": {
+ "text": "string | undefined"
+ },
+ "default": "undefined",
+ "fieldName": "label"
+ }
+ ],
+ "superclass": {
+ "name": "LitElement",
+ "package": "lit"
+ },
+ "tagName": "a-switch",
+ "customElement": true
+ }
+ ],
+ "exports": [
+ {
+ "kind": "js",
+ "name": "SwitchElement",
+ "declaration": {
+ "name": "SwitchElement",
+ "module": "src/components/switch.ts"
+ }
+ },
+ {
+ "kind": "custom-element-definition",
+ "name": "a-switch",
+ "declaration": {
+ "name": "SwitchElement",
+ "module": "src/components/switch.ts"
+ }
+ }
+ ]
+ },
{
"kind": "javascript-module",
"path": "src/components/tab.ts",
@@ -2220,6 +2807,12 @@
},
"privacy": "private"
},
+ {
+ "kind": "field",
+ "name": "_resizeObserver",
+ "privacy": "private",
+ "default": "new ResizeObserver(() => this._updateIndicator())"
+ },
{
"kind": "method",
"name": "_initialize",
@@ -2461,6 +3054,15 @@
"default": "\"body\"",
"attribute": "variant",
"reflects": true
+ },
+ {
+ "kind": "field",
+ "name": "size",
+ "type": {
+ "text": "\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | undefined"
+ },
+ "attribute": "size",
+ "reflects": true
}
],
"attributes": [
@@ -2471,6 +3073,13 @@
},
"default": "\"body\"",
"fieldName": "variant"
+ },
+ {
+ "name": "size",
+ "type": {
+ "text": "\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | undefined"
+ },
+ "fieldName": "size"
}
],
"superclass": {
diff --git a/packages/ui/package.json b/packages/ui/package.json
index bbe71ff..ded7048 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,7 +1,7 @@
{
"name": "@aura/ui",
"private": true,
- "version": "0.0.1",
+ "version": "0.0.2",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/packages/ui/src/components/badge.ts b/packages/ui/src/components/badge.ts
index 811dce1..edfee6d 100644
--- a/packages/ui/src/components/badge.ts
+++ b/packages/ui/src/components/badge.ts
@@ -4,22 +4,30 @@ import { customElement, property } from "lit/decorators.js"
@customElement("a-badge")
export class BadgeElement extends LitElement {
@property({ reflect: true })
- variant:
+ declare variant:
| "default"
| "secondary"
| "outline"
| "destructive"
| "accent"
- | "glass" = "default"
+ | "glass"
@property({ reflect: true })
- size: "sm" | "md" | "lg" | "xs" = "md"
+ declare size: "sm" | "md" | "lg" | "xs"
@property({ type: Boolean, reflect: true })
- rounded = false
+ declare rounded: boolean
@property({ type: Boolean, reflect: true })
- removable = false
+ declare removable: boolean
+
+ constructor() {
+ super()
+ this.variant = "default"
+ this.size = "md"
+ this.rounded = false
+ this.removable = false
+ }
static styles: CSSResultGroup = css`
:host {
diff --git a/packages/ui/src/components/button.ts b/packages/ui/src/components/button.ts
index e332e31..7306589 100644
--- a/packages/ui/src/components/button.ts
+++ b/packages/ui/src/components/button.ts
@@ -1,7 +1,7 @@
import { css, html, LitElement } from "lit"
import { customElement, property } from "lit/decorators.js"
-export type ButtonVariant = "default" | "secondary" | "ghost" | "outline"
+export type ButtonVariant = "default" | "secondary" | "ghost" | "outline" | "glass"
export type ButtonSize = "sm" | "md" | "lg" | "icon" | "icon-sm" | "icon-lg"
export type ButtonColors =
| "primary"
@@ -13,22 +13,39 @@ export type ButtonColors =
@customElement("a-button")
export class ButtonElement extends LitElement {
@property({ reflect: true })
- variant: ButtonVariant = "default"
+ declare variant: ButtonVariant
@property({ reflect: true })
- size: ButtonSize = "md"
+ declare size: ButtonSize
@property({ reflect: true })
- color: ButtonColors = "primary"
+ declare color: ButtonColors
@property({ reflect: true })
- type: "button" | "submit" | "reset" = "button"
+ declare type: "button" | "submit" | "reset"
@property({ type: Boolean, reflect: true })
- disabled: boolean = false
+ declare disabled: boolean
+
+ /**
+ * Toggle/pill state: a selected button renders filled in its palette color
+ * regardless of variant, so call sites don't juggle variant/color pairs.
+ */
+ @property({ type: Boolean, reflect: true })
+ declare selected: boolean
@property({})
- class: string | undefined
+ declare class: string | undefined
+
+ constructor() {
+ super()
+ this.variant = "default"
+ this.size = "md"
+ this.color = "primary"
+ this.type = "button"
+ this.disabled = false
+ this.selected = false
+ }
static styles = css`
:host {
@@ -199,11 +216,49 @@ export class ButtonElement extends LitElement {
--bg: color-mix(in oklch, var(--color) 10%, transparent);
}
- /* Optional: stronger hover for outline */
- /* :host([variant="outline"]) button:hover:not(:disabled) {
- --bg: color-mix(in oklch, var(--color) 15%, transparent);
- --border: oklch(from var(--color) calc(l - 0.05) c h);
- } */
+ /* Glass — frosted translucent surface, mirrors a-card[variant="glass"] */
+ :host([variant="glass"]) button {
+ --bg: transparent;
+ /* blend toward the theme foreground so any palette stays readable */
+ --fg: color-mix(in oklch, var(--color) 45%, var(--foreground));
+ --border: color-mix(in oklch, var(--color) 20%, transparent);
+ background: linear-gradient(
+ 135deg,
+ color-mix(in oklch, var(--color) 18%, transparent) 0%,
+ color-mix(in oklch, var(--color) 8%, transparent) 100%
+ );
+ backdrop-filter: blur(6px) saturate(180%);
+ -webkit-backdrop-filter: blur(6px) saturate(180%);
+ box-shadow:
+ 0 1px 2px oklch(0 0 0 / 0.05),
+ 0 8px 24px oklch(0 0 0 / 0.1);
+ }
+
+ :host([variant="glass"]) button:hover:not(:disabled) {
+ background: linear-gradient(
+ 135deg,
+ color-mix(in oklch, var(--color) 28%, transparent) 0%,
+ color-mix(in oklch, var(--color) 14%, transparent) 100%
+ );
+ box-shadow:
+ 0 2px 4px oklch(0 0 0 / 0.06),
+ 0 12px 32px oklch(0 0 0 / 0.16);
+ }
+
+ /* Selected (toggle) — filled, wins over any variant. Uses the primary
+ palette so a neutral pill group lights up consistently when chosen. */
+ :host([selected]) button {
+ --bg: var(--primary);
+ --fg: var(--primary-foreground);
+ --border: transparent;
+ background: var(--primary);
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ }
+
+ :host([selected]) button:hover:not(:disabled) {
+ background: oklch(from var(--primary) calc(l + 0.05) c h);
+ }
`
protected render() {
diff --git a/packages/ui/src/components/card.ts b/packages/ui/src/components/card.ts
index d873625..67faca2 100644
--- a/packages/ui/src/components/card.ts
+++ b/packages/ui/src/components/card.ts
@@ -3,8 +3,19 @@ import { customElement, property } from "lit/decorators.js"
@customElement("a-card")
export class CardElement extends LitElement {
+ /** Glass is the design default; use `variant="default"` for a solid card. */
@property({ reflect: true })
- variant: "default" | "glass" = "default"
+ declare variant: "default" | "glass"
+
+ /** Clickable card: pointer cursor, hover lift, press feedback. */
+ @property({ type: Boolean, reflect: true })
+ declare interactive: boolean
+
+ constructor() {
+ super()
+ this.variant = "glass"
+ this.interactive = false
+ }
static styles: CSSResultGroup = css`
:host {
@@ -29,11 +40,56 @@ export class CardElement extends LitElement {
/* Glass variant */
:host([variant="glass"]) {
- /* --card-bg: oklch(1 0 0 / 0.2); */
- --card-border: var(--border);
- backdrop-filter: blur(1px);
- -webkit-backdrop-filter: blur(1px);
- box-shadow: none;
+ --blur: 2px;
+ background: linear-gradient(
+ 135deg,
+ color-mix(in oklch, var(--card) 20%, transparent) 0%,
+ color-mix(in oklch, var(--card) 8%, transparent) 100%
+ );
+ border: none;
+ backdrop-filter: blur(var(--blur)) saturate(180%);
+ -webkit-backdrop-filter: blur(var(--blur)) saturate(180%);
+ box-shadow:
+ 0 1px 2px oklch(0 0 0 / 0.04),
+ 0 12px 40px oklch(0 0 0 / 0.12);
+ }
+
+ :host([variant="glass"]):hover {
+ box-shadow:
+ 0 2px 4px oklch(0 0 0 / 0.06),
+ 0 16px 50px oklch(0 0 0 / 0.18);
+ }
+
+ /* Interactive cards behave like buttons */
+ :host([interactive]) {
+ cursor: pointer;
+ transition:
+ background 0.2s ease,
+ border-color 0.2s ease,
+ box-shadow 0.2s ease,
+ transform 0.15s ease;
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ :host([interactive][variant="default"]:hover) {
+ background: color-mix(in oklch, var(--foreground) 6%, var(--card));
+ }
+
+ :host([interactive][variant="glass"]:hover) {
+ background: linear-gradient(
+ 135deg,
+ color-mix(in oklch, var(--card) 30%, transparent) 0%,
+ color-mix(in oklch, var(--card) 14%, transparent) 100%
+ );
+ }
+
+ :host([interactive]:active) {
+ transform: scale(0.99);
+ }
+
+ :host([interactive]:focus-visible) {
+ outline: 2px solid var(--ring);
+ outline-offset: 2px;
}
`
diff --git a/packages/ui/src/components/collapse.ts b/packages/ui/src/components/collapse.ts
index 7a2876d..3297bf6 100644
--- a/packages/ui/src/components/collapse.ts
+++ b/packages/ui/src/components/collapse.ts
@@ -3,11 +3,19 @@ import { customElement, property, state } from "lit/decorators.js"
@customElement("a-collapse")
export class CollapseElement extends LitElement {
- @property({ type: Boolean, reflect: true }) open = false
- @property() label = ""
- @property({ type: Boolean }) disabled = false
-
- @state() private _contentHeight = 0
+ @property({ type: Boolean, reflect: true }) declare open: boolean
+ @property() declare label: string
+ @property({ type: Boolean }) declare disabled: boolean
+
+ @state() private declare _contentHeight: number
+
+ constructor() {
+ super()
+ this.open = false
+ this.label = ""
+ this.disabled = false
+ this._contentHeight = 0
+ }
static styles = css`
:host {
diff --git a/packages/ui/src/components/color-picker.ts b/packages/ui/src/components/color-picker.ts
new file mode 100644
index 0000000..62a702f
--- /dev/null
+++ b/packages/ui/src/components/color-picker.ts
@@ -0,0 +1,382 @@
+import { css, html, LitElement, type CSSResultGroup } from "lit"
+import { customElement, property, state } from "lit/decorators.js"
+import { live } from "lit/directives/live.js"
+import "./popover"
+
+interface Rgb {
+ r: number
+ g: number
+ b: number
+}
+
+interface Hsv {
+ h: number
+ s: number
+ v: number
+}
+
+/** Delegates arbitrary CSS color syntax (oklch, hex, rgb, hsl, named…) to the browser's own parser. */
+function parseToRgb(value: string): Rgb {
+ const canvas = document.createElement("canvas")
+ canvas.width = 1
+ canvas.height = 1
+ const ctx = canvas.getContext("2d")!
+ ctx.fillStyle = "#000"
+ ctx.fillStyle = value
+ ctx.fillRect(0, 0, 1, 1)
+ const data = ctx.getImageData(0, 0, 1, 1).data
+ return { r: data[0]!, g: data[1]!, b: data[2]! }
+}
+
+function rgbToHsv(r: number, g: number, b: number): Hsv {
+ const rn = r / 255
+ const gn = g / 255
+ const bn = b / 255
+ const max = Math.max(rn, gn, bn)
+ const min = Math.min(rn, gn, bn)
+ const d = max - min
+
+ let h = 0
+ if (d !== 0) {
+ if (max === rn) h = ((gn - bn) / d) % 6
+ else if (max === gn) h = (bn - rn) / d + 2
+ else h = (rn - gn) / d + 4
+ h *= 60
+ if (h < 0) h += 360
+ }
+
+ const s = max === 0 ? 0 : d / max
+ return { h, s: s * 100, v: max * 100 }
+}
+
+function hsvToRgb(h: number, s: number, v: number): Rgb {
+ const sn = s / 100
+ const vn = v / 100
+ const c = vn * sn
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
+ const m = vn - c
+
+ let r = 0
+ let g = 0
+ let b = 0
+ if (h < 60) [r, g, b] = [c, x, 0]
+ else if (h < 120) [r, g, b] = [x, c, 0]
+ else if (h < 180) [r, g, b] = [0, c, x]
+ else if (h < 240) [r, g, b] = [0, x, c]
+ else if (h < 300) [r, g, b] = [x, 0, c]
+ else [r, g, b] = [c, 0, x]
+
+ return {
+ r: Math.round((r + m) * 255),
+ g: Math.round((g + m) * 255),
+ b: Math.round((b + m) * 255),
+ }
+}
+
+function rgbToHex(r: number, g: number, b: number): string {
+ const h = (n: number) => n.toString(16).padStart(2, "0")
+ return `#${h(r)}${h(g)}${h(b)}`
+}
+
+@customElement("a-color-picker")
+export class ColorPickerElement extends LitElement {
+ @property({ reflect: true }) declare value: string
+ @property() declare label: string | undefined
+ @property({ type: Boolean }) declare disabled: boolean
+
+ @state() private declare _h: number
+ @state() private declare _s: number
+ @state() private declare _v: number
+ @state() private declare _text: string
+
+ private _suppressSync = false
+
+ constructor() {
+ super()
+ this.value = "#000000"
+ this.label = undefined
+ this.disabled = false
+ this._h = 0
+ this._s = 0
+ this._v = 0
+ this._text = this.value
+ this._syncFromValue(this.value)
+ }
+
+ willUpdate(changed: Map) {
+ if (changed.has("value")) {
+ if (this._suppressSync) this._suppressSync = false
+ else this._syncFromValue(this.value)
+ }
+ }
+
+ private _syncFromValue(value: string) {
+ try {
+ const { r, g, b } = parseToRgb(value)
+ const { h, s, v } = rgbToHsv(r, g, b)
+ this._h = h
+ this._s = s
+ this._v = v
+ this._text = value
+ } catch {
+ /* unparsable — keep last known good hsv */
+ }
+ }
+
+ static styles: CSSResultGroup = css`
+ :host {
+ display: inline-flex;
+ flex-direction: column;
+ gap: 0.375rem;
+ }
+
+ label {
+ font-size: var(--sm);
+ font-weight: 500;
+ color: var(--muted-foreground);
+ }
+
+ .swatch-btn {
+ all: unset;
+ box-sizing: border-box;
+ width: 2.5rem;
+ height: 2.5rem;
+ border-radius: var(--radius);
+ border: 1px solid var(--border);
+ cursor: pointer;
+ position: relative;
+ overflow: hidden;
+ background-image:
+ linear-gradient(45deg, color-mix(in oklch, var(--muted-foreground) 18%, transparent) 25%, transparent 25%),
+ linear-gradient(-45deg, color-mix(in oklch, var(--muted-foreground) 18%, transparent) 25%, transparent 25%),
+ linear-gradient(45deg, transparent 75%, color-mix(in oklch, var(--muted-foreground) 18%, transparent) 75%),
+ linear-gradient(-45deg, transparent 75%, color-mix(in oklch, var(--muted-foreground) 18%, transparent) 75%);
+ background-size: 8px 8px;
+ background-position: 0 0, 0 4px, 4px -4px, -4px 0px;
+ }
+
+ .swatch-btn::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: var(--swatch-color);
+ }
+
+ .swatch-btn:hover {
+ border-color: color-mix(in oklch, var(--border) 85%, var(--foreground) 15%);
+ }
+
+ .swatch-btn:focus-visible {
+ outline: 2px solid var(--primary);
+ outline-offset: 2px;
+ }
+
+ .swatch-btn:disabled {
+ opacity: 0.52;
+ cursor: not-allowed;
+ }
+
+ .panel {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ width: 14rem;
+ }
+
+ .sv {
+ position: relative;
+ width: 100%;
+ height: 8rem;
+ border-radius: calc(var(--radius) - 2px);
+ background-color: hsl(var(--hue) 100% 50%);
+ background-image:
+ linear-gradient(to top, #000, transparent),
+ linear-gradient(to right, #fff, transparent);
+ cursor: crosshair;
+ touch-action: none;
+ }
+
+ .sv-thumb {
+ position: absolute;
+ width: 0.875rem;
+ height: 0.875rem;
+ border-radius: 50%;
+ border: 2px solid white;
+ box-shadow:
+ 0 0 0 1px oklch(0 0 0 / 0.35),
+ 0 1px 2px oklch(0 0 0 / 0.3);
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+ }
+
+ .hue {
+ position: relative;
+ width: 100%;
+ height: 0.875rem;
+ border-radius: 999px;
+ background: linear-gradient(to right, red, yellow, lime, cyan, blue, magenta, red);
+ cursor: pointer;
+ touch-action: none;
+ }
+
+ .hue-thumb {
+ position: absolute;
+ top: 50%;
+ width: 1rem;
+ height: 1rem;
+ border-radius: 50%;
+ background: white;
+ border: 2px solid white;
+ box-shadow:
+ 0 0 0 1px oklch(0 0 0 / 0.35),
+ 0 1px 2px oklch(0 0 0 / 0.3);
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+ }
+
+ .text-input {
+ width: 100%;
+ height: 2.25rem;
+ padding: 0 0.625rem;
+ box-sizing: border-box;
+ font-family: var(--font-geist-mono, monospace);
+ font-size: 0.8125rem;
+ color: var(--foreground);
+ background: color-mix(in oklch, var(--background) 82%, transparent);
+ border: 1px solid var(--border);
+ border-radius: calc(var(--radius) - 2px);
+ outline: none;
+ }
+
+ .text-input:focus {
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--primary) 30%, transparent);
+ }
+ `
+
+ render() {
+ return html`
+ ${this.label ? html`` : ""}
+
+
+
+
+ `
+ }
+
+ private _onSvPointerDown = (e: PointerEvent) => {
+ if (this.disabled) return
+ const el = e.currentTarget as HTMLElement
+ el.setPointerCapture(e.pointerId)
+ this._updateSv(e, el)
+
+ const onMove = (ev: PointerEvent) => this._updateSv(ev, el)
+ const onUp = () => {
+ el.removeEventListener("pointermove", onMove)
+ el.removeEventListener("pointerup", onUp)
+ }
+ el.addEventListener("pointermove", onMove)
+ el.addEventListener("pointerup", onUp)
+ }
+
+ private _updateSv(e: PointerEvent, el: HTMLElement) {
+ const rect = el.getBoundingClientRect()
+ const x = Math.min(Math.max(e.clientX - rect.left, 0), rect.width)
+ const y = Math.min(Math.max(e.clientY - rect.top, 0), rect.height)
+ this._s = (x / rect.width) * 100
+ this._v = 100 - (y / rect.height) * 100
+ this._emit()
+ }
+
+ private _onHuePointerDown = (e: PointerEvent) => {
+ if (this.disabled) return
+ const el = e.currentTarget as HTMLElement
+ el.setPointerCapture(e.pointerId)
+ this._updateHue(e, el)
+
+ const onMove = (ev: PointerEvent) => this._updateHue(ev, el)
+ const onUp = () => {
+ el.removeEventListener("pointermove", onMove)
+ el.removeEventListener("pointerup", onUp)
+ }
+ el.addEventListener("pointermove", onMove)
+ el.addEventListener("pointerup", onUp)
+ }
+
+ private _updateHue(e: PointerEvent, el: HTMLElement) {
+ const rect = el.getBoundingClientRect()
+ const x = Math.min(Math.max(e.clientX - rect.left, 0), rect.width)
+ this._h = (x / rect.width) * 360
+ this._emit()
+ }
+
+ private _emit() {
+ const { r, g, b } = hsvToRgb(this._h, this._s, this._v)
+ const hex = rgbToHex(r, g, b)
+ this._text = hex
+ this._suppressSync = true
+ this.value = hex
+
+ this.dispatchEvent(
+ new CustomEvent("change", {
+ detail: hex,
+ bubbles: false,
+ composed: false,
+ }),
+ )
+ }
+
+ private _onTextChange(e: Event) {
+ e.stopPropagation()
+ const target = e.target as HTMLInputElement
+ const raw = target.value.trim()
+ if (!raw) return
+
+ try {
+ const { r, g, b } = parseToRgb(raw)
+ const { h, s, v } = rgbToHsv(r, g, b)
+ this._h = h
+ this._s = s
+ this._v = v
+ } catch {
+ /* let the browser fail to render the swatch rather than reject input */
+ }
+
+ this._suppressSync = true
+ this._text = raw
+ this.value = raw
+
+ this.dispatchEvent(
+ new CustomEvent("change", {
+ detail: raw,
+ bubbles: false,
+ composed: false,
+ }),
+ )
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "a-color-picker": ColorPickerElement
+ }
+}
diff --git a/packages/ui/src/components/dialog.ts b/packages/ui/src/components/dialog.ts
index 8f7d458..8323177 100644
--- a/packages/ui/src/components/dialog.ts
+++ b/packages/ui/src/components/dialog.ts
@@ -3,9 +3,16 @@ import { customElement, property, state } from "lit/decorators.js"
@customElement("a-dialog")
export class DialogElement extends LitElement {
- @property({ type: Boolean }) open = false
+ @property({ type: Boolean }) declare open: boolean
- @state() private _animatingOut = false
+ @state() private declare _animatingOut: boolean
+ private _hideTimer?: ReturnType
+
+ constructor() {
+ super()
+ this.open = false
+ this._animatingOut = false
+ }
static styles = css`
:host {
@@ -50,7 +57,7 @@ export class DialogElement extends LitElement {
@@ -67,6 +74,12 @@ export class DialogElement extends LitElement {
show() {
if (this.open && !this._animatingOut) return
+ // Reopening mid-leave: cancel the pending close so `after-hide` (and the
+ // consumer's state cleanup) never fires against a now-open dialog.
+ if (this._hideTimer) {
+ clearTimeout(this._hideTimer)
+ this._hideTimer = undefined
+ }
this.open = true
this._animatingOut = false
@@ -82,6 +95,10 @@ export class DialogElement extends LitElement {
hide() {
if (!this.open || this._animatingOut) return
this._animatingOut = true
+ // Drop `.visible` now so the fade/scale-out transition actually plays over
+ // the next 220ms. Contents stay mounted (consumers keep their state until
+ // `after-hide`), so the exit animates the real content, not an empty shell.
+ this.open = false
this.dispatchEvent(
new CustomEvent("open-change", {
@@ -91,10 +108,15 @@ export class DialogElement extends LitElement {
}),
)
- // Wait for exit animation before fully closing
- setTimeout(() => {
- this.open = false
+ // Fire once the exit transition has finished and the dialog is fully
+ // transparent — only then may consumers unmount / clear their contents.
+ this._hideTimer = setTimeout(() => {
this._animatingOut = false
+ this._hideTimer = undefined
+
+ this.dispatchEvent(
+ new CustomEvent("after-hide", { bubbles: true, composed: true }),
+ )
}, 220)
}
diff --git a/packages/ui/src/components/flex.ts b/packages/ui/src/components/flex.ts
index 3b38ce8..86e88bc 100644
--- a/packages/ui/src/components/flex.ts
+++ b/packages/ui/src/components/flex.ts
@@ -4,19 +4,28 @@ import { customElement, property } from "lit/decorators.js"
@customElement("a-flex")
export class FlexElement extends LitElement {
@property({ reflect: true })
- direction: "col" | "row" = "row"
+ declare direction: "col" | "row"
@property({ type: Number, reflect: true })
- gap: number = 0
+ declare gap: number
@property({ type: Boolean, reflect: true })
- wrap: boolean = false
+ declare wrap: boolean
@property({ reflect: true })
- justify: "start" | "center" | "end" | "between" = "start"
+ declare justify: "start" | "center" | "end" | "between"
@property({ reflect: true })
- align: "start" | "end" | "center" = "start"
+ declare align: "start" | "end" | "center"
+
+ constructor() {
+ super()
+ this.direction = "row"
+ this.gap = 0
+ this.wrap = false
+ this.justify = "start"
+ this.align = "start"
+ }
static styles?: CSSResultGroup = css`
div {
diff --git a/packages/ui/src/components/grid.ts b/packages/ui/src/components/grid.ts
index 9d177a3..80592d6 100644
--- a/packages/ui/src/components/grid.ts
+++ b/packages/ui/src/components/grid.ts
@@ -11,27 +11,39 @@ export interface GridItem {
@customElement('a-grid')
export class GridElement extends LitElement {
@property({ type: Number, attribute: 'cols-lg', reflect: true })
- colsLg = 4;
+ declare colsLg: number;
@property({ type: Number, attribute: 'cols-md', reflect: true })
- colsMd = 3;
+ declare colsMd: number;
@property({ type: Number, attribute: 'cols-sm', reflect: true })
- colsSm = 2;
+ declare colsSm: number;
@property({ type: Number, attribute: 'cols-xs', reflect: true })
- colsXs = 1;
+ declare colsXs: number;
@property({ type: String, attribute: 'gap', reflect: true })
- gap = '1.25rem';
+ declare gap: string;
@property({ attribute: false })
- items: GridItem[] = [];
+ declare items: GridItem[];
@property({ type: String, attribute: 'card-aspect' })
- cardAspect = '4 / 3';
-
- @state() private _hasSlottedContent = false;
+ declare cardAspect: string;
+
+ @state() private declare _hasSlottedContent: boolean;
+
+ constructor() {
+ super();
+ this.colsLg = 4;
+ this.colsMd = 3;
+ this.colsSm = 2;
+ this.colsXs = 1;
+ this.gap = '1.25rem';
+ this.items = [];
+ this.cardAspect = '4 / 3';
+ this._hasSlottedContent = false;
+ }
static styles = css`
:host {
diff --git a/packages/ui/src/components/head.ts b/packages/ui/src/components/head.ts
index a286f77..7f85b20 100644
--- a/packages/ui/src/components/head.ts
+++ b/packages/ui/src/components/head.ts
@@ -4,15 +4,20 @@ import { customElement, property } from "lit/decorators.js"
@customElement("a-head")
export class HeadingElement extends LitElement {
@property({ type: String, reflect: true })
- level: "1" | "2" | "3" | "4" | "5" | "6" = "2"
+ declare level: "1" | "2" | "3" | "4" | "5" | "6"
+
+ constructor() {
+ super()
+ this.level = "2"
+ }
static styles = css`
+ /* No default margins — pages lay headings out with flex/gap; opt into
+ spacing with utility classes instead of fighting baked-in margins. */
:host {
display: block;
color: var(--foreground);
font-weight: 600;
- margin-top: 1.5em;
- margin-bottom: 0.5em;
}
:host([level="1"]) {
diff --git a/packages/ui/src/components/hover-card.ts b/packages/ui/src/components/hover-card.ts
index ce538f8..c4f1bfe 100644
--- a/packages/ui/src/components/hover-card.ts
+++ b/packages/ui/src/components/hover-card.ts
@@ -3,15 +3,22 @@ import { customElement, property, state } from "lit/decorators.js"
@customElement("a-hover-card")
export class HoverCardElement extends LitElement {
- @property({ type: Number }) openDelay = 100 // ms
- @property({ type: Number }) closeDelay = 200 // ms
- @property({ reflect: true }) side: "top" | "bottom" | "left" | "right" =
- "bottom"
+ @property({ type: Number }) declare openDelay: number // ms
+ @property({ type: Number }) declare closeDelay: number // ms
+ @property({ reflect: true }) declare side: "top" | "bottom" | "left" | "right"
- @state() private _open = false
+ @state() private declare _open: boolean
private _openTimer?: number
private _closeTimer?: number
+ constructor() {
+ super()
+ this.openDelay = 100
+ this.closeDelay = 200
+ this.side = "bottom"
+ this._open = false
+ }
+
static styles: CSSResultGroup = css`
:host {
display: inline-block;
diff --git a/packages/ui/src/components/icon.ts b/packages/ui/src/components/icon.ts
index b1899df..5926365 100644
--- a/packages/ui/src/components/icon.ts
+++ b/packages/ui/src/components/icon.ts
@@ -34,12 +34,19 @@ export class IconElement extends LitElement {
}
`
- @property({ reflect: true }) name?: string
- @property() src?: string
- @property() label = ""
+ @property({ reflect: true }) declare name?: string
+ @property() declare src?: string
+ @property() declare label: string
- @property({ reflect: true }) library = "default"
- @property({ reflect: true }) size: "sm" | "md" | "lg" | "" = ""
+ @property({ reflect: true }) declare library: string
+ @property({ reflect: true }) declare size: "sm" | "md" | "lg" | ""
+
+ constructor() {
+ super()
+ this.label = ""
+ this.library = "default"
+ this.size = ""
+ }
private get effectiveSrc(): string | null {
if (this.src) return this.src
diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts
index 35236c5..744675a 100644
--- a/packages/ui/src/components/index.ts
+++ b/packages/ui/src/components/index.ts
@@ -2,6 +2,7 @@ export * from "./badge"
export * from "./button"
export * from "./card"
export * from "./collapse"
+export * from "./color-picker"
export * from "./container"
export * from "./dialog"
export * from "./flex"
@@ -12,7 +13,9 @@ export * from "./icon"
export * from "./input"
export * from "./popover"
export * from "./scroll-area"
+export * from "./select"
export * from "./separator"
+export * from "./switch"
export * from "./tab"
export * from "./text"
export * from "./theme-provider"
diff --git a/packages/ui/src/components/input.ts b/packages/ui/src/components/input.ts
index 33434fc..e0e3a65 100644
--- a/packages/ui/src/components/input.ts
+++ b/packages/ui/src/components/input.ts
@@ -4,19 +4,30 @@ import { live } from "lit/directives/live.js"
@customElement("a-input")
export class InputElement extends LitElement {
- @property() type: "text" | "email" | "password" | "number" = "text"
- @property() label?: string
- @property() name: string = "input-text"
- @property() placeholder = ""
+ @property() declare type: "text" | "email" | "password" | "number"
+ @property() declare label?: string
+ @property() declare name: string
+ @property() declare placeholder: string
- @property({ reflect: true }) value = ""
- @property({ type: Boolean }) disabled = false
+ @property({ reflect: true }) declare value: string
+ @property({ type: Boolean }) declare disabled: boolean
- @state() private _hasPrefix = false
- @state() private _hasSuffix = false
+ @state() private declare _hasPrefix: boolean
+ @state() private declare _hasSuffix: boolean
private readonly _inputId = `a-input-${Math.random().toString(36).slice(2, 9)}`
+ constructor() {
+ super()
+ this.type = "text"
+ this.name = "input-text"
+ this.placeholder = ""
+ this.value = ""
+ this.disabled = false
+ this._hasPrefix = false
+ this._hasSuffix = false
+ }
+
static styles: CSSResultGroup = css`
:host {
display: block;
diff --git a/packages/ui/src/components/popover.ts b/packages/ui/src/components/popover.ts
index 42e1f80..797b70a 100644
--- a/packages/ui/src/components/popover.ts
+++ b/packages/ui/src/components/popover.ts
@@ -4,12 +4,20 @@ import { classMap } from 'lit/directives/class-map.js'
@customElement('a-popover')
export class PopoverElement extends LitElement {
- @property({ type: Boolean, reflect: true }) open = false
- @property({ reflect: true }) side: 'top' | 'right' | 'bottom' | 'left' = 'bottom'
- @property({ reflect: true }) align: 'start' | 'center' | 'end' = 'center'
- @property({ type: Number }) sideOffset = 4
-
- @queryAssignedElements({ slot: 'trigger' }) private triggerElements!: HTMLElement[]
+ @property({ type: Boolean, reflect: true }) declare open: boolean
+ @property({ reflect: true }) declare side: 'top' | 'right' | 'bottom' | 'left'
+ @property({ reflect: true }) declare align: 'start' | 'center' | 'end'
+ @property({ type: Number }) declare sideOffset: number
+
+ @queryAssignedElements({ slot: 'trigger' }) private declare triggerElements: HTMLElement[]
+
+ constructor() {
+ super()
+ this.open = false
+ this.side = 'bottom'
+ this.align = 'center'
+ this.sideOffset = 4
+ }
static styles: CSSResultGroup = css`
:host {
diff --git a/packages/ui/src/components/scroll-area.ts b/packages/ui/src/components/scroll-area.ts
index 045aeb6..c330430 100644
--- a/packages/ui/src/components/scroll-area.ts
+++ b/packages/ui/src/components/scroll-area.ts
@@ -4,16 +4,26 @@ import { customElement, property, query, state } from "lit/decorators.js"
@customElement("a-scroll-area")
export class ScrollAreaElement extends LitElement {
@property({ reflect: true })
- orientation: "vertical" | "horizontal" | "both" = "vertical"
+ declare orientation: "vertical" | "horizontal" | "both"
+
+ constructor() {
+ super()
+ this.orientation = "vertical"
+ this._thumbY = { top: 2, height: 0 }
+ this._thumbX = { left: 2, width: 0 }
+ this._hasScrollY = false
+ this._hasScrollX = false
+ this._dragging = null
+ }
- @state() private _thumbY = { top: 2, height: 0 }
- @state() private _thumbX = { left: 2, width: 0 }
- @state() private _hasScrollY = false
- @state() private _hasScrollX = false
- @state() private _dragging: "y" | "x" | null = null
+ @state() private declare _thumbY: { top: number; height: number }
+ @state() private declare _thumbX: { left: number; width: number }
+ @state() private declare _hasScrollY: boolean
+ @state() private declare _hasScrollX: boolean
+ @state() private declare _dragging: "y" | "x" | null
- @query(".viewport") private _viewport!: HTMLElement
- @query(".content") private _content!: HTMLElement
+ @query(".viewport") private declare _viewport: HTMLElement
+ @query(".content") private declare _content: HTMLElement
private _ro!: ResizeObserver
private _dragStart = 0
@@ -42,7 +52,9 @@ export class ScrollAreaElement extends LitElement {
.content {
min-width: 100%;
- display: table;
+ min-height: 100%;
+ display: flex;
+ flex-direction: column;
}
/* ── Scrollbar track ── */
diff --git a/packages/ui/src/components/select.ts b/packages/ui/src/components/select.ts
new file mode 100644
index 0000000..03ca923
--- /dev/null
+++ b/packages/ui/src/components/select.ts
@@ -0,0 +1,152 @@
+import { css, html, LitElement, type CSSResultGroup } from "lit"
+import { customElement, property } from "lit/decorators.js"
+import { live } from "lit/directives/live.js"
+
+export interface SelectOption {
+ label: string
+ value: string
+}
+
+@customElement("a-select")
+export class SelectElement extends LitElement {
+ @property() declare label: string | undefined
+ @property() declare name: string
+ @property({ reflect: true }) declare value: string
+ @property({ type: Boolean }) declare disabled: boolean
+ @property({ type: Array }) declare options: SelectOption[]
+ @property() declare placeholder: string
+
+ constructor() {
+ super()
+ this.label = undefined
+ this.name = "select"
+ this.value = ""
+ this.disabled = false
+ this.options = []
+ this.placeholder = ""
+ }
+
+ static styles: CSSResultGroup = css`
+ :host {
+ display: block;
+ position: relative;
+ margin-bottom: 0.5rem;
+ }
+
+ label {
+ display: block;
+ text-align: left;
+ margin-bottom: 0.5rem;
+ font-size: var(--sm);
+ font-weight: 500;
+ color: var(--muted-foreground);
+ }
+
+ .select-wrapper {
+ position: relative;
+ display: flex;
+ align-items: center;
+ }
+
+ select {
+ width: 100%;
+ height: 2.5rem;
+ padding: 0 2.25rem 0 0.875rem;
+ box-sizing: border-box;
+
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+
+ color: var(--foreground);
+ background: color-mix(in oklch, var(--background) 82%, transparent);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+
+ appearance: none;
+ -webkit-appearance: none;
+ cursor: pointer;
+
+ transition:
+ border-color 0.15s ease,
+ box-shadow 0.15s ease,
+ background-color 0.18s ease;
+ }
+
+ select:hover:not(:disabled):not(:focus) {
+ border-color: color-mix(in oklch, var(--border) 85%, var(--foreground) 15%);
+ background: color-mix(in oklch, var(--background) 78%, transparent);
+ }
+
+ select:focus {
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--primary) 30%, transparent);
+ background: color-mix(in oklch, var(--background) 75%, transparent);
+ outline: none;
+ }
+
+ select:disabled {
+ opacity: 0.52;
+ cursor: not-allowed;
+ background: color-mix(in oklch, var(--background) 90%, transparent);
+ border-color: color-mix(in oklch, var(--border) 60%, transparent);
+ }
+
+ .chevron {
+ position: absolute;
+ right: 0.75rem;
+ display: flex;
+ color: var(--muted-foreground);
+ pointer-events: none;
+ }
+ `
+
+ render() {
+ return html`
+ ${this.label ? html`
` : ""}
+
+
+
+
+
+
+ `
+ }
+
+ private _onChange(e: Event) {
+ e.stopPropagation()
+ const target = e.target as HTMLSelectElement
+ this.value = target.value
+
+ this.dispatchEvent(
+ new CustomEvent("change", {
+ detail: target.value,
+ bubbles: false,
+ composed: false,
+ }),
+ )
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "a-select": SelectElement
+ }
+}
diff --git a/packages/ui/src/components/separator.ts b/packages/ui/src/components/separator.ts
index c30a95d..230aa50 100644
--- a/packages/ui/src/components/separator.ts
+++ b/packages/ui/src/components/separator.ts
@@ -4,7 +4,12 @@ import { customElement, property } from "lit/decorators.js"
@customElement("a-separator")
export class SeparatorElement extends LitElement {
@property({ reflect: true })
- orientation: "horizontal" | "vertical" = "horizontal"
+ declare orientation: "horizontal" | "vertical"
+
+ constructor() {
+ super()
+ this.orientation = "horizontal"
+ }
static styles = css`
:host {
diff --git a/packages/ui/src/components/switch.ts b/packages/ui/src/components/switch.ts
new file mode 100644
index 0000000..b6467b4
--- /dev/null
+++ b/packages/ui/src/components/switch.ts
@@ -0,0 +1,118 @@
+import { css, html, LitElement, type CSSResultGroup } from "lit"
+import { customElement, property } from "lit/decorators.js"
+
+@customElement("a-switch")
+export class SwitchElement extends LitElement {
+ @property({ type: Boolean, reflect: true }) declare checked: boolean
+ @property({ type: Boolean, reflect: true }) declare disabled: boolean
+ @property() declare name: string
+ @property() declare label: string | undefined
+
+ constructor() {
+ super()
+ this.checked = false
+ this.disabled = false
+ this.name = "switch"
+ this.label = undefined
+ }
+
+ static styles: CSSResultGroup = css`
+ :host {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.625rem;
+ }
+
+ label {
+ font-size: 0.875rem;
+ color: var(--foreground);
+ cursor: pointer;
+ user-select: none;
+ }
+
+ button {
+ all: unset;
+ box-sizing: border-box;
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ flex-shrink: 0;
+ width: 2.75rem;
+ height: 1.5rem;
+ border-radius: 999px;
+ cursor: pointer;
+ border: 1px solid color-mix(in oklch, var(--border) 70%, transparent);
+ background: var(--input, color-mix(in oklch, var(--muted-foreground) 25%, transparent));
+ transition:
+ background-color 0.15s ease,
+ border-color 0.15s ease;
+ }
+
+ button[aria-checked="true"] {
+ background: var(--primary);
+ border-color: transparent;
+ }
+
+ button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+
+ button:focus-visible {
+ outline: 2px solid var(--primary);
+ outline-offset: 2px;
+ }
+
+ .thumb {
+ position: absolute;
+ top: 1px;
+ left: 1px;
+ width: 1.25rem;
+ height: 1.25rem;
+ border-radius: 50%;
+ background: white;
+ box-shadow: 0 1px 2px oklch(0 0 0 / 0.2);
+ transition: transform 0.15s ease;
+ }
+
+ button[aria-checked="true"] .thumb {
+ transform: translateX(1.25rem);
+ }
+ `
+
+ render() {
+ return html`
+
+ ${this.label ? html`
` : ""}
+ `
+ }
+
+ private _toggle() {
+ if (this.disabled) return
+ this.checked = !this.checked
+
+ this.dispatchEvent(
+ new CustomEvent("change", {
+ detail: this.checked,
+ bubbles: false,
+ composed: false,
+ }),
+ )
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "a-switch": SwitchElement
+ }
+}
diff --git a/packages/ui/src/components/tab.ts b/packages/ui/src/components/tab.ts
index 3d45cf8..bb683bc 100644
--- a/packages/ui/src/components/tab.ts
+++ b/packages/ui/src/components/tab.ts
@@ -8,10 +8,15 @@ import {
@customElement("a-tabs")
export class TabsElement extends LitElement {
@property({ type: String, reflect: true })
- value = ""
+ declare value: string
@queryAssignedElements({ selector: "a-tab" })
- private tabs!: HTMLElement[]
+ private declare tabs: HTMLElement[]
+
+ constructor() {
+ super()
+ this.value = ""
+ }
static styles = css`
:host {
@@ -49,9 +54,13 @@ export class TabsElement extends LitElement {
}
`
- constructor() {
- super()
- this.addEventListener("slotchange", () => this._initialize())
+ // Re-measure the indicator whenever the tab strip changes size (responsive
+ // layouts, tabs appearing/disappearing change every sibling's width).
+ private _resizeObserver = new ResizeObserver(() => this._updateIndicator())
+
+ disconnectedCallback() {
+ super.disconnectedCallback()
+ this._resizeObserver.disconnect()
}
private _initialize() {
@@ -64,6 +73,8 @@ export class TabsElement extends LitElement {
}
protected firstUpdated(_changedProperties: PropertyValues): void {
+ const container = this.renderRoot.querySelector(".tab-list")
+ if (container) this._resizeObserver.observe(container)
requestAnimationFrame(() => {
if (!this.value && this.tabs.length > 0) {
this.value = (this.tabs[0] as any).value || ""
@@ -92,14 +103,20 @@ export class TabsElement extends LitElement {
return
}
+ // Layout metrics, not getBoundingClientRect: rects are scaled while an
+ // ancestor animates with `transform: scale()` (e.g. a-dialog opening) and
+ // ResizeObserver never fires for transforms, so a scaled measurement
+ // would stick.
const container = this.renderRoot.querySelector(".tab-list") as HTMLElement
- const tabRect = activeTab.getBoundingClientRect()
- const containerRect = container.getBoundingClientRect()
-
- const left = tabRect.left - containerRect.left + container.scrollLeft
- const width = tabRect.width
+ const styles = getComputedStyle(container)
+ const gap = parseFloat(styles.columnGap) || 0
+ let left = parseFloat(styles.paddingLeft) || 0
+ for (const tab of this.tabs) {
+ if (tab === activeTab) break
+ left += tab.offsetWidth + gap
+ }
- indicator.style.width = `${width}px`
+ indicator.style.width = `${activeTab.offsetWidth}px`
indicator.style.transform = `translateX(${left}px)`
}
@@ -147,7 +164,9 @@ export class TabsElement extends LitElement {
return html`
@@ -162,16 +181,23 @@ export class TabsElement extends LitElement {
@customElement("a-tab")
export class TabElement extends LitElement {
@property({ type: String, reflect: true })
- value = ""
+ declare value: string
@property({ type: Boolean, reflect: true })
- active = false
+ declare active: boolean
@property({ type: Boolean, reflect: true })
- disabled = false
+ declare disabled: boolean
@property({})
- class: string | undefined
+ declare class: string | undefined
+
+ constructor() {
+ super()
+ this.value = ""
+ this.active = false
+ this.disabled = false
+ }
static styles = css`
:host {
@@ -241,7 +267,12 @@ export class TabElement extends LitElement {
@customElement("a-tab-panel")
export class TabPanelElement extends LitElement {
@property({ type: String, reflect: true })
- value = ""
+ declare value: string
+
+ constructor() {
+ super()
+ this.value = ""
+ }
static styles = css`
:host {
diff --git a/packages/ui/src/components/text.ts b/packages/ui/src/components/text.ts
index a5c67a4..066342a 100644
--- a/packages/ui/src/components/text.ts
+++ b/packages/ui/src/components/text.ts
@@ -4,7 +4,15 @@ import { customElement, property } from "lit/decorators.js"
@customElement("a-text")
export class TextElement extends LitElement {
@property({ type: String, reflect: true })
- variant: "title" | "lead" | "body" | "small" | "muted" = "body"
+ declare variant: "title" | "lead" | "body" | "small" | "muted"
+
+ @property({ type: String, reflect: true })
+ declare size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl"
+
+ constructor() {
+ super()
+ this.variant = "body"
+ }
static styles = css`
:host {
@@ -40,6 +48,36 @@ export class TextElement extends LitElement {
line-height: 1.375rem;
color: var(--muted-foreground);
}
+
+ :host([size="xs"]) {
+ font-size: 0.75rem;
+ line-height: 1rem;
+ }
+
+ :host([size="sm"]) {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ }
+
+ :host([size="md"]) {
+ font-size: 1rem;
+ line-height: 1.5rem;
+ }
+
+ :host([size="lg"]) {
+ font-size: 1.125rem;
+ line-height: 1.75rem;
+ }
+
+ :host([size="xl"]) {
+ font-size: 1.25rem;
+ line-height: 1.75rem;
+ }
+
+ :host([size="2xl"]) {
+ font-size: 1.5rem;
+ line-height: 2rem;
+ }
`
render() {
diff --git a/packages/widgets/src/verification/footer.ts b/packages/widgets/src/verification/footer.ts
index 8f7aa65..a9f2759 100644
--- a/packages/widgets/src/verification/footer.ts
+++ b/packages/widgets/src/verification/footer.ts
@@ -6,10 +6,7 @@ export class VerificationFooterElement extends LitElement {
static styles: CSSResultGroup = css`
:host {
display: block;
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
+ flex-shrink: 0;
font-size: inherit;
padding: 0.625em 1.25em;
border-top: 1px solid var(--border);
diff --git a/packages/widgets/src/verification/index.ts b/packages/widgets/src/verification/index.ts
index 72805e3..0ce3c9b 100644
--- a/packages/widgets/src/verification/index.ts
+++ b/packages/widgets/src/verification/index.ts
@@ -5,7 +5,7 @@ import { focusedProject } from '@/lib/projects'
import { projects } from '@/states/projects'
import { userBrightId } from '@/states/user'
import type { Project } from '@/types/projects'
-import { getProjects } from '@/utils/apis'
+import { getProjects, verifyProject, type VerificationSignature } from '@/utils/apis'
import { EvaluationCategory } from '@/utils/aura'
import { getLevelupProgress } from '@/utils/score'
import { getSubjectVerifications } from '@/utils/subject'
@@ -51,19 +51,20 @@ export class AppVerificationElement extends SignalWatcher(LitElement) {
@state() private step: Step = 'connect'
@state() private previousStep: Step = 'connect'
@state() private isLoadingVerification = false
+ @state() private isGeneratingSignature = false
@state() private verificationData: ProgressStepData | null = null
static styles: CSSResultGroup = css`
:host {
display: block;
- font-size: var(--verification-size, 1rem);
- max-height: 550px;
- overflow-y: auto;
- overflow-x: hidden;
+ font-size: var(--verification-size, 0.875rem);
+ height: 100%;
+ overflow: hidden;
}
.frame {
width: 100%;
+ height: 100%;
border-radius: var(--radius, 0.75rem);
overflow: hidden;
border: 1px solid var(--border);
@@ -72,6 +73,10 @@ export class AppVerificationElement extends SignalWatcher(LitElement) {
}
.content {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
padding: 1.25em;
}
@@ -101,9 +106,6 @@ export class AppVerificationElement extends SignalWatcher(LitElement) {
}
}
- a-scroll-area {
- padding-bottom: 1rem;
- }
`
connectedCallback(): void {
@@ -135,7 +137,9 @@ export class AppVerificationElement extends SignalWatcher(LitElement) {
const scrollBar = this.scrollbar
if (!scrollBar) return
- scrollBar.style.height = `${this.height - 30}px`
+ // Fill the host (which itself fills its container) so the widget takes the
+ // full available height instead of a fixed pixel box.
+ scrollBar.style.height = '100%'
}
private _fetchProjects() {
@@ -208,11 +212,40 @@ export class AppVerificationElement extends SignalWatcher(LitElement) {
this._goToStep('connect')
}
- private _handleContinue() {
+ private async _handleContinue() {
+ const brightId = userBrightId.get()
+ const data = this.verificationData
+ let signature: VerificationSignature | undefined
+
+ // Only verified users reach the success step, so generate the signature
+ // from the API before handing control back to the embedding app.
+ if (brightId) {
+ this.isGeneratingSignature = true
+ try {
+ const result = await verifyProject(this.projectId, {
+ userId: brightId,
+ client: focusedProject.get()?.name ?? 'aura-get-verified',
+ auraScore: data?.auraScore,
+ auraLevel: data?.auraLevel
+ })
+ signature = result?.signature
+ } catch (err) {
+ console.error('Failed to generate verification signature', err)
+ } finally {
+ this.isGeneratingSignature = false
+ }
+ }
+
window.parent.postMessage(
JSON.stringify({
type: 'verification-success',
- app: 'aura-get-verified'
+ app: 'aura-get-verified',
+ data: {
+ brightId,
+ signature,
+ auraLevel: data?.auraLevel,
+ auraScore: data?.auraScore
+ }
}),
'*'
)
@@ -273,6 +306,7 @@ export class AppVerificationElement extends SignalWatcher(LitElement) {
this._handleContinue()}
>
`
diff --git a/packages/widgets/src/verification/success-step.ts b/packages/widgets/src/verification/success-step.ts
index 2367e82..8be0110 100644
--- a/packages/widgets/src/verification/success-step.ts
+++ b/packages/widgets/src/verification/success-step.ts
@@ -6,6 +6,7 @@ import './level-badge'
export class VerificationSuccessElement extends LitElement {
@property() appName = ''
@property({ type: Number }) level = 1
+ @property({ type: Boolean }) loading = false
static styles: CSSResultGroup = css`
:host {
@@ -131,8 +132,12 @@ export class VerificationSuccessElement extends LitElement {
-
this._emit('continue')}>
- Continue to ${this.appName}
+ this._emit('continue')}
+ >
+ ${this.loading ? 'Generating signature…' : html`Continue to ${this.appName}`}
diff --git a/packages/widgets/src/verification/test-harness.ts b/packages/widgets/src/verification/test-harness.ts
index 3b29020..4c99dc5 100644
--- a/packages/widgets/src/verification/test-harness.ts
+++ b/packages/widgets/src/verification/test-harness.ts
@@ -240,6 +240,7 @@ export class VerificationTestHarnessElement extends SignalWatcher(LitElement) {
}
.preview-frame {
max-width: 480px;
+ height: 600px;
}
`