From 51e9979457bce0a6b528f1746fa99d09e48bb51b Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 9 Oct 2025 16:01:52 -0400 Subject: [PATCH 01/34] wip: zen nav bar --- packages/console/app/src/lib/beta.ts | 7 - packages/console/app/src/routes/workspace.tsx | 16 +- .../console/app/src/routes/workspace/[id].css | 160 ++++++------------ .../console/app/src/routes/workspace/[id].tsx | 89 +++------- .../billing}/billing-section.module.css | 0 .../{ => [id]/billing}/billing-section.tsx | 0 .../routes/workspace/[id]/billing/index.css | 116 +++++++++++++ .../routes/workspace/[id]/billing/index.tsx | 24 +++ .../billing}/monthly-limit-section.module.css | 0 .../billing}/monthly-limit-section.tsx | 0 .../billing}/payment-section.module.css | 0 .../{ => [id]/billing}/payment-section.tsx | 10 +- .../app/src/routes/workspace/[id]/common.tsx | 25 +++ .../app/src/routes/workspace/[id]/index.css | 116 +++++++++++++ .../app/src/routes/workspace/[id]/index.tsx | 39 +++++ .../src/routes/workspace/[id]/keys/index.css | 116 +++++++++++++ .../src/routes/workspace/[id]/keys/index.tsx | 12 ++ .../{ => [id]/keys}/key-section.module.css | 0 .../workspace/{ => [id]/keys}/key-section.tsx | 2 +- .../routes/workspace/[id]/members/index.css | 116 +++++++++++++ .../routes/workspace/[id]/members/index.tsx | 12 ++ .../members}/member-section.module.css | 0 .../{ => [id]/members}/member-section.tsx | 0 .../{ => [id]}/model-section.module.css | 0 .../workspace/{ => [id]}/model-section.tsx | 0 .../{ => [id]}/new-user-section.module.css | 0 .../workspace/{ => [id]}/new-user-section.tsx | 0 .../{ => [id]}/provider-section.module.css | 0 .../workspace/{ => [id]}/provider-section.tsx | 0 .../routes/workspace/[id]/settings/index.css | 116 +++++++++++++ .../routes/workspace/[id]/settings/index.tsx | 12 ++ .../settings}/settings-section.module.css | 0 .../{ => [id]/settings}/settings-section.tsx | 0 .../{ => [id]}/usage-section.module.css | 0 .../workspace/{ => [id]}/usage-section.tsx | 0 .../app/src/routes/workspace/common.tsx | 37 ++-- .../app/src/routes/workspace/index.tsx | 0 37 files changed, 811 insertions(+), 214 deletions(-) delete mode 100644 packages/console/app/src/lib/beta.ts rename packages/console/app/src/routes/workspace/{ => [id]/billing}/billing-section.module.css (100%) rename packages/console/app/src/routes/workspace/{ => [id]/billing}/billing-section.tsx (100%) create mode 100644 packages/console/app/src/routes/workspace/[id]/billing/index.css create mode 100644 packages/console/app/src/routes/workspace/[id]/billing/index.tsx rename packages/console/app/src/routes/workspace/{ => [id]/billing}/monthly-limit-section.module.css (100%) rename packages/console/app/src/routes/workspace/{ => [id]/billing}/monthly-limit-section.tsx (100%) rename packages/console/app/src/routes/workspace/{ => [id]/billing}/payment-section.module.css (100%) rename packages/console/app/src/routes/workspace/{ => [id]/billing}/payment-section.tsx (95%) create mode 100644 packages/console/app/src/routes/workspace/[id]/common.tsx create mode 100644 packages/console/app/src/routes/workspace/[id]/index.css create mode 100644 packages/console/app/src/routes/workspace/[id]/index.tsx create mode 100644 packages/console/app/src/routes/workspace/[id]/keys/index.css create mode 100644 packages/console/app/src/routes/workspace/[id]/keys/index.tsx rename packages/console/app/src/routes/workspace/{ => [id]/keys}/key-section.module.css (100%) rename packages/console/app/src/routes/workspace/{ => [id]/keys}/key-section.tsx (99%) create mode 100644 packages/console/app/src/routes/workspace/[id]/members/index.css create mode 100644 packages/console/app/src/routes/workspace/[id]/members/index.tsx rename packages/console/app/src/routes/workspace/{ => [id]/members}/member-section.module.css (100%) rename packages/console/app/src/routes/workspace/{ => [id]/members}/member-section.tsx (100%) rename packages/console/app/src/routes/workspace/{ => [id]}/model-section.module.css (100%) rename packages/console/app/src/routes/workspace/{ => [id]}/model-section.tsx (100%) rename packages/console/app/src/routes/workspace/{ => [id]}/new-user-section.module.css (100%) rename packages/console/app/src/routes/workspace/{ => [id]}/new-user-section.tsx (100%) rename packages/console/app/src/routes/workspace/{ => [id]}/provider-section.module.css (100%) rename packages/console/app/src/routes/workspace/{ => [id]}/provider-section.tsx (100%) create mode 100644 packages/console/app/src/routes/workspace/[id]/settings/index.css create mode 100644 packages/console/app/src/routes/workspace/[id]/settings/index.tsx rename packages/console/app/src/routes/workspace/{ => [id]/settings}/settings-section.module.css (100%) rename packages/console/app/src/routes/workspace/{ => [id]/settings}/settings-section.tsx (100%) rename packages/console/app/src/routes/workspace/{ => [id]}/usage-section.module.css (100%) rename packages/console/app/src/routes/workspace/{ => [id]}/usage-section.tsx (100%) delete mode 100644 packages/console/app/src/routes/workspace/index.tsx diff --git a/packages/console/app/src/lib/beta.ts b/packages/console/app/src/lib/beta.ts deleted file mode 100644 index d60a735e..00000000 --- a/packages/console/app/src/lib/beta.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { query } from "@solidjs/router" -import { Resource } from "@opencode-ai/console-resource" - -export const beta = query(async (workspaceID?: string) => { - "use server" - return Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true -}, "beta") diff --git a/packages/console/app/src/routes/workspace.tsx b/packages/console/app/src/routes/workspace.tsx index ac394f58..f87123d3 100644 --- a/packages/console/app/src/routes/workspace.tsx +++ b/packages/console/app/src/routes/workspace.tsx @@ -8,16 +8,16 @@ import { WorkspacePicker } from "./workspace-picker" import { withActor } from "~/context/auth.withActor" import { User } from "@opencode-ai/console-core/user.js" import { Actor } from "@opencode-ai/console-core/actor.js" -import { beta } from "~/lib/beta" +import { querySessionInfo } from "./workspace/common" -const getUserInfo = query(async (workspaceID: string) => { +const getUserEmail = query(async (workspaceID: string) => { "use server" return withActor(async () => { const actor = Actor.assert("user") const email = await User.getAccountEmail(actor.properties.userID) - return { email } + return email }, workspaceID) -}, "userInfo") +}, "userEmail") const logout = action(async () => { "use server" @@ -37,8 +37,8 @@ const logout = action(async () => { export default function WorkspaceLayout(props: RouteSectionProps) { const params = useParams() - const userInfo = createAsync(() => getUserInfo(params.id)) - const isBeta = createAsync(() => beta(params.id)) + const userEmail = createAsync(() => getUserEmail(params.id)) + const sessionInfo = createAsync(() => querySessionInfo(params.id)) return (
@@ -48,10 +48,10 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
- + - {userInfo()?.email} + {userEmail()}
) } diff --git a/packages/console/app/src/routes/workspace/billing-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/billing-section.module.css similarity index 100% rename from packages/console/app/src/routes/workspace/billing-section.module.css rename to packages/console/app/src/routes/workspace/[id]/billing/billing-section.module.css diff --git a/packages/console/app/src/routes/workspace/billing-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx similarity index 100% rename from packages/console/app/src/routes/workspace/billing-section.tsx rename to packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.css b/packages/console/app/src/routes/workspace/[id]/billing/index.css new file mode 100644 index 00000000..5124c78c --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/billing/index.css @@ -0,0 +1,116 @@ +[data-page="workspace-[id]"] { + max-width: 64rem; + padding: var(--space-10) var(--space-4); + margin: 0 auto; + width: 100%; + display: flex; + flex-direction: column; + gap: var(--space-10); + + @media (max-width: 30rem) { + padding-top: var(--space-4); + padding-bottom: var(--space-4); + + gap: var(--space-8); + } + + [data-slot="sections"] { + display: flex; + flex-direction: column; + gap: var(--space-16); + + @media (max-width: 30rem) { + gap: var(--space-8); + } + + section { + display: flex; + flex-direction: column; + gap: var(--space-8); + + @media (max-width: 30rem) { + gap: var(--space-6); + } + + /* Section titles */ + [data-slot="section-title"] { + display: flex; + flex-direction: column; + gap: var(--space-1); + + h2 { + font-size: var(--font-size-md); + font-weight: 600; + line-height: 1.2; + letter-spacing: -0.03125rem; + margin: 0; + color: var(--color-text-secondary); + text-transform: uppercase; + + @media (max-width: 30rem) { + font-size: var(--font-size-md); + } + } + + p { + line-height: 1.5; + font-size: var(--font-size-md); + color: var(--color-text-muted); + + a { + color: var(--color-text-muted); + } + + @media (max-width: 30rem) { + font-size: var(--font-size-sm); + } + } + } + } + + section:not(:last-child) { + border-bottom: 1px solid var(--color-border); + padding-bottom: var(--space-16); + + @media (max-width: 30rem) { + padding-bottom: var(--space-8); + } + } + } + + /* Title section */ + [data-component="title-section"] { + display: flex; + flex-direction: column; + gap: var(--space-2); + padding-bottom: var(--space-8); + border-bottom: 1px solid var(--color-border); + + @media (max-width: 30rem) { + padding-bottom: var(--space-6); + } + + h1 { + font-size: var(--font-size-2xl); + font-weight: 500; + line-height: 1.2; + letter-spacing: -0.03125rem; + margin: 0; + text-transform: uppercase; + + @media (max-width: 30rem) { + font-size: var(--font-size-xl); + } + } + + p { + line-height: 1.5; + font-size: var(--font-size-md); + color: var(--color-text-muted); + + a { + color: var(--color-text-muted); + } + } + } +} \ No newline at end of file diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx new file mode 100644 index 00000000..913d4f92 --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx @@ -0,0 +1,24 @@ +import "./index.css" +import { MonthlyLimitSection } from "./monthly-limit-section" +import { BillingSection } from "./billing-section" +import { PaymentSection } from "./payment-section" +import { Show } from "solid-js" +import { createAsync, useParams } from "@solidjs/router" +import { querySessionInfo } from "../../common" + +export default function () { + const params = useParams() + const userInfo = createAsync(() => querySessionInfo(params.id)) + + return ( +
+
+ + + + + +
+
+ ) +} diff --git a/packages/console/app/src/routes/workspace/monthly-limit-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.module.css similarity index 100% rename from packages/console/app/src/routes/workspace/monthly-limit-section.module.css rename to packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.module.css diff --git a/packages/console/app/src/routes/workspace/monthly-limit-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx similarity index 100% rename from packages/console/app/src/routes/workspace/monthly-limit-section.tsx rename to packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx diff --git a/packages/console/app/src/routes/workspace/payment-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/payment-section.module.css similarity index 100% rename from packages/console/app/src/routes/workspace/payment-section.module.css rename to packages/console/app/src/routes/workspace/[id]/billing/payment-section.module.css diff --git a/packages/console/app/src/routes/workspace/payment-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx similarity index 95% rename from packages/console/app/src/routes/workspace/payment-section.tsx rename to packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx index c35a5066..d3520bea 100644 --- a/packages/console/app/src/routes/workspace/payment-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx @@ -1,8 +1,8 @@ import { Billing } from "@opencode-ai/console-core/billing.js" import { query, action, useParams, createAsync, useAction } from "@solidjs/router" -import { For } from "solid-js" +import { For, Show } from "solid-js" import { withActor } from "~/context/auth.withActor" -import { formatDateUTC, formatDateForTable } from "./common" +import { formatDateUTC, formatDateForTable } from "../common" import styles from "./payment-section.module.css" const getPaymentsInfo = query(async (workspaceID: string) => { @@ -19,7 +19,6 @@ const downloadReceipt = action(async (workspaceID: string, paymentID: string) => export function PaymentSection() { const params = useParams() - // ORIGINAL CODE - COMMENTED OUT FOR TESTING const payments = createAsync(() => getPaymentsInfo(params.id)) const downloadReceiptAction = useAction(downloadReceipt) @@ -58,8 +57,7 @@ export function PaymentSection() { // ] return ( - payments() && - payments()!.length > 0 && ( + 0}>

Payments History

@@ -109,6 +107,6 @@ export function PaymentSection() {
- ) +
) } diff --git a/packages/console/app/src/routes/workspace/[id]/common.tsx b/packages/console/app/src/routes/workspace/[id]/common.tsx new file mode 100644 index 00000000..f85fd842 --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/common.tsx @@ -0,0 +1,25 @@ +export function formatDateForTable(date: Date) { + const options: Intl.DateTimeFormatOptions = { + day: "numeric", + month: "short", + hour: "numeric", + minute: "2-digit", + hour12: true, + } + return date.toLocaleDateString("en-GB", options).replace(",", ",") +} + +export function formatDateUTC(date: Date) { + const options: Intl.DateTimeFormatOptions = { + weekday: "short", + year: "numeric", + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + second: "2-digit", + timeZoneName: "short", + timeZone: "UTC", + } + return date.toLocaleDateString("en-US", options) +} diff --git a/packages/console/app/src/routes/workspace/[id]/index.css b/packages/console/app/src/routes/workspace/[id]/index.css new file mode 100644 index 00000000..5124c78c --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/index.css @@ -0,0 +1,116 @@ +[data-page="workspace-[id]"] { + max-width: 64rem; + padding: var(--space-10) var(--space-4); + margin: 0 auto; + width: 100%; + display: flex; + flex-direction: column; + gap: var(--space-10); + + @media (max-width: 30rem) { + padding-top: var(--space-4); + padding-bottom: var(--space-4); + + gap: var(--space-8); + } + + [data-slot="sections"] { + display: flex; + flex-direction: column; + gap: var(--space-16); + + @media (max-width: 30rem) { + gap: var(--space-8); + } + + section { + display: flex; + flex-direction: column; + gap: var(--space-8); + + @media (max-width: 30rem) { + gap: var(--space-6); + } + + /* Section titles */ + [data-slot="section-title"] { + display: flex; + flex-direction: column; + gap: var(--space-1); + + h2 { + font-size: var(--font-size-md); + font-weight: 600; + line-height: 1.2; + letter-spacing: -0.03125rem; + margin: 0; + color: var(--color-text-secondary); + text-transform: uppercase; + + @media (max-width: 30rem) { + font-size: var(--font-size-md); + } + } + + p { + line-height: 1.5; + font-size: var(--font-size-md); + color: var(--color-text-muted); + + a { + color: var(--color-text-muted); + } + + @media (max-width: 30rem) { + font-size: var(--font-size-sm); + } + } + } + } + + section:not(:last-child) { + border-bottom: 1px solid var(--color-border); + padding-bottom: var(--space-16); + + @media (max-width: 30rem) { + padding-bottom: var(--space-8); + } + } + } + + /* Title section */ + [data-component="title-section"] { + display: flex; + flex-direction: column; + gap: var(--space-2); + padding-bottom: var(--space-8); + border-bottom: 1px solid var(--color-border); + + @media (max-width: 30rem) { + padding-bottom: var(--space-6); + } + + h1 { + font-size: var(--font-size-2xl); + font-weight: 500; + line-height: 1.2; + letter-spacing: -0.03125rem; + margin: 0; + text-transform: uppercase; + + @media (max-width: 30rem) { + font-size: var(--font-size-xl); + } + } + + p { + line-height: 1.5; + font-size: var(--font-size-md); + color: var(--color-text-muted); + + a { + color: var(--color-text-muted); + } + } + } +} \ No newline at end of file diff --git a/packages/console/app/src/routes/workspace/[id]/index.tsx b/packages/console/app/src/routes/workspace/[id]/index.tsx new file mode 100644 index 00000000..1345bf40 --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/index.tsx @@ -0,0 +1,39 @@ +import "./index.css" +import { NewUserSection } from "./new-user-section" +import { UsageSection } from "./usage-section" +import { MemberSection } from "./members/member-section" +import { SettingsSection } from "./settings/settings-section" +import { ModelSection } from "./model-section" +import { ProviderSection } from "./provider-section" +import { Show } from "solid-js" +import { createAsync, useParams } from "@solidjs/router" +import { querySessionInfo } from "../common" + +export default function () { + const params = useParams() + const userInfo = createAsync(() => querySessionInfo(params.id)) + + return ( +
+
+

Zen

+

+ Curated list of models provided by opencode.{" "} + + Learn more + + . +

+
+ +
+ + + + + + +
+
+ ) +} diff --git a/packages/console/app/src/routes/workspace/[id]/keys/index.css b/packages/console/app/src/routes/workspace/[id]/keys/index.css new file mode 100644 index 00000000..5124c78c --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/keys/index.css @@ -0,0 +1,116 @@ +[data-page="workspace-[id]"] { + max-width: 64rem; + padding: var(--space-10) var(--space-4); + margin: 0 auto; + width: 100%; + display: flex; + flex-direction: column; + gap: var(--space-10); + + @media (max-width: 30rem) { + padding-top: var(--space-4); + padding-bottom: var(--space-4); + + gap: var(--space-8); + } + + [data-slot="sections"] { + display: flex; + flex-direction: column; + gap: var(--space-16); + + @media (max-width: 30rem) { + gap: var(--space-8); + } + + section { + display: flex; + flex-direction: column; + gap: var(--space-8); + + @media (max-width: 30rem) { + gap: var(--space-6); + } + + /* Section titles */ + [data-slot="section-title"] { + display: flex; + flex-direction: column; + gap: var(--space-1); + + h2 { + font-size: var(--font-size-md); + font-weight: 600; + line-height: 1.2; + letter-spacing: -0.03125rem; + margin: 0; + color: var(--color-text-secondary); + text-transform: uppercase; + + @media (max-width: 30rem) { + font-size: var(--font-size-md); + } + } + + p { + line-height: 1.5; + font-size: var(--font-size-md); + color: var(--color-text-muted); + + a { + color: var(--color-text-muted); + } + + @media (max-width: 30rem) { + font-size: var(--font-size-sm); + } + } + } + } + + section:not(:last-child) { + border-bottom: 1px solid var(--color-border); + padding-bottom: var(--space-16); + + @media (max-width: 30rem) { + padding-bottom: var(--space-8); + } + } + } + + /* Title section */ + [data-component="title-section"] { + display: flex; + flex-direction: column; + gap: var(--space-2); + padding-bottom: var(--space-8); + border-bottom: 1px solid var(--color-border); + + @media (max-width: 30rem) { + padding-bottom: var(--space-6); + } + + h1 { + font-size: var(--font-size-2xl); + font-weight: 500; + line-height: 1.2; + letter-spacing: -0.03125rem; + margin: 0; + text-transform: uppercase; + + @media (max-width: 30rem) { + font-size: var(--font-size-xl); + } + } + + p { + line-height: 1.5; + font-size: var(--font-size-md); + color: var(--color-text-muted); + + a { + color: var(--color-text-muted); + } + } + } +} \ No newline at end of file diff --git a/packages/console/app/src/routes/workspace/[id]/keys/index.tsx b/packages/console/app/src/routes/workspace/[id]/keys/index.tsx new file mode 100644 index 00000000..0fd3cdbd --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/keys/index.tsx @@ -0,0 +1,12 @@ +import "./index.css" +import { KeySection } from "./key-section" + +export default function () { + return ( +
+
+ +
+
+ ) +} diff --git a/packages/console/app/src/routes/workspace/key-section.module.css b/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css similarity index 100% rename from packages/console/app/src/routes/workspace/key-section.module.css rename to packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css diff --git a/packages/console/app/src/routes/workspace/key-section.tsx b/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx similarity index 99% rename from packages/console/app/src/routes/workspace/key-section.tsx rename to packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx index 3b7e399a..22b82ae0 100644 --- a/packages/console/app/src/routes/workspace/key-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx @@ -4,7 +4,7 @@ import { IconCopy, IconCheck } from "~/component/icon" import { Key } from "@opencode-ai/console-core/key.js" import { withActor } from "~/context/auth.withActor" import { createStore } from "solid-js/store" -import { formatDateUTC, formatDateForTable } from "./common" +import { formatDateUTC, formatDateForTable } from "../common" import styles from "./key-section.module.css" import { Actor } from "@opencode-ai/console-core/actor.js" diff --git a/packages/console/app/src/routes/workspace/[id]/members/index.css b/packages/console/app/src/routes/workspace/[id]/members/index.css new file mode 100644 index 00000000..5124c78c --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/members/index.css @@ -0,0 +1,116 @@ +[data-page="workspace-[id]"] { + max-width: 64rem; + padding: var(--space-10) var(--space-4); + margin: 0 auto; + width: 100%; + display: flex; + flex-direction: column; + gap: var(--space-10); + + @media (max-width: 30rem) { + padding-top: var(--space-4); + padding-bottom: var(--space-4); + + gap: var(--space-8); + } + + [data-slot="sections"] { + display: flex; + flex-direction: column; + gap: var(--space-16); + + @media (max-width: 30rem) { + gap: var(--space-8); + } + + section { + display: flex; + flex-direction: column; + gap: var(--space-8); + + @media (max-width: 30rem) { + gap: var(--space-6); + } + + /* Section titles */ + [data-slot="section-title"] { + display: flex; + flex-direction: column; + gap: var(--space-1); + + h2 { + font-size: var(--font-size-md); + font-weight: 600; + line-height: 1.2; + letter-spacing: -0.03125rem; + margin: 0; + color: var(--color-text-secondary); + text-transform: uppercase; + + @media (max-width: 30rem) { + font-size: var(--font-size-md); + } + } + + p { + line-height: 1.5; + font-size: var(--font-size-md); + color: var(--color-text-muted); + + a { + color: var(--color-text-muted); + } + + @media (max-width: 30rem) { + font-size: var(--font-size-sm); + } + } + } + } + + section:not(:last-child) { + border-bottom: 1px solid var(--color-border); + padding-bottom: var(--space-16); + + @media (max-width: 30rem) { + padding-bottom: var(--space-8); + } + } + } + + /* Title section */ + [data-component="title-section"] { + display: flex; + flex-direction: column; + gap: var(--space-2); + padding-bottom: var(--space-8); + border-bottom: 1px solid var(--color-border); + + @media (max-width: 30rem) { + padding-bottom: var(--space-6); + } + + h1 { + font-size: var(--font-size-2xl); + font-weight: 500; + line-height: 1.2; + letter-spacing: -0.03125rem; + margin: 0; + text-transform: uppercase; + + @media (max-width: 30rem) { + font-size: var(--font-size-xl); + } + } + + p { + line-height: 1.5; + font-size: var(--font-size-md); + color: var(--color-text-muted); + + a { + color: var(--color-text-muted); + } + } + } +} \ No newline at end of file diff --git a/packages/console/app/src/routes/workspace/[id]/members/index.tsx b/packages/console/app/src/routes/workspace/[id]/members/index.tsx new file mode 100644 index 00000000..5845e144 --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/members/index.tsx @@ -0,0 +1,12 @@ +import "./index.css" +import { MemberSection } from "./member-section" + +export default function () { + return ( +
+
+ +
+
+ ) +} diff --git a/packages/console/app/src/routes/workspace/member-section.module.css b/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css similarity index 100% rename from packages/console/app/src/routes/workspace/member-section.module.css rename to packages/console/app/src/routes/workspace/[id]/members/member-section.module.css diff --git a/packages/console/app/src/routes/workspace/member-section.tsx b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx similarity index 100% rename from packages/console/app/src/routes/workspace/member-section.tsx rename to packages/console/app/src/routes/workspace/[id]/members/member-section.tsx diff --git a/packages/console/app/src/routes/workspace/model-section.module.css b/packages/console/app/src/routes/workspace/[id]/model-section.module.css similarity index 100% rename from packages/console/app/src/routes/workspace/model-section.module.css rename to packages/console/app/src/routes/workspace/[id]/model-section.module.css diff --git a/packages/console/app/src/routes/workspace/model-section.tsx b/packages/console/app/src/routes/workspace/[id]/model-section.tsx similarity index 100% rename from packages/console/app/src/routes/workspace/model-section.tsx rename to packages/console/app/src/routes/workspace/[id]/model-section.tsx diff --git a/packages/console/app/src/routes/workspace/new-user-section.module.css b/packages/console/app/src/routes/workspace/[id]/new-user-section.module.css similarity index 100% rename from packages/console/app/src/routes/workspace/new-user-section.module.css rename to packages/console/app/src/routes/workspace/[id]/new-user-section.module.css diff --git a/packages/console/app/src/routes/workspace/new-user-section.tsx b/packages/console/app/src/routes/workspace/[id]/new-user-section.tsx similarity index 100% rename from packages/console/app/src/routes/workspace/new-user-section.tsx rename to packages/console/app/src/routes/workspace/[id]/new-user-section.tsx diff --git a/packages/console/app/src/routes/workspace/provider-section.module.css b/packages/console/app/src/routes/workspace/[id]/provider-section.module.css similarity index 100% rename from packages/console/app/src/routes/workspace/provider-section.module.css rename to packages/console/app/src/routes/workspace/[id]/provider-section.module.css diff --git a/packages/console/app/src/routes/workspace/provider-section.tsx b/packages/console/app/src/routes/workspace/[id]/provider-section.tsx similarity index 100% rename from packages/console/app/src/routes/workspace/provider-section.tsx rename to packages/console/app/src/routes/workspace/[id]/provider-section.tsx diff --git a/packages/console/app/src/routes/workspace/[id]/settings/index.css b/packages/console/app/src/routes/workspace/[id]/settings/index.css new file mode 100644 index 00000000..5124c78c --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/settings/index.css @@ -0,0 +1,116 @@ +[data-page="workspace-[id]"] { + max-width: 64rem; + padding: var(--space-10) var(--space-4); + margin: 0 auto; + width: 100%; + display: flex; + flex-direction: column; + gap: var(--space-10); + + @media (max-width: 30rem) { + padding-top: var(--space-4); + padding-bottom: var(--space-4); + + gap: var(--space-8); + } + + [data-slot="sections"] { + display: flex; + flex-direction: column; + gap: var(--space-16); + + @media (max-width: 30rem) { + gap: var(--space-8); + } + + section { + display: flex; + flex-direction: column; + gap: var(--space-8); + + @media (max-width: 30rem) { + gap: var(--space-6); + } + + /* Section titles */ + [data-slot="section-title"] { + display: flex; + flex-direction: column; + gap: var(--space-1); + + h2 { + font-size: var(--font-size-md); + font-weight: 600; + line-height: 1.2; + letter-spacing: -0.03125rem; + margin: 0; + color: var(--color-text-secondary); + text-transform: uppercase; + + @media (max-width: 30rem) { + font-size: var(--font-size-md); + } + } + + p { + line-height: 1.5; + font-size: var(--font-size-md); + color: var(--color-text-muted); + + a { + color: var(--color-text-muted); + } + + @media (max-width: 30rem) { + font-size: var(--font-size-sm); + } + } + } + } + + section:not(:last-child) { + border-bottom: 1px solid var(--color-border); + padding-bottom: var(--space-16); + + @media (max-width: 30rem) { + padding-bottom: var(--space-8); + } + } + } + + /* Title section */ + [data-component="title-section"] { + display: flex; + flex-direction: column; + gap: var(--space-2); + padding-bottom: var(--space-8); + border-bottom: 1px solid var(--color-border); + + @media (max-width: 30rem) { + padding-bottom: var(--space-6); + } + + h1 { + font-size: var(--font-size-2xl); + font-weight: 500; + line-height: 1.2; + letter-spacing: -0.03125rem; + margin: 0; + text-transform: uppercase; + + @media (max-width: 30rem) { + font-size: var(--font-size-xl); + } + } + + p { + line-height: 1.5; + font-size: var(--font-size-md); + color: var(--color-text-muted); + + a { + color: var(--color-text-muted); + } + } + } +} \ No newline at end of file diff --git a/packages/console/app/src/routes/workspace/[id]/settings/index.tsx b/packages/console/app/src/routes/workspace/[id]/settings/index.tsx new file mode 100644 index 00000000..972154aa --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/settings/index.tsx @@ -0,0 +1,12 @@ +import "./index.css" +import { SettingsSection } from "./settings-section" + +export default function () { + return ( +
+
+ +
+
+ ) +} diff --git a/packages/console/app/src/routes/workspace/settings-section.module.css b/packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css similarity index 100% rename from packages/console/app/src/routes/workspace/settings-section.module.css rename to packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css diff --git a/packages/console/app/src/routes/workspace/settings-section.tsx b/packages/console/app/src/routes/workspace/[id]/settings/settings-section.tsx similarity index 100% rename from packages/console/app/src/routes/workspace/settings-section.tsx rename to packages/console/app/src/routes/workspace/[id]/settings/settings-section.tsx diff --git a/packages/console/app/src/routes/workspace/usage-section.module.css b/packages/console/app/src/routes/workspace/[id]/usage-section.module.css similarity index 100% rename from packages/console/app/src/routes/workspace/usage-section.module.css rename to packages/console/app/src/routes/workspace/[id]/usage-section.module.css diff --git a/packages/console/app/src/routes/workspace/usage-section.tsx b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx similarity index 100% rename from packages/console/app/src/routes/workspace/usage-section.tsx rename to packages/console/app/src/routes/workspace/[id]/usage-section.tsx diff --git a/packages/console/app/src/routes/workspace/common.tsx b/packages/console/app/src/routes/workspace/common.tsx index f85fd842..d1f1aba8 100644 --- a/packages/console/app/src/routes/workspace/common.tsx +++ b/packages/console/app/src/routes/workspace/common.tsx @@ -1,25 +1,14 @@ -export function formatDateForTable(date: Date) { - const options: Intl.DateTimeFormatOptions = { - day: "numeric", - month: "short", - hour: "numeric", - minute: "2-digit", - hour12: true, - } - return date.toLocaleDateString("en-GB", options).replace(",", ",") -} +import { Resource } from "@opencode-ai/console-resource" +import { Actor } from "@opencode-ai/console-core/actor.js" +import { query } from "@solidjs/router" +import { withActor } from "~/context/auth.withActor" -export function formatDateUTC(date: Date) { - const options: Intl.DateTimeFormatOptions = { - weekday: "short", - year: "numeric", - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit", - second: "2-digit", - timeZoneName: "short", - timeZone: "UTC", - } - return date.toLocaleDateString("en-US", options) -} +export const querySessionInfo = query(async (workspaceID: string) => { + "use server" + return withActor(() => { + return { + isAdmin: Actor.userRole() === "admin", + isBeta: Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true, + } + }, workspaceID) +}, "session.get") diff --git a/packages/console/app/src/routes/workspace/index.tsx b/packages/console/app/src/routes/workspace/index.tsx deleted file mode 100644 index e69de29b..00000000 From 60dd987efd2435e5122fb98ea1c02041649416e7 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 9 Oct 2025 17:18:55 -0400 Subject: [PATCH 02/34] wip: zen --- .../workspace/[id]/members/member-section.tsx | 22 ++++++++++--------- .../workspace/[id]/model-section.module.css | 10 --------- .../routes/workspace/[id]/model-section.tsx | 9 +------- 3 files changed, 13 insertions(+), 28 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx index b13e8e5e..cfc8b5e7 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx @@ -310,16 +310,18 @@ export function MemberSection() { - - {(member) => ( - - )} - + 0}> + + {(member) => ( + + )} + + diff --git a/packages/console/app/src/routes/workspace/[id]/model-section.module.css b/packages/console/app/src/routes/workspace/[id]/model-section.module.css index 5a98c9b1..1a9a322f 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/model-section.module.css @@ -109,14 +109,4 @@ } } } -} - - -[data-component="empty-state"] { - display: flex; - align-items: center; - justify-content: center; - padding: 3rem; - color: var(--color-text-secondary); - font-size: 0.875rem; } \ No newline at end of file diff --git a/packages/console/app/src/routes/workspace/[id]/model-section.tsx b/packages/console/app/src/routes/workspace/[id]/model-section.tsx index 4128b4a2..13ec0cf4 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/model-section.tsx @@ -46,14 +46,7 @@ export function ModelSection() {

Manage models for your workspace.

- -

Loading models...

-
- } - > +
From bc0e00cbb7e68d80e826dd1606fddc9228e1210d Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 9 Oct 2025 22:38:42 -0400 Subject: [PATCH 03/34] wip: zen style header --- packages/console/app/src/component/icon.tsx | 76 +++++++++++---- packages/console/app/src/routes/user-menu.css | 68 ++++++++++++++ packages/console/app/src/routes/user-menu.tsx | 63 +++++++++++++ .../app/src/routes/workspace-picker.css | 92 +++---------------- .../app/src/routes/workspace-picker.tsx | 19 ++-- packages/console/app/src/routes/workspace.css | 28 +----- packages/console/app/src/routes/workspace.tsx | 36 ++------ 7 files changed, 215 insertions(+), 167 deletions(-) create mode 100644 packages/console/app/src/routes/user-menu.css create mode 100644 packages/console/app/src/routes/user-menu.tsx diff --git a/packages/console/app/src/component/icon.tsx b/packages/console/app/src/component/icon.tsx index 2b2dbe41..bb3c62da 100644 --- a/packages/console/app/src/component/icon.tsx +++ b/packages/console/app/src/component/icon.tsx @@ -2,26 +2,43 @@ import { JSX } from "solid-js" export function IconLogo(props: JSX.SvgSVGAttributes) { return ( - - - - - - - - - - - - -) + + + + + + + + + + + + ) } export function IconCopy(props: JSX.SvgSVGAttributes) { @@ -55,3 +72,22 @@ export function IconCreditCard(props: JSX.SvgSVGAttributes) { ) } + +export function IconChevron(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconWorkspaceLogo(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} diff --git a/packages/console/app/src/routes/user-menu.css b/packages/console/app/src/routes/user-menu.css new file mode 100644 index 00000000..28c7937f --- /dev/null +++ b/packages/console/app/src/routes/user-menu.css @@ -0,0 +1,68 @@ +[data-component="user-menu"] { + position: relative; + + [data-slot="trigger"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); + border: none; + border-radius: var(--border-radius-sm); + background-color: transparent; + color: var(--color-text); + font-size: var(--font-size-sm); + font-family: var(--font-sans); + cursor: pointer; + transition: all 0.15s ease; + + &:hover { + background-color: var(--color-surface-hover); + } + + span { + flex: 1; + text-align: left; + font-weight: 500; + color: var(--color-text-muted); + } + } + + [data-slot="chevron"] { + flex-shrink: 0; + color: var(--color-text-secondary); + } + + [data-slot="dropdown"] { + position: absolute; + top: 100%; + right: 0; + z-index: 1000; + margin-top: var(--space-1); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + min-width: 160px; + + @media (prefers-color-scheme: dark) { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + } + + form { + width: 100%; + } + } + + [data-slot="item"], + [data-slot="create-item"] { + width: 100%; + padding: var(--space-2-5) var(--space-3); + border: none; + background: none; + color: var(--color-danger); + font-size: var(--font-size-sm); + font-family: var(--font-sans); + text-align: left; + } +} \ No newline at end of file diff --git a/packages/console/app/src/routes/user-menu.tsx b/packages/console/app/src/routes/user-menu.tsx new file mode 100644 index 00000000..8c011fc0 --- /dev/null +++ b/packages/console/app/src/routes/user-menu.tsx @@ -0,0 +1,63 @@ +import { Show, onCleanup, createEffect } from "solid-js" +import { createStore } from "solid-js/store" +import { action, redirect } from "@solidjs/router" +import { getRequestEvent } from "solid-js/web" +import { useAuthSession } from "~/context/auth.session" +import { IconChevron } from "~/component/icon" +import "./user-menu.css" + +const logout = action(async () => { + "use server" + const auth = await useAuthSession() + const event = getRequestEvent() + const current = auth.data.current + if (current) + await auth.update((val) => { + delete val.account?.[current] + const first = Object.keys(val.account ?? {})[0] + val.current = first + event!.locals.actor = undefined + return val + }) + throw redirect("/zen") +}) + +export function UserMenu(props: { email: string | null | undefined }) { + const [store, setStore] = createStore({ + showDropdown: false, + }) + let dropdownRef: HTMLDivElement | undefined + + createEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef && !dropdownRef.contains(event.target as Node)) { + setStore("showDropdown", false) + } + } + + document.addEventListener("click", handleClickOutside) + + onCleanup(() => document.removeEventListener("click", handleClickOutside)) + }) + + return ( +
+
+ + + +
+ + + +
+
+
+
+ ) +} diff --git a/packages/console/app/src/routes/workspace-picker.css b/packages/console/app/src/routes/workspace-picker.css index c22ced86..c174cabe 100644 --- a/packages/console/app/src/routes/workspace-picker.css +++ b/packages/console/app/src/routes/workspace-picker.css @@ -15,19 +15,24 @@ justify-content: space-between; gap: var(--space-2); padding: var(--space-2) var(--space-3); - border: 1px solid var(--color-border); + border: none; border-radius: var(--border-radius-sm); - background-color: var(--color-bg); + background-color: transparent; color: var(--color-text); font-size: var(--font-size-sm); font-family: var(--font-sans); cursor: pointer; - min-width: 200px; + transition: all 0.15s ease; + + &:hover { + background-color: var(--color-surface-hover); + } span { flex: 1; text-align: left; font-weight: 500; + color: var(--color-text); } } @@ -36,20 +41,10 @@ color: var(--color-text-secondary); } - [data-slot="dropdown"] button { - text-decoration: none !important; - } - - /* Ensure text inside buttons has no underline */ - [data-slot="dropdown"] button * { - text-decoration: none !important; - } - [data-slot="dropdown"] { position: absolute; top: 100%; left: 0; - right: 0; z-index: 1000; margin-top: var(--space-1); border: 1px solid var(--color-border); @@ -58,14 +53,15 @@ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); max-height: 240px; overflow-y: auto; + min-width: 200px; @media (prefers-color-scheme: dark) { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } } - [data-slot="option"], - [data-slot="create-option"] { + [data-slot="item"], + [data-slot="create-item"] { width: 100%; padding: var(--space-2-5) var(--space-3); border: none; @@ -74,41 +70,6 @@ font-size: var(--font-size-sm); font-family: var(--font-sans); text-align: left; - cursor: pointer; - text-decoration: none; - - &:hover { - background-color: var(--color-surface); - text-decoration: none; - } - - &:focus { - text-decoration: none; - } - - &:active { - text-decoration: none; - } - - &:first-child { - border-top-left-radius: var(--border-radius-sm); - border-top-right-radius: var(--border-radius-sm); - } - - &:last-child { - border-bottom-left-radius: var(--border-radius-sm); - border-bottom-right-radius: var(--border-radius-sm); - } - } - - [data-slot="option"][data-selected="true"] { - background-color: transparent; - color: var(--color-text); - } - - [data-slot="create-option"] { - color: var(--color-text-secondary); - font-weight: 500; } [data-slot="create-form"] { @@ -150,35 +111,4 @@ color: var(--color-text-muted); } } - - button[type="submit"], - button[type="button"] { - padding: var(--space-2-5) var(--space-4); - background-color: var(--color-bg); - color: var(--color-text); - font-size: var(--font-size-sm); - font-family: var(--font-sans); - font-weight: 500; - cursor: pointer; - white-space: nowrap; - - &:focus { - outline: none; - box-shadow: none; - } - - &:active { - transform: translateY(1px); - } - - &[data-color="primary"] { - background-color: var(--color-text-secondary); - border-color: var(--color-text-secondary); - color: var(--color-bg); - } - - @media (max-width: 30rem) { - flex: 1; - } - } } \ No newline at end of file diff --git a/packages/console/app/src/routes/workspace-picker.tsx b/packages/console/app/src/routes/workspace-picker.tsx index 18182633..fb77d8f4 100644 --- a/packages/console/app/src/routes/workspace-picker.tsx +++ b/packages/console/app/src/routes/workspace-picker.tsx @@ -7,6 +7,7 @@ import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/ind import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" import { Workspace } from "@opencode-ai/console-core/workspace.js" +import { IconChevron } from "~/component/icon" import "./workspace-picker.css" const getWorkspaces = query(async () => { @@ -85,25 +86,17 @@ export function WorkspacePicker() { return (
-
setStore("showDropdown", !store.showDropdown)}> +
+ +
{(workspace) => ( )} -
diff --git a/packages/console/app/src/routes/workspace.css b/packages/console/app/src/routes/workspace.css index ed94365f..e8f12796 100644 --- a/packages/console/app/src/routes/workspace.css +++ b/packages/console/app/src/routes/workspace.css @@ -11,7 +11,6 @@ font-size: var(--font-size-sm); font-family: var(--font-sans); font-weight: 500; - text-transform: uppercase; cursor: pointer; transition: all 0.15s ease; @@ -55,9 +54,6 @@ a { color: var(--color-text); - text-decoration: underline; - text-underline-offset: var(--space-0-75); - text-decoration-thickness: 1px; } /* Workspace Header */ @@ -80,16 +76,14 @@ [data-slot="header-brand"] { flex: 0 0 auto; padding-top: 4px; - - svg { - width: 138px; - } + display: flex; + align-items: center; + gap: var(--space-4); [data-component="site-title"] { font-size: var(--font-size-lg); font-weight: 600; color: var(--color-text); - text-decoration: none; letter-spacing: -0.02em; } } @@ -109,19 +103,5 @@ display: none; } } - - a, - button { - appearance: none; - background: none; - border: none; - cursor: pointer; - padding: 0; - color: var(--color-text); - text-decoration: underline; - text-underline-offset: var(--space-0-75); - text-decoration-thickness: 1px; - text-transform: uppercase; - } } -} +} \ No newline at end of file diff --git a/packages/console/app/src/routes/workspace.tsx b/packages/console/app/src/routes/workspace.tsx index f87123d3..04e3f2c4 100644 --- a/packages/console/app/src/routes/workspace.tsx +++ b/packages/console/app/src/routes/workspace.tsx @@ -1,10 +1,9 @@ import { Show } from "solid-js" -import { getRequestEvent } from "solid-js/web" -import { query, action, redirect, createAsync, RouteSectionProps, useParams, A } from "@solidjs/router" +import { query, createAsync, RouteSectionProps, useParams, A } from "@solidjs/router" import "./workspace.css" -import { useAuthSession } from "~/context/auth.session" -import { IconLogo } from "../component/icon" +import { IconLogo, IconWorkspaceLogo } from "../component/icon" import { WorkspacePicker } from "./workspace-picker" +import { UserMenu } from "./user-menu" import { withActor } from "~/context/auth.withActor" import { User } from "@opencode-ai/console-core/user.js" import { Actor } from "@opencode-ai/console-core/actor.js" @@ -19,22 +18,6 @@ const getUserEmail = query(async (workspaceID: string) => { }, workspaceID) }, "userEmail") -const logout = action(async () => { - "use server" - const auth = await useAuthSession() - const event = getRequestEvent() - const current = auth.data.current - if (current) - await auth.update((val) => { - delete val.account?.[current] - const first = Object.keys(val.account ?? {})[0] - val.current = first - event!.locals.actor = undefined - return val - }) - throw redirect("/zen") -}) - export default function WorkspaceLayout(props: RouteSectionProps) { const params = useParams() const userEmail = createAsync(() => getUserEmail(params.id)) @@ -44,19 +27,14 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
-
- {userEmail()} -
- - +
+
+
{props.children}
From 03d50894360ddaf5d8dae990f3bf484b553de223 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 00:02:04 -0400 Subject: [PATCH 04/34] wip: zen style model --- .../app/src/routes/workspace/[id]/index.tsx | 14 +-- .../workspace/[id]/model-section.module.css | 95 ++++++++++++++++--- .../routes/workspace/[id]/model-section.tsx | 26 +++-- packages/console/core/src/actor.ts | 5 + packages/console/core/src/model.ts | 5 +- packages/console/core/src/provider.ts | 26 +++-- packages/console/core/src/user.ts | 11 +-- 7 files changed, 133 insertions(+), 49 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/index.tsx b/packages/console/app/src/routes/workspace/[id]/index.tsx index 1345bf40..46e6cf74 100644 --- a/packages/console/app/src/routes/workspace/[id]/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/index.tsx @@ -1,18 +1,10 @@ import "./index.css" import { NewUserSection } from "./new-user-section" import { UsageSection } from "./usage-section" -import { MemberSection } from "./members/member-section" -import { SettingsSection } from "./settings/settings-section" import { ModelSection } from "./model-section" import { ProviderSection } from "./provider-section" -import { Show } from "solid-js" -import { createAsync, useParams } from "@solidjs/router" -import { querySessionInfo } from "../common" export default function () { - const params = useParams() - const userInfo = createAsync(() => querySessionInfo(params.id)) - return (
@@ -28,10 +20,8 @@ export default function () {
- - - - + +
diff --git a/packages/console/app/src/routes/workspace/[id]/model-section.module.css b/packages/console/app/src/routes/workspace/[id]/model-section.module.css index 1a9a322f..0c5b3a45 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/model-section.module.css @@ -1,5 +1,3 @@ -.root {} - [data-slot="section-title"] { display: flex; flex-direction: column; @@ -62,28 +60,101 @@ color: var(--color-text); } - &[data-slot="model-status"] { - text-align: left; - color: var(--color-text); - } - &[data-slot="model-toggle"] { text-align: left; font-family: var(--font-sans); } + + [data-slot="model-toggle-label"] { + /* Toggle container */ + position: relative; + display: inline-block; + width: 2.5rem; + height: 1.5rem; + cursor: pointer; + + /* Hidden checkbox input */ + input { + opacity: 0; + width: 0; + height: 0; + } + + /* Toggle track (background) */ + span { + position: absolute; + inset: 0; + background-color: #ccc; + border: 1px solid #bbb; + border-radius: 1.5rem; + transition: all 0.3s ease; + cursor: pointer; + + /* Toggle handle (slider) */ + &::before { + content: ""; + position: absolute; + top: 50%; + left: 0.125rem; + width: 1.25rem; + height: 1.25rem; + background-color: white; + border: 1px solid #ddd; + border-radius: 50%; + transform: translateY(-50%); + transition: all 0.3s ease; + } + } + + /* Checked state - track */ + input:checked+span { + background-color: #21AD0E; + border-color: #148605; + + /* Checked state - handle */ + &::before { + transform: translateX(1rem) translateY(-50%); + } + } + + /* Hover states */ + &:hover span { + box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2); + } + + input:checked:hover+span { + box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3); + } + + /* Disabled state */ + &:has(input:disabled) { + cursor: not-allowed; + } + + input:disabled+span { + opacity: 0.5; + cursor: not-allowed; + } + + input:disabled:checked+span { + opacity: 0.5; + } + + input:disabled~span:hover { + box-shadow: none; + } + } } tbody tr { - &[data-enabled="false"] { - opacity: 0.6; - } - &:last-child td { border-bottom: none; } } +} - @media (max-width: 40rem) { +@media (max-width: 40rem) { + [data-slot="models-table-element"] { th, td { diff --git a/packages/console/app/src/routes/workspace/[id]/model-section.tsx b/packages/console/app/src/routes/workspace/[id]/model-section.tsx index 13ec0cf4..f4f2f913 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/model-section.tsx @@ -4,6 +4,7 @@ import { createMemo, For, Show } from "solid-js" import { withActor } from "~/context/auth.withActor" import { ZenModel } from "@opencode-ai/console-core/model.js" import styles from "./model-section.module.css" +import { querySessionInfo } from "../common" const getModelsInfo = query(async (workspaceID: string) => { "use server" @@ -39,11 +40,15 @@ const updateModel = action(async (form: FormData) => { export function ModelSection() { const params = useParams() const modelsInfo = createAsync(() => getModelsInfo(params.id)) + const userInfo = createAsync(() => querySessionInfo(params.id)) return (

Models

-

Manage models for your workspace.

+

+ Manage which models workspace members can access. Requests will fail if a member tries to use a disabled + model.{userInfo()?.isAdmin ? "" : " To use a disabled model, contact your workspace’s admin."} +

@@ -52,8 +57,7 @@ export function ModelSection() {
- - + @@ -61,15 +65,25 @@ export function ModelSection() { {(modelId) => { const isEnabled = createMemo(() => !modelsInfo()!.disabled.includes(modelId)) return ( - + - diff --git a/packages/console/core/src/actor.ts b/packages/console/core/src/actor.ts index 88c5e4b5..e8d1b7a6 100644 --- a/packages/console/core/src/actor.ts +++ b/packages/console/core/src/actor.ts @@ -67,6 +67,11 @@ export namespace Actor { return actor as Extract } + export const assertAdmin = () => { + if (userRole() === "admin") return + throw new Error(`Expected admin user, got ${userRole()}`) + } + export function workspace() { const actor = use() if ("workspaceID" in actor.properties) { diff --git a/packages/console/core/src/model.ts b/packages/console/core/src/model.ts index ae636c4f..48d7e16c 100644 --- a/packages/console/core/src/model.ts +++ b/packages/console/core/src/model.ts @@ -40,13 +40,14 @@ export namespace ZenModel { export namespace Model { export const enable = fn(z.object({ model: z.string() }), ({ model }) => { - const workspaceID = Actor.workspace() + Actor.assertAdmin() return Database.use((db) => - db.delete(ModelTable).where(and(eq(ModelTable.workspaceID, workspaceID), eq(ModelTable.model, model))), + db.delete(ModelTable).where(and(eq(ModelTable.workspaceID, Actor.workspace()), eq(ModelTable.model, model))), ) }) export const disable = fn(z.object({ model: z.string() }), ({ model }) => { + Actor.assertAdmin() return Database.use((db) => db .insert(ModelTable) diff --git a/packages/console/core/src/provider.ts b/packages/console/core/src/provider.ts index 1f8c07b9..cf2040b5 100644 --- a/packages/console/core/src/provider.ts +++ b/packages/console/core/src/provider.ts @@ -20,8 +20,9 @@ export namespace Provider { provider: z.string().min(1).max(64), credentials: z.string(), }), - ({ provider, credentials }) => - Database.use((tx) => + async ({ provider, credentials }) => { + Actor.assertAdmin() + return Database.use((tx) => tx .insert(ProviderTable) .values({ @@ -36,14 +37,21 @@ export namespace Provider { timeDeleted: null, }, }), - ), + ) + }, ) - export const remove = fn(z.object({ provider: z.string() }), ({ provider }) => - Database.transaction((tx) => - tx - .delete(ProviderTable) - .where(and(eq(ProviderTable.provider, provider), eq(ProviderTable.workspaceID, Actor.workspace()))), - ), + export const remove = fn( + z.object({ + provider: z.string(), + }), + async ({ provider }) => { + Actor.assertAdmin() + return Database.transaction((tx) => + tx + .delete(ProviderTable) + .where(and(eq(ProviderTable.provider, provider), eq(ProviderTable.workspaceID, Actor.workspace()))), + ) + }, ) } diff --git a/packages/console/core/src/user.ts b/packages/console/core/src/user.ts index 38c8e5e3..1580783f 100644 --- a/packages/console/core/src/user.ts +++ b/packages/console/core/src/user.ts @@ -13,11 +13,6 @@ import { Key } from "./key" import { KeyTable } from "./schema/key.sql" export namespace User { - const assertAdmin = () => { - if (Actor.userRole() === "admin") return - throw new Error(`Expected admin user, got ${Actor.userRole()}`) - } - const assertNotSelf = (id: string) => { if (Actor.userID() !== id) return throw new Error(`Expected not self actor, got self actor`) @@ -65,7 +60,7 @@ export namespace User { role: z.enum(UserRole), }), async ({ email, role }) => { - assertAdmin() + Actor.assertAdmin() const workspaceID = Actor.workspace() // create user @@ -176,7 +171,7 @@ export namespace User { monthlyLimit: z.number().nullable(), }), async ({ id, role, monthlyLimit }) => { - assertAdmin() + Actor.assertAdmin() if (role === "member") assertNotSelf(id) return await Database.use((tx) => tx @@ -188,7 +183,7 @@ export namespace User { ) export const remove = fn(z.string(), async (id) => { - assertAdmin() + Actor.assertAdmin() assertNotSelf(id) return await Database.use((tx) => From ad7b4b1fcd4b2c863d188ad7b20ccb246fe98d93 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 00:56:16 -0400 Subject: [PATCH 05/34] wip: zen style nav bar --- packages/console/app/src/component/icon.tsx | 93 ++++++++++++------- packages/console/app/src/routes/workspace.tsx | 2 +- .../console/app/src/routes/workspace/[id].css | 50 ++++++---- .../console/app/src/routes/workspace/[id].tsx | 34 +++---- .../app/src/routes/workspace/[id]/index.css | 2 +- .../app/src/routes/workspace/[id]/index.tsx | 3 +- 6 files changed, 115 insertions(+), 69 deletions(-) diff --git a/packages/console/app/src/component/icon.tsx b/packages/console/app/src/component/icon.tsx index bb3c62da..4d3865c8 100644 --- a/packages/console/app/src/component/icon.tsx +++ b/packages/console/app/src/component/icon.tsx @@ -2,41 +2,68 @@ import { JSX } from "solid-js" export function IconLogo(props: JSX.SvgSVGAttributes) { return ( - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/packages/console/app/src/routes/workspace.tsx b/packages/console/app/src/routes/workspace.tsx index 04e3f2c4..2ac629f5 100644 --- a/packages/console/app/src/routes/workspace.tsx +++ b/packages/console/app/src/routes/workspace.tsx @@ -1,7 +1,7 @@ import { Show } from "solid-js" import { query, createAsync, RouteSectionProps, useParams, A } from "@solidjs/router" import "./workspace.css" -import { IconLogo, IconWorkspaceLogo } from "../component/icon" +import { IconWorkspaceLogo } from "../component/icon" import { WorkspacePicker } from "./workspace-picker" import { UserMenu } from "./user-menu" import { withActor } from "~/context/auth.withActor" diff --git a/packages/console/app/src/routes/workspace/[id].css b/packages/console/app/src/routes/workspace/[id].css index 399d7e73..e21db2c2 100644 --- a/packages/console/app/src/routes/workspace/[id].css +++ b/packages/console/app/src/routes/workspace/[id].css @@ -14,26 +14,42 @@ border-right: 1px solid var(--color-border); padding: var(--space-6) var(--space-4); display: flex; - flex-direction: column; - gap: var(--space-2); + justify-content: flex-end; - [data-nav-button] { - padding: var(--space-3) var(--space-4); - border-radius: var(--border-radius-sm); - color: var(--color-text-muted); - text-decoration: none; - font-size: var(--font-size-sm); - font-weight: 500; - transition: all 0.15s ease; + [data-component="nav-items"] { + display: flex; + flex-direction: column; + gap: var(--space-2); - &:hover { - background-color: var(--color-surface-hover); - color: var(--color-text); - } + [data-nav-button] { + padding: var(--space-3) var(--space-4); + border-radius: var(--border-radius-sm); + color: var(--color-text-muted); + text-decoration: none; + font-size: var(--font-size-sm); + font-weight: 500; + transition: all 0.15s ease; - &.active { - background-color: var(--color-surface-hover); - color: var(--color-text); + &:hover { + color: var(--color-text); + } + + &.active { + color: var(--color-text); + font-weight: 700; + position: relative; + + &::before { + content: ''; + position: absolute; + left: calc(-1 * var(--space-0-5)); + top: 0; + bottom: 0; + width: 2px; + background-color: var(--color-text); + border-radius: 0 2px 2px 0; + } + } } } } diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx index 1da24dc3..21f20c70 100644 --- a/packages/console/app/src/routes/workspace/[id].tsx +++ b/packages/console/app/src/routes/workspace/[id].tsx @@ -10,23 +10,25 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
diff --git a/packages/console/app/src/routes/workspace/[id]/index.css b/packages/console/app/src/routes/workspace/[id]/index.css index 5124c78c..5d7550fb 100644 --- a/packages/console/app/src/routes/workspace/[id]/index.css +++ b/packages/console/app/src/routes/workspace/[id]/index.css @@ -1,6 +1,6 @@ [data-page="workspace-[id]"] { max-width: 64rem; - padding: var(--space-10) var(--space-4); + padding: var(--space-2) var(--space-4); margin: 0 auto; width: 100%; display: flex; diff --git a/packages/console/app/src/routes/workspace/[id]/index.tsx b/packages/console/app/src/routes/workspace/[id]/index.tsx index 46e6cf74..a5a8bb46 100644 --- a/packages/console/app/src/routes/workspace/[id]/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/index.tsx @@ -3,12 +3,13 @@ import { NewUserSection } from "./new-user-section" import { UsageSection } from "./usage-section" import { ModelSection } from "./model-section" import { ProviderSection } from "./provider-section" +import { IconLogo } from "~/component/icon" export default function () { return (
-

Zen

+

Curated list of models provided by opencode.{" "} From fec70ae9c98e50c964a37f9ff6bdc78d34103ad5 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 01:36:15 -0400 Subject: [PATCH 06/34] wip: zen --- packages/console/app/src/routes/workspace/[id].css | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/console/app/src/routes/workspace/[id].css b/packages/console/app/src/routes/workspace/[id].css index e21db2c2..3d87042f 100644 --- a/packages/console/app/src/routes/workspace/[id].css +++ b/packages/console/app/src/routes/workspace/[id].css @@ -11,7 +11,6 @@ [data-component="workspace-nav"] { width: 240px; flex-shrink: 0; - border-right: 1px solid var(--color-border); padding: var(--space-6) var(--space-4); display: flex; justify-content: flex-end; From 250393978b7e2351646eff24cfdccc9776f5ed4f Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 02:34:06 -0400 Subject: [PATCH 07/34] wip: style byok --- .../[id]/provider-section.module.css | 39 +++++++-- .../workspace/[id]/provider-section.tsx | 86 ++++++++++++------- 2 files changed, 84 insertions(+), 41 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/provider-section.module.css b/packages/console/app/src/routes/workspace/[id]/provider-section.module.css index 5f18862f..f5cfdd8f 100644 --- a/packages/console/app/src/routes/workspace/[id]/provider-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/provider-section.module.css @@ -35,11 +35,6 @@ &[data-slot="provider-status"] { text-align: left; color: var(--color-text); - } - - &[data-slot="provider-toggle"] { - text-align: left; - font-family: var(--font-sans); [data-slot="edit-form"] { display: flex; @@ -76,11 +71,32 @@ line-height: 1.4; } } + } + } - [data-slot="form-actions"] { - display: flex; - gap: var(--space-2); + &[data-slot="provider-action"] { + text-align: left; + font-family: var(--font-sans); + + [data-slot="configured-actions"] { + display: flex; + gap: var(--space-2); + + [data-slot="delete-form"] { + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; } + + &:hover [data-slot="delete-form"] { + opacity: 1; + pointer-events: auto; + } + } + + [data-slot="form-actions"] { + display: flex; + gap: var(--space-2); } } } @@ -90,6 +106,13 @@ opacity: 0.6; } + &:hover { + [data-slot="provider-action"] [data-slot="delete-form"] { + opacity: 1; + pointer-events: auto; + } + } + &:last-child td { border-bottom: none; } diff --git a/packages/console/app/src/routes/workspace/[id]/provider-section.tsx b/packages/console/app/src/routes/workspace/[id]/provider-section.tsx index 856b3a6a..6b7663e1 100644 --- a/packages/console/app/src/routes/workspace/[id]/provider-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/provider-section.tsx @@ -12,6 +12,10 @@ const PROVIDERS = [ type Provider = (typeof PROVIDERS)[number] +function maskCredentials(credentials: string) { + return `${credentials.slice(0, 8)}...${credentials.slice(-8)}` +} + const removeProvider = action(async (form: FormData) => { "use server" const provider = form.get("provider")?.toString() @@ -58,7 +62,7 @@ function ProviderRow(props: { provider: Provider }) { let input: HTMLInputElement - const isEnabled = () => providers()?.some((p) => p.provider === props.provider.key) + const providerData = () => providers()?.find((p) => p.provider === props.provider.key) createEffect(() => { if (!saveSubmission.pending && saveSubmission.result && !saveSubmission.result.error) { @@ -80,32 +84,14 @@ function ProviderRow(props: { provider: Provider }) { } return ( -

+ - - + ) } @@ -149,8 +169,8 @@ export function ProviderSection() { - - + + From 8d4607ebd56aa5ba528c3d61621765b60e80c196 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 02:37:50 -0400 Subject: [PATCH 08/34] wip: zen style byok --- .../app/src/routes/workspace/[id]/provider-section.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/provider-section.tsx b/packages/console/app/src/routes/workspace/[id]/provider-section.tsx index 6b7663e1..2677b36d 100644 --- a/packages/console/app/src/routes/workspace/[id]/provider-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/provider-section.tsx @@ -84,7 +84,7 @@ function ProviderRow(props: { provider: Provider }) { } return ( - + + + - From d83af721a6e6269481186df80d498aa1213a6e59 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 13:45:06 -0400 Subject: [PATCH 18/34] wip: zen style api keys --- .../[id]/keys/key-section.module.css | 20 ++++- .../workspace/[id]/keys/key-section.tsx | 79 ++++++++----------- 2 files changed, 52 insertions(+), 47 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css b/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css index 6a1d0c85..ad20f1fa 100644 --- a/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css @@ -1,4 +1,11 @@ .root { + [data-slot="title-row"] { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-4); + } + [data-component="empty-state"] { padding: var(--space-20) var(--space-6); text-align: center; @@ -150,6 +157,7 @@ } @media (max-width: 40rem) { + th, td { padding: var(--space-2) var(--space-3); @@ -157,16 +165,22 @@ } th { - &:nth-child(3) /* Date */ { + &:nth-child(3) + + /* Date */ + { display: none; } } td { - &:nth-child(3) /* Date */ { + &:nth-child(3) + + /* Date */ + { display: none; } } } } -} +} \ No newline at end of file diff --git a/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx b/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx index 22b82ae0..87c1541d 100644 --- a/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx @@ -43,8 +43,9 @@ const listKeys = query(async (workspaceID: string) => { return withActor(() => Key.list(), workspaceID) }, "key.list") -export function KeyCreateForm() { +export function KeySection() { const params = useParams() + const keys = createAsync(() => listKeys(params.id)) const submission = useSubmission(createKey) const [store, setStore] = createStore({ show: false }) @@ -52,69 +53,59 @@ export function KeyCreateForm() { createEffect(() => { if (!submission.pending && submission.result && !submission.result.error) { - hide() + setStore("show", false) } }) function show() { - // submission.clear() does not clear the result in some cases, ie. - // 1. Create key with empty name => error shows - // 2. Put in a key name and creates the key => form hides - // 3. Click add key button again => form shows with the same error if - // submission.clear() is called only once while (true) { submission.clear() if (!submission.result) break } setStore("show", true) - input.focus() + setTimeout(() => input?.focus(), 0) } function hide() { setStore("show", false) } - return ( - show()}> - Create API Key - - } - > -
-
- (input = r)} data-component="input" name="name" type="text" placeholder="Enter key name" /> - - {(err) =>
{err()}
} -
-
- -
- - -
- -
- ) -} - -export function KeySection() { - const params = useParams() - const keys = createAsync(() => listKeys(params.id)) - return (

API Keys

-

Manage your API keys for accessing opencode services.

+
+

Manage your API keys for accessing opencode services.

+ +
- + +
+
+ (input = r)} + data-component="input" + name="name" + type="text" + placeholder="Enter key name" + /> + + {(err) =>
{err()}
} +
+
+ +
+ + +
+ +
Date: Fri, 10 Oct 2025 13:48:56 -0400 Subject: [PATCH 19/34] wip: zen style members --- .../[id]/members/member-section.module.css | 47 ++++++ .../workspace/[id]/members/member-section.tsx | 144 ++++++++---------- 2 files changed, 112 insertions(+), 79 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css b/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css index 16b6ff8d..14246d58 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css @@ -1,4 +1,11 @@ .root { + [data-slot="title-row"] { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-4); + } + [data-component="empty-state"] { padding: var(--space-20) var(--space-6); text-align: center; @@ -64,6 +71,46 @@ margin-top: var(--space-1); line-height: 1.4; } + + [data-slot="role-selector"] { + display: flex; + flex-direction: column; + gap: var(--space-2); + + label { + display: flex; + gap: var(--space-3); + padding: var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + cursor: pointer; + + &:hover { + background-color: var(--color-bg-surface); + } + + input[type="radio"] { + margin-top: var(--space-1); + } + + div { + flex: 1; + + strong { + display: block; + color: var(--color-text); + font-family: var(--font-sans); + margin-bottom: var(--space-1); + } + + p { + font-size: var(--font-size-xs); + color: var(--color-text-muted); + font-family: var(--font-sans); + } + } + } + } } [data-slot="members-table"] { diff --git a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx index cfc8b5e7..e9f61724 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx @@ -81,83 +81,6 @@ const updateMember = action(async (form: FormData) => { ) }, "member.update") -export function MemberCreateForm() { - const params = useParams() - const submission = useSubmission(inviteMember) - const [store, setStore] = createStore({ show: false }) - - let input: HTMLInputElement - - createEffect(() => { - if (!submission.pending && submission.result && !submission.result.error) { - hide() - } - }) - - function show() { - // submission.clear() does not clear the result in some cases, ie. - // 1. Create key with empty name => error shows - // 2. Put in a key name and creates the key => form hides - // 3. Click add key button again => form shows with the same error if - // submission.clear() is called only once - while (true) { - submission.clear() - if (!submission.result) break - } - setStore("show", true) - input.focus() - } - - function hide() { - setStore("show", false) - } - - return ( - show()}> - Invite Member - - } - > -
-
- (input = r)} data-component="input" name="email" type="text" placeholder="Enter email" /> -
- - -
- - {(err) =>
{err()}
} -
-
- -
- - -
- -
- ) -} - function MemberRow(props: { member: any; workspaceID: string; actorID: string; actorRole: string }) { const [editing, setEditing] = createSignal(false) const submission = useSubmission(updateMember) @@ -289,14 +212,77 @@ function MemberRow(props: { member: any; workspaceID: string; actorID: string; a export function MemberSection() { const params = useParams() const data = createAsync(() => listMembers(params.id)) + const submission = useSubmission(inviteMember) + const [store, setStore] = createStore({ show: false }) + + let input: HTMLInputElement + + createEffect(() => { + if (!submission.pending && submission.result && !submission.result.error) { + setStore("show", false) + } + }) + + function show() { + while (true) { + submission.clear() + if (!submission.result) break + } + setStore("show", true) + setTimeout(() => input?.focus(), 0) + } + + function hide() { + setStore("show", false) + } return (

Members

+
+

Manage workspace members and their permissions.

+ + + +
- - + +
+
+ (input = r)} data-component="input" name="email" type="text" placeholder="Enter email" /> +
+ + +
+ + {(err) =>
{err()}
} +
+
+ +
+ + +
+
ModelStatusActionEnabled
{modelId}{isEnabled() ? "Enabled" : "Disabled"}
- +
{props.provider.name}{isEnabled() ? "Configured" : "Not Configured"} + show()}> - Configure - - } - > -
- - - -
-
- } + fallback={{providerData() ? maskCredentials(providerData()!.credentials) : "Not Configured"}} > -
+
(input = r)} @@ -122,17 +108,51 @@ function ProviderRow(props: { provider: Provider }) {
-
- - -
+ show()}> + Configure + + } + > +
+ +
+ + + +
+
+
+ } + > +
+ + +
+ +
ProviderStatusActionAPI Key
{props.provider.name} show()}> Configure From 64409182ec1af53aa970d5f8f372d57de10c173a Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 02:53:05 -0400 Subject: [PATCH 09/34] wip: zen style byok --- .../src/routes/workspace/[id]/provider-section.module.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/console/app/src/routes/workspace/[id]/provider-section.module.css b/packages/console/app/src/routes/workspace/[id]/provider-section.module.css index f5cfdd8f..b009d1aa 100644 --- a/packages/console/app/src/routes/workspace/[id]/provider-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/provider-section.module.css @@ -35,16 +35,19 @@ &[data-slot="provider-status"] { text-align: left; color: var(--color-text); + width: 50%; [data-slot="edit-form"] { display: flex; flex-direction: column; gap: var(--space-3); + max-width: 100%; [data-slot="input-wrapper"] { display: flex; flex-direction: column; gap: var(--space-1); + max-width: 100%; input { padding: var(--space-2) var(--space-3); @@ -54,6 +57,8 @@ color: var(--color-text); font-size: var(--font-size-sm); font-family: var(--font-mono); + width: 100%; + box-sizing: border-box; &:focus { outline: none; @@ -77,6 +82,8 @@ &[data-slot="provider-action"] { text-align: left; font-family: var(--font-sans); + width: 30%; + white-space: nowrap; [data-slot="configured-actions"] { display: flex; From 593d0737b5330b33a560ab40004e7eaa693f5251 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 03:15:55 -0400 Subject: [PATCH 10/34] wip: zen style byok --- .../src/routes/workspace/[id]/model-section.module.css | 6 ++++++ .../app/src/routes/workspace/[id]/model-section.tsx | 2 +- .../src/routes/workspace/[id]/provider-section.module.css | 8 ++------ .../app/src/routes/workspace/[id]/provider-section.tsx | 6 +++--- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/model-section.module.css b/packages/console/app/src/routes/workspace/[id]/model-section.module.css index 0c5b3a45..68408f78 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/model-section.module.css @@ -150,6 +150,12 @@ &:last-child td { border-bottom: none; } + + &[data-disabled="true"] { + td[data-slot="model-name"] { + color: var(--color-text-muted); + } + } } } diff --git a/packages/console/app/src/routes/workspace/[id]/model-section.tsx b/packages/console/app/src/routes/workspace/[id]/model-section.tsx index f4f2f913..96d6950c 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/model-section.tsx @@ -65,7 +65,7 @@ export function ModelSection() { {(modelId) => { const isEnabled = createMemo(() => !modelsInfo()!.disabled.includes(modelId)) return ( -
{modelId}
diff --git a/packages/console/app/src/routes/workspace/[id]/provider-section.module.css b/packages/console/app/src/routes/workspace/[id]/provider-section.module.css index b009d1aa..b693de77 100644 --- a/packages/console/app/src/routes/workspace/[id]/provider-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/provider-section.module.css @@ -32,9 +32,9 @@ font-weight: 500; } - &[data-slot="provider-status"] { + &[data-slot="provider-key"] { text-align: left; - color: var(--color-text); + color: var(--color-text-secondary); width: 50%; [data-slot="edit-form"] { @@ -109,10 +109,6 @@ } tbody tr { - &[data-enabled="false"] { - opacity: 0.6; - } - &:hover { [data-slot="provider-action"] [data-slot="delete-form"] { opacity: 1; diff --git a/packages/console/app/src/routes/workspace/[id]/provider-section.tsx b/packages/console/app/src/routes/workspace/[id]/provider-section.tsx index 2677b36d..06820b32 100644 --- a/packages/console/app/src/routes/workspace/[id]/provider-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/provider-section.tsx @@ -84,12 +84,12 @@ function ProviderRow(props: { provider: Provider }) { } return ( -
{props.provider.name} + {providerData() ? maskCredentials(providerData()!.credentials) : "Not Configured"}} + fallback={{providerData() ? maskCredentials(providerData()!.credentials) : "--"}} >
From c9155c117a0fe821f7a7b6cacb2713b3930211c7 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 09:03:49 -0400 Subject: [PATCH 11/34] wip: zen --- .../console/app/src/routes/workspace/[id]/index.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/console/app/src/routes/workspace/[id]/index.tsx b/packages/console/app/src/routes/workspace/[id]/index.tsx index a5a8bb46..3716674a 100644 --- a/packages/console/app/src/routes/workspace/[id]/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/index.tsx @@ -4,8 +4,14 @@ import { UsageSection } from "./usage-section" import { ModelSection } from "./model-section" import { ProviderSection } from "./provider-section" import { IconLogo } from "~/component/icon" +import { createAsync, useParams } from "@solidjs/router" +import { querySessionInfo } from "../common" +import { Show } from "solid-js" export default function () { + const params = useParams() + const userInfo = createAsync(() => querySessionInfo(params.id)) + return (
@@ -22,7 +28,9 @@ export default function () {
- + + +
From 920373d252a0426948d325b0803ce869e5686271 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 12:04:02 -0400 Subject: [PATCH 12/34] wip: zen settings --- .../routes/workspace/[id]/billing/index.css | 2 +- .../src/routes/workspace/[id]/keys/index.css | 2 +- .../routes/workspace/[id]/members/index.css | 2 +- .../routes/workspace/[id]/settings/index.css | 2 +- .../[id]/settings/settings-section.module.css | 87 +++++++++---------- .../[id]/settings/settings-section.tsx | 30 +++---- 6 files changed, 61 insertions(+), 64 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.css b/packages/console/app/src/routes/workspace/[id]/billing/index.css index 5124c78c..5d7550fb 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/index.css +++ b/packages/console/app/src/routes/workspace/[id]/billing/index.css @@ -1,6 +1,6 @@ [data-page="workspace-[id]"] { max-width: 64rem; - padding: var(--space-10) var(--space-4); + padding: var(--space-2) var(--space-4); margin: 0 auto; width: 100%; display: flex; diff --git a/packages/console/app/src/routes/workspace/[id]/keys/index.css b/packages/console/app/src/routes/workspace/[id]/keys/index.css index 5124c78c..5d7550fb 100644 --- a/packages/console/app/src/routes/workspace/[id]/keys/index.css +++ b/packages/console/app/src/routes/workspace/[id]/keys/index.css @@ -1,6 +1,6 @@ [data-page="workspace-[id]"] { max-width: 64rem; - padding: var(--space-10) var(--space-4); + padding: var(--space-2) var(--space-4); margin: 0 auto; width: 100%; display: flex; diff --git a/packages/console/app/src/routes/workspace/[id]/members/index.css b/packages/console/app/src/routes/workspace/[id]/members/index.css index 5124c78c..5d7550fb 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/index.css +++ b/packages/console/app/src/routes/workspace/[id]/members/index.css @@ -1,6 +1,6 @@ [data-page="workspace-[id]"] { max-width: 64rem; - padding: var(--space-10) var(--space-4); + padding: var(--space-2) var(--space-4); margin: 0 auto; width: 100%; display: flex; diff --git a/packages/console/app/src/routes/workspace/[id]/settings/index.css b/packages/console/app/src/routes/workspace/[id]/settings/index.css index 5124c78c..5d7550fb 100644 --- a/packages/console/app/src/routes/workspace/[id]/settings/index.css +++ b/packages/console/app/src/routes/workspace/[id]/settings/index.css @@ -1,6 +1,6 @@ [data-page="workspace-[id]"] { max-width: 64rem; - padding: var(--space-10) var(--space-4); + padding: var(--space-2) var(--space-4); margin: 0 auto; width: 100%; display: flex; diff --git a/packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css b/packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css index e3a5ad50..e61977cf 100644 --- a/packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css @@ -1,63 +1,61 @@ .root { - [data-slot="section-content"] { - display: flex; - flex-direction: column; - gap: var(--space-4); - } - [data-slot="setting"] { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--space-4); - padding: var(--space-4); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - - @media (max-width: 30rem) { - flex-direction: column; - gap: var(--space-3); - } - } - - [data-slot="setting-info"] { - flex: 1; display: flex; flex-direction: column; - gap: var(--space-1); + gap: var(--space-3); - h3 { - font-size: var(--font-size-md); - font-weight: 500; + p { + font-size: var(--font-size-sm); line-height: 1.2; margin: 0; - color: var(--color-text); + color: var(--color-text-muted); + } + + [data-slot="value-with-action"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + + @media (max-width: 30rem) { + flex-direction: column; + align-items: flex-start; + gap: var(--space-2); + } } [data-slot="current-value"] { font-size: var(--font-size-sm); - color: var(--color-text-muted); + color: var(--color-text); line-height: 1.4; margin: 0; } + + >button { + align-self: flex-start; + } } [data-slot="create-form"] { display: flex; flex-direction: column; - gap: var(--space-3); - min-width: 15rem; - width: fit-content; - - @media (max-width: 30rem) { - width: 100%; - min-width: auto; - } + gap: var(--space-2); [data-slot="input-container"] { display: flex; - flex-direction: column; - gap: var(--space-1); + flex-direction: row; + align-items: center; + gap: var(--space-2); + + @media (max-width: 30rem) { + flex-direction: column; + align-items: stretch; + } + + button { + white-space: nowrap; + flex-shrink: 0; + } } input { @@ -68,11 +66,13 @@ background-color: var(--color-bg); color: var(--color-text); font-size: var(--font-size-sm); - font-family: var(--font-mono); + line-height: 1.5; + min-width: 0; &:focus { outline: none; border-color: var(--color-accent); + box-shadow: 0 0 0 3px var(--color-accent-alpha); } &::placeholder { @@ -80,16 +80,15 @@ } } - [data-slot="form-actions"] { - display: flex; - gap: var(--space-2); - justify-content: flex-end; + >button[type="reset"] { + align-self: flex-start; } [data-slot="form-error"] { color: var(--color-danger); font-size: var(--font-size-sm); line-height: 1.4; + margin-top: calc(var(--space-1) * -1); } } -} +} \ No newline at end of file diff --git a/packages/console/app/src/routes/workspace/[id]/settings/settings-section.tsx b/packages/console/app/src/routes/workspace/[id]/settings/settings-section.tsx index 0fc0158d..cb15f6b4 100644 --- a/packages/console/app/src/routes/workspace/[id]/settings/settings-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/settings/settings-section.tsx @@ -79,10 +79,7 @@ export function SettingsSection() {
-
-

Workspace Name

-

{workspaceInfo()?.name}

-
+

Workspace Name

- - {(err) =>
{err()}
} -
-
- -
+ + -
+ + {(err) =>
{err()}
} +
} > - +
+

{workspaceInfo()?.name}

+ +
From 5ee3063aab7b45f1a9d22d9f99e212c4bde72d7f Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 12:16:57 -0400 Subject: [PATCH 13/34] wip: sync --- .../workspace/[id]/settings/settings-section.module.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css b/packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css index e61977cf..058fbe30 100644 --- a/packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/settings/settings-section.module.css @@ -1,11 +1,12 @@ .root { + max-width: 40rem; + [data-slot="setting"] { display: flex; flex-direction: column; gap: var(--space-3); p { - font-size: var(--font-size-sm); line-height: 1.2; margin: 0; color: var(--color-text-muted); @@ -25,7 +26,6 @@ } [data-slot="current-value"] { - font-size: var(--font-size-sm); color: var(--color-text); line-height: 1.4; margin: 0; From 5a90e5f9e21570a9ab4eccca24112fe0c9297b64 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 12:22:36 -0400 Subject: [PATCH 14/34] wip: zen --- packages/console/app/src/routes/workspace/[id]/index.css | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/console/app/src/routes/workspace/[id]/index.css b/packages/console/app/src/routes/workspace/[id]/index.css index 5d7550fb..dd05bac5 100644 --- a/packages/console/app/src/routes/workspace/[id]/index.css +++ b/packages/console/app/src/routes/workspace/[id]/index.css @@ -45,7 +45,6 @@ letter-spacing: -0.03125rem; margin: 0; color: var(--color-text-secondary); - text-transform: uppercase; @media (max-width: 30rem) { font-size: var(--font-size-md); From 310065bd0a7a287b10240c674a4cd6d4082e8c8d Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 12:46:42 -0400 Subject: [PATCH 15/34] wip: zen --- .../console/app/src/routes/workspace/[id].css | 175 +++++++++++++++--- .../console/app/src/routes/workspace/[id].tsx | 2 +- .../routes/workspace/[id]/billing/index.css | 116 ------------ .../routes/workspace/[id]/billing/index.tsx | 1 - .../app/src/routes/workspace/[id]/index.css | 115 ------------ .../app/src/routes/workspace/[id]/index.tsx | 1 - .../src/routes/workspace/[id]/keys/index.css | 116 ------------ .../src/routes/workspace/[id]/keys/index.tsx | 1 - .../routes/workspace/[id]/members/index.css | 116 ------------ .../routes/workspace/[id]/members/index.tsx | 1 - .../routes/workspace/[id]/settings/index.css | 116 ------------ .../routes/workspace/[id]/settings/index.tsx | 1 - 12 files changed, 147 insertions(+), 614 deletions(-) delete mode 100644 packages/console/app/src/routes/workspace/[id]/billing/index.css delete mode 100644 packages/console/app/src/routes/workspace/[id]/index.css delete mode 100644 packages/console/app/src/routes/workspace/[id]/keys/index.css delete mode 100644 packages/console/app/src/routes/workspace/[id]/members/index.css delete mode 100644 packages/console/app/src/routes/workspace/[id]/settings/index.css diff --git a/packages/console/app/src/routes/workspace/[id].css b/packages/console/app/src/routes/workspace/[id].css index 3d87042f..4aeb6014 100644 --- a/packages/console/app/src/routes/workspace/[id].css +++ b/packages/console/app/src/routes/workspace/[id].css @@ -14,40 +14,40 @@ padding: var(--space-6) var(--space-4); display: flex; justify-content: flex-end; +} - [data-component="nav-items"] { - display: flex; - flex-direction: column; - gap: var(--space-2); +[data-component="workspace-nav-items"] { + display: flex; + flex-direction: column; + gap: var(--space-2); - [data-nav-button] { - padding: var(--space-3) var(--space-4); - border-radius: var(--border-radius-sm); - color: var(--color-text-muted); - text-decoration: none; - font-size: var(--font-size-sm); - font-weight: 500; - transition: all 0.15s ease; + [data-nav-button] { + padding: var(--space-3) var(--space-4); + border-radius: var(--border-radius-sm); + color: var(--color-text-muted); + text-decoration: none; + font-size: var(--font-size-sm); + font-weight: 500; + transition: all 0.15s ease; - &:hover { - color: var(--color-text); - } + &:hover { + color: var(--color-text); + } - &.active { - color: var(--color-text); - font-weight: 700; - position: relative; + &.active { + color: var(--color-text); + font-weight: 700; + position: relative; - &::before { - content: ''; - position: absolute; - left: calc(-1 * var(--space-0-5)); - top: 0; - bottom: 0; - width: 2px; - background-color: var(--color-text); - border-radius: 0 2px 2px 0; - } + &::before { + content: ''; + position: absolute; + left: calc(-1 * var(--space-0-5)); + top: 0; + bottom: 0; + width: 2px; + background-color: var(--color-text); + border-radius: 0 2px 2px 0; } } } @@ -63,6 +63,123 @@ } } +[data-page="workspace-[id]"] { + max-width: 64rem; + padding: var(--space-2) var(--space-4); + margin: 0 auto; + width: 100%; + display: flex; + flex-direction: column; + gap: var(--space-10); + + @media (max-width: 30rem) { + padding-top: var(--space-4); + padding-bottom: var(--space-4); + + gap: var(--space-8); + } + + [data-slot="sections"] { + display: flex; + flex-direction: column; + gap: var(--space-16); + + @media (max-width: 30rem) { + gap: var(--space-8); + } + + section { + display: flex; + flex-direction: column; + gap: var(--space-8); + + @media (max-width: 30rem) { + gap: var(--space-6); + } + + /* Section titles */ + [data-slot="section-title"] { + display: flex; + flex-direction: column; + gap: var(--space-1); + + h2 { + font-size: var(--font-size-md); + font-weight: 600; + line-height: 1.2; + letter-spacing: -0.03125rem; + margin: 0; + color: var(--color-text-secondary); + text-transform: uppercase; + + @media (max-width: 30rem) { + font-size: var(--font-size-md); + } + } + + p { + line-height: 1.5; + font-size: var(--font-size-md); + color: var(--color-text-muted); + + a { + color: var(--color-text-muted); + } + + @media (max-width: 30rem) { + font-size: var(--font-size-sm); + } + } + } + } + + section:not(:last-child) { + border-bottom: 1px solid var(--color-border); + padding-bottom: var(--space-16); + + @media (max-width: 30rem) { + padding-bottom: var(--space-8); + } + } + } + + /* Title section */ + [data-component="title-section"] { + display: flex; + flex-direction: column; + gap: var(--space-2); + padding-bottom: var(--space-8); + border-bottom: 1px solid var(--color-border); + + @media (max-width: 30rem) { + padding-bottom: var(--space-6); + } + + h1 { + font-size: var(--font-size-2xl); + font-weight: 500; + line-height: 1.2; + letter-spacing: -0.03125rem; + margin: 0; + text-transform: uppercase; + + @media (max-width: 30rem) { + font-size: var(--font-size-xl); + } + } + + p { + line-height: 1.5; + font-size: var(--font-size-md); + color: var(--color-text-muted); + + a { + color: var(--color-text-muted); + } + } + } +} + @media (max-width: 48rem) { [data-component="workspace-container"] { flex-direction: column; diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx index 21f20c70..a28bf93b 100644 --- a/packages/console/app/src/routes/workspace/[id].tsx +++ b/packages/console/app/src/routes/workspace/[id].tsx @@ -10,7 +10,7 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
From 756fb616913e230721eb41117b59d23cd271c179 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 13:52:54 -0400 Subject: [PATCH 20/34] wip: zen --- .../workspace/[id]/keys/key-section.module.css | 13 +++++++++++++ .../src/routes/workspace/[id]/provider-section.tsx | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css b/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css index ad20f1fa..92329cb5 100644 --- a/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css @@ -147,10 +147,23 @@ &[data-slot="key-actions"] { font-family: var(--font-sans); + + button { + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + } } } tbody tr { + &:hover { + [data-slot="key-actions"] button { + opacity: 1; + pointer-events: auto; + } + } + &:last-child td { border-bottom: none; } diff --git a/packages/console/app/src/routes/workspace/[id]/provider-section.tsx b/packages/console/app/src/routes/workspace/[id]/provider-section.tsx index 65d9e7d0..b83fd263 100644 --- a/packages/console/app/src/routes/workspace/[id]/provider-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/provider-section.tsx @@ -89,7 +89,7 @@ function ProviderRow(props: { provider: Provider }) { - + + } > - - + + + From ee846235f2c375560ab6095d2481351032f55a0b Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 14:19:06 -0400 Subject: [PATCH 22/34] wip: zen --- .../app/src/routes/workspace/[id]/keys/key-section.module.css | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css b/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css index 92329cb5..1066b7f0 100644 --- a/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/keys/key-section.module.css @@ -114,6 +114,7 @@ align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); + margin-left: calc(-1 * var(--space-3)); font-size: var(--font-size-sm); font-weight: 400; border: none; From 4227b89ebcab636a7e7f8e2bbbd560ac5081a2c4 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 14:54:49 -0400 Subject: [PATCH 23/34] wip: zen --- .../[id]/members/member-section.module.css | 149 +++++++++++++----- .../workspace/[id]/members/member-section.tsx | 128 ++++++++++++--- packages/console/core/src/user.ts | 5 +- 3 files changed, 217 insertions(+), 65 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css b/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css index 8fd86653..4d142c48 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css @@ -30,83 +30,150 @@ border: 1px solid var(--color-border); border-radius: var(--border-radius-sm); - [data-slot="input-container"] { + [data-slot="input-row"] { + display: flex; + flex-direction: row; + gap: var(--space-3); + + @media (max-width: 40rem) { + flex-direction: column; + gap: var(--space-2); + } + } + + [data-slot="input-field"] { display: flex; flex-direction: column; gap: var(--space-1); - } - - @media (max-width: 30rem) { - gap: var(--space-2); - } - - input { flex: 1; - padding: var(--space-2) var(--space-3); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - background-color: var(--color-bg); - color: var(--color-text); - font-size: var(--font-size-sm); - font-family: var(--font-mono); - &:focus { - outline: none; - border-color: var(--color-accent); + p { + line-height: 1.2; + margin: 0; + color: var(--color-text-muted); + font-size: var(--font-size-sm); } - &::placeholder { - color: var(--color-text-disabled); + input { + flex: 1; + padding: var(--space-2) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + color: var(--color-text); + font-size: var(--font-size-sm); + line-height: 1.5; + min-width: 0; + + &:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 3px var(--color-accent-alpha); + } + + &::placeholder { + color: var(--color-text-disabled); + } } } [data-slot="form-actions"] { display: flex; gap: var(--space-2); + + >button[type="reset"] { + align-self: flex-start; + } } [data-slot="form-error"] { color: var(--color-danger); font-size: var(--font-size-sm); - margin-top: var(--space-1); line-height: 1.4; + margin-top: calc(var(--space-1) * -1); } [data-slot="role-selector"] { - display: flex; - flex-direction: column; - gap: var(--space-2); + position: relative; - label { + [data-slot="trigger"] { display: flex; - gap: var(--space-3); - padding: var(--space-3); + align-items: center; + justify-content: space-between; + gap: var(--space-2); + width: 100%; + padding: var(--space-2) var(--space-3); border: 1px solid var(--color-border); border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + color: var(--color-text); + font-size: var(--font-size-sm); + line-height: 1.5; cursor: pointer; + transition: all 0.15s ease; &:hover { - background-color: var(--color-bg-surface); + border-color: var(--color-accent); } - input[type="radio"] { - margin-top: var(--space-1); + &:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 3px var(--color-accent-alpha); } - div { - flex: 1; + [data-slot="chevron"] { + opacity: 0.6; + transition: transform 0.15s ease; + } + } - strong { - display: block; - color: var(--color-text); - font-family: var(--font-sans); - margin-bottom: var(--space-1); + [data-slot="dropdown"] { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 10; + margin-top: var(--space-1); + padding: var(--space-1); + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + + [data-slot="item"] { + display: block; + width: 100%; + padding: var(--space-2) var(--space-3); + border: none; + background-color: transparent; + color: var(--color-text); + font-size: var(--font-size-sm); + text-align: left; + cursor: pointer; + border-radius: var(--border-radius-sm); + transition: background-color 0.15s ease; + + &:hover { + background-color: var(--color-bg-surface); } - p { - font-size: var(--font-size-xs); - color: var(--color-text-muted); - font-family: var(--font-sans); + &[data-selected="true"] { + background-color: var(--color-accent-alpha); + } + + div { + strong { + display: block; + color: var(--color-text); + margin-bottom: var(--space-1); + } + + p { + font-size: var(--font-size-xs); + color: var(--color-text-muted); + margin: 0; + } } } } diff --git a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx index f1831156..89c0ac95 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx @@ -1,11 +1,12 @@ import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router" -import { createEffect, createSignal, For, Show } from "solid-js" +import { createEffect, createSignal, For, Show, onCleanup } from "solid-js" import { withActor } from "~/context/auth.withActor" import { createStore } from "solid-js/store" import styles from "./member-section.module.css" import { UserRole } from "@opencode-ai/console-core/schema/user.sql.js" import { Actor } from "@opencode-ai/console-core/actor.js" import { User } from "@opencode-ai/console-core/user.js" +import { IconChevron } from "~/component/icon" const listMembers = query(async (workspaceID: string) => { "use server" @@ -26,10 +27,13 @@ const inviteMember = action(async (form: FormData) => { if (!workspaceID) return { error: "Workspace ID is required" } const role = form.get("role")?.toString() as (typeof UserRole)[number] if (!role) return { error: "Role is required" } + const limit = form.get("limit")?.toString() + const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null + if (monthlyLimit !== null && monthlyLimit < 0) return { error: "Set a valid monthly limit" } return json( await withActor( () => - User.invite({ email, role }) + User.invite({ email, role, monthlyLimit }) .then((data) => ({ error: undefined, data })) .catch((e) => ({ error: e.message as string })), workspaceID, @@ -213,9 +217,15 @@ export function MemberSection() { const params = useParams() const data = createAsync(() => listMembers(params.id)) const submission = useSubmission(inviteMember) - const [store, setStore] = createStore({ show: false }) + const [store, setStore] = createStore({ + show: false, + selectedRole: "member" as (typeof UserRole)[number], + showRoleDropdown: false, + limit: "", + }) let input: HTMLInputElement + let roleDropdownRef: HTMLDivElement | undefined createEffect(() => { if (!submission.pending && submission.result && !submission.result.error) { @@ -223,17 +233,36 @@ export function MemberSection() { } }) + createEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (roleDropdownRef && !roleDropdownRef.contains(event.target as Node)) { + setStore("showRoleDropdown", false) + } + } + + document.addEventListener("click", handleClickOutside) + onCleanup(() => document.removeEventListener("click", handleClickOutside)) + }) + function show() { while (true) { submission.clear() if (!submission.result) break } setStore("show", true) + setStore("selectedRole", "member") + setStore("limit", "") setTimeout(() => input?.focus(), 0) } function hide() { setStore("show", false) + setStore("showRoleDropdown", false) + } + + const roleLabels = { + admin: { title: "Admin", description: "Can manage models, members, and billing" }, + member: { title: "Member", description: "Can only generate API keys for themselves" }, } return ( @@ -251,28 +280,81 @@ export function MemberSection() { -
- (input = r)} data-component="input" name="email" type="text" placeholder="Enter email" /> -
- - +
+
+

Email

+ (input = r)} + data-component="input" + name="email" + type="text" + placeholder="Enter email" + /> +
+
+

Role

+
+ + +
+ + +
+
+
- - {(err) =>
{err()}
} -
+
+
+

Usage limit

+ setStore("limit", e.currentTarget.value)} + min="0" + /> +
+
+ + {(err) =>
{err()}
} +
+
-
-

Usage limit

+

Monthly spending limit

Date: Fri, 10 Oct 2025 16:34:07 -0400 Subject: [PATCH 25/34] wip: zen --- .../[id]/members/member-section.module.css | 137 +++++++++++- .../workspace/[id]/members/member-section.tsx | 209 +++++++++++------- 2 files changed, 262 insertions(+), 84 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css b/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css index 4d142c48..d67a29eb 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css @@ -132,7 +132,6 @@ position: absolute; top: 100%; left: 0; - right: 0; z-index: 10; margin-top: var(--space-1); padding: var(--space-1); @@ -140,6 +139,8 @@ border: 1px solid var(--color-border); border-radius: var(--border-radius-sm); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + min-width: 280px; + width: max-content; [data-slot="item"] { display: block; @@ -199,6 +200,14 @@ font-weight: normal; color: var(--color-text-muted); text-transform: uppercase; + + &:nth-child(2) { + width: 180px; + } + + &:nth-child(3) { + width: 200px; + } } td { @@ -216,6 +225,94 @@ &[data-slot="member-role"] { font-family: var(--font-mono); + [data-slot="role-selector"] { + position: relative; + + [data-slot="trigger"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2); + width: 100%; + padding: var(--space-2) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + color: var(--color-text); + font-size: var(--font-size-sm); + line-height: 1.5; + cursor: pointer; + transition: all 0.15s ease; + font-family: var(--font-sans); + + &:hover { + border-color: var(--color-accent); + } + + &:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 3px var(--color-accent-alpha); + } + + [data-slot="chevron"] { + opacity: 0.6; + transition: transform 0.15s ease; + } + } + + [data-slot="dropdown"] { + position: absolute; + top: 100%; + left: 0; + z-index: 10; + margin-top: var(--space-1); + padding: var(--space-1); + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + min-width: 280px; + width: max-content; + + [data-slot="item"] { + display: block; + width: 100%; + padding: var(--space-2) var(--space-3); + border: none; + background-color: transparent; + color: var(--color-text); + font-size: var(--font-size-sm); + text-align: left; + cursor: pointer; + border-radius: var(--border-radius-sm); + transition: background-color 0.15s ease; + + &:hover { + background-color: var(--color-bg-surface); + } + + &[data-selected="true"] { + background-color: var(--color-accent-alpha); + } + + div { + strong { + display: block; + color: var(--color-text); + margin-bottom: var(--space-1); + } + + p { + font-size: var(--font-size-xs); + color: var(--color-text-muted); + margin: 0; + } + } + } + } + } + button { display: flex; align-items: center; @@ -248,6 +345,30 @@ } } + &[data-slot="member-usage"] { + input { + width: 100%; + padding: var(--space-2) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + color: var(--color-text); + font-size: var(--font-size-sm); + line-height: 1.5; + font-family: var(--font-mono); + + &:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 3px var(--color-accent-alpha); + } + + &::placeholder { + color: var(--color-text-disabled); + } + } + } + &[data-slot="member-date"] { color: var(--color-text); } @@ -257,7 +378,17 @@ display: flex; gap: var(--space-2); - form button { + [data-slot="inline-edit-form"] { + display: flex; + gap: var(--space-2); + + button { + opacity: 1; + pointer-events: auto; + } + } + + form:not([data-slot="inline-edit-form"]) button { opacity: 0; pointer-events: none; transition: opacity 0.15s ease; @@ -267,7 +398,7 @@ tbody tr { &:hover { - [data-slot="member-actions"] form button { + [data-slot="member-actions"] form:not([data-slot="inline-edit-form"]) button { opacity: 1; pointer-events: auto; } diff --git a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx index 8cbff503..99408d51 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx @@ -86,17 +86,50 @@ const updateMember = action(async (form: FormData) => { }, "member.update") function MemberRow(props: { member: any; workspaceID: string; actorID: string; actorRole: string }) { - const [editing, setEditing] = createSignal(false) const submission = useSubmission(updateMember) const isCurrentUser = () => props.actorID === props.member.id const isAdmin = () => props.actorRole === "admin" + const [store, setStore] = createStore({ + editing: false, + selectedRole: props.member.role as (typeof UserRole)[number], + showRoleDropdown: false, + limit: "", + }) + + let roleDropdownRef: HTMLDivElement | undefined createEffect(() => { if (!submission.pending && submission.result && !submission.result.error) { - setEditing(false) + setStore("editing", false) } }) + createEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (roleDropdownRef && !roleDropdownRef.contains(event.target as Node)) { + setStore("showRoleDropdown", false) + } + } + + document.addEventListener("click", handleClickOutside) + onCleanup(() => document.removeEventListener("click", handleClickOutside)) + }) + + function show() { + while (true) { + submission.clear() + if (!submission.result) break + } + setStore("editing", true) + setStore("selectedRole", props.member.role) + setStore("limit", props.member.monthlyLimit?.toString() ?? "") + } + + function hide() { + setStore("editing", false) + setStore("showRoleDropdown", false) + } + function getUsageDisplay() { const currentUsage = (() => { const dateLastUsed = props.member.timeMonthlyUsageUpdated @@ -120,96 +153,110 @@ function MemberRow(props: { member: any; workspaceID: string; actorID: string; a return `$${currentUsage} / ${limit}` } - return ( - -
- - - - - - - - } - > - - + + + + + + - - + + ) } @@ -370,7 +417,7 @@ export function MemberSection() { - + From 48008f91ac9d11fef82f2149e5b937381780f577 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 16:42:27 -0400 Subject: [PATCH 26/34] wip: zen --- .../routes/workspace/[id]/provider-section.module.css | 10 ++++++++-- .../app/src/routes/workspace/[id]/provider-section.tsx | 8 +++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/provider-section.module.css b/packages/console/app/src/routes/workspace/[id]/provider-section.module.css index b693de77..1a450d3d 100644 --- a/packages/console/app/src/routes/workspace/[id]/provider-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/provider-section.module.css @@ -18,6 +18,14 @@ font-weight: normal; color: var(--color-text-muted); text-transform: uppercase; + + &:nth-child(1) { + width: 180px; + } + + &:nth-child(3) { + width: 200px; + } } td { @@ -35,7 +43,6 @@ &[data-slot="provider-key"] { text-align: left; color: var(--color-text-secondary); - width: 50%; [data-slot="edit-form"] { display: flex; @@ -82,7 +89,6 @@ &[data-slot="provider-action"] { text-align: left; font-family: var(--font-sans); - width: 30%; white-space: nowrap; [data-slot="configured-actions"] { diff --git a/packages/console/app/src/routes/workspace/[id]/provider-section.tsx b/packages/console/app/src/routes/workspace/[id]/provider-section.tsx index b83fd263..6ec8477b 100644 --- a/packages/console/app/src/routes/workspace/[id]/provider-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/provider-section.tsx @@ -147,9 +147,11 @@ function ProviderRow(props: { provider: Provider }) { > {saveSubmission.pending ? "Saving..." : "Save"} - + + + From f14cd4a3db12161eab93ce6538528ed882439a50 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 19:39:01 -0400 Subject: [PATCH 27/34] wip: zen --- packages/console/app/src/component/modal.css | 66 +++++++++++++++++++ packages/console/app/src/component/modal.tsx | 24 +++++++ .../app/src/routes/workspace-picker.css | 35 +++++----- .../app/src/routes/workspace-picker.tsx | 28 +++++--- 4 files changed, 125 insertions(+), 28 deletions(-) create mode 100644 packages/console/app/src/component/modal.css create mode 100644 packages/console/app/src/component/modal.tsx diff --git a/packages/console/app/src/component/modal.css b/packages/console/app/src/component/modal.css new file mode 100644 index 00000000..23b6831c --- /dev/null +++ b/packages/console/app/src/component/modal.css @@ -0,0 +1,66 @@ +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +[data-component="modal"][data-slot="overlay"] { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.5); + animation: fadeIn 0.2s ease; + + @media (prefers-color-scheme: dark) { + background-color: rgba(0, 0, 0, 0.7); + } + + [data-slot="content"] { + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-md); + padding: var(--space-6); + min-width: 400px; + max-width: 90vw; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); + animation: slideUp 0.2s ease; + + @media (max-width: 30rem) { + min-width: 300px; + padding: var(--space-4); + } + + @media (prefers-color-scheme: dark) { + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); + } + } + + [data-slot="title"] { + margin: 0 0 var(--space-4) 0; + font-size: var(--font-size-lg); + font-weight: 600; + color: var(--color-text); + } +} \ No newline at end of file diff --git a/packages/console/app/src/component/modal.tsx b/packages/console/app/src/component/modal.tsx new file mode 100644 index 00000000..d6dc8a3d --- /dev/null +++ b/packages/console/app/src/component/modal.tsx @@ -0,0 +1,24 @@ +import { JSX, Show } from "solid-js" +import "./modal.css" + +interface ModalProps { + open: boolean + onClose: () => void + title?: string + children: JSX.Element +} + +export function Modal(props: ModalProps) { + return ( + +
+
e.stopPropagation()}> + +

{props.title}

+
+ {props.children} +
+
+
+ ) +} diff --git a/packages/console/app/src/routes/workspace-picker.css b/packages/console/app/src/routes/workspace-picker.css index c174cabe..dec48228 100644 --- a/packages/console/app/src/routes/workspace-picker.css +++ b/packages/console/app/src/routes/workspace-picker.css @@ -1,15 +1,15 @@ [data-component="workspace-picker"] { position: relative; - /* Override blue accent colors with neutral colors */ - --color-accent: var(--color-border); - --color-accent-hover: var(--color-border); - --color-accent-active: var(--color-border); - --color-primary: var(--color-border); - --color-primary-hover: var(--color-border); - --color-primary-active: var(--color-border); - --color-primary-alpha-20: transparent; [data-slot="trigger"] { + /* Override blue accent colors with neutral colors for dropdown trigger */ + --color-accent: var(--color-border); + --color-accent-hover: var(--color-border); + --color-accent-active: var(--color-border); + --color-primary: var(--color-border); + --color-primary-hover: var(--color-border); + --color-primary-active: var(--color-border); + --color-primary-alpha-20: transparent; display: flex; align-items: center; justify-content: space-between; @@ -73,22 +73,19 @@ } [data-slot="create-form"] { - margin-top: var(--space-4); - padding: var(--space-4); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - background-color: var(--color-surface); + width: 100%; } [data-slot="create-input-group"] { display: flex; - gap: var(--space-2); - align-items: center; + flex-direction: column; + gap: var(--space-3); + } - @media (max-width: 30rem) { - flex-direction: column; - align-items: stretch; - } + [data-slot="button-group"] { + display: flex; + gap: var(--space-2); + justify-content: flex-end; } [data-slot="create-input"] { diff --git a/packages/console/app/src/routes/workspace-picker.tsx b/packages/console/app/src/routes/workspace-picker.tsx index fb77d8f4..51de4cef 100644 --- a/packages/console/app/src/routes/workspace-picker.tsx +++ b/packages/console/app/src/routes/workspace-picker.tsx @@ -8,6 +8,7 @@ import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.j import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" import { Workspace } from "@opencode-ai/console-core/workspace.js" import { IconChevron } from "~/component/icon" +import { Modal } from "~/component/modal" import "./workspace-picker.css" const getWorkspaces = query(async () => { @@ -46,6 +47,7 @@ export function WorkspacePicker() { showDropdown: false, }) let dropdownRef: HTMLDivElement | undefined + let inputRef: HTMLInputElement | undefined const currentWorkspace = () => { const ws = workspaces()?.find((w) => w.id === params.id) @@ -56,6 +58,12 @@ export function WorkspacePicker() { setStore({ showForm: true, showDropdown: false }) } + createEffect(() => { + if (store.showForm && inputRef) { + setTimeout(() => inputRef?.focus(), 0) + } + }) + const handleSelectWorkspace = (workspaceID: string) => { if (workspaceID === params.id) { setStore("showDropdown", false) @@ -112,26 +120,28 @@ export function WorkspacePicker() { - + setStore("showForm", false)} title="Create New Workspace">
- - +
+ + +
-
+ ) } From cc590364e968bec62c62861f2450034e9574f76b Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 19:49:59 -0400 Subject: [PATCH 28/34] wip: zen --- packages/console/app/src/routes/workspace-picker.tsx | 7 ++++--- packages/console/app/src/routes/workspace/[id].tsx | 6 +++--- packages/console/core/src/actor.ts | 2 +- packages/console/core/src/workspace.ts | 1 + 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/console/app/src/routes/workspace-picker.tsx b/packages/console/app/src/routes/workspace-picker.tsx index 51de4cef..34a54497 100644 --- a/packages/console/app/src/routes/workspace-picker.tsx +++ b/packages/console/app/src/routes/workspace-picker.tsx @@ -1,4 +1,4 @@ -import { query, useParams, action, createAsync, redirect } from "@solidjs/router" +import { query, useParams, action, createAsync, redirect, useSubmission } from "@solidjs/router" import { For, Show, createEffect, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import { withActor } from "~/context/auth.withActor" @@ -42,6 +42,7 @@ const createWorkspace = action(async (form: FormData) => { export function WorkspacePicker() { const params = useParams() const workspaces = createAsync(() => getWorkspaces()) + const submission = useSubmission(createWorkspace) const [store, setStore] = createStore({ showForm: false, showDropdown: false, @@ -135,8 +136,8 @@ export function WorkspacePicker() { - diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx index a28bf93b..8347cd49 100644 --- a/packages/console/app/src/routes/workspace/[id].tsx +++ b/packages/console/app/src/routes/workspace/[id].tsx @@ -24,10 +24,10 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
Billing + + Settings + - - Settings -
{props.children}
diff --git a/packages/console/core/src/actor.ts b/packages/console/core/src/actor.ts index e8d1b7a6..48f4a636 100644 --- a/packages/console/core/src/actor.ts +++ b/packages/console/core/src/actor.ts @@ -69,7 +69,7 @@ export namespace Actor { export const assertAdmin = () => { if (userRole() === "admin") return - throw new Error(`Expected admin user, got ${userRole()}`) + throw new Error(`Action not allowed. Ask your workspace admin to perform this action.`) } export function workspace() { diff --git a/packages/console/core/src/workspace.ts b/packages/console/core/src/workspace.ts index 7a742e89..655112ae 100644 --- a/packages/console/core/src/workspace.ts +++ b/packages/console/core/src/workspace.ts @@ -52,6 +52,7 @@ export namespace Workspace { name: z.string().min(1).max(255), }), async ({ name }) => { + Actor.assertAdmin() const workspaceID = Actor.workspace() return await Database.use((tx) => tx From 4dda7cc6a47b433fd66e312f17d6a335da574338 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 19:56:40 -0400 Subject: [PATCH 29/34] wip: zen --- .../src/routes/workspace/[id]/members/member-section.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx index 99408d51..e60049a7 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx @@ -145,8 +145,8 @@ function MemberRow(props: { member: any; workspaceID: string; actorID: string; a month: "long", timeZone: "UTC", }) - if (current !== lastUsed) return 0 - return ((props.member.monthlyUsage ?? 0) / 100000000).toFixed(2) + const usage = current === lastUsed ? (props.member.monthlyUsage ?? 0) : 0 + return (usage / 100000000).toFixed(2) })() const limit = props.member.monthlyLimit ? `$${props.member.monthlyLimit}` : "no limit" @@ -417,7 +417,7 @@ export function MemberSection() { - + From ee1eb35269360b344357d8c9e2f3fc934bfecead Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 20:02:17 -0400 Subject: [PATCH 30/34] wip: zen --- .../[id]/billing/billing-section.tsx | 6 +--- .../[id]/billing/payment-section.tsx | 2 +- .../app/src/routes/workspace/[id]/common.tsx | 25 -------------- .../workspace/[id]/keys/key-section.tsx | 2 +- .../routes/workspace/[id]/usage-section.tsx | 2 +- .../app/src/routes/workspace/common.tsx | 34 ++++++++++++++++++- 6 files changed, 37 insertions(+), 34 deletions(-) delete mode 100644 packages/console/app/src/routes/workspace/[id]/common.tsx diff --git a/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx index 295ad339..f9084bbf 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx @@ -6,11 +6,7 @@ import { IconCreditCard } from "~/component/icon" import styles from "./billing-section.module.css" import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js" import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js" - -const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => { - "use server" - return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID) -}, "checkoutUrl") +import { createCheckoutUrl } from "../../common" const reload = action(async (form: FormData) => { "use server" diff --git a/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx index d3520bea..c830cee8 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx @@ -2,7 +2,7 @@ import { Billing } from "@opencode-ai/console-core/billing.js" import { query, action, useParams, createAsync, useAction } from "@solidjs/router" import { For, Show } from "solid-js" import { withActor } from "~/context/auth.withActor" -import { formatDateUTC, formatDateForTable } from "../common" +import { formatDateUTC, formatDateForTable } from "../../common" import styles from "./payment-section.module.css" const getPaymentsInfo = query(async (workspaceID: string) => { diff --git a/packages/console/app/src/routes/workspace/[id]/common.tsx b/packages/console/app/src/routes/workspace/[id]/common.tsx deleted file mode 100644 index f85fd842..00000000 --- a/packages/console/app/src/routes/workspace/[id]/common.tsx +++ /dev/null @@ -1,25 +0,0 @@ -export function formatDateForTable(date: Date) { - const options: Intl.DateTimeFormatOptions = { - day: "numeric", - month: "short", - hour: "numeric", - minute: "2-digit", - hour12: true, - } - return date.toLocaleDateString("en-GB", options).replace(",", ",") -} - -export function formatDateUTC(date: Date) { - const options: Intl.DateTimeFormatOptions = { - weekday: "short", - year: "numeric", - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit", - second: "2-digit", - timeZoneName: "short", - timeZone: "UTC", - } - return date.toLocaleDateString("en-US", options) -} diff --git a/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx b/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx index 87c1541d..565981c7 100644 --- a/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx @@ -4,7 +4,7 @@ import { IconCopy, IconCheck } from "~/component/icon" import { Key } from "@opencode-ai/console-core/key.js" import { withActor } from "~/context/auth.withActor" import { createStore } from "solid-js/store" -import { formatDateUTC, formatDateForTable } from "../common" +import { formatDateUTC, formatDateForTable } from "../../common" import styles from "./key-section.module.css" import { Actor } from "@opencode-ai/console-core/actor.js" diff --git a/packages/console/app/src/routes/workspace/[id]/usage-section.tsx b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx index 9f65fe5f..47a2e43f 100644 --- a/packages/console/app/src/routes/workspace/[id]/usage-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx @@ -1,7 +1,7 @@ import { Billing } from "@opencode-ai/console-core/billing.js" import { query, useParams, createAsync } from "@solidjs/router" import { createMemo, For, Show } from "solid-js" -import { formatDateUTC, formatDateForTable } from "./common" +import { formatDateUTC, formatDateForTable } from "../common" import { withActor } from "~/context/auth.withActor" import styles from "./usage-section.module.css" diff --git a/packages/console/app/src/routes/workspace/common.tsx b/packages/console/app/src/routes/workspace/common.tsx index d1f1aba8..27c69d9d 100644 --- a/packages/console/app/src/routes/workspace/common.tsx +++ b/packages/console/app/src/routes/workspace/common.tsx @@ -1,7 +1,34 @@ import { Resource } from "@opencode-ai/console-resource" import { Actor } from "@opencode-ai/console-core/actor.js" -import { query } from "@solidjs/router" +import { action, query } from "@solidjs/router" import { withActor } from "~/context/auth.withActor" +import { Billing } from "@opencode-ai/console-core/billing.js" + +export function formatDateForTable(date: Date) { + const options: Intl.DateTimeFormatOptions = { + day: "numeric", + month: "short", + hour: "numeric", + minute: "2-digit", + hour12: true, + } + return date.toLocaleDateString("en-GB", options).replace(",", ",") +} + +export function formatDateUTC(date: Date) { + const options: Intl.DateTimeFormatOptions = { + weekday: "short", + year: "numeric", + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + second: "2-digit", + timeZoneName: "short", + timeZone: "UTC", + } + return date.toLocaleDateString("en-US", options) +} export const querySessionInfo = query(async (workspaceID: string) => { "use server" @@ -12,3 +39,8 @@ export const querySessionInfo = query(async (workspaceID: string) => { } }, workspaceID) }, "session.get") + +export const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => { + "use server" + return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID) +}, "checkoutUrl") From 5b27130d60e2b294cc364085e283de93d5e8d2af Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 20:16:44 -0400 Subject: [PATCH 31/34] wip: zen --- .../console/app/src/routes/workspace/[id].css | 25 ++++++++++ .../app/src/routes/workspace/[id]/index.tsx | 50 ++++++++++++++++--- .../app/src/routes/workspace/common.tsx | 5 ++ 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id].css b/packages/console/app/src/routes/workspace/[id].css index 23c195ff..7315e6bb 100644 --- a/packages/console/app/src/routes/workspace/[id].css +++ b/packages/console/app/src/routes/workspace/[id].css @@ -177,10 +177,35 @@ line-height: 1.5; font-size: var(--font-size-md); color: var(--color-text-muted); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + + @media (max-width: 48rem) { + flex-direction: column; + align-items: flex-start; + gap: var(--space-3); + } a { color: var(--color-text-muted); } + + [data-slot="billing-info"] { + flex-shrink: 0; + margin-left: auto; + } + + [data-slot="balance"] { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + + b { + font-weight: 600; + color: var(--color-text); + } + } } } } diff --git a/packages/console/app/src/routes/workspace/[id]/index.tsx b/packages/console/app/src/routes/workspace/[id]/index.tsx index 6e0aa70e..7f196e45 100644 --- a/packages/console/app/src/routes/workspace/[id]/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/index.tsx @@ -3,24 +3,58 @@ import { UsageSection } from "./usage-section" import { ModelSection } from "./model-section" import { ProviderSection } from "./provider-section" import { IconLogo } from "~/component/icon" -import { createAsync, useParams } from "@solidjs/router" -import { querySessionInfo } from "../common" -import { Show } from "solid-js" +import { createAsync, useParams, useAction, useSubmission } from "@solidjs/router" +import { querySessionInfo, queryBillingInfo, createCheckoutUrl } from "../common" +import { Show, createMemo } from "solid-js" export default function () { const params = useParams() const userInfo = createAsync(() => querySessionInfo(params.id)) + const billingInfo = createAsync(() => queryBillingInfo(params.id)) + const createCheckoutUrlAction = useAction(createCheckoutUrl) + const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl) + + const balanceAmount = createMemo(() => { + return ((billingInfo()?.balance ?? 0) / 100000000).toFixed(2) + }) return (

- Curated list of models provided by opencode.{" "} - - Learn more - - . + + Reliable optimized models for coding agents.{" "} + + Learn more + + . + + + { + const baseUrl = window.location.href + const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl) + if (checkoutUrl) { + window.location.href = checkoutUrl + } + }} + > + {createCheckoutUrlSubmission.pending ? "Loading..." : "Enable billing"} + + } + > + + Current balance: ${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()} + + +

diff --git a/packages/console/app/src/routes/workspace/common.tsx b/packages/console/app/src/routes/workspace/common.tsx index 27c69d9d..fef1b3cd 100644 --- a/packages/console/app/src/routes/workspace/common.tsx +++ b/packages/console/app/src/routes/workspace/common.tsx @@ -44,3 +44,8 @@ export const createCheckoutUrl = action(async (workspaceID: string, successUrl: "use server" return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID) }, "checkoutUrl") + +export const queryBillingInfo = query(async (workspaceID: string) => { + "use server" + return withActor(() => Billing.get(), workspaceID) +}, "billing.get") From daa0ca40f2c31ad7bb1c3c13e5280f4125466ffc Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 20:30:13 -0400 Subject: [PATCH 32/34] wip: zen --- packages/console/app/src/routes/workspace/[id].css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/app/src/routes/workspace/[id].css b/packages/console/app/src/routes/workspace/[id].css index 7315e6bb..c8df0ae2 100644 --- a/packages/console/app/src/routes/workspace/[id].css +++ b/packages/console/app/src/routes/workspace/[id].css @@ -66,7 +66,7 @@ [data-page="workspace-[id]"] { max-width: 64rem; padding: var(--space-2) var(--space-4); - margin: 0 auto; + margin: 0; width: 100%; display: flex; flex-direction: column; From b946fd21b18184a61a2d62e073a18a0850866801 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 20:32:28 -0400 Subject: [PATCH 33/34] wip: zen --- packages/console/app/src/routes/workspace/[id].css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id].css b/packages/console/app/src/routes/workspace/[id].css index c8df0ae2..e2aa0774 100644 --- a/packages/console/app/src/routes/workspace/[id].css +++ b/packages/console/app/src/routes/workspace/[id].css @@ -108,7 +108,7 @@ line-height: 1.2; letter-spacing: -0.03125rem; margin: 0; - color: var(--color-text-secondary); + color: var(--color-text); @media (max-width: 30rem) { font-size: var(--font-size-md); @@ -176,7 +176,7 @@ p { line-height: 1.5; font-size: var(--font-size-md); - color: var(--color-text-muted); + color: var(--color-text); display: flex; align-items: center; justify-content: space-between; From c7dfbbeed0e7b5a7421b4b0d8c115a24f5ba7534 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 21:21:55 -0400 Subject: [PATCH 34/34] wip: zen --- packages/console/core/src/user.ts | 20 +++++- packages/console/mail/emails/components.tsx | 4 ++ .../mail/emails/templates/InviteEmail.tsx | 60 ++++++++---------- .../mail/emails/templates/static/logo.png | Bin 0 -> 1726 bytes 4 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 packages/console/mail/emails/templates/static/logo.png diff --git a/packages/console/core/src/user.ts b/packages/console/core/src/user.ts index 63877150..40d74f93 100644 --- a/packages/console/core/src/user.ts +++ b/packages/console/core/src/user.ts @@ -11,6 +11,7 @@ import { Account } from "./account" import { AccountTable } from "./schema/account.sql" import { Key } from "./key" import { KeyTable } from "./schema/key.sql" +import { WorkspaceTable } from "./schema/workspace.sql" export namespace User { const assertNotSelf = (id: string) => { @@ -115,6 +116,21 @@ export namespace User { // send email, ignore errors try { + const emailInfo = await Database.use((tx) => + tx + .select({ + email: AccountTable.email, + workspaceName: WorkspaceTable.name, + }) + .from(UserTable) + .innerJoin(AccountTable, eq(UserTable.accountID, AccountTable.id)) + .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, workspaceID)) + .where( + and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.id, Actor.assert("user").properties.userID)), + ) + .then((rows) => rows[0]), + ) + const { InviteEmail } = await import("@opencode-ai/console-mail/InviteEmail.jsx") await AWS.sendEmail({ to: email, @@ -122,8 +138,10 @@ export namespace User { body: render( // @ts-ignore InviteEmail({ + inviter: emailInfo.email, assetsUrl: `https://opencode.ai/email`, - workspace: workspaceID, + workspaceID: workspaceID, + workspaceName: emailInfo.workspaceName, }), ), }) diff --git a/packages/console/mail/emails/components.tsx b/packages/console/mail/emails/components.tsx index d030b6cb..ff845c8f 100644 --- a/packages/console/mail/emails/components.tsx +++ b/packages/console/mail/emails/components.tsx @@ -31,6 +31,10 @@ export function A({ children, ...props }: AProps) { return React.createElement("a", props, children) } +export function B({ children, ...props }: AProps) { + return React.createElement("b", props, children) +} + export function Span({ children, ...props }: SpanProps) { return React.createElement("span", props, children) } diff --git a/packages/console/mail/emails/templates/InviteEmail.tsx b/packages/console/mail/emails/templates/InviteEmail.tsx index 978080a9..5c963022 100644 --- a/packages/console/mail/emails/templates/InviteEmail.tsx +++ b/packages/console/mail/emails/templates/InviteEmail.tsx @@ -1,7 +1,7 @@ // @ts-nocheck import React from "react" import { Img, Row, Html, Link, Body, Head, Button, Column, Preview, Section, Container } from "@jsx-email/all" -import { Hr, Text, Fonts, SplitString, Title, A, Span } from "../components" +import { Hr, Text, Fonts, SplitString, Title, A, Span, B } from "../components" import { unit, body, @@ -23,17 +23,24 @@ const CONSOLE_URL = "https://opencode.ai/" const DOC_URL = "https://opencode.ai/docs/zen" interface InviteEmailProps { - workspace: string + inviter: string + workspaceID: string + workspaceName: string assetsUrl: string } -export const InviteEmail = ({ workspace, assetsUrl = LOCAL_ASSETS_URL }: InviteEmailProps) => { - const subject = `Join the ${workspace} workspace` - const messagePlain = `You've been invited to join the ${workspace} workspace in the OpenCode Zen Console.` - const url = `${CONSOLE_URL}workspace/${workspace}` +export const InviteEmail = ({ + inviter = "test@anoma.ly", + workspaceID = "wrk_01K6XFY7V53T8XN0A7X8G9BTN3", + workspaceName = "anomaly", + assetsUrl = LOCAL_ASSETS_URL, +}: InviteEmailProps) => { + const subject = `You were invited to the OpenCode Console` + const messagePlain = `${inviter} invited you to join the ${workspaceName} workspace (${workspaceID}).` + const url = `${CONSOLE_URL}workspace/${workspaceID}` return ( - {`OpenCode Zen — ${messagePlain}`} + {`OpenCode — ${messagePlain}`} {messagePlain} @@ -42,15 +49,10 @@ export const InviteEmail = ({ workspace, assetsUrl = LOCAL_ASSETS_URL }: InviteE
- - OpenCode Zen Logo + + OpenCode Logo - - - @@ -59,32 +61,26 @@ export const InviteEmail = ({ workspace, assetsUrl = LOCAL_ASSETS_URL }: InviteE -
- - OpenCode Zen - : - {workspace} - - - - - - -
- You've been invited to join the{" "} + {inviter} invited you to join the{" "} - {workspace} + {workspaceName} {" "} - workspace in the{" "} - - OpenCode Zen Console + workspace ({workspaceID}) in the{" "} + + OpenCode Console .
+
+ +
+
@@ -93,7 +89,7 @@ export const InviteEmail = ({ workspace, assetsUrl = LOCAL_ASSETS_URL }: InviteE - + Console diff --git a/packages/console/mail/emails/templates/static/logo.png b/packages/console/mail/emails/templates/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1d4a396398a2e8f8c0ac4aa935a9102b8683f5c1 GIT binary patch literal 1726 zcmb`I`&ZI;7{@-mJg?{S%X8lEAD)L26BWA7*2NY8 zur53da}yi9Qv?9E`hO0K;9H19^U#8DG&Bf6&s>$}X7%}l zkp}_ztIW=L#tH!IW8s*C$FnTv#hqn1+2-EaC4NG#1r-}}yFY33mQUaq#^+Sn_MwTb z&Nqu#nSS=zSAlIl;jDX<+slnasN{MvPSO}rywTa`$A0^ed%J@q6DQxrO>1P|QG|{4 zl^Wk`1DY_pGGx!fhdFdl71I`6anB<8<0c-co+9oDB*Qg^LgBhq0Q^CT0^r5UAd=~d zW^dYm9DC^wiZFK`Q6!Vet}>wKR+Gt%jWpxzShEUIRNl}a4gFSX!>&c;kjN#?RVrCB zG@vNl#Ob*JLskdOq_!ano%zLzW068s)TT(ttYh?2w%34CECN&VIh#zz=eTK-8m zHe)-gZl?^V*;^90LzdF|1KG_<_+Y#}`60(Kz%0eFuuogr(g9uD6%kQ!b}bf*H;pPs zz3MHiS^A(wZZelE;+gQ5<|GtV{J8dHDl39fEK>E&Bpr#vd!$W2O0mTsqtu%hK`e`C2b3bzr!; zSwU5L`bU!zp>Dkh^IWO5-SH{2f6c-ZnQp$luUxElu5*1npmueAR>z*-+Eg=l&}c$AZOhb zo`=+_`^EKux{cbI6k;!g??kn>>7Gn8$#kcj-W$>R-ugdpru%1e-=P3dV!T!Yjh37s z-5ar!K{sy)6di}I*$=9D?$p4_sw&6YTvnQuoGQ&mfI1b;$Bo*ZvlIf~z*7N`Yq3J> z=~f=(#ml+rh8vZSCNnRF-pyTRI?(NqNMt=+EtM+xxLM8AN$00--b2+(D{yA*p+5TGHgO?7J*` zPIk;g?D(c5QRO+^)J_`wpph5qKC1nCN;&uYKRbFF2yDOs)c-|?at>k39(Od+)G({b z!`$r3a04S{Lj%{}eSg-M_}#D)6S`yPxc~{m*1fvgg$%_rV@32jd_$LqP<1FiCkY+W z$9~9R6bQZHaQI$7==1Gu-g92J1{`qkPgn!Ks#UCBDZC_GykZ~ky7c;9ocY~=@Zcy6 I6qH!-A40eeDgXcg literal 0 HcmV?d00001
{providerData() ? maskCredentials(providerData()!.credentials) : "--"}} + fallback={{providerData() ? maskCredentials(providerData()!.credentials) : "-"}} >
From 9463ce8006d94ac8f66b71fea3fdf5efc49a744e Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 10 Oct 2025 14:11:48 -0400 Subject: [PATCH 21/34] wip: zen --- .../[id]/members/member-section.module.css | 15 +++++++++++++++ .../workspace/[id]/members/member-section.tsx | 14 ++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css b/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css index 14246d58..8fd86653 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.module.css @@ -187,10 +187,25 @@ &[data-slot="member-actions"] { font-family: var(--font-sans); + display: flex; + gap: var(--space-2); + + form button { + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + } } } tbody tr { + &:hover { + [data-slot="member-actions"] form button { + opacity: 1; + pointer-events: auto; + } + } + &:last-child td { border-bottom: none; } diff --git a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx index e9f61724..f1831156 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx @@ -125,8 +125,8 @@ function MemberRow(props: { member: any; workspaceID: string; actorID: string; a
{props.member.role} {getUsageDisplay()} {props.member.timeSeen ? "" : "invited"} - + + @@ -137,13 +137,13 @@ function MemberRow(props: { member: any; workspaceID: string; actorID: string; a - -
+
{props.member.accountEmail ?? props.member.email}
@@ -292,7 +292,9 @@ export function MemberSection() {
Role Usage
{props.member.accountEmail ?? props.member.email}{props.member.role}{getUsageDisplay()}{props.member.timeSeen ? "" : "invited"} - - - - - - - - -
-
-
{props.member.accountEmail ?? props.member.email}
- - + const roleLabels = { + admin: { title: "Admin", description: "Can manage models, members, and billing" }, + member: { title: "Member", description: "Can only generate API keys for themselves" }, + } - -
Role: {props.member.role}
- - - } + return ( +
{props.member.accountEmail ?? props.member.email} + {props.member.role}}> +
+ + +
+ +
- -
- -
- - - {(err) =>
{err()}
} -
- -
- -
+ +
+ {getUsageDisplay()}}> + setStore("limit", e.currentTarget.value)} + placeholder="No limit" + min="0" + /> + + {props.member.timeSeen ? "" : "invited"} + + + + + + + + + + + } + > +
+ + + + + - -
+ + + + +
Email RoleUsageLimit
Email RoleLimitMonth limit