@opencode/ui 0.0.0-dev-20607 → 0.0.0-dev-20610

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 (68) hide show
  1. package/package.json +1 -1
  2. package/src/actions/button/button.tsx +1 -0
  3. package/src/actions/icon-button/icon-button.tsx +1 -0
  4. package/src/actions/split-button/split-button.tsx +3 -0
  5. package/src/components/animated-number.tsx +13 -0
  6. package/src/components/app-icon.tsx +2 -0
  7. package/src/components/card.tsx +21 -0
  8. package/src/components/collapsible.tsx +1 -0
  9. package/src/components/context-menu.tsx +34 -0
  10. package/src/components/dock-surface.tsx +3 -0
  11. package/src/components/file-icon.tsx +12 -0
  12. package/src/components/image-preview.tsx +1 -0
  13. package/src/components/list.tsx +32 -0
  14. package/src/components/motion-spring.tsx +5 -0
  15. package/src/components/popover.tsx +12 -0
  16. package/src/components/provider-icon.tsx +1 -0
  17. package/src/components/resize-handle.tsx +8 -0
  18. package/src/components/scroll-view.tsx +39 -0
  19. package/src/components/spinner.tsx +2 -0
  20. package/src/components/text-field.tsx +6 -0
  21. package/src/components/text-reveal.tsx +19 -0
  22. package/src/components/text-strikethrough.tsx +8 -0
  23. package/src/components/typewriter.tsx +4 -0
  24. package/src/context/dialog.tsx +12 -0
  25. package/src/context/file.tsx +1 -0
  26. package/src/context/helper.tsx +4 -0
  27. package/src/context/i18n.tsx +20 -0
  28. package/src/context/index.ts +3 -0
  29. package/src/context/marked-base.tsx +7 -0
  30. package/src/context/marked-parser.tsx +7 -0
  31. package/src/context/marked.tsx +3 -0
  32. package/src/context/worker-pool.tsx +2 -0
  33. package/src/data-display/avatar/avatar.tsx +4 -0
  34. package/src/data-display/badge/badge.tsx +1 -0
  35. package/src/data-display/diff-changes/diff-changes.tsx +2 -0
  36. package/src/data-display/keybind/keybind.tsx +1 -0
  37. package/src/data-display/line-comment/line-comment.tsx +22 -0
  38. package/src/data-display/project-avatar/project-avatar.tsx +10 -0
  39. package/src/feedback/loader/loader.tsx +1 -0
  40. package/src/feedback/toast/description.ts +1 -0
  41. package/src/forms/checkbox/checkbox.tsx +1 -0
  42. package/src/forms/field/field.tsx +8 -0
  43. package/src/forms/inline-input/inline-input.tsx +4 -0
  44. package/src/forms/radio/radio.tsx +2 -0
  45. package/src/forms/select/select.tsx +11 -0
  46. package/src/forms/switch/switch.tsx +1 -0
  47. package/src/forms/text-input/text-input.tsx +3 -0
  48. package/src/hooks/create-auto-scroll.tsx +34 -0
  49. package/src/hooks/index.ts +1 -0
  50. package/src/hooks/use-filtered-list.tsx +14 -0
  51. package/src/i18n/no.ts +1 -0
  52. package/src/layout/divider/divider.tsx +1 -0
  53. package/src/navigation/menu/menu.tsx +13 -0
  54. package/src/navigation/segmented-control/segmented-control.tsx +16 -0
  55. package/src/navigation/tab-state-indicator.tsx +1 -0
  56. package/src/navigation/tabs/tabs.tsx +18 -0
  57. package/src/overlays/dialog/dialog.tsx +3 -0
  58. package/src/overlays/tooltip/tooltip.tsx +15 -0
  59. package/src/storybook/scaffold.tsx +3 -0
  60. package/src/theme/color.ts +27 -0
  61. package/src/theme/context.tsx +58 -0
  62. package/src/theme/default-themes.ts +35 -0
  63. package/src/theme/index.ts +3 -0
  64. package/src/theme/loader.ts +11 -0
  65. package/src/theme/resolve.ts +28 -0
  66. package/src/theme/v2/foreground.ts +8 -0
  67. package/src/theme/v2/resolve.ts +10 -0
  68. package/src/typography/text-shimmer/text-shimmer.tsx +1 -0
@@ -4,14 +4,21 @@ import { dict as en } from "../i18n/en"
4
4
  import type { Key, LocaleKey, PluralCategory, PluralKey, PluralLookupKey } from "../i18n/en"
5
5
 
6
6
  export type UiI18nKey = Key
7
+
7
8
  export type UiI18nPluralKey = PluralKey
9
+
8
10
  export type UiPluralCategory = PluralCategory
11
+
9
12
  export type UiI18nPluralLookupKey = PluralLookupKey
13
+
10
14
  export type UiI18nLocaleKey = LocaleKey
15
+
11
16
  type UiTranslationKey<Value extends string> = Value extends UiI18nPluralLookupKey ? never : Value
17
+
12
18
  export type UiI18nOrdinaryKey = UiTranslationKey<UiI18nKey>
13
19
 
14
20
  export type UiI18nParams = Record<string, string | number | boolean>
21
+
15
22
  export type UiTranslate = <Value extends string>(key: UiTranslationKey<Value>, params?: UiI18nParams) => string
16
23
 
17
24
  export type UiI18nSource = {
@@ -33,10 +40,13 @@ const rules = new Map<string, Intl.PluralRules>()
33
40
 
34
41
  export function pluralCategory(locale: string, count: number): UiPluralCategory {
35
42
  const cached = rules.get(locale)
43
+
36
44
  if (cached) return cached.select(count)
37
45
  const next = new Intl.PluralRules(locale)
46
+
38
47
  if (rules.size >= 32) rules.delete(rules.keys().next().value!)
39
48
  rules.set(locale, next)
49
+
40
50
  return next.select(count)
41
51
  }
42
52
 
@@ -46,13 +56,17 @@ export function pluralKey(key: UiI18nPluralKey, category: UiPluralCategory) {
46
56
 
47
57
  export function localizedListSeparator(locale: string, index: number, count: number) {
48
58
  if (index <= 0 || index >= count) return ""
59
+
49
60
  const parts = new Intl.ListFormat(locale, { style: "long", type: "conjunction" }).formatToParts(
50
61
  Array.from({ length: count }, (_, item) => String(item)),
51
62
  )
63
+
52
64
  const current = parts.findIndex((part) => part.type === "element" && part.value === String(index))
65
+
53
66
  const previous = parts.findLastIndex(
54
67
  (part, partIndex) => partIndex < current && part.type === "element" && part.value === String(index - 1),
55
68
  )
69
+
56
70
  return parts
57
71
  .slice(previous + 1, current)
58
72
  .map((part) => part.value)
@@ -61,9 +75,11 @@ export function localizedListSeparator(locale: string, index: number, count: num
61
75
 
62
76
  function resolveTemplate(text: string, params?: UiI18nParams) {
63
77
  if (!params) return text
78
+
64
79
  return text.replace(/{{\s*([^}]+?)\s*}}/g, (_, rawKey) => {
65
80
  const key = String(rawKey)
66
81
  const value = params[key]
82
+
67
83
  return value === undefined ? "" : String(value)
68
84
  })
69
85
  }
@@ -91,6 +107,7 @@ const fallbackSource: UiI18nSource = {
91
107
  locale: () => "en",
92
108
  t: (key, params) => {
93
109
  const value = en[key as UiI18nKey] ?? String(key)
110
+
94
111
  return resolveTemplate(value, params)
95
112
  },
96
113
  plural: (key, count, params) =>
@@ -98,15 +115,18 @@ const fallbackSource: UiI18nSource = {
98
115
  pluralForm: (key, category, params) => {
99
116
  const values = en as Partial<Record<UiI18nLocaleKey, string>>
100
117
  const value = values[pluralKey(key, category)] ?? values[`${key}.other`] ?? `${key}.other`
118
+
101
119
  return resolveTemplate(value, params)
102
120
  },
103
121
  }
122
+
104
123
  const fallback = createUiI18n(fallbackSource)
105
124
 
106
125
  const Context = createContext<UiI18n>(fallback)
107
126
 
108
127
  function UiI18nProvider(props: ParentProps<{ value: UiI18nSource }>) {
109
128
  const value = createUiI18n(props.value)
129
+
110
130
  return (
111
131
  <I18nProvider locale={(value.layoutLocale ?? value.locale)()}>
112
132
  <Context.Provider value={value}>{props.children}</Context.Provider>
@@ -1,4 +1,7 @@
1
1
  export * from "./helper"
2
+
2
3
  export * from "./file"
4
+
3
5
  export * from "./dialog"
6
+
4
7
  export * from "./i18n"
@@ -5,6 +5,7 @@ export function createMarkdownBase() {
5
5
  renderer: {
6
6
  link({ href, title, text }) {
7
7
  const titleAttr = title ? ` title="${title}"` : ""
8
+
8
9
  return `<a href="${href}"${titleAttr} class="external-link" target="_blank" rel="noopener noreferrer">${text}</a>`
9
10
  },
10
11
  },
@@ -16,6 +17,7 @@ let smallParser: Marked | undefined
16
17
  export function parseSmallMarkdown(text: string) {
17
18
  // Any possible KaTeX delimiter stays on the worker, including escaped ones.
18
19
  if (text.length > 1024 || text.includes("\\(") || text.includes("$$")) return
20
+
19
21
  // Ordinary prose does not need the block/inline lexer's cold regular expressions.
20
22
  // Keep every possible Markdown construct, autolink, and hard break on that lexer.
21
23
  if (
@@ -30,8 +32,10 @@ export function parseSmallMarkdown(text: string) {
30
32
  .map((paragraph) => `<p>${paragraph.replace(/["']/g, (value) => (value === '"' ? "&quot;" : "&#39;"))}</p>\n`)
31
33
  .join("")
32
34
  }
35
+
33
36
  const parser = (smallParser ??= createMarkdownBase())
34
37
  const paragraphs = text.replace(/\r\n?/g, "\n")
38
+
35
39
  // Inline formatting in ordinary paragraphs does not need the block lexer.
36
40
  // Any possible block opener, HTML, table, or reference definition stays on it.
37
41
  if (
@@ -45,11 +49,14 @@ export function parseSmallMarkdown(text: string) {
45
49
  .map((paragraph) => `<p>${parser.parseInline(paragraph, { async: false })}</p>\n`)
46
50
  .join("")
47
51
  }
52
+
48
53
  const tokens = parser.lexer(text)
49
54
  let code = false
50
55
  parser.walkTokens(tokens, (token) => {
51
56
  if (token.type === "code") code = true
52
57
  })
58
+
53
59
  if (code) return
60
+
54
61
  return parser.parser(tokens)
55
62
  }
@@ -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
  }
@@ -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
  }
@@ -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}
@@ -17,13 +17,17 @@ function groupOptions<T>(options: T[], groupBy?: (x: T) => string): { category:
17
17
  if (!groupBy) {
18
18
  return [{ category: "", options }]
19
19
  }
20
+
20
21
  const map = new Map<string, T[]>()
22
+
21
23
  for (const opt of options) {
22
24
  const key = groupBy(opt)
23
25
  const arr = map.get(key)
26
+
24
27
  if (arr) arr.push(opt)
25
28
  else map.set(key, [opt])
26
29
  }
30
+
27
31
  return [...map.entries()].map(([category, opts]) => ({ category, options: opts }))
28
32
  }
29
33
 
@@ -110,11 +114,15 @@ export function Select<T>(props: SelectProps<T>) {
110
114
 
111
115
  const move = (item: T | undefined) => {
112
116
  if (!local.onHighlight) return
117
+
113
118
  if (!item) {
114
119
  stop()
120
+
115
121
  return
116
122
  }
123
+
117
124
  const key = keyFor(item)
125
+
118
126
  if (state.key === key) return
119
127
  state.cleanup?.()
120
128
  state.cleanup = local.onHighlight(item)
@@ -178,6 +186,7 @@ export function Select<T>(props: SelectProps<T>) {
178
186
  }}
179
187
  onOpenChange={(open) => {
180
188
  local.onOpenChange?.(open)
189
+
181
190
  if (!open) stop()
182
191
  }}
183
192
  >
@@ -200,7 +209,9 @@ export function Select<T>(props: SelectProps<T>) {
200
209
  <Value<T> data-slot="select-v2-value-text" class={local.valueClass}>
201
210
  {(st) => {
202
211
  const selected = st.selectedOption()
212
+
203
213
  if (local.label && selected != null) return local.label(selected)
214
+
204
215
  return selected != null ? (selected as string) : ""
205
216
  }}
206
217
  </Value>
@@ -12,6 +12,7 @@ export interface SwitchProps extends ParentProps<ComponentProps<typeof Root>> {
12
12
 
13
13
  export function Switch(props: SwitchProps) {
14
14
  const [local, others] = splitProps(props, ["children", "class", "appearance", "hideLabel", "description"])
15
+
15
16
  return (
16
17
  <Root {...others} class={local.class} data-component="switch" data-appearance={local.appearance ?? "compact"}>
17
18
  <Input data-slot="switch-input" />
@@ -29,6 +29,7 @@ export interface TextInputProps extends Omit<ComponentProps<"input">, "type"> {
29
29
 
30
30
  export function TextInput(props: TextInputProps) {
31
31
  const i18n = useI18n()
32
+
32
33
  const [local, inputProps] = splitProps(props, [
33
34
  "class",
34
35
  "classList",
@@ -89,8 +90,10 @@ export function TextInput(props: TextInputProps) {
89
90
  onClick={(event) => {
90
91
  if (local.showClearButton) {
91
92
  local.onClearClick?.(event)
93
+
92
94
  return
93
95
  }
96
+
94
97
  local.onCopyClick?.(event)
95
98
  }}
96
99
  >