feat(app): v2 desktop UI improvements (#29689)
Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import { List, type ListRef } from "@opencode-ai/ui/list"
|
|||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js"
|
import { createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { Link } from "@/components/link"
|
import { Link } from "@/components/link"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
|||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { useMutation } from "@tanstack/solid-query"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { batch, For } from "solid-js"
|
import { batch, For } from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { Link } from "@/components/link"
|
import { Link } from "@/components/link"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { usePrompt } from "@/context/prompt"
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { extractPromptFromParts } from "@/utils/prompt"
|
import { extractPromptFromParts } from "@/utils/prompt"
|
||||||
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
|
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
|||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { useMutation } from "@tanstack/solid-query"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
|
import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
|
||||||
import { createStore, reconcile } from "solid-js/store"
|
import { createStore, reconcile } from "solid-js/store"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { onMount } from "solid-js"
|
import { onMount } from "solid-js"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt"
|
import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { uuid } from "@/utils/uuid"
|
import { uuid } from "@/utils/uuid"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Message, Session } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/core/util/binary"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Icon } from "@opencode-ai/ui/icon"
|
|||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
|
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
|
||||||
@@ -25,8 +25,8 @@ import { messageAgentColor } from "@/utils/agent"
|
|||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { StatusPopover, StatusPopoverV2 } from "../status-popover"
|
import { StatusPopover, StatusPopoverV2 } from "../status-popover"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/components/icon-button-v2.jsx"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/components/icon.jsx"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
|
|
||||||
const OPEN_APPS = [
|
const OPEN_APPS = [
|
||||||
"vscode",
|
"vscode",
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import type { JSX } from "solid-js"
|
import type { JSX } from "solid-js"
|
||||||
import { WordmarkV2 } from "@opencode-ai/ui/v2/components/wordmark-v2.jsx"
|
import { WordmarkV2 } from "@opencode-ai/ui/v2/wordmark-v2"
|
||||||
|
import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout"
|
||||||
|
|
||||||
export function NewSessionDesignView(props: { children: JSX.Element }) {
|
export function NewSessionDesignView(props: { children: JSX.Element }) {
|
||||||
return (
|
return (
|
||||||
<div data-component="session-new-design" class="relative size-full overflow-hidden bg-v2-background-bg-deep">
|
<div data-component="session-new-design" class="relative size-full overflow-hidden bg-v2-background-bg-deep">
|
||||||
<div class="absolute inset-x-0 top-[25.375%] flex justify-center px-6">
|
<div class="absolute inset-x-0 top-[25.375%] flex justify-center px-6">
|
||||||
<div class="w-full max-w-[720px]">
|
<div class={NEW_SESSION_CONTENT_WIDTH}>
|
||||||
<WordmarkV2 class="h-auto w-full text-v2-icon-icon-base" />
|
<WordmarkV2 class="h-auto w-full text-v2-icon-icon-base" />
|
||||||
<div class="mt-8">{props.children}</div>
|
<div class="mt-8">{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Switch } from "@opencode-ai/ui/switch"
|
|||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { showToast } from "@/utils/toast"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
@@ -86,6 +87,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
const dialog = useDialog()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
@@ -407,7 +409,13 @@ export const SettingsGeneral: Component = () => {
|
|||||||
<div data-action="settings-new-layout-designs">
|
<div data-action="settings-new-layout-designs">
|
||||||
<Switch
|
<Switch
|
||||||
checked={settings.general.newLayoutDesigns()}
|
checked={settings.general.newLayoutDesigns()}
|
||||||
onChange={(checked) => settings.general.setNewLayoutDesigns(checked)}
|
onChange={(checked) => {
|
||||||
|
settings.general.setNewLayoutDesigns(checked)
|
||||||
|
if (!checked) return
|
||||||
|
void import("@/components/settings-v2").then((module) => {
|
||||||
|
dialog.show(() => <module.DialogSettings />)
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js"
|
import { Component, For, Show, createMemo, lazy, onCleanup, onMount } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { SettingsList } from "./settings-list"
|
import { SettingsList } from "./settings-list"
|
||||||
|
|
||||||
|
const ButtonV2 = lazy(() => import("@opencode-ai/ui/v2/button-v2").then((module) => ({ default: module.ButtonV2 })))
|
||||||
|
const IconV2 = lazy(() => import("@opencode-ai/ui/v2/icon").then((module) => ({ default: module.Icon })))
|
||||||
|
const IconButtonV2 = lazy(() =>
|
||||||
|
import("@opencode-ai/ui/v2/icon-button-v2").then((module) => ({ default: module.IconButtonV2 })),
|
||||||
|
)
|
||||||
|
const TextInputV2 = lazy(() =>
|
||||||
|
import("@opencode-ai/ui/v2/text-input-v2").then((module) => ({ default: module.TextInputV2 })),
|
||||||
|
)
|
||||||
|
const SettingsListV2 = lazy(() =>
|
||||||
|
import("./settings-v2/parts/list").then((module) => ({ default: module.SettingsListV2 })),
|
||||||
|
)
|
||||||
|
|
||||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||||
const PALETTE_ID = "command.palette"
|
const PALETTE_ID = "command.palette"
|
||||||
const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
|
const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
|
||||||
@@ -257,7 +269,7 @@ function useKeyCapture(input: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingsKeybinds: Component = () => {
|
export const SettingsKeybinds: Component<{ v2?: boolean }> = (props) => {
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
@@ -371,85 +383,178 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
if (store.active) command.keybinds(true)
|
if (store.active) command.keybinds(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
const emptyResults = (
|
||||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
<Show when={store.filter && !hasResults()}>
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<div
|
||||||
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
classList={{
|
||||||
<div class="flex items-center justify-between gap-4">
|
"flex flex-col items-center justify-center py-12 text-center": !props.v2,
|
||||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.shortcuts.title")}</h2>
|
"settings-v2-shortcuts-status": props.v2,
|
||||||
<Button size="small" variant="secondary" onClick={resetAll} disabled={!hasOverrides()}>
|
}}
|
||||||
{language.t("settings.shortcuts.reset.button")}
|
>
|
||||||
</Button>
|
<span
|
||||||
</div>
|
classList={{
|
||||||
|
"text-14-regular text-text-weak": !props.v2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{language.t("settings.shortcuts.search.empty")}
|
||||||
|
</span>
|
||||||
|
<Show when={store.filter}>
|
||||||
|
<span
|
||||||
|
classList={{
|
||||||
|
"text-14-regular text-text-strong mt-1": !props.v2,
|
||||||
|
"settings-v2-shortcuts-status-filter": props.v2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
"{store.filter}"
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
|
||||||
<div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
|
const List = props.v2 ? SettingsListV2 : SettingsList
|
||||||
<Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
|
|
||||||
<TextField
|
const groups = (
|
||||||
variant="ghost"
|
<div
|
||||||
type="text"
|
classList={{
|
||||||
|
"settings-v2-shortcuts flex flex-col gap-8": props.v2,
|
||||||
|
"flex flex-col gap-8 max-w-[720px]": !props.v2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={GROUPS}>
|
||||||
|
{(group) => (
|
||||||
|
<Show when={(filtered().get(group) ?? []).length > 0}>
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"settings-v2-section": props.v2,
|
||||||
|
"flex flex-col gap-1": !props.v2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
classList={{
|
||||||
|
"settings-v2-section-title": props.v2,
|
||||||
|
"text-14-medium text-text-strong pb-2": !props.v2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{language.t(groupKey[group])}
|
||||||
|
</h3>
|
||||||
|
<List>
|
||||||
|
<For each={filtered().get(group) ?? []}>
|
||||||
|
{(id) => (
|
||||||
|
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
||||||
|
<span
|
||||||
|
classList={{
|
||||||
|
"text-14-regular text-text-strong": !props.v2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title(id)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-keybind-id={id}
|
||||||
|
classList={{
|
||||||
|
"settings-v2-keybind-button": props.v2,
|
||||||
|
"settings-v2-keybind-button--active": props.v2 && store.active === id,
|
||||||
|
"h-8 px-3 rounded-md text-12-regular": !props.v2,
|
||||||
|
"bg-surface-base text-text-subtle hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active":
|
||||||
|
!props.v2 && store.active !== id,
|
||||||
|
"border border-border-weak-base bg-surface-inset-base text-text-weak":
|
||||||
|
!props.v2 && store.active === id,
|
||||||
|
}}
|
||||||
|
onClick={() => start(id)}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={store.active === id}
|
||||||
|
fallback={command.keybind(id) || language.t("settings.shortcuts.unassigned")}
|
||||||
|
>
|
||||||
|
{language.t("settings.shortcuts.pressKeys")}
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
{emptyResults}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show
|
||||||
|
when={props.v2}
|
||||||
|
fallback={
|
||||||
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
|
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||||
|
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.shortcuts.title")}</h2>
|
||||||
|
<Button size="small" variant="secondary" onClick={resetAll} disabled={!hasOverrides()}>
|
||||||
|
{language.t("settings.shortcuts.reset.button")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
|
||||||
|
<Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
|
||||||
|
<TextField
|
||||||
|
variant="ghost"
|
||||||
|
type="text"
|
||||||
|
value={store.filter}
|
||||||
|
onChange={(v) => setStore("filter", v)}
|
||||||
|
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
||||||
|
spellcheck={false}
|
||||||
|
autocorrect="off"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<Show when={store.filter}>
|
||||||
|
<IconButton icon="circle-x" variant="ghost" onClick={() => setStore("filter", "")} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{groups}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<div class="settings-v2-tab-header settings-v2-tab-header--stacked">
|
||||||
|
<div class="settings-v2-tab-header-row">
|
||||||
|
<h2 class="settings-v2-tab-title">{language.t("settings.shortcuts.title")}</h2>
|
||||||
|
<ButtonV2 variant="ghost" onClick={resetAll} disabled={!hasOverrides()}>
|
||||||
|
{language.t("settings.shortcuts.reset.button")}
|
||||||
|
</ButtonV2>
|
||||||
|
</div>
|
||||||
|
<div class="settings-v2-tab-search">
|
||||||
|
<TextInputV2
|
||||||
|
type="search"
|
||||||
|
appearance="base"
|
||||||
value={store.filter}
|
value={store.filter}
|
||||||
onChange={(v) => setStore("filter", v)}
|
onInput={(event) => setStore("filter", event.currentTarget.value)}
|
||||||
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
class="flex-1"
|
aria-label={language.t("settings.shortcuts.search.placeholder")}
|
||||||
/>
|
/>
|
||||||
<Show when={store.filter}>
|
<Show when={store.filter}>
|
||||||
<IconButton icon="circle-x" variant="ghost" onClick={() => setStore("filter", "")} />
|
<IconButtonV2
|
||||||
|
type="button"
|
||||||
|
variant="ghost-muted"
|
||||||
|
size="small"
|
||||||
|
class="settings-v2-tab-search-clear"
|
||||||
|
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
|
||||||
|
onClick={() => setStore("filter", "")}
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="settings-v2-tab-body">{groups}</div>
|
||||||
|
</>
|
||||||
<div class="flex flex-col gap-8 max-w-[720px]">
|
</Show>
|
||||||
<For each={GROUPS}>
|
|
||||||
{(group) => (
|
|
||||||
<Show when={(filtered().get(group) ?? []).length > 0}>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t(groupKey[group])}</h3>
|
|
||||||
<SettingsList>
|
|
||||||
<For each={filtered().get(group) ?? []}>
|
|
||||||
{(id) => (
|
|
||||||
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
|
||||||
<span class="text-14-regular text-text-strong">{title(id)}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-keybind-id={id}
|
|
||||||
classList={{
|
|
||||||
"h-8 px-3 rounded-md text-12-regular": true,
|
|
||||||
"bg-surface-base text-text-subtle hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active":
|
|
||||||
store.active !== id,
|
|
||||||
"border border-border-weak-base bg-surface-inset-base text-text-weak": store.active === id,
|
|
||||||
}}
|
|
||||||
onClick={() => start(id)}
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={store.active === id}
|
|
||||||
fallback={command.keybind(id) || language.t("settings.shortcuts.unassigned")}
|
|
||||||
>
|
|
||||||
{language.t("settings.shortcuts.pressKeys")}
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</SettingsList>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
|
|
||||||
<Show when={store.filter && !hasResults()}>
|
|
||||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
|
||||||
<span class="text-14-regular text-text-weak">{language.t("settings.shortcuts.search.empty")}</span>
|
|
||||||
<Show when={store.filter}>
|
|
||||||
<span class="text-14-regular text-text-strong mt-1">"{store.filter}"</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Button } from "@opencode-ai/ui/button"
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { Tag } from "@opencode-ai/ui/tag"
|
import { Tag } from "@opencode-ai/ui/tag"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||||
import { createMemo, type Component, For, Show } from "solid-js"
|
import { createMemo, type Component, For, Show } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { Component } from "solid-js"
|
||||||
|
import { Dialog } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
|
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { SettingsGeneralV2 } from "./general"
|
||||||
|
import { SettingsKeybinds } from "../settings-keybinds"
|
||||||
|
import { SettingsProvidersV2 } from "./providers"
|
||||||
|
import { SettingsModelsV2 } from "./models"
|
||||||
|
import "./settings-v2.css"
|
||||||
|
|
||||||
|
export const DialogSettings: Component = () => {
|
||||||
|
const language = useLanguage()
|
||||||
|
const platform = usePlatform()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog size="x-large" class="settings-v2-dialog" data-component="settings-v2-dialog">
|
||||||
|
<TabsV2
|
||||||
|
orientation="vertical"
|
||||||
|
variant="settings"
|
||||||
|
defaultValue="general"
|
||||||
|
class="settings-v2"
|
||||||
|
data-component="settings-v2"
|
||||||
|
>
|
||||||
|
<TabsV2.List>
|
||||||
|
<div class="flex flex-col justify-between h-full w-full">
|
||||||
|
<div class="flex flex-col gap-3 w-full">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<TabsV2.SectionTitle>{language.t("settings.section.desktop")}</TabsV2.SectionTitle>
|
||||||
|
<div class="flex flex-col gap-1.5 w-full">
|
||||||
|
<TabsV2.Trigger value="general">
|
||||||
|
<Icon name="sliders" />
|
||||||
|
{language.t("settings.tab.general")}
|
||||||
|
</TabsV2.Trigger>
|
||||||
|
<TabsV2.Trigger value="shortcuts">
|
||||||
|
<Icon name="keyboard" />
|
||||||
|
{language.t("settings.tab.shortcuts")}
|
||||||
|
</TabsV2.Trigger>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<TabsV2.SectionTitle>{language.t("settings.section.server")}</TabsV2.SectionTitle>
|
||||||
|
<div class="flex flex-col gap-1.5 w-full">
|
||||||
|
<TabsV2.Trigger value="providers">
|
||||||
|
<Icon name="providers" />
|
||||||
|
{language.t("settings.providers.title")}
|
||||||
|
</TabsV2.Trigger>
|
||||||
|
<TabsV2.Trigger value="models">
|
||||||
|
<Icon name="models" />
|
||||||
|
{language.t("settings.models.title")}
|
||||||
|
</TabsV2.Trigger>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-v2-nav-footer">
|
||||||
|
<span>{language.t("app.name.desktop")}</span>
|
||||||
|
<span>v{platform.version}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsV2.List>
|
||||||
|
<TabsV2.Content value="general" class="settings-v2-panel">
|
||||||
|
<SettingsGeneralV2 />
|
||||||
|
</TabsV2.Content>
|
||||||
|
<TabsV2.Content value="shortcuts" class="settings-v2-panel">
|
||||||
|
<SettingsKeybinds v2 />
|
||||||
|
</TabsV2.Content>
|
||||||
|
<TabsV2.Content value="providers" class="settings-v2-panel">
|
||||||
|
<SettingsProvidersV2 />
|
||||||
|
</TabsV2.Content>
|
||||||
|
<TabsV2.Content value="models" class="settings-v2-panel">
|
||||||
|
<SettingsModelsV2 />
|
||||||
|
</TabsV2.Content>
|
||||||
|
</TabsV2>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,825 @@
|
|||||||
|
import { Component, Show, createMemo, createResource, onMount } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { SelectV2 } from "@opencode-ai/ui/select-v2"
|
||||||
|
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
|
||||||
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
|
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { showToast } from "@/utils/toast"
|
||||||
|
import { useParams } from "@solidjs/router"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { usePermission } from "@/context/permission"
|
||||||
|
import { usePlatform, type DisplayBackend } from "@/context/platform"
|
||||||
|
import { useServerSync } from "@/context/server-sync"
|
||||||
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
|
import {
|
||||||
|
monoDefault,
|
||||||
|
monoFontFamily,
|
||||||
|
monoInput,
|
||||||
|
sansDefault,
|
||||||
|
sansFontFamily,
|
||||||
|
sansInput,
|
||||||
|
terminalDefault,
|
||||||
|
terminalFontFamily,
|
||||||
|
terminalInput,
|
||||||
|
useSettings,
|
||||||
|
} from "@/context/settings"
|
||||||
|
import { decode64 } from "@/utils/base64"
|
||||||
|
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
|
||||||
|
import { Link } from "../link"
|
||||||
|
import { SettingsListV2 } from "./parts/list"
|
||||||
|
import { SettingsRowV2 } from "./parts/row"
|
||||||
|
import "./settings-v2.css"
|
||||||
|
|
||||||
|
let demoSoundState = {
|
||||||
|
cleanup: undefined as (() => void) | undefined,
|
||||||
|
timeout: undefined as NodeJS.Timeout | undefined,
|
||||||
|
run: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeOption = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShellOption = {
|
||||||
|
path: string
|
||||||
|
name: string
|
||||||
|
acceptable: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShellSelectOption = {
|
||||||
|
id: string
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
|
||||||
|
// delay the playback by 100ms during quick selection changes and pause existing sounds.
|
||||||
|
const stopDemoSound = () => {
|
||||||
|
demoSoundState.run += 1
|
||||||
|
if (demoSoundState.cleanup) {
|
||||||
|
demoSoundState.cleanup()
|
||||||
|
}
|
||||||
|
clearTimeout(demoSoundState.timeout)
|
||||||
|
demoSoundState.cleanup = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const playDemoSound = (id: string | undefined) => {
|
||||||
|
stopDemoSound()
|
||||||
|
if (!id) return
|
||||||
|
|
||||||
|
const run = ++demoSoundState.run
|
||||||
|
demoSoundState.timeout = setTimeout(() => {
|
||||||
|
void playSoundById(id).then((cleanup) => {
|
||||||
|
if (demoSoundState.run !== run) {
|
||||||
|
cleanup?.()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
demoSoundState.cleanup = cleanup
|
||||||
|
})
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingsGeneralV2: Component = () => {
|
||||||
|
const theme = useTheme()
|
||||||
|
const language = useLanguage()
|
||||||
|
const permission = usePermission()
|
||||||
|
const platform = usePlatform()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const params = useParams()
|
||||||
|
const settings = useSettings()
|
||||||
|
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
checking: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
|
||||||
|
const dir = createMemo(() => decode64(params.dir))
|
||||||
|
const accepting = createMemo(() => {
|
||||||
|
const value = dir()
|
||||||
|
if (!value) return false
|
||||||
|
if (!params.id) return permission.isAutoAcceptingDirectory(value)
|
||||||
|
return permission.isAutoAccepting(params.id, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleAccept = (checked: boolean) => {
|
||||||
|
const value = dir()
|
||||||
|
if (!value) return
|
||||||
|
|
||||||
|
if (!params.id) {
|
||||||
|
if (permission.isAutoAcceptingDirectory(value) === checked) return
|
||||||
|
permission.toggleAutoAcceptDirectory(value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
permission.enableAutoAccept(params.id, value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
permission.disableAutoAccept(params.id, value)
|
||||||
|
}
|
||||||
|
const desktop = createMemo(() => platform.platform === "desktop")
|
||||||
|
|
||||||
|
const check = () => {
|
||||||
|
if (!platform.checkUpdate) return
|
||||||
|
setStore("checking", true)
|
||||||
|
|
||||||
|
void platform
|
||||||
|
.checkUpdate()
|
||||||
|
.then((result) => {
|
||||||
|
if (!result.updateAvailable) {
|
||||||
|
showToast({
|
||||||
|
variant: "success",
|
||||||
|
icon: "circle-check",
|
||||||
|
title: language.t("settings.updates.toast.latest.title"),
|
||||||
|
description: language.t("settings.updates.toast.latest.description", { version: platform.version ?? "" }),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = platform.updateAndRestart
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: language.t("toast.update.action.installRestart"),
|
||||||
|
onClick: async () => {
|
||||||
|
await platform.updateAndRestart!()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: language.t("toast.update.action.notYet"),
|
||||||
|
onClick: "dismiss" as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
label: language.t("toast.update.action.notYet"),
|
||||||
|
onClick: "dismiss" as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
showToast({
|
||||||
|
persistent: true,
|
||||||
|
icon: "download",
|
||||||
|
title: language.t("toast.update.title"),
|
||||||
|
description: language.t("toast.update.description", { version: result.version ?? "" }),
|
||||||
|
actions,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||||
|
})
|
||||||
|
.finally(() => setStore("checking", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||||
|
|
||||||
|
const globalSync = useServerSync()
|
||||||
|
const globalSdk = useServerSDK()
|
||||||
|
|
||||||
|
const [shells] = createResource(
|
||||||
|
() =>
|
||||||
|
globalSdk.client.pty
|
||||||
|
.shells()
|
||||||
|
.then((res) => res.data ?? [])
|
||||||
|
.catch(() => [] as ShellOption[]),
|
||||||
|
{ initialValue: [] as ShellOption[] },
|
||||||
|
)
|
||||||
|
|
||||||
|
const [displayBackend, { refetch: refetchDisplayBackend }] = createResource(
|
||||||
|
() => (linux() && platform.getDisplayBackend ? true : false),
|
||||||
|
() => Promise.resolve(platform.getDisplayBackend?.() ?? null).catch(() => null as DisplayBackend | null),
|
||||||
|
{ initialValue: null as DisplayBackend | null },
|
||||||
|
)
|
||||||
|
|
||||||
|
const [pinchZoom, { mutate: setPinchZoom }] = createResource(
|
||||||
|
() => (desktop() && platform.getPinchZoomEnabled ? true : false),
|
||||||
|
() => Promise.resolve(platform.getPinchZoomEnabled?.() ?? false).catch(() => false),
|
||||||
|
{ initialValue: false },
|
||||||
|
)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
void theme.loadThemes()
|
||||||
|
})
|
||||||
|
|
||||||
|
const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") }
|
||||||
|
const currentShell = createMemo(() => globalSync.data.config.shell ?? "")
|
||||||
|
|
||||||
|
const shellOptions = createMemo<ShellSelectOption[]>(() => {
|
||||||
|
const list = shells.latest
|
||||||
|
const current = globalSync.data.config.shell
|
||||||
|
|
||||||
|
const nameCounts = new Map<string, number>()
|
||||||
|
for (const s of list) {
|
||||||
|
nameCounts.set(s.name, (nameCounts.get(s.name) || 0) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
autoOption,
|
||||||
|
...list.map((s) => {
|
||||||
|
const ambiguousName = (nameCounts.get(s.name) || 0) > 1
|
||||||
|
const text = ambiguousName ? s.path : s.name
|
||||||
|
const label = s.acceptable ? text : `${text} (${language.t("settings.general.row.shell.terminalOnly")})`
|
||||||
|
return {
|
||||||
|
id: s.path,
|
||||||
|
// Prefer name over path - "bash" is much cleaner than the explicit full route even when it may change due to PATH.
|
||||||
|
value: ambiguousName ? s.path : s.name,
|
||||||
|
label,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (current && !options.some((o) => o.value === current)) {
|
||||||
|
options.push({ id: current, value: current, label: current })
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
|
||||||
|
const onDisplayBackendChange = (checked: boolean) => {
|
||||||
|
const update = platform.setDisplayBackend?.(checked ? "wayland" : "auto")
|
||||||
|
if (!update) return
|
||||||
|
void update.finally(() => {
|
||||||
|
void refetchDisplayBackend()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPinchZoomChange = (checked: boolean) => {
|
||||||
|
setPinchZoom(checked)
|
||||||
|
const update = platform.setPinchZoomEnabled?.(checked)
|
||||||
|
if (!update) return
|
||||||
|
void update.catch(() => setPinchZoom(!checked))
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
|
||||||
|
{ value: "system", label: language.t("theme.scheme.system") },
|
||||||
|
{ value: "light", label: language.t("theme.scheme.light") },
|
||||||
|
{ value: "dark", label: language.t("theme.scheme.dark") },
|
||||||
|
])
|
||||||
|
|
||||||
|
const languageOptions = createMemo(() =>
|
||||||
|
language.locales.map((locale) => ({
|
||||||
|
value: locale,
|
||||||
|
label: language.label(locale),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
const noneSound = { id: "none", label: "sound.option.none" } as const
|
||||||
|
const soundOptions = [noneSound, ...SOUND_OPTIONS]
|
||||||
|
const mono = () => monoInput(settings.appearance.font())
|
||||||
|
const sans = () => sansInput(settings.appearance.uiFont())
|
||||||
|
const terminal = () => terminalInput(settings.appearance.terminalFont())
|
||||||
|
|
||||||
|
const soundSelectProps = (
|
||||||
|
enabled: () => boolean,
|
||||||
|
current: () => string,
|
||||||
|
setEnabled: (value: boolean) => void,
|
||||||
|
set: (id: string) => void,
|
||||||
|
) => ({
|
||||||
|
options: soundOptions,
|
||||||
|
current: enabled() ? (soundOptions.find((o) => o.id === current()) ?? noneSound) : noneSound,
|
||||||
|
value: (o: (typeof soundOptions)[number]) => o.id,
|
||||||
|
label: (o: (typeof soundOptions)[number]) => language.t(o.label),
|
||||||
|
onHighlight: (option: (typeof soundOptions)[number] | undefined) => {
|
||||||
|
if (!option) return
|
||||||
|
playDemoSound(option.id === "none" ? undefined : option.id)
|
||||||
|
},
|
||||||
|
onSelect: (option: (typeof soundOptions)[number] | undefined) => {
|
||||||
|
if (!option) return
|
||||||
|
if (option.id === "none") {
|
||||||
|
setEnabled(false)
|
||||||
|
stopDemoSound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setEnabled(true)
|
||||||
|
set(option.id)
|
||||||
|
playDemoSound(option.id)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const GeneralSection = () => (
|
||||||
|
<div class="settings-v2-section">
|
||||||
|
<SettingsListV2>
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.language.title")}
|
||||||
|
description={language.t("settings.general.row.language.description")}
|
||||||
|
>
|
||||||
|
<SelectV2
|
||||||
|
data-action="settings-language"
|
||||||
|
options={languageOptions()}
|
||||||
|
current={languageOptions().find((o) => o.value === language.locale())}
|
||||||
|
value={(o) => o.value}
|
||||||
|
label={(o) => o.label}
|
||||||
|
onSelect={(option) => option && language.setLocale(option.value)}
|
||||||
|
/>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("command.permissions.autoaccept.enable")}
|
||||||
|
description={language.t("toast.permissions.autoaccept.on.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-auto-accept-permissions">
|
||||||
|
<Switch checked={accepting()} disabled={!dir()} onChange={toggleAccept} />
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.shell.title")}
|
||||||
|
description={language.t("settings.general.row.shell.description")}
|
||||||
|
>
|
||||||
|
<SelectV2
|
||||||
|
data-action="settings-shell"
|
||||||
|
options={shellOptions()}
|
||||||
|
current={shellOptions().find((o) => o.value === currentShell()) ?? autoOption}
|
||||||
|
value={(o) => o.id}
|
||||||
|
label={(o) => o.label}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (!option) return
|
||||||
|
if (option.value === currentShell()) return
|
||||||
|
globalSync.updateConfig({ shell: option.value })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.reasoningSummaries.title")}
|
||||||
|
description={language.t("settings.general.row.reasoningSummaries.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-feed-reasoning-summaries">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showReasoningSummaries()}
|
||||||
|
onChange={(checked) => settings.general.setShowReasoningSummaries(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.shellToolPartsExpanded.title")}
|
||||||
|
description={language.t("settings.general.row.shellToolPartsExpanded.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-feed-shell-tool-parts-expanded">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.shellToolPartsExpanded()}
|
||||||
|
onChange={(checked) => settings.general.setShellToolPartsExpanded(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.editToolPartsExpanded.title")}
|
||||||
|
description={language.t("settings.general.row.editToolPartsExpanded.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-feed-edit-tool-parts-expanded">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.editToolPartsExpanded()}
|
||||||
|
onChange={(checked) => settings.general.setEditToolPartsExpanded(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.showSessionProgressBar.title")}
|
||||||
|
description={language.t("settings.general.row.showSessionProgressBar.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-session-progress-bar">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showSessionProgressBar()}
|
||||||
|
onChange={(checked) => settings.general.setShowSessionProgressBar(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.newLayoutDesigns.title")}
|
||||||
|
description={language.t("settings.general.row.newLayoutDesigns.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-new-layout-designs">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.newLayoutDesigns()}
|
||||||
|
onChange={(checked) => {
|
||||||
|
settings.general.setNewLayoutDesigns(checked)
|
||||||
|
if (checked) return
|
||||||
|
void import("@/components/dialog-settings").then((module) => {
|
||||||
|
dialog.show(() => <module.DialogSettings />)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
</SettingsListV2>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AdvancedSection = () => (
|
||||||
|
<div class="settings-v2-section">
|
||||||
|
<h3 class="settings-v2-section-title">{language.t("settings.general.section.advanced")}</h3>
|
||||||
|
|
||||||
|
<SettingsListV2>
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.showFileTree.title")}
|
||||||
|
description={language.t("settings.general.row.showFileTree.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-file-tree">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showFileTree()}
|
||||||
|
onChange={(checked) => settings.general.setShowFileTree(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.showNavigation.title")}
|
||||||
|
description={language.t("settings.general.row.showNavigation.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-navigation">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showNavigation()}
|
||||||
|
onChange={(checked) => settings.general.setShowNavigation(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.showSearch.title")}
|
||||||
|
description={language.t("settings.general.row.showSearch.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-search">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showSearch()}
|
||||||
|
onChange={(checked) => settings.general.setShowSearch(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.showTerminal.title")}
|
||||||
|
description={language.t("settings.general.row.showTerminal.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-terminal">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showTerminal()}
|
||||||
|
onChange={(checked) => settings.general.setShowTerminal(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.showStatus.title")}
|
||||||
|
description={language.t("settings.general.row.showStatus.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-status">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showStatus()}
|
||||||
|
onChange={(checked) => settings.general.setShowStatus(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.showCustomAgents.title")}
|
||||||
|
description={language.t("settings.general.row.showCustomAgents.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-custom-agents">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showCustomAgents()}
|
||||||
|
onChange={(checked) => settings.general.setShowCustomAgents(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
</SettingsListV2>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AppearanceSection = () => (
|
||||||
|
<div class="settings-v2-section">
|
||||||
|
<h3 class="settings-v2-section-title">{language.t("settings.general.section.appearance")}</h3>
|
||||||
|
|
||||||
|
<SettingsListV2>
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.colorScheme.title")}
|
||||||
|
description={language.t("settings.general.row.colorScheme.description")}
|
||||||
|
>
|
||||||
|
<SelectV2
|
||||||
|
data-action="settings-color-scheme"
|
||||||
|
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)}
|
||||||
|
onHighlight={(option) => {
|
||||||
|
if (!option) return
|
||||||
|
theme.previewColorScheme(option.value)
|
||||||
|
return () => theme.cancelPreview()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.theme.title")}
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
{language.t("settings.general.row.theme.description")}{" "}
|
||||||
|
<Link class="settings-v2-link" href="https://opencode.ai/docs/themes/">
|
||||||
|
{language.t("common.learnMore")}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectV2
|
||||||
|
data-action="settings-theme"
|
||||||
|
options={themeOptions()}
|
||||||
|
current={themeOptions().find((o) => o.id === theme.themeId())}
|
||||||
|
value={(o) => o.id}
|
||||||
|
label={(o) => o.name}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (!option) return
|
||||||
|
theme.setTheme(option.id)
|
||||||
|
}}
|
||||||
|
onHighlight={(option) => {
|
||||||
|
if (!option) return
|
||||||
|
theme.previewTheme(option.id)
|
||||||
|
return () => theme.cancelPreview()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.uiFont.title")}
|
||||||
|
description={language.t("settings.general.row.uiFont.description")}
|
||||||
|
>
|
||||||
|
<div class="w-full sm:w-[220px]">
|
||||||
|
<TextInputV2
|
||||||
|
data-action="settings-ui-font"
|
||||||
|
type="text"
|
||||||
|
appearance="base"
|
||||||
|
value={sans()}
|
||||||
|
onInput={(event) => settings.appearance.setUIFont(event.currentTarget.value)}
|
||||||
|
placeholder={sansDefault}
|
||||||
|
spellcheck={false}
|
||||||
|
autocorrect="off"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
aria-label={language.t("settings.general.row.uiFont.title")}
|
||||||
|
style={{ "font-family": sansFontFamily(settings.appearance.uiFont()) }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.font.title")}
|
||||||
|
description={language.t("settings.general.row.font.description")}
|
||||||
|
>
|
||||||
|
<div class="w-full sm:w-[220px]">
|
||||||
|
<TextInputV2
|
||||||
|
data-action="settings-code-font"
|
||||||
|
type="text"
|
||||||
|
appearance="base"
|
||||||
|
value={mono()}
|
||||||
|
onInput={(event) => settings.appearance.setFont(event.currentTarget.value)}
|
||||||
|
placeholder={monoDefault}
|
||||||
|
spellcheck={false}
|
||||||
|
autocorrect="off"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
aria-label={language.t("settings.general.row.font.title")}
|
||||||
|
style={{ "font-family": monoFontFamily(settings.appearance.font()) }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.terminalFont.title")}
|
||||||
|
description={language.t("settings.general.row.terminalFont.description")}
|
||||||
|
>
|
||||||
|
<div class="w-full sm:w-[220px]">
|
||||||
|
<TextInputV2
|
||||||
|
data-action="settings-terminal-font"
|
||||||
|
type="text"
|
||||||
|
appearance="base"
|
||||||
|
value={terminal()}
|
||||||
|
onInput={(event) => settings.appearance.setTerminalFont(event.currentTarget.value)}
|
||||||
|
placeholder={terminalDefault}
|
||||||
|
spellcheck={false}
|
||||||
|
autocorrect="off"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
aria-label={language.t("settings.general.row.terminalFont.title")}
|
||||||
|
style={{ "font-family": terminalFontFamily(settings.appearance.terminalFont()) }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
</SettingsListV2>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const NotificationsSection = () => (
|
||||||
|
<div class="settings-v2-section">
|
||||||
|
<h3 class="settings-v2-section-title">{language.t("settings.general.section.notifications")}</h3>
|
||||||
|
|
||||||
|
<SettingsListV2>
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.notifications.agent.title")}
|
||||||
|
description={language.t("settings.general.notifications.agent.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-notifications-agent">
|
||||||
|
<Switch
|
||||||
|
checked={settings.notifications.agent()}
|
||||||
|
onChange={(checked) => settings.notifications.setAgent(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.notifications.permissions.title")}
|
||||||
|
description={language.t("settings.general.notifications.permissions.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-notifications-permissions">
|
||||||
|
<Switch
|
||||||
|
checked={settings.notifications.permissions()}
|
||||||
|
onChange={(checked) => settings.notifications.setPermissions(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.notifications.errors.title")}
|
||||||
|
description={language.t("settings.general.notifications.errors.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-notifications-errors">
|
||||||
|
<Switch
|
||||||
|
checked={settings.notifications.errors()}
|
||||||
|
onChange={(checked) => settings.notifications.setErrors(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
</SettingsListV2>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const SoundsSection = () => (
|
||||||
|
<div class="settings-v2-section">
|
||||||
|
<h3 class="settings-v2-section-title">{language.t("settings.general.section.sounds")}</h3>
|
||||||
|
|
||||||
|
<SettingsListV2>
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.sounds.agent.title")}
|
||||||
|
description={language.t("settings.general.sounds.agent.description")}
|
||||||
|
>
|
||||||
|
<SelectV2
|
||||||
|
data-action="settings-sounds-agent"
|
||||||
|
{...soundSelectProps(
|
||||||
|
() => settings.sounds.agentEnabled(),
|
||||||
|
() => settings.sounds.agent(),
|
||||||
|
(value) => settings.sounds.setAgentEnabled(value),
|
||||||
|
(id) => settings.sounds.setAgent(id),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.sounds.permissions.title")}
|
||||||
|
description={language.t("settings.general.sounds.permissions.description")}
|
||||||
|
>
|
||||||
|
<SelectV2
|
||||||
|
data-action="settings-sounds-permissions"
|
||||||
|
{...soundSelectProps(
|
||||||
|
() => settings.sounds.permissionsEnabled(),
|
||||||
|
() => settings.sounds.permissions(),
|
||||||
|
(value) => settings.sounds.setPermissionsEnabled(value),
|
||||||
|
(id) => settings.sounds.setPermissions(id),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.sounds.errors.title")}
|
||||||
|
description={language.t("settings.general.sounds.errors.description")}
|
||||||
|
>
|
||||||
|
<SelectV2
|
||||||
|
data-action="settings-sounds-errors"
|
||||||
|
{...soundSelectProps(
|
||||||
|
() => settings.sounds.errorsEnabled(),
|
||||||
|
() => settings.sounds.errors(),
|
||||||
|
(value) => settings.sounds.setErrorsEnabled(value),
|
||||||
|
(id) => settings.sounds.setErrors(id),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SettingsRowV2>
|
||||||
|
</SettingsListV2>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const UpdatesSection = () => (
|
||||||
|
<div class="settings-v2-section">
|
||||||
|
<h3 class="settings-v2-section-title">{language.t("settings.general.section.updates")}</h3>
|
||||||
|
|
||||||
|
<SettingsListV2>
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.updates.row.startup.title")}
|
||||||
|
description={language.t("settings.updates.row.startup.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-updates-startup">
|
||||||
|
<Switch
|
||||||
|
checked={settings.updates.startup()}
|
||||||
|
disabled={!platform.checkUpdate}
|
||||||
|
onChange={(checked) => settings.updates.setStartup(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.releaseNotes.title")}
|
||||||
|
description={language.t("settings.general.row.releaseNotes.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-release-notes">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.releaseNotes()}
|
||||||
|
onChange={(checked) => settings.general.setReleaseNotes(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.updates.row.check.title")}
|
||||||
|
description={language.t("settings.updates.row.check.description")}
|
||||||
|
>
|
||||||
|
<ButtonV2 size="normal" variant="neutral" disabled={store.checking || !platform.checkUpdate} onClick={check}>
|
||||||
|
{store.checking
|
||||||
|
? language.t("settings.updates.action.checking")
|
||||||
|
: language.t("settings.updates.action.checkNow")}
|
||||||
|
</ButtonV2>
|
||||||
|
</SettingsRowV2>
|
||||||
|
</SettingsListV2>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const DisplaySection = () => (
|
||||||
|
<Show when={desktop()}>
|
||||||
|
<div class="settings-v2-section">
|
||||||
|
<h3 class="settings-v2-section-title">{language.t("settings.general.section.display")}</h3>
|
||||||
|
|
||||||
|
<SettingsListV2>
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.pinchZoom.title")}
|
||||||
|
description={language.t("settings.general.row.pinchZoom.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-pinch-zoom">
|
||||||
|
<Switch checked={pinchZoom.latest} onChange={onPinchZoomChange} />
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
|
<Show when={linux()}>
|
||||||
|
<SettingsRowV2
|
||||||
|
title={
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>{language.t("settings.general.row.wayland.title")}</span>
|
||||||
|
<Tooltip value={language.t("settings.general.row.wayland.tooltip")} placement="top">
|
||||||
|
<span class="text-text-weak">
|
||||||
|
<Icon name="help" size="small" />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
description={language.t("settings.general.row.wayland.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-wayland">
|
||||||
|
<Switch checked={displayBackend.latest === "wayland"} onChange={onDisplayBackendChange} />
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
</Show>
|
||||||
|
</SettingsListV2>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="settings-v2-tab-header">
|
||||||
|
<h2 class="settings-v2-tab-title">{language.t("settings.tab.general")}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-v2-tab-body">
|
||||||
|
<GeneralSection />
|
||||||
|
|
||||||
|
<AppearanceSection />
|
||||||
|
|
||||||
|
<NotificationsSection />
|
||||||
|
|
||||||
|
<SoundsSection />
|
||||||
|
|
||||||
|
<UpdatesSection />
|
||||||
|
|
||||||
|
<DisplaySection />
|
||||||
|
|
||||||
|
<Show when={desktop()}>
|
||||||
|
<AdvancedSection />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { DialogSettings } from "./dialog-settings-v2"
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
|
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
|
||||||
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
|
import { type Component, For, Show } from "solid-js"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useModels } from "@/context/models"
|
||||||
|
import { popularProviders } from "@/hooks/use-providers"
|
||||||
|
import { SettingsListV2 } from "./parts/list"
|
||||||
|
import { SettingsRowV2 } from "./parts/row"
|
||||||
|
import "./settings-v2.css"
|
||||||
|
|
||||||
|
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
|
||||||
|
|
||||||
|
const PROVIDER_ICON_SIZE = 16
|
||||||
|
|
||||||
|
export const SettingsModelsV2: Component = () => {
|
||||||
|
const language = useLanguage()
|
||||||
|
const models = useModels()
|
||||||
|
|
||||||
|
const list = useFilteredList<ModelItem>({
|
||||||
|
items: (_filter) => models.list(),
|
||||||
|
key: (x) => `${x.provider.id}:${x.id}`,
|
||||||
|
filterKeys: ["provider.name", "name", "id"],
|
||||||
|
sortBy: (a, b) => a.name.localeCompare(b.name),
|
||||||
|
groupBy: (x) => x.provider.id,
|
||||||
|
sortGroupsBy: (a, b) => {
|
||||||
|
const aIndex = popularProviders.indexOf(a.category)
|
||||||
|
const bIndex = popularProviders.indexOf(b.category)
|
||||||
|
const aPopular = aIndex >= 0
|
||||||
|
const bPopular = bIndex >= 0
|
||||||
|
|
||||||
|
if (aPopular && !bPopular) return -1
|
||||||
|
if (!aPopular && bPopular) return 1
|
||||||
|
if (aPopular && bPopular) return aIndex - bIndex
|
||||||
|
|
||||||
|
const aName = a.items[0].provider.name
|
||||||
|
const bName = b.items[0].provider.name
|
||||||
|
return aName.localeCompare(bName)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="settings-v2-tab-header settings-v2-tab-header--stacked">
|
||||||
|
<h2 class="settings-v2-tab-title">{language.t("settings.models.title")}</h2>
|
||||||
|
<div class="settings-v2-tab-search">
|
||||||
|
<TextInputV2
|
||||||
|
type="search"
|
||||||
|
appearance="base"
|
||||||
|
value={list.filter()}
|
||||||
|
onInput={(event) => list.onInput(event.currentTarget.value)}
|
||||||
|
placeholder={language.t("dialog.model.search.placeholder")}
|
||||||
|
spellcheck={false}
|
||||||
|
autocorrect="off"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
aria-label={language.t("dialog.model.search.placeholder")}
|
||||||
|
/>
|
||||||
|
<Show when={list.filter()}>
|
||||||
|
<IconButtonV2
|
||||||
|
type="button"
|
||||||
|
variant="ghost-muted"
|
||||||
|
size="small"
|
||||||
|
class="settings-v2-tab-search-clear"
|
||||||
|
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
|
||||||
|
onClick={() => list.clear()}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-v2-tab-body settings-v2-models">
|
||||||
|
<Show
|
||||||
|
when={!list.grouped.loading}
|
||||||
|
fallback={
|
||||||
|
<div class="settings-v2-models-status">
|
||||||
|
{language.t("common.loading")}
|
||||||
|
{language.t("common.loading.ellipsis")}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={list.flat().length > 0}
|
||||||
|
fallback={
|
||||||
|
<div class="settings-v2-models-status">
|
||||||
|
<span>{language.t("dialog.model.empty")}</span>
|
||||||
|
<Show when={list.filter()}>
|
||||||
|
<span class="settings-v2-models-status-filter">"{list.filter()}"</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={list.grouped.latest}>
|
||||||
|
{(group) => (
|
||||||
|
<div class="settings-v2-section" data-component="settings-models-provider">
|
||||||
|
<div class="settings-v2-models-group-header">
|
||||||
|
<ProviderIcon
|
||||||
|
id={group.category}
|
||||||
|
width={PROVIDER_ICON_SIZE}
|
||||||
|
height={PROVIDER_ICON_SIZE}
|
||||||
|
class="settings-v2-models-provider-icon shrink-0"
|
||||||
|
/>
|
||||||
|
<h3 class="settings-v2-section-title">{group.items[0].provider.name}</h3>
|
||||||
|
</div>
|
||||||
|
<SettingsListV2>
|
||||||
|
<For each={group.items}>
|
||||||
|
{(item) => {
|
||||||
|
const key = { providerID: item.provider.id, modelID: item.id }
|
||||||
|
return (
|
||||||
|
<SettingsRowV2 title={item.name} description="">
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
checked={models.visible(key)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
models.setVisibility(key, checked)
|
||||||
|
}}
|
||||||
|
hideLabel
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</SettingsListV2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Component, JSX } from "solid-js"
|
||||||
|
import "../settings-v2.css"
|
||||||
|
|
||||||
|
export const SettingsListV2: Component<{ children: JSX.Element }> = (props) => {
|
||||||
|
return <div data-component="settings-v2-list">{props.children}</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Component, JSX } from "solid-js"
|
||||||
|
import "../settings-v2.css"
|
||||||
|
|
||||||
|
export interface SettingsRowV2Props {
|
||||||
|
title: string | JSX.Element
|
||||||
|
description: string | JSX.Element
|
||||||
|
children: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingsRowV2: Component<SettingsRowV2Props> = (props) => {
|
||||||
|
return (
|
||||||
|
<div data-component="settings-v2-row">
|
||||||
|
<div data-slot="settings-v2-row-copy">
|
||||||
|
<div data-slot="settings-v2-row-title">{props.title}</div>
|
||||||
|
<div data-slot="settings-v2-row-description">{props.description}</div>
|
||||||
|
</div>
|
||||||
|
<div data-slot="settings-v2-row-control">{props.children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
|
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
|
import { showToast } from "@/utils/toast"
|
||||||
|
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||||
|
import { createMemo, type Component, For, Show } from "solid-js"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
|
import { useServerSync } from "@/context/server-sync"
|
||||||
|
import { DialogConnectProvider } from "../dialog-connect-provider"
|
||||||
|
import { DialogSelectProvider } from "../dialog-select-provider"
|
||||||
|
import { DialogCustomProvider } from "../dialog-custom-provider"
|
||||||
|
import { SettingsListV2 } from "./parts/list"
|
||||||
|
import "./settings-v2.css"
|
||||||
|
|
||||||
|
type ProviderSource = "env" | "api" | "config" | "custom"
|
||||||
|
type ProviderItem = ReturnType<ReturnType<typeof useProviders>["connected"]>[number]
|
||||||
|
|
||||||
|
const PROVIDER_NOTES = [
|
||||||
|
{ match: (id: string) => id === "opencode", key: "dialog.provider.opencode.note" },
|
||||||
|
{ match: (id: string) => id === "opencode-go", key: "dialog.provider.opencodeGo.tagline" },
|
||||||
|
{ match: (id: string) => id === "anthropic", key: "dialog.provider.anthropic.note" },
|
||||||
|
{ match: (id: string) => id.startsWith("github-copilot"), key: "dialog.provider.copilot.note" },
|
||||||
|
{ match: (id: string) => id === "openai", key: "dialog.provider.openai.note" },
|
||||||
|
{ match: (id: string) => id === "google", key: "dialog.provider.google.note" },
|
||||||
|
{ match: (id: string) => id === "openrouter", key: "dialog.provider.openrouter.note" },
|
||||||
|
{ match: (id: string) => id === "vercel", key: "dialog.provider.vercel.note" },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const PROVIDER_ICON_SIZE = 16
|
||||||
|
|
||||||
|
export const SettingsProvidersV2: Component = () => {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const language = useLanguage()
|
||||||
|
const globalSDK = useServerSDK()
|
||||||
|
const globalSync = useServerSync()
|
||||||
|
const providers = useProviders()
|
||||||
|
|
||||||
|
const connected = createMemo(() => {
|
||||||
|
return providers
|
||||||
|
.connected()
|
||||||
|
.filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input))
|
||||||
|
})
|
||||||
|
|
||||||
|
const popular = createMemo(() => {
|
||||||
|
const connectedIDs = new Set(connected().map((p) => p.id))
|
||||||
|
const items = providers
|
||||||
|
.popular()
|
||||||
|
.filter((p) => !connectedIDs.has(p.id))
|
||||||
|
.slice()
|
||||||
|
items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id))
|
||||||
|
return items
|
||||||
|
})
|
||||||
|
|
||||||
|
const source = (item: ProviderItem): ProviderSource | undefined => {
|
||||||
|
if (!("source" in item)) return
|
||||||
|
const value = item.source
|
||||||
|
if (value === "env" || value === "api" || value === "config" || value === "custom") return value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = (item: ProviderItem) => {
|
||||||
|
const current = source(item)
|
||||||
|
if (current === "env") return language.t("settings.providers.tag.environment")
|
||||||
|
if (current === "api") return language.t("provider.connect.method.apiKey")
|
||||||
|
if (current === "config") {
|
||||||
|
if (isConfigCustom(item.id)) return language.t("settings.providers.tag.custom")
|
||||||
|
return language.t("settings.providers.tag.config")
|
||||||
|
}
|
||||||
|
if (current === "custom") return language.t("settings.providers.tag.custom")
|
||||||
|
return language.t("settings.providers.tag.other")
|
||||||
|
}
|
||||||
|
|
||||||
|
const canDisconnect = (item: ProviderItem) => source(item) !== "env"
|
||||||
|
|
||||||
|
const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
|
||||||
|
|
||||||
|
const isConfigCustom = (providerID: string) => {
|
||||||
|
const provider = globalSync.data.config.provider?.[providerID]
|
||||||
|
if (!provider) return false
|
||||||
|
if (provider.npm !== "@ai-sdk/openai-compatible") return false
|
||||||
|
if (!provider.models || Object.keys(provider.models).length === 0) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const disableProvider = async (providerID: string, name: string) => {
|
||||||
|
const before = globalSync.data.config.disabled_providers ?? []
|
||||||
|
const next = before.includes(providerID) ? before : [...before, providerID]
|
||||||
|
globalSync.set("config", "disabled_providers", next)
|
||||||
|
|
||||||
|
await globalSync
|
||||||
|
.updateConfig({ disabled_providers: next })
|
||||||
|
.then(() => {
|
||||||
|
showToast({
|
||||||
|
variant: "success",
|
||||||
|
icon: "circle-check",
|
||||||
|
title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
|
||||||
|
description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
globalSync.set("config", "disabled_providers", before)
|
||||||
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const disconnect = async (providerID: string, name: string) => {
|
||||||
|
if (isConfigCustom(providerID)) {
|
||||||
|
await globalSDK.client.auth.remove({ providerID }).catch(() => undefined)
|
||||||
|
await disableProvider(providerID, name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await globalSDK.client.auth
|
||||||
|
.remove({ providerID })
|
||||||
|
.then(async () => {
|
||||||
|
await globalSDK.client.global.dispose()
|
||||||
|
showToast({
|
||||||
|
variant: "success",
|
||||||
|
icon: "circle-check",
|
||||||
|
title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
|
||||||
|
description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="settings-v2-tab-header">
|
||||||
|
<h2 class="settings-v2-tab-title">{language.t("settings.providers.title")}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-v2-tab-body settings-v2-providers">
|
||||||
|
<div class="settings-v2-section" data-component="connected-providers-section">
|
||||||
|
<h3 class="settings-v2-section-title">{language.t("settings.providers.section.connected")}</h3>
|
||||||
|
<SettingsListV2>
|
||||||
|
<Show
|
||||||
|
when={connected().length > 0}
|
||||||
|
fallback={
|
||||||
|
<div class="settings-v2-provider-empty">{language.t("settings.providers.connected.empty")}</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={connected()}>
|
||||||
|
{(item) => (
|
||||||
|
<div class="settings-v2-provider-row group">
|
||||||
|
<div class="settings-v2-provider-lead">
|
||||||
|
<ProviderIcon
|
||||||
|
id={item.id}
|
||||||
|
width={PROVIDER_ICON_SIZE}
|
||||||
|
height={PROVIDER_ICON_SIZE}
|
||||||
|
class="settings-v2-provider-icon shrink-0"
|
||||||
|
/>
|
||||||
|
<div class="settings-v2-provider-main">
|
||||||
|
<span class="settings-v2-provider-name truncate">{item.name}</span>
|
||||||
|
<Tag>{type(item)}</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show
|
||||||
|
when={canDisconnect(item)}
|
||||||
|
fallback={
|
||||||
|
<span class="settings-v2-provider-env-hint">
|
||||||
|
{language.t("settings.providers.connected.environmentDescription")}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ButtonV2 size="normal" variant="ghost-muted" onClick={() => void disconnect(item.id, item.name)}>
|
||||||
|
{language.t("common.disconnect")}
|
||||||
|
</ButtonV2>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</SettingsListV2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-v2-section">
|
||||||
|
<h3 class="settings-v2-section-title">{language.t("settings.providers.section.popular")}</h3>
|
||||||
|
<SettingsListV2>
|
||||||
|
<For each={popular()}>
|
||||||
|
{(item) => (
|
||||||
|
<div class="settings-v2-provider-row">
|
||||||
|
<div class="settings-v2-provider-lead">
|
||||||
|
<ProviderIcon
|
||||||
|
id={item.id}
|
||||||
|
width={PROVIDER_ICON_SIZE}
|
||||||
|
height={PROVIDER_ICON_SIZE}
|
||||||
|
class="settings-v2-provider-icon shrink-0"
|
||||||
|
/>
|
||||||
|
<div class="settings-v2-provider-copy">
|
||||||
|
<div class="settings-v2-provider-main">
|
||||||
|
<span class="settings-v2-provider-name">{item.name}</span>
|
||||||
|
<Show when={item.id === "opencode" || item.id === "opencode-go"}>
|
||||||
|
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show when={note(item.id)}>
|
||||||
|
{(key) => <p class="settings-v2-provider-description">{language.t(key())}</p>}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ButtonV2
|
||||||
|
size="normal"
|
||||||
|
variant="neutral"
|
||||||
|
icon="plus"
|
||||||
|
onClick={() => {
|
||||||
|
dialog.show(() => <DialogConnectProvider provider={item.id} />)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{language.t("common.connect")}
|
||||||
|
</ButtonV2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<div class="settings-v2-provider-row" data-component="custom-provider-section">
|
||||||
|
<div class="settings-v2-provider-lead">
|
||||||
|
<ProviderIcon
|
||||||
|
id="synthetic"
|
||||||
|
width={PROVIDER_ICON_SIZE}
|
||||||
|
height={PROVIDER_ICON_SIZE}
|
||||||
|
class="settings-v2-provider-icon shrink-0"
|
||||||
|
/>
|
||||||
|
<div class="settings-v2-provider-copy">
|
||||||
|
<div class="settings-v2-provider-main">
|
||||||
|
<span class="settings-v2-provider-name">{language.t("provider.custom.title")}</span>
|
||||||
|
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
||||||
|
</div>
|
||||||
|
<p class="settings-v2-provider-description">{language.t("settings.providers.custom.description")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ButtonV2
|
||||||
|
size="normal"
|
||||||
|
variant="neutral"
|
||||||
|
icon="plus"
|
||||||
|
onClick={() => {
|
||||||
|
dialog.show(() => <DialogCustomProvider back="close" />)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{language.t("common.connect")}
|
||||||
|
</ButtonV2>
|
||||||
|
</div>
|
||||||
|
</SettingsListV2>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-v2-providers-view-all"
|
||||||
|
onClick={() => {
|
||||||
|
dialog.show(() => <DialogSelectProvider />)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{language.t("dialog.provider.viewAll")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,515 @@
|
|||||||
|
@import "@opencode-ai/ui/v2/text-input-v2.css";
|
||||||
|
@import "@opencode-ai/ui/v2/button-v2.css";
|
||||||
|
|
||||||
|
[data-component="settings-v2"] {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="settings-v2-dialog"] [data-slot="dialog-body"] {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-panel::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-tab-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 40px 40px 32px;
|
||||||
|
background: linear-gradient(to bottom, var(--v2-background-bg-base) calc(100% - 24px), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-tab-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 640;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--v2-text-text-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-tab-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 36px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 40px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="settings-v2-row-description"] a.settings-v2-link {
|
||||||
|
color: var(--v2-text-text-accent);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="settings-v2-row-description"] a.settings-v2-link:hover {
|
||||||
|
color: var(--v2-text-text-accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-section-title {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 640;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--v2-text-text-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-section-title + [data-component="settings-v2-list"] {
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="settings-v2-list"] {
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--v2-background-bg-layer-01);
|
||||||
|
padding-inline: 20px;
|
||||||
|
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="dialog-container"]:has(.settings-v2-dialog) {
|
||||||
|
box-shadow: var(--v2-elevation-overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="settings-v2-row"] {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding-block: 20px;
|
||||||
|
border-bottom: 0.5px solid var(--v2-border-border-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="settings-v2-row"]:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
[data-component="settings-v2-row"] {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="settings-v2-row-copy"] {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="settings-v2-row-title"] {
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 530;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.04px;
|
||||||
|
color: var(--v2-text-text-base);
|
||||||
|
font-variation-settings: "slnt" 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="settings-v2-row-description"] {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 440;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--v2-text-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="settings-v2-row-control"] {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="settings-v2-row-control"] > div:has([data-component="switch"]),
|
||||||
|
[data-slot="settings-v2-row-control"] > [data-component="switch"] {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
[data-slot="settings-v2-row-control"] {
|
||||||
|
width: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="settings-v2-row-control"] [data-component="text-input-v2"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="settings-v2-dialog"] [data-component="select"][data-trigger-style="settings-v2"] {
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="settings-v2-dialog"]
|
||||||
|
[data-component="select"][data-trigger-style="settings-v2"]
|
||||||
|
[data-component="button-v2"] {
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-list"] {
|
||||||
|
background-color: var(--v2-background-bg-layer-01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-nav-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-nav-footer > span {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 440;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--v2-text-text-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-legacy-panel {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-legacy-panel [data-component="dialog"] {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-provider-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding-block: 20px;
|
||||||
|
border-bottom: 0.5px solid var(--v2-border-border-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-provider-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.settings-v2-provider-row {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-providers [data-component="provider-icon"] {
|
||||||
|
color: var(--v2-icon-icon-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-provider-lead {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-provider-lead:not(:has(.settings-v2-provider-copy)) {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-provider-copy {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-provider-main {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-provider-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 530;
|
||||||
|
line-height: 16px;
|
||||||
|
color: var(--v2-text-text-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-provider-description {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 440;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--v2-text-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-provider-empty {
|
||||||
|
padding-block: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 440;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--v2-text-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-provider-env-hint {
|
||||||
|
padding-right: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 440;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--v2-text-text-muted);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 200ms ease;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group:hover .settings-v2-provider-env-hint {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-providers-view-all {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 530;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--v2-text-text-accent);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-providers-view-all:hover {
|
||||||
|
color: var(--v2-text-text-accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-tab-body.settings-v2-providers {
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-tab-header:has(+ .settings-v2-tab-body.settings-v2-providers) {
|
||||||
|
padding-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-providers .settings-v2-section-title {
|
||||||
|
padding-bottom: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 530;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-providers .settings-v2-section-title + [data-component="settings-v2-list"] {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-tab-header.settings-v2-tab-header--stacked {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
padding-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-tab-header--stacked > .settings-v2-tab-header-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-tab-search {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-tab-search [data-component="text-input-v2"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-tab-search [data-slot="text-input-v2-input"] {
|
||||||
|
padding-right: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-tab-search-clear {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 6px;
|
||||||
|
z-index: 1;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-tab-body.settings-v2-models {
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-models-group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-models .settings-v2-section-title {
|
||||||
|
padding-bottom: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 530;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-models [data-component="provider-icon"] {
|
||||||
|
color: var(--v2-icon-icon-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-models .settings-v2-section-title + [data-component="settings-v2-list"] {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-models [data-slot="settings-v2-row-description"]:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-models [data-slot="settings-v2-row-copy"] {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-models [data-slot="settings-v2-row-title"] {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 440;
|
||||||
|
line-height: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-models-status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding-block: 48px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 440;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--v2-text-text-muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-models-status-filter {
|
||||||
|
color: var(--v2-text-text-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-shortcuts .settings-v2-section {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-shortcuts .settings-v2-section-title {
|
||||||
|
padding-bottom: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 530;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-shortcuts [data-component="settings-v2-list"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-shortcuts [data-component="settings-v2-list"] > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-shortcuts [data-component="settings-v2-list"] > div:not(:last-child) {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-bottom: 0.5px solid var(--v2-border-border-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-shortcuts [data-component="settings-v2-list"] > div > span {
|
||||||
|
font-weight: 440;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.04px;
|
||||||
|
color: var(--v2-text-text-base);
|
||||||
|
font-variation-settings: "slnt" 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-keybind-button {
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 6px 8px;
|
||||||
|
margin: -6px -8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 530;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0.05px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-feature-settings:
|
||||||
|
"tnum" on,
|
||||||
|
"lnum" on;
|
||||||
|
font-variation-settings: "slnt" 0;
|
||||||
|
color: var(--v2-text-text-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-keybind-button:hover {
|
||||||
|
background-color: var(--v2-background-bg-layer-02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-keybind-button:focus-visible {
|
||||||
|
outline: 2px solid var(--v2-border-border-focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-keybind-button--active {
|
||||||
|
color: var(--v2-text-text-faint);
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: var(--v2-background-bg-layer-02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-shortcuts-status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding-block: 48px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 440;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--v2-text-text-muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-v2-shortcuts-status-filter {
|
||||||
|
color: var(--v2-text-text-base);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { Icon } from "@opencode-ai/ui/icon"
|
|||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { useMutation, useQueryClient } from "@tanstack/solid-query"
|
import { useMutation, useQueryClient } from "@tanstack/solid-query"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/components/icon-button-v2.jsx"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/components/icon.jsx"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { Popover } from "@opencode-ai/ui/popover"
|
import { Popover } from "@opencode-ai/ui/popover"
|
||||||
import { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-js"
|
import { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { withAlpha } from "@opencode-ai/ui/theme/color"
|
|||||||
import { useTheme } from "@opencode-ai/ui/theme/context"
|
import { useTheme } from "@opencode-ai/ui/theme/context"
|
||||||
import { resolveThemeVariant } from "@opencode-ai/ui/theme/resolve"
|
import { resolveThemeVariant } from "@opencode-ai/ui/theme/resolve"
|
||||||
import type { HexColor } from "@opencode-ai/ui/theme/types"
|
import type { HexColor } from "@opencode-ai/ui/theme/types"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web"
|
import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web"
|
||||||
import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js"
|
import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js"
|
||||||
import { SerializeAddon } from "@/addons/serialize"
|
import { SerializeAddon } from "@/addons/serialize"
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import { Icon } from "@opencode-ai/ui/icon"
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { useTheme } from "@opencode-ai/ui/theme/context"
|
import { useTheme } from "@opencode-ai/ui/theme/context"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/components/icon-button-v2.jsx"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/components/icon.jsx"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
|
|
||||||
import { getAvatarColors, useLayout, type LocalProject } from "@/context/layout"
|
import { getProjectAvatarVariant, useLayout, type LocalProject } from "@/context/layout"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -20,8 +20,9 @@ import { useServerSync } from "@/context/server-sync"
|
|||||||
import { decodeDirectory } from "@/pages/directory-layout"
|
import { decodeDirectory } from "@/pages/directory-layout"
|
||||||
import { iife } from "@opencode-ai/core/util/iife"
|
import { iife } from "@opencode-ai/core/util/iife"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { Avatar as AvatarV2 } from "@opencode-ai/ui/v2/components/avatar-v2.jsx"
|
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers"
|
import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers"
|
||||||
|
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { StatusPopoverV2 } from "@/components/status-popover"
|
import { StatusPopoverV2 } from "@/components/status-popover"
|
||||||
import {
|
import {
|
||||||
@@ -370,22 +371,28 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
makeEventListener(
|
const openNewTab = () => navigate(newSessionHref())
|
||||||
document,
|
|
||||||
"keydown",
|
|
||||||
(event) => {
|
|
||||||
if (!event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return
|
|
||||||
if (event.key.toLowerCase() !== "w") return
|
|
||||||
if (!(closeCurrentSessionTab() || closeNewSessionTab())) return
|
|
||||||
|
|
||||||
event.preventDefault()
|
const closeActiveTab = () => closeCurrentSessionTab() || closeNewSessionTab()
|
||||||
event.stopPropagation()
|
|
||||||
},
|
|
||||||
{ capture: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
command.register(() => {
|
command.register(() => {
|
||||||
const commands = [
|
const commands = [
|
||||||
|
{
|
||||||
|
id: "tab.new",
|
||||||
|
category: "tab",
|
||||||
|
title: language.t("command.session.new"),
|
||||||
|
keybind: "mod+t",
|
||||||
|
hidden: true,
|
||||||
|
onSelect: openNewTab,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tab.close",
|
||||||
|
category: "tab",
|
||||||
|
title: language.t("command.tab.close"),
|
||||||
|
keybind: "mod+w",
|
||||||
|
hidden: true,
|
||||||
|
onSelect: closeActiveTab,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: `tab.prev`,
|
id: `tab.prev`,
|
||||||
category: "tab",
|
category: "tab",
|
||||||
@@ -489,6 +496,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
title={tab.info.title}
|
title={tab.info.title}
|
||||||
project={projectForSession(tab.info, projects(), projectByID())}
|
project={projectForSession(tab.info, projects(), projectByID())}
|
||||||
directory={tab.dir}
|
directory={tab.dir}
|
||||||
|
sessionId={tab.info.id}
|
||||||
onClose={() => tabsStoreActions.removeTab(tab.href)}
|
onClose={() => tabsStoreActions.removeTab(tab.href)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -736,26 +744,39 @@ function TabNavItem(props: {
|
|||||||
title: string
|
title: string
|
||||||
project?: LocalProject
|
project?: LocalProject
|
||||||
directory: string
|
directory: string
|
||||||
|
sessionId: string
|
||||||
|
hideClose?: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}) {
|
}) {
|
||||||
const match = useMatch(() => props.href)
|
const match = useMatch(() => props.href)
|
||||||
const isActive = () => !!match()
|
const isActive = () => !!match()
|
||||||
|
const closeTab = (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
props.onClose()
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="group relative flex h-7 min-w-24 max-w-60 flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
class="group relative flex h-7 min-w-24 max-w-60 flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
|
||||||
data-active={isActive()}
|
data-active={isActive()}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
if (event.button !== 1) return
|
||||||
|
closeTab(event)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={props.href}
|
href={props.href}
|
||||||
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden text-[13px] font-medium leading-5 text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base"
|
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base"
|
||||||
>
|
>
|
||||||
<ProjectTabAvatar project={props.project} directory={props.directory} />
|
<span data-slot="project-avatar-slot">
|
||||||
<span class="text-clip leading-5">{props.title}</span>
|
<ProjectTabAvatar project={props.project} directory={props.directory} sessionId={props.sessionId} />
|
||||||
|
</span>
|
||||||
|
<span class="min-w-0 flex-1 truncate">{props.title}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="absolute not-group-hover:not-group-data-[active=true]:left-52 group-hover:right-0 group-data-[active=true]:right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2">
|
<div class="absolute right-0 inset-y-0 flex flex-row items-center overflow-hidden rounded-r-[6px] pr-1 py-1 w-8 pl-2">
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-(image:--inactive-bg) group-hover:bg-(image:--active-bg) group-data-[active=true]:bg-(image:--active-bg)"
|
class="absolute inset-0 rounded-r-[6px] bg-(image:--inactive-bg) group-hover:bg-(image:--active-bg) group-data-[active=true]:bg-(image:--active-bg)"
|
||||||
style={{
|
style={{
|
||||||
"--inactive-bg": "linear-gradient(to right, transparent 0%, var(--tab-bg) 80%)",
|
"--inactive-bg": "linear-gradient(to right, transparent 0%, var(--tab-bg) 80%)",
|
||||||
"--active-bg": "linear-gradient(90deg, transparent 0%, var(--tab-bg) 25%)",
|
"--active-bg": "linear-gradient(90deg, transparent 0%, var(--tab-bg) 25%)",
|
||||||
@@ -765,7 +786,7 @@ function TabNavItem(props: {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
class="opacity-0 group-hover:opacity-100 group-data-[active='true']:opacity-100 z-10"
|
class="opacity-0 group-hover:opacity-100 group-data-[active='true']:opacity-100 z-10"
|
||||||
onClick={props.onClose}
|
onClick={closeTab}
|
||||||
icon={<IconV2 name="xmark-small" />}
|
icon={<IconV2 name="xmark-small" />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -773,22 +794,35 @@ function TabNavItem(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectTabAvatar(props: { project?: LocalProject; directory: string }) {
|
function ProjectTabAvatar(props: { project?: LocalProject; directory: string; sessionId: string }) {
|
||||||
|
const directory = () => props.directory
|
||||||
|
const sessionId = () => props.sessionId
|
||||||
|
const state = useSessionTabAvatarState(directory, sessionId)
|
||||||
return (
|
return (
|
||||||
<AvatarV2
|
<ProjectAvatar
|
||||||
fallback={displayName(props.project ?? { worktree: props.directory })}
|
fallback={displayName(props.project ?? { worktree: props.directory })}
|
||||||
src={getProjectAvatarSource(props.project?.id, props.project?.icon)}
|
src={getProjectAvatarSource(props.project?.id, props.project?.icon)}
|
||||||
kind="org"
|
variant={getProjectAvatarVariant(props.project?.icon?.color)}
|
||||||
size="small"
|
unread={state.unread()}
|
||||||
{...getAvatarColors(props.project?.icon?.color)}
|
loading={state.loading()}
|
||||||
class="size-4 rounded"
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NewSessionTabItem(props: { href: string; title: string; onClose: () => void }) {
|
function NewSessionTabItem(props: { href: string; title: string; onClose: () => void }) {
|
||||||
|
const closeTab = (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
props.onClose()
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div class="group relative flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--v2-overlay-simple-overlay-pressed)] pl-1.5 pr-8 whitespace-nowrap focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[var(--v2-border-border-focus)]">
|
<div
|
||||||
|
class="group relative flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--v2-overlay-simple-overlay-pressed)] pl-1.5 pr-8 whitespace-nowrap focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[var(--v2-border-border-focus)]"
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
if (event.button !== 1) return
|
||||||
|
closeTab(event)
|
||||||
|
}}
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href={props.href}
|
href={props.href}
|
||||||
aria-current="page"
|
aria-current="page"
|
||||||
@@ -807,11 +841,7 @@ function NewSessionTabItem(props: { href: string; title: string; onClose: () =>
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
}}
|
}}
|
||||||
onClick={(event) => {
|
onClick={closeTab}
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
props.onClose()
|
|
||||||
}}
|
|
||||||
icon={<IconV2 name="xmark-small" />}
|
icon={<IconV2 name="xmark-small" />}
|
||||||
aria-label="Close tab"
|
aria-label="Close tab"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { Show, type JSX } from "solid-js"
|
|||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/components/icon-button-v2.jsx"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/components/icon.jsx"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
|
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { DESKTOP_MENU, desktopMenuVisible, type DesktopMenuAction, type DesktopMenuEntry } from "@/desktop-menu"
|
import { DESKTOP_MENU, desktopMenuVisible, type DesktopMenuAction, type DesktopMenuEntry } from "@/desktop-menu"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type {
|
|||||||
Session,
|
Session,
|
||||||
Todo,
|
Todo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { retry } from "@opencode-ai/core/util/retry"
|
import { retry } from "@opencode-ai/core/util/retry"
|
||||||
import { batch } from "solid-js"
|
import { batch } from "solid-js"
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import { decode64 } from "@/utils/base64"
|
|||||||
import { same } from "@/utils/same"
|
import { same } from "@/utils/same"
|
||||||
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
|
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
|
||||||
import { createPathHelpers } from "./file/path"
|
import { createPathHelpers } from "./file/path"
|
||||||
|
import type { ProjectAvatarVariant } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
|
|
||||||
|
export type { ProjectAvatarVariant }
|
||||||
|
|
||||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||||
const DEFAULT_SIDEBAR_WIDTH = 344
|
const DEFAULT_SIDEBAR_WIDTH = 344
|
||||||
@@ -33,6 +36,16 @@ export function getAvatarColors(key?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getProjectAvatarVariant(key?: string): ProjectAvatarVariant {
|
||||||
|
if (key === "orange") return "orange"
|
||||||
|
if (key === "pink") return "pink"
|
||||||
|
if (key === "cyan") return "cyan"
|
||||||
|
if (key === "purple") return "purple"
|
||||||
|
if (key === "mint") return "cyan"
|
||||||
|
if (key === "lime") return "green"
|
||||||
|
return "gray"
|
||||||
|
}
|
||||||
|
|
||||||
type SessionTabs = {
|
type SessionTabs = {
|
||||||
active?: string
|
active?: string
|
||||||
all: string[]
|
all: string[]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client"
|
import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
|
|||||||
@@ -531,6 +531,8 @@ export const dict = {
|
|||||||
"home.projects": "Projects",
|
"home.projects": "Projects",
|
||||||
"home.project.add": "Add project",
|
"home.project.add": "Add project",
|
||||||
"home.sessions.search.placeholder": "Search sessions",
|
"home.sessions.search.placeholder": "Search sessions",
|
||||||
|
"home.sessions.search.sessions": "Sessions",
|
||||||
|
"home.sessions.search.noResults": "No sessions found for {{query}}",
|
||||||
"home.sessions.empty": "No sessions found",
|
"home.sessions.empty": "No sessions found",
|
||||||
"home.sessions.empty.description": "Start a new session for this project",
|
"home.sessions.empty.description": "Start a new session for this project",
|
||||||
"home.sessions.group.today": "Today",
|
"home.sessions.group.today": "Today",
|
||||||
|
|||||||
@@ -502,6 +502,8 @@ export const dict = {
|
|||||||
"home.projects": "项目",
|
"home.projects": "项目",
|
||||||
"home.project.add": "添加项目",
|
"home.project.add": "添加项目",
|
||||||
"home.sessions.search.placeholder": "搜索会话",
|
"home.sessions.search.placeholder": "搜索会话",
|
||||||
|
"home.sessions.search.sessions": "会话",
|
||||||
|
"home.sessions.search.noResults": "未找到与 {{query}} 相关的会话",
|
||||||
"home.sessions.empty": "未找到会话",
|
"home.sessions.empty": "未找到会话",
|
||||||
"home.sessions.group.today": "今天",
|
"home.sessions.group.today": "今天",
|
||||||
"home.sessions.group.yesterday": "昨天",
|
"home.sessions.group.yesterday": "昨天",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DataProvider } from "@opencode-ai/ui/context"
|
import { DataProvider } from "@opencode-ai/ui/context"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js"
|
import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js"
|
||||||
|
|||||||
+530
-285
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,8 @@ import { createStore, produce, reconcile } from "solid-js/store"
|
|||||||
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
||||||
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
|
import { toaster } from "@opencode-ai/ui/toast"
|
||||||
|
import { setV2Toast, showToast, ToastRegion } from "@/utils/toast"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { clearWorkspaceTerminals, getTerminalServerScope } from "@/context/terminal"
|
import { clearWorkspaceTerminals, getTerminalServerScope } from "@/context/terminal"
|
||||||
import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
|
import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
|
||||||
@@ -128,6 +129,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const newDesign = createMemo(() => settings.general.newLayoutDesigns())
|
const newDesign = createMemo(() => settings.general.newLayoutDesigns())
|
||||||
|
createEffect(() => setV2Toast(newDesign()))
|
||||||
const initialDirectory = decode64(params.dir)
|
const initialDirectory = decode64(params.dir)
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const route = createMemo(() => {
|
const route = createMemo(() => {
|
||||||
@@ -1227,7 +1229,10 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
function openSettings() {
|
function openSettings() {
|
||||||
const run = ++dialogRun
|
const run = ++dialogRun
|
||||||
void import("@/components/dialog-settings").then((x) => {
|
const module = settings.general.newLayoutDesigns()
|
||||||
|
? import("@/components/settings-v2")
|
||||||
|
: import("@/components/dialog-settings")
|
||||||
|
void module.then((x) => {
|
||||||
if (dialogDead || dialogRun !== run) return
|
if (dialogDead || dialogRun !== run) return
|
||||||
dialog.show(() => <x.DialogSettings />)
|
dialog.show(() => <x.DialogSettings />)
|
||||||
})
|
})
|
||||||
@@ -2380,7 +2385,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
</main>
|
</main>
|
||||||
{import.meta.env.DEV && <DebugBar />}
|
{import.meta.env.DEV && <DebugBar />}
|
||||||
<Toast.Region />
|
<ToastRegion v2={newDesign()} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -2533,7 +2538,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
</div>
|
</div>
|
||||||
{import.meta.env.DEV && <DebugBar />}
|
{import.meta.env.DEV && <DebugBar />}
|
||||||
</div>
|
</div>
|
||||||
<Toast.Region />
|
<ToastRegion v2={newDesign()} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { createMemo, type Accessor } from "solid-js"
|
||||||
|
import { useServerSync } from "@/context/server-sync"
|
||||||
|
import { useNotification } from "@/context/notification"
|
||||||
|
import { usePermission } from "@/context/permission"
|
||||||
|
import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree"
|
||||||
|
|
||||||
|
export function useSessionTabAvatarState(directory: Accessor<string>, sessionId: Accessor<string>) {
|
||||||
|
const globalSync = useServerSync()
|
||||||
|
const notification = useNotification()
|
||||||
|
const permission = usePermission()
|
||||||
|
const hasPermissions = createMemo(() => {
|
||||||
|
const [store] = globalSync.child(directory(), { bootstrap: false })
|
||||||
|
return !!sessionPermissionRequest(store.session, store.permission, sessionId(), (item) => {
|
||||||
|
return !permission.autoResponds(item, directory())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const unread = createMemo(() => hasPermissions() || notification.session.unseenCount(sessionId()) > 0)
|
||||||
|
const loading = createMemo(() => {
|
||||||
|
if (hasPermissions()) return false
|
||||||
|
const [store] = globalSync.child(directory(), { bootstrap: false })
|
||||||
|
return store.session_working(sessionId())
|
||||||
|
})
|
||||||
|
return { unread, loading }
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ import { Tabs } from "@opencode-ai/ui/tabs"
|
|||||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||||
import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
|
import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { checksum } from "@opencode-ai/core/util/encode"
|
import { checksum } from "@opencode-ai/core/util/encode"
|
||||||
import { useLocation, useSearchParams } from "@solidjs/router"
|
import { useLocation, useSearchParams } from "@solidjs/router"
|
||||||
import { NewSessionDesignView, NewSessionView, SessionHeader } from "@/components/session"
|
import { NewSessionDesignView, NewSessionView, SessionHeader } from "@/components/session"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type { SessionComposerState } from "@/pages/session/composer/session-comp
|
|||||||
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
|
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
|
||||||
import type { FollowupDraft } from "@/components/prompt-input/submit"
|
import type { FollowupDraft } from "@/components/prompt-input/submit"
|
||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
|
import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout"
|
||||||
|
|
||||||
export function SessionComposerRegion(props: {
|
export function SessionComposerRegion(props: {
|
||||||
state: SessionComposerState
|
state: SessionComposerState
|
||||||
@@ -150,8 +151,9 @@ export function SessionComposerRegion(props: {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"w-full px-3 pointer-events-auto": true,
|
"w-full pointer-events-auto": true,
|
||||||
"max-w-[720px] px-0": props.placement === "inline",
|
"px-3": props.placement !== "inline",
|
||||||
|
[NEW_SESSION_CONTENT_WIDTH]: props.placement === "inline",
|
||||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createEffect, createMemo, on, onCleanup } from "solid-js"
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
|
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useMutation } from "@tanstack/solid-query"
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
|||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
||||||
import { useComments } from "@/context/comments"
|
import { useComments } from "@/context/comments"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import type {
|
|||||||
ToolPart,
|
ToolPart,
|
||||||
UserMessage,
|
UserMessage,
|
||||||
} from "@opencode-ai/sdk/v2"
|
} from "@opencode-ai/sdk/v2"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/core/util/binary"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/** Inline new-session content width — keep in sync with session composer `placement === "inline"`. */
|
||||||
|
export const NEW_SESSION_CONTENT_WIDTH = "w-full max-w-[720px] px-0"
|
||||||
|
|
||||||
export function shouldUseV2NewSessionPage(input: { newLayoutDesigns: boolean; sessionID?: string }) {
|
export function shouldUseV2NewSessionPage(input: { newLayoutDesigns: boolean; sessionID?: string }) {
|
||||||
return input.newLayoutDesigns && !input.sessionID
|
return input.newLayoutDesigns && !input.sessionID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useSDK } from "@/context/sdk"
|
|||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useTerminal } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { findLast } from "@opencode-ai/core/util/array"
|
import { findLast } from "@opencode-ai/core/util/array"
|
||||||
import { createSessionTabs } from "@/pages/session/helpers"
|
import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
import { extractPromptFromParts } from "@/utils/prompt"
|
import { extractPromptFromParts } from "@/utils/prompt"
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Icon, type IconProps } from "@opencode-ai/ui/icon"
|
||||||
|
import { Toast, showToast as showLegacyToast, type ToastOptions, type ToastVariant } from "@opencode-ai/ui/toast"
|
||||||
|
import { ToastV2, showToastV2 } from "@opencode-ai/ui/v2/toast-v2"
|
||||||
|
|
||||||
|
let v2 = false
|
||||||
|
|
||||||
|
export function setV2Toast(value: boolean) {
|
||||||
|
v2 = value
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ToastRegion(props: { v2: boolean }) {
|
||||||
|
if (props.v2) return <ToastV2.Region />
|
||||||
|
return <Toast.Region />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showToast(options: ToastOptions | string) {
|
||||||
|
if (!v2) return showLegacyToast(options)
|
||||||
|
if (typeof options === "string") return showToastV2(options)
|
||||||
|
|
||||||
|
return showToastV2({
|
||||||
|
...options,
|
||||||
|
icon: resolveIcon(options.icon, options.variant),
|
||||||
|
actions: options.actions?.map((action) => ({
|
||||||
|
...action,
|
||||||
|
variant: action.onClick === "dismiss" ? "secondary" : "primary",
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveIcon(icon: IconProps["name"] | undefined, variant: ToastVariant | undefined) {
|
||||||
|
const name = icon ?? (variant === "success" ? "check" : undefined)
|
||||||
|
if (!name) return
|
||||||
|
return <Icon name={name} />
|
||||||
|
}
|
||||||
@@ -23,7 +23,8 @@
|
|||||||
"./icons/app": "./src/components/app-icons/types.ts",
|
"./icons/app": "./src/components/app-icons/types.ts",
|
||||||
"./fonts/*": "./src/assets/fonts/*",
|
"./fonts/*": "./src/assets/fonts/*",
|
||||||
"./audio/*": "./src/assets/audio/*",
|
"./audio/*": "./src/assets/audio/*",
|
||||||
"./v2/*": "./src/v2/*"
|
"./v2/*": "./src/v2/components/*.tsx",
|
||||||
|
"./v2/styles/*": "./src/v2/styles/*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
@@ -47,8 +48,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@shikijs/transformers": "3.9.2",
|
"@shikijs/transformers": "3.9.2",
|
||||||
"@solid-primitives/bounds": "0.1.3",
|
"@solid-primitives/bounds": "0.1.3",
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
width: fit-content;
|
||||||
|
height: 24px;
|
||||||
|
padding: 4px 4px 4px 8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--v2-text-text-base);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger-value"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: fit-content;
|
||||||
|
height: 13px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 530;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.04px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-variation-settings: "slnt" 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger"]:focus-visible {
|
||||||
|
outline: 2px solid var(--v2-border-border-focus);
|
||||||
|
outline-offset: 2.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="select"][data-trigger-style="settings-v2"]
|
||||||
|
[data-slot="select-select-trigger"]:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||||
|
background-color: var(--v2-overlay-simple-overlay-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="select"][data-trigger-style="settings-v2"]
|
||||||
|
[data-slot="select-select-trigger"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
||||||
|
background-color: var(--v2-overlay-simple-overlay-pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger"]:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger-icon"] {
|
||||||
|
display: flex;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="select"][data-trigger-style="settings-v2"]
|
||||||
|
[data-slot="select-select-trigger-icon"]
|
||||||
|
[data-slot="icon-svg"] {
|
||||||
|
margin-inline: -5px;
|
||||||
|
color: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="select-content"][data-trigger-style="settings-v2"] {
|
||||||
|
min-width: 160px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
[data-slot="select-select-content-list"] {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="select-select-item"] {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
line-height: var(--line-height-large);
|
||||||
|
letter-spacing: var(--letter-spacing-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import * as mod from "./select-v2"
|
||||||
|
import { create } from "../storybook/scaffold"
|
||||||
|
|
||||||
|
const story = create({
|
||||||
|
title: "UI/SelectV2",
|
||||||
|
mod,
|
||||||
|
args: {
|
||||||
|
options: ["One", "Two", "Three"],
|
||||||
|
current: "One",
|
||||||
|
placeholder: "Choose...",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "UI/SelectV2",
|
||||||
|
id: "components-select-v2",
|
||||||
|
component: story.meta.component,
|
||||||
|
tags: ["autodocs"],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Basic = story.Basic
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { Select as Kobalte } from "@kobalte/core/select"
|
||||||
|
import { createMemo, onCleanup, splitProps, type ComponentProps, type JSX } from "solid-js"
|
||||||
|
import { pipe, groupBy, entries, map } from "remeda"
|
||||||
|
import { Icon as IconV2 } from "../v2/components/icon"
|
||||||
|
import { Icon } from "./icon"
|
||||||
|
import "./select-v2.css"
|
||||||
|
|
||||||
|
export type SelectV2Props<T> = Omit<ComponentProps<typeof Kobalte<T>>, "value" | "onSelect" | "children"> & {
|
||||||
|
placeholder?: string
|
||||||
|
options: T[]
|
||||||
|
current?: T
|
||||||
|
value?: (x: T) => string
|
||||||
|
label?: (x: T) => string
|
||||||
|
groupBy?: (x: T) => string
|
||||||
|
valueClass?: ComponentProps<"div">["class"]
|
||||||
|
onSelect?: (value: T | undefined) => void
|
||||||
|
onHighlight?: (value: T | undefined) => (() => void) | void
|
||||||
|
class?: ComponentProps<"div">["class"]
|
||||||
|
classList?: ComponentProps<"div">["classList"]
|
||||||
|
children?: (item: T | undefined) => JSX.Element
|
||||||
|
triggerStyle?: JSX.CSSProperties
|
||||||
|
triggerProps?: Record<string, string | number | boolean | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectV2<T>(props: SelectV2Props<T> & { disabled?: boolean }) {
|
||||||
|
const [local, others] = splitProps(props, [
|
||||||
|
"class",
|
||||||
|
"classList",
|
||||||
|
"placeholder",
|
||||||
|
"options",
|
||||||
|
"current",
|
||||||
|
"value",
|
||||||
|
"label",
|
||||||
|
"groupBy",
|
||||||
|
"valueClass",
|
||||||
|
"onSelect",
|
||||||
|
"onHighlight",
|
||||||
|
"onOpenChange",
|
||||||
|
"children",
|
||||||
|
"triggerStyle",
|
||||||
|
"triggerProps",
|
||||||
|
])
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
key: undefined as string | undefined,
|
||||||
|
cleanup: undefined as (() => void) | void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
state.cleanup?.()
|
||||||
|
state.cleanup = undefined
|
||||||
|
state.key = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyFor = (item: T) => (local.value ? local.value(item) : (item as string))
|
||||||
|
|
||||||
|
const move = (item: T | undefined) => {
|
||||||
|
if (!local.onHighlight) return
|
||||||
|
if (!item) {
|
||||||
|
stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = keyFor(item)
|
||||||
|
if (state.key === key) return
|
||||||
|
state.cleanup?.()
|
||||||
|
state.cleanup = local.onHighlight(item)
|
||||||
|
state.key = key
|
||||||
|
}
|
||||||
|
|
||||||
|
onCleanup(stop)
|
||||||
|
|
||||||
|
const grouped = createMemo(() => {
|
||||||
|
const result = pipe(
|
||||||
|
local.options,
|
||||||
|
groupBy((x) => (local.groupBy ? local.groupBy(x) : "")),
|
||||||
|
entries(),
|
||||||
|
map(([k, v]) => ({ category: k, options: v })),
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<Kobalte<T, { category: string; options: T[] }>
|
||||||
|
{...others}
|
||||||
|
data-component="select"
|
||||||
|
data-trigger-style="settings-v2"
|
||||||
|
placement="bottom-end"
|
||||||
|
gutter={4}
|
||||||
|
value={local.current}
|
||||||
|
options={grouped()}
|
||||||
|
optionValue={(x) => (local.value ? local.value(x) : (x as string))}
|
||||||
|
optionTextValue={(x) => (local.label ? local.label(x) : (x as string))}
|
||||||
|
optionGroupChildren="options"
|
||||||
|
placeholder={local.placeholder}
|
||||||
|
sectionComponent={(local) => (
|
||||||
|
<Kobalte.Section data-slot="select-section">{local.section.rawValue.category}</Kobalte.Section>
|
||||||
|
)}
|
||||||
|
itemComponent={(itemProps) => (
|
||||||
|
<Kobalte.Item
|
||||||
|
{...itemProps}
|
||||||
|
data-slot="select-select-item"
|
||||||
|
classList={{
|
||||||
|
...local.classList,
|
||||||
|
[local.class ?? ""]: !!local.class,
|
||||||
|
}}
|
||||||
|
onPointerEnter={() => move(itemProps.item.rawValue)}
|
||||||
|
onPointerMove={() => move(itemProps.item.rawValue)}
|
||||||
|
onFocus={() => move(itemProps.item.rawValue)}
|
||||||
|
>
|
||||||
|
<Kobalte.ItemLabel data-slot="select-select-item-label">
|
||||||
|
{local.children
|
||||||
|
? local.children(itemProps.item.rawValue)
|
||||||
|
: local.label
|
||||||
|
? local.label(itemProps.item.rawValue)
|
||||||
|
: (itemProps.item.rawValue as string)}
|
||||||
|
</Kobalte.ItemLabel>
|
||||||
|
<Kobalte.ItemIndicator data-slot="select-select-item-indicator">
|
||||||
|
<Icon name="check-small" size="small" />
|
||||||
|
</Kobalte.ItemIndicator>
|
||||||
|
</Kobalte.Item>
|
||||||
|
)}
|
||||||
|
onChange={(v) => {
|
||||||
|
local.onSelect?.(v ?? undefined)
|
||||||
|
stop()
|
||||||
|
}}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
local.onOpenChange?.(open)
|
||||||
|
if (!open) stop()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Kobalte.Trigger
|
||||||
|
{...local.triggerProps}
|
||||||
|
type="button"
|
||||||
|
disabled={props.disabled}
|
||||||
|
data-slot="select-select-trigger"
|
||||||
|
as="button"
|
||||||
|
style={local.triggerStyle}
|
||||||
|
classList={{
|
||||||
|
...local.classList,
|
||||||
|
[local.class ?? ""]: !!local.class,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Kobalte.Value<T> data-slot="select-select-trigger-value" class={local.valueClass}>
|
||||||
|
{(state) => {
|
||||||
|
const selected = state.selectedOption() ?? local.current
|
||||||
|
if (!selected) return local.placeholder || ""
|
||||||
|
if (local.label) return local.label(selected)
|
||||||
|
return selected as string
|
||||||
|
}}
|
||||||
|
</Kobalte.Value>
|
||||||
|
<Kobalte.Icon data-slot="select-select-trigger-icon">
|
||||||
|
<IconV2 name="chevron-down" class="-mx-[5px]" />
|
||||||
|
</Kobalte.Icon>
|
||||||
|
</Kobalte.Trigger>
|
||||||
|
<Kobalte.Portal>
|
||||||
|
<Kobalte.Content
|
||||||
|
classList={{
|
||||||
|
...local.classList,
|
||||||
|
[local.class ?? ""]: !!local.class,
|
||||||
|
}}
|
||||||
|
data-component="select-content"
|
||||||
|
data-trigger-style="settings-v2"
|
||||||
|
>
|
||||||
|
<Kobalte.Listbox data-slot="select-select-content-list" />
|
||||||
|
</Kobalte.Content>
|
||||||
|
</Kobalte.Portal>
|
||||||
|
</Kobalte>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
box-shadow: 0 0 0 0.5px var(--accordion-v2-border);
|
box-shadow: 0 0 0 0.5px var(--accordion-v2-border);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
font-family: var(--v2-font-family-sans), "Inter", system-ui, sans-serif;
|
|
||||||
color: var(--accordion-v2-fg);
|
color: var(--accordion-v2-fg);
|
||||||
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
@@ -59,7 +58,6 @@
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
line-height: 100%;
|
line-height: 100%;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
height: 28px;
|
height: 28px;
|
||||||
border-radius: var(--avatar-radius);
|
border-radius: var(--avatar-radius);
|
||||||
border: 0.5px solid var(--v2-border-border-base);
|
border: 0.5px solid var(--v2-border-border-base);
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-weight: 530;
|
font-weight: 530;
|
||||||
font-size: var(--avatar-font-size);
|
font-size: var(--avatar-font-size);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|||||||
@@ -10,19 +10,18 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
border: 0.5px solid var(--border-border-base);
|
border: 0.5px solid var(--v2-border-border-base);
|
||||||
background: var(--background-bg-layer-02);
|
background: var(--v2-background-bg-layer-02);
|
||||||
|
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 530;
|
font-weight: 530;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
letter-spacing: 0.05px;
|
letter-spacing: 0.05px;
|
||||||
color: var(--text-text-muted);
|
color: var(--v2-text-text-muted);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tag"][data-high-contrast] {
|
[data-component="tag"][data-high-contrast] {
|
||||||
border-color: var(--border-border-strong);
|
border-color: var(--v2-border-border-strong);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: var(--v2-font-family-sans), var(--sans), system-ui, sans-serif;
|
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
|
|
||||||
[data-slot="basic-tool-v2-trigger"] {
|
[data-slot="basic-tool-v2-trigger"] {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 530;
|
font-weight: 530;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -145,3 +144,26 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ghost muted */
|
||||||
|
[data-component="button-v2"][data-variant="ghost-muted"] {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--v2-text-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="button-v2"][data-variant="ghost-muted"] [data-slot="icon-svg"] {
|
||||||
|
color: var(--v2-icon-icon-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="button-v2"][data-variant="ghost-muted"]:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||||
|
background-color: var(--v2-overlay-simple-overlay-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="button-v2"][data-variant="ghost-muted"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
||||||
|
background-color: var(--v2-overlay-simple-overlay-pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="button-v2"][data-variant="ghost-muted"]:is(:disabled, [data-state="disabled"]) {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const docs = `### Overview
|
|||||||
Button v2 with three visual variants and two sizes.
|
Button v2 with three visual variants and two sizes.
|
||||||
|
|
||||||
### API
|
### API
|
||||||
- \`variant\`: "neutral" | "contrast" | "ghost".
|
- \`variant\`: "neutral" | "contrast" | "ghost" | "ghost-muted".
|
||||||
- \`size\`: "normal" | "large".
|
- \`size\`: "normal" | "large".
|
||||||
- \`icon\`: Optional icon name.
|
- \`icon\`: Optional icon name.
|
||||||
- Inherits Kobalte Button props and native button attributes.
|
- Inherits Kobalte Button props and native button attributes.
|
||||||
@@ -39,7 +39,7 @@ export default {
|
|||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
control: "select",
|
control: "select",
|
||||||
options: ["neutral", "contrast", "ghost"],
|
options: ["neutral", "contrast", "ghost", "ghost-muted"],
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
control: "select",
|
control: "select",
|
||||||
@@ -63,6 +63,9 @@ export const Variants = {
|
|||||||
<ButtonV2 variant="neutral">Neutral</ButtonV2>
|
<ButtonV2 variant="neutral">Neutral</ButtonV2>
|
||||||
<ButtonV2 variant="contrast">Contrast</ButtonV2>
|
<ButtonV2 variant="contrast">Contrast</ButtonV2>
|
||||||
<ButtonV2 variant="ghost">Ghost</ButtonV2>
|
<ButtonV2 variant="ghost">Ghost</ButtonV2>
|
||||||
|
<ButtonV2 variant="ghost-muted" icon="edit">
|
||||||
|
Ghost muted
|
||||||
|
</ButtonV2>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -112,7 +115,7 @@ export const Icon = {
|
|||||||
|
|
||||||
export const AllStates = {
|
export const AllStates = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const variants = ["neutral", "contrast", "ghost"] as const
|
const variants = ["neutral", "contrast", "ghost", "ghost-muted"] as const
|
||||||
const states = ["default", "hover", "pressed", "focus", "disabled"] as const
|
const states = ["default", "hover", "pressed", "focus", "disabled"] as const
|
||||||
const toTitleCase = (value: string) => value.charAt(0).toUpperCase() + value.slice(1)
|
const toTitleCase = (value: string) => value.charAt(0).toUpperCase() + value.slice(1)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface ButtonV2Props
|
|||||||
extends ComponentProps<typeof Kobalte>,
|
extends ComponentProps<typeof Kobalte>,
|
||||||
Pick<ComponentProps<"button">, "class" | "classList" | "children"> {
|
Pick<ComponentProps<"button">, "class" | "classList" | "children"> {
|
||||||
size?: "small" | "normal" | "large"
|
size?: "small" | "normal" | "large"
|
||||||
variant?: "neutral" | "contrast" | "ghost"
|
variant?: "neutral" | "contrast" | "ghost" | "ghost-muted"
|
||||||
icon?: IconProps["name"]
|
icon?: IconProps["name"]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export function ButtonV2(props: ButtonV2Props) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={resolvedIcon()}>
|
<Show when={resolvedIcon()}>
|
||||||
<Icon name={resolvedIcon()!} size="small" />
|
<Icon name={resolvedIcon()!} />
|
||||||
</Show>
|
</Show>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Kobalte>
|
</Kobalte>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
|
|
||||||
[data-slot="checkbox-v2-error"] {
|
[data-slot="checkbox-v2-error"] {
|
||||||
color: var(--state-fg-danger);
|
color: var(--state-fg-danger);
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: var(--font-weight-regular);
|
font-weight: var(--font-weight-regular);
|
||||||
line-height: var(--line-height-normal);
|
line-height: var(--line-height-normal);
|
||||||
@@ -90,7 +89,6 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
@@ -109,7 +107,6 @@
|
|||||||
|
|
||||||
[data-slot="checkbox-v2-description"] {
|
[data-slot="checkbox-v2-description"] {
|
||||||
color: var(--text-text-muted);
|
color: var(--text-text-muted);
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
background-color: var(--overlay-simple-overlay-scrim);
|
background-color: var(--v2-overlay-simple-overlay-scrim);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="dialog"] {
|
[data-component="dialog"] {
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
width: 480px;
|
width: 480px;
|
||||||
height: 368px;
|
height: 368px;
|
||||||
background: var(--background-bg-layer-01);
|
background: var(--v2-background-bg-layer-01);
|
||||||
box-shadow: var(--elevation-overlay);
|
box-shadow: var(--v2-elevation-overlay);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
@@ -64,22 +64,20 @@
|
|||||||
|
|
||||||
[data-slot="dialog-title"] {
|
[data-slot="dialog-title"] {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: "Inter", var(--v2-font-family-sans);
|
|
||||||
font-weight: 530;
|
font-weight: 530;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 100%;
|
line-height: 100%;
|
||||||
letter-spacing: -0.13px;
|
letter-spacing: -0.13px;
|
||||||
color: var(--text-text-base);
|
color: var(--v2-text-text-base);
|
||||||
font-variation-settings: "slnt" 0;
|
font-variation-settings: "slnt" 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="dialog-description"] {
|
[data-slot="dialog-description"] {
|
||||||
font-family: "Inter", var(--v2-font-family-sans);
|
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 100%;
|
line-height: 100%;
|
||||||
letter-spacing: -0.04px;
|
letter-spacing: -0.04px;
|
||||||
color: var(--text-text-muted);
|
color: var(--v2-text-text-muted);
|
||||||
font-variation-settings: "slnt" 0;
|
font-variation-settings: "slnt" 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +92,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--overlay-simple-overlay-hover);
|
background: var(--v2-overlay-simple-overlay-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
[data-slot="diff-changes-additions"],
|
[data-slot="diff-changes-additions"],
|
||||||
[data-slot="diff-changes-deletions"] {
|
[data-slot="diff-changes-deletions"] {
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 530;
|
font-weight: 530;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -76,7 +75,6 @@
|
|||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 11px;
|
min-height: 11px;
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
@@ -2,20 +2,20 @@ import { onMount, type ComponentProps, splitProps } from "solid-js"
|
|||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
edit: {
|
edit: {
|
||||||
viewBox: "0 0 20 20",
|
viewBox: "0 0 16 16",
|
||||||
body: `<path d="M17.0832 17.0807V17.5807H17.5832V17.0807H17.0832ZM2.9165 17.0807H2.4165V17.5807H2.9165V17.0807ZM2.9165 2.91406V2.41406H2.4165V2.91406H2.9165ZM9.58317 3.41406H10.0832V2.41406H9.58317V2.91406V3.41406ZM17.5832 10.4141V9.91406H16.5832V10.4141H17.0832H17.5832ZM6.24984 11.2474L5.89628 10.8938L5.74984 11.0403V11.2474H6.24984ZM6.24984 13.7474H5.74984V14.2474H6.24984V13.7474ZM8.74984 13.7474V14.2474H8.95694L9.10339 14.101L8.74984 13.7474ZM15.2082 2.28906L15.5617 1.93551L15.2082 1.58196L14.8546 1.93551L15.2082 2.28906ZM17.7082 4.78906L18.0617 5.14262L18.4153 4.78906L18.0617 4.43551L17.7082 4.78906ZM17.0832 17.0807V16.5807H2.9165V17.0807V17.5807H17.0832V17.0807ZM2.9165 17.0807H3.4165V2.91406H2.9165H2.4165V17.0807H2.9165ZM2.9165 2.91406V3.41406H9.58317V2.91406V2.41406H2.9165V2.91406ZM17.0832 10.4141H16.5832V17.0807H17.0832H17.5832V10.4141H17.0832ZM6.24984 11.2474H5.74984V13.7474H6.24984H6.74984V11.2474H6.24984ZM6.24984 13.7474V14.2474H8.74984V13.7474V13.2474H6.24984V13.7474ZM6.24984 11.2474L6.60339 11.6009L15.5617 2.64262L15.2082 2.28906L14.8546 1.93551L5.89628 10.8938L6.24984 11.2474ZM15.2082 2.28906L14.8546 2.64262L17.3546 5.14262L17.7082 4.78906L18.0617 4.43551L15.5617 1.93551L15.2082 2.28906ZM17.7082 4.78906L17.3546 4.43551L8.39628 13.3938L8.74984 13.7474L9.10339 14.101L18.0617 5.14262L17.7082 4.78906Z" fill="currentColor"/>`,
|
body: `<path d="M13.5555 8.21534V13.5556H2.44434L2.44434 2.4445H7.78462M6.88878 9.11119C6.88878 9.11119 8.96327 9.0367 9.69678 8.3032L14.0301 3.96986C14.5824 3.4176 14.5824 2.52213 14.0301 1.96986C13.4778 1.4176 12.5824 1.4176 12.0301 1.96986L7.69678 6.3032C7.00513 6.99484 6.88878 9.11119 6.88878 9.11119Z" stroke="currentColor"/>`,
|
||||||
},
|
},
|
||||||
"folder-add-left": {
|
"folder-add-left": {
|
||||||
viewBox: "0 0 20 20",
|
viewBox: "0 0 16 16",
|
||||||
body: `<path d="M2.08333 9.58268V2.91602H8.33333L10 5.41602H17.9167V16.2493H8.75M3.75 12.0827V14.5827M3.75 14.5827V17.0827M3.75 14.5827H1.25M3.75 14.5827H6.25" stroke="currentColor" stroke-linecap="square"/>`,
|
body: `<path d="M7.5 13.3333H1.5V2H6.83333L8.83333 4H14.8333V6M10.1667 11.3333H15.5M12.8333 8.66667V14" stroke="currentColor" stroke-miterlimit="10" stroke-linecap="square"/>`,
|
||||||
},
|
},
|
||||||
"grid-plus": {
|
"grid-plus": {
|
||||||
viewBox: "0 0 16 16",
|
viewBox: "0 0 16 16",
|
||||||
body: `<path d="M13.9948 11.668H9.32812M11.6641 9.33203V13.9987M6.66667 9.33203V13.9987H2V9.33203H6.66667ZM6.66667 2V6.66667H2V2H6.66667ZM13.9948 2V6.66667H9.32812V2H13.9948Z" stroke="currentColor" stroke-miterlimit="10" stroke-linecap="square"/>`,
|
body: `<path d="M13.9948 11.668H9.32812M11.6641 9.33203V13.9987M6.66667 9.33203V13.9987H2V9.33203H6.66667ZM6.66667 2V6.66667H2V2H6.66667ZM13.9948 2V6.66667H9.32812V2H13.9948Z" stroke="currentColor" stroke-miterlimit="10" stroke-linecap="square"/>`,
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
viewBox: "0 0 20 20",
|
viewBox: "0 0 16 16",
|
||||||
body: `<path d="M7.91683 7.91927V6.2526H12.0835V8.7526L10.0002 10.0026V12.0859M10.0002 13.7526V13.7609M17.9168 10.0026C17.9168 14.3749 14.3724 17.9193 10.0002 17.9193C5.62791 17.9193 2.0835 14.3749 2.0835 10.0026C2.0835 5.63035 5.62791 2.08594 10.0002 2.08594C14.3724 2.08594 17.9168 5.63035 17.9168 10.0026Z" stroke="currentColor" stroke-linecap="square"/>`,
|
body: `<path d="M6.33345 6.33349V5.00015H9.66679V7.00015L8.00015 8.00015V9.66679M8.27485 11.6819H7.71897M14.4446 8.00011C14.4446 11.5593 11.5593 14.4446 8.00011 14.4446C4.44094 14.4446 1.55566 11.5593 1.55566 8.00011C1.55566 4.44094 4.44094 1.55566 8.00011 1.55566C11.5593 1.55566 14.4446 4.44094 14.4446 8.00011Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
},
|
},
|
||||||
"sidebar-right": {
|
"sidebar-right": {
|
||||||
viewBox: "0 0 20 20",
|
viewBox: "0 0 20 20",
|
||||||
@@ -31,7 +31,7 @@ const icons = {
|
|||||||
},
|
},
|
||||||
"magnifying-glass": {
|
"magnifying-glass": {
|
||||||
viewBox: "0 0 16 16",
|
viewBox: "0 0 16 16",
|
||||||
body: `<path d="M13 13L10.6418 10.6418M11.9552 7.47761C11.9552 9.95053 9.95053 11.9552 7.47761 11.9552C5.0047 11.9552 3 9.95053 3 7.47761C3 5.0047 5.0047 3 7.47761 3C9.95053 3 11.9552 5.0047 11.9552 7.47761Z" stroke="currentColor" stroke-linecap="square" vector-effect="non-scaling-stroke"/>`,
|
body: `<path d="M14 14L10.3454 10.3454M6.88889 11.7778C9.58889 11.7778 11.7778 9.58889 11.7778 6.88889C11.7778 4.18889 9.58889 2 6.88889 2C4.18889 2 2 4.18889 2 6.88889C2 9.58889 4.18889 11.7778 6.88889 11.7778Z" stroke="currentColor"/>`,
|
||||||
},
|
},
|
||||||
menu: {
|
menu: {
|
||||||
viewBox: "0 0 16 16",
|
viewBox: "0 0 16 16",
|
||||||
@@ -42,8 +42,16 @@ const icons = {
|
|||||||
body: `<path d="M8 2.88867V13.1109" stroke="currentColor" stroke-linejoin="round"/><path d="M2.88867 8H13.1109" stroke="currentColor" stroke-linejoin="round"/>`,
|
body: `<path d="M8 2.88867V13.1109" stroke="currentColor" stroke-linejoin="round"/><path d="M2.88867 8H13.1109" stroke="currentColor" stroke-linejoin="round"/>`,
|
||||||
},
|
},
|
||||||
"settings-gear": {
|
"settings-gear": {
|
||||||
|
viewBox: "0 0 16 16",
|
||||||
|
body: `<path d="M7.99998 1.3335L14 4.66683V11.3335L7.99998 14.6668L2 11.3335V4.66683L7.99998 1.3335Z" stroke="currentColor"/><path d="M9.99998 8.00016C9.99998 9.10476 9.10458 10.0002 7.99998 10.0002C6.89538 10.0002 5.99998 9.10476 5.99998 8.00016C5.99998 6.89556 6.89538 6.00016 7.99998 6.00016C9.10458 6.00016 9.99998 6.89556 9.99998 8.00016Z" stroke="currentColor"/>`,
|
||||||
|
},
|
||||||
|
"chevron-down": {
|
||||||
|
viewBox: "0 0 16 16",
|
||||||
|
body: `<path d="M5 6.5L8 9.5L11 6.5" stroke="currentColor"/>`,
|
||||||
|
},
|
||||||
|
close: {
|
||||||
viewBox: "0 0 20 20",
|
viewBox: "0 0 20 20",
|
||||||
body: `<path d="M7.62516 4.46094L5.05225 3.86719L3.86475 5.05469L4.4585 7.6276L2.0835 9.21094V10.7943L4.4585 12.3776L3.86475 14.9505L5.05225 16.138L7.62516 15.5443L9.2085 17.9193H10.7918L12.3752 15.5443L14.9481 16.138L16.1356 14.9505L15.5418 12.3776L17.9168 10.7943V9.21094L15.5418 7.6276L16.1356 5.05469L14.9481 3.86719L12.3752 4.46094L10.7918 2.08594H9.2085L7.62516 4.46094Z" stroke="currentColor"/><path d="M12.5002 10.0026C12.5002 11.3833 11.3809 12.5026 10.0002 12.5026C8.61945 12.5026 7.50016 11.3833 7.50016 10.0026C7.50016 8.62189 8.61945 7.5026 10.0002 7.5026C11.3809 7.5026 12.5002 8.62189 12.5002 10.0026Z" stroke="currentColor"/>`,
|
body: `<path d="M14.4446 5.55566L5.55566 14.4446M5.55566 5.55566L14.4446 14.4446" stroke="currentColor" stroke-linejoin="round"/>`,
|
||||||
},
|
},
|
||||||
"xmark-small": {
|
"xmark-small": {
|
||||||
viewBox: "0 0 16 16",
|
viewBox: "0 0 16 16",
|
||||||
|
|||||||
@@ -81,7 +81,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -138,7 +137,6 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
|
|
||||||
[data-component="keybind-v2"] {
|
[data-component="keybind-v2"] {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-family: var(--v2-font-family-sans), var(--sans), system-ui, sans-serif;
|
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0px;
|
padding: 0;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="keybind-v2"] *,
|
[data-component="keybind-v2"] *,
|
||||||
@@ -26,9 +26,9 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0px;
|
padding: 0;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
width: 14px;
|
min-width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-component="keybind-v2"][data-variant="neutral"] [data-slot="keybind-v2-key"] {
|
[data-component="keybind-v2"][data-variant="neutral"] [data-slot="keybind-v2-key"] {
|
||||||
background: var(--background-bg-layer-03);
|
background: var(--v2-background-bg-layer-03);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="keybind-v2"][data-variant="ghost"] [data-slot="keybind-v2-key"] {
|
[data-component="keybind-v2"][data-variant="ghost"] [data-slot="keybind-v2-key"] {
|
||||||
@@ -48,26 +48,27 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 14px;
|
min-width: 14px;
|
||||||
height: 14px;
|
height: 11px;
|
||||||
padding: 0px;
|
padding: 0;
|
||||||
flex: 1 1 auto;
|
flex: none;
|
||||||
align-self: stretch;
|
|
||||||
font-family: "Inter", var(--v2-font-family-sans), var(--sans), system-ui, sans-serif;
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 530;
|
font-weight: 530;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 100%;
|
line-height: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
letter-spacing: 0.05px;
|
letter-spacing: 0.05px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-feature-settings: "tnum" on, "lnum" on;
|
||||||
font-variation-settings: "slnt" 0;
|
font-variation-settings: "slnt" 0;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="keybind-v2"][data-variant="neutral"] [data-slot="keybind-v2-label"] {
|
[data-component="keybind-v2"][data-variant="neutral"] [data-slot="keybind-v2-label"] {
|
||||||
color: var(--text-text-muted);
|
color: var(--v2-text-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="keybind-v2"][data-variant="ghost"] [data-slot="keybind-v2-label"] {
|
[data-component="keybind-v2"][data-variant="ghost"] [data-slot="keybind-v2-label"] {
|
||||||
color: var(--text-text-faint);
|
color: var(--v2-text-text-faint);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
[data-component="line-comment-v2"] {
|
[data-component="line-comment-v2"] {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-family: var(--v2-font-family-sans), var(--sans), system-ui, sans-serif;
|
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -155,7 +154,6 @@
|
|||||||
border: 1px solid var(--border-border-base);
|
border: 1px solid var(--border-border-base);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: linear-gradient(180deg, var(--alpha-light-2) 0%, var(--alpha-light-0) 100%), var(--background-bg-base);
|
background: linear-gradient(180deg, var(--alpha-light-2) 0%, var(--alpha-light-0) 100%), var(--background-bg-base);
|
||||||
font-family: inherit;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
|
|||||||
@@ -6,12 +6,11 @@
|
|||||||
padding: 2px;
|
padding: 2px;
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
|
|
||||||
background: var(--v2-background-bg-layer-01);
|
background: var(--background-bg-layer-01);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: var(--v2-elevation-floating);
|
box-shadow: var(--elevation-floating);
|
||||||
|
|
||||||
outline: none;
|
outline: none;
|
||||||
font-family: var(--v2-font-family-sans), "Inter", system-ui, sans-serif;
|
|
||||||
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
@@ -31,9 +30,9 @@
|
|||||||
--menu-v2-fg-subtle: var(--text-text-muted);
|
--menu-v2-fg-subtle: var(--text-text-muted);
|
||||||
--menu-v2-icon: var(--icon-icon-base);
|
--menu-v2-icon: var(--icon-icon-base);
|
||||||
--menu-v2-accent: var(--text-text-accent);
|
--menu-v2-accent: var(--text-text-accent);
|
||||||
--menu-v2-badge-bg: var(--v2-background-bg-layer-02);
|
--menu-v2-badge-bg: var(--background-bg-layer-02);
|
||||||
--menu-v2-badge-border: var(--v2-border-border-base);
|
--menu-v2-badge-border: var(--border-border-base);
|
||||||
--menu-v2-hover: var(--v2-overlay-simple-overlay-hover);
|
--menu-v2-hover: var(--overlay-simple-overlay-hover);
|
||||||
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
@@ -52,7 +51,6 @@
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
font-family: var(--v2-font-family-sans), "Inter", system-ui, sans-serif;
|
|
||||||
font-variation-settings: "slnt" 0;
|
font-variation-settings: "slnt" 0;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
color: var(--menu-v2-fg);
|
color: var(--menu-v2-fg);
|
||||||
@@ -153,7 +151,7 @@
|
|||||||
height: 1px;
|
height: 1px;
|
||||||
width: calc(100% + 4px);
|
width: calc(100% + 4px);
|
||||||
margin: 2px -2px;
|
margin: 2px -2px;
|
||||||
background: var(--v2-border-border-muted);
|
background: var(--border-border-muted);
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +162,6 @@
|
|||||||
height: 28px;
|
height: 28px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
|
||||||
font-family: var(--v2-font-family-sans), "Inter", system-ui, sans-serif;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 530;
|
font-weight: 530;
|
||||||
line-height: 100%;
|
line-height: 100%;
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
[data-component="project-avatar-v2"] {
|
||||||
|
--project-avatar-bg: var(--v2-avatar-bg-gray);
|
||||||
|
--project-avatar-border: var(--v2-avatar-border-gray);
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, var(--v2-alpha-light-16) 0%, var(--v2-alpha-light-0) 100%), var(--project-avatar-bg);
|
||||||
|
box-shadow: inset 0 0 0 0.5px var(--project-avatar-border);
|
||||||
|
font-weight: 530;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0.05px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--v2-grey-100);
|
||||||
|
text-shadow: 0 0 4px var(--v2-alpha-dark-20);
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="project-avatar-v2"][data-variant="orange"] {
|
||||||
|
--project-avatar-bg: var(--v2-avatar-bg-orange);
|
||||||
|
--project-avatar-border: var(--v2-avatar-border-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="project-avatar-v2"][data-variant="yellow"] {
|
||||||
|
--project-avatar-bg: var(--v2-avatar-bg-yellow);
|
||||||
|
--project-avatar-border: var(--v2-avatar-border-yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="project-avatar-v2"][data-variant="cyan"] {
|
||||||
|
--project-avatar-bg: var(--v2-avatar-bg-cyan);
|
||||||
|
--project-avatar-border: var(--v2-avatar-border-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="project-avatar-v2"][data-variant="green"] {
|
||||||
|
--project-avatar-bg: var(--v2-avatar-bg-green);
|
||||||
|
--project-avatar-border: var(--v2-avatar-border-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="project-avatar-v2"][data-variant="red"] {
|
||||||
|
--project-avatar-bg: var(--v2-avatar-bg-red);
|
||||||
|
--project-avatar-border: var(--v2-avatar-border-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="project-avatar-v2"][data-variant="pink"] {
|
||||||
|
--project-avatar-bg: var(--v2-avatar-bg-pink);
|
||||||
|
--project-avatar-border: var(--v2-avatar-border-pink);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="project-avatar-v2"][data-variant="blue"] {
|
||||||
|
--project-avatar-bg: var(--v2-avatar-bg-blue);
|
||||||
|
--project-avatar-border: var(--v2-avatar-border-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="project-avatar-v2"][data-variant="purple"] {
|
||||||
|
--project-avatar-bg: var(--v2-avatar-bg-purple);
|
||||||
|
--project-avatar-border: var(--v2-avatar-border-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="project-avatar-v2"][data-variant="gray"] {
|
||||||
|
--project-avatar-bg: var(--v2-avatar-bg-gray);
|
||||||
|
--project-avatar-border: var(--v2-avatar-border-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="project-avatar-v2"][data-has-image] {
|
||||||
|
background: var(--project-avatar-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="project-avatar-v2"] [data-slot="project-avatar-image"] {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
object-fit: cover;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="project-avatar-v2"] [data-slot="project-avatar-loader"] {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: conic-gradient(
|
||||||
|
from 180deg at 50% 50%,
|
||||||
|
var(--v2-grey-100) 0deg,
|
||||||
|
var(--v2-grey-1200) 0.04deg,
|
||||||
|
var(--v2-alpha-dark-50) 90deg,
|
||||||
|
var(--v2-grey-100) 360deg
|
||||||
|
);
|
||||||
|
mix-blend-mode: soft-light;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: project-avatar-v2-loader-spin 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes project-avatar-v2-loader-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="project-avatar-slot"] {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="project-avatar-v2"][data-unread] {
|
||||||
|
overflow: visible;
|
||||||
|
outline: 2px solid var(--v2-background-bg-accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { For } from "solid-js"
|
||||||
|
import { ProjectAvatar, PROJECT_AVATAR_VARIANTS } from "./project-avatar-v2"
|
||||||
|
|
||||||
|
const docs = `### Overview
|
||||||
|
Saturated 16px project avatar with color variants and optional unread ring.
|
||||||
|
|
||||||
|
### API
|
||||||
|
- Required: \`fallback\` string.
|
||||||
|
- Optional: \`src\`, \`variant\`, \`unread\`.
|
||||||
|
|
||||||
|
### Variants
|
||||||
|
- Color: orange, yellow, cyan, green, red, pink, blue, purple, gray.
|
||||||
|
- Image vs initial content state.
|
||||||
|
- Unread ring when \`unread\` is set.
|
||||||
|
|
||||||
|
### Theming
|
||||||
|
- Uses \`--v2-avatar-bg-*\` and \`--v2-avatar-border-*\` tokens with inset box-shadow borders.
|
||||||
|
`
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "UI V2/ProjectAvatar",
|
||||||
|
id: "components-project-avatar-v2",
|
||||||
|
component: ProjectAvatar,
|
||||||
|
tags: ["autodocs"],
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component: docs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
variant: {
|
||||||
|
control: "select",
|
||||||
|
options: [...PROJECT_AVATAR_VARIANTS],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
fallback: "O",
|
||||||
|
variant: "orange",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Basic = {}
|
||||||
|
|
||||||
|
export const WithImage = {
|
||||||
|
args: {
|
||||||
|
src: "https://placehold.co/32x32/png",
|
||||||
|
fallback: "O",
|
||||||
|
variant: "blue",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AllVariants = {
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: "flex", gap: "16px", "align-items": "center" }}>
|
||||||
|
<For each={PROJECT_AVATAR_VARIANTS}>
|
||||||
|
{(variant) => <ProjectAvatar fallback={variant[0].toUpperCase()} variant={variant} />}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Unread = {
|
||||||
|
args: {
|
||||||
|
fallback: "O",
|
||||||
|
variant: "orange",
|
||||||
|
unread: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Loading = {
|
||||||
|
args: {
|
||||||
|
fallback: "O",
|
||||||
|
variant: "orange",
|
||||||
|
loading: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoadingAndUnread = {
|
||||||
|
args: {
|
||||||
|
fallback: "O",
|
||||||
|
variant: "blue",
|
||||||
|
loading: true,
|
||||||
|
unread: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { type ComponentProps, splitProps, Show } from "solid-js"
|
||||||
|
import "./project-avatar-v2.css"
|
||||||
|
|
||||||
|
const segmenter =
|
||||||
|
typeof Intl !== "undefined" && "Segmenter" in Intl
|
||||||
|
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
function first(value: string) {
|
||||||
|
if (!value) return ""
|
||||||
|
if (!segmenter) return Array.from(value)[0] ?? ""
|
||||||
|
return segmenter.segment(value)[Symbol.iterator]().next().value?.segment ?? Array.from(value)[0] ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PROJECT_AVATAR_VARIANTS = [
|
||||||
|
"orange",
|
||||||
|
"yellow",
|
||||||
|
"cyan",
|
||||||
|
"green",
|
||||||
|
"red",
|
||||||
|
"pink",
|
||||||
|
"blue",
|
||||||
|
"purple",
|
||||||
|
"gray",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type ProjectAvatarVariant = (typeof PROJECT_AVATAR_VARIANTS)[number]
|
||||||
|
|
||||||
|
export interface ProjectAvatarProps extends ComponentProps<"div"> {
|
||||||
|
fallback: string
|
||||||
|
src?: string
|
||||||
|
variant?: ProjectAvatarVariant
|
||||||
|
unread?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectAvatar(props: ProjectAvatarProps) {
|
||||||
|
const [split, rest] = splitProps(props, [
|
||||||
|
"fallback",
|
||||||
|
"src",
|
||||||
|
"variant",
|
||||||
|
"unread",
|
||||||
|
"loading",
|
||||||
|
"class",
|
||||||
|
"classList",
|
||||||
|
"style",
|
||||||
|
])
|
||||||
|
const src = split.src
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...rest}
|
||||||
|
data-component="project-avatar-v2"
|
||||||
|
data-variant={split.variant ?? "gray"}
|
||||||
|
data-has-image={src ? "" : undefined}
|
||||||
|
data-unread={split.unread ? "" : undefined}
|
||||||
|
data-loading={split.loading ? "" : undefined}
|
||||||
|
classList={{
|
||||||
|
...split.classList,
|
||||||
|
[split.class ?? ""]: !!split.class,
|
||||||
|
}}
|
||||||
|
style={typeof split.style === "object" ? split.style : undefined}
|
||||||
|
>
|
||||||
|
<Show when={src} fallback={first(split.fallback)}>
|
||||||
|
{(value) => <img src={value()} draggable={false} data-slot="project-avatar-image" />}
|
||||||
|
</Show>
|
||||||
|
<Show when={split.loading}>
|
||||||
|
<span data-slot="project-avatar-loader" aria-hidden="true" />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
color: var(--text-text-faint);
|
color: var(--text-text-faint);
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
@@ -20,7 +19,6 @@
|
|||||||
|
|
||||||
[data-slot="radio-v2-description"] {
|
[data-slot="radio-v2-description"] {
|
||||||
color: var(--text-text-faint);
|
color: var(--text-text-faint);
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
@@ -35,7 +33,6 @@
|
|||||||
|
|
||||||
[data-slot="radio-v2-error"] {
|
[data-slot="radio-v2-error"] {
|
||||||
color: var(--state-fg-danger);
|
color: var(--state-fg-danger);
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: var(--font-weight-regular);
|
font-weight: var(--font-weight-regular);
|
||||||
line-height: var(--line-height-normal);
|
line-height: var(--line-height-normal);
|
||||||
@@ -167,7 +164,6 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
@@ -186,7 +182,6 @@
|
|||||||
|
|
||||||
[data-slot="radio-v2-item-description"] {
|
[data-slot="radio-v2-item-description"] {
|
||||||
color: var(--text-text-muted);
|
color: var(--text-text-muted);
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: var(--v2-font-family-sans), var(--sans);
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
@@ -110,7 +110,6 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
outline: none;
|
outline: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
@@ -29,8 +29,8 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: none;
|
border: none;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, var(--alpha-light-0) 0%, var(--alpha-light-20) 100%), var(--background-bg-layer-03);
|
linear-gradient(180deg, var(--v2-alpha-light-0) 0%, var(--v2-alpha-light-20) 100%), var(--v2-background-bg-layer-03);
|
||||||
box-shadow: var(--elevation-switch-off);
|
box-shadow: var(--v2-elevation-switch-off);
|
||||||
transition:
|
transition:
|
||||||
background 90ms ease-out,
|
background 90ms ease-out,
|
||||||
opacity 90ms ease-out,
|
opacity 90ms ease-out,
|
||||||
@@ -43,15 +43,15 @@
|
|||||||
height: 12px;
|
height: 12px;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
border: 0.5px solid var(--overlay-gradient-depth-overlay-depth-top);
|
border: 0.5px solid var(--v2-overlay-gradient-depth-overlay-depth-top);
|
||||||
background:
|
background:
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
var(--overlay-gradient-depth-overlay-depth-top) 0%,
|
var(--v2-overlay-gradient-depth-overlay-depth-top) 0%,
|
||||||
var(--overlay-gradient-depth-overlay-depth-bot) 100%
|
var(--v2-overlay-gradient-depth-overlay-depth-bot) 100%
|
||||||
),
|
),
|
||||||
var(--grey-200);
|
var(--v2-grey-200);
|
||||||
box-shadow: var(--elevation-elements);
|
box-shadow: var(--v2-elevation-elements);
|
||||||
transition:
|
transition:
|
||||||
transform 90ms ease-out,
|
transform 90ms ease-out,
|
||||||
width 90ms ease-out,
|
width 90ms ease-out,
|
||||||
@@ -64,8 +64,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
color: var(--text-text-faint);
|
color: var(--v2-text-text-faint);
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
@@ -75,8 +74,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="switch-error"] {
|
[data-slot="switch-error"] {
|
||||||
color: var(--state-fg-danger);
|
color: var(--v2-state-fg-danger);
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: var(--font-weight-regular);
|
font-weight: var(--font-weight-regular);
|
||||||
line-height: var(--line-height-normal);
|
line-height: var(--line-height-normal);
|
||||||
@@ -89,8 +87,8 @@
|
|||||||
|
|
||||||
&:hover:not([data-disabled], [data-readonly]) [data-slot="switch-control"] {
|
&:hover:not([data-disabled], [data-readonly]) [data-slot="switch-control"] {
|
||||||
background:
|
background:
|
||||||
linear-gradient(0deg, var(--overlay-simple-overlay-hover), var(--overlay-simple-overlay-hover)),
|
linear-gradient(0deg, var(--v2-overlay-simple-overlay-hover), var(--v2-overlay-simple-overlay-hover)),
|
||||||
linear-gradient(180deg, var(--alpha-light-0) 0%, var(--alpha-light-20) 100%), var(--background-bg-layer-03);
|
linear-gradient(180deg, var(--v2-alpha-light-0) 0%, var(--v2-alpha-light-20) 100%), var(--v2-background-bg-layer-03);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover:not([data-disabled], [data-readonly]) [data-slot="switch-thumb"] {
|
&:hover:not([data-disabled], [data-readonly]) [data-slot="switch-thumb"] {
|
||||||
@@ -99,14 +97,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:not([data-readonly]) [data-slot="switch-input"]:focus-visible ~ [data-slot="switch-control"] {
|
&:not([data-readonly]) [data-slot="switch-input"]:focus-visible ~ [data-slot="switch-control"] {
|
||||||
outline: 2px solid var(--border-border-focus);
|
outline: 2px solid var(--v2-border-border-focus);
|
||||||
outline-offset: 1px;
|
outline-offset: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-checked] [data-slot="switch-control"] {
|
&[data-checked] [data-slot="switch-control"] {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, var(--alpha-light-0) 0%, var(--alpha-light-10) 100%), var(--background-bg-accent);
|
linear-gradient(180deg, var(--v2-alpha-light-0) 0%, var(--v2-alpha-light-10) 100%), var(--v2-background-bg-accent);
|
||||||
box-shadow: var(--elevation-switch-on);
|
box-shadow: var(--v2-elevation-switch-on);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-checked] [data-slot="switch-thumb"] {
|
&[data-checked] [data-slot="switch-thumb"] {
|
||||||
@@ -115,16 +113,16 @@
|
|||||||
background:
|
background:
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
var(--overlay-gradient-depth-overlay-depth-top) 0%,
|
var(--v2-overlay-gradient-depth-overlay-depth-top) 0%,
|
||||||
var(--overlay-gradient-depth-overlay-depth-bot) 100%
|
var(--v2-overlay-gradient-depth-overlay-depth-bot) 100%
|
||||||
),
|
),
|
||||||
var(--grey-300);
|
var(--v2-grey-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-checked]:hover:not([data-disabled], [data-readonly]) [data-slot="switch-control"] {
|
&[data-checked]:hover:not([data-disabled], [data-readonly]) [data-slot="switch-control"] {
|
||||||
background:
|
background:
|
||||||
linear-gradient(0deg, var(--overlay-simple-overlay-contrast-hover), var(--overlay-simple-overlay-contrast-hover)),
|
linear-gradient(0deg, var(--v2-overlay-simple-overlay-contrast-hover), var(--v2-overlay-simple-overlay-contrast-hover)),
|
||||||
linear-gradient(180deg, var(--alpha-light-0) 0%, var(--alpha-light-10) 100%), var(--background-bg-accent);
|
linear-gradient(180deg, var(--v2-alpha-light-0) 0%, var(--v2-alpha-light-10) 100%), var(--v2-background-bg-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-checked]:hover:not([data-disabled], [data-readonly]) [data-slot="switch-thumb"] {
|
&[data-checked]:hover:not([data-disabled], [data-readonly]) [data-slot="switch-thumb"] {
|
||||||
@@ -140,7 +138,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-invalid] [data-slot="switch-control"] {
|
&[data-invalid] [data-slot="switch-control"] {
|
||||||
border-color: var(--state-border-danger);
|
border-color: var(--v2-state-border-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-readonly] {
|
&[data-readonly] {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { splitProps, type ComponentProps } from "solid-js"
|
||||||
|
|
||||||
|
export function TabStateIndicator(props: ComponentProps<"svg">) {
|
||||||
|
const [local, rest] = splitProps(props, ["class", "classList", "width", "height"])
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...rest}
|
||||||
|
class={local.class}
|
||||||
|
classList={local.classList}
|
||||||
|
width={local.width ?? 16}
|
||||||
|
height={local.height ?? 16}
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden={rest["aria-hidden"] ?? "true"}
|
||||||
|
>
|
||||||
|
<g opacity="0.25" fill="#808080">
|
||||||
|
<rect x="13.5" y="2.5" width="2" height="2" transform="rotate(90 13.5 2.5)" />
|
||||||
|
<path d="M10.5 2.5L10.5 4.5L8.5 4.5L8.5 2.5L10.5 2.5Z" />
|
||||||
|
<path d="M4.5 2.5L4.5 4.5L2.5 4.5L2.5 2.5L4.5 2.5Z" />
|
||||||
|
<path d="M13.5 5.5L13.5 7.5L11.5 7.5L11.5 5.5L13.5 5.5Z" />
|
||||||
|
<path d="M4.5 5.5L4.5 7.5L2.5 7.5L2.5 5.5L4.5 5.5Z" />
|
||||||
|
<path d="M13.5 8.5L13.5 10.5L11.5 10.5L11.5 8.5L13.5 8.5Z" />
|
||||||
|
<path d="M4.5 8.5L4.5 10.5L2.5 10.5L2.5 8.5L4.5 8.5Z" />
|
||||||
|
<path d="M13.5 11.5L13.5 13.5L11.5 13.5L11.5 11.5L13.5 11.5Z" />
|
||||||
|
<path d="M7.5 11.5L7.5 13.5L5.5 13.5L5.5 11.5L7.5 11.5Z" />
|
||||||
|
<path d="M4.5 11.5L4.5 13.5L2.5 13.5L2.5 11.5L4.5 11.5Z" />
|
||||||
|
<path d="M7.5 2.5L7.5 4.5L5.5 4.5L5.5 2.5L7.5 2.5Z" />
|
||||||
|
<path d="M10.5 5.5L10.5 7.5L8.5 7.5L8.5 5.5L10.5 5.5Z" />
|
||||||
|
<path d="M7.5 5.5L7.5 7.5L5.5 7.5L5.5 5.5L7.5 5.5Z" />
|
||||||
|
<path d="M10.5 8.5L10.5 10.5L8.5 10.5L8.5 8.5L10.5 8.5Z" />
|
||||||
|
<path d="M7.5 8.5L7.5 10.5L5.5 10.5L5.5 8.5L7.5 8.5Z" />
|
||||||
|
<path d="M10.5 11.5L10.5 13.5L8.5 13.5L8.5 11.5L10.5 11.5Z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tabs-v2"][data-orientation="horizontal"] {
|
[data-component="tabs-v2"][data-orientation="horizontal"] {
|
||||||
@@ -75,11 +74,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: var(--text-text-faint);
|
color: var(--v2-text-text-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tabs-v2"] [data-slot="tabs-v2-close-button"]:hover {
|
[data-component="tabs-v2"] [data-slot="tabs-v2-close-button"]:hover {
|
||||||
color: var(--text-text-muted);
|
color: var(--v2-text-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tabs-v2"] [data-component="icon-button"] {
|
[data-component="tabs-v2"] [data-component="icon-button"] {
|
||||||
@@ -88,7 +87,7 @@
|
|||||||
|
|
||||||
[data-component="tabs-v2"] [data-slot="tabs-v2-trigger-wrapper"]:disabled {
|
[data-component="tabs-v2"] [data-slot="tabs-v2-trigger-wrapper"]:disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: var(--text-text-faint);
|
color: var(--v2-text-text-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="normal"][data-orientation="horizontal"] [data-slot="tabs-v2-list"],
|
[data-component="tabs-v2"][data-variant="normal"][data-orientation="horizontal"] [data-slot="tabs-v2-list"],
|
||||||
@@ -105,7 +104,7 @@
|
|||||||
height: 1px;
|
height: 1px;
|
||||||
content: "";
|
content: "";
|
||||||
width: calc(100% + 16px);
|
width: calc(100% + 16px);
|
||||||
background-color: var(--border-border-base);
|
background-color: var(--v2-border-border-base);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
left: -8px;
|
left: -8px;
|
||||||
@@ -119,7 +118,7 @@
|
|||||||
[data-component="tabs-v2"][data-variant="normal"][data-orientation="horizontal"] [data-slot="tabs-v2-trigger-wrapper"] {
|
[data-component="tabs-v2"][data-variant="normal"][data-orientation="horizontal"] [data-slot="tabs-v2-trigger-wrapper"] {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
color: var(--text-text-muted);
|
color: var(--v2-text-text-muted);
|
||||||
border-bottom: 1px solid transparent;
|
border-bottom: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,18 +129,18 @@
|
|||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="normal"][data-orientation="horizontal"]
|
[data-component="tabs-v2"][data-variant="normal"][data-orientation="horizontal"]
|
||||||
[data-slot="tabs-v2-trigger-wrapper"]:hover:not(:disabled):not([data-selected]) {
|
[data-slot="tabs-v2-trigger-wrapper"]:hover:not(:disabled):not([data-selected]) {
|
||||||
color: var(--text-text-base);
|
color: var(--v2-text-text-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="normal"][data-orientation="horizontal"]
|
[data-component="tabs-v2"][data-variant="normal"][data-orientation="horizontal"]
|
||||||
[data-slot="tabs-v2-trigger-wrapper"]:has([data-selected]) {
|
[data-slot="tabs-v2-trigger-wrapper"]:has([data-selected]) {
|
||||||
border-bottom-color: var(--text-text-faint);
|
border-bottom-color: var(--v2-text-text-faint);
|
||||||
color: var(--text-text-base);
|
color: var(--v2-text-text-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="normal"][data-orientation="horizontal"]
|
[data-component="tabs-v2"][data-variant="normal"][data-orientation="horizontal"]
|
||||||
[data-slot="tabs-v2-trigger-wrapper"]:not(:has([data-selected])) {
|
[data-slot="tabs-v2-trigger-wrapper"]:not(:has([data-selected])) {
|
||||||
color: var(--text-text-muted);
|
color: var(--v2-text-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="pill"][data-orientation="horizontal"] [data-slot="tabs-v2-trigger-wrapper"] {
|
[data-component="tabs-v2"][data-variant="pill"][data-orientation="horizontal"] [data-slot="tabs-v2-trigger-wrapper"] {
|
||||||
@@ -149,7 +148,7 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 0.5px solid transparent;
|
border: 0.5px solid transparent;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: var(--text-text-muted);
|
color: var(--v2-text-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="pill"][data-orientation="horizontal"] [data-slot="tabs-v2-trigger"] {
|
[data-component="tabs-v2"][data-variant="pill"][data-orientation="horizontal"] [data-slot="tabs-v2-trigger"] {
|
||||||
@@ -162,16 +161,16 @@
|
|||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="pill"][data-orientation="horizontal"]
|
[data-component="tabs-v2"][data-variant="pill"][data-orientation="horizontal"]
|
||||||
[data-slot="tabs-v2-trigger-wrapper"]:hover:not(:disabled):not(:has([data-selected])) {
|
[data-slot="tabs-v2-trigger-wrapper"]:hover:not(:disabled):not(:has([data-selected])) {
|
||||||
background-color: var(--background-bg-layer-01);
|
background-color: var(--v2-background-bg-layer-01);
|
||||||
color: var(--text-text-base);
|
color: var(--v2-text-text-base);
|
||||||
border: 0.5px solid var(--border-border-muted);
|
border: 0.5px solid var(--v2-border-border-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="pill"][data-orientation="horizontal"]
|
[data-component="tabs-v2"][data-variant="pill"][data-orientation="horizontal"]
|
||||||
[data-slot="tabs-v2-trigger-wrapper"]:has([data-selected]) {
|
[data-slot="tabs-v2-trigger-wrapper"]:has([data-selected]) {
|
||||||
background-color: var(--background-bg-layer-02);
|
background-color: var(--v2-background-bg-layer-02);
|
||||||
color: var(--text-text-base);
|
color: var(--v2-text-text-base);
|
||||||
border: 0.5px solid var(--border-border-muted);
|
border: 0.5px solid var(--v2-border-border-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-list"] {
|
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-list"] {
|
||||||
@@ -182,13 +181,13 @@
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border-right: 1px solid var(--border-border-base);
|
border-right: 1px solid var(--v2-border-border-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-section-title"] {
|
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-section-title"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
color: var(--text-text-muted);
|
color: var(--v2-text-text-muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
@@ -199,7 +198,7 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 0.5px solid transparent;
|
border: 0.5px solid transparent;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: var(--text-text-muted);
|
color: var(--v2-text-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-trigger"] {
|
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-trigger"] {
|
||||||
@@ -212,12 +211,12 @@
|
|||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"]
|
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"]
|
||||||
[data-slot="tabs-v2-trigger-wrapper"]:hover:not(:disabled):not(:has([data-selected])) {
|
[data-slot="tabs-v2-trigger-wrapper"]:hover:not(:disabled):not(:has([data-selected])) {
|
||||||
color: var(--text-text-base);
|
color: var(--v2-text-text-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"]
|
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"]
|
||||||
[data-slot="tabs-v2-trigger-wrapper"]:has([data-selected]) {
|
[data-slot="tabs-v2-trigger-wrapper"]:has([data-selected]) {
|
||||||
background-color: var(--background-bg-layer-02);
|
background-color: var(--v2-background-bg-layer-02);
|
||||||
color: var(--text-text-base);
|
color: var(--v2-text-text-base);
|
||||||
border: 0.5px solid var(--border-border-muted);
|
border: 0.5px solid var(--v2-border-border-muted);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,9 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
outline: 1px solid transparent;
|
outline: 1px solid transparent;
|
||||||
outline-offset: 0;
|
outline-offset: 0;
|
||||||
background: linear-gradient(180deg, var(--alpha-light-2) 0%, var(--alpha-light-0) 100%), var(--background-bg-base);
|
background:
|
||||||
box-shadow: var(--elevation-button-neutral);
|
linear-gradient(180deg, var(--v2-alpha-light-2) 0%, var(--v2-alpha-light-0) 100%), var(--v2-background-bg-base);
|
||||||
|
box-shadow: var(--v2-elevation-button-neutral);
|
||||||
flex: none;
|
flex: none;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
transition:
|
transition:
|
||||||
@@ -27,17 +28,17 @@
|
|||||||
|
|
||||||
[data-component="text-input-v2"]:where(:hover):not([data-disabled], [data-invalid]):not(:focus-within) {
|
[data-component="text-input-v2"]:where(:hover):not([data-disabled], [data-invalid]):not(:focus-within) {
|
||||||
background:
|
background:
|
||||||
linear-gradient(0deg, var(--overlay-simple-overlay-hover), var(--overlay-simple-overlay-hover)),
|
linear-gradient(0deg, var(--v2-overlay-simple-overlay-hover), var(--v2-overlay-simple-overlay-hover)),
|
||||||
linear-gradient(180deg, var(--alpha-light-2) 0%, var(--alpha-light-0) 100%), var(--background-bg-base);
|
linear-gradient(180deg, var(--v2-alpha-light-2) 0%, var(--v2-alpha-light-0) 100%), var(--v2-background-bg-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="text-input-v2"]:where(:focus-within):not([data-disabled], [data-invalid]) {
|
[data-component="text-input-v2"]:where(:focus-within):not([data-disabled], [data-invalid]) {
|
||||||
outline-color: var(--border-border-focus);
|
outline-color: var(--v2-border-border-focus);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="text-input-v2"]:where([data-invalid]):not([data-disabled]) {
|
[data-component="text-input-v2"]:where([data-invalid]):not([data-disabled]) {
|
||||||
outline-color: var(--state-fg-danger);
|
outline-color: var(--v2-state-fg-danger);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,18 +68,17 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
letter-spacing: -0.04px;
|
letter-spacing: -0.04px;
|
||||||
color: var(--text-text-base);
|
color: var(--v2-text-text-base);
|
||||||
font-variation-settings: "slnt" 0;
|
font-variation-settings: "slnt" 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="text-input-v2"] [data-slot="text-input-v2-input"]::placeholder {
|
[data-component="text-input-v2"] [data-slot="text-input-v2-input"]::placeholder {
|
||||||
color: var(--text-text-faint);
|
color: var(--v2-text-text-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="text-input-v2"][data-numeric] [data-slot="text-input-v2-input"] {
|
[data-component="text-input-v2"][data-numeric] [data-slot="text-input-v2-input"] {
|
||||||
@@ -99,19 +99,19 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--icon-icon-muted);
|
color: var(--v2-icon-icon-muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="text-input-v2"]
|
[data-component="text-input-v2"]
|
||||||
[data-slot="text-input-v2-icon-button"]:is(:hover, [data-state="hover"]):not(:disabled) {
|
[data-slot="text-input-v2-icon-button"]:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||||
background-color: var(--overlay-simple-overlay-hover);
|
background-color: var(--v2-overlay-simple-overlay-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="text-input-v2"]
|
[data-component="text-input-v2"]
|
||||||
[data-slot="text-input-v2-icon-button"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
[data-slot="text-input-v2-icon-button"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
||||||
background-color: var(--overlay-simple-overlay-pressed);
|
background-color: var(--v2-overlay-simple-overlay-pressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="text-input-v2"] [data-slot="text-input-v2-icon-button"]:focus {
|
[data-component="text-input-v2"] [data-slot="text-input-v2-icon-button"]:focus {
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-component="text-input-v2"] [data-slot="text-input-v2-icon-button"]:focus-visible {
|
[data-component="text-input-v2"] [data-slot="text-input-v2-icon-button"]:focus-visible {
|
||||||
outline: 2px solid var(--border-border-focus);
|
outline: 2px solid var(--v2-border-border-focus);
|
||||||
outline-offset: 1px;
|
outline-offset: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,11 +135,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-component="text-input-v2"][data-invalid]:not([data-disabled]) [data-slot="text-input-v2-input"] {
|
[data-component="text-input-v2"][data-invalid]:not([data-disabled]) [data-slot="text-input-v2-input"] {
|
||||||
color: var(--state-fg-danger);
|
color: var(--v2-state-fg-danger);
|
||||||
caret-color: var(--state-fg-danger);
|
caret-color: var(--v2-state-fg-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="text-input-v2"][data-invalid]:not([data-disabled]) [data-slot="text-input-v2-input"]::placeholder {
|
[data-component="text-input-v2"][data-invalid]:not([data-disabled]) [data-slot="text-input-v2-input"]::placeholder {
|
||||||
color: var(--state-fg-danger);
|
color: var(--v2-state-fg-danger);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Compact single-line text field with neutral elevation, optional trailing copy ac
|
|||||||
- **Focus** (\`:focus-within\`): focus border, elevation removed.
|
- **Focus** (\`:focus-within\`): focus border, elevation removed.
|
||||||
- **Invalid**: danger border and text.
|
- **Invalid**: danger border and text.
|
||||||
- **Disabled**: 50% opacity.
|
- **Disabled**: 50% opacity.
|
||||||
- Uses \`data-component="text-input-v2"\` with \`--background-bg-base\`, \`--elevation-button-neutral\`, \`--text-text-faint\` (placeholder), and \`--icon-icon-muted\` (copy icon).
|
- Uses \`data-component="text-input-v2"\` with \`--v2-background-bg-base\`, \`--v2-elevation-button-neutral\`, \`--v2-text-text-faint\` (placeholder), and \`--v2-icon-icon-muted\` (copy icon).
|
||||||
|
|
||||||
### Field
|
### Field
|
||||||
Compose with \`Field\` for label, helper prefix/suffix, and tooltip — see the **Field** story.
|
Compose with \`Field\` for label, helper prefix/suffix, and tooltip — see the **Field** story.
|
||||||
|
|||||||
@@ -53,7 +53,6 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
outline: none;
|
outline: none;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
font-family: var(--v2-font-family-sans);
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
@@ -43,9 +43,9 @@
|
|||||||
transition: transform 140ms ease-out;
|
transition: transform 140ms ease-out;
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: var(--text-text-base);
|
color: var(--v2-text-text-base);
|
||||||
background: var(--background-bg-layer-01);
|
background: var(--v2-background-bg-layer-01);
|
||||||
box-shadow: var(--elevation-floating);
|
box-shadow: var(--v2-elevation-floating);
|
||||||
|
|
||||||
&[data-opened] {
|
&[data-opened] {
|
||||||
animation: toastV2PopIn 140ms ease-out;
|
animation: toastV2PopIn 140ms ease-out;
|
||||||
@@ -71,9 +71,10 @@
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
|
color: var(--v2-icon-icon-base);
|
||||||
|
|
||||||
[data-component="icon"] {
|
[data-component="icon"] {
|
||||||
color: var(--text-text-base);
|
color: var(--v2-icon-icon-base);
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -109,11 +110,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="toast-v2-title"] {
|
[data-slot="toast-v2-title"] {
|
||||||
color: var(--text-text-base);
|
color: var(--v2-text-text-base);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-family: "Inter Variable";
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 530;
|
font-weight: 530;
|
||||||
@@ -124,11 +124,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="toast-v2-description"] {
|
[data-slot="toast-v2-description"] {
|
||||||
color: var(--text-text-muted);
|
color: var(--v2-text-text-muted);
|
||||||
text-wrap-style: pretty;
|
text-wrap-style: pretty;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
font-family: "Inter Variable";
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 440;
|
font-weight: 440;
|
||||||
@@ -147,7 +146,6 @@
|
|||||||
|
|
||||||
[data-slot="toast-v2-actions"] [data-component="button-v2"] {
|
[data-slot="toast-v2-actions"] [data-component="button-v2"] {
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
font-family: "Inter Variable";
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 530;
|
font-weight: 530;
|
||||||
@@ -166,10 +164,26 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
color: var(--v2-icon-icon-muted);
|
||||||
|
cursor: pointer;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--v2-overlay-simple-overlay-hover);
|
||||||
|
color: var(--v2-icon-icon-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: var(--v2-overlay-simple-overlay-pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--v2-border-border-focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ function ToastV2CloseButton(props: ToastCloseButtonProps & ComponentProps<"butto
|
|||||||
return (
|
return (
|
||||||
<Kobalte.CloseButton data-slot="toast-v2-close-button" aria-label="Dismiss" {...props}>
|
<Kobalte.CloseButton data-slot="toast-v2-close-button" aria-label="Dismiss" {...props}>
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
<path d="M4.25 11.75L11.75 4.25" stroke="#FAFAFA" />
|
<path d="M4.25 11.75L11.75 4.25" stroke="currentColor" />
|
||||||
<path d="M11.75 11.75L4.25 4.25" stroke="#FAFAFA" />
|
<path d="M11.75 11.75L4.25 4.25" stroke="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</Kobalte.CloseButton>
|
</Kobalte.CloseButton>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
padding: 0 0 0 10px;
|
padding: 0 0 0 10px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
border-left: 2px solid var(--tec-border);
|
border-left: 2px solid var(--tec-border);
|
||||||
font-family: var(--v2-font-family-sans), var(--sans), system-ui, sans-serif;
|
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
|
|
||||||
[data-slot="tool-error-card-trigger"] {
|
[data-slot="tool-error-card-trigger"] {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
box-shadow: var(--elevation-floating);
|
box-shadow: var(--elevation-floating);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
font-family: "Inter Variable";
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 530;
|
font-weight: 530;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
@@ -63,6 +63,26 @@
|
|||||||
--v2-state-fg-info: var(--v2-blue-800);
|
--v2-state-fg-info: var(--v2-blue-800);
|
||||||
--v2-state-border-info: var(--v2-blue-300);
|
--v2-state-border-info: var(--v2-blue-300);
|
||||||
|
|
||||||
|
/* ── Project avatar ── */
|
||||||
|
--v2-avatar-bg-orange: var(--v2-orange-700);
|
||||||
|
--v2-avatar-border-orange: var(--v2-orange-800);
|
||||||
|
--v2-avatar-bg-yellow: var(--v2-yellow-700);
|
||||||
|
--v2-avatar-border-yellow: var(--v2-yellow-800);
|
||||||
|
--v2-avatar-bg-cyan: var(--v2-cyan-700);
|
||||||
|
--v2-avatar-border-cyan: var(--v2-cyan-800);
|
||||||
|
--v2-avatar-bg-green: var(--v2-green-700);
|
||||||
|
--v2-avatar-border-green: var(--v2-green-800);
|
||||||
|
--v2-avatar-bg-red: var(--v2-red-700);
|
||||||
|
--v2-avatar-border-red: var(--v2-red-800);
|
||||||
|
--v2-avatar-bg-pink: var(--v2-pink-700);
|
||||||
|
--v2-avatar-border-pink: var(--v2-pink-800);
|
||||||
|
--v2-avatar-bg-blue: var(--v2-blue-700);
|
||||||
|
--v2-avatar-border-blue: var(--v2-blue-800);
|
||||||
|
--v2-avatar-bg-purple: var(--v2-purple-700);
|
||||||
|
--v2-avatar-border-purple: var(--v2-purple-800);
|
||||||
|
--v2-avatar-bg-gray: var(--v2-grey-700);
|
||||||
|
--v2-avatar-border-gray: var(--v2-grey-800);
|
||||||
|
|
||||||
/* ── Elevation ── */
|
/* ── Elevation ── */
|
||||||
--v2-elevation-raised:
|
--v2-elevation-raised:
|
||||||
0px 2px 4px 0px var(--v2-alpha-dark-4), 0px 1px 2px -1px var(--v2-alpha-dark-8),
|
0px 2px 4px 0px var(--v2-alpha-dark-4), 0px 1px 2px -1px var(--v2-alpha-dark-8),
|
||||||
@@ -285,6 +305,25 @@
|
|||||||
--v2-illustration-illustration-layer-01: var(--v2-grey-300);
|
--v2-illustration-illustration-layer-01: var(--v2-grey-300);
|
||||||
--v2-illustration-illustration-layer-02: var(--v2-grey-400);
|
--v2-illustration-illustration-layer-02: var(--v2-grey-400);
|
||||||
--v2-illustration-illustration-layer-03: var(--v2-grey-500);
|
--v2-illustration-illustration-layer-03: var(--v2-grey-500);
|
||||||
|
|
||||||
|
--v2-avatar-bg-orange: var(--v2-orange-700);
|
||||||
|
--v2-avatar-border-orange: var(--v2-orange-800);
|
||||||
|
--v2-avatar-bg-yellow: var(--v2-yellow-700);
|
||||||
|
--v2-avatar-border-yellow: var(--v2-yellow-800);
|
||||||
|
--v2-avatar-bg-cyan: var(--v2-cyan-700);
|
||||||
|
--v2-avatar-border-cyan: var(--v2-cyan-800);
|
||||||
|
--v2-avatar-bg-green: var(--v2-green-700);
|
||||||
|
--v2-avatar-border-green: var(--v2-green-800);
|
||||||
|
--v2-avatar-bg-red: var(--v2-red-700);
|
||||||
|
--v2-avatar-border-red: var(--v2-red-800);
|
||||||
|
--v2-avatar-bg-pink: var(--v2-pink-700);
|
||||||
|
--v2-avatar-border-pink: var(--v2-pink-800);
|
||||||
|
--v2-avatar-bg-blue: var(--v2-blue-700);
|
||||||
|
--v2-avatar-border-blue: var(--v2-blue-800);
|
||||||
|
--v2-avatar-bg-purple: var(--v2-purple-700);
|
||||||
|
--v2-avatar-border-purple: var(--v2-purple-800);
|
||||||
|
--v2-avatar-bg-gray: var(--v2-grey-700);
|
||||||
|
--v2-avatar-border-gray: var(--v2-grey-800);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Explicit dark mode via data attribute (Storybook toggle, runtime JS) */
|
/* Explicit dark mode via data attribute (Storybook toggle, runtime JS) */
|
||||||
@@ -346,6 +385,25 @@
|
|||||||
--v2-state-fg-info: var(--v2-blue-500);
|
--v2-state-fg-info: var(--v2-blue-500);
|
||||||
--v2-state-border-info: var(--v2-blue-900);
|
--v2-state-border-info: var(--v2-blue-900);
|
||||||
|
|
||||||
|
--v2-avatar-bg-orange: var(--v2-orange-1100);
|
||||||
|
--v2-avatar-border-orange: var(--v2-orange-600);
|
||||||
|
--v2-avatar-bg-yellow: var(--v2-yellow-1100);
|
||||||
|
--v2-avatar-border-yellow: var(--v2-yellow-700);
|
||||||
|
--v2-avatar-bg-cyan: var(--v2-cyan-1000);
|
||||||
|
--v2-avatar-border-cyan: var(--v2-cyan-700);
|
||||||
|
--v2-avatar-bg-green: var(--v2-green-1000);
|
||||||
|
--v2-avatar-border-green: var(--v2-green-600);
|
||||||
|
--v2-avatar-bg-red: var(--v2-red-1000);
|
||||||
|
--v2-avatar-border-red: var(--v2-red-700);
|
||||||
|
--v2-avatar-bg-pink: var(--v2-pink-1000);
|
||||||
|
--v2-avatar-border-pink: var(--v2-pink-700);
|
||||||
|
--v2-avatar-bg-blue: var(--v2-blue-900);
|
||||||
|
--v2-avatar-border-blue: var(--v2-blue-500);
|
||||||
|
--v2-avatar-bg-purple: var(--v2-purple-1000);
|
||||||
|
--v2-avatar-border-purple: var(--v2-purple-600);
|
||||||
|
--v2-avatar-bg-gray: var(--v2-grey-700);
|
||||||
|
--v2-avatar-border-gray: var(--v2-grey-500);
|
||||||
|
|
||||||
--v2-elevation-raised:
|
--v2-elevation-raised:
|
||||||
0px 2px 4px 0px var(--v2-alpha-dark-30), 0px 1px 2px 0px var(--v2-alpha-dark-30),
|
0px 2px 4px 0px var(--v2-alpha-dark-30), 0px 1px 2px 0px var(--v2-alpha-dark-30),
|
||||||
0px 0px 0px 0.5px var(--v2-alpha-light-16), 0px -0.5px 0px 0px var(--v2-alpha-light-6);
|
0px 0px 0px 0.5px var(--v2-alpha-light-16), 0px -0.5px 0px 0px var(--v2-alpha-light-6);
|
||||||
|
|||||||
Reference in New Issue
Block a user