Merge branch 'dev' of https://github.com/sst/opencode into dev
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
[data-component="avatar"] {
|
||||
--avatar-bg: var(--color-surface-info-base);
|
||||
--avatar-fg: var(--color-text-base);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -10,7 +11,7 @@
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
background-color: var(--avatar-bg);
|
||||
color: oklch(from var(--avatar-bg) calc(l * 0.72) calc(c * 8) h);
|
||||
color: var(--avatar-fg);
|
||||
}
|
||||
|
||||
[data-component="avatar"][data-has-image] {
|
||||
|
||||
@@ -4,11 +4,21 @@ 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", "size", "class", "classList", "style"])
|
||||
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
|
||||
@@ -23,6 +33,7 @@ export function Avatar(props: AvatarProps) {
|
||||
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]}>
|
||||
|
||||
@@ -88,7 +88,18 @@
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ const icons = {
|
||||
"layout-bottom-partial": `<path d="M2.5 17.5L2.5 12.2059L17.5 12.2059L17.5 17.5L2.5 17.5Z" fill="currentColor" fill-opacity="40%" /><path d="M2.5 17.5L2.5 2.5M2.5 17.5L17.5 17.5M2.5 17.5L2.5 12.2059M2.5 2.5L17.5 2.5M2.5 2.5L2.5 12.2059M17.5 2.5L17.5 17.5M17.5 2.5L17.5 12.2059M17.5 17.5L17.5 12.2059M17.5 12.2059L2.5 12.2059" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"layout-bottom-full": `<path d="M2.5 17.5L2.5 12.2059L17.5 12.2059L17.5 17.5L2.5 17.5Z" fill="currentColor"/><path d="M2.5 17.5L2.5 2.5M2.5 17.5L17.5 17.5M2.5 17.5L2.5 12.2059M2.5 2.5L17.5 2.5M2.5 2.5L2.5 12.2059M17.5 2.5L17.5 17.5M17.5 2.5L17.5 12.2059M17.5 17.5L17.5 12.2059M17.5 12.2059L2.5 12.2059" 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"/>`,
|
||||
}
|
||||
|
||||
export interface IconProps extends ComponentProps<"svg"> {
|
||||
|
||||
@@ -98,16 +98,15 @@
|
||||
display: block;
|
||||
}
|
||||
[data-slot="list-item-extra-icon"] {
|
||||
display: block !important;
|
||||
color: var(--icon-strong-base) !important;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
background: var(--surface-raised-base-active);
|
||||
}
|
||||
&:hover {
|
||||
[data-slot="list-item-extra-icon"] {
|
||||
color: var(--icon-strong-base) !important;
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { ComponentProps, createMemo, For, Match, Show, splitProps, Switch } from "solid-js"
|
||||
import { ComponentProps, For, Match, Show, splitProps, Switch } from "solid-js"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
import { Spinner } from "./spinner"
|
||||
import { Tooltip } from "@kobalte/core/tooltip"
|
||||
|
||||
export function MessageNav(
|
||||
@@ -9,20 +8,15 @@ export function MessageNav(
|
||||
messages: UserMessage[]
|
||||
current?: UserMessage
|
||||
size: "normal" | "compact"
|
||||
working?: boolean
|
||||
onMessageSelect: (message: UserMessage) => void
|
||||
},
|
||||
) {
|
||||
const [local, others] = splitProps(props, ["messages", "current", "size", "working", "onMessageSelect"])
|
||||
const lastUserMessage = createMemo(() => {
|
||||
return local.messages?.at(0)
|
||||
})
|
||||
const [local, others] = splitProps(props, ["messages", "current", "size", "onMessageSelect"])
|
||||
|
||||
const content = () => (
|
||||
<ul role="list" data-component="message-nav" data-size={local.size} {...others}>
|
||||
<For each={local.messages}>
|
||||
{(message) => {
|
||||
const messageWorking = createMemo(() => message.id === lastUserMessage()?.id && local.working)
|
||||
const handleClick = () => local.onMessageSelect(message)
|
||||
|
||||
return (
|
||||
@@ -35,14 +29,7 @@ export function MessageNav(
|
||||
</Match>
|
||||
<Match when={local.size === "normal"}>
|
||||
<button data-slot="message-nav-message-button" onClick={handleClick}>
|
||||
<Switch>
|
||||
<Match when={messageWorking()}>
|
||||
<Spinner />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
|
||||
</Match>
|
||||
</Switch>
|
||||
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
|
||||
<div
|
||||
data-slot="message-nav-title-preview"
|
||||
data-active={message.id === local.current?.id || undefined}
|
||||
@@ -64,7 +51,7 @@ export function MessageNav(
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={local.size === "compact"}>
|
||||
<Tooltip openDelay={0} closeDelay={300} placement="left-start" gutter={-65} shift={-16} overlap>
|
||||
<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">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Dialog, DialogProps } from "./dialog"
|
||||
import { Icon } from "./icon"
|
||||
import { IconButton } from "./icon-button"
|
||||
import { List, ListRef, ListProps } from "./list"
|
||||
import { Input } from "./input"
|
||||
import { TextField } from "./text-field"
|
||||
|
||||
interface SelectDialogProps<T>
|
||||
extends Omit<ListProps<T>, "filter">,
|
||||
@@ -55,7 +55,7 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
|
||||
<div data-component="select-dialog-input">
|
||||
<div data-slot="select-dialog-input-container">
|
||||
<Icon name="magnifying-glass" />
|
||||
<Input
|
||||
<TextField
|
||||
ref={inputRef}
|
||||
autofocus
|
||||
variant="ghost"
|
||||
|
||||
@@ -6,21 +6,12 @@ import "./session-message-rail.css"
|
||||
export interface SessionMessageRailProps extends ComponentProps<"div"> {
|
||||
messages: UserMessage[]
|
||||
current?: UserMessage
|
||||
working?: boolean
|
||||
wide?: boolean
|
||||
onMessageSelect: (message: UserMessage) => void
|
||||
}
|
||||
|
||||
export function SessionMessageRail(props: SessionMessageRailProps) {
|
||||
const [local, others] = splitProps(props, [
|
||||
"messages",
|
||||
"current",
|
||||
"working",
|
||||
"wide",
|
||||
"onMessageSelect",
|
||||
"class",
|
||||
"classList",
|
||||
])
|
||||
const [local, others] = splitProps(props, ["messages", "current", "wide", "onMessageSelect", "class", "classList"])
|
||||
|
||||
return (
|
||||
<Show when={(local.messages?.length ?? 0) > 1}>
|
||||
@@ -39,7 +30,6 @@ export function SessionMessageRail(props: SessionMessageRailProps) {
|
||||
current={local.current}
|
||||
onMessageSelect={local.onMessageSelect}
|
||||
size="compact"
|
||||
working={local.working}
|
||||
/>
|
||||
</div>
|
||||
<div data-slot="session-message-rail-full">
|
||||
@@ -48,7 +38,6 @@ export function SessionMessageRail(props: SessionMessageRailProps) {
|
||||
current={local.current}
|
||||
onMessageSelect={local.onMessageSelect}
|
||||
size={local.wide ? "normal" : "compact"}
|
||||
working={local.working}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,10 +42,10 @@ export function SessionTurn(
|
||||
const userMessages = createMemo(() =>
|
||||
messages()
|
||||
.filter((m) => m.role === "user")
|
||||
.sort((a, b) => b.id.localeCompare(a.id)),
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
)
|
||||
const lastUserMessage = createMemo(() => {
|
||||
return userMessages()?.at(0)
|
||||
return userMessages()?.at(-1)
|
||||
})
|
||||
const message = createMemo(() => userMessages()?.find((m) => m.id === props.messageID))
|
||||
|
||||
|
||||
@@ -40,6 +40,37 @@
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
|
||||
[data-slot="input-wrapper"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
/* 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);
|
||||
|
||||
@@ -47,12 +78,11 @@
|
||||
height: 32px;
|
||||
padding: 2px 12px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
background: var(--input-base);
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
/* text-14-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
@@ -64,19 +94,6 @@
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
&[data-invalid] {
|
||||
background: var(--surface-critical-weak);
|
||||
border: 1px solid var(--border-critical-selected);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
@@ -84,6 +101,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="input-copy-button"] {
|
||||
flex-shrink: 0;
|
||||
color: var(--icon-base);
|
||||
|
||||
&:hover {
|
||||
color: var(--icon-strong-base);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="input-error"] {
|
||||
color: var(--text-on-critical-base);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { TextField as Kobalte } from "@kobalte/core/text-field"
|
||||
import { Show, splitProps } from "solid-js"
|
||||
import { createSignal, Show, splitProps } from "solid-js"
|
||||
import type { ComponentProps } from "solid-js"
|
||||
import { IconButton } from "./icon-button"
|
||||
import { Tooltip } from "./tooltip"
|
||||
|
||||
export interface InputProps
|
||||
export interface TextFieldProps
|
||||
extends ComponentProps<typeof Kobalte.Input>,
|
||||
Partial<
|
||||
Pick<
|
||||
@@ -20,13 +22,13 @@ export interface InputProps
|
||||
> {
|
||||
label?: string
|
||||
hideLabel?: boolean
|
||||
hidden?: boolean
|
||||
description?: string
|
||||
error?: string
|
||||
variant?: "normal" | "ghost"
|
||||
copyable?: boolean
|
||||
}
|
||||
|
||||
export function Input(props: InputProps) {
|
||||
export function TextField(props: TextFieldProps) {
|
||||
const [local, others] = splitProps(props, [
|
||||
"name",
|
||||
"defaultValue",
|
||||
@@ -39,12 +41,21 @@ export function Input(props: InputProps) {
|
||||
"readOnly",
|
||||
"class",
|
||||
"label",
|
||||
"hidden",
|
||||
"hideLabel",
|
||||
"description",
|
||||
"error",
|
||||
"variant",
|
||||
"copyable",
|
||||
])
|
||||
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)
|
||||
}
|
||||
|
||||
return (
|
||||
<Kobalte
|
||||
data-component="input"
|
||||
@@ -57,7 +68,6 @@ export function Input(props: InputProps) {
|
||||
required={local.required}
|
||||
disabled={local.disabled}
|
||||
readOnly={local.readOnly}
|
||||
style={{ height: local.hidden ? 0 : undefined }}
|
||||
validationState={local.validationState}
|
||||
>
|
||||
<Show when={local.label}>
|
||||
@@ -65,7 +75,20 @@ export function Input(props: InputProps) {
|
||||
{local.label}
|
||||
</Kobalte.Label>
|
||||
</Show>
|
||||
<Kobalte.Input {...others} data-slot="input-input" class={local.class} />
|
||||
<div data-slot="input-wrapper">
|
||||
<Kobalte.Input {...others} data-slot="input-input" class={local.class} />
|
||||
<Show when={local.copyable}>
|
||||
<Tooltip value={copied() ? "Copied" : "Copy to clipboard"} placement="top" gutter={8}>
|
||||
<IconButton
|
||||
type="button"
|
||||
icon={copied() ? "check" : "copy"}
|
||||
variant="ghost"
|
||||
onClick={handleCopy}
|
||||
data-slot="input-copy-button"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={local.description}>
|
||||
<Kobalte.Description data-slot="input-description">{local.description}</Kobalte.Description>
|
||||
</Show>
|
||||
@@ -73,3 +96,8 @@ export function Input(props: InputProps) {
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
|
||||
/** @deprecated Use TextField instead */
|
||||
export const Input = TextField
|
||||
/** @deprecated Use TextFieldProps instead */
|
||||
export type InputProps = TextFieldProps
|
||||
203
packages/ui/src/components/toast.css
Normal file
203
packages/ui/src/components/toast.css
Normal file
@@ -0,0 +1,203 @@
|
||||
[data-component="toast-region"] {
|
||||
position: fixed;
|
||||
bottom: 32px;
|
||||
right: 32px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 400px;
|
||||
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"] {
|
||||
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-inverted-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: rgba(253, 252, 252, 0.94);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="toast-content"] {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="toast-title"] {
|
||||
color: var(--text-inverted-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);
|
||||
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-slot="toast-description"] {
|
||||
color: var(--text-inverted-base);
|
||||
|
||||
/* 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;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
[data-slot="toast-action"] {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
color: var(--text-inverted-strong);
|
||||
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;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
color: var(--text-inverted-weak);
|
||||
}
|
||||
}
|
||||
|
||||
[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%);
|
||||
}
|
||||
}
|
||||
160
packages/ui/src/components/toast.tsx
Normal file
160
packages/ui/src/components/toast.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
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 { 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">) {
|
||||
return <Kobalte.CloseButton data-slot="toast-close-button" as={IconButton} icon="close" variant="ghost" {...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: () => void
|
||||
}
|
||||
|
||||
export interface ToastOptions {
|
||||
title?: string
|
||||
description?: string
|
||||
icon?: IconProps["name"]
|
||||
variant?: ToastVariant
|
||||
duration?: number
|
||||
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} 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={action.onClick}>
|
||||
{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>
|
||||
))
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
max-width: 320px;
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--surface-float-base);
|
||||
color: var(--text-inverted-base);
|
||||
color: rgba(253, 252, 252, 0.94);
|
||||
padding: 2px 8px;
|
||||
border: 0.5px solid rgba(253, 252, 252, 0.2);
|
||||
|
||||
Reference in New Issue
Block a user