@selvajs/ui 6.6.0-beta.2 → 6.6.0-beta.20

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 (401) hide show
  1. package/README.md +0 -3
  2. package/dist/components/ErrorScreen.svelte +7 -3
  3. package/dist/components/compute/AppLayout.svelte +307 -123
  4. package/dist/components/compute/AppLayout.svelte.d.ts +28 -0
  5. package/dist/components/compute/CollapsedPanelStrip.svelte +4 -2
  6. package/dist/components/compute/ComputeApp.svelte +46 -22
  7. package/dist/components/compute/ComputeApp.svelte.d.ts +20 -3
  8. package/dist/components/compute/ComputeMessagesDialog.svelte +31 -41
  9. package/dist/components/compute/ParameterPresetManager.svelte +16 -65
  10. package/dist/components/compute/ParameterPresetManager.svelte.d.ts +4 -5
  11. package/dist/components/compute/SolveMessages.svelte +128 -121
  12. package/dist/components/compute/SolveMessages.svelte.d.ts +12 -12
  13. package/dist/components/layout/AppShell.svelte +10 -6
  14. package/dist/components/layout/FooterItemRenderer.svelte +2 -2
  15. package/dist/components/layout/FooterItemRenderer.svelte.d.ts +1 -1
  16. package/dist/components/layout/PageContent.svelte +3 -2
  17. package/dist/components/layout/PageFooter.svelte +26 -14
  18. package/dist/components/layout/PageHeader.svelte +14 -10
  19. package/dist/components/layout/SectionHeader.svelte +4 -2
  20. package/dist/components/layout/SideNav.svelte +57 -8
  21. package/dist/components/layout/SubNav.svelte +2 -2
  22. package/dist/components/preview/ChartOutput.svelte +131 -65
  23. package/dist/components/preview/Group.svelte +42 -22
  24. package/dist/components/preview/HelpButton.svelte +5 -3
  25. package/dist/components/preview/ImageOutput.svelte +435 -71
  26. package/dist/components/preview/InputControl.svelte +125 -51
  27. package/dist/components/preview/InputControl.svelte.d.ts +5 -1
  28. package/dist/components/preview/OutputDisplay.svelte +42 -28
  29. package/dist/components/preview/TabBar.svelte +8 -6
  30. package/dist/components/preview/TabBar.svelte.d.ts +2 -0
  31. package/dist/components/preview/TabContent.svelte +66 -31
  32. package/dist/components/preview/TabContent.svelte.d.ts +5 -1
  33. package/dist/components/preview/TabLayout.svelte +29 -8
  34. package/dist/components/preview/TabLayout.svelte.d.ts +5 -0
  35. package/dist/components/preview/inputs/CheckboxInput.svelte +1 -1
  36. package/dist/components/preview/inputs/ChecklistInput.svelte +11 -6
  37. package/dist/components/preview/inputs/ChecklistInput.svelte.d.ts +2 -1
  38. package/dist/components/preview/inputs/ColorInput.svelte +357 -13
  39. package/dist/components/preview/inputs/ColorInput.svelte.d.ts +2 -0
  40. package/dist/components/preview/inputs/DropdownInput.svelte +20 -5
  41. package/dist/components/preview/inputs/DropdownInput.svelte.d.ts +2 -1
  42. package/dist/components/preview/inputs/FileInput.svelte +115 -103
  43. package/dist/components/preview/inputs/GeometryInput.svelte +128 -0
  44. package/dist/components/preview/inputs/GeometryInput.svelte.d.ts +21 -0
  45. package/dist/components/preview/inputs/NumberInput.svelte +98 -16
  46. package/dist/components/preview/inputs/NumberInput.svelte.d.ts +2 -0
  47. package/dist/components/preview/inputs/OptionGlyph.svelte +29 -0
  48. package/dist/components/preview/inputs/OptionGlyph.svelte.d.ts +8 -0
  49. package/dist/components/preview/inputs/SegmentedInput.svelte +45 -0
  50. package/dist/components/preview/inputs/SegmentedInput.svelte.d.ts +12 -0
  51. package/dist/components/preview/inputs/TextInput.svelte +11 -6
  52. package/dist/components/preview/inputs/TileInput.svelte +83 -0
  53. package/dist/components/preview/inputs/TileInput.svelte.d.ts +12 -0
  54. package/dist/components/preview/inputs/index.d.ts +3 -0
  55. package/dist/components/preview/inputs/index.js +3 -0
  56. package/dist/components/primitives/CalculateButton.svelte +56 -24
  57. package/dist/components/primitives/CalculateButton.svelte.d.ts +3 -0
  58. package/dist/components/primitives/Callout.svelte +8 -6
  59. package/dist/components/primitives/Callout.svelte.d.ts +4 -4
  60. package/dist/components/primitives/DataTable.svelte +38 -4
  61. package/dist/components/primitives/DataTable.svelte.d.ts +2 -0
  62. package/dist/components/primitives/FilterableDropdown.svelte +13 -1
  63. package/dist/components/primitives/FilterableDropdown.svelte.d.ts +5 -0
  64. package/dist/components/primitives/ImageUploadField.svelte +1 -1
  65. package/dist/components/primitives/SolvingIndicator.svelte +245 -15
  66. package/dist/components/primitives/SolvingIndicator.svelte.d.ts +15 -0
  67. package/dist/components/primitives/alert-dialog/alert-dialog-content.svelte +1 -1
  68. package/dist/components/primitives/badge/badge.svelte +4 -4
  69. package/dist/components/primitives/badge/badge.svelte.d.ts +10 -10
  70. package/dist/components/primitives/button/button.svelte +10 -8
  71. package/dist/components/primitives/button/button.svelte.d.ts +20 -17
  72. package/dist/components/primitives/card/card.svelte +1 -1
  73. package/dist/components/primitives/card/card.svelte.d.ts +1 -1
  74. package/dist/components/primitives/checkbox/checkbox.svelte +1 -1
  75. package/dist/components/primitives/command/command-input.svelte +1 -1
  76. package/dist/components/primitives/command/command-item.svelte +1 -1
  77. package/dist/components/primitives/command/command-link-item.svelte +1 -1
  78. package/dist/components/primitives/command/command.svelte +1 -1
  79. package/dist/components/primitives/context-menu/context-menu-checkbox-item.svelte +1 -1
  80. package/dist/components/primitives/context-menu/context-menu-content.svelte +1 -1
  81. package/dist/components/primitives/context-menu/context-menu-item.svelte +1 -1
  82. package/dist/components/primitives/context-menu/context-menu-radio-item.svelte +1 -1
  83. package/dist/components/primitives/context-menu/context-menu-sub-content.svelte +1 -1
  84. package/dist/components/primitives/context-menu/context-menu-sub-trigger.svelte +1 -1
  85. package/dist/components/primitives/dialog/dialog-content.svelte +7 -3
  86. package/dist/components/primitives/drawer/Drawer.svelte +1 -1
  87. package/dist/components/primitives/dropdown-menu/dropdown-menu-checkbox-item.svelte +31 -11
  88. package/dist/components/primitives/dropdown-menu/dropdown-menu-checkbox-item.svelte.d.ts +2 -0
  89. package/dist/components/primitives/dropdown-menu/dropdown-menu-content.svelte +5 -1
  90. package/dist/components/primitives/dropdown-menu/dropdown-menu-content.svelte.d.ts +2 -0
  91. package/dist/components/primitives/dropdown-menu/dropdown-menu-item.svelte +4 -2
  92. package/dist/components/primitives/dropdown-menu/dropdown-menu-item.svelte.d.ts +2 -1
  93. package/dist/components/primitives/dropdown-menu/dropdown-menu-radio-item.svelte +1 -1
  94. package/dist/components/primitives/dropdown-menu/dropdown-menu-shortcut.svelte +1 -1
  95. package/dist/components/primitives/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  96. package/dist/components/primitives/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
  97. package/dist/components/primitives/index.d.ts +1 -1
  98. package/dist/components/primitives/index.js +1 -1
  99. package/dist/components/primitives/input/input.svelte +4 -4
  100. package/dist/components/primitives/input-group/input-group-input.svelte.d.ts +1 -1
  101. package/dist/components/primitives/input-group/input-group.svelte +1 -1
  102. package/dist/components/primitives/mode-toggle/mode-toggle.svelte +5 -2
  103. package/dist/components/primitives/popover/popover-content.svelte +1 -1
  104. package/dist/components/primitives/resizable/resizable-handle.svelte +2 -2
  105. package/dist/components/primitives/scroll-area/scroll-area.svelte +1 -1
  106. package/dist/components/primitives/search/search.svelte +2 -2
  107. package/dist/components/primitives/select/select-content.svelte +1 -1
  108. package/dist/components/primitives/select/select-item.svelte +1 -1
  109. package/dist/components/primitives/select/select-trigger.svelte +1 -1
  110. package/dist/components/primitives/slider/slider.svelte +8 -3
  111. package/dist/components/primitives/slider/slider.svelte.d.ts +5 -1
  112. package/dist/components/primitives/switch/switch.svelte +1 -1
  113. package/dist/components/primitives/tabs/tabs-list.svelte +2 -2
  114. package/dist/components/primitives/tabs/tabs-list.svelte.d.ts +6 -6
  115. package/dist/components/primitives/tabs/tabs-trigger.svelte +3 -3
  116. package/dist/components/primitives/tabs/tabs-trigger.svelte.d.ts +7 -7
  117. package/dist/components/primitives/textarea/textarea.svelte +1 -1
  118. package/dist/components/primitives/toggle/toggle.svelte +11 -3
  119. package/dist/components/primitives/toggle/toggle.svelte.d.ts +13 -4
  120. package/dist/components/primitives/toggle-group/toggle-group-item.svelte +1 -1
  121. package/dist/components/primitives/toggle-group/toggle-group.svelte +1 -1
  122. package/dist/components/primitives/tooltip/index.d.ts +6 -0
  123. package/dist/components/primitives/tooltip/index.js +8 -0
  124. package/dist/components/primitives/tooltip/tooltip-content.svelte +34 -0
  125. package/dist/components/primitives/tooltip/tooltip-content.svelte.d.ts +10 -0
  126. package/dist/components/primitives/tooltip/tooltip-portal.svelte +7 -0
  127. package/dist/components/primitives/tooltip/tooltip-portal.svelte.d.ts +4 -0
  128. package/dist/components/primitives/tooltip/tooltip-provider.svelte +7 -0
  129. package/dist/components/primitives/tooltip/tooltip-provider.svelte.d.ts +4 -0
  130. package/dist/components/primitives/tooltip/tooltip-trigger.svelte +7 -0
  131. package/dist/components/primitives/tooltip/tooltip-trigger.svelte.d.ts +4 -0
  132. package/dist/components/primitives/tooltip/tooltip.svelte +7 -0
  133. package/dist/components/primitives/tooltip/tooltip.svelte.d.ts +4 -0
  134. package/dist/components/viewer/MeshMetadataPanel.svelte +119 -0
  135. package/dist/components/viewer/MeshMetadataPanel.svelte.d.ts +9 -0
  136. package/dist/components/viewer/SceneManager.svelte +32 -21
  137. package/dist/components/viewer/SceneManager.svelte.d.ts +3 -1
  138. package/dist/components/viewer/Viewer.svelte +195 -207
  139. package/dist/components/viewer/Viewer.svelte.d.ts +7 -2
  140. package/dist/components/viewer/ViewerDock.svelte +404 -0
  141. package/dist/components/viewer/ViewerDock.svelte.d.ts +42 -0
  142. package/dist/composables/useFooterItem.svelte.d.ts +1 -1
  143. package/dist/composables/useFooterItem.svelte.js +1 -1
  144. package/dist/compute/liveProgress.svelte.d.ts +16 -0
  145. package/dist/compute/liveProgress.svelte.js +90 -0
  146. package/dist/compute/run-panel-action.d.ts +8 -0
  147. package/dist/compute/run-panel-action.js +12 -0
  148. package/dist/compute/solving.svelte.d.ts +11 -7
  149. package/dist/compute/solving.svelte.js +64 -45
  150. package/dist/compute/useSolveSession.svelte.js +3 -0
  151. package/dist/constants.d.ts +6 -8
  152. package/dist/constants.js +8 -11
  153. package/dist/contexts/hostContext.svelte.d.ts +39 -0
  154. package/dist/contexts/hostContext.svelte.js +9 -0
  155. package/dist/features/chart/plotly-compat.d.ts +15 -0
  156. package/dist/features/chart/plotly-compat.js +124 -0
  157. package/dist/i18n/localeContext.svelte.d.ts +10 -3
  158. package/dist/i18n/localeContext.svelte.js +27 -5
  159. package/dist/i18n/messages.d.ts +164 -2
  160. package/dist/i18n/messages.js +281 -11
  161. package/dist/index.d.ts +6 -1
  162. package/dist/index.js +5 -0
  163. package/dist/public.d.ts +2 -1
  164. package/dist/public.js +1 -0
  165. package/dist/schema/host-support.d.ts +29 -0
  166. package/dist/schema/host-support.js +97 -0
  167. package/dist/schema/option-display.d.ts +12 -0
  168. package/dist/schema/option-display.js +29 -0
  169. package/dist/schema/param-exporter.d.ts +4 -1
  170. package/dist/schema/param-exporter.js +10 -6
  171. package/dist/schema/visibility-rules.d.ts +7 -1
  172. package/dist/schema/visibility-rules.js +38 -0
  173. package/dist/styles/base.css +116 -83
  174. package/dist/types/actionButton.d.ts +17 -3
  175. package/dist/types/presetLabels.d.ts +3 -2
  176. package/dist/types/presetLabels.js +9 -8
  177. package/dist/utils/color-space.d.ts +21 -0
  178. package/dist/utils/color-space.js +90 -0
  179. package/dist/utils/file-download.js +10 -22
  180. package/dist/utils/host-files.d.ts +12 -0
  181. package/dist/utils/host-files.js +46 -0
  182. package/dist/utils/loadScript.d.ts +2 -0
  183. package/dist/utils/loadScript.js +2 -0
  184. package/dist/utils/webp-to-png.d.ts +10 -0
  185. package/dist/utils/webp-to-png.js +41 -0
  186. package/package.json +30 -22
  187. package/src/lib/components/ErrorScreen.svelte +7 -3
  188. package/src/lib/components/compute/AppLayout.svelte +307 -123
  189. package/src/lib/components/compute/CollapsedPanelStrip.svelte +4 -2
  190. package/src/lib/components/compute/ComputeApp.svelte +46 -22
  191. package/src/lib/components/compute/ComputeMessagesDialog.svelte +32 -42
  192. package/src/lib/components/compute/ParameterPresetManager.svelte +16 -65
  193. package/src/lib/components/compute/SolveMessages.svelte +130 -123
  194. package/src/lib/components/layout/AppShell.svelte +10 -6
  195. package/src/lib/components/layout/FooterItemRenderer.svelte +2 -2
  196. package/src/lib/components/layout/PageContent.svelte +3 -2
  197. package/src/lib/components/layout/PageFooter.svelte +26 -14
  198. package/src/lib/components/layout/PageHeader.svelte +14 -10
  199. package/src/lib/components/layout/SectionHeader.svelte +4 -2
  200. package/src/lib/components/layout/SideNav.svelte +57 -8
  201. package/src/lib/components/layout/SubNav.svelte +2 -2
  202. package/src/lib/components/preview/ChartOutput.svelte +131 -65
  203. package/src/lib/components/preview/Group.svelte +42 -22
  204. package/src/lib/components/preview/HelpButton.svelte +5 -3
  205. package/src/lib/components/preview/ImageOutput.svelte +435 -71
  206. package/src/lib/components/preview/InputControl.svelte +125 -51
  207. package/src/lib/components/preview/OutputDisplay.svelte +42 -28
  208. package/src/lib/components/preview/TabBar.svelte +8 -6
  209. package/src/lib/components/preview/TabContent.svelte +66 -31
  210. package/src/lib/components/preview/TabLayout.svelte +29 -8
  211. package/src/lib/components/preview/inputs/CheckboxInput.svelte +1 -1
  212. package/src/lib/components/preview/inputs/ChecklistInput.svelte +11 -6
  213. package/src/lib/components/preview/inputs/ColorInput.svelte +357 -13
  214. package/src/lib/components/preview/inputs/DropdownInput.svelte +20 -5
  215. package/src/lib/components/preview/inputs/FileInput.svelte +115 -103
  216. package/src/lib/components/preview/inputs/GeometryInput.svelte +128 -0
  217. package/src/lib/components/preview/inputs/NumberInput.svelte +98 -16
  218. package/src/lib/components/preview/inputs/OptionGlyph.svelte +29 -0
  219. package/src/lib/components/preview/inputs/SegmentedInput.svelte +45 -0
  220. package/src/lib/components/preview/inputs/TextInput.svelte +11 -6
  221. package/src/lib/components/preview/inputs/TileInput.svelte +83 -0
  222. package/src/lib/components/preview/inputs/index.ts +3 -0
  223. package/src/lib/components/primitives/CalculateButton.svelte +56 -24
  224. package/src/lib/components/primitives/Callout.svelte +9 -7
  225. package/src/lib/components/primitives/DataTable.svelte +38 -4
  226. package/src/lib/components/primitives/FilterableDropdown.svelte +14 -2
  227. package/src/lib/components/primitives/ImageUploadField.svelte +1 -1
  228. package/src/lib/components/primitives/IssueList.svelte +1 -1
  229. package/src/lib/components/primitives/SeverityIcon.svelte +1 -1
  230. package/src/lib/components/primitives/SolvingIndicator.svelte +245 -15
  231. package/src/lib/components/primitives/StateDisplay.svelte +1 -1
  232. package/src/lib/components/primitives/alert-dialog/alert-dialog-action.svelte +2 -2
  233. package/src/lib/components/primitives/alert-dialog/alert-dialog-cancel.svelte +2 -2
  234. package/src/lib/components/primitives/alert-dialog/alert-dialog-content.svelte +2 -2
  235. package/src/lib/components/primitives/alert-dialog/alert-dialog-description.svelte +1 -1
  236. package/src/lib/components/primitives/alert-dialog/alert-dialog-footer.svelte +1 -1
  237. package/src/lib/components/primitives/alert-dialog/alert-dialog-header.svelte +1 -1
  238. package/src/lib/components/primitives/alert-dialog/alert-dialog-overlay.svelte +1 -1
  239. package/src/lib/components/primitives/alert-dialog/alert-dialog-title.svelte +1 -1
  240. package/src/lib/components/primitives/badge/badge.svelte +5 -5
  241. package/src/lib/components/primitives/button/button.svelte +10 -8
  242. package/src/lib/components/primitives/button-group/button-group-separator.svelte +2 -2
  243. package/src/lib/components/primitives/button-group/button-group-text.svelte +1 -1
  244. package/src/lib/components/primitives/button-group/button-group.svelte +1 -1
  245. package/src/lib/components/primitives/card/card-content.svelte +1 -1
  246. package/src/lib/components/primitives/card/card-description.svelte +1 -1
  247. package/src/lib/components/primitives/card/card-footer.svelte +1 -1
  248. package/src/lib/components/primitives/card/card-header.svelte +1 -1
  249. package/src/lib/components/primitives/card/card-title.svelte +1 -1
  250. package/src/lib/components/primitives/card/card.svelte +2 -2
  251. package/src/lib/components/primitives/checkbox/checkbox.svelte +2 -2
  252. package/src/lib/components/primitives/command/command-dialog.svelte +2 -2
  253. package/src/lib/components/primitives/command/command-empty.svelte +1 -1
  254. package/src/lib/components/primitives/command/command-group.svelte +1 -1
  255. package/src/lib/components/primitives/command/command-input.svelte +3 -3
  256. package/src/lib/components/primitives/command/command-item.svelte +2 -2
  257. package/src/lib/components/primitives/command/command-link-item.svelte +2 -2
  258. package/src/lib/components/primitives/command/command-list.svelte +1 -1
  259. package/src/lib/components/primitives/command/command-separator.svelte +1 -1
  260. package/src/lib/components/primitives/command/command-shortcut.svelte +1 -1
  261. package/src/lib/components/primitives/command/command.svelte +2 -2
  262. package/src/lib/components/primitives/context-menu/context-menu-checkbox-item.svelte +2 -2
  263. package/src/lib/components/primitives/context-menu/context-menu-content.svelte +2 -2
  264. package/src/lib/components/primitives/context-menu/context-menu-group-heading.svelte +1 -1
  265. package/src/lib/components/primitives/context-menu/context-menu-item.svelte +2 -2
  266. package/src/lib/components/primitives/context-menu/context-menu-label.svelte +1 -1
  267. package/src/lib/components/primitives/context-menu/context-menu-radio-item.svelte +2 -2
  268. package/src/lib/components/primitives/context-menu/context-menu-separator.svelte +1 -1
  269. package/src/lib/components/primitives/context-menu/context-menu-shortcut.svelte +1 -1
  270. package/src/lib/components/primitives/context-menu/context-menu-sub-content.svelte +2 -2
  271. package/src/lib/components/primitives/context-menu/context-menu-sub-trigger.svelte +2 -2
  272. package/src/lib/components/primitives/dialog/dialog-content.svelte +8 -4
  273. package/src/lib/components/primitives/dialog/dialog-description.svelte +1 -1
  274. package/src/lib/components/primitives/dialog/dialog-footer.svelte +1 -1
  275. package/src/lib/components/primitives/dialog/dialog-header.svelte +1 -1
  276. package/src/lib/components/primitives/dialog/dialog-overlay.svelte +1 -1
  277. package/src/lib/components/primitives/dialog/dialog-title.svelte +1 -1
  278. package/src/lib/components/primitives/drawer/Drawer.svelte +1 -1
  279. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-checkbox-item.svelte +32 -12
  280. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-content.svelte +6 -2
  281. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-group-heading.svelte +1 -1
  282. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-item.svelte +5 -3
  283. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-label.svelte +1 -1
  284. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-radio-item.svelte +2 -2
  285. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-separator.svelte +1 -1
  286. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-shortcut.svelte +2 -2
  287. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-sub-content.svelte +2 -2
  288. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-sub-trigger.svelte +2 -2
  289. package/src/lib/components/primitives/empty/empty-content.svelte +1 -1
  290. package/src/lib/components/primitives/empty/empty-description.svelte +1 -1
  291. package/src/lib/components/primitives/empty/empty-header.svelte +1 -1
  292. package/src/lib/components/primitives/empty/empty-media.svelte +1 -1
  293. package/src/lib/components/primitives/empty/empty-title.svelte +1 -1
  294. package/src/lib/components/primitives/empty/empty.svelte +1 -1
  295. package/src/lib/components/primitives/field/field-content.svelte +1 -1
  296. package/src/lib/components/primitives/field/field-description.svelte +1 -1
  297. package/src/lib/components/primitives/field/field-error.svelte +1 -1
  298. package/src/lib/components/primitives/field/field-group.svelte +1 -1
  299. package/src/lib/components/primitives/field/field-label.svelte +2 -2
  300. package/src/lib/components/primitives/field/field-legend.svelte +1 -1
  301. package/src/lib/components/primitives/field/field-separator.svelte +2 -2
  302. package/src/lib/components/primitives/field/field-set.svelte +1 -1
  303. package/src/lib/components/primitives/field/field-title.svelte +1 -1
  304. package/src/lib/components/primitives/field/field.svelte +1 -1
  305. package/src/lib/components/primitives/index.ts +1 -1
  306. package/src/lib/components/primitives/input/input.svelte +5 -5
  307. package/src/lib/components/primitives/input-group/input-group-addon.svelte +1 -1
  308. package/src/lib/components/primitives/input-group/input-group-button.svelte +2 -2
  309. package/src/lib/components/primitives/input-group/input-group-input.svelte +2 -2
  310. package/src/lib/components/primitives/input-group/input-group-text.svelte +1 -1
  311. package/src/lib/components/primitives/input-group/input-group-textarea.svelte +2 -2
  312. package/src/lib/components/primitives/input-group/input-group.svelte +2 -2
  313. package/src/lib/components/primitives/label/label.svelte +1 -1
  314. package/src/lib/components/primitives/mode-toggle/mode-toggle.svelte +5 -2
  315. package/src/lib/components/primitives/popover/popover-content.svelte +2 -2
  316. package/src/lib/components/primitives/popover/popover-description.svelte +1 -1
  317. package/src/lib/components/primitives/popover/popover-header.svelte +1 -1
  318. package/src/lib/components/primitives/popover/popover-title.svelte +1 -1
  319. package/src/lib/components/primitives/popover/popover-trigger.svelte +1 -1
  320. package/src/lib/components/primitives/resizable/resizable-handle.svelte +3 -3
  321. package/src/lib/components/primitives/resizable/resizable-pane-group.svelte +1 -1
  322. package/src/lib/components/primitives/scroll-area/scroll-area-scrollbar.svelte +1 -1
  323. package/src/lib/components/primitives/scroll-area/scroll-area.svelte +2 -2
  324. package/src/lib/components/primitives/search/search.svelte +3 -3
  325. package/src/lib/components/primitives/select/select-content.svelte +2 -2
  326. package/src/lib/components/primitives/select/select-group-heading.svelte +1 -1
  327. package/src/lib/components/primitives/select/select-item.svelte +2 -2
  328. package/src/lib/components/primitives/select/select-label.svelte +1 -1
  329. package/src/lib/components/primitives/select/select-scroll-down-button.svelte +1 -1
  330. package/src/lib/components/primitives/select/select-scroll-up-button.svelte +1 -1
  331. package/src/lib/components/primitives/select/select-separator.svelte +2 -2
  332. package/src/lib/components/primitives/select/select-trigger.svelte +2 -2
  333. package/src/lib/components/primitives/separator/separator.svelte +1 -1
  334. package/src/lib/components/primitives/slider/slider.svelte +9 -4
  335. package/src/lib/components/primitives/spinner/spinner.svelte +1 -1
  336. package/src/lib/components/primitives/switch/switch.svelte +2 -2
  337. package/src/lib/components/primitives/tabs/tabs-content.svelte +1 -1
  338. package/src/lib/components/primitives/tabs/tabs-list.svelte +3 -3
  339. package/src/lib/components/primitives/tabs/tabs-trigger.svelte +4 -4
  340. package/src/lib/components/primitives/tabs/tabs.svelte +1 -1
  341. package/src/lib/components/primitives/textarea/textarea.svelte +2 -2
  342. package/src/lib/components/primitives/toggle/toggle.svelte +12 -4
  343. package/src/lib/components/primitives/toggle-group/toggle-group-item.svelte +3 -3
  344. package/src/lib/components/primitives/toggle-group/toggle-group.svelte +3 -3
  345. package/src/lib/components/primitives/tooltip/index.ts +19 -0
  346. package/src/lib/components/primitives/tooltip/tooltip-content.svelte +34 -0
  347. package/src/lib/components/primitives/tooltip/tooltip-portal.svelte +7 -0
  348. package/src/lib/components/primitives/tooltip/tooltip-provider.svelte +7 -0
  349. package/src/lib/components/primitives/tooltip/tooltip-trigger.svelte +7 -0
  350. package/src/lib/components/primitives/tooltip/tooltip.svelte +7 -0
  351. package/src/lib/components/viewer/MeshMetadataPanel.svelte +119 -0
  352. package/src/lib/components/viewer/SceneManager.svelte +32 -21
  353. package/src/lib/components/viewer/Viewer.svelte +196 -208
  354. package/src/lib/components/viewer/ViewerDock.svelte +404 -0
  355. package/src/lib/composables/useFooterItem.svelte.ts +1 -1
  356. package/src/lib/compute/liveProgress.svelte.ts +103 -0
  357. package/src/lib/compute/run-panel-action.ts +16 -0
  358. package/src/lib/compute/solving.svelte.ts +66 -50
  359. package/src/lib/compute/useSolveSession.svelte.ts +3 -0
  360. package/src/lib/constants.ts +8 -11
  361. package/src/lib/contexts/hostContext.svelte.ts +51 -0
  362. package/src/lib/features/chart/plotly-compat.ts +141 -0
  363. package/src/lib/i18n/localeContext.svelte.ts +40 -5
  364. package/src/lib/i18n/messages.ts +487 -13
  365. package/src/lib/index.ts +23 -0
  366. package/src/lib/public.ts +2 -0
  367. package/src/lib/schema/host-support.ts +129 -0
  368. package/src/lib/schema/option-display.ts +44 -0
  369. package/src/lib/schema/param-exporter.ts +10 -7
  370. package/src/lib/schema/visibility-rules.ts +50 -1
  371. package/src/lib/styles/base.css +116 -83
  372. package/src/lib/types/actionButton.ts +16 -3
  373. package/src/lib/types/presetLabels.ts +13 -11
  374. package/src/lib/utils/color-space.ts +107 -0
  375. package/src/lib/utils/file-download.ts +11 -21
  376. package/src/lib/utils/host-files.ts +61 -0
  377. package/src/lib/utils/loadScript.ts +3 -0
  378. package/src/lib/utils/webp-to-png.ts +48 -0
  379. package/dist/components/primitives/theme-switcher/ThemeSwitcher.svelte +0 -39
  380. package/dist/components/primitives/theme-switcher/ThemeSwitcher.svelte.d.ts +0 -3
  381. package/dist/components/primitives/theme-switcher/index.d.ts +0 -1
  382. package/dist/components/primitives/theme-switcher/index.js +0 -1
  383. package/dist/components/viewer/MeshMetadataDialog.svelte +0 -126
  384. package/dist/components/viewer/MeshMetadataDialog.svelte.d.ts +0 -10
  385. package/dist/styles/themes/cyberpunk.css +0 -98
  386. package/dist/styles/themes/neutral.css +0 -72
  387. package/dist/styles/themes/ocean.css +0 -75
  388. package/dist/styles/themes/selva.css +0 -105
  389. package/dist/theme/themeStore.svelte.d.ts +0 -12
  390. package/dist/theme/themeStore.svelte.js +0 -40
  391. package/dist/theme/themes.d.ts +0 -5
  392. package/dist/theme/themes.js +0 -13
  393. package/src/lib/components/primitives/theme-switcher/ThemeSwitcher.svelte +0 -39
  394. package/src/lib/components/primitives/theme-switcher/index.ts +0 -1
  395. package/src/lib/components/viewer/MeshMetadataDialog.svelte +0 -126
  396. package/src/lib/styles/themes/cyberpunk.css +0 -98
  397. package/src/lib/styles/themes/neutral.css +0 -72
  398. package/src/lib/styles/themes/ocean.css +0 -75
  399. package/src/lib/styles/themes/selva.css +0 -105
  400. package/src/lib/theme/themeStore.svelte.ts +0 -50
  401. package/src/lib/theme/themes.ts +0 -18
@@ -1,8 +1,20 @@
1
1
  <script lang="ts">
2
2
  import type { OutputImageLayoutItem } from '@selvajs/schemas';
3
3
  import type { FileData } from '@selvajs/compute/core';
4
- import { Download, Maximize, Minimize } from '@lucide/svelte';
5
- import { downloadFiles, isFileData, MIME_BY_EXT } from '../../utils/file-download';
4
+ import {
5
+ ChevronLeft,
6
+ ChevronRight,
7
+ Download,
8
+ LoaderCircle,
9
+ Maximize,
10
+ Minimize,
11
+ Scan,
12
+ ZoomIn,
13
+ ZoomOut
14
+ } from '@lucide/svelte';
15
+ import { downloadFiles, isFileData, MIME_BY_EXT } from '../../utils/file-download.js';
16
+ import { getLocaleContext } from '../../i18n/localeContext.svelte.js';
17
+ import { formatMessage } from '../../i18n/messages.js';
6
18
 
7
19
  interface Props {
8
20
  item: OutputImageLayoutItem;
@@ -11,133 +23,485 @@
11
23
 
12
24
  let { item, value }: Props = $props();
13
25
 
26
+ const locale = getLocaleContext();
27
+ const t = $derived(locale.messages);
28
+
14
29
  const SUPPORTED_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg']);
30
+ const ZOOM_STEP = 1.5;
31
+ const MAX_ZOOM_OVER_NATURAL = 32;
32
+ // Above this many screen pixels per image pixel, smoothing hides exactly the detail the user
33
+ // zoomed in to see, so raster images switch to nearest-neighbour.
34
+ const PIXELATED_FROM_SCALE = 2;
35
+
36
+ const extOf = (f: FileData) => (f.fileType ?? '').toLowerCase();
15
37
 
16
- const file = $derived.by<FileData | null>(() => {
17
- if (Array.isArray(value)) return value.find(isFileData) ?? null;
18
- return isFileData(value) ? value : null;
38
+ const files = $derived.by<FileData[]>(() => {
39
+ if (Array.isArray(value)) return value.filter(isFileData);
40
+ return isFileData(value) ? [value] : [];
19
41
  });
42
+ const images = $derived(files.filter((f) => SUPPORTED_EXTS.has(extOf(f))));
43
+ const unsupported = $derived(images.length === 0 ? files[0] : undefined);
20
44
 
21
- const ext = $derived(file ? (file.fileType ?? '').toLowerCase() : '');
22
- const isSupported = $derived(SUPPORTED_EXTS.has(ext));
45
+ let index = $state(0);
46
+ const currentIndex = $derived(Math.max(0, Math.min(index, images.length - 1)));
47
+ const current = $derived(images[currentIndex] ?? null);
48
+ const isSvg = $derived(current ? extOf(current) === '.svg' : false);
23
49
 
24
50
  // Binary formats (PNG/JPG/WEBP/GIF) arrive base64; SVG arrives as plain UTF-8 XML
25
51
  // and must be percent-encoded into the data URL.
26
52
  // Rendering through <img src> rather than inlining the SVG is a security choice:
27
53
  // browsers disable scripting in image-context SVGs, so untrusted SVG cannot
28
54
  // execute against the host. Don't switch to {@html} or an inline <svg>.
29
- const dataUrl = $derived.by(() => {
30
- if (!file || !isSupported) return null;
31
- const mime = MIME_BY_EXT[ext] ?? 'application/octet-stream';
32
- if (file.isBase64Encoded) {
33
- return `data:${mime};base64,${file.data}`;
34
- }
55
+ function toDataUrl(file: FileData): string {
56
+ const mime = MIME_BY_EXT[extOf(file)] ?? 'application/octet-stream';
57
+ if (file.isBase64Encoded) return `data:${mime};base64,${file.data}`;
35
58
  return `data:${mime};utf8,${encodeURIComponent(file.data)}`;
36
- });
59
+ }
60
+
61
+ const dataUrl = $derived(current ? toDataUrl(current) : null);
37
62
 
38
63
  const cfg = $derived(item.config ?? {});
39
64
  const allowDownload = $derived(cfg.allowDownload ?? true);
40
- const allowFullscreen = $derived(cfg.allowFullscreen ?? true);
41
- const backgroundColor = $derived(cfg.backgroundColor);
65
+ // Some embedded WebViews (WKWebView in the Rhino panel) ship without the Fullscreen API at all.
66
+ const fullscreenSupported = typeof document !== 'undefined' && !!document.fullscreenEnabled;
67
+ const allowFullscreen = $derived((cfg.allowFullscreen ?? true) && fullscreenSupported);
68
+ // SVGs are line drawings (often black on transparent), so a checkerboard hurts legibility.
69
+ const backgroundColor = $derived(cfg.backgroundColor ?? (isSvg ? '#ffffff' : undefined));
42
70
 
43
71
  let wrapperEl = $state<HTMLDivElement | null>(null);
72
+ let stageEl = $state<HTMLDivElement | null>(null);
44
73
  let isFullscreen = $state(false);
45
74
  let downloading = $state(false);
46
75
  let error = $state<string | null>(null);
76
+ let decodeFailed = $state(false);
77
+
78
+ // View state. `scale` is screen px per image px; (x, y) is the image's top-left in stage px.
79
+ // The image is sized with width/height rather than a CSS transform so the browser re-rasters
80
+ // SVGs and resamples bitmaps at the displayed size instead of stretching a cached layer.
81
+ let stageW = $state(0);
82
+ let stageH = $state(0);
83
+ let naturalW = $state(0);
84
+ let naturalH = $state(0);
85
+ let scale = $state(1);
86
+ let x = $state(0);
87
+ let y = $state(0);
88
+ let fitted = $state(true);
89
+
90
+ const ready = $derived(naturalW > 0 && naturalH > 0 && stageW > 0 && stageH > 0);
91
+ const fitScale = $derived(ready ? Math.min(stageW / naturalW, stageH / naturalH) : 1);
92
+ const minScale = $derived(Math.min(fitScale, 1));
93
+ const maxScale = $derived(Math.max(fitScale, 1) * MAX_ZOOM_OVER_NATURAL);
94
+ const zoomPercent = $derived(Math.round(scale * 100));
95
+ const pannable = $derived(naturalW * scale > stageW + 1 || naturalH * scale > stageH + 1);
96
+ const pixelated = $derived(!isSvg && scale >= PIXELATED_FROM_SCALE);
97
+
98
+ function clampAxis(pos: number, size: number, stage: number) {
99
+ if (size <= stage) return (stage - size) / 2;
100
+ return Math.min(0, Math.max(stage - size, pos));
101
+ }
102
+
103
+ function applyView(nextScale: number, nextX: number, nextY: number) {
104
+ scale = nextScale;
105
+ x = clampAxis(nextX, naturalW * nextScale, stageW);
106
+ y = clampAxis(nextY, naturalH * nextScale, stageH);
107
+ }
108
+
109
+ function fit() {
110
+ fitted = true;
111
+ applyView(fitScale, 0, 0);
112
+ }
113
+
114
+ function zoomTo(target: number, px = stageW / 2, py = stageH / 2) {
115
+ const next = Math.min(maxScale, Math.max(minScale, target));
116
+ const ratio = next / scale;
117
+ fitted = Math.abs(next - fitScale) < 1e-3;
118
+ applyView(next, px - (px - x) * ratio, py - (py - y) * ratio);
119
+ }
120
+
121
+ // Refit on stage resize (fullscreen, layout changes) unless the user has zoomed in.
122
+ $effect(() => {
123
+ if (!stageEl) return;
124
+ const ro = new ResizeObserver(([entry]) => {
125
+ stageW = entry.contentRect.width;
126
+ stageH = entry.contentRect.height;
127
+ if (fitted) fit();
128
+ else applyView(Math.max(scale, minScale), x, y);
129
+ });
130
+ ro.observe(stageEl);
131
+ return () => ro.disconnect();
132
+ });
133
+
134
+ function onImageLoad(e: Event) {
135
+ const img = e.currentTarget as HTMLImageElement;
136
+ decodeFailed = false;
137
+ // A viewBox-only SVG can report 0×0; fall back to the browser's default replaced-element size.
138
+ const w = img.naturalWidth || 300;
139
+ const h = img.naturalHeight || 150;
140
+ // Keep the zoom across solves when the new image has the same size: the user is usually
141
+ // watching one detail change as they move a slider.
142
+ const sameSize = w === naturalW && h === naturalH;
143
+ naturalW = w;
144
+ naturalH = h;
145
+ if (!sameSize || fitted) fit();
146
+ }
147
+
148
+ // ---- pointer: drag to pan, two fingers to pinch -------------------------------------------
149
+ // Not reactive state: only read inside handlers.
150
+ const pointers: Record<number, { x: number; y: number }> = {};
151
+ const pointerList = () => Object.values(pointers);
152
+ let dragging = $state(false);
153
+ let pinchDistance = 0;
154
+
155
+ function stagePoint(e: { clientX: number; clientY: number }) {
156
+ const r = stageEl!.getBoundingClientRect();
157
+ return { x: e.clientX - r.left, y: e.clientY - r.top };
158
+ }
159
+
160
+ function onPointerDown(e: PointerEvent) {
161
+ if (e.button !== 0 || (e.target as Element).closest('button')) return;
162
+ stageEl?.setPointerCapture(e.pointerId);
163
+ pointers[e.pointerId] = stagePoint(e);
164
+ const count = pointerList().length;
165
+ dragging = count === 1 && pannable;
166
+ if (count === 2) {
167
+ const [a, b] = pointerList();
168
+ pinchDistance = Math.hypot(a.x - b.x, a.y - b.y);
169
+ }
170
+ }
171
+
172
+ function onPointerMove(e: PointerEvent) {
173
+ const prev = pointers[e.pointerId];
174
+ if (!prev) return;
175
+ const p = stagePoint(e);
176
+ pointers[e.pointerId] = p;
177
+
178
+ if (pointerList().length === 2) {
179
+ const [a, b] = pointerList();
180
+ const distance = Math.hypot(a.x - b.x, a.y - b.y);
181
+ if (pinchDistance > 0)
182
+ zoomTo(scale * (distance / pinchDistance), (a.x + b.x) / 2, (a.y + b.y) / 2);
183
+ pinchDistance = distance;
184
+ } else if (dragging) {
185
+ fitted = false;
186
+ applyView(scale, x + p.x - prev.x, y + p.y - prev.y);
187
+ }
188
+ }
189
+
190
+ function onPointerUp(e: PointerEvent) {
191
+ delete pointers[e.pointerId];
192
+ const count = pointerList().length;
193
+ if (count < 2) pinchDistance = 0;
194
+ if (count === 0) dragging = false;
195
+ }
196
+
197
+ function onDoubleClick(e: MouseEvent) {
198
+ if (!fitted) return fit();
199
+ const p = stagePoint(e);
200
+ // Large images go to 1:1 (true pixels); small ones are already ≥1:1 when fitted, so magnify.
201
+ zoomTo(fitScale < 0.8 ? 1 : fitScale * 3, p.x, p.y);
202
+ }
203
+
204
+ // ---- wheel: zoom on Ctrl/⌘ (and trackpad pinch, which sends ctrlKey) or in fullscreen -------
205
+ let showWheelHint = $state(false);
206
+ let wheelHintTimeout: ReturnType<typeof setTimeout>;
207
+
208
+ // Registered by hand: preventDefault needs a non-passive listener.
209
+ $effect(() => {
210
+ if (!stageEl) return;
211
+ const el = stageEl;
212
+ function onWheel(e: WheelEvent) {
213
+ if (!ready) return;
214
+ if (!(e.ctrlKey || e.metaKey || isFullscreen)) {
215
+ // Let the page scroll, but say how to zoom.
216
+ showWheelHint = true;
217
+ clearTimeout(wheelHintTimeout);
218
+ wheelHintTimeout = setTimeout(() => (showWheelHint = false), 1200);
219
+ return;
220
+ }
221
+ e.preventDefault();
222
+ showWheelHint = false;
223
+ const delta = e.deltaMode === 1 ? e.deltaY * 16 : e.deltaY;
224
+ const p = stagePoint(e);
225
+ zoomTo(scale * Math.exp(-delta * 0.002), p.x, p.y);
226
+ }
227
+ el.addEventListener('wheel', onWheel, { passive: false });
228
+ return () => {
229
+ el.removeEventListener('wheel', onWheel);
230
+ clearTimeout(wheelHintTimeout);
231
+ };
232
+ });
233
+
234
+ function onKeyDown(e: KeyboardEvent) {
235
+ const actions: Record<string, () => void> = {
236
+ '+': () => zoomTo(scale * ZOOM_STEP),
237
+ '=': () => zoomTo(scale * ZOOM_STEP),
238
+ '-': () => zoomTo(scale / ZOOM_STEP),
239
+ '0': fit,
240
+ '1': () => zoomTo(1),
241
+ ArrowLeft: () => step(-1),
242
+ ArrowRight: () => step(1)
243
+ };
244
+ if (allowFullscreen) actions.f = toggleFullscreen;
245
+ const action = actions[e.key];
246
+ if (!action) return;
247
+ e.preventDefault();
248
+ action();
249
+ }
250
+
251
+ function step(delta: number) {
252
+ if (images.length < 2) return;
253
+ select((currentIndex + delta + images.length) % images.length);
254
+ }
255
+
256
+ function select(i: number) {
257
+ if (i === currentIndex) return;
258
+ index = i;
259
+ // A different image in the set: start fitted rather than carrying over the old zoom.
260
+ naturalW = 0;
261
+ naturalH = 0;
262
+ fitted = true;
263
+ }
47
264
 
48
265
  function toggleFullscreen() {
49
266
  if (!wrapperEl) return;
50
267
  if (!document.fullscreenElement) {
51
268
  wrapperEl.requestFullscreen().catch((err) => {
52
- error = `Fullscreen unavailable: ${err.message}`;
269
+ error = formatMessage(t.fullscreenUnavailable, { error: err.message });
53
270
  });
54
271
  } else {
55
272
  document.exitFullscreen().catch((err) => {
56
- error = `Exit fullscreen failed: ${err.message}`;
273
+ error = formatMessage(t.exitFullscreenFailed, { error: err.message });
57
274
  });
58
275
  }
59
276
  }
60
277
 
61
278
  $effect(() => {
62
279
  function onFullscreenChange() {
63
- isFullscreen = !!document.fullscreenElement;
280
+ isFullscreen = document.fullscreenElement === wrapperEl;
64
281
  }
65
282
  document.addEventListener('fullscreenchange', onFullscreenChange);
66
283
  return () => document.removeEventListener('fullscreenchange', onFullscreenChange);
67
284
  });
68
285
 
69
286
  async function handleDownload() {
70
- if (!file) return;
287
+ if (!current) return;
71
288
  downloading = true;
72
289
  error = null;
73
290
  try {
74
- await downloadFiles(file, file.fileName);
291
+ await downloadFiles(current, current.fileName);
75
292
  } catch (e) {
76
- error = e instanceof Error ? e.message : 'Download failed';
293
+ error = e instanceof Error ? e.message : t.downloadFailed;
77
294
  } finally {
78
295
  downloading = false;
79
296
  }
80
297
  }
298
+
299
+ const toolButton =
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';
81
301
  </script>
82
302
 
83
- <div
84
- bind:this={wrapperEl}
85
- class="group rounded relative overflow-hidden border border-border {isFullscreen
86
- ? 'bg-background'
87
- : ''}"
88
- >
89
- {#if error}
90
- <div class="px-4 py-3 text-sm text-destructive">{error}</div>
91
- {:else if !file}
92
- <div class="px-4 py-8 text-sm text-center text-muted-foreground">Waiting for image...</div>
93
- {:else if !isSupported}
94
- <div class="px-4 py-3 text-sm text-destructive">
95
- Unsupported image format: <span class="font-mono">{ext || '(unknown)'}</span>. Supported:
96
- .png, .jpg, .jpeg, .webp, .gif, .svg
97
- </div>
98
- {:else}
99
- <div
100
- class="w-full bg-(--image-bg) {isFullscreen ? 'h-screen' : 'h-95'}"
101
- style:--image-bg={backgroundColor}
102
- >
103
- <img
104
- src={dataUrl}
105
- alt={item.displayName ?? file.fileName}
106
- class="h-full w-full object-contain"
107
- />
108
- </div>
109
-
110
- {#if allowDownload || allowFullscreen}
303
+ <!-- The layout hides this output until a solve returns a file, so there is no empty state here. -->
304
+ {#if error || unsupported || current}
305
+ <div
306
+ bind:this={wrapperEl}
307
+ class="flex flex-col overflow-hidden rounded-lg border border-border-strong bg-background {isFullscreen
308
+ ? 'h-dvh'
309
+ : ''}"
310
+ >
311
+ {#if error}
312
+ <div class="px-4 py-3 text-sm text-destructive">{error}</div>
313
+ {:else if unsupported}
314
+ <div class="px-4 py-3 text-sm text-destructive">
315
+ {formatMessage(t.imageUnsupported, {
316
+ ext: extOf(unsupported) || t.imageUnknownFormat,
317
+ formats: [...SUPPORTED_EXTS].join(', ')
318
+ })}
319
+ </div>
320
+ {:else if current}
321
+ <!-- The stage is a pan/zoom surface with its own keyboard shortcuts; no native element fits. -->
322
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
323
+ <!-- eslint-disable shadcn/no-inline-styles -- backgroundColor is the author's per-output color, not a theme token -->
111
324
  <div
112
- class="gap-1 right-2 top-2 absolute z-10 flex items-center opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 {isFullscreen
113
- ? 'opacity-100'
114
- : ''}"
325
+ bind:this={stageEl}
326
+ role="application"
327
+ aria-roledescription={t.imageViewer}
328
+ aria-label={item.displayName ?? current.fileName}
329
+ tabindex="0"
330
+ class="relative w-full overflow-hidden outline-none select-none focus-visible:ring-2 focus-visible:ring-ring
331
+ {isFullscreen ? 'flex-1' : 'h-95'}
332
+ {dragging ? 'cursor-grabbing' : pannable ? 'cursor-grab' : 'cursor-zoom-in'}
333
+ {fitted ? 'touch-pan-y' : 'touch-none'}
334
+ {backgroundColor ? 'bg-(--image-bg)' : 'checkerboard'}"
335
+ style:--image-bg={backgroundColor}
336
+ onpointerdown={onPointerDown}
337
+ onpointermove={onPointerMove}
338
+ onpointerup={onPointerUp}
339
+ onpointercancel={onPointerUp}
340
+ ondblclick={onDoubleClick}
341
+ onkeydown={onKeyDown}
115
342
  >
116
- {#if allowDownload && file}
343
+ <!-- eslint-enable shadcn/no-inline-styles -->
344
+ {#if decodeFailed}
345
+ <div class="inset-0 text-sm absolute flex items-center justify-center text-destructive">
346
+ {formatMessage(t.imageDecodeFailed, { file: `${current.fileName}${current.fileType}` })}
347
+ </div>
348
+ {:else}
349
+ {#if !ready}
350
+ <div
351
+ class="inset-0 gap-2 text-sm absolute flex items-center justify-center text-muted-foreground"
352
+ >
353
+ <LoaderCircle size={16} class="animate-spin" />
354
+ {t.imageLoading}
355
+ </div>
356
+ {/if}
357
+ <img
358
+ src={dataUrl}
359
+ alt=""
360
+ draggable="false"
361
+ onload={onImageLoad}
362
+ onerror={() => (decodeFailed = true)}
363
+ class="absolute top-(--img-y) left-(--img-x) h-(--img-h) w-(--img-w) max-w-none
364
+ {pixelated ? 'pixelated' : ''}
365
+ {ready ? '' : 'invisible'}"
366
+ style:--img-x="{x}px"
367
+ style:--img-y="{y}px"
368
+ style:--img-w="{naturalW * scale}px"
369
+ style:--img-h="{naturalH * scale}px"
370
+ />
371
+ {/if}
372
+
373
+ {#if images.length > 1}
117
374
  <button
118
- onclick={handleDownload}
119
- disabled={downloading}
120
- title="Download image"
121
- class="rounded p-1.5 backdrop-blur-sm flex items-center border border-border bg-background/80 text-foreground transition-colors hover:bg-background disabled:cursor-not-allowed disabled:opacity-50"
375
+ onclick={() => step(-1)}
376
+ title="{t.previousImage} (←)"
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-strong bg-background/80 text-foreground hover:bg-background"
122
379
  >
123
- <Download size={14} />
380
+ <ChevronLeft size={16} />
381
+ </button>
382
+ <button
383
+ onclick={() => step(1)}
384
+ title="{t.nextImage} (→)"
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-strong bg-background/80 text-foreground hover:bg-background"
387
+ >
388
+ <ChevronRight size={16} />
124
389
  </button>
125
390
  {/if}
126
391
 
127
- {#if allowFullscreen}
392
+ <div
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
+ ? 'opacity-100'
395
+ : 'opacity-0'}"
396
+ >
397
+ {t.zoomHint}
398
+ </div>
399
+ </div>
400
+
401
+ {#if images.length > 1}
402
+ <div class="gap-1.5 p-1.5 flex overflow-x-auto border-t border-border">
403
+ {#each images as image, i (i)}
404
+ <button
405
+ onclick={() => select(i)}
406
+ title="{image.fileName}{image.fileType}"
407
+ class="h-12 w-16 shrink-0 overflow-hidden rounded-sm border-2 bg-muted transition-colors {i ===
408
+ currentIndex
409
+ ? 'border-primary'
410
+ : 'border-transparent opacity-70 hover:opacity-100'}"
411
+ >
412
+ <img src={toDataUrl(image)} alt="" class="h-full w-full object-contain" />
413
+ </button>
414
+ {/each}
415
+ </div>
416
+ {/if}
417
+
418
+ <div class="gap-2 px-2 py-1 text-xs flex items-center border-t border-border">
419
+ <div class="min-w-0 gap-1.5 flex flex-1 items-center text-muted-foreground">
420
+ <span class="truncate text-foreground">{current.fileName}{current.fileType}</span>
421
+ {#if ready}
422
+ <span class="shrink-0 tabular-nums">{naturalW}×{naturalH}</span>
423
+ {/if}
424
+ </div>
425
+
426
+ <div class="gap-0.5 flex shrink-0 items-center">
128
427
  <button
129
- onclick={toggleFullscreen}
130
- title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
131
- class="rounded p-1.5 backdrop-blur-sm flex items-center border border-border bg-background/80 text-foreground transition-colors hover:bg-background"
428
+ class={toolButton}
429
+ onclick={() => zoomTo(scale / ZOOM_STEP)}
430
+ disabled={!ready || scale <= minScale + 1e-3}
431
+ title="{t.zoomOut} (−)"
432
+ aria-label={t.zoomOut}
132
433
  >
133
- {#if isFullscreen}
134
- <Minimize size={14} />
135
- {:else}
136
- <Maximize size={14} />
137
- {/if}
434
+ <ZoomOut size={14} />
138
435
  </button>
139
- {/if}
436
+ <span class="w-11 text-center text-muted-foreground tabular-nums">{zoomPercent}%</span>
437
+ <button
438
+ class={toolButton}
439
+ onclick={() => zoomTo(scale * ZOOM_STEP)}
440
+ disabled={!ready || scale >= maxScale - 1e-3}
441
+ title="{t.zoomIn} (+)"
442
+ aria-label={t.zoomIn}
443
+ >
444
+ <ZoomIn size={14} />
445
+ </button>
446
+ <button
447
+ class={toolButton}
448
+ onclick={fit}
449
+ disabled={!ready || fitted}
450
+ title="{t.zoomFit} (0)"
451
+ aria-label={t.zoomFit}
452
+ >
453
+ <Scan size={14} />
454
+ </button>
455
+ <button
456
+ class="{toolButton} font-medium tabular-nums"
457
+ onclick={() => zoomTo(1)}
458
+ disabled={!ready || Math.abs(scale - 1) < 1e-3}
459
+ title="{t.zoomActual} (1)"
460
+ aria-label={t.zoomActual}
461
+ >
462
+ 1:1
463
+ </button>
464
+
465
+ {#if allowDownload || allowFullscreen}
466
+ <div class="mx-1 h-4 w-px bg-border"></div>
467
+ {/if}
468
+ {#if allowDownload}
469
+ <button
470
+ class={toolButton}
471
+ onclick={handleDownload}
472
+ disabled={downloading}
473
+ title={t.downloadImage}
474
+ aria-label={t.downloadImage}
475
+ >
476
+ <Download size={14} />
477
+ </button>
478
+ {/if}
479
+ {#if allowFullscreen}
480
+ <button
481
+ class={toolButton}
482
+ onclick={toggleFullscreen}
483
+ title="{isFullscreen ? t.exitFullscreen : t.fullscreen} (F)"
484
+ aria-label={isFullscreen ? t.exitFullscreen : t.fullscreen}
485
+ >
486
+ {#if isFullscreen}
487
+ <Minimize size={14} />
488
+ {:else}
489
+ <Maximize size={14} />
490
+ {/if}
491
+ </button>
492
+ {/if}
493
+ </div>
140
494
  </div>
141
495
  {/if}
142
- {/if}
143
- </div>
496
+ </div>
497
+ {/if}
498
+
499
+ <style>
500
+ /* Shows transparency in PNG/WebP/SVG output instead of blending it into the page. */
501
+ .checkerboard {
502
+ background: repeating-conic-gradient(var(--muted) 0 25%, transparent 0 50%) 0 0 / 16px 16px;
503
+ }
504
+ .pixelated {
505
+ image-rendering: pixelated;
506
+ }
507
+ </style>