diff options
| author | Frank <[email protected]> | 2025-10-09 22:38:42 -0400 |
|---|---|---|
| committer | Frank <[email protected]> | 2025-10-09 22:38:42 -0400 |
| commit | bc0e00cbb7e68d80e826dd1606fddc9228e1210d (patch) | |
| tree | 7e8717d49825669d9089785e1325ba341c17b41d /packages | |
| parent | 60dd987efd2435e5122fb98ea1c02041649416e7 (diff) | |
| download | opencode-bc0e00cbb7e68d80e826dd1606fddc9228e1210d.tar.gz opencode-bc0e00cbb7e68d80e826dd1606fddc9228e1210d.zip | |
wip: zen style header
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/console/app/src/component/icon.tsx | 76 | ||||
| -rw-r--r-- | packages/console/app/src/routes/user-menu.css | 68 | ||||
| -rw-r--r-- | packages/console/app/src/routes/user-menu.tsx | 63 | ||||
| -rw-r--r-- | packages/console/app/src/routes/workspace-picker.css | 92 | ||||
| -rw-r--r-- | packages/console/app/src/routes/workspace-picker.tsx | 19 | ||||
| -rw-r--r-- | packages/console/app/src/routes/workspace.css | 28 | ||||
| -rw-r--r-- | packages/console/app/src/routes/workspace.tsx | 36 |
7 files changed, 215 insertions, 167 deletions
diff --git a/packages/console/app/src/component/icon.tsx b/packages/console/app/src/component/icon.tsx index 2b2dbe411..bb3c62da9 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<SVGSVGElement>) { return ( - <svg {...props} width="234" height="42" viewBox="0 0 234 42" fill="none" - xmlns="http://www.w3.org/2000/svg"> - <path fill-rule="evenodd" clip-rule="evenodd" - d="M54 36H36V42H30V6H54V36ZM36 30H48V12H36V30Z" fill="currentColor"/> - <path fill-rule="evenodd" clip-rule="evenodd" - d="M24 36H0V6H24V36ZM6 30H18V12H6V30Z" fill="currentColor"/> - <path fill-rule="evenodd" clip-rule="evenodd" - d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" fill="currentColor"/> - <path d="M108 12H96V36H90V6H108V12Z" fill="currentColor"/> - <path d="M114 36H108V12H114V36Z" fill="currentColor"/> - <path d="M144 12H126V30H144V36H120V6H144V12Z" fill="currentColor"/> - <path fill-rule="evenodd" clip-rule="evenodd" - d="M174 36H150V6H174V36ZM156 30H168V12H156V30Z" fill="currentColor"/> - <path fill-rule="evenodd" clip-rule="evenodd" - d="M204 36H180V6H198V0H204V36ZM186 30H198V12H186V30Z" fill="currentColor"/> - <path fill-rule="evenodd" clip-rule="evenodd" - d="M234 24H216V30H234V36H210V6H234V24ZM216 18H228V12H216V18Z" fill="currentColor"/> - </svg> - -) + <svg {...props} width="234" height="42" viewBox="0 0 234 42" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M54 36H36V42H30V6H54V36ZM36 30H48V12H36V30Z" + fill="currentColor" + /> + <path fill-rule="evenodd" clip-rule="evenodd" d="M24 36H0V6H24V36ZM6 30H18V12H6V30Z" fill="currentColor" /> + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" + fill="currentColor" + /> + <path d="M108 12H96V36H90V6H108V12Z" fill="currentColor" /> + <path d="M114 36H108V12H114V36Z" fill="currentColor" /> + <path d="M144 12H126V30H144V36H120V6H144V12Z" fill="currentColor" /> + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M174 36H150V6H174V36ZM156 30H168V12H156V30Z" + fill="currentColor" + /> + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M204 36H180V6H198V0H204V36ZM186 30H198V12H186V30Z" + fill="currentColor" + /> + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M234 24H216V30H234V36H210V6H234V24ZM216 18H228V12H216V18Z" + fill="currentColor" + /> + </svg> + ) } export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) { @@ -55,3 +72,22 @@ export function IconCreditCard(props: JSX.SvgSVGAttributes<SVGSVGElement>) { </svg> ) } + +export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) { + return ( + <svg {...props} width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path + fill="currentColor" + d="M4.00024 5.04041L7.37401 1.66663L6.66691 0.959525L4.00024 3.62619L1.33357 0.959525L0.626465 1.66663L4.00024 5.04041Z" + /> + </svg> + ) +} + +export function IconWorkspaceLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) { + return ( + <svg {...props} width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M18 6H6V24H18V6ZM24 30H0V0H24V30Z" fill="currentColor" /> + </svg> + ) +} 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 000000000..28c7937f5 --- /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 000000000..8c011fc0b --- /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 ( + <div data-component="user-menu"> + <div ref={dropdownRef}> + <button data-slot="trigger" type="button" onClick={() => setStore("showDropdown", !store.showDropdown)}> + <span>{props.email}</span> + <IconChevron data-slot="chevron" /> + </button> + + <Show when={store.showDropdown}> + <div data-slot="dropdown"> + <form action={logout} method="post"> + <button type="submit" formaction={logout} data-slot="item"> + Logout + </button> + </form> + </div> + </Show> + </div> + </div> + ) +} diff --git a/packages/console/app/src/routes/workspace-picker.css b/packages/console/app/src/routes/workspace-picker.css index c22ced867..c174cabe5 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 181826335..fb77d8f45 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 ( <div data-component="workspace-picker"> <div ref={dropdownRef}> - <div data-slot="trigger" onClick={() => setStore("showDropdown", !store.showDropdown)}> + <button data-slot="trigger" type="button" onClick={() => setStore("showDropdown", !store.showDropdown)}> <span>{currentWorkspace()}</span> - <svg data-slot="chevron" width="12" height="8" viewBox="0 0 12 8" fill="none"> - <path - d="M1 1L6 6L11 1" - stroke="currentColor" - stroke-width="2" - stroke-linecap="round" - stroke-linejoin="round" - /> - </svg> - </div> + <IconChevron data-slot="chevron" /> + </button> <Show when={store.showDropdown}> <div data-slot="dropdown"> <For each={workspaces()}> {(workspace) => ( <button - data-slot="option" + data-slot="item" data-selected={workspace.id === params.id} type="button" onClick={() => handleSelectWorkspace(workspace.id)} @@ -112,7 +105,7 @@ export function WorkspacePicker() { </button> )} </For> - <button data-slot="create-option" type="button" onClick={() => handleWorkspaceNew()}> + <button data-slot="create-item" type="button" onClick={() => handleWorkspaceNew()}> + Create New Workspace </button> </div> diff --git a/packages/console/app/src/routes/workspace.css b/packages/console/app/src/routes/workspace.css index ed94365f0..e8f12796e 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 f87123d31..04e3f2c47 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) { <header data-component="workspace-header"> <div data-slot="header-brand"> <A href="/" data-component="site-title"> - <IconLogo /> + <IconWorkspaceLogo /> </A> - </div> - <div data-slot="header-actions"> <Show when={sessionInfo()?.isBeta}> <WorkspacePicker /> </Show> - <span data-slot="user">{userEmail()}</span> - <form action={logout} method="post"> - <button type="submit" formaction={logout}> - Logout - </button> - </form> + </div> + <div data-slot="header-actions"> + <UserMenu email={userEmail()} /> </div> </header> <div>{props.children}</div> |
