Vendor opencode source for docker build
This commit is contained in:
98
opencode/packages/ui/src/components/accordion.css
Normal file
98
opencode/packages/ui/src/components/accordion.css
Normal file
@@ -0,0 +1,98 @@
|
||||
[data-component="accordion"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
|
||||
[data-slot="accordion-item"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
overflow: clip;
|
||||
|
||||
[data-slot="accordion-header"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
[data-slot="accordion-trigger"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
padding: 8px 12px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
|
||||
background-color: var(--surface-base);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: clip;
|
||||
color: var(--text-strong);
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
/* text-12-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-base);
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
&[data-disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-expanded] {
|
||||
[data-slot="accordion-trigger"] {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
[data-slot="accordion-content"] {
|
||||
border: 1px solid var(--border-weak-base);
|
||||
border-top: none;
|
||||
border-bottom-left-radius: var(--radius-md);
|
||||
border-bottom-right-radius: var(--radius-md);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="accordion-content"] {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--kb-accordion-content-height);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
height: var(--kb-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
92
opencode/packages/ui/src/components/accordion.tsx
Normal file
92
opencode/packages/ui/src/components/accordion.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Accordion as Kobalte } from "@kobalte/core/accordion"
|
||||
import { splitProps } from "solid-js"
|
||||
import type { ComponentProps, ParentProps } from "solid-js"
|
||||
|
||||
export interface AccordionProps extends ComponentProps<typeof Kobalte> {}
|
||||
export interface AccordionItemProps extends ComponentProps<typeof Kobalte.Item> {}
|
||||
export interface AccordionHeaderProps extends ComponentProps<typeof Kobalte.Header> {}
|
||||
export interface AccordionTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {}
|
||||
export interface AccordionContentProps extends ComponentProps<typeof Kobalte.Content> {}
|
||||
|
||||
function AccordionRoot(props: AccordionProps) {
|
||||
const [split, rest] = splitProps(props, ["class", "classList"])
|
||||
return (
|
||||
<Kobalte
|
||||
{...rest}
|
||||
data-component="accordion"
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionItem(props: AccordionItemProps) {
|
||||
const [split, rest] = splitProps(props, ["class", "classList"])
|
||||
return (
|
||||
<Kobalte.Item
|
||||
{...rest}
|
||||
data-slot="accordion-item"
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionHeader(props: ParentProps<AccordionHeaderProps>) {
|
||||
const [split, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.Header
|
||||
{...rest}
|
||||
data-slot="accordion-header"
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
>
|
||||
{split.children}
|
||||
</Kobalte.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionTrigger(props: ParentProps<AccordionTriggerProps>) {
|
||||
const [split, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.Trigger
|
||||
{...rest}
|
||||
data-slot="accordion-trigger"
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
>
|
||||
{split.children}
|
||||
</Kobalte.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent(props: ParentProps<AccordionContentProps>) {
|
||||
const [split, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.Content
|
||||
{...rest}
|
||||
data-slot="accordion-content"
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
>
|
||||
{split.children}
|
||||
</Kobalte.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export const Accordion = Object.assign(AccordionRoot, {
|
||||
Item: AccordionItem,
|
||||
Header: AccordionHeader,
|
||||
Trigger: AccordionTrigger,
|
||||
Content: AccordionContent,
|
||||
})
|
||||
9
opencode/packages/ui/src/components/app-icon.css
Normal file
9
opencode/packages/ui/src/components/app-icon.css
Normal file
@@ -0,0 +1,9 @@
|
||||
img[data-component="app-icon"] {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
padding: 2px;
|
||||
border-radius: 0.125rem;
|
||||
background: var(--smoke-light-2);
|
||||
border: 1px solid var(--smoke-light-alpha-4);
|
||||
object-fit: contain;
|
||||
}
|
||||
56
opencode/packages/ui/src/components/app-icon.tsx
Normal file
56
opencode/packages/ui/src/components/app-icon.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { Component, ComponentProps } from "solid-js"
|
||||
import { splitProps } from "solid-js"
|
||||
import type { IconName } from "./app-icons/types"
|
||||
|
||||
import androidStudio from "../assets/icons/app/android-studio.svg"
|
||||
import antigravity from "../assets/icons/app/antigravity.svg"
|
||||
import cursor from "../assets/icons/app/cursor.svg"
|
||||
import fileExplorer from "../assets/icons/app/file-explorer.svg"
|
||||
import finder from "../assets/icons/app/finder.png"
|
||||
import ghostty from "../assets/icons/app/ghostty.svg"
|
||||
import iterm2 from "../assets/icons/app/iterm2.svg"
|
||||
import powershell from "../assets/icons/app/powershell.svg"
|
||||
import terminal from "../assets/icons/app/terminal.png"
|
||||
import textmate from "../assets/icons/app/textmate.png"
|
||||
import vscode from "../assets/icons/app/vscode.svg"
|
||||
import xcode from "../assets/icons/app/xcode.png"
|
||||
import zed from "../assets/icons/app/zed.svg"
|
||||
import sublimetext from "../assets/icons/app/sublimetext.svg"
|
||||
|
||||
const icons = {
|
||||
vscode,
|
||||
cursor,
|
||||
zed,
|
||||
"file-explorer": fileExplorer,
|
||||
finder,
|
||||
terminal,
|
||||
iterm2,
|
||||
ghostty,
|
||||
xcode,
|
||||
"android-studio": androidStudio,
|
||||
antigravity,
|
||||
textmate,
|
||||
powershell,
|
||||
"sublime-text": sublimetext,
|
||||
} satisfies Record<IconName, string>
|
||||
|
||||
export type AppIconProps = Omit<ComponentProps<"img">, "src"> & {
|
||||
id: IconName
|
||||
}
|
||||
|
||||
export const AppIcon: Component<AppIconProps> = (props) => {
|
||||
const [local, rest] = splitProps(props, ["id", "class", "classList", "alt", "draggable"])
|
||||
return (
|
||||
<img
|
||||
data-component="app-icon"
|
||||
{...rest}
|
||||
src={icons[local.id]}
|
||||
alt={local.alt ?? ""}
|
||||
draggable={local.draggable ?? false}
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
114
opencode/packages/ui/src/components/app-icons/sprite.svg
Normal file
114
opencode/packages/ui/src/components/app-icons/sprite.svg
Normal file
@@ -0,0 +1,114 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="0" height="0">
|
||||
<defs>
|
||||
<symbol id="vscode" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" rx="5" fill="#007ACC" />
|
||||
<g transform="scale(1.5)">
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M11.5 11.19V4.8L7.3 7.99M1.17 6.07a.6.6 0 0 1-.01-.81L2 4.48c.14-.13.48-.18.73 0l2.39 1.83 5.55-5.09c.22-.22.61-.32 1.05-.08l2.8 1.34c.25.15.49.38.49.81v9.49c0 .28-.2.58-.42.7l-3.08 1.48c-.22.09-.64 0-.79-.14L5.11 9.69l-2.38 1.83c-.27.18-.6.13-.74 0l-.84-.77c-.22-.23-.2-.61.04-.84l2.1-1.9"
|
||||
/>
|
||||
</g>
|
||||
</symbol>
|
||||
|
||||
<symbol id="cursor" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" rx="5" fill="#111827" />
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23"
|
||||
/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="zed" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" rx="5" fill="#084CCF" />
|
||||
<g transform="translate(12 12) scale(0.9) translate(-12 -12)">
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M2.25 1.5a.75.75 0 0 0-.75.75v16.5H0V2.25A2.25 2.25 0 0 1 2.25 0h20.095c1.002 0 1.504 1.212.795 1.92L10.764 14.298h3.486V12.75h1.5v1.922a1.125 1.125 0 0 1-1.125 1.125H9.264l-2.578 2.578h11.689V9h1.5v9.375a1.5 1.5 0 0 1-1.5 1.5H5.185L2.562 22.5H21.75a.75.75 0 0 0 .75-.75V5.25H24v16.5A2.25 2.25 0 0 1 21.75 24H1.655C.653 24 .151 22.788.86 22.08L13.19 9.75H9.75v1.5h-1.5V9.375A1.125 1.125 0 0 1 9.375 8.25h5.314l2.625-2.625H5.625V15h-1.5V5.625a1.5 1.5 0 0 1 1.5-1.5h13.19L21.438 1.5z"
|
||||
/>
|
||||
</g>
|
||||
</symbol>
|
||||
|
||||
<symbol id="finder" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" rx="5" fill="#8ED0FF" />
|
||||
<path d="M12 0H19a5 5 0 0 1 5 5V19a5 5 0 0 1-5 5H12Z" fill="#2D7BF7" />
|
||||
<path d="M12 3v18" stroke="#0B2A4A" stroke-opacity="0.35" stroke-width="1.5" />
|
||||
<circle cx="8.3" cy="9.2" r="1.1" fill="#0B2A4A" />
|
||||
<circle cx="15.7" cy="9.2" r="1.1" fill="#0B2A4A" />
|
||||
<path
|
||||
d="M7.3 15c1.2 1.55 2.9 2.4 4.7 2.4s3.5-.85 4.7-2.4"
|
||||
stroke="#0B2A4A"
|
||||
stroke-width="1.5"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="terminal" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" rx="5" fill="#111827" />
|
||||
<rect
|
||||
x="3.5"
|
||||
y="4.5"
|
||||
width="17"
|
||||
height="15"
|
||||
rx="2.5"
|
||||
fill="#0B1220"
|
||||
stroke="#334155"
|
||||
stroke-opacity="0.5"
|
||||
/>
|
||||
<path
|
||||
d="M7.8 9.2 11 12 7.8 14.8"
|
||||
stroke="#34D399"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<rect x="12.2" y="14.2" width="5.4" height="1.6" rx="0.8" fill="#34D399" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="iterm2" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" rx="5" fill="#0B0B0B" />
|
||||
<rect x="3.2" y="4.2" width="17.6" height="15.6" rx="2.4" fill="#000" stroke="#60A5FA" stroke-width="1.2" />
|
||||
<circle cx="5.5" cy="6.3" r="0.75" fill="#F87171" />
|
||||
<circle cx="7.6" cy="6.3" r="0.75" fill="#FBBF24" />
|
||||
<circle cx="9.7" cy="6.3" r="0.75" fill="#34D399" />
|
||||
<path
|
||||
d="M7.9 10.2 10.6 12 7.9 13.8"
|
||||
stroke="#34D399"
|
||||
stroke-width="1.6"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<rect x="11.6" y="13.3" width="5" height="1.4" rx="0.7" fill="#34D399" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="ghostty" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" rx="5" fill="#3551F3" />
|
||||
<g transform="translate(12 12) scale(0.9) translate(-12 -12)">
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M12 0C6.7 0 2.4 4.3 2.4 9.6v11.146c0 1.772 1.45 3.267 3.222 3.254a3.18 3.18 0 0 0 1.955-.686 1.96 1.96 0 0 1 2.444 0 3.18 3.18 0 0 0 1.976.686c.75 0 1.436-.257 1.98-.686.715-.563 1.71-.587 2.419-.018.59.476 1.355.743 2.182.699 1.705-.094 3.022-1.537 3.022-3.244V9.601C21.6 4.3 17.302 0 12 0M6.069 6.562a1 1 0 0 1 .46.131l3.578 2.065v.002a.974.974 0 0 1 0 1.687L6.53 12.512a.975.975 0 0 1-.976-1.687L7.67 9.602 5.553 8.38a.975.975 0 0 1 .515-1.818m7.438 2.063h4.7a.975.975 0 1 1 0 1.95h-4.7a.975.975 0 0 1 0-1.95"
|
||||
/>
|
||||
</g>
|
||||
</symbol>
|
||||
|
||||
<symbol id="xcode" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" rx="5" fill="#147EFB" />
|
||||
<path d="M6 8H18" stroke="#fff" stroke-opacity="0.18" stroke-width="1.2" stroke-linecap="round" />
|
||||
<path d="M8 6V18" stroke="#fff" stroke-opacity="0.18" stroke-width="1.2" stroke-linecap="round" />
|
||||
<path d="M6 18H18" stroke="#fff" stroke-opacity="0.18" stroke-width="1.2" stroke-linecap="round" />
|
||||
<path d="M18 6V18" stroke="#fff" stroke-opacity="0.18" stroke-width="1.2" stroke-linecap="round" />
|
||||
<g transform="translate(12 12) rotate(-35) translate(-12 -12)">
|
||||
<rect x="11.1" y="6.2" width="2" height="12.6" rx="1" fill="#fff" />
|
||||
<rect x="9.2" y="5.3" width="5.6" height="2.7" rx="1" fill="#fff" />
|
||||
</g>
|
||||
</symbol>
|
||||
|
||||
<symbol id="android-studio" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" rx="5" fill="#3DDC84" />
|
||||
<circle cx="12" cy="12.2" r="6.8" fill="#3B82F6" />
|
||||
<circle cx="12" cy="12.2" r="4.8" fill="none" stroke="#fff" stroke-width="1.6" />
|
||||
<path d="M12 9.4l2.2 5-2.2-1.3-2.2 1.3z" fill="#fff" />
|
||||
<circle cx="12" cy="12.2" r="0.9" fill="#fff" />
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
20
opencode/packages/ui/src/components/app-icons/types.ts
Normal file
20
opencode/packages/ui/src/components/app-icons/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// This file is generated by icon spritesheet generator
|
||||
|
||||
export const iconNames = [
|
||||
"vscode",
|
||||
"cursor",
|
||||
"zed",
|
||||
"file-explorer",
|
||||
"finder",
|
||||
"terminal",
|
||||
"iterm2",
|
||||
"ghostty",
|
||||
"xcode",
|
||||
"android-studio",
|
||||
"antigravity",
|
||||
"textmate",
|
||||
"powershell",
|
||||
"sublime-text",
|
||||
] as const
|
||||
|
||||
export type IconName = (typeof iconNames)[number]
|
||||
49
opencode/packages/ui/src/components/avatar.css
Normal file
49
opencode/packages/ui/src/components/avatar.css
Normal file
@@ -0,0 +1,49 @@
|
||||
[data-component="avatar"] {
|
||||
--avatar-bg: var(--color-surface-info-base);
|
||||
--avatar-fg: var(--color-text-base);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--color-border-weak-base);
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
background-color: var(--avatar-bg);
|
||||
color: var(--avatar-fg);
|
||||
}
|
||||
|
||||
[data-component="avatar"][data-has-image] {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
[data-component="avatar"][data-size="small"] {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
[data-component="avatar"][data-size="normal"] {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
[data-component="avatar"][data-size="large"] {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
font-size: 1.25rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
[data-component="avatar"] [data-slot="avatar-image"] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: inherit;
|
||||
}
|
||||
44
opencode/packages/ui/src/components/avatar.tsx
Normal file
44
opencode/packages/ui/src/components/avatar.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { type ComponentProps, splitProps, Show } from "solid-js"
|
||||
|
||||
export interface AvatarProps extends ComponentProps<"div"> {
|
||||
fallback: string
|
||||
src?: string
|
||||
background?: string
|
||||
foreground?: string
|
||||
size?: "small" | "normal" | "large"
|
||||
}
|
||||
|
||||
export function Avatar(props: AvatarProps) {
|
||||
const [split, rest] = splitProps(props, [
|
||||
"fallback",
|
||||
"src",
|
||||
"background",
|
||||
"foreground",
|
||||
"size",
|
||||
"class",
|
||||
"classList",
|
||||
"style",
|
||||
])
|
||||
const src = split.src // did this so i can zero it out to test fallback
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
data-component="avatar"
|
||||
data-size={split.size || "normal"}
|
||||
data-has-image={src ? "" : undefined}
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
style={{
|
||||
...(typeof split.style === "object" ? split.style : {}),
|
||||
...(!src && split.background ? { "--avatar-bg": split.background } : {}),
|
||||
...(!src && split.foreground ? { "--avatar-fg": split.foreground } : {}),
|
||||
}}
|
||||
>
|
||||
<Show when={src} fallback={split.fallback?.[0]}>
|
||||
{(src) => <img src={src()} draggable={false} data-slot="avatar-image" />}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
97
opencode/packages/ui/src/components/basic-tool.css
Normal file
97
opencode/packages/ui/src/components/basic-tool.css
Normal file
@@ -0,0 +1,97 @@
|
||||
[data-component="tool-trigger"] {
|
||||
content-visibility: auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
gap: 20px;
|
||||
justify-content: space-between;
|
||||
|
||||
[data-slot="basic-tool-tool-trigger-content"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-slot="basic-tool-tool-info"] {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="basic-tool-tool-info-structured"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
[data-slot="basic-tool-tool-info-main"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-slot="basic-tool-tool-title"] {
|
||||
flex-shrink: 0;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-base);
|
||||
|
||||
&.capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="basic-tool-tool-subtitle"] {
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-weak);
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="basic-tool-tool-arg"] {
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-weak);
|
||||
}
|
||||
}
|
||||
118
opencode/packages/ui/src/components/basic-tool.tsx
Normal file
118
opencode/packages/ui/src/components/basic-tool.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { createEffect, createSignal, For, Match, Show, Switch, type JSX } from "solid-js"
|
||||
import { Collapsible } from "./collapsible"
|
||||
import { Icon, IconProps } from "./icon"
|
||||
|
||||
export type TriggerTitle = {
|
||||
title: string
|
||||
titleClass?: string
|
||||
subtitle?: string
|
||||
subtitleClass?: string
|
||||
args?: string[]
|
||||
argsClass?: string
|
||||
action?: JSX.Element
|
||||
}
|
||||
|
||||
const isTriggerTitle = (val: any): val is TriggerTitle => {
|
||||
return (
|
||||
typeof val === "object" && val !== null && "title" in val && (typeof Node === "undefined" || !(val instanceof Node))
|
||||
)
|
||||
}
|
||||
|
||||
export interface BasicToolProps {
|
||||
icon: IconProps["name"]
|
||||
trigger: TriggerTitle | JSX.Element
|
||||
children?: JSX.Element
|
||||
hideDetails?: boolean
|
||||
defaultOpen?: boolean
|
||||
forceOpen?: boolean
|
||||
locked?: boolean
|
||||
onSubtitleClick?: () => void
|
||||
}
|
||||
|
||||
export function BasicTool(props: BasicToolProps) {
|
||||
const [open, setOpen] = createSignal(props.defaultOpen ?? false)
|
||||
|
||||
createEffect(() => {
|
||||
if (props.forceOpen) setOpen(true)
|
||||
})
|
||||
|
||||
const handleOpenChange = (value: boolean) => {
|
||||
if (props.locked && !value) return
|
||||
setOpen(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible open={open()} onOpenChange={handleOpenChange}>
|
||||
<Collapsible.Trigger>
|
||||
<div data-component="tool-trigger">
|
||||
<div data-slot="basic-tool-tool-trigger-content">
|
||||
<Icon name={props.icon} size="small" />
|
||||
<div data-slot="basic-tool-tool-info">
|
||||
<Switch>
|
||||
<Match when={isTriggerTitle(props.trigger) && props.trigger}>
|
||||
{(trigger) => (
|
||||
<div data-slot="basic-tool-tool-info-structured">
|
||||
<div data-slot="basic-tool-tool-info-main">
|
||||
<span
|
||||
data-slot="basic-tool-tool-title"
|
||||
classList={{
|
||||
[trigger().titleClass ?? ""]: !!trigger().titleClass,
|
||||
}}
|
||||
>
|
||||
{trigger().title}
|
||||
</span>
|
||||
<Show when={trigger().subtitle}>
|
||||
<span
|
||||
data-slot="basic-tool-tool-subtitle"
|
||||
classList={{
|
||||
[trigger().subtitleClass ?? ""]: !!trigger().subtitleClass,
|
||||
clickable: !!props.onSubtitleClick,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (props.onSubtitleClick) {
|
||||
e.stopPropagation()
|
||||
props.onSubtitleClick()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{trigger().subtitle}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={trigger().args?.length}>
|
||||
<For each={trigger().args}>
|
||||
{(arg) => (
|
||||
<span
|
||||
data-slot="basic-tool-tool-arg"
|
||||
classList={{
|
||||
[trigger().argsClass ?? ""]: !!trigger().argsClass,
|
||||
}}
|
||||
>
|
||||
{arg}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={trigger().action}>{trigger().action}</Show>
|
||||
</div>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={true}>{props.trigger as JSX.Element}</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={props.children && !props.hideDetails && !props.locked}>
|
||||
<Collapsible.Arrow />
|
||||
</Show>
|
||||
</div>
|
||||
</Collapsible.Trigger>
|
||||
<Show when={props.children && !props.hideDetails}>
|
||||
<Collapsible.Content>{props.children}</Collapsible.Content>
|
||||
</Show>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
|
||||
export function GenericTool(props: { tool: string; hideDetails?: boolean }) {
|
||||
return <BasicTool icon="mcp" trigger={{ title: props.tool }} hideDetails={props.hideDetails} />
|
||||
}
|
||||
172
opencode/packages/ui/src/components/button.css
Normal file
172
opencode/packages/ui/src/components/button.css
Normal file
@@ -0,0 +1,172 @@
|
||||
[data-component="button"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: var(--radius-md);
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
outline: none;
|
||||
white-space: nowrap;
|
||||
|
||||
&[data-variant="primary"] {
|
||||
background-color: var(--button-primary-base);
|
||||
border-color: var(--border-weak-base);
|
||||
color: var(--icon-invert-base);
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
color: var(--icon-invert-base);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--icon-strong-hover);
|
||||
}
|
||||
&:focus:not(:disabled) {
|
||||
background-color: var(--icon-strong-focus);
|
||||
}
|
||||
&:active:not(:disabled) {
|
||||
background-color: var(--icon-strong-active);
|
||||
}
|
||||
&:disabled {
|
||||
background-color: var(--icon-strong-disabled);
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
color: var(--icon-invert-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant="ghost"] {
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
color: var(--text-strong);
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
color: var(--icon-base);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
&:focus-visible:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
&:active:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
}
|
||||
&:disabled {
|
||||
color: var(--text-weak);
|
||||
cursor: not-allowed;
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
color: var(--icon-disabled);
|
||||
}
|
||||
}
|
||||
&[data-selected="true"]:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
&[data-active="true"] {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant="secondary"] {
|
||||
border: transparent;
|
||||
background-color: var(--button-secondary-base);
|
||||
color: var(--text-strong);
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--button-secondary-hover);
|
||||
}
|
||||
&:focus:not(:disabled) {
|
||||
background-color: var(--button-secondary-base);
|
||||
}
|
||||
&:focus-visible:not(:active) {
|
||||
background-color: var(--button-secondary-base);
|
||||
box-shadow: var(--shadow-xs-border-focus);
|
||||
}
|
||||
&:focus-visible:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
&:active:not(:disabled) {
|
||||
background-color: var(--button-secondary-base);
|
||||
scale: 0.99;
|
||||
transition: all 150ms ease-out;
|
||||
}
|
||||
&:disabled {
|
||||
border-color: var(--border-disabled);
|
||||
background-color: var(--surface-disabled);
|
||||
color: var(--text-weak);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
color: var(--icon-strong-base);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-size="small"] {
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
&[data-icon] {
|
||||
padding: 0 12px 0 4px;
|
||||
}
|
||||
|
||||
font-size: var(--font-size-small);
|
||||
line-height: var(--line-height-large);
|
||||
gap: 4px;
|
||||
|
||||
/* text-12-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
&[data-size="normal"] {
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
padding: 0 6px;
|
||||
&[data-icon] {
|
||||
padding: 0 12px 0 4px;
|
||||
}
|
||||
|
||||
font-size: var(--font-size-small);
|
||||
gap: 6px;
|
||||
|
||||
/* text-12-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
&[data-size="large"] {
|
||||
height: 32px;
|
||||
padding: 6px 12px;
|
||||
|
||||
&[data-icon] {
|
||||
padding: 0 12px 0 8px;
|
||||
}
|
||||
|
||||
gap: 4px;
|
||||
|
||||
/* text-14-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 142.857% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
33
opencode/packages/ui/src/components/button.tsx
Normal file
33
opencode/packages/ui/src/components/button.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Button as Kobalte } from "@kobalte/core/button"
|
||||
import { type ComponentProps, Show, splitProps } from "solid-js"
|
||||
import { Icon, IconProps } from "./icon"
|
||||
|
||||
export interface ButtonProps
|
||||
extends ComponentProps<typeof Kobalte>,
|
||||
Pick<ComponentProps<"button">, "class" | "classList" | "children"> {
|
||||
size?: "small" | "normal" | "large"
|
||||
variant?: "primary" | "secondary" | "ghost"
|
||||
icon?: IconProps["name"]
|
||||
}
|
||||
|
||||
export function Button(props: ButtonProps) {
|
||||
const [split, rest] = splitProps(props, ["variant", "size", "icon", "class", "classList"])
|
||||
return (
|
||||
<Kobalte
|
||||
{...rest}
|
||||
data-component="button"
|
||||
data-size={split.size || "normal"}
|
||||
data-variant={split.variant || "secondary"}
|
||||
data-icon={split.icon}
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
>
|
||||
<Show when={split.icon}>
|
||||
<Icon name={split.icon!} size="small" />
|
||||
</Show>
|
||||
{props.children}
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
29
opencode/packages/ui/src/components/card.css
Normal file
29
opencode/packages/ui/src/components/card.css
Normal file
@@ -0,0 +1,29 @@
|
||||
[data-component="card"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--surface-inset-base);
|
||||
border: 1px solid var(--border-weaker-base);
|
||||
transition: background-color 0.15s ease;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 6px 12px;
|
||||
overflow: clip;
|
||||
|
||||
&[data-variant="error"] {
|
||||
background-color: var(--surface-critical-weak);
|
||||
border: 1px solid var(--border-critical-base);
|
||||
color: rgba(218, 51, 25, 0.6);
|
||||
|
||||
/* text-12-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
&[data-component="icon"] {
|
||||
color: var(--icon-critical-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
opencode/packages/ui/src/components/card.tsx
Normal file
22
opencode/packages/ui/src/components/card.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { type ComponentProps, splitProps } from "solid-js"
|
||||
|
||||
export interface CardProps extends ComponentProps<"div"> {
|
||||
variant?: "normal" | "error" | "warning" | "success" | "info"
|
||||
}
|
||||
|
||||
export function Card(props: CardProps) {
|
||||
const [split, rest] = splitProps(props, ["variant", "class", "classList"])
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
data-component="card"
|
||||
data-variant={split.variant || "normal"}
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
121
opencode/packages/ui/src/components/checkbox.css
Normal file
121
opencode/packages/ui/src/components/checkbox.css
Normal file
@@ -0,0 +1,121 @@
|
||||
[data-component="checkbox"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: default;
|
||||
|
||||
[data-slot="checkbox-checkbox-input"] {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="checkbox-checkbox-control"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 2px;
|
||||
aspect-ratio: 1;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
/* background-color: var(--surface-weak); */
|
||||
}
|
||||
|
||||
[data-slot="checkbox-checkbox-indicator"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--icon-base);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* [data-slot="checkbox-checkbox-content"] { */
|
||||
/* } */
|
||||
|
||||
[data-slot="checkbox-checkbox-label"] {
|
||||
user-select: none;
|
||||
color: var(--text-base);
|
||||
|
||||
/* text-12-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
[data-slot="checkbox-checkbox-description"] {
|
||||
color: var(--text-base);
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 12px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-normal);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
[data-slot="checkbox-checkbox-error"] {
|
||||
color: var(--text-error);
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 12px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-normal);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
&:hover:not([data-disabled], [data-readonly]) [data-slot="checkbox-checkbox-control"] {
|
||||
border-color: var(--border-hover);
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
&:focus-within:not([data-readonly]) [data-slot="checkbox-checkbox-control"] {
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: 0 0 0 2px var(--surface-focus);
|
||||
}
|
||||
|
||||
&[data-checked] [data-slot="checkbox-checkbox-control"],
|
||||
&[data-indeterminate] [data-slot="checkbox-checkbox-control"] {
|
||||
border-color: var(--border-base);
|
||||
background-color: var(--surface-weak);
|
||||
}
|
||||
|
||||
&[data-checked]:hover:not([data-disabled], [data-readonly]) [data-slot="checkbox-checkbox-control"],
|
||||
&[data-indeterminate]:hover:not([data-disabled]) [data-slot="checkbox-checkbox-control"] {
|
||||
border-color: var(--border-hover);
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
&[data-checked] [data-slot="checkbox-checkbox-indicator"],
|
||||
&[data-indeterminate] [data-slot="checkbox-checkbox-indicator"] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&[data-disabled] [data-slot="checkbox-checkbox-control"] {
|
||||
border-color: var(--border-disabled);
|
||||
background-color: var(--surface-disabled);
|
||||
}
|
||||
|
||||
&[data-invalid] [data-slot="checkbox-checkbox-control"] {
|
||||
border-color: var(--border-error);
|
||||
}
|
||||
|
||||
&[data-readonly] {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
43
opencode/packages/ui/src/components/checkbox.tsx
Normal file
43
opencode/packages/ui/src/components/checkbox.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Checkbox as Kobalte } from "@kobalte/core/checkbox"
|
||||
import { Show, splitProps } from "solid-js"
|
||||
import type { ComponentProps, JSX, ParentProps } from "solid-js"
|
||||
|
||||
export interface CheckboxProps extends ParentProps<ComponentProps<typeof Kobalte>> {
|
||||
hideLabel?: boolean
|
||||
description?: string
|
||||
icon?: JSX.Element
|
||||
}
|
||||
|
||||
export function Checkbox(props: CheckboxProps) {
|
||||
const [local, others] = splitProps(props, ["children", "class", "label", "hideLabel", "description", "icon"])
|
||||
return (
|
||||
<Kobalte {...others} data-component="checkbox">
|
||||
<Kobalte.Input data-slot="checkbox-checkbox-input" />
|
||||
<Kobalte.Control data-slot="checkbox-checkbox-control">
|
||||
<Kobalte.Indicator data-slot="checkbox-checkbox-indicator">
|
||||
{local.icon || (
|
||||
<svg viewBox="0 0 12 12" fill="none" width="10" height="10" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3 7.17905L5.02703 8.85135L9 3.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="square"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</Kobalte.Indicator>
|
||||
</Kobalte.Control>
|
||||
<div data-slot="checkbox-checkbox-content">
|
||||
<Show when={props.children}>
|
||||
<Kobalte.Label data-slot="checkbox-checkbox-label" classList={{ "sr-only": local.hideLabel }}>
|
||||
{props.children}
|
||||
</Kobalte.Label>
|
||||
</Show>
|
||||
<Show when={local.description}>
|
||||
<Kobalte.Description data-slot="checkbox-checkbox-description">{local.description}</Kobalte.Description>
|
||||
</Show>
|
||||
<Kobalte.ErrorMessage data-slot="checkbox-checkbox-error" />
|
||||
</div>
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
4
opencode/packages/ui/src/components/code.css
Normal file
4
opencode/packages/ui/src/components/code.css
Normal file
@@ -0,0 +1,4 @@
|
||||
[data-component="code"] {
|
||||
content-visibility: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
1003
opencode/packages/ui/src/components/code.tsx
Normal file
1003
opencode/packages/ui/src/components/code.tsx
Normal file
File diff suppressed because it is too large
Load Diff
103
opencode/packages/ui/src/components/collapsible.css
Normal file
103
opencode/packages/ui/src/components/collapsible.css
Normal file
@@ -0,0 +1,103 @@
|
||||
[data-component="collapsible"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--surface-inset-base);
|
||||
border: 1px solid var(--border-weaker-base);
|
||||
transition: background-color 0.15s ease;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: clip;
|
||||
|
||||
[data-slot="collapsible-trigger"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
padding: 6px 8px 6px 12px;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
color: var(--text-base);
|
||||
|
||||
/* text-12-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
/* &:hover { */
|
||||
/* background-color: var(--surface-base); */
|
||||
/* } */
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
&[data-disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
[data-slot="collapsible-arrow"] {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="collapsible-content"] {
|
||||
overflow: hidden;
|
||||
/* animation: slideUp 250ms ease-out; */
|
||||
|
||||
/* &[data-expanded] { */
|
||||
/* animation: slideDown 250ms ease-out; */
|
||||
/* } */
|
||||
}
|
||||
|
||||
&[data-variant="ghost"] {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
> [data-slot="collapsible-trigger"] {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
||||
/* &:hover { */
|
||||
/* color: var(--text-strong); */
|
||||
/* } */
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
&[data-disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant="ghost"][data-scope="filetree"] {
|
||||
> [data-slot="collapsible-trigger"] {
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--kb-collapsible-content-height);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
height: var(--kb-collapsible-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
46
opencode/packages/ui/src/components/collapsible.tsx
Normal file
46
opencode/packages/ui/src/components/collapsible.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Collapsible as Kobalte, CollapsibleRootProps } from "@kobalte/core/collapsible"
|
||||
import { ComponentProps, ParentProps, splitProps } from "solid-js"
|
||||
import { Icon } from "./icon"
|
||||
|
||||
export interface CollapsibleProps extends ParentProps<CollapsibleRootProps> {
|
||||
class?: string
|
||||
classList?: ComponentProps<"div">["classList"]
|
||||
variant?: "normal" | "ghost"
|
||||
}
|
||||
|
||||
function CollapsibleRoot(props: CollapsibleProps) {
|
||||
const [local, others] = splitProps(props, ["class", "classList", "variant"])
|
||||
return (
|
||||
<Kobalte
|
||||
data-component="collapsible"
|
||||
data-variant={local.variant || "normal"}
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
{...others}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleTrigger(props: ComponentProps<typeof Kobalte.Trigger>) {
|
||||
return <Kobalte.Trigger data-slot="collapsible-trigger" {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleContent(props: ComponentProps<typeof Kobalte.Content>) {
|
||||
return <Kobalte.Content data-slot="collapsible-content" {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleArrow(props?: ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="collapsible-arrow" {...(props || {})}>
|
||||
<Icon name="chevron-grabber-vertical" size="small" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Collapsible = Object.assign(CollapsibleRoot, {
|
||||
Arrow: CollapsibleArrow,
|
||||
Trigger: CollapsibleTrigger,
|
||||
Content: CollapsibleContent,
|
||||
})
|
||||
134
opencode/packages/ui/src/components/context-menu.css
Normal file
134
opencode/packages/ui/src/components/context-menu.css
Normal file
@@ -0,0 +1,134 @@
|
||||
[data-component="context-menu-content"],
|
||||
[data-component="context-menu-sub-content"] {
|
||||
min-width: 8rem;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
background-clip: padding-box;
|
||||
background-color: var(--surface-raised-stronger-non-alpha);
|
||||
padding: 4px;
|
||||
z-index: 100;
|
||||
transform-origin: var(--kb-menu-content-transform-origin);
|
||||
|
||||
&:focus-within,
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
animation: contextMenuContentHide var(--transition-duration) var(--transition-easing) forwards;
|
||||
|
||||
@starting-style {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&[data-expanded] {
|
||||
pointer-events: auto;
|
||||
animation: contextMenuContentShow var(--transition-duration) var(--transition-easing) forwards;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="context-menu-content"],
|
||||
[data-component="context-menu-sub-content"] {
|
||||
[data-slot="context-menu-item"],
|
||||
[data-slot="context-menu-checkbox-item"],
|
||||
[data-slot="context-menu-radio-item"],
|
||||
[data-slot="context-menu-sub-trigger"] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: default;
|
||||
outline: none;
|
||||
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-strong);
|
||||
|
||||
transition-property: background-color, color;
|
||||
transition-duration: var(--transition-duration);
|
||||
transition-timing-function: var(--transition-easing);
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
color: var(--text-weak);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="context-menu-sub-trigger"] {
|
||||
&[data-expanded] {
|
||||
background: var(--surface-raised-base-hover);
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="context-menu-item-indicator"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
[data-slot="context-menu-item-label"] {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
[data-slot="context-menu-item-description"] {
|
||||
font-size: var(--font-size-x-small);
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="context-menu-separator"] {
|
||||
height: 1px;
|
||||
margin: 4px -4px;
|
||||
border-top-color: var(--border-weak-base);
|
||||
}
|
||||
|
||||
[data-slot="context-menu-group-label"] {
|
||||
padding: 4px 8px;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-x-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="context-menu-arrow"] {
|
||||
fill: var(--surface-raised-stronger-non-alpha);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes contextMenuContentShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scaleY(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes contextMenuContentHide {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scaleY(0.95);
|
||||
}
|
||||
}
|
||||
308
opencode/packages/ui/src/components/context-menu.tsx
Normal file
308
opencode/packages/ui/src/components/context-menu.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
import { ContextMenu as Kobalte } from "@kobalte/core/context-menu"
|
||||
import { splitProps } from "solid-js"
|
||||
import type { ComponentProps, ParentProps } from "solid-js"
|
||||
|
||||
export interface ContextMenuProps extends ComponentProps<typeof Kobalte> {}
|
||||
export interface ContextMenuTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {}
|
||||
export interface ContextMenuIconProps extends ComponentProps<typeof Kobalte.Icon> {}
|
||||
export interface ContextMenuPortalProps extends ComponentProps<typeof Kobalte.Portal> {}
|
||||
export interface ContextMenuContentProps extends ComponentProps<typeof Kobalte.Content> {}
|
||||
export interface ContextMenuArrowProps extends ComponentProps<typeof Kobalte.Arrow> {}
|
||||
export interface ContextMenuSeparatorProps extends ComponentProps<typeof Kobalte.Separator> {}
|
||||
export interface ContextMenuGroupProps extends ComponentProps<typeof Kobalte.Group> {}
|
||||
export interface ContextMenuGroupLabelProps extends ComponentProps<typeof Kobalte.GroupLabel> {}
|
||||
export interface ContextMenuItemProps extends ComponentProps<typeof Kobalte.Item> {}
|
||||
export interface ContextMenuItemLabelProps extends ComponentProps<typeof Kobalte.ItemLabel> {}
|
||||
export interface ContextMenuItemDescriptionProps extends ComponentProps<typeof Kobalte.ItemDescription> {}
|
||||
export interface ContextMenuItemIndicatorProps extends ComponentProps<typeof Kobalte.ItemIndicator> {}
|
||||
export interface ContextMenuRadioGroupProps extends ComponentProps<typeof Kobalte.RadioGroup> {}
|
||||
export interface ContextMenuRadioItemProps extends ComponentProps<typeof Kobalte.RadioItem> {}
|
||||
export interface ContextMenuCheckboxItemProps extends ComponentProps<typeof Kobalte.CheckboxItem> {}
|
||||
export interface ContextMenuSubProps extends ComponentProps<typeof Kobalte.Sub> {}
|
||||
export interface ContextMenuSubTriggerProps extends ComponentProps<typeof Kobalte.SubTrigger> {}
|
||||
export interface ContextMenuSubContentProps extends ComponentProps<typeof Kobalte.SubContent> {}
|
||||
|
||||
function ContextMenuRoot(props: ContextMenuProps) {
|
||||
return <Kobalte {...props} data-component="context-menu" />
|
||||
}
|
||||
|
||||
function ContextMenuTrigger(props: ParentProps<ContextMenuTriggerProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.Trigger
|
||||
{...rest}
|
||||
data-slot="context-menu-trigger"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuIcon(props: ParentProps<ContextMenuIconProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.Icon
|
||||
{...rest}
|
||||
data-slot="context-menu-icon"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.Icon>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuPortal(props: ContextMenuPortalProps) {
|
||||
return <Kobalte.Portal {...props} />
|
||||
}
|
||||
|
||||
function ContextMenuContent(props: ParentProps<ContextMenuContentProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.Content
|
||||
{...rest}
|
||||
data-component="context-menu-content"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.Content>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuArrow(props: ContextMenuArrowProps) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList"])
|
||||
return (
|
||||
<Kobalte.Arrow
|
||||
{...rest}
|
||||
data-slot="context-menu-arrow"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuSeparator(props: ContextMenuSeparatorProps) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList"])
|
||||
return (
|
||||
<Kobalte.Separator
|
||||
{...rest}
|
||||
data-slot="context-menu-separator"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuGroup(props: ParentProps<ContextMenuGroupProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.Group
|
||||
{...rest}
|
||||
data-slot="context-menu-group"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.Group>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuGroupLabel(props: ParentProps<ContextMenuGroupLabelProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.GroupLabel
|
||||
{...rest}
|
||||
data-slot="context-menu-group-label"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.GroupLabel>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuItem(props: ParentProps<ContextMenuItemProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.Item
|
||||
{...rest}
|
||||
data-slot="context-menu-item"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuItemLabel(props: ParentProps<ContextMenuItemLabelProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.ItemLabel
|
||||
{...rest}
|
||||
data-slot="context-menu-item-label"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.ItemLabel>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuItemDescription(props: ParentProps<ContextMenuItemDescriptionProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.ItemDescription
|
||||
{...rest}
|
||||
data-slot="context-menu-item-description"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.ItemDescription>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuItemIndicator(props: ParentProps<ContextMenuItemIndicatorProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.ItemIndicator
|
||||
{...rest}
|
||||
data-slot="context-menu-item-indicator"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.ItemIndicator>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup(props: ParentProps<ContextMenuRadioGroupProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.RadioGroup
|
||||
{...rest}
|
||||
data-slot="context-menu-radio-group"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.RadioGroup>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuRadioItem(props: ParentProps<ContextMenuRadioItemProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.RadioItem
|
||||
{...rest}
|
||||
data-slot="context-menu-radio-item"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuCheckboxItem(props: ParentProps<ContextMenuCheckboxItemProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.CheckboxItem
|
||||
{...rest}
|
||||
data-slot="context-menu-checkbox-item"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuSub(props: ContextMenuSubProps) {
|
||||
return <Kobalte.Sub {...props} />
|
||||
}
|
||||
|
||||
function ContextMenuSubTrigger(props: ParentProps<ContextMenuSubTriggerProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.SubTrigger
|
||||
{...rest}
|
||||
data-slot="context-menu-sub-trigger"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuSubContent(props: ParentProps<ContextMenuSubContentProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.SubContent
|
||||
{...rest}
|
||||
data-component="context-menu-sub-content"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.SubContent>
|
||||
)
|
||||
}
|
||||
|
||||
export const ContextMenu = Object.assign(ContextMenuRoot, {
|
||||
Trigger: ContextMenuTrigger,
|
||||
Icon: ContextMenuIcon,
|
||||
Portal: ContextMenuPortal,
|
||||
Content: ContextMenuContent,
|
||||
Arrow: ContextMenuArrow,
|
||||
Separator: ContextMenuSeparator,
|
||||
Group: ContextMenuGroup,
|
||||
GroupLabel: ContextMenuGroupLabel,
|
||||
Item: ContextMenuItem,
|
||||
ItemLabel: ContextMenuItemLabel,
|
||||
ItemDescription: ContextMenuItemDescription,
|
||||
ItemIndicator: ContextMenuItemIndicator,
|
||||
RadioGroup: ContextMenuRadioGroup,
|
||||
RadioItem: ContextMenuRadioItem,
|
||||
CheckboxItem: ContextMenuCheckboxItem,
|
||||
Sub: ContextMenuSub,
|
||||
SubTrigger: ContextMenuSubTrigger,
|
||||
SubContent: ContextMenuSubContent,
|
||||
})
|
||||
181
opencode/packages/ui/src/components/dialog.css
Normal file
181
opencode/packages/ui/src/components/dialog.css
Normal file
@@ -0,0 +1,181 @@
|
||||
/* [data-component="dialog-trigger"] { } */
|
||||
|
||||
[data-component="dialog-overlay"] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
background-color: hsl(from var(--background-base) h s l / 0.2);
|
||||
}
|
||||
|
||||
[data-component="dialog"] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
|
||||
[data-slot="dialog-container"] {
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
width: min(calc(100vw - 16px), 640px);
|
||||
height: min(calc(100vh - 16px), 512px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-items: start;
|
||||
overflow: visible;
|
||||
|
||||
[data-slot="dialog-content"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
min-height: 280px;
|
||||
overflow: auto;
|
||||
pointer-events: auto;
|
||||
|
||||
/* Hide scrollbar */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* padding: 8px; */
|
||||
/* padding: 8px 8px 0 8px; */
|
||||
border-radius: var(--radius-xl);
|
||||
background: var(--surface-raised-stronger-non-alpha);
|
||||
background-clip: padding-box;
|
||||
box-shadow: var(--shadow-lg-border-base);
|
||||
|
||||
[data-slot="dialog-header"] {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
align-self: stretch;
|
||||
|
||||
[data-slot="dialog-title"] {
|
||||
color: var(--text-strong);
|
||||
|
||||
/* text-16-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-large);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-x-large); /* 150% */
|
||||
letter-spacing: var(--letter-spacing-tight);
|
||||
}
|
||||
/* [data-slot="dialog-close-button"] {} */
|
||||
}
|
||||
|
||||
[data-slot="dialog-description"] {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
padding-left: 24px;
|
||||
padding-top: 0;
|
||||
margin-top: -8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
align-self: stretch;
|
||||
|
||||
color: var(--text-base);
|
||||
|
||||
/* text-14-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large); /* 142.857% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
[data-slot="dialog-body"] {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-fit] {
|
||||
[data-slot="dialog-container"] {
|
||||
height: auto;
|
||||
|
||||
[data-slot="dialog-content"] {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-size="large"] [data-slot="dialog-container"] {
|
||||
width: min(calc(100vw - 32px), 800px);
|
||||
height: min(calc(100vh - 32px), 600px);
|
||||
}
|
||||
|
||||
&[data-size="x-large"] [data-slot="dialog-container"] {
|
||||
width: min(calc(100vw - 32px), 960px);
|
||||
height: min(calc(100vh - 32px), 600px);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="dialog"][data-transition] [data-slot="dialog-content"] {
|
||||
animation: contentHide 100ms ease-in forwards;
|
||||
|
||||
&[data-expanded] {
|
||||
animation: contentShow 150ms ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes overlayShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes overlayHide {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes contentShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes contentHide {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
72
opencode/packages/ui/src/components/dialog.tsx
Normal file
72
opencode/packages/ui/src/components/dialog.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Dialog as Kobalte } from "@kobalte/core/dialog"
|
||||
import { ComponentProps, JSXElement, Match, ParentProps, Show, Switch } from "solid-js"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import { IconButton } from "./icon-button"
|
||||
|
||||
export interface DialogProps extends ParentProps {
|
||||
title?: JSXElement
|
||||
description?: JSXElement
|
||||
action?: JSXElement
|
||||
size?: "normal" | "large" | "x-large"
|
||||
class?: ComponentProps<"div">["class"]
|
||||
classList?: ComponentProps<"div">["classList"]
|
||||
fit?: boolean
|
||||
transition?: boolean
|
||||
}
|
||||
|
||||
export function Dialog(props: DialogProps) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<div
|
||||
data-component="dialog"
|
||||
data-fit={props.fit ? true : undefined}
|
||||
data-size={props.size || "normal"}
|
||||
data-transition={props.transition ? true : undefined}
|
||||
>
|
||||
<div data-slot="dialog-container">
|
||||
<Kobalte.Content
|
||||
data-slot="dialog-content"
|
||||
data-no-header={!props.title && !props.action ? "" : undefined}
|
||||
classList={{
|
||||
...(props.classList ?? {}),
|
||||
[props.class ?? ""]: !!props.class,
|
||||
}}
|
||||
onOpenAutoFocus={(e) => {
|
||||
const target = e.currentTarget as HTMLElement | null
|
||||
const autofocusEl = target?.querySelector("[autofocus]") as HTMLElement | null
|
||||
if (autofocusEl) {
|
||||
e.preventDefault()
|
||||
autofocusEl.focus()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Show when={props.title || props.action}>
|
||||
<div data-slot="dialog-header">
|
||||
<Show when={props.title}>
|
||||
<Kobalte.Title data-slot="dialog-title">{props.title}</Kobalte.Title>
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={props.action}>{props.action}</Match>
|
||||
<Match when={true}>
|
||||
<Kobalte.CloseButton
|
||||
data-slot="dialog-close-button"
|
||||
as={IconButton}
|
||||
icon="close"
|
||||
variant="ghost"
|
||||
aria-label={i18n.t("ui.common.close")}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.description}>
|
||||
<Kobalte.Description data-slot="dialog-description" style={{ "margin-left": "-4px" }}>
|
||||
{props.description}
|
||||
</Kobalte.Description>
|
||||
</Show>
|
||||
<div data-slot="dialog-body">{props.children}</div>
|
||||
</Kobalte.Content>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
41
opencode/packages/ui/src/components/diff-changes.css
Normal file
41
opencode/packages/ui/src/components/diff-changes.css
Normal file
@@ -0,0 +1,41 @@
|
||||
[data-component="diff-changes"] {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
[data-slot="diff-changes-additions"] {
|
||||
font-family: var(--font-family-mono);
|
||||
font-feature-settings: var(--font-family-mono--font-feature-settings);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
text-align: right;
|
||||
color: var(--text-diff-add-base);
|
||||
}
|
||||
|
||||
[data-slot="diff-changes-deletions"] {
|
||||
font-family: var(--font-family-mono);
|
||||
font-feature-settings: var(--font-family-mono--font-feature-settings);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
text-align: right;
|
||||
color: var(--text-diff-delete-base);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="diff-changes"][data-variant="bars"] {
|
||||
width: 18px;
|
||||
flex-shrink: 0;
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
115
opencode/packages/ui/src/components/diff-changes.tsx
Normal file
115
opencode/packages/ui/src/components/diff-changes.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { createMemo, For, Match, Show, Switch } from "solid-js"
|
||||
|
||||
export function DiffChanges(props: {
|
||||
class?: string
|
||||
changes: { additions: number; deletions: number } | { additions: number; deletions: number }[]
|
||||
variant?: "default" | "bars"
|
||||
}) {
|
||||
const variant = () => props.variant ?? "default"
|
||||
|
||||
const additions = createMemo(() =>
|
||||
Array.isArray(props.changes)
|
||||
? props.changes.reduce((acc, diff) => acc + (diff.additions ?? 0), 0)
|
||||
: props.changes.additions,
|
||||
)
|
||||
const deletions = createMemo(() =>
|
||||
Array.isArray(props.changes)
|
||||
? props.changes.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0)
|
||||
: props.changes.deletions,
|
||||
)
|
||||
const total = createMemo(() => (additions() ?? 0) + (deletions() ?? 0))
|
||||
|
||||
const blockCounts = createMemo(() => {
|
||||
const TOTAL_BLOCKS = 5
|
||||
|
||||
const adds = additions() ?? 0
|
||||
const dels = deletions() ?? 0
|
||||
|
||||
if (adds === 0 && dels === 0) {
|
||||
return { added: 0, deleted: 0, neutral: TOTAL_BLOCKS }
|
||||
}
|
||||
|
||||
const total = adds + dels
|
||||
|
||||
if (total < 5) {
|
||||
const added = adds > 0 ? 1 : 0
|
||||
const deleted = dels > 0 ? 1 : 0
|
||||
const neutral = TOTAL_BLOCKS - added - deleted
|
||||
return { added, deleted, neutral }
|
||||
}
|
||||
|
||||
const ratio = adds > dels ? adds / dels : dels / adds
|
||||
let BLOCKS_FOR_COLORS = TOTAL_BLOCKS
|
||||
|
||||
if (total < 20) {
|
||||
BLOCKS_FOR_COLORS = TOTAL_BLOCKS - 1
|
||||
} else if (ratio < 4) {
|
||||
BLOCKS_FOR_COLORS = TOTAL_BLOCKS - 1
|
||||
}
|
||||
|
||||
const percentAdded = adds / total
|
||||
const percentDeleted = dels / total
|
||||
|
||||
const added_raw = percentAdded * BLOCKS_FOR_COLORS
|
||||
const deleted_raw = percentDeleted * BLOCKS_FOR_COLORS
|
||||
|
||||
let added = adds > 0 ? Math.max(1, Math.round(added_raw)) : 0
|
||||
let deleted = dels > 0 ? Math.max(1, Math.round(deleted_raw)) : 0
|
||||
|
||||
// Cap bars based on actual change magnitude
|
||||
if (adds > 0 && adds <= 5) added = Math.min(added, 1)
|
||||
if (adds > 5 && adds <= 10) added = Math.min(added, 2)
|
||||
if (dels > 0 && dels <= 5) deleted = Math.min(deleted, 1)
|
||||
if (dels > 5 && dels <= 10) deleted = Math.min(deleted, 2)
|
||||
|
||||
let total_allocated = added + deleted
|
||||
if (total_allocated > BLOCKS_FOR_COLORS) {
|
||||
if (added_raw > deleted_raw) {
|
||||
added = BLOCKS_FOR_COLORS - deleted
|
||||
} else {
|
||||
deleted = BLOCKS_FOR_COLORS - added
|
||||
}
|
||||
total_allocated = added + deleted
|
||||
}
|
||||
|
||||
const neutral = Math.max(0, TOTAL_BLOCKS - total_allocated)
|
||||
|
||||
return { added, deleted, neutral }
|
||||
})
|
||||
|
||||
const ADD_COLOR = "var(--icon-diff-add-base)"
|
||||
const DELETE_COLOR = "var(--icon-diff-delete-base)"
|
||||
const NEUTRAL_COLOR = "var(--icon-weak-base)"
|
||||
|
||||
const visibleBlocks = createMemo(() => {
|
||||
const counts = blockCounts()
|
||||
const blocks = [
|
||||
...Array(counts.added).fill(ADD_COLOR),
|
||||
...Array(counts.deleted).fill(DELETE_COLOR),
|
||||
...Array(counts.neutral).fill(NEUTRAL_COLOR),
|
||||
]
|
||||
return blocks.slice(0, 5)
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={variant() === "default" ? total() > 0 : true}>
|
||||
<div data-component="diff-changes" data-variant={variant()} classList={{ [props.class ?? ""]: true }}>
|
||||
<Switch>
|
||||
<Match when={variant() === "bars"}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 12" fill="none">
|
||||
<g>
|
||||
<For each={visibleBlocks()}>
|
||||
{(color, i) => <rect x={i() * 4} width="2" height="12" rx="1" fill={color} />}
|
||||
</For>
|
||||
</g>
|
||||
</svg>
|
||||
</Match>
|
||||
<Match when={variant() === "default"}>
|
||||
<span data-slot="diff-changes-additions">{`+${additions()}`}</span>
|
||||
<span data-slot="diff-changes-deletions">{`-${deletions()}`}</span>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
287
opencode/packages/ui/src/components/diff-ssr.tsx
Normal file
287
opencode/packages/ui/src/components/diff-ssr.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
import { DIFFS_TAG_NAME, FileDiff, type SelectedLineRange } from "@pierre/diffs"
|
||||
import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
||||
import { createEffect, onCleanup, onMount, Show, splitProps } from "solid-js"
|
||||
import { Dynamic, isServer } from "solid-js/web"
|
||||
import { createDefaultOptions, styleVariables, type DiffProps } from "../pierre"
|
||||
import { useWorkerPool } from "../context/worker-pool"
|
||||
|
||||
export type SSRDiffProps<T = {}> = DiffProps<T> & {
|
||||
preloadedDiff: PreloadMultiFileDiffResult<T>
|
||||
}
|
||||
|
||||
export function Diff<T>(props: SSRDiffProps<T>) {
|
||||
let container!: HTMLDivElement
|
||||
let fileDiffRef!: HTMLElement
|
||||
const [local, others] = splitProps(props, [
|
||||
"before",
|
||||
"after",
|
||||
"class",
|
||||
"classList",
|
||||
"annotations",
|
||||
"selectedLines",
|
||||
"commentedLines",
|
||||
])
|
||||
const workerPool = useWorkerPool(props.diffStyle)
|
||||
|
||||
let fileDiffInstance: FileDiff<T> | undefined
|
||||
const cleanupFunctions: Array<() => void> = []
|
||||
|
||||
const getRoot = () => fileDiffRef?.shadowRoot ?? undefined
|
||||
|
||||
const applyScheme = () => {
|
||||
const scheme = document.documentElement.dataset.colorScheme
|
||||
if (scheme === "dark" || scheme === "light") {
|
||||
fileDiffRef.dataset.colorScheme = scheme
|
||||
return
|
||||
}
|
||||
|
||||
fileDiffRef.removeAttribute("data-color-scheme")
|
||||
}
|
||||
|
||||
const lineIndex = (split: boolean, element: HTMLElement) => {
|
||||
const raw = element.dataset.lineIndex
|
||||
if (!raw) return
|
||||
const values = raw
|
||||
.split(",")
|
||||
.map((value) => parseInt(value, 10))
|
||||
.filter((value) => !Number.isNaN(value))
|
||||
if (values.length === 0) return
|
||||
if (!split) return values[0]
|
||||
if (values.length === 2) return values[1]
|
||||
return values[0]
|
||||
}
|
||||
|
||||
const rowIndex = (root: ShadowRoot, split: boolean, line: number, side: "additions" | "deletions" | undefined) => {
|
||||
const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-alt-line="${line}"]`)).filter(
|
||||
(node): node is HTMLElement => node instanceof HTMLElement,
|
||||
)
|
||||
if (nodes.length === 0) return
|
||||
|
||||
const targetSide = side ?? "additions"
|
||||
|
||||
for (const node of nodes) {
|
||||
if (findSide(node) === targetSide) return lineIndex(split, node)
|
||||
if (parseInt(node.dataset.altLine ?? "", 10) === line) return lineIndex(split, node)
|
||||
}
|
||||
}
|
||||
|
||||
const fixSelection = (range: SelectedLineRange | null) => {
|
||||
if (!range) return range
|
||||
const root = getRoot()
|
||||
if (!root) return
|
||||
|
||||
const diffs = root.querySelector("[data-diffs]")
|
||||
if (!(diffs instanceof HTMLElement)) return
|
||||
|
||||
const split = diffs.dataset.type === "split"
|
||||
|
||||
const start = rowIndex(root, split, range.start, range.side)
|
||||
const end = rowIndex(root, split, range.end, range.endSide ?? range.side)
|
||||
|
||||
if (start === undefined || end === undefined) {
|
||||
if (root.querySelector("[data-line], [data-alt-line]") == null) return
|
||||
return null
|
||||
}
|
||||
if (start <= end) return range
|
||||
|
||||
const side = range.endSide ?? range.side
|
||||
const swapped: SelectedLineRange = {
|
||||
start: range.end,
|
||||
end: range.start,
|
||||
}
|
||||
if (side) swapped.side = side
|
||||
if (range.endSide && range.side) swapped.endSide = range.side
|
||||
|
||||
return swapped
|
||||
}
|
||||
|
||||
const setSelectedLines = (range: SelectedLineRange | null, attempt = 0) => {
|
||||
const diff = fileDiffInstance
|
||||
if (!diff) return
|
||||
|
||||
const fixed = fixSelection(range)
|
||||
if (fixed === undefined) {
|
||||
if (attempt >= 120) return
|
||||
requestAnimationFrame(() => setSelectedLines(range, attempt + 1))
|
||||
return
|
||||
}
|
||||
|
||||
diff.setSelectedLines(fixed)
|
||||
}
|
||||
|
||||
const findSide = (element: HTMLElement): "additions" | "deletions" => {
|
||||
const line = element.closest("[data-line], [data-alt-line]")
|
||||
if (line instanceof HTMLElement) {
|
||||
const type = line.dataset.lineType
|
||||
if (type === "change-deletion") return "deletions"
|
||||
if (type === "change-addition" || type === "change-additions") return "additions"
|
||||
}
|
||||
|
||||
const code = element.closest("[data-code]")
|
||||
if (!(code instanceof HTMLElement)) return "additions"
|
||||
return code.hasAttribute("data-deletions") ? "deletions" : "additions"
|
||||
}
|
||||
|
||||
const applyCommentedLines = (ranges: SelectedLineRange[]) => {
|
||||
const root = getRoot()
|
||||
if (!root) return
|
||||
|
||||
const existing = Array.from(root.querySelectorAll("[data-comment-selected]"))
|
||||
for (const node of existing) {
|
||||
if (!(node instanceof HTMLElement)) continue
|
||||
node.removeAttribute("data-comment-selected")
|
||||
}
|
||||
|
||||
const diffs = root.querySelector("[data-diffs]")
|
||||
if (!(diffs instanceof HTMLElement)) return
|
||||
|
||||
const split = diffs.dataset.type === "split"
|
||||
|
||||
const code = Array.from(diffs.querySelectorAll("[data-code]")).filter(
|
||||
(node): node is HTMLElement => node instanceof HTMLElement,
|
||||
)
|
||||
if (code.length === 0) return
|
||||
|
||||
const lineIndex = (element: HTMLElement) => {
|
||||
const raw = element.dataset.lineIndex
|
||||
if (!raw) return
|
||||
const values = raw
|
||||
.split(",")
|
||||
.map((value) => parseInt(value, 10))
|
||||
.filter((value) => !Number.isNaN(value))
|
||||
if (values.length === 0) return
|
||||
if (!split) return values[0]
|
||||
if (values.length === 2) return values[1]
|
||||
return values[0]
|
||||
}
|
||||
|
||||
const rowIndex = (line: number, side: "additions" | "deletions" | undefined) => {
|
||||
const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-alt-line="${line}"]`)).filter(
|
||||
(node): node is HTMLElement => node instanceof HTMLElement,
|
||||
)
|
||||
if (nodes.length === 0) return
|
||||
|
||||
const targetSide = side ?? "additions"
|
||||
|
||||
for (const node of nodes) {
|
||||
if (findSide(node) === targetSide) return lineIndex(node)
|
||||
if (parseInt(node.dataset.altLine ?? "", 10) === line) return lineIndex(node)
|
||||
}
|
||||
}
|
||||
|
||||
for (const range of ranges) {
|
||||
const start = rowIndex(range.start, range.side)
|
||||
if (start === undefined) continue
|
||||
|
||||
const end = (() => {
|
||||
const same = range.end === range.start && (range.endSide == null || range.endSide === range.side)
|
||||
if (same) return start
|
||||
return rowIndex(range.end, range.endSide ?? range.side)
|
||||
})()
|
||||
if (end === undefined) continue
|
||||
|
||||
const first = Math.min(start, end)
|
||||
const last = Math.max(start, end)
|
||||
|
||||
for (const block of code) {
|
||||
for (const element of Array.from(block.children)) {
|
||||
if (!(element instanceof HTMLElement)) continue
|
||||
const idx = lineIndex(element)
|
||||
if (idx === undefined) continue
|
||||
if (idx > last) break
|
||||
if (idx < first) continue
|
||||
element.setAttribute("data-comment-selected", "")
|
||||
const next = element.nextSibling
|
||||
if (next instanceof HTMLElement && next.hasAttribute("data-line-annotation")) {
|
||||
next.setAttribute("data-comment-selected", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (isServer || !props.preloadedDiff) return
|
||||
|
||||
applyScheme()
|
||||
|
||||
if (typeof MutationObserver !== "undefined") {
|
||||
const root = document.documentElement
|
||||
const monitor = new MutationObserver(() => applyScheme())
|
||||
monitor.observe(root, { attributes: true, attributeFilter: ["data-color-scheme"] })
|
||||
onCleanup(() => monitor.disconnect())
|
||||
}
|
||||
|
||||
fileDiffInstance = new FileDiff<T>(
|
||||
{
|
||||
...createDefaultOptions(props.diffStyle),
|
||||
...others,
|
||||
...props.preloadedDiff,
|
||||
},
|
||||
workerPool,
|
||||
)
|
||||
// @ts-expect-error - fileContainer is private but needed for SSR hydration
|
||||
fileDiffInstance.fileContainer = fileDiffRef
|
||||
fileDiffInstance.hydrate({
|
||||
oldFile: local.before,
|
||||
newFile: local.after,
|
||||
lineAnnotations: local.annotations,
|
||||
fileContainer: fileDiffRef,
|
||||
containerWrapper: container,
|
||||
})
|
||||
|
||||
setSelectedLines(local.selectedLines ?? null)
|
||||
|
||||
createEffect(() => {
|
||||
fileDiffInstance?.setLineAnnotations(local.annotations ?? [])
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
setSelectedLines(local.selectedLines ?? null)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const ranges = local.commentedLines ?? []
|
||||
requestAnimationFrame(() => applyCommentedLines(ranges))
|
||||
})
|
||||
|
||||
// Hydrate annotation slots with interactive SolidJS components
|
||||
// if (props.annotations.length > 0 && props.renderAnnotation != null) {
|
||||
// for (const annotation of props.annotations) {
|
||||
// const slotName = `annotation-${annotation.side}-${annotation.lineNumber}`;
|
||||
// const slotElement = fileDiffRef.querySelector(
|
||||
// `[slot="${slotName}"]`
|
||||
// ) as HTMLElement;
|
||||
//
|
||||
// if (slotElement != null) {
|
||||
// // Clear the static server-rendered content from the slot
|
||||
// slotElement.innerHTML = '';
|
||||
//
|
||||
// // Mount a fresh SolidJS component into this slot using render().
|
||||
// // This enables full SolidJS reactivity (signals, effects, etc.)
|
||||
// const dispose = render(
|
||||
// () => props.renderAnnotation!(annotation),
|
||||
// slotElement
|
||||
// );
|
||||
// cleanupFunctions.push(dispose);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
// Clean up FileDiff event handlers and dispose SolidJS components
|
||||
fileDiffInstance?.cleanUp()
|
||||
cleanupFunctions.forEach((dispose) => dispose())
|
||||
})
|
||||
|
||||
return (
|
||||
<div data-component="diff" style={styleVariables} ref={container}>
|
||||
<Dynamic component={DIFFS_TAG_NAME} ref={fileDiffRef} id="ssr-diff">
|
||||
<Show when={isServer}>
|
||||
<template shadowrootmode="open" innerHTML={props.preloadedDiff.prerenderedHTML} />
|
||||
</Show>
|
||||
</Dynamic>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
35
opencode/packages/ui/src/components/diff.css
Normal file
35
opencode/packages/ui/src/components/diff.css
Normal file
@@ -0,0 +1,35 @@
|
||||
[data-component="diff"] {
|
||||
content-visibility: auto;
|
||||
|
||||
[data-slot="diff-hunk-separator-line-number"] {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background-color: var(--surface-diff-hidden-strong);
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
[data-slot="diff-hunk-separator-line-number-icon"] {
|
||||
aspect-ratio: 1;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--icon-strong-base);
|
||||
}
|
||||
}
|
||||
[data-slot="diff-hunk-separator-content"] {
|
||||
position: sticky;
|
||||
background-color: var(--surface-diff-hidden-base);
|
||||
color: var(--text-base);
|
||||
width: var(--diffs-column-content-width);
|
||||
left: var(--diffs-column-number-width);
|
||||
padding-left: 8px;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
text-align: left;
|
||||
|
||||
[data-slot="diff-hunk-separator-content-span"] {
|
||||
mix-blend-mode: var(--text-mix-blend-mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
612
opencode/packages/ui/src/components/diff.tsx
Normal file
612
opencode/packages/ui/src/components/diff.tsx
Normal file
@@ -0,0 +1,612 @@
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import { FileDiff, type SelectedLineRange } from "@pierre/diffs"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { createEffect, createMemo, createSignal, onCleanup, splitProps } from "solid-js"
|
||||
import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
|
||||
import { getWorkerPool } from "../pierre/worker"
|
||||
|
||||
type SelectionSide = "additions" | "deletions"
|
||||
|
||||
function findElement(node: Node | null): HTMLElement | undefined {
|
||||
if (!node) return
|
||||
if (node instanceof HTMLElement) return node
|
||||
return node.parentElement ?? undefined
|
||||
}
|
||||
|
||||
function findLineNumber(node: Node | null): number | undefined {
|
||||
const element = findElement(node)
|
||||
if (!element) return
|
||||
|
||||
const line = element.closest("[data-line], [data-alt-line]")
|
||||
if (!(line instanceof HTMLElement)) return
|
||||
|
||||
const value = (() => {
|
||||
const primary = parseInt(line.dataset.line ?? "", 10)
|
||||
if (!Number.isNaN(primary)) return primary
|
||||
|
||||
const alt = parseInt(line.dataset.altLine ?? "", 10)
|
||||
if (!Number.isNaN(alt)) return alt
|
||||
})()
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function findSide(node: Node | null): SelectionSide | undefined {
|
||||
const element = findElement(node)
|
||||
if (!element) return
|
||||
|
||||
const line = element.closest("[data-line], [data-alt-line]")
|
||||
if (line instanceof HTMLElement) {
|
||||
const type = line.dataset.lineType
|
||||
if (type === "change-deletion") return "deletions"
|
||||
if (type === "change-addition" || type === "change-additions") return "additions"
|
||||
}
|
||||
|
||||
const code = element.closest("[data-code]")
|
||||
if (!(code instanceof HTMLElement)) return
|
||||
|
||||
if (code.hasAttribute("data-deletions")) return "deletions"
|
||||
return "additions"
|
||||
}
|
||||
|
||||
export function Diff<T>(props: DiffProps<T>) {
|
||||
let container!: HTMLDivElement
|
||||
let observer: MutationObserver | undefined
|
||||
let renderToken = 0
|
||||
let selectionFrame: number | undefined
|
||||
let dragFrame: number | undefined
|
||||
let dragStart: number | undefined
|
||||
let dragEnd: number | undefined
|
||||
let dragSide: SelectionSide | undefined
|
||||
let dragEndSide: SelectionSide | undefined
|
||||
let dragMoved = false
|
||||
let lastSelection: SelectedLineRange | null = null
|
||||
let pendingSelectionEnd = false
|
||||
|
||||
const [local, others] = splitProps(props, [
|
||||
"before",
|
||||
"after",
|
||||
"class",
|
||||
"classList",
|
||||
"annotations",
|
||||
"selectedLines",
|
||||
"commentedLines",
|
||||
"onRendered",
|
||||
])
|
||||
|
||||
const mobile = createMediaQuery("(max-width: 640px)")
|
||||
|
||||
const options = createMemo(() => {
|
||||
const opts = {
|
||||
...createDefaultOptions(props.diffStyle),
|
||||
...others,
|
||||
}
|
||||
if (!mobile()) return opts
|
||||
return {
|
||||
...opts,
|
||||
disableLineNumbers: true,
|
||||
}
|
||||
})
|
||||
|
||||
let instance: FileDiff<T> | undefined
|
||||
const [current, setCurrent] = createSignal<FileDiff<T> | undefined>(undefined)
|
||||
const [rendered, setRendered] = createSignal(0)
|
||||
|
||||
const getRoot = () => {
|
||||
const host = container.querySelector("diffs-container")
|
||||
if (!(host instanceof HTMLElement)) return
|
||||
|
||||
const root = host.shadowRoot
|
||||
if (!root) return
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
const applyScheme = () => {
|
||||
const host = container.querySelector("diffs-container")
|
||||
if (!(host instanceof HTMLElement)) return
|
||||
|
||||
const scheme = document.documentElement.dataset.colorScheme
|
||||
if (scheme === "dark" || scheme === "light") {
|
||||
host.dataset.colorScheme = scheme
|
||||
return
|
||||
}
|
||||
|
||||
host.removeAttribute("data-color-scheme")
|
||||
}
|
||||
|
||||
const lineIndex = (split: boolean, element: HTMLElement) => {
|
||||
const raw = element.dataset.lineIndex
|
||||
if (!raw) return
|
||||
const values = raw
|
||||
.split(",")
|
||||
.map((value) => parseInt(value, 10))
|
||||
.filter((value) => !Number.isNaN(value))
|
||||
if (values.length === 0) return
|
||||
if (!split) return values[0]
|
||||
if (values.length === 2) return values[1]
|
||||
return values[0]
|
||||
}
|
||||
|
||||
const rowIndex = (root: ShadowRoot, split: boolean, line: number, side: SelectionSide | undefined) => {
|
||||
const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-alt-line="${line}"]`)).filter(
|
||||
(node): node is HTMLElement => node instanceof HTMLElement,
|
||||
)
|
||||
if (nodes.length === 0) return
|
||||
|
||||
const targetSide = side ?? "additions"
|
||||
|
||||
for (const node of nodes) {
|
||||
if (findSide(node) === targetSide) return lineIndex(split, node)
|
||||
if (parseInt(node.dataset.altLine ?? "", 10) === line) return lineIndex(split, node)
|
||||
}
|
||||
}
|
||||
|
||||
const fixSelection = (range: SelectedLineRange | null) => {
|
||||
if (!range) return range
|
||||
const root = getRoot()
|
||||
if (!root) return
|
||||
|
||||
const diffs = root.querySelector("[data-diffs]")
|
||||
if (!(diffs instanceof HTMLElement)) return
|
||||
|
||||
const split = diffs.dataset.type === "split"
|
||||
|
||||
const start = rowIndex(root, split, range.start, range.side)
|
||||
const end = rowIndex(root, split, range.end, range.endSide ?? range.side)
|
||||
if (start === undefined || end === undefined) {
|
||||
if (root.querySelector("[data-line], [data-alt-line]") == null) return
|
||||
return null
|
||||
}
|
||||
if (start <= end) return range
|
||||
|
||||
const side = range.endSide ?? range.side
|
||||
const swapped: SelectedLineRange = {
|
||||
start: range.end,
|
||||
end: range.start,
|
||||
}
|
||||
|
||||
if (side) swapped.side = side
|
||||
if (range.endSide && range.side) swapped.endSide = range.side
|
||||
|
||||
return swapped
|
||||
}
|
||||
|
||||
const notifyRendered = () => {
|
||||
observer?.disconnect()
|
||||
observer = undefined
|
||||
renderToken++
|
||||
|
||||
const token = renderToken
|
||||
let settle = 0
|
||||
|
||||
const isReady = (root: ShadowRoot) => root.querySelector("[data-line]") != null
|
||||
|
||||
const notify = () => {
|
||||
if (token !== renderToken) return
|
||||
|
||||
observer?.disconnect()
|
||||
observer = undefined
|
||||
requestAnimationFrame(() => {
|
||||
if (token !== renderToken) return
|
||||
setSelectedLines(lastSelection)
|
||||
local.onRendered?.()
|
||||
})
|
||||
}
|
||||
|
||||
const schedule = () => {
|
||||
settle++
|
||||
const current = settle
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (token !== renderToken) return
|
||||
if (current !== settle) return
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (token !== renderToken) return
|
||||
if (current !== settle) return
|
||||
|
||||
notify()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const observeRoot = (root: ShadowRoot) => {
|
||||
observer?.disconnect()
|
||||
observer = new MutationObserver(() => {
|
||||
if (token !== renderToken) return
|
||||
if (!isReady(root)) return
|
||||
|
||||
schedule()
|
||||
})
|
||||
|
||||
observer.observe(root, { childList: true, subtree: true })
|
||||
|
||||
if (!isReady(root)) return
|
||||
schedule()
|
||||
}
|
||||
|
||||
const root = getRoot()
|
||||
if (typeof MutationObserver === "undefined") {
|
||||
if (!root || !isReady(root)) return
|
||||
setSelectedLines(lastSelection)
|
||||
local.onRendered?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (root) {
|
||||
observeRoot(root)
|
||||
return
|
||||
}
|
||||
|
||||
observer = new MutationObserver(() => {
|
||||
if (token !== renderToken) return
|
||||
|
||||
const root = getRoot()
|
||||
if (!root) return
|
||||
|
||||
observeRoot(root)
|
||||
})
|
||||
|
||||
observer.observe(container, { childList: true, subtree: true })
|
||||
}
|
||||
|
||||
const applyCommentedLines = (ranges: SelectedLineRange[]) => {
|
||||
const root = getRoot()
|
||||
if (!root) return
|
||||
|
||||
const existing = Array.from(root.querySelectorAll("[data-comment-selected]"))
|
||||
for (const node of existing) {
|
||||
if (!(node instanceof HTMLElement)) continue
|
||||
node.removeAttribute("data-comment-selected")
|
||||
}
|
||||
|
||||
const diffs = root.querySelector("[data-diffs]")
|
||||
if (!(diffs instanceof HTMLElement)) return
|
||||
|
||||
const split = diffs.dataset.type === "split"
|
||||
|
||||
const code = Array.from(diffs.querySelectorAll("[data-code]")).filter(
|
||||
(node): node is HTMLElement => node instanceof HTMLElement,
|
||||
)
|
||||
if (code.length === 0) return
|
||||
|
||||
for (const range of ranges) {
|
||||
const start = rowIndex(root, split, range.start, range.side)
|
||||
if (start === undefined) continue
|
||||
|
||||
const end = (() => {
|
||||
const same = range.end === range.start && (range.endSide == null || range.endSide === range.side)
|
||||
if (same) return start
|
||||
return rowIndex(root, split, range.end, range.endSide ?? range.side)
|
||||
})()
|
||||
if (end === undefined) continue
|
||||
|
||||
const first = Math.min(start, end)
|
||||
const last = Math.max(start, end)
|
||||
|
||||
for (const block of code) {
|
||||
for (const element of Array.from(block.children)) {
|
||||
if (!(element instanceof HTMLElement)) continue
|
||||
const idx = lineIndex(split, element)
|
||||
if (idx === undefined) continue
|
||||
if (idx > last) break
|
||||
if (idx < first) continue
|
||||
element.setAttribute("data-comment-selected", "")
|
||||
const next = element.nextSibling
|
||||
if (next instanceof HTMLElement && next.hasAttribute("data-line-annotation")) {
|
||||
next.setAttribute("data-comment-selected", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setSelectedLines = (range: SelectedLineRange | null) => {
|
||||
const active = current()
|
||||
if (!active) return
|
||||
|
||||
const fixed = fixSelection(range)
|
||||
if (fixed === undefined) {
|
||||
lastSelection = range
|
||||
return
|
||||
}
|
||||
|
||||
lastSelection = fixed
|
||||
active.setSelectedLines(fixed)
|
||||
}
|
||||
|
||||
const updateSelection = () => {
|
||||
const root = getRoot()
|
||||
if (!root) return
|
||||
|
||||
const selection =
|
||||
(root as unknown as { getSelection?: () => Selection | null }).getSelection?.() ?? window.getSelection()
|
||||
if (!selection || selection.isCollapsed) return
|
||||
|
||||
const domRange =
|
||||
(
|
||||
selection as unknown as {
|
||||
getComposedRanges?: (options?: { shadowRoots?: ShadowRoot[] }) => Range[]
|
||||
}
|
||||
).getComposedRanges?.({ shadowRoots: [root] })?.[0] ??
|
||||
(selection.rangeCount > 0 ? selection.getRangeAt(0) : undefined)
|
||||
|
||||
const startNode = domRange?.startContainer ?? selection.anchorNode
|
||||
const endNode = domRange?.endContainer ?? selection.focusNode
|
||||
if (!startNode || !endNode) return
|
||||
|
||||
if (!root.contains(startNode) || !root.contains(endNode)) return
|
||||
|
||||
const start = findLineNumber(startNode)
|
||||
const end = findLineNumber(endNode)
|
||||
if (start === undefined || end === undefined) return
|
||||
|
||||
const startSide = findSide(startNode)
|
||||
const endSide = findSide(endNode)
|
||||
const side = startSide ?? endSide
|
||||
|
||||
const selected: SelectedLineRange = {
|
||||
start,
|
||||
end,
|
||||
}
|
||||
|
||||
if (side) selected.side = side
|
||||
if (endSide && side && endSide !== side) selected.endSide = endSide
|
||||
|
||||
setSelectedLines(selected)
|
||||
}
|
||||
|
||||
const scheduleSelectionUpdate = () => {
|
||||
if (selectionFrame !== undefined) return
|
||||
|
||||
selectionFrame = requestAnimationFrame(() => {
|
||||
selectionFrame = undefined
|
||||
updateSelection()
|
||||
|
||||
if (!pendingSelectionEnd) return
|
||||
pendingSelectionEnd = false
|
||||
props.onLineSelectionEnd?.(lastSelection)
|
||||
})
|
||||
}
|
||||
|
||||
const updateDragSelection = () => {
|
||||
if (dragStart === undefined || dragEnd === undefined) return
|
||||
|
||||
const selected: SelectedLineRange = {
|
||||
start: dragStart,
|
||||
end: dragEnd,
|
||||
}
|
||||
|
||||
if (dragSide) selected.side = dragSide
|
||||
if (dragEndSide && dragSide && dragEndSide !== dragSide) selected.endSide = dragEndSide
|
||||
|
||||
setSelectedLines(selected)
|
||||
}
|
||||
|
||||
const scheduleDragUpdate = () => {
|
||||
if (dragFrame !== undefined) return
|
||||
|
||||
dragFrame = requestAnimationFrame(() => {
|
||||
dragFrame = undefined
|
||||
updateDragSelection()
|
||||
})
|
||||
}
|
||||
|
||||
const lineFromMouseEvent = (event: MouseEvent) => {
|
||||
const path = event.composedPath()
|
||||
|
||||
let numberColumn = false
|
||||
let line: number | undefined
|
||||
let side: SelectionSide | undefined
|
||||
|
||||
for (const item of path) {
|
||||
if (!(item instanceof HTMLElement)) continue
|
||||
|
||||
numberColumn = numberColumn || item.dataset.columnNumber != null
|
||||
|
||||
if (side === undefined) {
|
||||
const type = item.dataset.lineType
|
||||
if (type === "change-deletion") side = "deletions"
|
||||
if (type === "change-addition" || type === "change-additions") side = "additions"
|
||||
}
|
||||
|
||||
if (side === undefined && item.dataset.code != null) {
|
||||
side = item.hasAttribute("data-deletions") ? "deletions" : "additions"
|
||||
}
|
||||
|
||||
if (line === undefined) {
|
||||
const primary = item.dataset.line ? parseInt(item.dataset.line, 10) : Number.NaN
|
||||
if (!Number.isNaN(primary)) {
|
||||
line = primary
|
||||
} else {
|
||||
const alt = item.dataset.altLine ? parseInt(item.dataset.altLine, 10) : Number.NaN
|
||||
if (!Number.isNaN(alt)) line = alt
|
||||
}
|
||||
}
|
||||
|
||||
if (numberColumn && line !== undefined && side !== undefined) break
|
||||
}
|
||||
|
||||
return { line, numberColumn, side }
|
||||
}
|
||||
|
||||
const handleMouseDown = (event: MouseEvent) => {
|
||||
if (props.enableLineSelection !== true) return
|
||||
if (event.button !== 0) return
|
||||
|
||||
const { line, numberColumn, side } = lineFromMouseEvent(event)
|
||||
if (numberColumn) return
|
||||
if (line === undefined) return
|
||||
|
||||
dragStart = line
|
||||
dragEnd = line
|
||||
dragSide = side
|
||||
dragEndSide = side
|
||||
dragMoved = false
|
||||
}
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (props.enableLineSelection !== true) return
|
||||
if (dragStart === undefined) return
|
||||
|
||||
if ((event.buttons & 1) === 0) {
|
||||
dragStart = undefined
|
||||
dragEnd = undefined
|
||||
dragSide = undefined
|
||||
dragEndSide = undefined
|
||||
dragMoved = false
|
||||
return
|
||||
}
|
||||
|
||||
const { line, side } = lineFromMouseEvent(event)
|
||||
if (line === undefined) return
|
||||
|
||||
dragEnd = line
|
||||
dragEndSide = side
|
||||
dragMoved = true
|
||||
scheduleDragUpdate()
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (props.enableLineSelection !== true) return
|
||||
if (dragStart === undefined) return
|
||||
|
||||
if (!dragMoved) {
|
||||
pendingSelectionEnd = false
|
||||
const line = dragStart
|
||||
const selected: SelectedLineRange = {
|
||||
start: line,
|
||||
end: line,
|
||||
}
|
||||
if (dragSide) selected.side = dragSide
|
||||
setSelectedLines(selected)
|
||||
props.onLineSelectionEnd?.(lastSelection)
|
||||
dragStart = undefined
|
||||
dragEnd = undefined
|
||||
dragSide = undefined
|
||||
dragEndSide = undefined
|
||||
dragMoved = false
|
||||
return
|
||||
}
|
||||
|
||||
pendingSelectionEnd = true
|
||||
scheduleDragUpdate()
|
||||
scheduleSelectionUpdate()
|
||||
|
||||
dragStart = undefined
|
||||
dragEnd = undefined
|
||||
dragSide = undefined
|
||||
dragEndSide = undefined
|
||||
dragMoved = false
|
||||
}
|
||||
|
||||
const handleSelectionChange = () => {
|
||||
if (props.enableLineSelection !== true) return
|
||||
if (dragStart === undefined) return
|
||||
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.isCollapsed) return
|
||||
|
||||
scheduleSelectionUpdate()
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const opts = options()
|
||||
const workerPool = getWorkerPool(props.diffStyle)
|
||||
const annotations = local.annotations
|
||||
const beforeContents = typeof local.before?.contents === "string" ? local.before.contents : ""
|
||||
const afterContents = typeof local.after?.contents === "string" ? local.after.contents : ""
|
||||
|
||||
instance?.cleanUp()
|
||||
instance = new FileDiff<T>(opts, workerPool)
|
||||
setCurrent(instance)
|
||||
|
||||
container.innerHTML = ""
|
||||
instance.render({
|
||||
oldFile: {
|
||||
...local.before,
|
||||
contents: beforeContents,
|
||||
cacheKey: checksum(beforeContents),
|
||||
},
|
||||
newFile: {
|
||||
...local.after,
|
||||
contents: afterContents,
|
||||
cacheKey: checksum(afterContents),
|
||||
},
|
||||
lineAnnotations: annotations,
|
||||
containerWrapper: container,
|
||||
})
|
||||
|
||||
applyScheme()
|
||||
|
||||
setRendered((value) => value + 1)
|
||||
notifyRendered()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof document === "undefined") return
|
||||
if (typeof MutationObserver === "undefined") return
|
||||
|
||||
const root = document.documentElement
|
||||
const monitor = new MutationObserver(() => applyScheme())
|
||||
monitor.observe(root, { attributes: true, attributeFilter: ["data-color-scheme"] })
|
||||
applyScheme()
|
||||
|
||||
onCleanup(() => monitor.disconnect())
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
rendered()
|
||||
const ranges = local.commentedLines ?? []
|
||||
requestAnimationFrame(() => applyCommentedLines(ranges))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const selected = local.selectedLines ?? null
|
||||
setSelectedLines(selected)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (props.enableLineSelection !== true) return
|
||||
|
||||
container.addEventListener("mousedown", handleMouseDown)
|
||||
container.addEventListener("mousemove", handleMouseMove)
|
||||
window.addEventListener("mouseup", handleMouseUp)
|
||||
document.addEventListener("selectionchange", handleSelectionChange)
|
||||
|
||||
onCleanup(() => {
|
||||
container.removeEventListener("mousedown", handleMouseDown)
|
||||
container.removeEventListener("mousemove", handleMouseMove)
|
||||
window.removeEventListener("mouseup", handleMouseUp)
|
||||
document.removeEventListener("selectionchange", handleSelectionChange)
|
||||
})
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
observer?.disconnect()
|
||||
|
||||
if (selectionFrame !== undefined) {
|
||||
cancelAnimationFrame(selectionFrame)
|
||||
selectionFrame = undefined
|
||||
}
|
||||
|
||||
if (dragFrame !== undefined) {
|
||||
cancelAnimationFrame(dragFrame)
|
||||
dragFrame = undefined
|
||||
}
|
||||
|
||||
dragStart = undefined
|
||||
dragEnd = undefined
|
||||
dragSide = undefined
|
||||
dragEndSide = undefined
|
||||
dragMoved = false
|
||||
lastSelection = null
|
||||
pendingSelectionEnd = false
|
||||
|
||||
instance?.cleanUp()
|
||||
setCurrent(undefined)
|
||||
})
|
||||
|
||||
return <div data-component="diff" style={styleVariables} ref={container} />
|
||||
}
|
||||
125
opencode/packages/ui/src/components/dropdown-menu.css
Normal file
125
opencode/packages/ui/src/components/dropdown-menu.css
Normal file
@@ -0,0 +1,125 @@
|
||||
[data-component="dropdown-menu-content"],
|
||||
[data-component="dropdown-menu-sub-content"] {
|
||||
min-width: 8rem;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid color-mix(in oklch, var(--border-base) 50%, transparent);
|
||||
background-clip: padding-box;
|
||||
background-color: var(--surface-raised-stronger-non-alpha);
|
||||
padding: 4px;
|
||||
box-shadow: var(--shadow-md);
|
||||
z-index: 50;
|
||||
transform-origin: var(--kb-menu-content-transform-origin);
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[data-closed] {
|
||||
animation: dropdown-menu-close 0.15s ease-out;
|
||||
}
|
||||
|
||||
&[data-expanded] {
|
||||
animation: dropdown-menu-open 0.15s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="dropdown-menu-content"],
|
||||
[data-component="dropdown-menu-sub-content"] {
|
||||
[data-slot="dropdown-menu-item"],
|
||||
[data-slot="dropdown-menu-checkbox-item"],
|
||||
[data-slot="dropdown-menu-radio-item"],
|
||||
[data-slot="dropdown-menu-sub-trigger"] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-strong);
|
||||
|
||||
&[data-highlighted] {
|
||||
background: var(--surface-raised-base-hover);
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
color: var(--text-weak);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="dropdown-menu-sub-trigger"] {
|
||||
&[data-expanded] {
|
||||
background: var(--surface-raised-base-hover);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="dropdown-menu-item-indicator"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
[data-slot="dropdown-menu-item-label"] {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
[data-slot="dropdown-menu-item-description"] {
|
||||
font-size: var(--font-size-x-small);
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="dropdown-menu-separator"] {
|
||||
height: 1px;
|
||||
margin: 4px -4px;
|
||||
border-top-color: var(--border-weak-base);
|
||||
}
|
||||
|
||||
[data-slot="dropdown-menu-group-label"] {
|
||||
padding: 4px 8px;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-x-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="dropdown-menu-arrow"] {
|
||||
fill: var(--surface-raised-stronger-non-alpha);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropdown-menu-open {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropdown-menu-close {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
308
opencode/packages/ui/src/components/dropdown-menu.tsx
Normal file
308
opencode/packages/ui/src/components/dropdown-menu.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
import { DropdownMenu as Kobalte } from "@kobalte/core/dropdown-menu"
|
||||
import { splitProps } from "solid-js"
|
||||
import type { ComponentProps, ParentProps } from "solid-js"
|
||||
|
||||
export interface DropdownMenuProps extends ComponentProps<typeof Kobalte> {}
|
||||
export interface DropdownMenuTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {}
|
||||
export interface DropdownMenuIconProps extends ComponentProps<typeof Kobalte.Icon> {}
|
||||
export interface DropdownMenuPortalProps extends ComponentProps<typeof Kobalte.Portal> {}
|
||||
export interface DropdownMenuContentProps extends ComponentProps<typeof Kobalte.Content> {}
|
||||
export interface DropdownMenuArrowProps extends ComponentProps<typeof Kobalte.Arrow> {}
|
||||
export interface DropdownMenuSeparatorProps extends ComponentProps<typeof Kobalte.Separator> {}
|
||||
export interface DropdownMenuGroupProps extends ComponentProps<typeof Kobalte.Group> {}
|
||||
export interface DropdownMenuGroupLabelProps extends ComponentProps<typeof Kobalte.GroupLabel> {}
|
||||
export interface DropdownMenuItemProps extends ComponentProps<typeof Kobalte.Item> {}
|
||||
export interface DropdownMenuItemLabelProps extends ComponentProps<typeof Kobalte.ItemLabel> {}
|
||||
export interface DropdownMenuItemDescriptionProps extends ComponentProps<typeof Kobalte.ItemDescription> {}
|
||||
export interface DropdownMenuItemIndicatorProps extends ComponentProps<typeof Kobalte.ItemIndicator> {}
|
||||
export interface DropdownMenuRadioGroupProps extends ComponentProps<typeof Kobalte.RadioGroup> {}
|
||||
export interface DropdownMenuRadioItemProps extends ComponentProps<typeof Kobalte.RadioItem> {}
|
||||
export interface DropdownMenuCheckboxItemProps extends ComponentProps<typeof Kobalte.CheckboxItem> {}
|
||||
export interface DropdownMenuSubProps extends ComponentProps<typeof Kobalte.Sub> {}
|
||||
export interface DropdownMenuSubTriggerProps extends ComponentProps<typeof Kobalte.SubTrigger> {}
|
||||
export interface DropdownMenuSubContentProps extends ComponentProps<typeof Kobalte.SubContent> {}
|
||||
|
||||
function DropdownMenuRoot(props: DropdownMenuProps) {
|
||||
return <Kobalte {...props} data-component="dropdown-menu" />
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger(props: ParentProps<DropdownMenuTriggerProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.Trigger
|
||||
{...rest}
|
||||
data-slot="dropdown-menu-trigger"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuIcon(props: ParentProps<DropdownMenuIconProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.Icon
|
||||
{...rest}
|
||||
data-slot="dropdown-menu-icon"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.Icon>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuPortal(props: DropdownMenuPortalProps) {
|
||||
return <Kobalte.Portal {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuContent(props: ParentProps<DropdownMenuContentProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.Content
|
||||
{...rest}
|
||||
data-component="dropdown-menu-content"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.Content>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuArrow(props: DropdownMenuArrowProps) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList"])
|
||||
return (
|
||||
<Kobalte.Arrow
|
||||
{...rest}
|
||||
data-slot="dropdown-menu-arrow"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator(props: DropdownMenuSeparatorProps) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList"])
|
||||
return (
|
||||
<Kobalte.Separator
|
||||
{...rest}
|
||||
data-slot="dropdown-menu-separator"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup(props: ParentProps<DropdownMenuGroupProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.Group
|
||||
{...rest}
|
||||
data-slot="dropdown-menu-group"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.Group>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroupLabel(props: ParentProps<DropdownMenuGroupLabelProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.GroupLabel
|
||||
{...rest}
|
||||
data-slot="dropdown-menu-group-label"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.GroupLabel>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem(props: ParentProps<DropdownMenuItemProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.Item
|
||||
{...rest}
|
||||
data-slot="dropdown-menu-item"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItemLabel(props: ParentProps<DropdownMenuItemLabelProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.ItemLabel
|
||||
{...rest}
|
||||
data-slot="dropdown-menu-item-label"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.ItemLabel>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItemDescription(props: ParentProps<DropdownMenuItemDescriptionProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.ItemDescription
|
||||
{...rest}
|
||||
data-slot="dropdown-menu-item-description"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.ItemDescription>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItemIndicator(props: ParentProps<DropdownMenuItemIndicatorProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.ItemIndicator
|
||||
{...rest}
|
||||
data-slot="dropdown-menu-item-indicator"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.ItemIndicator>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup(props: ParentProps<DropdownMenuRadioGroupProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.RadioGroup
|
||||
{...rest}
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.RadioGroup>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem(props: ParentProps<DropdownMenuRadioItemProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.RadioItem
|
||||
{...rest}
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem(props: ParentProps<DropdownMenuCheckboxItemProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.CheckboxItem
|
||||
{...rest}
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub(props: DropdownMenuSubProps) {
|
||||
return <Kobalte.Sub {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger(props: ParentProps<DropdownMenuSubTriggerProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.SubTrigger
|
||||
{...rest}
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent(props: ParentProps<DropdownMenuSubContentProps>) {
|
||||
const [local, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.SubContent
|
||||
{...rest}
|
||||
data-component="dropdown-menu-sub-content"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
{local.children}
|
||||
</Kobalte.SubContent>
|
||||
)
|
||||
}
|
||||
|
||||
export const DropdownMenu = Object.assign(DropdownMenuRoot, {
|
||||
Trigger: DropdownMenuTrigger,
|
||||
Icon: DropdownMenuIcon,
|
||||
Portal: DropdownMenuPortal,
|
||||
Content: DropdownMenuContent,
|
||||
Arrow: DropdownMenuArrow,
|
||||
Separator: DropdownMenuSeparator,
|
||||
Group: DropdownMenuGroup,
|
||||
GroupLabel: DropdownMenuGroupLabel,
|
||||
Item: DropdownMenuItem,
|
||||
ItemLabel: DropdownMenuItemLabel,
|
||||
ItemDescription: DropdownMenuItemDescription,
|
||||
ItemIndicator: DropdownMenuItemIndicator,
|
||||
RadioGroup: DropdownMenuRadioGroup,
|
||||
RadioItem: DropdownMenuRadioItem,
|
||||
CheckboxItem: DropdownMenuCheckboxItem,
|
||||
Sub: DropdownMenuSub,
|
||||
SubTrigger: DropdownMenuSubTrigger,
|
||||
SubContent: DropdownMenuSubContent,
|
||||
})
|
||||
13
opencode/packages/ui/src/components/favicon.tsx
Normal file
13
opencode/packages/ui/src/components/favicon.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Link, Meta } from "@solidjs/meta"
|
||||
|
||||
export const Favicon = () => {
|
||||
return (
|
||||
<>
|
||||
<Link rel="icon" type="image/png" href="/favicon-96x96-v3.png" sizes="96x96" />
|
||||
<Link rel="shortcut icon" href="/favicon-v3.ico" />
|
||||
<Link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-v3.png" />
|
||||
<Link rel="manifest" href="/site.webmanifest" />
|
||||
<Meta name="apple-mobile-web-app-title" content="OpenCode" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
5
opencode/packages/ui/src/components/file-icon.css
Normal file
5
opencode/packages/ui/src/components/file-icon.css
Normal file
@@ -0,0 +1,5 @@
|
||||
[data-component="file-icon"] {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
583
opencode/packages/ui/src/components/file-icon.tsx
Normal file
583
opencode/packages/ui/src/components/file-icon.tsx
Normal file
@@ -0,0 +1,583 @@
|
||||
import type { Component, JSX } from "solid-js"
|
||||
import { createMemo, splitProps } from "solid-js"
|
||||
import sprite from "./file-icons/sprite.svg"
|
||||
import type { IconName } from "./file-icons/types"
|
||||
|
||||
export type FileIconProps = JSX.GSVGAttributes<SVGSVGElement> & {
|
||||
node: { path: string; type: "file" | "directory" }
|
||||
expanded?: boolean
|
||||
}
|
||||
|
||||
export const FileIcon: Component<FileIconProps> = (props) => {
|
||||
const [local, rest] = splitProps(props, ["node", "class", "classList", "expanded"])
|
||||
const name = createMemo(() => chooseIconName(local.node.path, local.node.type, local.expanded || false))
|
||||
return (
|
||||
<svg
|
||||
data-component="file-icon"
|
||||
{...rest}
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
<use href={`${sprite}#${name()}`} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
type IconMaps = {
|
||||
fileNames: Record<string, IconName>
|
||||
fileExtensions: Record<string, IconName>
|
||||
folderNames: Record<string, IconName>
|
||||
defaults: {
|
||||
file: IconName
|
||||
folder: IconName
|
||||
folderOpen: IconName
|
||||
}
|
||||
}
|
||||
|
||||
const ICON_MAPS: IconMaps = {
|
||||
fileNames: {
|
||||
// Documentation files
|
||||
"readme.md": "Readme",
|
||||
"changelog.md": "Changelog",
|
||||
"contributing.md": "Contributing",
|
||||
"conduct.md": "Conduct",
|
||||
license: "Certificate",
|
||||
authors: "Authors",
|
||||
credits: "Credits",
|
||||
install: "Installation",
|
||||
|
||||
// Node.js files
|
||||
"package.json": "Nodejs",
|
||||
"package-lock.json": "Nodejs",
|
||||
"yarn.lock": "Yarn",
|
||||
"pnpm-lock.yaml": "Pnpm",
|
||||
"bun.lock": "Bun",
|
||||
"bun.lockb": "Bun",
|
||||
"bunfig.toml": "Bun",
|
||||
".nvmrc": "Nodejs",
|
||||
".node-version": "Nodejs",
|
||||
|
||||
// Docker files
|
||||
dockerfile: "Docker",
|
||||
"docker-compose.yml": "Docker",
|
||||
"docker-compose.yaml": "Docker",
|
||||
".dockerignore": "Docker",
|
||||
|
||||
// Config files
|
||||
"jest.config.js": "Jest",
|
||||
"jest.config.ts": "Jest",
|
||||
"jest.config.mjs": "Jest",
|
||||
"vitest.config.js": "Vitest",
|
||||
"vitest.config.ts": "Vitest",
|
||||
"tailwind.config.js": "Tailwindcss",
|
||||
"tailwind.config.ts": "Tailwindcss",
|
||||
"turbo.json": "Turborepo",
|
||||
"tsconfig.json": "Tsconfig",
|
||||
"jsconfig.json": "Jsconfig",
|
||||
".eslintrc": "Eslint",
|
||||
".eslintrc.js": "Eslint",
|
||||
".eslintrc.json": "Eslint",
|
||||
".prettierrc": "Prettier",
|
||||
".prettierrc.js": "Prettier",
|
||||
".prettierrc.json": "Prettier",
|
||||
"vite.config.js": "Vite",
|
||||
"vite.config.ts": "Vite",
|
||||
"webpack.config.js": "Webpack",
|
||||
"rollup.config.js": "Rollup",
|
||||
"astro.config.mjs": "AstroConfig",
|
||||
"astro.config.js": "AstroConfig",
|
||||
"next.config.js": "Next",
|
||||
"next.config.mjs": "Next",
|
||||
"nuxt.config.js": "Nuxt",
|
||||
"nuxt.config.ts": "Nuxt",
|
||||
"svelte.config.js": "Svelte",
|
||||
"gatsby-config.js": "Gatsby",
|
||||
"remix.config.js": "Remix",
|
||||
"prisma.schema": "Prisma",
|
||||
".gitignore": "Git",
|
||||
".gitattributes": "Git",
|
||||
makefile: "Makefile",
|
||||
cmake: "Cmake",
|
||||
"cargo.toml": "Rust",
|
||||
"go.mod": "GoMod",
|
||||
"go.sum": "GoMod",
|
||||
"requirements.txt": "Python",
|
||||
"pyproject.toml": "Python",
|
||||
pipfile: "Python",
|
||||
"poetry.lock": "Poetry",
|
||||
gemfile: "Gemfile",
|
||||
rakefile: "Ruby",
|
||||
"composer.json": "Php",
|
||||
"build.gradle": "Gradle",
|
||||
"pom.xml": "Maven",
|
||||
"deno.json": "Deno",
|
||||
"deno.jsonc": "Deno",
|
||||
"vercel.json": "Vercel",
|
||||
"netlify.toml": "Netlify",
|
||||
".env": "Tune",
|
||||
".env.local": "Tune",
|
||||
".env.development": "Tune",
|
||||
".env.production": "Tune",
|
||||
".env.example": "Tune",
|
||||
".editorconfig": "Editorconfig",
|
||||
"robots.txt": "Robots",
|
||||
"favicon.ico": "Favicon",
|
||||
browserlist: "Browserlist",
|
||||
".babelrc": "Babel",
|
||||
"babel.config.js": "Babel",
|
||||
"gulpfile.js": "Gulp",
|
||||
"gruntfile.js": "Grunt",
|
||||
"capacitor.config.json": "Capacitor",
|
||||
"ionic.config.json": "Ionic",
|
||||
"angular.json": "Angular",
|
||||
".storybook": "Storybook",
|
||||
"storybook.config.js": "Storybook",
|
||||
"cypress.config.js": "Cypress",
|
||||
"playwright.config.js": "Playwright",
|
||||
"puppeteer.config.js": "Puppeteer",
|
||||
"wrangler.toml": "Wrangler",
|
||||
"firebase.json": "Firebase",
|
||||
supabase: "Supabase",
|
||||
terraform: "Terraform",
|
||||
kubernetes: "Kubernetes",
|
||||
".gitpod.yml": "Gitpod",
|
||||
".devcontainer": "Vscode",
|
||||
"travis.yml": "Travis",
|
||||
"appveyor.yml": "Appveyor",
|
||||
".circleci": "Circleci",
|
||||
"renovate.json": "Renovate",
|
||||
"dependabot.yml": "Dependabot",
|
||||
"lerna.json": "Lerna",
|
||||
"nx.json": "Nx",
|
||||
},
|
||||
fileExtensions: {
|
||||
// Test files
|
||||
"spec.ts": "TestTs",
|
||||
"test.ts": "TestTs",
|
||||
"spec.tsx": "TestJsx",
|
||||
"test.tsx": "TestJsx",
|
||||
"spec.js": "TestJs",
|
||||
"test.js": "TestJs",
|
||||
"spec.jsx": "TestJsx",
|
||||
"test.jsx": "TestJsx",
|
||||
|
||||
// JavaScript/TypeScript
|
||||
"js.map": "JavascriptMap",
|
||||
"d.ts": "TypescriptDef",
|
||||
ts: "Typescript",
|
||||
tsx: "React_ts",
|
||||
js: "Javascript",
|
||||
jsx: "React",
|
||||
mjs: "Javascript",
|
||||
cjs: "Javascript",
|
||||
|
||||
// Web languages
|
||||
html: "Html",
|
||||
htm: "Html",
|
||||
css: "Css",
|
||||
scss: "Sass",
|
||||
sass: "Sass",
|
||||
less: "Less",
|
||||
styl: "Stylus",
|
||||
|
||||
// Data formats
|
||||
json: "Json",
|
||||
xml: "Xml",
|
||||
yml: "Yaml",
|
||||
yaml: "Yaml",
|
||||
toml: "Toml",
|
||||
hjson: "Hjson",
|
||||
|
||||
// Documentation
|
||||
md: "Markdown",
|
||||
mdx: "Mdx",
|
||||
tex: "Tex",
|
||||
|
||||
// Programming languages
|
||||
py: "Python",
|
||||
pyx: "Python",
|
||||
pyw: "Python",
|
||||
rs: "Rust",
|
||||
go: "Go",
|
||||
java: "Java",
|
||||
kt: "Kotlin",
|
||||
scala: "Scala",
|
||||
php: "Php",
|
||||
rb: "Ruby",
|
||||
cs: "Csharp",
|
||||
vb: "Visualstudio",
|
||||
cpp: "Cpp",
|
||||
cc: "Cpp",
|
||||
cxx: "Cpp",
|
||||
c: "C",
|
||||
h: "H",
|
||||
hpp: "Hpp",
|
||||
swift: "Swift",
|
||||
m: "ObjectiveC",
|
||||
mm: "ObjectiveCpp",
|
||||
dart: "Dart",
|
||||
lua: "Lua",
|
||||
pl: "Perl",
|
||||
r: "R",
|
||||
jl: "Julia",
|
||||
hs: "Haskell",
|
||||
elm: "Elm",
|
||||
ml: "Ocaml",
|
||||
clj: "Clojure",
|
||||
cljs: "Clojure",
|
||||
erl: "Erlang",
|
||||
ex: "Elixir",
|
||||
exs: "Elixir",
|
||||
nim: "Nim",
|
||||
zig: "Zig",
|
||||
v: "Vlang",
|
||||
odin: "Odin",
|
||||
gleam: "Gleam",
|
||||
grain: "Grain",
|
||||
roc: "Rocket",
|
||||
fs: "Fsharp",
|
||||
|
||||
// Shell scripts
|
||||
sh: "Console",
|
||||
bash: "Console",
|
||||
zsh: "Console",
|
||||
fish: "Console",
|
||||
ps1: "Powershell",
|
||||
|
||||
// Config/build files
|
||||
cfg: "Settings",
|
||||
ini: "Settings",
|
||||
conf: "Settings",
|
||||
properties: "Settings",
|
||||
|
||||
// Media files
|
||||
svg: "Svg",
|
||||
png: "Image",
|
||||
jpg: "Image",
|
||||
jpeg: "Image",
|
||||
gif: "Image",
|
||||
webp: "Image",
|
||||
bmp: "Image",
|
||||
ico: "Favicon",
|
||||
mp4: "Video",
|
||||
mov: "Video",
|
||||
avi: "Video",
|
||||
webm: "Video",
|
||||
mp3: "Audio",
|
||||
wav: "Audio",
|
||||
flac: "Audio",
|
||||
|
||||
// Archive files
|
||||
zip: "Zip",
|
||||
tar: "Zip",
|
||||
gz: "Zip",
|
||||
rar: "Zip",
|
||||
"7z": "Zip",
|
||||
|
||||
// Document files
|
||||
pdf: "Pdf",
|
||||
doc: "Word",
|
||||
docx: "Word",
|
||||
ppt: "Powerpoint",
|
||||
pptx: "Powerpoint",
|
||||
xls: "Document",
|
||||
xlsx: "Document",
|
||||
|
||||
// Database files
|
||||
sql: "Database",
|
||||
db: "Database",
|
||||
sqlite: "Database",
|
||||
|
||||
// Other
|
||||
env: "Tune",
|
||||
log: "Log",
|
||||
lock: "Lock",
|
||||
key: "Key",
|
||||
pem: "Certificate",
|
||||
crt: "Certificate",
|
||||
proto: "Proto",
|
||||
graphql: "Graphql",
|
||||
gql: "Graphql",
|
||||
wasm: "Webassembly",
|
||||
dockerfile: "Docker",
|
||||
},
|
||||
folderNames: {
|
||||
// Source code
|
||||
src: "FolderSrc",
|
||||
source: "FolderSrc",
|
||||
lib: "FolderLib",
|
||||
libs: "FolderLib",
|
||||
|
||||
// Testing
|
||||
test: "FolderTest",
|
||||
tests: "FolderTest",
|
||||
testing: "FolderTest",
|
||||
spec: "FolderTest",
|
||||
specs: "FolderTest",
|
||||
__tests__: "FolderTest",
|
||||
e2e: "FolderTest",
|
||||
integration: "FolderTest",
|
||||
unit: "FolderTest",
|
||||
cypress: "FolderCypress",
|
||||
|
||||
// Dependencies
|
||||
node_modules: "FolderNode",
|
||||
vendor: "FolderPackages",
|
||||
packages: "FolderPackages",
|
||||
deps: "FolderPackages",
|
||||
|
||||
// Build/dist
|
||||
build: "FolderBuildkite",
|
||||
dist: "FolderDist",
|
||||
out: "FolderDist",
|
||||
output: "FolderDist",
|
||||
target: "FolderTarget",
|
||||
|
||||
// Configuration
|
||||
config: "FolderConfig",
|
||||
configs: "FolderConfig",
|
||||
configuration: "FolderConfig",
|
||||
settings: "FolderConfig",
|
||||
env: "FolderEnvironment",
|
||||
environments: "FolderEnvironment",
|
||||
|
||||
// Docker
|
||||
docker: "FolderDocker",
|
||||
dockerfiles: "FolderDocker",
|
||||
containers: "FolderDocker",
|
||||
|
||||
// Documentation
|
||||
docs: "FolderDocs",
|
||||
doc: "FolderDocs",
|
||||
documentation: "FolderDocs",
|
||||
readme: "FolderDocs",
|
||||
|
||||
// Public/assets
|
||||
public: "FolderPublic",
|
||||
static: "FolderPublic",
|
||||
assets: "FolderImages",
|
||||
images: "FolderImages",
|
||||
img: "FolderImages",
|
||||
icons: "FolderImages",
|
||||
media: "FolderImages",
|
||||
fonts: "FolderFont",
|
||||
styles: "FolderCss",
|
||||
stylesheets: "FolderCss",
|
||||
css: "FolderCss",
|
||||
sass: "FolderSass",
|
||||
scss: "FolderSass",
|
||||
less: "FolderLess",
|
||||
|
||||
// Scripts
|
||||
scripts: "FolderScripts",
|
||||
script: "FolderScripts",
|
||||
tools: "FolderTools",
|
||||
utils: "FolderUtils",
|
||||
utilities: "FolderUtils",
|
||||
helpers: "FolderHelper",
|
||||
|
||||
// Framework specific
|
||||
components: "FolderComponents",
|
||||
component: "FolderComponents",
|
||||
views: "FolderViews",
|
||||
view: "FolderViews",
|
||||
layouts: "FolderLayout",
|
||||
layout: "FolderLayout",
|
||||
templates: "FolderTemplate",
|
||||
template: "FolderTemplate",
|
||||
hooks: "FolderHook",
|
||||
hook: "FolderHook",
|
||||
store: "FolderStore",
|
||||
stores: "FolderStore",
|
||||
state: "FolderNgrxStore",
|
||||
reducers: "FolderReduxReducer",
|
||||
reducer: "FolderReduxReducer",
|
||||
services: "FolderApi",
|
||||
service: "FolderApi",
|
||||
api: "FolderApi",
|
||||
apis: "FolderApi",
|
||||
routes: "FolderRoutes",
|
||||
route: "FolderRoutes",
|
||||
routing: "FolderRoutes",
|
||||
middleware: "FolderMiddleware",
|
||||
middlewares: "FolderMiddleware",
|
||||
controllers: "FolderController",
|
||||
controller: "FolderController",
|
||||
models: "FolderDatabase",
|
||||
model: "FolderDatabase",
|
||||
schemas: "FolderDatabase",
|
||||
schema: "FolderDatabase",
|
||||
migrations: "FolderDatabase",
|
||||
migration: "FolderDatabase",
|
||||
seeders: "FolderSeeders",
|
||||
seeder: "FolderSeeders",
|
||||
|
||||
// TypeScript
|
||||
types: "FolderTypescript",
|
||||
typing: "FolderTypescript",
|
||||
typings: "FolderTypescript",
|
||||
"@types": "FolderTypescript",
|
||||
interfaces: "FolderInterface",
|
||||
interface: "FolderInterface",
|
||||
|
||||
// Mobile
|
||||
android: "FolderAndroid",
|
||||
ios: "FolderIos",
|
||||
mobile: "FolderMobile",
|
||||
flutter: "FolderFlutter",
|
||||
|
||||
// Infrastructure
|
||||
kubernetes: "FolderKubernetes",
|
||||
k8s: "FolderKubernetes",
|
||||
terraform: "FolderTerraform",
|
||||
aws: "FolderAws",
|
||||
azure: "FolderAzurePipelines",
|
||||
firebase: "FolderFirebase",
|
||||
supabase: "FolderSupabase",
|
||||
vercel: "FolderVercel",
|
||||
netlify: "FolderNetlify",
|
||||
|
||||
// CI/CD
|
||||
".github": "FolderGithub",
|
||||
".gitlab": "FolderGitlab",
|
||||
".circleci": "FolderCircleci",
|
||||
ci: "FolderCi",
|
||||
".ci": "FolderCi",
|
||||
workflows: "FolderGhWorkflows",
|
||||
|
||||
// Git
|
||||
".git": "FolderGit",
|
||||
|
||||
// Development tools
|
||||
".vscode": "FolderVscode",
|
||||
".idea": "FolderIntellij",
|
||||
".cursor": "FolderCursor",
|
||||
".devcontainer": "FolderContainer",
|
||||
".storybook": "FolderStorybook",
|
||||
|
||||
// Localization
|
||||
i18n: "FolderI18n",
|
||||
locales: "FolderI18n",
|
||||
locale: "FolderI18n",
|
||||
lang: "FolderI18n",
|
||||
languages: "FolderI18n",
|
||||
|
||||
// Other common patterns
|
||||
temp: "FolderTemp",
|
||||
tmp: "FolderTemp",
|
||||
logs: "FolderLog",
|
||||
log: "FolderLog",
|
||||
backup: "FolderBackup",
|
||||
backups: "FolderBackup",
|
||||
examples: "FolderExamples",
|
||||
example: "FolderExamples",
|
||||
demo: "FolderExamples",
|
||||
demos: "FolderExamples",
|
||||
samples: "FolderExamples",
|
||||
sample: "FolderExamples",
|
||||
fixtures: "FolderTest",
|
||||
mocks: "FolderMock",
|
||||
mock: "FolderMock",
|
||||
data: "FolderDatabase",
|
||||
database: "FolderDatabase",
|
||||
db: "FolderDatabase",
|
||||
sql: "FolderDatabase",
|
||||
prisma: "FolderPrisma",
|
||||
drizzle: "FolderDrizzle",
|
||||
|
||||
// Security
|
||||
security: "FolderSecure",
|
||||
auth: "FolderSecure",
|
||||
authentication: "FolderSecure",
|
||||
authorization: "FolderSecure",
|
||||
keys: "FolderKeys",
|
||||
certs: "FolderKeys",
|
||||
certificates: "FolderKeys",
|
||||
|
||||
// Content
|
||||
content: "FolderContent",
|
||||
posts: "FolderContent",
|
||||
articles: "FolderContent",
|
||||
blog: "FolderContent",
|
||||
|
||||
// Functions
|
||||
functions: "FolderFunctions",
|
||||
function: "FolderFunctions",
|
||||
lambda: "FolderFunctions",
|
||||
lambdas: "FolderFunctions",
|
||||
serverless: "FolderServerless",
|
||||
|
||||
// Jobs/tasks
|
||||
jobs: "FolderJob",
|
||||
job: "FolderJob",
|
||||
tasks: "FolderTasks",
|
||||
task: "FolderTasks",
|
||||
cron: "FolderTasks",
|
||||
queue: "FolderQueue",
|
||||
queues: "FolderQueue",
|
||||
|
||||
// Desktop platforms
|
||||
desktop: "FolderDesktop",
|
||||
windows: "FolderWindows",
|
||||
macos: "FolderMacos",
|
||||
linux: "FolderLinux",
|
||||
},
|
||||
defaults: {
|
||||
file: "Document",
|
||||
folder: "Folder",
|
||||
folderOpen: "FolderOpen",
|
||||
},
|
||||
}
|
||||
|
||||
const toOpenVariant = (icon: IconName): IconName => {
|
||||
if (!icon.startsWith("Folder")) return icon
|
||||
if (icon.endsWith("_light")) return icon.replace("_light", "Open_light") as IconName
|
||||
if (!icon.endsWith("Open")) return (icon + "Open") as IconName
|
||||
return icon
|
||||
}
|
||||
|
||||
const basenameOf = (p: string) =>
|
||||
p
|
||||
.replace(/[/\\]+$/, "")
|
||||
.split(/[\\/]/)
|
||||
.pop() ?? ""
|
||||
|
||||
const folderNameVariants = (name: string) => {
|
||||
const n = name.toLowerCase()
|
||||
return [n, `.${n}`, `_${n}`, `__${n}__`]
|
||||
}
|
||||
|
||||
const dottedSuffixesDesc = (name: string) => {
|
||||
const n = name.toLowerCase()
|
||||
const idxs: number[] = []
|
||||
for (let i = 0; i < n.length; i++) if (n[i] === ".") idxs.push(i)
|
||||
const out = new Set<string>()
|
||||
out.add(n) // allow exact whole-name "extensions" like "dockerfile"
|
||||
for (const i of idxs) if (i + 1 < n.length) out.add(n.slice(i + 1))
|
||||
return Array.from(out).sort((a, b) => b.length - a.length) // longest first
|
||||
}
|
||||
|
||||
export function chooseIconName(path: string, type: "directory" | "file", expanded: boolean): IconName {
|
||||
const base = basenameOf(path)
|
||||
const baseLower = base.toLowerCase()
|
||||
|
||||
if (type === "directory") {
|
||||
for (const cand of folderNameVariants(baseLower)) {
|
||||
const icon = ICON_MAPS.folderNames[cand]
|
||||
if (icon) return expanded ? toOpenVariant(icon) : icon
|
||||
}
|
||||
return expanded ? ICON_MAPS.defaults.folderOpen : ICON_MAPS.defaults.folder
|
||||
}
|
||||
|
||||
const byName = ICON_MAPS.fileNames[baseLower]
|
||||
if (byName) return byName
|
||||
|
||||
for (const ext of dottedSuffixesDesc(baseLower)) {
|
||||
const icon = ICON_MAPS.fileExtensions[ext]
|
||||
if (icon) return icon
|
||||
}
|
||||
|
||||
return ICON_MAPS.defaults.file
|
||||
}
|
||||
11707
opencode/packages/ui/src/components/file-icons/sprite.svg
Normal file
11707
opencode/packages/ui/src/components/file-icons/sprite.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 922 KiB |
1095
opencode/packages/ui/src/components/file-icons/types.ts
Normal file
1095
opencode/packages/ui/src/components/file-icons/types.ts
Normal file
File diff suppressed because it is too large
Load Diff
166
opencode/packages/ui/src/components/font.tsx
Normal file
166
opencode/packages/ui/src/components/font.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { Style, Link } from "@solidjs/meta"
|
||||
import inter from "../assets/fonts/inter.woff2"
|
||||
import ibmPlexMonoRegular from "../assets/fonts/ibm-plex-mono.woff2"
|
||||
import ibmPlexMonoMedium from "../assets/fonts/ibm-plex-mono-medium.woff2"
|
||||
import ibmPlexMonoBold from "../assets/fonts/ibm-plex-mono-bold.woff2"
|
||||
|
||||
import cascadiaCode from "../assets/fonts/cascadia-code-nerd-font.woff2"
|
||||
import cascadiaCodeBold from "../assets/fonts/cascadia-code-nerd-font-bold.woff2"
|
||||
import firaCode from "../assets/fonts/fira-code-nerd-font.woff2"
|
||||
import firaCodeBold from "../assets/fonts/fira-code-nerd-font-bold.woff2"
|
||||
import hack from "../assets/fonts/hack-nerd-font.woff2"
|
||||
import hackBold from "../assets/fonts/hack-nerd-font-bold.woff2"
|
||||
import inconsolata from "../assets/fonts/inconsolata-nerd-font.woff2"
|
||||
import inconsolataBold from "../assets/fonts/inconsolata-nerd-font-bold.woff2"
|
||||
import intelOneMono from "../assets/fonts/intel-one-mono-nerd-font.woff2"
|
||||
import intelOneMonoBold from "../assets/fonts/intel-one-mono-nerd-font-bold.woff2"
|
||||
import jetbrainsMono from "../assets/fonts/jetbrains-mono-nerd-font.woff2"
|
||||
import jetbrainsMonoBold from "../assets/fonts/jetbrains-mono-nerd-font-bold.woff2"
|
||||
import mesloLgs from "../assets/fonts/meslo-lgs-nerd-font.woff2"
|
||||
import mesloLgsBold from "../assets/fonts/meslo-lgs-nerd-font-bold.woff2"
|
||||
import robotoMono from "../assets/fonts/roboto-mono-nerd-font.woff2"
|
||||
import robotoMonoBold from "../assets/fonts/roboto-mono-nerd-font-bold.woff2"
|
||||
import sourceCodePro from "../assets/fonts/source-code-pro-nerd-font.woff2"
|
||||
import sourceCodeProBold from "../assets/fonts/source-code-pro-nerd-font-bold.woff2"
|
||||
import ubuntuMono from "../assets/fonts/ubuntu-mono-nerd-font.woff2"
|
||||
import ubuntuMonoBold from "../assets/fonts/ubuntu-mono-nerd-font-bold.woff2"
|
||||
import iosevka from "../assets/fonts/iosevka-nerd-font.woff2"
|
||||
import iosevkaBold from "../assets/fonts/iosevka-nerd-font-bold.woff2"
|
||||
|
||||
type MonoFont = {
|
||||
family: string
|
||||
regular: string
|
||||
bold: string
|
||||
}
|
||||
|
||||
export const MONO_NERD_FONTS = [
|
||||
{
|
||||
family: "JetBrains Mono Nerd Font",
|
||||
regular: jetbrainsMono,
|
||||
bold: jetbrainsMonoBold,
|
||||
},
|
||||
{
|
||||
family: "Fira Code Nerd Font",
|
||||
regular: firaCode,
|
||||
bold: firaCodeBold,
|
||||
},
|
||||
{
|
||||
family: "Cascadia Code Nerd Font",
|
||||
regular: cascadiaCode,
|
||||
bold: cascadiaCodeBold,
|
||||
},
|
||||
{
|
||||
family: "Hack Nerd Font",
|
||||
regular: hack,
|
||||
bold: hackBold,
|
||||
},
|
||||
{
|
||||
family: "Source Code Pro Nerd Font",
|
||||
regular: sourceCodePro,
|
||||
bold: sourceCodeProBold,
|
||||
},
|
||||
{
|
||||
family: "Inconsolata Nerd Font",
|
||||
regular: inconsolata,
|
||||
bold: inconsolataBold,
|
||||
},
|
||||
{
|
||||
family: "Roboto Mono Nerd Font",
|
||||
regular: robotoMono,
|
||||
bold: robotoMonoBold,
|
||||
},
|
||||
{
|
||||
family: "Ubuntu Mono Nerd Font",
|
||||
regular: ubuntuMono,
|
||||
bold: ubuntuMonoBold,
|
||||
},
|
||||
{
|
||||
family: "Intel One Mono Nerd Font",
|
||||
regular: intelOneMono,
|
||||
bold: intelOneMonoBold,
|
||||
},
|
||||
{
|
||||
family: "Meslo LGS Nerd Font",
|
||||
regular: mesloLgs,
|
||||
bold: mesloLgsBold,
|
||||
},
|
||||
{
|
||||
family: "Iosevka Nerd Font",
|
||||
regular: iosevka,
|
||||
bold: iosevkaBold,
|
||||
},
|
||||
] satisfies MonoFont[]
|
||||
|
||||
const monoNerdCss = MONO_NERD_FONTS.map(
|
||||
(font) => `
|
||||
@font-face {
|
||||
font-family: "${font.family}";
|
||||
src: url("${font.regular}") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "${font.family}";
|
||||
src: url("${font.bold}") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}`,
|
||||
).join("")
|
||||
|
||||
export const Font = () => {
|
||||
return (
|
||||
<>
|
||||
<Style>{`
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: url("${inter}") format("woff2-variations");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Inter Fallback";
|
||||
src: local("Arial");
|
||||
size-adjust: 100%;
|
||||
ascent-override: 97%;
|
||||
descent-override: 25%;
|
||||
line-gap-override: 1%;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("${ibmPlexMonoRegular}") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("${ibmPlexMonoMedium}") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("${ibmPlexMonoBold}") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono Fallback";
|
||||
src: local("Courier New");
|
||||
size-adjust: 100%;
|
||||
ascent-override: 97%;
|
||||
descent-override: 25%;
|
||||
line-gap-override: 1%;
|
||||
}
|
||||
${monoNerdCss}
|
||||
`}</Style>
|
||||
<Link rel="preload" href={inter} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<Link rel="preload" href={ibmPlexMonoRegular} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
61
opencode/packages/ui/src/components/hover-card.css
Normal file
61
opencode/packages/ui/src/components/hover-card.css
Normal file
@@ -0,0 +1,61 @@
|
||||
[data-slot="hover-card-trigger"] {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-component="hover-card-content"] {
|
||||
z-index: 50;
|
||||
min-width: 200px;
|
||||
max-width: 320px;
|
||||
max-height: calc(100vh - 1rem);
|
||||
border-radius: 8px;
|
||||
background-color: var(--surface-raised-stronger-non-alpha);
|
||||
pointer-events: auto;
|
||||
|
||||
border: 1px solid color-mix(in oklch, var(--border-base) 50%, transparent);
|
||||
background-clip: padding-box;
|
||||
box-shadow: var(--shadow-md);
|
||||
|
||||
transform-origin: var(--kb-hovercard-content-transform-origin);
|
||||
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[data-closed] {
|
||||
animation: hover-card-close 0.15s ease-out;
|
||||
}
|
||||
|
||||
&[data-expanded] {
|
||||
animation: hover-card-open 0.15s ease-out;
|
||||
}
|
||||
|
||||
[data-slot="hover-card-body"] {
|
||||
padding: 4px;
|
||||
max-height: inherit;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hover-card-open {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hover-card-close {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
32
opencode/packages/ui/src/components/hover-card.tsx
Normal file
32
opencode/packages/ui/src/components/hover-card.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { HoverCard as Kobalte } from "@kobalte/core/hover-card"
|
||||
import { ComponentProps, JSXElement, ParentProps, splitProps } from "solid-js"
|
||||
|
||||
export interface HoverCardProps extends ParentProps, Omit<ComponentProps<typeof Kobalte>, "children"> {
|
||||
trigger: JSXElement
|
||||
mount?: HTMLElement
|
||||
class?: ComponentProps<"div">["class"]
|
||||
classList?: ComponentProps<"div">["classList"]
|
||||
}
|
||||
|
||||
export function HoverCard(props: HoverCardProps) {
|
||||
const [local, rest] = splitProps(props, ["trigger", "mount", "class", "classList", "children"])
|
||||
|
||||
return (
|
||||
<Kobalte gutter={4} {...rest}>
|
||||
<Kobalte.Trigger as="div" data-slot="hover-card-trigger">
|
||||
{local.trigger}
|
||||
</Kobalte.Trigger>
|
||||
<Kobalte.Portal mount={local.mount}>
|
||||
<Kobalte.Content
|
||||
data-component="hover-card-content"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
<div data-slot="hover-card-body">{local.children}</div>
|
||||
</Kobalte.Content>
|
||||
</Kobalte.Portal>
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
145
opencode/packages/ui/src/components/icon-button.css
Normal file
145
opencode/packages/ui/src/components/icon-button.css
Normal file
@@ -0,0 +1,145 @@
|
||||
[data-component="icon-button"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-sm);
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
aspect-ratio: 1;
|
||||
flex-shrink: 0;
|
||||
|
||||
&[data-variant="primary"] {
|
||||
background-color: var(--icon-strong-base);
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
/* color: var(--icon-weak-base); */
|
||||
color: var(--icon-invert-base);
|
||||
|
||||
/* &:hover:not(:disabled) { */
|
||||
/* color: var(--icon-weak-hover); */
|
||||
/* } */
|
||||
/* &:active:not(:disabled) { */
|
||||
/* color: var(--icon-strong-active); */
|
||||
/* } */
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--icon-strong-hover);
|
||||
}
|
||||
&:focus:not(:disabled) {
|
||||
background-color: var(--icon-strong-focus);
|
||||
}
|
||||
&:active:not(:disabled) {
|
||||
background-color: var(--icon-strong-active);
|
||||
}
|
||||
&:disabled {
|
||||
background-color: var(--icon-strong-disabled);
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
color: var(--icon-invert-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant="secondary"] {
|
||||
border: transparent;
|
||||
background-color: var(--button-secondary-base);
|
||||
color: var(--text-strong);
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--button-secondary-hover);
|
||||
}
|
||||
&:focus:not(:disabled) {
|
||||
background-color: var(--button-secondary-base);
|
||||
}
|
||||
&:focus-visible:not(:active) {
|
||||
background-color: var(--button-secondary-base);
|
||||
box-shadow: var(--shadow-xs-border-focus);
|
||||
}
|
||||
&:focus-visible:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
&:active:not(:disabled) {
|
||||
background-color: var(--button-secondary-base);
|
||||
}
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
color: var(--icon-strong-base);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--icon-strong-disabled);
|
||||
color: var(--icon-invert-base);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant="ghost"] {
|
||||
background-color: transparent;
|
||||
/* color: var(--icon-base); */
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
color: var(--icon-base);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
|
||||
/* [data-slot="icon-svg"] { */
|
||||
/* color: var(--icon-hover); */
|
||||
/* } */
|
||||
}
|
||||
&:focus-visible:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
&:active:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
/* [data-slot="icon-svg"] { */
|
||||
/* color: var(--icon-active); */
|
||||
/* } */
|
||||
}
|
||||
&:selected:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
/* [data-slot="icon-svg"] { */
|
||||
/* color: var(--icon-selected); */
|
||||
/* } */
|
||||
}
|
||||
&:disabled {
|
||||
color: var(--icon-invert-base);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-size="normal"] {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
font-size: var(--font-size-small);
|
||||
line-height: var(--line-height-large);
|
||||
gap: calc(var(--spacing) * 0.5);
|
||||
}
|
||||
|
||||
&[data-size="small"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&[data-size="large"] {
|
||||
height: 32px;
|
||||
/* padding: 0 8px 0 6px; */
|
||||
gap: 8px;
|
||||
|
||||
/* text-12-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
28
opencode/packages/ui/src/components/icon-button.tsx
Normal file
28
opencode/packages/ui/src/components/icon-button.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Button as Kobalte } from "@kobalte/core/button"
|
||||
import { type ComponentProps, splitProps } from "solid-js"
|
||||
import { Icon, IconProps } from "./icon"
|
||||
|
||||
export interface IconButtonProps extends ComponentProps<typeof Kobalte> {
|
||||
icon: IconProps["name"]
|
||||
size?: "small" | "normal" | "large"
|
||||
iconSize?: IconProps["size"]
|
||||
variant?: "primary" | "secondary" | "ghost"
|
||||
}
|
||||
|
||||
export function IconButton(props: ComponentProps<"button"> & IconButtonProps) {
|
||||
const [split, rest] = splitProps(props, ["variant", "size", "iconSize", "class", "classList"])
|
||||
return (
|
||||
<Kobalte
|
||||
{...rest}
|
||||
data-component="icon-button"
|
||||
data-size={split.size || "normal"}
|
||||
data-variant={split.variant || "secondary"}
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
>
|
||||
<Icon name={props.icon} size={split.iconSize ?? (split.size === "large" ? "normal" : "small")} />
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
34
opencode/packages/ui/src/components/icon.css
Normal file
34
opencode/packages/ui/src/components/icon.css
Normal file
@@ -0,0 +1,34 @@
|
||||
[data-component="icon"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
/* resize: both; */
|
||||
aspect-ratio: 1/1;
|
||||
color: var(--icon-base);
|
||||
|
||||
&[data-size="small"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&[data-size="normal"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&[data-size="medium"] {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&[data-size="large"] {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
104
opencode/packages/ui/src/components/icon.tsx
Normal file
104
opencode/packages/ui/src/components/icon.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { splitProps, type ComponentProps } from "solid-js"
|
||||
|
||||
const icons = {
|
||||
"align-right": `<path d="M12.292 6.04167L16.2503 9.99998L12.292 13.9583M2.91699 9.99998H15.6253M17.0837 3.75V16.25" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"arrow-up": `<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99991 2.24121L16.0921 8.33343L15.2083 9.21731L10.6249 4.63397V17.5001H9.37492V4.63398L4.7916 9.21731L3.90771 8.33343L9.99991 2.24121Z" fill="currentColor"/>`,
|
||||
"arrow-left": `<path d="M8.33464 4.58398L2.91797 10.0007L8.33464 15.4173M3.33464 10.0007H17.0846" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"arrow-right": `<path d="M11.6654 4.58398L17.082 10.0007L11.6654 15.4173M16.6654 10.0007H2.91536" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
archive: `<path d="M16.8747 6.24935H17.3747V5.74935H16.8747V6.24935ZM16.8747 16.8743V17.3743H17.3747V16.8743H16.8747ZM3.12467 16.8743H2.62467V17.3743H3.12467V16.8743ZM3.12467 6.24935V5.74935H2.62467V6.24935H3.12467ZM2.08301 2.91602V2.41602H1.58301V2.91602H2.08301ZM17.9163 2.91602H18.4163V2.41602H17.9163V2.91602ZM17.9163 6.24935V6.74935H18.4163V6.24935H17.9163ZM2.08301 6.24935H1.58301V6.74935H2.08301V6.24935ZM8.33301 9.08268H7.83301V10.0827H8.33301V9.58268V9.08268ZM11.6663 10.0827H12.1663V9.08268H11.6663V9.58268V10.0827ZM16.8747 6.24935H16.3747V16.8743H16.8747H17.3747V6.24935H16.8747ZM16.8747 16.8743V16.3743H3.12467V16.8743V17.3743H16.8747V16.8743ZM3.12467 16.8743H3.62467V6.24935H3.12467H2.62467V16.8743H3.12467ZM3.12467 6.24935V6.74935H16.8747V6.24935V5.74935H3.12467V6.24935ZM2.08301 2.91602V3.41602H17.9163V2.91602V2.41602H2.08301V2.91602ZM17.9163 2.91602H17.4163V6.24935H17.9163H18.4163V2.91602H17.9163ZM17.9163 6.24935V5.74935H2.08301V6.24935V6.74935H17.9163V6.24935ZM2.08301 6.24935H2.58301V2.91602H2.08301H1.58301V6.24935H2.08301ZM8.33301 9.58268V10.0827H11.6663V9.58268V9.08268H8.33301V9.58268Z" fill="currentColor"/>`,
|
||||
"bubble-5": `<path d="M18.3327 9.99935C18.3327 5.57227 15.0919 2.91602 9.99935 2.91602C4.90676 2.91602 1.66602 5.57227 1.66602 9.99935C1.66602 11.1487 2.45505 13.1006 2.57637 13.3939C2.58707 13.4197 2.59766 13.4434 2.60729 13.4697C2.69121 13.6987 3.04209 14.9354 1.66602 16.7674C3.51787 17.6528 5.48453 16.1973 5.48453 16.1973C6.84518 16.9193 8.46417 17.0827 9.99935 17.0827C15.0919 17.0827 18.3327 14.4264 18.3327 9.99935Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
brain: `<path d="M13.332 8.7487C11.4911 8.7487 9.9987 7.25631 9.9987 5.41536M6.66536 11.2487C8.50631 11.2487 9.9987 12.7411 9.9987 14.582M9.9987 2.78209L9.9987 17.0658M16.004 15.0475C17.1255 14.5876 17.9154 13.4849 17.9154 12.1978C17.9154 11.3363 17.5615 10.5575 16.9913 9.9987C17.5615 9.43991 17.9154 8.66108 17.9154 7.79962C17.9154 6.21199 16.7136 4.90504 15.1702 4.73878C14.7858 3.21216 13.4039 2.08203 11.758 2.08203C11.1171 2.08203 10.5162 2.25337 9.9987 2.55275C9.48117 2.25337 8.88032 2.08203 8.23944 2.08203C6.59353 2.08203 5.21157 3.21216 4.82722 4.73878C3.28377 4.90504 2.08203 6.21199 2.08203 7.79962C2.08203 8.66108 2.43585 9.43991 3.00609 9.9987C2.43585 10.5575 2.08203 11.3363 2.08203 12.1978C2.08203 13.4849 2.87191 14.5876 3.99339 15.0475C4.46688 16.7033 5.9917 17.9154 7.79962 17.9154C8.61335 17.9154 9.36972 17.6698 9.9987 17.2488C10.6277 17.6698 11.384 17.9154 12.1978 17.9154C14.0057 17.9154 15.5305 16.7033 16.004 15.0475Z" stroke="currentColor"/>`,
|
||||
"bullet-list": `<path d="M9.58329 13.7497H17.0833M9.58329 6.24967H17.0833M6.24996 6.24967C6.24996 7.17015 5.50377 7.91634 4.58329 7.91634C3.66282 7.91634 2.91663 7.17015 2.91663 6.24967C2.91663 5.3292 3.66282 4.58301 4.58329 4.58301C5.50377 4.58301 6.24996 5.3292 6.24996 6.24967ZM6.24996 13.7497C6.24996 14.6701 5.50377 15.4163 4.58329 15.4163C3.66282 15.4163 2.91663 14.6701 2.91663 13.7497C2.91663 12.8292 3.66282 12.083 4.58329 12.083C5.50377 12.083 6.24996 12.8292 6.24996 13.7497Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"check-small": `<path d="M6.5 11.4412L8.97059 13.5L13.5 6.5" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"chevron-down": `<path d="M6.6665 8.33325L9.99984 11.6666L13.3332 8.33325" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"chevron-right": `<path d="M8.33301 13.3327L11.6663 9.99935L8.33301 6.66602" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"chevron-grabber-vertical": `<path d="M6.66675 12.4998L10.0001 15.8332L13.3334 12.4998M6.66675 7.49984L10.0001 4.1665L13.3334 7.49984" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"chevron-double-right": `<path d="M11.6654 13.3346L14.9987 10.0013L11.6654 6.66797M5.83203 13.3346L9.16536 10.0013L5.83203 6.66797" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"circle-x": `<path fill-rule="evenodd" clip-rule="evenodd" d="M1.6665 10.0003C1.6665 5.39795 5.39746 1.66699 9.99984 1.66699C14.6022 1.66699 18.3332 5.39795 18.3332 10.0003C18.3332 14.6027 14.6022 18.3337 9.99984 18.3337C5.39746 18.3337 1.6665 14.6027 1.6665 10.0003ZM7.49984 6.91107L6.91058 7.50033L9.41058 10.0003L6.91058 12.5003L7.49984 13.0896L9.99984 10.5896L12.4998 13.0896L13.0891 12.5003L10.5891 10.0003L13.0891 7.50033L12.4998 6.91107L9.99984 9.41107L7.49984 6.91107Z" fill="currentColor"/>`,
|
||||
close: `<path d="M3.75 3.75L16.25 16.25M16.25 3.75L3.75 16.25" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"close-small": `<path d="M6 6L14 14M14 6L6 14" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
checklist: `<path d="M9.58342 13.7498H17.0834M9.58342 6.24984H17.0834M2.91675 6.6665L4.58341 7.9165L7.08341 4.1665M2.91675 14.1665L4.58341 15.4165L7.08341 11.6665" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
console: `<path d="M3.75 5.4165L8.33333 9.99984L3.75 14.5832M10.4167 14.5832H16.25" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
expand: `<path d="M4.58301 10.4163V15.4163H9.58301M10.4163 4.58301H15.4163V9.58301" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
collapse: `<path d="M16.666 8.33398H11.666V3.33398" stroke="currentColor" stroke-linecap="square"/><path d="M8.33398 16.666V11.666H3.33398" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
code: `<path d="M8.7513 7.5013L6.2513 10.0013L8.7513 12.5013M11.2513 7.5013L13.7513 10.0013L11.2513 12.5013M2.91797 2.91797H17.0846V17.0846H2.91797V2.91797Z" stroke="currentColor"/>`,
|
||||
"code-lines": `<path d="M2.08325 3.75H11.2499M14.5833 3.75H17.9166M2.08325 10L7.08325 10M10.4166 10L17.9166 10M2.08325 16.25L8.74992 16.25M12.0833 16.25L17.9166 16.25" stroke="currentColor" stroke-linecap="square" stroke-linejoin="round"/>`,
|
||||
"circle-ban-sign": `<path d="M15.3675 4.63087L4.55742 15.441M17.9163 9.9987C17.9163 14.371 14.3719 17.9154 9.99967 17.9154C7.81355 17.9154 5.83438 17.0293 4.40175 15.5966C2.96911 14.164 2.08301 12.1848 2.08301 9.9987C2.08301 5.62644 5.62742 2.08203 9.99967 2.08203C12.1858 2.08203 14.165 2.96813 15.5976 4.40077C17.0302 5.8334 17.9163 7.81257 17.9163 9.9987Z" stroke="currentColor" stroke-linecap="round"/>`,
|
||||
"edit-small-2": `<path d="M17.0834 17.0833V17.5833H17.5834V17.0833H17.0834ZM2.91675 17.0833H2.41675V17.5833H2.91675V17.0833ZM2.91675 2.91659V2.41659H2.41675V2.91659H2.91675ZM9.58341 3.41659H10.0834V2.41659H9.58341V2.91659V3.41659ZM17.5834 10.4166V9.91659H16.5834V10.4166H17.0834H17.5834ZM10.4167 7.08325L10.0632 6.7297L9.91675 6.87615V7.08325H10.4167ZM10.4167 9.58325H9.91675V10.0833H10.4167V9.58325ZM12.9167 9.58325V10.0833H13.1239L13.2703 9.93681L12.9167 9.58325ZM15.4167 2.08325L15.7703 1.7297L15.4167 1.37615L15.0632 1.7297L15.4167 2.08325ZM17.9167 4.58325L18.2703 4.93681L18.6239 4.58325L18.2703 4.2297L17.9167 4.58325ZM17.0834 17.0833V16.5833H2.91675V17.0833V17.5833H17.0834V17.0833ZM2.91675 17.0833H3.41675V2.91659H2.91675H2.41675V17.0833H2.91675ZM2.91675 2.91659V3.41659H9.58341V2.91659V2.41659H2.91675V2.91659ZM17.0834 10.4166H16.5834V17.0833H17.0834H17.5834V10.4166H17.0834ZM10.4167 7.08325H9.91675V9.58325H10.4167H10.9167V7.08325H10.4167ZM10.4167 9.58325V10.0833H12.9167V9.58325V9.08325H10.4167V9.58325ZM10.4167 7.08325L10.7703 7.43681L15.7703 2.43681L15.4167 2.08325L15.0632 1.7297L10.0632 6.7297L10.4167 7.08325ZM15.4167 2.08325L15.0632 2.43681L17.5632 4.93681L17.9167 4.58325L18.2703 4.2297L15.7703 1.7297L15.4167 2.08325ZM17.9167 4.58325L17.5632 4.2297L12.5632 9.2297L12.9167 9.58325L13.2703 9.93681L18.2703 4.93681L17.9167 4.58325Z" fill="currentColor"/>`,
|
||||
eye: `<path d="M10 4.58325C5.83333 4.58325 2.5 9.99992 2.5 9.99992C2.5 9.99992 5.83333 15.4166 10 15.4166C14.1667 15.4166 17.5 9.99992 17.5 9.99992C17.5 9.99992 14.1667 4.58325 10 4.58325Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><circle cx="10" cy="10" r="2.5" stroke="currentColor"/>`,
|
||||
enter: `<path d="M5.83333 15.8334L2.5 12.5L5.83333 9.16671M3.33333 12.5H17.9167V4.58337H10" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
folder: `<path d="M2.08301 2.91675V16.2501H17.9163V5.41675H9.99967L8.33301 2.91675H2.08301Z" stroke="currentColor" stroke-linecap="round"/>`,
|
||||
"magnifying-glass": `<path d="M15.8332 15.8337L13.0819 13.0824M14.6143 9.39088C14.6143 12.2759 12.2755 14.6148 9.39039 14.6148C6.50532 14.6148 4.1665 12.2759 4.1665 9.39088C4.1665 6.5058 6.50532 4.16699 9.39039 4.16699C12.2755 4.16699 14.6143 6.5058 14.6143 9.39088Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"plus-small": `<path d="M9.99984 5.41699V10.0003M9.99984 10.0003V14.5837M9.99984 10.0003H5.4165M9.99984 10.0003H14.5832" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
plus: `<path d="M9.9987 2.20703V9.9987M9.9987 9.9987V17.7904M9.9987 9.9987H2.20703M9.9987 9.9987H17.7904" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"pencil-line": `<path d="M9.58301 17.9166H17.9163M17.9163 5.83325L14.1663 2.08325L2.08301 14.1666V17.9166H5.83301L17.9163 5.83325Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
mcp: `<g><path d="M0.972656 9.37176L9.5214 1.60019C10.7018 0.527151 12.6155 0.527151 13.7957 1.60019C14.9761 2.67321 14.9761 4.41295 13.7957 5.48599L7.3397 11.3552" stroke="currentColor" stroke-linecap="round"/><path d="M7.42871 11.2747L13.7957 5.48643C14.9761 4.41338 16.8898 4.41338 18.0702 5.48643L18.1147 5.52688C19.2951 6.59993 19.2951 8.33966 18.1147 9.4127L10.3831 16.4414C9.98966 16.7991 9.98966 17.379 10.3831 17.7366L11.9707 19.1799" stroke="currentColor" stroke-linecap="round"/><path d="M11.6587 3.54346L5.33619 9.29119C4.15584 10.3642 4.15584 12.1039 5.33619 13.177C6.51649 14.25 8.43019 14.25 9.61054 13.177L15.9331 7.42923" stroke="currentColor" stroke-linecap="round"/></g>`,
|
||||
glasses: `<path d="M0.416626 7.91667H1.66663M19.5833 7.91667H18.3333M11.866 7.57987C11.3165 7.26398 10.6793 7.08333 9.99996 7.08333C9.32061 7.08333 8.68344 7.26398 8.13389 7.57987M8.74996 10C8.74996 12.0711 7.07103 13.75 4.99996 13.75C2.92889 13.75 1.24996 12.0711 1.24996 10C1.24996 7.92893 2.92889 6.25 4.99996 6.25C7.07103 6.25 8.74996 7.92893 8.74996 10ZM18.75 10C18.75 12.0711 17.071 13.75 15 13.75C12.9289 13.75 11.25 12.0711 11.25 10C11.25 7.92893 12.9289 6.25 15 6.25C17.071 6.25 18.75 7.92893 18.75 10Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"magnifying-glass-menu": `<path d="M2.08325 10.0002H4.58325M2.08325 5.41683H5.41659M2.08325 14.5835H5.41659M16.4583 13.9585L18.7499 16.2502M17.9166 10.0002C17.9166 12.9917 15.4915 15.4168 12.4999 15.4168C9.50838 15.4168 7.08325 12.9917 7.08325 10.0002C7.08325 7.00862 9.50838 4.5835 12.4999 4.5835C15.4915 4.5835 17.9166 7.00862 17.9166 10.0002Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"window-cursor": `<path d="M17.9166 10.4167V3.75H2.08325V17.0833H10.4166M17.9166 13.5897L11.6666 11.6667L13.5897 17.9167L15.032 15.0321L17.9166 13.5897Z" stroke="currentColor" stroke-width="1.07143" stroke-linecap="square"/><path d="M5.00024 6.125C5.29925 6.12518 5.54126 6.36795 5.54126 6.66699C5.54108 6.96589 5.29914 7.20783 5.00024 7.20801C4.7012 7.20801 4.45843 6.966 4.45825 6.66699C4.45825 6.36784 4.70109 6.125 5.00024 6.125ZM7.91626 6.125C8.21541 6.125 8.45825 6.36784 8.45825 6.66699C8.45808 6.966 8.21531 7.20801 7.91626 7.20801C7.61736 7.20783 7.37542 6.96589 7.37524 6.66699C7.37524 6.36795 7.61726 6.12518 7.91626 6.125ZM10.8333 6.125C11.1324 6.125 11.3752 6.36784 11.3752 6.66699C11.3751 6.966 11.1323 7.20801 10.8333 7.20801C10.5342 7.20801 10.2914 6.966 10.2913 6.66699C10.2913 6.36784 10.5341 6.125 10.8333 6.125Z" fill="currentColor" stroke="currentColor" stroke-width="0.25" stroke-linecap="square"/>`,
|
||||
task: `<path d="M9.99992 2.0835V17.9168M7.08325 3.75016H2.08325V16.2502H7.08325M12.9166 16.2502H17.9166V3.75016H12.9166" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
stop: `<rect x="5" y="5" width="10" height="10" fill="currentColor"/>`,
|
||||
"layout-left": `<path d="M2.91675 2.91699L2.91675 2.41699L2.41675 2.41699L2.41675 2.91699L2.91675 2.91699ZM17.0834 2.91699L17.5834 2.91699L17.5834 2.41699L17.0834 2.41699L17.0834 2.91699ZM17.0834 17.0837L17.0834 17.5837L17.5834 17.5837L17.5834 17.0837L17.0834 17.0837ZM2.91675 17.0837L2.41675 17.0837L2.41675 17.5837L2.91675 17.5837L2.91675 17.0837ZM7.41674 17.0837L7.41674 17.5837L8.41674 17.5837L8.41674 17.0837L7.91674 17.0837L7.41674 17.0837ZM8.41674 2.91699L8.41674 2.41699L7.41674 2.41699L7.41674 2.91699L7.91674 2.91699L8.41674 2.91699ZM2.91675 2.91699L2.91675 3.41699L17.0834 3.41699L17.0834 2.91699L17.0834 2.41699L2.91675 2.41699L2.91675 2.91699ZM17.0834 2.91699L16.5834 2.91699L16.5834 17.0837L17.0834 17.0837L17.5834 17.0837L17.5834 2.91699L17.0834 2.91699ZM17.0834 17.0837L17.0834 16.5837L2.91675 16.5837L2.91675 17.0837L2.91675 17.5837L17.0834 17.5837L17.0834 17.0837ZM2.91675 17.0837L3.41675 17.0837L3.41675 2.91699L2.91675 2.91699L2.41675 2.91699L2.41675 17.0837L2.91675 17.0837ZM7.91674 17.0837L8.41674 17.0837L8.41674 2.91699L7.91674 2.91699L7.41674 2.91699L7.41674 17.0837L7.91674 17.0837Z" fill="currentColor"/>`,
|
||||
"layout-left-partial": `<path d="M2.91732 2.91602L7.91732 2.91602L7.91732 17.0827H2.91732L2.91732 2.91602Z" fill="currentColor" fill-opacity="40%" /><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M2.91732 2.91602L7.91732 2.91602M17.084 2.91602L17.084 17.0827M17.084 2.91602L7.91732 2.91602M17.084 17.0827L2.91732 17.0827M17.084 17.0827L7.91732 17.0827M2.91732 17.0827H7.91732M7.91732 17.0827L7.91732 2.91602" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"layout-left-full": `<path d="M2.91732 2.91602L7.91732 2.91602L7.91732 17.0827H2.91732L2.91732 2.91602Z" fill="currentColor"/><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M2.91732 2.91602L7.91732 2.91602M17.084 2.91602L17.084 17.0827M17.084 2.91602L7.91732 2.91602M17.084 17.0827L2.91732 17.0827M17.084 17.0827L7.91732 17.0827M2.91732 17.0827H7.91732M7.91732 17.0827L7.91732 2.91602" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"layout-right": `<path d="M17.0832 2.91699L17.5832 2.91699L17.5832 2.41699L17.0832 2.41699L17.0832 2.91699ZM2.91651 2.91699L2.91651 2.41699L2.41651 2.41699L2.41651 2.91699L2.91651 2.91699ZM2.9165 17.0837L2.4165 17.0837L2.4165 17.5837L2.9165 17.5837L2.9165 17.0837ZM17.0832 17.0837L17.0832 17.5837L17.5832 17.5837L17.5832 17.0837L17.0832 17.0837ZM11.5832 17.0837L11.5832 17.5837L12.5832 17.5837L12.5832 17.0837L12.0832 17.0837L11.5832 17.0837ZM12.5832 2.91699L12.5832 2.41699L11.5832 2.41699L11.5832 2.91699L12.0832 2.91699L12.5832 2.91699ZM17.0832 2.91699L17.0832 2.41699L2.91651 2.41699L2.91651 2.91699L2.91651 3.41699L17.0832 3.41699L17.0832 2.91699ZM2.91651 2.91699L2.41651 2.91699L2.4165 17.0837L2.9165 17.0837L3.4165 17.0837L3.41651 2.91699L2.91651 2.91699ZM2.9165 17.0837L2.9165 17.5837L17.0832 17.5837L17.0832 17.0837L17.0832 16.5837L2.9165 16.5837L2.9165 17.0837ZM17.0832 17.0837L17.5832 17.0837L17.5832 2.91699L17.0832 2.91699L16.5832 2.91699L16.5832 17.0837L17.0832 17.0837ZM12.0832 17.0837L12.5832 17.0837L12.5832 2.91699L12.0832 2.91699L11.5832 2.91699L11.5832 17.0837L12.0832 17.0837Z" fill="currentColor"/>`,
|
||||
"layout-right-partial": `<path d="M12.0827 2.91602L2.91602 2.91602L2.91602 17.0827L12.0827 17.0827L12.0827 2.91602Z" fill="currentColor" fill-opacity="40%" /><path d="M2.91602 2.91602L17.0827 2.91602L17.0827 17.0827L2.91602 17.0827M2.91602 2.91602L2.91602 17.0827M2.91602 2.91602L12.0827 2.91602L12.0827 17.0827L2.91602 17.0827" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"layout-right-full": `<path d="M12.0827 2.91602L2.91602 2.91602L2.91602 17.0827L12.0827 17.0827L12.0827 2.91602Z" fill="currentColor"/><path d="M2.91602 2.91602L17.0827 2.91602L17.0827 17.0827L2.91602 17.0827M2.91602 2.91602L2.91602 17.0827M2.91602 2.91602L12.0827 2.91602L12.0827 17.0827L2.91602 17.0827" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"square-arrow-top-right": `<path d="M7.91675 2.9165H2.91675V17.0832H17.0834V12.0832M12.0834 2.9165H17.0834V7.9165M9.58342 10.4165L16.6667 3.33317" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"speech-bubble": `<path d="M18.3334 10.0003C18.3334 5.57324 15.0927 2.91699 10.0001 2.91699C4.90749 2.91699 1.66675 5.57324 1.66675 10.0003C1.66675 11.1497 2.45578 13.1016 2.5771 13.3949C2.5878 13.4207 2.59839 13.4444 2.60802 13.4706C2.69194 13.6996 3.04282 14.9364 1.66675 16.7684C3.5186 17.6538 5.48526 16.1982 5.48526 16.1982C6.84592 16.9202 8.46491 17.0837 10.0001 17.0837C15.0927 17.0837 18.3334 14.4274 18.3334 10.0003Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
comment: `<path d="M16.25 3.75H3.75V16.25L6.875 14.4643H16.25V3.75Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"folder-add-left": `<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"/>`,
|
||||
github: `<path d="M10.0001 1.62549C14.6042 1.62549 18.3334 5.35465 18.3334 9.95882C18.333 11.7049 17.785 13.4068 16.7666 14.8251C15.7482 16.2434 14.3107 17.3066 12.6563 17.8651C12.2397 17.9484 12.0834 17.688 12.0834 17.4692C12.0834 17.188 12.0938 16.2922 12.0938 15.1776C12.0938 14.3963 11.8334 13.8963 11.5313 13.6359C13.3855 13.4276 15.3334 12.7192 15.3334 9.52132C15.3334 8.60465 15.0105 7.86507 14.4792 7.28174C14.5626 7.0734 14.8542 6.21924 14.3959 5.0734C14.3959 5.0734 13.698 4.84424 12.1042 5.92757C11.4376 5.74007 10.7292 5.64632 10.0209 5.64632C9.31258 5.64632 8.60425 5.74007 7.93758 5.92757C6.34383 4.85465 5.64592 5.0734 5.64592 5.0734C5.18758 6.21924 5.47925 7.0734 5.56258 7.28174C5.03133 7.86507 4.70842 8.61507 4.70842 9.52132C4.70842 12.7088 6.64592 13.4276 8.50008 13.6359C8.2605 13.8442 8.04175 14.2088 7.96883 14.7505C7.48967 14.9692 6.29175 15.3234 5.54175 14.063C5.3855 13.813 4.91675 13.1984 4.2605 13.2088C3.56258 13.2192 3.97925 13.6047 4.27092 13.7609C4.62508 13.9588 5.03133 14.6984 5.12508 14.938C5.29175 15.4067 5.83342 16.3026 7.92717 15.9172C7.92717 16.6151 7.93758 17.2713 7.93758 17.4692C7.93758 17.688 7.78133 17.938 7.36467 17.8651C5.70491 17.3126 4.26126 16.2515 3.23851 14.8324C2.21576 13.4133 1.66583 11.7081 1.66675 9.95882C1.66675 5.35465 5.39592 1.62549 10.0001 1.62549Z" fill="currentColor"/>`,
|
||||
discord: `<path d="M16.0742 4.45014C14.9244 3.92097 13.7106 3.54556 12.4638 3.3335C12.2932 3.64011 12.1388 3.95557 12.0013 4.27856C10.6732 4.07738 9.32261 4.07738 7.99451 4.27856C7.85694 3.9556 7.70257 3.64014 7.53203 3.3335C6.28441 3.54735 5.06981 3.92365 3.91889 4.45291C1.63401 7.85128 1.01462 11.1652 1.32431 14.4322C2.6624 15.426 4.16009 16.1819 5.7523 16.6668C6.11082 16.1821 6.42806 15.6678 6.70066 15.1295C6.18289 14.9351 5.68315 14.6953 5.20723 14.4128C5.33249 14.3215 5.45499 14.2274 5.57336 14.136C6.95819 14.7907 8.46965 15.1302 9.99997 15.1302C11.5303 15.1302 13.0418 14.7907 14.4266 14.136C14.5463 14.2343 14.6688 14.3284 14.7927 14.4128C14.3159 14.6957 13.8152 14.9361 13.2965 15.1309C13.5688 15.669 13.8861 16.1828 14.2449 16.6668C15.8385 16.1838 17.3373 15.4283 18.6756 14.4335C19.039 10.645 18.0549 7.36145 16.0742 4.45014ZM7.09294 12.423C6.22992 12.423 5.51693 11.6357 5.51693 10.6671C5.51693 9.69852 6.20514 8.90427 7.09019 8.90427C7.97524 8.90427 8.68272 9.69852 8.66758 10.6671C8.65244 11.6357 7.97248 12.423 7.09294 12.423ZM12.907 12.423C12.0426 12.423 11.3324 11.6357 11.3324 10.6671C11.3324 9.69852 12.0206 8.90427 12.907 8.90427C13.7934 8.90427 14.4954 9.69852 14.4803 10.6671C14.4651 11.6357 13.7865 12.423 12.907 12.423Z" fill="currentColor"/>`,
|
||||
"layout-bottom": `<path d="M2.91699 17.0832L2.41699 17.0832L2.41699 17.5832L2.91699 17.5832L2.91699 17.0832ZM2.91699 2.91653L2.91699 2.41653L2.41699 2.41653L2.41699 2.91653L2.91699 2.91653ZM17.0837 2.91653L17.5837 2.91653L17.5837 2.41653L17.0837 2.41653L17.0837 2.91653ZM17.0837 17.0832L17.5837 17.0832L17.5837 17.5832L17.0837 17.5832L17.0837 17.0832ZM17.0837 12.5827L17.5837 12.5827L17.5837 11.5827L17.0837 11.5827L17.0837 12.0827L17.0837 12.5827ZM2.91699 11.5827L2.41699 11.5827L2.41699 12.5827L2.91699 12.5827L2.91699 12.0827L2.91699 11.5827ZM2.91699 17.0832L3.41699 17.0832L3.41699 2.91653L2.91699 2.91653L2.41699 2.91653L2.41699 17.0832L2.91699 17.0832ZM2.91699 2.91653L2.91699 3.41653L17.0837 3.41653L17.0837 2.91653L17.0837 2.41653L2.91699 2.41653L2.91699 2.91653ZM17.0837 2.91653L16.5837 2.91653L16.5837 17.0832L17.0837 17.0832L17.5837 17.0832L17.5837 2.91653L17.0837 2.91653ZM17.0837 17.0832L17.0837 16.5832L2.91699 16.5832L2.91699 17.0832L2.91699 17.5832L17.0837 17.5832L17.0837 17.0832ZM17.0837 12.0827L17.0837 11.5827L2.91699 11.5827L2.91699 12.0827L2.91699 12.5827L17.0837 12.5827L17.0837 12.0827Z" fill="currentColor"/>`,
|
||||
"layout-bottom-partial": `<path d="M2.91732 12.0827L17.084 12.0827L17.084 17.0827H2.91732L2.91732 12.0827Z" fill="currentColor" fill-opacity="40%" /><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M17.084 2.91602L17.084 17.0827M17.084 17.0827L2.91732 17.0827M2.91732 12.0827L17.084 12.0827" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"layout-bottom-full": `<path d="M2.91732 12.0827L17.084 12.0827L17.084 17.0827H2.91732L2.91732 12.0827Z" fill="currentColor"/><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M17.084 2.91602L17.084 17.0827M17.084 17.0827L2.91732 17.0827M2.91732 12.0827L17.084 12.0827" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"dot-grid": `<path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" fill="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" fill="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" fill="currentColor"/><path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" stroke="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" stroke="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" stroke="currentColor"/>`,
|
||||
"circle-check": `<path d="M12.4987 7.91732L8.7487 12.5007L7.08203 10.834M17.9154 10.0007C17.9154 14.3729 14.371 17.9173 9.9987 17.9173C5.62644 17.9173 2.08203 14.3729 2.08203 10.0007C2.08203 5.6284 5.62644 2.08398 9.9987 2.08398C14.371 2.08398 17.9154 5.6284 17.9154 10.0007Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
copy: `<path d="M6.2513 6.24935V2.91602H17.0846V13.7493H13.7513M13.7513 6.24935V17.0827H2.91797V6.24935H13.7513Z" stroke="currentColor" stroke-linecap="round"/>`,
|
||||
check: `<path d="M5 11.9657L8.37838 14.7529L15 5.83398" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
photo: `<path d="M16.6665 16.6666L11.6665 11.6666L9.99984 13.3333L6.6665 9.99996L3.08317 13.5833M2.9165 2.91663H17.0832V17.0833H2.9165V2.91663ZM13.3332 7.49996C13.3332 8.30537 12.6803 8.95829 11.8748 8.95829C11.0694 8.95829 10.4165 8.30537 10.4165 7.49996C10.4165 6.69454 11.0694 6.04163 11.8748 6.04163C12.6803 6.04163 13.3332 6.69454 13.3332 7.49996Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
share: `<path d="M10.0013 12.0846L10.0013 3.33464M13.7513 6.66797L10.0013 2.91797L6.2513 6.66797M17.0846 10.418V17.0846H2.91797V10.418" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
download: `<path d="M13.9583 10.6257L10 14.584L6.04167 10.6257M10 2.08398V13.959M16.25 17.9173H3.75" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
menu: `<path d="M2.5 5H17.5M2.5 10H17.5M2.5 15H17.5" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
server: `<rect x="3.35547" y="1.92969" width="13.2857" height="16.1429" stroke="currentColor"/><rect x="3.35547" y="11.9297" width="13.2857" height="6.14286" stroke="currentColor"/><rect x="12.8555" y="14.2852" width="1.42857" height="1.42857" fill="currentColor"/><rect x="10" y="14.2852" width="1.42857" height="1.42857" fill="currentColor"/>`,
|
||||
branch: `<path d="M14.2036 7.19987L14.2079 6.69989L13.2079 6.69132L13.2036 7.1913L13.7036 7.19559L14.2036 7.19987ZM8.14804 5.09032H7.64804C7.64804 5.75797 7.06861 6.34471 6.29619 6.34471V6.84471V7.34471C7.56926 7.34471 8.64804 6.36051 8.64804 5.09032H8.14804ZM6.29619 6.84471V6.34471C5.52376 6.34471 4.94434 5.75797 4.94434 5.09032H4.44434H3.94434C3.94434 6.36051 5.02311 7.34471 6.29619 7.34471V6.84471ZM4.44434 5.09032H4.94434C4.94434 4.42267 5.52376 3.83594 6.29619 3.83594V3.33594V2.83594C5.02311 2.83594 3.94434 3.82013 3.94434 5.09032H4.44434ZM6.29619 3.33594V3.83594C7.06861 3.83594 7.64804 4.42267 7.64804 5.09032H8.14804H8.64804C8.64804 3.82013 7.56926 2.83594 6.29619 2.83594V3.33594ZM8.14804 14.9149H7.64804C7.64804 15.5825 7.06861 16.1693 6.29619 16.1693V16.6693V17.1693C7.56926 17.1693 8.64804 16.1851 8.64804 14.9149H8.14804ZM6.29619 16.6693V16.1693C5.52376 16.1693 4.94434 15.5825 4.94434 14.9149H4.44434H3.94434C3.94434 16.1851 5.02311 17.1693 6.29619 17.1693V16.6693ZM4.44434 14.9149H4.94434C4.94434 14.2472 5.52376 13.6605 6.29619 13.6605V13.1605V12.6605C5.02311 12.6605 3.94434 13.6447 3.94434 14.9149H4.44434ZM6.29619 13.1605V13.6605C7.06861 13.6605 7.64804 14.2472 7.64804 14.9149H8.14804H8.64804C8.64804 13.6447 7.56926 12.6605 6.29619 12.6605V13.1605ZM15.5554 5.09032H15.0554C15.0554 5.75797 14.476 6.34471 13.7036 6.34471V6.84471V7.34471C14.9767 7.34471 16.0554 6.36051 16.0554 5.09032H15.5554ZM13.7036 6.84471V6.34471C12.9312 6.34471 12.3517 5.75797 12.3517 5.09032H11.8517H11.3517C11.3517 6.36051 12.4305 7.34471 13.7036 7.34471V6.84471ZM11.8517 5.09032H12.3517C12.3517 4.42267 12.9312 3.83594 13.7036 3.83594V3.33594V2.83594C12.4305 2.83594 11.3517 3.82013 11.3517 5.09032H11.8517ZM13.7036 3.33594V3.83594C14.476 3.83594 15.0554 4.42267 15.0554 5.09032H15.5554H16.0554C16.0554 3.82013 14.9767 2.83594 13.7036 2.83594V3.33594ZM13.7036 7.19559L13.2036 7.1913L13.1544 12.9277L13.6544 12.932L14.1544 12.9363L14.2036 7.19987L13.7036 7.19559ZM6.29619 6.84471H5.79619V13.1605H6.29619H6.79619V6.84471H6.29619ZM11.6545 14.9149V14.4149H8.14804V14.9149V15.4149H11.6545V14.9149ZM13.6544 12.932L13.1544 12.9277C13.1474 13.7511 12.4779 14.4149 11.6545 14.4149V14.9149V15.4149C13.0269 15.4149 14.1426 14.3086 14.1544 12.9363L13.6544 12.932Z" fill="currentColor"/>`,
|
||||
edit: `<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"/>`,
|
||||
help: `<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"/>`,
|
||||
"settings-gear": `<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"/>`,
|
||||
dash: `<rect x="5" y="9.5" width="10" height="1" fill="currentColor"/>`,
|
||||
"cloud-upload": `<path d="M12.0833 16.25H15C17.0711 16.25 18.75 14.5711 18.75 12.5C18.75 10.5649 17.2843 8.97217 15.4025 8.77133C15.2 6.13103 12.8586 4.08333 10 4.08333C7.71532 4.08333 5.76101 5.49781 4.96501 7.49881C2.84892 7.90461 1.25 9.76559 1.25 11.6667C1.25 13.9813 3.30203 16.25 5.83333 16.25H7.91667M10 16.25V10.4167M12.0833 11.875L10 9.79167L7.91667 11.875" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
trash: `<path d="M4.58342 17.9134L4.58369 17.4134L4.22787 17.5384L4.22766 18.0384H4.58342V17.9134ZM15.4167 17.9134V18.0384H15.7725L15.7723 17.5384L15.4167 17.9134ZM2.08342 3.95508V3.45508H1.58342V3.95508H2.08342V4.45508V3.95508ZM17.9167 4.45508V4.95508H18.4167V4.45508H17.9167V3.95508V4.45508ZM4.16677 4.58008L3.66701 4.5996L4.22816 17.5379L4.72792 17.4934L5.22767 17.4489L4.66652 4.54055L4.16677 4.58008ZM4.58342 18.0384V17.9134H15.4167V18.0384V18.5384H4.58342V18.0384ZM15.4167 17.9134L15.8332 17.5379L16.2498 4.5996L15.7501 4.58008L15.2503 4.56055L14.8337 17.4989L15.4167 17.9134ZM15.8334 4.58008V4.08008H4.16677V4.58008V5.08008H15.8334V4.58008ZM2.08342 4.45508V4.95508H4.16677V4.58008V4.08008H2.08342V4.45508ZM15.8334 4.58008V5.08008H17.9167V4.45508V3.95508H15.8334V4.58008ZM6.83951 4.35149L7.432 4.55047C7.79251 3.47701 8.80699 2.70508 10.0001 2.70508V2.20508V1.70508C8.25392 1.70508 6.77335 2.83539 6.24702 4.15251L6.83951 4.35149ZM10.0001 2.20508V2.70508C11.1932 2.70508 12.2077 3.47701 12.5682 4.55047L13.1607 4.35149L13.7532 4.15251C13.2269 2.83539 11.7463 1.70508 10.0001 1.70508V2.20508Z" fill="currentColor"/>`,
|
||||
sliders: `<path d="M3.625 6.25H10.9375M16.375 13.75H10.5625M3.625 13.75H4.9375M11.125 6.25C11.125 4.79969 12.2997 3.625 13.75 3.625C15.2003 3.625 16.375 4.79969 16.375 6.25C16.375 7.70031 15.2003 8.875 13.75 8.875C12.2997 8.875 11.125 7.70031 11.125 6.25ZM10.375 13.75C10.375 15.2003 9.20031 16.375 7.75 16.375C6.29969 16.375 5.125 15.2003 5.125 13.75C5.125 12.2997 6.29969 11.125 7.75 11.125C9.20031 11.125 10.375 12.2997 10.375 13.75Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
keyboard: `<path d="M5.125 7.375V4.375H14.875V2.875M8.3125 13.9375H11.6875M8.125 13.9375H11.875M2.125 7.375H17.875V17.125H2.125V7.375ZM5.5 10.375H5.125V10.75H5.5V10.375ZM8.5 10.375H8.125V10.75H8.5V10.375ZM11.875 10.375H11.5V10.75H11.875V10.375ZM14.875 10.375H14.5V10.75H14.875V10.375ZM14.875 13.75H14.5V14.125H14.875V13.75ZM5.5 13.75H5.125V14.125H5.5V13.75Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
selector: `<path d="M6.66626 12.5033L9.99959 15.8366L13.3329 12.5033M6.66626 7.50326L9.99959 4.16992L13.3329 7.50326" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"arrow-down-to-line": `<path d="M15.2083 11.6667L10 16.875L4.79167 11.6667M10 16.25V3.125" stroke="currentColor" stroke-width="1.25" stroke-linecap="square"/>`,
|
||||
link: `<path d="M2.08334 12.0833L1.72979 11.7298L1.37624 12.0833L1.72979 12.4369L2.08334 12.0833ZM7.91668 17.9167L7.56312 18.2702L7.91668 18.6238L8.27023 18.2702L7.91668 17.9167ZM17.9167 7.91666L18.2702 8.27022L18.6238 7.91666L18.2702 7.56311L17.9167 7.91666ZM12.0833 2.08333L12.4369 1.72977L12.0833 1.37622L11.7298 1.72977L12.0833 2.08333ZM8.39646 5.06311L8.0429 5.41666L8.75001 6.12377L9.10356 5.77021L8.75001 5.41666L8.39646 5.06311ZM5.77023 9.10355L6.12378 8.74999L5.41668 8.04289L5.06312 8.39644L5.41668 8.74999L5.77023 9.10355ZM14.2298 10.8964L13.8762 11.25L14.5833 11.9571L14.9369 11.6035L14.5833 11.25L14.2298 10.8964ZM11.6036 14.9369L11.9571 14.5833L11.25 13.8762L10.8965 14.2298L11.25 14.5833L11.6036 14.9369ZM7.14646 12.1464L6.7929 12.5L7.50001 13.2071L7.85356 12.8535L7.50001 12.5L7.14646 12.1464ZM12.8536 7.85355L13.2071 7.49999L12.5 6.79289L12.1465 7.14644L12.5 7.49999L12.8536 7.85355ZM2.08334 12.0833L1.72979 12.4369L7.56312 18.2702L7.91668 17.9167L8.27023 17.5631L2.4369 11.7298L2.08334 12.0833ZM17.9167 7.91666L18.2702 7.56311L12.4369 1.72977L12.0833 2.08333L11.7298 2.43688L17.5631 8.27022L17.9167 7.91666ZM12.0833 2.08333L11.7298 1.72977L8.39646 5.06311L8.75001 5.41666L9.10356 5.77021L12.4369 2.43688L12.0833 2.08333ZM5.41668 8.74999L5.06312 8.39644L1.72979 11.7298L2.08334 12.0833L2.4369 12.4369L5.77023 9.10355L5.41668 8.74999ZM14.5833 11.25L14.9369 11.6035L18.2702 8.27022L17.9167 7.91666L17.5631 7.56311L14.2298 10.8964L14.5833 11.25ZM7.91668 17.9167L8.27023 18.2702L11.6036 14.9369L11.25 14.5833L10.8965 14.2298L7.56312 17.5631L7.91668 17.9167ZM7.50001 12.5L7.85356 12.8535L12.8536 7.85355L12.5 7.49999L12.1465 7.14644L7.14646 12.1464L7.50001 12.5Z" fill="currentColor"/>`,
|
||||
providers: `<path d="M10.0001 4.37562V2.875M13 4.37793V2.87793M7.00014 4.37793V2.875M10 17.1279V15.6279M13 17.1279V15.6279M7 17.1279V15.6279M15.625 13.0029H17.125M15.625 7.00293H17.125M15.625 10.0029H17.125M2.875 10.0029H4.375M2.875 13.0029H4.375M2.875 7.00293H4.375M4.375 4.37793H15.625V15.6279H4.375V4.37793ZM12.6241 10.0022C12.6241 11.4519 11.4488 12.6272 9.99908 12.6272C8.54934 12.6272 7.37408 11.4519 7.37408 10.0022C7.37408 8.55245 8.54934 7.3772 9.99908 7.3772C11.4488 7.3772 12.6241 8.55245 12.6241 10.0022Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
models: `<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5 10C12.2917 10 10 12.2917 10 17.5C10 12.2917 7.70833 10 2.5 10C7.70833 10 10 7.70833 10 2.5C10 7.70833 12.2917 10 17.5 10Z" stroke="currentColor"/>`,
|
||||
}
|
||||
|
||||
export interface IconProps extends ComponentProps<"svg"> {
|
||||
name: keyof typeof icons
|
||||
size?: "small" | "normal" | "medium" | "large"
|
||||
}
|
||||
|
||||
export function Icon(props: IconProps) {
|
||||
const [local, others] = splitProps(props, ["name", "size", "class", "classList"])
|
||||
return (
|
||||
<div data-component="icon" data-size={local.size || "normal"}>
|
||||
<svg
|
||||
data-slot="icon-svg"
|
||||
classList={{
|
||||
...(local.classList || {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
fill="none"
|
||||
viewBox="0 0 20 20"
|
||||
innerHTML={icons[local.name as keyof typeof icons]}
|
||||
aria-hidden="true"
|
||||
{...others}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
63
opencode/packages/ui/src/components/image-preview.css
Normal file
63
opencode/packages/ui/src/components/image-preview.css
Normal file
@@ -0,0 +1,63 @@
|
||||
[data-component="image-preview"] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
[data-slot="image-preview-container"] {
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
width: min(calc(100vw - 32px), 90vw);
|
||||
max-width: 1200px;
|
||||
height: min(calc(100vh - 32px), 90vh);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
[data-slot="image-preview-content"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--surface-raised-stronger-non-alpha);
|
||||
box-shadow:
|
||||
0 15px 45px 0 rgba(19, 16, 16, 0.35),
|
||||
0 3.35px 10.051px 0 rgba(19, 16, 16, 0.25),
|
||||
0 0.998px 2.993px 0 rgba(19, 16, 16, 0.2);
|
||||
overflow: hidden;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-slot="image-preview-header"] {
|
||||
display: flex;
|
||||
padding: 8px 8px 0;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
[data-slot="image-preview-body"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
[data-slot="image-preview-image"] {
|
||||
max-width: 100%;
|
||||
max-height: calc(90vh - 100px);
|
||||
object-fit: contain;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
opencode/packages/ui/src/components/image-preview.tsx
Normal file
32
opencode/packages/ui/src/components/image-preview.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Dialog as Kobalte } from "@kobalte/core/dialog"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import { IconButton } from "./icon-button"
|
||||
|
||||
export interface ImagePreviewProps {
|
||||
src: string
|
||||
alt?: string
|
||||
}
|
||||
|
||||
export function ImagePreview(props: ImagePreviewProps) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<div data-component="image-preview">
|
||||
<div data-slot="image-preview-container">
|
||||
<Kobalte.Content data-slot="image-preview-content">
|
||||
<div data-slot="image-preview-header">
|
||||
<Kobalte.CloseButton
|
||||
data-slot="image-preview-close"
|
||||
as={IconButton}
|
||||
icon="close"
|
||||
variant="ghost"
|
||||
aria-label={i18n.t("ui.common.close")}
|
||||
/>
|
||||
</div>
|
||||
<div data-slot="image-preview-body">
|
||||
<img src={props.src} alt={props.alt ?? i18n.t("ui.imagePreview.alt")} data-slot="image-preview-image" />
|
||||
</div>
|
||||
</Kobalte.Content>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
17
opencode/packages/ui/src/components/inline-input.css
Normal file
17
opencode/packages/ui/src/components/inline-input.css
Normal file
@@ -0,0 +1,17 @@
|
||||
[data-component="inline-input"] {
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
font: inherit;
|
||||
letter-spacing: inherit;
|
||||
line-height: inherit;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 1px var(--border-interactive-focus);
|
||||
}
|
||||
}
|
||||
11
opencode/packages/ui/src/components/inline-input.tsx
Normal file
11
opencode/packages/ui/src/components/inline-input.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { ComponentProps } from "solid-js"
|
||||
import { splitProps } from "solid-js"
|
||||
|
||||
export type InlineInputProps = ComponentProps<"input"> & {
|
||||
width?: string
|
||||
}
|
||||
|
||||
export function InlineInput(props: InlineInputProps) {
|
||||
const [local, others] = splitProps(props, ["class", "width"])
|
||||
return <input data-component="inline-input" class={local.class} style={{ width: local.width }} {...others} />
|
||||
}
|
||||
18
opencode/packages/ui/src/components/keybind.css
Normal file
18
opencode/packages/ui/src/components/keybind.css
Normal file
@@ -0,0 +1,18 @@
|
||||
[data-component="keybind"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
height: 20px;
|
||||
padding: 0 8px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-base);
|
||||
box-shadow: var(--shadow-xxs-border);
|
||||
|
||||
/* text-12-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 12px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1;
|
||||
color: var(--text-weak);
|
||||
}
|
||||
20
opencode/packages/ui/src/components/keybind.tsx
Normal file
20
opencode/packages/ui/src/components/keybind.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { ComponentProps, ParentProps } from "solid-js"
|
||||
|
||||
export interface KeybindProps extends ParentProps {
|
||||
class?: string
|
||||
classList?: ComponentProps<"span">["classList"]
|
||||
}
|
||||
|
||||
export function Keybind(props: KeybindProps) {
|
||||
return (
|
||||
<span
|
||||
data-component="keybind"
|
||||
classList={{
|
||||
...(props.classList ?? {}),
|
||||
[props.class ?? ""]: !!props.class,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
115
opencode/packages/ui/src/components/line-comment.css
Normal file
115
opencode/packages/ui/src/components/line-comment.css
Normal file
@@ -0,0 +1,115 @@
|
||||
[data-component="line-comment"] {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
z-index: var(--line-comment-z, 30);
|
||||
}
|
||||
|
||||
[data-component="line-comment"][data-open] {
|
||||
z-index: var(--line-comment-open-z, 100);
|
||||
}
|
||||
|
||||
[data-component="line-comment"] [data-slot="line-comment-button"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--icon-interactive-base);
|
||||
box-shadow: var(--shadow-xs);
|
||||
cursor: default;
|
||||
border: none;
|
||||
}
|
||||
|
||||
[data-component="line-comment"] [data-component="icon"] {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
[data-component="line-comment"] [data-slot="line-comment-button"]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-component="line-comment"] [data-slot="line-comment-button"]:focus-visible {
|
||||
box-shadow: var(--shadow-xs-border-focus);
|
||||
}
|
||||
|
||||
[data-component="line-comment"] [data-slot="line-comment-popover"] {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: -8px;
|
||||
z-index: var(--line-comment-popover-z, 40);
|
||||
min-width: 200px;
|
||||
max-width: min(320px, calc(100vw - 48px));
|
||||
border-radius: 8px;
|
||||
background: var(--surface-raised-stronger-non-alpha);
|
||||
box-shadow: var(--shadow-lg-border-base);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
[data-component="line-comment"][data-variant="editor"] [data-slot="line-comment-popover"] {
|
||||
width: 380px;
|
||||
max-width: min(380px, calc(100vw - 48px));
|
||||
padding: 8px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
[data-component="line-comment"] [data-slot="line-comment-content"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
[data-component="line-comment"] [data-slot="line-comment-text"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-x-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-strong);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
[data-component="line-comment"] [data-slot="line-comment-label"],
|
||||
[data-component="line-comment"] [data-slot="line-comment-editor-label"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-weak);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-component="line-comment"] [data-slot="line-comment-editor"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-component="line-comment"] [data-slot="line-comment-textarea"] {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
padding: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface-base);
|
||||
border: 1px solid var(--border-base);
|
||||
color: var(--text-strong);
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
line-height: var(--line-height-large);
|
||||
}
|
||||
|
||||
[data-component="line-comment"] [data-slot="line-comment-textarea"]:focus {
|
||||
outline: none;
|
||||
box-shadow: var(--shadow-xs-border-select);
|
||||
}
|
||||
|
||||
[data-component="line-comment"] [data-slot="line-comment-actions"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-component="line-comment"] [data-slot="line-comment-editor-label"] {
|
||||
margin-right: auto;
|
||||
}
|
||||
168
opencode/packages/ui/src/components/line-comment.tsx
Normal file
168
opencode/packages/ui/src/components/line-comment.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import { onMount, Show, splitProps, type JSX } from "solid-js"
|
||||
import { Button } from "./button"
|
||||
import { Icon } from "./icon"
|
||||
import { useI18n } from "../context/i18n"
|
||||
|
||||
export type LineCommentVariant = "default" | "editor"
|
||||
|
||||
export type LineCommentAnchorProps = {
|
||||
id?: string
|
||||
top?: number
|
||||
open: boolean
|
||||
variant?: LineCommentVariant
|
||||
onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
|
||||
onMouseEnter?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
|
||||
onPopoverFocusOut?: JSX.EventHandlerUnion<HTMLDivElement, FocusEvent>
|
||||
class?: string
|
||||
popoverClass?: string
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
export const LineCommentAnchor = (props: LineCommentAnchorProps) => {
|
||||
const hidden = () => props.top === undefined
|
||||
const variant = () => props.variant ?? "default"
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="line-comment"
|
||||
data-variant={variant()}
|
||||
data-comment-id={props.id}
|
||||
data-open={props.open ? "" : undefined}
|
||||
classList={{
|
||||
[props.class ?? ""]: !!props.class,
|
||||
}}
|
||||
style={{
|
||||
top: `${props.top ?? 0}px`,
|
||||
opacity: hidden() ? 0 : 1,
|
||||
"pointer-events": hidden() ? "none" : "auto",
|
||||
}}
|
||||
>
|
||||
<button type="button" data-slot="line-comment-button" onClick={props.onClick} onMouseEnter={props.onMouseEnter}>
|
||||
<Icon name="comment" size="small" />
|
||||
</button>
|
||||
<Show when={props.open}>
|
||||
<div
|
||||
data-slot="line-comment-popover"
|
||||
classList={{
|
||||
[props.popoverClass ?? ""]: !!props.popoverClass,
|
||||
}}
|
||||
onFocusOut={props.onPopoverFocusOut}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export type LineCommentProps = Omit<LineCommentAnchorProps, "children" | "variant"> & {
|
||||
comment: JSX.Element
|
||||
selection: JSX.Element
|
||||
}
|
||||
|
||||
export const LineComment = (props: LineCommentProps) => {
|
||||
const i18n = useI18n()
|
||||
const [split, rest] = splitProps(props, ["comment", "selection"])
|
||||
|
||||
return (
|
||||
<LineCommentAnchor {...rest} variant="default">
|
||||
<div data-slot="line-comment-content">
|
||||
<div data-slot="line-comment-text">{split.comment}</div>
|
||||
<div data-slot="line-comment-label">
|
||||
{i18n.t("ui.lineComment.label.prefix")}
|
||||
{split.selection}
|
||||
{i18n.t("ui.lineComment.label.suffix")}
|
||||
</div>
|
||||
</div>
|
||||
</LineCommentAnchor>
|
||||
)
|
||||
}
|
||||
|
||||
export type LineCommentEditorProps = Omit<LineCommentAnchorProps, "children" | "open" | "variant" | "onClick"> & {
|
||||
value: string
|
||||
selection: JSX.Element
|
||||
onInput: (value: string) => void
|
||||
onCancel: VoidFunction
|
||||
onSubmit: (value: string) => void
|
||||
placeholder?: string
|
||||
rows?: number
|
||||
autofocus?: boolean
|
||||
cancelLabel?: string
|
||||
submitLabel?: string
|
||||
}
|
||||
|
||||
export const LineCommentEditor = (props: LineCommentEditorProps) => {
|
||||
const i18n = useI18n()
|
||||
const [split, rest] = splitProps(props, [
|
||||
"value",
|
||||
"selection",
|
||||
"onInput",
|
||||
"onCancel",
|
||||
"onSubmit",
|
||||
"placeholder",
|
||||
"rows",
|
||||
"autofocus",
|
||||
"cancelLabel",
|
||||
"submitLabel",
|
||||
])
|
||||
|
||||
const refs = {
|
||||
textarea: undefined as HTMLTextAreaElement | undefined,
|
||||
}
|
||||
|
||||
const focus = () => refs.textarea?.focus()
|
||||
|
||||
const submit = () => {
|
||||
const value = split.value.trim()
|
||||
if (!value) return
|
||||
split.onSubmit(value)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (split.autofocus === false) return
|
||||
requestAnimationFrame(focus)
|
||||
})
|
||||
|
||||
return (
|
||||
<LineCommentAnchor {...rest} open={true} variant="editor" onClick={() => focus()}>
|
||||
<div data-slot="line-comment-editor">
|
||||
<textarea
|
||||
ref={(el) => {
|
||||
refs.textarea = el
|
||||
}}
|
||||
data-slot="line-comment-textarea"
|
||||
rows={split.rows ?? 3}
|
||||
placeholder={split.placeholder ?? i18n.t("ui.lineComment.placeholder")}
|
||||
value={split.value}
|
||||
onInput={(e) => split.onInput(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
split.onCancel()
|
||||
return
|
||||
}
|
||||
if (e.key !== "Enter") return
|
||||
if (e.shiftKey) return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
submit()
|
||||
}}
|
||||
/>
|
||||
<div data-slot="line-comment-actions">
|
||||
<div data-slot="line-comment-editor-label">
|
||||
{i18n.t("ui.lineComment.editorLabel.prefix")}
|
||||
{split.selection}
|
||||
{i18n.t("ui.lineComment.editorLabel.suffix")}
|
||||
</div>
|
||||
<Button size="small" variant="ghost" onClick={split.onCancel}>
|
||||
{split.cancelLabel ?? i18n.t("ui.common.cancel")}
|
||||
</Button>
|
||||
<Button size="small" variant="primary" disabled={split.value.trim().length === 0} onClick={submit}>
|
||||
{split.submitLabel ?? i18n.t("ui.lineComment.submit")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</LineCommentAnchor>
|
||||
)
|
||||
}
|
||||
331
opencode/packages/ui/src/components/list.css
Normal file
331
opencode/packages/ui/src/components/list.css
Normal file
@@ -0,0 +1,331 @@
|
||||
@property --bottom-fade {
|
||||
syntax: "<length>";
|
||||
inherits: false;
|
||||
initial-value: 0px;
|
||||
}
|
||||
|
||||
@keyframes scroll {
|
||||
0% {
|
||||
--bottom-fade: 20px;
|
||||
}
|
||||
90% {
|
||||
--bottom-fade: 20px;
|
||||
}
|
||||
100% {
|
||||
--bottom-fade: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="list"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow: hidden;
|
||||
padding: 0 12px;
|
||||
|
||||
[data-slot="list-search-wrapper"] {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
margin-bottom: 4px;
|
||||
|
||||
> [data-component="icon-button"] {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
background-color: transparent;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:focus-visible:not(:disabled),
|
||||
&:active:not(:disabled) {
|
||||
background-color: transparent;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) [data-slot="icon-svg"] {
|
||||
color: var(--icon-hover);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) [data-slot="icon-svg"] {
|
||||
color: var(--icon-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="list-search"] {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface-base);
|
||||
|
||||
[data-slot="list-search-container"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1 0 0;
|
||||
max-height: 20px;
|
||||
|
||||
[data-slot="list-search-input"] {
|
||||
width: 100%;
|
||||
|
||||
&[data-slot="input-input"] {
|
||||
line-height: 20px;
|
||||
max-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> [data-component="icon-button"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: transparent;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:focus-visible:not(:disabled),
|
||||
&:active:not(:disabled) {
|
||||
background-color: transparent;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) [data-slot="icon-svg"] {
|
||||
color: var(--icon-hover);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) [data-slot="icon-svg"] {
|
||||
color: var(--icon-active);
|
||||
}
|
||||
}
|
||||
|
||||
> [data-component="icon-button"] {
|
||||
background-color: transparent;
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:focus:not(:disabled),
|
||||
&:active:not(:disabled) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) [data-slot="icon-svg"] {
|
||||
color: var(--icon-hover);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) [data-slot="icon-svg"] {
|
||||
color: var(--icon-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="list-scroll"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
mask: linear-gradient(to bottom, #ffff calc(100% - var(--bottom-fade)), #0000);
|
||||
animation: scroll;
|
||||
animation-timeline: --scroll;
|
||||
scroll-timeline: --scroll y;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="list-empty-state"] {
|
||||
display: flex;
|
||||
padding: 32px 48px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
|
||||
[data-slot="list-message"] {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
max-width: 100%;
|
||||
color: var(--text-weak);
|
||||
white-space: nowrap;
|
||||
|
||||
/* text-14-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large); /* 142.857% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
[data-slot="list-filter"] {
|
||||
color: var(--text-strong);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="list-group"] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
[data-slot="list-header"] {
|
||||
display: flex;
|
||||
z-index: 10;
|
||||
padding: 8px 12px 8px 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
background: var(--surface-raised-stronger-non-alpha);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
color: var(--text-weak);
|
||||
|
||||
/* text-14-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 142.857% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 16px;
|
||||
background: linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha), transparent);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
&[data-stuck="true"]::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="list-items"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
|
||||
[data-slot="list-item"] {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 6px 8px 6px 8px;
|
||||
align-items: center;
|
||||
color: var(--text-strong);
|
||||
scroll-margin-top: 28px;
|
||||
|
||||
/* text-14-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 142.857% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
[data-slot="list-item-selected-icon"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
aspect-ratio: 1/1;
|
||||
[data-component="icon"] {
|
||||
color: var(--icon-strong-base);
|
||||
}
|
||||
}
|
||||
[data-slot="list-item-active-icon"] {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
aspect-ratio: 1/1;
|
||||
[data-component="icon"] {
|
||||
color: var(--icon-strong-base);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="list-item-extra-icon"] {
|
||||
color: var(--icon-base);
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
[data-slot="list-item-divider"] {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: var(--list-divider-inset, 16px);
|
||||
right: var(--list-divider-inset, 16px);
|
||||
height: 1px;
|
||||
background: var(--border-weak-base);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-slot="list-item"]:last-child [data-slot="list-item-divider"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&[data-active="true"] {
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface-raised-base-hover);
|
||||
[data-slot="list-item-active-icon"] {
|
||||
display: inline-flex;
|
||||
}
|
||||
[data-slot="list-item-extra-icon"] {
|
||||
display: block !important;
|
||||
color: var(--icon-strong-base) !important;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
background: var(--surface-raised-base-active);
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="list-item-add"] {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 6px 8px 6px 8px;
|
||||
align-items: center;
|
||||
color: var(--text-strong);
|
||||
|
||||
/* text-14-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 142.857% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
[data-component="input"] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
387
opencode/packages/ui/src/components/list.tsx
Normal file
387
opencode/packages/ui/src/components/list.tsx
Normal file
@@ -0,0 +1,387 @@
|
||||
import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
|
||||
import { createEffect, createSignal, For, onCleanup, type JSX, on, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import { Icon, type IconProps } from "./icon"
|
||||
import { IconButton } from "./icon-button"
|
||||
import { TextField } from "./text-field"
|
||||
|
||||
function findByKey(container: HTMLElement, key: string) {
|
||||
const nodes = container.querySelectorAll<HTMLElement>('[data-slot="list-item"][data-key]')
|
||||
for (const node of nodes) {
|
||||
if (node.getAttribute("data-key") === key) return node
|
||||
}
|
||||
}
|
||||
|
||||
export interface ListSearchProps {
|
||||
placeholder?: string
|
||||
autofocus?: boolean
|
||||
hideIcon?: boolean
|
||||
class?: string
|
||||
action?: JSX.Element
|
||||
}
|
||||
|
||||
export interface ListAddProps {
|
||||
class?: string
|
||||
render: () => JSX.Element
|
||||
}
|
||||
|
||||
export interface ListAddProps {
|
||||
class?: string
|
||||
render: () => JSX.Element
|
||||
}
|
||||
|
||||
export interface ListProps<T> extends FilteredListProps<T> {
|
||||
class?: string
|
||||
children: (item: T) => JSX.Element
|
||||
emptyMessage?: string
|
||||
loadingMessage?: string
|
||||
onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
|
||||
onMove?: (item: T | undefined) => void
|
||||
onFilter?: (value: string) => void
|
||||
activeIcon?: IconProps["name"]
|
||||
filter?: string
|
||||
search?: ListSearchProps | boolean
|
||||
itemWrapper?: (item: T, node: JSX.Element) => JSX.Element
|
||||
divider?: boolean
|
||||
add?: ListAddProps
|
||||
}
|
||||
|
||||
export interface ListRef {
|
||||
onKeyDown: (e: KeyboardEvent) => void
|
||||
setScrollRef: (el: HTMLDivElement | undefined) => void
|
||||
setFilter: (value: string) => void
|
||||
}
|
||||
|
||||
export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void }) {
|
||||
const i18n = useI18n()
|
||||
const [scrollRef, setScrollRef] = createSignal<HTMLDivElement | undefined>(undefined)
|
||||
const [internalFilter, setInternalFilter] = createSignal("")
|
||||
let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
|
||||
const [store, setStore] = createStore({
|
||||
mouseActive: false,
|
||||
})
|
||||
|
||||
const scrollIntoView = (container: HTMLDivElement, node: HTMLElement, block: "center" | "nearest") => {
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const nodeRect = node.getBoundingClientRect()
|
||||
const top = nodeRect.top - containerRect.top + container.scrollTop
|
||||
const bottom = top + nodeRect.height
|
||||
const viewTop = container.scrollTop
|
||||
const viewBottom = viewTop + container.clientHeight
|
||||
const target =
|
||||
block === "center"
|
||||
? top - container.clientHeight / 2 + nodeRect.height / 2
|
||||
: top < viewTop
|
||||
? top
|
||||
: bottom > viewBottom
|
||||
? bottom - container.clientHeight
|
||||
: viewTop
|
||||
const max = Math.max(0, container.scrollHeight - container.clientHeight)
|
||||
container.scrollTop = Math.max(0, Math.min(target, max))
|
||||
}
|
||||
|
||||
const { filter, grouped, flat, active, setActive, onKeyDown, onInput, refetch } = useFilteredList<T>(props)
|
||||
|
||||
const searchProps = () => (typeof props.search === "object" ? props.search : {})
|
||||
const searchAction = () => searchProps().action
|
||||
const addProps = () => props.add
|
||||
const showAdd = () => !!addProps()
|
||||
|
||||
const moved = (event: MouseEvent) => event.movementX !== 0 || event.movementY !== 0
|
||||
|
||||
const applyFilter = (value: string, options?: { ref?: boolean }) => {
|
||||
const prev = filter()
|
||||
setInternalFilter(value)
|
||||
onInput(value)
|
||||
props.onFilter?.(value)
|
||||
|
||||
if (!options?.ref) return
|
||||
|
||||
// Force a refetch even if the value is unchanged.
|
||||
// This is important for programmatic changes like Tab completion.
|
||||
if (prev === value) {
|
||||
refetch()
|
||||
return
|
||||
}
|
||||
queueMicrotask(() => refetch())
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (props.filter === undefined) return
|
||||
if (props.filter === internalFilter()) return
|
||||
setInternalFilter(props.filter)
|
||||
onInput(props.filter)
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
filter,
|
||||
() => {
|
||||
scrollRef()?.scrollTo(0, 0)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const scroll = scrollRef()
|
||||
if (!scroll) return
|
||||
if (!props.current) return
|
||||
const key = props.key(props.current)
|
||||
requestAnimationFrame(() => {
|
||||
const element = findByKey(scroll, key)
|
||||
if (!element) return
|
||||
scrollIntoView(scroll, element, "center")
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const all = flat()
|
||||
if (store.mouseActive || all.length === 0) return
|
||||
const scroll = scrollRef()
|
||||
if (!scroll) return
|
||||
if (active() === props.key(all[0])) {
|
||||
scroll.scrollTo(0, 0)
|
||||
return
|
||||
}
|
||||
const key = active()
|
||||
if (!key) return
|
||||
const element = findByKey(scroll, key)
|
||||
if (!element) return
|
||||
scrollIntoView(scroll, element, "center")
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const all = flat()
|
||||
const current = active()
|
||||
const item = all.find((x) => props.key(x) === current)
|
||||
props.onMove?.(item)
|
||||
})
|
||||
|
||||
const handleSelect = (item: T | undefined, index: number) => {
|
||||
props.onSelect?.(item, index)
|
||||
}
|
||||
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
setStore("mouseActive", false)
|
||||
if (e.key === "Escape") return
|
||||
|
||||
const all = flat()
|
||||
const selected = all.find((x) => props.key(x) === active())
|
||||
const index = selected ? all.indexOf(selected) : -1
|
||||
props.onKeyEvent?.(e, selected)
|
||||
|
||||
if (e.defaultPrevented) return
|
||||
|
||||
if (e.key === "Enter" && !e.isComposing) {
|
||||
e.preventDefault()
|
||||
if (selected) handleSelect(selected, index)
|
||||
} else if (props.search) {
|
||||
if (e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && (e.key === "n" || e.key === "p")) {
|
||||
onKeyDown(e)
|
||||
return
|
||||
}
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||
onKeyDown(e)
|
||||
}
|
||||
} else {
|
||||
onKeyDown(e)
|
||||
}
|
||||
}
|
||||
|
||||
props.ref?.({
|
||||
onKeyDown: handleKey,
|
||||
setScrollRef,
|
||||
setFilter: (value) => applyFilter(value, { ref: true }),
|
||||
})
|
||||
|
||||
const renderAdd = () => {
|
||||
const add = addProps()
|
||||
if (!add) return null
|
||||
return (
|
||||
<div data-slot="list-item-add" classList={{ [add.class ?? ""]: !!add.class }}>
|
||||
{add.render()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function GroupHeader(groupProps: { category: string }): JSX.Element {
|
||||
const [stuck, setStuck] = createSignal(false)
|
||||
const [header, setHeader] = createSignal<HTMLDivElement | undefined>(undefined)
|
||||
|
||||
createEffect(() => {
|
||||
const scroll = scrollRef()
|
||||
const node = header()
|
||||
if (!scroll || !node) return
|
||||
|
||||
const handler = () => {
|
||||
const rect = node.getBoundingClientRect()
|
||||
const scrollRect = scroll.getBoundingClientRect()
|
||||
setStuck(rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0)
|
||||
}
|
||||
|
||||
scroll.addEventListener("scroll", handler, { passive: true })
|
||||
handler()
|
||||
onCleanup(() => scroll.removeEventListener("scroll", handler))
|
||||
})
|
||||
|
||||
return (
|
||||
<div data-slot="list-header" data-stuck={stuck()} ref={setHeader}>
|
||||
{groupProps.category}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const emptyMessage = () => {
|
||||
if (grouped.loading) return props.loadingMessage ?? i18n.t("ui.list.loading")
|
||||
if (props.emptyMessage) return props.emptyMessage
|
||||
|
||||
const query = filter()
|
||||
if (!query) return i18n.t("ui.list.empty")
|
||||
|
||||
const suffix = i18n.t("ui.list.emptyWithFilter.suffix")
|
||||
return (
|
||||
<>
|
||||
<span>{i18n.t("ui.list.emptyWithFilter.prefix")}</span>
|
||||
<span data-slot="list-filter">"{query}"</span>
|
||||
<Show when={suffix}>
|
||||
<span>{suffix}</span>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-component="list" classList={{ [props.class ?? ""]: !!props.class }}>
|
||||
<Show when={!!props.search}>
|
||||
<div data-slot="list-search-wrapper">
|
||||
<div
|
||||
data-slot="list-search"
|
||||
classList={{ [searchProps().class ?? ""]: !!searchProps().class }}
|
||||
onPointerDown={(event) => {
|
||||
const container = event.currentTarget
|
||||
if (!(container instanceof HTMLElement)) return
|
||||
|
||||
const node = container.querySelector("input, textarea")
|
||||
const input = node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement ? node : inputRef
|
||||
input?.focus()
|
||||
|
||||
// Prevent global listeners (e.g. dnd sensors) from cancelling focus.
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div data-slot="list-search-container">
|
||||
<Show when={!searchProps().hideIcon}>
|
||||
<Icon name="magnifying-glass" />
|
||||
</Show>
|
||||
<TextField
|
||||
autofocus={searchProps().autofocus}
|
||||
variant="ghost"
|
||||
data-slot="list-search-input"
|
||||
type="text"
|
||||
ref={(el: HTMLInputElement | HTMLTextAreaElement) => {
|
||||
inputRef = el
|
||||
}}
|
||||
value={internalFilter()}
|
||||
onChange={(value) => applyFilter(value)}
|
||||
onKeyDown={handleKey}
|
||||
placeholder={searchProps().placeholder}
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
/>
|
||||
</div>
|
||||
<Show when={internalFilter()}>
|
||||
<IconButton
|
||||
icon="circle-x"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setInternalFilter("")
|
||||
queueMicrotask(() => inputRef?.focus())
|
||||
}}
|
||||
aria-label={i18n.t("ui.list.clearFilter")}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
{searchAction()}
|
||||
</div>
|
||||
</Show>
|
||||
<div ref={setScrollRef} data-slot="list-scroll">
|
||||
<Show
|
||||
when={flat().length > 0 || showAdd()}
|
||||
fallback={
|
||||
<div data-slot="list-empty-state">
|
||||
<div data-slot="list-message">{emptyMessage()}</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={grouped.latest}>
|
||||
{(group, groupIndex) => {
|
||||
const isLastGroup = () => groupIndex() === grouped.latest.length - 1
|
||||
return (
|
||||
<div data-slot="list-group">
|
||||
<Show when={group.category}>
|
||||
<GroupHeader category={group.category} />
|
||||
</Show>
|
||||
<div data-slot="list-items">
|
||||
<For each={group.items}>
|
||||
{(item, i) => {
|
||||
const node = (
|
||||
<button
|
||||
data-slot="list-item"
|
||||
data-key={props.key(item)}
|
||||
data-active={props.key(item) === active()}
|
||||
data-selected={item === props.current}
|
||||
onClick={() => handleSelect(item, i())}
|
||||
onKeyDown={handleKey}
|
||||
type="button"
|
||||
onMouseMove={(event) => {
|
||||
if (!moved(event)) return
|
||||
setStore("mouseActive", true)
|
||||
setActive(props.key(item))
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (!store.mouseActive) return
|
||||
setActive(null)
|
||||
}}
|
||||
>
|
||||
{props.children(item)}
|
||||
<Show when={item === props.current}>
|
||||
<span data-slot="list-item-selected-icon">
|
||||
<Icon name="check-small" />
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={props.activeIcon}>
|
||||
{(icon) => (
|
||||
<span data-slot="list-item-active-icon">
|
||||
<Icon name={icon()} />
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
{props.divider && (i() !== group.items.length - 1 || (showAdd() && isLastGroup())) && (
|
||||
<span data-slot="list-item-divider" />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
if (props.itemWrapper) return props.itemWrapper(item, node)
|
||||
return node
|
||||
}}
|
||||
</For>
|
||||
<Show when={showAdd() && isLastGroup()}>{renderAdd()}</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<Show when={grouped.latest.length === 0 && showAdd()}>
|
||||
<div data-slot="list-group">
|
||||
<div data-slot="list-items">{renderAdd()}</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
4
opencode/packages/ui/src/components/logo.css
Normal file
4
opencode/packages/ui/src/components/logo.css
Normal file
@@ -0,0 +1,4 @@
|
||||
[data-component="logo-mark"] {
|
||||
width: 16px;
|
||||
aspect-ratio: 4/5;
|
||||
}
|
||||
62
opencode/packages/ui/src/components/logo.tsx
Normal file
62
opencode/packages/ui/src/components/logo.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { ComponentProps } from "solid-js"
|
||||
|
||||
export const Mark = (props: { class?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
data-component="logo-mark"
|
||||
classList={{ [props.class ?? ""]: !!props.class }}
|
||||
viewBox="0 0 16 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path data-slot="logo-logo-mark-shadow" d="M12 16H4V8H12V16Z" fill="var(--icon-weak-base)" />
|
||||
<path data-slot="logo-logo-mark-o" d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="var(--icon-strong-base)" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const Splash = (props: Pick<ComponentProps<"svg">, "ref" | "class">) => {
|
||||
return (
|
||||
<svg
|
||||
ref={props.ref}
|
||||
data-component="logo-splash"
|
||||
classList={{ [props.class ?? ""]: !!props.class }}
|
||||
viewBox="0 0 80 100"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M60 80H20V40H60V80Z" fill="var(--icon-base)" />
|
||||
<path d="M60 20H20V80H60V20ZM80 100H0V0H80V100Z" fill="var(--icon-strong-base)" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const Logo = (props: { class?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 234 42"
|
||||
fill="none"
|
||||
classList={{ [props.class ?? ""]: !!props.class }}
|
||||
>
|
||||
<g>
|
||||
<path d="M18 30H6V18H18V30Z" fill="var(--icon-weak-base)" />
|
||||
<path d="M18 12H6V30H18V12ZM24 36H0V6H24V36Z" fill="var(--icon-base)" />
|
||||
<path d="M48 30H36V18H48V30Z" fill="var(--icon-weak-base)" />
|
||||
<path d="M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z" fill="var(--icon-base)" />
|
||||
<path d="M84 24V30H66V24H84Z" fill="var(--icon-weak-base)" />
|
||||
<path d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" fill="var(--icon-base)" />
|
||||
<path d="M108 36H96V18H108V36Z" fill="var(--icon-weak-base)" />
|
||||
<path d="M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z" fill="var(--icon-base)" />
|
||||
<path d="M144 30H126V18H144V30Z" fill="var(--icon-weak-base)" />
|
||||
<path d="M144 12H126V30H144V36H120V6H144V12Z" fill="var(--icon-strong-base)" />
|
||||
<path d="M168 30H156V18H168V30Z" fill="var(--icon-weak-base)" />
|
||||
<path d="M168 12H156V30H168V12ZM174 36H150V6H174V36Z" fill="var(--icon-strong-base)" />
|
||||
<path d="M198 30H186V18H198V30Z" fill="var(--icon-weak-base)" />
|
||||
<path d="M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z" fill="var(--icon-strong-base)" />
|
||||
<path d="M234 24V30H216V24H234Z" fill="var(--icon-weak-base)" />
|
||||
<path d="M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z" fill="var(--icon-strong-base)" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
211
opencode/packages/ui/src/components/markdown.css
Normal file
211
opencode/packages/ui/src/components/markdown.css
Normal file
@@ -0,0 +1,211 @@
|
||||
[data-component="markdown"] {
|
||||
/* Reset & Base Typography */
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
color: var(--text-base);
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base); /* 14px */
|
||||
line-height: var(--line-height-x-large);
|
||||
|
||||
/* Spacing for flow */
|
||||
> *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
> *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Headings: Same size, distinguished by color and spacing */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--text-strong);
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: var(--line-height-large);
|
||||
}
|
||||
|
||||
/* Emphasis & Strong: Neutral strong color */
|
||||
strong,
|
||||
b {
|
||||
color: var(--text-strong);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
/* Paragraphs */
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
color: var(--text-interactive-base);
|
||||
text-decoration: none;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
ul,
|
||||
ol {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1.5rem;
|
||||
list-style-position: outside;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
li > p:first-child {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li > p + p {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
li::marker {
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
/* Nested lists spacing */
|
||||
li > ul,
|
||||
li > ol {
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
padding-left: 1rem; /* Minimal indent for nesting only */
|
||||
}
|
||||
|
||||
/* Blockquotes */
|
||||
blockquote {
|
||||
border-left: 2px solid var(--border-weak-base);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 0.5rem;
|
||||
color: var(--text-weak);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Horizontal Rule - Invisible spacing only */
|
||||
hr {
|
||||
border: none;
|
||||
height: 0;
|
||||
margin: 2.5rem 0;
|
||||
}
|
||||
|
||||
.shiki {
|
||||
font-size: 13px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 0.5px solid var(--border-weak-base);
|
||||
}
|
||||
|
||||
[data-component="markdown-code"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-slot="markdown-copy-button"] {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
[data-component="markdown-code"]:hover [data-slot="markdown-copy-button"] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-slot="markdown-copy-button"] [data-slot="check-icon"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="markdown-copy-button"][data-copied="true"] [data-slot="copy-icon"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="markdown-copy-button"][data-copied="true"] [data-slot="check-icon"] {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
overflow: auto;
|
||||
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:not(pre) > code {
|
||||
font-family: var(--font-family-mono);
|
||||
font-feature-settings: var(--font-family-mono--font-feature-settings);
|
||||
color: var(--syntax-string);
|
||||
font-weight: var(--font-weight-medium);
|
||||
/* font-size: 13px; */
|
||||
|
||||
/* padding: 2px 2px; */
|
||||
/* margin: 0 1.5px; */
|
||||
/* border-radius: 2px; */
|
||||
/* background: var(--surface-base); */
|
||||
/* box-shadow: 0 0 0 0.5px var(--border-weak-base); */
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.5rem 0;
|
||||
font-size: var(--font-size-base);
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
/* Minimal borders for structure, matching TUI "lines" roughly but keeping it web-clean */
|
||||
border-bottom: 1px solid var(--border-weaker-base);
|
||||
padding: 0.75rem 0.5rem;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
th {
|
||||
color: var(--text-strong);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border-bottom: 1px solid var(--border-weak-base);
|
||||
}
|
||||
|
||||
/* Images */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
margin: 1.5rem 0;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
264
opencode/packages/ui/src/components/markdown.tsx
Normal file
264
opencode/packages/ui/src/components/markdown.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import { useMarked } from "../context/marked"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import DOMPurify from "dompurify"
|
||||
import morphdom from "morphdom"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import { ComponentProps, createEffect, createResource, createSignal, onCleanup, splitProps } from "solid-js"
|
||||
import { isServer } from "solid-js/web"
|
||||
|
||||
type Entry = {
|
||||
hash: string
|
||||
html: string
|
||||
}
|
||||
|
||||
const max = 200
|
||||
const cache = new Map<string, Entry>()
|
||||
|
||||
if (typeof window !== "undefined" && DOMPurify.isSupported) {
|
||||
DOMPurify.addHook("afterSanitizeAttributes", (node: Element) => {
|
||||
if (!(node instanceof HTMLAnchorElement)) return
|
||||
if (node.target !== "_blank") return
|
||||
|
||||
const rel = node.getAttribute("rel") ?? ""
|
||||
const set = new Set(rel.split(/\s+/).filter(Boolean))
|
||||
set.add("noopener")
|
||||
set.add("noreferrer")
|
||||
node.setAttribute("rel", Array.from(set).join(" "))
|
||||
})
|
||||
}
|
||||
|
||||
const config = {
|
||||
USE_PROFILES: { html: true, mathMl: true },
|
||||
SANITIZE_NAMED_PROPS: true,
|
||||
FORBID_TAGS: ["style"],
|
||||
FORBID_CONTENTS: ["style", "script"],
|
||||
}
|
||||
|
||||
const iconPaths = {
|
||||
copy: '<path d="M6.2513 6.24935V2.91602H17.0846V13.7493H13.7513M13.7513 6.24935V17.0827H2.91797V6.24935H13.7513Z" stroke="currentColor" stroke-linecap="round"/>',
|
||||
check: '<path d="M5 11.9657L8.37838 14.7529L15 5.83398" stroke="currentColor" stroke-linecap="square"/>',
|
||||
}
|
||||
|
||||
function sanitize(html: string) {
|
||||
if (!DOMPurify.isSupported) return ""
|
||||
return DOMPurify.sanitize(html, config)
|
||||
}
|
||||
|
||||
type CopyLabels = {
|
||||
copy: string
|
||||
copied: string
|
||||
}
|
||||
|
||||
function createIcon(path: string, slot: string) {
|
||||
const icon = document.createElement("div")
|
||||
icon.setAttribute("data-component", "icon")
|
||||
icon.setAttribute("data-size", "small")
|
||||
icon.setAttribute("data-slot", slot)
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
|
||||
svg.setAttribute("data-slot", "icon-svg")
|
||||
svg.setAttribute("fill", "none")
|
||||
svg.setAttribute("viewBox", "0 0 20 20")
|
||||
svg.setAttribute("aria-hidden", "true")
|
||||
svg.innerHTML = path
|
||||
icon.appendChild(svg)
|
||||
return icon
|
||||
}
|
||||
|
||||
function createCopyButton(labels: CopyLabels) {
|
||||
const button = document.createElement("button")
|
||||
button.type = "button"
|
||||
button.setAttribute("data-component", "icon-button")
|
||||
button.setAttribute("data-variant", "secondary")
|
||||
button.setAttribute("data-size", "small")
|
||||
button.setAttribute("data-slot", "markdown-copy-button")
|
||||
button.setAttribute("aria-label", labels.copy)
|
||||
button.setAttribute("title", labels.copy)
|
||||
button.appendChild(createIcon(iconPaths.copy, "copy-icon"))
|
||||
button.appendChild(createIcon(iconPaths.check, "check-icon"))
|
||||
return button
|
||||
}
|
||||
|
||||
function setCopyState(button: HTMLButtonElement, labels: CopyLabels, copied: boolean) {
|
||||
if (copied) {
|
||||
button.setAttribute("data-copied", "true")
|
||||
button.setAttribute("aria-label", labels.copied)
|
||||
button.setAttribute("title", labels.copied)
|
||||
return
|
||||
}
|
||||
button.removeAttribute("data-copied")
|
||||
button.setAttribute("aria-label", labels.copy)
|
||||
button.setAttribute("title", labels.copy)
|
||||
}
|
||||
|
||||
function setupCodeCopy(root: HTMLDivElement, labels: CopyLabels) {
|
||||
const timeouts = new Map<HTMLButtonElement, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const updateLabel = (button: HTMLButtonElement) => {
|
||||
const copied = button.getAttribute("data-copied") === "true"
|
||||
setCopyState(button, labels, copied)
|
||||
}
|
||||
|
||||
const ensureWrapper = (block: HTMLPreElement) => {
|
||||
const parent = block.parentElement
|
||||
if (!parent) return
|
||||
const wrapped = parent.getAttribute("data-component") === "markdown-code"
|
||||
if (wrapped) return
|
||||
const wrapper = document.createElement("div")
|
||||
wrapper.setAttribute("data-component", "markdown-code")
|
||||
parent.replaceChild(wrapper, block)
|
||||
wrapper.appendChild(block)
|
||||
wrapper.appendChild(createCopyButton(labels))
|
||||
}
|
||||
|
||||
const handleClick = async (event: MouseEvent) => {
|
||||
const target = event.target
|
||||
if (!(target instanceof Element)) return
|
||||
const button = target.closest('[data-slot="markdown-copy-button"]')
|
||||
if (!(button instanceof HTMLButtonElement)) return
|
||||
const code = button.closest('[data-component="markdown-code"]')?.querySelector("code")
|
||||
const content = code?.textContent ?? ""
|
||||
if (!content) return
|
||||
const clipboard = navigator?.clipboard
|
||||
if (!clipboard) return
|
||||
await clipboard.writeText(content)
|
||||
setCopyState(button, labels, true)
|
||||
const existing = timeouts.get(button)
|
||||
if (existing) clearTimeout(existing)
|
||||
const timeout = setTimeout(() => setCopyState(button, labels, false), 2000)
|
||||
timeouts.set(button, timeout)
|
||||
}
|
||||
|
||||
const blocks = Array.from(root.querySelectorAll("pre"))
|
||||
for (const block of blocks) {
|
||||
ensureWrapper(block)
|
||||
}
|
||||
|
||||
const buttons = Array.from(root.querySelectorAll('[data-slot="markdown-copy-button"]'))
|
||||
for (const button of buttons) {
|
||||
if (button instanceof HTMLButtonElement) updateLabel(button)
|
||||
}
|
||||
|
||||
root.addEventListener("click", handleClick)
|
||||
|
||||
return () => {
|
||||
root.removeEventListener("click", handleClick)
|
||||
for (const timeout of timeouts.values()) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function touch(key: string, value: Entry) {
|
||||
cache.delete(key)
|
||||
cache.set(key, value)
|
||||
|
||||
if (cache.size <= max) return
|
||||
|
||||
const first = cache.keys().next().value
|
||||
if (!first) return
|
||||
cache.delete(first)
|
||||
}
|
||||
|
||||
export function Markdown(
|
||||
props: ComponentProps<"div"> & {
|
||||
text: string
|
||||
cacheKey?: string
|
||||
class?: string
|
||||
classList?: Record<string, boolean>
|
||||
},
|
||||
) {
|
||||
const [local, others] = splitProps(props, ["text", "cacheKey", "class", "classList"])
|
||||
const marked = useMarked()
|
||||
const i18n = useI18n()
|
||||
const [root, setRoot] = createSignal<HTMLDivElement>()
|
||||
const [html] = createResource(
|
||||
() => local.text,
|
||||
async (markdown) => {
|
||||
if (isServer) return ""
|
||||
|
||||
const hash = checksum(markdown)
|
||||
const key = local.cacheKey ?? hash
|
||||
|
||||
if (key && hash) {
|
||||
const cached = cache.get(key)
|
||||
if (cached && cached.hash === hash) {
|
||||
touch(key, cached)
|
||||
return cached.html
|
||||
}
|
||||
}
|
||||
|
||||
const next = await marked.parse(markdown)
|
||||
const safe = sanitize(next)
|
||||
if (key && hash) touch(key, { hash, html: safe })
|
||||
return safe
|
||||
},
|
||||
{ initialValue: "" },
|
||||
)
|
||||
|
||||
let copySetupTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let copyCleanup: (() => void) | undefined
|
||||
|
||||
createEffect(() => {
|
||||
const container = root()
|
||||
const content = html()
|
||||
if (!container) return
|
||||
if (isServer) return
|
||||
|
||||
if (!content) {
|
||||
container.innerHTML = ""
|
||||
return
|
||||
}
|
||||
|
||||
const temp = document.createElement("div")
|
||||
temp.innerHTML = content
|
||||
|
||||
morphdom(container, temp, {
|
||||
childrenOnly: true,
|
||||
onBeforeElUpdated: (fromEl, toEl) => {
|
||||
if (fromEl.isEqualNode(toEl)) return false
|
||||
if (fromEl.getAttribute("data-component") === "markdown-code") {
|
||||
const fromPre = fromEl.querySelector("pre")
|
||||
const toPre = toEl.querySelector("pre")
|
||||
if (fromPre && toPre && !fromPre.isEqualNode(toPre)) {
|
||||
morphdom(fromPre, toPre)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
onBeforeNodeDiscarded: (node) => {
|
||||
if (node instanceof Element) {
|
||||
if (node.getAttribute("data-slot") === "markdown-copy-button") return false
|
||||
if (node.getAttribute("data-component") === "markdown-code") return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
if (copySetupTimer) clearTimeout(copySetupTimer)
|
||||
copySetupTimer = setTimeout(() => {
|
||||
if (copyCleanup) copyCleanup()
|
||||
copyCleanup = setupCodeCopy(container, {
|
||||
copy: i18n.t("ui.message.copy"),
|
||||
copied: i18n.t("ui.message.copied"),
|
||||
})
|
||||
}, 150)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (copySetupTimer) clearTimeout(copySetupTimer)
|
||||
if (copyCleanup) copyCleanup()
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="markdown"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
ref={setRoot}
|
||||
{...others}
|
||||
/>
|
||||
)
|
||||
}
|
||||
122
opencode/packages/ui/src/components/message-nav.css
Normal file
122
opencode/packages/ui/src/components/message-nav.css
Normal file
@@ -0,0 +1,122 @@
|
||||
[data-component="message-nav"] {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
|
||||
&[data-size="normal"] {
|
||||
width: 240px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&[data-size="compact"] {
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="message-nav-item"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
justify-content: flex-end;
|
||||
|
||||
[data-component="message-nav"][data-size="normal"] & {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="message-nav-tick-button"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
height: 12px;
|
||||
width: 24px;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
|
||||
&[data-active] [data-slot="message-nav-tick-line"] {
|
||||
background-color: var(--icon-strong-base);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="message-nav-tick-line"] {
|
||||
height: 1px;
|
||||
width: 16px;
|
||||
background-color: var(--icon-base);
|
||||
transition:
|
||||
width 0.2s,
|
||||
background-color 0.2s;
|
||||
}
|
||||
|
||||
[data-slot="message-nav-tick-button"]:hover [data-slot="message-nav-tick-line"] {
|
||||
width: 100%;
|
||||
background-color: var(--icon-strong-base);
|
||||
}
|
||||
|
||||
[data-slot="message-nav-message-button"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
column-gap: 12px;
|
||||
cursor: default;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 4px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
[data-slot="message-nav-title-preview"] {
|
||||
font-size: 14px; /* text-14-regular */
|
||||
color: var(--text-base);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
|
||||
&[data-active] {
|
||||
color: var(--text-strong);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="message-nav-item"]:hover [data-slot="message-nav-message-button"] {
|
||||
background-color: var(--surface-base);
|
||||
}
|
||||
[data-slot="message-nav-item"]:active [data-slot="message-nav-message-button"] {
|
||||
background-color: var(--surface-base-active);
|
||||
}
|
||||
|
||||
[data-slot="message-nav-item"]:active [data-slot="message-nav-title-preview"] {
|
||||
color: var(--text-base);
|
||||
}
|
||||
|
||||
[data-slot="message-nav-tooltip"] {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
[data-slot="message-nav-tooltip-content"] {
|
||||
display: flex;
|
||||
padding: 4px 4px 6px 4px;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface-raised-stronger-non-alpha);
|
||||
max-height: calc(100vh - 6rem);
|
||||
overflow-y: auto;
|
||||
|
||||
/* border/shadow-xs/base */
|
||||
box-shadow:
|
||||
0 0 0 1px var(--border-weak-base, rgba(17, 0, 0, 0.12)),
|
||||
0 1px 2px -1px rgba(19, 16, 16, 0.04),
|
||||
0 1px 2px 0 rgba(19, 16, 16, 0.06),
|
||||
0 1px 3px 0 rgba(19, 16, 16, 0.08);
|
||||
|
||||
* {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
87
opencode/packages/ui/src/components/message-nav.tsx
Normal file
87
opencode/packages/ui/src/components/message-nav.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { ComponentProps, For, Match, Show, splitProps, Switch } from "solid-js"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
import { Tooltip } from "@kobalte/core/tooltip"
|
||||
import { useI18n } from "../context/i18n"
|
||||
|
||||
export function MessageNav(
|
||||
props: ComponentProps<"ul"> & {
|
||||
messages: UserMessage[]
|
||||
current?: UserMessage
|
||||
size: "normal" | "compact"
|
||||
onMessageSelect: (message: UserMessage) => void
|
||||
getLabel?: (message: UserMessage) => string | undefined
|
||||
},
|
||||
) {
|
||||
const i18n = useI18n()
|
||||
const [local, others] = splitProps(props, ["messages", "current", "size", "onMessageSelect", "getLabel"])
|
||||
|
||||
const content = () => (
|
||||
<ul role="list" data-component="message-nav" data-size={local.size} {...others}>
|
||||
<For each={local.messages}>
|
||||
{(message) => {
|
||||
const handleClick = () => local.onMessageSelect(message)
|
||||
|
||||
const handleKeyPress = (event: KeyboardEvent) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
local.onMessageSelect(message)
|
||||
}
|
||||
|
||||
return (
|
||||
<li data-slot="message-nav-item">
|
||||
<Switch>
|
||||
<Match when={local.size === "compact"}>
|
||||
<div
|
||||
data-slot="message-nav-tick-button"
|
||||
data-active={message.id === local.current?.id || undefined}
|
||||
role="button"
|
||||
tabindex={0}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyPress}
|
||||
>
|
||||
<div data-slot="message-nav-tick-line" />
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={local.size === "normal"}>
|
||||
<button data-slot="message-nav-message-button" onClick={handleClick} onKeyDown={handleKeyPress}>
|
||||
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
|
||||
<div
|
||||
data-slot="message-nav-title-preview"
|
||||
data-active={message.id === local.current?.id || undefined}
|
||||
>
|
||||
<Show
|
||||
when={local.getLabel?.(message) ?? message.summary?.title}
|
||||
fallback={i18n.t("ui.messageNav.newMessage")}
|
||||
>
|
||||
{local.getLabel?.(message) ?? message.summary?.title}
|
||||
</Show>
|
||||
</div>
|
||||
</button>
|
||||
</Match>
|
||||
</Switch>
|
||||
</li>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</ul>
|
||||
)
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={local.size === "compact"}>
|
||||
<Tooltip openDelay={0} closeDelay={300} placement="right-start" gutter={-40} shift={-10} overlap>
|
||||
<Tooltip.Trigger as="div">{content()}</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content data-slot="message-nav-tooltip">
|
||||
<div data-slot="message-nav-tooltip-content">
|
||||
<MessageNav {...props} size="normal" class="" />
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip>
|
||||
</Match>
|
||||
<Match when={local.size === "normal"}>{content()}</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
828
opencode/packages/ui/src/components/message-part.css
Normal file
828
opencode/packages/ui/src/components/message-part.css
Normal file
@@ -0,0 +1,828 @@
|
||||
[data-component="assistant-message"] {
|
||||
content-visibility: auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-component="user-message"] {
|
||||
font-family: var(--font-family-sans);
|
||||
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);
|
||||
color: var(--text-base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
[data-slot="user-message-attachments"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-slot="user-message-attachment"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: var(--surface-weak);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
transition: border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--border-strong-base);
|
||||
}
|
||||
|
||||
&[data-type="image"] {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
&[data-type="file"] {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="user-message-attachment-image"] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
[data-slot="user-message-attachment-icon"] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--icon-weak);
|
||||
|
||||
[data-component="icon"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="user-message-text"] {
|
||||
position: relative;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
background: var(--surface-weak);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
|
||||
[data-highlight="file"] {
|
||||
color: var(--syntax-property);
|
||||
}
|
||||
|
||||
[data-highlight="agent"] {
|
||||
color: var(--syntax-type);
|
||||
}
|
||||
|
||||
[data-slot="user-message-copy-wrapper"] {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover [data-slot="user-message-copy-wrapper"] {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.text-text-strong {
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="text-part"] {
|
||||
width: 100%;
|
||||
|
||||
[data-slot="text-part-body"] {
|
||||
position: relative;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
[data-slot="text-part-copy-wrapper"] {
|
||||
position: absolute;
|
||||
top: -28px;
|
||||
right: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
[data-slot="text-part-body"]:hover [data-slot="text-part-copy-wrapper"] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-component="markdown"] {
|
||||
margin-top: 0;
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="reasoning-part"] {
|
||||
width: 100%;
|
||||
color: var(--text-base);
|
||||
opacity: 0.8;
|
||||
line-height: var(--line-height-large);
|
||||
|
||||
[data-component="markdown"] {
|
||||
margin-top: 24px;
|
||||
font-style: italic !important;
|
||||
|
||||
p:has(strong) {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="tool-error"] {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
color: var(--icon-critical-base);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
[data-slot="message-part-tool-error-content"] {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-slot="message-part-tool-error-title"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-on-critical-base);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-slot="message-part-tool-error-message"] {
|
||||
color: var(--text-on-critical-weak);
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="tool-output"] {
|
||||
white-space: pre;
|
||||
padding: 8px 12px;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
[data-component="markdown"] {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
&[data-scrollable] {
|
||||
height: auto;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-component="markdown"] {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="edit-trigger"],
|
||||
[data-component="write-trigger"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
|
||||
[data-slot="message-part-title-area"] {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="message-part-title"] {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-base);
|
||||
}
|
||||
|
||||
[data-slot="message-part-title-text"] {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
[data-slot="message-part-title-filename"] {
|
||||
/* No text-transform - preserve original filename casing */
|
||||
}
|
||||
|
||||
[data-slot="message-part-path"] {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="message-part-directory"] {
|
||||
color: var(--text-weak);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
[data-slot="message-part-filename"] {
|
||||
color: var(--text-strong);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-slot="message-part-actions"] {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="edit-content"] {
|
||||
border-top: 1px solid var(--border-weaker-base);
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="write-content"] {
|
||||
border-top: 1px solid var(--border-weaker-base);
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
|
||||
[data-component="code"] {
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
|
||||
/* Hide scrollbar */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="tool-action"] {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-component="todos"] {
|
||||
padding: 10px 12px 24px 48px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
[data-slot="message-part-todo-content"] {
|
||||
&[data-completed="completed"] {
|
||||
text-decoration: line-through;
|
||||
color: var(--text-weaker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="task-tools"] {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
[data-slot="task-tool-item"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-weak);
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
flex-shrink: 0;
|
||||
color: var(--icon-weak);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="task-tool-title"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="task-tool-subtitle"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
color: var(--text-weaker);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="diagnostics"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--surface-critical-weak);
|
||||
border-top: 1px solid var(--border-critical-base);
|
||||
|
||||
[data-slot="diagnostic"] {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-small);
|
||||
line-height: var(--line-height-large);
|
||||
}
|
||||
|
||||
[data-slot="diagnostic-label"] {
|
||||
color: var(--text-on-critical-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-slot="diagnostic-location"] {
|
||||
color: var(--text-on-critical-weak);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-slot="diagnostic-message"] {
|
||||
color: var(--text-on-critical-base);
|
||||
word-break: break-word;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="user-message"] [data-slot="user-message-text"],
|
||||
[data-component="text-part"],
|
||||
[data-component="reasoning-part"],
|
||||
[data-component="tool-error"],
|
||||
[data-component="tool-output"],
|
||||
[data-component="edit-content"],
|
||||
[data-component="write-content"],
|
||||
[data-component="todos"],
|
||||
[data-component="diagnostics"],
|
||||
.error-card {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
[data-component="tool-part-wrapper"] {
|
||||
width: 100%;
|
||||
|
||||
&[data-permission="true"],
|
||||
&[data-question="true"] {
|
||||
position: sticky;
|
||||
top: calc(2px + var(--sticky-header-height, 40px));
|
||||
bottom: 0px;
|
||||
z-index: 20;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
box-shadow: var(--shadow-xs-border-base);
|
||||
background-color: var(--surface-raised-base);
|
||||
overflow: visible;
|
||||
overflow-anchor: none;
|
||||
|
||||
& > *:first-child {
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
& > *:last-child {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-component="collapsible"] {
|
||||
border: none;
|
||||
}
|
||||
|
||||
[data-component="card"] {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-permission="true"] {
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -1.5px;
|
||||
top: -5px;
|
||||
border-radius: 7.5px;
|
||||
border: 1.5px solid transparent;
|
||||
background:
|
||||
linear-gradient(var(--background-base) 0 0) padding-box,
|
||||
conic-gradient(
|
||||
from var(--border-angle),
|
||||
transparent 0deg,
|
||||
transparent 0deg,
|
||||
var(--border-warning-strong, var(--border-warning-selected)) 300deg,
|
||||
var(--border-warning-base) 360deg
|
||||
)
|
||||
border-box;
|
||||
animation: chase-border 2.5s linear infinite;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-question="true"] {
|
||||
background: var(--background-base);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
}
|
||||
}
|
||||
|
||||
@property --border-angle {
|
||||
syntax: "<angle>";
|
||||
initial-value: 0deg;
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@keyframes chase-border {
|
||||
from {
|
||||
--border-angle: 0deg;
|
||||
}
|
||||
|
||||
to {
|
||||
--border-angle: 360deg;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="permission-prompt"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--surface-raised-strong);
|
||||
border-radius: 0 0 6px 6px;
|
||||
|
||||
[data-slot="permission-actions"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="question-prompt"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
background-color: var(--surface-inset-base);
|
||||
border-radius: 0 0 6px 6px;
|
||||
gap: 12px;
|
||||
|
||||
[data-slot="question-tabs"] {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
[data-slot="question-tab"] {
|
||||
padding: 4px 12px;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--surface-base);
|
||||
color: var(--text-base);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.15s,
|
||||
background-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-base-hover);
|
||||
}
|
||||
|
||||
&[data-active="true"] {
|
||||
background-color: var(--surface-raised-base);
|
||||
}
|
||||
|
||||
&[data-answered="true"] {
|
||||
color: var(--text-strong);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="question-content"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
[data-slot="question-text"] {
|
||||
font-size: 14px;
|
||||
color: var(--text-base);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="question-options"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
[data-slot="question-option"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--surface-base);
|
||||
border: 1px solid var(--border-weaker-base);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
transition:
|
||||
background-color 0.15s,
|
||||
border-color 0.15s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-base-hover);
|
||||
border-color: var(--border-default);
|
||||
}
|
||||
|
||||
&[data-picked="true"] {
|
||||
[data-component="icon"] {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="option-label"] {
|
||||
font-size: 14px;
|
||||
color: var(--text-base);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-slot="option-description"] {
|
||||
font-size: 12px;
|
||||
color: var(--text-weak);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="custom-input-form"] {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
align-items: stretch;
|
||||
|
||||
[data-slot="custom-input"] {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 6px;
|
||||
background-color: var(--surface-base);
|
||||
color: var(--text-base);
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-weak);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="button"] {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="question-review"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
[data-slot="review-title"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="review-item"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
font-size: 13px;
|
||||
|
||||
[data-slot="review-label"] {
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="review-value"] {
|
||||
color: var(--text-strong);
|
||||
|
||||
&[data-answered="false"] {
|
||||
color: var(--text-weak);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="question-actions"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="question-answers"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
|
||||
[data-slot="question-answer-item"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
font-size: 13px;
|
||||
|
||||
[data-slot="question-text"] {
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="answer-text"] {
|
||||
color: var(--text-strong);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="apply-patch-files"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-component="apply-patch-file"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid var(--border-weaker-base);
|
||||
|
||||
&:first-child {
|
||||
border-top: 1px solid var(--border-weaker-base);
|
||||
}
|
||||
|
||||
[data-slot="apply-patch-file-header"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--surface-inset-base);
|
||||
}
|
||||
|
||||
[data-slot="apply-patch-file-action"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
color: var(--text-base);
|
||||
flex-shrink: 0;
|
||||
|
||||
&[data-type="delete"] {
|
||||
color: var(--text-critical-base);
|
||||
}
|
||||
|
||||
&[data-type="add"] {
|
||||
color: var(--text-success-base);
|
||||
}
|
||||
|
||||
&[data-type="move"] {
|
||||
color: var(--text-warning-base);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="apply-patch-file-path"] {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--text-weak);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
[data-slot="apply-patch-deletion-count"] {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--text-critical-base);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="apply-patch-file-diff"] {
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="tool-loaded-file"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0 4px 28px;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
color: var(--text-weak);
|
||||
|
||||
[data-component="icon"] {
|
||||
flex-shrink: 0;
|
||||
color: var(--icon-weak);
|
||||
}
|
||||
}
|
||||
1586
opencode/packages/ui/src/components/message-part.tsx
Normal file
1586
opencode/packages/ui/src/components/message-part.tsx
Normal file
File diff suppressed because it is too large
Load Diff
98
opencode/packages/ui/src/components/popover.css
Normal file
98
opencode/packages/ui/src/components/popover.css
Normal file
@@ -0,0 +1,98 @@
|
||||
[data-slot="popover-trigger"] {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
[data-component="popover-content"] {
|
||||
z-index: 50;
|
||||
min-width: 200px;
|
||||
max-width: 320px;
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--surface-raised-stronger-non-alpha);
|
||||
|
||||
border: 1px solid color-mix(in oklch, var(--border-base) 50%, transparent);
|
||||
background-clip: padding-box;
|
||||
box-shadow: var(--shadow-md);
|
||||
|
||||
transform-origin: var(--kb-popover-content-transform-origin);
|
||||
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[data-closed] {
|
||||
animation: popover-close 0.15s ease-out;
|
||||
}
|
||||
|
||||
&[data-expanded] {
|
||||
animation: popover-open 0.15s ease-out;
|
||||
}
|
||||
|
||||
[data-slot="popover-header"] {
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
padding-bottom: 0;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
[data-slot="popover-title"] {
|
||||
flex: 1;
|
||||
color: var(--text-strong);
|
||||
margin: 0;
|
||||
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
[data-slot="popover-close-button"] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="popover-description"] {
|
||||
padding: 0 12px;
|
||||
margin: 0;
|
||||
color: var(--text-base);
|
||||
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
[data-slot="popover-body"] {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
[data-slot="popover-arrow"] {
|
||||
fill: var(--surface-raised-stronger-non-alpha);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes popover-open {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes popover-close {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
166
opencode/packages/ui/src/components/popover.tsx
Normal file
166
opencode/packages/ui/src/components/popover.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { Popover as Kobalte } from "@kobalte/core/popover"
|
||||
import {
|
||||
ComponentProps,
|
||||
JSXElement,
|
||||
ParentProps,
|
||||
Show,
|
||||
createEffect,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
splitProps,
|
||||
ValidComponent,
|
||||
} from "solid-js"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import { IconButton } from "./icon-button"
|
||||
|
||||
export interface PopoverProps<T extends ValidComponent = "div">
|
||||
extends ParentProps,
|
||||
Omit<ComponentProps<typeof Kobalte>, "children"> {
|
||||
trigger?: JSXElement
|
||||
triggerAs?: T
|
||||
triggerProps?: ComponentProps<T>
|
||||
title?: JSXElement
|
||||
description?: JSXElement
|
||||
class?: ComponentProps<"div">["class"]
|
||||
classList?: ComponentProps<"div">["classList"]
|
||||
style?: ComponentProps<"div">["style"]
|
||||
portal?: boolean
|
||||
}
|
||||
|
||||
export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>) {
|
||||
const i18n = useI18n()
|
||||
const [local, rest] = splitProps(props, [
|
||||
"trigger",
|
||||
"triggerAs",
|
||||
"triggerProps",
|
||||
"title",
|
||||
"description",
|
||||
"class",
|
||||
"classList",
|
||||
"style",
|
||||
"children",
|
||||
"portal",
|
||||
"open",
|
||||
"defaultOpen",
|
||||
"onOpenChange",
|
||||
"modal",
|
||||
])
|
||||
|
||||
const [contentRef, setContentRef] = createSignal<HTMLElement | undefined>(undefined)
|
||||
const [triggerRef, setTriggerRef] = createSignal<HTMLElement | undefined>(undefined)
|
||||
const [dismiss, setDismiss] = createSignal<"escape" | "outside" | null>(null)
|
||||
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = createSignal<boolean>(local.defaultOpen ?? false)
|
||||
|
||||
const controlled = () => local.open !== undefined
|
||||
const opened = () => {
|
||||
if (controlled()) return local.open ?? false
|
||||
return uncontrolledOpen()
|
||||
}
|
||||
|
||||
const onOpenChange = (next: boolean) => {
|
||||
if (next) setDismiss(null)
|
||||
if (local.onOpenChange) local.onOpenChange(next)
|
||||
if (controlled()) return
|
||||
setUncontrolledOpen(next)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!opened()) return
|
||||
|
||||
const inside = (node: Node | null | undefined) => {
|
||||
if (!node) return false
|
||||
const content = contentRef()
|
||||
if (content && content.contains(node)) return true
|
||||
const trigger = triggerRef()
|
||||
if (trigger && trigger.contains(node)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
const close = (reason: "escape" | "outside") => {
|
||||
setDismiss(reason)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== "Escape") return
|
||||
close("escape")
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
const target = event.target
|
||||
if (!(target instanceof Node)) return
|
||||
if (inside(target)) return
|
||||
close("outside")
|
||||
}
|
||||
|
||||
const onFocusIn = (event: FocusEvent) => {
|
||||
const target = event.target
|
||||
if (!(target instanceof Node)) return
|
||||
if (inside(target)) return
|
||||
close("outside")
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown, true)
|
||||
window.addEventListener("pointerdown", onPointerDown, true)
|
||||
window.addEventListener("focusin", onFocusIn, true)
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("keydown", onKeyDown, true)
|
||||
window.removeEventListener("pointerdown", onPointerDown, true)
|
||||
window.removeEventListener("focusin", onFocusIn, true)
|
||||
})
|
||||
})
|
||||
|
||||
const content = () => (
|
||||
<Kobalte.Content
|
||||
ref={(el: HTMLElement | undefined) => setContentRef(el)}
|
||||
data-component="popover-content"
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
style={local.style}
|
||||
onCloseAutoFocus={(event: Event) => {
|
||||
if (dismiss() === "outside") event.preventDefault()
|
||||
setDismiss(null)
|
||||
}}
|
||||
>
|
||||
{/* <Kobalte.Arrow data-slot="popover-arrow" /> */}
|
||||
<Show when={local.title}>
|
||||
<div data-slot="popover-header">
|
||||
<Kobalte.Title data-slot="popover-title">{local.title}</Kobalte.Title>
|
||||
<Kobalte.CloseButton
|
||||
data-slot="popover-close-button"
|
||||
as={IconButton}
|
||||
icon="close"
|
||||
variant="ghost"
|
||||
aria-label={i18n.t("ui.common.close")}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={local.description}>
|
||||
<Kobalte.Description data-slot="popover-description">{local.description}</Kobalte.Description>
|
||||
</Show>
|
||||
<div data-slot="popover-body">{local.children}</div>
|
||||
</Kobalte.Content>
|
||||
)
|
||||
|
||||
return (
|
||||
<Kobalte gutter={4} {...rest} open={opened()} onOpenChange={onOpenChange} modal={local.modal ?? false}>
|
||||
<Kobalte.Trigger
|
||||
ref={(el: HTMLElement) => setTriggerRef(el)}
|
||||
as={local.triggerAs ?? "div"}
|
||||
data-slot="popover-trigger"
|
||||
{...(local.triggerProps as any)}
|
||||
>
|
||||
{local.trigger}
|
||||
</Kobalte.Trigger>
|
||||
<Show when={local.portal ?? true} fallback={content()}>
|
||||
<Kobalte.Portal>{content()}</Kobalte.Portal>
|
||||
</Show>
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
12
opencode/packages/ui/src/components/progress-circle.css
Normal file
12
opencode/packages/ui/src/components/progress-circle.css
Normal file
@@ -0,0 +1,12 @@
|
||||
[data-component="progress-circle"] {
|
||||
transform: rotate(-90deg);
|
||||
|
||||
[data-slot="progress-circle-background"] {
|
||||
stroke: var(--border-weak-base);
|
||||
}
|
||||
|
||||
[data-slot="progress-circle-progress"] {
|
||||
stroke: var(--border-active);
|
||||
transition: stroke-dashoffset 0.35s cubic-bezier(0.65, 0, 0.35, 1);
|
||||
}
|
||||
}
|
||||
57
opencode/packages/ui/src/components/progress-circle.tsx
Normal file
57
opencode/packages/ui/src/components/progress-circle.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { type ComponentProps, createMemo, splitProps } from "solid-js"
|
||||
|
||||
export interface ProgressCircleProps extends Pick<ComponentProps<"svg">, "class" | "classList"> {
|
||||
percentage: number
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
}
|
||||
|
||||
export function ProgressCircle(props: ProgressCircleProps) {
|
||||
const [split, rest] = splitProps(props, ["percentage", "size", "strokeWidth", "class", "classList"])
|
||||
|
||||
const size = () => split.size || 16
|
||||
const strokeWidth = () => split.strokeWidth || 3
|
||||
|
||||
const viewBoxSize = 16
|
||||
const center = viewBoxSize / 2
|
||||
const radius = () => center - strokeWidth() / 2
|
||||
const circumference = createMemo(() => 2 * Math.PI * radius())
|
||||
|
||||
const offset = createMemo(() => {
|
||||
const clampedPercentage = Math.max(0, Math.min(100, split.percentage || 0))
|
||||
const progress = clampedPercentage / 100
|
||||
return circumference() * (1 - progress)
|
||||
})
|
||||
|
||||
return (
|
||||
<svg
|
||||
{...rest}
|
||||
width={size()}
|
||||
height={size()}
|
||||
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
|
||||
fill="none"
|
||||
data-component="progress-circle"
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
>
|
||||
<circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={radius()}
|
||||
data-slot="progress-circle-background"
|
||||
stroke-width={strokeWidth()}
|
||||
/>
|
||||
<circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={radius()}
|
||||
data-slot="progress-circle-progress"
|
||||
stroke-width={strokeWidth()}
|
||||
stroke-dasharray={circumference().toString()}
|
||||
stroke-dashoffset={offset()}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
5
opencode/packages/ui/src/components/provider-icon.css
Normal file
5
opencode/packages/ui/src/components/provider-icon.css
Normal file
@@ -0,0 +1,5 @@
|
||||
[data-component="provider-icon"] {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
24
opencode/packages/ui/src/components/provider-icon.tsx
Normal file
24
opencode/packages/ui/src/components/provider-icon.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Component, JSX } from "solid-js"
|
||||
import { splitProps } from "solid-js"
|
||||
import sprite from "./provider-icons/sprite.svg"
|
||||
import type { IconName } from "./provider-icons/types"
|
||||
|
||||
export type ProviderIconProps = JSX.SVGElementTags["svg"] & {
|
||||
id: IconName
|
||||
}
|
||||
|
||||
export const ProviderIcon: Component<ProviderIconProps> = (props) => {
|
||||
const [local, rest] = splitProps(props, ["id", "class", "classList"])
|
||||
return (
|
||||
<svg
|
||||
data-component="provider-icon"
|
||||
{...rest}
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
<use href={`${sprite}#${local.id}`} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
882
opencode/packages/ui/src/components/provider-icons/sprite.svg
Normal file
882
opencode/packages/ui/src/components/provider-icons/sprite.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 238 KiB |
81
opencode/packages/ui/src/components/provider-icons/types.ts
Normal file
81
opencode/packages/ui/src/components/provider-icons/types.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
// This file is generated by icon spritesheet generator
|
||||
|
||||
export const iconNames = [
|
||||
"zhipuai",
|
||||
"zhipuai-coding-plan",
|
||||
"zenmux",
|
||||
"zai",
|
||||
"zai-coding-plan",
|
||||
"xiaomi",
|
||||
"xai",
|
||||
"wandb",
|
||||
"vultr",
|
||||
"vercel",
|
||||
"venice",
|
||||
"v0",
|
||||
"upstage",
|
||||
"togetherai",
|
||||
"synthetic",
|
||||
"submodel",
|
||||
"siliconflow",
|
||||
"siliconflow-cn",
|
||||
"scaleway",
|
||||
"sap-ai-core",
|
||||
"requesty",
|
||||
"poe",
|
||||
"perplexity",
|
||||
"ovhcloud",
|
||||
"openrouter",
|
||||
"opencode",
|
||||
"openai",
|
||||
"ollama-cloud",
|
||||
"nvidia",
|
||||
"nebius",
|
||||
"nano-gpt",
|
||||
"morph",
|
||||
"moonshotai",
|
||||
"moonshotai-cn",
|
||||
"modelscope",
|
||||
"mistral",
|
||||
"minimax",
|
||||
"minimax-cn",
|
||||
"lucidquery",
|
||||
"lmstudio",
|
||||
"llama",
|
||||
"kimi-for-coding",
|
||||
"io-net",
|
||||
"inference",
|
||||
"inception",
|
||||
"iflowcn",
|
||||
"huggingface",
|
||||
"helicone",
|
||||
"groq",
|
||||
"google",
|
||||
"google-vertex",
|
||||
"google-vertex-anthropic",
|
||||
"github-models",
|
||||
"github-copilot",
|
||||
"friendli",
|
||||
"fireworks-ai",
|
||||
"fastrouter",
|
||||
"deepseek",
|
||||
"deepinfra",
|
||||
"cortecs",
|
||||
"cohere",
|
||||
"cloudflare-workers-ai",
|
||||
"cloudflare-ai-gateway",
|
||||
"chutes",
|
||||
"cerebras",
|
||||
"baseten",
|
||||
"bailing",
|
||||
"azure",
|
||||
"azure-cognitive-services",
|
||||
"anthropic",
|
||||
"amazon-bedrock",
|
||||
"alibaba",
|
||||
"alibaba-cn",
|
||||
"aihubmix",
|
||||
"abacus",
|
||||
] as const
|
||||
|
||||
export type IconName = (typeof iconNames)[number]
|
||||
157
opencode/packages/ui/src/components/radio-group.css
Normal file
157
opencode/packages/ui/src/components/radio-group.css
Normal file
@@ -0,0 +1,157 @@
|
||||
[data-component="radio-group"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--spacing) * 2);
|
||||
|
||||
[data-slot="radio-group-wrapper"] {
|
||||
all: unset;
|
||||
background-color: var(--surface-base);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
[data-slot="radio-group-items"] {
|
||||
display: inline-flex;
|
||||
list-style: none;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
[data-slot="radio-group-indicator"] {
|
||||
background: var(--button-secondary-base);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
content: "";
|
||||
opacity: var(--indicator-opacity, 1);
|
||||
position: absolute;
|
||||
transition:
|
||||
opacity 300ms ease-in-out,
|
||||
box-shadow 100ms ease-in-out,
|
||||
width 150ms ease,
|
||||
height 150ms ease,
|
||||
transform 150ms ease;
|
||||
}
|
||||
|
||||
[data-slot="radio-group-item"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Separator between items */
|
||||
[data-slot="radio-group-item"]:not(:first-of-type)::before {
|
||||
background: var(--border-weak-base);
|
||||
border-radius: var(--radius-xs);
|
||||
content: "";
|
||||
inset: 6px 0;
|
||||
position: absolute;
|
||||
transition: opacity 150ms ease;
|
||||
width: 1px;
|
||||
transform: translateX(-0.5px);
|
||||
}
|
||||
|
||||
/* Hide separator when item or previous item is checked */
|
||||
[data-slot="radio-group-item"]:has([data-slot="radio-group-item-input"][data-checked])::before,
|
||||
[data-slot="radio-group-item"]:has([data-slot="radio-group-item-input"][data-checked])
|
||||
+ [data-slot="radio-group-item"]::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-slot="radio-group-item-label"] {
|
||||
color: var(--text-weak);
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: calc(var(--spacing) * 1);
|
||||
line-height: 1;
|
||||
padding: 6px 12px;
|
||||
place-content: center;
|
||||
position: relative;
|
||||
transition-duration: 150ms;
|
||||
transition-property: color, opacity;
|
||||
transition-timing-function: ease-in-out;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
[data-slot="radio-group-item-input"] {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
/* Checked state */
|
||||
[data-slot="radio-group-item-input"][data-checked] + [data-slot="radio-group-item-label"] {
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
[data-slot="radio-group-item-input"][data-disabled] + [data-slot="radio-group-item-label"] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Hover state for unchecked, enabled items */
|
||||
[data-slot="radio-group-item-input"]:not([data-checked], [data-disabled]) + [data-slot="radio-group-item-label"] {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
[data-slot="radio-group-item-input"]:not([data-checked], [data-disabled])
|
||||
+ [data-slot="radio-group-item-label"]:hover {
|
||||
color: var(--text-base);
|
||||
}
|
||||
|
||||
[data-slot="radio-group-item-input"]:not([data-checked], [data-disabled])
|
||||
+ [data-slot="radio-group-item-label"]:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Focus state */
|
||||
[data-slot="radio-group-wrapper"]:has([data-slot="radio-group-item-input"]:focus-visible)
|
||||
[data-slot="radio-group-indicator"] {
|
||||
box-shadow: var(--shadow-xs-border-focus);
|
||||
}
|
||||
|
||||
/* Hide indicator when nothing is checked */
|
||||
[data-slot="radio-group-wrapper"]:not(:has([data-slot="radio-group-item-input"][data-checked]))
|
||||
[data-slot="radio-group-indicator"] {
|
||||
--indicator-opacity: 0;
|
||||
}
|
||||
|
||||
/* Vertical orientation */
|
||||
&[aria-orientation="vertical"] [data-slot="radio-group-items"] {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&[aria-orientation="vertical"] [data-slot="radio-group-item"]:not(:first-of-type)::before {
|
||||
height: 1px;
|
||||
width: auto;
|
||||
inset: 0 6px;
|
||||
transform: translateY(-0.5px);
|
||||
}
|
||||
|
||||
/* Small size variant */
|
||||
&[data-size="small"] {
|
||||
[data-slot="radio-group-item-label"] {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
[data-slot="radio-group-item"]:not(:first-of-type)::before {
|
||||
inset: 4px 0;
|
||||
}
|
||||
|
||||
&[aria-orientation="vertical"] [data-slot="radio-group-item"]:not(:first-of-type)::before {
|
||||
inset: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Disabled root state */
|
||||
&[data-disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
75
opencode/packages/ui/src/components/radio-group.tsx
Normal file
75
opencode/packages/ui/src/components/radio-group.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { SegmentedControl as Kobalte } from "@kobalte/core/segmented-control"
|
||||
import { For, splitProps } from "solid-js"
|
||||
import type { ComponentProps, JSX } from "solid-js"
|
||||
|
||||
export type RadioGroupProps<T> = Omit<
|
||||
ComponentProps<typeof Kobalte>,
|
||||
"value" | "defaultValue" | "onChange" | "children"
|
||||
> & {
|
||||
options: T[]
|
||||
current?: T
|
||||
defaultValue?: T
|
||||
value?: (x: T) => string
|
||||
label?: (x: T) => JSX.Element | string
|
||||
onSelect?: (value: T | undefined) => void
|
||||
class?: ComponentProps<"div">["class"]
|
||||
classList?: ComponentProps<"div">["classList"]
|
||||
size?: "small" | "medium"
|
||||
}
|
||||
|
||||
export function RadioGroup<T>(props: RadioGroupProps<T>) {
|
||||
const [local, others] = splitProps(props, [
|
||||
"class",
|
||||
"classList",
|
||||
"options",
|
||||
"current",
|
||||
"defaultValue",
|
||||
"value",
|
||||
"label",
|
||||
"onSelect",
|
||||
"size",
|
||||
])
|
||||
|
||||
const getValue = (item: T): string => {
|
||||
if (local.value) return local.value(item)
|
||||
return String(item)
|
||||
}
|
||||
|
||||
const getLabel = (item: T): JSX.Element | string => {
|
||||
if (local.label) return local.label(item)
|
||||
return String(item)
|
||||
}
|
||||
|
||||
const findOption = (v: string): T | undefined => {
|
||||
return local.options.find((opt) => getValue(opt) === v)
|
||||
}
|
||||
|
||||
return (
|
||||
<Kobalte
|
||||
{...others}
|
||||
data-component="radio-group"
|
||||
data-size={local.size ?? "medium"}
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
value={local.current ? getValue(local.current) : undefined}
|
||||
defaultValue={local.defaultValue ? getValue(local.defaultValue) : undefined}
|
||||
onChange={(v) => local.onSelect?.(findOption(v))}
|
||||
>
|
||||
<div role="presentation" data-slot="radio-group-wrapper">
|
||||
<Kobalte.Indicator data-slot="radio-group-indicator" />
|
||||
<div role="presentation" data-slot="radio-group-items">
|
||||
<For each={local.options}>
|
||||
{(option) => (
|
||||
<Kobalte.Item value={getValue(option)} data-slot="radio-group-item">
|
||||
<Kobalte.ItemInput data-slot="radio-group-item-input" />
|
||||
<Kobalte.ItemLabel data-slot="radio-group-item-label">{getLabel(option)}</Kobalte.ItemLabel>
|
||||
</Kobalte.Item>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
58
opencode/packages/ui/src/components/resize-handle.css
Normal file
58
opencode/packages/ui/src/components/resize-handle.css
Normal file
@@ -0,0 +1,58 @@
|
||||
[data-component="resize-handle"] {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover::after,
|
||||
&:active::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&[data-direction="horizontal"] {
|
||||
inset-block: 0;
|
||||
inset-inline-end: 0;
|
||||
width: 8px;
|
||||
transform: translateX(50%);
|
||||
cursor: col-resize;
|
||||
|
||||
&[data-edge="start"] {
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: auto;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&::after {
|
||||
width: 3px;
|
||||
inset-block: 0;
|
||||
inset-inline-start: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-direction="vertical"] {
|
||||
inset-inline: 0;
|
||||
inset-block-start: 0;
|
||||
height: 8px;
|
||||
transform: translateY(-50%);
|
||||
cursor: row-resize;
|
||||
|
||||
&[data-edge="end"] {
|
||||
inset-block-start: auto;
|
||||
inset-block-end: 0;
|
||||
transform: translateY(50%);
|
||||
}
|
||||
|
||||
&::after {
|
||||
height: 3px;
|
||||
inset-inline: 0;
|
||||
inset-block-start: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
opencode/packages/ui/src/components/resize-handle.tsx
Normal file
82
opencode/packages/ui/src/components/resize-handle.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { splitProps, type JSX } from "solid-js"
|
||||
|
||||
export interface ResizeHandleProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "onResize"> {
|
||||
direction: "horizontal" | "vertical"
|
||||
edge?: "start" | "end"
|
||||
size: number
|
||||
min: number
|
||||
max: number
|
||||
onResize: (size: number) => void
|
||||
onCollapse?: () => void
|
||||
collapseThreshold?: number
|
||||
}
|
||||
|
||||
export function ResizeHandle(props: ResizeHandleProps) {
|
||||
const [local, rest] = splitProps(props, [
|
||||
"direction",
|
||||
"edge",
|
||||
"size",
|
||||
"min",
|
||||
"max",
|
||||
"onResize",
|
||||
"onCollapse",
|
||||
"collapseThreshold",
|
||||
"class",
|
||||
"classList",
|
||||
])
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
const edge = local.edge ?? (local.direction === "vertical" ? "start" : "end")
|
||||
const start = local.direction === "horizontal" ? e.clientX : e.clientY
|
||||
const startSize = local.size
|
||||
let current = startSize
|
||||
|
||||
document.body.style.userSelect = "none"
|
||||
document.body.style.overflow = "hidden"
|
||||
|
||||
const onMouseMove = (moveEvent: MouseEvent) => {
|
||||
const pos = local.direction === "horizontal" ? moveEvent.clientX : moveEvent.clientY
|
||||
const delta =
|
||||
local.direction === "vertical"
|
||||
? edge === "end"
|
||||
? pos - start
|
||||
: start - pos
|
||||
: edge === "start"
|
||||
? start - pos
|
||||
: pos - start
|
||||
current = startSize + delta
|
||||
const clamped = Math.min(local.max, Math.max(local.min, current))
|
||||
local.onResize(clamped)
|
||||
}
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.body.style.userSelect = ""
|
||||
document.body.style.overflow = ""
|
||||
document.removeEventListener("mousemove", onMouseMove)
|
||||
document.removeEventListener("mouseup", onMouseUp)
|
||||
|
||||
const threshold = local.collapseThreshold ?? 0
|
||||
if (local.onCollapse && threshold > 0 && current < threshold) {
|
||||
local.onCollapse()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("mousemove", onMouseMove)
|
||||
document.addEventListener("mouseup", onMouseUp)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
data-component="resize-handle"
|
||||
data-direction={local.direction}
|
||||
data-edge={local.edge ?? (local.direction === "vertical" ? "start" : "end")}
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
)
|
||||
}
|
||||
202
opencode/packages/ui/src/components/select.css
Normal file
202
opencode/packages/ui/src/components/select.css
Normal file
@@ -0,0 +1,202 @@
|
||||
[data-component="select"] {
|
||||
[data-slot="select-select-trigger"] {
|
||||
padding: 0 4px 0 8px;
|
||||
box-shadow: none;
|
||||
|
||||
[data-slot="select-select-trigger-value"] {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
[data-slot="select-select-trigger-icon"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--text-weak);
|
||||
transition: transform 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
&[data-expanded] {
|
||||
&[data-variant="secondary"] {
|
||||
background-color: var(--button-secondary-hover);
|
||||
}
|
||||
&[data-variant="ghost"] {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
}
|
||||
&[data-variant="primary"] {
|
||||
background-color: var(--icon-strong-active);
|
||||
}
|
||||
}
|
||||
|
||||
&:not([data-expanded]):focus-visible {
|
||||
&[data-variant="secondary"] {
|
||||
background-color: var(--button-secondary-base);
|
||||
}
|
||||
&[data-variant="ghost"] {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
&[data-variant="primary"] {
|
||||
background-color: var(--icon-strong-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-trigger-style="settings"] {
|
||||
[data-slot="select-select-trigger"] {
|
||||
padding: 6px 6px 6px 12px;
|
||||
box-shadow: none;
|
||||
border-radius: 6px;
|
||||
min-width: 160px;
|
||||
height: 32px;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
background-color: transparent;
|
||||
|
||||
[data-slot="select-select-trigger-value"] {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-regular);
|
||||
}
|
||||
[data-slot="select-select-trigger-icon"] {
|
||||
width: 16px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--text-weak);
|
||||
background-color: var(--surface-raised-base);
|
||||
border-radius: 4px;
|
||||
transition: transform 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
&[data-slot="select-select-trigger"]:hover:not(:disabled),
|
||||
&[data-slot="select-select-trigger"][data-expanded],
|
||||
&[data-slot="select-select-trigger"][data-expanded]:hover:not(:disabled) {
|
||||
background-color: var(--input-base);
|
||||
box-shadow: var(--shadow-xs-border-base);
|
||||
}
|
||||
|
||||
&:not([data-expanded]):focus {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="select-content"] {
|
||||
min-width: 104px;
|
||||
max-width: 23rem;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--surface-raised-stronger-non-alpha);
|
||||
padding: 4px;
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
z-index: 60;
|
||||
|
||||
&[data-expanded] {
|
||||
animation: select-open 0.15s ease-out;
|
||||
}
|
||||
|
||||
[data-slot="select-select-content-list"] {
|
||||
overflow-y: auto;
|
||||
max-height: 12rem;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
> *:not([role="presentation"]) + *:not([role="presentation"]) {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="select-select-item"] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
gap: 12px;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
|
||||
/* text-12-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
color: var(--text-strong);
|
||||
|
||||
transition:
|
||||
background-color 0.2s ease-in-out,
|
||||
color 0.2s ease-in-out;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
|
||||
&[data-highlighted] {
|
||||
background: var(--surface-raised-base-hover);
|
||||
}
|
||||
&[data-disabled] {
|
||||
background-color: var(--surface-raised-base);
|
||||
pointer-events: none;
|
||||
}
|
||||
[data-slot="select-select-item-indicator"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&:hover {
|
||||
background: var(--surface-raised-base-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="select-content"][data-trigger-style="settings"] {
|
||||
min-width: 160px;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
|
||||
[data-slot="select-select-content-list"] {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
[data-slot="select-select-item"] {
|
||||
/* text-14-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes select-open {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
171
opencode/packages/ui/src/components/select.tsx
Normal file
171
opencode/packages/ui/src/components/select.tsx
Normal file
@@ -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 { Button, ButtonProps } from "./button"
|
||||
import { Icon } from "./icon"
|
||||
|
||||
export type SelectProps<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
|
||||
triggerVariant?: "settings"
|
||||
}
|
||||
|
||||
export function Select<T>(props: SelectProps<T> & Omit<ButtonProps, "children">) {
|
||||
const [local, others] = splitProps(props, [
|
||||
"class",
|
||||
"classList",
|
||||
"placeholder",
|
||||
"options",
|
||||
"current",
|
||||
"value",
|
||||
"label",
|
||||
"groupBy",
|
||||
"valueClass",
|
||||
"onSelect",
|
||||
"onHighlight",
|
||||
"onOpenChange",
|
||||
"children",
|
||||
"triggerStyle",
|
||||
"triggerVariant",
|
||||
])
|
||||
|
||||
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) : "")),
|
||||
// mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))),
|
||||
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={local.triggerVariant}
|
||||
placement={local.triggerVariant === "settings" ? "bottom-end" : "bottom-start"}
|
||||
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
|
||||
disabled={props.disabled}
|
||||
data-slot="select-select-trigger"
|
||||
as={Button}
|
||||
size={props.size}
|
||||
variant={props.variant}
|
||||
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">
|
||||
<Icon name={local.triggerVariant === "settings" ? "selector" : "chevron-down"} size="small" />
|
||||
</Kobalte.Icon>
|
||||
</Kobalte.Trigger>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Content
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
data-component="select-content"
|
||||
data-trigger-style={local.triggerVariant}
|
||||
>
|
||||
<Kobalte.Listbox data-slot="select-select-content-list" />
|
||||
</Kobalte.Content>
|
||||
</Kobalte.Portal>
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
225
opencode/packages/ui/src/components/session-review.css
Normal file
225
opencode/packages/ui/src/components/session-review.css
Normal file
@@ -0,0 +1,225 @@
|
||||
[data-component="session-review"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
contain: strict;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="session-review-container"] {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
[data-slot="session-review-header"] {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background-color: var(--background-stronger);
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
[data-slot="session-review-title"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-large);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
[data-slot="session-review-actions"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 16px;
|
||||
padding-right: 1px;
|
||||
}
|
||||
|
||||
[data-component="sticky-accordion-header"] {
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
[data-component="sticky-accordion-header"][data-expanded]::before,
|
||||
[data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before {
|
||||
top: -40px;
|
||||
}
|
||||
|
||||
[data-slot="accordion-trigger"] {
|
||||
background-color: var(--background-stronger) !important;
|
||||
}
|
||||
|
||||
[data-slot="session-review-accordion-item"][data-selected] {
|
||||
[data-slot="session-review-accordion-content"] {
|
||||
box-shadow: var(--shadow-xs-border-select);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="accordion-item"] {
|
||||
[data-slot="accordion-content"] {
|
||||
display: none;
|
||||
}
|
||||
&[data-expanded] {
|
||||
[data-slot="accordion-content"] {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="accordion-content"] {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
[data-slot="session-review-accordion-content"] {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-slot="session-review-trigger-content"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
[data-slot="session-review-file-info"] {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="session-review-file-name-container"] {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="session-review-directory"] {
|
||||
color: var(--text-base);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
[data-slot="session-review-filename"] {
|
||||
color: var(--text-strong);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-slot="session-review-view-button"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
margin-left: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-base);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-strong);
|
||||
background: var(--surface-base);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="accordion-trigger"]:hover [data-slot="session-review-view-button"] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-slot="session-review-trigger-actions"] {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
[data-slot="session-review-change"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
[data-slot="session-review-change"][data-type="added"] {
|
||||
color: var(--icon-diff-add-base);
|
||||
}
|
||||
|
||||
[data-slot="session-review-change"][data-type="removed"] {
|
||||
color: var(--icon-diff-delete-base);
|
||||
}
|
||||
|
||||
[data-slot="session-review-change"][data-type="modified"] {
|
||||
color: var(--icon-diff-modified-base);
|
||||
}
|
||||
|
||||
[data-slot="session-review-file-container"] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[data-slot="session-review-image-container"] {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: var(--background-stronger);
|
||||
}
|
||||
|
||||
[data-slot="session-review-image"] {
|
||||
max-width: 100%;
|
||||
max-height: 60vh;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-weak-base);
|
||||
background: var(--background-base);
|
||||
}
|
||||
|
||||
[data-slot="session-review-image-placeholder"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="session-review-audio-container"] {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: var(--background-stronger);
|
||||
}
|
||||
|
||||
[data-slot="session-review-audio"] {
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
[data-slot="session-review-audio-placeholder"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="session-review-diff-wrapper"] {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
--line-comment-z: 5;
|
||||
--line-comment-popover-z: 30;
|
||||
--line-comment-open-z: 6;
|
||||
}
|
||||
}
|
||||
674
opencode/packages/ui/src/components/session-review.tsx
Normal file
674
opencode/packages/ui/src/components/session-review.tsx
Normal file
@@ -0,0 +1,674 @@
|
||||
import { Accordion } from "./accordion"
|
||||
import { Button } from "./button"
|
||||
import { RadioGroup } from "./radio-group"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
import { FileIcon } from "./file-icon"
|
||||
import { Icon } from "./icon"
|
||||
import { LineComment, LineCommentEditor } from "./line-comment"
|
||||
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
||||
import { useDiffComponent } from "../context/diff"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import { createEffect, createMemo, createSignal, For, Match, Show, Switch, type JSX } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { type FileContent, type FileDiff } from "@opencode-ai/sdk/v2"
|
||||
import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
||||
import { type SelectedLineRange } from "@pierre/diffs"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
|
||||
export type SessionReviewDiffStyle = "unified" | "split"
|
||||
|
||||
export type SessionReviewComment = {
|
||||
id: string
|
||||
file: string
|
||||
selection: SelectedLineRange
|
||||
comment: string
|
||||
}
|
||||
|
||||
export type SessionReviewLineComment = {
|
||||
file: string
|
||||
selection: SelectedLineRange
|
||||
comment: string
|
||||
preview?: string
|
||||
}
|
||||
|
||||
export type SessionReviewFocus = { file: string; id: string }
|
||||
|
||||
export interface SessionReviewProps {
|
||||
title?: JSX.Element
|
||||
empty?: JSX.Element
|
||||
split?: boolean
|
||||
diffStyle?: SessionReviewDiffStyle
|
||||
onDiffStyleChange?: (diffStyle: SessionReviewDiffStyle) => void
|
||||
onDiffRendered?: () => void
|
||||
onLineComment?: (comment: SessionReviewLineComment) => void
|
||||
comments?: SessionReviewComment[]
|
||||
focusedComment?: SessionReviewFocus | null
|
||||
onFocusedCommentChange?: (focus: SessionReviewFocus | null) => void
|
||||
focusedFile?: string
|
||||
open?: string[]
|
||||
onOpenChange?: (open: string[]) => void
|
||||
scrollRef?: (el: HTMLDivElement) => void
|
||||
onScroll?: JSX.EventHandlerUnion<HTMLDivElement, Event>
|
||||
class?: string
|
||||
classList?: Record<string, boolean | undefined>
|
||||
classes?: { root?: string; header?: string; container?: string }
|
||||
actions?: JSX.Element
|
||||
diffs: (FileDiff & { preloaded?: PreloadMultiFileDiffResult<any> })[]
|
||||
onViewFile?: (file: string) => void
|
||||
readFile?: (path: string) => Promise<FileContent | undefined>
|
||||
}
|
||||
|
||||
const imageExtensions = new Set(["png", "jpg", "jpeg", "gif", "webp", "avif", "bmp", "ico", "tif", "tiff", "heic"])
|
||||
const audioExtensions = new Set(["mp3", "wav", "ogg", "m4a", "aac", "flac", "opus"])
|
||||
|
||||
function normalizeMimeType(type: string | undefined): string | undefined {
|
||||
if (!type) return
|
||||
|
||||
const mime = type.split(";", 1)[0]?.trim().toLowerCase()
|
||||
if (!mime) return
|
||||
|
||||
if (mime === "audio/x-aac") return "audio/aac"
|
||||
if (mime === "audio/x-m4a") return "audio/mp4"
|
||||
|
||||
return mime
|
||||
}
|
||||
|
||||
function getExtension(file: string): string {
|
||||
const idx = file.lastIndexOf(".")
|
||||
if (idx === -1) return ""
|
||||
return file.slice(idx + 1).toLowerCase()
|
||||
}
|
||||
|
||||
function isImageFile(file: string): boolean {
|
||||
return imageExtensions.has(getExtension(file))
|
||||
}
|
||||
|
||||
function isAudioFile(file: string): boolean {
|
||||
return audioExtensions.has(getExtension(file))
|
||||
}
|
||||
|
||||
function dataUrl(content: FileContent | undefined): string | undefined {
|
||||
if (!content) return
|
||||
if (content.encoding !== "base64") return
|
||||
const mime = normalizeMimeType(content.mimeType)
|
||||
if (!mime) return
|
||||
if (!mime.startsWith("image/") && !mime.startsWith("audio/")) return
|
||||
return `data:${mime};base64,${content.content}`
|
||||
}
|
||||
|
||||
function dataUrlFromValue(value: unknown): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
if (value.startsWith("data:image/")) return value
|
||||
if (value.startsWith("data:audio/x-aac;")) return value.replace("data:audio/x-aac;", "data:audio/aac;")
|
||||
if (value.startsWith("data:audio/x-m4a;")) return value.replace("data:audio/x-m4a;", "data:audio/mp4;")
|
||||
if (value.startsWith("data:audio/")) return value
|
||||
return
|
||||
}
|
||||
if (!value || typeof value !== "object") return
|
||||
|
||||
const content = (value as { content?: unknown }).content
|
||||
const encoding = (value as { encoding?: unknown }).encoding
|
||||
const mimeType = (value as { mimeType?: unknown }).mimeType
|
||||
|
||||
if (typeof content !== "string") return
|
||||
if (encoding !== "base64") return
|
||||
if (typeof mimeType !== "string") return
|
||||
const mime = normalizeMimeType(mimeType)
|
||||
if (!mime) return
|
||||
if (!mime.startsWith("image/") && !mime.startsWith("audio/")) return
|
||||
|
||||
return `data:${mime};base64,${content}`
|
||||
}
|
||||
|
||||
function diffId(file: string): string | undefined {
|
||||
const sum = checksum(file)
|
||||
if (!sum) return
|
||||
return `session-review-diff-${sum}`
|
||||
}
|
||||
|
||||
type SessionReviewSelection = {
|
||||
file: string
|
||||
range: SelectedLineRange
|
||||
}
|
||||
|
||||
function findSide(element: HTMLElement): "additions" | "deletions" | undefined {
|
||||
const typed = element.closest("[data-line-type]")
|
||||
if (typed instanceof HTMLElement) {
|
||||
const type = typed.dataset.lineType
|
||||
if (type === "change-deletion") return "deletions"
|
||||
if (type === "change-addition" || type === "change-additions") return "additions"
|
||||
}
|
||||
|
||||
const code = element.closest("[data-code]")
|
||||
if (!(code instanceof HTMLElement)) return
|
||||
return code.hasAttribute("data-deletions") ? "deletions" : "additions"
|
||||
}
|
||||
|
||||
function findMarker(root: ShadowRoot, range: SelectedLineRange) {
|
||||
const marker = (line: number, side?: "additions" | "deletions") => {
|
||||
const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-alt-line="${line}"]`)).filter(
|
||||
(node): node is HTMLElement => node instanceof HTMLElement,
|
||||
)
|
||||
if (nodes.length === 0) return
|
||||
if (!side) return nodes[0]
|
||||
const match = nodes.find((node) => findSide(node) === side)
|
||||
return match ?? nodes[0]
|
||||
}
|
||||
|
||||
const a = marker(range.start, range.side)
|
||||
const b = marker(range.end, range.endSide ?? range.side)
|
||||
if (!a) return b
|
||||
if (!b) return a
|
||||
return a.getBoundingClientRect().top > b.getBoundingClientRect().top ? a : b
|
||||
}
|
||||
|
||||
function markerTop(wrapper: HTMLElement, marker: HTMLElement) {
|
||||
const wrapperRect = wrapper.getBoundingClientRect()
|
||||
const rect = marker.getBoundingClientRect()
|
||||
return rect.top - wrapperRect.top + Math.max(0, (rect.height - 20) / 2)
|
||||
}
|
||||
|
||||
export const SessionReview = (props: SessionReviewProps) => {
|
||||
let scroll: HTMLDivElement | undefined
|
||||
let focusToken = 0
|
||||
const i18n = useI18n()
|
||||
const diffComponent = useDiffComponent()
|
||||
const anchors = new Map<string, HTMLElement>()
|
||||
const [store, setStore] = createStore({
|
||||
open: props.diffs.length > 10 ? [] : props.diffs.map((d) => d.file),
|
||||
})
|
||||
|
||||
const [selection, setSelection] = createSignal<SessionReviewSelection | null>(null)
|
||||
const [commenting, setCommenting] = createSignal<SessionReviewSelection | null>(null)
|
||||
const [opened, setOpened] = createSignal<SessionReviewFocus | null>(null)
|
||||
|
||||
const open = () => props.open ?? store.open
|
||||
const diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified")
|
||||
const hasDiffs = () => props.diffs.length > 0
|
||||
|
||||
const handleChange = (open: string[]) => {
|
||||
props.onOpenChange?.(open)
|
||||
if (props.open !== undefined) return
|
||||
setStore("open", open)
|
||||
}
|
||||
|
||||
const handleExpandOrCollapseAll = () => {
|
||||
const next = open().length > 0 ? [] : props.diffs.map((d) => d.file)
|
||||
handleChange(next)
|
||||
}
|
||||
|
||||
const selectionLabel = (range: SelectedLineRange) => {
|
||||
const start = Math.min(range.start, range.end)
|
||||
const end = Math.max(range.start, range.end)
|
||||
if (start === end) return `line ${start}`
|
||||
return `lines ${start}-${end}`
|
||||
}
|
||||
|
||||
const selectionSide = (range: SelectedLineRange) => range.endSide ?? range.side ?? "additions"
|
||||
|
||||
const selectionPreview = (diff: FileDiff, range: SelectedLineRange) => {
|
||||
const side = selectionSide(range)
|
||||
const contents = side === "deletions" ? diff.before : diff.after
|
||||
if (typeof contents !== "string" || contents.length === 0) return undefined
|
||||
|
||||
const start = Math.max(1, Math.min(range.start, range.end))
|
||||
const end = Math.max(range.start, range.end)
|
||||
const lines = contents.split("\n").slice(start - 1, end)
|
||||
if (lines.length === 0) return undefined
|
||||
return lines.slice(0, 2).join("\n")
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const focus = props.focusedComment
|
||||
if (!focus) return
|
||||
|
||||
focusToken++
|
||||
const token = focusToken
|
||||
|
||||
setOpened(focus)
|
||||
|
||||
const comment = (props.comments ?? []).find((c) => c.file === focus.file && c.id === focus.id)
|
||||
if (comment) setSelection({ file: comment.file, range: comment.selection })
|
||||
|
||||
const current = open()
|
||||
if (!current.includes(focus.file)) {
|
||||
handleChange([...current, focus.file])
|
||||
}
|
||||
|
||||
const scrollTo = (attempt: number) => {
|
||||
if (token !== focusToken) return
|
||||
|
||||
const root = scroll
|
||||
if (!root) return
|
||||
|
||||
const anchor = root.querySelector(`[data-comment-id="${focus.id}"]`)
|
||||
const ready =
|
||||
anchor instanceof HTMLElement && anchor.style.pointerEvents !== "none" && anchor.style.opacity !== "0"
|
||||
|
||||
const target = ready ? anchor : anchors.get(focus.file)
|
||||
if (!target) {
|
||||
if (attempt >= 120) return
|
||||
requestAnimationFrame(() => scrollTo(attempt + 1))
|
||||
return
|
||||
}
|
||||
|
||||
const rootRect = root.getBoundingClientRect()
|
||||
const targetRect = target.getBoundingClientRect()
|
||||
const offset = targetRect.top - rootRect.top
|
||||
const next = root.scrollTop + offset - rootRect.height / 2 + targetRect.height / 2
|
||||
root.scrollTop = Math.max(0, next)
|
||||
|
||||
if (ready) return
|
||||
if (attempt >= 120) return
|
||||
requestAnimationFrame(() => scrollTo(attempt + 1))
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => scrollTo(0))
|
||||
|
||||
requestAnimationFrame(() => props.onFocusedCommentChange?.(null))
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="session-review"
|
||||
ref={(el) => {
|
||||
scroll = el
|
||||
props.scrollRef?.(el)
|
||||
}}
|
||||
onScroll={props.onScroll}
|
||||
classList={{
|
||||
...(props.classList ?? {}),
|
||||
[props.classes?.root ?? ""]: !!props.classes?.root,
|
||||
[props.class ?? ""]: !!props.class,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
data-slot="session-review-header"
|
||||
classList={{
|
||||
[props.classes?.header ?? ""]: !!props.classes?.header,
|
||||
}}
|
||||
>
|
||||
<div data-slot="session-review-title">{props.title ?? i18n.t("ui.sessionReview.title")}</div>
|
||||
<div data-slot="session-review-actions">
|
||||
<Show when={hasDiffs() && props.onDiffStyleChange}>
|
||||
<RadioGroup
|
||||
options={["unified", "split"] as const}
|
||||
current={diffStyle()}
|
||||
value={(style) => style}
|
||||
label={(style) =>
|
||||
i18n.t(style === "unified" ? "ui.sessionReview.diffStyle.unified" : "ui.sessionReview.diffStyle.split")
|
||||
}
|
||||
onSelect={(style) => style && props.onDiffStyleChange?.(style)}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={hasDiffs()}>
|
||||
<Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}>
|
||||
<Switch>
|
||||
<Match when={open().length > 0}>{i18n.t("ui.sessionReview.collapseAll")}</Match>
|
||||
<Match when={true}>{i18n.t("ui.sessionReview.expandAll")}</Match>
|
||||
</Switch>
|
||||
</Button>
|
||||
</Show>
|
||||
{props.actions}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-slot="session-review-container"
|
||||
classList={{
|
||||
[props.classes?.container ?? ""]: !!props.classes?.container,
|
||||
}}
|
||||
>
|
||||
<Show when={hasDiffs()} fallback={props.empty}>
|
||||
<Accordion multiple value={open()} onChange={handleChange}>
|
||||
<For each={props.diffs}>
|
||||
{(diff) => {
|
||||
let wrapper: HTMLDivElement | undefined
|
||||
|
||||
const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
|
||||
const commentedLines = createMemo(() => comments().map((c) => c.selection))
|
||||
|
||||
const beforeText = () => (typeof diff.before === "string" ? diff.before : "")
|
||||
const afterText = () => (typeof diff.after === "string" ? diff.after : "")
|
||||
|
||||
const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0)
|
||||
const isDeleted = () =>
|
||||
diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
|
||||
const isImage = () => isImageFile(diff.file)
|
||||
const isAudio = () => isAudioFile(diff.file)
|
||||
|
||||
const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
|
||||
const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc)
|
||||
const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle")
|
||||
|
||||
const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
|
||||
const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc)
|
||||
const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle")
|
||||
const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined)
|
||||
|
||||
const selectedLines = createMemo(() => {
|
||||
const current = selection()
|
||||
if (!current || current.file !== diff.file) return null
|
||||
return current.range
|
||||
})
|
||||
|
||||
const draftRange = createMemo(() => {
|
||||
const current = commenting()
|
||||
if (!current || current.file !== diff.file) return null
|
||||
return current.range
|
||||
})
|
||||
|
||||
const [draft, setDraft] = createSignal("")
|
||||
const [positions, setPositions] = createSignal<Record<string, number>>({})
|
||||
const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
|
||||
|
||||
const getRoot = () => {
|
||||
const el = wrapper
|
||||
if (!el) return
|
||||
|
||||
const host = el.querySelector("diffs-container")
|
||||
if (!(host instanceof HTMLElement)) return
|
||||
return host.shadowRoot ?? undefined
|
||||
}
|
||||
|
||||
const updateAnchors = () => {
|
||||
const el = wrapper
|
||||
if (!el) return
|
||||
|
||||
const root = getRoot()
|
||||
if (!root) return
|
||||
|
||||
const next: Record<string, number> = {}
|
||||
for (const item of comments()) {
|
||||
const marker = findMarker(root, item.selection)
|
||||
if (!marker) continue
|
||||
next[item.id] = markerTop(el, marker)
|
||||
}
|
||||
setPositions(next)
|
||||
|
||||
const range = draftRange()
|
||||
if (!range) {
|
||||
setDraftTop(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
const marker = findMarker(root, range)
|
||||
if (!marker) {
|
||||
setDraftTop(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
setDraftTop(markerTop(el, marker))
|
||||
}
|
||||
|
||||
const scheduleAnchors = () => {
|
||||
requestAnimationFrame(updateAnchors)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
comments()
|
||||
scheduleAnchors()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const range = draftRange()
|
||||
if (!range) return
|
||||
setDraft("")
|
||||
scheduleAnchors()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!open().includes(diff.file)) return
|
||||
if (!isImage()) return
|
||||
if (imageSrc()) return
|
||||
if (imageStatus() !== "idle") return
|
||||
if (isDeleted()) return
|
||||
|
||||
const reader = props.readFile
|
||||
if (!reader) return
|
||||
|
||||
setImageStatus("loading")
|
||||
reader(diff.file)
|
||||
.then((result) => {
|
||||
const src = dataUrl(result)
|
||||
if (!src) {
|
||||
setImageStatus("error")
|
||||
return
|
||||
}
|
||||
setImageSrc(src)
|
||||
setImageStatus("idle")
|
||||
})
|
||||
.catch(() => {
|
||||
setImageStatus("error")
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!open().includes(diff.file)) return
|
||||
if (!isAudio()) return
|
||||
if (audioSrc()) return
|
||||
if (audioStatus() !== "idle") return
|
||||
|
||||
const reader = props.readFile
|
||||
if (!reader) return
|
||||
|
||||
setAudioStatus("loading")
|
||||
reader(diff.file)
|
||||
.then((result) => {
|
||||
const src = dataUrl(result)
|
||||
if (!src) {
|
||||
setAudioStatus("error")
|
||||
return
|
||||
}
|
||||
setAudioMime(normalizeMimeType(result?.mimeType))
|
||||
setAudioSrc(src)
|
||||
setAudioStatus("idle")
|
||||
})
|
||||
.catch(() => {
|
||||
setAudioStatus("error")
|
||||
})
|
||||
})
|
||||
|
||||
const handleLineSelected = (range: SelectedLineRange | null) => {
|
||||
if (!props.onLineComment) return
|
||||
|
||||
if (!range) {
|
||||
setSelection(null)
|
||||
return
|
||||
}
|
||||
|
||||
setSelection({ file: diff.file, range })
|
||||
}
|
||||
|
||||
const handleLineSelectionEnd = (range: SelectedLineRange | null) => {
|
||||
if (!props.onLineComment) return
|
||||
|
||||
if (!range) {
|
||||
setCommenting(null)
|
||||
return
|
||||
}
|
||||
|
||||
setSelection({ file: diff.file, range })
|
||||
setCommenting({ file: diff.file, range })
|
||||
}
|
||||
|
||||
const openComment = (comment: SessionReviewComment) => {
|
||||
setOpened({ file: comment.file, id: comment.id })
|
||||
setSelection({ file: comment.file, range: comment.selection })
|
||||
}
|
||||
|
||||
const isCommentOpen = (comment: SessionReviewComment) => {
|
||||
const current = opened()
|
||||
if (!current) return false
|
||||
return current.file === comment.file && current.id === comment.id
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion.Item
|
||||
value={diff.file}
|
||||
id={diffId(diff.file)}
|
||||
data-file={diff.file}
|
||||
data-slot="session-review-accordion-item"
|
||||
data-selected={props.focusedFile === diff.file ? "" : undefined}
|
||||
>
|
||||
<StickyAccordionHeader>
|
||||
<Accordion.Trigger>
|
||||
<div data-slot="session-review-trigger-content">
|
||||
<div data-slot="session-review-file-info">
|
||||
<FileIcon node={{ path: diff.file, type: "file" }} />
|
||||
<div data-slot="session-review-file-name-container">
|
||||
<Show when={diff.file.includes("/")}>
|
||||
<span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span>
|
||||
</Show>
|
||||
<span data-slot="session-review-filename">{getFilename(diff.file)}</span>
|
||||
<Show when={props.onViewFile}>
|
||||
<button
|
||||
data-slot="session-review-view-button"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
props.onViewFile?.(diff.file)
|
||||
}}
|
||||
>
|
||||
<Icon name="eye" size="small" />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div data-slot="session-review-trigger-actions">
|
||||
<Switch>
|
||||
<Match when={isAdded()}>
|
||||
<span data-slot="session-review-change" data-type="added">
|
||||
{i18n.t("ui.sessionReview.change.added")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={isDeleted()}>
|
||||
<span data-slot="session-review-change" data-type="removed">
|
||||
{i18n.t("ui.sessionReview.change.removed")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={isImage()}>
|
||||
<span data-slot="session-review-change" data-type="modified">
|
||||
{i18n.t("ui.sessionReview.change.modified")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<DiffChanges changes={diff} />
|
||||
</Match>
|
||||
</Switch>
|
||||
<Icon name="chevron-grabber-vertical" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Trigger>
|
||||
</StickyAccordionHeader>
|
||||
<Accordion.Content data-slot="session-review-accordion-content">
|
||||
<div
|
||||
data-slot="session-review-diff-wrapper"
|
||||
ref={(el) => {
|
||||
wrapper = el
|
||||
anchors.set(diff.file, el)
|
||||
scheduleAnchors()
|
||||
}}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={isImage() && imageSrc()}>
|
||||
<div data-slot="session-review-image-container">
|
||||
<img data-slot="session-review-image" src={imageSrc()} alt={diff.file} />
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isImage() && isDeleted()}>
|
||||
<div data-slot="session-review-image-container" data-removed>
|
||||
<span data-slot="session-review-image-placeholder">
|
||||
{i18n.t("ui.sessionReview.change.removed")}
|
||||
</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isImage() && !imageSrc()}>
|
||||
<div data-slot="session-review-image-container">
|
||||
<span data-slot="session-review-image-placeholder">
|
||||
{imageStatus() === "loading" ? "Loading..." : "Image"}
|
||||
</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!isImage()}>
|
||||
<Dynamic
|
||||
component={diffComponent}
|
||||
preloadedDiff={diff.preloaded}
|
||||
diffStyle={diffStyle()}
|
||||
onRendered={() => {
|
||||
props.onDiffRendered?.()
|
||||
scheduleAnchors()
|
||||
}}
|
||||
enableLineSelection={props.onLineComment != null}
|
||||
onLineSelected={handleLineSelected}
|
||||
onLineSelectionEnd={handleLineSelectionEnd}
|
||||
selectedLines={selectedLines()}
|
||||
commentedLines={commentedLines()}
|
||||
before={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.before === "string" ? diff.before : "",
|
||||
}}
|
||||
after={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.after === "string" ? diff.after : "",
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<For each={comments()}>
|
||||
{(comment) => (
|
||||
<LineComment
|
||||
id={comment.id}
|
||||
top={positions()[comment.id]}
|
||||
onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
|
||||
onClick={() => {
|
||||
if (isCommentOpen(comment)) {
|
||||
setOpened(null)
|
||||
return
|
||||
}
|
||||
|
||||
openComment(comment)
|
||||
}}
|
||||
open={isCommentOpen(comment)}
|
||||
comment={comment.comment}
|
||||
selection={selectionLabel(comment.selection)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={draftRange()}>
|
||||
{(range) => (
|
||||
<Show when={draftTop() !== undefined}>
|
||||
<LineCommentEditor
|
||||
top={draftTop()}
|
||||
value={draft()}
|
||||
selection={selectionLabel(range())}
|
||||
onInput={setDraft}
|
||||
onCancel={() => setCommenting(null)}
|
||||
onSubmit={(comment) => {
|
||||
props.onLineComment?.({
|
||||
file: diff.file,
|
||||
selection: range(),
|
||||
comment,
|
||||
preview: selectionPreview(diff, range()),
|
||||
})
|
||||
setCommenting(null)
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Accordion>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
603
opencode/packages/ui/src/components/session-turn.css
Normal file
603
opencode/packages/ui/src/components/session-turn.css
Normal file
@@ -0,0 +1,603 @@
|
||||
[data-component="session-turn"] {
|
||||
--session-turn-sticky-height: 0px;
|
||||
--sticky-header-height: calc(var(--session-title-height, 0px) + var(--session-turn-sticky-height, 0px) + 24px);
|
||||
/* flex: 1; */
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
[data-slot="session-turn-content"] {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-content"]::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-message-container"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
gap: 18px;
|
||||
overflow-anchor: none;
|
||||
|
||||
[data-slot="session-turn-badge"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-x-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-normal);
|
||||
white-space: nowrap;
|
||||
color: var(--text-base);
|
||||
background: var(--surface-raised-base);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="session-turn-attachments"] {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-sticky"] {
|
||||
width: calc(100% + 9px);
|
||||
position: sticky;
|
||||
top: var(--session-title-height, 0px);
|
||||
z-index: 20;
|
||||
background-color: var(--background-stronger);
|
||||
margin-left: -9px;
|
||||
padding-left: 9px;
|
||||
/* padding-bottom: 12px; */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--background-stronger);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 32px;
|
||||
background: linear-gradient(to bottom, var(--background-stronger), transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="session-turn-message-header"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-message-content"] {
|
||||
margin-top: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
[data-component="user-message"] [data-slot="user-message-text"] {
|
||||
max-height: var(--user-message-collapsed-height, 64px);
|
||||
}
|
||||
|
||||
[data-component="user-message"][data-expanded="true"] [data-slot="user-message-text"] {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
[data-component="user-message"][data-can-expand="true"] [data-slot="user-message-text"] {
|
||||
padding-right: 36px;
|
||||
padding-bottom: 28px;
|
||||
}
|
||||
|
||||
[data-component="user-message"][data-can-expand="true"]:not([data-expanded="true"])
|
||||
[data-slot="user-message-text"]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 8px;
|
||||
bottom: 0px;
|
||||
background:
|
||||
linear-gradient(to bottom, transparent, var(--surface-weak)),
|
||||
linear-gradient(to bottom, transparent, var(--surface-weak));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-component="user-message"] [data-slot="user-message-text"] [data-slot="user-message-expand"] {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 6px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[data-component="user-message"][data-can-expand="true"]
|
||||
[data-slot="user-message-text"]
|
||||
[data-slot="user-message-expand"],
|
||||
[data-component="user-message"][data-expanded="true"]
|
||||
[data-slot="user-message-text"]
|
||||
[data-slot="user-message-expand"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: var(--text-weak);
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="user-message"][data-expanded="true"]
|
||||
[data-slot="user-message-text"]
|
||||
[data-slot="user-message-expand"]
|
||||
[data-slot="icon-svg"] {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
[data-component="user-message"] [data-slot="user-message-text"] [data-slot="user-message-expand"]:hover {
|
||||
background: var(--surface-raised-base);
|
||||
color: var(--text-base);
|
||||
}
|
||||
|
||||
[data-slot="session-turn-user-badges"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-message-title"] {
|
||||
width: 100%;
|
||||
font-size: var(--font-size-large);
|
||||
font-weight: 500;
|
||||
color: var(--text-strong);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-message-title"] h1 {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-typewriter"] {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-summary-section"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-summary-header"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
align-self: stretch;
|
||||
|
||||
[data-slot="session-turn-summary-title-row"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-response"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-response-copy-wrapper"] {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover [data-slot="session-turn-response-copy-wrapper"],
|
||||
&:focus-within [data-slot="session-turn-response-copy-wrapper"] {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-x-large);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="session-turn-summary-title"] {
|
||||
font-size: 13px;
|
||||
/* text-12-medium */
|
||||
font-weight: 500;
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="session-turn-markdown"],
|
||||
[data-slot="session-turn-accordion"] [data-slot="accordion-content"] {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-markdown"] {
|
||||
&[data-diffs="true"] {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
&[data-fade="true"] > * {
|
||||
animation: fadeUp 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
|
||||
&:nth-child(1) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
&:nth-child(5) {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
&:nth-child(6) {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
&:nth-child(7) {
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
|
||||
&:nth-child(8) {
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
|
||||
&:nth-child(9) {
|
||||
animation-delay: 0.9s;
|
||||
}
|
||||
|
||||
&:nth-child(10) {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
&:nth-child(11) {
|
||||
animation-delay: 1.1s;
|
||||
}
|
||||
|
||||
&:nth-child(12) {
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
|
||||
&:nth-child(13) {
|
||||
animation-delay: 1.3s;
|
||||
}
|
||||
|
||||
&:nth-child(14) {
|
||||
animation-delay: 1.4s;
|
||||
}
|
||||
|
||||
&:nth-child(15) {
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
|
||||
&:nth-child(16) {
|
||||
animation-delay: 1.6s;
|
||||
}
|
||||
|
||||
&:nth-child(17) {
|
||||
animation-delay: 1.7s;
|
||||
}
|
||||
|
||||
&:nth-child(18) {
|
||||
animation-delay: 1.8s;
|
||||
}
|
||||
|
||||
&:nth-child(19) {
|
||||
animation-delay: 1.9s;
|
||||
}
|
||||
|
||||
&:nth-child(20) {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
&:nth-child(21) {
|
||||
animation-delay: 2.1s;
|
||||
}
|
||||
|
||||
&:nth-child(22) {
|
||||
animation-delay: 2.2s;
|
||||
}
|
||||
|
||||
&:nth-child(23) {
|
||||
animation-delay: 2.3s;
|
||||
}
|
||||
|
||||
&:nth-child(24) {
|
||||
animation-delay: 2.4s;
|
||||
}
|
||||
|
||||
&:nth-child(25) {
|
||||
animation-delay: 2.5s;
|
||||
}
|
||||
|
||||
&:nth-child(26) {
|
||||
animation-delay: 2.6s;
|
||||
}
|
||||
|
||||
&:nth-child(27) {
|
||||
animation-delay: 2.7s;
|
||||
}
|
||||
|
||||
&:nth-child(28) {
|
||||
animation-delay: 2.8s;
|
||||
}
|
||||
|
||||
&:nth-child(29) {
|
||||
animation-delay: 2.9s;
|
||||
}
|
||||
|
||||
&:nth-child(30) {
|
||||
animation-delay: 3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="session-turn-summary-section"] {
|
||||
position: relative;
|
||||
|
||||
[data-slot="session-turn-summary-copy"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover [data-slot="session-turn-summary-copy"] {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="session-turn-accordion"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-component="sticky-accordion-header"] {
|
||||
top: var(--sticky-header-height, 0px);
|
||||
}
|
||||
|
||||
[data-component="sticky-accordion-header"][data-expanded]::before,
|
||||
[data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before {
|
||||
top: calc(-1 * var(--sticky-header-height, 0px));
|
||||
}
|
||||
|
||||
[data-slot="session-turn-accordion-trigger-content"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 20px;
|
||||
|
||||
[data-expandable="false"] {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="session-turn-file-info"] {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-file-icon"] {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-file-path"] {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-directory"] {
|
||||
color: var(--text-base);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-filename"] {
|
||||
color: var(--text-strong);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-accordion-actions"] {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-accordion-content"] {
|
||||
max-height: 240px;
|
||||
/* max-h-60 */
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-accordion-content"]::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-response-section"] {
|
||||
width: calc(100% + 9px);
|
||||
min-width: 0;
|
||||
margin-left: -9px;
|
||||
padding-left: 9px;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-collapsible"] {
|
||||
gap: 32px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-collapsible-trigger-content"] {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-weak);
|
||||
|
||||
[data-slot="session-turn-trigger-icon"] {
|
||||
color: var(--icon-base);
|
||||
}
|
||||
|
||||
[data-component="spinner"] {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
[data-component="icon"] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="session-turn-retry-message"] {
|
||||
font-weight: 500;
|
||||
color: var(--syntax-critical);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-retry-seconds"] {
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="session-turn-retry-attempt"] {
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="session-turn-status-text"] {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-details-text"] {
|
||||
font-size: 13px;
|
||||
/* text-12-medium */
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-card {
|
||||
color: var(--text-on-critical-base);
|
||||
max-height: 240px;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-collapsible-content-inner"] {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
gap: 12px;
|
||||
margin-left: 12px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
border-left: 1px solid var(--border-base);
|
||||
|
||||
> :first-child > [data-component="markdown"]:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="session-turn-permission-parts"] {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-question-parts"] {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-answered-question-parts"] {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
805
opencode/packages/ui/src/components/session-turn.tsx
Normal file
805
opencode/packages/ui/src/components/session-turn.tsx
Normal file
@@ -0,0 +1,805 @@
|
||||
import {
|
||||
AssistantMessage,
|
||||
FilePart,
|
||||
Message as MessageType,
|
||||
Part as PartType,
|
||||
type PermissionRequest,
|
||||
type QuestionRequest,
|
||||
TextPart,
|
||||
ToolPart,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { useData } from "../context"
|
||||
import { type UiI18nKey, type UiI18nParams, useI18n } from "../context/i18n"
|
||||
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { createEffect, createMemo, createSignal, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js"
|
||||
import { Message, Part } from "./message-part"
|
||||
import { Markdown } from "./markdown"
|
||||
import { IconButton } from "./icon-button"
|
||||
import { Card } from "./card"
|
||||
import { Button } from "./button"
|
||||
import { Spinner } from "./spinner"
|
||||
import { Tooltip } from "./tooltip"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { DateTime, DurationUnit, Interval } from "luxon"
|
||||
import { createAutoScroll } from "../hooks"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
|
||||
type Translator = (key: UiI18nKey, params?: UiI18nParams) => string
|
||||
|
||||
function record(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function unwrap(message: string) {
|
||||
const text = message.replace(/^Error:\s*/, "").trim()
|
||||
|
||||
const parse = (value: string) => {
|
||||
try {
|
||||
return JSON.parse(value) as unknown
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const read = (value: string) => {
|
||||
const first = parse(value)
|
||||
if (typeof first !== "string") return first
|
||||
return parse(first.trim())
|
||||
}
|
||||
|
||||
let json = read(text)
|
||||
|
||||
if (json === undefined) {
|
||||
const start = text.indexOf("{")
|
||||
const end = text.lastIndexOf("}")
|
||||
if (start !== -1 && end > start) {
|
||||
json = read(text.slice(start, end + 1))
|
||||
}
|
||||
}
|
||||
|
||||
if (!record(json)) return message
|
||||
|
||||
const err = record(json.error) ? json.error : undefined
|
||||
if (err) {
|
||||
const type = typeof err.type === "string" ? err.type : undefined
|
||||
const msg = typeof err.message === "string" ? err.message : undefined
|
||||
if (type && msg) return `${type}: ${msg}`
|
||||
if (msg) return msg
|
||||
if (type) return type
|
||||
const code = typeof err.code === "string" ? err.code : undefined
|
||||
if (code) return code
|
||||
}
|
||||
|
||||
const msg = typeof json.message === "string" ? json.message : undefined
|
||||
if (msg) return msg
|
||||
|
||||
const reason = typeof json.error === "string" ? json.error : undefined
|
||||
if (reason) return reason
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
function computeStatusFromPart(part: PartType | undefined, t: Translator): string | undefined {
|
||||
if (!part) return undefined
|
||||
|
||||
if (part.type === "tool") {
|
||||
switch (part.tool) {
|
||||
case "task":
|
||||
return t("ui.sessionTurn.status.delegating")
|
||||
case "todowrite":
|
||||
case "todoread":
|
||||
return t("ui.sessionTurn.status.planning")
|
||||
case "read":
|
||||
return t("ui.sessionTurn.status.gatheringContext")
|
||||
case "list":
|
||||
case "grep":
|
||||
case "glob":
|
||||
return t("ui.sessionTurn.status.searchingCodebase")
|
||||
case "webfetch":
|
||||
return t("ui.sessionTurn.status.searchingWeb")
|
||||
case "edit":
|
||||
case "write":
|
||||
return t("ui.sessionTurn.status.makingEdits")
|
||||
case "bash":
|
||||
return t("ui.sessionTurn.status.runningCommands")
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
const text = part.text ?? ""
|
||||
const match = text.trimStart().match(/^\*\*(.+?)\*\*/)
|
||||
if (match) return t("ui.sessionTurn.status.thinkingWithTopic", { topic: match[1].trim() })
|
||||
return t("ui.sessionTurn.status.thinking")
|
||||
}
|
||||
if (part.type === "text") {
|
||||
return t("ui.sessionTurn.status.gatheringThoughts")
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function same<T>(a: readonly T[], b: readonly T[]) {
|
||||
if (a === b) return true
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((x, i) => x === b[i])
|
||||
}
|
||||
|
||||
function isAttachment(part: PartType | undefined) {
|
||||
if (part?.type !== "file") return false
|
||||
const mime = (part as FilePart).mime ?? ""
|
||||
return mime.startsWith("image/") || mime === "application/pdf"
|
||||
}
|
||||
|
||||
function AssistantMessageItem(props: {
|
||||
message: AssistantMessage
|
||||
responsePartId: string | undefined
|
||||
hideResponsePart: boolean
|
||||
hideReasoning: boolean
|
||||
hidden?: () => readonly { messageID: string; callID: string }[]
|
||||
}) {
|
||||
const data = useData()
|
||||
const emptyParts: PartType[] = []
|
||||
const msgParts = createMemo(() => data.store.part[props.message.id] ?? emptyParts)
|
||||
const lastTextPart = createMemo(() => {
|
||||
const parts = msgParts()
|
||||
for (let i = parts.length - 1; i >= 0; i--) {
|
||||
const part = parts[i]
|
||||
if (part?.type === "text") return part as TextPart
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const filteredParts = createMemo(() => {
|
||||
let parts = msgParts()
|
||||
|
||||
if (props.hideReasoning) {
|
||||
parts = parts.filter((part) => part?.type !== "reasoning")
|
||||
}
|
||||
|
||||
if (props.hideResponsePart) {
|
||||
const responsePartId = props.responsePartId
|
||||
if (responsePartId && responsePartId === lastTextPart()?.id) {
|
||||
parts = parts.filter((part) => part?.id !== responsePartId)
|
||||
}
|
||||
}
|
||||
|
||||
const hidden = props.hidden?.() ?? []
|
||||
if (hidden.length === 0) return parts
|
||||
|
||||
const id = props.message.id
|
||||
return parts.filter((part) => {
|
||||
if (part?.type !== "tool") return true
|
||||
const tool = part as ToolPart
|
||||
return !hidden.some((h) => h.messageID === id && h.callID === tool.callID)
|
||||
})
|
||||
})
|
||||
|
||||
return <Message message={props.message} parts={filteredParts()} />
|
||||
}
|
||||
|
||||
export function SessionTurn(
|
||||
props: ParentProps<{
|
||||
sessionID: string
|
||||
sessionTitle?: string
|
||||
messageID: string
|
||||
lastUserMessageID?: string
|
||||
stepsExpanded?: boolean
|
||||
onStepsExpandedToggle?: () => void
|
||||
onUserInteracted?: () => void
|
||||
classes?: {
|
||||
root?: string
|
||||
content?: string
|
||||
container?: string
|
||||
}
|
||||
}>,
|
||||
) {
|
||||
const i18n = useI18n()
|
||||
const data = useData()
|
||||
|
||||
const emptyMessages: MessageType[] = []
|
||||
const emptyParts: PartType[] = []
|
||||
const emptyFiles: FilePart[] = []
|
||||
const emptyAssistant: AssistantMessage[] = []
|
||||
const emptyPermissions: PermissionRequest[] = []
|
||||
const emptyQuestions: QuestionRequest[] = []
|
||||
const emptyQuestionParts: { part: ToolPart; message: AssistantMessage }[] = []
|
||||
const idle = { type: "idle" as const }
|
||||
|
||||
const allMessages = createMemo(() => data.store.message[props.sessionID] ?? emptyMessages)
|
||||
|
||||
const messageIndex = createMemo(() => {
|
||||
const messages = allMessages() ?? emptyMessages
|
||||
const result = Binary.search(messages, props.messageID, (m) => m.id)
|
||||
|
||||
const index = result.found ? result.index : messages.findIndex((m) => m.id === props.messageID)
|
||||
if (index < 0) return -1
|
||||
|
||||
const msg = messages[index]
|
||||
if (!msg || msg.role !== "user") return -1
|
||||
|
||||
return index
|
||||
})
|
||||
|
||||
const message = createMemo(() => {
|
||||
const index = messageIndex()
|
||||
if (index < 0) return undefined
|
||||
|
||||
const messages = allMessages() ?? emptyMessages
|
||||
const msg = messages[index]
|
||||
if (!msg || msg.role !== "user") return undefined
|
||||
|
||||
return msg
|
||||
})
|
||||
|
||||
const lastUserMessageID = createMemo(() => {
|
||||
if (props.lastUserMessageID) return props.lastUserMessageID
|
||||
|
||||
const messages = allMessages() ?? emptyMessages
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i]
|
||||
if (msg?.role === "user") return msg.id
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const isLastUserMessage = createMemo(() => props.messageID === lastUserMessageID())
|
||||
|
||||
const parts = createMemo(() => {
|
||||
const msg = message()
|
||||
if (!msg) return emptyParts
|
||||
return data.store.part[msg.id] ?? emptyParts
|
||||
})
|
||||
|
||||
const attachmentParts = createMemo(() => {
|
||||
const msgParts = parts()
|
||||
if (msgParts.length === 0) return emptyFiles
|
||||
return msgParts.filter((part) => isAttachment(part)) as FilePart[]
|
||||
})
|
||||
|
||||
const stickyParts = createMemo(() => {
|
||||
const msgParts = parts()
|
||||
if (msgParts.length === 0) return emptyParts
|
||||
if (attachmentParts().length === 0) return msgParts
|
||||
return msgParts.filter((part) => !isAttachment(part))
|
||||
})
|
||||
|
||||
const assistantMessages = createMemo(
|
||||
() => {
|
||||
const msg = message()
|
||||
if (!msg) return emptyAssistant
|
||||
|
||||
const messages = allMessages() ?? emptyMessages
|
||||
const index = messageIndex()
|
||||
if (index < 0) return emptyAssistant
|
||||
|
||||
const result: AssistantMessage[] = []
|
||||
for (let i = index + 1; i < messages.length; i++) {
|
||||
const item = messages[i]
|
||||
if (!item) continue
|
||||
if (item.role === "user") break
|
||||
if (item.role === "assistant" && item.parentID === msg.id) result.push(item as AssistantMessage)
|
||||
}
|
||||
return result
|
||||
},
|
||||
emptyAssistant,
|
||||
{ equals: same },
|
||||
)
|
||||
|
||||
const lastAssistantMessage = createMemo(() => assistantMessages().at(-1))
|
||||
|
||||
const error = createMemo(() => assistantMessages().find((m) => m.error)?.error)
|
||||
const errorText = createMemo(() => {
|
||||
const msg = error()?.data?.message
|
||||
if (typeof msg === "string") return unwrap(msg)
|
||||
if (msg === undefined || msg === null) return ""
|
||||
return unwrap(String(msg))
|
||||
})
|
||||
|
||||
const lastTextPart = createMemo(() => {
|
||||
const msgs = assistantMessages()
|
||||
for (let mi = msgs.length - 1; mi >= 0; mi--) {
|
||||
const msgParts = data.store.part[msgs[mi].id] ?? emptyParts
|
||||
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
|
||||
const part = msgParts[pi]
|
||||
if (part?.type === "text") return part as TextPart
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const hasSteps = createMemo(() => {
|
||||
for (const m of assistantMessages()) {
|
||||
const msgParts = data.store.part[m.id]
|
||||
if (!msgParts) continue
|
||||
for (const p of msgParts) {
|
||||
if (p?.type === "tool") return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const permissions = createMemo(() => data.store.permission?.[props.sessionID] ?? emptyPermissions)
|
||||
const nextPermission = createMemo(() => permissions()[0])
|
||||
|
||||
const questions = createMemo(() => data.store.question?.[props.sessionID] ?? emptyQuestions)
|
||||
const nextQuestion = createMemo(() => questions()[0])
|
||||
|
||||
const hidden = createMemo(() => {
|
||||
const out: { messageID: string; callID: string }[] = []
|
||||
const perm = nextPermission()
|
||||
if (perm?.tool) out.push(perm.tool)
|
||||
const question = nextQuestion()
|
||||
if (question?.tool) out.push(question.tool)
|
||||
return out
|
||||
})
|
||||
|
||||
const answeredQuestionParts = createMemo(() => {
|
||||
if (props.stepsExpanded) return emptyQuestionParts
|
||||
if (questions().length > 0) return emptyQuestionParts
|
||||
|
||||
const result: { part: ToolPart; message: AssistantMessage }[] = []
|
||||
|
||||
for (const msg of assistantMessages()) {
|
||||
const parts = data.store.part[msg.id] ?? emptyParts
|
||||
for (const part of parts) {
|
||||
if (part?.type !== "tool") continue
|
||||
const tool = part as ToolPart
|
||||
if (tool.tool !== "question") continue
|
||||
// @ts-expect-error metadata may not exist on all tool states
|
||||
const answers = tool.state?.metadata?.answers
|
||||
if (answers && answers.length > 0) {
|
||||
result.push({ part: tool, message: msg })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const shellModePart = createMemo(() => {
|
||||
const p = parts()
|
||||
if (p.length === 0) return
|
||||
if (!p.every((part) => part?.type === "text" && part?.synthetic)) return
|
||||
|
||||
const msgs = assistantMessages()
|
||||
if (msgs.length !== 1) return
|
||||
|
||||
const msgParts = data.store.part[msgs[0].id] ?? emptyParts
|
||||
if (msgParts.length !== 1) return
|
||||
|
||||
const assistantPart = msgParts[0]
|
||||
if (assistantPart?.type === "tool" && assistantPart.tool === "bash") return assistantPart
|
||||
})
|
||||
|
||||
const isShellMode = createMemo(() => !!shellModePart())
|
||||
|
||||
const rawStatus = createMemo(() => {
|
||||
const msgs = assistantMessages()
|
||||
let last: PartType | undefined
|
||||
let currentTask: ToolPart | undefined
|
||||
|
||||
for (let mi = msgs.length - 1; mi >= 0; mi--) {
|
||||
const msgParts = data.store.part[msgs[mi].id] ?? emptyParts
|
||||
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
|
||||
const part = msgParts[pi]
|
||||
if (!part) continue
|
||||
if (!last) last = part
|
||||
|
||||
if (
|
||||
part.type === "tool" &&
|
||||
part.tool === "task" &&
|
||||
part.state &&
|
||||
"metadata" in part.state &&
|
||||
part.state.metadata?.sessionId &&
|
||||
part.state.status === "running"
|
||||
) {
|
||||
currentTask = part as ToolPart
|
||||
break
|
||||
}
|
||||
}
|
||||
if (currentTask) break
|
||||
}
|
||||
|
||||
const taskSessionId =
|
||||
currentTask?.state && "metadata" in currentTask.state
|
||||
? (currentTask.state.metadata?.sessionId as string | undefined)
|
||||
: undefined
|
||||
|
||||
if (taskSessionId) {
|
||||
const taskMessages = data.store.message[taskSessionId] ?? emptyMessages
|
||||
for (let mi = taskMessages.length - 1; mi >= 0; mi--) {
|
||||
const msg = taskMessages[mi]
|
||||
if (!msg || msg.role !== "assistant") continue
|
||||
|
||||
const msgParts = data.store.part[msg.id] ?? emptyParts
|
||||
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
|
||||
const part = msgParts[pi]
|
||||
if (part) return computeStatusFromPart(part, i18n.t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return computeStatusFromPart(last, i18n.t)
|
||||
})
|
||||
|
||||
const status = createMemo(() => data.store.session_status[props.sessionID] ?? idle)
|
||||
const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
|
||||
const retry = createMemo(() => {
|
||||
// session_status is session-scoped; only show retry on the active (last) turn
|
||||
if (!isLastUserMessage()) return
|
||||
const s = status()
|
||||
if (s.type !== "retry") return
|
||||
return s
|
||||
})
|
||||
|
||||
const response = createMemo(() => lastTextPart()?.text)
|
||||
const responsePartId = createMemo(() => lastTextPart()?.id)
|
||||
const hasDiffs = createMemo(() => (message()?.summary?.diffs?.length ?? 0) > 0)
|
||||
const hideResponsePart = createMemo(() => !working() && !!responsePartId())
|
||||
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
const content = response() ?? ""
|
||||
if (!content) return
|
||||
await navigator.clipboard.writeText(content)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
const [rootRef, setRootRef] = createSignal<HTMLDivElement | undefined>()
|
||||
const [stickyRef, setStickyRef] = createSignal<HTMLDivElement | undefined>()
|
||||
|
||||
const updateStickyHeight = (height: number) => {
|
||||
const root = rootRef()
|
||||
if (!root) return
|
||||
const next = Math.ceil(height)
|
||||
root.style.setProperty("--session-turn-sticky-height", `${next}px`)
|
||||
}
|
||||
|
||||
function duration() {
|
||||
const msg = message()
|
||||
if (!msg) return ""
|
||||
const completed = lastAssistantMessage()?.time.completed
|
||||
const from = DateTime.fromMillis(msg.time.created)
|
||||
const to = completed ? DateTime.fromMillis(completed) : DateTime.now()
|
||||
const interval = Interval.fromDateTimes(from, to)
|
||||
const unit: DurationUnit[] = interval.length("seconds") > 60 ? ["minutes", "seconds"] : ["seconds"]
|
||||
|
||||
const locale = i18n.locale()
|
||||
const human = interval.toDuration(unit).normalize().reconfigure({ locale }).toHuman({
|
||||
notation: "compact",
|
||||
unitDisplay: "narrow",
|
||||
compactDisplay: "short",
|
||||
showZeros: false,
|
||||
})
|
||||
return locale.startsWith("zh") ? human.replaceAll("、", "") : human
|
||||
}
|
||||
|
||||
const autoScroll = createAutoScroll({
|
||||
working,
|
||||
onUserInteracted: props.onUserInteracted,
|
||||
overflowAnchor: "auto",
|
||||
})
|
||||
|
||||
createResizeObserver(
|
||||
() => stickyRef(),
|
||||
({ height }) => {
|
||||
updateStickyHeight(height)
|
||||
},
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const root = rootRef()
|
||||
if (!root) return
|
||||
const sticky = stickyRef()
|
||||
if (!sticky) {
|
||||
root.style.setProperty("--session-turn-sticky-height", "0px")
|
||||
return
|
||||
}
|
||||
updateStickyHeight(sticky.getBoundingClientRect().height)
|
||||
})
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
retrySeconds: 0,
|
||||
status: rawStatus(),
|
||||
duration: duration(),
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const r = retry()
|
||||
if (!r) {
|
||||
setStore("retrySeconds", 0)
|
||||
return
|
||||
}
|
||||
const updateSeconds = () => {
|
||||
const next = r.next
|
||||
if (next) setStore("retrySeconds", Math.max(0, Math.round((next - Date.now()) / 1000)))
|
||||
}
|
||||
updateSeconds()
|
||||
const timer = setInterval(updateSeconds, 1000)
|
||||
onCleanup(() => clearInterval(timer))
|
||||
})
|
||||
|
||||
let retryLog = ""
|
||||
createEffect(() => {
|
||||
const r = retry()
|
||||
if (!r) return
|
||||
const key = `${r.attempt}:${r.next}:${r.message}`
|
||||
if (key === retryLog) return
|
||||
retryLog = key
|
||||
console.warn("[session-turn] retry", {
|
||||
sessionID: props.sessionID,
|
||||
messageID: props.messageID,
|
||||
attempt: r.attempt,
|
||||
next: r.next,
|
||||
raw: r.message,
|
||||
parsed: unwrap(r.message),
|
||||
})
|
||||
})
|
||||
|
||||
let errorLog = ""
|
||||
createEffect(() => {
|
||||
const value = error()?.data?.message
|
||||
if (value === undefined || value === null) return
|
||||
const raw = typeof value === "string" ? value : String(value)
|
||||
if (!raw) return
|
||||
if (raw === errorLog) return
|
||||
errorLog = raw
|
||||
console.warn("[session-turn] assistant-error", {
|
||||
sessionID: props.sessionID,
|
||||
messageID: props.messageID,
|
||||
raw,
|
||||
parsed: unwrap(raw),
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const update = () => {
|
||||
setStore("duration", duration())
|
||||
}
|
||||
|
||||
update()
|
||||
|
||||
// Only keep ticking while the active (in-progress) turn is running.
|
||||
if (!working()) return
|
||||
|
||||
const timer = setInterval(update, 1000)
|
||||
onCleanup(() => clearInterval(timer))
|
||||
})
|
||||
|
||||
let lastStatusChange = Date.now()
|
||||
let statusTimeout: number | undefined
|
||||
createEffect(() => {
|
||||
const newStatus = rawStatus()
|
||||
if (newStatus === store.status || !newStatus) return
|
||||
|
||||
const timeSinceLastChange = Date.now() - lastStatusChange
|
||||
if (timeSinceLastChange >= 2500) {
|
||||
setStore("status", newStatus)
|
||||
lastStatusChange = Date.now()
|
||||
if (statusTimeout) {
|
||||
clearTimeout(statusTimeout)
|
||||
statusTimeout = undefined
|
||||
}
|
||||
} else {
|
||||
if (statusTimeout) clearTimeout(statusTimeout)
|
||||
statusTimeout = setTimeout(() => {
|
||||
setStore("status", rawStatus())
|
||||
lastStatusChange = Date.now()
|
||||
statusTimeout = undefined
|
||||
}, 2500 - timeSinceLastChange) as unknown as number
|
||||
}
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (!statusTimeout) return
|
||||
clearTimeout(statusTimeout)
|
||||
})
|
||||
|
||||
return (
|
||||
<div data-component="session-turn" class={props.classes?.root} ref={setRootRef}>
|
||||
<div
|
||||
ref={autoScroll.scrollRef}
|
||||
onScroll={autoScroll.handleScroll}
|
||||
data-slot="session-turn-content"
|
||||
class={props.classes?.content}
|
||||
>
|
||||
<div onClick={autoScroll.handleInteraction}>
|
||||
<Show when={message()}>
|
||||
{(msg) => (
|
||||
<div
|
||||
ref={autoScroll.contentRef}
|
||||
data-message={msg().id}
|
||||
data-slot="session-turn-message-container"
|
||||
class={props.classes?.container}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={isShellMode()}>
|
||||
<Part part={shellModePart()!} message={msg()} defaultOpen />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Show when={attachmentParts().length > 0}>
|
||||
<div data-slot="session-turn-attachments" aria-live="off">
|
||||
<Message message={msg()} parts={attachmentParts()} />
|
||||
</div>
|
||||
</Show>
|
||||
<div data-slot="session-turn-sticky" ref={setStickyRef}>
|
||||
{/* User Message */}
|
||||
<div data-slot="session-turn-message-content" aria-live="off">
|
||||
<Message message={msg()} parts={stickyParts()} />
|
||||
</div>
|
||||
|
||||
{/* Trigger (sticky) */}
|
||||
<Show when={working() || hasSteps()}>
|
||||
<div data-slot="session-turn-response-trigger">
|
||||
<Button
|
||||
data-expandable={assistantMessages().length > 0}
|
||||
data-slot="session-turn-collapsible-trigger-content"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={props.onStepsExpandedToggle ?? (() => {})}
|
||||
aria-expanded={props.stepsExpanded}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={working()}>
|
||||
<Spinner />
|
||||
</Match>
|
||||
<Match when={!props.stepsExpanded}>
|
||||
<svg
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-slot="session-turn-trigger-icon"
|
||||
>
|
||||
<path
|
||||
d="M8.125 1.875H1.875L5 8.125L8.125 1.875Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Match>
|
||||
<Match when={props.stepsExpanded}>
|
||||
<svg
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-icon-base"
|
||||
>
|
||||
<path
|
||||
d="M8.125 8.125H1.875L5 1.875L8.125 8.125Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Switch>
|
||||
<Match when={retry()}>
|
||||
<span data-slot="session-turn-retry-message">
|
||||
{(() => {
|
||||
const r = retry()
|
||||
if (!r) return ""
|
||||
const msg = unwrap(r.message)
|
||||
return msg.length > 60 ? msg.slice(0, 60) + "..." : msg
|
||||
})()}
|
||||
</span>
|
||||
<span data-slot="session-turn-retry-seconds">
|
||||
· {i18n.t("ui.sessionTurn.retry.retrying")}
|
||||
{store.retrySeconds > 0
|
||||
? " " + i18n.t("ui.sessionTurn.retry.inSeconds", { seconds: store.retrySeconds })
|
||||
: ""}
|
||||
</span>
|
||||
<span data-slot="session-turn-retry-attempt">(#{retry()?.attempt})</span>
|
||||
</Match>
|
||||
<Match when={working()}>
|
||||
<span data-slot="session-turn-status-text">
|
||||
{store.status ?? i18n.t("ui.sessionTurn.status.consideringNextSteps")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={props.stepsExpanded}>
|
||||
<span data-slot="session-turn-status-text">{i18n.t("ui.sessionTurn.steps.hide")}</span>
|
||||
</Match>
|
||||
<Match when={!props.stepsExpanded}>
|
||||
<span data-slot="session-turn-status-text">{i18n.t("ui.sessionTurn.steps.show")}</span>
|
||||
</Match>
|
||||
</Switch>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span aria-live="off">{store.duration}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
{/* Response */}
|
||||
<Show when={props.stepsExpanded && assistantMessages().length > 0}>
|
||||
<div data-slot="session-turn-collapsible-content-inner" aria-hidden={working()}>
|
||||
<For each={assistantMessages()}>
|
||||
{(assistantMessage) => (
|
||||
<AssistantMessageItem
|
||||
message={assistantMessage}
|
||||
responsePartId={responsePartId()}
|
||||
hideResponsePart={hideResponsePart()}
|
||||
hideReasoning={!working()}
|
||||
hidden={hidden}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<Show when={error()}>
|
||||
<Card variant="error" class="error-card">
|
||||
{errorText()}
|
||||
</Card>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!props.stepsExpanded && answeredQuestionParts().length > 0}>
|
||||
<div data-slot="session-turn-answered-question-parts">
|
||||
<For each={answeredQuestionParts()}>
|
||||
{({ part, message }) => <Part part={part} message={message} />}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
{/* Response */}
|
||||
<div class="sr-only" aria-live="polite">
|
||||
{!working() && response() ? response() : ""}
|
||||
</div>
|
||||
<Show when={!working() && response()}>
|
||||
<div data-slot="session-turn-summary-section">
|
||||
<div data-slot="session-turn-summary-header">
|
||||
<div data-slot="session-turn-summary-title-row">
|
||||
<h2 data-slot="session-turn-summary-title">{i18n.t("ui.sessionTurn.summary.response")}</h2>
|
||||
<Show when={response()}>
|
||||
<div data-slot="session-turn-response-copy-wrapper">
|
||||
<Tooltip
|
||||
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
|
||||
placement="top"
|
||||
gutter={8}
|
||||
>
|
||||
<IconButton
|
||||
icon={copied() ? "check" : "copy"}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleCopy()
|
||||
}}
|
||||
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div data-slot="session-turn-response">
|
||||
<Markdown
|
||||
data-slot="session-turn-markdown"
|
||||
data-diffs={hasDiffs()}
|
||||
text={response() ?? ""}
|
||||
cacheKey={responsePartId()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={error() && !props.stepsExpanded}>
|
||||
<Card variant="error" class="error-card">
|
||||
{errorText()}
|
||||
</Card>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
6
opencode/packages/ui/src/components/spinner.css
Normal file
6
opencode/packages/ui/src/components/spinner.css
Normal file
@@ -0,0 +1,6 @@
|
||||
[data-component="spinner"] {
|
||||
color: inherit;
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
51
opencode/packages/ui/src/components/spinner.tsx
Normal file
51
opencode/packages/ui/src/components/spinner.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ComponentProps, For } from "solid-js"
|
||||
|
||||
const outerIndices = new Set([1, 2, 4, 7, 8, 11, 13, 14])
|
||||
const cornerIndices = new Set([0, 3, 12, 15])
|
||||
const squares = Array.from({ length: 16 }, (_, i) => ({
|
||||
id: i,
|
||||
x: (i % 4) * 4,
|
||||
y: Math.floor(i / 4) * 4,
|
||||
delay: Math.random() * 1.5,
|
||||
duration: 1 + Math.random() * 1,
|
||||
outer: outerIndices.has(i),
|
||||
corner: cornerIndices.has(i),
|
||||
}))
|
||||
|
||||
export function Spinner(props: {
|
||||
class?: string
|
||||
classList?: ComponentProps<"div">["classList"]
|
||||
style?: ComponentProps<"div">["style"]
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 15 15"
|
||||
data-component="spinner"
|
||||
classList={{
|
||||
...(props.classList ?? {}),
|
||||
[props.class ?? ""]: !!props.class,
|
||||
}}
|
||||
fill="currentColor"
|
||||
>
|
||||
<For each={squares}>
|
||||
{(square) => (
|
||||
<rect
|
||||
x={square.x}
|
||||
y={square.y}
|
||||
width="3"
|
||||
height="3"
|
||||
rx="1"
|
||||
style={{
|
||||
opacity: square.corner ? 0 : undefined,
|
||||
animation: square.corner
|
||||
? undefined
|
||||
: `${square.outer ? "pulse-opacity-dim" : "pulse-opacity"} ${square.duration}s ease-in-out infinite`,
|
||||
"animation-delay": square.corner ? undefined : `${square.delay}s`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[data-component="sticky-accordion-header"] {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
[data-component="sticky-accordion-header"][data-expanded],
|
||||
[data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"] {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
[data-component="sticky-accordion-header"][data-expanded]::before,
|
||||
[data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before {
|
||||
content: "";
|
||||
z-index: -10;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--background-stronger);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Accordion } from "./accordion"
|
||||
import { ParentProps } from "solid-js"
|
||||
|
||||
export function StickyAccordionHeader(
|
||||
props: ParentProps<{ class?: string; classList?: Record<string, boolean | undefined> }>,
|
||||
) {
|
||||
return (
|
||||
<Accordion.Header
|
||||
data-component="sticky-accordion-header"
|
||||
classList={{
|
||||
...(props.classList ?? {}),
|
||||
[props.class ?? ""]: !!props.class,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Accordion.Header>
|
||||
)
|
||||
}
|
||||
132
opencode/packages/ui/src/components/switch.css
Normal file
132
opencode/packages/ui/src/components/switch.css
Normal file
@@ -0,0 +1,132 @@
|
||||
[data-component="switch"] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: default;
|
||||
|
||||
[data-slot="switch-input"] {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="switch-control"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 28px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--border-weak-base);
|
||||
background: var(--surface-base);
|
||||
transition:
|
||||
background-color 150ms,
|
||||
border-color 150ms;
|
||||
}
|
||||
|
||||
[data-slot="switch-thumb"] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
box-sizing: content-box;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--border-base);
|
||||
background: var(--icon-invert-base);
|
||||
|
||||
/* shadows/shadow-xs */
|
||||
box-shadow:
|
||||
0 1px 2px -1px rgba(19, 16, 16, 0.04),
|
||||
0 1px 2px 0 rgba(19, 16, 16, 0.06),
|
||||
0 1px 3px 0 rgba(19, 16, 16, 0.08);
|
||||
|
||||
transform: translateX(-1px);
|
||||
transition:
|
||||
transform 150ms,
|
||||
background-color 150ms;
|
||||
}
|
||||
|
||||
[data-slot="switch-label"] {
|
||||
user-select: none;
|
||||
color: var(--text-base);
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
[data-slot="switch-description"] {
|
||||
color: var(--text-base);
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 12px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-normal);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
[data-slot="switch-error"] {
|
||||
color: var(--text-error);
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 12px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-normal);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
&:hover:not([data-disabled], [data-readonly]) [data-slot="switch-control"] {
|
||||
border-color: var(--border-hover);
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
&:focus-within:not([data-readonly]) [data-slot="switch-control"] {
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: 0 0 0 2px var(--surface-focus);
|
||||
}
|
||||
|
||||
&[data-checked] [data-slot="switch-control"] {
|
||||
box-sizing: border-box;
|
||||
border-color: var(--icon-strong-base);
|
||||
background-color: var(--icon-strong-base);
|
||||
}
|
||||
|
||||
&[data-checked] [data-slot="switch-thumb"] {
|
||||
border: none;
|
||||
transform: translateX(12px);
|
||||
background-color: var(--icon-invert-base);
|
||||
}
|
||||
|
||||
&[data-checked]:hover:not([data-disabled], [data-readonly]) [data-slot="switch-control"] {
|
||||
border-color: var(--border-hover);
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&[data-disabled] [data-slot="switch-control"] {
|
||||
border-color: var(--border-disabled);
|
||||
background-color: var(--surface-disabled);
|
||||
}
|
||||
|
||||
&[data-disabled] [data-slot="switch-thumb"] {
|
||||
background-color: var(--icon-disabled);
|
||||
}
|
||||
|
||||
&[data-invalid] [data-slot="switch-control"] {
|
||||
border-color: var(--border-error);
|
||||
}
|
||||
|
||||
&[data-readonly] {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
29
opencode/packages/ui/src/components/switch.tsx
Normal file
29
opencode/packages/ui/src/components/switch.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Switch as Kobalte } from "@kobalte/core/switch"
|
||||
import { Show, splitProps } from "solid-js"
|
||||
import type { ComponentProps, ParentProps } from "solid-js"
|
||||
|
||||
export interface SwitchProps extends ParentProps<ComponentProps<typeof Kobalte>> {
|
||||
hideLabel?: boolean
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function Switch(props: SwitchProps) {
|
||||
const [local, others] = splitProps(props, ["children", "class", "hideLabel", "description"])
|
||||
return (
|
||||
<Kobalte {...others} data-component="switch">
|
||||
<Kobalte.Input data-slot="switch-input" />
|
||||
<Show when={local.children}>
|
||||
<Kobalte.Label data-slot="switch-label" classList={{ "sr-only": local.hideLabel }}>
|
||||
{local.children}
|
||||
</Kobalte.Label>
|
||||
</Show>
|
||||
<Show when={local.description}>
|
||||
<Kobalte.Description data-slot="switch-description">{local.description}</Kobalte.Description>
|
||||
</Show>
|
||||
<Kobalte.ErrorMessage data-slot="switch-error" />
|
||||
<Kobalte.Control data-slot="switch-control">
|
||||
<Kobalte.Thumb data-slot="switch-thumb" />
|
||||
</Kobalte.Control>
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
451
opencode/packages/ui/src/components/tabs.css
Normal file
451
opencode/packages/ui/src/components/tabs.css
Normal file
@@ -0,0 +1,451 @@
|
||||
[data-component="tabs"] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--background-stronger);
|
||||
overflow: clip;
|
||||
|
||||
[data-slot="tabs-list"] {
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
|
||||
/* Hide scrollbar */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* After element to fill remaining space */
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
border-bottom: 1px solid var(--border-weak-base);
|
||||
background-color: var(--background-base);
|
||||
}
|
||||
|
||||
&:empty::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger-wrapper"] {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--text-base);
|
||||
|
||||
/* text-14-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 142.857% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
max-width: 280px;
|
||||
border-bottom: 1px solid var(--border-weak-base);
|
||||
border-right: 1px solid var(--border-weak-base);
|
||||
background-color: var(--background-base);
|
||||
|
||||
[data-slot="tabs-trigger"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 24px 14px 12px;
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger-close-button"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-component="icon-button"] {
|
||||
margin: -0.25rem;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
color: var(--text-weaker);
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
&:has([data-hidden]) {
|
||||
[data-slot="tabs-trigger-close-button"] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
[data-slot="tabs-trigger-close-button"] {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:has([data-selected]) {
|
||||
color: var(--text-strong);
|
||||
background-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
[data-slot="tabs-trigger-close-button"] {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:hover:not(:disabled):not([data-selected]) {
|
||||
color: var(--text-strong);
|
||||
}
|
||||
&:has([data-slot="tabs-trigger-close-button"]) {
|
||||
padding-right: 12px;
|
||||
|
||||
[data-slot="tabs-trigger"] {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-content"] {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
|
||||
/* Hide scrollbar */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant="alt"] {
|
||||
[data-slot="tabs-list"] {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid var(--border-weak-base);
|
||||
background-color: transparent;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
&:empty::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger-wrapper"] {
|
||||
border: none;
|
||||
color: var(--text-base);
|
||||
background-color: transparent;
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: transparent;
|
||||
gap: 4px;
|
||||
|
||||
/* text-14-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-x-large); /* 171.429% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
[data-slot="tabs-trigger"] {
|
||||
height: 100%;
|
||||
padding: 4px;
|
||||
background-color: transparent;
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger-close-button"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-component="icon-button"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:has([data-selected]) {
|
||||
color: var(--text-strong);
|
||||
background-color: transparent;
|
||||
border-bottom-color: var(--icon-strong-base);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not([data-selected]) {
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
&:has([data-slot="tabs-trigger-close-button"]) {
|
||||
padding-right: 0;
|
||||
[data-slot="tabs-trigger"] {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* [data-slot="tabs-content"] { */
|
||||
/* } */
|
||||
}
|
||||
|
||||
&[data-variant="pill"][data-orientation="horizontal"] {
|
||||
background-color: transparent;
|
||||
|
||||
[data-slot="tabs-list"] {
|
||||
height: auto;
|
||||
padding: 6px 0;
|
||||
gap: 4px;
|
||||
background-color: var(--background-base);
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger-wrapper"] {
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background-color: transparent;
|
||||
gap: 0;
|
||||
|
||||
/* text-13-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
[data-slot="tabs-trigger"] {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0 12px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
&:has([data-selected]) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
color: var(--text-strong);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant="pill"][data-orientation="horizontal"][data-scope="filetree"] {
|
||||
[data-slot="tabs-list"] {
|
||||
height: 48px;
|
||||
padding-inline: 12px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger-wrapper"] {
|
||||
height: 26px;
|
||||
border-radius: 6px;
|
||||
color: var(--text-weak);
|
||||
|
||||
&:not(:has([data-selected])):hover:not(:disabled) {
|
||||
color: var(--text-base);
|
||||
}
|
||||
|
||||
&:has([data-selected]) {
|
||||
color: var(--text-strong);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-orientation="vertical"] {
|
||||
flex-direction: row;
|
||||
|
||||
[data-slot="tabs-list"] {
|
||||
flex-direction: column;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
background-color: var(--background-base);
|
||||
border-right: 1px solid var(--border-weak-base);
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger-wrapper"] {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: transparent;
|
||||
|
||||
[data-slot="tabs-trigger"] {
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
gap: 8px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
|
||||
&:has([data-selected]) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-content"] {
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&[data-variant="alt"] {
|
||||
[data-slot="tabs-list"] {
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
border: none;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger-wrapper"] {
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
|
||||
[data-slot="tabs-trigger"] {
|
||||
border: none;
|
||||
padding: 0 8px;
|
||||
gap: 8px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
|
||||
&:has([data-selected]) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant="settings"] {
|
||||
[data-slot="tabs-list"] {
|
||||
width: 150px;
|
||||
min-width: 150px;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
}
|
||||
padding: 12px;
|
||||
gap: 0;
|
||||
background-color: var(--background-base);
|
||||
border-right: 1px solid var(--border-weak-base);
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-section-title"] {
|
||||
width: 100%;
|
||||
padding: 0 0 0 4px;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger-wrapper"] {
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
|
||||
/* text-14-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
|
||||
[data-slot="tabs-trigger"] {
|
||||
border: none;
|
||||
padding: 0 8px;
|
||||
gap: 12px;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
[data-component="icon"] {
|
||||
color: var(--icon-base);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-hover);
|
||||
}
|
||||
|
||||
&:has([data-selected]) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
color: var(--text-strong);
|
||||
|
||||
[data-component="icon"] {
|
||||
color: var(--icon-strong-base);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-content"] {
|
||||
background-color: var(--surface-raised-stronger-non-alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
118
opencode/packages/ui/src/components/tabs.tsx
Normal file
118
opencode/packages/ui/src/components/tabs.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Tabs as Kobalte } from "@kobalte/core/tabs"
|
||||
import { Show, splitProps, type JSX } from "solid-js"
|
||||
import type { ComponentProps, ParentProps, Component } from "solid-js"
|
||||
|
||||
export interface TabsProps extends ComponentProps<typeof Kobalte> {
|
||||
variant?: "normal" | "alt" | "pill" | "settings"
|
||||
orientation?: "horizontal" | "vertical"
|
||||
}
|
||||
export interface TabsListProps extends ComponentProps<typeof Kobalte.List> {}
|
||||
export interface TabsTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {
|
||||
classes?: {
|
||||
button?: string
|
||||
}
|
||||
hideCloseButton?: boolean
|
||||
closeButton?: JSX.Element
|
||||
onMiddleClick?: () => void
|
||||
}
|
||||
export interface TabsContentProps extends ComponentProps<typeof Kobalte.Content> {}
|
||||
|
||||
function TabsRoot(props: TabsProps) {
|
||||
const [split, rest] = splitProps(props, ["class", "classList", "variant", "orientation"])
|
||||
return (
|
||||
<Kobalte
|
||||
{...rest}
|
||||
orientation={split.orientation}
|
||||
data-component="tabs"
|
||||
data-variant={split.variant || "normal"}
|
||||
data-orientation={split.orientation || "horizontal"}
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsList(props: TabsListProps) {
|
||||
const [split, rest] = splitProps(props, ["class", "classList"])
|
||||
return (
|
||||
<Kobalte.List
|
||||
{...rest}
|
||||
data-slot="tabs-list"
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger(props: ParentProps<TabsTriggerProps>) {
|
||||
const [split, rest] = splitProps(props, [
|
||||
"class",
|
||||
"classList",
|
||||
"classes",
|
||||
"children",
|
||||
"closeButton",
|
||||
"hideCloseButton",
|
||||
"onMiddleClick",
|
||||
])
|
||||
return (
|
||||
<div
|
||||
data-slot="tabs-trigger-wrapper"
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
onAuxClick={(e) => {
|
||||
if (e.button === 1 && split.onMiddleClick) {
|
||||
e.preventDefault()
|
||||
split.onMiddleClick()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Kobalte.Trigger
|
||||
{...rest}
|
||||
data-slot="tabs-trigger"
|
||||
classList={{ [split.classes?.button ?? ""]: split.classes?.button }}
|
||||
>
|
||||
{split.children}
|
||||
</Kobalte.Trigger>
|
||||
<Show when={split.closeButton}>
|
||||
{(closeButton) => (
|
||||
<div data-slot="tabs-trigger-close-button" data-hidden={split.hideCloseButton}>
|
||||
{closeButton()}
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent(props: ParentProps<TabsContentProps>) {
|
||||
const [split, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<Kobalte.Content
|
||||
{...rest}
|
||||
data-slot="tabs-content"
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
>
|
||||
{split.children}
|
||||
</Kobalte.Content>
|
||||
)
|
||||
}
|
||||
|
||||
const TabsSectionTitle: Component<ParentProps> = (props) => {
|
||||
return <div data-slot="tabs-section-title">{props.children}</div>
|
||||
}
|
||||
|
||||
export const Tabs = Object.assign(TabsRoot, {
|
||||
List: TabsList,
|
||||
Trigger: TabsTrigger,
|
||||
Content: TabsContent,
|
||||
SectionTitle: TabsSectionTitle,
|
||||
})
|
||||
37
opencode/packages/ui/src/components/tag.css
Normal file
37
opencode/packages/ui/src/components/tag.css
Normal file
@@ -0,0 +1,37 @@
|
||||
[data-component="tag"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
|
||||
border-radius: var(--radius-xs);
|
||||
border: 0.5px solid var(--border-weak-base);
|
||||
background: var(--surface-raised-base);
|
||||
color: var(--text-base);
|
||||
|
||||
&[data-size="normal"] {
|
||||
height: 18px;
|
||||
padding: 0 6px;
|
||||
|
||||
/* text-12-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
&[data-size="large"] {
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
|
||||
/* text-14-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 142.857% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
}
|
||||
22
opencode/packages/ui/src/components/tag.tsx
Normal file
22
opencode/packages/ui/src/components/tag.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { type ComponentProps, splitProps } from "solid-js"
|
||||
|
||||
export interface TagProps extends ComponentProps<"span"> {
|
||||
size?: "normal" | "large"
|
||||
}
|
||||
|
||||
export function Tag(props: TagProps) {
|
||||
const [split, rest] = splitProps(props, ["size", "class", "classList", "children"])
|
||||
return (
|
||||
<span
|
||||
{...rest}
|
||||
data-component="tag"
|
||||
data-size={split.size || "normal"}
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
>
|
||||
{split.children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
134
opencode/packages/ui/src/components/text-field.css
Normal file
134
opencode/packages/ui/src/components/text-field.css
Normal file
@@ -0,0 +1,134 @@
|
||||
[data-component="input"] {
|
||||
width: 100%;
|
||||
|
||||
[data-slot="input-input"] {
|
||||
width: 100%;
|
||||
color: var(--text-strong);
|
||||
|
||||
/* text-14-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large); /* 142.857% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-weak);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant="normal"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
[data-slot="input-label"] {
|
||||
color: var(--text-weak);
|
||||
|
||||
/* text-12-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
[data-slot="input-wrapper"] {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding-right: 4px;
|
||||
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
background: var(--input-base);
|
||||
|
||||
&:focus-within:not(:has([data-readonly])) {
|
||||
border-color: transparent;
|
||||
/* border/shadow-xs/select */
|
||||
box-shadow:
|
||||
0 0 0 3px var(--border-weak-selected),
|
||||
0 0 0 1px var(--border-selected),
|
||||
0 1px 2px -1px rgba(19, 16, 16, 0.25),
|
||||
0 1px 2px 0 rgba(19, 16, 16, 0.08),
|
||||
0 1px 3px 0 rgba(19, 16, 16, 0.12);
|
||||
}
|
||||
|
||||
&:has([data-invalid]) {
|
||||
background: var(--surface-critical-weak);
|
||||
border: 1px solid var(--border-critical-selected);
|
||||
}
|
||||
|
||||
&:not(:has([data-slot="input-copy-button"])) {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="input-input"] {
|
||||
color: var(--text-strong);
|
||||
|
||||
display: flex;
|
||||
height: 32px;
|
||||
padding: 2px 12px;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
/* text-14-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large); /* 142.857% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-weak);
|
||||
}
|
||||
}
|
||||
|
||||
textarea[data-slot="input-input"] {
|
||||
height: auto;
|
||||
min-height: 32px;
|
||||
padding: 6px 12px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
[data-slot="input-copy-button"] {
|
||||
flex-shrink: 0;
|
||||
margin-top: 4px;
|
||||
color: var(--icon-base);
|
||||
|
||||
&:hover {
|
||||
color: var(--icon-strong-base);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="input-error"] {
|
||||
color: var(--text-on-critical-base);
|
||||
|
||||
/* text-12-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
120
opencode/packages/ui/src/components/text-field.tsx
Normal file
120
opencode/packages/ui/src/components/text-field.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { TextField as Kobalte } from "@kobalte/core/text-field"
|
||||
import { createSignal, Show, splitProps } from "solid-js"
|
||||
import type { ComponentProps } from "solid-js"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import { IconButton } from "./icon-button"
|
||||
import { Tooltip } from "./tooltip"
|
||||
|
||||
export interface TextFieldProps
|
||||
extends ComponentProps<typeof Kobalte.Input>,
|
||||
Partial<
|
||||
Pick<
|
||||
ComponentProps<typeof Kobalte>,
|
||||
| "name"
|
||||
| "defaultValue"
|
||||
| "value"
|
||||
| "onChange"
|
||||
| "onKeyDown"
|
||||
| "validationState"
|
||||
| "required"
|
||||
| "disabled"
|
||||
| "readOnly"
|
||||
>
|
||||
> {
|
||||
label?: string
|
||||
hideLabel?: boolean
|
||||
description?: string
|
||||
error?: string
|
||||
variant?: "normal" | "ghost"
|
||||
copyable?: boolean
|
||||
multiline?: boolean
|
||||
}
|
||||
|
||||
export function TextField(props: TextFieldProps) {
|
||||
const i18n = useI18n()
|
||||
const [local, others] = splitProps(props, [
|
||||
"name",
|
||||
"defaultValue",
|
||||
"value",
|
||||
"onChange",
|
||||
"onKeyDown",
|
||||
"validationState",
|
||||
"required",
|
||||
"disabled",
|
||||
"readOnly",
|
||||
"class",
|
||||
"label",
|
||||
"hideLabel",
|
||||
"description",
|
||||
"error",
|
||||
"variant",
|
||||
"copyable",
|
||||
"multiline",
|
||||
])
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
|
||||
async function handleCopy() {
|
||||
const value = local.value ?? local.defaultValue ?? ""
|
||||
await navigator.clipboard.writeText(value)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
if (local.copyable) handleCopy()
|
||||
}
|
||||
|
||||
return (
|
||||
<Kobalte
|
||||
data-component="input"
|
||||
data-variant={local.variant || "normal"}
|
||||
name={local.name}
|
||||
defaultValue={local.defaultValue}
|
||||
value={local.value}
|
||||
onChange={local.onChange}
|
||||
onKeyDown={local.onKeyDown}
|
||||
onClick={handleClick}
|
||||
required={local.required}
|
||||
disabled={local.disabled}
|
||||
readOnly={local.readOnly}
|
||||
validationState={local.validationState}
|
||||
>
|
||||
<Show when={local.label}>
|
||||
<Kobalte.Label data-slot="input-label" classList={{ "sr-only": local.hideLabel }}>
|
||||
{local.label}
|
||||
</Kobalte.Label>
|
||||
</Show>
|
||||
<div data-slot="input-wrapper">
|
||||
<Show
|
||||
when={local.multiline}
|
||||
fallback={<Kobalte.Input {...others} data-slot="input-input" class={local.class} />}
|
||||
>
|
||||
<Kobalte.TextArea {...others} autoResize data-slot="input-input" class={local.class} />
|
||||
</Show>
|
||||
<Show when={local.copyable}>
|
||||
<Tooltip
|
||||
value={copied() ? i18n.t("ui.textField.copied") : i18n.t("ui.textField.copyLink")}
|
||||
placement="top"
|
||||
gutter={4}
|
||||
forceOpen={copied()}
|
||||
skipDelayDuration={0}
|
||||
>
|
||||
<IconButton
|
||||
type="button"
|
||||
icon={copied() ? "check" : "link"}
|
||||
variant="ghost"
|
||||
onClick={handleCopy}
|
||||
tabIndex={-1}
|
||||
data-slot="input-copy-button"
|
||||
aria-label={copied() ? i18n.t("ui.textField.copied") : i18n.t("ui.textField.copyLink")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={local.description}>
|
||||
<Kobalte.Description data-slot="input-description">{local.description}</Kobalte.Description>
|
||||
</Show>
|
||||
<Kobalte.ErrorMessage data-slot="input-error">{local.error}</Kobalte.ErrorMessage>
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
218
opencode/packages/ui/src/components/toast.css
Normal file
218
opencode/packages/ui/src/components/toast.css
Normal file
@@ -0,0 +1,218 @@
|
||||
[data-component="toast-region"] {
|
||||
position: fixed;
|
||||
bottom: 48px;
|
||||
right: 32px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: min(400px, calc(100vw - 64px));
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
|
||||
[data-slot="toast-list"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="toast"] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
padding: 16px 20px;
|
||||
pointer-events: auto;
|
||||
transition: all 150ms ease-out;
|
||||
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
background: var(--surface-float-base);
|
||||
color: var(--text-invert-base);
|
||||
box-shadow: var(--shadow-md);
|
||||
|
||||
[data-slot="toast-inner"] {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&[data-opened] {
|
||||
animation: toastPopIn 150ms ease-out;
|
||||
}
|
||||
|
||||
&[data-closed] {
|
||||
animation: toastPopOut 100ms ease-in forwards;
|
||||
}
|
||||
|
||||
&[data-swipe="move"] {
|
||||
transform: translateX(var(--kb-toast-swipe-move-x));
|
||||
}
|
||||
|
||||
&[data-swipe="cancel"] {
|
||||
transform: translateX(0);
|
||||
transition: transform 200ms ease-out;
|
||||
}
|
||||
|
||||
&[data-swipe="end"] {
|
||||
animation: toastSwipeOut 100ms ease-out forwards;
|
||||
}
|
||||
|
||||
/* &[data-variant="success"] { */
|
||||
/* border-color: var(--color-semantic-positive); */
|
||||
/* } */
|
||||
/**/
|
||||
/* &[data-variant="error"] { */
|
||||
/* border-color: var(--color-semantic-danger); */
|
||||
/* } */
|
||||
/**/
|
||||
/* &[data-variant="loading"] { */
|
||||
/* border-color: var(--color-semantic-info); */
|
||||
/* } */
|
||||
|
||||
[data-slot="toast-icon"] {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
[data-component="icon"] {
|
||||
color: var(--text-invert-stronger);
|
||||
/* color: var(--icon-invert-base); */
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="toast-content"] {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-slot="toast-title"] {
|
||||
color: var(--text-invert-strong);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
/* text-14-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 142.857% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-slot="toast-description"] {
|
||||
color: var(--text-invert-base);
|
||||
text-wrap-style: pretty;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
|
||||
/* text-14-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-x-large); /* 171.429% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-slot="toast-actions"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
[data-slot="toast-action"] {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
color: var(--text-invert-weak);
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
color: var(--text-invert-strong);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="toast-close-button"] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-slot="toast-progress-track"] {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background-color: var(--surface-base);
|
||||
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-slot="toast-progress-fill"] {
|
||||
height: 100%;
|
||||
width: var(--kb-toast-progress-fill-width);
|
||||
background-color: var(--color-primary);
|
||||
transition: width 250ms linear;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toastPopIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toastPopOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toastSwipeOut {
|
||||
from {
|
||||
transform: translateX(var(--kb-toast-swipe-end-x));
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
185
opencode/packages/ui/src/components/toast.tsx
Normal file
185
opencode/packages/ui/src/components/toast.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { Toast as Kobalte, toaster } from "@kobalte/core/toast"
|
||||
import type { ToastRootProps, ToastCloseButtonProps, ToastTitleProps, ToastDescriptionProps } from "@kobalte/core/toast"
|
||||
import type { ComponentProps, JSX } from "solid-js"
|
||||
import { Show } from "solid-js"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import { Icon, type IconProps } from "./icon"
|
||||
import { IconButton } from "./icon-button"
|
||||
|
||||
export interface ToastRegionProps extends ComponentProps<typeof Kobalte.Region> {}
|
||||
|
||||
function ToastRegion(props: ToastRegionProps) {
|
||||
return (
|
||||
<Portal>
|
||||
<Kobalte.Region data-component="toast-region" {...props}>
|
||||
<Kobalte.List data-slot="toast-list" />
|
||||
</Kobalte.Region>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ToastRootComponentProps extends ToastRootProps {
|
||||
class?: string
|
||||
classList?: ComponentProps<"li">["classList"]
|
||||
children?: JSX.Element
|
||||
}
|
||||
|
||||
function ToastRoot(props: ToastRootComponentProps) {
|
||||
return (
|
||||
<Kobalte
|
||||
data-component="toast"
|
||||
classList={{
|
||||
...(props.classList ?? {}),
|
||||
[props.class ?? ""]: !!props.class,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ToastIcon(props: { name: IconProps["name"] }) {
|
||||
return (
|
||||
<div data-slot="toast-icon">
|
||||
<Icon name={props.name} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ToastContent(props: ComponentProps<"div">) {
|
||||
return <div data-slot="toast-content" {...props} />
|
||||
}
|
||||
|
||||
function ToastTitle(props: ToastTitleProps & ComponentProps<"div">) {
|
||||
return <Kobalte.Title data-slot="toast-title" {...props} />
|
||||
}
|
||||
|
||||
function ToastDescription(props: ToastDescriptionProps & ComponentProps<"div">) {
|
||||
return <Kobalte.Description data-slot="toast-description" {...props} />
|
||||
}
|
||||
|
||||
function ToastActions(props: ComponentProps<"div">) {
|
||||
return <div data-slot="toast-actions" {...props} />
|
||||
}
|
||||
|
||||
function ToastCloseButton(props: ToastCloseButtonProps & ComponentProps<"button">) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<Kobalte.CloseButton
|
||||
data-slot="toast-close-button"
|
||||
as={IconButton}
|
||||
icon="close"
|
||||
variant="ghost"
|
||||
aria-label={i18n.t("ui.common.dismiss")}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ToastProgressTrack(props: ComponentProps<typeof Kobalte.ProgressTrack>) {
|
||||
return <Kobalte.ProgressTrack data-slot="toast-progress-track" {...props} />
|
||||
}
|
||||
|
||||
function ToastProgressFill(props: ComponentProps<typeof Kobalte.ProgressFill>) {
|
||||
return <Kobalte.ProgressFill data-slot="toast-progress-fill" {...props} />
|
||||
}
|
||||
|
||||
export const Toast = Object.assign(ToastRoot, {
|
||||
Region: ToastRegion,
|
||||
Icon: ToastIcon,
|
||||
Content: ToastContent,
|
||||
Title: ToastTitle,
|
||||
Description: ToastDescription,
|
||||
Actions: ToastActions,
|
||||
CloseButton: ToastCloseButton,
|
||||
ProgressTrack: ToastProgressTrack,
|
||||
ProgressFill: ToastProgressFill,
|
||||
})
|
||||
|
||||
export { toaster }
|
||||
|
||||
export type ToastVariant = "default" | "success" | "error" | "loading"
|
||||
|
||||
export interface ToastAction {
|
||||
label: string
|
||||
onClick: "dismiss" | (() => void)
|
||||
}
|
||||
|
||||
export interface ToastOptions {
|
||||
title?: string
|
||||
description?: string
|
||||
icon?: IconProps["name"]
|
||||
variant?: ToastVariant
|
||||
duration?: number
|
||||
persistent?: boolean
|
||||
actions?: ToastAction[]
|
||||
}
|
||||
|
||||
export function showToast(options: ToastOptions | string) {
|
||||
const opts = typeof options === "string" ? { description: options } : options
|
||||
return toaster.show((props) => (
|
||||
<Toast
|
||||
toastId={props.toastId}
|
||||
duration={opts.duration}
|
||||
persistent={opts.persistent}
|
||||
data-variant={opts.variant ?? "default"}
|
||||
>
|
||||
<Show when={opts.icon}>
|
||||
<Toast.Icon name={opts.icon!} />
|
||||
</Show>
|
||||
<Toast.Content>
|
||||
<Show when={opts.title}>
|
||||
<Toast.Title>{opts.title}</Toast.Title>
|
||||
</Show>
|
||||
<Show when={opts.description}>
|
||||
<Toast.Description>{opts.description}</Toast.Description>
|
||||
</Show>
|
||||
<Show when={opts.actions?.length}>
|
||||
<Toast.Actions>
|
||||
{opts.actions!.map((action) => (
|
||||
<button
|
||||
data-slot="toast-action"
|
||||
onClick={() => {
|
||||
if (typeof action.onClick === "function") {
|
||||
action.onClick()
|
||||
}
|
||||
toaster.dismiss(props.toastId)
|
||||
}}
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
</Toast.Actions>
|
||||
</Show>
|
||||
</Toast.Content>
|
||||
<Toast.CloseButton />
|
||||
</Toast>
|
||||
))
|
||||
}
|
||||
|
||||
export interface ToastPromiseOptions<T, U = unknown> {
|
||||
loading?: JSX.Element
|
||||
success?: (data: T) => JSX.Element
|
||||
error?: (error: U) => JSX.Element
|
||||
}
|
||||
|
||||
export function showPromiseToast<T, U = unknown>(
|
||||
promise: Promise<T> | (() => Promise<T>),
|
||||
options: ToastPromiseOptions<T, U>,
|
||||
) {
|
||||
return toaster.promise(promise, (props) => (
|
||||
<Toast
|
||||
toastId={props.toastId}
|
||||
data-variant={props.state === "pending" ? "loading" : props.state === "fulfilled" ? "success" : "error"}
|
||||
>
|
||||
<Toast.Content>
|
||||
<Toast.Description>
|
||||
{props.state === "pending" && options.loading}
|
||||
{props.state === "fulfilled" && options.success?.(props.data!)}
|
||||
{props.state === "rejected" && options.error?.(props.error)}
|
||||
</Toast.Description>
|
||||
</Toast.Content>
|
||||
<Toast.CloseButton />
|
||||
</Toast>
|
||||
))
|
||||
}
|
||||
74
opencode/packages/ui/src/components/tooltip.css
Normal file
74
opencode/packages/ui/src/components/tooltip.css
Normal file
@@ -0,0 +1,74 @@
|
||||
[data-component="tooltip-trigger"] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
[data-slot="tooltip-keybind"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-slot="tooltip-keybind-key"] {
|
||||
color: var(--text-invert-base);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
}
|
||||
|
||||
[data-component="tooltip"] {
|
||||
z-index: 1000;
|
||||
max-width: 320px;
|
||||
border-radius: var(--radius-sm);
|
||||
background-color: var(--surface-float-base);
|
||||
color: var(--text-invert-strong);
|
||||
background: var(--surface-float-base);
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--border-weak-base, rgba(0, 0, 0, 0.07));
|
||||
|
||||
box-shadow: var(--shadow-md);
|
||||
pointer-events: none !important;
|
||||
/* transition: all 150ms ease-out; */
|
||||
/* transform: translate3d(0, 0, 0); */
|
||||
/* transform-origin: var(--kb-tooltip-content-transform-origin); */
|
||||
|
||||
/* text-12-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
&[data-expanded] {
|
||||
opacity: 1;
|
||||
/* transform: translate3d(0, 0, 0); */
|
||||
}
|
||||
|
||||
&[data-closed]:not([data-force-open="true"]) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* &[data-placement="top"] { */
|
||||
/* &[data-closed] { */
|
||||
/* transform: translate3d(0, 4px, 0); */
|
||||
/* } */
|
||||
/* } */
|
||||
/**/
|
||||
/* &[data-placement="bottom"] { */
|
||||
/* &[data-closed] { */
|
||||
/* transform: translate3d(0, -4px, 0); */
|
||||
/* } */
|
||||
/* } */
|
||||
/**/
|
||||
/* &[data-placement="left"] { */
|
||||
/* &[data-closed] { */
|
||||
/* transform: translate3d(4px, 0, 0); */
|
||||
/* } */
|
||||
/* } */
|
||||
/**/
|
||||
/* &[data-placement="right"] { */
|
||||
/* &[data-closed] { */
|
||||
/* transform: translate3d(-4px, 0, 0); */
|
||||
/* } */
|
||||
/* } */
|
||||
}
|
||||
86
opencode/packages/ui/src/components/tooltip.tsx
Normal file
86
opencode/packages/ui/src/components/tooltip.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Tooltip as KobalteTooltip } from "@kobalte/core/tooltip"
|
||||
import { children, createSignal, Match, onMount, splitProps, Switch, type JSX } from "solid-js"
|
||||
import type { ComponentProps } from "solid-js"
|
||||
|
||||
export interface TooltipProps extends ComponentProps<typeof KobalteTooltip> {
|
||||
value: JSX.Element
|
||||
class?: string
|
||||
contentClass?: string
|
||||
contentStyle?: JSX.CSSProperties
|
||||
inactive?: boolean
|
||||
forceOpen?: boolean
|
||||
}
|
||||
|
||||
export interface TooltipKeybindProps extends Omit<TooltipProps, "value"> {
|
||||
title: string
|
||||
keybind: string
|
||||
}
|
||||
|
||||
export function TooltipKeybind(props: TooltipKeybindProps) {
|
||||
const [local, others] = splitProps(props, ["title", "keybind"])
|
||||
return (
|
||||
<Tooltip
|
||||
{...others}
|
||||
value={
|
||||
<div data-slot="tooltip-keybind">
|
||||
<span>{local.title}</span>
|
||||
<span data-slot="tooltip-keybind-key">{local.keybind}</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function Tooltip(props: TooltipProps) {
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const [local, others] = splitProps(props, [
|
||||
"children",
|
||||
"class",
|
||||
"contentClass",
|
||||
"contentStyle",
|
||||
"inactive",
|
||||
"forceOpen",
|
||||
])
|
||||
|
||||
const c = children(() => local.children)
|
||||
|
||||
onMount(() => {
|
||||
const childElements = c()
|
||||
if (childElements instanceof HTMLElement) {
|
||||
childElements.addEventListener("focusin", () => setOpen(true))
|
||||
childElements.addEventListener("focusout", () => setOpen(false))
|
||||
} else if (Array.isArray(childElements)) {
|
||||
for (const child of childElements) {
|
||||
if (child instanceof HTMLElement) {
|
||||
child.addEventListener("focusin", () => setOpen(true))
|
||||
child.addEventListener("focusout", () => setOpen(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={local.inactive}>{local.children}</Match>
|
||||
<Match when={true}>
|
||||
<KobalteTooltip gutter={4} {...others} open={local.forceOpen || open()} onOpenChange={setOpen}>
|
||||
<KobalteTooltip.Trigger as={"div"} data-component="tooltip-trigger" class={local.class}>
|
||||
{c()}
|
||||
</KobalteTooltip.Trigger>
|
||||
<KobalteTooltip.Portal>
|
||||
<KobalteTooltip.Content
|
||||
data-component="tooltip"
|
||||
data-placement={props.placement}
|
||||
data-force-open={local.forceOpen}
|
||||
class={local.contentClass}
|
||||
style={local.contentStyle}
|
||||
>
|
||||
{others.value}
|
||||
{/* <KobalteTooltip.Arrow data-slot="tooltip-arrow" /> */}
|
||||
</KobalteTooltip.Content>
|
||||
</KobalteTooltip.Portal>
|
||||
</KobalteTooltip>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
14
opencode/packages/ui/src/components/typewriter.css
Normal file
14
opencode/packages/ui/src/components/typewriter.css
Normal file
@@ -0,0 +1,14 @@
|
||||
@keyframes blink {
|
||||
0%,
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
51%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.blinking-cursor {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
55
opencode/packages/ui/src/components/typewriter.tsx
Normal file
55
opencode/packages/ui/src/components/typewriter.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { createEffect, onCleanup, Show, type ValidComponent } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
|
||||
export const Typewriter = <T extends ValidComponent = "p">(props: { text?: string; class?: string; as?: T }) => {
|
||||
const [store, setStore] = createStore({
|
||||
typing: false,
|
||||
displayed: "",
|
||||
cursor: true,
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const text = props.text
|
||||
if (!text) return
|
||||
|
||||
let i = 0
|
||||
const timeouts: ReturnType<typeof setTimeout>[] = []
|
||||
setStore("typing", true)
|
||||
setStore("displayed", "")
|
||||
setStore("cursor", true)
|
||||
|
||||
const getTypingDelay = () => {
|
||||
const random = Math.random()
|
||||
if (random < 0.05) return 150 + Math.random() * 100
|
||||
if (random < 0.15) return 80 + Math.random() * 60
|
||||
return 30 + Math.random() * 50
|
||||
}
|
||||
|
||||
const type = () => {
|
||||
if (i < text.length) {
|
||||
setStore("displayed", text.slice(0, i + 1))
|
||||
i++
|
||||
timeouts.push(setTimeout(type, getTypingDelay()))
|
||||
} else {
|
||||
setStore("typing", false)
|
||||
timeouts.push(setTimeout(() => setStore("cursor", false), 2000))
|
||||
}
|
||||
}
|
||||
|
||||
timeouts.push(setTimeout(type, 200))
|
||||
|
||||
onCleanup(() => {
|
||||
for (const timeout of timeouts) clearTimeout(timeout)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<Dynamic component={props.as || "p"} class={props.class}>
|
||||
{store.displayed}
|
||||
<Show when={store.cursor}>
|
||||
<span classList={{ "blinking-cursor": !store.typing }}>│</span>
|
||||
</Show>
|
||||
</Dynamic>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user