summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-06 15:21:00 -0600
committerAdam <[email protected]>2026-01-20 07:33:44 -0600
commit8bcbfd63960120efa3cb770f8e07de1bb57e93b0 (patch)
tree75429fd33bf5bc2d3a81e27db72b5671a2eb7629
parente521fee0023a604bb6d5ef39b4b892cbf1a0f9d4 (diff)
downloadopencode-8bcbfd63960120efa3cb770f8e07de1bb57e93b0.tar.gz
opencode-8bcbfd63960120efa3cb770f8e07de1bb57e93b0.zip
wip(app): settings
-rw-r--r--packages/app/src/app.tsx43
-rw-r--r--packages/app/src/components/dialog-settings.tsx87
-rw-r--r--packages/app/src/components/settings-agents.tsx12
-rw-r--r--packages/app/src/components/settings-commands.tsx12
-rw-r--r--packages/app/src/components/settings-general.tsx134
-rw-r--r--packages/app/src/components/settings-keybinds.tsx12
-rw-r--r--packages/app/src/components/settings-mcp.tsx12
-rw-r--r--packages/app/src/components/settings-models.tsx12
-rw-r--r--packages/app/src/components/settings-permissions.tsx12
-rw-r--r--packages/app/src/components/settings-providers.tsx12
-rw-r--r--packages/app/src/context/settings.tsx103
-rw-r--r--packages/app/src/pages/layout.tsx5
-rw-r--r--packages/ui/src/components/dialog.css16
-rw-r--r--packages/ui/src/components/dialog.tsx4
-rw-r--r--packages/ui/src/components/tabs.css122
-rw-r--r--packages/ui/src/components/tabs.tsx9
16 files changed, 563 insertions, 44 deletions
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index d03d10d0e..33a5556ef 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -14,6 +14,7 @@ import { PermissionProvider } from "@/context/permission"
import { LayoutProvider } from "@/context/layout"
import { GlobalSDKProvider } from "@/context/global-sdk"
import { ServerProvider, useServer } from "@/context/server"
+import { SettingsProvider } from "@/context/settings"
import { TerminalProvider } from "@/context/terminal"
import { PromptProvider } from "@/context/prompt"
import { FileProvider } from "@/context/file"
@@ -82,15 +83,17 @@ export function AppInterface(props: { defaultUrl?: string }) {
<GlobalSyncProvider>
<Router
root={(props) => (
- <PermissionProvider>
- <LayoutProvider>
- <NotificationProvider>
- <CommandProvider>
- <Layout>{props.children}</Layout>
- </CommandProvider>
- </NotificationProvider>
- </LayoutProvider>
- </PermissionProvider>
+ <SettingsProvider>
+ <PermissionProvider>
+ <LayoutProvider>
+ <NotificationProvider>
+ <CommandProvider>
+ <Layout>{props.children}</Layout>
+ </CommandProvider>
+ </NotificationProvider>
+ </LayoutProvider>
+ </PermissionProvider>
+ </SettingsProvider>
)}
>
<Route
@@ -105,16 +108,18 @@ export function AppInterface(props: { defaultUrl?: string }) {
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
- component={() => (
- <TerminalProvider>
- <FileProvider>
- <PromptProvider>
- <Suspense fallback={<Loading />}>
- <Session />
- </Suspense>
- </PromptProvider>
- </FileProvider>
- </TerminalProvider>
+ component={(p) => (
+ <Show when={p.params.id ?? "new"} keyed>
+ <TerminalProvider>
+ <FileProvider>
+ <PromptProvider>
+ <Suspense fallback={<Loading />}>
+ <Session />
+ </Suspense>
+ </PromptProvider>
+ </FileProvider>
+ </TerminalProvider>
+ </Show>
)}
/>
</Route>
diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx
new file mode 100644
index 000000000..872cc4c80
--- /dev/null
+++ b/packages/app/src/components/dialog-settings.tsx
@@ -0,0 +1,87 @@
+import { Component, createSignal } from "solid-js"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { Tabs } from "@opencode-ai/ui/tabs"
+import { Icon } from "@opencode-ai/ui/icon"
+import { TextField } from "@opencode-ai/ui/text-field"
+import { SettingsGeneral } from "./settings-general"
+import { SettingsKeybinds } from "./settings-keybinds"
+import { SettingsPermissions } from "./settings-permissions"
+import { SettingsProviders } from "./settings-providers"
+import { SettingsModels } from "./settings-models"
+import { SettingsAgents } from "./settings-agents"
+import { SettingsCommands } from "./settings-commands"
+import { SettingsMcp } from "./settings-mcp"
+
+export const DialogSettings: Component = () => {
+ const [search, setSearch] = createSignal("")
+
+ return (
+ <Dialog size="large">
+ <Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
+ <Tabs.List>
+ <div class="settings-dialog__search px-3 pb-3">
+ <TextField placeholder="Search" value={search()} onChange={setSearch} variant="normal" />
+ </div>
+ <Tabs.SectionTitle>Desktop</Tabs.SectionTitle>
+ <Tabs.Trigger value="general">
+ <Icon name="settings-gear" />
+ General
+ </Tabs.Trigger>
+ <Tabs.Trigger value="shortcuts">
+ <Icon name="console" />
+ Shortcuts
+ </Tabs.Trigger>
+ <Tabs.SectionTitle>Server</Tabs.SectionTitle>
+ <Tabs.Trigger value="permissions">
+ <Icon name="checklist" />
+ Permissions
+ </Tabs.Trigger>
+ <Tabs.Trigger value="providers">
+ <Icon name="server" />
+ Providers
+ </Tabs.Trigger>
+ <Tabs.Trigger value="models">
+ <Icon name="brain" />
+ Models
+ </Tabs.Trigger>
+ <Tabs.Trigger value="agents">
+ <Icon name="task" />
+ Agents
+ </Tabs.Trigger>
+ <Tabs.Trigger value="commands">
+ <Icon name="console" />
+ Commands
+ </Tabs.Trigger>
+ <Tabs.Trigger value="mcp">
+ <Icon name="mcp" />
+ MCP
+ </Tabs.Trigger>
+ </Tabs.List>
+ <Tabs.Content value="general" class="no-scrollbar">
+ <SettingsGeneral />
+ </Tabs.Content>
+ <Tabs.Content value="shortcuts" class="no-scrollbar">
+ <SettingsKeybinds />
+ </Tabs.Content>
+ <Tabs.Content value="permissions" class="no-scrollbar">
+ <SettingsPermissions />
+ </Tabs.Content>
+ <Tabs.Content value="providers" class="no-scrollbar">
+ <SettingsProviders />
+ </Tabs.Content>
+ <Tabs.Content value="models" class="no-scrollbar">
+ <SettingsModels />
+ </Tabs.Content>
+ <Tabs.Content value="agents" class="no-scrollbar">
+ <SettingsAgents />
+ </Tabs.Content>
+ <Tabs.Content value="commands" class="no-scrollbar">
+ <SettingsCommands />
+ </Tabs.Content>
+ <Tabs.Content value="mcp" class="no-scrollbar">
+ <SettingsMcp />
+ </Tabs.Content>
+ </Tabs>
+ </Dialog>
+ )
+}
diff --git a/packages/app/src/components/settings-agents.tsx b/packages/app/src/components/settings-agents.tsx
new file mode 100644
index 000000000..892be152b
--- /dev/null
+++ b/packages/app/src/components/settings-agents.tsx
@@ -0,0 +1,12 @@
+import { Component } from "solid-js"
+
+export const SettingsAgents: Component = () => {
+ return (
+ <div class="flex flex-col h-full overflow-y-auto">
+ <div class="flex flex-col gap-6 p-6 max-w-[600px]">
+ <h2 class="text-16-medium text-text-strong">Agents</h2>
+ <p class="text-14-regular text-text-weak">Agent settings will be configurable here.</p>
+ </div>
+ </div>
+ )
+}
diff --git a/packages/app/src/components/settings-commands.tsx b/packages/app/src/components/settings-commands.tsx
new file mode 100644
index 000000000..e98c0eeb0
--- /dev/null
+++ b/packages/app/src/components/settings-commands.tsx
@@ -0,0 +1,12 @@
+import { Component } from "solid-js"
+
+export const SettingsCommands: Component = () => {
+ return (
+ <div class="flex flex-col h-full overflow-y-auto">
+ <div class="flex flex-col gap-6 p-6 max-w-[600px]">
+ <h2 class="text-16-medium text-text-strong">Commands</h2>
+ <p class="text-14-regular text-text-weak">Command settings will be configurable here.</p>
+ </div>
+ </div>
+ )
+}
diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx
new file mode 100644
index 000000000..e9965b0fa
--- /dev/null
+++ b/packages/app/src/components/settings-general.tsx
@@ -0,0 +1,134 @@
+import { Component, createMemo, type JSX } from "solid-js"
+import { Select } from "@opencode-ai/ui/select"
+import { Switch } from "@opencode-ai/ui/switch"
+import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
+import { useSettings } from "@/context/settings"
+
+export const SettingsGeneral: Component = () => {
+ const theme = useTheme()
+ const settings = useSettings()
+
+ const themeOptions = createMemo(() =>
+ Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
+ )
+
+ const colorSchemeOptions: { value: ColorScheme; label: string }[] = [
+ { value: "system", label: "System setting" },
+ { value: "light", label: "Light" },
+ { value: "dark", label: "Dark" },
+ ]
+
+ const fontOptions = [
+ { value: "ibm-plex-mono", label: "IBM Plex Mono" },
+ { value: "fira-code", label: "Fira Code" },
+ { value: "jetbrains-mono", label: "JetBrains Mono" },
+ { value: "source-code-pro", label: "Source Code Pro" },
+ ]
+
+ return (
+ <div class="flex flex-col h-full overflow-y-auto no-scrollbar">
+ <div class="flex flex-col gap-8 p-8 max-w-[720px]">
+ {/* Header */}
+ <h2 class="text-16-medium text-text-strong">General</h2>
+
+ {/* Appearance Section */}
+ <div class="flex flex-col gap-1">
+ <h3 class="text-14-medium text-text-strong pb-2">Appearance</h3>
+
+ <SettingsRow title="Appearance" description="Customise how OpenCode looks on your device">
+ <Select
+ options={colorSchemeOptions}
+ current={colorSchemeOptions.find((o) => o.value === theme.colorScheme())}
+ value={(o) => o.value}
+ label={(o) => o.label}
+ onSelect={(option) => option && theme.setColorScheme(option.value)}
+ variant="secondary"
+ size="small"
+ />
+ </SettingsRow>
+
+ <SettingsRow
+ title="Theme"
+ description={
+ <>
+ Customise how OpenCode is themed.{" "}
+ <a href="#" class="text-text-interactive-base">
+ Learn more
+ </a>
+ </>
+ }
+ >
+ <Select
+ options={themeOptions()}
+ current={themeOptions().find((o) => o.id === theme.themeId())}
+ value={(o) => o.id}
+ label={(o) => o.name}
+ onSelect={(option) => option && theme.setTheme(option.id)}
+ variant="secondary"
+ size="small"
+ />
+ </SettingsRow>
+
+ <SettingsRow title="Font" description="Customise the mono font used in code blocks">
+ <Select
+ options={fontOptions}
+ current={fontOptions.find((o) => o.value === settings.appearance.font())}
+ value={(o) => o.value}
+ label={(o) => o.label}
+ onSelect={(option) => option && settings.appearance.setFont(option.value)}
+ variant="secondary"
+ size="small"
+ />
+ </SettingsRow>
+ </div>
+
+ {/* System notifications Section */}
+ <div class="flex flex-col gap-1">
+ <h3 class="text-14-medium text-text-strong pb-2">System notifications</h3>
+
+ <SettingsRow
+ title="Agent"
+ description="Show system notification when the agent is complete or needs attention"
+ >
+ <Switch
+ checked={settings.notifications.agent()}
+ onChange={(checked) => settings.notifications.setAgent(checked)}
+ />
+ </SettingsRow>
+
+ <SettingsRow title="Permissions" description="Show system notification when a permission is required">
+ <Switch
+ checked={settings.notifications.permissions()}
+ onChange={(checked) => settings.notifications.setPermissions(checked)}
+ />
+ </SettingsRow>
+
+ <SettingsRow title="Errors" description="Show system notification when an error occurs">
+ <Switch
+ checked={settings.notifications.errors()}
+ onChange={(checked) => settings.notifications.setErrors(checked)}
+ />
+ </SettingsRow>
+ </div>
+ </div>
+ </div>
+ )
+}
+
+interface SettingsRowProps {
+ title: string
+ description: string | JSX.Element
+ children: JSX.Element
+}
+
+const SettingsRow: Component<SettingsRowProps> = (props) => {
+ return (
+ <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
+ <div class="flex flex-col gap-0.5">
+ <span class="text-14-medium text-text-strong">{props.title}</span>
+ <span class="text-12-regular text-text-weak">{props.description}</span>
+ </div>
+ <div class="flex-shrink-0">{props.children}</div>
+ </div>
+ )
+}
diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx
new file mode 100644
index 000000000..3688559bc
--- /dev/null
+++ b/packages/app/src/components/settings-keybinds.tsx
@@ -0,0 +1,12 @@
+import { Component } from "solid-js"
+
+export const SettingsKeybinds: Component = () => {
+ return (
+ <div class="flex flex-col h-full overflow-y-auto">
+ <div class="flex flex-col gap-6 p-6 max-w-[600px]">
+ <h2 class="text-16-medium text-text-strong">Shortcuts</h2>
+ <p class="text-14-regular text-text-weak">Keyboard shortcuts will be configurable here.</p>
+ </div>
+ </div>
+ )
+}
diff --git a/packages/app/src/components/settings-mcp.tsx b/packages/app/src/components/settings-mcp.tsx
new file mode 100644
index 000000000..ea6bf350f
--- /dev/null
+++ b/packages/app/src/components/settings-mcp.tsx
@@ -0,0 +1,12 @@
+import { Component } from "solid-js"
+
+export const SettingsMcp: Component = () => {
+ return (
+ <div class="flex flex-col h-full overflow-y-auto">
+ <div class="flex flex-col gap-6 p-6 max-w-[600px]">
+ <h2 class="text-16-medium text-text-strong">MCP</h2>
+ <p class="text-14-regular text-text-weak">MCP settings will be configurable here.</p>
+ </div>
+ </div>
+ )
+}
diff --git a/packages/app/src/components/settings-models.tsx b/packages/app/src/components/settings-models.tsx
new file mode 100644
index 000000000..5fbeb144e
--- /dev/null
+++ b/packages/app/src/components/settings-models.tsx
@@ -0,0 +1,12 @@
+import { Component } from "solid-js"
+
+export const SettingsModels: Component = () => {
+ return (
+ <div class="flex flex-col h-full overflow-y-auto">
+ <div class="flex flex-col gap-6 p-6 max-w-[600px]">
+ <h2 class="text-16-medium text-text-strong">Models</h2>
+ <p class="text-14-regular text-text-weak">Model settings will be configurable here.</p>
+ </div>
+ </div>
+ )
+}
diff --git a/packages/app/src/components/settings-permissions.tsx b/packages/app/src/components/settings-permissions.tsx
new file mode 100644
index 000000000..67c3bfb62
--- /dev/null
+++ b/packages/app/src/components/settings-permissions.tsx
@@ -0,0 +1,12 @@
+import { Component } from "solid-js"
+
+export const SettingsPermissions: Component = () => {
+ return (
+ <div class="flex flex-col h-full overflow-y-auto">
+ <div class="flex flex-col gap-6 p-6 max-w-[600px]">
+ <h2 class="text-16-medium text-text-strong">Permissions</h2>
+ <p class="text-14-regular text-text-weak">Permission settings will be configurable here.</p>
+ </div>
+ </div>
+ )
+}
diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx
new file mode 100644
index 000000000..cf90b6c13
--- /dev/null
+++ b/packages/app/src/components/settings-providers.tsx
@@ -0,0 +1,12 @@
+import { Component } from "solid-js"
+
+export const SettingsProviders: Component = () => {
+ return (
+ <div class="flex flex-col h-full overflow-y-auto">
+ <div class="flex flex-col gap-6 p-6 max-w-[600px]">
+ <h2 class="text-16-medium text-text-strong">Providers</h2>
+ <p class="text-14-regular text-text-weak">Provider settings will be configurable here.</p>
+ </div>
+ </div>
+ )
+}
diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx
new file mode 100644
index 000000000..6aca57ae2
--- /dev/null
+++ b/packages/app/src/context/settings.tsx
@@ -0,0 +1,103 @@
+import { createStore } from "solid-js/store"
+import { createMemo } from "solid-js"
+import { createSimpleContext } from "@opencode-ai/ui/context"
+import { persisted } from "@/utils/persist"
+
+export interface NotificationSettings {
+ agent: boolean
+ permissions: boolean
+ errors: boolean
+}
+
+export interface Settings {
+ general: {
+ autoSave: boolean
+ }
+ appearance: {
+ fontSize: number
+ font: string
+ }
+ keybinds: Record<string, string>
+ permissions: {
+ autoApprove: boolean
+ }
+ notifications: NotificationSettings
+}
+
+const defaultSettings: Settings = {
+ general: {
+ autoSave: true,
+ },
+ appearance: {
+ fontSize: 14,
+ font: "ibm-plex-mono",
+ },
+ keybinds: {},
+ permissions: {
+ autoApprove: false,
+ },
+ notifications: {
+ agent: false,
+ permissions: false,
+ errors: false,
+ },
+}
+
+export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({
+ name: "Settings",
+ init: () => {
+ const [store, setStore, _, ready] = persisted("settings.v1", createStore<Settings>(defaultSettings))
+
+ return {
+ ready,
+ get current() {
+ return store
+ },
+ general: {
+ autoSave: createMemo(() => store.general?.autoSave ?? defaultSettings.general.autoSave),
+ setAutoSave(value: boolean) {
+ setStore("general", "autoSave", value)
+ },
+ },
+ appearance: {
+ fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize),
+ setFontSize(value: number) {
+ setStore("appearance", "fontSize", value)
+ },
+ font: createMemo(() => store.appearance?.font ?? defaultSettings.appearance.font),
+ setFont(value: string) {
+ setStore("appearance", "font", value)
+ },
+ },
+ keybinds: {
+ get: (action: string) => store.keybinds?.[action],
+ set(action: string, keybind: string) {
+ setStore("keybinds", action, keybind)
+ },
+ reset(action: string) {
+ setStore("keybinds", action, undefined!)
+ },
+ },
+ permissions: {
+ autoApprove: createMemo(() => store.permissions?.autoApprove ?? defaultSettings.permissions.autoApprove),
+ setAutoApprove(value: boolean) {
+ setStore("permissions", "autoApprove", value)
+ },
+ },
+ notifications: {
+ agent: createMemo(() => store.notifications?.agent ?? defaultSettings.notifications.agent),
+ setAgent(value: boolean) {
+ setStore("notifications", "agent", value)
+ },
+ permissions: createMemo(() => store.notifications?.permissions ?? defaultSettings.notifications.permissions),
+ setPermissions(value: boolean) {
+ setStore("notifications", "permissions", value)
+ },
+ errors: createMemo(() => store.notifications?.errors ?? defaultSettings.notifications.errors),
+ setErrors(value: boolean) {
+ setStore("notifications", "errors", value)
+ },
+ },
+ }
+ },
+})
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 5f5954c90..8c04f10db 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -59,6 +59,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { DialogSelectProvider } from "@/components/dialog-select-provider"
import { DialogSelectServer } from "@/components/dialog-select-server"
+import { DialogSettings } from "@/components/dialog-settings"
import { useCommand, type CommandOption } from "@/context/command"
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
import { navStart } from "@/utils/perf"
@@ -880,6 +881,10 @@ export default function Layout(props: ParentProps) {
dialog.show(() => <DialogSelectServer />)
}
+ function openSettings() {
+ dialog.show(() => <DialogSettings />)
+ }
+
function navigateToProject(directory: string | undefined) {
if (!directory) return
server.projects.touch(directory)
diff --git a/packages/ui/src/components/dialog.css b/packages/ui/src/components/dialog.css
index 177dc0a79..96e967a6f 100644
--- a/packages/ui/src/components/dialog.css
+++ b/packages/ui/src/components/dialog.css
@@ -30,6 +30,7 @@
flex-direction: column;
align-items: center;
justify-items: start;
+ overflow: visible;
[data-slot="dialog-content"] {
display: flex;
@@ -39,6 +40,14 @@
width: 100%;
max-height: 100%;
min-height: 280px;
+ overflow: auto;
+
+ /* Hide scrollbar */
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
/* padding: 8px; */
/* padding: 8px 8px 0 8px; */
@@ -108,7 +117,7 @@
display: flex;
flex-direction: column;
flex: 1;
- overflow-y: auto;
+ overflow: hidden;
&:focus-visible {
outline: none;
@@ -129,6 +138,11 @@
}
}
}
+
+ &[data-size="large"] [data-slot="dialog-container"] {
+ width: min(calc(100vw - 32px), 800px);
+ height: min(calc(100vh - 32px), 600px);
+ }
}
@keyframes overlayShow {
diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx
index 797fbcbfc..1aff55030 100644
--- a/packages/ui/src/components/dialog.tsx
+++ b/packages/ui/src/components/dialog.tsx
@@ -6,6 +6,7 @@ export interface DialogProps extends ParentProps {
title?: JSXElement
description?: JSXElement
action?: JSXElement
+ size?: "normal" | "large"
class?: ComponentProps<"div">["class"]
classList?: ComponentProps<"div">["classList"]
fit?: boolean
@@ -13,10 +14,11 @@ export interface DialogProps extends ParentProps {
export function Dialog(props: DialogProps) {
return (
- <div data-component="dialog" data-fit={props.fit ? true : undefined}>
+ <div data-component="dialog" data-fit={props.fit ? true : undefined} data-size={props.size || "normal"}>
<div data-slot="dialog-container">
<Kobalte.Content
data-slot="dialog-content"
+ data-no-header={!props.title && !props.action ? "" : undefined}
classList={{
...(props.classList ?? {}),
[props.class ?? ""]: !!props.class,
diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css
index 3ec7ece90..a74bcc5d5 100644
--- a/packages/ui/src/components/tabs.css
+++ b/packages/ui/src/components/tabs.css
@@ -215,24 +215,36 @@
height: 100%;
overflow-x: hidden;
overflow-y: auto;
+ padding: 8px;
+ gap: 4px;
+ background-color: var(--background-base);
+ border-right: 1px solid var(--border-weak-base);
&::after {
- width: 100%;
- height: auto;
- flex-grow: 1;
- border-bottom: none;
- border-right: 1px solid var(--border-weak-base);
+ display: none;
}
}
[data-slot="tabs-trigger-wrapper"] {
width: 100%;
- height: auto;
- border-bottom: none;
- border-right: 1px solid var(--border-weak-base);
+ height: 32px;
+ border: none;
+ border-radius: 8px;
+ background-color: transparent;
+
+ [data-slot="tabs-trigger"] {
+ padding: 0 8px;
+ gap: 8px;
+ justify-content: flex-start;
+ }
+
+ &:hover:not(:disabled) {
+ background-color: var(--surface-raised-base-hover);
+ }
&:has([data-selected]) {
- border-right-color: transparent;
+ background-color: var(--surface-raised-base-hover);
+ color: var(--text-strong);
}
}
@@ -243,32 +255,100 @@
&[data-variant="alt"] {
[data-slot="tabs-list"] {
- padding-left: 0;
- padding-right: 0;
- padding-top: 24px;
- padding-bottom: 24px;
- border-bottom: none;
- border-right: 1px solid var(--border-weak-base);
+ padding: 8px;
+ gap: 4px;
+ border: none;
&::after {
+ display: none;
+ }
+ }
+
+ [data-slot="tabs-trigger-wrapper"] {
+ height: 32px;
+ border: none;
+ border-radius: 8px;
+
+ [data-slot="tabs-trigger"] {
border: none;
+ padding: 0 8px;
+ gap: 8px;
+ justify-content: flex-start;
+ }
+
+ &:hover:not(:disabled) {
+ background-color: var(--surface-raised-base-hover);
+ }
+
+ &:has([data-selected]) {
+ background-color: var(--surface-raised-base-hover);
+ color: var(--text-strong);
+ }
+ }
+ }
+
+ &[data-variant="settings"] {
+ [data-slot="tabs-list"] {
+ width: 180px;
+ min-width: 180px;
+ padding: 12px;
+ gap: 0;
+ background-color: var(--background-base);
+ border-right: 1px solid var(--border-weak-base);
+
+ &::after {
+ display: none;
}
}
+ [data-slot="tabs-section-title"] {
+ padding: 8px 8px 4px 8px;
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-small);
+ font-weight: var(--font-weight-medium);
+ color: var(--text-weak);
+ }
+
[data-slot="tabs-trigger-wrapper"] {
- border-bottom: none;
- border-right-width: 2px;
- border-right-style: solid;
- border-right-color: transparent;
+ height: 32px;
+ border: none;
+ border-radius: var(--radius-md);
+
+ /* text-14-regular */
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-base);
+ font-weight: var(--font-weight-regular);
+ line-height: var(--line-height-large);
[data-slot="tabs-trigger"] {
- border-bottom: none;
+ border: none;
+ padding: 0 8px;
+ gap: 8px;
+ justify-content: flex-start;
+ width: 100%;
+ }
+
+ [data-component="icon"] {
+ color: var(--icon-base);
+ }
+
+ &:hover:not(:disabled) {
+ background-color: var(--surface-raised-base-hover);
}
&:has([data-selected]) {
- border-right-color: var(--icon-strong-base);
+ background-color: var(--surface-raised-base-hover);
+ color: var(--text-strong);
+
+ [data-component="icon"] {
+ color: var(--icon-strong-base);
+ }
}
}
+
+ [data-slot="tabs-content"] {
+ background-color: var(--surface-raised-stronger-non-alpha);
+ }
}
}
}
diff --git a/packages/ui/src/components/tabs.tsx b/packages/ui/src/components/tabs.tsx
index 8c892a6e5..825bfa859 100644
--- a/packages/ui/src/components/tabs.tsx
+++ b/packages/ui/src/components/tabs.tsx
@@ -1,9 +1,9 @@
import { Tabs as Kobalte } from "@kobalte/core/tabs"
import { Show, splitProps, type JSX } from "solid-js"
-import type { ComponentProps, ParentProps } from "solid-js"
+import type { ComponentProps, ParentProps, Component } from "solid-js"
export interface TabsProps extends ComponentProps<typeof Kobalte> {
- variant?: "normal" | "alt"
+ variant?: "normal" | "alt" | "settings"
orientation?: "horizontal" | "vertical"
}
export interface TabsListProps extends ComponentProps<typeof Kobalte.List> {}
@@ -106,8 +106,13 @@ function TabsContent(props: ParentProps<TabsContentProps>) {
)
}
+const TabsSectionTitle: Component<ParentProps> = (props) => {
+ return <div data-slot="tabs-section-title">{props.children}</div>
+}
+
export const Tabs = Object.assign(TabsRoot, {
List: TabsList,
Trigger: TabsTrigger,
Content: TabsContent,
+ SectionTitle: TabsSectionTitle,
})