@selvajs/ui 6.6.0-beta.14 → 6.6.0-beta.16

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 (205) hide show
  1. package/README.md +0 -3
  2. package/dist/components/compute/AppLayout.svelte +10 -5
  3. package/dist/components/compute/CollapsedPanelStrip.svelte +1 -1
  4. package/dist/components/compute/ParameterPresetManager.svelte +1 -1
  5. package/dist/components/compute/SolveMessages.svelte +2 -2
  6. package/dist/components/layout/FooterItemRenderer.svelte +1 -1
  7. package/dist/components/layout/PageFooter.svelte +2 -2
  8. package/dist/components/layout/PageHeader.svelte +1 -1
  9. package/dist/components/layout/SideNav.svelte +3 -3
  10. package/dist/components/layout/SubNav.svelte +2 -2
  11. package/dist/components/preview/ChartOutput.svelte +3 -3
  12. package/dist/components/preview/Group.svelte +7 -7
  13. package/dist/components/preview/ImageOutput.svelte +6 -6
  14. package/dist/components/preview/InputControl.svelte +1 -0
  15. package/dist/components/preview/OutputDisplay.svelte +8 -6
  16. package/dist/components/preview/TabBar.svelte +6 -4
  17. package/dist/components/preview/TabBar.svelte.d.ts +2 -0
  18. package/dist/components/preview/TabContent.svelte +1 -1
  19. package/dist/components/preview/TabLayout.svelte +3 -4
  20. package/dist/components/preview/inputs/ChecklistInput.svelte +3 -3
  21. package/dist/components/preview/inputs/ColorInput.svelte +356 -14
  22. package/dist/components/preview/inputs/ColorInput.svelte.d.ts +1 -0
  23. package/dist/components/preview/inputs/FileInput.svelte +3 -3
  24. package/dist/components/preview/inputs/SegmentedInput.svelte +2 -1
  25. package/dist/components/preview/inputs/TileInput.svelte +3 -2
  26. package/dist/components/primitives/CalculateButton.svelte +2 -2
  27. package/dist/components/primitives/Callout.svelte +7 -5
  28. package/dist/components/primitives/Callout.svelte.d.ts +4 -4
  29. package/dist/components/primitives/DataTable.svelte +1 -1
  30. package/dist/components/primitives/ImageUploadField.svelte +1 -1
  31. package/dist/components/primitives/alert-dialog/alert-dialog-content.svelte +1 -1
  32. package/dist/components/primitives/badge/badge.svelte +2 -2
  33. package/dist/components/primitives/badge/badge.svelte.d.ts +6 -6
  34. package/dist/components/primitives/button/button.svelte +6 -7
  35. package/dist/components/primitives/button/button.svelte.d.ts +18 -18
  36. package/dist/components/primitives/card/card.svelte +1 -1
  37. package/dist/components/primitives/card/card.svelte.d.ts +1 -1
  38. package/dist/components/primitives/checkbox/checkbox.svelte +1 -1
  39. package/dist/components/primitives/command/command-input.svelte +1 -1
  40. package/dist/components/primitives/command/command-item.svelte +1 -1
  41. package/dist/components/primitives/command/command-link-item.svelte +1 -1
  42. package/dist/components/primitives/command/command.svelte +1 -1
  43. package/dist/components/primitives/context-menu/context-menu-checkbox-item.svelte +1 -1
  44. package/dist/components/primitives/context-menu/context-menu-content.svelte +1 -1
  45. package/dist/components/primitives/context-menu/context-menu-item.svelte +1 -1
  46. package/dist/components/primitives/context-menu/context-menu-radio-item.svelte +1 -1
  47. package/dist/components/primitives/context-menu/context-menu-sub-content.svelte +1 -1
  48. package/dist/components/primitives/context-menu/context-menu-sub-trigger.svelte +1 -1
  49. package/dist/components/primitives/dialog/dialog-content.svelte +2 -2
  50. package/dist/components/primitives/drawer/Drawer.svelte +1 -1
  51. package/dist/components/primitives/dropdown-menu/dropdown-menu-checkbox-item.svelte +31 -11
  52. package/dist/components/primitives/dropdown-menu/dropdown-menu-checkbox-item.svelte.d.ts +2 -0
  53. package/dist/components/primitives/dropdown-menu/dropdown-menu-content.svelte +5 -1
  54. package/dist/components/primitives/dropdown-menu/dropdown-menu-content.svelte.d.ts +2 -0
  55. package/dist/components/primitives/dropdown-menu/dropdown-menu-item.svelte +4 -2
  56. package/dist/components/primitives/dropdown-menu/dropdown-menu-item.svelte.d.ts +2 -1
  57. package/dist/components/primitives/dropdown-menu/dropdown-menu-radio-item.svelte +1 -1
  58. package/dist/components/primitives/dropdown-menu/dropdown-menu-shortcut.svelte +1 -1
  59. package/dist/components/primitives/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  60. package/dist/components/primitives/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
  61. package/dist/components/primitives/index.d.ts +1 -1
  62. package/dist/components/primitives/index.js +1 -1
  63. package/dist/components/primitives/input/input.svelte +2 -2
  64. package/dist/components/primitives/input-group/input-group.svelte +1 -1
  65. package/dist/components/primitives/popover/popover-content.svelte +1 -1
  66. package/dist/components/primitives/resizable/resizable-handle.svelte +2 -2
  67. package/dist/components/primitives/search/search.svelte +1 -1
  68. package/dist/components/primitives/select/select-content.svelte +1 -1
  69. package/dist/components/primitives/select/select-item.svelte +1 -1
  70. package/dist/components/primitives/select/select-trigger.svelte +1 -1
  71. package/dist/components/primitives/slider/slider.svelte +1 -1
  72. package/dist/components/primitives/switch/switch.svelte +1 -1
  73. package/dist/components/primitives/tabs/tabs-list.svelte +1 -1
  74. package/dist/components/primitives/tabs/tabs-list.svelte.d.ts +3 -3
  75. package/dist/components/primitives/tabs/tabs-trigger.svelte +1 -1
  76. package/dist/components/primitives/tabs/tabs-trigger.svelte.d.ts +3 -3
  77. package/dist/components/primitives/textarea/textarea.svelte +1 -1
  78. package/dist/components/primitives/toggle/toggle.svelte +8 -2
  79. package/dist/components/primitives/toggle/toggle.svelte.d.ts +10 -4
  80. package/dist/components/primitives/toggle-group/toggle-group-item.svelte +1 -1
  81. package/dist/components/primitives/toggle-group/toggle-group.svelte +1 -1
  82. package/dist/components/primitives/tooltip/index.d.ts +6 -0
  83. package/dist/components/primitives/tooltip/index.js +8 -0
  84. package/dist/components/primitives/tooltip/tooltip-content.svelte +34 -0
  85. package/dist/components/primitives/tooltip/tooltip-content.svelte.d.ts +10 -0
  86. package/dist/components/primitives/tooltip/tooltip-portal.svelte +7 -0
  87. package/dist/components/primitives/tooltip/tooltip-portal.svelte.d.ts +4 -0
  88. package/dist/components/primitives/tooltip/tooltip-provider.svelte +7 -0
  89. package/dist/components/primitives/tooltip/tooltip-provider.svelte.d.ts +4 -0
  90. package/dist/components/primitives/tooltip/tooltip-trigger.svelte +7 -0
  91. package/dist/components/primitives/tooltip/tooltip-trigger.svelte.d.ts +4 -0
  92. package/dist/components/primitives/tooltip/tooltip.svelte +7 -0
  93. package/dist/components/primitives/tooltip/tooltip.svelte.d.ts +4 -0
  94. package/dist/components/viewer/MeshMetadataPanel.svelte +1 -1
  95. package/dist/components/viewer/SceneManager.svelte +19 -19
  96. package/dist/components/viewer/Viewer.svelte +90 -164
  97. package/dist/components/viewer/Viewer.svelte.d.ts +3 -1
  98. package/dist/components/viewer/ViewerDock.svelte +404 -0
  99. package/dist/components/viewer/ViewerDock.svelte.d.ts +42 -0
  100. package/dist/i18n/messages.d.ts +12 -0
  101. package/dist/i18n/messages.js +20 -0
  102. package/dist/styles/base.css +81 -80
  103. package/dist/utils/color-space.d.ts +21 -0
  104. package/dist/utils/color-space.js +90 -0
  105. package/package.json +12 -13
  106. package/src/lib/components/compute/AppLayout.svelte +10 -5
  107. package/src/lib/components/compute/CollapsedPanelStrip.svelte +1 -1
  108. package/src/lib/components/compute/ParameterPresetManager.svelte +1 -1
  109. package/src/lib/components/compute/SolveMessages.svelte +2 -2
  110. package/src/lib/components/layout/FooterItemRenderer.svelte +1 -1
  111. package/src/lib/components/layout/PageFooter.svelte +2 -2
  112. package/src/lib/components/layout/PageHeader.svelte +1 -1
  113. package/src/lib/components/layout/SideNav.svelte +3 -3
  114. package/src/lib/components/layout/SubNav.svelte +2 -2
  115. package/src/lib/components/preview/ChartOutput.svelte +3 -3
  116. package/src/lib/components/preview/Group.svelte +7 -7
  117. package/src/lib/components/preview/ImageOutput.svelte +6 -6
  118. package/src/lib/components/preview/InputControl.svelte +1 -0
  119. package/src/lib/components/preview/OutputDisplay.svelte +8 -6
  120. package/src/lib/components/preview/TabBar.svelte +6 -4
  121. package/src/lib/components/preview/TabContent.svelte +1 -1
  122. package/src/lib/components/preview/TabLayout.svelte +3 -4
  123. package/src/lib/components/preview/inputs/ChecklistInput.svelte +3 -3
  124. package/src/lib/components/preview/inputs/ColorInput.svelte +356 -14
  125. package/src/lib/components/preview/inputs/FileInput.svelte +3 -3
  126. package/src/lib/components/preview/inputs/SegmentedInput.svelte +2 -1
  127. package/src/lib/components/preview/inputs/TileInput.svelte +3 -2
  128. package/src/lib/components/primitives/CalculateButton.svelte +2 -2
  129. package/src/lib/components/primitives/Callout.svelte +7 -5
  130. package/src/lib/components/primitives/DataTable.svelte +1 -1
  131. package/src/lib/components/primitives/ImageUploadField.svelte +1 -1
  132. package/src/lib/components/primitives/alert-dialog/alert-dialog-content.svelte +1 -1
  133. package/src/lib/components/primitives/badge/badge.svelte +2 -2
  134. package/src/lib/components/primitives/button/button.svelte +6 -7
  135. package/src/lib/components/primitives/card/card.svelte +1 -1
  136. package/src/lib/components/primitives/checkbox/checkbox.svelte +1 -1
  137. package/src/lib/components/primitives/command/command-input.svelte +1 -1
  138. package/src/lib/components/primitives/command/command-item.svelte +1 -1
  139. package/src/lib/components/primitives/command/command-link-item.svelte +1 -1
  140. package/src/lib/components/primitives/command/command.svelte +1 -1
  141. package/src/lib/components/primitives/context-menu/context-menu-checkbox-item.svelte +1 -1
  142. package/src/lib/components/primitives/context-menu/context-menu-content.svelte +1 -1
  143. package/src/lib/components/primitives/context-menu/context-menu-item.svelte +1 -1
  144. package/src/lib/components/primitives/context-menu/context-menu-radio-item.svelte +1 -1
  145. package/src/lib/components/primitives/context-menu/context-menu-sub-content.svelte +1 -1
  146. package/src/lib/components/primitives/context-menu/context-menu-sub-trigger.svelte +1 -1
  147. package/src/lib/components/primitives/dialog/dialog-content.svelte +2 -2
  148. package/src/lib/components/primitives/drawer/Drawer.svelte +1 -1
  149. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-checkbox-item.svelte +31 -11
  150. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-content.svelte +5 -1
  151. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-item.svelte +4 -2
  152. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-radio-item.svelte +1 -1
  153. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-shortcut.svelte +1 -1
  154. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  155. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
  156. package/src/lib/components/primitives/index.ts +1 -1
  157. package/src/lib/components/primitives/input/input.svelte +2 -2
  158. package/src/lib/components/primitives/input-group/input-group.svelte +1 -1
  159. package/src/lib/components/primitives/popover/popover-content.svelte +1 -1
  160. package/src/lib/components/primitives/resizable/resizable-handle.svelte +2 -2
  161. package/src/lib/components/primitives/search/search.svelte +1 -1
  162. package/src/lib/components/primitives/select/select-content.svelte +1 -1
  163. package/src/lib/components/primitives/select/select-item.svelte +1 -1
  164. package/src/lib/components/primitives/select/select-trigger.svelte +1 -1
  165. package/src/lib/components/primitives/slider/slider.svelte +1 -1
  166. package/src/lib/components/primitives/switch/switch.svelte +1 -1
  167. package/src/lib/components/primitives/tabs/tabs-list.svelte +1 -1
  168. package/src/lib/components/primitives/tabs/tabs-trigger.svelte +1 -1
  169. package/src/lib/components/primitives/textarea/textarea.svelte +1 -1
  170. package/src/lib/components/primitives/toggle/toggle.svelte +8 -2
  171. package/src/lib/components/primitives/toggle-group/toggle-group-item.svelte +1 -1
  172. package/src/lib/components/primitives/toggle-group/toggle-group.svelte +1 -1
  173. package/src/lib/components/primitives/tooltip/index.ts +19 -0
  174. package/src/lib/components/primitives/tooltip/tooltip-content.svelte +34 -0
  175. package/src/lib/components/primitives/tooltip/tooltip-portal.svelte +7 -0
  176. package/src/lib/components/primitives/tooltip/tooltip-provider.svelte +7 -0
  177. package/src/lib/components/primitives/tooltip/tooltip-trigger.svelte +7 -0
  178. package/src/lib/components/primitives/tooltip/tooltip.svelte +7 -0
  179. package/src/lib/components/viewer/MeshMetadataPanel.svelte +1 -1
  180. package/src/lib/components/viewer/SceneManager.svelte +19 -19
  181. package/src/lib/components/viewer/Viewer.svelte +90 -164
  182. package/src/lib/components/viewer/ViewerDock.svelte +404 -0
  183. package/src/lib/i18n/messages.ts +32 -0
  184. package/src/lib/styles/base.css +81 -80
  185. package/src/lib/utils/color-space.ts +107 -0
  186. package/dist/components/primitives/theme-switcher/ThemeSwitcher.svelte +0 -39
  187. package/dist/components/primitives/theme-switcher/ThemeSwitcher.svelte.d.ts +0 -3
  188. package/dist/components/primitives/theme-switcher/index.d.ts +0 -1
  189. package/dist/components/primitives/theme-switcher/index.js +0 -1
  190. package/dist/styles/themes/cyberpunk.css +0 -98
  191. package/dist/styles/themes/neutral.css +0 -72
  192. package/dist/styles/themes/ocean.css +0 -75
  193. package/dist/styles/themes/selva.css +0 -105
  194. package/dist/theme/themeStore.svelte.d.ts +0 -12
  195. package/dist/theme/themeStore.svelte.js +0 -40
  196. package/dist/theme/themes.d.ts +0 -5
  197. package/dist/theme/themes.js +0 -13
  198. package/src/lib/components/primitives/theme-switcher/ThemeSwitcher.svelte +0 -39
  199. package/src/lib/components/primitives/theme-switcher/index.ts +0 -1
  200. package/src/lib/styles/themes/cyberpunk.css +0 -98
  201. package/src/lib/styles/themes/neutral.css +0 -72
  202. package/src/lib/styles/themes/ocean.css +0 -75
  203. package/src/lib/styles/themes/selva.css +0 -105
  204. package/src/lib/theme/themeStore.svelte.ts +0 -50
  205. package/src/lib/theme/themes.ts +0 -18
@@ -297,14 +297,14 @@
297
297
  }
298
298
 
299
299
  const toolButton =
300
- 'flex h-7 min-w-7 items-center justify-center rounded px-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-40';
300
+ 'flex h-7 min-w-7 items-center justify-center rounded-sm px-1.5 text-muted-foreground transition-colors hover:bg-hover hover:text-foreground disabled:pointer-events-none disabled:opacity-40';
301
301
  </script>
302
302
 
303
303
  <!-- The layout hides this output until a solve returns a file, so there is no empty state here. -->
304
304
  {#if error || unsupported || current}
305
305
  <div
306
306
  bind:this={wrapperEl}
307
- class="flex flex-col overflow-hidden rounded-lg border border-border bg-background {isFullscreen
307
+ class="flex flex-col overflow-hidden rounded-lg border border-border-strong bg-background {isFullscreen
308
308
  ? 'h-dvh'
309
309
  : ''}"
310
310
  >
@@ -375,7 +375,7 @@
375
375
  onclick={() => step(-1)}
376
376
  title="{t.previousImage} (←)"
377
377
  aria-label={t.previousImage}
378
- class="left-2 p-1.5 backdrop-blur-sm absolute top-1/2 -translate-y-1/2 rounded-full border border-border bg-background/80 text-foreground hover:bg-background"
378
+ class="left-2 p-1.5 backdrop-blur-sm absolute top-1/2 -translate-y-1/2 rounded-full border border-border-strong bg-background/80 text-foreground hover:bg-background"
379
379
  >
380
380
  <ChevronLeft size={16} />
381
381
  </button>
@@ -383,14 +383,14 @@
383
383
  onclick={() => step(1)}
384
384
  title="{t.nextImage} (→)"
385
385
  aria-label={t.nextImage}
386
- class="right-2 p-1.5 backdrop-blur-sm absolute top-1/2 -translate-y-1/2 rounded-full border border-border bg-background/80 text-foreground hover:bg-background"
386
+ class="right-2 p-1.5 backdrop-blur-sm absolute top-1/2 -translate-y-1/2 rounded-full border border-border-strong bg-background/80 text-foreground hover:bg-background"
387
387
  >
388
388
  <ChevronRight size={16} />
389
389
  </button>
390
390
  {/if}
391
391
 
392
392
  <div
393
- class="px-2.5 py-1 text-xs bottom-2 rounded pointer-events-none absolute left-1/2 -translate-x-1/2 bg-foreground/80 text-background transition-opacity {showWheelHint
393
+ class="px-2.5 py-1 text-xs bottom-2 pointer-events-none absolute left-1/2 -translate-x-1/2 rounded-sm bg-foreground text-background transition-opacity {showWheelHint
394
394
  ? 'opacity-100'
395
395
  : 'opacity-0'}"
396
396
  >
@@ -404,7 +404,7 @@
404
404
  <button
405
405
  onclick={() => select(i)}
406
406
  title="{image.fileName}{image.fileType}"
407
- class="h-12 w-16 rounded shrink-0 overflow-hidden border-2 bg-muted transition-colors {i ===
407
+ class="h-12 w-16 shrink-0 overflow-hidden rounded-sm border-2 bg-muted transition-colors {i ===
408
408
  currentIndex
409
409
  ? 'border-primary'
410
410
  : 'border-transparent opacity-70 hover:opacity-100'}"
@@ -378,6 +378,7 @@
378
378
  {inputId}
379
379
  value={typeof value === 'string' ? value : '#000000'}
380
380
  onChange={(newValue) => commit(newValue)}
381
+ {disabled}
381
382
  />
382
383
  {/if}
383
384
  </div>
@@ -38,7 +38,7 @@
38
38
  );
39
39
  const hasValue = $derived(value != null);
40
40
 
41
- const boxClass = 'min-h-12.5 rounded-md px-3 py-3 text-sm border border-border/60';
41
+ const boxClass = 'min-h-12.5 rounded-md px-3 py-3 text-sm border border-border-strong';
42
42
 
43
43
  async function copyToClipboard() {
44
44
  try {
@@ -194,7 +194,7 @@
194
194
  <span class="font-medium">{node.name}</span>
195
195
  </button>
196
196
  {#if isOpen}
197
- <div class="ml-4 pl-2 border-l border-border/50">
197
+ <div class="ml-4 pl-2 border-l border-border-strong">
198
198
  {@render treeNodes(node.children, folderPath)}
199
199
  </div>
200
200
  {/if}
@@ -217,12 +217,14 @@
217
217
 
218
218
  {#snippet fileDisplay()}
219
219
  {#if fileCount === 0}
220
- <div class="px-3 py-3 text-sm rounded-md border border-border bg-muted text-muted-foreground">
220
+ <div
221
+ class="px-3 py-3 text-sm rounded-md border border-border-strong bg-muted text-muted-foreground"
222
+ >
221
223
  {t.waitingForFiles}
222
224
  </div>
223
225
  {:else}
224
226
  <div class="gap-2 flex flex-col">
225
- <div class="px-3 py-2 rounded-md border border-border bg-muted">
227
+ <div class="px-3 py-2 rounded-md border border-border-strong bg-muted">
226
228
  <div class="gap-2 flex items-center justify-between">
227
229
  <span class="min-w-0 text-sm font-medium truncate text-foreground">
228
230
  {fileCount === 1
@@ -313,10 +315,10 @@
313
315
  <div class="group relative">
314
316
  {#if isObjectValue}
315
317
  <pre
316
- class="{boxClass} max-h-96 font-mono overflow-auto bg-muted/10 wrap-anywhere text-foreground">{formattedValue}</pre>
318
+ class="{boxClass} max-h-96 font-mono overflow-auto bg-muted wrap-anywhere text-foreground">{formattedValue}</pre>
317
319
  {:else}
318
320
  <div
319
- class="{boxClass} bg-muted/10 wrap-anywhere wrap-break-word whitespace-pre-wrap text-foreground"
321
+ class="{boxClass} bg-muted wrap-anywhere wrap-break-word whitespace-pre-wrap text-foreground"
320
322
  >
321
323
  {#if hasValue}{value}{:else}{@render placeholder()}{/if}
322
324
  </div>
@@ -12,9 +12,11 @@
12
12
  tabs: TabConfig[];
13
13
  activeTabId: string;
14
14
  onTabChange: (tabId: string) => void;
15
+ /** Matches TabContent's tighter gutter in the stacked phone layout. */
16
+ bare?: boolean;
15
17
  }
16
18
 
17
- let { tabs, activeTabId, onTabChange }: Props = $props();
19
+ let { tabs, activeTabId, onTabChange, bare = false }: Props = $props();
18
20
 
19
21
  const SCROLL_BUTTON_WIDTH = 32;
20
22
 
@@ -64,7 +66,7 @@
64
66
  }
65
67
  </script>
66
68
 
67
- <div class="tab-bar relative shrink-0 border-b border-border">
69
+ <div class="tab-bar relative shrink-0 border-b border-border {bare ? 'mx-1' : 'mx-3'}">
68
70
  <Tabs.List bind:ref={listRef} variant="line" onscroll={updateOverflow} onwheel={handleWheel}>
69
71
  {#each tabs as tab (tab.id)}
70
72
  <Tabs.Trigger value={tab.id} onclick={() => onTabChange(tab.id)} variant="line">
@@ -127,11 +129,11 @@
127
129
  .scroll-button.back {
128
130
  left: 0;
129
131
  padding: 0 1.25rem 0 0.375rem;
130
- background: linear-gradient(to right, var(--card) 55%, transparent);
132
+ background: linear-gradient(to right, var(--tab-bar-bg, var(--background)) 55%, transparent);
131
133
  }
132
134
  .scroll-button.forward {
133
135
  right: 0;
134
136
  padding: 0 0.375rem 0 1.25rem;
135
- background: linear-gradient(to left, var(--card) 55%, transparent);
137
+ background: linear-gradient(to left, var(--tab-bar-bg, var(--background)) 55%, transparent);
136
138
  }
137
139
  </style>
@@ -137,7 +137,7 @@
137
137
  {#if bare}
138
138
  {@render body()}
139
139
  {:else}
140
- <div class="h-full bg-muted dark:bg-background">
140
+ <div class="h-full">
141
141
  <!-- `auto`, not the default `hover`: a hover-only bar comes and goes with the pointer,
142
142
  and a host that freezes mid-solve drops the pointer state with it. -->
143
143
  <ScrollArea class="h-full" orientation="vertical" type="auto">
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import type { UISchema, SupportedTypes } from '@selvajs/schemas';
3
- import * as Card from '$lib/components/primitives/card';
4
3
  import * as Tabs from '$lib/components/primitives/tabs';
5
4
  import TabBar from './TabBar.svelte';
6
5
  import TabContent from './TabContent.svelte';
@@ -81,7 +80,7 @@
81
80
  {#snippet tabs()}
82
81
  <Tabs.Root value={activeTabId} variant="line" class="min-h-0 flex-1">
83
82
  {#if showTabBar}
84
- <TabBar tabs={visibleTabs} {activeTabId} onTabChange={handleTabChange} />
83
+ <TabBar tabs={visibleTabs} {activeTabId} onTabChange={handleTabChange} {bare} />
85
84
  {/if}
86
85
 
87
86
  {#each visibleTabs as tab (tab.id)}
@@ -104,7 +103,7 @@
104
103
  {#if bare}
105
104
  {@render tabs()}
106
105
  {:else}
107
- <Card.Root variant="flush" class="min-h-0 flex-1">
106
+ <div class="min-h-0 flex flex-1 flex-col">
108
107
  {@render tabs()}
109
- </Card.Root>
108
+ </div>
110
109
  {/if}
@@ -30,7 +30,7 @@
30
30
  }
31
31
  </script>
32
32
 
33
- <div class="divide-y divide-border/60 overflow-hidden rounded-md border border-input">
33
+ <div class="divide-y divide-border overflow-hidden rounded-md border border-input">
34
34
  {#each Object.entries(options) as [name, expr] (name)}
35
35
  {@const optionValue = expr ?? name}
36
36
  {@const optionId = `${inputId}-${optionValue}`}
@@ -38,8 +38,8 @@
38
38
  {@const glyph = optionGlyph(config?.optionMedia?.[name], assets)}
39
39
  <label
40
40
  for={optionId}
41
- class="gap-3 px-3 py-2 text-sm flex items-center transition-colors select-none hover:bg-accent/50 {isSelected
42
- ? 'bg-accent/30'
41
+ class="gap-3 px-3 py-2 text-sm flex items-center transition-colors select-none hover:bg-hover {isSelected
42
+ ? 'bg-selected'
43
43
  : ''} {disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}"
44
44
  >
45
45
  <Checkbox
@@ -1,23 +1,365 @@
1
1
  <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import { Check, Pipette } from '@lucide/svelte';
4
+ import * as Popover from '$lib/components/primitives/popover';
5
+ import { Input } from '$lib/components/primitives/input';
6
+ import { getLocaleContext } from '$lib/i18n/localeContext.svelte';
7
+ import { formatMessage } from '$lib/i18n/messages';
8
+ import { debounce } from '$lib/utils/debounce';
9
+ import {
10
+ contrastRatio,
11
+ hexToHsv,
12
+ hsvToHex,
13
+ normalizeHex,
14
+ type Hsv
15
+ } from '$lib/utils/color-space';
16
+
2
17
  interface Props {
3
18
  inputId: string;
4
19
  value?: string;
5
20
  onChange: (value: string) => void;
21
+ disabled?: boolean;
22
+ }
23
+
24
+ let { inputId, value = '#000000', onChange, disabled = false }: Props = $props();
25
+
26
+ const PRESETS = [
27
+ '#000000',
28
+ '#ffffff',
29
+ '#9ca3af',
30
+ '#ef4444',
31
+ '#f97316',
32
+ '#eab308',
33
+ '#22c55e',
34
+ '#14b8a6',
35
+ '#3b82f6',
36
+ '#8b5cf6',
37
+ '#ec4899',
38
+ '#78350f'
39
+ ];
40
+
41
+ interface EyeDropperApi {
42
+ open(): Promise<{ sRGBHex: string }>;
43
+ }
44
+ const eyeDropperCtor = (globalThis as { EyeDropper?: new () => EyeDropperApi }).EyeDropper;
45
+
46
+ const locale = getLocaleContext();
47
+ const t = $derived(locale.messages);
48
+
49
+ const parse = (hex: string): Hsv => hexToHsv(normalizeHex(hex) ?? '#000000')!;
50
+
51
+ let hsv = $state<Hsv>(untrack(() => parse(value)));
52
+ let draft = $state('');
53
+ let hexError = $state(false);
54
+ let areaEl = $state<HTMLElement | null>(null);
55
+
56
+ const currentHex = $derived(hsvToHex(hsv));
57
+ const hueColor = $derived(hsvToHex({ h: hsv.h, s: 1, v: 1 }));
58
+
59
+ // An external value only replaces the picker's state when it names a different color.
60
+ // Rebuilding from the hex alone would reset the hue whenever s or v hits 0.
61
+ $effect(() => {
62
+ const next = normalizeHex(value) ?? '#000000';
63
+ if (next !== untrack(() => currentHex)) hsv = parse(next);
64
+ });
65
+
66
+ $effect(() => {
67
+ draft = currentHex;
68
+ hexError = false;
69
+ });
70
+
71
+ // Dragging only updates local state; the host sees one change per gesture, since each
72
+ // change can trigger a solve.
73
+ function commit() {
74
+ commitDebounced.cancel();
75
+ if (currentHex !== normalizeHex(value)) onChange(currentHex);
76
+ }
77
+ const commitDebounced = debounce(commit, 150);
78
+
79
+ const clamp01 = (n: number) => Math.min(1, Math.max(0, n));
80
+
81
+ function setFromPointer(e: PointerEvent) {
82
+ if (!areaEl) return;
83
+ const rect = areaEl.getBoundingClientRect();
84
+ hsv = {
85
+ h: hsv.h,
86
+ s: clamp01((e.clientX - rect.left) / rect.width),
87
+ v: clamp01(1 - (e.clientY - rect.top) / rect.height)
88
+ };
89
+ }
90
+
91
+ function onAreaPointerDown(e: PointerEvent) {
92
+ if (disabled || e.button !== 0) return;
93
+ e.preventDefault();
94
+ areaEl?.focus();
95
+ areaEl?.setPointerCapture(e.pointerId);
96
+ setFromPointer(e);
97
+ }
98
+
99
+ function onAreaPointerMove(e: PointerEvent) {
100
+ if (areaEl?.hasPointerCapture(e.pointerId)) setFromPointer(e);
101
+ }
102
+
103
+ function onAreaPointerUp(e: PointerEvent) {
104
+ if (!areaEl?.hasPointerCapture(e.pointerId)) return;
105
+ areaEl.releasePointerCapture(e.pointerId);
106
+ commit();
107
+ }
108
+
109
+ function onAreaKeydown(e: KeyboardEvent) {
110
+ const step = e.shiftKey ? 0.1 : 0.01;
111
+ let { s, v } = hsv;
112
+ switch (e.key) {
113
+ case 'ArrowLeft':
114
+ s -= step;
115
+ break;
116
+ case 'ArrowRight':
117
+ s += step;
118
+ break;
119
+ case 'ArrowUp':
120
+ v += step;
121
+ break;
122
+ case 'ArrowDown':
123
+ v -= step;
124
+ break;
125
+ case 'PageUp':
126
+ v += 0.1;
127
+ break;
128
+ case 'PageDown':
129
+ v -= 0.1;
130
+ break;
131
+ default:
132
+ return;
133
+ }
134
+ e.preventDefault();
135
+ hsv = { h: hsv.h, s: clamp01(s), v: clamp01(v) };
136
+ commitDebounced();
6
137
  }
7
138
 
8
- let { inputId, value = '#000000', onChange }: Props = $props();
139
+ function onHueInput(e: Event) {
140
+ hsv = { ...hsv, h: Number((e.currentTarget as HTMLInputElement).value) };
141
+ }
142
+
143
+ function applyHex(hex: string) {
144
+ hsv = parse(hex);
145
+ commit();
146
+ }
147
+
148
+ function commitDraft() {
149
+ const hex = normalizeHex(draft);
150
+ if (!hex) {
151
+ hexError = true;
152
+ return;
153
+ }
154
+ hexError = false;
155
+ applyHex(hex);
156
+ draft = hex;
157
+ }
158
+
159
+ function onHexKeydown(e: KeyboardEvent) {
160
+ if (e.key === 'Enter') {
161
+ e.preventDefault();
162
+ commitDraft();
163
+ }
164
+ }
165
+
166
+ function onHexBlur() {
167
+ if (normalizeHex(draft)) commitDraft();
168
+ else {
169
+ draft = currentHex;
170
+ hexError = false;
171
+ }
172
+ }
173
+
174
+ async function pickFromScreen() {
175
+ if (!eyeDropperCtor) return;
176
+ try {
177
+ const { sRGBHex } = await new eyeDropperCtor().open();
178
+ applyHex(sRGBHex);
179
+ } catch {
180
+ // The user dismissed the eyedropper.
181
+ }
182
+ }
183
+
184
+ const checkOn = (hex: string) =>
185
+ (contrastRatio(hex, '#000000') ?? 0) > (contrastRatio(hex, '#ffffff') ?? 0)
186
+ ? 'text-black'
187
+ : 'text-white';
9
188
  </script>
10
189
 
11
- <div class="gap-2 flex items-center">
12
- <input
13
- id={inputId}
14
- type="color"
15
- value={value || '#000000'}
16
- class="h-9 w-12 rounded p-1 cursor-pointer border border-input bg-background dark:bg-input/30"
17
- onchange={(e) => {
18
- const newValue = (e.currentTarget as HTMLInputElement).value;
19
- onChange(newValue);
20
- }}
21
- />
22
- <span class="font-mono text-sm text-muted-foreground">{value || '#000000'}</span>
23
- </div>
190
+ <Popover.Root>
191
+ <Popover.Trigger>
192
+ {#snippet child({ props })}
193
+ <button
194
+ {...props}
195
+ type="button"
196
+ id={inputId}
197
+ {disabled}
198
+ aria-label={formatMessage(t.colorPick, { color: currentHex })}
199
+ class="h-9 gap-2.5 px-2.5 shadow-xs max-w-48 flex w-full cursor-pointer items-center rounded-md border border-input bg-background text-left outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
200
+ >
201
+ <span
202
+ class="size-5 shrink-0 rounded-md bg-(--swatch) ring-1 ring-border ring-inset"
203
+ style:--swatch={currentHex}
204
+ ></span>
205
+ <span class="font-mono text-sm text-muted-foreground">{currentHex}</span>
206
+ </button>
207
+ {/snippet}
208
+ </Popover.Trigger>
209
+
210
+ <Popover.Content align="start">
211
+ <div
212
+ bind:this={areaEl}
213
+ role="slider"
214
+ tabindex={disabled ? -1 : 0}
215
+ aria-label={t.colorArea}
216
+ aria-valuemin={0}
217
+ aria-valuemax={100}
218
+ aria-valuenow={Math.round(hsv.v * 100)}
219
+ aria-valuetext={formatMessage(t.colorAreaValue, {
220
+ s: Math.round(hsv.s * 100),
221
+ v: Math.round(hsv.v * 100)
222
+ })}
223
+ aria-disabled={disabled}
224
+ class="color-area h-36 relative w-full cursor-crosshair touch-none rounded-md outline-none focus-visible:ring-3 focus-visible:ring-ring"
225
+ style:--hue={hueColor}
226
+ onpointerdown={onAreaPointerDown}
227
+ onpointermove={onAreaPointerMove}
228
+ onpointerup={onAreaPointerUp}
229
+ onpointercancel={onAreaPointerUp}
230
+ onkeydown={onAreaKeydown}
231
+ >
232
+ <span
233
+ class="color-thumb size-4 border-white pointer-events-none absolute -translate-x-1/2 -translate-y-1/2 rounded-full border-2 bg-(--swatch)"
234
+ style:--x="{hsv.s * 100}%"
235
+ style:--y="{(1 - hsv.v) * 100}%"
236
+ style:--swatch={currentHex}
237
+ ></span>
238
+ </div>
239
+
240
+ <input
241
+ type="range"
242
+ class="color-hue"
243
+ min="0"
244
+ max="360"
245
+ step="1"
246
+ aria-label={t.colorHue}
247
+ value={Math.round(hsv.h)}
248
+ {disabled}
249
+ oninput={onHueInput}
250
+ onchange={commit}
251
+ />
252
+
253
+ <div class="gap-2 flex items-start">
254
+ <div class="min-w-0 flex-1">
255
+ <Input
256
+ type="text"
257
+ aria-label={t.colorHex}
258
+ aria-invalid={hexError}
259
+ aria-describedby={hexError ? `${inputId}-hex-error` : undefined}
260
+ autocomplete="off"
261
+ spellcheck={false}
262
+ maxlength={7}
263
+ {disabled}
264
+ bind:value={draft}
265
+ onkeydown={onHexKeydown}
266
+ onblur={onHexBlur}
267
+ />
268
+ </div>
269
+ {#if eyeDropperCtor}
270
+ <button
271
+ type="button"
272
+ {disabled}
273
+ aria-label={t.colorEyedropper}
274
+ title={t.colorEyedropper}
275
+ class="size-9 flex shrink-0 cursor-pointer items-center justify-center rounded-md border border-input bg-background outline-none hover:bg-hover focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
276
+ onclick={pickFromScreen}
277
+ >
278
+ <Pipette class="size-4" aria-hidden="true" />
279
+ </button>
280
+ {/if}
281
+ </div>
282
+ {#if hexError}
283
+ <p id="{inputId}-hex-error" role="alert" class="text-xs text-destructive">
284
+ {t.colorHexInvalid}
285
+ </p>
286
+ {/if}
287
+
288
+ <div role="group" aria-label={t.colorPresets} class="gap-1.5 grid grid-cols-6">
289
+ {#each PRESETS as preset (preset)}
290
+ {@const selected = preset === currentHex}
291
+ <button
292
+ type="button"
293
+ {disabled}
294
+ aria-label={preset}
295
+ aria-pressed={selected}
296
+ class="h-6 flex cursor-pointer items-center justify-center rounded-md bg-(--swatch) ring-1 ring-border outline-none ring-inset focus-visible:ring-3 focus-visible:ring-ring disabled:cursor-not-allowed"
297
+ style:--swatch={preset}
298
+ onclick={() => applyHex(preset)}
299
+ >
300
+ {#if selected}
301
+ <Check class="size-3.5 {checkOn(preset)}" aria-hidden="true" />
302
+ {/if}
303
+ </button>
304
+ {/each}
305
+ </div>
306
+ </Popover.Content>
307
+ </Popover.Root>
308
+
309
+ <style>
310
+ .color-area {
311
+ background-color: var(--hue);
312
+ background-image:
313
+ linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, transparent);
314
+ }
315
+ .color-thumb {
316
+ left: var(--x);
317
+ top: var(--y);
318
+ box-shadow: 0 0 0 1px rgb(0 0 0 / 0.4);
319
+ }
320
+ .color-hue {
321
+ appearance: none;
322
+ -webkit-appearance: none;
323
+ height: 0.75rem;
324
+ width: 100%;
325
+ margin: 0;
326
+ border-radius: 9999px;
327
+ cursor: pointer;
328
+ outline: none;
329
+ background: linear-gradient(
330
+ to right,
331
+ #f00 0%,
332
+ #ff0 16.67%,
333
+ #0f0 33.33%,
334
+ #0ff 50%,
335
+ #00f 66.67%,
336
+ #f0f 83.33%,
337
+ #f00 100%
338
+ );
339
+ }
340
+ .color-hue::-webkit-slider-thumb {
341
+ -webkit-appearance: none;
342
+ height: 1rem;
343
+ width: 1rem;
344
+ border-radius: 9999px;
345
+ border: 2px solid #fff;
346
+ background: transparent;
347
+ box-shadow: 0 0 0 1px rgb(0 0 0 / 0.4);
348
+ }
349
+ .color-hue::-moz-range-thumb {
350
+ height: 1rem;
351
+ width: 1rem;
352
+ box-sizing: border-box;
353
+ border-radius: 9999px;
354
+ border: 2px solid #fff;
355
+ background: transparent;
356
+ box-shadow: 0 0 0 1px rgb(0 0 0 / 0.4);
357
+ }
358
+ .color-hue:focus-visible {
359
+ box-shadow: 0 0 0 3px var(--ring);
360
+ }
361
+ .color-hue:disabled {
362
+ cursor: not-allowed;
363
+ opacity: 0.5;
364
+ }
365
+ </style>
@@ -282,10 +282,10 @@
282
282
  type="button"
283
283
  onclick={() => (activeMode = mode)}
284
284
  aria-pressed={activeMode === mode}
285
- class={`px-3 py-1 font-medium flex-1 rounded-sm transition-colors ${
285
+ class={`px-3 py-1 font-medium flex-1 rounded-sm border transition-colors ${
286
286
  activeMode === mode
287
- ? 'shadow-xs bg-background text-foreground'
288
- : 'text-muted-foreground hover:text-foreground'
287
+ ? 'shadow-xs border-border-strong bg-raised text-foreground'
288
+ : 'border-transparent text-muted-foreground hover:text-foreground'
289
289
  }`}
290
290
  >
291
291
  {mode === 'upload' ? t.fileModeUpload : t.fileModeUrl}
@@ -23,7 +23,8 @@
23
23
 
24
24
  <ToggleGroup.Root
25
25
  type="single"
26
- variant="outline"
26
+ variant="segmented"
27
+ spacing={1}
27
28
  size={hasIcons ? 'stacked' : 'default'}
28
29
  class="w-full"
29
30
  aria-labelledby={labelId}
@@ -45,6 +45,7 @@
45
45
  bind:this={tiles[index]}
46
46
  type="button"
47
47
  role="radio"
48
+ title={option.name}
48
49
  aria-checked={selected}
49
50
  tabindex={index === focusIndex ? 0 : -1}
50
51
  {disabled}
@@ -52,7 +53,7 @@
52
53
  onkeydown={(e) => onKeydown(e, index)}
53
54
  class="gap-1.5 p-1.5 min-w-0 flex flex-col rounded-lg border text-left transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 {selected
54
55
  ? 'border-primary bg-primary/5 ring-1 ring-primary'
55
- : 'border-input hover:bg-accent/40'}"
56
+ : 'border-input hover:bg-hover'}"
56
57
  >
57
58
  <span
58
59
  class="flex aspect-square w-full items-center justify-center overflow-hidden rounded-md bg-muted text-muted-foreground"
@@ -65,7 +66,7 @@
65
66
  <span class="text-lg font-medium" aria-hidden="true">{option.name.charAt(0)}</span>
66
67
  {/if}
67
68
  </span>
68
- <span class="px-0.5 text-xs leading-tight truncate" class:font-medium={selected}>
69
+ <span class="px-0.5 text-xs leading-tight line-clamp-2" class:font-medium={selected}>
69
70
  {option.name}
70
71
  </span>
71
72
  </button>
@@ -38,7 +38,7 @@
38
38
  });
39
39
  </script>
40
40
 
41
- <div class="bottom-0 mt-3 pb-3 pt-2 backdrop-blur-sm px-2 sticky border-t border-border/50">
41
+ <div class="bottom-0 mt-3 pb-3 pt-2 backdrop-blur-sm px-2 sticky border-t border-border">
42
42
  <span class="sr-only" role="status" aria-live="polite">{announcement}</span>
43
43
  <div class="gap-2 flex">
44
44
  <Button
@@ -55,7 +55,7 @@
55
55
  <Loader class="h-4 w-4 animate-spin" />
56
56
  {t.solvingEllipsis}
57
57
  <kbd
58
- class="px-1 font-sans rounded border border-current/30 text-2xs opacity-70"
58
+ class="px-1 font-sans rounded-sm border border-current/30 text-2xs opacity-70"
59
59
  title={t.solveMessagesEscHint}
60
60
  >
61
61
  Esc
@@ -8,7 +8,7 @@
8
8
  variants: {
9
9
  tone: {
10
10
  info: 'border-info/40 bg-info/10',
11
- tip: 'border-border bg-muted/40',
11
+ tip: 'border-border bg-muted',
12
12
  success: 'border-success/40 bg-success/10',
13
13
  warning: 'border-warning/40 bg-warning/10',
14
14
  danger: 'border-destructive/40 bg-destructive/10'
@@ -38,7 +38,7 @@
38
38
  title?: string;
39
39
  /** Replaces the tone's default icon. `null` drops the icon entirely. */
40
40
  icon?: Component | null;
41
- children: Snippet;
41
+ children?: Snippet;
42
42
  }
43
43
 
44
44
  let { tone = 'info', title, icon, class: className, children, ...restProps }: Props = $props();
@@ -59,8 +59,10 @@
59
59
  {#if title}
60
60
  <p class={cn('leading-tight font-medium', accent)}>{title}</p>
61
61
  {/if}
62
- <div class="leading-relaxed text-muted-foreground [&_a]:underline [&_strong]:text-foreground">
63
- {@render children()}
64
- </div>
62
+ {#if children}
63
+ <div class="leading-relaxed text-muted-foreground [&_a]:underline [&_strong]:text-foreground">
64
+ {@render children()}
65
+ </div>
66
+ {/if}
65
67
  </div>
66
68
  </div>