@opencode/ui 2.0.22 → 2.0.24

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.
Files changed (82) hide show
  1. package/dist/data-display/accordion/accordion.d.ts +3 -1
  2. package/dist/feedback/toast/toast.d.ts +3 -2
  3. package/dist/i18n/en.d.ts +5 -0
  4. package/dist/icons/catalog.d.ts +257 -0
  5. package/dist/icons/icon/additional-icons.d.ts +1 -0
  6. package/dist/icons/icon/icon.d.ts +1 -237
  7. package/package.json +5 -1
  8. package/src/actions/button/button.tsx +1 -0
  9. package/src/actions/icon-button/icon-button.tsx +1 -0
  10. package/src/actions/split-button/split-button.tsx +3 -0
  11. package/src/components/animated-number.tsx +13 -0
  12. package/src/components/app-icon.tsx +2 -0
  13. package/src/components/card.tsx +21 -0
  14. package/src/components/collapsible.tsx +1 -0
  15. package/src/components/context-menu.tsx +34 -0
  16. package/src/components/dock-surface.tsx +3 -0
  17. package/src/components/file-icon.tsx +12 -0
  18. package/src/components/image-preview.tsx +1 -0
  19. package/src/components/list.tsx +32 -0
  20. package/src/components/motion-spring.tsx +5 -0
  21. package/src/components/popover.tsx +12 -0
  22. package/src/components/provider-icon.tsx +1 -0
  23. package/src/components/resize-handle.tsx +8 -0
  24. package/src/components/scroll-view.tsx +39 -0
  25. package/src/components/spinner.tsx +2 -0
  26. package/src/components/text-field.tsx +6 -0
  27. package/src/components/text-reveal.tsx +19 -0
  28. package/src/components/text-strikethrough.tsx +8 -0
  29. package/src/components/typewriter.tsx +4 -0
  30. package/src/context/dialog.tsx +12 -0
  31. package/src/context/file.tsx +1 -0
  32. package/src/context/helper.tsx +4 -0
  33. package/src/context/i18n.tsx +20 -0
  34. package/src/context/index.ts +3 -0
  35. package/src/context/marked-base.tsx +7 -0
  36. package/src/context/marked-parser.tsx +7 -0
  37. package/src/context/marked.tsx +3 -0
  38. package/src/context/worker-pool.tsx +2 -0
  39. package/src/data-display/accordion/accordion.tsx +16 -2
  40. package/src/data-display/avatar/avatar.tsx +4 -0
  41. package/src/data-display/badge/badge.tsx +1 -0
  42. package/src/data-display/diff-changes/diff-changes.tsx +2 -0
  43. package/src/data-display/keybind/keybind.tsx +1 -0
  44. package/src/data-display/line-comment/line-comment.tsx +22 -0
  45. package/src/data-display/project-avatar/project-avatar.tsx +10 -0
  46. package/src/feedback/loader/loader.tsx +1 -0
  47. package/src/feedback/toast/description.ts +1 -0
  48. package/src/feedback/toast/toast.tsx +32 -6
  49. package/src/forms/checkbox/checkbox.tsx +1 -0
  50. package/src/forms/field/field.tsx +8 -0
  51. package/src/forms/inline-input/inline-input.tsx +4 -0
  52. package/src/forms/radio/radio.tsx +2 -0
  53. package/src/forms/select/select.tsx +11 -0
  54. package/src/forms/switch/switch.tsx +1 -0
  55. package/src/forms/text-input/text-input.tsx +3 -0
  56. package/src/hooks/create-auto-scroll.tsx +34 -0
  57. package/src/hooks/index.ts +1 -0
  58. package/src/hooks/use-filtered-list.tsx +14 -0
  59. package/src/i18n/en.ts +10 -0
  60. package/src/i18n/no.ts +1 -0
  61. package/src/icons/catalog.ts +273 -0
  62. package/src/icons/icon/additional-icons.ts +1 -0
  63. package/src/icons/icon/icon.tsx +10 -250
  64. package/src/layout/divider/divider.tsx +1 -0
  65. package/src/navigation/menu/menu.tsx +13 -0
  66. package/src/navigation/segmented-control/segmented-control.tsx +16 -0
  67. package/src/navigation/tab-state-indicator.tsx +1 -0
  68. package/src/navigation/tabs/tabs.css +6 -72
  69. package/src/navigation/tabs/tabs.tsx +18 -0
  70. package/src/overlays/dialog/dialog.tsx +3 -0
  71. package/src/overlays/tooltip/tooltip.tsx +15 -0
  72. package/src/storybook/scaffold.tsx +3 -0
  73. package/src/styles/theme.css +1 -1
  74. package/src/theme/color.ts +27 -0
  75. package/src/theme/context.tsx +58 -0
  76. package/src/theme/default-themes.ts +35 -0
  77. package/src/theme/index.ts +3 -0
  78. package/src/theme/loader.ts +11 -0
  79. package/src/theme/resolve.ts +28 -0
  80. package/src/theme/v2/foreground.ts +8 -0
  81. package/src/theme/v2/resolve.ts +10 -0
  82. package/src/typography/text-shimmer/text-shimmer.tsx +1 -0
@@ -8,6 +8,7 @@ export function createMarkdownParser(highlight: (code: string, language: string)
8
8
  }
9
9
 
10
10
  const inlineMathRegex = /^\\\(((?:\\.|[^\\\n])*?)\\\)/
11
+
11
12
  const blockMathRegex = /^\$\$\n([\s\S]+?)\n\$\$(?:\n|$)/
12
13
 
13
14
  const katexExtension: MarkedExtension = {
@@ -17,12 +18,16 @@ const katexExtension: MarkedExtension = {
17
18
  level: "inline",
18
19
  start(src) {
19
20
  const index = src.indexOf("\\(")
21
+
20
22
  if (index === -1) return
23
+
21
24
  return index
22
25
  },
23
26
  tokenizer(src) {
24
27
  const match = src.match(inlineMathRegex)
28
+
25
29
  if (!match) return
30
+
26
31
  return {
27
32
  type: "inlineKatex",
28
33
  raw: match[0],
@@ -37,7 +42,9 @@ const katexExtension: MarkedExtension = {
37
42
  level: "block",
38
43
  tokenizer(src) {
39
44
  const match = src.match(blockMathRegex)
45
+
40
46
  if (!match) return
47
+
41
48
  return {
42
49
  type: "blockKatex",
43
50
  raw: match[0],
@@ -17,8 +17,11 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext(
17
17
  langs: [],
18
18
  preferredHighlighter: "shiki-wasm",
19
19
  })
20
+
20
21
  const name = language in bundledLanguages ? language : "text"
22
+
21
23
  if (!highlighter.getLoadedLanguages().includes(name)) await highlighter.loadLanguage(name as BundledLanguage)
24
+
22
25
  return highlighter.codeToHtml(code, {
23
26
  lang: name,
24
27
  theme: "OpenCode",
@@ -15,6 +15,8 @@ export const WorkerPoolProvider = ctx.provider
15
15
 
16
16
  export function useWorkerPool(diffStyle: "unified" | "split" | undefined) {
17
17
  const pools = ctx.use()
18
+
18
19
  if (diffStyle === "split") return pools.split
20
+
19
21
  return pools.unified
20
22
  }
@@ -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}
@@ -8,7 +8,9 @@ const segmenter =
8
8
 
9
9
  function first(value: string) {
10
10
  if (!value) return ""
11
+
11
12
  if (!segmenter) return Array.from(value)[0] ?? ""
13
+
12
14
  return segmenter.segment(value)[Symbol.iterator]().next().value?.segment ?? Array.from(value)[0] ?? ""
13
15
  }
14
16
 
@@ -33,7 +35,9 @@ export function Avatar(props: AvatarProps) {
33
35
  "classList",
34
36
  "style",
35
37
  ])
38
+
36
39
  const src = split.src // did this so i can zero it out to test fallback
40
+
37
41
  return (
38
42
  <div
39
43
  {...rest}
@@ -8,6 +8,7 @@ export interface BadgeProps extends ComponentProps<"span"> {
8
8
 
9
9
  export function Badge(props: BadgeProps) {
10
10
  const [split, rest] = splitProps(props, ["class", "classList", "children", "appearance", "variant"])
11
+
11
12
  return (
12
13
  <span
13
14
  {...rest}
@@ -11,11 +11,13 @@ export function DiffChanges(props: {
11
11
  ? props.changes.reduce((acc, diff) => acc + (diff.additions ?? 0), 0)
12
12
  : props.changes.additions,
13
13
  )
14
+
14
15
  const deletions = createMemo(() =>
15
16
  Array.isArray(props.changes)
16
17
  ? props.changes.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0)
17
18
  : props.changes.deletions,
18
19
  )
20
+
19
21
  const total = createMemo(() => (additions() ?? 0) + (deletions() ?? 0))
20
22
 
21
23
  return (
@@ -8,6 +8,7 @@ export interface KeybindProps extends ComponentProps<"div"> {
8
8
 
9
9
  export function Keybind(props: KeybindProps) {
10
10
  const [local, rest] = splitProps(props, ["keys", "variant", "class", "classList"])
11
+
11
12
  return (
12
13
  <div
13
14
  {...rest}
@@ -35,6 +35,7 @@ export interface LineCommentProps extends ComponentProps<"div"> {
35
35
 
36
36
  export function LineComment(props: LineCommentProps) {
37
37
  const [local, rest] = splitProps(props, ["comment", "selection", "actions", "class", "classList"])
38
+
38
39
  return (
39
40
  <div
40
41
  {...rest}
@@ -78,11 +79,13 @@ export interface LineCommentEditorProps extends Omit<ComponentProps<"div">, "chi
78
79
 
79
80
  function pathFilename(path: string) {
80
81
  const index = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))
82
+
81
83
  return index === -1 ? path : path.slice(index + 1)
82
84
  }
83
85
 
84
86
  function pathDirectory(path: string) {
85
87
  const index = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))
88
+
86
89
  return index === -1 ? "" : path.slice(0, index + 1)
87
90
  }
88
91
 
@@ -118,12 +121,16 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
118
121
 
119
122
  const currentMention = () => {
120
123
  const textarea = textareaRef
124
+
121
125
  if (!textarea) return
126
+
122
127
  if (!local.mention) return
128
+
123
129
  if (textarea.selectionStart !== textarea.selectionEnd) return
124
130
 
125
131
  const end = textarea.selectionStart
126
132
  const match = textarea.value.slice(0, end).match(/@(\S*)$/)
133
+
127
134
  if (!match) return
128
135
 
129
136
  return {
@@ -138,6 +145,7 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
138
145
 
139
146
  const textarea = textareaRef
140
147
  const query = currentMention()
148
+
141
149
  if (!textarea || !query) return
142
150
 
143
151
  const value = `${textarea.value.slice(0, query.start)}@${item.path} ${textarea.value.slice(query.end)}`
@@ -155,8 +163,10 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
155
163
  const mention = useFilteredList<{ path: string }>({
156
164
  items: async (query) => {
157
165
  if (!local.mention) return []
166
+
158
167
  if (!query.trim()) return []
159
168
  const paths = await local.mention.items(query)
169
+
160
170
  return paths.map((path) => ({ path }))
161
171
  },
162
172
  key: (item) => item.path,
@@ -167,8 +177,10 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
167
177
 
168
178
  const syncMention = () => {
169
179
  const item = currentMention()
180
+
170
181
  if (!item) {
171
182
  closeMention()
183
+
172
184
  return
173
185
  }
174
186
 
@@ -178,6 +190,7 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
178
190
 
179
191
  const selectActiveMention = () => {
180
192
  const items = mention.flat()
193
+
181
194
  if (items.length === 0) return
182
195
  const active = mention.active()
183
196
  selectMention(items.find((item) => item.path === active) ?? items[0])
@@ -185,6 +198,7 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
185
198
 
186
199
  const submit = () => {
187
200
  const v = local.value.trim()
201
+
188
202
  if (!v) return
189
203
  local.onSubmit(v)
190
204
  }
@@ -225,12 +239,14 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
225
239
  onSelect={() => syncMention()}
226
240
  onKeyDown={(e) => {
227
241
  e.stopPropagation()
242
+
228
243
  if (e.isComposing || e.keyCode === 229) return
229
244
 
230
245
  if (mentionOpen()) {
231
246
  if (e.key === "Escape") {
232
247
  e.preventDefault()
233
248
  closeMention()
249
+
234
250
  return
235
251
  }
236
252
 
@@ -238,14 +254,17 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
238
254
  if (mention.flat().length === 0) return
239
255
  e.preventDefault()
240
256
  selectActiveMention()
257
+
241
258
  return
242
259
  }
243
260
 
244
261
  const nav = e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Enter"
245
262
  const ctrlNav = e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && (e.key === "n" || e.key === "p")
263
+
246
264
  if ((nav || ctrlNav) && mention.flat().length > 0) {
247
265
  mention.onKeyDown(e)
248
266
  e.preventDefault()
267
+
249
268
  return
250
269
  }
251
270
  }
@@ -254,8 +273,10 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
254
273
  e.preventDefault()
255
274
  e.currentTarget.blur()
256
275
  local.onCancel()
276
+
257
277
  return
258
278
  }
279
+
259
280
  if (e.key === "Enter" && !e.shiftKey) {
260
281
  e.preventDefault()
261
282
  submit()
@@ -268,6 +289,7 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
268
289
  {(item) => {
269
290
  const directory = item.path.endsWith("/") ? item.path : pathDirectory(item.path)
270
291
  const name = item.path.endsWith("/") ? "" : pathFilename(item.path)
292
+
271
293
  return (
272
294
  <button
273
295
  type="button"
@@ -8,7 +8,9 @@ const segmenter =
8
8
 
9
9
  function first(value: string) {
10
10
  if (!value) return ""
11
+
11
12
  if (!segmenter) return Array.from(value)[0] ?? ""
13
+
12
14
  return segmenter.segment(value)[Symbol.iterator]().next().value?.segment ?? Array.from(value)[0] ?? ""
13
15
  }
14
16
 
@@ -33,14 +35,19 @@ const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
33
35
 
34
36
  export function getProjectAvatarSource(id?: string, icon?: { color?: string; url?: string; override?: string }) {
35
37
  if (id === OPENCODE_PROJECT_ID) return "https://opencode.ai/favicon.svg"
38
+
36
39
  if (icon?.override) return icon.override
40
+
37
41
  if (icon?.color) return undefined
42
+
38
43
  return icon?.url
39
44
  }
40
45
 
41
46
  export function getProjectAvatarVariant(key?: string): ProjectAvatarVariant {
42
47
  if (key === "mint") return "cyan"
48
+
43
49
  if (key === "lime") return "green"
50
+
44
51
  if (
45
52
  key === "orange" ||
46
53
  key === "yellow" ||
@@ -53,12 +60,14 @@ export function getProjectAvatarVariant(key?: string): ProjectAvatarVariant {
53
60
  key === "gray"
54
61
  )
55
62
  return key
63
+
56
64
  return "gray"
57
65
  }
58
66
 
59
67
  export const displayName = (project: { name?: string; worktree: string }) => {
60
68
  const trimmed = project.worktree.replace(/[/\\]+$/, "")
61
69
  const filename = trimmed.slice(Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\")) + 1)
70
+
62
71
  return project.name || filename || project.worktree
63
72
  }
64
73
 
@@ -71,6 +80,7 @@ export interface ProjectAvatarProps extends ComponentProps<"div"> {
71
80
 
72
81
  export function ProjectAvatar(props: ProjectAvatarProps) {
73
82
  const [split, rest] = splitProps(props, ["fallback", "src", "variant", "unread", "class", "classList", "style"])
83
+
74
84
  return (
75
85
  <div
76
86
  {...rest}
@@ -3,6 +3,7 @@ import "./loader.css"
3
3
 
4
4
  export function Loader(props: ComponentProps<"svg">) {
5
5
  const [local, rest] = splitProps(props, ["class", "classList", "width", "height"])
6
+
6
7
  return (
7
8
  <svg
8
9
  {...rest}
@@ -5,5 +5,6 @@ export const toastDescriptionLimit = 2000
5
5
 
6
6
  export function boundToastDescription<T extends { description?: string }>(options: T): T {
7
7
  if (!options.description || options.description.length <= toastDescriptionLimit) return options
8
+
8
9
  return { ...options, description: `${options.description.slice(0, toastDescriptionLimit)}…` }
9
10
  }
@@ -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 } as JSX.CSSProperties}
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 (typeof local.onClick === "function") local.onClick(event)
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?: () => unknown
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
- icon?: JSX.Element
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
- icon: entry.options.icon,
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
- if (typeof onClick === "function") onClick()
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
 
@@ -10,6 +10,7 @@ export interface CheckboxProps extends ParentProps<ComponentProps<typeof Root>>
10
10
 
11
11
  export function Checkbox(props: CheckboxProps) {
12
12
  const [local, others] = splitProps(props, ["children", "class", "label", "hideLabel", "description", "icon"])
13
+
13
14
  return (
14
15
  <Root {...others} data-component="checkbox">
15
16
  <Input data-slot="checkbox-checkbox-input" />
@@ -31,9 +31,11 @@ const FieldContext = createContext<FieldContextValue>()
31
31
 
32
32
  function useField() {
33
33
  const ctx = useContext(FieldContext)
34
+
34
35
  if (!ctx) {
35
36
  throw new Error("Field subcomponents must be used within <Field>")
36
37
  }
38
+
37
39
  return ctx
38
40
  }
39
41
 
@@ -72,17 +74,22 @@ function FieldRoot(props: ParentProps<FieldProps>) {
72
74
  unregisterSuffix: () => setSuffixCount((n) => Math.max(0, n - 1)),
73
75
  getDescribedBy: () => {
74
76
  const ids: string[] = []
77
+
75
78
  if (prefixCount() > 0) ids.push(prefixId)
79
+
76
80
  if (suffixCount() > 0) ids.push(suffixId)
81
+
77
82
  return ids.length > 0 ? ids.join(" ") : undefined
78
83
  },
79
84
  }
80
85
 
81
86
  const syncControlA11y = () => {
82
87
  const root = rootRef
88
+
83
89
  if (!root) return
84
90
 
85
91
  const control = root.querySelector(CONTROL_SELECTOR) as HTMLInputElement | HTMLTextAreaElement | null
92
+
86
93
  if (!control) return
87
94
 
88
95
  const shell = control.closest(
@@ -93,6 +100,7 @@ function FieldRoot(props: ParentProps<FieldProps>) {
93
100
  control.setAttribute("aria-labelledby", labelId)
94
101
 
95
102
  const describedBy = ctx.getDescribedBy()
103
+
96
104
  if (describedBy) {
97
105
  control.setAttribute("aria-describedby", describedBy)
98
106
  } else {
@@ -10,11 +10,15 @@ export function InlineInput(props: InlineInputProps) {
10
10
 
11
11
  const style = () => {
12
12
  if (!local.style) return { width: local.width }
13
+
13
14
  if (typeof local.style === "string") {
14
15
  if (!local.width) return local.style
16
+
15
17
  return `${local.style};width:${local.width}`
16
18
  }
19
+
17
20
  if (!local.width) return local.style
21
+
18
22
  return { ...local.style, width: local.width }
19
23
  }
20
24
 
@@ -21,6 +21,7 @@ export interface RadioGroupProps extends ParentProps<ComponentProps<typeof Root>
21
21
 
22
22
  export function RadioGroup(props: RadioGroupProps) {
23
23
  const [local, others] = splitProps(props, ["class", "classList", "children", "label", "description", "hideLabel"])
24
+
24
25
  return (
25
26
  <Root
26
27
  {...others}
@@ -54,6 +55,7 @@ export interface RadioItemProps extends ComponentProps<typeof Item> {
54
55
 
55
56
  export function RadioItem(props: RadioItemProps) {
56
57
  const [local, others] = splitProps(props, ["class", "classList", "label", "description", "hideLabel"])
58
+
57
59
  return (
58
60
  <Item
59
61
  {...others}