@happyvertical/smrt-ui 0.30.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 (330) hide show
  1. package/AGENTS.md +50 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/dist/actions/__tests__/ripple.test.js +28 -0
  5. package/dist/actions/permission.d.ts +34 -0
  6. package/dist/actions/permission.d.ts.map +1 -0
  7. package/dist/actions/permission.js +70 -0
  8. package/dist/actions/ripple.d.ts +7 -0
  9. package/dist/actions/ripple.d.ts.map +1 -0
  10. package/dist/actions/ripple.js +65 -0
  11. package/dist/components/calendar/Calendar.svelte +520 -0
  12. package/dist/components/calendar/Calendar.svelte.d.ts +17 -0
  13. package/dist/components/calendar/Calendar.svelte.d.ts.map +1 -0
  14. package/dist/components/calendar/DayView.svelte +389 -0
  15. package/dist/components/calendar/DayView.svelte.d.ts +13 -0
  16. package/dist/components/calendar/DayView.svelte.d.ts.map +1 -0
  17. package/dist/components/calendar/index.d.ts +6 -0
  18. package/dist/components/calendar/index.d.ts.map +1 -0
  19. package/dist/components/calendar/index.js +5 -0
  20. package/dist/components/chat/MessageBubble.svelte +126 -0
  21. package/dist/components/chat/MessageBubble.svelte.d.ts +30 -0
  22. package/dist/components/chat/MessageBubble.svelte.d.ts.map +1 -0
  23. package/dist/components/chat/ReactionPicker.svelte +89 -0
  24. package/dist/components/chat/ReactionPicker.svelte.d.ts +19 -0
  25. package/dist/components/chat/ReactionPicker.svelte.d.ts.map +1 -0
  26. package/dist/components/chat/TypingIndicator.svelte +90 -0
  27. package/dist/components/chat/TypingIndicator.svelte.d.ts +17 -0
  28. package/dist/components/chat/TypingIndicator.svelte.d.ts.map +1 -0
  29. package/dist/components/chat/__tests__/chat-primitives.test.js +67 -0
  30. package/dist/components/chat/index.d.ts +10 -0
  31. package/dist/components/chat/index.d.ts.map +1 -0
  32. package/dist/components/chat/index.js +9 -0
  33. package/dist/components/data/DataTable.svelte +519 -0
  34. package/dist/components/data/DataTable.svelte.d.ts +49 -0
  35. package/dist/components/data/DataTable.svelte.d.ts.map +1 -0
  36. package/dist/components/data/__tests__/DataTable.test.js +48 -0
  37. package/dist/components/data/__tests__/data-table-helpers.test.js +36 -0
  38. package/dist/components/data/index.d.ts +6 -0
  39. package/dist/components/data/index.d.ts.map +1 -0
  40. package/dist/components/data/index.js +5 -0
  41. package/dist/components/data/types.d.ts +104 -0
  42. package/dist/components/data/types.d.ts.map +1 -0
  43. package/dist/components/data/types.js +45 -0
  44. package/dist/components/display/ConfidenceBadge.svelte +142 -0
  45. package/dist/components/display/ConfidenceBadge.svelte.d.ts +25 -0
  46. package/dist/components/display/ConfidenceBadge.svelte.d.ts.map +1 -0
  47. package/dist/components/display/CurrencyDisplay.svelte +106 -0
  48. package/dist/components/display/CurrencyDisplay.svelte.d.ts +30 -0
  49. package/dist/components/display/CurrencyDisplay.svelte.d.ts.map +1 -0
  50. package/dist/components/display/DateDisplay.svelte +122 -0
  51. package/dist/components/display/DateDisplay.svelte.d.ts +24 -0
  52. package/dist/components/display/DateDisplay.svelte.d.ts.map +1 -0
  53. package/dist/components/display/Icon.svelte +77 -0
  54. package/dist/components/display/Icon.svelte.d.ts +28 -0
  55. package/dist/components/display/Icon.svelte.d.ts.map +1 -0
  56. package/dist/components/display/StatusBadge.svelte +256 -0
  57. package/dist/components/display/StatusBadge.svelte.d.ts +24 -0
  58. package/dist/components/display/StatusBadge.svelte.d.ts.map +1 -0
  59. package/dist/components/display/__tests__/ConfidenceBadge.test.js +96 -0
  60. package/dist/components/display/__tests__/CurrencyDisplay.test.js +114 -0
  61. package/dist/components/display/__tests__/DateDisplay.test.js +114 -0
  62. package/dist/components/display/__tests__/Icon.test.js +93 -0
  63. package/dist/components/display/__tests__/StatusBadge.test.js +98 -0
  64. package/dist/components/display/index.d.ts +10 -0
  65. package/dist/components/display/index.d.ts.map +1 -0
  66. package/dist/components/display/index.js +9 -0
  67. package/dist/components/display/types.d.ts +5 -0
  68. package/dist/components/display/types.d.ts.map +1 -0
  69. package/dist/components/display/types.js +4 -0
  70. package/dist/components/feedback/ConfirmDialog.svelte +226 -0
  71. package/dist/components/feedback/ConfirmDialog.svelte.d.ts +25 -0
  72. package/dist/components/feedback/ConfirmDialog.svelte.d.ts.map +1 -0
  73. package/dist/components/feedback/LoadingOverlay.svelte +281 -0
  74. package/dist/components/feedback/LoadingOverlay.svelte.d.ts +31 -0
  75. package/dist/components/feedback/LoadingOverlay.svelte.d.ts.map +1 -0
  76. package/dist/components/feedback/Modal.svelte +393 -0
  77. package/dist/components/feedback/Modal.svelte.d.ts +46 -0
  78. package/dist/components/feedback/Modal.svelte.d.ts.map +1 -0
  79. package/dist/components/feedback/ProgressBar.svelte +162 -0
  80. package/dist/components/feedback/ProgressBar.svelte.d.ts +21 -0
  81. package/dist/components/feedback/ProgressBar.svelte.d.ts.map +1 -0
  82. package/dist/components/feedback/__tests__/ConfirmDialog.test.js +111 -0
  83. package/dist/components/feedback/__tests__/LoadingOverlay.test.js +99 -0
  84. package/dist/components/feedback/__tests__/Modal.test.js +72 -0
  85. package/dist/components/feedback/__tests__/ProgressBar.test.js +89 -0
  86. package/dist/components/feedback/index.d.ts +8 -0
  87. package/dist/components/feedback/index.d.ts.map +1 -0
  88. package/dist/components/feedback/index.js +10 -0
  89. package/dist/components/layout/Container.svelte +53 -0
  90. package/dist/components/layout/Container.svelte.d.ts +11 -0
  91. package/dist/components/layout/Container.svelte.d.ts.map +1 -0
  92. package/dist/components/layout/EmptyState.svelte +187 -0
  93. package/dist/components/layout/EmptyState.svelte.d.ts +28 -0
  94. package/dist/components/layout/EmptyState.svelte.d.ts.map +1 -0
  95. package/dist/components/layout/Footer.svelte +63 -0
  96. package/dist/components/layout/Footer.svelte.d.ts +8 -0
  97. package/dist/components/layout/Footer.svelte.d.ts.map +1 -0
  98. package/dist/components/layout/Grid.svelte +241 -0
  99. package/dist/components/layout/Grid.svelte.d.ts +56 -0
  100. package/dist/components/layout/Grid.svelte.d.ts.map +1 -0
  101. package/dist/components/layout/Header.svelte +86 -0
  102. package/dist/components/layout/Header.svelte.d.ts +9 -0
  103. package/dist/components/layout/Header.svelte.d.ts.map +1 -0
  104. package/dist/components/layout/Masthead.svelte +219 -0
  105. package/dist/components/layout/Masthead.svelte.d.ts +13 -0
  106. package/dist/components/layout/Masthead.svelte.d.ts.map +1 -0
  107. package/dist/components/layout/PageHeader.svelte +131 -0
  108. package/dist/components/layout/PageHeader.svelte.d.ts +26 -0
  109. package/dist/components/layout/PageHeader.svelte.d.ts.map +1 -0
  110. package/dist/components/layout/SummaryCard.svelte +203 -0
  111. package/dist/components/layout/SummaryCard.svelte.d.ts +20 -0
  112. package/dist/components/layout/SummaryCard.svelte.d.ts.map +1 -0
  113. package/dist/components/layout/__tests__/Container.test.js +62 -0
  114. package/dist/components/layout/__tests__/EmptyState.test.js +83 -0
  115. package/dist/components/layout/__tests__/Footer.test.js +50 -0
  116. package/dist/components/layout/__tests__/Grid.test.js +121 -0
  117. package/dist/components/layout/__tests__/Header.test.js +48 -0
  118. package/dist/components/layout/__tests__/Masthead.test.js +93 -0
  119. package/dist/components/layout/__tests__/PageHeader.test.js +80 -0
  120. package/dist/components/layout/__tests__/SummaryCard.test.js +82 -0
  121. package/dist/components/layout/index.d.ts +12 -0
  122. package/dist/components/layout/index.d.ts.map +1 -0
  123. package/dist/components/layout/index.js +11 -0
  124. package/dist/components/memberships/MembershipCard.svelte +163 -0
  125. package/dist/components/memberships/MembershipCard.svelte.d.ts +12 -0
  126. package/dist/components/memberships/MembershipCard.svelte.d.ts.map +1 -0
  127. package/dist/components/memberships/MembershipList.svelte +98 -0
  128. package/dist/components/memberships/MembershipList.svelte.d.ts +19 -0
  129. package/dist/components/memberships/MembershipList.svelte.d.ts.map +1 -0
  130. package/dist/components/nav/FilterChips.svelte +152 -0
  131. package/dist/components/nav/FilterChips.svelte.d.ts +19 -0
  132. package/dist/components/nav/FilterChips.svelte.d.ts.map +1 -0
  133. package/dist/components/nav/Tabs.svelte +252 -0
  134. package/dist/components/nav/Tabs.svelte.d.ts +34 -0
  135. package/dist/components/nav/Tabs.svelte.d.ts.map +1 -0
  136. package/dist/components/nav/__tests__/FilterChips.test.js +94 -0
  137. package/dist/components/nav/__tests__/Tabs.test.js +128 -0
  138. package/dist/components/nav/index.d.ts +7 -0
  139. package/dist/components/nav/index.d.ts.map +1 -0
  140. package/dist/components/nav/index.js +6 -0
  141. package/dist/components/nav/types.d.ts +24 -0
  142. package/dist/components/nav/types.d.ts.map +1 -0
  143. package/dist/components/nav/types.js +4 -0
  144. package/dist/components/permissions/PermissionCheck.svelte +45 -0
  145. package/dist/components/permissions/PermissionCheck.svelte.d.ts +19 -0
  146. package/dist/components/permissions/PermissionCheck.svelte.d.ts.map +1 -0
  147. package/dist/components/roles/RoleBadge.svelte +84 -0
  148. package/dist/components/roles/RoleBadge.svelte.d.ts +13 -0
  149. package/dist/components/roles/RoleBadge.svelte.d.ts.map +1 -0
  150. package/dist/components/roles/RoleSelector.svelte +216 -0
  151. package/dist/components/roles/RoleSelector.svelte.d.ts +13 -0
  152. package/dist/components/roles/RoleSelector.svelte.d.ts.map +1 -0
  153. package/dist/components/theme/ThemeProvider.svelte +71 -0
  154. package/dist/components/theme/ThemeProvider.svelte.d.ts +10 -0
  155. package/dist/components/theme/ThemeProvider.svelte.d.ts.map +1 -0
  156. package/dist/components/theme/context.svelte.d.ts +15 -0
  157. package/dist/components/theme/context.svelte.d.ts.map +1 -0
  158. package/dist/components/theme/context.svelte.js +42 -0
  159. package/dist/components/theme/index.d.ts +3 -0
  160. package/dist/components/theme/index.d.ts.map +1 -0
  161. package/dist/components/theme/index.js +2 -0
  162. package/dist/components/ui/Avatar.svelte +167 -0
  163. package/dist/components/ui/Avatar.svelte.d.ts +26 -0
  164. package/dist/components/ui/Avatar.svelte.d.ts.map +1 -0
  165. package/dist/components/ui/Badge.svelte +70 -0
  166. package/dist/components/ui/Badge.svelte.d.ts +12 -0
  167. package/dist/components/ui/Badge.svelte.d.ts.map +1 -0
  168. package/dist/components/ui/Button.svelte +226 -0
  169. package/dist/components/ui/Button.svelte.d.ts +28 -0
  170. package/dist/components/ui/Button.svelte.d.ts.map +1 -0
  171. package/dist/components/ui/Card.svelte +122 -0
  172. package/dist/components/ui/Card.svelte.d.ts +15 -0
  173. package/dist/components/ui/Card.svelte.d.ts.map +1 -0
  174. package/dist/components/ui/Chip.svelte +167 -0
  175. package/dist/components/ui/Chip.svelte.d.ts +33 -0
  176. package/dist/components/ui/Chip.svelte.d.ts.map +1 -0
  177. package/dist/components/ui/Dropdown.svelte +250 -0
  178. package/dist/components/ui/Dropdown.svelte.d.ts +20 -0
  179. package/dist/components/ui/Dropdown.svelte.d.ts.map +1 -0
  180. package/dist/components/ui/Pagination.svelte +294 -0
  181. package/dist/components/ui/Pagination.svelte.d.ts +21 -0
  182. package/dist/components/ui/Pagination.svelte.d.ts.map +1 -0
  183. package/dist/components/ui/Skeleton.svelte +113 -0
  184. package/dist/components/ui/Skeleton.svelte.d.ts +24 -0
  185. package/dist/components/ui/Skeleton.svelte.d.ts.map +1 -0
  186. package/dist/components/ui/Tooltip.svelte +120 -0
  187. package/dist/components/ui/Tooltip.svelte.d.ts +24 -0
  188. package/dist/components/ui/Tooltip.svelte.d.ts.map +1 -0
  189. package/dist/components/ui/Tree.svelte +209 -0
  190. package/dist/components/ui/Tree.svelte.d.ts +17 -0
  191. package/dist/components/ui/Tree.svelte.d.ts.map +1 -0
  192. package/dist/components/ui/__tests__/Badge.test.js +76 -0
  193. package/dist/components/ui/__tests__/Button.test.js +69 -0
  194. package/dist/components/ui/__tests__/Card.test.js +103 -0
  195. package/dist/components/ui/__tests__/Pagination.test.js +99 -0
  196. package/dist/components/ui/__tests__/gap-primitives-interactive.test.js +112 -0
  197. package/dist/components/ui/__tests__/gap-primitives.test.js +84 -0
  198. package/dist/components/ui/index.d.ts +14 -0
  199. package/dist/components/ui/index.d.ts.map +1 -0
  200. package/dist/components/ui/index.js +18 -0
  201. package/dist/i18n/Trans.svelte +29 -0
  202. package/dist/i18n/Trans.svelte.d.ts +24 -0
  203. package/dist/i18n/Trans.svelte.d.ts.map +1 -0
  204. package/dist/i18n/__tests__/i18n.test.js +74 -0
  205. package/dist/i18n/__tests__/render-parity.spec.js +37 -0
  206. package/dist/i18n/context.svelte.d.ts +43 -0
  207. package/dist/i18n/context.svelte.d.ts.map +1 -0
  208. package/dist/i18n/context.svelte.js +69 -0
  209. package/dist/i18n/index.d.ts +17 -0
  210. package/dist/i18n/index.d.ts.map +1 -0
  211. package/dist/i18n/index.js +24 -0
  212. package/dist/i18n/registry.d.ts +44 -0
  213. package/dist/i18n/registry.d.ts.map +1 -0
  214. package/dist/i18n/registry.js +60 -0
  215. package/dist/i18n/render.d.ts +22 -0
  216. package/dist/i18n/render.d.ts.map +1 -0
  217. package/dist/i18n/render.js +44 -0
  218. package/dist/i18n/strings.d.ts +7 -0
  219. package/dist/i18n/strings.d.ts.map +1 -0
  220. package/dist/i18n/strings.js +19 -0
  221. package/dist/i18n/strings.ui.d.ts +34 -0
  222. package/dist/i18n/strings.ui.d.ts.map +1 -0
  223. package/dist/i18n/strings.ui.js +44 -0
  224. package/dist/i18n/use-i18n.d.ts +20 -0
  225. package/dist/i18n/use-i18n.d.ts.map +1 -0
  226. package/dist/i18n/use-i18n.js +21 -0
  227. package/dist/index.d.ts +28 -0
  228. package/dist/index.d.ts.map +1 -0
  229. package/dist/index.js +38 -0
  230. package/dist/registry/index.d.ts +6 -0
  231. package/dist/registry/index.d.ts.map +1 -0
  232. package/dist/registry/index.js +4 -0
  233. package/dist/registry/module-registry.d.ts +58 -0
  234. package/dist/registry/module-registry.d.ts.map +1 -0
  235. package/dist/registry/module-registry.js +94 -0
  236. package/dist/styles/index.d.ts +4 -0
  237. package/dist/styles/index.d.ts.map +1 -0
  238. package/dist/styles/index.js +6 -0
  239. package/dist/styles/tokens.css +76 -0
  240. package/dist/test-support/a11y.d.ts +16 -0
  241. package/dist/test-support/a11y.d.ts.map +1 -0
  242. package/dist/test-support/a11y.js +32 -0
  243. package/dist/test-support/setup.d.ts +11 -0
  244. package/dist/test-support/setup.d.ts.map +1 -0
  245. package/dist/test-support/setup.js +33 -0
  246. package/dist/theme/ThemeProvider.svelte +207 -0
  247. package/dist/theme/ThemeProvider.svelte.d.ts +22 -0
  248. package/dist/theme/ThemeProvider.svelte.d.ts.map +1 -0
  249. package/dist/theme/context.d.ts +49 -0
  250. package/dist/theme/context.d.ts.map +1 -0
  251. package/dist/theme/context.js +32 -0
  252. package/dist/theme/index.d.ts +7 -0
  253. package/dist/theme/index.d.ts.map +1 -0
  254. package/dist/theme/index.js +9 -0
  255. package/dist/theme/tokens.d.ts +309 -0
  256. package/dist/theme/tokens.d.ts.map +1 -0
  257. package/dist/theme/tokens.js +418 -0
  258. package/dist/themes/CUSTOM_THEME_GUIDE.md +341 -0
  259. package/dist/themes/README.md +675 -0
  260. package/dist/themes/ThemeProvider.svelte +275 -0
  261. package/dist/themes/ThemeProvider.svelte.d.ts +24 -0
  262. package/dist/themes/ThemeProvider.svelte.d.ts.map +1 -0
  263. package/dist/themes/__tests__/css-generator.test.js +32 -0
  264. package/dist/themes/__tests__/registry.test.js +43 -0
  265. package/dist/themes/__tests__/token-aliases.test.js +176 -0
  266. package/dist/themes/components/ColorSchemeToggle.svelte +205 -0
  267. package/dist/themes/components/ColorSchemeToggle.svelte.d.ts +14 -0
  268. package/dist/themes/components/ColorSchemeToggle.svelte.d.ts.map +1 -0
  269. package/dist/themes/components/ThemeSwitcher.svelte +188 -0
  270. package/dist/themes/components/ThemeSwitcher.svelte.d.ts +14 -0
  271. package/dist/themes/components/ThemeSwitcher.svelte.d.ts.map +1 -0
  272. package/dist/themes/components/index.d.ts +8 -0
  273. package/dist/themes/components/index.d.ts.map +1 -0
  274. package/dist/themes/components/index.js +7 -0
  275. package/dist/themes/context.svelte.d.ts +30 -0
  276. package/dist/themes/context.svelte.d.ts.map +1 -0
  277. package/dist/themes/context.svelte.js +42 -0
  278. package/dist/themes/create-theme.d.ts +99 -0
  279. package/dist/themes/create-theme.d.ts.map +1 -0
  280. package/dist/themes/create-theme.js +389 -0
  281. package/dist/themes/css-generator.d.ts +44 -0
  282. package/dist/themes/css-generator.d.ts.map +1 -0
  283. package/dist/themes/css-generator.js +226 -0
  284. package/dist/themes/glass/index.d.ts +14 -0
  285. package/dist/themes/glass/index.d.ts.map +1 -0
  286. package/dist/themes/glass/index.js +286 -0
  287. package/dist/themes/index.d.ts +31 -0
  288. package/dist/themes/index.d.ts.map +1 -0
  289. package/dist/themes/index.js +37 -0
  290. package/dist/themes/material/index.d.ts +13 -0
  291. package/dist/themes/material/index.d.ts.map +1 -0
  292. package/dist/themes/material/index.js +269 -0
  293. package/dist/themes/registry.d.ts +64 -0
  294. package/dist/themes/registry.d.ts.map +1 -0
  295. package/dist/themes/registry.js +122 -0
  296. package/dist/themes/shared.d.ts +78 -0
  297. package/dist/themes/shared.d.ts.map +1 -0
  298. package/dist/themes/shared.js +179 -0
  299. package/dist/themes/studio/index.d.ts +14 -0
  300. package/dist/themes/studio/index.d.ts.map +1 -0
  301. package/dist/themes/studio/index.js +270 -0
  302. package/dist/themes/styles/all.css +12 -0
  303. package/dist/themes/styles/glass.css +432 -0
  304. package/dist/themes/styles/index.d.ts +22 -0
  305. package/dist/themes/styles/index.d.ts.map +1 -0
  306. package/dist/themes/styles/index.js +23 -0
  307. package/dist/themes/styles/material.css +364 -0
  308. package/dist/themes/styles/studio.css +416 -0
  309. package/dist/themes/types.d.ts +273 -0
  310. package/dist/themes/types.d.ts.map +1 -0
  311. package/dist/themes/types.js +15 -0
  312. package/dist/types-generic.d.ts +75 -0
  313. package/dist/types-generic.d.ts.map +1 -0
  314. package/dist/types-generic.js +1 -0
  315. package/dist/utils/forms/__tests__/formatters.test.js +27 -0
  316. package/dist/utils/forms/formatters.d.ts +14 -0
  317. package/dist/utils/forms/formatters.d.ts.map +1 -0
  318. package/dist/utils/forms/formatters.js +77 -0
  319. package/dist/utils/import-optional.d.ts +5 -0
  320. package/dist/utils/import-optional.d.ts.map +1 -0
  321. package/dist/utils/import-optional.js +7 -0
  322. package/dist/utils/theme/__tests__/color.test.js +72 -0
  323. package/dist/utils/theme/__tests__/typography.test.js +11 -0
  324. package/dist/utils/theme/color.d.ts +70 -0
  325. package/dist/utils/theme/color.d.ts.map +1 -0
  326. package/dist/utils/theme/color.js +221 -0
  327. package/dist/utils/theme/typography.d.ts +27 -0
  328. package/dist/utils/theme/typography.d.ts.map +1 -0
  329. package/dist/utils/theme/typography.js +30 -0
  330. package/package.json +143 -0
@@ -0,0 +1,252 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Tabs - Tab navigation component
4
+ * refactored for Material 3
5
+ *
6
+ * Provides tabbed navigation with optional counts and content slots.
7
+ *
8
+ * Accessibility:
9
+ * - Supports keyboard navigation with Arrow keys
10
+ * - Proper ARIA roles: tablist, tab, tabpanel
11
+ * - aria-selected indicates active tab
12
+ * - aria-controls links tab to panel
13
+ */
14
+ import type { Snippet } from 'svelte';
15
+ import { ripple } from '../../actions/ripple.js';
16
+ import type { Tab } from './types.js';
17
+
18
+ export interface Props {
19
+ /** Available tabs */
20
+ tabs: Tab[];
21
+ /** Currently active tab id */
22
+ active: string;
23
+ /** Called when tab changes */
24
+ onchange?: (id: string) => void;
25
+ /** Size variant */
26
+ size?: 'sm' | 'md' | 'lg';
27
+ /** Visual variant */
28
+ variant?: 'primary' | 'secondary';
29
+ /** Tab content slot */
30
+ children?: Snippet;
31
+ /** Accessible label for the tablist */
32
+ 'aria-label'?: string;
33
+ }
34
+
35
+ const {
36
+ tabs,
37
+ active,
38
+ onchange,
39
+ size = 'md',
40
+ variant = 'primary',
41
+ children,
42
+ 'aria-label': ariaLabel,
43
+ }: Props = $props();
44
+
45
+ /** Get enabled tabs only */
46
+ const enabledTabs = $derived(tabs.filter((t) => !t.disabled));
47
+
48
+ let tablistEl: HTMLElement | null = $state(null);
49
+
50
+ // Generate unique ID for this tabs instance
51
+ const instanceId = $props.id();
52
+
53
+ function handleClick(id: string) {
54
+ if (id !== active) {
55
+ onchange?.(id);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Handle keyboard navigation for accessibility
61
+ * ArrowRight/ArrowLeft: Move between tabs
62
+ * Home: Go to first tab
63
+ * End: Go to last tab
64
+ */
65
+ function handleKeydown(event: KeyboardEvent, tabId: string) {
66
+ const currentIndex = enabledTabs.findIndex((t) => t.id === tabId);
67
+ let nextIndex: number | null = null;
68
+
69
+ switch (event.key) {
70
+ case 'ArrowRight':
71
+ event.preventDefault();
72
+ nextIndex = (currentIndex + 1) % enabledTabs.length;
73
+ break;
74
+ case 'ArrowLeft':
75
+ event.preventDefault();
76
+ nextIndex = (currentIndex - 1 + enabledTabs.length) % enabledTabs.length;
77
+ break;
78
+ case 'Home':
79
+ event.preventDefault();
80
+ nextIndex = 0;
81
+ break;
82
+ case 'End':
83
+ event.preventDefault();
84
+ nextIndex = enabledTabs.length - 1;
85
+ break;
86
+ }
87
+
88
+ if (nextIndex !== null && nextIndex !== currentIndex) {
89
+ const nextTab = enabledTabs[nextIndex];
90
+ if (nextTab) {
91
+ onchange?.(nextTab.id);
92
+ // Focus the next tab button after the DOM updates
93
+ requestAnimationFrame(() => {
94
+ const tabButton = tablistEl?.querySelector(
95
+ `[data-tab-id="${CSS.escape(nextTab.id)}"]`,
96
+ ) as HTMLElement;
97
+ tabButton?.focus();
98
+ });
99
+ }
100
+ }
101
+ }
102
+ </script>
103
+
104
+ <div class="tabs-container">
105
+ <div
106
+ class="tabs-nav"
107
+ class:sm={size === 'sm'}
108
+ class:lg={size === 'lg'}
109
+ class:secondary={variant === 'secondary'}
110
+ role="tablist"
111
+ aria-label={ariaLabel}
112
+ bind:this={tablistEl}
113
+ >
114
+ {#each tabs as tab (tab.id)}
115
+ <button
116
+ type="button"
117
+ class="tab-button"
118
+ class:active={tab.id === active}
119
+ disabled={tab.disabled}
120
+ role="tab"
121
+ id="tab-{instanceId}-{tab.id}"
122
+ aria-selected={tab.id === active}
123
+ aria-controls="tab-panel-{instanceId}"
124
+ tabindex={tab.id === active ? 0 : -1}
125
+ data-tab-id={tab.id}
126
+ onclick={() => handleClick(tab.id)}
127
+ onkeydown={(e) => handleKeydown(e, tab.id)}
128
+ use:ripple
129
+ >
130
+ <span class="tab-content-wrapper">
131
+ <span class="tab-label">{tab.label}</span>
132
+ {#if tab.count !== undefined}
133
+ <span class="tab-count">{tab.count}</span>
134
+ {/if}
135
+ </span>
136
+ <div class="active-indicator"></div>
137
+ </button>
138
+ {/each}
139
+ </div>
140
+
141
+ {#if children}
142
+ <div
143
+ class="tab-panel"
144
+ role="tabpanel"
145
+ id="tab-panel-{instanceId}"
146
+ aria-labelledby="tab-{instanceId}-{active}"
147
+ >
148
+ {@render children()}
149
+ </div>
150
+ {/if}
151
+ </div>
152
+
153
+ <style>
154
+ .tabs-container {
155
+ display: flex;
156
+ flex-direction: column;
157
+ width: 100%;
158
+ }
159
+
160
+ .tabs-nav {
161
+ display: flex;
162
+ border-bottom: 1px solid var(--smrt-color-surface-variant);
163
+ width: 100%;
164
+ }
165
+
166
+ .tab-button {
167
+ flex: 1;
168
+ display: inline-flex;
169
+ flex-direction: column;
170
+ align-items: center;
171
+ justify-content: center;
172
+ padding: 0 var(--smrt-spacing-4, 16px);
173
+ height: 48px;
174
+ font: var(--smrt-typography-title-small-font);
175
+ font-weight: var(--smrt-typography-weight-medium, 500);
176
+ color: var(--smrt-color-on-surface-variant);
177
+ background: transparent;
178
+ border: none;
179
+ cursor: pointer;
180
+ transition: all 200ms cubic-bezier(0.2, 0, 0, 1);
181
+ position: relative;
182
+ overflow: hidden;
183
+ min-width: 90px;
184
+ }
185
+
186
+ .sm .tab-button {
187
+ height: 40px;
188
+ font: var(--smrt-typography-label-large-font);
189
+ }
190
+
191
+ .lg .tab-button {
192
+ height: 56px;
193
+ font: var(--smrt-typography-title-medium-font);
194
+ }
195
+
196
+ .tab-button:hover:not(:disabled) {
197
+ background-color: var(--smrt-color-surface-container-high);
198
+ color: var(--smrt-color-on-surface);
199
+ }
200
+
201
+ .tab-button:focus-visible {
202
+ outline: 2px solid var(--smrt-color-primary);
203
+ outline-offset: -2px;
204
+ }
205
+
206
+ .tab-button.active {
207
+ color: var(--smrt-color-primary);
208
+ }
209
+
210
+ .tab-button:disabled {
211
+ opacity: 0.38;
212
+ cursor: not-allowed;
213
+ }
214
+
215
+ .tab-content-wrapper {
216
+ display: flex;
217
+ align-items: center;
218
+ gap: var(--smrt-spacing-2, 8px);
219
+ height: 100%;
220
+ }
221
+
222
+ .tab-count {
223
+ font: var(--smrt-typography-label-small-font);
224
+ opacity: 0.7;
225
+ }
226
+
227
+ .active-indicator {
228
+ position: absolute;
229
+ bottom: 0;
230
+ height: 3px;
231
+ background-color: var(--smrt-color-primary);
232
+ border-radius: var(--smrt-radius-sm, 4px) var(--smrt-radius-sm, 4px) 0 0;
233
+ transition: width 200ms, opacity 200ms;
234
+ width: 0;
235
+ opacity: 0;
236
+ }
237
+
238
+ .active .active-indicator {
239
+ width: 40px; /* Primary variant indicator width */
240
+ opacity: 1;
241
+ }
242
+
243
+ /* Secondary variant - full width indicator */
244
+ .secondary .active .active-indicator {
245
+ width: 100%;
246
+ height: 2px;
247
+ }
248
+
249
+ .tab-panel {
250
+ padding-top: 1.5rem;
251
+ }
252
+ </style>
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Tabs - Tab navigation component
3
+ * refactored for Material 3
4
+ *
5
+ * Provides tabbed navigation with optional counts and content slots.
6
+ *
7
+ * Accessibility:
8
+ * - Supports keyboard navigation with Arrow keys
9
+ * - Proper ARIA roles: tablist, tab, tabpanel
10
+ * - aria-selected indicates active tab
11
+ * - aria-controls links tab to panel
12
+ */
13
+ import type { Snippet } from 'svelte';
14
+ import type { Tab } from './types.js';
15
+ export interface Props {
16
+ /** Available tabs */
17
+ tabs: Tab[];
18
+ /** Currently active tab id */
19
+ active: string;
20
+ /** Called when tab changes */
21
+ onchange?: (id: string) => void;
22
+ /** Size variant */
23
+ size?: 'sm' | 'md' | 'lg';
24
+ /** Visual variant */
25
+ variant?: 'primary' | 'secondary';
26
+ /** Tab content slot */
27
+ children?: Snippet;
28
+ /** Accessible label for the tablist */
29
+ 'aria-label'?: string;
30
+ }
31
+ declare const Tabs: import("svelte").Component<Props, {}, "">;
32
+ type Tabs = ReturnType<typeof Tabs>;
33
+ export default Tabs;
34
+ //# sourceMappingURL=Tabs.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tabs.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/nav/Tabs.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAGtC,MAAM,WAAW,KAAK;IACpB,qBAAqB;IACrB,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,mBAAmB;IACnB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,qBAAqB;IACrB,OAAO,CAAC,EAAE,SAAS,GAAG,WAAW,CAAC;IAClC,uBAAuB;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAsGD,QAAA,MAAM,IAAI,2CAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Component test for FilterChips (Sweep S11, #1416).
3
+ *
4
+ * FilterChips is a single-select filter: a `role="radiogroup"` of
5
+ * `role="radio"` buttons where the selected option carries `aria-checked`.
6
+ * (There is no per-chip remove/dismiss affordance in the source — selection is
7
+ * the only interaction.) This suite asserts that structure/state, drives
8
+ * selection via click, and proves axe-cleanliness, including the optional
9
+ * "All" option.
10
+ */
11
+ import { render, screen, within } from '@testing-library/svelte';
12
+ import userEvent from '@testing-library/user-event';
13
+ import { describe, expect, it, vi } from 'vitest';
14
+ import { expectNoA11yViolations } from '../../../test-support/a11y';
15
+ import FilterChips from '../FilterChips.svelte';
16
+ const OPTIONS = [
17
+ { value: 'all-news', label: 'News', count: 12 },
18
+ { value: 'sports', label: 'Sports', count: 5 },
19
+ { value: 'tech', label: 'Tech' },
20
+ ];
21
+ describe('FilterChips', () => {
22
+ it('renders a radiogroup with one radio per option', () => {
23
+ render(FilterChips, { props: { options: OPTIONS, selected: 'all-news' } });
24
+ const group = screen.getByRole('radiogroup');
25
+ expect(within(group).getAllByRole('radio')).toHaveLength(3);
26
+ });
27
+ it('marks the selected option with aria-checked=true and others false', () => {
28
+ render(FilterChips, { props: { options: OPTIONS, selected: 'sports' } });
29
+ expect(screen.getByRole('radio', { name: /Sports/ })).toHaveAttribute('aria-checked', 'true');
30
+ expect(screen.getByRole('radio', { name: /News/ })).toHaveAttribute('aria-checked', 'false');
31
+ });
32
+ it('renders the optional count alongside its chip label', () => {
33
+ render(FilterChips, { props: { options: OPTIONS, selected: 'all-news' } });
34
+ expect(screen.getByRole('radio', { name: /News/ })).toHaveTextContent('12');
35
+ });
36
+ it('fires onchange with the clicked option value', async () => {
37
+ const onchange = vi.fn();
38
+ render(FilterChips, {
39
+ props: { options: OPTIONS, selected: 'all-news', onchange },
40
+ });
41
+ await userEvent.click(screen.getByRole('radio', { name: /Tech/ }));
42
+ expect(onchange).toHaveBeenCalledWith('tech');
43
+ });
44
+ it('does not fire onchange when the already-selected chip is clicked', async () => {
45
+ const onchange = vi.fn();
46
+ render(FilterChips, {
47
+ props: { options: OPTIONS, selected: 'sports', onchange },
48
+ });
49
+ await userEvent.click(screen.getByRole('radio', { name: /Sports/ }));
50
+ expect(onchange).not.toHaveBeenCalled();
51
+ });
52
+ it('does not fire onchange for a disabled option', async () => {
53
+ const onchange = vi.fn();
54
+ const options = [
55
+ { value: 'a', label: 'A' },
56
+ { value: 'b', label: 'B', disabled: true },
57
+ ];
58
+ render(FilterChips, { props: { options, selected: 'a', onchange } });
59
+ const disabled = screen.getByRole('radio', { name: 'B' });
60
+ expect(disabled).toBeDisabled();
61
+ await userEvent.click(disabled);
62
+ expect(onchange).not.toHaveBeenCalled();
63
+ });
64
+ it('prepends an "All" option (empty value) when showAll is set', async () => {
65
+ const onchange = vi.fn();
66
+ render(FilterChips, {
67
+ props: {
68
+ options: OPTIONS,
69
+ selected: 'sports',
70
+ showAll: true,
71
+ allLabel: 'Everything',
72
+ onchange,
73
+ },
74
+ });
75
+ const radios = screen.getAllByRole('radio');
76
+ expect(radios).toHaveLength(4);
77
+ const all = screen.getByRole('radio', { name: /Everything/ });
78
+ expect(all).toHaveAttribute('aria-checked', 'false');
79
+ await userEvent.click(all);
80
+ expect(onchange).toHaveBeenCalledWith('');
81
+ });
82
+ it('is axe-clean for a default selected group', async () => {
83
+ const { container } = render(FilterChips, {
84
+ props: { options: OPTIONS, selected: 'all-news' },
85
+ });
86
+ await expectNoA11yViolations(container);
87
+ });
88
+ it('is axe-clean with the "All" option enabled', async () => {
89
+ const { container } = render(FilterChips, {
90
+ props: { options: OPTIONS, selected: '', showAll: true },
91
+ });
92
+ await expectNoA11yViolations(container);
93
+ });
94
+ });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Component test for Tabs (Sweep S11, #1416).
3
+ *
4
+ * Tabs renders an ARIA tablist of `role="tab"` buttons with roving tabindex,
5
+ * `aria-selected` on the active tab, and a `role="tabpanel"` for children.
6
+ * This suite asserts that structure/state, drives click + Arrow/Home/End
7
+ * keyboard navigation (which fire `onchange`), and proves axe-cleanliness.
8
+ */
9
+ import { render, screen, within } from '@testing-library/svelte';
10
+ import userEvent from '@testing-library/user-event';
11
+ import { createRawSnippet } from 'svelte';
12
+ import { describe, expect, it, vi } from 'vitest';
13
+ import { expectNoA11yViolations } from '../../../test-support/a11y';
14
+ import Tabs from '../Tabs.svelte';
15
+ const TABS = [
16
+ { id: 'overview', label: 'Overview' },
17
+ { id: 'activity', label: 'Activity', count: 4 },
18
+ { id: 'settings', label: 'Settings' },
19
+ ];
20
+ /** Build a text Snippet for the Tabs `children` (panel) prop. */
21
+ function panelSnippet(text) {
22
+ return createRawSnippet(() => ({ render: () => `<span>${text}</span>` }));
23
+ }
24
+ describe('Tabs', () => {
25
+ it('renders a tablist with one tab button per tab', () => {
26
+ render(Tabs, {
27
+ props: { tabs: TABS, active: 'overview', 'aria-label': 'Sections' },
28
+ });
29
+ const tablist = screen.getByRole('tablist', { name: 'Sections' });
30
+ expect(within(tablist).getAllByRole('tab')).toHaveLength(3);
31
+ });
32
+ it('marks the active tab with aria-selected and a tabindex of 0', () => {
33
+ render(Tabs, { props: { tabs: TABS, active: 'activity' } });
34
+ const active = screen.getByRole('tab', { name: /Activity/ });
35
+ expect(active).toHaveAttribute('aria-selected', 'true');
36
+ expect(active).toHaveAttribute('tabindex', '0');
37
+ const inactive = screen.getByRole('tab', { name: 'Overview' });
38
+ expect(inactive).toHaveAttribute('aria-selected', 'false');
39
+ expect(inactive).toHaveAttribute('tabindex', '-1');
40
+ });
41
+ it('renders the optional count alongside its tab label', () => {
42
+ render(Tabs, { props: { tabs: TABS, active: 'overview' } });
43
+ expect(screen.getByRole('tab', { name: /Activity/ })).toHaveTextContent('4');
44
+ });
45
+ it('renders children inside a tabpanel labelled by the active tab', () => {
46
+ render(Tabs, {
47
+ props: {
48
+ tabs: TABS,
49
+ active: 'overview',
50
+ children: panelSnippet('Overview content'),
51
+ },
52
+ });
53
+ const panel = screen.getByRole('tabpanel');
54
+ expect(within(panel).getByText('Overview content')).toBeInTheDocument();
55
+ });
56
+ it('fires onchange with the clicked tab id', async () => {
57
+ const onchange = vi.fn();
58
+ render(Tabs, { props: { tabs: TABS, active: 'overview', onchange } });
59
+ await userEvent.click(screen.getByRole('tab', { name: 'Settings' }));
60
+ expect(onchange).toHaveBeenCalledWith('settings');
61
+ });
62
+ it('does not fire onchange when the already-active tab is clicked', async () => {
63
+ const onchange = vi.fn();
64
+ render(Tabs, { props: { tabs: TABS, active: 'overview', onchange } });
65
+ await userEvent.click(screen.getByRole('tab', { name: 'Overview' }));
66
+ expect(onchange).not.toHaveBeenCalled();
67
+ });
68
+ // ArrowRight/ArrowLeft are asserted in separate renders rather than chained:
69
+ // the component moves focus to the newly-activated tab, so a second arrow key
70
+ // in the same render would target whichever tab focus landed on — racy across
71
+ // suite ordering. Each test focuses one known tab and asserts a single hop.
72
+ it('moves to the next tab with ArrowRight', async () => {
73
+ const onchange = vi.fn();
74
+ render(Tabs, { props: { tabs: TABS, active: 'overview', onchange } });
75
+ screen.getByRole('tab', { name: 'Overview' }).focus();
76
+ await userEvent.keyboard('{ArrowRight}');
77
+ expect(onchange).toHaveBeenLastCalledWith('activity');
78
+ });
79
+ it('moves to the previous tab with ArrowLeft', async () => {
80
+ const onchange = vi.fn();
81
+ // Render with Activity active so it owns the roving tabindex and is focusable.
82
+ // Activity carries a count badge, so its accessible name is "Activity 4".
83
+ render(Tabs, { props: { tabs: TABS, active: 'activity', onchange } });
84
+ screen.getByRole('tab', { name: /Activity/ }).focus();
85
+ await userEvent.keyboard('{ArrowLeft}');
86
+ expect(onchange).toHaveBeenLastCalledWith('overview');
87
+ });
88
+ it('wraps from the first tab to the last with ArrowLeft', async () => {
89
+ const onchange = vi.fn();
90
+ render(Tabs, { props: { tabs: TABS, active: 'overview', onchange } });
91
+ screen.getByRole('tab', { name: 'Overview' }).focus();
92
+ await userEvent.keyboard('{ArrowLeft}');
93
+ expect(onchange).toHaveBeenLastCalledWith('settings');
94
+ });
95
+ it('jumps to the first/last tab with Home/End', async () => {
96
+ const onchange = vi.fn();
97
+ render(Tabs, { props: { tabs: TABS, active: 'activity', onchange } });
98
+ screen.getByRole('tab', { name: /Activity/ }).focus();
99
+ await userEvent.keyboard('{End}');
100
+ expect(onchange).toHaveBeenLastCalledWith('settings');
101
+ await userEvent.keyboard('{Home}');
102
+ expect(onchange).toHaveBeenLastCalledWith('overview');
103
+ });
104
+ it('skips disabled tabs during keyboard navigation', async () => {
105
+ const onchange = vi.fn();
106
+ const tabs = [
107
+ { id: 'a', label: 'A' },
108
+ { id: 'b', label: 'B', disabled: true },
109
+ { id: 'c', label: 'C' },
110
+ ];
111
+ render(Tabs, { props: { tabs, active: 'a', onchange } });
112
+ expect(screen.getByRole('tab', { name: 'B' })).toBeDisabled();
113
+ screen.getByRole('tab', { name: 'A' }).focus();
114
+ await userEvent.keyboard('{ArrowRight}');
115
+ expect(onchange).toHaveBeenLastCalledWith('c');
116
+ });
117
+ it('is axe-clean with a labelled tablist and panel', async () => {
118
+ const { container } = render(Tabs, {
119
+ props: {
120
+ tabs: TABS,
121
+ active: 'overview',
122
+ 'aria-label': 'Sections',
123
+ children: panelSnippet('Panel content'),
124
+ },
125
+ });
126
+ await expectNoA11yViolations(container);
127
+ });
128
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Navigation components
3
+ */
4
+ export { default as FilterChips } from './FilterChips.svelte';
5
+ export { default as Tabs } from './Tabs.svelte';
6
+ export type { FilterOption, Tab } from './types.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/nav/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;AAEhD,YAAY,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Navigation components
3
+ */
4
+ // Export components
5
+ export { default as FilterChips } from './FilterChips.svelte';
6
+ export { default as Tabs } from './Tabs.svelte';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Navigation component types
3
+ */
4
+ export interface FilterOption {
5
+ /** Option value */
6
+ value: string;
7
+ /** Display label */
8
+ label: string;
9
+ /** Optional count badge */
10
+ count?: number;
11
+ /** Disabled state */
12
+ disabled?: boolean;
13
+ }
14
+ export interface Tab {
15
+ /** Tab identifier */
16
+ id: string;
17
+ /** Display label */
18
+ label: string;
19
+ /** Optional count badge */
20
+ count?: number;
21
+ /** Disabled state */
22
+ disabled?: boolean;
23
+ }
24
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/nav/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,YAAY;IAC3B,mBAAmB;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,GAAG;IAClB,qBAAqB;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Navigation component types
3
+ */
4
+ export {};
@@ -0,0 +1,45 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ interface Props {
5
+ /** Single permission to check */
6
+ permission?: string;
7
+ /** Multiple permissions to check */
8
+ permissions?: string[];
9
+ /** User's current permissions */
10
+ userPermissions: string[];
11
+ /** 'all' requires all permissions, 'any' requires at least one */
12
+ mode?: 'all' | 'any';
13
+ /** Content to show when permission check passes */
14
+ children: Snippet;
15
+ /** Optional fallback content when check fails */
16
+ fallback?: Snippet;
17
+ }
18
+
19
+ const {
20
+ permission,
21
+ permissions = [],
22
+ userPermissions,
23
+ mode = 'all',
24
+ children,
25
+ fallback,
26
+ }: Props = $props();
27
+
28
+ const requiredPermissions = $derived(permission ? [permission] : permissions);
29
+
30
+ const hasAccess = $derived(() => {
31
+ if (requiredPermissions.length === 0) return true;
32
+
33
+ if (mode === 'any') {
34
+ return requiredPermissions.some((p) => userPermissions.includes(p));
35
+ }
36
+
37
+ return requiredPermissions.every((p) => userPermissions.includes(p));
38
+ });
39
+ </script>
40
+
41
+ {#if hasAccess()}
42
+ {@render children()}
43
+ {:else if fallback}
44
+ {@render fallback()}
45
+ {/if}
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ /** Single permission to check */
4
+ permission?: string;
5
+ /** Multiple permissions to check */
6
+ permissions?: string[];
7
+ /** User's current permissions */
8
+ userPermissions: string[];
9
+ /** 'all' requires all permissions, 'any' requires at least one */
10
+ mode?: 'all' | 'any';
11
+ /** Content to show when permission check passes */
12
+ children: Snippet;
13
+ /** Optional fallback content when check fails */
14
+ fallback?: Snippet;
15
+ }
16
+ declare const PermissionCheck: import("svelte").Component<Props, {}, "">;
17
+ type PermissionCheck = ReturnType<typeof PermissionCheck>;
18
+ export default PermissionCheck;
19
+ //# sourceMappingURL=PermissionCheck.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PermissionCheck.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/permissions/PermissionCheck.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGtC,UAAU,KAAK;IACb,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,iCAAiC;IACjC,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,kEAAkE;IAClE,IAAI,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACrB,mDAAmD;IACnD,QAAQ,EAAE,OAAO,CAAC;IAClB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAmCD,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}