@brokenrubik/ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +264 -2
  2. package/dist/components/accordion.d.ts +13 -0
  3. package/dist/components/accordion.d.ts.map +1 -0
  4. package/dist/components/accordion.js +25 -0
  5. package/dist/components/accordion.js.map +1 -0
  6. package/dist/components/alert-dialog.d.ts +21 -0
  7. package/dist/components/alert-dialog.d.ts.map +1 -0
  8. package/dist/components/alert-dialog.js +46 -0
  9. package/dist/components/alert-dialog.js.map +1 -0
  10. package/dist/components/alert.d.ts +24 -0
  11. package/dist/components/alert.d.ts.map +1 -0
  12. package/dist/components/alert.js +42 -0
  13. package/dist/components/alert.js.map +1 -0
  14. package/dist/components/avatar.d.ts +27 -0
  15. package/dist/components/avatar.d.ts.map +1 -0
  16. package/dist/components/avatar.js +53 -0
  17. package/dist/components/avatar.js.map +1 -0
  18. package/dist/components/badge.d.ts +21 -0
  19. package/dist/components/badge.d.ts.map +1 -0
  20. package/dist/components/badge.js +40 -0
  21. package/dist/components/badge.js.map +1 -0
  22. package/dist/components/button.d.ts +18 -0
  23. package/dist/components/button.d.ts.map +1 -0
  24. package/dist/components/button.js +39 -0
  25. package/dist/components/button.js.map +1 -0
  26. package/dist/components/calendar.d.ts +13 -0
  27. package/dist/components/calendar.d.ts.map +1 -0
  28. package/dist/components/calendar.js +68 -0
  29. package/dist/components/calendar.js.map +1 -0
  30. package/dist/components/card.d.ts +15 -0
  31. package/dist/components/card.d.ts.map +1 -0
  32. package/dist/components/card.js +31 -0
  33. package/dist/components/card.js.map +1 -0
  34. package/dist/components/checkbox.d.ts +6 -0
  35. package/dist/components/checkbox.d.ts.map +1 -0
  36. package/dist/components/checkbox.js +12 -0
  37. package/dist/components/checkbox.js.map +1 -0
  38. package/dist/components/command.d.ts +24 -0
  39. package/dist/components/command.d.ts.map +1 -0
  40. package/dist/components/command.js +41 -0
  41. package/dist/components/command.js.map +1 -0
  42. package/dist/components/date-picker.d.ts +24 -0
  43. package/dist/components/date-picker.d.ts.map +1 -0
  44. package/dist/components/date-picker.js +18 -0
  45. package/dist/components/date-picker.js.map +1 -0
  46. package/dist/components/dialog.d.ts +23 -0
  47. package/dist/components/dialog.d.ts.map +1 -0
  48. package/dist/components/dialog.js +45 -0
  49. package/dist/components/dialog.js.map +1 -0
  50. package/dist/components/dropdown-menu.d.ts +34 -0
  51. package/dist/components/dropdown-menu.d.ts.map +1 -0
  52. package/dist/components/dropdown-menu.js +61 -0
  53. package/dist/components/dropdown-menu.js.map +1 -0
  54. package/dist/components/empty-state.d.ts +13 -0
  55. package/dist/components/empty-state.d.ts.map +1 -0
  56. package/dist/components/empty-state.js +25 -0
  57. package/dist/components/empty-state.js.map +1 -0
  58. package/dist/components/field.d.ts +24 -0
  59. package/dist/components/field.d.ts.map +1 -0
  60. package/dist/components/field.js +34 -0
  61. package/dist/components/field.js.map +1 -0
  62. package/dist/components/input.d.ts +17 -0
  63. package/dist/components/input.d.ts.map +1 -0
  64. package/dist/components/input.js +18 -0
  65. package/dist/components/input.js.map +1 -0
  66. package/dist/components/kbd.d.ts +14 -0
  67. package/dist/components/kbd.d.ts.map +1 -0
  68. package/dist/components/kbd.js +31 -0
  69. package/dist/components/kbd.js.map +1 -0
  70. package/dist/components/label.d.ts +6 -0
  71. package/dist/components/label.d.ts.map +1 -0
  72. package/dist/components/label.js +11 -0
  73. package/dist/components/label.js.map +1 -0
  74. package/dist/components/pagination.d.ts +18 -0
  75. package/dist/components/pagination.d.ts.map +1 -0
  76. package/dist/components/pagination.js +33 -0
  77. package/dist/components/pagination.js.map +1 -0
  78. package/dist/components/popover.d.ts +12 -0
  79. package/dist/components/popover.d.ts.map +1 -0
  80. package/dist/components/popover.js +29 -0
  81. package/dist/components/popover.js.map +1 -0
  82. package/dist/components/progress.d.ts +17 -0
  83. package/dist/components/progress.d.ts.map +1 -0
  84. package/dist/components/progress.js +26 -0
  85. package/dist/components/progress.js.map +1 -0
  86. package/dist/components/radio-group.d.ts +7 -0
  87. package/dist/components/radio-group.d.ts.map +1 -0
  88. package/dist/components/radio-group.js +14 -0
  89. package/dist/components/radio-group.js.map +1 -0
  90. package/dist/components/select.d.ts +20 -0
  91. package/dist/components/select.d.ts.map +1 -0
  92. package/dist/components/select.js +46 -0
  93. package/dist/components/select.js.map +1 -0
  94. package/dist/components/separator.d.ts +6 -0
  95. package/dist/components/separator.d.ts.map +1 -0
  96. package/dist/components/separator.js +11 -0
  97. package/dist/components/separator.js.map +1 -0
  98. package/dist/components/sheet.d.ts +19 -0
  99. package/dist/components/sheet.d.ts.map +1 -0
  100. package/dist/components/sheet.js +47 -0
  101. package/dist/components/sheet.js.map +1 -0
  102. package/dist/components/skeleton.d.ts +5 -0
  103. package/dist/components/skeleton.d.ts.map +1 -0
  104. package/dist/components/skeleton.js +9 -0
  105. package/dist/components/skeleton.js.map +1 -0
  106. package/dist/components/sonner.d.ts +13 -0
  107. package/dist/components/sonner.d.ts.map +1 -0
  108. package/dist/components/sonner.js +40 -0
  109. package/dist/components/sonner.js.map +1 -0
  110. package/dist/components/switch.d.ts +6 -0
  111. package/dist/components/switch.d.ts.map +1 -0
  112. package/dist/components/switch.js +11 -0
  113. package/dist/components/switch.js.map +1 -0
  114. package/dist/components/tabs.d.ts +13 -0
  115. package/dist/components/tabs.d.ts.map +1 -0
  116. package/dist/components/tabs.js +24 -0
  117. package/dist/components/tabs.js.map +1 -0
  118. package/dist/components/textarea.d.ts +5 -0
  119. package/dist/components/textarea.d.ts.map +1 -0
  120. package/dist/components/textarea.js +10 -0
  121. package/dist/components/textarea.js.map +1 -0
  122. package/dist/components/toggle-group.d.ts +11 -0
  123. package/dist/components/toggle-group.d.ts.map +1 -0
  124. package/dist/components/toggle-group.js +18 -0
  125. package/dist/components/toggle-group.js.map +1 -0
  126. package/dist/components/tooltip.d.ts +13 -0
  127. package/dist/components/tooltip.d.ts.map +1 -0
  128. package/dist/components/tooltip.js +24 -0
  129. package/dist/components/tooltip.js.map +1 -0
  130. package/dist/index.d.ts +34 -0
  131. package/dist/index.d.ts.map +1 -0
  132. package/dist/index.js +36 -0
  133. package/dist/index.js.map +1 -0
  134. package/dist/lib/tone.d.ts +8 -0
  135. package/dist/lib/tone.d.ts.map +1 -0
  136. package/dist/lib/tone.js +14 -0
  137. package/dist/lib/tone.js.map +1 -0
  138. package/dist/lib/utils.d.ts +20 -0
  139. package/dist/lib/utils.d.ts.map +1 -0
  140. package/dist/lib/utils.js +40 -0
  141. package/dist/lib/utils.js.map +1 -0
  142. package/dist/styles/base.css +37 -0
  143. package/dist/styles/fonts.css +16 -0
  144. package/dist/styles/styles.css +14 -0
  145. package/dist/styles/theme.css +170 -0
  146. package/dist/styles/tokens.css +228 -0
  147. package/docs/design-handoff.md +112 -0
  148. package/docs/figma-vs-tailwind.md +135 -0
  149. package/docs/visual-testing.md +156 -0
  150. package/package.json +96 -4
  151. package/src/components/accordion.tsx +66 -0
  152. package/src/components/alert-dialog.tsx +139 -0
  153. package/src/components/alert.tsx +87 -0
  154. package/src/components/avatar.tsx +105 -0
  155. package/src/components/badge.tsx +63 -0
  156. package/src/components/button.tsx +70 -0
  157. package/src/components/calendar.tsx +131 -0
  158. package/src/components/card.tsx +72 -0
  159. package/src/components/checkbox.tsx +31 -0
  160. package/src/components/command.tsx +149 -0
  161. package/src/components/date-picker.tsx +86 -0
  162. package/src/components/dialog.tsx +126 -0
  163. package/src/components/dropdown-menu.tsx +225 -0
  164. package/src/components/empty-state.tsx +63 -0
  165. package/src/components/field.tsx +79 -0
  166. package/src/components/input.tsx +49 -0
  167. package/src/components/kbd.tsx +48 -0
  168. package/src/components/label.tsx +22 -0
  169. package/src/components/pagination.tsx +94 -0
  170. package/src/components/popover.tsx +65 -0
  171. package/src/components/progress.tsx +52 -0
  172. package/src/components/radio-group.tsx +42 -0
  173. package/src/components/select.tsx +167 -0
  174. package/src/components/separator.tsx +29 -0
  175. package/src/components/sheet.tsx +112 -0
  176. package/src/components/skeleton.tsx +16 -0
  177. package/src/components/sonner.tsx +58 -0
  178. package/src/components/switch.tsx +27 -0
  179. package/src/components/tabs.tsx +56 -0
  180. package/src/components/textarea.tsx +17 -0
  181. package/src/components/toggle-group.tsx +42 -0
  182. package/src/components/tooltip.tsx +55 -0
  183. package/src/index.ts +35 -0
  184. package/src/lib/tone.ts +16 -0
  185. package/src/lib/utils.ts +46 -0
  186. package/src/styles/base.css +37 -0
  187. package/src/styles/fonts.css +16 -0
  188. package/src/styles/styles.css +14 -0
  189. package/src/styles/theme.css +170 -0
  190. package/src/styles/tokens.css +228 -0
@@ -0,0 +1,135 @@
1
+ # BR Admin Library vs Tailwind / shadcn defaults
2
+
3
+ What the BR Admin Library (Figma `ONZyUryZr526mtbex2HVPk`, pages *Foundations* `2004:2` and
4
+ *Components · Base* `2004:3`) changes compared with Tailwind v4 and shadcn/ui out of the box.
5
+ Read from the Figma file on 2026-11-09; colors checked again through the Figma MCP on 2026-11-11. Every value here is in `src/styles/tokens.css` or
6
+ `src/styles/theme.css`.
7
+
8
+ ## 1. Color tokens
9
+
10
+ Token names are the shadcn names, so `var(--primary)` in code is `primary` in Figma.
11
+
12
+ | Token | shadcn default (neutral) | BR light | BR dark |
13
+ | --- | --- | --- | --- |
14
+ | `background` | white | `#ffffff` | `#0f172a` (slate-900) |
15
+ | `foreground` | neutral-950 | `#0f172a` | `#e2e8f0` |
16
+ | `card` / `popover` | white | `#ffffff` | `#162032` (slate-875, not in Tailwind) |
17
+ | `primary` | neutral-900 | **`#9547ff` BR violet** | `#9547ff` |
18
+ | `primary-foreground` | neutral-50 | `#ffffff` | `#ffffff` |
19
+ | `secondary`, `muted`, `accent` | neutral-100 | `#f1f5f9` (slate-100) | `#1e293b`; accent `rgb(255 255 255 / .05)` |
20
+ | `muted-foreground` | neutral-500 | `#64748b` (slate-500) | `#94a3b8` |
21
+ | `border` | neutral-200 | `#e2e8f0` (slate-200) | `rgb(255 255 255 / .1)` |
22
+ | `input` | = border | **`#8391a7`** (3:1 contrast, WCAG non-text) | `#64748b` |
23
+ | `ring` | neutral-400 | `#9547ff` | `#9547ff` |
24
+ | `destructive` | red-ish oklch | `#dc2626` (red-600) | `#dc2626` |
25
+
26
+ **Tokens that shadcn does not have** (all in Figma):
27
+
28
+ - `primary-strong` `#6b1fd0` / `#a875ff` and `primary-tint` `#f4edff` / violet 20 %: violet as
29
+ text and as a soft fill. Rule: never use `primary` for text.
30
+ - State triads, *base* (dots, bars) + `-strong` (text, icons) + `-tint` (soft fills):
31
+ `success` `#10b981` / `#047857` / `#ecfdf5`, `warning` `#f59e0b` / `#b45309` / `#fffbeb`,
32
+ `destructive` `#dc2626` / `#dc2626` / `#fef2f2`. Dark: strong = 400 shades, tint = base 15 %.
33
+ - `highlight` `#dff95f` BR lime, "you are here" only, with `highlight-foreground` `#0f172a`.
34
+ - `brand-coral` `#ff707a`: marketing only.
35
+ - `sidebar-*`: navy sidebar in both themes (`#0f172a` light, `#0b1120` dark), lime marker, plus
36
+ `sidebar-muted-foreground`, `sidebar-label` and `sidebar-accent-strong` (shadcn has none of
37
+ them). `sidebar-ring` is violet `#a875ff` on the navy sidebar and `#9547ff` on the light one.
38
+ - Two more Figma modes, *Light · Sidebar claro* and *Dark · Sidebar claro*, re-point only the
39
+ sidebar tokens (`.sidebar-light` on `<html>`, in either theme). In the dark one, the `app-*`
40
+ colors keep their light values.
41
+ - `info` `#3b82f6` / `info-strong` `#1d4ed8` / `info-tint` `#eff6ff` (dark: `#3b82f6` /
42
+ `#60a5fa` / blue 15 %), the same triad as the other states.
43
+ - `overlay`: the dialog and sheet backdrop, black at 50 % (dark: 60 %).
44
+ - `surface-subtle` `#fbfdfd` (dark: = `background`): a slightly raised page area.
45
+ - `chart-1…5`: violet, slate-500, violet-300 `#c4a6ff`, slate-300, violet-700.
46
+ - `app-proma`, `app-folio`, `app-docubuilder`, `app-timma`, `app-mara`: app identity colors
47
+ (logo, app icon, App Rail only).
48
+
49
+ **Source:** every color above is a Figma variable in the *Theme* collection with the code syntax
50
+ `var(--name)`. The values were read through the Figma MCP on 2026-11-11
51
+ (`visual/figma/variables.json`). The only token that is not a Figma variable is `--radius`, the
52
+ base that the radius scale is derived from.
53
+
54
+ ## 2. Radius
55
+
56
+ | | shadcn (`--radius: .625rem`) | Tailwind v4 default | **BR (Figma radius/\*)** |
57
+ | --- | --- | --- | --- |
58
+ | xs | – | 2 px | **4 px** (checkbox, muted key chip) |
59
+ | sm | 6 px | 4 px | **5 px** (menu item, toggle item, close button) |
60
+ | md | 8 px | 6 px | **6 px** (input, select, badge, icon button, menu panel, tooltip) |
61
+ | lg | 10 px | 8 px | **8 px** (button, card, alert, popover, toast) |
62
+ | xl | 14 px | 12 px | **12 px** (dialog) |
63
+
64
+ One knob, as in shadcn: `--radius: 0.5rem`; the scale is `lg = radius`, `md = −2`, `sm = −3`,
65
+ `xs = −4`, `xl = +4`.
66
+
67
+ ## 3. Shadows (Figma effect styles, slate-900 tinted, not black)
68
+
69
+ | | Tailwind v4 | **BR** |
70
+ | --- | --- | --- |
71
+ | `shadow-xs` | `0 1px 2px 0 rgb(0 0 0 / .05)` | `0 1px 2px 0 rgb(15 23 42 / .05)` |
72
+ | `shadow-md` | `0 4px 6px -1px / .1, 0 2px 4px -2px / .1` | `0 4px 12px -2px / .08, 0 2px 4px -2px / .05` |
73
+ | `shadow-lg` | `0 10px 15px -3px / .1, 0 4px 6px -4px / .1` | `0 16px 32px -8px / .16, 0 4px 8px -4px / .06` |
74
+
75
+ Cards are flat (no shadow). Outline buttons, inputs, switch thumb: `shadow-xs`. Menus,
76
+ popovers, tooltips, calendar: `shadow-md`. Dialogs, sheets, toasts, command: `shadow-lg`.
77
+
78
+ ## 4. Typography
79
+
80
+ Tailwind: system font, `text-sm` = 14/20 is the usual UI size. **BR: IBM Plex Sans / Plex Mono
81
+ (the package loads them, `fonts.css`), a dense 13 px base.** The Figma text styles are `text-*`
82
+ utilities (size, line height, tracking, weight); Tailwind's own `text-xs … text-9xl` stay.
83
+
84
+ | Utility | Figma style | Size / line | Weight | Tracking |
85
+ | --- | --- | --- | --- | --- |
86
+ | `text-display` | Heading/Display | 32/40 | 600 | −0.48 |
87
+ | `text-page-title` | Heading/Page title | 20/28 | 600 | −0.4 |
88
+ | `text-section` | Heading/Section | 15/20 | 600 | −0.2 |
89
+ | `text-card-title` | Heading/Card | 14/20 | 600 | −0.1 |
90
+ | `text-body-lg` | Body/Large | 15/22 | 400 | 0 |
91
+ | `text-body` | Body/Default | **13/20** | 400 | 0 |
92
+ | `text-body-sm` | Body/Small | 12/16 | 400 | 0 |
93
+ | `text-caption` | Label/Caption | 11/16 | 400 | 0 |
94
+ | `text-overline` | Label/Overline (+ `uppercase`) | 11/16 | 500 | 0.4 |
95
+ | `text-nav` / `text-nav-label` | Nav/Item, Nav/Group label | 14/20, 12/16 | 500 | −0.15, 0 |
96
+ | `text-initials`, `-sm` | Label/Initials (small) | 11/16, 8/12 | 600 | 0.2 |
97
+ | `text-mono`, `-sm` | Mono/Default, Mono/Small | 12/16, 11/16 | 400 | 0 |
98
+ | `text-doc-title`, `-h2`, `-body` | Doc/\* | 36/44, 20/28, 15/26 | 600, 600, 400 | |
99
+
100
+ "Medium" styles = the same utility + `font-medium`.
101
+
102
+ ## 5. Spacing
103
+
104
+ No change: Figma uses Tailwind's 4 px grid (rows and controls 4–12, sections 24–48).
105
+
106
+ ## 6. Components vs shadcn new-york v4
107
+
108
+ | Component | shadcn default | BR (Figma) |
109
+ | --- | --- | --- |
110
+ | Button | h-9 / sm h-8, `rounded-md`, `text-sm` | **h-8 / sm h-7**, 8 px radius, Body/Default Medium (13) / Body/Small Medium; secondary = slate fill (not outline) |
111
+ | Icon button | size-9 | **28 / 24 px**, 6 px radius |
112
+ | Input, Select, Date picker | h-9, `border-input` = border color, 3 px ring/50 focus | **h-8**, `input` border `#8391a7`, focus = 1.5 px violet border, Shadow/xs |
113
+ | Badge | pill, solid colors | **20 px, 6 px radius, tint + strong text**, 7 tones |
114
+ | Card | `rounded-xl`, py-6, gap-6, shadow-sm | **8 px radius, p-4, flat** |
115
+ | Dialog | max-w-lg, `rounded-lg`, p-6 | **480 px, 12 px radius**, title 20/28 |
116
+ | Tabs | segmented pill in a muted list | **underline tabs** (40 px, violet 2 px underline); segmented = ToggleGroup |
117
+ | Toggle group | outline items | muted track, white "on" item with Shadow/xs |
118
+ | Menu item | py-1.5, `text-sm` | **32 px**, 5 px radius, Body/Default; shortcut Label/Caption |
119
+ | Alert | card with red text for destructive | **tinted per tone**, -strong text and icon; info = bordered, violet icon |
120
+ | Tooltip | primary fill + arrow | **foreground fill (inverted), no arrow**, Shadow/md |
121
+ | Avatar | grey fallback | 20 / 28 px, **tinted per person** (purple, amber, red, emerald, slate) |
122
+ | Kbd ("Key Chip") | not in shadcn | lime space key (20 / 24) and muted mono hint |
123
+ | Empty state | not in shadcn | bordered box, 40 px icon tile |
124
+
125
+ ## 7. Drift found in the apps (2026-11-09)
126
+
127
+ | App | Difference from Figma |
128
+ | --- | --- |
129
+ | Proma | `accent` is violet (`#f5effe` / `#6b21d6`) instead of slate; `radius-sm` 4 px (Figma 5); own shadow scale |
130
+ | Timma | dark `accent` `#1e293b` (Figma `rgb(255 255 255 / .05)`); Button `secondary` = outline and primary is semibold (Figma: slate fill, medium) |
131
+ | Mara, Folio | dark `accent` `#1e293b`; ~200 lines of tokens copied per app |
132
+ | DocuBuilder | still shadcn defaults (neutral oklch, `--radius: .625rem`) |
133
+
134
+ Each app keeps its own copy of the tokens today. With this package they import one file and
135
+ keep only their real exceptions (see README › Extend).
@@ -0,0 +1,156 @@
1
+ # Visual testing against Figma
2
+
3
+ The visual test proves that the components look like the BR Admin Library in Figma. It runs in
4
+ CI on every pull request. This page tells how it works, how to run it, how to read the result,
5
+ and how to change it. Read it before you change a component or the test.
6
+
7
+ ## What the test checks
8
+
9
+ For each component set on the Figma page *Components · Base*:
10
+
11
+ 1. **The spec page draws the set.** `/?spec=Button` renders every variant of the Figma set
12
+ "Button" at its Figma x/y position, in a box of the same size as the Figma set. The
13
+ positions come from `playground/src/spec/figma-layout.json`.
14
+ 2. **Pixel diff.** Playwright takes a screenshot of that box at 2x. `pixelmatch` compares it with
15
+ the Figma PNG export (`visual/figma/<node-id>.png`, also 2x). The result is the percentage of
16
+ pixels that differ.
17
+ 3. **Geometry.** In the browser, the test measures each variant (width, height) and the texts and
18
+ icons in it. It compares them with the Figma boxes in `visual/figma/geometry.json`.
19
+
20
+ ### Pass and fail
21
+
22
+ | Check | Fails when |
23
+ | --- | --- |
24
+ | Pixel diff | The % is more than the set's value in `visual/baseline.json` + 0.5. A set with a Figma PNG but no baseline also fails. |
25
+ | Size, fixed side | Width or height differs by 0.75 px or more. |
26
+ | Size, "hug" side | Width or height differs by 2.5 px or more. (Figma "hug" means the size follows the text. Chrome draws text up to ±1 px wider or narrower per word than Figma.) |
27
+ | Image size | The screenshot and the Figma PNG have different sizes. |
28
+ | Text and icon positions | Never fails. Differences of 0.75 px or more show as warnings (`--verbose`). |
29
+
30
+ ### Why there is a baseline and not 0 %
31
+
32
+ Chrome and Figma draw letters differently (rasterization and glyph widths). A component that is
33
+ correct still has 0–3 % of pixels that differ, all on the text edges. The baseline records that
34
+ accepted value for each set. A real change (a wrong padding, color or radius) adds more than
35
+ 0.5 points or fails the geometry check.
36
+
37
+ To confirm that the test catches errors, change `h-8` to `h-9` in the Button default size and
38
+ run `pnpm visual -- --sets Button`: it fails with `H+4 px` on each md variant and a higher pixel
39
+ diff.
40
+
41
+ ## Run it
42
+
43
+ ```bash
44
+ pnpm visual # build + test all sets
45
+ node scripts/visual/compare.mjs --sets Button,Badge # only these sets (needs a build first)
46
+ node scripts/visual/compare.mjs --url http://localhost:5173 # against a running dev server
47
+ node scripts/visual/compare.mjs --verbose # also print the warnings
48
+ pnpm visual:update # build + test + accept the results
49
+ ```
50
+
51
+ Without `--url`, the script starts `vite preview` on port 4179 with `playground/dist`, so
52
+ build first (`pnpm site:build`; `pnpm visual` does it). Chromium must be installed once:
53
+ `pnpm exec playwright install chromium`.
54
+
55
+ Exit code: `0` = all pass, `1` = a set failed, `2` = setup error (no build, no token).
56
+
57
+ ## Read the result
58
+
59
+ The terminal prints one line per set:
60
+
61
+ ```
62
+ PASS Badge 1.7 % (baseline 1.7 %)
63
+ FAIL Button 2.21 % (baseline 1.28 %)
64
+ ✗ Variant=Primary, Size=md: H+4 px
65
+ ✗ pixel diff 2.21 % > baseline 1.28 % + 0.5
66
+ NO-SPEC Tooltip –
67
+ ```
68
+
69
+ - `H+4 px`: the code is 4 px higher than Figma. `W-1`: 1 px narrower.
70
+ - `NO-SPEC`: `Spec.tsx` has no renderer for this set, so the set is not tested.
71
+
72
+ Open `visual/out/report.html` in a browser. For each set it shows the Figma PNG, the code
73
+ screenshot and the diff image (changed pixels in pink). In CI, download the artifact
74
+ *visual-report* from the workflow run and open `out/report.html`.
75
+
76
+ How to find the cause:
77
+
78
+ 1. Look at the diff image. Pink only on letter edges = text rasterization (normal). Pink on a
79
+ whole edge, a whole box or a color area = a real difference.
80
+ 2. A size error tells which variant and which side. Compare the component classes with the
81
+ Figma values (padding, height, gap, border width).
82
+ 3. Run with `--verbose` to see which text or icon moved, and by how many px.
83
+
84
+ ## Change the test
85
+
86
+ ### Accept a new baseline
87
+
88
+ Run `pnpm visual:update` only when you know why the numbers changed:
89
+
90
+ - The component now matches Figma better (the number went down).
91
+ - Figma changed and you updated the component to match.
92
+ - A new Chromium or font version changes the text rendering of all sets a little.
93
+
94
+ Commit `visual/baseline.json` with the change and write the reason in the pull request.
95
+
96
+ **CI and local machines.** CI runs Chromium from Playwright 1.49.1 on Ubuntu 24.04 with the
97
+ IBM Plex fonts that the package loads (`src/styles/fonts.css`, files from `@fontsource`). A different machine can give slightly different numbers. If CI fails
98
+ by a small amount on all sets, take the values from the CI artifact (`out/results.json`, field
99
+ `mismatch`) and put them in `visual/baseline.json`.
100
+
101
+ ### Add a set
102
+
103
+ 1. Make sure the set is in `playground/src/spec/figma-layout.json` (run `pnpm figma:pull` if it
104
+ is new in Figma).
105
+ 2. In `playground/src/spec/Spec.tsx`, add a renderer to `renderers` with the exact Figma set
106
+ name as the key. The function gets the variant props (`p.Variant`, `p.Size`, `p.State`…)
107
+ and returns the component in that state, with the same texts and icons as Figma.
108
+ - States that need interaction (focus, hover): add the classes of that state (see
109
+ `FOCUS`).
110
+ - Overlays (menus, dialogs) render in a portal. Use `pin(v)` for the position and put
111
+ `data-spec-portal={v.name}` on the content, so the geometry check measures it.
112
+ - If the measured element is not the first element, put `data-spec-root` on the correct one.
113
+ 3. Open `/?spec=<Set name>` in the dev server and compare with Figma by eye.
114
+ 4. `pnpm visual -- --sets "<Set name>"`. Fix the component until the size errors are gone.
115
+ 5. `pnpm visual:update` to record its baseline.
116
+
117
+ Sets without a renderer today: Menu Item, Tooltip, Calendar Day, Popover, Sheet, Command, Toast,
118
+ Pagination.
119
+
120
+ ### Update the Figma reference
121
+
122
+ ```bash
123
+ FIGMA_TOKEN=figd_… pnpm figma:pull # layout + geometry + PNGs
124
+ FIGMA_TOKEN=figd_… pnpm figma:pull -- --no-images # layout + geometry only
125
+ ```
126
+
127
+ The token is a Figma personal access token with the scope `file_content:read` (Figma →
128
+ Settings → Security → Personal access tokens). Figma limits the API rate. On HTTP 429 the
129
+ script waits and tries again up to 5 times. If it still fails, wait some minutes.
130
+
131
+ After a pull, `visual/figma/meta.json` has the Figma file version. Commit the pulled files with
132
+ the component changes that match them.
133
+
134
+ ## Use the tools in another project
135
+
136
+ The scripts are not specific to these components. They need three things:
137
+
138
+ 1. A Figma page with component sets. Set `FIGMA_FILE_KEY` and `FIGMA_PAGE_ID` for
139
+ `figma-pull.mjs`.
140
+ 2. A page that renders a set by name at the positions in `figma-layout.json`, with
141
+ `id="spec"` on the box and `data-spec-variant="<variant name>"` on each variant. Copy
142
+ `playground/src/spec/Spec.tsx` and replace the renderers.
143
+ 3. A built site that `vite preview` can serve, or any running server (`--url`).
144
+
145
+ The paths (`playground/…`, `visual/…`) are constants at the top of each script. For a
146
+ second project, copy the two scripts and change those constants.
147
+
148
+ ## Files
149
+
150
+ | File | Made by | Edit by hand |
151
+ | --- | --- | --- |
152
+ | `playground/src/spec/figma-layout.json` | `figma-pull` | no |
153
+ | `visual/figma/*.png`, `geometry.json`, `meta.json` | `figma-pull` | no |
154
+ | `visual/baseline.json` | `compare --update-baseline` | only with values from a CI run |
155
+ | `visual/out/*` | `compare` | no (not in git) |
156
+ | `playground/src/spec/Spec.tsx` | people | yes |
package/package.json CHANGED
@@ -1,6 +1,98 @@
1
1
  {
2
2
  "name": "@brokenrubik/ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "BrokenRubik UI: the BR Admin Library (Figma) as shadcn-style React components and Tailwind v4 design tokens.",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+ssh://git@github.com/BrokenRubik/ui.git"
9
+ },
10
+ "packageManager": "pnpm@10.4.1",
11
+ "engines": {
12
+ "node": ">=22"
13
+ },
14
+ "type": "module",
15
+ "sideEffects": [
16
+ "*.css"
17
+ ],
18
+ "main": "./dist/index.js",
19
+ "module": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ },
26
+ "./utils": {
27
+ "types": "./dist/lib/utils.d.ts",
28
+ "import": "./dist/lib/utils.js"
29
+ },
30
+ "./components/*": {
31
+ "types": "./dist/components/*.d.ts",
32
+ "import": "./dist/components/*.js"
33
+ },
34
+ "./styles.css": "./dist/styles/styles.css",
35
+ "./fonts.css": "./dist/styles/fonts.css",
36
+ "./tokens.css": "./dist/styles/tokens.css",
37
+ "./theme.css": "./dist/styles/theme.css",
38
+ "./base.css": "./dist/styles/base.css",
39
+ "./package.json": "./package.json"
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "src",
44
+ "README.md",
45
+ "docs"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public",
49
+ "provenance": true
50
+ },
51
+ "scripts": {
52
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/copy-styles.mjs",
53
+ "dev": "tsc -p tsconfig.build.json --watch",
54
+ "typecheck": "tsc --noEmit && pnpm build && pnpm --filter playground typecheck",
55
+ "playground": "pnpm --filter playground dev",
56
+ "site:build": "pnpm build && pnpm --filter playground build",
57
+ "visual": "pnpm site:build && node scripts/visual/compare.mjs",
58
+ "visual:update": "pnpm site:build && node scripts/visual/compare.mjs --update-baseline",
59
+ "figma:pull": "node scripts/visual/figma-pull.mjs",
60
+ "figma:sync": "node scripts/figma-sync.mjs",
61
+ "prepublishOnly": "pnpm typecheck"
62
+ },
63
+ "peerDependencies": {
64
+ "react": ">=18.2",
65
+ "react-dom": ">=18.2",
66
+ "tailwindcss": ">=4.0"
67
+ },
68
+ "dependencies": {
69
+ "@fontsource/ibm-plex-mono": "^5.2.7",
70
+ "@fontsource/ibm-plex-sans": "^5.2.8",
71
+ "class-variance-authority": "^0.7.1",
72
+ "clsx": "^2.1.1",
73
+ "cmdk": "^1.1.1",
74
+ "lucide-react": "^1.70.0",
75
+ "radix-ui": "^1.8.1",
76
+ "react-day-picker": "^10.0.2",
77
+ "sonner": "^2.0.8",
78
+ "tailwind-merge": "^3.7.0",
79
+ "tw-animate-css": "^1.4.0"
80
+ },
81
+ "devDependencies": {
82
+ "@types/react": "^19.3.0",
83
+ "@types/react-dom": "^19.2.3",
84
+ "pixelmatch": "^6.0.0",
85
+ "playwright": "1.65.0",
86
+ "pngjs": "^7.0.0",
87
+ "react": "^19.3.1",
88
+ "react-dom": "^19.3.1",
89
+ "tailwindcss": "^4.3.5",
90
+ "typescript": "~7.0.2"
91
+ },
92
+ "pnpm": {
93
+ "onlyBuiltDependencies": [
94
+ "esbuild",
95
+ "@tailwindcss/oxide"
96
+ ]
97
+ }
98
+ }
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Accordion as AccordionPrimitive } from 'radix-ui';
5
+ import { ChevronDownIcon } from 'lucide-react';
6
+
7
+ import { cn } from '../lib/utils.js';
8
+
9
+ /**
10
+ * Figma: Components · Base › Accordion Item (2085:277). Bottom border; trigger 12 px
11
+ * vertical padding, Body/Default Medium, muted chevron; content Body/Default muted, 12 px
12
+ * bottom padding.
13
+ */
14
+ function Accordion(props: React.ComponentProps<typeof AccordionPrimitive.Root>) {
15
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
16
+ }
17
+
18
+ function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
19
+ return (
20
+ <AccordionPrimitive.Item
21
+ data-slot="accordion-item"
22
+ className={cn('border-b border-border', className)}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ function AccordionTrigger({
29
+ className,
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
33
+ return (
34
+ <AccordionPrimitive.Header className="flex">
35
+ <AccordionPrimitive.Trigger
36
+ data-slot="accordion-trigger"
37
+ className={cn(
38
+ 'flex flex-1 items-center justify-between gap-4 py-3 text-left text-body font-medium text-foreground transition-all outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
39
+ className,
40
+ )}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <ChevronDownIcon className="pointer-events-none size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
45
+ </AccordionPrimitive.Trigger>
46
+ </AccordionPrimitive.Header>
47
+ );
48
+ }
49
+
50
+ function AccordionContent({
51
+ className,
52
+ children,
53
+ ...props
54
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
55
+ return (
56
+ <AccordionPrimitive.Content
57
+ data-slot="accordion-content"
58
+ className="overflow-hidden text-body text-muted-foreground data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
59
+ {...props}
60
+ >
61
+ <div className={cn('pb-3', className)}>{children}</div>
62
+ </AccordionPrimitive.Content>
63
+ );
64
+ }
65
+
66
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -0,0 +1,139 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { AlertDialog as AlertDialogPrimitive } from 'radix-ui';
5
+ import type { VariantProps } from 'class-variance-authority';
6
+
7
+ import { cn } from '../lib/utils.js';
8
+ import { buttonVariants } from './button.js';
9
+ import { overlayClassName } from './dialog.js';
10
+
11
+ /**
12
+ * Figma: Components · Base › Dialog, Type=Destructive (2084:160): the confirm dialog.
13
+ * Same chrome as Dialog, no close button; Cancel (outline) + action (default or destructive).
14
+ */
15
+ function AlertDialog(props: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
16
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
17
+ }
18
+
19
+ function AlertDialogTrigger(props: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
20
+ return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
21
+ }
22
+
23
+ function AlertDialogPortal(props: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
24
+ return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
25
+ }
26
+
27
+ function AlertDialogOverlay({
28
+ className,
29
+ ...props
30
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
31
+ return (
32
+ <AlertDialogPrimitive.Overlay
33
+ data-slot="alert-dialog-overlay"
34
+ className={cn(overlayClassName, className)}
35
+ {...props}
36
+ />
37
+ );
38
+ }
39
+
40
+ function AlertDialogContent({
41
+ className,
42
+ ...props
43
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
44
+ return (
45
+ <AlertDialogPortal>
46
+ <AlertDialogOverlay />
47
+ <AlertDialogPrimitive.Content
48
+ data-slot="alert-dialog-content"
49
+ className={cn(
50
+ 'fixed top-1/2 left-1/2 z-50 flex w-[calc(100%-2rem)] max-w-[480px] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 rounded-xl border border-border bg-background p-6 text-body text-foreground shadow-lg outline-none data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
51
+ className,
52
+ )}
53
+ {...props}
54
+ />
55
+ </AlertDialogPortal>
56
+ );
57
+ }
58
+
59
+ function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
60
+ return <div data-slot="alert-dialog-header" className={cn('flex flex-col gap-1.5', className)} {...props} />;
61
+ }
62
+
63
+ function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
64
+ return (
65
+ <div
66
+ data-slot="alert-dialog-footer"
67
+ className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
68
+ {...props}
69
+ />
70
+ );
71
+ }
72
+
73
+ function AlertDialogTitle({
74
+ className,
75
+ ...props
76
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
77
+ return (
78
+ <AlertDialogPrimitive.Title
79
+ data-slot="alert-dialog-title"
80
+ className={cn('text-page-title', className)}
81
+ {...props}
82
+ />
83
+ );
84
+ }
85
+
86
+ function AlertDialogDescription({
87
+ className,
88
+ ...props
89
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
90
+ return (
91
+ <AlertDialogPrimitive.Description
92
+ data-slot="alert-dialog-description"
93
+ className={cn('text-body text-muted-foreground', className)}
94
+ {...props}
95
+ />
96
+ );
97
+ }
98
+
99
+ function AlertDialogAction({
100
+ className,
101
+ variant = 'default',
102
+ ...props
103
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
104
+ Pick<VariantProps<typeof buttonVariants>, 'variant'>) {
105
+ return (
106
+ <AlertDialogPrimitive.Action
107
+ data-slot="alert-dialog-action"
108
+ className={cn(buttonVariants({ variant }), className)}
109
+ {...props}
110
+ />
111
+ );
112
+ }
113
+
114
+ function AlertDialogCancel({
115
+ className,
116
+ ...props
117
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
118
+ return (
119
+ <AlertDialogPrimitive.Cancel
120
+ data-slot="alert-dialog-cancel"
121
+ className={cn(buttonVariants({ variant: 'outline' }), className)}
122
+ {...props}
123
+ />
124
+ );
125
+ }
126
+
127
+ export {
128
+ AlertDialog,
129
+ AlertDialogAction,
130
+ AlertDialogCancel,
131
+ AlertDialogContent,
132
+ AlertDialogDescription,
133
+ AlertDialogFooter,
134
+ AlertDialogHeader,
135
+ AlertDialogOverlay,
136
+ AlertDialogPortal,
137
+ AlertDialogTitle,
138
+ AlertDialogTrigger,
139
+ };