@opencode-ai/ui 0.0.0-beta-202606251302

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 (346) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +110 -0
  3. package/src/assets/audio/alert-01.aac +0 -0
  4. package/src/assets/audio/alert-01.mp3 +0 -0
  5. package/src/assets/audio/alert-02.aac +0 -0
  6. package/src/assets/audio/alert-02.mp3 +0 -0
  7. package/src/assets/audio/alert-03.aac +0 -0
  8. package/src/assets/audio/alert-03.mp3 +0 -0
  9. package/src/assets/audio/alert-04.aac +0 -0
  10. package/src/assets/audio/alert-04.mp3 +0 -0
  11. package/src/assets/audio/alert-05.aac +0 -0
  12. package/src/assets/audio/alert-05.mp3 +0 -0
  13. package/src/assets/audio/alert-06.aac +0 -0
  14. package/src/assets/audio/alert-06.mp3 +0 -0
  15. package/src/assets/audio/alert-07.aac +0 -0
  16. package/src/assets/audio/alert-07.mp3 +0 -0
  17. package/src/assets/audio/alert-08.aac +0 -0
  18. package/src/assets/audio/alert-08.mp3 +0 -0
  19. package/src/assets/audio/alert-09.aac +0 -0
  20. package/src/assets/audio/alert-09.mp3 +0 -0
  21. package/src/assets/audio/alert-10.aac +0 -0
  22. package/src/assets/audio/alert-10.mp3 +0 -0
  23. package/src/assets/audio/bip-bop-01.aac +0 -0
  24. package/src/assets/audio/bip-bop-01.mp3 +0 -0
  25. package/src/assets/audio/bip-bop-02.aac +0 -0
  26. package/src/assets/audio/bip-bop-02.mp3 +0 -0
  27. package/src/assets/audio/bip-bop-03.aac +0 -0
  28. package/src/assets/audio/bip-bop-03.mp3 +0 -0
  29. package/src/assets/audio/bip-bop-04.aac +0 -0
  30. package/src/assets/audio/bip-bop-04.mp3 +0 -0
  31. package/src/assets/audio/bip-bop-05.aac +0 -0
  32. package/src/assets/audio/bip-bop-05.mp3 +0 -0
  33. package/src/assets/audio/bip-bop-06.aac +0 -0
  34. package/src/assets/audio/bip-bop-06.mp3 +0 -0
  35. package/src/assets/audio/bip-bop-07.aac +0 -0
  36. package/src/assets/audio/bip-bop-07.mp3 +0 -0
  37. package/src/assets/audio/bip-bop-08.aac +0 -0
  38. package/src/assets/audio/bip-bop-08.mp3 +0 -0
  39. package/src/assets/audio/bip-bop-09.aac +0 -0
  40. package/src/assets/audio/bip-bop-09.mp3 +0 -0
  41. package/src/assets/audio/bip-bop-10.aac +0 -0
  42. package/src/assets/audio/bip-bop-10.mp3 +0 -0
  43. package/src/assets/audio/nope-01.aac +0 -0
  44. package/src/assets/audio/nope-01.mp3 +0 -0
  45. package/src/assets/audio/nope-02.aac +0 -0
  46. package/src/assets/audio/nope-02.mp3 +0 -0
  47. package/src/assets/audio/nope-03.aac +0 -0
  48. package/src/assets/audio/nope-03.mp3 +0 -0
  49. package/src/assets/audio/nope-04.aac +0 -0
  50. package/src/assets/audio/nope-04.mp3 +0 -0
  51. package/src/assets/audio/nope-05.aac +0 -0
  52. package/src/assets/audio/nope-05.mp3 +0 -0
  53. package/src/assets/audio/nope-06.aac +0 -0
  54. package/src/assets/audio/nope-06.mp3 +0 -0
  55. package/src/assets/audio/nope-07.aac +0 -0
  56. package/src/assets/audio/nope-07.mp3 +0 -0
  57. package/src/assets/audio/nope-08.aac +0 -0
  58. package/src/assets/audio/nope-08.mp3 +0 -0
  59. package/src/assets/audio/nope-09.aac +0 -0
  60. package/src/assets/audio/nope-09.mp3 +0 -0
  61. package/src/assets/audio/nope-10.aac +0 -0
  62. package/src/assets/audio/nope-10.mp3 +0 -0
  63. package/src/assets/audio/nope-11.aac +0 -0
  64. package/src/assets/audio/nope-11.mp3 +0 -0
  65. package/src/assets/audio/nope-12.aac +0 -0
  66. package/src/assets/audio/nope-12.mp3 +0 -0
  67. package/src/assets/audio/staplebops-01.aac +0 -0
  68. package/src/assets/audio/staplebops-01.mp3 +0 -0
  69. package/src/assets/audio/staplebops-02.aac +0 -0
  70. package/src/assets/audio/staplebops-02.mp3 +0 -0
  71. package/src/assets/audio/staplebops-03.aac +0 -0
  72. package/src/assets/audio/staplebops-03.mp3 +0 -0
  73. package/src/assets/audio/staplebops-04.aac +0 -0
  74. package/src/assets/audio/staplebops-04.mp3 +0 -0
  75. package/src/assets/audio/staplebops-05.aac +0 -0
  76. package/src/assets/audio/staplebops-05.mp3 +0 -0
  77. package/src/assets/audio/staplebops-06.aac +0 -0
  78. package/src/assets/audio/staplebops-06.mp3 +0 -0
  79. package/src/assets/audio/staplebops-07.aac +0 -0
  80. package/src/assets/audio/staplebops-07.mp3 +0 -0
  81. package/src/assets/audio/yup-01.aac +0 -0
  82. package/src/assets/audio/yup-01.mp3 +0 -0
  83. package/src/assets/audio/yup-02.aac +0 -0
  84. package/src/assets/audio/yup-02.mp3 +0 -0
  85. package/src/assets/audio/yup-03.aac +0 -0
  86. package/src/assets/audio/yup-03.mp3 +0 -0
  87. package/src/assets/audio/yup-04.aac +0 -0
  88. package/src/assets/audio/yup-04.mp3 +0 -0
  89. package/src/assets/audio/yup-05.aac +0 -0
  90. package/src/assets/audio/yup-05.mp3 +0 -0
  91. package/src/assets/audio/yup-06.aac +0 -0
  92. package/src/assets/audio/yup-06.mp3 +0 -0
  93. package/src/assets/fonts/Inter.ttf +0 -0
  94. package/src/assets/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  95. package/src/assets/icons/app/android-studio.svg +369 -0
  96. package/src/assets/icons/app/antigravity.svg +97 -0
  97. package/src/assets/icons/app/cursor.svg +16 -0
  98. package/src/assets/icons/app/file-explorer.svg +20 -0
  99. package/src/assets/icons/app/finder.png +0 -0
  100. package/src/assets/icons/app/ghostty.svg +13 -0
  101. package/src/assets/icons/app/iterm2.svg +13 -0
  102. package/src/assets/icons/app/powershell.svg +14 -0
  103. package/src/assets/icons/app/sublimetext.svg +17 -0
  104. package/src/assets/icons/app/terminal.png +0 -0
  105. package/src/assets/icons/app/textmate.png +0 -0
  106. package/src/assets/icons/app/vscode.svg +39 -0
  107. package/src/assets/icons/app/warp.png +0 -0
  108. package/src/assets/icons/app/xcode.png +0 -0
  109. package/src/assets/icons/app/zed-dark.svg +15 -0
  110. package/src/assets/icons/app/zed.svg +15 -0
  111. package/src/components/accordion.css +123 -0
  112. package/src/components/accordion.tsx +92 -0
  113. package/src/components/animated-number.css +75 -0
  114. package/src/components/animated-number.tsx +109 -0
  115. package/src/components/app-icon.css +5 -0
  116. package/src/components/app-icon.tsx +85 -0
  117. package/src/components/app-icons/sprite.svg +114 -0
  118. package/src/components/app-icons/types.ts +21 -0
  119. package/src/components/avatar.css +49 -0
  120. package/src/components/avatar.tsx +55 -0
  121. package/src/components/button.css +194 -0
  122. package/src/components/button.tsx +33 -0
  123. package/src/components/card.css +94 -0
  124. package/src/components/card.tsx +123 -0
  125. package/src/components/checkbox.css +131 -0
  126. package/src/components/checkbox.tsx +43 -0
  127. package/src/components/collapsible.css +148 -0
  128. package/src/components/collapsible.tsx +48 -0
  129. package/src/components/context-menu.css +134 -0
  130. package/src/components/context-menu.tsx +308 -0
  131. package/src/components/dialog.css +181 -0
  132. package/src/components/dialog.tsx +72 -0
  133. package/src/components/diff-changes.css +42 -0
  134. package/src/components/diff-changes.tsx +115 -0
  135. package/src/components/dock-surface.css +23 -0
  136. package/src/components/dock-surface.tsx +54 -0
  137. package/src/components/dropdown-menu.css +135 -0
  138. package/src/components/dropdown-menu.tsx +308 -0
  139. package/src/components/favicon.tsx +13 -0
  140. package/src/components/file-icon.css +26 -0
  141. package/src/components/file-icon.tsx +588 -0
  142. package/src/components/file-icons/sprite.svg +11707 -0
  143. package/src/components/file-icons/types.ts +1095 -0
  144. package/src/components/font.tsx +1 -0
  145. package/src/components/hover-card.css +61 -0
  146. package/src/components/hover-card.tsx +32 -0
  147. package/src/components/icon-button.css +181 -0
  148. package/src/components/icon-button.tsx +29 -0
  149. package/src/components/icon.css +34 -0
  150. package/src/components/icon.tsx +169 -0
  151. package/src/components/image-preview.css +63 -0
  152. package/src/components/image-preview.tsx +32 -0
  153. package/src/components/inline-input.css +17 -0
  154. package/src/components/inline-input.tsx +22 -0
  155. package/src/components/keybind.css +18 -0
  156. package/src/components/keybind.tsx +20 -0
  157. package/src/components/list.css +331 -0
  158. package/src/components/list.tsx +394 -0
  159. package/src/components/logo.css +4 -0
  160. package/src/components/logo.tsx +62 -0
  161. package/src/components/motion-spring.tsx +58 -0
  162. package/src/components/popover.css +98 -0
  163. package/src/components/popover.tsx +153 -0
  164. package/src/components/progress-circle.css +12 -0
  165. package/src/components/progress-circle.tsx +57 -0
  166. package/src/components/progress.css +63 -0
  167. package/src/components/progress.tsx +39 -0
  168. package/src/components/provider-icon.css +5 -0
  169. package/src/components/provider-icon.tsx +25 -0
  170. package/src/components/provider-icons/sprite.svg +1135 -0
  171. package/src/components/provider-icons/types.ts +105 -0
  172. package/src/components/radio-group.css +187 -0
  173. package/src/components/radio-group.tsx +83 -0
  174. package/src/components/resize-handle.css +58 -0
  175. package/src/components/resize-handle.tsx +82 -0
  176. package/src/components/scroll-view.css +66 -0
  177. package/src/components/scroll-view.tsx +250 -0
  178. package/src/components/select.css +202 -0
  179. package/src/components/select.tsx +174 -0
  180. package/src/components/spinner.css +6 -0
  181. package/src/components/spinner.tsx +52 -0
  182. package/src/components/sticky-accordion-header.css +6 -0
  183. package/src/components/sticky-accordion-header.tsx +18 -0
  184. package/src/components/switch.css +132 -0
  185. package/src/components/switch.tsx +29 -0
  186. package/src/components/tabs.css +635 -0
  187. package/src/components/tabs.tsx +125 -0
  188. package/src/components/tag.css +37 -0
  189. package/src/components/tag.tsx +22 -0
  190. package/src/components/text-field.css +134 -0
  191. package/src/components/text-field.tsx +128 -0
  192. package/src/components/text-reveal.css +150 -0
  193. package/src/components/text-reveal.tsx +143 -0
  194. package/src/components/text-shimmer.css +119 -0
  195. package/src/components/text-shimmer.tsx +62 -0
  196. package/src/components/text-strikethrough.css +27 -0
  197. package/src/components/text-strikethrough.tsx +84 -0
  198. package/src/components/toast.css +236 -0
  199. package/src/components/toast.tsx +185 -0
  200. package/src/components/tooltip.css +74 -0
  201. package/src/components/tooltip.tsx +161 -0
  202. package/src/components/typewriter.css +14 -0
  203. package/src/components/typewriter.tsx +55 -0
  204. package/src/context/dialog.tsx +197 -0
  205. package/src/context/file.tsx +10 -0
  206. package/src/context/helper.tsx +38 -0
  207. package/src/context/i18n.tsx +38 -0
  208. package/src/context/index.ts +4 -0
  209. package/src/context/marked.tsx +522 -0
  210. package/src/context/worker-pool.tsx +20 -0
  211. package/src/custom-elements.d.ts +17 -0
  212. package/src/hooks/create-auto-scroll.tsx +237 -0
  213. package/src/hooks/index.ts +2 -0
  214. package/src/hooks/use-filtered-list.tsx +134 -0
  215. package/src/i18n/ar.ts +168 -0
  216. package/src/i18n/br.ts +168 -0
  217. package/src/i18n/bs.ts +172 -0
  218. package/src/i18n/da.ts +167 -0
  219. package/src/i18n/de.ts +173 -0
  220. package/src/i18n/en.ts +176 -0
  221. package/src/i18n/es.ts +168 -0
  222. package/src/i18n/fr.ts +168 -0
  223. package/src/i18n/ja.ts +167 -0
  224. package/src/i18n/ko.ts +168 -0
  225. package/src/i18n/no.ts +171 -0
  226. package/src/i18n/pl.ts +167 -0
  227. package/src/i18n/ru.ts +167 -0
  228. package/src/i18n/th.ts +169 -0
  229. package/src/i18n/tr.ts +174 -0
  230. package/src/i18n/uk.ts +167 -0
  231. package/src/i18n/zh.ts +171 -0
  232. package/src/i18n/zht.ts +171 -0
  233. package/src/storybook/fixtures.ts +51 -0
  234. package/src/storybook/scaffold.tsx +62 -0
  235. package/src/styles/animations.css +141 -0
  236. package/src/styles/base.css +404 -0
  237. package/src/styles/colors.css +772 -0
  238. package/src/styles/index.css +53 -0
  239. package/src/styles/tailwind/colors.css +285 -0
  240. package/src/styles/tailwind/index.css +78 -0
  241. package/src/styles/tailwind/utilities.css +131 -0
  242. package/src/styles/theme.css +609 -0
  243. package/src/styles/utilities.css +118 -0
  244. package/src/theme/color.ts +299 -0
  245. package/src/theme/context.tsx +370 -0
  246. package/src/theme/default-themes.ts +116 -0
  247. package/src/theme/index.ts +78 -0
  248. package/src/theme/loader.ts +112 -0
  249. package/src/theme/resolve.ts +540 -0
  250. package/src/theme/themes/amoled.json +49 -0
  251. package/src/theme/themes/aura.json +51 -0
  252. package/src/theme/themes/ayu.json +51 -0
  253. package/src/theme/themes/carbonfox.json +53 -0
  254. package/src/theme/themes/catppuccin-frappe.json +85 -0
  255. package/src/theme/themes/catppuccin-macchiato.json +85 -0
  256. package/src/theme/themes/catppuccin.json +45 -0
  257. package/src/theme/themes/cobalt2.json +87 -0
  258. package/src/theme/themes/cursor.json +91 -0
  259. package/src/theme/themes/dracula.json +49 -0
  260. package/src/theme/themes/everforest.json +89 -0
  261. package/src/theme/themes/flexoki.json +86 -0
  262. package/src/theme/themes/github.json +85 -0
  263. package/src/theme/themes/gruvbox.json +45 -0
  264. package/src/theme/themes/kanagawa.json +89 -0
  265. package/src/theme/themes/lucent-orng.json +87 -0
  266. package/src/theme/themes/material.json +87 -0
  267. package/src/theme/themes/matrix.json +113 -0
  268. package/src/theme/themes/mercury.json +86 -0
  269. package/src/theme/themes/monokai.json +49 -0
  270. package/src/theme/themes/nightowl.json +46 -0
  271. package/src/theme/themes/nord.json +46 -0
  272. package/src/theme/themes/oc-2.json +468 -0
  273. package/src/theme/themes/one-dark.json +89 -0
  274. package/src/theme/themes/onedarkpro.json +45 -0
  275. package/src/theme/themes/opencode.json +89 -0
  276. package/src/theme/themes/orng.json +87 -0
  277. package/src/theme/themes/osaka-jade.json +88 -0
  278. package/src/theme/themes/palenight.json +85 -0
  279. package/src/theme/themes/rosepine.json +85 -0
  280. package/src/theme/themes/shadesofpurple.json +51 -0
  281. package/src/theme/themes/solarized.json +49 -0
  282. package/src/theme/themes/synthwave84.json +87 -0
  283. package/src/theme/themes/tokyonight.json +47 -0
  284. package/src/theme/themes/vercel.json +90 -0
  285. package/src/theme/themes/vesper.json +51 -0
  286. package/src/theme/themes/zenburn.json +87 -0
  287. package/src/theme/types.ts +75 -0
  288. package/src/theme/v2/avatar.ts +48 -0
  289. package/src/theme/v2/default-primitives.ts +114 -0
  290. package/src/theme/v2/foreground.ts +60 -0
  291. package/src/theme/v2/mapping.ts +138 -0
  292. package/src/theme/v2/resolve.ts +153 -0
  293. package/src/v2/components/accordion-v2.css +139 -0
  294. package/src/v2/components/accordion-v2.tsx +86 -0
  295. package/src/v2/components/avatar-v2.css +70 -0
  296. package/src/v2/components/avatar-v2.tsx +59 -0
  297. package/src/v2/components/badge-v2.css +27 -0
  298. package/src/v2/components/badge-v2.tsx +20 -0
  299. package/src/v2/components/button-v2.css +186 -0
  300. package/src/v2/components/button-v2.tsx +35 -0
  301. package/src/v2/components/checkbox-v2.css +184 -0
  302. package/src/v2/components/checkbox-v2.tsx +65 -0
  303. package/src/v2/components/dialog-v2.css +150 -0
  304. package/src/v2/components/dialog-v2.tsx +93 -0
  305. package/src/v2/components/diff-changes-v2.css +24 -0
  306. package/src/v2/components/diff-changes-v2.tsx +28 -0
  307. package/src/v2/components/field-v2.css +94 -0
  308. package/src/v2/components/field-v2.tsx +265 -0
  309. package/src/v2/components/icon-button-v2.css +155 -0
  310. package/src/v2/components/icon-button-v2.tsx +37 -0
  311. package/src/v2/components/icon.tsx +129 -0
  312. package/src/v2/components/inline-input-v2.css +218 -0
  313. package/src/v2/components/inline-input-v2.tsx +90 -0
  314. package/src/v2/components/keybind-v2.css +76 -0
  315. package/src/v2/components/keybind-v2.tsx +30 -0
  316. package/src/v2/components/line-comment-v2.css +204 -0
  317. package/src/v2/components/line-comment-v2.tsx +155 -0
  318. package/src/v2/components/menu-v2.css +190 -0
  319. package/src/v2/components/menu-v2.tsx +225 -0
  320. package/src/v2/components/project-avatar-v2.css +126 -0
  321. package/src/v2/components/project-avatar-v2.tsx +64 -0
  322. package/src/v2/components/radio-v2.css +202 -0
  323. package/src/v2/components/radio-v2.tsx +72 -0
  324. package/src/v2/components/segmented-control-v2.css +80 -0
  325. package/src/v2/components/segmented-control-v2.tsx +208 -0
  326. package/src/v2/components/select-v2.css +285 -0
  327. package/src/v2/components/select-v2.tsx +208 -0
  328. package/src/v2/components/switch-v2.css +154 -0
  329. package/src/v2/components/switch-v2.tsx +28 -0
  330. package/src/v2/components/tab-state-indicator.tsx +37 -0
  331. package/src/v2/components/tabs-v2.css +225 -0
  332. package/src/v2/components/tabs-v2.tsx +147 -0
  333. package/src/v2/components/text-input-v2.css +145 -0
  334. package/src/v2/components/text-input-v2.tsx +67 -0
  335. package/src/v2/components/text-shimmer-v2.css +125 -0
  336. package/src/v2/components/text-shimmer-v2.tsx +63 -0
  337. package/src/v2/components/textarea-v2.css +78 -0
  338. package/src/v2/components/textarea-v2.tsx +31 -0
  339. package/src/v2/components/toast-v2.css +215 -0
  340. package/src/v2/components/toast-v2.tsx +144 -0
  341. package/src/v2/components/tooltip-v2.css +53 -0
  342. package/src/v2/components/tooltip-v2.tsx +146 -0
  343. package/src/v2/components/wordmark-v2.tsx +92 -0
  344. package/src/v2/styles/colors.css +172 -0
  345. package/src/v2/styles/tailwind.css +2 -0
  346. package/src/v2/styles/theme.css +441 -0
@@ -0,0 +1,394 @@
1
+ import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
2
+ import { createEffect, For, type JSX, on, Show } from "solid-js"
3
+ import { createStore } from "solid-js/store"
4
+ import { makeEventListener } from "@solid-primitives/event-listener"
5
+ import { useI18n } from "../context/i18n"
6
+ import { Icon, type IconProps } from "./icon"
7
+ import { IconButton } from "./icon-button"
8
+ import { TextField } from "./text-field"
9
+
10
+ function findByKey(container: HTMLElement, key: string) {
11
+ const nodes = container.querySelectorAll<HTMLElement>('[data-slot="list-item"][data-key]')
12
+ for (const node of nodes) {
13
+ if (node.getAttribute("data-key") === key) return node
14
+ }
15
+ }
16
+
17
+ export interface ListSearchProps {
18
+ placeholder?: string
19
+ autofocus?: boolean
20
+ hideIcon?: boolean
21
+ class?: string
22
+ action?: JSX.Element
23
+ }
24
+
25
+ export interface ListAddProps {
26
+ class?: string
27
+ render: () => JSX.Element
28
+ }
29
+
30
+ export interface ListAddProps {
31
+ class?: string
32
+ render: () => JSX.Element
33
+ }
34
+
35
+ export interface ListProps<T> extends FilteredListProps<T> {
36
+ class?: string
37
+ children: (item: T) => JSX.Element
38
+ emptyMessage?: string
39
+ loadingMessage?: string
40
+ onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
41
+ onMove?: (item: T | undefined) => void
42
+ onFilter?: (value: string) => void
43
+ activeIcon?: IconProps["name"]
44
+ filter?: string
45
+ search?: ListSearchProps | boolean
46
+ itemWrapper?: (item: T, node: JSX.Element) => JSX.Element
47
+ divider?: boolean
48
+ add?: ListAddProps
49
+ groupHeader?: (group: { category: string; items: T[] }) => JSX.Element
50
+ }
51
+
52
+ export interface ListRef {
53
+ onKeyDown: (e: KeyboardEvent) => void
54
+ setScrollRef: (el: HTMLDivElement | undefined) => void
55
+ setFilter: (value: string) => void
56
+ }
57
+
58
+ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void }) {
59
+ const i18n = useI18n()
60
+ let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
61
+ const [store, setStore] = createStore({
62
+ mouseActive: false,
63
+ scrollRef: undefined as HTMLDivElement | undefined,
64
+ internalFilter: "",
65
+ })
66
+ const scrollRef = () => store.scrollRef
67
+ const setScrollRef = (el: HTMLDivElement | undefined) => setStore("scrollRef", el)
68
+ const internalFilter = () => store.internalFilter
69
+ const setInternalFilter = (value: string) => setStore("internalFilter", value)
70
+
71
+ const scrollIntoView = (container: HTMLDivElement, node: HTMLElement, block: "center" | "nearest") => {
72
+ const containerRect = container.getBoundingClientRect()
73
+ const nodeRect = node.getBoundingClientRect()
74
+ const top = nodeRect.top - containerRect.top + container.scrollTop
75
+ const bottom = top + nodeRect.height
76
+ const viewTop = container.scrollTop
77
+ const viewBottom = viewTop + container.clientHeight
78
+ const target =
79
+ block === "center"
80
+ ? top - container.clientHeight / 2 + nodeRect.height / 2
81
+ : top < viewTop
82
+ ? top
83
+ : bottom > viewBottom
84
+ ? bottom - container.clientHeight
85
+ : viewTop
86
+ const max = Math.max(0, container.scrollHeight - container.clientHeight)
87
+ container.scrollTop = Math.max(0, Math.min(target, max))
88
+ }
89
+
90
+ const { filter, grouped, flat, active, setActive, onKeyDown, onInput, refetch } = useFilteredList<T>(props)
91
+
92
+ const searchProps = () => (typeof props.search === "object" ? props.search : {})
93
+ const searchAction = () => searchProps().action
94
+ const addProps = () => props.add
95
+ const showAdd = () => !!addProps()
96
+
97
+ const moved = (event: MouseEvent) => event.movementX !== 0 || event.movementY !== 0
98
+
99
+ const applyFilter = (value: string, options?: { ref?: boolean }) => {
100
+ const prev = filter()
101
+ setInternalFilter(value)
102
+ onInput(value)
103
+ props.onFilter?.(value)
104
+
105
+ if (!options?.ref) return
106
+
107
+ // Force a refetch even if the value is unchanged.
108
+ // This is important for programmatic changes like Tab completion.
109
+ if (prev === value) {
110
+ void refetch()
111
+ return
112
+ }
113
+ queueMicrotask(() => refetch())
114
+ }
115
+
116
+ createEffect(() => {
117
+ if (props.filter === undefined) return
118
+ if (props.filter === internalFilter()) return
119
+ setInternalFilter(props.filter)
120
+ onInput(props.filter)
121
+ })
122
+
123
+ createEffect(
124
+ on(
125
+ filter,
126
+ () => {
127
+ scrollRef()?.scrollTo(0, 0)
128
+ },
129
+ { defer: true },
130
+ ),
131
+ )
132
+
133
+ createEffect(() => {
134
+ const scroll = scrollRef()
135
+ if (!scroll) return
136
+ if (!props.current) return
137
+ const key = props.key(props.current)
138
+ requestAnimationFrame(() => {
139
+ const element = findByKey(scroll, key)
140
+ if (!element) return
141
+ scrollIntoView(scroll, element, "center")
142
+ })
143
+ })
144
+
145
+ createEffect(() => {
146
+ const all = flat()
147
+ if (store.mouseActive || all.length === 0) return
148
+ const scroll = scrollRef()
149
+ if (!scroll) return
150
+ if (active() === props.key(all[0])) {
151
+ scroll.scrollTo(0, 0)
152
+ return
153
+ }
154
+ const key = active()
155
+ if (!key) return
156
+ const element = findByKey(scroll, key)
157
+ if (!element) return
158
+ scrollIntoView(scroll, element, "center")
159
+ })
160
+
161
+ createEffect(() => {
162
+ const all = flat()
163
+ const current = active()
164
+ const item = all.find((x) => props.key(x) === current)
165
+ props.onMove?.(item)
166
+ })
167
+
168
+ const handleSelect = (item: T | undefined, index: number) => {
169
+ props.onSelect?.(item, index)
170
+ }
171
+
172
+ const handleKey = (e: KeyboardEvent) => {
173
+ setStore("mouseActive", false)
174
+ if (e.key === "Escape") return
175
+
176
+ const all = flat()
177
+ const selected = all.find((x) => props.key(x) === active())
178
+ const index = selected ? all.indexOf(selected) : -1
179
+ props.onKeyEvent?.(e, selected)
180
+
181
+ if (e.defaultPrevented) return
182
+
183
+ if (e.key === "Enter" && !e.isComposing) {
184
+ e.preventDefault()
185
+ if (selected) handleSelect(selected, index)
186
+ } else if (props.search) {
187
+ if (e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && (e.key === "n" || e.key === "p")) {
188
+ onKeyDown(e)
189
+ return
190
+ }
191
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
192
+ onKeyDown(e)
193
+ }
194
+ } else {
195
+ onKeyDown(e)
196
+ }
197
+ }
198
+
199
+ props.ref?.({
200
+ onKeyDown: handleKey,
201
+ setScrollRef,
202
+ setFilter: (value) => applyFilter(value, { ref: true }),
203
+ })
204
+
205
+ const renderAdd = () => {
206
+ const add = addProps()
207
+ if (!add) return null
208
+ return (
209
+ <div data-slot="list-item-add" classList={{ [add.class ?? ""]: !!add.class }}>
210
+ {add.render()}
211
+ </div>
212
+ )
213
+ }
214
+
215
+ function GroupHeader(groupProps: { group: { category: string; items: T[] } }): JSX.Element {
216
+ const [state, setState] = createStore({
217
+ stuck: false,
218
+ header: undefined as HTMLDivElement | undefined,
219
+ })
220
+
221
+ createEffect(() => {
222
+ const scroll = scrollRef()
223
+ const node = state.header
224
+ if (!scroll || !node) return
225
+
226
+ const handler = () => {
227
+ const rect = node.getBoundingClientRect()
228
+ const scrollRect = scroll.getBoundingClientRect()
229
+ setState("stuck", rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0)
230
+ }
231
+
232
+ makeEventListener(scroll, "scroll", handler, { passive: true })
233
+ handler()
234
+ })
235
+
236
+ return (
237
+ <div data-slot="list-header" data-stuck={state.stuck} ref={(el) => setState("header", el)}>
238
+ {props.groupHeader?.(groupProps.group) ?? groupProps.group.category}
239
+ </div>
240
+ )
241
+ }
242
+
243
+ const emptyMessage = () => {
244
+ if (grouped.loading) return props.loadingMessage ?? i18n.t("ui.list.loading")
245
+ if (props.emptyMessage) return props.emptyMessage
246
+
247
+ const query = filter()
248
+ if (!query) return i18n.t("ui.list.empty")
249
+
250
+ const suffix = i18n.t("ui.list.emptyWithFilter.suffix")
251
+ return (
252
+ <>
253
+ <span>{i18n.t("ui.list.emptyWithFilter.prefix")}</span>
254
+ <span data-slot="list-filter">&quot;{query}&quot;</span>
255
+ <Show when={suffix}>
256
+ <span>{suffix}</span>
257
+ </Show>
258
+ </>
259
+ )
260
+ }
261
+
262
+ return (
263
+ <div data-component="list" classList={{ [props.class ?? ""]: !!props.class }}>
264
+ <Show when={!!props.search}>
265
+ <div data-slot="list-search-wrapper">
266
+ <div
267
+ data-slot="list-search"
268
+ classList={{ [searchProps().class ?? ""]: !!searchProps().class }}
269
+ onPointerDown={(event) => {
270
+ const container = event.currentTarget
271
+ if (!(container instanceof HTMLElement)) return
272
+
273
+ const node = container.querySelector("input, textarea")
274
+ const input = node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement ? node : inputRef
275
+ input?.focus()
276
+
277
+ // Prevent global listeners (e.g. dnd sensors) from cancelling focus.
278
+ event.stopPropagation()
279
+ }}
280
+ >
281
+ <div data-slot="list-search-container">
282
+ <Show when={!searchProps().hideIcon}>
283
+ <Icon name="magnifying-glass" />
284
+ </Show>
285
+ <TextField
286
+ autofocus={searchProps().autofocus}
287
+ variant="ghost"
288
+ data-slot="list-search-input"
289
+ type="text"
290
+ ref={(el: HTMLInputElement | HTMLTextAreaElement) => {
291
+ inputRef = el
292
+ }}
293
+ value={internalFilter()}
294
+ onChange={(value) => applyFilter(value)}
295
+ onKeyDown={handleKey}
296
+ placeholder={searchProps().placeholder}
297
+ spellcheck={false}
298
+ autocorrect="off"
299
+ autocomplete="off"
300
+ autocapitalize="off"
301
+ />
302
+ </div>
303
+ <Show when={internalFilter()}>
304
+ <IconButton
305
+ icon="circle-x"
306
+ variant="ghost"
307
+ onClick={() => {
308
+ setInternalFilter("")
309
+ queueMicrotask(() => inputRef?.focus())
310
+ }}
311
+ aria-label={i18n.t("ui.list.clearFilter")}
312
+ />
313
+ </Show>
314
+ </div>
315
+ {searchAction()}
316
+ </div>
317
+ </Show>
318
+ <div ref={setScrollRef} data-slot="list-scroll">
319
+ <Show
320
+ when={flat().length > 0 || showAdd()}
321
+ fallback={
322
+ <div data-slot="list-empty-state">
323
+ <div data-slot="list-message">{emptyMessage()}</div>
324
+ </div>
325
+ }
326
+ >
327
+ <For each={grouped.latest}>
328
+ {(group, groupIndex) => {
329
+ const isLastGroup = () => groupIndex() === grouped.latest.length - 1
330
+ return (
331
+ <div data-slot="list-group">
332
+ <Show when={group.category}>
333
+ <GroupHeader group={group} />
334
+ </Show>
335
+ <div data-slot="list-items">
336
+ <For each={group.items}>
337
+ {(item, i) => {
338
+ const node = (
339
+ <button
340
+ data-slot="list-item"
341
+ data-key={props.key(item)}
342
+ data-active={props.key(item) === active()}
343
+ data-selected={item === props.current}
344
+ onClick={() => handleSelect(item, i())}
345
+ onKeyDown={handleKey}
346
+ type="button"
347
+ onMouseMove={(event) => {
348
+ if (!moved(event)) return
349
+ setStore("mouseActive", true)
350
+ setActive(props.key(item))
351
+ }}
352
+ onMouseLeave={() => {
353
+ if (!store.mouseActive) return
354
+ setActive(null)
355
+ }}
356
+ >
357
+ {props.children(item)}
358
+ <Show when={item === props.current}>
359
+ <span data-slot="list-item-selected-icon">
360
+ <Icon name="check-small" />
361
+ </span>
362
+ </Show>
363
+ <Show when={props.activeIcon}>
364
+ {(icon) => (
365
+ <span data-slot="list-item-active-icon">
366
+ <Icon name={icon()} />
367
+ </span>
368
+ )}
369
+ </Show>
370
+ {props.divider && (i() !== group.items.length - 1 || (showAdd() && isLastGroup())) && (
371
+ <span data-slot="list-item-divider" />
372
+ )}
373
+ </button>
374
+ )
375
+ if (props.itemWrapper) return props.itemWrapper(item, node)
376
+ return node
377
+ }}
378
+ </For>
379
+ <Show when={showAdd() && isLastGroup()}>{renderAdd()}</Show>
380
+ </div>
381
+ </div>
382
+ )
383
+ }}
384
+ </For>
385
+ <Show when={grouped.latest.length === 0 && showAdd()}>
386
+ <div data-slot="list-group">
387
+ <div data-slot="list-items">{renderAdd()}</div>
388
+ </div>
389
+ </Show>
390
+ </Show>
391
+ </div>
392
+ </div>
393
+ )
394
+ }
@@ -0,0 +1,4 @@
1
+ [data-component="logo-mark"] {
2
+ width: 16px;
3
+ aspect-ratio: 4/5;
4
+ }
@@ -0,0 +1,62 @@
1
+ import { type ComponentProps } from "solid-js"
2
+
3
+ export const Mark = (props: { class?: string }) => {
4
+ return (
5
+ <svg
6
+ data-component="logo-mark"
7
+ classList={{ [props.class ?? ""]: !!props.class }}
8
+ viewBox="0 0 16 20"
9
+ fill="none"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ >
12
+ <path data-slot="logo-logo-mark-shadow" d="M12 16H4V8H12V16Z" fill="var(--icon-weak-base)" />
13
+ <path data-slot="logo-logo-mark-o" d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="var(--icon-strong-base)" />
14
+ </svg>
15
+ )
16
+ }
17
+
18
+ export const Splash = (props: Pick<ComponentProps<"svg">, "ref" | "class">) => {
19
+ return (
20
+ <svg
21
+ ref={props.ref}
22
+ data-component="logo-splash"
23
+ classList={{ [props.class ?? ""]: !!props.class }}
24
+ viewBox="0 0 80 100"
25
+ fill="none"
26
+ xmlns="http://www.w3.org/2000/svg"
27
+ >
28
+ <path d="M60 80H20V40H60V80Z" fill="var(--icon-base)" />
29
+ <path d="M60 20H20V80H60V20ZM80 100H0V0H80V100Z" fill="var(--icon-strong-base)" />
30
+ </svg>
31
+ )
32
+ }
33
+
34
+ export const Logo = (props: { class?: string }) => {
35
+ return (
36
+ <svg
37
+ xmlns="http://www.w3.org/2000/svg"
38
+ viewBox="0 0 234 42"
39
+ fill="none"
40
+ classList={{ [props.class ?? ""]: !!props.class }}
41
+ >
42
+ <g>
43
+ <path d="M18 30H6V18H18V30Z" fill="var(--icon-weak-base)" />
44
+ <path d="M18 12H6V30H18V12ZM24 36H0V6H24V36Z" fill="var(--icon-base)" />
45
+ <path d="M48 30H36V18H48V30Z" fill="var(--icon-weak-base)" />
46
+ <path d="M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z" fill="var(--icon-base)" />
47
+ <path d="M84 24V30H66V24H84Z" fill="var(--icon-weak-base)" />
48
+ <path d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" fill="var(--icon-base)" />
49
+ <path d="M108 36H96V18H108V36Z" fill="var(--icon-weak-base)" />
50
+ <path d="M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z" fill="var(--icon-base)" />
51
+ <path d="M144 30H126V18H144V30Z" fill="var(--icon-weak-base)" />
52
+ <path d="M144 12H126V30H144V36H120V6H144V12Z" fill="var(--icon-strong-base)" />
53
+ <path d="M168 30H156V18H168V30Z" fill="var(--icon-weak-base)" />
54
+ <path d="M168 12H156V30H168V12ZM174 36H150V6H174V36Z" fill="var(--icon-strong-base)" />
55
+ <path d="M198 30H186V18H198V30Z" fill="var(--icon-weak-base)" />
56
+ <path d="M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z" fill="var(--icon-strong-base)" />
57
+ <path d="M234 24V30H216V24H234Z" fill="var(--icon-weak-base)" />
58
+ <path d="M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z" fill="var(--icon-strong-base)" />
59
+ </g>
60
+ </svg>
61
+ )
62
+ }
@@ -0,0 +1,58 @@
1
+ import { attachSpring, motionValue } from "motion"
2
+ import type { SpringOptions } from "motion"
3
+ import { createComputed, createEffect, createSignal, onCleanup } from "solid-js"
4
+
5
+ type Opt = Partial<Pick<SpringOptions, "visualDuration" | "bounce" | "stiffness" | "damping" | "mass" | "velocity">>
6
+ const eq = (a: Opt | undefined, b: Opt | undefined) =>
7
+ a?.visualDuration === b?.visualDuration &&
8
+ a?.bounce === b?.bounce &&
9
+ a?.stiffness === b?.stiffness &&
10
+ a?.damping === b?.damping &&
11
+ a?.mass === b?.mass &&
12
+ a?.velocity === b?.velocity
13
+
14
+ export function useSpring(target: () => number, options?: Opt | (() => Opt), snapKey?: () => unknown) {
15
+ const read = () => (typeof options === "function" ? options() : options)
16
+ const [value, setValue] = createSignal(target())
17
+ const source = motionValue(value())
18
+ const spring = motionValue(value())
19
+ let config = read()
20
+ let snapValue = snapKey?.()
21
+ let stop = attachSpring(spring, source, config)
22
+ let off = spring.on("change", (next: number) => setValue(next))
23
+
24
+ createComputed(() => {
25
+ const next = target()
26
+ const nextSnap = snapKey?.()
27
+ if (snapKey && nextSnap !== snapValue) {
28
+ // State boundaries should adopt their target without animating from the previous context.
29
+ snapValue = nextSnap
30
+ stop()
31
+ spring.jump(next)
32
+ source.jump(next)
33
+ stop = attachSpring(spring, source, config)
34
+ setValue(next)
35
+ return
36
+ }
37
+ source.set(next)
38
+ })
39
+
40
+ createEffect(() => {
41
+ if (!options) return
42
+ const next = read()
43
+ if (eq(config, next)) return
44
+ config = next
45
+ stop()
46
+ stop = attachSpring(spring, source, next)
47
+ setValue(spring.get())
48
+ })
49
+
50
+ onCleanup(() => {
51
+ off()
52
+ stop()
53
+ spring.destroy()
54
+ source.destroy()
55
+ })
56
+
57
+ return value
58
+ }
@@ -0,0 +1,98 @@
1
+ [data-slot="popover-trigger"] {
2
+ display: inline-flex;
3
+ }
4
+
5
+ [data-component="popover-content"] {
6
+ z-index: 50;
7
+ min-width: 200px;
8
+ max-width: 320px;
9
+ border-radius: var(--radius-md);
10
+ background-color: var(--surface-raised-stronger-non-alpha);
11
+
12
+ border: 1px solid color-mix(in oklch, var(--border-base) 50%, transparent);
13
+ background-clip: padding-box;
14
+ box-shadow: var(--shadow-md);
15
+
16
+ transform-origin: var(--kb-popover-content-transform-origin);
17
+
18
+ &:focus-within {
19
+ outline: none;
20
+ }
21
+
22
+ &[data-closed] {
23
+ animation: popover-close 0.15s ease-out;
24
+ }
25
+
26
+ &[data-expanded] {
27
+ animation: popover-open 0.15s ease-out;
28
+ }
29
+
30
+ [data-slot="popover-header"] {
31
+ display: flex;
32
+ padding: 12px;
33
+ padding-bottom: 0;
34
+ justify-content: space-between;
35
+ align-items: center;
36
+ gap: 8px;
37
+
38
+ [data-slot="popover-title"] {
39
+ flex: 1;
40
+ color: var(--text-strong);
41
+ margin: 0;
42
+
43
+ font-family: var(--font-family-sans);
44
+ font-size: var(--font-size-base);
45
+ font-style: normal;
46
+ font-weight: var(--font-weight-medium);
47
+ line-height: var(--line-height-large);
48
+ letter-spacing: var(--letter-spacing-normal);
49
+ }
50
+
51
+ [data-slot="popover-close-button"] {
52
+ flex-shrink: 0;
53
+ }
54
+ }
55
+
56
+ [data-slot="popover-description"] {
57
+ padding: 0 12px;
58
+ margin: 0;
59
+ color: var(--text-base);
60
+
61
+ font-family: var(--font-family-sans);
62
+ font-size: var(--font-size-small);
63
+ font-style: normal;
64
+ font-weight: var(--font-weight-regular);
65
+ line-height: var(--line-height-large);
66
+ letter-spacing: var(--letter-spacing-normal);
67
+ }
68
+
69
+ [data-slot="popover-body"] {
70
+ padding: 12px;
71
+ }
72
+
73
+ [data-slot="popover-arrow"] {
74
+ fill: var(--surface-raised-stronger-non-alpha);
75
+ }
76
+ }
77
+
78
+ @keyframes popover-open {
79
+ from {
80
+ opacity: 0;
81
+ transform: scale(0.96);
82
+ }
83
+ to {
84
+ opacity: 1;
85
+ transform: scale(1);
86
+ }
87
+ }
88
+
89
+ @keyframes popover-close {
90
+ from {
91
+ opacity: 1;
92
+ transform: scale(1);
93
+ }
94
+ to {
95
+ opacity: 0;
96
+ transform: scale(0.96);
97
+ }
98
+ }