diff options
| author | Adam <[email protected]> | 2026-01-06 15:21:00 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-20 07:33:44 -0600 |
| commit | 8bcbfd63960120efa3cb770f8e07de1bb57e93b0 (patch) | |
| tree | 75429fd33bf5bc2d3a81e27db72b5671a2eb7629 | |
| parent | e521fee0023a604bb6d5ef39b4b892cbf1a0f9d4 (diff) | |
| download | opencode-8bcbfd63960120efa3cb770f8e07de1bb57e93b0.tar.gz opencode-8bcbfd63960120efa3cb770f8e07de1bb57e93b0.zip | |
wip(app): settings
| -rw-r--r-- | packages/app/src/app.tsx | 43 | ||||
| -rw-r--r-- | packages/app/src/components/dialog-settings.tsx | 87 | ||||
| -rw-r--r-- | packages/app/src/components/settings-agents.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/components/settings-commands.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/components/settings-general.tsx | 134 | ||||
| -rw-r--r-- | packages/app/src/components/settings-keybinds.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/components/settings-mcp.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/components/settings-models.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/components/settings-permissions.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/components/settings-providers.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/context/settings.tsx | 103 | ||||
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 5 | ||||
| -rw-r--r-- | packages/ui/src/components/dialog.css | 16 | ||||
| -rw-r--r-- | packages/ui/src/components/dialog.tsx | 4 | ||||
| -rw-r--r-- | packages/ui/src/components/tabs.css | 122 | ||||
| -rw-r--r-- | packages/ui/src/components/tabs.tsx | 9 |
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, }) |
