@teseor/css 1.0.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 (298) hide show
  1. package/README.md +102 -0
  2. package/dist/compiled.css +4463 -0
  3. package/dist/index.css +4445 -0
  4. package/fonts/.gitkeep +9 -0
  5. package/package.json +38 -0
  6. package/src/00-config/layers/layers.scss +1 -0
  7. package/src/00-config/mixins/_border.scss +13 -0
  8. package/src/00-config/mixins/index.scss +1 -0
  9. package/src/00-config/tokens/_variables.scss +120 -0
  10. package/src/00-config/tokens/borders/index.scss +6 -0
  11. package/src/00-config/tokens/colors/colors.docs.json +168 -0
  12. package/src/00-config/tokens/colors/index.scss +25 -0
  13. package/src/00-config/tokens/grid/grid.docs.json +54 -0
  14. package/src/00-config/tokens/grid/index.scss +11 -0
  15. package/src/00-config/tokens/index.scss +15 -0
  16. package/src/00-config/tokens/input.scss +14 -0
  17. package/src/00-config/tokens/motion/index.scss +25 -0
  18. package/src/00-config/tokens/radius/index.scss +7 -0
  19. package/src/00-config/tokens/semantic/colors/index.scss +30 -0
  20. package/src/00-config/tokens/semantic/spacing/index.scss +10 -0
  21. package/src/00-config/tokens/shadows/index.scss +5 -0
  22. package/src/00-config/tokens/spacing/index.scss +15 -0
  23. package/src/00-config/tokens/spacing/spacing.docs.json +114 -0
  24. package/src/00-config/tokens/theming.docs.json +77 -0
  25. package/src/00-config/tokens/typography/index.scss +119 -0
  26. package/src/00-config/tokens/zindex/index.scss +14 -0
  27. package/src/01-reset/index.scss +1 -0
  28. package/src/01-reset/reset.scss +8 -0
  29. package/src/02-base/index.scss +2 -0
  30. package/src/02-base/root/root.scss +12 -0
  31. package/src/02-base/typography/typography.docs.json +94 -0
  32. package/src/02-base/typography/typography.scss +145 -0
  33. package/src/03-layout/app-shell/app-shell.api.json +26 -0
  34. package/src/03-layout/app-shell/app-shell.docs.json +119 -0
  35. package/src/03-layout/app-shell/index.scss +5 -0
  36. package/src/03-layout/box/box.api.json +15 -0
  37. package/src/03-layout/box/box.docs.json +57 -0
  38. package/src/03-layout/box/index.scss +34 -0
  39. package/src/03-layout/center/center.api.json +6 -0
  40. package/src/03-layout/center/center.docs.json +24 -0
  41. package/src/03-layout/center/index.scss +11 -0
  42. package/src/03-layout/cluster/cluster.api.json +10 -0
  43. package/src/03-layout/cluster/cluster.docs.json +129 -0
  44. package/src/03-layout/cluster/index.scss +18 -0
  45. package/src/03-layout/container/container.api.json +13 -0
  46. package/src/03-layout/container/index.scss +17 -0
  47. package/src/03-layout/flex/flex.api.json +15 -0
  48. package/src/03-layout/flex/flex.docs.json +56 -0
  49. package/src/03-layout/flex/index.scss +51 -0
  50. package/src/03-layout/grid/grid.api.json +10 -0
  51. package/src/03-layout/grid/grid.docs.json +116 -0
  52. package/src/03-layout/grid/index.scss +15 -0
  53. package/src/03-layout/index.scss +11 -0
  54. package/src/03-layout/main/index.scss +17 -0
  55. package/src/03-layout/main/main.api.json +13 -0
  56. package/src/03-layout/sidebar/index.scss +30 -0
  57. package/src/03-layout/sidebar/sidebar.api.json +13 -0
  58. package/src/03-layout/sidebar-nav/index.scss +209 -0
  59. package/src/03-layout/sidebar-nav/sidebar-nav-all.png +0 -0
  60. package/src/03-layout/sidebar-nav/sidebar-nav.api.json +115 -0
  61. package/src/03-layout/sidebar-nav/sidebar-nav.docs.json +100 -0
  62. package/src/03-layout/sidebar-nav/sidebar-nav.visual.spec.ts +14 -0
  63. package/src/03-layout/stack/index.scss +13 -0
  64. package/src/03-layout/stack/stack.api.json +10 -0
  65. package/src/03-layout/stack/stack.docs.json +131 -0
  66. package/src/04-components/accordion/accordion-visual.png +0 -0
  67. package/src/04-components/accordion/accordion.api.json +19 -0
  68. package/src/04-components/accordion/accordion.docs.json +51 -0
  69. package/src/04-components/accordion/accordion.visual.spec.ts +14 -0
  70. package/src/04-components/accordion/index.scss +78 -0
  71. package/src/04-components/alert/alert-visual.png +0 -0
  72. package/src/04-components/alert/alert.api.json +91 -0
  73. package/src/04-components/alert/alert.docs.json +79 -0
  74. package/src/04-components/alert/alert.visual.spec.ts +14 -0
  75. package/src/04-components/alert/index.scss +108 -0
  76. package/src/04-components/avatar/avatar-visual.png +0 -0
  77. package/src/04-components/avatar/avatar.api.json +70 -0
  78. package/src/04-components/avatar/avatar.docs.json +200 -0
  79. package/src/04-components/avatar/avatar.visual.spec.ts +14 -0
  80. package/src/04-components/avatar/index.scss +104 -0
  81. package/src/04-components/badge/badge-visual.png +0 -0
  82. package/src/04-components/badge/badge.api.json +86 -0
  83. package/src/04-components/badge/badge.docs.json +89 -0
  84. package/src/04-components/badge/badge.visual.spec.ts +14 -0
  85. package/src/04-components/badge/index.scss +64 -0
  86. package/src/04-components/breadcrumb/breadcrumb-visual.png +0 -0
  87. package/src/04-components/breadcrumb/breadcrumb.api.json +31 -0
  88. package/src/04-components/breadcrumb/breadcrumb.docs.json +59 -0
  89. package/src/04-components/breadcrumb/breadcrumb.visual.spec.ts +14 -0
  90. package/src/04-components/breadcrumb/index.scss +77 -0
  91. package/src/04-components/button/button-visual.png +0 -0
  92. package/src/04-components/button/button.api.json +138 -0
  93. package/src/04-components/button/button.docs.json +75 -0
  94. package/src/04-components/button/button.visual.spec.ts +14 -0
  95. package/src/04-components/button/index.scss +222 -0
  96. package/src/04-components/button-group/button-group-visual.png +0 -0
  97. package/src/04-components/button-group/button-group.api.json +11 -0
  98. package/src/04-components/button-group/button-group.docs.json +52 -0
  99. package/src/04-components/button-group/button-group.visual.spec.ts +14 -0
  100. package/src/04-components/button-group/index.scss +78 -0
  101. package/src/04-components/card/card-visual.png +0 -0
  102. package/src/04-components/card/card.api.json +53 -0
  103. package/src/04-components/card/card.docs.json +103 -0
  104. package/src/04-components/card/card.visual.spec.ts +14 -0
  105. package/src/04-components/card/index.scss +41 -0
  106. package/src/04-components/checkbox/checkbox-visual.png +0 -0
  107. package/src/04-components/checkbox/checkbox.api.json +50 -0
  108. package/src/04-components/checkbox/checkbox.docs.json +151 -0
  109. package/src/04-components/checkbox/checkbox.visual.spec.ts +14 -0
  110. package/src/04-components/checkbox/index.scss +104 -0
  111. package/src/04-components/code/code-visual.png +0 -0
  112. package/src/04-components/code/code.api.json +47 -0
  113. package/src/04-components/code/code.docs.json +84 -0
  114. package/src/04-components/code/code.visual.spec.ts +14 -0
  115. package/src/04-components/code/index.scss +64 -0
  116. package/src/04-components/data-list/data-list-visual.png +0 -0
  117. package/src/04-components/data-list/data-list.api.json +23 -0
  118. package/src/04-components/data-list/data-list.docs.json +79 -0
  119. package/src/04-components/data-list/data-list.visual.spec.ts +14 -0
  120. package/src/04-components/data-list/index.scss +98 -0
  121. package/src/04-components/dialog/dialog-visual.png +0 -0
  122. package/src/04-components/dialog/dialog.api.json +23 -0
  123. package/src/04-components/dialog/dialog.docs.json +58 -0
  124. package/src/04-components/dialog/dialog.visual.spec.ts +14 -0
  125. package/src/04-components/dialog/index.scss +74 -0
  126. package/src/04-components/disclosure/disclosure-visual.png +0 -0
  127. package/src/04-components/disclosure/disclosure.api.json +31 -0
  128. package/src/04-components/disclosure/disclosure.docs.json +73 -0
  129. package/src/04-components/disclosure/disclosure.visual.spec.ts +14 -0
  130. package/src/04-components/disclosure/index.scss +111 -0
  131. package/src/04-components/divider/divider-visual.png +0 -0
  132. package/src/04-components/divider/divider.api.json +30 -0
  133. package/src/04-components/divider/divider.docs.json +68 -0
  134. package/src/04-components/divider/divider.visual.spec.ts +14 -0
  135. package/src/04-components/divider/index.scss +115 -0
  136. package/src/04-components/drawer/drawer-visual.png +0 -0
  137. package/src/04-components/drawer/drawer.api.json +39 -0
  138. package/src/04-components/drawer/drawer.docs.json +78 -0
  139. package/src/04-components/drawer/drawer.visual.spec.ts +14 -0
  140. package/src/04-components/drawer/index.scss +251 -0
  141. package/src/04-components/field/field-visual.png +0 -0
  142. package/src/04-components/field/field.api.json +11 -0
  143. package/src/04-components/field/field.docs.json +59 -0
  144. package/src/04-components/field/field.visual.spec.ts +14 -0
  145. package/src/04-components/field/index.scss +44 -0
  146. package/src/04-components/form-error/form-error-visual.png +0 -0
  147. package/src/04-components/form-error/form-error.api.json +19 -0
  148. package/src/04-components/form-error/form-error.docs.json +40 -0
  149. package/src/04-components/form-error/form-error.visual.spec.ts +14 -0
  150. package/src/04-components/form-error/index.scss +31 -0
  151. package/src/04-components/form-helper/form-helper-visual.png +0 -0
  152. package/src/04-components/form-helper/form-helper.api.json +19 -0
  153. package/src/04-components/form-helper/form-helper.docs.json +31 -0
  154. package/src/04-components/form-helper/form-helper.visual.spec.ts +14 -0
  155. package/src/04-components/form-helper/index.scss +24 -0
  156. package/src/04-components/heading/heading-visual.png +0 -0
  157. package/src/04-components/heading/heading.api.json +83 -0
  158. package/src/04-components/heading/heading.docs.json +98 -0
  159. package/src/04-components/heading/heading.visual.spec.ts +14 -0
  160. package/src/04-components/heading/index.scss +61 -0
  161. package/src/04-components/icon/icon-visual.png +0 -0
  162. package/src/04-components/icon/icon.api.json +56 -0
  163. package/src/04-components/icon/icon.docs.json +155 -0
  164. package/src/04-components/icon/icon.visual.spec.ts +14 -0
  165. package/src/04-components/icon/index.scss +79 -0
  166. package/src/04-components/index.scss +41 -0
  167. package/src/04-components/input/index.scss +154 -0
  168. package/src/04-components/input/input-visual.png +0 -0
  169. package/src/04-components/input/input.api.json +85 -0
  170. package/src/04-components/input/input.docs.json +189 -0
  171. package/src/04-components/input/input.visual.spec.ts +14 -0
  172. package/src/04-components/label/index.scss +46 -0
  173. package/src/04-components/label/label-visual.png +0 -0
  174. package/src/04-components/label/label.api.json +27 -0
  175. package/src/04-components/label/label.docs.json +73 -0
  176. package/src/04-components/label/label.visual.spec.ts +14 -0
  177. package/src/04-components/link/index.scss +73 -0
  178. package/src/04-components/link/link-visual.png +0 -0
  179. package/src/04-components/link/link.api.json +39 -0
  180. package/src/04-components/link/link.docs.json +104 -0
  181. package/src/04-components/link/link.visual.spec.ts +14 -0
  182. package/src/04-components/menu/index.scss +145 -0
  183. package/src/04-components/menu/menu-visual.png +0 -0
  184. package/src/04-components/menu/menu.api.json +47 -0
  185. package/src/04-components/menu/menu.docs.json +81 -0
  186. package/src/04-components/menu/menu.visual.spec.ts +14 -0
  187. package/src/04-components/modal/index.scss +93 -0
  188. package/src/04-components/modal/modal-visual.png +0 -0
  189. package/src/04-components/modal/modal.api.json +43 -0
  190. package/src/04-components/modal/modal.docs.json +86 -0
  191. package/src/04-components/modal/modal.visual.spec.ts +14 -0
  192. package/src/04-components/overlay/index.scss +49 -0
  193. package/src/04-components/overlay/overlay-visual.png +0 -0
  194. package/src/04-components/overlay/overlay.api.json +23 -0
  195. package/src/04-components/overlay/overlay.docs.json +63 -0
  196. package/src/04-components/overlay/overlay.visual.spec.ts +14 -0
  197. package/src/04-components/pagination/index.scss +114 -0
  198. package/src/04-components/pagination/pagination-visual.png +0 -0
  199. package/src/04-components/pagination/pagination.api.json +39 -0
  200. package/src/04-components/pagination/pagination.docs.json +88 -0
  201. package/src/04-components/pagination/pagination.visual.spec.ts +14 -0
  202. package/src/04-components/popover/index.scss +114 -0
  203. package/src/04-components/popover/popover-visual.png +0 -0
  204. package/src/04-components/popover/popover.api.json +39 -0
  205. package/src/04-components/popover/popover.docs.json +70 -0
  206. package/src/04-components/popover/popover.visual.spec.ts +14 -0
  207. package/src/04-components/progress/index.scss +102 -0
  208. package/src/04-components/progress/progress-visual.png +0 -0
  209. package/src/04-components/progress/progress.api.json +23 -0
  210. package/src/04-components/progress/progress.docs.json +87 -0
  211. package/src/04-components/progress/progress.visual.spec.ts +14 -0
  212. package/src/04-components/radio/index.scss +96 -0
  213. package/src/04-components/radio/radio-visual.png +0 -0
  214. package/src/04-components/radio/radio.api.json +46 -0
  215. package/src/04-components/radio/radio.docs.json +159 -0
  216. package/src/04-components/radio/radio.visual.spec.ts +14 -0
  217. package/src/04-components/select/index.scss +113 -0
  218. package/src/04-components/select/select-visual.png +0 -0
  219. package/src/04-components/select/select.api.json +85 -0
  220. package/src/04-components/select/select.docs.json +162 -0
  221. package/src/04-components/select/select.visual.spec.ts +14 -0
  222. package/src/04-components/skeleton/index.scss +99 -0
  223. package/src/04-components/skeleton/skeleton-visual.png +0 -0
  224. package/src/04-components/skeleton/skeleton.api.json +19 -0
  225. package/src/04-components/skeleton/skeleton.docs.json +62 -0
  226. package/src/04-components/skeleton/skeleton.visual.spec.ts +14 -0
  227. package/src/04-components/spinner/index.scss +60 -0
  228. package/src/04-components/spinner/spinner-visual.png +0 -0
  229. package/src/04-components/spinner/spinner.api.json +63 -0
  230. package/src/04-components/spinner/spinner.docs.json +88 -0
  231. package/src/04-components/spinner/spinner.visual.spec.ts +15 -0
  232. package/src/04-components/status/index.scss +85 -0
  233. package/src/04-components/status/status-visual.png +0 -0
  234. package/src/04-components/status/status.api.json +19 -0
  235. package/src/04-components/status/status.docs.json +60 -0
  236. package/src/04-components/status/status.visual.spec.ts +14 -0
  237. package/src/04-components/table/index.scss +60 -0
  238. package/src/04-components/table/table-visual.png +0 -0
  239. package/src/04-components/table/table.api.json +27 -0
  240. package/src/04-components/table/table.docs.json +59 -0
  241. package/src/04-components/table/table.visual.spec.ts +14 -0
  242. package/src/04-components/tabs/index.scss +77 -0
  243. package/src/04-components/tabs/tabs-visual.png +0 -0
  244. package/src/04-components/tabs/tabs.api.json +31 -0
  245. package/src/04-components/tabs/tabs.docs.json +61 -0
  246. package/src/04-components/tabs/tabs.visual.spec.ts +14 -0
  247. package/src/04-components/tag/index.scss +98 -0
  248. package/src/04-components/tag/tag-visual.png +0 -0
  249. package/src/04-components/tag/tag.api.json +86 -0
  250. package/src/04-components/tag/tag.docs.json +108 -0
  251. package/src/04-components/tag/tag.visual.spec.ts +14 -0
  252. package/src/04-components/textarea/index.scss +124 -0
  253. package/src/04-components/textarea/textarea-visual.png +0 -0
  254. package/src/04-components/textarea/textarea.api.json +93 -0
  255. package/src/04-components/textarea/textarea.docs.json +191 -0
  256. package/src/04-components/textarea/textarea.visual.spec.ts +14 -0
  257. package/src/04-components/toast/index.scss +180 -0
  258. package/src/04-components/toast/toast-visual.png +0 -0
  259. package/src/04-components/toast/toast.api.json +43 -0
  260. package/src/04-components/toast/toast.docs.json +99 -0
  261. package/src/04-components/toast/toast.visual.spec.ts +14 -0
  262. package/src/04-components/toggle/index.scss +117 -0
  263. package/src/04-components/toggle/toggle-visual.png +0 -0
  264. package/src/04-components/toggle/toggle.api.json +43 -0
  265. package/src/04-components/toggle/toggle.docs.json +92 -0
  266. package/src/04-components/toggle/toggle.visual.spec.ts +14 -0
  267. package/src/04-components/tooltip/index.scss +102 -0
  268. package/src/04-components/tooltip/tooltip-visual.png +0 -0
  269. package/src/04-components/tooltip/tooltip.api.json +39 -0
  270. package/src/04-components/tooltip/tooltip.docs.json +63 -0
  271. package/src/04-components/tooltip/tooltip.visual.spec.ts +14 -0
  272. package/src/05-utilities/border/index.scss +21 -0
  273. package/src/05-utilities/display/display.api.json +6 -0
  274. package/src/05-utilities/display/display.docs.json +28 -0
  275. package/src/05-utilities/display/display.scss +195 -0
  276. package/src/05-utilities/index.scss +6 -0
  277. package/src/05-utilities/spacing/spacing.api.json +6 -0
  278. package/src/05-utilities/spacing/spacing.docs.json +43 -0
  279. package/src/05-utilities/spacing/spacing.scss +399 -0
  280. package/src/05-utilities/text/text.api.json +38 -0
  281. package/src/05-utilities/text/text.docs.json +132 -0
  282. package/src/05-utilities/text/text.scss +246 -0
  283. package/src/05-utilities/view-transition/index.scss +198 -0
  284. package/src/05-utilities/view-transition/view-transition.api.json +21 -0
  285. package/src/05-utilities/view-transition/view-transition.docs.json +63 -0
  286. package/src/05-utilities/visually-hidden/index.scss +38 -0
  287. package/src/05-utilities/visually-hidden/visually-hidden.api.json +8 -0
  288. package/src/05-utilities/visually-hidden/visually-hidden.docs.json +29 -0
  289. package/src/99-debug/grid-overlay.scss +79 -0
  290. package/src/99-debug/index.scss +1 -0
  291. package/src/index.scss +30 -0
  292. package/src/testing/api-types.ts +20 -0
  293. package/src/testing/grid-alignment.spec.ts +92 -0
  294. package/src/testing/html-generator.ts +151 -0
  295. package/src/testing/index.ts +15 -0
  296. package/src/testing/page-setup.ts +131 -0
  297. package/src/testing/rhythm.ts +146 -0
  298. package/src/testing/scaffold.ts +50 -0
package/src/index.scss ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * CSS Library - Main Entry Point
3
+ *
4
+ * Import order follows numbered folder structure for cascade control.
5
+ */
6
+
7
+ /* 00. Config: Layer definitions + Design tokens */
8
+ @use './00-config/layers/layers' as *;
9
+ @use './00-config/tokens/index' as *;
10
+
11
+ /* 01. Reset styles */
12
+ @use './01-reset/index' as *;
13
+
14
+ /* 02. Base styles (typography, etc.) */
15
+ @use './02-base/index' as *;
16
+
17
+ /* 03. Layout primitives */
18
+ @use './03-layout/index' as *;
19
+
20
+ /* 04. Components */
21
+ @use './04-components/index' as *;
22
+
23
+ /* 05. Utilities (high specificity, load last) */
24
+ @use './05-utilities/index' as *;
25
+
26
+ /* 99. Debug tools (dev only) */
27
+ @use './99-debug/index' as *;
28
+
29
+ /* Fonts (Google Fonts CDN) */
30
+ @import "https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;700&family=Noto+Sans+Mono:wght@400;500&display=swap";
@@ -0,0 +1,20 @@
1
+ export interface ComponentAPI {
2
+ name: string;
3
+ baseClass: string;
4
+ element: string;
5
+ description: string;
6
+ modifiers: Record<
7
+ string,
8
+ {
9
+ values?: string[];
10
+ type?: string;
11
+ default?: string | null;
12
+ description: string;
13
+ }
14
+ >;
15
+ states?: string[];
16
+ combinations?: Array<{
17
+ modifiers: string[];
18
+ disabled?: boolean;
19
+ }>;
20
+ }
@@ -0,0 +1,92 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ // Skip in CI - needs docs server running
4
+ test.skip(!!process.env.CI, 'Grid alignment test requires docs server');
5
+
6
+ // Elements that THEMSELVES don't follow grid (fluid page containers only)
7
+ const SKIP_SELF_SELECTORS = [
8
+ // Page structure - fluid by nature (viewport-dependent)
9
+ 'html',
10
+ 'body',
11
+ 'main',
12
+ 'aside',
13
+ '.ui-sidebar',
14
+ '.ui-main',
15
+ '.ui-app-shell',
16
+ ];
17
+
18
+ test('all components should align to vertical grid', async ({ page }) => {
19
+ await page.goto('/');
20
+
21
+ const violations = await page.evaluate((skipSelf: string[]) => {
22
+ // Get unit from CSS custom property
23
+ const temp = document.createElement('div');
24
+ temp.style.cssText = 'position:absolute;visibility:hidden;width:var(--unit,0.5rem)';
25
+ document.body.appendChild(temp);
26
+ const unit = temp.getBoundingClientRect().width || 8;
27
+ document.body.removeChild(temp);
28
+
29
+ const INLINE_TAGS = [
30
+ 'STRONG',
31
+ 'B',
32
+ 'EM',
33
+ 'I',
34
+ 'SMALL',
35
+ 'SPAN',
36
+ 'A',
37
+ 'CODE',
38
+ 'ABBR',
39
+ 'CITE',
40
+ 'Q',
41
+ 'SUB',
42
+ 'SUP',
43
+ 'MARK',
44
+ 'BR',
45
+ 'WBR',
46
+ ];
47
+
48
+ const results: Array<{ selector: string; height: number; expected: number; offBy: number }> =
49
+ [];
50
+
51
+ for (const el of document.querySelectorAll('*')) {
52
+ const tagName = el.tagName;
53
+
54
+ // Skip script, style, link, meta, etc.
55
+ if (['SCRIPT', 'STYLE', 'LINK', 'META', 'TITLE', 'HEAD'].includes(tagName)) continue;
56
+
57
+ // Skip inline elements
58
+ if (INLINE_TAGS.includes(tagName)) continue;
59
+
60
+ // Skip elements that are fluid containers (but check their children)
61
+ if (skipSelf.some((sel) => el.matches(sel))) continue;
62
+
63
+ const height = el.getBoundingClientRect().height;
64
+ if (height === 0) continue;
65
+
66
+ const remainder = height % unit;
67
+ const isAligned = remainder < 0.5 || remainder > unit - 0.5;
68
+
69
+ if (!isAligned) {
70
+ const classes = el.className?.toString?.() || '';
71
+ const firstClass = classes.split(' ')[0];
72
+ results.push({
73
+ selector: `${tagName.toLowerCase()}${firstClass ? `.${firstClass}` : ''}`,
74
+ height: Math.round(height * 10) / 10,
75
+ expected: Math.round(height / unit) * unit,
76
+ offBy: Math.round(remainder * 10) / 10,
77
+ });
78
+ }
79
+ }
80
+
81
+ return results;
82
+ }, SKIP_SELF_SELECTORS);
83
+
84
+ if (violations.length > 0) {
85
+ console.log(`Found ${violations.length} grid violations:`);
86
+ console.table(violations);
87
+ }
88
+
89
+ expect(violations, `Grid violations found:\n${JSON.stringify(violations, null, 2)}`).toHaveLength(
90
+ 0,
91
+ );
92
+ });
@@ -0,0 +1,151 @@
1
+ import type { ComponentAPI } from './api-types';
2
+
3
+ const PREFIX = 'ui-';
4
+
5
+ function generateCombinations(
6
+ api: ComponentAPI,
7
+ ): Array<{ modifiers: string[]; disabled?: boolean }> {
8
+ if (api.combinations) return api.combinations;
9
+
10
+ const enumModifiers = Object.entries(api.modifiers)
11
+ .filter(([, def]) => def.values)
12
+ .map(([, def]) => def.values!);
13
+
14
+ if (enumModifiers.length < 2) return [];
15
+
16
+ const [first, second] = enumModifiers;
17
+ const combos: Array<{ modifiers: string[] }> = [];
18
+
19
+ for (const a of first) {
20
+ for (const b of second) {
21
+ combos.push({ modifiers: [a, b] });
22
+ }
23
+ }
24
+
25
+ return combos;
26
+ }
27
+
28
+ function capitalize(s: string): string {
29
+ return s.charAt(0).toUpperCase() + s.slice(1);
30
+ }
31
+
32
+ export function generateVariationsHtml(api: ComponentAPI): string {
33
+ const baseClass = `${PREFIX}${api.baseClass}`;
34
+ const element = api.element;
35
+ const title = capitalize(api.name);
36
+
37
+ const sections: string[] = [];
38
+
39
+ // Default section
40
+ sections.push(`
41
+ <div class="test-section">
42
+ <div class="test-section-title">Default</div>
43
+ <div class="test-row">
44
+ <${element} class="${baseClass}">Default</${element}>
45
+ </div>
46
+ </div>
47
+ `);
48
+
49
+ // Generate sections for each modifier
50
+ for (const [modName, modDef] of Object.entries(api.modifiers)) {
51
+ const sectionTitle = capitalize(modName);
52
+
53
+ if (modDef.values) {
54
+ const buttons: string[] = [];
55
+
56
+ if (modName === 'size') {
57
+ buttons.push(`<${element} class="${baseClass}">Default</${element}>`);
58
+ }
59
+ if (modName === 'variant') {
60
+ buttons.push(`<${element} class="${baseClass}">Primary</${element}>`);
61
+ }
62
+
63
+ for (const value of modDef.values) {
64
+ const modClass = `${baseClass}--${value}`;
65
+ const label = capitalize(value);
66
+ buttons.push(`<${element} class="${baseClass} ${modClass}">${label}</${element}>`);
67
+ }
68
+
69
+ sections.push(`
70
+ <div class="test-section">
71
+ <div class="test-section-title">${sectionTitle}</div>
72
+ <div class="test-row">
73
+ ${buttons.join('\n ')}
74
+ </div>
75
+ </div>
76
+ `);
77
+ } else if (modDef.type === 'boolean') {
78
+ const modClass = `${baseClass}--${modName}`;
79
+ const content = modName === 'icon' ? '+' : 'Full Width';
80
+ const blockClass = modName === 'block' ? ' test-element--block' : '';
81
+
82
+ sections.push(`
83
+ <div class="test-section">
84
+ <div class="test-section-title">${sectionTitle}</div>
85
+ <div class="test-row">
86
+ <div class="${blockClass.trim()}">
87
+ <${element} class="${baseClass} ${modClass}">${content}</${element}>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ `);
92
+ }
93
+ }
94
+
95
+ // States section
96
+ if (api.states && api.states.length > 0) {
97
+ const stateButtons: string[] = [];
98
+ stateButtons.push(`<${element} class="${baseClass}">Normal</${element}>`);
99
+
100
+ if (api.states.includes('hover')) {
101
+ stateButtons.push(`<${element} class="${baseClass} ${baseClass}--hover">Hover</${element}>`);
102
+ }
103
+ if (api.states.includes('focus')) {
104
+ stateButtons.push(`<${element} class="${baseClass} ${baseClass}--focus">Focus</${element}>`);
105
+ }
106
+ if (api.states.includes('active')) {
107
+ stateButtons.push(
108
+ `<${element} class="${baseClass} ${baseClass}--active">Active</${element}>`,
109
+ );
110
+ }
111
+ if (api.states.includes('disabled')) {
112
+ stateButtons.push(`<${element} class="${baseClass}" disabled>Disabled</${element}>`);
113
+ }
114
+
115
+ sections.push(`
116
+ <div class="test-section">
117
+ <div class="test-section-title">States</div>
118
+ <div class="test-row">
119
+ ${stateButtons.join('\n ')}
120
+ </div>
121
+ </div>
122
+ `);
123
+ }
124
+
125
+ // Combinations section
126
+ const combos = generateCombinations(api);
127
+ if (combos.length > 0) {
128
+ const comboButtons = combos.map((combo) => {
129
+ const classes = combo.modifiers.map((m) => `${baseClass}--${m}`).join(' ');
130
+ const label = combo.modifiers.map((m) => capitalize(m)).join(' ');
131
+ const disabled = combo.disabled ? ' disabled' : '';
132
+ return `<${element} class="${baseClass} ${classes}"${disabled}>${label}</${element}>`;
133
+ });
134
+
135
+ sections.push(`
136
+ <div class="test-section">
137
+ <div class="test-section-title">Combinations</div>
138
+ <div class="test-grid">
139
+ ${comboButtons.join('\n ')}
140
+ </div>
141
+ </div>
142
+ `);
143
+ }
144
+
145
+ return `
146
+ <div class="test-container">
147
+ <h1 class="test-title">${title}</h1>
148
+ ${sections.join('')}
149
+ </div>
150
+ `;
151
+ }
@@ -0,0 +1,15 @@
1
+ export type { ComponentAPI } from './api-types';
2
+ export type { RhythmViolation } from './rhythm';
3
+ export { scaffoldCss } from './scaffold';
4
+ export { generateVariationsHtml } from './html-generator';
5
+ export { validateGridRhythm } from './rhythm';
6
+ export {
7
+ loadCss,
8
+ loadComponentApi,
9
+ loadDocsJson,
10
+ generateHtmlFromDocs,
11
+ setupVisualTest,
12
+ setupVisualTestFromApi,
13
+ setupVisualTestFromDocs,
14
+ saveForLostPixel,
15
+ } from './page-setup';
@@ -0,0 +1,131 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import type { Page } from '@playwright/test';
4
+
5
+ const LOSTPIXEL_DIR = resolve(__dirname, '../../.lostpixel');
6
+ import type { ComponentAPI } from './api-types';
7
+ import { generateVariationsHtml } from './html-generator';
8
+ import { scaffoldCss } from './scaffold';
9
+
10
+ interface DocsItem {
11
+ tag: string;
12
+ class: string;
13
+ text: string;
14
+ attrs?: Record<string, string>;
15
+ }
16
+
17
+ interface DocsExample {
18
+ html?: string;
19
+ items?: DocsItem[];
20
+ layout?: string;
21
+ title?: string;
22
+ }
23
+
24
+ interface DocsSection {
25
+ title: string;
26
+ examples: DocsExample[];
27
+ }
28
+
29
+ interface DocsJson {
30
+ sections: DocsSection[];
31
+ }
32
+
33
+ const DIST_PATH = resolve(__dirname, '../../dist');
34
+
35
+ export function loadCss(filename = 'index.css'): string {
36
+ return readFileSync(resolve(DIST_PATH, filename), 'utf-8');
37
+ }
38
+
39
+ export function loadComponentApi(apiPath: string): ComponentAPI {
40
+ return JSON.parse(readFileSync(apiPath, 'utf-8'));
41
+ }
42
+
43
+ function renderItem(item: DocsItem): string {
44
+ const attrs = item.attrs
45
+ ? Object.entries(item.attrs)
46
+ .map(([k, v]) => (v === '' ? k : `${k}="${v}"`))
47
+ .join(' ')
48
+ : '';
49
+ const attrStr = attrs ? ` ${attrs}` : '';
50
+ return `<${item.tag} class="${item.class}"${attrStr}>${item.text}</${item.tag}>`;
51
+ }
52
+
53
+ export function loadDocsJson(docsPath: string): DocsJson {
54
+ return JSON.parse(readFileSync(docsPath, 'utf-8'));
55
+ }
56
+
57
+ export function generateHtmlFromDocs(docs: DocsJson): string {
58
+ const sections: string[] = [];
59
+
60
+ for (const section of docs.sections) {
61
+ const examples: string[] = [];
62
+
63
+ for (const example of section.examples) {
64
+ let html = '';
65
+
66
+ if (example.html) {
67
+ html = example.html;
68
+ } else if (example.items) {
69
+ const items = example.items.map(renderItem).join('\n');
70
+ html = example.layout === 'cluster' ? `<div class="ui-cluster">${items}</div>` : items;
71
+ }
72
+
73
+ if (html) {
74
+ examples.push(`<div class="test-example">${html}</div>`);
75
+ }
76
+ }
77
+
78
+ sections.push(`
79
+ <div class="test-section">
80
+ <div class="test-section-title">${section.title}</div>
81
+ ${examples.join('\n')}
82
+ </div>
83
+ `);
84
+ }
85
+
86
+ return `<div class="test-container">${sections.join('')}</div>`;
87
+ }
88
+
89
+ export async function setupVisualTest(
90
+ page: Page,
91
+ options: {
92
+ html: string;
93
+ css?: string;
94
+ includeTokens?: boolean;
95
+ },
96
+ ): Promise<void> {
97
+ const { html, css, includeTokens = true } = options;
98
+ const componentCss = includeTokens ? loadCss() : css || '';
99
+
100
+ const fullHtml = `
101
+ <!DOCTYPE html>
102
+ <html>
103
+ <head>
104
+ <meta charset="utf-8">
105
+ <style>${scaffoldCss}</style>
106
+ <style>${componentCss}</style>
107
+ </head>
108
+ <body>${html}</body>
109
+ </html>
110
+ `;
111
+
112
+ await page.setContent(fullHtml);
113
+ }
114
+
115
+ export async function setupVisualTestFromApi(page: Page, apiPath: string): Promise<void> {
116
+ const api = loadComponentApi(apiPath);
117
+ const html = generateVariationsHtml(api);
118
+ await setupVisualTest(page, { html });
119
+ }
120
+
121
+ export async function setupVisualTestFromDocs(page: Page, docsPath: string): Promise<void> {
122
+ const docs = loadDocsJson(docsPath);
123
+ const html = generateHtmlFromDocs(docs);
124
+ await setupVisualTest(page, { html });
125
+ }
126
+
127
+ export async function saveForLostPixel(page: Page, name: string): Promise<void> {
128
+ mkdirSync(LOSTPIXEL_DIR, { recursive: true });
129
+ const screenshot = await page.screenshot({ fullPage: true });
130
+ writeFileSync(resolve(LOSTPIXEL_DIR, `${name}.png`), screenshot);
131
+ }
@@ -0,0 +1,146 @@
1
+ import type { Page } from '@playwright/test';
2
+
3
+ // Elements that are fluid containers (viewport-dependent)
4
+ const SKIP_SELECTORS = [
5
+ 'html',
6
+ 'body',
7
+ 'main',
8
+ 'aside',
9
+ '.ui-sidebar',
10
+ '.ui-main',
11
+ '.ui-app-shell',
12
+ '.ui-drawer', // Fluid panel - adapts to container/viewport
13
+ ];
14
+
15
+ // Inline elements that don't follow grid
16
+ const INLINE_TAGS = [
17
+ 'STRONG',
18
+ 'B',
19
+ 'EM',
20
+ 'I',
21
+ 'SMALL',
22
+ 'SPAN',
23
+ 'A',
24
+ 'CODE',
25
+ 'ABBR',
26
+ 'CITE',
27
+ 'Q',
28
+ 'SUB',
29
+ 'SUP',
30
+ 'MARK',
31
+ 'BR',
32
+ 'WBR',
33
+ ];
34
+
35
+ export interface RhythmViolation {
36
+ selector: string;
37
+ height: number;
38
+ expected: number;
39
+ offBy: number;
40
+ }
41
+
42
+ /**
43
+ * Validates that all elements in a component follow the 8px grid rhythm
44
+ * Throws an error if violations are found
45
+ */
46
+ export async function validateGridRhythm(
47
+ page: Page,
48
+ componentName: string,
49
+ ): Promise<RhythmViolation[]> {
50
+ const violations = await page.evaluate(
51
+ ({ skipSelectors, inlineTags, component }) => {
52
+ // Get unit from CSS custom property (default 8px)
53
+ // Note: PostCSS prefixes --unit to --ui-unit in compiled CSS
54
+ const temp = document.createElement('div');
55
+ temp.style.cssText = 'position:absolute;visibility:hidden;width:var(--ui-unit, 8px)';
56
+ document.body.appendChild(temp);
57
+ const unit = temp.getBoundingClientRect().width || 8;
58
+ document.body.removeChild(temp);
59
+
60
+ const results: Array<{ selector: string; height: number; expected: number; offBy: number }> =
61
+ [];
62
+
63
+ // Target component elements
64
+ const selector = `.ui-${component}, .ui-${component} *`;
65
+ const elements = document.querySelectorAll(selector);
66
+
67
+ for (const el of elements) {
68
+ const tagName = el.tagName;
69
+
70
+ // Skip script, style, SVG internals, etc.
71
+ const upperTag = tagName.toUpperCase();
72
+ if (
73
+ [
74
+ 'SCRIPT',
75
+ 'STYLE',
76
+ 'LINK',
77
+ 'META',
78
+ 'TITLE',
79
+ 'HEAD',
80
+ 'SVG',
81
+ 'PATH',
82
+ 'CIRCLE',
83
+ 'RECT',
84
+ 'LINE',
85
+ 'POLYGON',
86
+ 'POLYLINE',
87
+ 'ELLIPSE',
88
+ 'G',
89
+ 'DEFS',
90
+ 'USE',
91
+ 'SYMBOL',
92
+ 'CLIPPATH',
93
+ 'MASK',
94
+ 'PATTERN',
95
+ 'IMAGE',
96
+ 'TEXT',
97
+ 'TSPAN',
98
+ 'TEXTPATH',
99
+ 'FOREIGNOBJECT',
100
+ ].includes(upperTag)
101
+ )
102
+ continue;
103
+
104
+ // Skip inline elements
105
+ if (inlineTags.includes(tagName)) continue;
106
+
107
+ // Skip BEM element children (__element) - only check blocks and modifiers
108
+ const classes = el.className?.toString?.() || '';
109
+ if (classes.includes('__')) continue;
110
+
111
+ // Skip fluid containers
112
+ if (skipSelectors.some((sel) => el.matches(sel))) continue;
113
+
114
+ const height = el.getBoundingClientRect().height;
115
+ // Skip zero-height and decorative lines (borders, dividers < half unit)
116
+ if (height === 0 || height < unit / 2) continue;
117
+
118
+ const remainder = height % unit;
119
+ const isAligned = remainder < 0.5 || remainder > unit - 0.5;
120
+
121
+ if (!isAligned) {
122
+ const firstClass = classes.split(' ')[0];
123
+ results.push({
124
+ selector: `${tagName.toLowerCase()}${firstClass ? `.${firstClass}` : ''}`,
125
+ height: Math.round(height * 10) / 10,
126
+ expected: Math.round(height / unit) * unit,
127
+ offBy: Math.round(remainder * 10) / 10,
128
+ });
129
+ }
130
+ }
131
+
132
+ return results;
133
+ },
134
+ { skipSelectors: SKIP_SELECTORS, inlineTags: INLINE_TAGS, component: componentName },
135
+ );
136
+
137
+ if (violations.length > 0) {
138
+ console.log(`Grid rhythm violations in ${componentName}:`);
139
+ console.table(violations);
140
+ throw new Error(
141
+ `Grid rhythm violations in ${componentName}:\n${JSON.stringify(violations, null, 2)}`,
142
+ );
143
+ }
144
+
145
+ return violations;
146
+ }
@@ -0,0 +1,50 @@
1
+ export const scaffoldCss = `
2
+ html {
3
+ font-size: 16px;
4
+ }
5
+ body {
6
+ background-image:
7
+ linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
8
+ linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
9
+ linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
10
+ linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
11
+ background-size: 12px 12px;
12
+ background-position: 0 0, 0 6px, 6px -6px, -6px 0px;
13
+ background-color: #fafafa;
14
+ }
15
+ .test-container {
16
+ padding: 1.5rem;
17
+ }
18
+ .test-title {
19
+ font-family: var(--ui-font-sans, system-ui, sans-serif);
20
+ font-size: 1.5rem;
21
+ font-weight: 600;
22
+ margin-block-end: 1.5rem;
23
+ }
24
+ .test-section {
25
+ margin-block-end: 1.5rem;
26
+ }
27
+ .test-section-title {
28
+ font-family: var(--ui-font-sans, system-ui, sans-serif);
29
+ font-size: 0.75rem;
30
+ color: #666;
31
+ text-transform: uppercase;
32
+ letter-spacing: 0.05em;
33
+ margin-block-end: 0.5rem;
34
+ }
35
+ .test-row {
36
+ display: flex;
37
+ gap: 1rem;
38
+ align-items: center;
39
+ flex-wrap: wrap;
40
+ }
41
+ .test-grid {
42
+ display: grid;
43
+ grid-template-columns: repeat(auto-fill, minmax(100px, max-content));
44
+ gap: 1rem;
45
+ align-items: start;
46
+ }
47
+ .test-element--block {
48
+ width: 200px;
49
+ }
50
+ `;