@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,250 @@
1
+ <script module lang="ts">
2
+ let menuIdCounter = 0;
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * Dropdown / Menu — a trigger button and a positioned menu list.
8
+ *
9
+ * Implements the WAI-ARIA menu-button pattern: trigger has
10
+ * `aria-haspopup="menu"` + `aria-expanded`; the list is `role="menu"` with
11
+ * `role="menuitem"` buttons under roving focus (ArrowUp/Down/Home/End), Enter/
12
+ * Space to activate, Escape to close + refocus the trigger, and click-outside to
13
+ * dismiss. CSS-anchored via `placement` — no JS positioning dependency.
14
+ */
15
+ import { tick } from 'svelte';
16
+ import type { Snippet } from 'svelte';
17
+ import type { DropdownPlacement, MenuItem } from '../../types-generic';
18
+
19
+ export interface Props {
20
+ /** Trigger button text (ignored when a `trigger` snippet is given). */
21
+ label?: string;
22
+ /** Custom trigger content. */
23
+ trigger?: Snippet;
24
+ /** Menu items. */
25
+ items: MenuItem[];
26
+ /** Menu placement relative to the trigger. */
27
+ placement?: DropdownPlacement;
28
+ /** Fired with the activated item's id. */
29
+ onselect?: (id: string) => void;
30
+ /** Disable the trigger. */
31
+ disabled?: boolean;
32
+ }
33
+
34
+ const {
35
+ label,
36
+ trigger,
37
+ items,
38
+ placement = 'bottom-start',
39
+ onselect,
40
+ disabled = false,
41
+ }: Props = $props();
42
+
43
+ const menuId = `smrt-menu-${(menuIdCounter += 1)}`;
44
+ let open = $state(false);
45
+ let wrapEl = $state<HTMLElement | null>(null);
46
+ let triggerEl = $state<HTMLButtonElement | null>(null);
47
+ let itemEls = $state<Array<HTMLButtonElement | null>>([]);
48
+
49
+ const enabledIndexes = $derived(
50
+ items.map((it, i) => (it.disabled ? -1 : i)).filter((i) => i >= 0),
51
+ );
52
+
53
+ async function openMenu(focusLast = false) {
54
+ open = true;
55
+ await tick();
56
+ const target = focusLast
57
+ ? enabledIndexes[enabledIndexes.length - 1]
58
+ : enabledIndexes[0];
59
+ if (target != null) itemEls[target]?.focus();
60
+ }
61
+
62
+ function closeMenu(refocus = true) {
63
+ open = false;
64
+ if (refocus) triggerEl?.focus();
65
+ }
66
+
67
+ function selectItem(item: MenuItem) {
68
+ if (item.disabled) return;
69
+ item.onselect?.();
70
+ onselect?.(item.id);
71
+ closeMenu();
72
+ }
73
+
74
+ function onTriggerKeydown(event: KeyboardEvent) {
75
+ if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
76
+ event.preventDefault();
77
+ openMenu();
78
+ } else if (event.key === 'ArrowUp') {
79
+ event.preventDefault();
80
+ openMenu(true);
81
+ }
82
+ }
83
+
84
+ function focusByOffset(offset: number) {
85
+ const order = enabledIndexes;
86
+ if (order.length === 0) return;
87
+ const current = itemEls.findIndex((el) => el === document.activeElement);
88
+ const pos = order.indexOf(current);
89
+ const next = order[(pos + offset + order.length) % order.length];
90
+ itemEls[next]?.focus();
91
+ }
92
+
93
+ function onMenuKeydown(event: KeyboardEvent) {
94
+ switch (event.key) {
95
+ case 'ArrowDown':
96
+ event.preventDefault();
97
+ focusByOffset(1);
98
+ break;
99
+ case 'ArrowUp':
100
+ event.preventDefault();
101
+ focusByOffset(-1);
102
+ break;
103
+ case 'Home':
104
+ event.preventDefault();
105
+ itemEls[enabledIndexes[0]]?.focus();
106
+ break;
107
+ case 'End':
108
+ event.preventDefault();
109
+ itemEls[enabledIndexes[enabledIndexes.length - 1]]?.focus();
110
+ break;
111
+ case 'Escape':
112
+ event.preventDefault();
113
+ closeMenu();
114
+ break;
115
+ case 'Tab':
116
+ closeMenu(false);
117
+ break;
118
+ }
119
+ }
120
+
121
+ // Dismiss on outside click while open.
122
+ $effect(() => {
123
+ if (!open) return;
124
+ const onDocPointer = (event: MouseEvent) => {
125
+ if (wrapEl && !wrapEl.contains(event.target as Node)) closeMenu(false);
126
+ };
127
+ document.addEventListener('click', onDocPointer, true);
128
+ return () => document.removeEventListener('click', onDocPointer, true);
129
+ });
130
+ </script>
131
+
132
+ <span class="dropdown" bind:this={wrapEl}>
133
+ <button
134
+ bind:this={triggerEl}
135
+ type="button"
136
+ class="dropdown__trigger"
137
+ aria-haspopup="menu"
138
+ aria-expanded={open}
139
+ aria-controls={open ? menuId : undefined}
140
+ {disabled}
141
+ onclick={() => (open ? closeMenu(false) : openMenu())}
142
+ onkeydown={onTriggerKeydown}
143
+ >
144
+ {#if trigger}{@render trigger()}{:else}{label}{/if}
145
+ </button>
146
+
147
+ {#if open}
148
+ <div
149
+ id={menuId}
150
+ role="menu"
151
+ tabindex={-1}
152
+ class="dropdown__menu dropdown__menu--{placement}"
153
+ onkeydown={onMenuKeydown}
154
+ >
155
+ {#each items as item, i (item.id)}
156
+ <button
157
+ bind:this={itemEls[i]}
158
+ type="button"
159
+ role="menuitem"
160
+ class="dropdown__item"
161
+ disabled={item.disabled}
162
+ tabindex={-1}
163
+ onclick={() => selectItem(item)}
164
+ >
165
+ {item.label}
166
+ </button>
167
+ {/each}
168
+ </div>
169
+ {/if}
170
+ </span>
171
+
172
+ <style>
173
+ .dropdown {
174
+ position: relative;
175
+ display: inline-flex;
176
+ }
177
+
178
+ .dropdown__trigger {
179
+ appearance: none;
180
+ font: inherit;
181
+ cursor: pointer;
182
+ padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
183
+ border-radius: var(--smrt-radius-medium, 8px);
184
+ border: 1px solid var(--smrt-color-outline-variant, #cac4d0);
185
+ background: var(--smrt-color-surface);
186
+ color: var(--smrt-color-on-surface);
187
+ }
188
+ .dropdown__trigger:focus-visible {
189
+ outline: 2px solid var(--smrt-color-primary);
190
+ outline-offset: 2px;
191
+ }
192
+ .dropdown__trigger:disabled {
193
+ opacity: 0.5;
194
+ cursor: not-allowed;
195
+ }
196
+
197
+ .dropdown__menu {
198
+ position: absolute;
199
+ z-index: var(--smrt-z-index-dropdown, 1000);
200
+ min-width: 10rem;
201
+ margin: var(--smrt-spacing-1, 4px) 0;
202
+ padding: var(--smrt-spacing-1, 4px);
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: var(--smrt-spacing-1, 4px);
206
+ border-radius: var(--smrt-radius-medium, 8px);
207
+ background: var(--smrt-color-surface-container, #f3edf7);
208
+ border: 1px solid var(--smrt-color-outline-variant, #cac4d0);
209
+ box-shadow: var(--smrt-elevation-2, 0 2px 6px rgba(0, 0, 0, 0.15));
210
+ }
211
+
212
+ .dropdown__menu--bottom-start {
213
+ top: 100%;
214
+ left: 0;
215
+ }
216
+ .dropdown__menu--bottom-end {
217
+ top: 100%;
218
+ right: 0;
219
+ }
220
+ .dropdown__menu--top-start {
221
+ bottom: 100%;
222
+ left: 0;
223
+ }
224
+ .dropdown__menu--top-end {
225
+ bottom: 100%;
226
+ right: 0;
227
+ }
228
+
229
+ .dropdown__item {
230
+ appearance: none;
231
+ font: inherit;
232
+ text-align: left;
233
+ cursor: pointer;
234
+ padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
235
+ border: none;
236
+ border-radius: var(--smrt-radius-small, 4px);
237
+ background: none;
238
+ color: var(--smrt-color-on-surface);
239
+ white-space: nowrap;
240
+ }
241
+ .dropdown__item:hover:not(:disabled),
242
+ .dropdown__item:focus-visible {
243
+ background: var(--smrt-color-surface-container-high);
244
+ outline: none;
245
+ }
246
+ .dropdown__item:disabled {
247
+ opacity: 0.5;
248
+ cursor: not-allowed;
249
+ }
250
+ </style>
@@ -0,0 +1,20 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { DropdownPlacement, MenuItem } from '../../types-generic';
3
+ export interface Props {
4
+ /** Trigger button text (ignored when a `trigger` snippet is given). */
5
+ label?: string;
6
+ /** Custom trigger content. */
7
+ trigger?: Snippet;
8
+ /** Menu items. */
9
+ items: MenuItem[];
10
+ /** Menu placement relative to the trigger. */
11
+ placement?: DropdownPlacement;
12
+ /** Fired with the activated item's id. */
13
+ onselect?: (id: string) => void;
14
+ /** Disable the trigger. */
15
+ disabled?: boolean;
16
+ }
17
+ declare const Dropdown: import("svelte").Component<Props, {}, "">;
18
+ type Dropdown = ReturnType<typeof Dropdown>;
19
+ export default Dropdown;
20
+ //# sourceMappingURL=Dropdown.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/ui/Dropdown.svelte.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGvE,MAAM,WAAW,KAAK;IACpB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kBAAkB;IAClB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAgID,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,294 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Pagination - Page navigation component
4
+ *
5
+ * Provides page navigation with first/prev/next/last controls.
6
+ * Supports both link-based (SSR) and callback-based (client-side) modes.
7
+ *
8
+ * Accessibility:
9
+ * - Proper ARIA labels on all controls
10
+ * - aria-current marks current page
11
+ * - Native list semantics for numbered pages
12
+ */
13
+
14
+ import { M } from '../../i18n/strings.ui.js';
15
+ import { useI18n } from '../../i18n/use-i18n.js';
16
+
17
+ const { t } = useI18n();
18
+
19
+ /** Props for Pagination component */
20
+ export interface Props {
21
+ /** Current active page (1-based) */
22
+ currentPage: number;
23
+ /** Total number of pages */
24
+ totalPages: number;
25
+ /** Base URL for link mode (e.g., '/articles') */
26
+ baseUrl?: string;
27
+ /** When provided, renders buttons instead of links and calls this on page change */
28
+ onPageChange?: (page: number) => void;
29
+ /** Accessible label for pagination nav */
30
+ 'aria-label'?: string;
31
+ /** Show first/last page buttons */
32
+ showFirstLast?: boolean;
33
+ /** Maximum number of visible page numbers */
34
+ maxVisible?: number;
35
+ }
36
+
37
+ const {
38
+ currentPage,
39
+ totalPages,
40
+ baseUrl = '/articles',
41
+ onPageChange,
42
+ 'aria-label': ariaLabel = 'Pagination',
43
+ showFirstLast = true,
44
+ maxVisible = 5,
45
+ }: Props = $props();
46
+
47
+ function getPageUrl(page: number): string {
48
+ if (page === 1) return baseUrl || '/';
49
+ return `${baseUrl}/page/${page}`;
50
+ }
51
+
52
+ // Generate array of page numbers to display
53
+ function getPageNumbers(): (number | 'ellipsis')[] {
54
+ const pages: (number | 'ellipsis')[] = [];
55
+
56
+ if (totalPages <= maxVisible + 2) {
57
+ // Show all pages if there aren't many
58
+ for (let i = 1; i <= totalPages; i++) {
59
+ pages.push(i);
60
+ }
61
+ } else {
62
+ // Always show first page
63
+ pages.push(1);
64
+
65
+ // Calculate range around current page
66
+ let start = Math.max(2, currentPage - 1);
67
+ let end = Math.min(totalPages - 1, currentPage + 1);
68
+
69
+ // Adjust if at the start
70
+ if (currentPage <= 3) {
71
+ end = Math.min(totalPages - 1, maxVisible - 1);
72
+ }
73
+
74
+ // Adjust if at the end
75
+ if (currentPage >= totalPages - 2) {
76
+ start = Math.max(2, totalPages - maxVisible + 2);
77
+ }
78
+
79
+ // Add ellipsis before middle section if needed
80
+ if (start > 2) {
81
+ pages.push('ellipsis');
82
+ }
83
+
84
+ // Add middle pages
85
+ for (let i = start; i <= end; i++) {
86
+ pages.push(i);
87
+ }
88
+
89
+ // Add ellipsis after middle section if needed
90
+ if (end < totalPages - 1) {
91
+ pages.push('ellipsis');
92
+ }
93
+
94
+ // Always show last page
95
+ pages.push(totalPages);
96
+ }
97
+
98
+ return pages;
99
+ }
100
+
101
+ const pageNumbers = $derived(getPageNumbers());
102
+ const hasPrevPage = $derived(currentPage > 1);
103
+ const hasNextPage = $derived(currentPage < totalPages);
104
+ const isFirstPage = $derived(currentPage === 1);
105
+ const isLastPage = $derived(currentPage === totalPages);
106
+ </script>
107
+
108
+ {#if totalPages > 1}
109
+ <nav class="pagination" aria-label={ariaLabel}>
110
+ <!-- First page -->
111
+ {#if showFirstLast}
112
+ {#if onPageChange}
113
+ <button class="page-link nav-link first" type="button" disabled={isFirstPage} onclick={() => onPageChange(1)} aria-label={t(M['ui.pagination.first_page'])}>
114
+ &laquo; First
115
+ </button>
116
+ {:else if !isFirstPage}
117
+ <a href={getPageUrl(1)} class="page-link nav-link first" aria-label={t(M['ui.pagination.first_page'])}>
118
+ &laquo; First
119
+ </a>
120
+ {:else}
121
+ <span class="page-link nav-link disabled first" aria-hidden="true">&laquo; First</span>
122
+ {/if}
123
+ {/if}
124
+
125
+ <!-- Previous page -->
126
+ {#if onPageChange}
127
+ <button class="page-link nav-link" type="button" disabled={!hasPrevPage} onclick={() => onPageChange(currentPage - 1)} aria-label={t(M['ui.pagination.previous_page'])}>
128
+ &larr; Prev
129
+ </button>
130
+ {:else if hasPrevPage}
131
+ <a href={getPageUrl(currentPage - 1)} class="page-link nav-link" aria-label={t(M['ui.pagination.previous_page'])}>
132
+ &larr; Prev
133
+ </a>
134
+ {:else}
135
+ <span class="page-link nav-link disabled" aria-hidden="true">&larr; Prev</span>
136
+ {/if}
137
+
138
+ <ul class="page-numbers">
139
+ {#each pageNumbers as page}
140
+ {#if page === 'ellipsis'}
141
+ <li class="page-numbers__item">
142
+ <span class="ellipsis" aria-hidden="true">&hellip;</span>
143
+ </li>
144
+ {:else if page === currentPage}
145
+ <li class="page-numbers__item">
146
+ <span class="page-link current" aria-current="page" aria-label={t(M['ui.pagination.page_current'], { page })}>{page}</span>
147
+ </li>
148
+ {:else if onPageChange}
149
+ <li class="page-numbers__item">
150
+ <button class="page-link" type="button" onclick={() => onPageChange(page as number)} aria-label={t(M['ui.pagination.go_to_page'], { page })}>{page}</button>
151
+ </li>
152
+ {:else}
153
+ <li class="page-numbers__item">
154
+ <a href={getPageUrl(page)} class="page-link" aria-label={t(M['ui.pagination.go_to_page'], { page })}>{page}</a>
155
+ </li>
156
+ {/if}
157
+ {/each}
158
+ </ul>
159
+
160
+ <!-- Next page -->
161
+ {#if onPageChange}
162
+ <button class="page-link nav-link" type="button" disabled={!hasNextPage} onclick={() => onPageChange(currentPage + 1)} aria-label={t(M['ui.pagination.next_page'])}>
163
+ Next &rarr;
164
+ </button>
165
+ {:else if hasNextPage}
166
+ <a href={getPageUrl(currentPage + 1)} class="page-link nav-link" aria-label={t(M['ui.pagination.next_page'])}>
167
+ Next &rarr;
168
+ </a>
169
+ {:else}
170
+ <span class="page-link nav-link disabled" aria-hidden="true">Next &rarr;</span>
171
+ {/if}
172
+
173
+ <!-- Last page -->
174
+ {#if showFirstLast}
175
+ {#if onPageChange}
176
+ <button class="page-link nav-link last" type="button" disabled={isLastPage} onclick={() => onPageChange(totalPages)} aria-label={t(M['ui.pagination.last_page'], { totalPages })}>
177
+ Last &raquo;
178
+ </button>
179
+ {:else if !isLastPage}
180
+ <a href={getPageUrl(totalPages)} class="page-link nav-link last" aria-label={t(M['ui.pagination.last_page'], { totalPages })}>
181
+ Last &raquo;
182
+ </a>
183
+ {:else}
184
+ <span class="page-link nav-link disabled last" aria-hidden="true">Last &raquo;</span>
185
+ {/if}
186
+ {/if}
187
+ </nav>
188
+ {/if}
189
+
190
+ <style>
191
+ .pagination {
192
+ display: flex;
193
+ justify-content: center;
194
+ align-items: center;
195
+ gap: var(--smrt-spacing-2, 0.5rem);
196
+ margin-top: var(--smrt-spacing-8, 2rem);
197
+ padding: var(--smrt-spacing-4, 1rem) 0;
198
+ flex-wrap: wrap;
199
+ }
200
+
201
+ .page-numbers {
202
+ list-style: none;
203
+ display: flex;
204
+ align-items: center;
205
+ gap: var(--smrt-spacing-1, 0.25rem);
206
+ margin: 0;
207
+ padding: 0;
208
+ }
209
+
210
+ .page-numbers__item {
211
+ margin: 0;
212
+ padding: 0;
213
+ }
214
+
215
+ .page-link {
216
+ display: inline-flex;
217
+ align-items: center;
218
+ justify-content: center;
219
+ min-width: 2.25rem;
220
+ height: 2.25rem;
221
+ padding: 0 var(--smrt-spacing-2, 0.5rem);
222
+ border-radius: var(--smrt-radius-small, 0.25rem);
223
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
224
+ font-weight: var(--smrt-typography-weight-medium, 500);
225
+ text-decoration: none;
226
+ transition: background-color var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease), color var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease);
227
+ }
228
+
229
+ .page-link:not(.current):not(.disabled) {
230
+ background: var(--smrt-color-surface-container-high, #f3f4f6);
231
+ color: var(--smrt-color-on-surface, #374151);
232
+ }
233
+
234
+ .page-link:not(.current):not(.disabled):hover {
235
+ background: var(--smrt-color-primary, #005ac1);
236
+ color: var(--smrt-color-on-primary, white);
237
+ }
238
+
239
+ .page-link.current {
240
+ background: var(--smrt-color-primary, #005ac1);
241
+ color: white;
242
+ cursor: default;
243
+ }
244
+
245
+ .page-link.disabled,
246
+ .page-link:disabled {
247
+ background: var(--smrt-color-surface-container-high, #f3f4f6);
248
+ color: var(--smrt-color-on-surface-variant, #9ca3af);
249
+ cursor: not-allowed;
250
+ }
251
+
252
+ button.page-link {
253
+ border: none;
254
+ font: inherit;
255
+ cursor: pointer;
256
+ }
257
+
258
+ .nav-link {
259
+ padding: 0 var(--smrt-spacing-4, 1rem);
260
+ }
261
+
262
+ .nav-link.first,
263
+ .nav-link.last {
264
+ display: none;
265
+ }
266
+
267
+ @media (min-width: 640px) {
268
+ .nav-link.first,
269
+ .nav-link.last {
270
+ display: inline-flex;
271
+ }
272
+ }
273
+
274
+ .ellipsis {
275
+ display: inline-flex;
276
+ align-items: center;
277
+ justify-content: center;
278
+ min-width: 2.25rem;
279
+ height: 2.25rem;
280
+ color: var(--smrt-color-on-surface-variant, #6b7280);
281
+ }
282
+
283
+ @media (max-width: 480px) {
284
+ .page-link {
285
+ min-width: 2rem;
286
+ height: 2rem;
287
+ font-size: var(--smrt-typography-body-medium-size, 0.8125rem);
288
+ }
289
+
290
+ .nav-link {
291
+ padding: 0 var(--smrt-spacing-2, 0.5rem);
292
+ }
293
+ }
294
+ </style>
@@ -0,0 +1,21 @@
1
+ /** Props for Pagination component */
2
+ export interface Props {
3
+ /** Current active page (1-based) */
4
+ currentPage: number;
5
+ /** Total number of pages */
6
+ totalPages: number;
7
+ /** Base URL for link mode (e.g., '/articles') */
8
+ baseUrl?: string;
9
+ /** When provided, renders buttons instead of links and calls this on page change */
10
+ onPageChange?: (page: number) => void;
11
+ /** Accessible label for pagination nav */
12
+ 'aria-label'?: string;
13
+ /** Show first/last page buttons */
14
+ showFirstLast?: boolean;
15
+ /** Maximum number of visible page numbers */
16
+ maxVisible?: number;
17
+ }
18
+ declare const Pagination: import("svelte").Component<Props, {}, "">;
19
+ type Pagination = ReturnType<typeof Pagination>;
20
+ export default Pagination;
21
+ //# sourceMappingURL=Pagination.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Pagination.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/ui/Pagination.svelte.ts"],"names":[],"mappings":"AAkBA,qCAAqC;AACrC,MAAM,WAAW,KAAK;IACpB,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mCAAmC;IACnC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAuKD,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,113 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Skeleton — shape-matching loading placeholder.
4
+ *
5
+ * Variants: `text` (one or more lines), `circle`, `rect`. The wrapper is a
6
+ * polite `role="status"` region with an sr-only label so assistive tech
7
+ * announces that content is loading; the shimmer honors `prefers-reduced-motion`.
8
+ */
9
+ export type SkeletonVariant = 'text' | 'circle' | 'rect';
10
+
11
+ export interface Props {
12
+ /** Placeholder shape. */
13
+ variant?: SkeletonVariant;
14
+ /** CSS width (e.g. '100%', '8rem'). */
15
+ width?: string;
16
+ /** CSS height (e.g. '1rem', '48px'). */
17
+ height?: string;
18
+ /** Number of lines for the `text` variant. */
19
+ lines?: number;
20
+ /** Accessible loading label. */
21
+ label?: string;
22
+ }
23
+
24
+ const {
25
+ variant = 'text',
26
+ width,
27
+ height,
28
+ lines = 1,
29
+ label = 'Loading…',
30
+ }: Props = $props();
31
+
32
+ const lineIndexes = $derived(
33
+ variant === 'text'
34
+ ? Array.from({ length: Math.max(1, lines) }, (_, i) => i)
35
+ : [0],
36
+ );
37
+ </script>
38
+
39
+ <span class="skeleton-wrap" role="status" aria-label={label}>
40
+ {#if variant === 'text'}
41
+ {#each lineIndexes as i (i)}
42
+ <span
43
+ class="skeleton skeleton--text"
44
+ style:width={width}
45
+ style:height={height}
46
+ aria-hidden="true"
47
+ ></span>
48
+ {/each}
49
+ {:else}
50
+ <span
51
+ class="skeleton skeleton--{variant}"
52
+ style:width={width}
53
+ style:height={height}
54
+ aria-hidden="true"
55
+ ></span>
56
+ {/if}
57
+ </span>
58
+
59
+ <style>
60
+ .skeleton-wrap {
61
+ display: inline-flex;
62
+ flex-direction: column;
63
+ gap: var(--smrt-spacing-2, 8px);
64
+ width: 100%;
65
+ }
66
+
67
+ .skeleton {
68
+ display: block;
69
+ background: var(--smrt-color-surface-container-highest, #e7e0ec);
70
+ background-image: linear-gradient(
71
+ 90deg,
72
+ transparent 0%,
73
+ color-mix(in srgb, var(--smrt-color-surface) 60%, transparent) 50%,
74
+ transparent 100%
75
+ );
76
+ background-size: 200% 100%;
77
+ animation: smrt-skeleton-shimmer 1.4s ease-in-out infinite;
78
+ border-radius: var(--smrt-radius-small, 4px);
79
+ }
80
+
81
+ .skeleton--text {
82
+ width: 100%;
83
+ height: var(--smrt-typography-body-medium-size, 1rem);
84
+ border-radius: var(--smrt-radius-extra-small, 4px);
85
+ }
86
+
87
+ .skeleton--circle {
88
+ width: 48px;
89
+ height: 48px;
90
+ border-radius: var(--smrt-radius-full, 9999px);
91
+ }
92
+
93
+ .skeleton--rect {
94
+ width: 100%;
95
+ height: 96px;
96
+ border-radius: var(--smrt-radius-medium, 8px);
97
+ }
98
+
99
+ @keyframes smrt-skeleton-shimmer {
100
+ from {
101
+ background-position: 200% 0;
102
+ }
103
+ to {
104
+ background-position: -200% 0;
105
+ }
106
+ }
107
+
108
+ @media (prefers-reduced-motion: reduce) {
109
+ .skeleton {
110
+ animation: none;
111
+ }
112
+ }
113
+ </style>