diff --git a/apps/app/package.json b/apps/app/package.json index 3fe3a46bd..3341a3eaf 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -6,10 +6,13 @@ "scripts": { "dev": "OPENWORK_DEV_MODE=1 vite", "dev:windows": "vite", + "prebuild": "pnpm --dir ../../packages/ui build", "build": "vite build", "dev:web": "OPENWORK_DEV_MODE=1 vite", + "prebuild:web": "pnpm --dir ../../packages/ui build", "build:web": "vite build", "preview": "vite preview", + "pretypecheck": "pnpm --dir ../../packages/ui build", "typecheck": "tsc -p tsconfig.json --noEmit", "test:health": "node scripts/health.mjs", "test:mention-send": "node scripts/mention-send.mjs", @@ -32,6 +35,7 @@ "bump:set": "node scripts/bump-version.mjs --set" }, "dependencies": { + "@openwork/ui": "workspace:*", "@codemirror/commands": "^6.8.0", "@codemirror/lang-markdown": "^6.3.3", "@codemirror/language": "^6.11.0", diff --git a/apps/share/package.json b/apps/share/package.json index 79a26f4e2..1d7083eb0 100644 --- a/apps/share/package.json +++ b/apps/share/package.json @@ -11,7 +11,6 @@ "test:e2e": "playwright test" }, "dependencies": { - "@paper-design/shaders-react": "0.0.71", "@vercel/blob": "^0.27.0", "botid": "^1.5.11", "jsonc-parser": "^3.3.1", diff --git a/apps/ui-demo/index.html b/apps/ui-demo/index.html new file mode 100644 index 000000000..66f28a9a4 --- /dev/null +++ b/apps/ui-demo/index.html @@ -0,0 +1,12 @@ + + + + + + OpenWork UI Demo + + +
+ + + diff --git a/apps/ui-demo/package.json b/apps/ui-demo/package.json new file mode 100644 index 000000000..2cb9bde60 --- /dev/null +++ b/apps/ui-demo/package.json @@ -0,0 +1,24 @@ +{ + "name": "@openwork/ui-demo", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "pnpm --dir ../../packages/ui build && vite --host 0.0.0.0 --port 3333 --strictPort", + "build": "pnpm --dir ../../packages/ui build && vite build", + "preview": "vite preview --host 0.0.0.0 --port 3333 --strictPort", + "typecheck": "pnpm --dir ../../packages/ui build && tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@openwork/ui": "workspace:*", + "react": "19.2.4", + "react-dom": "19.2.4" + }, + "devDependencies": { + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "^5.0.4", + "typescript": "^5.9.3", + "vite": "^7.1.12" + } +} diff --git a/apps/ui-demo/src/app.tsx b/apps/ui-demo/src/app.tsx new file mode 100644 index 000000000..76a3ff727 --- /dev/null +++ b/apps/ui-demo/src/app.tsx @@ -0,0 +1,216 @@ +import { + PaperGrainGradient, + PaperMeshGradient, + getSeededPaperGrainGradientConfig, + getSeededPaperMeshGradientConfig, +} from "@openwork/ui/react" +import { useMemo, useState } from "react" + +const sampleIds = [ + "om_01kmhbscaze02vp04ykqa4tcsb", + "om_01kmhbscazf4cjf1bssx6v9q9", + "ow_01kmj2wc68r1zk4n8v7j6v1n2k", +] + +export function App() { + const [seed, setSeed] = useState(sampleIds[0]) + const normalizedSeed = seed.trim() || sampleIds[0] + const parsedSeed = parseTypeId(normalizedSeed) + const meshConfig = useMemo(() => getSeededPaperMeshGradientConfig(normalizedSeed), [normalizedSeed]) + const grainConfig = useMemo(() => getSeededPaperGrainGradientConfig(normalizedSeed), [normalizedSeed]) + + return ( +
+
+
+ +
+
+ OpenWork UI demo +

Seeded Paper gradients on their own dev surface

+

+ Type a TypeID-like string, inspect the deterministic values derived from it, and preview + the gradients that `@openwork/ui/react` will render anywhere else in the repo. +

+
+ +
+ Deterministic + Same seed, same result. +

Useful for stable identity-driven art direction across apps.

+
+
+ +
+
+ + setSeed(event.target.value)} + spellCheck={false} + /> + +
+ {sampleIds.map((sampleId) => ( + + ))} +
+
+ +
+ + + + +
+
+ +
+ } + /> + + } + /> +
+ +
+
+ Determinism check +
+ + + + + + +
+

+ These two cards use the same seed and should always match. +

+
+ +
+ Import paths +
+ @openwork/ui/react + @openwork/ui/solid +
+
{`import { PaperMeshGradient, PaperGrainGradient } from "@openwork/ui/react"
+
+
+`}
+
+
+
+ ) +} + +function GradientCard({ + title, + subtitle, + colors, + config, + surface, +}: { + title: string + subtitle: string + colors: string[] + config: Record + surface: React.ReactNode +}) { + return ( +
+
+ {surface} +
+
+ @openwork/ui/react +

{title}

+

{subtitle}

+
+
+ +
+
+ Colors +
+ {colors.map((color) => ( +
+ + {color} +
+ ))} +
+
+ +
+ Calculated values +
{JSON.stringify(config, null, 2)}
+
+
+
+ ) +} + +function MiniPreview({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+ {title} +
{children}
+
+ ) +} + +function SeedMeta({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ) +} + +function parseTypeId(value: string) { + const separatorIndex = value.indexOf("_") + + if (separatorIndex === -1) { + return { + prefix: null, + suffix: value, + suffixAnchor: value.slice(0, 5) || null, + suffixTail: value.slice(5) || null, + } + } + + const prefix = value.slice(0, separatorIndex) || null + const suffix = value.slice(separatorIndex + 1) || null + + return { + prefix, + suffix, + suffixAnchor: suffix?.slice(0, 5) || null, + suffixTail: suffix?.slice(5) || null, + } +} diff --git a/apps/ui-demo/src/main.tsx b/apps/ui-demo/src/main.tsx new file mode 100644 index 000000000..8624da889 --- /dev/null +++ b/apps/ui-demo/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react" +import ReactDOM from "react-dom/client" +import { App } from "./app" +import "./styles.css" + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +) diff --git a/apps/ui-demo/src/styles.css b/apps/ui-demo/src/styles.css new file mode 100644 index 000000000..69007a899 --- /dev/null +++ b/apps/ui-demo/src/styles.css @@ -0,0 +1,343 @@ +:root { + color-scheme: light; + font-family: "IBM Plex Sans", "Inter", system-ui, sans-serif; + background: + radial-gradient(circle at top left, rgba(57, 181, 74, 0.16), transparent 28%), + radial-gradient(circle at top right, rgba(39, 98, 255, 0.14), transparent 30%), + linear-gradient(180deg, #f6f1e8 0%, #efe7d7 100%); + color: #1f2c2b; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; +} + +button, +input, +textarea, +select { + font: inherit; +} + +code, +pre, +.sample-chip, +.seed-input { + font-family: "IBM Plex Mono", "SFMono-Regular", ui-monospace, monospace; +} + +.app-shell { + position: relative; + min-height: 100vh; + padding: 32px; + overflow: hidden; +} + +.ambient { + position: fixed; + pointer-events: none; + border-radius: 999px; + filter: blur(70px); + opacity: 0.45; +} + +.ambient-a { + top: -120px; + left: -80px; + width: 320px; + height: 320px; + background: rgba(76, 175, 80, 0.22); +} + +.ambient-b { + right: -80px; + bottom: 80px; + width: 280px; + height: 280px; + background: rgba(59, 130, 246, 0.2); +} + +.panel { + position: relative; + border: 1px solid rgba(24, 30, 28, 0.08); + background: rgba(255, 251, 245, 0.82); + backdrop-filter: blur(18px); + border-radius: 28px; + box-shadow: 0 24px 80px -48px rgba(29, 24, 17, 0.45); +} + +.hero-card { + display: grid; + gap: 24px; + grid-template-columns: minmax(0, 1.5fr) minmax(260px, 0.8fr); + padding: 32px; +} + +.hero-copy h1 { + margin: 10px 0 0; + font-size: clamp(2.4rem, 6vw, 4.5rem); + line-height: 0.92; + letter-spacing: -0.08em; +} + +.hero-copy p, +.rule-card p, +.support-copy, +.surface-copy p { + margin: 0; + color: #516160; + line-height: 1.7; +} + +.hero-copy p { + margin-top: 18px; + max-width: 62ch; +} + +.rule-card { + align-self: end; + padding: 20px; + border-radius: 24px; + background: #16201f; + color: #f0f7f3; +} + +.rule-card p { + margin-top: 8px; + color: rgba(240, 247, 243, 0.74); +} + +.eyebrow { + display: inline-block; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.muted { + color: #6d7a79; +} + +.on-dark { + color: rgba(255, 255, 255, 0.68); +} + +.controls-grid, +.footer-grid { + display: grid; + gap: 24px; + grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.95fr); + margin-top: 24px; +} + +.input-panel, +.seed-meta-grid, +.code-panel { + padding: 24px; +} + +.seed-input { + width: 100%; + margin-top: 10px; + padding: 15px 16px; + border-radius: 20px; + border: 1px solid rgba(22, 32, 31, 0.12); + background: rgba(255, 255, 255, 0.92); + color: #182321; + font-size: 0.92rem; + outline: none; + transition: border-color 120ms ease, box-shadow 120ms ease; +} + +.seed-input:focus { + border-color: #287d75; + box-shadow: 0 0 0 5px rgba(40, 125, 117, 0.12); +} + +.sample-list { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 16px; +} + +.sample-chip { + border: 1px solid rgba(22, 32, 31, 0.1); + background: rgba(255, 255, 255, 0.75); + color: #425251; + border-radius: 999px; + padding: 9px 14px; + cursor: pointer; +} + +.sample-chip.active { + background: #1f6978; + color: white; + border-color: #1f6978; +} + +.seed-meta-grid { + display: grid; + gap: 12px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + background: #16201f; +} + +.seed-meta-card { + padding: 18px; + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.04); +} + +.seed-meta-card code { + display: block; + margin-top: 8px; + color: #f4fbf7; + word-break: break-all; + line-height: 1.6; +} + +.preview-grid { + display: grid; + gap: 24px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-top: 24px; +} + +.preview-card { + overflow: hidden; +} + +.gradient-surface { + position: relative; + min-height: 340px; + background: #101818; +} + +.gradient-fill { + position: absolute; + inset: 0; +} + +.surface-overlay { + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(10, 18, 18, 0.08), rgba(10, 18, 18, 0.34)); +} + +.surface-copy { + position: absolute; + inset-inline: 0; + bottom: 0; + padding: 24px; + color: white; +} + +.surface-copy h2 { + margin: 10px 0 0; + font-size: 2rem; + letter-spacing: -0.05em; +} + +.surface-copy p { + margin-top: 10px; + color: rgba(255, 255, 255, 0.78); +} + +.details-stack { + display: grid; + gap: 20px; + padding: 24px; +} + +.swatch-list, +.pill-stack { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 14px; +} + +.swatch-pill, +.import-pill { + display: inline-flex; + align-items: center; + gap: 10px; + border-radius: 999px; + border: 1px solid rgba(22, 32, 31, 0.08); + background: rgba(248, 243, 235, 0.9); + padding: 10px 14px; +} + +.swatch-dot { + width: 14px; + height: 14px; + border-radius: 999px; + border: 1px solid rgba(0, 0, 0, 0.12); +} + +pre { + overflow-x: auto; + margin: 14px 0 0; + border-radius: 22px; + background: #16201f; + color: #dcebe4; + padding: 18px; + font-size: 0.8rem; + line-height: 1.7; +} + +.mini-grid { + display: grid; + gap: 16px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-top: 16px; +} + +.mini-surface { + position: relative; + min-height: 180px; + margin-top: 8px; + overflow: hidden; + border-radius: 22px; + background: #111918; +} + +.support-copy { + margin-top: 16px; +} + +@media (max-width: 980px) { + .hero-card, + .controls-grid, + .preview-grid, + .footer-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .app-shell { + padding: 18px; + } + + .hero-card, + .input-panel, + .seed-meta-grid, + .code-panel, + .details-stack { + padding: 18px; + } + + .seed-meta-grid, + .mini-grid { + grid-template-columns: 1fr; + } +} diff --git a/apps/ui-demo/tsconfig.json b/apps/ui-demo/tsconfig.json new file mode 100644 index 000000000..2957d0dfd --- /dev/null +++ b/apps/ui-demo/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/apps/ui-demo/vite.config.ts b/apps/ui-demo/vite.config.ts new file mode 100644 index 000000000..4f3f02a3e --- /dev/null +++ b/apps/ui-demo/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "vite" +import react from "@vitejs/plugin-react" + +export default defineConfig({ + plugins: [react()], + server: { + host: "0.0.0.0", + port: 3333, + strictPort: true, + }, + preview: { + host: "0.0.0.0", + port: 3333, + strictPort: true, + }, + build: { + target: "es2022", + }, +}) diff --git a/ee/apps/den-api/.env.example b/ee/apps/den-api/.env.example index 23afc77f3..8a6a5e0ca 100644 --- a/ee/apps/den-api/.env.example +++ b/ee/apps/den-api/.env.example @@ -18,6 +18,8 @@ POLAR_FEATURE_GATE_ENABLED=false POLAR_ACCESS_TOKEN= POLAR_PRODUCT_ID= POLAR_BENEFIT_ID= +POLAR_WORKER_PRODUCT_ID= +POLAR_WORKER_BENEFIT_ID= POLAR_SUCCESS_URL= POLAR_RETURN_URL= DAYTONA_API_KEY= diff --git a/ee/apps/den-api/README.md b/ee/apps/den-api/README.md index 0469126f3..ac3a8356a 100644 --- a/ee/apps/den-api/README.md +++ b/ee/apps/den-api/README.md @@ -12,6 +12,13 @@ It carries the full migrated Den API route surface in a foldered Hono structure pnpm --filter @openwork-ee/den-api dev:local ``` +## Billing model + +- `POLAR_PRODUCT_ID` / `POLAR_BENEFIT_ID`: base OpenWork Cloud team plan +- `POLAR_WORKER_PRODUCT_ID` / `POLAR_WORKER_BENEFIT_ID`: per-worker add-on product +- The base plan unlocks the shared cloud workspace. +- Workers are counted separately and billed as additional recurring subscriptions. + ## Current routes - `GET /` -> `302 https://openworklabs.com` diff --git a/ee/apps/den-api/src/billing/polar.ts b/ee/apps/den-api/src/billing/polar.ts index d202a6f3c..bcd592f2a 100644 --- a/ee/apps/den-api/src/billing/polar.ts +++ b/ee/apps/den-api/src/billing/polar.ts @@ -15,12 +15,6 @@ type PolarCustomerSession = { customer_portal_url?: string } -type PolarCustomer = { - id?: string - email?: string - external_id?: string | null -} - type PolarListResource = { items?: T[] } @@ -119,6 +113,10 @@ export type CloudWorkerBillingStatus = { hasActivePlan: boolean checkoutRequired: boolean checkoutUrl: string | null + activeWorkerSubscriptions: number + workerCheckoutUrl: string | null + workerCheckoutRequired: boolean + workerPrice: CloudWorkerBillingPrice | null portalUrl: string | null price: CloudWorkerBillingPrice | null subscription: CloudWorkerBillingSubscription | null @@ -139,6 +137,7 @@ type CloudAccessInput = { userId: string email: string name: string + orgId?: string | null } type BillingStatusOptions = { @@ -163,6 +162,10 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null } +function getExternalCustomerId(input: CloudAccessInput) { + return input.orgId?.trim() || input.userId +} + async function polarFetch(path: string, init: RequestInit = {}) { const headers = new Headers(init.headers) headers.set("Authorization", `Bearer ${env.polar.accessToken}`) @@ -199,26 +202,28 @@ function assertPaywallConfig() { } } -async function getCustomerStateByExternalId(externalCustomerId: string): Promise { - const encodedExternalId = encodeURIComponent(externalCustomerId) - const { response, payload, text } = await polarFetchJson(`/v1/customers/external/${encodedExternalId}/state`, { - method: "GET", - }) - - if (response.status === 404) { - return null +function assertWorkerPaywallConfig() { + if (!env.polar.featureGateEnabled) { + return } - if (!response.ok) { - throw new Error(`Polar customer state lookup failed (${response.status}): ${text.slice(0, 400)}`) + if (!env.polar.accessToken) { + throw new Error("POLAR_ACCESS_TOKEN is required when POLAR_FEATURE_GATE_ENABLED=true") + } + if (!env.polar.workerProductId) { + throw new Error("POLAR_WORKER_PRODUCT_ID is required when POLAR_FEATURE_GATE_ENABLED=true") + } + if (!env.polar.successUrl) { + throw new Error("POLAR_SUCCESS_URL is required when POLAR_FEATURE_GATE_ENABLED=true") + } + if (!env.polar.returnUrl) { + throw new Error("POLAR_RETURN_URL is required when POLAR_FEATURE_GATE_ENABLED=true") } - - return payload } -async function getCustomerStateById(customerId: string): Promise { - const encodedCustomerId = encodeURIComponent(customerId) - const { response, payload, text } = await polarFetchJson(`/v1/customers/${encodedCustomerId}/state`, { +async function getCustomerStateByExternalId(externalCustomerId: string): Promise { + const encodedExternalId = encodeURIComponent(externalCustomerId) + const { response, payload, text } = await polarFetchJson(`/v1/customers/external/${encodedExternalId}/state`, { method: "GET", }) @@ -227,64 +232,27 @@ async function getCustomerStateById(customerId: string): Promise { - const normalizedEmail = email.trim().toLowerCase() - if (!normalizedEmail) { - return null - } - - const encodedEmail = encodeURIComponent(normalizedEmail) - const { response, payload, text } = await polarFetchJson>(`/v1/customers/?email=${encodedEmail}`, { - method: "GET", - }) - - if (!response.ok) { - throw new Error(`Polar customer lookup by email failed (${response.status}): ${text.slice(0, 400)}`) - } - - const customers = payload?.items ?? [] - const exact = customers.find((customer) => customer.email?.trim().toLowerCase() === normalizedEmail) - return exact ?? customers[0] ?? null -} - -async function linkCustomerExternalId(customer: PolarCustomer, externalCustomerId: string): Promise { - if (!customer.id) { - return - } - - if (typeof customer.external_id === "string" && customer.external_id.length > 0) { - return - } - - const encodedCustomerId = encodeURIComponent(customer.id) - await polarFetch(`/v1/customers/${encodedCustomerId}`, { - method: "PATCH", - body: JSON.stringify({ - external_id: externalCustomerId, - }), - }) -} - -function hasRequiredBenefit(state: PolarCustomerState | null) { - if (!state?.granted_benefits || !env.polar.benefitId) { +function hasBenefit(state: PolarCustomerState | null, benefitId: string | undefined) { + if (!state?.granted_benefits || !benefitId) { return false } - return state.granted_benefits.some((grant) => grant.benefit_id === env.polar.benefitId) + return state.granted_benefits.some((grant) => grant.benefit_id === benefitId) } -async function createCheckoutSession(input: CloudAccessInput): Promise { +async function createCheckoutSessionForProduct(input: CloudAccessInput, productId: string): Promise { + const externalCustomerId = getExternalCustomerId(input) const payload = { - products: [env.polar.productId], + products: [productId], success_url: env.polar.successUrl, return_url: env.polar.returnUrl, - external_customer_id: input.userId, + external_customer_id: externalCustomerId, customer_email: input.email, customer_name: input.name, } @@ -325,8 +293,9 @@ async function evaluateCloudWorkerAccess( assertPaywallConfig() - const externalState = await getCustomerStateByExternalId(input.userId) - if (hasRequiredBenefit(externalState)) { + const externalCustomerId = getExternalCustomerId(input) + const externalState = await getCustomerStateByExternalId(externalCustomerId) + if (hasBenefit(externalState, env.polar.benefitId)) { return { featureGateEnabled: true, hasActivePlan: true, @@ -334,26 +303,36 @@ async function evaluateCloudWorkerAccess( } } - const customer = await getCustomerByEmail(input.email) - if (customer?.id) { - const emailState = await getCustomerStateById(customer.id) - if (hasRequiredBenefit(emailState)) { - await linkCustomerExternalId(customer, input.userId).catch(() => undefined) - return { - featureGateEnabled: true, - hasActivePlan: true, - checkoutUrl: null, - } - } - } - + const productId = env.polar.productId return { featureGateEnabled: true, hasActivePlan: false, - checkoutUrl: options.includeCheckoutUrl ? await createCheckoutSession(input) : null, + checkoutUrl: options.includeCheckoutUrl && productId ? await createCheckoutSessionForProduct(input, productId) : null, } } +async function getActiveWorkerSubscriptionCount(input: CloudAccessInput): Promise { + assertWorkerPaywallConfig() + + const subscriptions = await listSubscriptionsByExternalCustomer(getExternalCustomerId(input), { + activeOnly: true, + limit: 100, + productId: env.polar.workerProductId, + }) + + return subscriptions.filter((subscription) => isActiveSubscriptionStatus(subscription.status)).length +} + +async function createWorkerCheckoutSession(input: CloudAccessInput): Promise { + assertWorkerPaywallConfig() + const workerProductId = env.polar.workerProductId + if (!workerProductId) { + throw new Error("POLAR_WORKER_PRODUCT_ID is required when POLAR_FEATURE_GATE_ENABLED=true") + } + + return createCheckoutSessionForProduct(input, workerProductId) +} + function normalizeRecurringInterval(value: string | null | undefined): string | null { return typeof value === "string" && value.trim().length > 0 ? value : null } @@ -419,12 +398,12 @@ async function getSubscriptionById(subscriptionId: string): Promise { const params = new URLSearchParams() params.set("external_customer_id", externalCustomerId) - if (env.polar.productId) { - params.set("product_id", env.polar.productId) + if (options.productId) { + params.set("product_id", options.productId) } params.set("limit", String(options.limit ?? 1)) params.set("sorting", "-started_at") @@ -458,21 +437,26 @@ async function listSubscriptionsByExternalCustomer( } async function getPrimarySubscriptionForCustomer(externalCustomerId: string): Promise { - const active = await listSubscriptionsByExternalCustomer(externalCustomerId, { activeOnly: true, limit: 1 }) + const active = await listSubscriptionsByExternalCustomer(externalCustomerId, { + activeOnly: true, + limit: 1, + productId: env.polar.productId, + }) if (active[0]) { return active[0] } - const recent = await listSubscriptionsByExternalCustomer(externalCustomerId, { activeOnly: false, limit: 1 }) + const recent = await listSubscriptionsByExternalCustomer(externalCustomerId, { + activeOnly: false, + limit: 1, + productId: env.polar.productId, + }) return recent[0] ?? null } async function listRecentOrdersByExternalCustomer(externalCustomerId: string, limit = 6): Promise { const params = new URLSearchParams() params.set("external_customer_id", externalCustomerId) - if (env.polar.productId) { - params.set("product_id", env.polar.productId) - } params.set("limit", String(limit)) params.set("sorting", "-created_at") @@ -649,6 +633,28 @@ export async function requireCloudWorkerAccess(input: CloudAccessInput): Promise } } +export async function requireAdditionalCloudWorkerAccess(input: CloudAccessInput & { ownedWorkerCount: number }): Promise { + if (!env.polar.featureGateEnabled) { + return { allowed: true } + } + + assertWorkerPaywallConfig() + const activeWorkerSubscriptions = await getActiveWorkerSubscriptionCount(input) + if (input.ownedWorkerCount < activeWorkerSubscriptions) { + return { allowed: true } + } + + const checkoutUrl = await createWorkerCheckoutSession(input) + if (!checkoutUrl) { + throw new Error("Polar worker checkout URL unavailable") + } + + return { + allowed: false, + checkoutUrl, + } +} + export async function getCloudWorkerBillingStatus( input: CloudAccessInput, options: BillingStatusOptions = {}, @@ -665,6 +671,10 @@ export async function getCloudWorkerBillingStatus( hasActivePlan: true, checkoutRequired: false, checkoutUrl: null, + activeWorkerSubscriptions: 0, + workerCheckoutUrl: null, + workerCheckoutRequired: false, + workerPrice: null, portalUrl: null, price: null, subscription: null, @@ -676,11 +686,31 @@ export async function getCloudWorkerBillingStatus( await sendSubscribedToDenEvent(input) } + let activeWorkerSubscriptions = 0 + let workerCheckoutUrl: string | null = null + let workerPrice: CloudWorkerBillingPrice | null = null + + if (evaluation.hasActivePlan) { + assertWorkerPaywallConfig() + const workerProductId = env.polar.workerProductId + if (!workerProductId) { + throw new Error("POLAR_WORKER_PRODUCT_ID is required when POLAR_FEATURE_GATE_ENABLED=true") + } + + ;[activeWorkerSubscriptions, workerCheckoutUrl, workerPrice] = await Promise.all([ + getActiveWorkerSubscriptionCount(input), + options.includeCheckoutUrl ? createWorkerCheckoutSession(input).catch(() => null) : Promise.resolve(null), + getProductBillingPrice(workerProductId).catch(() => null), + ]) + } else if (env.polar.workerProductId) { + workerPrice = await getProductBillingPrice(env.polar.workerProductId).catch(() => null) + } + const [subscriptionResult, priceResult, portalResult, invoicesResult] = await Promise.all([ - getPrimarySubscriptionForCustomer(input.userId).catch(() => null), + getPrimarySubscriptionForCustomer(getExternalCustomerId(input)).catch(() => null), env.polar.productId ? getProductBillingPrice(env.polar.productId).catch(() => null) : Promise.resolve(null), - includePortalUrl ? createCustomerPortalUrl(input.userId).catch(() => null) : Promise.resolve(null), - includeInvoices ? listBillingInvoices(input.userId).catch(() => []) : Promise.resolve([]), + includePortalUrl ? createCustomerPortalUrl(getExternalCustomerId(input)).catch(() => null) : Promise.resolve(null), + includeInvoices ? listBillingInvoices(getExternalCustomerId(input)).catch(() => []) : Promise.resolve([]), ]) const subscription = toBillingSubscription(subscriptionResult) @@ -693,6 +723,10 @@ export async function getCloudWorkerBillingStatus( hasActivePlan: evaluation.hasActivePlan, checkoutRequired: evaluation.featureGateEnabled && !evaluation.hasActivePlan, checkoutUrl: evaluation.checkoutUrl, + activeWorkerSubscriptions, + workerCheckoutUrl, + workerCheckoutRequired: evaluation.hasActivePlan && activeWorkerSubscriptions <= 0, + workerPrice, portalUrl, price: productPrice ?? toBillingPriceFromSubscription(subscription), subscription, @@ -732,24 +766,15 @@ export async function getCloudWorkerAdminBillingStatus( let paidByBenefit = false if (env.polar.benefitId) { - const externalState = await getCustomerStateByExternalId(input.userId) - if (hasRequiredBenefit(externalState)) { + const externalCustomerId = getExternalCustomerId(input) + const externalState = await getCustomerStateByExternalId(externalCustomerId) + if (hasBenefit(externalState, env.polar.benefitId)) { paidByBenefit = true note = "Benefit granted via external customer id." - } else { - const customer = await getCustomerByEmail(input.email) - if (customer?.id) { - const emailState = await getCustomerStateById(customer.id) - if (hasRequiredBenefit(emailState)) { - paidByBenefit = true - note = "Benefit granted via matching customer email." - await linkCustomerExternalId(customer, input.userId).catch(() => undefined) - } - } } } - const subscription = env.polar.productId ? await getPrimarySubscriptionForCustomer(input.userId) : null + const subscription = env.polar.productId ? await getPrimarySubscriptionForCustomer(getExternalCustomerId(input)) : null const normalizedSubscription = toBillingSubscription(subscription) const paidBySubscription = isActiveSubscriptionStatus(normalizedSubscription?.status) @@ -789,9 +814,10 @@ export async function setCloudWorkerSubscriptionCancellation( assertPaywallConfig() - const activeSubscriptions = await listSubscriptionsByExternalCustomer(input.userId, { + const activeSubscriptions = await listSubscriptionsByExternalCustomer(getExternalCustomerId(input), { activeOnly: true, limit: 1, + productId: env.polar.productId, }) const active = activeSubscriptions[0] if (!active?.id) { diff --git a/ee/apps/den-api/src/env.ts b/ee/apps/den-api/src/env.ts index ce84d9b88..37177a24d 100644 --- a/ee/apps/den-api/src/env.ts +++ b/ee/apps/den-api/src/env.ts @@ -50,6 +50,8 @@ const EnvSchema = z.object({ POLAR_ACCESS_TOKEN: z.string().optional(), POLAR_PRODUCT_ID: z.string().optional(), POLAR_BENEFIT_ID: z.string().optional(), + POLAR_WORKER_PRODUCT_ID: z.string().optional(), + POLAR_WORKER_BENEFIT_ID: z.string().optional(), POLAR_SUCCESS_URL: z.string().optional(), POLAR_RETURN_URL: z.string().optional(), DAYTONA_API_URL: z.string().optional(), @@ -208,6 +210,8 @@ export const env = { accessToken: parsed.POLAR_ACCESS_TOKEN, productId: parsed.POLAR_PRODUCT_ID, benefitId: parsed.POLAR_BENEFIT_ID, + workerProductId: parsed.POLAR_WORKER_PRODUCT_ID, + workerBenefitId: parsed.POLAR_WORKER_BENEFIT_ID, successUrl: parsed.POLAR_SUCCESS_URL, returnUrl: parsed.POLAR_RETURN_URL, }, diff --git a/ee/apps/den-api/src/routes/org/core.ts b/ee/apps/den-api/src/routes/org/core.ts index 469f9c77c..f00083540 100644 --- a/ee/apps/den-api/src/routes/org/core.ts +++ b/ee/apps/den-api/src/routes/org/core.ts @@ -101,9 +101,19 @@ export function registerOrgCoreRoutes { + async (c) => { + const session = c.get("session") + const organizationContext = c.get("organizationContext") + + if (session?.id) { + await setSessionActiveOrganization( + normalizeDenTypeId("session", session.id), + organizationContext.organization.id, + ) + } + return c.json({ - ...c.get("organizationContext"), + ...organizationContext, currentMemberTeams: c.get("memberTeams") ?? [], }) }, diff --git a/ee/apps/den-api/src/routes/workers/billing.ts b/ee/apps/den-api/src/routes/workers/billing.ts index c39c2c22d..bbea49131 100644 --- a/ee/apps/den-api/src/routes/workers/billing.ts +++ b/ee/apps/den-api/src/routes/workers/billing.ts @@ -1,63 +1,92 @@ import type { Hono } from "hono" import { env } from "../../env.js" -import { jsonValidator, queryValidator, requireUserMiddleware } from "../../middleware/index.js" +import { jsonValidator, queryValidator, requireUserMiddleware, resolveUserOrganizationsMiddleware } from "../../middleware/index.js" import { getRequiredUserEmail } from "../../user.js" import type { WorkerRouteVariables } from "./shared.js" import { billingQuerySchema, billingSubscriptionSchema, getWorkerBilling, setWorkerBillingSubscription, queryIncludesFlag } from "./shared.js" export function registerWorkerBillingRoutes(app: Hono) { - app.get("/v1/workers/billing", requireUserMiddleware, queryValidator(billingQuerySchema), async (c) => { + app.get("/v1/workers/billing", requireUserMiddleware, resolveUserOrganizationsMiddleware, queryValidator(billingQuerySchema), async (c) => { const user = c.get("user") + const orgId = c.get("activeOrganizationId") const query = c.req.valid("query") const email = getRequiredUserEmail(user) if (!email) { return c.json({ error: "user_email_required" }, 400) } + if (!orgId) { + return c.json({ error: "organization_required" }, 409) + } - const billing = await getWorkerBilling({ - userId: user.id, - email, - name: user.name ?? user.email ?? "OpenWork User", - includeCheckoutUrl: queryIncludesFlag(query.includeCheckout), - includePortalUrl: !queryIncludesFlag(query.excludePortal), - includeInvoices: !queryIncludesFlag(query.excludeInvoices), - }) + let billing + try { + billing = await getWorkerBilling({ + userId: user.id, + orgId, + email, + name: user.name ?? user.email ?? "OpenWork User", + includeCheckoutUrl: queryIncludesFlag(query.includeCheckout), + includePortalUrl: !queryIncludesFlag(query.excludePortal), + includeInvoices: !queryIncludesFlag(query.excludeInvoices), + }) + } catch (error) { + return c.json({ + error: "billing_unavailable", + message: error instanceof Error ? error.message : "Billing is unavailable.", + }, 503) + } return c.json({ billing: { ...billing, productId: env.polar.productId, benefitId: env.polar.benefitId, + workerProductId: env.polar.workerProductId, + workerBenefitId: env.polar.workerBenefitId, }, }) }) - app.post("/v1/workers/billing/subscription", requireUserMiddleware, jsonValidator(billingSubscriptionSchema), async (c) => { + app.post("/v1/workers/billing/subscription", requireUserMiddleware, resolveUserOrganizationsMiddleware, jsonValidator(billingSubscriptionSchema), async (c) => { const user = c.get("user") + const orgId = c.get("activeOrganizationId") const input = c.req.valid("json") const email = getRequiredUserEmail(user) if (!email) { return c.json({ error: "user_email_required" }, 400) } + if (!orgId) { + return c.json({ error: "organization_required" }, 409) + } const billingInput = { userId: user.id, + orgId, email, name: user.name ?? user.email ?? "OpenWork User", } - const subscription = await setWorkerBillingSubscription({ - ...billingInput, - cancelAtPeriodEnd: input.cancelAtPeriodEnd, - }) - const billing = await getWorkerBilling({ - ...billingInput, - includeCheckoutUrl: false, - includePortalUrl: true, - includeInvoices: true, - }) + let subscription + let billing + try { + subscription = await setWorkerBillingSubscription({ + ...billingInput, + cancelAtPeriodEnd: input.cancelAtPeriodEnd, + }) + billing = await getWorkerBilling({ + ...billingInput, + includeCheckoutUrl: false, + includePortalUrl: true, + includeInvoices: true, + }) + } catch (error) { + return c.json({ + error: "billing_unavailable", + message: error instanceof Error ? error.message : "Billing is unavailable.", + }, 503) + } return c.json({ subscription, @@ -65,6 +94,8 @@ export function registerWorkerBillingRoutes 0) { + if (input.destination === "cloud" && !env.devMode) { const email = getRequiredUserEmail(user) if (!email) { return c.json({ error: "user_email_required" }, 400) } - const access = await requireCloudAccessOrPayment({ - userId: user.id, - email, - name: user.name ?? user.email ?? "OpenWork User", - }) - if (!access.allowed) { + try { + const baseAccess = await requireCloudAccessOrPayment({ + userId: user.id, + orgId, + email, + name: user.name ?? user.email ?? "OpenWork User", + }) + if (!baseAccess.allowed) { + return c.json({ + error: "payment_required", + message: "OpenWork Cloud billing is required before launching workers.", + polar: { + checkoutUrl: baseAccess.checkoutUrl, + productId: env.polar.productId, + benefitId: env.polar.benefitId, + }, + }, 402) + } + + const ownedWorkerCount = await countOrgCloudWorkers(orgId) + const workerAccess = await requireAdditionalCloudCapacityOrPayment({ + userId: user.id, + orgId, + email, + name: user.name ?? user.email ?? "OpenWork User", + ownedWorkerCount, + }) + if (!workerAccess.allowed) { + return c.json({ + error: "payment_required", + message: "No workers are included by default. Purchase a worker add-on to launch another hosted worker.", + polar: { + checkoutUrl: workerAccess.checkoutUrl, + productId: env.polar.workerProductId, + benefitId: env.polar.workerBenefitId, + }, + }, 402) + } + } catch (error) { return c.json({ - error: "payment_required", - message: "Additional cloud workers require an active Den Cloud plan.", - polar: { - checkoutUrl: access.checkoutUrl, - productId: env.polar.productId, - benefitId: env.polar.benefitId, - }, - }, 402) + error: "billing_unavailable", + message: error instanceof Error ? error.message : "Worker billing is unavailable.", + }, 503) } } diff --git a/ee/apps/den-api/src/routes/workers/shared.ts b/ee/apps/den-api/src/routes/workers/shared.ts index 996104c51..2870b448f 100644 --- a/ee/apps/den-api/src/routes/workers/shared.ts +++ b/ee/apps/den-api/src/routes/workers/shared.ts @@ -12,7 +12,7 @@ import { } from "@openwork-ee/den-db/schema" import { createDenTypeId, normalizeDenTypeId } from "@openwork-ee/utils/typeid" import { z } from "zod" -import { getCloudWorkerBillingStatus, requireCloudWorkerAccess, setCloudWorkerSubscriptionCancellation } from "../../billing/polar.js" +import { getCloudWorkerBillingStatus, requireAdditionalCloudWorkerAccess, requireCloudWorkerAccess, setCloudWorkerSubscriptionCancellation } from "../../billing/polar.js" import { db } from "../../db.js" import { env } from "../../env.js" import type { UserOrganizationsContext } from "../../middleware/index.js" @@ -281,12 +281,11 @@ export async function fetchWorkerRuntimeJson(input: { return { ok: false as const, status: lastStatus, payload: lastPayload } } -export async function countUserCloudWorkers(userId: UserId) { +export async function countOrgCloudWorkers(orgId: OrgId) { const rows = await db .select({ id: WorkerTable.id }) .from(WorkerTable) - .where(and(eq(WorkerTable.created_by_user_id, userId), eq(WorkerTable.destination, "cloud"))) - .limit(2) + .where(and(eq(WorkerTable.org_id, orgId), eq(WorkerTable.destination, "cloud"))) return rows.length } @@ -379,14 +378,26 @@ export async function continueCloudProvisioning(input: { export async function requireCloudAccessOrPayment(input: { userId: UserId + orgId: OrgId email: string name: string }) { return requireCloudWorkerAccess(input) } +export async function requireAdditionalCloudCapacityOrPayment(input: { + userId: UserId + orgId: OrgId + email: string + name: string + ownedWorkerCount: number +}) { + return requireAdditionalCloudWorkerAccess(input) +} + export async function getWorkerBilling(input: { userId: UserId + orgId: OrgId email: string name: string includeCheckoutUrl: boolean @@ -396,6 +407,7 @@ export async function getWorkerBilling(input: { return getCloudWorkerBillingStatus( { userId: input.userId, + orgId: input.orgId, email: input.email, name: input.name, }, @@ -409,6 +421,7 @@ export async function getWorkerBilling(input: { export async function setWorkerBillingSubscription(input: { userId: UserId + orgId: OrgId email: string name: string cancelAtPeriodEnd: boolean @@ -416,6 +429,7 @@ export async function setWorkerBillingSubscription(input: { return setCloudWorkerSubscriptionCancellation( { userId: input.userId, + orgId: input.orgId, email: input.email, name: input.name, }, diff --git a/ee/apps/den-web/app/(den)/_components/auth-panel.tsx b/ee/apps/den-web/app/(den)/_components/auth-panel.tsx new file mode 100644 index 000000000..a4c845cb9 --- /dev/null +++ b/ee/apps/den-web/app/(den)/_components/auth-panel.tsx @@ -0,0 +1,387 @@ +"use client"; + +import { ArrowRight, CheckCircle2 } from "lucide-react"; +import { usePathname, useRouter } from "next/navigation"; +import { useEffect, useRef, useState, type ReactNode } from "react"; +import { isSamePathname } from "../_lib/client-route"; +import type { AuthMode } from "../_lib/den-flow"; +import { useDenFlow } from "../_providers/den-flow-provider"; + +type PanelContent = { + title: string; + copy: string; + submitLabel: string; + togglePrompt?: string; + toggleActionLabel?: string; +}; + +function getDesktopGrant(url: string | null) { + if (!url) return null; + try { + const parsed = new URL(url); + const grant = parsed.searchParams.get("grant")?.trim() ?? ""; + return grant || null; + } catch { + return null; + } +} + +function GitHubLogo() { + return ( + + ); +} + +function GoogleLogo() { + return ( + + ); +} + +function SocialButton({ + children, + onClick, + disabled, +}: { + children: ReactNode; + onClick: () => void; + disabled: boolean; +}) { + return ( + + ); +} + +export function AuthPanel({ + prefilledEmail, + prefillKey, + initialMode = "sign-up", + lockEmail = false, + hideSocialAuth = false, + hideEmailField = false, + eyebrow = "Account", + signUpContent, + signInContent, + verificationContent, +}: { + prefilledEmail?: string; + prefillKey?: string; + initialMode?: AuthMode; + lockEmail?: boolean; + hideSocialAuth?: boolean; + hideEmailField?: boolean; + eyebrow?: string; + signUpContent?: Partial; + signInContent?: Partial; + verificationContent?: Partial; +}) { + const router = useRouter(); + const pathname = usePathname(); + const prefillRef = useRef(null); + const [copiedDesktopField, setCopiedDesktopField] = useState<"link" | "code" | null>(null); + const { + authMode, + setAuthMode, + email, + setEmail, + password, + setPassword, + verificationCode, + setVerificationCode, + verificationRequired, + authBusy, + authInfo, + authError, + desktopAuthRequested, + desktopRedirectUrl, + desktopRedirectBusy, + showAuthFeedback, + submitAuth, + submitVerificationCode, + resendVerificationCode, + cancelVerification, + beginSocialAuth, + resolveUserLandingRoute, + } = useDenFlow(); + + const resolvedSignUpContent: PanelContent = { + title: "Get started.", + copy: "Free to try. Team plans from $50/mo.", + submitLabel: "Create account", + togglePrompt: "Have an account?", + toggleActionLabel: "Sign in", + ...signUpContent, + }; + + const resolvedSignInContent: PanelContent = { + title: "Welcome back.", + copy: "Sign in to open your team workspace.", + submitLabel: "Sign in", + togglePrompt: "Need an account?", + toggleActionLabel: "Create one", + ...signInContent, + }; + + const resolvedVerificationContent: PanelContent = { + title: "Verify your email.", + copy: "Enter the six-digit code from your inbox.", + submitLabel: "Verify email", + ...verificationContent, + }; + + const desktopGrant = getDesktopGrant(desktopRedirectUrl); + const activeContent = verificationRequired + ? resolvedVerificationContent + : authMode === "sign-in" + ? resolvedSignInContent + : resolvedSignUpContent; + const showLockedEmailSummary = Boolean(prefilledEmail && lockEmail && hideEmailField); + + useEffect(() => { + const key = prefillKey ?? prefilledEmail?.trim() ?? null; + if (!key || prefillRef.current === key) { + return; + } + + prefillRef.current = key; + setAuthMode(initialMode); + setEmail(prefilledEmail?.trim() ?? ""); + setPassword(""); + setVerificationCode(""); + }, [initialMode, prefillKey, prefilledEmail, setAuthMode, setEmail, setPassword, setVerificationCode]); + + const copyDesktopValue = async (field: "link" | "code", value: string | null) => { + if (!value) return; + await navigator.clipboard.writeText(value); + setCopiedDesktopField(field); + window.setTimeout(() => { + setCopiedDesktopField((current) => (current === field ? null : current)); + }, 1800); + }; + + return ( +
+
+

{eyebrow}

+
+

{activeContent.title}

+

{activeContent.copy}

+
+
+ + {desktopAuthRequested ? ( +
+

Finish sign-in here, then jump back into the OpenWork desktop app.

+ {desktopRedirectUrl ? ( +
+
+ + + {desktopGrant ? ( + + ) : null} +
+

+ If OpenWork does not open automatically, copy the sign-in link or one-time code and paste it into the OpenWork desktop app. +

+
+ ) : null} +
+ ) : null} + +
{ + const next = verificationRequired + ? await submitVerificationCode(event) + : await submitAuth(event); + if (next === "dashboard" || next === "join-org") { + const target = await resolveUserLandingRoute(); + if (target && !isSamePathname(pathname, target)) { + router.replace(target); + } + } else if (next === "checkout" && !isSamePathname(pathname, "/checkout")) { + router.replace("/checkout"); + } + }} + > + {!verificationRequired && !hideSocialAuth ? ( + <> + void beginSocialAuth("github")} + disabled={authBusy || desktopRedirectBusy} + > + + Continue with GitHub + + + void beginSocialAuth("google")} + disabled={authBusy || desktopRedirectBusy} + > + + Continue with Google + + + + + ) : null} + + {showLockedEmailSummary ? ( +
+

Invited email

+

{prefilledEmail}

+
+ ) : null} + + {!hideEmailField ? ( + + ) : null} + + {!verificationRequired ? ( + + ) : ( + + )} + + + + {verificationRequired ? ( +
+ + +
+ ) : null} +
+ + {!verificationRequired ? ( +
+

+ {authMode === "sign-in" + ? resolvedSignInContent.togglePrompt + : resolvedSignUpContent.togglePrompt} +

+ +
+ ) : null} + + {showAuthFeedback ? ( +
+

{authInfo}

+ {authError ?

{authError}

: null} + {!authError && verificationRequired ? ( +
+ + Waiting for your verification code +
+ ) : null} +
+ ) : null} +
+ ); +} diff --git a/ee/apps/den-web/app/(den)/_components/auth-screen.tsx b/ee/apps/den-web/app/(den)/_components/auth-screen.tsx index 60de4d313..4b47936ad 100644 --- a/ee/apps/den-web/app/(den)/_components/auth-screen.tsx +++ b/ee/apps/den-web/app/(den)/_components/auth-screen.tsx @@ -1,87 +1,32 @@ "use client"; -import { Dithering, MeshGradient } from "@paper-design/shaders-react"; -import { ArrowRight, CheckCircle2 } from "lucide-react"; +import { PaperMeshGradient } from "@openwork/ui/react"; +import { Dithering } from "@paper-design/shaders-react"; import { usePathname, useRouter } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; import { isSamePathname } from "../_lib/client-route"; import { useDenFlow } from "../_providers/den-flow-provider"; - -function getDesktopGrant(url: string | null) { - if (!url) return null; - try { - const parsed = new URL(url); - const grant = parsed.searchParams.get("grant")?.trim() ?? ""; - return grant || null; - } catch { - return null; - } -} - -function GitHubLogo() { - return ( - - ); -} - -function GoogleLogo() { - return ( - - ); -} +import { AuthPanel } from "./auth-panel"; function FeatureCard({ title, body }: { title: string; body: string }) { return ( -
-

{title}

-

{body}

+
+

{title}

+

{body}

); } -function SocialButton({ - children, - onClick, - disabled, -}: { - children: React.ReactNode; - onClick: () => void; - disabled: boolean; -}) { - return ( - - ); -} - function LoadingPanel({ title, body }: { title: string; body: string }) { return ( -
+
-

- OpenWork Cloud -

-

{title}

-

{body}

+

OpenWork Cloud

+

{title}

+

{body}

-
-
+
+
); @@ -91,45 +36,9 @@ export function AuthScreen() { const router = useRouter(); const pathname = usePathname(); const routingRef = useRef(false); - const [copiedDesktopField, setCopiedDesktopField] = useState<"link" | "code" | null>(null); - const { - authMode, - setAuthMode, - email, - setEmail, - password, - setPassword, - verificationCode, - setVerificationCode, - verificationRequired, - authBusy, - authInfo, - authError, - user, - sessionHydrated, - desktopAuthRequested, - desktopRedirectUrl, - desktopRedirectBusy, - showAuthFeedback, - submitAuth, - submitVerificationCode, - resendVerificationCode, - cancelVerification, - beginSocialAuth, - resolveUserLandingRoute, - } = useDenFlow(); - const desktopGrant = getDesktopGrant(desktopRedirectUrl); + const { user, sessionHydrated, desktopAuthRequested, resolveUserLandingRoute } = useDenFlow(); const hasResolvedSession = sessionHydrated && Boolean(user) && !desktopAuthRequested; - const copyDesktopValue = async (field: "link" | "code", value: string | null) => { - if (!value) return; - await navigator.clipboard.writeText(value); - setCopiedDesktopField(field); - window.setTimeout(() => { - setCopiedDesktopField((current) => (current === field ? null : current)); - }, 1800); - }; - useEffect(() => { if (!hasResolvedSession || routingRef.current) { return; @@ -147,18 +56,6 @@ export function AuthScreen() { }); }, [hasResolvedSession, pathname, resolveUserLandingRoute, router]); - const panelTitle = verificationRequired - ? "Verify your email." - : authMode === "sign-up" - ? "Create your Cloud account." - : "Sign in to Cloud."; - - const panelCopy = verificationRequired - ? "Enter the six-digit code from your inbox to finish setup." - : authMode === "sign-up" - ? "Start with email, GitHub, or Google." - : "Welcome back. Keep your team setup in sync across Cloud and desktop."; - if (!sessionHydrated) { return (
@@ -171,7 +68,7 @@ export function AuthScreen() {
-
+
- - Shared setups + OpenWork Cloud

- Share your OpenWork setup with your team. + One setup, every seat.

- Provision shared setups, invite your org, and keep background workspaces available across Cloud and desktop. + Configure once. Your whole team gets the same tools, agents, and providers.

@@ -219,16 +116,16 @@ export function AuthScreen() {
@@ -240,212 +137,7 @@ export function AuthScreen() { body="We found your account and are sending you to the right Cloud destination now." /> ) : ( -
-
-

- Account -

-

{panelTitle}

-

{panelCopy}

-
- - {desktopAuthRequested ? ( -
- Finish auth here and we'll send you back into the OpenWork desktop app. - {desktopRedirectUrl ? ( -
-
- - - {desktopGrant ? ( - - ) : null} -
-

- If OpenWork does not open automatically, copy the sign-in link or one-time code and paste it into the OpenWork desktop app. -

-
- ) : null} -
- ) : null} - -
{ - const next = verificationRequired - ? await submitVerificationCode(event) - : await submitAuth(event); - if (next === "dashboard" || next === "join-org") { - const target = await resolveUserLandingRoute(); - if (target && !isSamePathname(pathname, target)) { - router.replace(target); - } - } else if (next === "checkout" && !isSamePathname(pathname, "/checkout")) { - router.replace("/checkout"); - } - }} - > - {!verificationRequired ? ( - <> - void beginSocialAuth("github")} - disabled={authBusy || desktopRedirectBusy} - > - - Continue with GitHub - - - void beginSocialAuth("google")} - disabled={authBusy || desktopRedirectBusy} - > - - Continue with Google - - - - - ) : null} - - - - {!verificationRequired ? ( - - ) : ( - - )} - - - - {verificationRequired ? ( -
- - -
- ) : null} -
- - {!verificationRequired ? ( -
-

{authMode === "sign-in" ? "Need an account?" : "Already have an account?"}

- -
- ) : null} - - {showAuthFeedback ? ( -
-

{authInfo}

- {authError ?

{authError}

: null} - {!authError && verificationRequired ? ( -
- - Waiting for your verification code -
- ) : null} -
- ) : null} -
+ )}
diff --git a/ee/apps/den-web/app/(den)/_components/checkout-screen.tsx b/ee/apps/den-web/app/(den)/_components/checkout-screen.tsx index 2fc483bee..566784f63 100644 --- a/ee/apps/den-web/app/(den)/_components/checkout-screen.tsx +++ b/ee/apps/den-web/app/(den)/_components/checkout-screen.tsx @@ -11,31 +11,59 @@ import { useDenFlow } from "../_providers/den-flow-provider"; const MOCK_BILLING = process.env.NEXT_PUBLIC_DEN_MOCK_BILLING === "1"; const MOCK_CHECKOUT_URL = (process.env.NEXT_PUBLIC_DEN_MOCK_CHECKOUT_URL ?? "").trim() || null; -function formatSubscriptionStatus(value: string | null | undefined) { - if (!value) return "Purchase required"; - return value - .split(/[_\s]+/) - .filter(Boolean) - .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1).toLowerCase()) - .join(" "); +function formatRecurringPrice( + price: { amount: number | null; currency: string | null; recurringInterval: string | null } | null | undefined, + fallback: string, +) { + if (!price || price.amount === null || !price.currency) { + return fallback; + } + + const interval = price.recurringInterval === "month" + ? "mo" + : (price.recurringInterval ?? "period"); + + return `${formatMoneyMinor(price.amount, price.currency)}/${interval}`; } function LoadingPanel({ title, body }: { title: string; body: string }) { return ( -
-
-

{title}

-

{body}

+
+
+

OpenWork Cloud

+

{title}

+

{body}

); } +function BulletList({ items }: { items: string[] }) { + return ( +
+ {items.map((item) => ( +
+
+ ))} +
+ ); +} + +function FeatureInsetCard({ title, body }: { title: string; body: string }) { + return ( +
+

{title}

+

{body}

+
+ ); +} + export function CheckoutScreen({ customerSessionToken }: { customerSessionToken: string | null }) { const router = useRouter(); const pathname = usePathname(); const handledReturnRef = useRef(false); - const redirectingRef = useRef(false); const [resuming, setResuming] = useState(false); const [redirectMessage, setRedirectMessage] = useState(null); const { @@ -46,10 +74,9 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken: billingCheckoutBusy, billingError, effectiveCheckoutUrl, - onboardingPending, + effectiveWorkerCheckoutUrl, refreshBilling, refreshCheckoutReturn, - resolveUserLandingRoute, } = useDenFlow(); const mockMode = MOCK_BILLING && process.env.NODE_ENV !== "production"; @@ -59,9 +86,13 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken: featureGateEnabled: true, hasActivePlan: false, checkoutRequired: true, - checkoutUrl: MOCK_CHECKOUT_URL, - portalUrl: null, - price: { amount: 5000, currency: "usd", recurringInterval: "month", recurringIntervalCount: 1 }, + checkoutUrl: MOCK_CHECKOUT_URL, + activeWorkerSubscriptions: 0, + workerCheckoutUrl: MOCK_CHECKOUT_URL, + workerCheckoutRequired: true, + workerPrice: { amount: 5000, currency: "usd", recurringInterval: "month", recurringIntervalCount: 1 }, + portalUrl: null, + price: { amount: 5000, currency: "usd", recurringInterval: "month", recurringIntervalCount: 1 }, subscription: null, invoices: [], productId: null, @@ -110,7 +141,11 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken: return; } - if (!billingSummary?.hasActivePlan && !effectiveCheckoutUrl && !billingBusy && !billingCheckoutBusy) { + const needsCheckoutUrl = billingSummary?.hasActivePlan + ? !effectiveWorkerCheckoutUrl + : !effectiveCheckoutUrl; + + if (needsCheckoutUrl && !billingBusy && !billingCheckoutBusy) { void refreshBilling({ includeCheckout: true, quiet: true }); } }, [ @@ -118,38 +153,18 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken: billingCheckoutBusy, billingSummary?.hasActivePlan, effectiveCheckoutUrl, + effectiveWorkerCheckoutUrl, refreshBilling, resuming, sessionHydrated, user, ]); - useEffect(() => { - if (!sessionHydrated || !user || resuming || onboardingPending || mockMode || redirectingRef.current) { - return; - } - - redirectingRef.current = true; - void resolveUserLandingRoute() - .then((target) => { - if (target && !isSamePathname(pathname, target)) { - setRedirectMessage("Redirecting to your workspace..."); - router.replace(target); - return; - } - - setRedirectMessage(null); - }) - .finally(() => { - redirectingRef.current = false; - }); - }, [mockMode, onboardingPending, pathname, resolveUserLandingRoute, resuming, router, sessionHydrated, user]); - if (!sessionHydrated || (!user && !mockMode)) { return ( ); } @@ -159,14 +174,55 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken: } const billingPrice = billingSummary?.price ?? null; + const workerBillingPrice = billingSummary?.workerPrice ?? null; const showLoading = resuming || (billingBusy && !billingSummary && !MOCK_BILLING); - const checkoutHref = effectiveCheckoutUrl ?? MOCK_CHECKOUT_URL ?? null; - const planAmountLabel = - billingPrice && billingPrice.amount !== null - ? `${formatMoneyMinor(billingPrice.amount, billingPrice.currency)}/${billingPrice.recurringInterval}` - : "$50.00/month"; - const subscription = billingSummary?.subscription ?? null; - const subscriptionStatus = formatSubscriptionStatus(subscription?.status); + const isWorkerCheckout = billingSummary?.hasActivePlan === true; + const checkoutHref = isWorkerCheckout + ? effectiveWorkerCheckoutUrl ?? MOCK_CHECKOUT_URL ?? null + : effectiveCheckoutUrl ?? MOCK_CHECKOUT_URL ?? null; + const planAmountLabel = formatRecurringPrice(billingPrice, "$50/mo"); + const workerAmountLabel = formatRecurringPrice(workerBillingPrice, "$50/mo"); + + const heroTitle = isWorkerCheckout ? "Add cloud compute." : "Your team, one click away."; + const heroCopy = isWorkerCheckout + ? "Your team plan is active. Add capacity when you need more always-on work." + : "From $50/mo for up to 5 people. Add cloud compute when you need it."; + const primaryCtaLabel = checkoutHref + ? (isWorkerCheckout ? `Add cloud compute — ${workerAmountLabel}` : `Start team plan — ${planAmountLabel}`) + : (isWorkerCheckout ? "Refresh cloud compute link" : "Refresh checkout link"); + const metaItems = isWorkerCheckout + ? ["Per worker add-on", "Billed monthly", user?.email ?? "Signed in"] + : ["5 seats included", "Billed monthly", user?.email ?? "Signed in"]; + + const primarySurface = isWorkerCheckout + ? { + kicker: "Cloud compute", + title: "More room for always-on work.", + copy: "Your team plan is live. Add capacity for cloud agents when workloads grow.", + bullets: [ + "Scale compute one worker at a time", + "Keep always-on workflows moving", + "Add capacity only when the team needs it", + ], + cards: [ + { title: "Per worker", body: `${workerAmountLabel} billed monthly.` }, + { title: "Cloud agents", body: "Always-on workflows for the org. In alpha." }, + ], + } + : { + kicker: "Team plan", + title: "Everyone on the same page.", + copy: "Push one config to your whole org. Add cloud agents or your own models whenever you're ready.", + bullets: [ + "Shared config and tools across seats", + "Cloud agents that run while you sleep", + "Bring your own LLM provider soon", + ], + cards: [ + { title: "Cloud agents", body: "Always-on workflows for the org. In alpha." }, + { title: "Your models", body: "Connect your provider when the team is ready." }, + ], + }; return (
@@ -174,16 +230,14 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken:

OpenWork Cloud

-

Purchase worker access before launch.

-

- Workers are disabled by default. Add one hosted OpenWork worker for $50/month, then launch it from your dashboard. -

+

{heroTitle}

+

{heroCopy}

{checkoutHref ? ( - Purchase worker — $50/month + {primaryCtaLabel} ) : ( )} - - Use desktop only - + + {isWorkerCheckout ? ( + billingSummary?.portalUrl ? ( + + Billing portal + + ) : null + ) : ( + + Stay on desktop + + )}
-
- $50/month per worker - - {planAmountLabel} billed monthly - - {user?.email ?? "Signed in"} +
+ {metaItems.map((item, index) => ( + + {index > 0 ? : null} + {item} + + ))}
@@ -218,119 +282,46 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken: ) : null} {billingSummary ? ( -
-
-
-
- OpenWork Cloud -

Share your setup across your team.

-

- Manage your team's setup, invite teammates, and keep everything in sync. -

-
- -
-
Share setup across your team and org
-
Background agents in alpha for selected workflows
-
Custom LLM providers for teams, coming soon
-
- -
-
-

Background agents

-

- Keep selected workflows running in the background. Alpha. -

-
-
-

Custom LLM providers

-

- Standardize provider access for your team. Coming soon. -

-
-
-
- -
-
- Desktop app -

Stay local when you need to.

-

- Run locally for free, keep your data on your machine, and add OpenWork Cloud when your team is ready. -

-
- -
-
Run locally for free
-
Keep data on your machine
-
Move into OpenWork Cloud later
-
- - -
-
- - +
) : null}
diff --git a/ee/apps/den-web/app/(den)/_components/dashboard-screen.tsx b/ee/apps/den-web/app/(den)/_components/dashboard-screen.tsx index 1ecf7e9e3..b825d5f23 100644 --- a/ee/apps/den-web/app/(den)/_components/dashboard-screen.tsx +++ b/ee/apps/den-web/app/(den)/_components/dashboard-screen.tsx @@ -190,6 +190,7 @@ export function DashboardScreen({ showSidebar = true }: { showSidebar?: boolean isSelectedWorkerFailed, ownedWorkerCount, billingSummary, + effectiveWorkerCheckoutUrl, refreshWorkers, checkWorkerStatus, generateWorkerToken, @@ -234,6 +235,8 @@ export function DashboardScreen({ showSidebar = true }: { showSidebar?: boolean const webDisabled = !openworkAppConnectUrl || !isReady; const desktopDisabled = !openworkDeepLink || !isReady; const showConnectionHint = !openworkDeepLink || !hasWorkspaceScopedUrl; + const workerAllowance = billingSummary?.activeWorkerSubscriptions ?? 0; + const workerCapacityRemaining = Math.max(workerAllowance - ownedWorkerCount, 0); const mainContent = (
@@ -557,16 +560,37 @@ export function DashboardScreen({ showSidebar = true }: { showSidebar?: boolean

{billingSummary?.featureGateEnabled ? billingSummary.hasActivePlan - ? "Your account has active worker billing." - : "Workers stay disabled until you purchase one for $50/month." + ? workerAllowance > 0 + ? `${workerAllowance} worker subscription${workerAllowance === 1 ? "" : "s"} active for this org. ${workerCapacityRemaining} remaining.` + : "Your team plan is active, but no workers are included for this org by default." + : "Activate the base team plan before purchasing workers." : "Billing gates are disabled in this environment."}

- - Open billing - + {billingSummary?.hasActivePlan ? ( + effectiveWorkerCheckoutUrl ? ( + + Purchase worker + + ) : ( + + Open billing + + ) + ) : ( + + Open billing + + )}
@@ -575,13 +599,23 @@ export function DashboardScreen({ showSidebar = true }: { showSidebar?: boolean

No workers yet

-

Purchase your first worker to unlock connection details and runtime controls.

- - Purchase worker billing - +

Your base plan includes 0 workers. Purchase one for $50/month to unlock hosted runtime controls.

+ {effectiveWorkerCheckoutUrl ? ( + + Purchase first worker + + ) : ( + + Open billing + + )}
)} @@ -666,8 +700,8 @@ export function DashboardScreen({ showSidebar = true }: { showSidebar?: boolean Signed in as {user.email}
{billingSummary?.featureGateEnabled && !billingSummary.hasActivePlan - ? "Purchase required before the next launch." - : `${ownedWorkerCount} worker${ownedWorkerCount === 1 ? "" : "s"} in your account.`} + ? "Base plan required before the next launch." + : `${ownedWorkerCount} worker${ownedWorkerCount === 1 ? "" : "s"} in this org · ${workerAllowance} purchased.`}
diff --git a/ee/apps/den-web/app/(den)/_components/join-org-screen.tsx b/ee/apps/den-web/app/(den)/_components/join-org-screen.tsx index 34e70e321..e615f956d 100644 --- a/ee/apps/den-web/app/(den)/_components/join-org-screen.tsx +++ b/ee/apps/den-web/app/(den)/_components/join-org-screen.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { getErrorMessage, requestJson } from "../_lib/den-flow"; import { PENDING_ORG_INVITATION_STORAGE_KEY, @@ -13,17 +13,20 @@ import { type DenInvitationPreview, } from "../_lib/den-org"; import { useDenFlow } from "../_providers/den-flow-provider"; +import { AuthPanel } from "./auth-panel"; function LoadingCard({ title, body }: { title: string; body: string }) { return ( -
-

OpenWork Cloud

-
-

{title}

-

{body}

-
-
-
+
+
+

OpenWork Cloud

+
+

{title}

+

{body}

+
+
+
+
); @@ -32,13 +35,13 @@ function LoadingCard({ title, body }: { title: string; body: string }) { function statusMessage(preview: DenInvitationPreview | null) { switch (preview?.invitation.status) { case "accepted": - return "This invitation has already been accepted."; + return "This invite has already been used."; case "canceled": - return "This invitation has been canceled."; + return "This invite was canceled."; case "expired": - return "This invitation has expired."; + return "This invite expired."; default: - return "This invitation is no longer available."; + return "This invite is no longer available."; } } @@ -51,25 +54,10 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) { const [joinBusy, setJoinBusy] = useState(false); const [joinError, setJoinError] = useState(null); - const signUpHref = useMemo(() => { - if (!invitationId) { - return "/?mode=sign-up"; - } - - return `/?mode=sign-up&invite=${encodeURIComponent(invitationId)}`; - }, [invitationId]); - - const signInHref = useMemo(() => { - if (!invitationId) { - return "/?mode=sign-in"; - } - - return `/?mode=sign-in&invite=${encodeURIComponent(invitationId)}`; - }, [invitationId]); - const invitedEmailMatches = preview && user ? preview.invitation.email.trim().toLowerCase() === user.email.trim().toLowerCase() : false; + const roleLabel = preview ? formatRoleLabel(preview.invitation.role) : ""; useEffect(() => { let cancelled = false; @@ -102,7 +90,7 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) { } setPreview(null); - setPreviewError(getErrorMessage(payload, response.status === 404 ? "This invitation is no longer available." : `Could not load the invitation (${response.status}).`)); + setPreviewError(getErrorMessage(payload, response.status === 404 ? "This invite is no longer available." : `Could not load the invite (${response.status}).`)); return; } @@ -117,7 +105,7 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) { } catch (error) { if (!cancelled) { setPreview(null); - setPreviewError(error instanceof Error ? error.message : "Could not load the invitation."); + setPreviewError(error instanceof Error ? error.message : "Could not load the invite."); } } finally { if (!cancelled) { @@ -153,7 +141,7 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) { ); if (!response.ok) { - setJoinError(getErrorMessage(payload, response.status === 404 ? "This invitation could not be accepted." : `Could not join the organization (${response.status}).`)); + setJoinError(getErrorMessage(payload, response.status === 404 ? "This invite could not be accepted." : `Could not join the organization (${response.status}).`)); return; } @@ -173,26 +161,87 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) { async function handleSwitchAccount() { await signOut(); + if (typeof window !== "undefined" && invitationId) { + window.sessionStorage.setItem(PENDING_ORG_INVITATION_STORAGE_KEY, invitationId); + } router.replace(getJoinOrgRoute(invitationId)); } if (!sessionHydrated || previewBusy) { - return ; + return ; } if (!preview) { return ( -
-
-

OpenWork Cloud

-

Invitation unavailable.

-

{previewError ?? "This invitation could not be loaded."}

+
+
+
+

OpenWork Cloud

+

This invite can't be opened.

+

{previewError ?? "This invite could not be loaded."}

+
+
+ + Back to OpenWork Cloud + +
-
- - Back to OpenWork Cloud - +
+ ); + } + + if (preview.invitation.status === "pending" && !user) { + return ( +
+
+
+

OpenWork Cloud

+
+

You've been invited to

+

{preview.organization.name}

+
+
+ +
+ Role · {roleLabel} +
+ +
+

+ Your team is already set up and waiting. +

+

Member access is ready as soon as you join.

+
+ +
); } @@ -200,80 +249,75 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) { const showAcceptAction = preview.invitation.status === "pending" && Boolean(user) && invitedEmailMatches; return ( -
-
-

OpenWork Cloud

-
-

You've been invited to

-

{preview.organization.name}

+
+
+
+

OpenWork Cloud

+
+

You've been invited to

+

{preview.organization.name}

+
-

Role: {formatRoleLabel(preview.invitation.role)}

-
- {user ? ( -
- Signed in as {user.email} +
+ Role · {roleLabel} + {user ? {user.email} : null}
- ) : null} - {preview.invitation.status !== "pending" ? ( -
-

{statusMessage(preview)}

-
- - {user && invitedEmailMatches ? "Open organization" : "Back to OpenWork Cloud"} - + {user ? ( +
+

Signed in as

+

{user.email}

-
- ) : !user ? ( -
-

Create an account or sign in first, then come back here to confirm the invitation.

-
- - Create account to continue - - - Sign in instead - + ) : null} + + {preview.invitation.status !== "pending" ? ( +
+

{statusMessage(preview)}

+
+ + {user && invitedEmailMatches ? "Open team" : "Back to OpenWork Cloud"} + +
-
- ) : !invitedEmailMatches ? ( -
-

- This invite was sent to {preview.invitation.email}. Sign in with that email to join the organization. -

-
- + ) : !invitedEmailMatches ? ( +
+

+ This invite is for {preview.invitation.email}. Switch accounts to continue. +

+
+ +
-
- ) : ( -
-

Click to join

-
- + ) : ( +
+

You're one click away from the team workspace.

+
+ +
-
- )} + )} - {joinError ?

{joinError}

: null} - {previewError ?

{previewError}

: null} + {joinError ?
{joinError}
: null} + {previewError ?
{previewError}
: null} +
); } diff --git a/ee/apps/den-web/app/(den)/_lib/den-flow.ts b/ee/apps/den-web/app/(den)/_lib/den-flow.ts index 8a98f2406..fba692def 100644 --- a/ee/apps/den-web/app/(den)/_lib/den-flow.ts +++ b/ee/apps/den-web/app/(den)/_lib/den-flow.ts @@ -43,6 +43,10 @@ export type BillingSummary = { hasActivePlan: boolean; checkoutRequired: boolean; checkoutUrl: string | null; + activeWorkerSubscriptions: number; + workerCheckoutUrl: string | null; + workerCheckoutRequired: boolean; + workerPrice: BillingPrice | null; portalUrl: string | null; price: BillingPrice | null; subscription: BillingSubscription | null; @@ -593,6 +597,10 @@ export function getBillingSummary(payload: unknown): BillingSummary | null { hasActivePlan, checkoutRequired, checkoutUrl: typeof billing.checkoutUrl === "string" ? billing.checkoutUrl : null, + activeWorkerSubscriptions: typeof billing.activeWorkerSubscriptions === "number" ? billing.activeWorkerSubscriptions : 0, + workerCheckoutUrl: typeof billing.workerCheckoutUrl === "string" ? billing.workerCheckoutUrl : null, + workerCheckoutRequired: billing.workerCheckoutRequired === true, + workerPrice: getBillingPrice(billing.workerPrice), portalUrl: typeof billing.portalUrl === "string" ? billing.portalUrl : null, price: getBillingPrice(billing.price), subscription: getBillingSubscription(billing.subscription), diff --git a/ee/apps/den-web/app/(den)/_providers/den-flow-provider.tsx b/ee/apps/den-web/app/(den)/_providers/den-flow-provider.tsx index 9ca62a29e..561c0736c 100644 --- a/ee/apps/den-web/app/(den)/_providers/den-flow-provider.tsx +++ b/ee/apps/den-web/app/(den)/_providers/den-flow-provider.tsx @@ -96,6 +96,7 @@ type DenFlowContextValue = { billingSubscriptionBusy: boolean; billingError: string | null; effectiveCheckoutUrl: string | null; + effectiveWorkerCheckoutUrl: string | null; refreshBilling: (options?: { includeCheckout?: boolean; quiet?: boolean }) => Promise; handleSubscriptionCancellation: (cancelAtPeriodEnd: boolean) => Promise; refreshCheckoutReturn: (sessionTokenPresent: boolean) => Promise; @@ -266,17 +267,18 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { activeWorker?.workerName ?? null, { autoConnect: true } ); - const ownedWorkerCount = workers.filter((item) => item.isMine).length; + const ownedWorkerCount = workers.length; + const workerAllowance = billingSummary?.activeWorkerSubscriptions ?? 0; const additionalWorkerNeedsPlan = Boolean( user && - ownedWorkerCount > 0 && billingSummary?.featureGateEnabled && - !billingSummary.hasActivePlan + ownedWorkerCount >= workerAllowance ); const selectedWorkerStatus = activeWorker?.status ?? selectedWorker?.status ?? "unknown"; const selectedStatusMeta = getWorkerStatusMeta(selectedWorkerStatus); const isSelectedWorkerFailed = selectedWorkerStatus.trim().toLowerCase() === "failed"; const effectiveCheckoutUrl = checkoutUrl ?? billingSummary?.checkoutUrl ?? null; + const effectiveWorkerCheckoutUrl = checkoutUrl ?? billingSummary?.workerCheckoutUrl ?? null; const onboardingPending = Boolean(onboardingIntent?.shouldLaunch && !onboardingIntent.completed); const onboardingDecisionBusy = onboardingPending && !billingLoadedOnce && (billingBusy || billingCheckoutBusy || !sessionHydrated); @@ -342,6 +344,14 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { }); } + function shouldRouteToCheckout(summary: BillingSummary | null | undefined) { + if (!summary) { + return false; + } + + return summary.featureGateEnabled && summary.checkoutRequired && !summary.hasActivePlan; + } + function setAuthMode(mode: AuthMode) { setAuthModeState(mode); setVerificationRequired(false); @@ -1055,11 +1065,8 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { persistOnboardingIntent(intent); const summary = await refreshBilling({ includeCheckout: true, quiet: true }); - if (!summary) { - return "checkout" as const; - } - return !summary.featureGateEnabled || summary.hasActivePlan ? ("dashboard" as const) : ("checkout" as const); + return shouldRouteToCheckout(summary) ? ("checkout" as const) : ("dashboard" as const); } async function resolveUserLandingRoute() { @@ -1082,11 +1089,7 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { billingSummary ?? (billingBusy || billingCheckoutBusy ? null : await refreshBilling({ includeCheckout: true, quiet: true })); - if (!summary) { - return "/checkout"; - } - - return !summary.featureGateEnabled || summary.hasActivePlan ? (dashboardRoute ?? "/") : "/checkout"; + return shouldRouteToCheckout(summary) ? "/checkout" : (dashboardRoute ?? "/"); } async function submitAuth(event: FormEvent) { @@ -1324,13 +1327,12 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { return current; } - return { - ...current, - hasActivePlan: false, - checkoutRequired: true, - checkoutUrl: url ?? current.checkoutUrl - }; - }); + return { + ...current, + workerCheckoutRequired: true, + workerCheckoutUrl: url ?? current.workerCheckoutUrl + }; + }); setLaunchStatus("Payment is required. Complete checkout and return to continue launch."); setLaunchError(url ? null : "Checkout URL missing from paywall response."); appendEvent("warning", "Paywall required", url ? "Checkout URL generated" : "Checkout URL missing"); @@ -1743,15 +1745,10 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { } const summary = await refreshBilling({ includeCheckout: false, quiet: true }); - if (!summary) { - return "/checkout" as const; - } - - if (!summary.featureGateEnabled || summary.hasActivePlan) { - return (await resolveDashboardRoute()) ?? "/"; - } - return "/checkout" as const; + return shouldRouteToCheckout(summary) + ? ("/checkout" as const) + : ((await resolveDashboardRoute()) ?? "/"); } function selectWorker(item: WorkerListItem) { @@ -2015,7 +2012,7 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { return; } - if (ownedWorkerCount > 0) { + if (ownedWorkerCount >= workerAllowance) { markOnboardingComplete(); return; } @@ -2031,7 +2028,7 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { onboardingAutoLaunchKeyRef.current = autoLaunchKey; markOnboardingComplete(); - }, [billingSummary?.featureGateEnabled, billingSummary?.hasActivePlan, launchBusy, onboardingIntent?.workerName, onboardingPending, ownedWorkerCount, user?.id]); + }, [billingSummary?.featureGateEnabled, billingSummary?.hasActivePlan, billingSummary?.activeWorkerSubscriptions, launchBusy, onboardingIntent?.workerName, onboardingPending, ownedWorkerCount, workerAllowance, user?.id]); useEffect(() => { if (!user) { @@ -2078,6 +2075,7 @@ export function DenFlowProvider({ children }: { children: ReactNode }) { billingSubscriptionBusy, billingError, effectiveCheckoutUrl, + effectiveWorkerCheckoutUrl, refreshBilling, handleSubscriptionCancellation, refreshCheckoutReturn, diff --git a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/background-agents-screen.tsx b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/background-agents-screen.tsx index 9d3f00dc5..4fbe577bd 100644 --- a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/background-agents-screen.tsx +++ b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/background-agents-screen.tsx @@ -16,7 +16,8 @@ import { Plus, RefreshCw, } from "lucide-react"; -import { Dithering, MeshGradient } from "@paper-design/shaders-react"; +import { PaperMeshGradient } from "@openwork/ui/react"; +import { Dithering } from "@paper-design/shaders-react"; import { OPENWORK_APP_CONNECT_BASE_URL, buildOpenworkAppConnectUrl, @@ -406,7 +407,7 @@ export function BackgroundAgentsScreen() { colorFront="#FEFEFE" style={{ backgroundColor: "#23301C", width: "100%", height: "100%" }} > -

{billingSummary?.hasActivePlan - ? `This workspace's plan is currently ${statusLabel.toLowerCase()} and renews on ${nextBillingDate}.` - : "Workers are $50/month each. Purchase a worker to enable hosted launches for your team."} + ? `This organization's plan is currently ${statusLabel.toLowerCase()} and renews on ${nextBillingDate}.` + : "Start your OpenWork Cloud base plan when your team is ready to share templates and cloud workflows."}

@@ -103,17 +114,34 @@ export function BillingDashboardScreen() {
-

Plan cost

+

Base plan cost

{planAmountLabel}
-

Next billing date

+

Included seats

+
5 seats
+
+ +
+

Purchased workers

+
{workerSubscriptionCount}
+
+ +
+

Worker add-on price

+
+ {workerPrice ? `${formatMoneyMinor(workerPrice.amount, workerPrice.currency)} · ${formatRecurringInterval(workerPrice.recurringInterval, workerPrice.recurringIntervalCount)}` : "$50.00 · month"} +
+
+ +
+

Base plan renews

{nextBillingDate}
-

Next payment amount

+

Estimated monthly total

{nextPaymentAmount}
@@ -144,7 +172,17 @@ export function BillingDashboardScreen() { rel="noreferrer" className="rounded-full bg-gray-900 px-5 py-2.5 text-[14px] font-medium text-white transition-colors hover:bg-gray-800" > - Purchase worker + Purchase base plan + + ) : null} + + {billingSummary?.hasActivePlan && billingSummary?.workerCheckoutUrl ? ( + + Purchase worker add-on ) : null} @@ -184,27 +222,6 @@ export function BillingDashboardScreen() {
-
-

Pricing

-
-
-

Solo

-

$0

-

Free forever · open source

-
-
-

Cloud worker

-

$50/month

-

Per worker · 5 seats included

-
-
-

Enterprise

-

Custom

-

Windows included · talk to us

-
-
-
-

Invoices

diff --git a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/custom-llm-providers-screen.tsx b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/custom-llm-providers-screen.tsx index 74b53998b..764ec31ee 100644 --- a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/custom-llm-providers-screen.tsx +++ b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/custom-llm-providers-screen.tsx @@ -1,7 +1,8 @@ "use client"; import { Cpu } from "lucide-react"; -import { Dithering, MeshGradient } from "@paper-design/shaders-react"; +import { PaperMeshGradient } from "@openwork/ui/react"; +import { Dithering } from "@paper-design/shaders-react"; const comingSoonItems = [ "Standardize provider access across your team.", @@ -25,7 +26,7 @@ export function CustomLlmProvidersScreen() { colorFront="#FEFEFE" style={{ backgroundColor: "#1C2A30", width: "100%", height: "100%" }} > -
-
+

Pricing

+

+ Start solo for free. Purchase Windows support when you need it. OpenWork Cloud starts at 5 seats, then add hosted workers separately when you want runtime. Talk to us for enterprise licensing. +

- +
diff --git a/ee/apps/landing/components/den-hero.tsx b/ee/apps/landing/components/den-hero.tsx index 874fb1027..60d16f75a 100644 --- a/ee/apps/landing/components/den-hero.tsx +++ b/ee/apps/landing/components/den-hero.tsx @@ -14,7 +14,9 @@ export function DenHero(props: DenHeroProps) { Agents that never sleep

- Cloud gives you a personal cloud workspace for long-running tasks, background automation, and the same agent workflows you already use locally in OpenWork, without keeping your own machine awake. + OpenWork Cloud gives your team a shared workspace for long-running tasks, + background automation, and the same agent workflows you already use locally, + with hosted workers added only when you need runtime.

@@ -27,8 +29,8 @@ export function DenHero(props: DenHeroProps) { Get started
- $50/mo per worker - Free for a limited time + $50/mo for 5 seats + $50/mo per worker add-on
diff --git a/ee/apps/landing/components/landing-home.tsx b/ee/apps/landing/components/landing-home.tsx index 48d3eb48d..6e337d7b6 100644 --- a/ee/apps/landing/components/landing-home.tsx +++ b/ee/apps/landing/components/landing-home.tsx @@ -111,9 +111,9 @@ export function LandingHome(props: Props) {
Solo free forever - Workers $50/month + Windows support $99/year - Enterprise talk to us + Cloud $50/month for 5 seats
@@ -269,8 +269,8 @@ export function LandingHome(props: Props) {

Hosted sandboxed workers

- Workers are disabled by default. Purchase one for $50/month when - you need hosted runtime. + Cloud starts at $50/month for 5 seats. Workers are disabled by + default and added separately for $50/month each.

Learn more diff --git a/ee/apps/landing/components/pricing-grid.tsx b/ee/apps/landing/components/pricing-grid.tsx index 459f1be2c..5adc34895 100644 --- a/ee/apps/landing/components/pricing-grid.tsx +++ b/ee/apps/landing/components/pricing-grid.tsx @@ -1,6 +1,6 @@ "use client"; -import { ArrowUpRight, Cloud, Download, Shield, CornerRightDown } from "lucide-react"; +import { ArrowUpRight, Cloud, Download, Monitor, Shield } from "lucide-react"; import { ResponsiveGrain } from "./responsive-grain"; type PricingGridProps = { @@ -27,11 +27,9 @@ type PricingCard = { function PricingCardView({ card }: { card: PricingCard }) { return ( -
- {/* ── Header card ── */} -
- {/* Shader layer — hidden by default, revealed on hover */} -
+
+
+
-
+
-
+

{card.title}

{card.isCustomPricing ? ( -
{card.price}
+
{card.price}
) : (
- {card.price} - + {card.price} + {card.priceSub}
@@ -67,7 +65,7 @@ function PricingCardView({ card }: { card: PricingCard }) { {card.ctaLabel} @@ -75,7 +73,6 @@ function PricingCardView({ card }: { card: PricingCard }) {
- {/* ── Features list ── */}
{card.features.map((feature, idx) => { @@ -83,9 +80,9 @@ function PricingCardView({ card }: { card: PricingCard }) { return (
- + {feature.text}
); @@ -93,7 +90,6 @@ function PricingCardView({ card }: { card: PricingCard }) {
- {/* ── Footer ── */}
{card.footer}
@@ -121,19 +117,37 @@ export function PricingGrid(props: PricingGridProps) { gradientShape: "wave", }, { - id: "cloud-workers", - title: "Cloud workers", + id: "windows-support", + title: "Windows support", + price: "$99", + priceSub: "per year · 1 seat", + ctaLabel: "Purchase Windows support", + href: props.windowsCheckoutUrl, + external: /^https?:\/\//.test(props.windowsCheckoutUrl), + features: [ + { text: "1 Windows seat", icon: Monitor }, + { text: "Binary access", icon: Monitor }, + { text: "1 year of updates", icon: Monitor }, + ], + footer: "Manual fulfillment in phase one", + gradientColors: ["#7C3AED", "#E11D48", "#9333EA", "#1F2937"], + gradientBack: "#111827", + gradientShape: "corners", + }, + { + id: "cloud-teams", + title: "Cloud teams", price: "$50", - priceSub: "per month · per worker", - ctaLabel: "Purchase worker", + priceSub: "per month · 5 seats", + ctaLabel: "Start cloud plan", href: "https://app.openworklabs.com/checkout", external: true, features: [ { text: "5 seats included", icon: Cloud }, - { text: "Hosted OpenWork worker", icon: Cloud }, + { text: "0 workers included by default", icon: Cloud }, { text: "$50 per additional worker", icon: Cloud }, ], - footer: "Workers disabled by default", + footer: "Base plan first, then add worker capacity as needed", gradientColors: ["#2563EB", "#0284C7", "#0EA5E9", "#0F172A"], gradientBack: "#0C1220", gradientShape: "ripple", @@ -163,22 +177,30 @@ export function PricingGrid(props: PricingGridProps) {
{props.showHeader !== false ? (
-

- Pricing -

+
+
+ Pricing +
+

+ Gray by default. Clear when you hover. +

+
+

+ Solo stays free forever. Windows is annual. Cloud starts at 5 seats, and workers are added separately. Enterprise starts with a conversation. +

) : null} -
+
{cards.map((card) => ( -
+
))}

- Prices exclude taxes. + Prices exclude taxes. Windows delivery is manual in phase one.

); diff --git a/ee/apps/landing/components/responsive-grain.tsx b/ee/apps/landing/components/responsive-grain.tsx index 68910f30e..f76af2ca5 100644 --- a/ee/apps/landing/components/responsive-grain.tsx +++ b/ee/apps/landing/components/responsive-grain.tsx @@ -1,9 +1,9 @@ "use client"; -import { GrainGradient } from "@paper-design/shaders-react"; -import { useEffect, useRef, useState } from "react"; +import { PaperGrainGradient } from "@openwork/ui/react"; type Props = { + seed?: string; colors: string[]; colorBack: string; softness: number; @@ -15,43 +15,17 @@ type Props = { }; export function ResponsiveGrain(props: Props) { - const containerRef = useRef(null); - const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); - - useEffect(() => { - if (!containerRef.current) return; - - const observer = new ResizeObserver((entries) => { - for (const entry of entries) { - setDimensions({ - width: entry.contentRect.width, - height: entry.contentRect.height - }); - } - }); - - observer.observe(containerRef.current); - return () => observer.disconnect(); - }, []); - return ( -
- {dimensions.width > 0 && dimensions.height > 0 ? ( - - ) : null} -
+ seed={props.seed} + colors={props.colors} + colorBack={props.colorBack} + softness={props.softness} + intensity={props.intensity} + noise={props.noise} + shape={props.shape} + speed={props.speed} + /> ); } diff --git a/ee/apps/landing/next.config.js b/ee/apps/landing/next.config.js index f0c4686bc..3bd3d6380 100644 --- a/ee/apps/landing/next.config.js +++ b/ee/apps/landing/next.config.js @@ -5,6 +5,7 @@ const mintlifyOrigin = "https://differentai.mintlify.dev"; const nextConfig = { reactStrictMode: true, + transpilePackages: ["@openwork/ui"], async rewrites() { return [ { diff --git a/ee/apps/landing/package.json b/ee/apps/landing/package.json index b15973bc0..c7ef53502 100644 --- a/ee/apps/landing/package.json +++ b/ee/apps/landing/package.json @@ -4,12 +4,13 @@ "version": "0.0.0", "scripts": { "dev": "OPENWORK_DEV_MODE=1 next dev --hostname 0.0.0.0", + "prebuild": "pnpm --dir ../../../packages/ui build", "build": "next build", "start": "next start --hostname 0.0.0.0", "lint": "next lint" }, "dependencies": { - "@paper-design/shaders-react": "0.0.71", + "@openwork/ui": "workspace:*", "botid": "^1.5.11", "framer-motion": "^12.35.1", "lucide-react": "^0.577.0", diff --git a/package.json b/package.json index 9da017288..d14d067f2 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev:windows": ".\\scripts\\dev-windows.cmd", "dev:windows:x64": ".\\scripts\\dev-windows.cmd x64", "dev:ui": "OPENWORK_DEV_MODE=1 pnpm --filter @openwork/app dev", + "dev:ui-demo": "pnpm --filter @openwork/ui-demo dev", "dev:story": "OPENWORK_DEV_MODE=1 pnpm --filter @openwork/story-book dev", "dev:web": "OPENWORK_DEV_MODE=1 pnpm --filter @openwork-ee/den-web dev", "dev:web-local": "pnpm dev:den-local", diff --git a/packages/ui/README.md b/packages/ui/README.md new file mode 100644 index 000000000..4c21a033c --- /dev/null +++ b/packages/ui/README.md @@ -0,0 +1,39 @@ +# @openwork/ui + +Shared UI primitives for OpenWork apps. + +This package intentionally ships two framework-specific entrypoints: + +- `@openwork/ui/react` for React apps like `ee/apps/den-web` +- `@openwork/ui/solid` for Solid apps like `apps/app` + +The public API should stay aligned across both entrypoints. If you add a new component, add both implementations in the same task unless there is a documented blocker. + +## Paper components + +The first shared components live under the `paper` namespace and wrap Paper Design shaders with OpenWork-specific defaults and deterministic seed support. + +Current components: + +- `PaperMeshGradient` +- `PaperGrainGradient` + +Both accept a `seed` prop. Pass a TypeID-like string such as `om_01kmhbscaze02vp04ykqa4tcsb` and the component will deterministically derive colors and shader params from it. The same seed always produces the same result. + +Explicit props still work and override the seeded values, so the merge order is: + +1. OpenWork defaults +2. Seed-derived values from `seed` +3. Explicit props passed by the caller + +## Layout convention + +These components default to `fill={true}`, which means they render at `width: 100%` and `height: 100%`. Put them inside a sized container and they will fill it without needing manual width or height props. + +## Agent notes + +- Shared seed logic lives in `src/common/paper.ts` +- React wrappers live in `src/react/paper/*` +- Solid wrappers live in `src/solid/paper/*` +- Keep the framework prop names aligned unless there is a hard runtime mismatch +- Prefer extending the existing seed helpers instead of inventing per-app one-off shader configs diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 000000000..efedcd9c8 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,38 @@ +{ + "name": "@openwork/ui", + "private": true, + "type": "module", + "exports": { + "./react": { + "types": "./src/react/index.ts", + "development": "./src/react/index.ts", + "default": "./src/react/index.ts" + }, + "./solid": { + "types": "./src/solid/index.ts", + "development": "./src/solid/index.ts", + "default": "./src/solid/index.ts" + } + }, + "files": [ + "dist", + "src", + "README.md" + ], + "scripts": { + "build": "tsup" + }, + "dependencies": { + "@paper-design/shaders": "0.0.72", + "@paper-design/shaders-react": "0.0.72" + }, + "peerDependencies": { + "react": "^18 || ^19", + "solid-js": "^1.9.0" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "tsup": "^8.5.0", + "typescript": "^5.6.3" + } +} diff --git a/packages/ui/src/common/paper.ts b/packages/ui/src/common/paper.ts new file mode 100644 index 000000000..37469a001 --- /dev/null +++ b/packages/ui/src/common/paper.ts @@ -0,0 +1,416 @@ +import type { + GrainGradientParams, + GrainGradientShape, + MeshGradientParams, +} from "@paper-design/shaders" + +export type PaperMeshGradientConfig = Required< + Pick< + MeshGradientParams, + "colors" | "distortion" | "swirl" | "grainMixer" | "grainOverlay" | "speed" | "frame" + > +> + +export type PaperGrainGradientConfig = Required< + Pick< + GrainGradientParams, + "colorBack" | "colors" | "softness" | "intensity" | "noise" | "shape" | "speed" | "frame" + > +> + +export type SeededPaperOption = { + seed?: string +} + +export const paperMeshGradientDefaults: PaperMeshGradientConfig = { + colors: ["#e0eaff", "#241d9a", "#f75092", "#9f50d3"], + distortion: 0.8, + swirl: 0.1, + grainMixer: 0, + grainOverlay: 0, + speed: 0.1, + frame: 0, +} + +export const paperGrainGradientDefaults: PaperGrainGradientConfig = { + colors: ["#7300ff", "#eba8ff", "#00bfff", "#2b00ff"], + colorBack: "#000000", + softness: 0.5, + intensity: 0.5, + noise: 0.25, + shape: "ripple", + speed: 0.4, + frame: 0, +} + +const grainShapes: GrainGradientShape[] = [ + "corners", + "wave", + "dots", + "truchet", + "ripple", + "blob", + "sphere", +] + +const meshPaletteFamilies = [ + ["#e0eaff", "#241d9a", "#f75092", "#9f50d3"], + ["#ddfff5", "#006c67", "#35d8c0", "#8cff7a"], + ["#ffe5c2", "#8a2500", "#ff7b39", "#ffd166"], + ["#f5f7ff", "#0d1b52", "#3f8cff", "#00c2ff"], + ["#fff2f2", "#6f1237", "#ff4d6d", "#ffb703"], + ["#f0ffe1", "#254d00", "#8cc63f", "#00a76f"], + ["#f5edff", "#44206b", "#b5179e", "#7209b7"], + ["#f4f1ea", "#3a2f1f", "#927c55", "#d0c2a8"], +] + +const grainPaletteFamilies = [ + ["#7300ff", "#eba8ff", "#00bfff", "#2b00ff"], + ["#0df2c1", "#0b7cff", "#74efff", "#1a2cff"], + ["#ff7a18", "#ffd166", "#ff4d6d", "#5f0f40"], + ["#8dff6a", "#1f7a1f", "#d7ff70", "#00c48c"], + ["#f6a6ff", "#7027c9", "#ff66c4", "#20115b"], + ["#b9ecff", "#006494", "#00a6a6", "#072ac8"], + ["#f7f0d6", "#8c5e34", "#d68c45", "#4e342e"], + ["#ffd9f5", "#ff006e", "#8338ec", "#3a0ca3"], +] + +const paletteModes = [ + { + hueOffsets: [0, 22, 182, 238], + saturations: [0.92, 0.7, 0.84, 0.74], + lightnesses: [0.82, 0.28, 0.6, 0.5], + }, + { + hueOffsets: [0, 118, 242, 304], + saturations: [0.88, 0.76, 0.82, 0.7], + lightnesses: [0.8, 0.42, 0.58, 0.48], + }, + { + hueOffsets: [0, 44, 156, 214], + saturations: [0.94, 0.78, 0.86, 0.72], + lightnesses: [0.78, 0.4, 0.6, 0.46], + }, + { + hueOffsets: [0, 76, 184, 326], + saturations: [0.86, 0.8, 0.78, 0.76], + lightnesses: [0.82, 0.52, 0.42, 0.58], + }, + { + hueOffsets: [0, 140, 196, 224], + saturations: [0.84, 0.7, 0.76, 0.88], + lightnesses: [0.86, 0.46, 0.36, 0.54], + }, + { + hueOffsets: [0, 162, 212, 342], + saturations: [0.9, 0.72, 0.8, 0.82], + lightnesses: [0.8, 0.38, 0.52, 0.56], + }, +] + +type MeshGradientOverrides = SeededPaperOption & Partial +type GrainGradientOverrides = SeededPaperOption & Partial + +export function getSeededPaperMeshGradientConfig(seed: string): PaperMeshGradientConfig { + const random = createRandom(seed, "mesh") + + return { + colors: createSeededPalette(paperMeshGradientDefaults.colors, seed, "mesh-colors", { + families: meshPaletteFamilies, + hueShift: 42, + saturationShift: 0.18, + lightnessShift: 0.14, + baseBlend: [0.08, 0.2], + }), + distortion: roundTo(clamp(0.58 + random() * 0.32, 0, 1), 3), + swirl: roundTo(clamp(0.03 + random() * 0.28, 0, 1), 3), + grainMixer: roundTo(clamp(random() * 0.18, 0, 1), 3), + grainOverlay: roundTo(clamp(random() * 0.12, 0, 1), 3), + speed: roundTo(0.05 + random() * 0.11, 3), + frame: Math.round(random() * 240000), + } +} + +export function getSeededPaperGrainGradientConfig(seed: string): PaperGrainGradientConfig { + const random = createRandom(seed, "grain") + const colors = createSeededPalette(paperGrainGradientDefaults.colors, seed, "grain-colors", { + families: grainPaletteFamilies, + hueShift: 58, + saturationShift: 0.22, + lightnessShift: 0.18, + baseBlend: [0.04, 0.14], + }) + const anchorColor = colors[Math.floor(random() * colors.length)] ?? colors[0] + + return { + colors, + colorBack: createSeededBackground(anchorColor, seed, "grain-background"), + softness: roundTo(clamp(0.22 + random() * 0.56, 0, 1), 3), + intensity: roundTo(clamp(0.2 + random() * 0.6, 0, 1), 3), + noise: roundTo(clamp(0.12 + random() * 0.34, 0, 1), 3), + shape: grainShapes[Math.floor(random() * grainShapes.length)] ?? paperGrainGradientDefaults.shape, + speed: roundTo(0.2 + random() * 0.6, 3), + frame: Math.round(random() * 320000), + } +} + +export function resolvePaperMeshGradientConfig( + options: MeshGradientOverrides = {}, +): PaperMeshGradientConfig { + const seeded = options.seed ? getSeededPaperMeshGradientConfig(options.seed) : paperMeshGradientDefaults + + return { + colors: options.colors ?? seeded.colors, + distortion: options.distortion ?? seeded.distortion, + swirl: options.swirl ?? seeded.swirl, + grainMixer: options.grainMixer ?? seeded.grainMixer, + grainOverlay: options.grainOverlay ?? seeded.grainOverlay, + speed: options.speed ?? seeded.speed, + frame: options.frame ?? seeded.frame, + } +} + +export function resolvePaperGrainGradientConfig( + options: GrainGradientOverrides = {}, +): PaperGrainGradientConfig { + const seeded = options.seed ? getSeededPaperGrainGradientConfig(options.seed) : paperGrainGradientDefaults + + return { + colors: options.colors ?? seeded.colors, + colorBack: options.colorBack ?? seeded.colorBack, + softness: options.softness ?? seeded.softness, + intensity: options.intensity ?? seeded.intensity, + noise: options.noise ?? seeded.noise, + shape: options.shape ?? seeded.shape, + speed: options.speed ?? seeded.speed, + frame: options.frame ?? seeded.frame, + } +} + +function buildSeedSource(seed: string) { + const trimmedSeed = seed.trim() + const separatorIndex = trimmedSeed.indexOf("_") + + if (separatorIndex === -1) { + return trimmedSeed + } + + const prefix = trimmedSeed.slice(0, separatorIndex) + const suffix = trimmedSeed.slice(separatorIndex + 1) + const suffixTail = suffix.slice(5) || suffix + + return `${trimmedSeed}|${prefix}|${suffix}|${suffixTail}` +} + +function createSeededPalette( + baseColors: string[], + seed: string, + namespace: string, + options: { + families: string[][] + hueShift: number + saturationShift: number + lightnessShift: number + baseBlend: [number, number] + }, +) { + const familyRandom = createRandom(seed, `${namespace}:family`) + const primaryIndex = Math.floor(familyRandom() * options.families.length) + const secondaryOffset = 1 + Math.floor(familyRandom() * (options.families.length - 1)) + const secondaryIndex = (primaryIndex + secondaryOffset) % options.families.length + const primary = options.families[primaryIndex] ?? baseColors + const secondary = options.families[secondaryIndex] ?? [...baseColors].reverse() + const primaryShift = Math.floor(familyRandom() * primary.length) + const secondaryShift = Math.floor(familyRandom() * secondary.length) + const paletteMode = paletteModes[Math.floor(familyRandom() * paletteModes.length)] ?? paletteModes[0] + const baseHue = familyRandom() * 360 + + return baseColors.map((color, index) => { + const random = createRandom(seed, `${namespace}:${index}`) + const primaryColor = primary[(index + primaryShift) % primary.length] ?? color + const secondaryColor = secondary[(index + secondaryShift) % secondary.length] ?? primaryColor + const proceduralColor = hslToHex( + (baseHue + paletteMode.hueOffsets[index % paletteMode.hueOffsets.length] + (random() * 2 - 1) * 18 + 360) % 360, + clamp(paletteMode.saturations[index % paletteMode.saturations.length] + (random() * 2 - 1) * 0.08, 0, 1), + clamp(paletteMode.lightnesses[index % paletteMode.lightnesses.length] + (random() * 2 - 1) * 0.08, 0, 1), + ) + const mixedFamilyColor = mixHexColors(primaryColor, secondaryColor, 0.18 + random() * 0.64) + const remixedFamilyColor = mixHexColors( + mixedFamilyColor, + primary[(index + secondaryShift + 1) % primary.length] ?? mixedFamilyColor, + random() * 0.32, + ) + const proceduralFamilyColor = mixHexColors(proceduralColor, remixedFamilyColor, 0.22 + random() * 0.34) + const [minBaseBlend, maxBaseBlend] = options.baseBlend + const blendedBaseColor = mixHexColors( + proceduralFamilyColor, + color, + minBaseBlend + random() * (maxBaseBlend - minBaseBlend), + ) + + return adjustHexColor(blendedBaseColor, { + hueShift: (random() * 2 - 1) * options.hueShift + (random() * 2 - 1) * 14, + saturationShift: (random() * 2 - 1) * options.saturationShift + 0.06, + lightnessShift: (random() * 2 - 1) * options.lightnessShift, + }) + }) +} + +function createSeededBackground(baseColor: string, seed: string, namespace: string) { + const [red, green, blue] = hexToRgb(baseColor) + const [hue] = rgbToHsl(red, green, blue) + const random = createRandom(seed, namespace) + + return hslToHex( + hue, + clamp(0.18 + random() * 0.18, 0, 1), + clamp(0.03 + random() * 0.09, 0, 1), + ) +} + +function adjustHexColor( + hex: string, + adjustments: { hueShift: number; saturationShift: number; lightnessShift: number }, +) { + const [red, green, blue] = hexToRgb(hex) + const [hue, saturation, lightness] = rgbToHsl(red, green, blue) + + return hslToHex( + (hue + adjustments.hueShift + 360) % 360, + clamp(saturation + adjustments.saturationShift, 0, 1), + clamp(lightness + adjustments.lightnessShift, 0, 1), + ) +} + +function mixHexColors(colorA: string, colorB: string, amount: number) { + const [redA, greenA, blueA] = hexToRgb(colorA) + const [redB, greenB, blueB] = hexToRgb(colorB) + const mixAmount = clamp(amount, 0, 1) + + return rgbToHex( + Math.round(redA + (redB - redA) * mixAmount), + Math.round(greenA + (greenB - greenA) * mixAmount), + Math.round(blueA + (blueB - blueA) * mixAmount), + ) +} + +function createRandom(seed: string, namespace: string) { + return mulberry32(hashString(`${buildSeedSource(seed)}::${namespace}`)) +} + +function hashString(input: string) { + let hash = 2166136261 + + for (let index = 0; index < input.length; index += 1) { + hash ^= input.charCodeAt(index) + hash = Math.imul(hash, 16777619) + } + + return hash >>> 0 +} + +function mulberry32(seed: number) { + return function nextRandom() { + let value = seed += 0x6d2b79f5 + value = Math.imul(value ^ (value >>> 15), value | 1) + value ^= value + Math.imul(value ^ (value >>> 7), value | 61) + return ((value ^ (value >>> 14)) >>> 0) / 4294967296 + } +} + +function clamp(value: number, min: number, max: number) { + return Math.min(max, Math.max(min, value)) +} + +function roundTo(value: number, precision: number) { + const power = 10 ** precision + return Math.round(value * power) / power +} + +function hexToRgb(hex: string): [number, number, number] { + const normalized = hex.replace(/^#/, "") + const expanded = normalized.length === 3 + ? normalized.split("").map((part) => `${part}${part}`).join("") + : normalized + + if (expanded.length !== 6) { + throw new Error(`Unsupported hex color: ${hex}`) + } + + const value = Number.parseInt(expanded, 16) + + return [ + (value >> 16) & 255, + (value >> 8) & 255, + value & 255, + ] +} + +function rgbToHsl(red: number, green: number, blue: number): [number, number, number] { + const normalizedRed = red / 255 + const normalizedGreen = green / 255 + const normalizedBlue = blue / 255 + const max = Math.max(normalizedRed, normalizedGreen, normalizedBlue) + const min = Math.min(normalizedRed, normalizedGreen, normalizedBlue) + const lightness = (max + min) / 2 + + if (max === min) { + return [0, 0, lightness] + } + + const delta = max - min + const saturation = lightness > 0.5 + ? delta / (2 - max - min) + : delta / (max + min) + + let hue = 0 + + switch (max) { + case normalizedRed: + hue = (normalizedGreen - normalizedBlue) / delta + (normalizedGreen < normalizedBlue ? 6 : 0) + break + case normalizedGreen: + hue = (normalizedBlue - normalizedRed) / delta + 2 + break + default: + hue = (normalizedRed - normalizedGreen) / delta + 4 + break + } + + return [hue * 60, saturation, lightness] +} + +function hslToHex(hue: number, saturation: number, lightness: number) { + if (saturation === 0) { + const value = Math.round(lightness * 255) + return rgbToHex(value, value, value) + } + + const hueToRgb = (p: number, q: number, t: number) => { + let normalizedT = t + + if (normalizedT < 0) normalizedT += 1 + if (normalizedT > 1) normalizedT -= 1 + if (normalizedT < 1 / 6) return p + (q - p) * 6 * normalizedT + if (normalizedT < 1 / 2) return q + if (normalizedT < 2 / 3) return p + (q - p) * (2 / 3 - normalizedT) * 6 + return p + } + + const normalizedHue = hue / 360 + const q = lightness < 0.5 + ? lightness * (1 + saturation) + : lightness + saturation - lightness * saturation + const p = 2 * lightness - q + const red = hueToRgb(p, q, normalizedHue + 1 / 3) + const green = hueToRgb(p, q, normalizedHue) + const blue = hueToRgb(p, q, normalizedHue - 1 / 3) + + return rgbToHex(Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255)) +} + +function rgbToHex(red: number, green: number, blue: number) { + return `#${[red, green, blue] + .map((value) => value.toString(16).padStart(2, "0")) + .join("")}` +} diff --git a/packages/ui/src/react/index.ts b/packages/ui/src/react/index.ts new file mode 100644 index 000000000..5945ca552 --- /dev/null +++ b/packages/ui/src/react/index.ts @@ -0,0 +1,17 @@ +export { + getSeededPaperGrainGradientConfig, + getSeededPaperMeshGradientConfig, + paperGrainGradientDefaults, + paperMeshGradientDefaults, + resolvePaperGrainGradientConfig, + resolvePaperMeshGradientConfig, +} from "../common/paper" +export type { + PaperGrainGradientConfig, + PaperMeshGradientConfig, + SeededPaperOption, +} from "../common/paper" +export { PaperGrainGradient } from "./paper/grain-gradient" +export type { PaperGrainGradientProps } from "./paper/grain-gradient" +export { PaperMeshGradient } from "./paper/mesh-gradient" +export type { PaperMeshGradientProps } from "./paper/mesh-gradient" diff --git a/packages/ui/src/react/paper/grain-gradient.tsx b/packages/ui/src/react/paper/grain-gradient.tsx new file mode 100644 index 000000000..a247eecdc --- /dev/null +++ b/packages/ui/src/react/paper/grain-gradient.tsx @@ -0,0 +1,101 @@ +"use client" + +import { + defaultObjectSizing, + defaultPatternSizing, + type GrainGradientShape, +} from "@paper-design/shaders" +import { GrainGradient, type GrainGradientProps } from "@paper-design/shaders-react" +import { resolvePaperGrainGradientConfig } from "../../common/paper" + +export interface PaperGrainGradientProps + extends Omit< + GrainGradientProps, + "colorBack" | "colors" | "softness" | "intensity" | "noise" | "shape" | "speed" | "frame" + > { + seed?: string + fill?: boolean + colorBack?: string + colors?: string[] + softness?: number + intensity?: number + noise?: number + shape?: GrainGradientProps["shape"] + speed?: number + frame?: number +} + +export function PaperGrainGradient({ + seed, + fill = true, + colorBack, + colors, + softness, + intensity, + noise, + shape, + speed, + frame, + fit, + rotation, + scale, + originX, + originY, + offsetX, + offsetY, + worldWidth, + worldHeight, + width, + height, + ...props +}: PaperGrainGradientProps) { + const resolved = resolvePaperGrainGradientConfig({ + seed, + colorBack, + colors, + softness, + intensity, + noise, + shape, + speed, + frame, + }) + + const sizingDefaults = getSizingDefaults(resolved.shape) + + return ( + + ) +} + +function getSizingDefaults(shape: GrainGradientShape) { + switch (shape) { + case "wave": + case "dots": + case "truchet": + return defaultPatternSizing + default: + return defaultObjectSizing + } +} diff --git a/packages/ui/src/react/paper/mesh-gradient.tsx b/packages/ui/src/react/paper/mesh-gradient.tsx new file mode 100644 index 000000000..da9fe68ae --- /dev/null +++ b/packages/ui/src/react/paper/mesh-gradient.tsx @@ -0,0 +1,61 @@ +"use client" + +import { MeshGradient, type MeshGradientProps } from "@paper-design/shaders-react" +import { resolvePaperMeshGradientConfig } from "../../common/paper" + +export interface PaperMeshGradientProps + extends Omit< + MeshGradientProps, + "colors" | "distortion" | "swirl" | "grainMixer" | "grainOverlay" | "speed" | "frame" + > { + seed?: string + fill?: boolean + colors?: string[] + distortion?: number + swirl?: number + grainMixer?: number + grainOverlay?: number + speed?: number + frame?: number +} + +export function PaperMeshGradient({ + seed, + fill = true, + colors, + distortion, + swirl, + grainMixer, + grainOverlay, + speed, + frame, + width, + height, + ...props +}: PaperMeshGradientProps) { + const resolved = resolvePaperMeshGradientConfig({ + seed, + colors, + distortion, + swirl, + grainMixer, + grainOverlay, + speed, + frame, + }) + + return ( + + ) +} diff --git a/packages/ui/src/solid/index.ts b/packages/ui/src/solid/index.ts new file mode 100644 index 000000000..5945ca552 --- /dev/null +++ b/packages/ui/src/solid/index.ts @@ -0,0 +1,17 @@ +export { + getSeededPaperGrainGradientConfig, + getSeededPaperMeshGradientConfig, + paperGrainGradientDefaults, + paperMeshGradientDefaults, + resolvePaperGrainGradientConfig, + resolvePaperMeshGradientConfig, +} from "../common/paper" +export type { + PaperGrainGradientConfig, + PaperMeshGradientConfig, + SeededPaperOption, +} from "../common/paper" +export { PaperGrainGradient } from "./paper/grain-gradient" +export type { PaperGrainGradientProps } from "./paper/grain-gradient" +export { PaperMeshGradient } from "./paper/mesh-gradient" +export type { PaperMeshGradientProps } from "./paper/mesh-gradient" diff --git a/packages/ui/src/solid/paper/grain-gradient.tsx b/packages/ui/src/solid/paper/grain-gradient.tsx new file mode 100644 index 000000000..81c057a2f --- /dev/null +++ b/packages/ui/src/solid/paper/grain-gradient.tsx @@ -0,0 +1,125 @@ +import { + defaultObjectSizing, + defaultPatternSizing, + getShaderColorFromString, + getShaderNoiseTexture, + grainGradientFragmentShader, + GrainGradientShapes, + ShaderFitOptions, + type GrainGradientParams, +} from "@paper-design/shaders" +import type { JSX } from "solid-js" +import { resolvePaperGrainGradientConfig } from "../../common/paper" +import { SolidShaderMount } from "./shader-mount" + +type SharedGrainProps = Pick< + GrainGradientParams, + "fit" | "rotation" | "scale" | "originX" | "originY" | "offsetX" | "offsetY" | "worldWidth" | "worldHeight" +> + +export interface PaperGrainGradientProps + extends Omit, "ref">, + Partial { + ref?: (element: HTMLDivElement) => void + seed?: string + fill?: boolean + colorBack?: string + colors?: string[] + softness?: number + intensity?: number + noise?: number + shape?: GrainGradientParams["shape"] + speed?: number + frame?: number + minPixelRatio?: number + maxPixelCount?: number + webGlContextAttributes?: WebGLContextAttributes + width?: string | number + height?: string | number +} + +export function PaperGrainGradient({ + seed, + fill = true, + colorBack, + colors, + softness, + intensity, + noise, + shape, + speed, + frame, + fit, + rotation, + scale, + originX, + originY, + offsetX, + offsetY, + worldWidth, + worldHeight, + minPixelRatio, + maxPixelCount, + webGlContextAttributes, + width, + height, + ...props +}: PaperGrainGradientProps) { + const resolved = resolvePaperGrainGradientConfig({ + seed, + colorBack, + colors, + softness, + intensity, + noise, + shape, + speed, + frame, + }) + + const sizingDefaults = getSizingDefaults(resolved.shape) + + return ( + + ) +} + +function getSizingDefaults(shape: NonNullable) { + switch (shape) { + case "wave": + case "dots": + case "truchet": + return defaultPatternSizing + default: + return defaultObjectSizing + } +} diff --git a/packages/ui/src/solid/paper/mesh-gradient.tsx b/packages/ui/src/solid/paper/mesh-gradient.tsx new file mode 100644 index 000000000..234c6f42a --- /dev/null +++ b/packages/ui/src/solid/paper/mesh-gradient.tsx @@ -0,0 +1,104 @@ +import { + defaultObjectSizing, + getShaderColorFromString, + meshGradientFragmentShader, + ShaderFitOptions, + type MeshGradientParams, +} from "@paper-design/shaders" +import type { JSX } from "solid-js" +import { resolvePaperMeshGradientConfig } from "../../common/paper" +import { SolidShaderMount } from "./shader-mount" + +type SharedMeshProps = Pick< + MeshGradientParams, + "fit" | "rotation" | "scale" | "originX" | "originY" | "offsetX" | "offsetY" | "worldWidth" | "worldHeight" +> + +export interface PaperMeshGradientProps + extends Omit, "ref">, + Partial { + ref?: (element: HTMLDivElement) => void + seed?: string + fill?: boolean + colors?: string[] + distortion?: number + swirl?: number + grainMixer?: number + grainOverlay?: number + speed?: number + frame?: number + minPixelRatio?: number + maxPixelCount?: number + webGlContextAttributes?: WebGLContextAttributes + width?: string | number + height?: string | number +} + +export function PaperMeshGradient({ + seed, + fill = true, + colors, + distortion, + swirl, + grainMixer, + grainOverlay, + speed, + frame, + fit = defaultObjectSizing.fit, + rotation = defaultObjectSizing.rotation, + scale = defaultObjectSizing.scale, + originX = defaultObjectSizing.originX, + originY = defaultObjectSizing.originY, + offsetX = defaultObjectSizing.offsetX, + offsetY = defaultObjectSizing.offsetY, + worldWidth = defaultObjectSizing.worldWidth, + worldHeight = defaultObjectSizing.worldHeight, + minPixelRatio, + maxPixelCount, + webGlContextAttributes, + width, + height, + ...props +}: PaperMeshGradientProps) { + const resolved = resolvePaperMeshGradientConfig({ + seed, + colors, + distortion, + swirl, + grainMixer, + grainOverlay, + speed, + frame, + }) + + return ( + + ) +} diff --git a/packages/ui/src/solid/paper/shader-mount.tsx b/packages/ui/src/solid/paper/shader-mount.tsx new file mode 100644 index 000000000..c8c0d4fe3 --- /dev/null +++ b/packages/ui/src/solid/paper/shader-mount.tsx @@ -0,0 +1,115 @@ +import { ShaderMount, type ShaderMountUniforms } from "@paper-design/shaders" +import { createEffect, onCleanup, onMount, splitProps, type JSX } from "solid-js" + +type SolidShaderMountProps = Omit, "ref"> & { + ref?: (element: HTMLDivElement) => void + fragmentShader: string + uniforms: ShaderMountUniforms + speed?: number + frame?: number + minPixelRatio?: number + maxPixelCount?: number + webGlContextAttributes?: WebGLContextAttributes + width?: string | number + height?: string | number +} + +export function SolidShaderMount(props: SolidShaderMountProps) { + const [local, rest] = splitProps(props, [ + "ref", + "fragmentShader", + "uniforms", + "speed", + "frame", + "minPixelRatio", + "maxPixelCount", + "webGlContextAttributes", + "width", + "height", + "style", + ]) + + let element: HTMLDivElement | undefined + let shaderMount: ShaderMount | undefined + + onMount(() => { + if (!element) { + return + } + + shaderMount = new ShaderMount( + element, + local.fragmentShader, + local.uniforms, + local.webGlContextAttributes, + local.speed, + local.frame, + local.minPixelRatio, + local.maxPixelCount, + ) + + onCleanup(() => { + shaderMount?.dispose() + shaderMount = undefined + }) + }) + + createEffect(() => { + shaderMount?.setUniforms(local.uniforms) + }) + + createEffect(() => { + shaderMount?.setSpeed(local.speed) + }) + + createEffect(() => { + if (local.frame !== undefined) { + shaderMount?.setFrame(local.frame) + } + }) + + createEffect(() => { + shaderMount?.setMinPixelRatio(local.minPixelRatio) + }) + + createEffect(() => { + shaderMount?.setMaxPixelCount(local.maxPixelCount) + }) + + return ( +
{ + element = node + local.ref?.(node) + }} + style={mergeStyle(local.style, local.width, local.height)} + /> + ) +} + +function mergeStyle( + style: JSX.CSSProperties | string | undefined, + width: string | number | undefined, + height: string | number | undefined, +) { + if (typeof style === "string") { + return [ + width !== undefined ? `width:${toCssSize(width)}` : "", + height !== undefined ? `height:${toCssSize(height)}` : "", + style, + ] + .filter(Boolean) + .join(";") + } + + return { + ...(style ?? {}), + ...(width !== undefined ? { width: toCssSize(width) } : {}), + ...(height !== undefined ? { height: toCssSize(height) } : {}), + } +} + +function toCssSize(value: string | number) { + return typeof value === "number" ? `${value}px` : value +} diff --git a/packages/ui/tsconfig.react.json b/packages/ui/tsconfig.react.json new file mode 100644 index 000000000..5c8688165 --- /dev/null +++ b/packages/ui/tsconfig.react.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "jsx": "react-jsx" + }, + "include": [ + "src/common/**/*", + "src/react/**/*" + ] +} diff --git a/packages/ui/tsconfig.solid.json b/packages/ui/tsconfig.solid.json new file mode 100644 index 000000000..de511d1be --- /dev/null +++ b/packages/ui/tsconfig.solid.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "jsx": "preserve", + "jsxImportSource": "solid-js/h" + }, + "include": [ + "src/common/**/*", + "src/solid/**/*" + ] +} diff --git a/packages/ui/tsup.config.ts b/packages/ui/tsup.config.ts new file mode 100644 index 000000000..88fe74670 --- /dev/null +++ b/packages/ui/tsup.config.ts @@ -0,0 +1,46 @@ +import { defineConfig } from "tsup" + +export default defineConfig([ + { + entry: { + "react/index": "src/react/index.ts", + }, + tsconfig: "./tsconfig.react.json", + format: ["esm"], + dts: { + tsconfig: "./tsconfig.react.json", + }, + clean: true, + target: "es2022", + platform: "browser", + sourcemap: false, + splitting: false, + treeshake: true, + external: ["react", "react/jsx-runtime"], + esbuildOptions(options) { + options.jsx = "automatic" + options.jsxImportSource = "react" + }, + }, + { + entry: { + "solid/index": "src/solid/index.ts", + }, + tsconfig: "./tsconfig.solid.json", + format: ["esm"], + dts: { + tsconfig: "./tsconfig.solid.json", + }, + clean: false, + target: "es2022", + platform: "browser", + sourcemap: false, + splitting: false, + treeshake: true, + external: ["solid-js", "solid-js/jsx-runtime"], + esbuildOptions(options) { + options.jsx = "automatic" + options.jsxImportSource = "solid-js/h" + }, + }, +]) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35b9f67c2..38943a658 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,9 @@ importers: '@opencode-ai/sdk': specifier: ^1.1.31 version: 1.1.39 + '@openwork/ui': + specifier: workspace:* + version: link:../../packages/ui '@radix-ui/colors': specifier: ^3.0.0 version: 3.0.0 @@ -218,9 +221,6 @@ importers: apps/share: dependencies: - '@paper-design/shaders-react': - specifier: 0.0.71 - version: 0.0.71(@types/react@18.2.79)(react@19.2.4) '@vercel/blob': specifier: ^0.27.0 version: 0.27.3 @@ -353,6 +353,34 @@ importers: specifier: ^2.11.0 version: 2.11.10(solid-js@1.9.9)(vite@6.4.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) + apps/ui-demo: + dependencies: + '@openwork/ui': + specifier: workspace:* + version: link:../../packages/ui + react: + specifier: 19.2.4 + version: 19.2.4 + react-dom: + specifier: 19.2.4 + version: 19.2.4(react@19.2.4) + devDependencies: + '@types/react': + specifier: 19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^5.0.4 + version: 5.2.0(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^7.1.12 + version: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + ee/apps/den-api: dependencies: '@daytonaio/sdk': @@ -396,60 +424,14 @@ importers: specifier: ^5.5.4 version: 5.9.3 - ee/apps/den-controller: - dependencies: - '@daytonaio/sdk': - specifier: ^0.150.0 - version: 0.150.0(ws@8.19.0) - '@openwork-ee/den-db': - specifier: workspace:* - version: link:../../packages/den-db - '@openwork-ee/utils': - specifier: workspace:* - version: link:../../packages/utils - better-auth: - specifier: ^1.4.18 - version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.11)(mysql2@3.17.4))(mysql2@3.17.4)(next@16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10) - better-call: - specifier: ^1.1.8 - version: 1.1.8(zod@4.3.6) - cors: - specifier: ^2.8.5 - version: 2.8.6 - dotenv: - specifier: ^16.4.5 - version: 16.6.1 - express: - specifier: ^4.19.2 - version: 4.22.1 - mysql2: - specifier: ^3.11.3 - version: 3.17.4 - zod: - specifier: ^4.3.6 - version: 4.3.6 - devDependencies: - '@types/cors': - specifier: ^2.8.17 - version: 2.8.19 - '@types/express': - specifier: ^4.17.21 - version: 4.17.25 - '@types/node': - specifier: ^20.11.30 - version: 20.12.12 - tsx: - specifier: ^4.15.7 - version: 4.21.0 - typescript: - specifier: ^5.5.4 - version: 5.9.3 - ee/apps/den-web: dependencies: + '@openwork/ui': + specifier: workspace:* + version: link:../../../packages/ui '@paper-design/shaders-react': - specifier: 0.0.71 - version: 0.0.71(@types/react@19.2.14)(react@19.2.4) + specifier: 0.0.72 + version: 0.0.72(@types/react@19.2.14)(react@19.2.4) lucide-react: specifier: ^0.577.0 version: 0.577.0(react@19.2.4) @@ -521,9 +503,9 @@ importers: ee/apps/landing: dependencies: - '@paper-design/shaders-react': - specifier: 0.0.71 - version: 0.0.71(@types/react@18.2.79)(react@18.2.0) + '@openwork/ui': + specifier: workspace:* + version: link:../../../packages/ui botid: specifier: ^1.5.11 version: 1.5.11(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0) @@ -612,6 +594,31 @@ importers: specifier: ^5.5.4 version: 5.9.3 + packages/ui: + dependencies: + '@paper-design/shaders': + specifier: 0.0.72 + version: 0.0.72 + '@paper-design/shaders-react': + specifier: 0.0.72 + version: 0.0.72(@types/react@19.2.14)(react@19.2.4) + react: + specifier: ^18 || ^19 + version: 19.2.4 + solid-js: + specifier: ^1.9.0 + version: 1.9.9 + devDependencies: + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + tsup: + specifier: ^8.5.0 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: ^5.6.3 + version: 5.9.3 + packages: '@alloc/quick-lru@5.2.0': @@ -791,6 +798,10 @@ packages: resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.28.6': resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==} engines: {node: '>=6.9.0'} @@ -803,10 +814,18 @@ packages: resolution: {integrity: sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==} engines: {node: '>=6.9.0'} + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.28.6': resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.27.3': resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} @@ -882,6 +901,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-syntax-jsx@7.28.6': resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} engines: {node: '>=6.9.0'} @@ -900,6 +924,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.28.6': resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} engines: {node: '>=6.9.0'} @@ -920,10 +956,18 @@ packages: resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.28.6': resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@better-auth/core@1.4.18': resolution: {integrity: sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg==} peerDependencies: @@ -2188,8 +2232,8 @@ packages: peerDependencies: solid-js: 1.9.9 - '@paper-design/shaders-react@0.0.71': - resolution: {integrity: sha512-kTqjIlyZcpkwqJie+3ldEDscTtx1oOi8eRBD5QgWKI21GaNn/SSg26092M5zzqr3e8dVANv0ktS2ICSjbMFKbw==} + '@paper-design/shaders-react@0.0.72': + resolution: {integrity: sha512-q6KwquL93ZVNcuSM7pqzW0z/VLjnDVb/NSpYyGJBxf7MEHHCXx37E+zw/Px6RZLt3SGCUerIDDCGCMT385oW0w==} peerDependencies: '@types/react': ^18 || ^19 react: ^18 || ^19 @@ -2197,8 +2241,8 @@ packages: '@types/react': optional: true - '@paper-design/shaders@0.0.71': - resolution: {integrity: sha512-brCt05YxxyjBrhnE3l1wJJHcFXsM8aE4lmpd9TMQp+p0dMU3F+OWkJZL9m/RC1Tt7om5xr0Wg7d0HYm+b9NYZA==} + '@paper-design/shaders@0.0.72': + resolution: {integrity: sha512-rk2BFuV5ood2DaivbxJC2jQMzaB434isDUzdUQ85Cy0OWnUMuxl8kyGMR74TDPyjo3EvcHIyreNLkJdRG+GfSA==} '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -2245,6 +2289,9 @@ packages: '@radix-ui/colors@3.0.0': resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==} + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + '@rollup/rollup-android-arm-eabi@4.55.1': resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} cpu: [arm] @@ -2876,30 +2923,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/body-parser@1.19.6': - resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - - '@types/cors@2.8.19': - resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/express-serve-static-core@4.19.8': - resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} - - '@types/express@4.17.25': - resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} - - '@types/http-errors@2.0.5': - resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/minimatch@5.1.2': resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} @@ -2918,12 +2944,6 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/qs@6.14.0': - resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@18.2.25': resolution: {integrity: sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA==} @@ -2941,15 +2961,6 @@ packages: '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - '@types/send@0.17.6': - resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} - - '@types/send@1.2.1': - resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - - '@types/serve-static@1.15.10': - resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} - '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -2957,6 +2968,12 @@ packages: resolution: {integrity: sha512-WizeAxzOTmv0JL7wOaxvLIU/KdBcrclM1ZUOdSlIZAxsTTTe1jsyBthStLby0Ueh7FnmKYAjLz26qRJTk5SDkQ==} engines: {node: '>=16.14'} + '@vitejs/plugin-react@5.2.0': + resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + '@webgpu/types@0.1.69': resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} @@ -2964,10 +2981,6 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -2999,9 +3012,6 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} @@ -3139,10 +3149,6 @@ packages: bmp-ts@1.0.9: resolution: {integrity: sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==} - body-parser@1.20.4: - resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - botid@1.5.11: resolution: {integrity: sha512-KOO1A3+/vFVJk5aFoG3sNwiogKFPVR+x4aw4gQ1b2e0XoE+i5xp48/EZn+WqR07jRHeDGwHWQUOtV5WVm7xiww==} peerDependencies: @@ -3219,10 +3225,6 @@ packages: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -3231,10 +3233,6 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} @@ -3290,28 +3288,9 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.0.7: - resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -3323,14 +3302,6 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -3351,14 +3322,6 @@ packages: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -3481,19 +3444,12 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - enhanced-resolve@5.18.4: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} @@ -3542,13 +3498,6 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -3570,10 +3519,6 @@ packages: resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} engines: {node: '>=0.10.0'} - express@4.22.1: - resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} - engines: {node: '>= 0.10.0'} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -3605,10 +3550,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@1.3.2: - resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} - engines: {node: '>= 0.8'} - find-babel-config@2.1.2: resolution: {integrity: sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==} @@ -3635,10 +3576,6 @@ packages: forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -3656,10 +3593,6 @@ packages: react-dom: optional: true - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -3751,14 +3684,6 @@ packages: html-entities@2.3.3: resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -3775,10 +3700,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -4002,25 +3923,14 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - merge-anything@5.1.7: resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} engines: {node: '>=12.13'} - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -4033,11 +3943,6 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -4075,9 +3980,6 @@ packages: motion-utils@12.29.2: resolution: {integrity: sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==} - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -4101,10 +4003,6 @@ packages: resolution: {integrity: sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA==} engines: {node: ^20.0.0 || >=22.0.0} - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - next@14.2.5: resolution: {integrity: sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==} engines: {node: '>=18.17.0'} @@ -4194,10 +4092,6 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - omggif@1.0.10: resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} @@ -4205,10 +4099,6 @@ packages: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -4256,10 +4146,6 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -4275,9 +4161,6 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@0.1.12: - resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -4427,31 +4310,15 @@ packages: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - qs@6.14.2: - resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} - engines: {node: '>=0.6'} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@2.5.3: - resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} - engines: {node: '>= 0.8'} - react-dom@18.2.0: resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -4462,6 +4329,10 @@ packages: peerDependencies: react: ^19.2.4 + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -4571,10 +4442,6 @@ packages: engines: {node: '>=10'} hasBin: true - send@0.19.2: - resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} - engines: {node: '>= 0.8.0'} - seroval-plugins@1.3.3: resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==} engines: {node: '>=10'} @@ -4585,16 +4452,9 @@ packages: resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} - serve-static@1.16.3: - resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} - engines: {node: '>= 0.8.0'} - set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -4603,22 +4463,6 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - simple-xml-to-json@1.2.3: resolution: {integrity: sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA==} engines: {node: '>=20.12.2'} @@ -4664,10 +4508,6 @@ packages: resolution: {integrity: sha512-AzlMO+t51v6cFvKZ+Oe9DJnL1OXEH5s9bEy6di5aOrUpcP7PCzI/wIeXF0u3zg0L89gwnceoKxrLId0ZpYnNXw==} engines: {node: '>=18.0'} - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - stream-browserify@3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} @@ -4778,10 +4618,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - token-types@4.2.1: resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} engines: {node: '>=10'} @@ -4827,10 +4663,6 @@ packages: resolution: {integrity: sha512-Rb4qk5YT8RUwwdXtkLpkVhNEe/lor6+WV7S5tTlLpxSz6MjV5Qi8jGNn4gS6NAvrYGA/rNrE6YUQM85sCZUDbQ==} hasBin: true - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - typeid-js@1.2.0: resolution: {integrity: sha512-t76ZucAnvGC60ea/HjVsB0TSoB0cw9yjnfurUgtInXQWUI/VcrlZGpO23KN3iSe8yOGUgb1zr7W7uEzJ3hSljA==} @@ -4864,10 +4696,6 @@ packages: resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} engines: {node: '>=14.0'} - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -4880,18 +4708,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - vite-plugin-solid@2.11.10: resolution: {integrity: sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==} peerDependencies: @@ -4942,6 +4762,46 @@ packages: yaml: optional: true + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitefu@1.1.1: resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} peerDependencies: @@ -5497,6 +5357,12 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.28.6': {} '@babel/core@7.28.0': @@ -5539,6 +5405,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/generator@7.28.6': dependencies: '@babel/parser': 7.28.6 @@ -5547,6 +5433,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.27.3': dependencies: '@babel/types': 7.28.6 @@ -5610,6 +5504,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.6 + transitivePeerDependencies: + - supports-color + '@babel/helper-optimise-call-expression@7.27.1': dependencies: '@babel/types': 7.28.6 @@ -5647,6 +5550,10 @@ snapshots: dependencies: '@babel/types': 7.28.6 + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 @@ -5670,6 +5577,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 @@ -5710,11 +5627,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.28.6': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)': dependencies: '@better-auth/utils': 0.3.0 @@ -6884,28 +6818,14 @@ snapshots: - typescript - web-tree-sitter - '@paper-design/shaders-react@0.0.71(@types/react@18.2.79)(react@18.2.0)': - dependencies: - '@paper-design/shaders': 0.0.71 - react: 18.2.0 - optionalDependencies: - '@types/react': 18.2.79 - - '@paper-design/shaders-react@0.0.71(@types/react@18.2.79)(react@19.2.4)': + '@paper-design/shaders-react@0.0.72(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@paper-design/shaders': 0.0.71 - react: 19.2.4 - optionalDependencies: - '@types/react': 18.2.79 - - '@paper-design/shaders-react@0.0.71(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@paper-design/shaders': 0.0.71 + '@paper-design/shaders': 0.0.72 react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 - '@paper-design/shaders@0.0.71': {} + '@paper-design/shaders@0.0.72': {} '@pinojs/redact@0.4.0': {} @@ -6940,6 +6860,8 @@ snapshots: '@radix-ui/colors@3.0.0': {} + '@rolldown/pluginutils@1.0.0-rc.3': {} + '@rollup/rollup-android-arm-eabi@4.55.1': optional: true @@ -7610,39 +7532,8 @@ snapshots: dependencies: '@babel/types': 7.28.6 - '@types/body-parser@1.19.6': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 20.12.12 - - '@types/connect@3.4.38': - dependencies: - '@types/node': 20.12.12 - - '@types/cors@2.8.19': - dependencies: - '@types/node': 20.12.12 - '@types/estree@1.0.8': {} - '@types/express-serve-static-core@4.19.8': - dependencies: - '@types/node': 20.12.12 - '@types/qs': 6.14.0 - '@types/range-parser': 1.2.7 - '@types/send': 1.2.1 - - '@types/express@4.17.25': - dependencies: - '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 4.19.8 - '@types/qs': 6.14.0 - '@types/serve-static': 1.15.10 - - '@types/http-errors@2.0.5': {} - - '@types/mime@1.3.5': {} - '@types/minimatch@5.1.2': {} '@types/node@16.9.1': {} @@ -7661,10 +7552,6 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/qs@6.14.0': {} - - '@types/range-parser@1.2.7': {} - '@types/react-dom@18.2.25': dependencies: '@types/react': 18.2.79 @@ -7684,21 +7571,6 @@ snapshots: '@types/retry@0.12.0': {} - '@types/send@0.17.6': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 20.12.12 - - '@types/send@1.2.1': - dependencies: - '@types/node': 20.12.12 - - '@types/serve-static@1.15.10': - dependencies: - '@types/http-errors': 2.0.5 - '@types/node': 20.12.12 - '@types/send': 0.17.6 - '@types/ws@8.18.1': dependencies: '@types/node': 20.12.12 @@ -7711,6 +7583,18 @@ snapshots: throttleit: 2.1.0 undici: 5.29.0 + '@vitejs/plugin-react@5.2.0(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + '@webgpu/types@0.1.69': optional: true @@ -7718,11 +7602,6 @@ snapshots: dependencies: event-target-shim: 5.0.1 - accepts@1.3.8: - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - acorn-import-attributes@1.9.5(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -7746,8 +7625,6 @@ snapshots: arg@5.0.2: {} - array-flatten@1.1.1: {} - async-retry@1.3.3: dependencies: retry: 0.13.1 @@ -7862,23 +7739,6 @@ snapshots: bmp-ts@1.0.9: {} - body-parser@1.20.4: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.1 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.14.2 - raw-body: 2.5.3 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - botid@1.5.11(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0): optionalDependencies: next: 14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -7958,8 +7818,6 @@ snapshots: dependencies: streamsearch: 1.1.0 - bytes@3.1.2: {} - cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -7967,11 +7825,6 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - camelcase-css@2.0.1: {} caniuse-lite@1.0.30001764: {} @@ -8022,33 +7875,14 @@ snapshots: consola@3.4.2: {} - content-disposition@0.5.4: - dependencies: - safe-buffer: 5.2.1 - - content-type@1.0.5: {} - convert-source-map@2.0.0: {} - cookie-signature@1.0.7: {} - - cookie@0.7.2: {} - - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - crelt@1.0.6: {} cssesc@3.0.0: {} csstype@3.2.3: {} - debug@2.6.9: - dependencies: - ms: 2.0.0 - debug@4.4.3: dependencies: ms: 2.1.3 @@ -8059,10 +7893,6 @@ snapshots: denque@2.1.0: {} - depd@2.0.0: {} - - destroy@1.2.0: {} - detect-libc@2.1.2: {} didyoumean@1.2.2: {} @@ -8098,14 +7928,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - ee-first@1.1.1: {} - electron-to-chromium@1.5.267: {} emoji-regex@8.0.0: {} - encodeurl@2.0.0: {} - enhanced-resolve@5.18.4: dependencies: graceful-fs: 4.2.11 @@ -8220,10 +8046,6 @@ snapshots: escalade@3.2.0: {} - escape-html@1.0.3: {} - - etag@1.8.1: {} - event-target-shim@5.0.1: {} eventemitter3@4.0.7: {} @@ -8238,42 +8060,6 @@ snapshots: dependencies: homedir-polyfill: 1.0.3 - express@4.22.1: - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.4 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.0.7 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.3.2 - fresh: 0.5.2 - http-errors: 2.0.1 - merge-descriptors: 1.0.3 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.12 - proxy-addr: 2.0.7 - qs: 6.14.2 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.2 - serve-static: 1.16.3 - setprototypeof: 1.2.0 - statuses: 2.0.2 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8309,18 +8095,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@1.3.2: - dependencies: - debug: 2.6.9 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - find-babel-config@2.1.2: dependencies: json5: 2.2.3 @@ -8347,8 +8121,6 @@ snapshots: forwarded-parse@2.1.2: {} - forwarded@0.2.0: {} - fraction.js@4.3.7: {} framer-motion@12.35.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): @@ -8360,8 +8132,6 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - fresh@0.5.2: {} - fs.realpath@1.0.0: {} fsevents@2.3.2: @@ -8456,18 +8226,6 @@ snapshots: html-entities@2.3.3: {} - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - - iconv-lite@0.4.24: - dependencies: - safer-buffer: 2.1.2 - iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -8487,8 +8245,6 @@ snapshots: inherits@2.0.4: {} - ipaddr.js@1.9.1: {} - is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -8671,18 +8427,12 @@ snapshots: math-intrinsics@1.1.0: {} - media-typer@0.3.0: {} - merge-anything@5.1.7: dependencies: is-what: 4.1.16 - merge-descriptors@1.0.3: {} - merge2@1.4.1: {} - methods@1.1.2: {} - micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -8694,8 +8444,6 @@ snapshots: dependencies: mime-db: 1.52.0 - mime@1.6.0: {} - mime@3.0.0: {} minimatch@10.1.1: @@ -8729,8 +8477,6 @@ snapshots: motion-utils@12.29.2: {} - ms@2.0.0: {} - ms@2.1.3: {} mysql2@3.17.4: @@ -8758,8 +8504,6 @@ snapshots: nanostores@1.1.0: {} - negotiator@0.6.3: {} - next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@next/env': 14.2.5 @@ -8853,16 +8597,10 @@ snapshots: object-hash@3.0.0: {} - object-inspect@1.13.4: {} - omggif@1.0.10: {} on-exit-leak-free@2.1.2: {} - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - p-finally@1.0.0: {} p-limit@2.3.0: @@ -8906,8 +8644,6 @@ snapshots: dependencies: entities: 6.0.1 - parseurl@1.3.3: {} - path-exists@3.0.0: {} path-expression-matcher@1.1.3: {} @@ -8919,8 +8655,6 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-to-regexp@0.1.12: {} - pathe@2.0.3: {} peek-readable@4.1.0: {} @@ -9063,30 +8797,12 @@ snapshots: '@types/node': 20.12.12 long: 5.3.2 - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - proxy-from-env@1.1.0: {} - qs@6.14.2: - dependencies: - side-channel: 1.1.0 - queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} - range-parser@1.2.1: {} - - raw-body@2.5.3: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - react-dom@18.2.0(react@18.2.0): dependencies: loose-envify: 1.4.0 @@ -9098,6 +8814,8 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 + react-refresh@0.18.0: {} + react@18.2.0: dependencies: loose-envify: 1.4.0 @@ -9216,43 +8934,14 @@ snapshots: semver@7.7.4: {} - send@0.19.2: - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.1 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - seroval-plugins@1.3.3(seroval@1.3.2): dependencies: seroval: 1.3.2 seroval@1.3.2: {} - serve-static@1.16.3: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.19.2 - transitivePeerDependencies: - - supports-color - set-cookie-parser@2.7.2: {} - setprototypeof@1.2.0: {} - sharp@0.34.5: dependencies: '@img/colour': 1.1.0 @@ -9286,34 +8975,6 @@ snapshots: shell-quote@1.8.3: {} - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - simple-xml-to-json@1.2.3: {} solid-js@1.9.10: @@ -9360,8 +9021,6 @@ snapshots: stage-js@1.0.0-alpha.17: optional: true - statuses@2.0.2: {} - stream-browserify@3.0.0: dependencies: inherits: 2.0.4 @@ -9483,8 +9142,6 @@ snapshots: dependencies: is-number: 7.0.0 - toidentifier@1.0.1: {} - token-types@4.2.1: dependencies: '@tokenizer/token': 0.3.0 @@ -9542,11 +9199,6 @@ snapshots: '@turbo/windows-64': 2.8.20 '@turbo/windows-arm64': 2.8.20 - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - typeid-js@1.2.0: dependencies: uuid: 10.0.0 @@ -9569,8 +9221,6 @@ snapshots: dependencies: '@fastify/busboy': 2.1.1 - unpipe@1.0.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 @@ -9583,12 +9233,8 @@ snapshots: util-deprecate@1.0.2: {} - utils-merge@1.0.1: {} - uuid@10.0.0: {} - vary@1.1.2: {} - vite-plugin-solid@2.11.10(solid-js@1.9.9)(vite@6.4.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@babel/core': 7.28.6 @@ -9618,6 +9264,22 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 + vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.55.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.4.0 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + tsx: 4.21.0 + yaml: 2.8.2 + vitefu@1.1.1(vite@6.4.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)): optionalDependencies: vite: 6.4.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)