summaryrefslogtreecommitdiffhomepage
path: root/packages/console
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-10-09 22:38:42 -0400
committerFrank <[email protected]>2025-10-09 22:38:42 -0400
commitbc0e00cbb7e68d80e826dd1606fddc9228e1210d (patch)
tree7e8717d49825669d9089785e1325ba341c17b41d /packages/console
parent60dd987efd2435e5122fb98ea1c02041649416e7 (diff)
downloadopencode-bc0e00cbb7e68d80e826dd1606fddc9228e1210d.tar.gz
opencode-bc0e00cbb7e68d80e826dd1606fddc9228e1210d.zip
wip: zen style header
Diffstat (limited to 'packages/console')
-rw-r--r--packages/console/app/src/component/icon.tsx76
-rw-r--r--packages/console/app/src/routes/user-menu.css68
-rw-r--r--packages/console/app/src/routes/user-menu.tsx63
-rw-r--r--packages/console/app/src/routes/workspace-picker.css92
-rw-r--r--packages/console/app/src/routes/workspace-picker.tsx19
-rw-r--r--packages/console/app/src/routes/workspace.css28
-rw-r--r--packages/console/app/src/routes/workspace.tsx36
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>