From bc0e00cbb7e68d80e826dd1606fddc9228e1210d Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 9 Oct 2025 22:38:42 -0400 Subject: [PATCH] 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}