diff --git a/.env.example b/.env.example index 32b67ce..d11d10e 100644 --- a/.env.example +++ b/.env.example @@ -56,6 +56,9 @@ AGENT_BROWSER_SNAPSHOT_ID= # Vercel Cron authenticates scheduled idle cleanup with this 32+ character server secret. CRON_SECRET= +# Optional independently rotatable operator secret for an immediate provider +# health receipt after a release. It never enters browser bundles or generated apps. +DROPS_PLATFORM_HEALTH_OPERATOR_SECRET= # Optional integer from 5 through 240; defaults to 20 minutes. DROPS_STUDIO_SANDBOX_IDLE_MINUTES= # Set to "1" only to opt into the live Sandbox contract test. @@ -110,6 +113,9 @@ DROPS_TEAM_INVITE_SECRET= # DROPS_MANAGED_DATA_PROVIDER accepts only "d1" or "postgres" in adapter wiring. DROPS_MANAGED_DATA_PROVIDER= DATABASE_URL= +# Vercel Marketplace Neon uses these prefixed server-only values. +DROPS_MANAGED_DATABASE_URL= +DROPS_MANAGED_POSTGRES_URL= DROPS_COLLABORATION_TRANSPORT_URL= # Generic enterprise OIDC. Values stay server-only and external login remains diff --git a/app/api/access/route.ts b/app/api/access/route.ts index add2143..fece9e6 100644 --- a/app/api/access/route.ts +++ b/app/api/access/route.ts @@ -65,6 +65,8 @@ export async function GET(request: NextRequest) { const access = accessMetadata({ tier: context.configured && readiness.available ? "guest" : "fallback", used: context.used, + projectSyncAvailable: context.configured + && memberProjectSyncReadiness(readinessEnvironment), }); const response = NextResponse.json( { diff --git a/app/api/platform/capabilities/route.ts b/app/api/platform/capabilities/route.ts index 0b1fb7f..ec75b7c 100644 --- a/app/api/platform/capabilities/route.ts +++ b/app/api/platform/capabilities/route.ts @@ -1,12 +1,12 @@ import { NextResponse } from "next/server.js"; -import { platformCapabilitySnapshot } from "@/lib/platform-capabilities"; +import { platformCapabilitySnapshotWithHealth } from "@/lib/platform-capabilities"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export async function GET() { - return NextResponse.json(platformCapabilitySnapshot(), { + return NextResponse.json(await platformCapabilitySnapshotWithHealth(), { status: 200, headers: { "cache-control": "private, no-store, max-age=0", diff --git a/app/api/platform/health/route.ts b/app/api/platform/health/route.ts new file mode 100644 index 0000000..55f55ae --- /dev/null +++ b/app/api/platform/health/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from "next/server.js"; +import { createHash, timingSafeEqual } from "node:crypto"; + +import { runPlatformProviderHealthChecks } from "@/lib/platform-provider-health"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; +export const maxDuration = 300; + +function digest(value: string): Buffer { + return createHash("sha256").update(value).digest(); +} + +function authorized(request: NextRequest): boolean { + const authorization = request.headers.get("authorization")?.trim(); + if (!authorization) return false; + const secrets = [ + process.env.CRON_SECRET?.trim(), + process.env.DROPS_PLATFORM_HEALTH_OPERATOR_SECRET?.trim(), + ].filter((value): value is string => Boolean(value)); + const presented = digest(authorization); + return secrets.some((secret) => timingSafeEqual( + presented, + digest(`Bearer ${secret}`), + )); +} + +export async function GET(request: NextRequest) { + if (!authorized(request)) { + return NextResponse.json( + { error: "Platform health authorization is required." }, + { status: 401, headers: { "cache-control": "private, no-store" } }, + ); + } + const receipt = await runPlatformProviderHealthChecks(); + return NextResponse.json(receipt, { + headers: { "cache-control": "private, no-store" }, + }); +} + +export const POST = GET; diff --git a/app/api/project-data/route.ts b/app/api/project-data/route.ts index 28d59f2..6e83a01 100644 --- a/app/api/project-data/route.ts +++ b/app/api/project-data/route.ts @@ -6,7 +6,9 @@ import { ProjectDataError, ProjectDataStore, authorizeProjectDataCapability, + createDurableProjectDataBackend, verifyProjectDataCapability, + type ProjectDataBackend, type ProjectDataCapabilityPayload, type ProjectDataPermission, } from "../../../lib/project-data/index.ts"; @@ -25,22 +27,30 @@ function json(payload: Record, status = 200): NextResponse { return NextResponse.json(payload, { status, headers: NO_STORE_HEADERS }); } -function backend() { +let backendPromise: Promise | null = null; + +async function backend() { if (globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__) { return globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__; } - if (process.env.DROPS_STUDIO_LOCAL_PROJECT_DATA !== "1") { + backendPromise ??= (async () => { + const durable = await createDurableProjectDataBackend(); + if (durable) return durable; + if (process.env.DROPS_STUDIO_LOCAL_PROJECT_DATA === "1") { + return new MemoryProjectDataBackend(); + } throw new ProjectDataError( "storage_unavailable", "Project data storage is not configured. The generated app can continue with its labelled browser-local fallback.", ); - } - globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = new MemoryProjectDataBackend(); - return globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__; + })(); + const resolved = await backendPromise; + globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = resolved; + return resolved; } -function store(): ProjectDataStore { - return new ProjectDataStore(backend()); +async function store(): Promise { + return new ProjectDataStore(await backend()); } function bearer(request: NextRequest): string { @@ -97,7 +107,20 @@ function requireSameOrigin(request: NextRequest): void { const origin = request.headers.get("origin"); if (!origin) throw new ProjectDataError("forbidden", "A same-origin project data mutation is required."); try { - if (new URL(origin).origin !== request.nextUrl.origin) throw new Error("origin mismatch"); + const host = request.headers.get("host")?.split(",")[0]?.trim(); + const protocol = + request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim().replace(/:$/, "") + || request.nextUrl.protocol.replace(/:$/, ""); + const visibleOrigin = host + ? new URL(`${protocol}://${host}`).origin + : request.nextUrl.origin; + const parsedOrigin = new URL(origin).origin; + if ( + parsedOrigin !== request.nextUrl.origin + && parsedOrigin !== visibleOrigin + ) { + throw new Error("origin mismatch"); + } } catch { throw new ProjectDataError("forbidden", "Cross-origin project data mutation rejected."); } @@ -170,12 +193,12 @@ export async function GET(request: NextRequest): Promise { exactFields(query, ["projectId", "namespace", "id"]); const { projectId, namespace } = scope(authorization, query, "read"); if (query.id) { - const document = await store().get(projectId, namespace, query.id); + const document = await (await store()).get(projectId, namespace, query.id); if (!document) throw new ProjectDataError("not_found", "Project data document was not found."); - return json({ document, persistence: backend().kind }); + return json({ document, persistence: (await backend()).kind }); } - const documents = await store().list(projectId, namespace); - return json({ documents, persistence: backend().kind }); + const documents = await (await store()).list(projectId, namespace); + return json({ documents, persistence: (await backend()).kind }); } catch (error) { return responseError(error); } @@ -189,8 +212,8 @@ export async function POST(request: NextRequest): Promise { const input = await requestBody(request); exactFields(input, ["projectId", "namespace", "id", "data"]); const { projectId, namespace } = scope(authorization, input, "write"); - const document = await store().create({ projectId, namespace, id: input.id, data: input.data }); - return json({ document, persistence: backend().kind }, 201); + const document = await (await store()).create({ projectId, namespace, id: input.id, data: input.data }); + return json({ document, persistence: (await backend()).kind }, 201); } catch (error) { return responseError(error); } @@ -204,14 +227,14 @@ export async function PUT(request: NextRequest): Promise { const input = await requestBody(request); exactFields(input, ["projectId", "namespace", "id", "expectedRevision", "data"]); const { projectId, namespace } = scope(authorization, input, "write"); - const document = await store().update({ + const document = await (await store()).update({ projectId, namespace, id: input.id, expectedRevision: input.expectedRevision, data: input.data, }); - return json({ document, persistence: backend().kind }); + return json({ document, persistence: (await backend()).kind }); } catch (error) { return responseError(error); } @@ -225,8 +248,8 @@ export async function DELETE(request: NextRequest): Promise { const input = await requestBody(request); exactFields(input, ["projectId", "namespace", "id", "expectedRevision"]); const { projectId, namespace } = scope(authorization, input, "delete"); - await store().delete(projectId, namespace, input.id, input.expectedRevision); - return json({ deleted: true, persistence: backend().kind }); + await (await store()).delete(projectId, namespace, input.id, input.expectedRevision); + return json({ deleted: true, persistence: (await backend()).kind }); } catch (error) { return responseError(error); } diff --git a/app/api/projects/v2/route.ts b/app/api/projects/v2/route.ts index 90c1540..2bcd4d9 100644 --- a/app/api/projects/v2/route.ts +++ b/app/api/projects/v2/route.ts @@ -87,7 +87,21 @@ function requireSameOrigin(request: NextRequest): void { const origin = request.headers.get("origin"); if (!origin && process.env.NODE_ENV !== "production") return; try { - if (!origin || new URL(origin).origin !== request.nextUrl.origin) throw new Error(); + const host = request.headers.get("host")?.split(",")[0]?.trim(); + const protocol = + request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim().replace(/:$/, "") + || request.nextUrl.protocol.replace(/:$/, ""); + const visibleOrigin = host ? `${protocol}://${host}` : request.nextUrl.origin; + const parsedOrigin = origin ? new URL(origin).origin : ""; + if ( + !parsedOrigin + || ( + parsedOrigin !== request.nextUrl.origin + && parsedOrigin !== visibleOrigin + ) + ) { + throw new Error(); + } } catch { throw new RouteError(403, { error: "A same-origin Project V2 mutation is required." }); } diff --git a/app/platform/page.tsx b/app/platform/page.tsx index 3e59095..05b4e64 100644 --- a/app/platform/page.tsx +++ b/app/platform/page.tsx @@ -3,11 +3,11 @@ import { Boxes, ShieldCheck } from "lucide-react"; import { PlatformOverview } from "@/components/platform/platform-overview"; import { PlatformShell } from "@/components/platform/platform-shell"; import { PageIntro, StatusBadge } from "@/components/platform/platform-ui"; -import { platformCapabilitySnapshot } from "@/lib/platform-capabilities"; +import { platformCapabilitySnapshotWithHealth } from "@/lib/platform-capabilities"; export const dynamic = "force-dynamic"; -export default function PlatformPage() { - const snapshot = platformCapabilitySnapshot(); +export default async function PlatformPage() { + const snapshot = await platformCapabilitySnapshotWithHealth(); return
Capability-aware UI

Working, local, and setup states stay distinct

Truthful states
} />
; } diff --git a/app/styles/drops-studio.builder.css b/app/styles/drops-studio.builder.css index 69221c7..7c2d16e 100644 --- a/app/styles/drops-studio.builder.css +++ b/app/styles/drops-studio.builder.css @@ -1,8 +1,12 @@ -.studio-grid { display: grid; gap: clamp(48px, 5vw, 84px); grid-template-columns: minmax(0, 1.05fr) minmax(460px, .83fr); margin: 0 auto; max-width: 1500px; padding: 74px 44px 96px; position: relative; z-index: 2; } -.builder-column { min-width: 0; } +.studio-grid { display: grid; gap: clamp(30px, 3.4vw, 58px); grid-template-columns: minmax(420px, .9fr) minmax(560px, 1.1fr); margin: 0 auto; max-width: 1600px; padding: 52px clamp(24px, 3.8vw, 44px) 88px; position: relative; z-index: 2; } +.builder-column { display: contents; } +.builder-primary { grid-column: 1; grid-row: 1; min-width: 0; } +.builder-column > .preset-section { grid-column: 1 / -1; grid-row: 2; min-width: 0; } +.builder-column > .setup-card { grid-column: 1 / -1; grid-row: 3; min-width: 0; } +.studio-grid > .preview-column { grid-column: 2; grid-row: 1; } .hero-copy { margin-bottom: 28px; } .eyebrow { align-items: center; color: var(--blue); display: flex; font-size: 12px; font-weight: 760; gap: 7px; letter-spacing: .13em; margin-bottom: 18px; } -.hero-copy h1 { font-size: clamp(48px, 5vw, 76px); letter-spacing: -.065em; line-height: .98; margin: 0; max-width: 790px; } +.hero-copy h1 { font-size: clamp(48px, 4.4vw, 68px); letter-spacing: -.065em; line-height: .98; margin: 0; max-width: 790px; } .hero-copy h1 span { color: var(--blue); } .hero-description { display: grid; gap: 2px; margin: 22px 0 0; max-width: 690px; } .hero-copy .hero-description p { color: var(--muted); font-size: 16px; line-height: 1.62; margin: 0; max-width: 690px; } diff --git a/app/styles/drops-studio.previews.css b/app/styles/drops-studio.previews.css index bf89856..e665873 100644 --- a/app/styles/drops-studio.previews.css +++ b/app/styles/drops-studio.previews.css @@ -446,3 +446,395 @@ .preview-game-native > footer { justify-self: center; } + +.landing-studio-frame { + background: #fff; + border: 1px solid #dce5f1; + border-radius: 20px; + box-shadow: 0 28px 80px rgba(39, 75, 139, .16); + overflow: hidden; +} + +.landing-studio-toolbar, +.landing-studio-statusbar { + align-items: center; + background: rgba(255, 255, 255, .97); + display: flex; + justify-content: space-between; +} + +.landing-studio-toolbar { + border-bottom: 1px solid #e4eaf3; + min-height: 58px; + padding: 8px 10px 8px 14px; +} + +.landing-studio-toolbar > div, +.landing-studio-actions, +.landing-studio-toolbar button, +.landing-copilot-title, +.landing-copilot-card, +.landing-studio-copilot > button, +.landing-studio-statusbar span { + align-items: center; + display: flex; +} + +.landing-studio-toolbar > div:first-child { + gap: 8px; + min-width: 0; +} + +.landing-studio-toolbar strong { + color: #0b1b38; + font-size: 13px; + letter-spacing: -.02em; +} + +.landing-studio-toolbar b { + color: #33435d; + font-size: 12px; + font-weight: 650; + max-width: 160px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.landing-studio-toolbar > div:first-child > span { + color: #a6b2c4; +} + +.landing-studio-actions { + gap: 7px; +} + +.landing-studio-toolbar button { + background: #fff; + border: 1px solid #dce4ef; + border-radius: 9px; + color: #31415b; + cursor: pointer; + font-size: 12px; + font-weight: 720; + gap: 6px; + min-height: 40px; + padding: 0 11px; +} + +.landing-studio-toolbar button:hover { + background: #f7faff; + border-color: #aec5fb; + color: #245fe5; +} + +.landing-studio-toolbar button.primary { + background: #245fe5; + border-color: #245fe5; + box-shadow: 0 8px 20px rgba(36, 95, 229, .24); + color: #fff; +} + +.landing-studio-toolbar button.primary:hover { + background: #194dca; + border-color: #194dca; +} + +.landing-studio-toolbar button svg, +.landing-studio-copilot > button svg, +.landing-studio-statusbar svg { + height: 14px; + width: 14px; +} + +.landing-studio-body { + display: grid; + grid-template-columns: 82px minmax(0, 1fr) 190px; + min-height: 510px; +} + +.landing-studio-rail { + background: #fbfdff; + border-right: 1px solid #e4eaf3; + display: flex; + flex-direction: column; + gap: 5px; + padding: 12px 8px; +} + +.landing-studio-rail span { + align-items: center; + border-radius: 8px; + color: #627089; + display: flex; + flex-direction: column; + font-size: 12px; + font-weight: 670; + gap: 4px; + justify-content: center; + min-height: 52px; + text-align: center; +} + +.landing-studio-rail span.active { + background: #eaf2ff; + color: #245fe5; +} + +.landing-studio-rail svg { + height: 16px; + width: 16px; +} + +.landing-studio-main { + background: #f4f8ff; + min-width: 0; + overflow: hidden; +} + +.landing-studio-address { + align-items: center; + background: #fff; + border-bottom: 1px solid #e4eaf3; + display: grid; + gap: 8px; + grid-template-columns: auto minmax(0, 1fr) auto; + min-height: 44px; + padding: 6px 10px; +} + +.landing-studio-address span { + align-items: center; + background: #edf8f3; + border-radius: 99px; + color: #087449; + display: flex; + font-size: 12px; + font-weight: 750; + gap: 5px; + padding: 4px 8px; +} + +.landing-studio-address span i { + background: #20b879; + border-radius: 50%; + height: 6px; + width: 6px; +} + +.landing-studio-address b { + background: #f7f9fc; + border: 1px solid #e3e9f1; + border-radius: 7px; + color: #637189; + font-size: 12px; + font-weight: 520; + overflow: hidden; + padding: 5px 8px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.landing-studio-address > svg { + color: #718099; + height: 16px; + width: 16px; +} + +.landing-studio-main > .preview-device { + border: 0; + border-radius: 0; + box-shadow: none; + height: 466px; + min-height: 0; +} + +.landing-studio-main > .preview-device::before { + inset-block-start: 72px; +} + +.landing-studio-main .preview-device-header { + height: 68px; + padding: 0 15px; +} + +.landing-studio-main .preview-stage { + min-height: 348px; + padding: 18px 12px 14px; +} + +.landing-studio-main .telegram-native .preview-stage { + min-height: 340px; + padding: 18px 16px 12px; +} + +.landing-studio-main .telegram-native .telegram-card { + border-radius: 16px; + padding: 18px; +} + +.landing-studio-main .game-native .preview-stage, +.landing-studio-main .catcher-game { + height: 398px; + min-height: 398px; +} + +.landing-studio-copilot { + background: #fbfdff; + border-left: 1px solid #e4eaf3; + display: flex; + flex-direction: column; + gap: 10px; + padding: 15px 12px; +} + +.landing-copilot-title { + color: #142545; + gap: 7px; +} + +.landing-copilot-title > svg { + color: #245fe5; + height: 17px; + width: 17px; +} + +.landing-copilot-title strong { + font-size: 13px; +} + +.landing-copilot-title span { + background: #edf3ff; + border-radius: 99px; + color: #245fe5; + font-size: 12px; + margin-left: auto; + padding: 3px 6px; +} + +.landing-studio-copilot > p { + color: #65738a; + font-size: 12px; + line-height: 1.5; + margin: 2px 0 4px; +} + +.landing-copilot-card { + background: #fff; + border: 1px solid #e1e8f3; + border-radius: 11px; + gap: 8px; + padding: 10px; +} + +.landing-copilot-card > svg { + color: #245fe5; + flex: 0 0 auto; + height: 16px; + width: 16px; +} + +.landing-copilot-card strong, +.landing-copilot-card span { + display: block; +} + +.landing-copilot-card strong { + color: #273850; + font-size: 12px; +} + +.landing-copilot-card span { + color: #637189; + font-size: 12px; + line-height: 1.4; + margin-top: 3px; +} + +.landing-studio-copilot > button { + background: #edf3ff; + border: 1px solid #cadbff; + border-radius: 10px; + color: #245fe5; + cursor: pointer; + font-size: 12px; + font-weight: 740; + gap: 6px; + justify-content: center; + margin-top: auto; + min-height: 44px; + padding: 0 9px; +} + +.landing-studio-copilot > button svg:last-child { + margin-left: auto; +} + +.landing-studio-statusbar { + border-top: 1px solid #e4eaf3; + color: #65738a; + font-size: 12px; + gap: 10px; + min-height: 40px; + padding: 7px 13px; +} + +.landing-studio-statusbar span { + gap: 5px; +} + +.landing-studio-statusbar span:first-child { + color: #087449; +} + +@media (max-width: 1260px) { + .landing-studio-body { + grid-template-columns: 70px minmax(0, 1fr); + } + + .landing-studio-copilot { + display: none; + } +} + +@media (max-width: 900px) { + .landing-studio-body { + grid-template-columns: 82px minmax(0, 1fr) 190px; + } + + .landing-studio-copilot { + display: flex; + } +} + +@media (max-width: 700px) { + .landing-studio-toolbar { + align-items: flex-start; + gap: 8px; + } + + .landing-studio-toolbar > div:first-child > span, + .landing-studio-toolbar > div:first-child > b, + .landing-studio-actions button:first-child, + .landing-studio-rail, + .landing-studio-copilot, + .landing-studio-statusbar span:not(:first-child) { + display: none; + } + + .landing-studio-body { + grid-template-columns: minmax(0, 1fr); + } + + .landing-studio-toolbar button { + padding-inline: 9px; + } + + .landing-studio-toolbar button.primary { + font-size: 12px; + } + + .landing-studio-statusbar { + justify-content: center; + } +} diff --git a/app/styles/drops-studio.responsive.css b/app/styles/drops-studio.responsive.css index e664923..0301ed5 100644 --- a/app/styles/drops-studio.responsive.css +++ b/app/styles/drops-studio.responsive.css @@ -1,5 +1,5 @@ @media (max-width: 1160px) { - .studio-grid { gap: 38px; grid-template-columns: minmax(0, 1fr) 440px; padding-left: 30px; padding-right: 30px; } + .studio-grid { grid-template-columns: minmax(0, .9fr) minmax(540px, 1.1fr); } .hero-copy h1 { font-size: 52px; } .field-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .tool-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -18,7 +18,11 @@ .mobile-menu { display: flex; } .api-vault-button { display: none; } .studio-grid { display: flex; flex-direction: column; padding: 52px 22px 72px; } + .builder-column { display: contents; } + .builder-primary { order: 1; } .preview-column { order: 2; position: static; } + .builder-column > .preset-section { order: 3; } + .builder-column > .setup-card { order: 4; } .preview-device { margin: 0 auto; max-width: 580px; } .preview-status-row { margin-left: auto; margin-right: auto; max-width: 580px; } .preview-footnote { margin-left: auto; margin-right: auto; max-width: 580px; } diff --git a/app/styles/drops-studio.setup.css b/app/styles/drops-studio.setup.css index a115c23..80ce857 100644 --- a/app/styles/drops-studio.setup.css +++ b/app/styles/drops-studio.setup.css @@ -27,9 +27,11 @@ .blueprint-boundary strong { color: #2c3c56; } .config-field { min-width: 0; } .config-field > span { color: #738096; display: block; font-size: 14px; font-weight: 780; letter-spacing: .08em; margin: 0 0 6px 2px; } -.field-select { align-items: center; background: #f7f9fc; border: 1px solid #dfe6f0; border-radius: 10px; cursor: pointer; display: flex; font-size: 14px; font-weight: 610; gap: 6px; height: 44px; justify-content: space-between; max-width: 100%; min-width: 44px; padding: 0 11px; width: 100%; } +.field-select-wrap { position: relative; } +.field-select-wrap > svg { color: #60708c; height: 16px; pointer-events: none; position: absolute; right: 11px; top: 14px; width: 16px; } +.field-select { appearance: none; align-items: center; background: #f7f9fc; border: 1px solid #dfe6f0; border-radius: 10px; color: #14213a; cursor: pointer; display: flex; font-family: inherit; font-size: 14px; font-weight: 610; gap: 6px; height: 44px; justify-content: space-between; max-width: 100%; min-width: 44px; padding: 0 36px 0 11px; width: 100%; } .field-select:hover { border-color: #b8caff; } -.field-select > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.field-select:focus-visible { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(49, 108, 255, .16); outline: 0; } .select-content { animation: select-in .14s ease-out; background: white; border: 1px solid #dfe5ef; border-radius: 12px; box-shadow: 0 18px 45px rgba(28, 49, 87, .17); min-width: var(--radix-select-trigger-width); overflow: hidden; padding: 5px; z-index: 100; } @keyframes select-in { from { opacity: 0; transform: translateY(-4px); } } .select-item { align-items: center; border-radius: 8px; cursor: pointer; display: flex; font-size: 14px; justify-content: space-between; min-height: 44px; min-width: 44px; outline: 0; padding: 8px 10px; } diff --git a/app/styles/drops-studio.shell.css b/app/styles/drops-studio.shell.css index 737c642..4b51221 100644 --- a/app/styles/drops-studio.shell.css +++ b/app/styles/drops-studio.shell.css @@ -9,7 +9,7 @@ background: rgba(251, 253, 255, .88); border-bottom: 1px solid rgba(218, 227, 240, .8); display: flex; - height: 88px; + height: 72px; justify-content: space-between; padding: 0 max(32px, calc((100vw - 1500px) / 2)); position: relative; diff --git a/components/drops-studio-setup.tsx b/components/drops-studio-setup.tsx index 6119284..5a6d8e7 100644 --- a/components/drops-studio-setup.tsx +++ b/components/drops-studio-setup.tsx @@ -7,6 +7,7 @@ import { BadgeCheck, BrainCircuit, Check, + ChevronDown, Cloud, Code2, LoaderCircle, @@ -14,7 +15,6 @@ import { Sparkles, type LucideIcon, } from "lucide-react"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import type { Preset } from "@/lib/presets"; import type { GeneratedProjectSpec } from "@/lib/project-types"; @@ -65,23 +65,21 @@ function SelectControl({ ariaLabel: string; }) { return ( - onChange(event.currentTarget.value)} + > {options.map((option) => ( - + + ))} - - + +