@opencode/ui 2.0.21 → 2.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context/dialog.d.ts +7 -3
- package/dist/data-display/accordion/accordion.d.ts +3 -1
- package/dist/data-display/project-avatar/project-avatar.d.ts +10 -0
- package/dist/feedback/toast/toast.d.ts +3 -2
- package/dist/i18n/en.d.ts +4 -0
- package/dist/icons/catalog.d.ts +253 -0
- package/dist/icons/icon/additional-icons.d.ts +1 -0
- package/dist/icons/icon/icon.d.ts +1 -237
- package/package.json +5 -1
- package/src/context/dialog.tsx +99 -52
- package/src/data-display/accordion/accordion.tsx +16 -2
- package/src/data-display/project-avatar/project-avatar.tsx +33 -0
- package/src/feedback/toast/toast.tsx +32 -6
- package/src/i18n/en.ts +9 -0
- package/src/icons/catalog.ts +269 -0
- package/src/icons/icon/additional-icons.ts +1 -0
- package/src/icons/icon/icon.tsx +10 -250
- package/src/navigation/tabs/tabs.css +2 -68
- package/src/styles/tailwind/index.css +1 -0
- package/src/styles/theme.css +1 -0
package/src/context/dialog.tsx
CHANGED
|
@@ -37,35 +37,66 @@ export function useDialogLayer() {
|
|
|
37
37
|
|
|
38
38
|
function init() {
|
|
39
39
|
const [stack, setStack] = createSignal<Active[]>([])
|
|
40
|
-
|
|
40
|
+
// A dialog is closing from the moment its close starts until its exit animation ends and it is disposed.
|
|
41
|
+
const closing = new Map<string, ReturnType<typeof setTimeout> | undefined>()
|
|
42
|
+
// The same ids, reactive, so the top dialog that stays open owns the focus trap during an exit animation.
|
|
43
|
+
const [exiting, setExiting] = createSignal<ReadonlySet<string>>(new Set())
|
|
41
44
|
const lock = { value: false }
|
|
45
|
+
const state = { disposed: false }
|
|
46
|
+
|
|
47
|
+
// Detach the stack before disposing, so cleanups that close dialogs find nothing to admit. Every dialog is
|
|
48
|
+
// drained even when one throws. A replaced dialog that was not already closing still hears onClose.
|
|
49
|
+
const disposeAll = (notify: boolean) => {
|
|
50
|
+
const items = stack()
|
|
51
|
+
const exited = new Set(closing.keys())
|
|
52
|
+
setStack([])
|
|
53
|
+
closing.forEach((timer) => clearTimeout(timer))
|
|
54
|
+
closing.clear()
|
|
55
|
+
setExiting(new Set<string>())
|
|
56
|
+
items.forEach((item) => {
|
|
57
|
+
if (notify && !exited.has(item.id)) isolate(() => item.onClose?.())
|
|
58
|
+
isolate(item.dispose)
|
|
59
|
+
})
|
|
60
|
+
}
|
|
42
61
|
|
|
43
62
|
onCleanup(() => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
timer.current = undefined
|
|
63
|
+
state.disposed = true
|
|
64
|
+
disposeAll(false)
|
|
47
65
|
})
|
|
48
66
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
const finish = (current: Active) => {
|
|
68
|
+
// Scheduled first, so the dialog still goes away when a callback throws.
|
|
69
|
+
closing.set(
|
|
70
|
+
current.id,
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
closing.delete(current.id)
|
|
73
|
+
if (closing.size === 0) lock.value = false
|
|
74
|
+
setExiting((ids) => new Set([...ids].filter((id) => id !== current.id)))
|
|
75
|
+
setStack((items) => items.filter((item) => item.id !== current.id))
|
|
76
|
+
current.dispose()
|
|
77
|
+
}, 100),
|
|
78
|
+
)
|
|
79
|
+
setExiting((ids) => new Set([...ids, current.id]))
|
|
80
|
+
isolate(() => current.onClose?.())
|
|
55
81
|
current.setClosing(true)
|
|
82
|
+
}
|
|
56
83
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
84
|
+
/** Programmatic close. Without an id it closes the top dialog, one at a time; with an id it never waits. */
|
|
85
|
+
const close = (id?: string) => {
|
|
86
|
+
const current = id ? stack().find((item) => item.id === id) : stack().at(-1)
|
|
87
|
+
if (!current || closing.has(current.id) || (!id && lock.value)) return
|
|
88
|
+
closing.set(current.id, undefined)
|
|
89
|
+
lock.value = true
|
|
90
|
+
finish(current)
|
|
91
|
+
}
|
|
62
92
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
93
|
+
/** Escape, a backdrop click, or Kobalte dismissing: only the top dialog, and one per exit animation. */
|
|
94
|
+
const dismiss = (id?: string) => {
|
|
95
|
+
const current = stack().at(-1)
|
|
96
|
+
if (!current || (id && current.id !== id) || closing.has(current.id) || lock.value) return
|
|
97
|
+
closing.set(current.id, undefined)
|
|
98
|
+
lock.value = true
|
|
99
|
+
finish(current)
|
|
69
100
|
}
|
|
70
101
|
|
|
71
102
|
createEffect(() => {
|
|
@@ -73,7 +104,7 @@ function init() {
|
|
|
73
104
|
|
|
74
105
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
75
106
|
if (event.key !== "Escape") return
|
|
76
|
-
|
|
107
|
+
dismiss()
|
|
77
108
|
event.preventDefault()
|
|
78
109
|
event.stopPropagation()
|
|
79
110
|
}
|
|
@@ -81,9 +112,17 @@ function init() {
|
|
|
81
112
|
makeEventListener(window, "keydown", onKeyDown, { capture: true })
|
|
82
113
|
})
|
|
83
114
|
|
|
84
|
-
const mount = (element: DialogElement, owner: Owner, onClose: (() => void) | undefined,
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
const mount = (element: DialogElement, owner: Owner, onClose: (() => void) | undefined, key?: string) => {
|
|
116
|
+
// A deferred open (e.g. from a focus callback) must not mount after the provider is gone.
|
|
117
|
+
if (state.disposed) return
|
|
118
|
+
const id = key ?? Math.random().toString(36).slice(2)
|
|
119
|
+
// The layer follows the dialog's current place in the stack, so a new top dialog always renders above.
|
|
120
|
+
const layer = () =>
|
|
121
|
+
Math.max(
|
|
122
|
+
0,
|
|
123
|
+
stack().findIndex((item) => item.id === id),
|
|
124
|
+
)
|
|
125
|
+
const zIndex = () => String(50 + layer() * 10)
|
|
87
126
|
let dispose: (() => void) | undefined
|
|
88
127
|
let setClosing: ((closing: boolean) => void) | undefined
|
|
89
128
|
|
|
@@ -96,27 +135,26 @@ function init() {
|
|
|
96
135
|
setClosing = setClosingSignal
|
|
97
136
|
return (
|
|
98
137
|
<Kobalte
|
|
99
|
-
modal={stack().
|
|
138
|
+
modal={stack().findLast((item) => !exiting().has(item.id))?.id === id}
|
|
100
139
|
open={!closing()}
|
|
101
140
|
onOpenChange={(open: boolean) => {
|
|
102
|
-
if (open
|
|
103
|
-
close(id)
|
|
141
|
+
if (!open) dismiss(id)
|
|
104
142
|
}}
|
|
105
143
|
>
|
|
106
144
|
<Kobalte.Portal>
|
|
107
145
|
<Kobalte.Overlay
|
|
108
146
|
data-component="dialog-overlay"
|
|
109
|
-
style={{ "z-index":
|
|
147
|
+
style={{ "z-index": zIndex() }}
|
|
110
148
|
onClick={() => {
|
|
111
|
-
if (backdropDismiss())
|
|
149
|
+
if (backdropDismiss()) dismiss(id)
|
|
112
150
|
}}
|
|
113
151
|
/>
|
|
114
152
|
<div
|
|
115
|
-
data-dialog-layer={layer}
|
|
153
|
+
data-dialog-layer={layer()}
|
|
116
154
|
style={{
|
|
117
155
|
position: "fixed",
|
|
118
156
|
inset: "0",
|
|
119
|
-
"z-index":
|
|
157
|
+
"z-index": zIndex(),
|
|
120
158
|
display: "flex",
|
|
121
159
|
"align-items": "center",
|
|
122
160
|
"justify-content": "center",
|
|
@@ -137,24 +175,15 @@ function init() {
|
|
|
137
175
|
setStack((items) => [...items, active])
|
|
138
176
|
}
|
|
139
177
|
|
|
140
|
-
const push = (element: DialogElement, owner: Owner, onClose?: () => void) => {
|
|
141
|
-
if (timer.current !== undefined) {
|
|
142
|
-
clearTimeout(timer.current)
|
|
143
|
-
timer.current = undefined
|
|
144
|
-
}
|
|
178
|
+
const push = (element: DialogElement, owner: Owner, onClose?: () => void, id?: string) => {
|
|
145
179
|
lock.value = false
|
|
146
|
-
mount(element, owner, onClose,
|
|
180
|
+
mount(element, owner, onClose, id)
|
|
147
181
|
}
|
|
148
182
|
|
|
149
|
-
const show = (element: DialogElement, owner: Owner, onClose?: () => void) => {
|
|
150
|
-
|
|
151
|
-
setStack([])
|
|
152
|
-
if (timer.current !== undefined) {
|
|
153
|
-
clearTimeout(timer.current)
|
|
154
|
-
timer.current = undefined
|
|
155
|
-
}
|
|
183
|
+
const show = (element: DialogElement, owner: Owner, onClose?: () => void, id?: string) => {
|
|
184
|
+
disposeAll(true)
|
|
156
185
|
lock.value = false
|
|
157
|
-
mount(element, owner, onClose,
|
|
186
|
+
mount(element, owner, onClose, id)
|
|
158
187
|
}
|
|
159
188
|
|
|
160
189
|
return {
|
|
@@ -164,6 +193,14 @@ function init() {
|
|
|
164
193
|
push,
|
|
165
194
|
}
|
|
166
195
|
}
|
|
196
|
+
/** Runs a dialog callback so its throw is reported without stopping the caller's cleanup. */
|
|
197
|
+
function isolate(fn: () => void) {
|
|
198
|
+
try {
|
|
199
|
+
fn()
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error("[dialog]", error)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
167
204
|
|
|
168
205
|
export function DialogProvider(props: ParentProps) {
|
|
169
206
|
const ctx = init()
|
|
@@ -192,16 +229,26 @@ export function useDialog() {
|
|
|
192
229
|
get active() {
|
|
193
230
|
return ctx.stack().at(-1)
|
|
194
231
|
},
|
|
195
|
-
|
|
232
|
+
/**
|
|
233
|
+
* id lets the caller close this dialog later rather than whichever is on top. Opening is deferred; once
|
|
234
|
+
* signal aborts, the open neither mounts nor replaces anything and onClose runs as for a replaced dialog.
|
|
235
|
+
*/
|
|
236
|
+
show(element: DialogElement, onClose?: () => void, id?: string, signal?: AbortSignal) {
|
|
196
237
|
const base = ctx.stack().at(-1)?.owner ?? owner
|
|
197
|
-
return startTransition(() =>
|
|
238
|
+
return startTransition(() => {
|
|
239
|
+
if (signal?.aborted) return isolate(() => onClose?.())
|
|
240
|
+
ctx.show(element, base, onClose, id)
|
|
241
|
+
})
|
|
198
242
|
},
|
|
199
|
-
push(element: DialogElement, onClose?: () => void) {
|
|
243
|
+
push(element: DialogElement, onClose?: () => void, id?: string, signal?: AbortSignal) {
|
|
200
244
|
const base = ctx.stack().at(-1)?.owner ?? owner
|
|
201
|
-
return startTransition(() =>
|
|
245
|
+
return startTransition(() => {
|
|
246
|
+
if (signal?.aborted) return isolate(() => onClose?.())
|
|
247
|
+
ctx.push(element, base, onClose, id)
|
|
248
|
+
})
|
|
202
249
|
},
|
|
203
|
-
close() {
|
|
204
|
-
ctx.close()
|
|
250
|
+
close(id?: string) {
|
|
251
|
+
ctx.close(id)
|
|
205
252
|
},
|
|
206
253
|
}
|
|
207
254
|
}
|
|
@@ -2,17 +2,27 @@ import { Content, Header, Item, Root, Trigger } from "@kobalte/core/accordion"
|
|
|
2
2
|
import { splitProps } from "solid-js"
|
|
3
3
|
import type { ComponentProps, ParentProps } from "solid-js"
|
|
4
4
|
|
|
5
|
-
export interface AccordionProps extends ComponentProps<typeof Root> {
|
|
5
|
+
export interface AccordionProps extends Omit<ComponentProps<typeof Root>, "value"> {
|
|
6
|
+
/** Controlled expanded items. Kobalte reads this iterable into its own selection; it never mutates this array. */
|
|
7
|
+
value?: readonly string[]
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
export interface AccordionItemProps extends ComponentProps<typeof Item> {}
|
|
11
|
+
|
|
7
12
|
export interface AccordionHeaderProps extends ComponentProps<typeof Header> {}
|
|
13
|
+
|
|
8
14
|
export interface AccordionTriggerProps extends ComponentProps<typeof Trigger> {}
|
|
15
|
+
|
|
9
16
|
export interface AccordionContentProps extends ComponentProps<typeof Content> {}
|
|
10
17
|
|
|
11
18
|
function AccordionRoot(props: AccordionProps) {
|
|
12
|
-
const [split, rest] = splitProps(props, ["class", "classList"])
|
|
19
|
+
const [split, rest] = splitProps(props, ["class", "classList", "value"])
|
|
20
|
+
|
|
21
|
+
// SAFETY: Kobalte's createMultipleSelectionState copies selectedKeys into a Selection before changing it.
|
|
13
22
|
return (
|
|
14
23
|
<Root
|
|
15
24
|
{...rest}
|
|
25
|
+
value={split.value as string[] | undefined}
|
|
16
26
|
data-component="accordion"
|
|
17
27
|
classList={{
|
|
18
28
|
...split.classList,
|
|
@@ -24,6 +34,7 @@ function AccordionRoot(props: AccordionProps) {
|
|
|
24
34
|
|
|
25
35
|
function AccordionItem(props: AccordionItemProps) {
|
|
26
36
|
const [split, rest] = splitProps(props, ["class", "classList"])
|
|
37
|
+
|
|
27
38
|
return (
|
|
28
39
|
<Item
|
|
29
40
|
{...rest}
|
|
@@ -38,6 +49,7 @@ function AccordionItem(props: AccordionItemProps) {
|
|
|
38
49
|
|
|
39
50
|
function AccordionHeader(props: ParentProps<AccordionHeaderProps>) {
|
|
40
51
|
const [split, rest] = splitProps(props, ["class", "classList", "children"])
|
|
52
|
+
|
|
41
53
|
return (
|
|
42
54
|
<Header
|
|
43
55
|
{...rest}
|
|
@@ -54,6 +66,7 @@ function AccordionHeader(props: ParentProps<AccordionHeaderProps>) {
|
|
|
54
66
|
|
|
55
67
|
function AccordionTrigger(props: ParentProps<AccordionTriggerProps>) {
|
|
56
68
|
const [split, rest] = splitProps(props, ["class", "classList", "children"])
|
|
69
|
+
|
|
57
70
|
return (
|
|
58
71
|
<Trigger
|
|
59
72
|
{...rest}
|
|
@@ -70,6 +83,7 @@ function AccordionTrigger(props: ParentProps<AccordionTriggerProps>) {
|
|
|
70
83
|
|
|
71
84
|
function AccordionContent(props: ParentProps<AccordionContentProps>) {
|
|
72
85
|
const [split, rest] = splitProps(props, ["class", "classList", "children"])
|
|
86
|
+
|
|
73
87
|
return (
|
|
74
88
|
<Content
|
|
75
89
|
{...rest}
|
|
@@ -29,6 +29,39 @@ export type ProjectAvatarVariant = (typeof PROJECT_AVATAR_VARIANTS)[number]
|
|
|
29
29
|
// "outline" is a neutral, muted style (e.g. recently closed projects) and is not part of the color rotation.
|
|
30
30
|
export type ProjectAvatarStyle = ProjectAvatarVariant | "outline"
|
|
31
31
|
|
|
32
|
+
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
|
33
|
+
|
|
34
|
+
export function getProjectAvatarSource(id?: string, icon?: { color?: string; url?: string; override?: string }) {
|
|
35
|
+
if (id === OPENCODE_PROJECT_ID) return "https://opencode.ai/favicon.svg"
|
|
36
|
+
if (icon?.override) return icon.override
|
|
37
|
+
if (icon?.color) return undefined
|
|
38
|
+
return icon?.url
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getProjectAvatarVariant(key?: string): ProjectAvatarVariant {
|
|
42
|
+
if (key === "mint") return "cyan"
|
|
43
|
+
if (key === "lime") return "green"
|
|
44
|
+
if (
|
|
45
|
+
key === "orange" ||
|
|
46
|
+
key === "yellow" ||
|
|
47
|
+
key === "cyan" ||
|
|
48
|
+
key === "green" ||
|
|
49
|
+
key === "red" ||
|
|
50
|
+
key === "pink" ||
|
|
51
|
+
key === "blue" ||
|
|
52
|
+
key === "purple" ||
|
|
53
|
+
key === "gray"
|
|
54
|
+
)
|
|
55
|
+
return key
|
|
56
|
+
return "gray"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const displayName = (project: { name?: string; worktree: string }) => {
|
|
60
|
+
const trimmed = project.worktree.replace(/[/\\]+$/, "")
|
|
61
|
+
const filename = trimmed.slice(Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\")) + 1)
|
|
62
|
+
return project.name || filename || project.worktree
|
|
63
|
+
}
|
|
64
|
+
|
|
32
65
|
export interface ProjectAvatarProps extends ComponentProps<"div"> {
|
|
33
66
|
fallback: string
|
|
34
67
|
src?: string
|
|
@@ -21,9 +21,12 @@ function ToastRegion(props: ToastRegionProps) {
|
|
|
21
21
|
element.tabIndex = hidden ? -1 : 0
|
|
22
22
|
})
|
|
23
23
|
}
|
|
24
|
+
|
|
24
25
|
let connected = false
|
|
26
|
+
|
|
25
27
|
const connect = () => {
|
|
26
28
|
const regions = document.querySelectorAll(".toast-v2-region")
|
|
29
|
+
|
|
27
30
|
if (!regions.length) return
|
|
28
31
|
observer.disconnect()
|
|
29
32
|
regions.forEach((region) => {
|
|
@@ -37,14 +40,17 @@ function ToastRegion(props: ToastRegionProps) {
|
|
|
37
40
|
connected = true
|
|
38
41
|
sync()
|
|
39
42
|
}
|
|
43
|
+
|
|
40
44
|
const observer = new MutationObserver(() => {
|
|
41
45
|
if (!connected) connect()
|
|
42
46
|
else sync()
|
|
43
47
|
})
|
|
48
|
+
|
|
44
49
|
observer.observe(document.body, { subtree: true, childList: true })
|
|
45
50
|
queueMicrotask(connect)
|
|
46
51
|
onCleanup(() => observer.disconnect())
|
|
47
52
|
})
|
|
53
|
+
|
|
48
54
|
return (
|
|
49
55
|
<Portal>
|
|
50
56
|
<Toaster
|
|
@@ -55,7 +61,7 @@ function ToastRegion(props: ToastRegionProps) {
|
|
|
55
61
|
duration={5000}
|
|
56
62
|
swipeDirections={["bottom"]}
|
|
57
63
|
className={["toast-v2-region", local.className, local.class].filter(Boolean).join(" ")}
|
|
58
|
-
style={{ "--width": "320px", ...local.style }
|
|
64
|
+
style={{ "--width": "320px", ...local.style }}
|
|
59
65
|
toastOptions={{
|
|
60
66
|
...local.toastOptions,
|
|
61
67
|
unstyled: true,
|
|
@@ -103,6 +109,7 @@ function ToastCloseButton(props: ComponentProps<"button">) {
|
|
|
103
109
|
const i18n = useI18n()
|
|
104
110
|
const toastId = useContext(ToastContext)
|
|
105
111
|
const [local, rest] = splitProps(props, ["children", "onClick"])
|
|
112
|
+
|
|
106
113
|
return (
|
|
107
114
|
<button
|
|
108
115
|
type="button"
|
|
@@ -110,7 +117,8 @@ function ToastCloseButton(props: ComponentProps<"button">) {
|
|
|
110
117
|
aria-label={i18n.t("ui.common.dismiss")}
|
|
111
118
|
{...rest}
|
|
112
119
|
onClick={(event) => {
|
|
113
|
-
if (
|
|
120
|
+
if (local.onClick instanceof Function) local.onClick(event)
|
|
121
|
+
|
|
114
122
|
if (!event.defaultPrevented && toastId !== undefined) toaster.dismiss(toastId)
|
|
115
123
|
}}
|
|
116
124
|
>
|
|
@@ -147,7 +155,7 @@ export const toaster = {
|
|
|
147
155
|
duration?: number
|
|
148
156
|
persistent?: boolean
|
|
149
157
|
/** Reactive value the toast's height depends on; sonner re-measures when it changes. */
|
|
150
|
-
resize?: () =>
|
|
158
|
+
resize?: () => void
|
|
151
159
|
},
|
|
152
160
|
) {
|
|
153
161
|
const toastId = --toastV2Id
|
|
@@ -162,10 +170,12 @@ export const toaster = {
|
|
|
162
170
|
description: options?.resize
|
|
163
171
|
? (): JSX.Element => {
|
|
164
172
|
options.resize?.()
|
|
173
|
+
|
|
165
174
|
return undefined
|
|
166
175
|
}
|
|
167
176
|
: undefined,
|
|
168
177
|
})
|
|
178
|
+
|
|
169
179
|
return toastId
|
|
170
180
|
},
|
|
171
181
|
dismiss(toastId?: number) {
|
|
@@ -175,6 +185,7 @@ export const toaster = {
|
|
|
175
185
|
} else {
|
|
176
186
|
releaseToast(activeToastById.get(toastId))
|
|
177
187
|
}
|
|
188
|
+
|
|
178
189
|
return toast.dismiss(toastId)
|
|
179
190
|
},
|
|
180
191
|
}
|
|
@@ -188,7 +199,8 @@ export interface ToastAction {
|
|
|
188
199
|
export interface ToastOptions {
|
|
189
200
|
title?: string
|
|
190
201
|
description?: string
|
|
191
|
-
|
|
202
|
+
/** An element, or a render function the toast calls under its own owner, for icons created from imperative code. */
|
|
203
|
+
icon?: JSX.Element | (() => JSX.Element)
|
|
192
204
|
variant?: "default" | "success" | "error" | "loading"
|
|
193
205
|
duration?: number
|
|
194
206
|
persistent?: boolean
|
|
@@ -203,10 +215,14 @@ interface ActiveToast {
|
|
|
203
215
|
}
|
|
204
216
|
|
|
205
217
|
const activeToastByKey = new Map<string, ActiveToast>()
|
|
218
|
+
|
|
206
219
|
const activeToastById = new Map<number, ActiveToast>()
|
|
207
220
|
|
|
208
221
|
export function showToast(options: ToastOptions | string) {
|
|
222
|
+
// SAFETY: `options` is declared `ToastOptions | string`; `typeof` separates the string shorthand from the object.
|
|
223
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- see SAFETY above
|
|
209
224
|
const opts = boundToastDescription(typeof options === "string" ? { description: options } : options)
|
|
225
|
+
|
|
210
226
|
const key = JSON.stringify({
|
|
211
227
|
title: opts.title,
|
|
212
228
|
description: opts.description,
|
|
@@ -215,6 +231,7 @@ export function showToast(options: ToastOptions | string) {
|
|
|
215
231
|
persistent: opts.persistent,
|
|
216
232
|
actions: opts.actions?.map((action) => [action.label, action.variant]),
|
|
217
233
|
})
|
|
234
|
+
|
|
218
235
|
const active = activeToastByKey.get(key)
|
|
219
236
|
const toasts = toast.getToasts()
|
|
220
237
|
|
|
@@ -222,6 +239,7 @@ export function showToast(options: ToastOptions | string) {
|
|
|
222
239
|
active.options = opts
|
|
223
240
|
publishToast(active)
|
|
224
241
|
pulseToast(active.id)
|
|
242
|
+
|
|
225
243
|
return active.id
|
|
226
244
|
}
|
|
227
245
|
|
|
@@ -233,6 +251,7 @@ export function showToast(options: ToastOptions | string) {
|
|
|
233
251
|
activeToastByKey.set(key, entry)
|
|
234
252
|
activeToastById.set(entry.id, entry)
|
|
235
253
|
publishToast(entry)
|
|
254
|
+
|
|
236
255
|
return entry.id
|
|
237
256
|
}
|
|
238
257
|
|
|
@@ -241,7 +260,9 @@ function publishToast(entry: ActiveToast) {
|
|
|
241
260
|
toast(entry.options.title ?? "", {
|
|
242
261
|
id: entry.id,
|
|
243
262
|
description: entry.options.description,
|
|
244
|
-
|
|
263
|
+
// SAFETY: solid-sonner inserts `icon` as a JSX child, and Solid calls a function child under the toast's render
|
|
264
|
+
// owner; solid-sonner's type omits function children.
|
|
265
|
+
icon: entry.options.icon as JSX.Element,
|
|
245
266
|
action: entry.actions,
|
|
246
267
|
closeButton: true,
|
|
247
268
|
duration: entry.options.persistent ? Number.POSITIVE_INFINITY : entry.options.duration,
|
|
@@ -255,6 +276,7 @@ function publishToast(entry: ActiveToast) {
|
|
|
255
276
|
|
|
256
277
|
function createToastActions(entry: ActiveToast) {
|
|
257
278
|
if (!entry.options.actions?.length) return undefined
|
|
279
|
+
|
|
258
280
|
return (
|
|
259
281
|
<Toast.Actions>
|
|
260
282
|
{/* Static map, not <For>: this JSX is created imperatively outside any Solid
|
|
@@ -268,7 +290,8 @@ function createToastActions(entry: ActiveToast) {
|
|
|
268
290
|
data-action-variant={action.variant ?? "primary"}
|
|
269
291
|
onClick={() => {
|
|
270
292
|
const onClick = entry.options.actions?.[index]?.onClick
|
|
271
|
-
|
|
293
|
+
|
|
294
|
+
if (onClick && onClick !== "dismiss") onClick()
|
|
272
295
|
toaster.dismiss(entry.id)
|
|
273
296
|
}}
|
|
274
297
|
>
|
|
@@ -283,6 +306,7 @@ function pulseToast(toastId: number) {
|
|
|
283
306
|
if (typeof document === "undefined" || typeof requestAnimationFrame === "undefined") return
|
|
284
307
|
requestAnimationFrame(() => {
|
|
285
308
|
const element = document.querySelector<HTMLElement>(`[data-testid="toast-v2-${toastId}"]`)
|
|
309
|
+
|
|
286
310
|
if (!element || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
|
|
287
311
|
element.animate([{ scale: 1 }, { scale: 1.025 }, { scale: 1 }], { duration: 160, easing: "ease-out" })
|
|
288
312
|
})
|
|
@@ -290,7 +314,9 @@ function pulseToast(toastId: number) {
|
|
|
290
314
|
|
|
291
315
|
function releaseToast(entry: ActiveToast | undefined) {
|
|
292
316
|
if (!entry) return
|
|
317
|
+
|
|
293
318
|
if (activeToastByKey.get(entry.key) === entry) activeToastByKey.delete(entry.key)
|
|
319
|
+
|
|
294
320
|
if (activeToastById.get(entry.id) === entry) activeToastById.delete(entry.id)
|
|
295
321
|
}
|
|
296
322
|
|
package/src/i18n/en.ts
CHANGED
|
@@ -108,6 +108,7 @@ const source = {
|
|
|
108
108
|
"ui.messagePart.questions.dismissed": "Questions dismissed",
|
|
109
109
|
"ui.messagePart.compaction": "Session compacted",
|
|
110
110
|
"ui.messagePart.compaction.started": "Session compaction started",
|
|
111
|
+
"ui.messagePart.compaction.queued": "Session compaction queued",
|
|
111
112
|
"ui.messagePart.compaction.running": "Compacting",
|
|
112
113
|
"ui.messagePart.compaction.failed": "Session compaction failed",
|
|
113
114
|
"ui.messagePart.compaction.cancelled": "Session compaction cancelled",
|
|
@@ -242,12 +243,15 @@ const source = {
|
|
|
242
243
|
"ui.message.copyMessage": "Copy message",
|
|
243
244
|
"ui.message.forkMessage": "Fork to new session",
|
|
244
245
|
"ui.message.revertMessage": "Revert message",
|
|
246
|
+
"ui.message.moveToQueue": "Move to queue",
|
|
247
|
+
"ui.message.deletePending": "Delete",
|
|
245
248
|
"ui.message.copyResponse": "Copy response",
|
|
246
249
|
"ui.message.copied": "Copied",
|
|
247
250
|
"ui.message.thought": "Thought",
|
|
248
251
|
"ui.message.duration.seconds": "{{count}}s",
|
|
249
252
|
"ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s",
|
|
250
253
|
"ui.message.interrupted": "Interrupted",
|
|
254
|
+
"ui.message.modelVariant": "{{model}} ({{variant}})",
|
|
251
255
|
"ui.sessionTimeline.notice.model": "Model",
|
|
252
256
|
"ui.sessionTimeline.notice.modelSwitched": "Switched to {{model}}",
|
|
253
257
|
"ui.sessionTimeline.notice.agentChanged": "Agent changed",
|
|
@@ -275,10 +279,15 @@ const source = {
|
|
|
275
279
|
} satisfies Record<string, string>
|
|
276
280
|
|
|
277
281
|
export type Key = keyof typeof source
|
|
282
|
+
|
|
278
283
|
export type PluralCategory = "zero" | "one" | "two" | "few" | "many" | "other"
|
|
284
|
+
|
|
279
285
|
export type PluralKey = {
|
|
280
286
|
[Entry in Key]: Entry extends `${infer Base}.other` ? (`${Base}.one` extends Key ? Base : never) : never
|
|
281
287
|
}[Key]
|
|
288
|
+
|
|
282
289
|
export type PluralLookupKey = `${PluralKey}.${PluralCategory}`
|
|
290
|
+
|
|
283
291
|
export type LocaleKey = Key | PluralLookupKey
|
|
292
|
+
|
|
284
293
|
export const dict: typeof source & Record<string, string> = source
|