zabi-components 8.0.0 → 8.1.0

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 (364) hide show
  1. package/CHANGELOG.md +1924 -0
  2. package/README.md +243 -10
  3. package/THEME.md +149 -332
  4. package/THEMING.md +1192 -266
  5. package/dist/atoms/ActionPanel.svelte +15 -5
  6. package/dist/atoms/Avatar.svelte +104 -0
  7. package/dist/atoms/Avatar.svelte.d.ts +34 -0
  8. package/dist/atoms/Badge.svelte +36 -7
  9. package/dist/atoms/Badge.svelte.d.ts +2 -2
  10. package/dist/atoms/Button.svelte +177 -27
  11. package/dist/atoms/Button.svelte.d.ts +30 -2
  12. package/dist/atoms/Card.svelte +4 -4
  13. package/dist/atoms/CardHeader.svelte +16 -4
  14. package/dist/atoms/Checkbox.svelte +48 -21
  15. package/dist/atoms/Checkbox.svelte.d.ts +4 -2
  16. package/dist/atoms/CodeBlock.svelte +20 -2
  17. package/dist/atoms/CodeBlock.svelte.d.ts +4 -0
  18. package/dist/atoms/ColorPicker.svelte +259 -49
  19. package/dist/atoms/ColorPicker.svelte.d.ts +3 -0
  20. package/dist/atoms/Container.svelte +16 -8
  21. package/dist/atoms/DateField.svelte +107 -0
  22. package/dist/atoms/DateField.svelte.d.ts +53 -0
  23. package/dist/atoms/FieldMessages.svelte +69 -0
  24. package/dist/atoms/FieldMessages.svelte.d.ts +23 -0
  25. package/dist/atoms/FloatingActionButton.svelte +174 -0
  26. package/dist/atoms/FloatingActionButton.svelte.d.ts +51 -0
  27. package/dist/atoms/Heading.svelte +37 -5
  28. package/dist/atoms/Heading.svelte.d.ts +7 -0
  29. package/dist/atoms/IconButton.svelte +220 -24
  30. package/dist/atoms/IconButton.svelte.d.ts +49 -4
  31. package/dist/atoms/Input.svelte +253 -52
  32. package/dist/atoms/Input.svelte.d.ts +32 -2
  33. package/dist/atoms/List.svelte +4 -1
  34. package/dist/atoms/ListItem.svelte +44 -11
  35. package/dist/atoms/ListItem.svelte.d.ts +5 -0
  36. package/dist/atoms/Progress.svelte +27 -8
  37. package/dist/atoms/Progress.svelte.d.ts +4 -0
  38. package/dist/atoms/Radio.svelte +19 -3
  39. package/dist/atoms/Radio.svelte.d.ts +4 -2
  40. package/dist/atoms/Rating.svelte +494 -0
  41. package/dist/atoms/Rating.svelte.d.ts +56 -0
  42. package/dist/atoms/Select.svelte +433 -91
  43. package/dist/atoms/Select.svelte.d.ts +49 -3
  44. package/dist/atoms/SelectionControl.svelte +94 -17
  45. package/dist/atoms/SelectionControl.svelte.d.ts +4 -3
  46. package/dist/atoms/Skeleton.svelte +1 -1
  47. package/dist/atoms/Slider.svelte +309 -0
  48. package/dist/atoms/Slider.svelte.d.ts +40 -0
  49. package/dist/atoms/Spinner.svelte +57 -0
  50. package/dist/atoms/Spinner.svelte.d.ts +19 -0
  51. package/dist/atoms/Table.svelte +151 -6
  52. package/dist/atoms/Table.svelte.d.ts +15 -3
  53. package/dist/atoms/TemporalField.svelte +140 -0
  54. package/dist/atoms/TemporalField.svelte.d.ts +32 -0
  55. package/dist/atoms/Text.svelte +23 -11
  56. package/dist/atoms/Text.svelte.d.ts +6 -3
  57. package/dist/atoms/Textarea.svelte +55 -44
  58. package/dist/atoms/Textarea.svelte.d.ts +14 -2
  59. package/dist/atoms/ThemeToggle.svelte +223 -44
  60. package/dist/atoms/ThemeToggle.svelte.d.ts +26 -3
  61. package/dist/atoms/TimeField.svelte +108 -0
  62. package/dist/atoms/TimeField.svelte.d.ts +54 -0
  63. package/dist/atoms/Toast.svelte +17 -4
  64. package/dist/atoms/Toast.svelte.d.ts +2 -0
  65. package/dist/atoms/Toggle.svelte +41 -8
  66. package/dist/atoms/Toggle.svelte.d.ts +11 -2
  67. package/dist/atoms/Tooltip.svelte +508 -23
  68. package/dist/atoms/Tooltip.svelte.d.ts +11 -0
  69. package/dist/atoms/ZabiStringsProvider.svelte +45 -0
  70. package/dist/atoms/ZabiStringsProvider.svelte.d.ts +27 -0
  71. package/dist/atoms/index.d.ts +17 -0
  72. package/dist/atoms/index.js +12 -0
  73. package/dist/atoms/selection-control.styles.d.ts +36 -5
  74. package/dist/atoms/selection-control.styles.js +40 -9
  75. package/dist/components/atoms/index.d.ts +17 -0
  76. package/dist/components/atoms/index.d.ts.map +1 -1
  77. package/dist/components/atoms/selection-control.styles.d.ts +36 -5
  78. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
  79. package/dist/components/index.d.ts +80 -2
  80. package/dist/components/index.d.ts.map +1 -1
  81. package/dist/components/molecules/index.d.ts +46 -0
  82. package/dist/components/molecules/index.d.ts.map +1 -1
  83. package/dist/components/molecules/toast-store.d.ts +53 -7
  84. package/dist/components/molecules/toast-store.d.ts.map +1 -1
  85. package/dist/components/organisms/index.d.ts +5 -0
  86. package/dist/components/organisms/index.d.ts.map +1 -1
  87. package/dist/components/types/variants.d.ts +15 -1
  88. package/dist/components/types/variants.d.ts.map +1 -1
  89. package/dist/components/util/app-shell.d.ts +28 -0
  90. package/dist/components/util/app-shell.d.ts.map +1 -0
  91. package/dist/components/util/avatar.d.ts +36 -0
  92. package/dist/components/util/avatar.d.ts.map +1 -0
  93. package/dist/components/util/bottom-sheet.d.ts +30 -0
  94. package/dist/components/util/bottom-sheet.d.ts.map +1 -0
  95. package/dist/components/util/bottom-tab-bar.d.ts +35 -0
  96. package/dist/components/util/bottom-tab-bar.d.ts.map +1 -0
  97. package/dist/components/util/calendar.d.ts +56 -0
  98. package/dist/components/util/calendar.d.ts.map +1 -0
  99. package/dist/components/util/collapsible.d.ts +58 -0
  100. package/dist/components/util/collapsible.d.ts.map +1 -0
  101. package/dist/components/util/confirm-dialog.d.ts +12 -0
  102. package/dist/components/util/confirm-dialog.d.ts.map +1 -0
  103. package/dist/components/util/date.d.ts +96 -0
  104. package/dist/components/util/date.d.ts.map +1 -0
  105. package/dist/components/util/drawer.d.ts +13 -0
  106. package/dist/components/util/drawer.d.ts.map +1 -0
  107. package/dist/components/util/dropdown.d.ts +47 -0
  108. package/dist/components/util/dropdown.d.ts.map +1 -0
  109. package/dist/components/util/field.d.ts +39 -0
  110. package/dist/components/util/field.d.ts.map +1 -0
  111. package/dist/components/util/fit-in-viewport.d.ts +201 -0
  112. package/dist/components/util/fit-in-viewport.d.ts.map +1 -0
  113. package/dist/components/util/focus-utils.d.ts +75 -2
  114. package/dist/components/util/focus-utils.d.ts.map +1 -1
  115. package/dist/components/util/hydration.d.ts +45 -0
  116. package/dist/components/util/hydration.d.ts.map +1 -0
  117. package/dist/components/util/keyboard-inset.d.ts +18 -0
  118. package/dist/components/util/keyboard-inset.d.ts.map +1 -0
  119. package/dist/components/util/media-grid.d.ts +51 -0
  120. package/dist/components/util/media-grid.d.ts.map +1 -0
  121. package/dist/components/util/media.d.ts +6 -0
  122. package/dist/components/util/media.d.ts.map +1 -0
  123. package/dist/components/util/overlay-chrome.d.ts +53 -0
  124. package/dist/components/util/overlay-chrome.d.ts.map +1 -0
  125. package/dist/components/util/overlay.d.ts +72 -0
  126. package/dist/components/util/overlay.d.ts.map +1 -0
  127. package/dist/components/util/photo.d.ts +215 -0
  128. package/dist/components/util/photo.d.ts.map +1 -0
  129. package/dist/components/util/portal.d.ts +23 -0
  130. package/dist/components/util/portal.d.ts.map +1 -0
  131. package/dist/components/util/pull-to-refresh.d.ts +27 -0
  132. package/dist/components/util/pull-to-refresh.d.ts.map +1 -0
  133. package/dist/components/util/radio-keys.d.ts +25 -0
  134. package/dist/components/util/radio-keys.d.ts.map +1 -0
  135. package/dist/components/util/rating.d.ts +23 -0
  136. package/dist/components/util/rating.d.ts.map +1 -0
  137. package/dist/components/util/ready-made-strings.d.ts +77 -0
  138. package/dist/components/util/ready-made-strings.d.ts.map +1 -0
  139. package/dist/components/util/scroll-padding.d.ts +20 -0
  140. package/dist/components/util/scroll-padding.d.ts.map +1 -0
  141. package/dist/components/util/segmented-control.d.ts +16 -0
  142. package/dist/components/util/segmented-control.d.ts.map +1 -0
  143. package/dist/components/util/select.d.ts +41 -0
  144. package/dist/components/util/select.d.ts.map +1 -0
  145. package/dist/components/util/sheet-drag.d.ts +60 -0
  146. package/dist/components/util/sheet-drag.d.ts.map +1 -0
  147. package/dist/components/util/sidebar.d.ts +63 -0
  148. package/dist/components/util/sidebar.d.ts.map +1 -0
  149. package/dist/components/util/sortable-list.d.ts +65 -0
  150. package/dist/components/util/sortable-list.d.ts.map +1 -0
  151. package/dist/components/util/stepper.d.ts +40 -0
  152. package/dist/components/util/stepper.d.ts.map +1 -0
  153. package/dist/components/util/swipeable-list-item.d.ts +36 -0
  154. package/dist/components/util/swipeable-list-item.d.ts.map +1 -0
  155. package/dist/components/util/theme-mode.d.ts +80 -0
  156. package/dist/components/util/theme-mode.d.ts.map +1 -0
  157. package/dist/components/util/toaster.d.ts +93 -0
  158. package/dist/components/util/toaster.d.ts.map +1 -0
  159. package/dist/components/util/tooltip.d.ts +10 -0
  160. package/dist/components/util/tooltip.d.ts.map +1 -0
  161. package/dist/components/util/top-navbar.d.ts +11 -0
  162. package/dist/components/util/top-navbar.d.ts.map +1 -0
  163. package/dist/components/util/touch-target.d.ts +31 -0
  164. package/dist/components/util/touch-target.d.ts.map +1 -0
  165. package/dist/components/util/zabi-strings.d.ts +133 -0
  166. package/dist/components/util/zabi-strings.d.ts.map +1 -0
  167. package/dist/create-theme/cli.js +169 -0
  168. package/dist/create-theme/index.d.ts +176 -0
  169. package/dist/create-theme/index.js +817 -0
  170. package/dist/create-theme/lib/ramp-math.js +128 -0
  171. package/dist/create-theme/lib/resolve.js +141 -0
  172. package/dist/create-theme/theme-data.js +1116 -0
  173. package/dist/index.d.ts +80 -2
  174. package/dist/index.js +57 -2
  175. package/dist/molecules/Alert.svelte +33 -5
  176. package/dist/molecules/Alert.svelte.d.ts +2 -0
  177. package/dist/molecules/AppBar.svelte +415 -0
  178. package/dist/molecules/AppBar.svelte.d.ts +69 -0
  179. package/dist/molecules/AvatarGroup.svelte +116 -0
  180. package/dist/molecules/AvatarGroup.svelte.d.ts +40 -0
  181. package/dist/molecules/BottomSheet.svelte +540 -0
  182. package/dist/molecules/BottomSheet.svelte.d.ts +93 -0
  183. package/dist/molecules/BottomTabBar.svelte +297 -0
  184. package/dist/molecules/BottomTabBar.svelte.d.ts +64 -0
  185. package/dist/molecules/Calendar.svelte +597 -0
  186. package/dist/molecules/Calendar.svelte.d.ts +72 -0
  187. package/dist/molecules/Collapsible.svelte +243 -0
  188. package/dist/molecules/Collapsible.svelte.d.ts +44 -0
  189. package/dist/molecules/CollapsibleGroup.svelte +126 -0
  190. package/dist/molecules/CollapsibleGroup.svelte.d.ts +14 -0
  191. package/dist/molecules/ComponentDemo.svelte +16 -4
  192. package/dist/molecules/ComponentDemo.svelte.d.ts +3 -0
  193. package/dist/molecules/ConfirmDialog.svelte +253 -0
  194. package/dist/molecules/ConfirmDialog.svelte.d.ts +55 -0
  195. package/dist/molecules/ContactForm.svelte +34 -17
  196. package/dist/molecules/ContactForm.svelte.d.ts +8 -0
  197. package/dist/molecules/Drawer.svelte +394 -0
  198. package/dist/molecules/Drawer.svelte.d.ts +55 -0
  199. package/dist/molecules/Dropdown.svelte +509 -48
  200. package/dist/molecules/Dropdown.svelte.d.ts +45 -6
  201. package/dist/molecules/DropdownItem.svelte +122 -0
  202. package/dist/molecules/DropdownItem.svelte.d.ts +28 -0
  203. package/dist/molecules/EmptyState.svelte +55 -10
  204. package/dist/molecules/EmptyState.svelte.d.ts +8 -0
  205. package/dist/molecules/FormField.svelte +9 -1
  206. package/dist/molecules/FormField.svelte.d.ts +2 -0
  207. package/dist/molecules/Header.svelte +1 -1
  208. package/dist/molecules/ImageUpload.svelte +383 -70
  209. package/dist/molecules/ImageUpload.svelte.d.ts +47 -2
  210. package/dist/molecules/MediaGrid.svelte +527 -0
  211. package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
  212. package/dist/molecules/Modal.svelte +288 -72
  213. package/dist/molecules/Modal.svelte.d.ts +51 -2
  214. package/dist/molecules/NavigationMenuContent.svelte +58 -1
  215. package/dist/molecules/NavigationMenuItem.svelte +0 -1
  216. package/dist/molecules/NavigationMenuLink.svelte +1 -1
  217. package/dist/molecules/NavigationMenuList.svelte +10 -3
  218. package/dist/molecules/NavigationMenuTrigger.svelte +2 -2
  219. package/dist/molecules/Page.svelte +32 -2
  220. package/dist/molecules/Page.svelte.d.ts +11 -0
  221. package/dist/molecules/PhotoGrid.svelte +433 -0
  222. package/dist/molecules/PhotoGrid.svelte.d.ts +62 -0
  223. package/dist/molecules/PhotoViewer.svelte +1116 -0
  224. package/dist/molecules/PhotoViewer.svelte.d.ts +56 -0
  225. package/dist/molecules/PropsTable.svelte +22 -10
  226. package/dist/molecules/PropsTable.svelte.d.ts +3 -0
  227. package/dist/molecules/PullToRefresh.svelte +263 -0
  228. package/dist/molecules/PullToRefresh.svelte.d.ts +52 -0
  229. package/dist/molecules/RadioGroup.svelte +52 -14
  230. package/dist/molecules/RadioGroup.svelte.d.ts +14 -3
  231. package/dist/molecules/Section.svelte +1 -1
  232. package/dist/molecules/SegmentedControl.svelte +323 -0
  233. package/dist/molecules/SegmentedControl.svelte.d.ts +34 -0
  234. package/dist/molecules/SidebarBrandHeader.svelte +11 -1
  235. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +3 -0
  236. package/dist/molecules/SidebarFooter.svelte +29 -6
  237. package/dist/molecules/SidebarFooter.svelte.d.ts +3 -0
  238. package/dist/molecules/SlideUp.svelte +220 -61
  239. package/dist/molecules/SlideUp.svelte.d.ts +26 -0
  240. package/dist/molecules/SortableList.svelte +632 -0
  241. package/dist/molecules/SortableList.svelte.d.ts +69 -0
  242. package/dist/molecules/Stepper.svelte +548 -0
  243. package/dist/molecules/Stepper.svelte.d.ts +39 -0
  244. package/dist/molecules/StickyActionBar.svelte +191 -0
  245. package/dist/molecules/StickyActionBar.svelte.d.ts +65 -0
  246. package/dist/molecules/SwipeableListItem.svelte +334 -0
  247. package/dist/molecules/SwipeableListItem.svelte.d.ts +54 -0
  248. package/dist/molecules/Tabs.svelte +233 -9
  249. package/dist/molecules/Tabs.svelte.d.ts +7 -0
  250. package/dist/molecules/Toaster.svelte +350 -5
  251. package/dist/molecules/Toaster.svelte.d.ts +95 -2
  252. package/dist/molecules/ToasterToast.svelte +253 -67
  253. package/dist/molecules/ToasterToast.svelte.d.ts +9 -0
  254. package/dist/molecules/UnsavedChangesBar.svelte +212 -0
  255. package/dist/molecules/UnsavedChangesBar.svelte.d.ts +43 -0
  256. package/dist/molecules/index.d.ts +46 -0
  257. package/dist/molecules/index.js +27 -0
  258. package/dist/molecules/toast-store.d.ts +53 -7
  259. package/dist/molecules/toast-store.js +21 -0
  260. package/dist/organisms/AppShell.svelte +196 -0
  261. package/dist/organisms/AppShell.svelte.d.ts +61 -0
  262. package/dist/organisms/SidebarAccountPanel.svelte +107 -12
  263. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +19 -0
  264. package/dist/organisms/SidebarNavigation.svelte +96 -15
  265. package/dist/organisms/SidebarNavigation.svelte.d.ts +29 -1
  266. package/dist/organisms/SidebarPanel.svelte +31 -10
  267. package/dist/organisms/SidebarShell.svelte +162 -11
  268. package/dist/organisms/SidebarShell.svelte.d.ts +56 -2
  269. package/dist/organisms/TopNavbar.svelte +256 -31
  270. package/dist/organisms/TopNavbar.svelte.d.ts +26 -0
  271. package/dist/organisms/index.d.ts +5 -0
  272. package/dist/organisms/index.js +3 -0
  273. package/dist/types/components.d.ts +4 -0
  274. package/dist/types/components.d.ts.map +1 -1
  275. package/dist/types/index.d.ts +605 -25
  276. package/dist/types/index.d.ts.map +1 -1
  277. package/dist/types/variants.ts +17 -1
  278. package/dist/util/app-shell.d.ts +27 -0
  279. package/dist/util/app-shell.js +72 -0
  280. package/dist/util/avatar.d.ts +35 -0
  281. package/dist/util/avatar.js +52 -0
  282. package/dist/util/bottom-sheet.d.ts +29 -0
  283. package/dist/util/bottom-sheet.js +43 -0
  284. package/dist/util/bottom-tab-bar.d.ts +34 -0
  285. package/dist/util/bottom-tab-bar.js +54 -0
  286. package/dist/util/calendar.d.ts +55 -0
  287. package/dist/util/calendar.js +74 -0
  288. package/dist/util/collapsible.d.ts +57 -0
  289. package/dist/util/collapsible.js +23 -0
  290. package/dist/util/confirm-dialog.d.ts +11 -0
  291. package/dist/util/confirm-dialog.js +2 -0
  292. package/dist/util/date.d.ts +95 -0
  293. package/dist/util/date.js +237 -0
  294. package/dist/util/drawer.d.ts +12 -0
  295. package/dist/util/drawer.js +7 -0
  296. package/dist/util/dropdown.d.ts +46 -0
  297. package/dist/util/dropdown.js +21 -0
  298. package/dist/util/field.d.ts +38 -0
  299. package/dist/util/field.js +38 -0
  300. package/dist/util/fit-in-viewport.d.ts +200 -0
  301. package/dist/util/fit-in-viewport.js +399 -0
  302. package/dist/util/focus-utils.d.ts +75 -2
  303. package/dist/util/focus-utils.js +182 -9
  304. package/dist/util/hydration.d.ts +44 -0
  305. package/dist/util/hydration.js +58 -0
  306. package/dist/util/keyboard-inset.d.ts +17 -0
  307. package/dist/util/keyboard-inset.js +53 -0
  308. package/dist/util/media-grid.d.ts +50 -0
  309. package/dist/util/media-grid.js +81 -0
  310. package/dist/util/media.d.ts +5 -0
  311. package/dist/util/media.js +10 -0
  312. package/dist/util/overlay-chrome.d.ts +52 -0
  313. package/dist/util/overlay-chrome.js +56 -0
  314. package/dist/util/overlay.d.ts +71 -0
  315. package/dist/util/overlay.js +240 -0
  316. package/dist/util/photo.d.ts +214 -0
  317. package/dist/util/photo.js +206 -0
  318. package/dist/util/portal.d.ts +22 -0
  319. package/dist/util/portal.js +50 -0
  320. package/dist/util/pull-to-refresh.d.ts +26 -0
  321. package/dist/util/pull-to-refresh.js +30 -0
  322. package/dist/util/radio-keys.d.ts +24 -0
  323. package/dist/util/radio-keys.js +49 -0
  324. package/dist/util/rating.d.ts +22 -0
  325. package/dist/util/rating.js +26 -0
  326. package/dist/util/ready-made-strings.d.ts +76 -0
  327. package/dist/util/ready-made-strings.js +67 -0
  328. package/dist/util/scroll-padding.d.ts +19 -0
  329. package/dist/util/scroll-padding.js +44 -0
  330. package/dist/util/segmented-control.d.ts +15 -0
  331. package/dist/util/segmented-control.js +1 -0
  332. package/dist/util/select.d.ts +40 -0
  333. package/dist/util/select.js +63 -0
  334. package/dist/util/sheet-drag.d.ts +59 -0
  335. package/dist/util/sheet-drag.js +241 -0
  336. package/dist/util/sidebar.d.ts +62 -0
  337. package/dist/util/sidebar.js +38 -0
  338. package/dist/util/sortable-list.d.ts +64 -0
  339. package/dist/util/sortable-list.js +53 -0
  340. package/dist/util/stepper.d.ts +39 -0
  341. package/dist/util/stepper.js +38 -0
  342. package/dist/util/swipeable-list-item.d.ts +35 -0
  343. package/dist/util/swipeable-list-item.js +32 -0
  344. package/dist/util/theme-mode.d.ts +79 -0
  345. package/dist/util/theme-mode.js +121 -0
  346. package/dist/util/toaster.d.ts +92 -0
  347. package/dist/util/toaster.js +75 -0
  348. package/dist/util/tooltip.d.ts +9 -0
  349. package/dist/util/tooltip.js +18 -0
  350. package/dist/util/top-navbar.d.ts +10 -0
  351. package/dist/util/top-navbar.js +5 -0
  352. package/dist/util/touch-target.d.ts +30 -0
  353. package/dist/util/touch-target.js +30 -0
  354. package/dist/util/zabi-strings.d.ts +132 -0
  355. package/dist/util/zabi-strings.js +105 -0
  356. package/dist/zabi-components-colors.css +5 -2
  357. package/dist/zabi-components-theme-dark-only.css +543 -241
  358. package/dist/zabi-components-theme-dark.css +543 -241
  359. package/dist/zabi-components-theme-only.css +1584 -367
  360. package/dist/zabi-components-theme.css +1584 -367
  361. package/dist/zabi-components.css +3885 -518
  362. package/docs/lucide-icons.md +40 -17
  363. package/docs/theme-imports.md +81 -11
  364. package/package.json +35 -8
@@ -0,0 +1,323 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { SizeVariant } from "../types/variants.js";
4
+ import { cn } from "../util/cn.js";
5
+ import { resetValueOf, groupBinding } from "../util/hydration.js";
6
+ import { isRtl, radioKeyIndex } from "../util/radio-keys.js";
7
+ import type { SegmentedControlOption } from "../util/segmented-control.js";
8
+ import { generateId } from "../util/ssr-safe.js";
9
+
10
+ /** Other attributes (`data-*`, `id`, ...) land on the host element. */
11
+ type Props = Omit<
12
+ HTMLAttributes<HTMLDivElement>,
13
+ "class" | "onchange" | "aria-label" | "aria-labelledby"
14
+ > & {
15
+ /** Extra classes for the host element. */
16
+ class?: string;
17
+ /** Two to four choices, shown in one row. */
18
+ options: SegmentedControlOption[];
19
+ /** Selected value; supports `bind:value`. `undefined` for no selection. */
20
+ value?: string | undefined;
21
+ /** Accessible name of the group. Not shown. */
22
+ label?: string;
23
+ /** Accessible name of the group, the same as `label`. */
24
+ "aria-label"?: string;
25
+ /** Id of a visible element that names the group, in place of `label`. */
26
+ "aria-labelledby"?: string;
27
+ /** Height, on the scale Input and Button use. At least 44px on a touch screen. */
28
+ size?: SizeVariant;
29
+ /** Equal-width segments that fill the row. Off: each segment is as wide as its label. */
30
+ fullWidth?: boolean;
31
+ /**
32
+ * Name the value is submitted under in a form. Without a name the
33
+ * control is not part of the form around it: it submits nothing and a
34
+ * form reset leaves it alone.
35
+ */
36
+ name?: string;
37
+ disabled?: boolean;
38
+ /** Called with the new value when the user picks another segment. */
39
+ onchange?: (value: string) => void;
40
+ };
41
+
42
+ let {
43
+ class: className = "",
44
+ options,
45
+ value = $bindable(),
46
+ label = "",
47
+ "aria-label": ariaLabel,
48
+ "aria-labelledby": ariaLabelledby,
49
+ size = "md",
50
+ fullWidth = true,
51
+ name = "",
52
+ disabled = false,
53
+ onchange,
54
+ ...restProps
55
+ }: Props = $props();
56
+
57
+ $effect(() => {
58
+ if (import.meta.env?.DEV && (options.length < 2 || options.length > 4)) {
59
+ console.warn(
60
+ `[zabi-components] SegmentedControl is made for 2 to 4 options and got ${options.length}. Use Tabs, RadioGroup or Select for more.`,
61
+ );
62
+ }
63
+ });
64
+
65
+ /**
66
+ * The radios always share a name, the caller's or one made here: without
67
+ * one they are not a group to the browser, and before the page hydrates a
68
+ * second choice would leave the first one checked beside it.
69
+ *
70
+ * With a name made here they are also kept out of any form around them
71
+ * (`form` names a form that does not exist), so nothing is submitted
72
+ * that the caller did not name, as before.
73
+ */
74
+ const fallbackName = generateId("segmented");
75
+ const groupName = $derived(name || fallbackName);
76
+
77
+ const enabled = $derived(
78
+ options.filter((option) => !disabled && !option.disabled),
79
+ );
80
+
81
+ /** The segment that takes Tab: the selected one, or the first that can be chosen. */
82
+ const tabStop = $derived(
83
+ enabled.find((option) => option.value === value)?.value ?? enabled[0]?.value,
84
+ );
85
+
86
+ const inputs: Record<string, HTMLInputElement | undefined> = $state({});
87
+
88
+ /** A press on the selected segment is not a change: a single selection never clears. */
89
+ function select(next: string) {
90
+ if (disabled || next === value) return;
91
+ value = next;
92
+ onchange?.(next);
93
+ }
94
+
95
+ /**
96
+ * The radios are bound, so a segment picked before the page hydrated is
97
+ * kept: Svelte's binding hands it over here instead of overwriting it
98
+ * (see util/hydration.ts), and it goes through `select` like any other
99
+ * choice, `onchange` included. A form reset empties the selection, as it
100
+ * empties a bound native input.
101
+ */
102
+ const chosen = groupBinding<string>(
103
+ () => value,
104
+ (next) => select(next),
105
+ () => {
106
+ if (!disabled) value = resetValueOf<string>(Object.values(inputs));
107
+ },
108
+ );
109
+
110
+ /** `focused` is the value of the segment the key was pressed on, which need not be the selected one. */
111
+ function handleKeydown(event: KeyboardEvent, focused: string) {
112
+ const next = radioKeyIndex(
113
+ event.key,
114
+ enabled.findIndex((option) => option.value === focused),
115
+ enabled.length,
116
+ isRtl(event.currentTarget as Element),
117
+ enabled.some((option) => option.value === value),
118
+ );
119
+ if (next === -1) return;
120
+ event.preventDefault();
121
+ select(enabled[next].value);
122
+ inputs[enabled[next].value]?.focus();
123
+ }
124
+ </script>
125
+
126
+ <!-- Native radio inputs: one Tab stop, Space selects, a press on the selected
127
+ one changes nothing, and the value goes with a form. -->
128
+ <div
129
+ class={cn("segmented", fullWidth ? "w-full" : "", className)}
130
+ data-size={size}
131
+ data-full-width={fullWidth ? "" : undefined}
132
+ role="radiogroup"
133
+ aria-label={ariaLabelledby ? undefined : label || ariaLabel}
134
+ aria-labelledby={ariaLabelledby}
135
+ aria-disabled={disabled ? "true" : undefined}
136
+ {...restProps}
137
+ >
138
+ {#each options as option (option.value)}
139
+ {@const optionDisabled = disabled || !!option.disabled}
140
+ {@const Icon = option.icon}
141
+ <label class="segment">
142
+ <input
143
+ bind:this={inputs[option.value]}
144
+ type="radio"
145
+ class="sr-only"
146
+ name={groupName}
147
+ form={name ? undefined : groupName}
148
+ value={option.value}
149
+ bind:group={chosen.value}
150
+ disabled={optionDisabled}
151
+ tabindex={option.value === tabStop ? 0 : -1}
152
+ onkeydown={(event) => handleKeydown(event, option.value)}
153
+ />
154
+ <span class="segment-face text-sm font-medium">
155
+ {#if Icon}
156
+ <span class="flex shrink-0" aria-hidden="true"><Icon size={16} /></span>
157
+ {/if}
158
+ <span class="segment-label">{option.label}</span>
159
+ </span>
160
+ </label>
161
+ {/each}
162
+ </div>
163
+
164
+ <style>
165
+ /*
166
+ * The states hang off the radio input (`:checked`, `:disabled`,
167
+ * `:focus-visible`), which utilities on the face could only reach through
168
+ * a `peer-*` variant per declaration.
169
+ *
170
+ * Columns are as many as fit at 3.5rem each, so two to four segments share
171
+ * one row down to a 320px screen, and fold into rows instead of squeezing
172
+ * the labels when the text is zoomed or the container is narrower still.
173
+ */
174
+ .segmented {
175
+ --zabi-segment-height: 2.125rem;
176
+ /* In px: side padding that grew with zoomed text would take the room the text needs. */
177
+ --zabi-segment-pad: 12px;
178
+ display: grid;
179
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 3.5rem), 1fr));
180
+ /* One value for the padding and the gap, and for the segments' corner radius below. */
181
+ --zabi-segment-inset: 0.125rem;
182
+ gap: var(--zabi-segment-inset);
183
+ padding: var(--zabi-segment-inset);
184
+ border: 1px solid var(--color-border);
185
+ border-radius: var(--radius-control);
186
+ background: var(--color-action-secondary);
187
+ }
188
+
189
+ .segmented:not([data-full-width]) {
190
+ display: inline-flex;
191
+ flex-wrap: wrap;
192
+ max-width: 100%;
193
+ }
194
+
195
+ .segmented[data-size="sm"] {
196
+ --zabi-segment-height: 1.625rem;
197
+ --zabi-segment-pad: 8px;
198
+ }
199
+
200
+ .segmented[data-size="lg"] {
201
+ --zabi-segment-height: 2.625rem;
202
+ --zabi-segment-pad: 16px;
203
+ }
204
+
205
+ /* A finger needs 44px whatever the size says. */
206
+ @media (pointer: coarse) {
207
+ .segmented,
208
+ .segmented[data-size] {
209
+ --zabi-segment-height: 2.75rem;
210
+ }
211
+ }
212
+
213
+ .segment {
214
+ position: relative;
215
+ display: flex;
216
+ min-width: 0;
217
+ cursor: pointer;
218
+ -webkit-tap-highlight-color: transparent;
219
+ }
220
+
221
+ .segmented:not([data-full-width]) .segment {
222
+ flex: 0 1 auto;
223
+ min-width: 3.5rem;
224
+ }
225
+
226
+ .segment-face {
227
+ display: flex;
228
+ flex: 1;
229
+ align-items: center;
230
+ justify-content: center;
231
+ /* An icon moves above its label before the label has to break. */
232
+ flex-wrap: wrap;
233
+ gap: 0.125rem 0.5rem;
234
+ min-width: 0;
235
+ min-height: var(--zabi-segment-height);
236
+ padding: 0.25rem var(--zabi-segment-pad);
237
+ /* Nested corners: the track's radius less what lies between its edge and
238
+ the segment, which is the track's padding and its 1px border. */
239
+ border-radius: calc(var(--radius-control) - var(--zabi-segment-inset) - 1px);
240
+ color: var(--color-body);
241
+ line-height: 1.25;
242
+ text-align: center;
243
+ transition:
244
+ background-color 150ms,
245
+ color 150ms;
246
+ }
247
+
248
+ /* A long label wraps onto a second line, and breaks inside a word only when a word alone does not fit. */
249
+ .segment-label {
250
+ min-width: 0;
251
+ overflow-wrap: anywhere;
252
+ hyphens: auto;
253
+ }
254
+
255
+ /* Only where a pointer can hover: after a tap, `:hover` stays on the segment until the next tap elsewhere. */
256
+ @media (hover: hover) {
257
+ .segment:hover input:enabled:not(:checked) + .segment-face {
258
+ background: var(--color-surface-hover);
259
+ }
260
+ }
261
+
262
+ .segment:active input:enabled:not(:checked) + .segment-face {
263
+ background: var(--color-surface-active);
264
+ }
265
+
266
+ input:checked + .segment-face {
267
+ background: var(--color-action-primary);
268
+ color: var(--color-action-primary-text);
269
+ }
270
+
271
+ /* Same geometry as `.focus-ring`: a 2px gap, then a 2px ring. */
272
+ input:focus-visible + .segment-face {
273
+ box-shadow:
274
+ 0 0 0 2px var(--color-focus-ring-offset),
275
+ 0 0 0 4px var(--color-focus-ring);
276
+ }
277
+
278
+ .segment:has(input:disabled) {
279
+ cursor: not-allowed;
280
+ }
281
+
282
+ input:disabled + .segment-face {
283
+ opacity: 0.5;
284
+ }
285
+
286
+ /*
287
+ * Forced colours paint every background with the canvas colour and drop
288
+ * every box-shadow, which would leave the selected segment looking like
289
+ * the others and keyboard focus with no mark. The selection takes the
290
+ * system's own selection colours, and focus an outline, as `.focus-ring`
291
+ * gets in app.css.
292
+ */
293
+ @media (forced-colors: active) {
294
+ input:checked + .segment-face {
295
+ forced-color-adjust: none;
296
+ background: Highlight;
297
+ color: HighlightText;
298
+ }
299
+
300
+ input:disabled + .segment-face {
301
+ opacity: 1;
302
+ color: GrayText;
303
+ }
304
+
305
+ input:checked:disabled + .segment-face {
306
+ background: GrayText;
307
+ color: Canvas;
308
+ }
309
+
310
+ /* The selected face opted out of forced colours above, so its shadow ring would come back beside the outline. */
311
+ input:focus-visible + .segment-face {
312
+ box-shadow: none;
313
+ outline: 2px solid Highlight;
314
+ outline-offset: 2px;
315
+ }
316
+ }
317
+
318
+ @media (prefers-reduced-motion: reduce) {
319
+ .segment-face {
320
+ transition: none;
321
+ }
322
+ }
323
+ </style>
@@ -0,0 +1,34 @@
1
+ import type { HTMLAttributes } from "svelte/elements";
2
+ import type { SizeVariant } from "../types/variants.js";
3
+ import type { SegmentedControlOption } from "../util/segmented-control.js";
4
+ /** Other attributes (`data-*`, `id`, ...) land on the host element. */
5
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "onchange" | "aria-label" | "aria-labelledby"> & {
6
+ /** Extra classes for the host element. */
7
+ class?: string;
8
+ /** Two to four choices, shown in one row. */
9
+ options: SegmentedControlOption[];
10
+ /** Selected value; supports `bind:value`. `undefined` for no selection. */
11
+ value?: string | undefined;
12
+ /** Accessible name of the group. Not shown. */
13
+ label?: string;
14
+ /** Accessible name of the group, the same as `label`. */
15
+ "aria-label"?: string;
16
+ /** Id of a visible element that names the group, in place of `label`. */
17
+ "aria-labelledby"?: string;
18
+ /** Height, on the scale Input and Button use. At least 44px on a touch screen. */
19
+ size?: SizeVariant;
20
+ /** Equal-width segments that fill the row. Off: each segment is as wide as its label. */
21
+ fullWidth?: boolean;
22
+ /**
23
+ * Name the value is submitted under in a form. Without a name the
24
+ * control is not part of the form around it: it submits nothing and a
25
+ * form reset leaves it alone.
26
+ */
27
+ name?: string;
28
+ disabled?: boolean;
29
+ /** Called with the new value when the user picks another segment. */
30
+ onchange?: (value: string) => void;
31
+ };
32
+ declare const SegmentedControl: import("svelte").Component<Props, {}, "value">;
33
+ type SegmentedControl = ReturnType<typeof SegmentedControl>;
34
+ export default SegmentedControl;
@@ -1,11 +1,18 @@
1
1
  <script lang="ts">
2
+ import { zabiStringsFor } from "../util/zabi-strings.js";
2
3
  import { cn } from "../util/cn.js";
4
+ import {
5
+ DEFAULT_SIDEBAR_BRAND_HEADER_STRINGS,
6
+ type SidebarBrandHeaderStrings,
7
+ } from "../util/sidebar.js";
3
8
 
4
9
  interface Props {
5
10
  collapsed?: boolean;
6
11
  brandName?: string;
7
12
  logoSrc?: string;
8
13
  logoAlt?: string;
14
+ /** `brandAlt`: the logo's alt text when neither `logoAlt` nor `brandName` is given. */
15
+ strings?: Partial<SidebarBrandHeaderStrings>;
9
16
  class?: string;
10
17
  /** @deprecated use `class`. */
11
18
  className?: string;
@@ -16,10 +23,13 @@
16
23
  brandName = "",
17
24
  logoSrc = "",
18
25
  logoAlt = "",
26
+ strings,
19
27
  class: classAttr = "",
20
28
  className: legacyClass = "",
21
29
  }: Props = $props();
22
30
 
31
+ const provided = zabiStringsFor("sidebarBrandHeader");
32
+
23
33
  /** `class` is the public prop; `className` is a deprecated alias.
24
34
  * Both are merged here so existing call sites keep working. */
25
35
  const className = $derived(`${classAttr} ${legacyClass}`.trim());
@@ -52,7 +62,7 @@
52
62
  >
53
63
  <img
54
64
  src={logoSrc}
55
- alt={logoAlt || trimmedName || "Brand"}
65
+ alt={logoAlt || trimmedName || (strings?.brandAlt ?? provided()?.brandAlt ?? DEFAULT_SIDEBAR_BRAND_HEADER_STRINGS.brandAlt)}
56
66
  class="size-full object-cover"
57
67
  width="36"
58
68
  height="36"
@@ -1,8 +1,11 @@
1
+ import { type SidebarBrandHeaderStrings } from "../util/sidebar.js";
1
2
  interface Props {
2
3
  collapsed?: boolean;
3
4
  brandName?: string;
4
5
  logoSrc?: string;
5
6
  logoAlt?: string;
7
+ /** `brandAlt`: the logo's alt text when neither `logoAlt` nor `brandName` is given. */
8
+ strings?: Partial<SidebarBrandHeaderStrings>;
6
9
  class?: string;
7
10
  /** @deprecated use `class`. */
8
11
  className?: string;
@@ -1,10 +1,15 @@
1
1
  <script lang="ts">
2
+ import { mergeStrings } from "../util/ready-made-strings.js";
3
+ import { zabiStringsFor } from "../util/zabi-strings.js";
2
4
  import type { Snippet } from "svelte";
3
5
  import { fixedSidebarFlyout } from "../util/fixed-sidebar-flyout.js";
4
6
  import { cn } from "../util/cn.js";
7
+ import { DEFAULT_SIDEBAR_FOOTER_STRINGS, type SidebarFooterStrings } from "../util/sidebar.js";
5
8
 
6
9
  interface Props {
7
10
  collapsed?: boolean;
11
+ /** The footer's name and the profile button's, for another language. */
12
+ strings?: Partial<SidebarFooterStrings>;
8
13
  showProfile?: boolean;
9
14
  profileName?: string;
10
15
  profileEmail?: string;
@@ -27,6 +32,7 @@
27
32
 
28
33
  let {
29
34
  collapsed = false,
35
+ strings,
30
36
  showProfile = true,
31
37
  profileName = "Zabi",
32
38
  profileEmail = "hello@zabi.dev",
@@ -35,7 +41,7 @@
35
41
  logoutLabel = "Logout",
36
42
  showThemeToggle = true,
37
43
  lightModeLabel = "Light mode",
38
- isLightMode = $bindable(false),
44
+ isLightMode = $bindable<Exclude<Props["isLightMode"], undefined>>(),
39
45
  onLogout,
40
46
  onThemeToggle,
41
47
  class: classAttr = "",
@@ -46,15 +52,32 @@
46
52
  profilePanel,
47
53
  }: Props = $props();
48
54
 
55
+ // No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
56
+ // one that has a fallback (`props_invalid_value`), and a page that throws
57
+ // while it hydrates never becomes interactive. The default is applied
58
+ // here instead: at once, for the server and the first render, and again
59
+ // whenever a parent hands back `undefined`.
60
+ const applyDefaults = () => {
61
+ if (isLightMode === undefined) isLightMode = false;
62
+ };
63
+ applyDefaults();
64
+ $effect.pre(applyDefaults);
65
+
49
66
  /** `class` is the public prop; `className` is a deprecated alias.
50
67
  * Both are merged here so existing call sites keep working. */
51
68
  const className = $derived(cn(`${classAttr} ${legacyClass}`));
52
69
 
70
+ /** The app-wide words for this component, from a `ZabiStringsProvider` above it, if there is one. */
71
+ const provided = zabiStringsFor("sidebarFooter");
72
+ const text = $derived(mergeStrings(DEFAULT_SIDEBAR_FOOTER_STRINGS, provided(), strings));
73
+
53
74
  const showFooter = $derived(showProfile || showLogout || showThemeToggle);
54
75
  const showPanelLauncher = $derived(showProfile);
55
76
 
77
+ // Nested corners: the avatar sits 0.5rem (`px-2 py-2`) inside the profile
78
+ // button, so its radius is the button's less that gap.
56
79
  const avatarClasses =
57
- "size-10 rounded-container bg-action-primary text-action-primary flex items-center justify-center text-sm font-semibold shrink-0 ring-1 ring-border-focus";
80
+ "size-10 rounded-[calc(var(--radius-container)-0.5rem)] bg-action-primary text-action-primary flex items-center justify-center text-sm font-semibold shrink-0 ring-1 ring-border";
58
81
 
59
82
  function getTextToneClass(isMuted = false): string {
60
83
  return isMuted ? "text-description" : "text-headline";
@@ -69,21 +92,21 @@
69
92
  <div class="w-full shrink-0" data-sidebar-flyout-root>
70
93
  <footer
71
94
  class={cn(`flex w-full shrink-0 flex-col gap-3 border-t border-border pt-3 pb-1 ${className}`)}
72
- aria-label="Account and settings"
95
+ aria-label={text.accountAndSettings}
73
96
  >
74
97
  {#if showPanelLauncher}
75
98
  <button
76
99
  type="button"
77
100
  data-sidebar-flyout-anchor="profile"
78
- class="w-full cursor-pointer rounded-container px-2 py-2 outline-none transition-colors hover:bg-nav-menu-hover hover:text-nav-menu-item-hover hover:ring-1 hover:ring-border/60 active:bg-surface-active focus-ring focus-ring--nav"
101
+ class="w-full cursor-pointer rounded-container px-2 py-2 outline-none transition-colors hover:bg-nav-menu-hover hover:text-nav-menu-item-hover hover:ring-1 hover:ring-border active:bg-surface-active focus-ring focus-ring--nav"
79
102
  aria-haspopup="dialog"
80
103
  aria-expanded={profilePanelOpen}
81
104
  aria-controls={profilePanelControlsId.trim()
82
105
  ? profilePanelControlsId.trim()
83
106
  : undefined}
84
107
  aria-label={collapsed
85
- ? `Open account panel`
86
- : `Open account panel for ${profileName}`}
108
+ ? text.openAccountPanel
109
+ : text.openAccountPanelFor(profileName)}
87
110
  onclick={handleProfileClick}
88
111
  >
89
112
  <span class="flex w-full items-center gap-3 text-left">
@@ -1,6 +1,9 @@
1
1
  import type { Snippet } from "svelte";
2
+ import { type SidebarFooterStrings } from "../util/sidebar.js";
2
3
  interface Props {
3
4
  collapsed?: boolean;
5
+ /** The footer's name and the profile button's, for another language. */
6
+ strings?: Partial<SidebarFooterStrings>;
4
7
  showProfile?: boolean;
5
8
  profileName?: string;
6
9
  profileEmail?: string;