feat(desktop): collapsible sidebar

This commit is contained in:
Adam
2025-11-06 09:48:46 -06:00
parent 146bae82cb
commit 6ba7c54bab
9 changed files with 158 additions and 52 deletions

View File

@@ -543,6 +543,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
disabled={!session.prompt.dirty() && !session.working()}
icon={session.working() ? "stop" : "arrow-up"}
variant="primary"
class="rounded-full"
/>
</Tooltip>
</div>

View File

@@ -5,6 +5,7 @@ import type { FileContent, FileNode, Model, Provider, File as FileStatus } from
import { createSimpleContext } from "./helper"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
import { makePersisted } from "@solid-primitives/storage"
export type LocalFile = FileNode &
Partial<{
@@ -456,11 +457,45 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
})()
const layout = (() => {
const [store, setStore] = makePersisted(
createStore({
sidebar: {
opened: true,
width: 240,
},
}),
{
name: "layout",
},
)
return {
sidebar: {
opened: createMemo(() => store.sidebar.opened),
open() {
setStore("sidebar", "opened", true)
},
close() {
setStore("sidebar", "opened", false)
},
toggle() {
setStore("sidebar", "opened", (x) => !x)
},
width: createMemo(() => store.sidebar.width),
resize(width: number) {
setStore("sidebar", "width", width)
},
},
}
})()
const result = {
model,
agent,
file,
context,
layout,
}
return result
},

View File

@@ -1,27 +1,42 @@
import { Button, Tooltip, DiffChanges } from "@opencode-ai/ui"
import { Button, Tooltip, DiffChanges, IconButton } from "@opencode-ai/ui"
import { createMemo, For, ParentProps, Show } from "solid-js"
import { getFilename } from "@/utils"
import { DateTime } from "luxon"
import { useSync } from "@/context/sync"
import { A, useParams } from "@solidjs/router"
import { useLocal } from "@/context/local"
export default function Layout(props: ParentProps) {
const params = useParams()
const sync = useSync()
const local = useLocal()
return (
<div class="relative h-screen flex flex-col">
<header class="hidden h-12 shrink-0 bg-background-strong border-b border-border-weak-base"></header>
<div class="h-[calc(100vh-0rem)] flex">
<div class="w-70 shrink-0 bg-background-weak border-r border-border-weak-base flex flex-col items-start">
<div class="h-10 shrink-0 flex items-center self-stretch px-5 border-b border-border-weak-base">
<span class="text-14-regular overflow-hidden text-ellipsis">{getFilename(sync.data.path.directory)}</span>
</div>
<div class="flex flex-col items-start gap-4 self-stretch flex-1 py-4 px-3 overflow-hidden">
<Button as={A} href="/session" class="w-full" size="large" icon="edit-small-2">
New Session
</Button>
<div class="w-full h-full overflow-y-auto no-scrollbar flex flex-col flex-1">
<div
classList={{
"@container w-16 pb-4 shrink-0 bg-background-weak": true,
"flex flex-col items-start self-stretch justify-between": true,
"border-r border-border-weak-base": true,
"w-70": local.layout.sidebar.opened(),
}}
>
<div class="flex flex-col justify-center items-start gap-4 self-stretch py-2 overflow-hidden mx-auto @[4rem]:mx-0">
<div class="h-8 shrink-0 flex items-center self-stretch px-3">
<Tooltip placement="right" value="Collapse sidebar">
<IconButton icon="layout-left" variant="ghost" size="large" onClick={local.layout.sidebar.toggle} />
</Tooltip>
</div>
<div class="w-full px-3">
<Button as={A} href="/session" class="hidden @[4rem]:flex w-full" size="large" icon="edit-small-2">
New Session
</Button>
<Tooltip placement="right" value="New session">
<IconButton as={A} href="/session" icon="edit-small-2" size="large" class="@[4rem]:hidden" />
</Tooltip>
</div>
<div class="hidden @[4rem]:flex size-full overflow-y-auto no-scrollbar flex-col flex-1 px-3">
<nav class="w-full">
<For each={sync.data.session}>
{(session) => {
@@ -30,7 +45,7 @@ export default function Layout(props: ParentProps) {
<A
data-active={session.id === params.id}
href={`/session/${session.id}`}
class="group/session focus:outline-none"
class="group/session focus:outline-none cursor-default"
>
<Tooltip placement="right" value={session.title}>
<div
@@ -75,6 +90,29 @@ export default function Layout(props: ParentProps) {
</Show>
</div>
</div>
<div class="flex flex-col items-start shrink-0 px-3 py-1 mx-auto @[4rem]:mx-0">
<Button
as={"a"}
href="https://opencode.ai/desktop-feedback"
target="_blank"
class="hidden @[4rem]:flex w-full gap-2 text-12-medium text-text-base stroke-[1.5px]"
variant="ghost"
icon="speech-bubble"
>
Share feedback
</Button>
<Tooltip placement="right" value="Share feedback">
<IconButton
as={"a"}
href="https://opencode.ai/desktop-feedback"
target="_blank"
icon="speech-bubble"
variant="ghost"
size="large"
class="@[4rem]:hidden stroke-[1.5px]"
/>
</Tooltip>
</div>
</div>
<main class="size-full overflow-x-hidden">{props.children}</main>
</div>

View File

@@ -7,6 +7,7 @@
border-radius: 6px;
text-decoration: none;
user-select: none;
cursor: default;
outline: none;
&[data-variant="primary"] {
@@ -93,11 +94,12 @@
gap: 8px;
/* text-12-medium */
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-size: var(--font-size-small);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large); /* 171.429% */
line-height: var(--line-height-large); /* 166.667% */
letter-spacing: var(--letter-spacing-normal);
}

View File

@@ -2,20 +2,11 @@
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 100%;
border-radius: 6px;
text-decoration: none;
user-select: none;
aspect-ratio: 1;
&:disabled {
background-color: var(--icon-strong-disabled);
color: var(--icon-invert-base);
cursor: not-allowed;
}
&:focus {
outline: none;
}
flex-shrink: 0;
&[data-variant="primary"] {
background-color: var(--icon-strong-base);
@@ -51,45 +42,62 @@
}
&[data-variant="secondary"] {
border: transparent;
background-color: var(--button-secondary-base);
color: var(--text-strong);
box-shadow: var(--shadow-xs-border);
&:hover:not(:disabled) {
background-color: var(--surface-hover);
background-color: var(--button-secondary-hover);
}
&:active:not(:disabled) {
background-color: var(--surface-active);
background-color: var(--button-secondary-base);
}
&:focus:not(:disabled) {
background-color: var(--surface-focus);
background-color: var(--button-secondary-base);
}
&:focus-visible:not(:active) {
background-color: var(--button-secondary-base);
box-shadow: var(--shadow-xs-border-focus);
}
&:focus-visible:active {
box-shadow: none;
}
[data-slot="icon"] {
color: var(--icon-strong-base);
}
}
&[data-variant="ghost"] {
background-color: transparent;
/* color: var(--icon-base); */
[data-slot="icon"] {
color: var(--icon-weak-base);
&:hover:not(:disabled) {
color: var(--icon-weak-hover);
}
&:active:not(:disabled) {
color: var(--icon-string-active);
}
color: var(--icon-base);
}
/* color: var(--text-strong); */
/**/
/* &:hover:not(:disabled) { */
/* background-color: var(--surface-hover); */
/* } */
/* &:active:not(:disabled) { */
/* background-color: var(--surface-active); */
/* } */
/* &:focus:not(:disabled) { */
/* background-color: var(--surface-focus); */
/* } */
&:hover:not(:disabled) {
background-color: var(--surface-base-hover);
[data-slot="icon"] {
color: var(--icon-hover);
}
}
&:active:not(:disabled) {
[data-slot="icon"] {
color: var(--icon-active);
}
}
&:selected:not(:disabled) {
background-color: var(--surface-base-active);
[data-slot="icon"] {
color: var(--icon-selected);
}
}
&:focus:not(:disabled) {
background-color: var(--surface-focus);
}
}
&[data-size="normal"] {
@@ -103,9 +111,14 @@
&[data-size="large"] {
height: 32px;
padding: 0 8px 0 6px;
/* padding: 0 8px 0 6px; */
gap: 8px;
[data-slot="icon"] {
height: 16px;
width: 16px;
}
/* text-12-medium */
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
@@ -114,4 +127,14 @@
line-height: var(--line-height-large); /* 166.667% */
letter-spacing: var(--letter-spacing-normal);
}
&:disabled {
background-color: var(--icon-strong-disabled);
color: var(--icon-invert-base);
cursor: not-allowed;
}
&:focus {
outline: none;
}
}

View File

@@ -2,7 +2,7 @@ import { Button as Kobalte } from "@kobalte/core/button"
import { type ComponentProps, splitProps } from "solid-js"
import { Icon, IconProps } from "./icon"
export interface IconButtonProps {
export interface IconButtonProps extends ComponentProps<typeof Kobalte> {
icon: IconProps["name"]
size?: "normal" | "large"
iconSize?: IconProps["size"]
@@ -22,7 +22,11 @@ export function IconButton(props: ComponentProps<"button"> & IconButtonProps) {
[split.class ?? ""]: !!split.class,
}}
>
<Icon data-slot="icon" name={props.icon} size={split.iconSize ?? (split.size === "large" ? "normal" : "small")} />
<Icon
data-slot="icon"
name={props.icon}
size={split.iconSize ?? (split.size === "large" ? "normal" : "small")}
/>
</Kobalte>
)
}

View File

@@ -152,6 +152,8 @@ const newIcons = {
"circle-ban-sign": `<path d="M15.3675 4.63087L4.55742 15.441M17.9163 9.9987C17.9163 14.371 14.3719 17.9154 9.99967 17.9154C7.81355 17.9154 5.83438 17.0293 4.40175 15.5966C2.96911 14.164 2.08301 12.1848 2.08301 9.9987C2.08301 5.62644 5.62742 2.08203 9.99967 2.08203C12.1858 2.08203 14.165 2.96813 15.5976 4.40077C17.0302 5.8334 17.9163 7.81257 17.9163 9.9987Z" stroke="currentColor" stroke-linecap="round"/>`,
stop: `<rect x="6" y="6" width="8" height="8" fill="currentColor"/>`,
enter: `<path d="M5.83333 15.8334L2.5 12.5L5.83333 9.16671M3.33333 12.5H17.9167V4.58337H10" stroke="currentColor" stroke-linecap="square"/>`,
"layout-left": `<path d="M2.91675 2.91699L2.91675 2.41699L2.41675 2.41699L2.41675 2.91699L2.91675 2.91699ZM17.0834 2.91699L17.5834 2.91699L17.5834 2.41699L17.0834 2.41699L17.0834 2.91699ZM17.0834 17.0837L17.0834 17.5837L17.5834 17.5837L17.5834 17.0837L17.0834 17.0837ZM2.91675 17.0837L2.41675 17.0837L2.41675 17.5837L2.91675 17.5837L2.91675 17.0837ZM7.41674 17.0837L7.41674 17.5837L8.41674 17.5837L8.41674 17.0837L7.91674 17.0837L7.41674 17.0837ZM8.41674 2.91699L8.41674 2.41699L7.41674 2.41699L7.41674 2.91699L7.91674 2.91699L8.41674 2.91699ZM2.91675 2.91699L2.91675 3.41699L17.0834 3.41699L17.0834 2.91699L17.0834 2.41699L2.91675 2.41699L2.91675 2.91699ZM17.0834 2.91699L16.5834 2.91699L16.5834 17.0837L17.0834 17.0837L17.5834 17.0837L17.5834 2.91699L17.0834 2.91699ZM17.0834 17.0837L17.0834 16.5837L2.91675 16.5837L2.91675 17.0837L2.91675 17.5837L17.0834 17.5837L17.0834 17.0837ZM2.91675 17.0837L3.41675 17.0837L3.41675 2.91699L2.91675 2.91699L2.41675 2.91699L2.41675 17.0837L2.91675 17.0837ZM7.91674 17.0837L8.41674 17.0837L8.41674 2.91699L7.91674 2.91699L7.41674 2.91699L7.41674 17.0837L7.91674 17.0837Z" fill="currentColor"/>`,
"speech-bubble": `<path d="M18.3334 10.0003C18.3334 5.57324 15.0927 2.91699 10.0001 2.91699C4.90749 2.91699 1.66675 5.57324 1.66675 10.0003C1.66675 11.1497 2.45578 13.1016 2.5771 13.3949C2.5878 13.4207 2.59839 13.4444 2.60802 13.4706C2.69194 13.6996 3.04282 14.9364 1.66675 16.7684C3.5186 17.6538 5.48526 16.1982 5.48526 16.1982C6.84592 16.9202 8.46491 17.0837 10.0001 17.0837C15.0927 17.0837 18.3334 14.4274 18.3334 10.0003Z" stroke="currentColor" stroke-linecap="square"/>`,
}
export interface IconProps extends ComponentProps<"svg"> {

View File

@@ -7,7 +7,7 @@
overflow: clip;
[data-slot="list"] {
height: 40px;
height: 48px;
width: 100%;
position: relative;
display: flex;
@@ -39,7 +39,7 @@
[data-slot="trigger"] {
position: relative;
height: 100%;
padding: 8px 24px;
padding: 14px 24px;
display: flex;
align-items: center;
color: var(--text-base);

View File

@@ -110,6 +110,7 @@ export default defineConfig({
],
redirects: {
"/discord": "https://discord.gg/opencode",
"/desktop-feedback": "https://discord.gg/h5TNnkFVNy",
},
})