@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,24 @@
1
+ /**
2
+ * Skeleton — shape-matching loading placeholder.
3
+ *
4
+ * Variants: `text` (one or more lines), `circle`, `rect`. The wrapper is a
5
+ * polite `role="status"` region with an sr-only label so assistive tech
6
+ * announces that content is loading; the shimmer honors `prefers-reduced-motion`.
7
+ */
8
+ export type SkeletonVariant = 'text' | 'circle' | 'rect';
9
+ export interface Props {
10
+ /** Placeholder shape. */
11
+ variant?: SkeletonVariant;
12
+ /** CSS width (e.g. '100%', '8rem'). */
13
+ width?: string;
14
+ /** CSS height (e.g. '1rem', '48px'). */
15
+ height?: string;
16
+ /** Number of lines for the `text` variant. */
17
+ lines?: number;
18
+ /** Accessible loading label. */
19
+ label?: string;
20
+ }
21
+ declare const Skeleton: import("svelte").Component<Props, {}, "">;
22
+ type Skeleton = ReturnType<typeof Skeleton>;
23
+ export default Skeleton;
24
+ //# sourceMappingURL=Skeleton.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Skeleton.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/ui/Skeleton.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEzD,MAAM,WAAW,KAAK;IACpB,yBAAyB;IACzB,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAkCD,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,120 @@
1
+ <script module lang="ts">
2
+ let tooltipIdCounter = 0;
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * Tooltip — anchored, delayed text bubble for a trigger.
8
+ *
9
+ * Shows on hover and keyboard focus, hides on leave / blur / Escape. The trigger
10
+ * is wired to the bubble via `aria-describedby` (always set; the bubble is in the
11
+ * DOM and revealed on open), and the bubble carries `role="tooltip"`. CSS-anchored
12
+ * via a `placement` prop — no JS positioning dependency.
13
+ */
14
+ import type { Snippet } from 'svelte';
15
+ import type { TooltipPlacement } from '../../types-generic';
16
+
17
+ export interface Props {
18
+ /** Tooltip text. */
19
+ text: string;
20
+ /** Placement relative to the trigger. */
21
+ placement?: TooltipPlacement;
22
+ /** Show delay in ms. */
23
+ delay?: number;
24
+ /** The trigger content. */
25
+ children: Snippet;
26
+ }
27
+
28
+ const { text, placement = 'top', delay = 400, children }: Props = $props();
29
+
30
+ const tipId = `smrt-tooltip-${(tooltipIdCounter += 1)}`;
31
+ let open = $state(false);
32
+ let timer: ReturnType<typeof setTimeout> | null = null;
33
+
34
+ function show() {
35
+ if (timer) clearTimeout(timer);
36
+ timer = setTimeout(() => {
37
+ open = true;
38
+ }, delay);
39
+ }
40
+
41
+ function hide() {
42
+ if (timer) clearTimeout(timer);
43
+ timer = null;
44
+ open = false;
45
+ }
46
+
47
+ function onKeydown(event: KeyboardEvent) {
48
+ if (event.key === 'Escape') hide();
49
+ }
50
+ </script>
51
+
52
+ <!-- Presentational wrapper: it only listens for hover/focus/Escape to toggle the
53
+ bubble; the real interactive element is the consumer's trigger child. -->
54
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
55
+ <span
56
+ class="tooltip-wrap"
57
+ onmouseenter={show}
58
+ onmouseleave={hide}
59
+ onfocusin={show}
60
+ onfocusout={hide}
61
+ onkeydown={onKeydown}
62
+ >
63
+ <span class="tooltip-trigger" aria-describedby={tipId}>
64
+ {@render children()}
65
+ </span>
66
+ <span id={tipId} role="tooltip" class="tooltip tooltip--{placement}" class:open>
67
+ {text}
68
+ </span>
69
+ </span>
70
+
71
+ <style>
72
+ .tooltip-wrap {
73
+ position: relative;
74
+ display: inline-flex;
75
+ }
76
+
77
+ .tooltip {
78
+ position: absolute;
79
+ z-index: var(--smrt-z-index-tooltip, 1080);
80
+ display: none;
81
+ width: max-content;
82
+ max-width: 16rem;
83
+ padding: var(--smrt-spacing-1, 4px) var(--smrt-spacing-2, 8px);
84
+ border-radius: var(--smrt-radius-small, 4px);
85
+ background: var(--smrt-color-inverse-surface, #313033);
86
+ color: var(--smrt-color-inverse-on-surface, #f4eff4);
87
+ font-size: var(--smrt-typography-body-small-size, 0.75rem);
88
+ line-height: var(--smrt-typography-body-small-line-height, 1.4);
89
+ pointer-events: none;
90
+ }
91
+
92
+ .tooltip.open {
93
+ display: block;
94
+ }
95
+
96
+ .tooltip--top {
97
+ bottom: 100%;
98
+ left: 50%;
99
+ transform: translateX(-50%);
100
+ margin-bottom: var(--smrt-spacing-1, 4px);
101
+ }
102
+ .tooltip--bottom {
103
+ top: 100%;
104
+ left: 50%;
105
+ transform: translateX(-50%);
106
+ margin-top: var(--smrt-spacing-1, 4px);
107
+ }
108
+ .tooltip--left {
109
+ right: 100%;
110
+ top: 50%;
111
+ transform: translateY(-50%);
112
+ margin-right: var(--smrt-spacing-1, 4px);
113
+ }
114
+ .tooltip--right {
115
+ left: 100%;
116
+ top: 50%;
117
+ transform: translateY(-50%);
118
+ margin-left: var(--smrt-spacing-1, 4px);
119
+ }
120
+ </style>
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Tooltip — anchored, delayed text bubble for a trigger.
3
+ *
4
+ * Shows on hover and keyboard focus, hides on leave / blur / Escape. The trigger
5
+ * is wired to the bubble via `aria-describedby` (always set; the bubble is in the
6
+ * DOM and revealed on open), and the bubble carries `role="tooltip"`. CSS-anchored
7
+ * via a `placement` prop — no JS positioning dependency.
8
+ */
9
+ import type { Snippet } from 'svelte';
10
+ import type { TooltipPlacement } from '../../types-generic';
11
+ export interface Props {
12
+ /** Tooltip text. */
13
+ text: string;
14
+ /** Placement relative to the trigger. */
15
+ placement?: TooltipPlacement;
16
+ /** Show delay in ms. */
17
+ delay?: number;
18
+ /** The trigger content. */
19
+ children: Snippet;
20
+ }
21
+ declare const Tooltip: import("svelte").Component<Props, {}, "">;
22
+ type Tooltip = ReturnType<typeof Tooltip>;
23
+ export default Tooltip;
24
+ //# sourceMappingURL=Tooltip.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tooltip.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/ui/Tooltip.svelte.ts"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,MAAM,WAAW,KAAK;IACpB,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,wBAAwB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2BAA2B;IAC3B,QAAQ,EAAE,OAAO,CAAC;CACnB;AAgDD,QAAA,MAAM,OAAO,2CAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
@@ -0,0 +1,209 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Tree — recursive, selectable tree view (generalizes the workspace NavTree).
4
+ *
5
+ * Renders a flat-DOM WAI-ARIA tree (`role="tree"` → `role="treeitem"` with
6
+ * `aria-level`), which keeps full keyboard support simple: roving tabindex, Arrow
7
+ * Up/Down to move between visible items, Right to expand / descend, Left to
8
+ * collapse / ascend, Home/End, and Enter/Space to select.
9
+ */
10
+ import { tick, untrack } from 'svelte';
11
+ import type { TreeNode } from '../../types-generic';
12
+
13
+ export interface Props {
14
+ /** Root nodes. */
15
+ nodes: TreeNode[];
16
+ /** Selected node id. */
17
+ selectedId?: string;
18
+ /** Initially expanded node ids. */
19
+ expanded?: string[];
20
+ /** Fired with the selected node id. */
21
+ onselect?: (id: string) => void;
22
+ /** Accessible label for the tree. */
23
+ 'aria-label'?: string;
24
+ }
25
+
26
+ const {
27
+ nodes,
28
+ selectedId,
29
+ expanded: initialExpanded = [],
30
+ onselect,
31
+ 'aria-label': ariaLabel = 'Tree',
32
+ }: Props = $props();
33
+
34
+ // Uncontrolled: seed once from the initial prop value.
35
+ let expandedSet = $state(new Set(untrack(() => initialExpanded)));
36
+ let activeId = $state<string | null>(null);
37
+ let itemEls = $state<Array<HTMLDivElement | null>>([]);
38
+
39
+ interface FlatNode {
40
+ node: TreeNode;
41
+ depth: number;
42
+ hasChildren: boolean;
43
+ isExpanded: boolean;
44
+ }
45
+
46
+ const flat = $derived.by(() => {
47
+ const out: FlatNode[] = [];
48
+ const walk = (list: TreeNode[], depth: number) => {
49
+ for (const node of list) {
50
+ const hasChildren = !!node.children?.length;
51
+ const isExpanded = expandedSet.has(node.id);
52
+ out.push({ node, depth, hasChildren, isExpanded });
53
+ if (hasChildren && isExpanded) walk(node.children ?? [], depth + 1);
54
+ }
55
+ };
56
+ walk(nodes, 0);
57
+ return out;
58
+ });
59
+
60
+ // The treeitem that owns tabindex=0 — active, else selected, else first.
61
+ const tabbableId = $derived(activeId ?? selectedId ?? flat[0]?.node.id ?? null);
62
+
63
+ function setExpanded(id: string, value: boolean) {
64
+ const next = new Set(expandedSet);
65
+ if (value) next.add(id);
66
+ else next.delete(id);
67
+ expandedSet = next;
68
+ }
69
+
70
+ async function focusIndex(index: number) {
71
+ const target = flat[index];
72
+ if (!target) return;
73
+ activeId = target.node.id;
74
+ await tick();
75
+ itemEls[index]?.focus();
76
+ }
77
+
78
+ function select(node: TreeNode) {
79
+ activeId = node.id;
80
+ onselect?.(node.id);
81
+ }
82
+
83
+ function onItemKeydown(event: KeyboardEvent, index: number, item: FlatNode) {
84
+ switch (event.key) {
85
+ case 'ArrowDown':
86
+ event.preventDefault();
87
+ focusIndex(Math.min(index + 1, flat.length - 1));
88
+ break;
89
+ case 'ArrowUp':
90
+ event.preventDefault();
91
+ focusIndex(Math.max(index - 1, 0));
92
+ break;
93
+ case 'ArrowRight':
94
+ event.preventDefault();
95
+ if (item.hasChildren && !item.isExpanded) setExpanded(item.node.id, true);
96
+ else if (item.hasChildren) focusIndex(index + 1);
97
+ break;
98
+ case 'ArrowLeft':
99
+ event.preventDefault();
100
+ if (item.hasChildren && item.isExpanded) {
101
+ setExpanded(item.node.id, false);
102
+ } else {
103
+ // Move to parent: nearest preceding item at a shallower depth.
104
+ for (let i = index - 1; i >= 0; i--) {
105
+ if (flat[i].depth < item.depth) {
106
+ focusIndex(i);
107
+ break;
108
+ }
109
+ }
110
+ }
111
+ break;
112
+ case 'Home':
113
+ event.preventDefault();
114
+ focusIndex(0);
115
+ break;
116
+ case 'End':
117
+ event.preventDefault();
118
+ focusIndex(flat.length - 1);
119
+ break;
120
+ case 'Enter':
121
+ case ' ':
122
+ event.preventDefault();
123
+ if (item.hasChildren) setExpanded(item.node.id, !item.isExpanded);
124
+ select(item.node);
125
+ break;
126
+ }
127
+ }
128
+ </script>
129
+
130
+ <div role="tree" aria-label={ariaLabel} class="tree">
131
+ {#each flat as item, i (item.node.id)}
132
+ <div
133
+ bind:this={itemEls[i]}
134
+ role="treeitem"
135
+ class="tree__item"
136
+ class:selected={selectedId === item.node.id}
137
+ style:padding-left="calc({item.depth} * var(--smrt-spacing-4, 16px) + var(--smrt-spacing-2, 8px))"
138
+ aria-level={item.depth + 1}
139
+ aria-selected={selectedId === item.node.id}
140
+ aria-expanded={item.hasChildren ? item.isExpanded : undefined}
141
+ tabindex={tabbableId === item.node.id ? 0 : -1}
142
+ onclick={() => {
143
+ if (item.hasChildren) setExpanded(item.node.id, !item.isExpanded);
144
+ select(item.node);
145
+ }}
146
+ onkeydown={(e) => onItemKeydown(e, i, item)}
147
+ >
148
+ {#if item.hasChildren}
149
+ <span class="tree__twisty" class:open={item.isExpanded} aria-hidden="true"
150
+ >▸</span
151
+ >
152
+ {:else}
153
+ <span class="tree__twisty tree__twisty--leaf" aria-hidden="true"></span>
154
+ {/if}
155
+ <span class="tree__label">{item.node.label}</span>
156
+ </div>
157
+ {/each}
158
+ </div>
159
+
160
+ <style>
161
+ .tree {
162
+ display: flex;
163
+ flex-direction: column;
164
+ }
165
+
166
+ .tree__item {
167
+ display: flex;
168
+ align-items: center;
169
+ gap: var(--smrt-spacing-1, 4px);
170
+ padding-top: var(--smrt-spacing-1, 4px);
171
+ padding-bottom: var(--smrt-spacing-1, 4px);
172
+ padding-right: var(--smrt-spacing-2, 8px);
173
+ border-radius: var(--smrt-radius-small, 4px);
174
+ cursor: pointer;
175
+ color: var(--smrt-color-on-surface);
176
+ user-select: none;
177
+ }
178
+ .tree__item:hover {
179
+ background: var(--smrt-color-surface-container-high);
180
+ }
181
+ .tree__item:focus-visible {
182
+ outline: 2px solid var(--smrt-color-primary);
183
+ outline-offset: -2px;
184
+ }
185
+ .tree__item.selected {
186
+ background: var(--smrt-color-primary-container);
187
+ color: var(--smrt-color-on-primary-container);
188
+ }
189
+
190
+ .tree__twisty {
191
+ display: inline-flex;
192
+ width: 1em;
193
+ justify-content: center;
194
+ transition: transform 0.15s ease;
195
+ font-size: 0.75em;
196
+ }
197
+ .tree__twisty.open {
198
+ transform: rotate(90deg);
199
+ }
200
+ .tree__twisty--leaf {
201
+ visibility: hidden;
202
+ }
203
+
204
+ @media (prefers-reduced-motion: reduce) {
205
+ .tree__twisty {
206
+ transition: none;
207
+ }
208
+ }
209
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { TreeNode } from '../../types-generic';
2
+ export interface Props {
3
+ /** Root nodes. */
4
+ nodes: TreeNode[];
5
+ /** Selected node id. */
6
+ selectedId?: string;
7
+ /** Initially expanded node ids. */
8
+ expanded?: string[];
9
+ /** Fired with the selected node id. */
10
+ onselect?: (id: string) => void;
11
+ /** Accessible label for the tree. */
12
+ 'aria-label'?: string;
13
+ }
14
+ declare const Tree: import("svelte").Component<Props, {}, "">;
15
+ type Tree = ReturnType<typeof Tree>;
16
+ export default Tree;
17
+ //# sourceMappingURL=Tree.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tree.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/ui/Tree.svelte.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGpD,MAAM,WAAW,KAAK;IACpB,kBAAkB;IAClB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,wBAAwB;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAoID,QAAA,MAAM,IAAI,2CAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Component test for Badge (Sweep S11, #1416).
3
+ *
4
+ * Badge is a presentational `<span>` — it renders children plus variant/size
5
+ * classes and forwards rest attributes. This suite asserts that real API
6
+ * (props from Badge.svelte) and proves the output is axe-clean.
7
+ */
8
+ import { render, screen } from '@testing-library/svelte';
9
+ import { createRawSnippet } from 'svelte';
10
+ import { describe, expect, it } from 'vitest';
11
+ import { expectNoA11yViolations } from '../../../test-support/a11y';
12
+ import Badge from '../Badge.svelte';
13
+ /** Build a text Snippet for the Badge's `children` prop. */
14
+ function textSnippet(text) {
15
+ return createRawSnippet(() => ({ render: () => `<span>${text}</span>` }));
16
+ }
17
+ describe('Badge', () => {
18
+ it('renders its children content', () => {
19
+ render(Badge, { props: { children: textSnippet('New') } });
20
+ expect(screen.getByText('New')).toBeInTheDocument();
21
+ });
22
+ it('applies the default variant and md size classes', () => {
23
+ const { container } = render(Badge, {
24
+ props: { children: textSnippet('Default') },
25
+ });
26
+ const badge = container.querySelector('.badge');
27
+ expect(badge).toHaveClass('default');
28
+ expect(badge).toHaveClass('md');
29
+ });
30
+ it.each([
31
+ 'primary',
32
+ 'success',
33
+ 'warning',
34
+ 'error',
35
+ 'info',
36
+ ])('reflects the %s variant as a class', (variant) => {
37
+ const { container } = render(Badge, {
38
+ props: { variant, children: textSnippet('Status') },
39
+ });
40
+ expect(container.querySelector('.badge')).toHaveClass(variant);
41
+ });
42
+ it('reflects the sm size as a class', () => {
43
+ const { container } = render(Badge, {
44
+ props: { size: 'sm', children: textSnippet('Small') },
45
+ });
46
+ expect(container.querySelector('.badge')).toHaveClass('sm');
47
+ });
48
+ it('forwards rest attributes (e.g. aria-label, data-testid) to the span', () => {
49
+ const { container } = render(Badge, {
50
+ props: {
51
+ 'aria-label': 'Unread count: 3',
52
+ 'data-testid': 'unread-badge',
53
+ children: textSnippet('3'),
54
+ },
55
+ });
56
+ const badge = container.querySelector('.badge');
57
+ expect(badge).toHaveAttribute('aria-label', 'Unread count: 3');
58
+ expect(badge).toHaveAttribute('data-testid', 'unread-badge');
59
+ });
60
+ it('is axe-clean for a default badge', async () => {
61
+ const { container } = render(Badge, {
62
+ props: { children: textSnippet('Accessible') },
63
+ });
64
+ await expectNoA11yViolations(container);
65
+ });
66
+ it('is axe-clean for a variant badge with an accessible label', async () => {
67
+ const { container } = render(Badge, {
68
+ props: {
69
+ variant: 'error',
70
+ 'aria-label': '5 errors',
71
+ children: textSnippet('5'),
72
+ },
73
+ });
74
+ await expectNoA11yViolations(container);
75
+ });
76
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Golden test for Button (Sweep L4, #1423).
3
+ *
4
+ * The reference pattern for component tests in this package: render via Testing
5
+ * Library, assert role/name/state, drive interaction with user-event, and prove
6
+ * the rendered output is axe-clean. New primitive tests should mirror this shape.
7
+ */
8
+ import { render, screen } from '@testing-library/svelte';
9
+ import userEvent from '@testing-library/user-event';
10
+ import { createRawSnippet } from 'svelte';
11
+ import { describe, expect, it, vi } from 'vitest';
12
+ import { expectNoA11yViolations } from '../../../test-support/a11y';
13
+ import Button from '../Button.svelte';
14
+ /** Build a text Snippet for the Button's `children` prop. */
15
+ function textSnippet(text) {
16
+ return createRawSnippet(() => ({ render: () => `<span>${text}</span>` }));
17
+ }
18
+ describe('Button', () => {
19
+ it('renders children as a <button> with type=button by default', () => {
20
+ render(Button, { props: { children: textSnippet('Save') } });
21
+ const button = screen.getByRole('button', { name: 'Save' });
22
+ expect(button).toBeInTheDocument();
23
+ expect(button).toHaveAttribute('type', 'button');
24
+ });
25
+ it('fires onclick when clicked', async () => {
26
+ const onclick = vi.fn();
27
+ render(Button, { props: { children: textSnippet('Go'), onclick } });
28
+ await userEvent.click(screen.getByRole('button', { name: 'Go' }));
29
+ expect(onclick).toHaveBeenCalledTimes(1);
30
+ });
31
+ it('is keyboard-activatable when focused', async () => {
32
+ const onclick = vi.fn();
33
+ render(Button, { props: { children: textSnippet('Go'), onclick } });
34
+ const button = screen.getByRole('button', { name: 'Go' });
35
+ button.focus();
36
+ expect(button).toHaveFocus();
37
+ await userEvent.keyboard('{Enter}');
38
+ expect(onclick).toHaveBeenCalled();
39
+ });
40
+ it('does not fire onclick when disabled', async () => {
41
+ const onclick = vi.fn();
42
+ render(Button, {
43
+ props: { children: textSnippet('Go'), disabled: true, onclick },
44
+ });
45
+ const button = screen.getByRole('button', { name: 'Go' });
46
+ expect(button).toBeDisabled();
47
+ await userEvent.click(button);
48
+ expect(onclick).not.toHaveBeenCalled();
49
+ });
50
+ it('reflects loading as aria-busy and disables interaction', () => {
51
+ render(Button, { props: { children: textSnippet('Go'), loading: true } });
52
+ const button = screen.getByRole('button', { name: 'Go' });
53
+ expect(button).toHaveAttribute('aria-busy', 'true');
54
+ expect(button).toBeDisabled();
55
+ });
56
+ it('renders as a link (role=link) when href is provided', () => {
57
+ render(Button, {
58
+ props: { children: textSnippet('Home'), href: '/home' },
59
+ });
60
+ const link = screen.getByRole('link', { name: 'Home' });
61
+ expect(link).toHaveAttribute('href', '/home');
62
+ });
63
+ it('is axe-clean', async () => {
64
+ const { container } = render(Button, {
65
+ props: { children: textSnippet('Accessible') },
66
+ });
67
+ await expectNoA11yViolations(container);
68
+ });
69
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Component test for Card (Sweep S11, #1416).
3
+ *
4
+ * Mirrors the Button golden pattern: render → assert structure/variant → axe.
5
+ * Card is a presentational surface — it has no interactive behavior of its own
6
+ * (no clickable/keyboard handler in the source), so this suite covers rendering
7
+ * of content/header/footer snippets, variant + padding classes, the hoverable
8
+ * flag, attribute pass-through (`...rest`), and a11y.
9
+ */
10
+ import { render, screen } from '@testing-library/svelte';
11
+ import { createRawSnippet } from 'svelte';
12
+ import { describe, expect, it } from 'vitest';
13
+ import { expectNoA11yViolations } from '../../../test-support/a11y';
14
+ import Card from '../Card.svelte';
15
+ /** Build a text Snippet for a Card slot. */
16
+ function textSnippet(text) {
17
+ return createRawSnippet(() => ({ render: () => `<span>${text}</span>` }));
18
+ }
19
+ describe('Card', () => {
20
+ it('renders children inside the card content region', () => {
21
+ render(Card, { props: { children: textSnippet('Body text') } });
22
+ expect(screen.getByText('Body text')).toBeInTheDocument();
23
+ });
24
+ it('applies the default variant and md padding classes', () => {
25
+ const { container } = render(Card, {
26
+ props: { children: textSnippet('Body') },
27
+ });
28
+ const card = container.querySelector('.card');
29
+ expect(card).toHaveClass('default');
30
+ expect(card).toHaveClass('padding-md');
31
+ });
32
+ it('reflects an explicit variant and padding', () => {
33
+ const { container } = render(Card, {
34
+ props: {
35
+ variant: 'elevated',
36
+ padding: 'lg',
37
+ children: textSnippet('Body'),
38
+ },
39
+ });
40
+ const card = container.querySelector('.card');
41
+ expect(card).toHaveClass('elevated');
42
+ expect(card).toHaveClass('padding-lg');
43
+ });
44
+ it('adds the hoverable class only when hoverable is true', () => {
45
+ const { container: plain } = render(Card, {
46
+ props: { children: textSnippet('Body') },
47
+ });
48
+ expect(plain.querySelector('.card')).not.toHaveClass('hoverable');
49
+ const { container: hover } = render(Card, {
50
+ props: { hoverable: true, children: textSnippet('Body') },
51
+ });
52
+ expect(hover.querySelector('.card')).toHaveClass('hoverable');
53
+ });
54
+ it('renders header and footer slots when provided', () => {
55
+ const { container } = render(Card, {
56
+ props: {
57
+ header: textSnippet('Header'),
58
+ footer: textSnippet('Footer'),
59
+ children: textSnippet('Body'),
60
+ },
61
+ });
62
+ expect(container.querySelector('.card-header')).toBeInTheDocument();
63
+ expect(container.querySelector('.card-footer')).toBeInTheDocument();
64
+ expect(screen.getByText('Header')).toBeInTheDocument();
65
+ expect(screen.getByText('Footer')).toBeInTheDocument();
66
+ });
67
+ it('omits header and footer regions when those slots are absent', () => {
68
+ const { container } = render(Card, {
69
+ props: { children: textSnippet('Body') },
70
+ });
71
+ expect(container.querySelector('.card-header')).toBeNull();
72
+ expect(container.querySelector('.card-footer')).toBeNull();
73
+ });
74
+ it('forwards rest attributes (e.g. data-testid, aria-label) to the root', () => {
75
+ const { container } = render(Card, {
76
+ props: {
77
+ 'data-testid': 'profile-card',
78
+ 'aria-label': 'Profile',
79
+ children: textSnippet('Body'),
80
+ },
81
+ });
82
+ const card = container.querySelector('.card');
83
+ expect(card).toHaveAttribute('data-testid', 'profile-card');
84
+ expect(card).toHaveAttribute('aria-label', 'Profile');
85
+ });
86
+ it('is axe-clean for a content-only card', async () => {
87
+ const { container } = render(Card, {
88
+ props: { children: textSnippet('Accessible body') },
89
+ });
90
+ await expectNoA11yViolations(container);
91
+ });
92
+ it('is axe-clean with header and footer regions', async () => {
93
+ const { container } = render(Card, {
94
+ props: {
95
+ variant: 'outlined',
96
+ header: textSnippet('Heading'),
97
+ footer: textSnippet('Actions'),
98
+ children: textSnippet('Accessible body'),
99
+ },
100
+ });
101
+ await expectNoA11yViolations(container);
102
+ });
103
+ });