feat(app): updates to design system (#34066)

This commit is contained in:
Aarav Sareen 2026-06-26 18:52:35 +05:30 committed by GitHub
commit 9c38b91e91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 284 additions and 98 deletions

View file

@ -1,6 +1,6 @@
import "@pierre/trees/web-components"
import { FileTree } from "@pierre/trees"
import { Dialog, DialogFooter } from "@opencode-ai/ui/v2/dialog-v2"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@ -27,6 +27,7 @@ import {
pickerRoot,
} from "./directory-picker-domain"
import "./dialog-select-directory-v2.css"
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
interface DialogSelectDirectoryV2Props {
title?: string
@ -266,8 +267,12 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
onCleanup(() => tree?.cleanUp())
return (
<Dialog title={props.title ?? language.t("command.project.open")} size="large" class="directory-picker-v2">
<div class="directory-picker-v2-body">
<Dialog size="large" class="directory-picker-v2">
<DialogHeader>
<DialogTitle>{props.title ?? language.t("command.project.open")}</DialogTitle>
</DialogHeader>
<DividerV2 />
<DialogBody class="directory-picker-v2-body pt-4!">
<div class="directory-picker-v2-path" ref={pathArea}>
<TextInputV2
value={input()}
@ -349,7 +354,7 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
</Show>
</div>
<div class="directory-picker-v2-selection">{policy.result(root(), selected(), rootValid())}</div>
</div>
</DialogBody>
<DialogFooter>
<ButtonV2 variant="neutral" onClick={() => dialog.close()}>
{language.t("common.cancel")}

View file

@ -1,5 +1,6 @@
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Dialog, DialogFooter } from "@opencode-ai/ui/v2/dialog-v2"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { type Component, Show, createEffect, createSignal, onCleanup, onMount } from "solid-js"
@ -52,8 +53,12 @@ export const DialogServerV2: Component<{
}
return (
<Dialog title={title()} fit class="settings-v2-server-dialog">
<div class="flex w-full min-w-0 flex-1 flex-col px-4">
<Dialog fit class="settings-v2-server-dialog">
<DialogHeader hideClose={true}>
<DialogTitle>{title()}</DialogTitle>
</DialogHeader>
<DividerV2 />
<DialogBody class="flex w-full min-w-0 flex-1 flex-col px-4 pt-4 pb-2">
<div class="flex w-full min-w-0 flex-col gap-6">
<div class="flex w-full min-w-0 flex-col gap-2">
<label class="settings-v2-server-dialog-label">{language.t("dialog.server.add.url")}</label>
@ -115,7 +120,7 @@ export const DialogServerV2: Component<{
</div>
</div>
</div>
</div>
</DialogBody>
<DialogFooter>
<ButtonV2 variant="neutral" disabled={controller.formBusy()} onClick={() => dialog.close()}>
{language.t("common.cancel")}

View file

@ -633,7 +633,7 @@
[data-component="dialog-v2"].settings-v2-server-dialog [data-slot="dialog-header"] {
align-items: center;
padding: 24px 24px 0;
padding: 24px 24px 16px;
}
[data-component="dialog-v2"].settings-v2-server-dialog [data-slot="dialog-body"] {

View file

@ -1,7 +1,8 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Dialog } from "@opencode-ai/ui/v2/dialog-v2"
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
@ -30,8 +31,14 @@ export function AddServerMenu(props: { onAddServer: () => void }) {
const language = useLanguage()
const openAddWsl = () => {
dialog.push(() => (
<Dialog title={language.t("wsl.server.add")} size="large" fit class="settings-v2-wsl-dialog">
<DialogAddWslServer />
<Dialog size="large" fit class="settings-v2-wsl-dialog">
<DialogHeader hideClose={true}>
<DialogTitle>{language.t("wsl.server.add")}</DialogTitle>
</DialogHeader>
<DividerV2 />
<DialogBody>
<DialogAddWslServer />
</DialogBody>
</Dialog>
))
}

View file

@ -89,6 +89,30 @@
cursor: not-allowed;
}
/* Danger */
[data-component="button-v2"][data-variant="danger"] {
background-color: var(--v2-background-bg-button-neutral);
color: var(--v2-state-fg-danger);
box-shadow: var(--v2-elevation-button-neutral);
}
[data-component="button-v2"][data-variant="danger"]:is(:hover, [data-state="hover"]):not(:disabled) {
background-image:
linear-gradient(90deg, var(--v2-overlay-simple-overlay-hover) 0%, var(--v2-overlay-simple-overlay-hover) 100%),
linear-gradient(90deg, var(--v2-background-bg-button-neutral) 0%, var(--v2-background-bg-button-neutral) 100%);
}
[data-component="button-v2"][data-variant="danger"]:is(:active, [data-state="pressed"]):not(:disabled) {
background-image:
linear-gradient(90deg, var(--v2-overlay-simple-overlay-pressed) 0%, var(--v2-overlay-simple-overlay-pressed) 100%),
linear-gradient(90deg, var(--v2-background-bg-button-neutral) 0%, var(--v2-background-bg-button-neutral) 100%);
}
[data-component="button-v2"][data-variant="danger"]:is(:disabled, [data-state="disabled"]) {
opacity: 0.5;
cursor: not-allowed;
}
/* Contrast */
[data-component="button-v2"][data-variant="contrast"] {
background-image:

View file

@ -4,7 +4,7 @@ const docs = `### Overview
Button v2 with three visual variants and two sizes.
### API
- \`variant\`: "neutral" | "contrast" | "ghost" | "ghost-muted".
- \`variant\`: "neutral" | "danger" | "contrast" | "ghost" | "ghost-muted".
- \`size\`: "normal" | "large".
- \`icon\`: Optional icon name.
- Inherits Kobalte Button props and native button attributes.
@ -39,7 +39,7 @@ export default {
},
variant: {
control: "select",
options: ["neutral", "contrast", "ghost", "ghost-muted"],
options: ["neutral", "danger", "contrast", "ghost", "ghost-muted"],
},
size: {
control: "select",
@ -61,6 +61,7 @@ export const Variants = {
}}
>
<ButtonV2 variant="neutral">Neutral</ButtonV2>
<ButtonV2 variant="danger">Danger</ButtonV2>
<ButtonV2 variant="contrast">Contrast</ButtonV2>
<ButtonV2 variant="ghost">Ghost</ButtonV2>
<ButtonV2 variant="ghost-muted" icon="edit">
@ -115,7 +116,7 @@ export const Icon = {
export const AllStates = {
render: () => {
const variants = ["neutral", "contrast", "ghost", "ghost-muted"] as const
const variants = ["neutral", "danger", "contrast", "ghost", "ghost-muted"] as const
const states = ["default", "hover", "pressed", "focus", "disabled"] as const
const toTitleCase = (value: string) => value.charAt(0).toUpperCase() + value.slice(1)
return (

View file

@ -7,7 +7,7 @@ export interface ButtonV2Props
extends ComponentProps<typeof Kobalte>,
Pick<ComponentProps<"button">, "class" | "classList" | "children"> {
size?: "small" | "normal" | "large"
variant?: "neutral" | "contrast" | "ghost" | "ghost-muted"
variant?: "neutral" | "danger" | "contrast" | "ghost" | "ghost-muted"
icon?: IconProps["name"]
}

View file

@ -49,34 +49,51 @@
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 16px;
gap: 8px;
flex-shrink: 0;
align-self: stretch;
width: 100%;
gap: 0;
padding: 16px;
flex-shrink: 0;
[data-slot="dialog-header-title"] {
margin: 0;
flex: 1;
min-width: 0;
font-weight: 530;
font-size: 13px;
line-height: 16px;
letter-spacing: -0.04px;
color: var(--v2-text-text-base);
font-variation-settings: "slnt" 0;
}
[data-slot="dialog-title-group"] {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
gap: 4px;
flex: 1;
min-width: 0;
}
[data-slot="dialog-title"] {
margin: 0;
flex: none;
flex-grow: 0;
font-weight: 530;
font-size: 15px;
line-height: 100%;
line-height: 20px;
letter-spacing: -0.13px;
color: var(--v2-text-text-base);
font-variation-settings: "slnt" 0;
}
[data-slot="dialog-description"] {
flex: none;
flex-grow: 0;
font-weight: 440;
font-size: 13px;
line-height: 100%;
line-height: 20px;
letter-spacing: -0.04px;
color: var(--v2-text-text-muted);
font-variation-settings: "slnt" 0;
@ -86,8 +103,11 @@
display: flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
width: 20px;
height: 20px;
margin-top: -2px;
margin-right: -2px;
margin-left: auto;
border-radius: 4px;
flex-shrink: 0;
cursor: pointer;
@ -97,6 +117,10 @@
background: var(--v2-overlay-simple-overlay-hover);
}
}
&[data-hide-close] [data-slot="dialog-close-button"] {
display: none;
}
}
[data-slot="dialog-footer"] {

View file

@ -1,17 +1,17 @@
import { Dialog as KobalteDialog } from "@kobalte/core/dialog"
import { Dialog, DialogFooter } from "./dialog-v2"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle, DialogTitleGroup } from "./dialog-v2"
import { ButtonV2 } from "./button-v2"
const docs = `### Overview
Dialog content wrapper built on Kobalte's dialog primitive with v2 styling.
### API
- Optional: \`title\`, \`description\`, \`action\`.
- \`size\`: normal | large | x-large.
- \`fit\` and \`transition\` control layout and animation.
Compose with \`DialogHeader\`, \`DialogTitle\`, \`DialogTitleGroup\`, \`DialogBody\`, and \`DialogFooter\`.
### Variants and states
- Sizes and optional header/action controls.
### API
- \`Dialog\`: \`size\` (normal | large | x-large), \`variant\`, \`fit\`.
- \`DialogHeader\`: row container with optional \`closeLabel\` and \`hideClose\`.
- \`DialogTitle\`: accessible single-line header title.
- \`DialogTitleGroup\`: column with \`title\` and required \`description\`.
### Accessibility
- Focus trapping and aria attributes provided by Kobalte Dialog.
@ -34,6 +34,14 @@ export default {
},
}
function dialogHeader(title: string, description: string) {
return (
<DialogHeader>
<DialogTitleGroup title={title} description={description} />
</DialogHeader>
)
}
export const Basic = {
render: () => (
<KobalteDialog defaultOpen>
@ -42,8 +50,9 @@ export const Basic = {
</KobalteDialog.Trigger>
<KobalteDialog.Portal>
<KobalteDialog.Overlay />
<Dialog title="Dialog" description="Description">
Dialog body content.
<Dialog>
{dialogHeader("Dialog", "Description")}
<DialogBody>Dialog body content.</DialogBody>
</Dialog>
</KobalteDialog.Portal>
</KobalteDialog>
@ -59,8 +68,9 @@ export const Sizes = {
</KobalteDialog.Trigger>
<KobalteDialog.Portal>
<KobalteDialog.Overlay />
<Dialog title="Normal" description="Normal size">
Normal dialog content.
<Dialog>
{dialogHeader("Normal", "Normal size")}
<DialogBody>Normal dialog content.</DialogBody>
</Dialog>
</KobalteDialog.Portal>
</KobalteDialog>
@ -71,8 +81,9 @@ export const Sizes = {
</KobalteDialog.Trigger>
<KobalteDialog.Portal>
<KobalteDialog.Overlay />
<Dialog size="large" title="Large" description="Large size">
Large dialog content.
<Dialog size="large">
{dialogHeader("Large", "Large size")}
<DialogBody>Large dialog content.</DialogBody>
</Dialog>
</KobalteDialog.Portal>
</KobalteDialog>
@ -83,8 +94,9 @@ export const Sizes = {
</KobalteDialog.Trigger>
<KobalteDialog.Portal>
<KobalteDialog.Overlay />
<Dialog size="x-large" title="Extra large" description="X-large size">
X-large dialog content.
<Dialog size="x-large">
{dialogHeader("Extra large", "X-large size")}
<DialogBody>X-large dialog content.</DialogBody>
</Dialog>
</KobalteDialog.Portal>
</KobalteDialog>
@ -92,24 +104,41 @@ export const Sizes = {
),
}
export const CustomAction = {
export const TitleOnly = {
render: () => (
<KobalteDialog>
<KobalteDialog defaultOpen>
<KobalteDialog.Trigger as={ButtonV2} variant="neutral">
Open action dialog
Open dialog
</KobalteDialog.Trigger>
<KobalteDialog.Portal>
<KobalteDialog.Overlay />
<Dialog
title="Custom action"
description="Dialog with a custom header action"
action={
<Dialog>
<DialogHeader>
<DialogTitle>Open project</DialogTitle>
</DialogHeader>
<DialogBody>Dialog body content.</DialogBody>
</Dialog>
</KobalteDialog.Portal>
</KobalteDialog>
),
}
export const HeaderControls = {
render: () => (
<KobalteDialog>
<KobalteDialog.Trigger as={ButtonV2} variant="neutral">
Open dialog
</KobalteDialog.Trigger>
<KobalteDialog.Portal>
<KobalteDialog.Overlay />
<Dialog>
<DialogHeader>
<DialogTitleGroup title="Custom header" description="Dialog with an extra header control" />
<ButtonV2 variant="neutral" size="small">
Help
</ButtonV2>
}
>
Dialog body content.
</DialogHeader>
<DialogBody>Dialog body content.</DialogBody>
</Dialog>
</KobalteDialog.Portal>
</KobalteDialog>
@ -124,7 +153,8 @@ export const WithFooter = {
</KobalteDialog.Trigger>
<KobalteDialog.Portal>
<KobalteDialog.Overlay />
<Dialog title="Save changes" description="Your changes will be lost if you don't save them." fit>
<Dialog fit>
{dialogHeader("Save changes", "Your changes will be lost if you don't save them.")}
<DialogFooter>
<ButtonV2 variant="neutral">Cancel</ButtonV2>
<ButtonV2 variant="contrast">Save</ButtonV2>
@ -143,7 +173,8 @@ export const WithFooterThreeButtons = {
</KobalteDialog.Trigger>
<KobalteDialog.Portal>
<KobalteDialog.Overlay />
<Dialog title="Unsaved changes" description="You have unsaved changes. What would you like to do?" fit>
<Dialog fit>
{dialogHeader("Unsaved changes", "You have unsaved changes. What would you like to do?")}
<DialogFooter>
<span style={{ "margin-right": "auto" }}>
<ButtonV2 variant="ghost">Remind me later</ButtonV2>
@ -165,8 +196,9 @@ export const Fit = {
</KobalteDialog.Trigger>
<KobalteDialog.Portal>
<KobalteDialog.Overlay />
<Dialog title="Fit content" fit>
Dialog fits its content.
<Dialog fit>
{dialogHeader("Fit content", "Dialog fits its content.")}
<DialogBody>Dialog fits its content.</DialogBody>
</Dialog>
</KobalteDialog.Portal>
</KobalteDialog>

View file

@ -3,9 +3,6 @@ import { type ComponentProps, type JSXElement, type ParentProps, Show, children,
import "./dialog-v2.css"
export interface DialogProps extends ParentProps {
title?: JSXElement
description?: JSXElement
action?: JSXElement
size?: "normal" | "large" | "x-large"
variant?: "default" | "settings"
class?: ComponentProps<"div">["class"]
@ -13,26 +10,76 @@ export interface DialogProps extends ParentProps {
fit?: boolean
}
export interface DialogHeaderProps extends ParentProps {
closeLabel?: string
hideClose?: boolean
}
export interface DialogTitleGroupProps {
title?: JSXElement
description: JSXElement
}
export function DialogFooter(props: ParentProps) {
return <div data-slot="dialog-footer">{props.children}</div>
}
export function DialogBody(props: ParentProps & { class?: ComponentProps<"div">["class"] }) {
const [local] = splitProps(props, ["class", "children"])
return (
<div data-slot="dialog-body" class={local.class}>
{local.children}
</div>
)
}
export function DialogTitle(props: ParentProps) {
return <Kobalte.Title data-slot="dialog-header-title">{props.children}</Kobalte.Title>
}
export function DialogTitleGroup(props: DialogTitleGroupProps) {
const title = children(() => props.title)
const description = children(() => props.description)
return (
<div data-slot="dialog-title-group">
<Show when={title()}>{(t) => <Kobalte.Title data-slot="dialog-title">{t()}</Kobalte.Title>}</Show>
<Kobalte.Description data-slot="dialog-description">{description()}</Kobalte.Description>
</div>
)
}
export function DialogHeader(props: DialogHeaderProps) {
const [local] = splitProps(props, ["closeLabel", "hideClose", "children"])
const hideClose = () => local.hideClose === true
return (
<div data-slot="dialog-header" data-hide-close={hideClose() ? "" : undefined}>
{local.children}
{!hideClose() && (
<Kobalte.CloseButton data-slot="dialog-close-button" aria-label={local.closeLabel ?? "Close"}>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M12.4446 3.55469L3.55566 12.4436M3.55566 3.55469L12.4446 12.4436"
stroke="currentColor"
stroke-linejoin="round"
/>
</svg>
</Kobalte.CloseButton>
)}
</div>
)
}
export function Dialog(props: DialogProps) {
const [local] = splitProps(props, [
"title",
"description",
"action",
"size",
"variant",
"class",
"classList",
"fit",
"children",
])
const title = children(() => local.title)
const description = children(() => local.description)
const action = children(() => local.action)
const hasHeader = () => title() || action()
const [local] = splitProps(props, ["size", "variant", "class", "classList", "fit", "children"])
return (
<div
@ -44,7 +91,6 @@ export function Dialog(props: DialogProps) {
<div data-slot="dialog-container">
<Kobalte.Content
data-slot="dialog-content"
data-no-header={!hasHeader() ? "" : undefined}
classList={{
...local.classList,
[local.class ?? ""]: !!local.class,
@ -58,34 +104,7 @@ export function Dialog(props: DialogProps) {
}
}}
>
<Show when={hasHeader()}>
<div data-slot="dialog-header">
<div data-slot="dialog-title-group">
<Show when={title()}>{(t) => <Kobalte.Title data-slot="dialog-title">{t()}</Kobalte.Title>}</Show>
<Show when={description()}>
{(d) => <Kobalte.Description data-slot="dialog-description">{d()}</Kobalte.Description>}
</Show>
</div>
<Show when={action()}>{(a) => a()}</Show>
<Kobalte.CloseButton data-slot="dialog-close-button" aria-label="Close">
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M12.4446 3.55469L3.55566 12.4436M3.55566 3.55469L12.4446 12.4436"
stroke="currentColor"
stroke-linejoin="round"
/>
</svg>
</Kobalte.CloseButton>
</div>
</Show>
<div data-slot="dialog-body">{local.children}</div>
{local.children}
</Kobalte.Content>
</div>
</div>

View file

@ -0,0 +1,11 @@
[data-component="divider-v2"] {
box-sizing: border-box;
width: 100%;
height: 0.5px;
margin-block: 0.25px;
border: none;
background: var(--v2-border-border-strong);
flex: none;
align-self: stretch;
flex-grow: 0;
}

View file

@ -0,0 +1,38 @@
import { DividerV2 } from "./divider-v2"
const docs = `### Overview
Horizontal hairline divider for v2 layouts.
### API
- Inherits native div attributes.
- Stretches to full width of its flex parent.
### Theming/tokens
- Uses \`data-component="divider-v2"\`.
- Border color: \`--v2-border-border-strong\`.
`
export default {
title: "UI V2/Divider",
id: "components-divider-v2",
component: DividerV2,
tags: ["autodocs"],
parameters: {
frameWidth: "320px",
docs: {
description: {
component: docs,
},
},
},
}
export const Basic = {
render: () => (
<div style={{ display: "flex", "flex-direction": "column", gap: "16px", padding: "16px" }}>
<span>Above</span>
<DividerV2 />
<span>Below</span>
</div>
),
}

View file

@ -0,0 +1,20 @@
import { type ComponentProps, splitProps } from "solid-js"
import "./divider-v2.css"
export interface DividerV2Props extends ComponentProps<"div"> {}
export function DividerV2(props: DividerV2Props) {
const [local, rest] = splitProps(props, ["class", "classList"])
return (
<div
{...rest}
role="separator"
aria-orientation="horizontal"
data-component="divider-v2"
classList={{
...local.classList,
[local.class ?? ""]: !!local.class,
}}
/>
)
}