fix(app): session screen accessibility improvements (#9907)
This commit is contained in:
@@ -143,7 +143,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog title={<IconButton tabIndex={-1} icon="arrow-left" variant="ghost" onClick={goBack} />}>
|
<Dialog title={<IconButton tabIndex={-1} icon="arrow-left" variant="ghost" onClick={goBack} aria-label="Go back" />}>
|
||||||
<div class="flex flex-col gap-6 px-2.5 pb-3">
|
<div class="flex flex-col gap-6 px-2.5 pb-3">
|
||||||
<div class="px-2.5 flex gap-4 items-center">
|
<div class="px-2.5 flex gap-4 items-center">
|
||||||
<ProviderIcon id={props.provider as IconName} class="size-5 shrink-0 icon-strong-base" />
|
<ProviderIcon id={props.provider as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||||
|
|||||||
@@ -193,6 +193,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
{(color) => (
|
{(color) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-label={`Select ${color} color`}
|
||||||
|
aria-pressed={store.color === color}
|
||||||
classList={{
|
classList={{
|
||||||
"flex items-center justify-center size-10 p-0.5 rounded-lg overflow-hidden transition-colors cursor-default": true,
|
"flex items-center justify-center size-10 p-0.5 rounded-lg overflow-hidden transition-colors cursor-default": true,
|
||||||
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover":
|
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover":
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Popover as Kobalte } from "@kobalte/core/popover"
|
import { Popover as Kobalte } from "@kobalte/core/popover"
|
||||||
import { Component, createMemo, createSignal, JSX, Show } from "solid-js"
|
import { Component, ComponentProps, createMemo, createSignal, JSX, Show, ValidComponent } from "solid-js"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { popularProviders } from "@/hooks/use-providers"
|
import { popularProviders } from "@/hooks/use-providers"
|
||||||
@@ -86,10 +86,12 @@ const ModelList: Component<{
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModelSelectorPopover: Component<{
|
export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
||||||
provider?: string
|
provider?: string
|
||||||
children: JSX.Element
|
children?: JSX.Element
|
||||||
}> = (props) => {
|
triggerAs?: T
|
||||||
|
triggerProps?: ComponentProps<T>
|
||||||
|
}) {
|
||||||
const [open, setOpen] = createSignal(false)
|
const [open, setOpen] = createSignal(false)
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
||||||
@@ -101,7 +103,9 @@ export const ModelSelectorPopover: Component<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
|
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
|
||||||
<Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger>
|
<Kobalte.Trigger as={props.triggerAs ?? "div"} {...(props.triggerProps as any)}>
|
||||||
|
{props.children}
|
||||||
|
</Kobalte.Trigger>
|
||||||
<Kobalte.Portal>
|
<Kobalte.Portal>
|
||||||
<Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
|
<Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
|
||||||
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
|
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ export function DialogSelectServer() {
|
|||||||
icon="circle-x"
|
icon="circle-x"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="bg-transparent transition-opacity shrink-0 hover:scale-110"
|
class="bg-transparent transition-opacity shrink-0 hover:scale-110"
|
||||||
|
aria-label="Remove server"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
handleRemove(i)
|
handleRemove(i)
|
||||||
|
|||||||
@@ -1487,6 +1487,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
onClick={() => prompt.context.removeActive()}
|
onClick={() => prompt.context.removeActive()}
|
||||||
|
aria-label="Remove active file from context"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1524,6 +1525,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
onClick={() => prompt.context.remove(item.key)}
|
onClick={() => prompt.context.remove(item.key)}
|
||||||
|
aria-label="Remove file from context"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1556,6 +1558,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeImageAttachment(attachment.id)}
|
onClick={() => removeImageAttachment(attachment.id)}
|
||||||
class="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-stronger-non-alpha border border-border-base flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-surface-raised-base-hover"
|
class="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-stronger-non-alpha border border-border-base flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-surface-raised-base-hover"
|
||||||
|
aria-label="Remove attachment"
|
||||||
>
|
>
|
||||||
<Icon name="close" class="size-3 text-text-weak" />
|
<Icon name="close" class="size-3 text-text-weak" />
|
||||||
</button>
|
</button>
|
||||||
@@ -1574,6 +1577,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
editorRef = el
|
editorRef = el
|
||||||
props.ref?.(el)
|
props.ref?.(el)
|
||||||
}}
|
}}
|
||||||
|
role="textbox"
|
||||||
|
aria-multiline="true"
|
||||||
|
aria-label={
|
||||||
|
store.mode === "shell"
|
||||||
|
? language.t("prompt.placeholder.shell")
|
||||||
|
: language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })
|
||||||
|
}
|
||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
onInput={handleInput}
|
onInput={handleInput}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
@@ -1638,21 +1648,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ModelSelectorPopover>
|
<TooltipKeybind
|
||||||
<TooltipKeybind
|
placement="top"
|
||||||
placement="top"
|
title={language.t("command.model.choose")}
|
||||||
title={language.t("command.model.choose")}
|
keybind={command.keybind("model.choose")}
|
||||||
keybind={command.keybind("model.choose")}
|
>
|
||||||
|
<ModelSelectorPopover
|
||||||
|
triggerAs={Button}
|
||||||
|
triggerProps={{ variant: "ghost" }}
|
||||||
>
|
>
|
||||||
<Button as="div" variant="ghost">
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
<Show when={local.model.current()?.provider?.id}>
|
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
|
||||||
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
|
</Show>
|
||||||
</Show>
|
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
<Icon name="chevron-down" size="small" />
|
||||||
<Icon name="chevron-down" size="small" />
|
</ModelSelectorPopover>
|
||||||
</Button>
|
</TooltipKeybind>
|
||||||
</TooltipKeybind>
|
|
||||||
</ModelSelectorPopover>
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={local.model.variant.list().length > 0}>
|
<Show when={local.model.variant.list().length > 0}>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
@@ -1683,6 +1694,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
"text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
|
"text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
|
||||||
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
|
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
|
||||||
}}
|
}}
|
||||||
|
aria-label="Toggle auto-accept permissions"
|
||||||
|
aria-pressed={permission.isAutoAccepting(params.id!, sdk.directory)}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name="chevron-double-right"
|
name="chevron-double-right"
|
||||||
@@ -1711,7 +1724,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<SessionContextUsage />
|
<SessionContextUsage />
|
||||||
<Show when={store.mode === "normal"}>
|
<Show when={store.mode === "normal"}>
|
||||||
<Tooltip placement="top" value={language.t("prompt.action.attachFile")}>
|
<Tooltip placement="top" value={language.t("prompt.action.attachFile")}>
|
||||||
<Button type="button" variant="ghost" class="size-6" onClick={() => fileInputRef.click()}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
class="size-6"
|
||||||
|
onClick={() => fileInputRef.click()}
|
||||||
|
aria-label="Attach file"
|
||||||
|
>
|
||||||
<Icon name="photo" class="size-4.5" />
|
<Icon name="photo" class="size-4.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -1743,6 +1762,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
icon={working() ? "stop" : "arrow-up"}
|
icon={working() ? "stop" : "arrow-up"}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
class="h-6 w-4.5"
|
class="h-6 w-4.5"
|
||||||
|
aria-label={working() ? "Stop" : "Send message"}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Match when={variant() === "indicator"}>{circle()}</Match>
|
<Match when={variant() === "indicator"}>{circle()}</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<Button type="button" variant="ghost" class="size-6" onClick={openContext}>
|
<Button type="button" variant="ghost" class="size-6" onClick={openContext} aria-label="View context usage">
|
||||||
{circle()}
|
{circle()}
|
||||||
</Button>
|
</Button>
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ export function SessionHeader() {
|
|||||||
type="button"
|
type="button"
|
||||||
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||||
onClick={() => command.trigger("file.open")}
|
onClick={() => command.trigger("file.open")}
|
||||||
|
aria-label="Search files"
|
||||||
>
|
>
|
||||||
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-visible">
|
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-visible">
|
||||||
<Icon name="magnifying-glass" size="normal" class="icon-base shrink-0" />
|
<Icon name="magnifying-glass" size="normal" class="icon-base shrink-0" />
|
||||||
@@ -184,6 +185,10 @@ export function SessionHeader() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="group/review-toggle size-6 p-0"
|
class="group/review-toggle size-6 p-0"
|
||||||
onClick={() => view().reviewPanel.toggle()}
|
onClick={() => view().reviewPanel.toggle()}
|
||||||
|
aria-label="Toggle review panel"
|
||||||
|
aria-expanded={view().reviewPanel.opened()}
|
||||||
|
aria-controls="review-panel"
|
||||||
|
tabIndex={showReview() ? 0 : -1}
|
||||||
>
|
>
|
||||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||||
<Icon
|
<Icon
|
||||||
@@ -214,6 +219,9 @@ export function SessionHeader() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="group/terminal-toggle size-6 p-0"
|
class="group/terminal-toggle size-6 p-0"
|
||||||
onClick={() => view().terminal.toggle()}
|
onClick={() => view().terminal.toggle()}
|
||||||
|
aria-label="Toggle terminal"
|
||||||
|
aria-expanded={view().terminal.opened()}
|
||||||
|
aria-controls="terminal-panel"
|
||||||
>
|
>
|
||||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||||
<Icon
|
<Icon
|
||||||
@@ -235,32 +243,23 @@ export function SessionHeader() {
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<Show when={showShare()}>
|
||||||
class="flex items-center"
|
<div class="flex items-center">
|
||||||
classList={{
|
<Popover
|
||||||
"opacity-0 pointer-events-none": !showShare(),
|
title={language.t("session.share.popover.title")}
|
||||||
}}
|
description={
|
||||||
aria-hidden={!showShare()}
|
shareUrl()
|
||||||
>
|
? language.t("session.share.popover.description.shared")
|
||||||
<Popover
|
: language.t("session.share.popover.description.unshared")
|
||||||
title={language.t("session.share.popover.title")}
|
}
|
||||||
description={
|
triggerAs={Button}
|
||||||
shareUrl()
|
triggerProps={{
|
||||||
? language.t("session.share.popover.description.shared")
|
variant: "secondary",
|
||||||
: language.t("session.share.popover.description.unshared")
|
classList: { "rounded-r-none": shareUrl() !== undefined },
|
||||||
}
|
style: { scale: 1 },
|
||||||
trigger={
|
}}
|
||||||
<Tooltip class="shrink-0" value={language.t("command.session.share")}>
|
trigger={language.t("session.share.action.share")}
|
||||||
<Button
|
>
|
||||||
variant="secondary"
|
|
||||||
classList={{ "rounded-r-none": shareUrl() !== undefined }}
|
|
||||||
style={{ scale: 1 }}
|
|
||||||
>
|
|
||||||
{language.t("session.share.action.share")}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<Show
|
<Show
|
||||||
when={shareUrl()}
|
when={shareUrl()}
|
||||||
@@ -322,10 +321,12 @@ export function SessionHeader() {
|
|||||||
class="rounded-l-none"
|
class="rounded-l-none"
|
||||||
onClick={copyLink}
|
onClick={copyLink}
|
||||||
disabled={state.unshare}
|
disabled={state.unshare}
|
||||||
|
aria-label="Copy share link"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
|
|||||||
value={props.tab}
|
value={props.tab}
|
||||||
closeButton={
|
closeButton={
|
||||||
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
||||||
<IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} />
|
<IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} aria-label="Close tab" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
hideCloseButton
|
hideCloseButton
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
close()
|
close()
|
||||||
}}
|
}}
|
||||||
|
aria-label="Close terminal"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1916,6 +1916,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
"bg-surface-base-hover border border-border-weak-base": !selected() && open(),
|
"bg-surface-base-hover border border-border-weak-base": !selected() && open(),
|
||||||
}}
|
}}
|
||||||
onClick={() => navigateToProject(props.project.worktree)}
|
onClick={() => navigateToProject(props.project.worktree)}
|
||||||
|
onBlur={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
<ProjectIcon project={props.project} notify />
|
<ProjectIcon project={props.project} notify />
|
||||||
</button>
|
</button>
|
||||||
@@ -2343,7 +2344,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
<div class="relative bg-background-base flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
<div class="relative bg-background-base flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||||
<Titlebar />
|
<Titlebar />
|
||||||
<div class="flex-1 min-h-0 flex">
|
<div class="flex-1 min-h-0 flex">
|
||||||
<div
|
<nav
|
||||||
|
aria-label="Projects and sessions"
|
||||||
classList={{
|
classList={{
|
||||||
"hidden xl:block": true,
|
"hidden xl:block": true,
|
||||||
"relative shrink-0": true,
|
"relative shrink-0": true,
|
||||||
@@ -2364,7 +2366,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
onCollapse={layout.sidebar.close}
|
onCollapse={layout.sidebar.close}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</nav>
|
||||||
<div class="xl:hidden">
|
<div class="xl:hidden">
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
@@ -2376,7 +2378,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
|
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<nav
|
||||||
|
aria-label="Projects and sessions"
|
||||||
classList={{
|
classList={{
|
||||||
"@container fixed top-10 bottom-0 left-0 z-50 w-72 bg-background-base transition-transform duration-200 ease-out": true,
|
"@container fixed top-10 bottom-0 left-0 z-50 w-72 bg-background-base transition-transform duration-200 ease-out": true,
|
||||||
"translate-x-0": layout.mobileSidebar.opened(),
|
"translate-x-0": layout.mobileSidebar.opened(),
|
||||||
@@ -2385,7 +2388,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<SidebarContent mobile />
|
<SidebarContent mobile />
|
||||||
</div>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main
|
<main
|
||||||
|
|||||||
@@ -782,7 +782,7 @@ export default function Page() {
|
|||||||
const activeElement = document.activeElement as HTMLElement | undefined
|
const activeElement = document.activeElement as HTMLElement | undefined
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
const isProtected = activeElement.closest("[data-prevent-autofocus]")
|
const isProtected = activeElement.closest("[data-prevent-autofocus]")
|
||||||
const isInput = /^(INPUT|TEXTAREA|SELECT)$/.test(activeElement.tagName) || activeElement.isContentEditable
|
const isInput = /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(activeElement.tagName) || activeElement.isContentEditable
|
||||||
if (isProtected || isInput) return
|
if (isProtected || isInput) return
|
||||||
}
|
}
|
||||||
if (dialog.active) return
|
if (dialog.active) return
|
||||||
@@ -1404,6 +1404,7 @@ export default function Page() {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
ref={autoScroll.contentRef}
|
ref={autoScroll.contentRef}
|
||||||
|
role="log"
|
||||||
class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
|
class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
|
||||||
classList={{
|
classList={{
|
||||||
"w-full": true,
|
"w-full": true,
|
||||||
@@ -1552,7 +1553,7 @@ export default function Page() {
|
|||||||
|
|
||||||
{/* Desktop tabs panel (Review + Context + Files) - hidden on mobile */}
|
{/* Desktop tabs panel (Review + Context + Files) - hidden on mobile */}
|
||||||
<Show when={isDesktop() && showTabs()}>
|
<Show when={isDesktop() && showTabs()}>
|
||||||
<div class="relative flex-1 min-w-0 h-full border-l border-border-weak-base">
|
<aside id="review-panel" aria-label="Review and files" class="relative flex-1 min-w-0 h-full border-l border-border-weak-base">
|
||||||
<DragDropProvider
|
<DragDropProvider
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
@@ -1586,7 +1587,7 @@ export default function Page() {
|
|||||||
value="context"
|
value="context"
|
||||||
closeButton={
|
closeButton={
|
||||||
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
||||||
<IconButton icon="close" variant="ghost" onClick={() => tabs().close("context")} />
|
<IconButton icon="close" variant="ghost" onClick={() => tabs().close("context")} aria-label="Close context tab" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
hideCloseButton
|
hideCloseButton
|
||||||
@@ -1612,6 +1613,7 @@ export default function Page() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
iconSize="large"
|
iconSize="large"
|
||||||
onClick={() => dialog.show(() => <DialogSelectFile />)}
|
onClick={() => dialog.show(() => <DialogSelectFile />)}
|
||||||
|
aria-label="Open file"
|
||||||
/>
|
/>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
@@ -1913,12 +1915,15 @@ export default function Page() {
|
|||||||
</Show>
|
</Show>
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DragDropProvider>
|
</DragDropProvider>
|
||||||
</div>
|
</aside>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={isDesktop() && view().terminal.opened()}>
|
<Show when={isDesktop() && view().terminal.opened()}>
|
||||||
<div
|
<div
|
||||||
|
id="terminal-panel"
|
||||||
|
role="region"
|
||||||
|
aria-label="Terminal"
|
||||||
class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base"
|
class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base"
|
||||||
style={{ height: `${layout.terminal.height()}px` }}
|
style={{ height: `${layout.terminal.height()}px` }}
|
||||||
>
|
>
|
||||||
@@ -1990,7 +1995,7 @@ export default function Page() {
|
|||||||
keybind={command.keybind("terminal.new")}
|
keybind={command.keybind("terminal.new")}
|
||||||
class="flex items-center"
|
class="flex items-center"
|
||||||
>
|
>
|
||||||
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={terminal.new} />
|
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={terminal.new} aria-label="New terminal" />
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function Dialog(props: DialogProps) {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.action}>{props.action}</Match>
|
<Match when={props.action}>{props.action}</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<Kobalte.CloseButton data-slot="dialog-close-button" as={IconButton} icon="close" variant="ghost" />
|
<Kobalte.CloseButton data-slot="dialog-close-button" as={IconButton} icon="close" variant="ghost" aria-label="Close" />
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function ImagePreview(props: ImagePreviewProps) {
|
|||||||
<div data-slot="image-preview-container">
|
<div data-slot="image-preview-container">
|
||||||
<Kobalte.Content data-slot="image-preview-content">
|
<Kobalte.Content data-slot="image-preview-content">
|
||||||
<div data-slot="image-preview-header">
|
<div data-slot="image-preview-header">
|
||||||
<Kobalte.CloseButton data-slot="image-preview-close" as={IconButton} icon="close" variant="ghost" />
|
<Kobalte.CloseButton data-slot="image-preview-close" as={IconButton} icon="close" variant="ghost" aria-label="Close" />
|
||||||
</div>
|
</div>
|
||||||
<div data-slot="image-preview-body">
|
<div data-slot="image-preview-body">
|
||||||
<img src={props.src} alt={props.alt ?? i18n.t("ui.imagePreview.alt")} data-slot="image-preview-image" />
|
<img src={props.src} alt={props.alt ?? i18n.t("ui.imagePreview.alt")} data-slot="image-preview-image" />
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Show when={internalFilter()}>
|
<Show when={internalFilter()}>
|
||||||
<IconButton icon="circle-x" variant="ghost" onClick={() => setInternalFilter("")} />
|
<IconButton icon="circle-x" variant="ghost" onClick={() => setInternalFilter("")} aria-label="Clear filter" />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
{searchAction()}
|
{searchAction()}
|
||||||
|
|||||||
@@ -429,6 +429,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
handleCopy()
|
handleCopy()
|
||||||
}}
|
}}
|
||||||
|
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
import { Popover as Kobalte } from "@kobalte/core/popover"
|
import { Popover as Kobalte } from "@kobalte/core/popover"
|
||||||
import { ComponentProps, JSXElement, ParentProps, Show, splitProps } from "solid-js"
|
import { ComponentProps, JSXElement, ParentProps, Show, splitProps, ValidComponent } from "solid-js"
|
||||||
import { IconButton } from "./icon-button"
|
import { IconButton } from "./icon-button"
|
||||||
|
|
||||||
export interface PopoverProps extends ParentProps, Omit<ComponentProps<typeof Kobalte>, "children"> {
|
export interface PopoverProps<T extends ValidComponent = "div"> extends ParentProps, Omit<ComponentProps<typeof Kobalte>, "children"> {
|
||||||
trigger: JSXElement
|
trigger?: JSXElement
|
||||||
|
triggerAs?: T
|
||||||
|
triggerProps?: ComponentProps<T>
|
||||||
title?: JSXElement
|
title?: JSXElement
|
||||||
description?: JSXElement
|
description?: JSXElement
|
||||||
class?: ComponentProps<"div">["class"]
|
class?: ComponentProps<"div">["class"]
|
||||||
classList?: ComponentProps<"div">["classList"]
|
classList?: ComponentProps<"div">["classList"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Popover(props: PopoverProps) {
|
export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>) {
|
||||||
const [local, rest] = splitProps(props, ["trigger", "title", "description", "class", "classList", "children"])
|
const [local, rest] = splitProps(props, ["trigger", "triggerAs", "triggerProps", "title", "description", "class", "classList", "children"])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Kobalte gutter={4} {...rest}>
|
<Kobalte gutter={4} {...rest}>
|
||||||
<Kobalte.Trigger as="div" data-slot="popover-trigger">
|
<Kobalte.Trigger as={local.triggerAs ?? "div"} data-slot="popover-trigger" {...(local.triggerProps as any)}>
|
||||||
{local.trigger}
|
{local.trigger}
|
||||||
</Kobalte.Trigger>
|
</Kobalte.Trigger>
|
||||||
<Kobalte.Portal>
|
<Kobalte.Portal>
|
||||||
@@ -30,7 +32,7 @@ export function Popover(props: PopoverProps) {
|
|||||||
<Show when={local.title}>
|
<Show when={local.title}>
|
||||||
<div data-slot="popover-header">
|
<div data-slot="popover-header">
|
||||||
<Kobalte.Title data-slot="popover-title">{local.title}</Kobalte.Title>
|
<Kobalte.Title data-slot="popover-title">{local.title}</Kobalte.Title>
|
||||||
<Kobalte.CloseButton data-slot="popover-close-button" as={IconButton} icon="close" variant="ghost" />
|
<Kobalte.CloseButton data-slot="popover-close-button" as={IconButton} icon="close" variant="ghost" aria-label="Close" />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={local.description}>
|
<Show when={local.description}>
|
||||||
|
|||||||
@@ -506,13 +506,13 @@ export function SessionTurn(
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<Show when={attachmentParts().length > 0}>
|
<Show when={attachmentParts().length > 0}>
|
||||||
<div data-slot="session-turn-attachments">
|
<div data-slot="session-turn-attachments" aria-live="off">
|
||||||
<Message message={msg()} parts={attachmentParts()} />
|
<Message message={msg()} parts={attachmentParts()} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div data-slot="session-turn-sticky" ref={setStickyRef}>
|
<div data-slot="session-turn-sticky" ref={setStickyRef}>
|
||||||
{/* User Message */}
|
{/* User Message */}
|
||||||
<div data-slot="session-turn-message-content">
|
<div data-slot="session-turn-message-content" aria-live="off">
|
||||||
<Message message={msg()} parts={stickyParts()} />
|
<Message message={msg()} parts={stickyParts()} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -525,6 +525,7 @@ export function SessionTurn(
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={props.onStepsExpandedToggle ?? (() => {})}
|
onClick={props.onStepsExpandedToggle ?? (() => {})}
|
||||||
|
aria-expanded={props.stepsExpanded}
|
||||||
>
|
>
|
||||||
<Show when={working()}>
|
<Show when={working()}>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@@ -552,8 +553,8 @@ export function SessionTurn(
|
|||||||
<Match when={props.stepsExpanded}>{i18n.t("ui.sessionTurn.steps.hide")}</Match>
|
<Match when={props.stepsExpanded}>{i18n.t("ui.sessionTurn.steps.hide")}</Match>
|
||||||
<Match when={!props.stepsExpanded}>{i18n.t("ui.sessionTurn.steps.show")}</Match>
|
<Match when={!props.stepsExpanded}>{i18n.t("ui.sessionTurn.steps.show")}</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
<span>·</span>
|
<span aria-hidden="true">·</span>
|
||||||
<span>{store.duration}</span>
|
<span aria-live="off">{store.duration}</span>
|
||||||
<Show when={assistantMessages().length > 0}>
|
<Show when={assistantMessages().length > 0}>
|
||||||
<Icon name="chevron-grabber-vertical" size="small" />
|
<Icon name="chevron-grabber-vertical" size="small" />
|
||||||
</Show>
|
</Show>
|
||||||
@@ -563,7 +564,7 @@ export function SessionTurn(
|
|||||||
</div>
|
</div>
|
||||||
{/* Response */}
|
{/* Response */}
|
||||||
<Show when={props.stepsExpanded && assistantMessages().length > 0}>
|
<Show when={props.stepsExpanded && assistantMessages().length > 0}>
|
||||||
<div data-slot="session-turn-collapsible-content-inner">
|
<div data-slot="session-turn-collapsible-content-inner" aria-hidden={working()}>
|
||||||
<For each={assistantMessages()}>
|
<For each={assistantMessages()}>
|
||||||
{(assistantMessage) => (
|
{(assistantMessage) => (
|
||||||
<AssistantMessageItem
|
<AssistantMessageItem
|
||||||
@@ -589,6 +590,9 @@ export function SessionTurn(
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
{/* Response */}
|
{/* Response */}
|
||||||
|
<div class="sr-only" aria-live="polite">
|
||||||
|
{!working() && response() ? response() : ""}
|
||||||
|
</div>
|
||||||
<Show when={!working() && (response() || hasDiffs())}>
|
<Show when={!working() && (response() || hasDiffs())}>
|
||||||
<div data-slot="session-turn-summary-section">
|
<div data-slot="session-turn-summary-section">
|
||||||
<div data-slot="session-turn-summary-header">
|
<div data-slot="session-turn-summary-header">
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export function TextField(props: TextFieldProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
data-slot="input-copy-button"
|
data-slot="input-copy-button"
|
||||||
|
aria-label={copied() ? i18n.t("ui.textField.copied") : i18n.t("ui.textField.copyToClipboard")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ function ToastActions(props: ComponentProps<"div">) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ToastCloseButton(props: ToastCloseButtonProps & ComponentProps<"button">) {
|
function ToastCloseButton(props: ToastCloseButtonProps & ComponentProps<"button">) {
|
||||||
return <Kobalte.CloseButton data-slot="toast-close-button" as={IconButton} icon="close" variant="ghost" {...props} />
|
return <Kobalte.CloseButton data-slot="toast-close-button" as={IconButton} icon="close" variant="ghost" aria-label="Dismiss" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToastProgressTrack(props: ComponentProps<typeof Kobalte.ProgressTrack>) {
|
function ToastProgressTrack(props: ComponentProps<typeof Kobalte.ProgressTrack>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user