feat(app): add desktop v2 home, session entry, and titlebar (#28442)

Co-authored-by: Brendan Allan <git@brendonovich.dev>
This commit is contained in:
Luke Parker
2026-05-21 15:48:13 +10:00
committed by GitHub
parent 6602341c0d
commit b207e32249
26 changed files with 2065 additions and 633 deletions

View File

@@ -1,4 +1,4 @@
import { ComponentProps } from "solid-js"
import { type ComponentProps } from "solid-js"
export const Mark = (props: { class?: string }) => {
return (

View File

@@ -232,4 +232,53 @@
--color-border-color: var(--border-color);
--color-button-ghost-hover: var(--button-ghost-hover);
--color-button-ghost-hover2: var(--button-ghost-hover2);
}
--color-v2-background-bg-base: var(--v2-background-bg-base);
--color-v2-background-bg-deep: var(--v2-background-bg-deep);
--color-v2-background-bg-layer-01: var(--v2-background-bg-layer-01);
--color-v2-background-bg-layer-02: var(--v2-background-bg-layer-02);
--color-v2-background-bg-layer-03: var(--v2-background-bg-layer-03);
--color-v2-background-bg-inverse: var(--v2-background-bg-inverse);
--color-v2-background-bg-contrast: var(--v2-background-bg-contrast);
--color-v2-background-bg-button-neutral: var(--v2-background-bg-button-neutral);
--color-v2-background-bg-accent: var(--v2-background-bg-accent);
--color-v2-text-text-base: var(--v2-text-text-base);
--color-v2-text-text-muted: var(--v2-text-text-muted);
--color-v2-text-text-faint: var(--v2-text-text-faint);
--color-v2-text-text-inverse: var(--v2-text-text-inverse);
--color-v2-text-text-contrast: var(--v2-text-text-contrast);
--color-v2-text-text-accent: var(--v2-text-text-accent);
--color-v2-text-text-accent-hover: var(--v2-text-text-accent-hover);
--color-v2-icon-icon-base: var(--v2-icon-icon-base);
--color-v2-icon-icon-muted: var(--v2-icon-icon-muted);
--color-v2-icon-icon-inverse: var(--v2-icon-icon-inverse);
--color-v2-icon-icon-contrast: var(--v2-icon-icon-contrast);
--color-v2-icon-icon-accent: var(--v2-icon-icon-accent);
--color-v2-icon-icon-accent-hover: var(--v2-icon-icon-accent-hover);
--color-v2-border-border-muted: var(--v2-border-border-muted);
--color-v2-border-border-base: var(--v2-border-border-base);
--color-v2-border-border-strong: var(--v2-border-border-strong);
--color-v2-border-border-inverse: var(--v2-border-border-inverse);
--color-v2-border-border-focus: var(--v2-border-border-focus);
--color-v2-overlay-simple-overlay-hover: var(--v2-overlay-simple-overlay-hover);
--color-v2-overlay-simple-overlay-pressed: var(--v2-overlay-simple-overlay-pressed);
--color-v2-overlay-simple-overlay-contrast-hover: var(--v2-overlay-simple-overlay-contrast-hover);
--color-v2-overlay-simple-overlay-contrast-pressed: var(--v2-overlay-simple-overlay-contrast-pressed);
--color-v2-overlay-simple-overlay-scrim: var(--v2-overlay-simple-overlay-scrim);
--color-v2-overlay-gradient-depth-overlay-depth-top: var(--v2-overlay-gradient-depth-overlay-depth-top);
--color-v2-overlay-gradient-depth-overlay-depth-bot: var(--v2-overlay-gradient-depth-overlay-depth-bot);
--color-v2-overlay-simple-tab-active-scrim: var(--v2-overlay-simple-tab-active-scrim);
--color-v2-overlay-simple-tab-hover-scrim: var(--v2-overlay-simple-tab-hover-scrim);
--color-v2-overlay-simple-tab-scrim: var(--v2-overlay-simple-tab-scrim);
--color-v2-state-bg-success: var(--v2-state-bg-success);
--color-v2-state-fg-success: var(--v2-state-fg-success);
--color-v2-state-border-success: var(--v2-state-border-success);
--color-v2-state-bg-warning: var(--v2-state-bg-warning);
--color-v2-state-fg-warning: var(--v2-state-fg-warning);
--color-v2-state-border-warning: var(--v2-state-border-warning);
--color-v2-state-bg-danger: var(--v2-state-bg-danger);
--color-v2-state-fg-danger: var(--v2-state-fg-danger);
--color-v2-state-border-danger: var(--v2-state-border-danger);
--color-v2-state-bg-info: var(--v2-state-bg-info);
--color-v2-state-fg-info: var(--v2-state-fg-info);
--color-v2-state-border-info: var(--v2-state-border-info);
}

View File

@@ -1,6 +1,6 @@
[data-component="avatar"] {
--avatar-bg: var(--background-bg-layer-02);
--avatar-fg: var(--text-text-muted);
--avatar-bg: var(--v2-background-bg-layer-02);
--avatar-fg: var(--v2-text-text-muted);
--avatar-radius: 9999px;
--avatar-font-size: 11px;
--avatar-tracking: 0.05px;
@@ -13,8 +13,8 @@
width: 28px;
height: 28px;
border-radius: var(--avatar-radius);
border: 0.5px solid var(--border-border-base);
font-family: var(--font-sans);
border: 0.5px solid var(--v2-border-border-base);
font-family: var(--font-family-sans);
font-weight: 530;
font-size: var(--avatar-font-size);
line-height: 1;
@@ -30,7 +30,7 @@
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, var(--alpha-light-16) 0%, var(--alpha-light-0) 100%);
background: linear-gradient(180deg, var(--v2-alpha-light-16) 0%, var(--v2-alpha-light-0) 100%);
pointer-events: none;
}

View File

@@ -16,7 +16,7 @@
font-weight: 530;
font-size: 13px;
line-height: 1;
color: var(--text-text-base);
color: var(--v2-text-text-base);
text-shadow: none;
font-variant-numeric: tabular-nums;
letter-spacing: -0.04px;
@@ -30,7 +30,7 @@
}
[data-component="button-v2"]:is(:focus-visible, [data-state="focus"]):not(:disabled) {
outline: 2px solid var(--border-border-focus);
outline: 2px solid var(--v2-border-border-focus);
outline-offset: 2.5px;
}
@@ -68,21 +68,21 @@
/* Neutral */
[data-component="button-v2"][data-variant="neutral"] {
background-color: var(--background-bg-button-neutral);
color: var(--text-text-base);
box-shadow: var(--elevation-button-neutral);
background-color: var(--v2-background-bg-button-neutral);
color: var(--v2-text-text-base);
box-shadow: var(--v2-elevation-button-neutral);
}
[data-component="button-v2"][data-variant="neutral"]:is(:hover, [data-state="hover"]):not(:disabled) {
background-image:
linear-gradient(90deg, var(--overlay-simple-overlay-hover) 0%, var(--overlay-simple-overlay-hover) 100%),
linear-gradient(90deg, var(--background-bg-button-neutral) 0%, var(--background-bg-button-neutral) 100%);
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="neutral"]:is(:active, [data-state="pressed"]):not(:disabled) {
background-image:
linear-gradient(90deg, var(--overlay-simple-overlay-pressed) 0%, var(--overlay-simple-overlay-pressed) 100%),
linear-gradient(90deg, var(--background-bg-button-neutral) 0%, var(--background-bg-button-neutral) 100%);
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="neutral"]:is(:disabled, [data-state="disabled"]) {
@@ -93,33 +93,33 @@
/* Contrast */
[data-component="button-v2"][data-variant="contrast"] {
background-image:
linear-gradient(180deg, var(--alpha-light-20) 0%, var(--alpha-light-0) 100%),
linear-gradient(90deg, var(--background-bg-contrast) 0%, var(--background-bg-contrast) 100%);
color: var(--text-text-contrast);
linear-gradient(180deg, var(--v2-alpha-light-20) 0%, var(--v2-alpha-light-0) 100%),
linear-gradient(90deg, var(--v2-background-bg-contrast) 0%, var(--v2-background-bg-contrast) 100%);
color: var(--v2-text-text-contrast);
text-shadow: 0 0.5px 0.5px rgba(0, 0, 0, 0.3);
box-shadow: var(--elevation-button-contrast);
box-shadow: var(--v2-elevation-button-contrast);
}
[data-component="button-v2"][data-variant="contrast"]:is(:hover, [data-state="hover"]):not(:disabled) {
background-image:
linear-gradient(
90deg,
var(--overlay-simple-overlay-contrast-hover) 0%,
var(--overlay-simple-overlay-contrast-hover) 100%
var(--v2-overlay-simple-overlay-contrast-hover) 0%,
var(--v2-overlay-simple-overlay-contrast-hover) 100%
),
linear-gradient(180deg, var(--alpha-light-20) 0%, var(--alpha-light-0) 100%),
linear-gradient(90deg, var(--background-bg-contrast) 0%, var(--background-bg-contrast) 100%);
linear-gradient(180deg, var(--v2-alpha-light-20) 0%, var(--v2-alpha-light-0) 100%),
linear-gradient(90deg, var(--v2-background-bg-contrast) 0%, var(--v2-background-bg-contrast) 100%);
}
[data-component="button-v2"][data-variant="contrast"]:is(:active, [data-state="pressed"]):not(:disabled) {
background-image:
linear-gradient(
90deg,
var(--overlay-simple-overlay-contrast-pressed) 0%,
var(--overlay-simple-overlay-contrast-pressed) 100%
var(--v2-overlay-simple-overlay-contrast-pressed) 0%,
var(--v2-overlay-simple-overlay-contrast-pressed) 100%
),
linear-gradient(180deg, var(--alpha-light-20) 0%, var(--alpha-light-0) 100%),
linear-gradient(90deg, var(--background-bg-contrast) 0%, var(--background-bg-contrast) 100%);
linear-gradient(180deg, var(--v2-alpha-light-20) 0%, var(--v2-alpha-light-0) 100%),
linear-gradient(90deg, var(--v2-background-bg-contrast) 0%, var(--v2-background-bg-contrast) 100%);
}
[data-component="button-v2"][data-variant="contrast"]:is(:disabled, [data-state="disabled"]) {
@@ -130,15 +130,15 @@
/* Ghost */
[data-component="button-v2"][data-variant="ghost"] {
background-color: transparent;
color: var(--text-text-base);
color: var(--v2-text-text-base);
}
[data-component="button-v2"][data-variant="ghost"]:is(:hover, [data-state="hover"]):not(:disabled) {
background-color: var(--overlay-simple-overlay-hover);
background-color: var(--v2-overlay-simple-overlay-hover);
}
[data-component="button-v2"][data-variant="ghost"]:is(:active, [data-state="pressed"]):not(:disabled) {
background-color: var(--overlay-simple-overlay-pressed);
background-color: var(--v2-overlay-simple-overlay-pressed);
}
[data-component="button-v2"][data-variant="ghost"]:is(:disabled, [data-state="disabled"]) {

View File

@@ -1,29 +1,94 @@
import { type ComponentProps, splitProps } from "solid-js"
import { onMount, type ComponentProps, splitProps } from "solid-js"
const icons = {
edit: {
viewBox: "0 0 20 20",
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"/>`,
},
"folder-add-left": {
viewBox: "0 0 20 20",
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"/>`,
},
"grid-plus": {
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"/>`,
},
help: {
viewBox: "0 0 20 20",
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"/>`,
},
"magnifying-glass": {
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"/>`,
},
menu: {
viewBox: "0 0 16 16",
body: `<path d="M2 8H14M2 4.664H14M2 11.336H14" stroke="currentColor"/>`,
},
plus: {
viewBox: "0 0 16 16",
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": {
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"/>`,
},
"xmark-small": {
viewBox: "0 0 16 16",
body: `<path d="M4.25 11.75L11.75 4.25M11.75 11.75L4.25 4.25" stroke="currentColor"/>`,
},
}
const spriteID = "opencode-v2-icon-sprite"
const symbol = (name: keyof typeof icons) => `opencode-v2-icon-${name}`
let spriteInserted = false
function ensureSprite() {
if (spriteInserted) return
if (typeof document === "undefined") return
if (document.getElementById(spriteID)) {
spriteInserted = true
return
}
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
svg.id = spriteID
svg.setAttribute("aria-hidden", "true")
svg.setAttribute("width", "0")
svg.setAttribute("height", "0")
svg.style.position = "absolute"
svg.style.overflow = "hidden"
svg.innerHTML = Object.entries(icons)
.map(([name, icon]) => `<symbol id="${symbol(name as keyof typeof icons)}" viewBox="${icon.viewBox}">${icon.body}</symbol>`)
.join("")
document.body.insertBefore(svg, document.body.firstChild)
spriteInserted = true
}
export interface IconProps extends ComponentProps<"svg"> {
name: string
name: keyof typeof icons | (string & {})
size?: "small" | "normal" | "large"
}
/**
* Placeholder icon component
*/
export function Icon(props: IconProps) {
const [split, rest] = splitProps(props, ["name", "size"])
const iconName = () => (icons[split.name as keyof typeof icons] ? (split.name as keyof typeof icons) : "plus")
const icon = () => icons[iconName()]
const pixelSize = split.size === "small" ? 14 : split.size === "large" ? 20 : 16
onMount(ensureSprite)
return (
<svg
{...rest}
data-slot="icon-svg"
width={pixelSize}
height={pixelSize}
viewBox="0 0 16 16"
viewBox={icon().viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden={rest["aria-hidden"] ?? "true"}
>
<path d="M8 2.88867V13.1109" stroke="currentColor" stroke-linejoin="round" />
<path d="M2.88867 8H13.1109" stroke="currentColor" stroke-linejoin="round" />
<use href={`#${symbol(iconName())}`} />
</svg>
)
}

View File

@@ -42,7 +42,7 @@ const CheckSmall = () => (
export type SelectV2Props<T> = Omit<
ComponentProps<typeof Kobalte<T, { category: string; options: T[] }>>,
"value" | "onChange" | "children" | "options" | "itemComponent" | "sectionComponent" | "defaultValue" | "multiple"
"value" | "onSelect" | "children" | "options" | "itemComponent" | "sectionComponent" | "defaultValue" | "multiple"
> & {
placeholder?: string
options: T[]
@@ -58,6 +58,7 @@ export type SelectV2Props<T> = Omit<
invalid?: boolean
numeric?: boolean
children?: (item: T) => JSX.Element
valueClass?: string
}
export function SelectV2<T>(props: SelectV2Props<T>) {
@@ -78,6 +79,7 @@ export function SelectV2<T>(props: SelectV2Props<T>) {
"invalid",
"numeric",
"disabled",
"valueClass",
])
const state: { key?: string; cleanup?: void | (() => void) } = {}
@@ -172,7 +174,7 @@ export function SelectV2<T>(props: SelectV2Props<T>) {
}}
>
<div data-slot="select-v2-value">
<Kobalte.Value<T> data-slot="select-v2-value-text">
<Kobalte.Value<T> data-slot="select-v2-value-text" class={local.valueClass}>
{(st) => {
const selected = st.selectedOption()
if (local.label && selected != null) return local.label(selected)

View File

@@ -0,0 +1,92 @@
import { createUniqueId, type ComponentProps } from "solid-js"
export function WordmarkV2(props: Pick<ComponentProps<"svg">, "class">) {
const filter = createUniqueId()
const mask = createUniqueId()
const maskGradient = createUniqueId()
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 720.002 129.001"
fill="none"
preserveAspectRatio="none"
classList={{ [props.class ?? ""]: !!props.class }}
>
<g opacity="0.16" filter={`url(#${filter})`} mask={`url(#${mask})`}>
<path
opacity="0.7"
d="M55.3846 36.8583H18.4615V92.144H55.3846V36.8583ZM73.8462 110.573H0V18.4297H73.8462V110.573Z"
fill="currentColor"
/>
<path
opacity="0.7"
d="M110.774 92.144H147.697V36.8583H110.774V92.144ZM166.159 110.573H110.774V129.001H92.3125V18.4297H166.159V110.573Z"
fill="currentColor"
/>
<path
opacity="0.7"
d="M258.463 73.7154H203.079V92.144H258.463V110.573H184.617V18.4297H258.463V73.7154ZM203.079 55.2868H240.002V36.8583H203.079V55.2868Z"
fill="currentColor"
/>
<path
opacity="0.7"
d="M332.306 36.8583H295.383V110.573H276.922V18.4297H332.306V36.8583ZM350.768 110.573H332.306V36.8583H350.768V110.573Z"
fill="currentColor"
/>
<path
opacity="0.7"
d="M443.081 36.8583H387.696V92.144H443.081V110.573H369.234V18.4297H443.081V36.8583Z"
fill="currentColor"
/>
<path
opacity="0.7"
d="M516.924 36.8583H480.001V92.144H516.924V36.8583ZM535.385 110.573H461.539V18.4297H535.385V110.573Z"
fill="currentColor"
/>
<path
opacity="0.7"
d="M609.228 36.8571H572.305V92.1429H609.228V36.8571ZM627.69 110.571H553.844V18.4286H609.228V0H627.69V110.571Z"
fill="currentColor"
/>
<path
opacity="0.7"
d="M664.618 36.8583V55.2868H701.541V36.8583H664.618ZM720.002 73.7154H664.618V92.144H720.002V110.573H646.156V18.4297H720.002V73.7154Z"
fill="currentColor"
/>
</g>
<defs>
<mask id={mask} maskUnits="userSpaceOnUse" x="0" y="0" width="720" height="129">
<rect width="720" height="129" fill={`url(#${maskGradient})`} />
</mask>
<linearGradient id={maskGradient} x1="360" y1="0" x2="360" y2="112" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.7" />
<stop offset="1" stop-color="white" stop-opacity="0" />
</linearGradient>
<filter
id={filter}
x="0"
y="0"
width="720.002"
height="130.001"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="1" />
<feGaussianBlur stdDeviation="1" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" />
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_4938_16028" />
</filter>
</defs>
</svg>
)
}