@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,112 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Dialog as SheetPrimitive } from 'radix-ui';
5
+ import { XIcon } from 'lucide-react';
6
+
7
+ import { cn } from '../lib/utils.js';
8
+ import { closeButtonClassName, overlayClassName } from './dialog.js';
9
+
10
+ /**
11
+ * Figma: Components · Base › Sheet (2084:161), code `<Sheet side="right">`. 400 px panel,
12
+ * 24 px padding, 16 px gap, border on the open side, Shadow/lg. Header like Dialog.
13
+ */
14
+ function Sheet(props: React.ComponentProps<typeof SheetPrimitive.Root>) {
15
+ return <SheetPrimitive.Root data-slot="sheet" {...props} />;
16
+ }
17
+
18
+ function SheetTrigger(props: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
19
+ return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
20
+ }
21
+
22
+ function SheetClose(props: React.ComponentProps<typeof SheetPrimitive.Close>) {
23
+ return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
24
+ }
25
+
26
+ function SheetPortal(props: React.ComponentProps<typeof SheetPrimitive.Portal>) {
27
+ return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
28
+ }
29
+
30
+ function SheetOverlay({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
31
+ return <SheetPrimitive.Overlay data-slot="sheet-overlay" className={cn(overlayClassName, className)} {...props} />;
32
+ }
33
+
34
+ function SheetContent({
35
+ className,
36
+ children,
37
+ side = 'right',
38
+ showCloseButton = true,
39
+ ...props
40
+ }: React.ComponentProps<typeof SheetPrimitive.Content> & {
41
+ side?: 'top' | 'right' | 'bottom' | 'left';
42
+ showCloseButton?: boolean;
43
+ }) {
44
+ return (
45
+ <SheetPortal>
46
+ <SheetOverlay />
47
+ <SheetPrimitive.Content
48
+ data-slot="sheet-content"
49
+ data-side={side}
50
+ className={cn(
51
+ 'fixed z-50 flex flex-col gap-4 overflow-y-auto bg-background p-6 text-body text-foreground shadow-lg outline-none transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-200 data-[state=open]:animate-in data-[state=open]:duration-300',
52
+ side === 'right' &&
53
+ 'inset-y-0 right-0 h-full w-full border-l border-border data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:w-[400px]',
54
+ side === 'left' &&
55
+ 'inset-y-0 left-0 h-full w-full border-r border-border data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:w-[400px]',
56
+ side === 'top' &&
57
+ 'inset-x-0 top-0 h-auto border-b border-border data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
58
+ side === 'bottom' &&
59
+ 'inset-x-0 bottom-0 h-auto border-t border-border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
60
+ className,
61
+ )}
62
+ {...props}
63
+ >
64
+ {children}
65
+ {showCloseButton && (
66
+ <SheetPrimitive.Close data-slot="sheet-close" className={cn(closeButtonClassName, 'absolute top-6 right-6')}>
67
+ <XIcon />
68
+ <span className="sr-only">Close</span>
69
+ </SheetPrimitive.Close>
70
+ )}
71
+ </SheetPrimitive.Content>
72
+ </SheetPortal>
73
+ );
74
+ }
75
+
76
+ function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
77
+ return <div data-slot="sheet-header" className={cn('flex flex-col gap-1.5 pr-9', className)} {...props} />;
78
+ }
79
+
80
+ function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
81
+ return (
82
+ <div data-slot="sheet-footer" className={cn('mt-auto flex justify-end gap-2', className)} {...props} />
83
+ );
84
+ }
85
+
86
+ function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
87
+ return <SheetPrimitive.Title data-slot="sheet-title" className={cn('text-page-title', className)} {...props} />;
88
+ }
89
+
90
+ function SheetDescription({
91
+ className,
92
+ ...props
93
+ }: React.ComponentProps<typeof SheetPrimitive.Description>) {
94
+ return (
95
+ <SheetPrimitive.Description
96
+ data-slot="sheet-description"
97
+ className={cn('text-body text-muted-foreground', className)}
98
+ {...props}
99
+ />
100
+ );
101
+ }
102
+
103
+ export {
104
+ Sheet,
105
+ SheetTrigger,
106
+ SheetClose,
107
+ SheetContent,
108
+ SheetHeader,
109
+ SheetFooter,
110
+ SheetTitle,
111
+ SheetDescription,
112
+ };
@@ -0,0 +1,16 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '../lib/utils.js';
4
+
5
+ /** Figma: Components · Base › Skeleton (2085:214). Muted, 6 px radius (rounded-full for circles). */
6
+ function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
7
+ return (
8
+ <div
9
+ data-slot="skeleton"
10
+ className={cn('animate-pulse rounded-md bg-muted motion-reduce:animate-none', className)}
11
+ {...props}
12
+ />
13
+ );
14
+ }
15
+
16
+ export { Skeleton };
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Toaster as Sonner, toast, type ToasterProps } from 'sonner';
5
+ import {
6
+ CircleAlertIcon,
7
+ CircleCheckIcon,
8
+ InfoIcon,
9
+ Loader2Icon,
10
+ TriangleAlertIcon,
11
+ } from 'lucide-react';
12
+
13
+ import { cn } from '../lib/utils.js';
14
+ import { buttonVariants } from './button.js';
15
+
16
+ /**
17
+ * Figma: Components · Base › Toast (2085:201), code `<Toaster>` (sonner). 360 px popover,
18
+ * 8 px radius, 12/12/12/16 px padding, Shadow/lg. Title Body/Default Medium, description
19
+ * Body/Small muted, action = small outline button. Success and error add a -strong icon.
20
+ *
21
+ * Mount <Toaster /> once. Call toast('Task moved', { description, action }) anywhere.
22
+ * `theme` follows the `.dark` class by default; pass theme="dark" | "light" to force it.
23
+ */
24
+ function Toaster({ className, toastOptions, ...props }: ToasterProps) {
25
+ return (
26
+ <Sonner
27
+ className={cn('toaster group', className)}
28
+ icons={{
29
+ success: <CircleCheckIcon className="size-4 text-success-strong" />,
30
+ info: <InfoIcon className="size-4 text-primary-strong" />,
31
+ warning: <TriangleAlertIcon className="size-4 text-warning-strong" />,
32
+ error: <CircleAlertIcon className="size-4 text-destructive-strong" />,
33
+ loading: <Loader2Icon className="size-4 animate-spin text-muted-foreground" />,
34
+ }}
35
+ toastOptions={{
36
+ unstyled: true,
37
+ ...toastOptions,
38
+ classNames: {
39
+ toast:
40
+ 'group/toast flex w-[var(--width)] items-center gap-3 rounded-lg border border-border bg-popover py-3 pr-3 pl-4 text-popover-foreground shadow-lg font-sans',
41
+ content: 'flex min-w-0 flex-1 flex-col gap-0.5',
42
+ title: 'text-body font-medium text-foreground',
43
+ description: 'text-body-sm text-muted-foreground',
44
+ icon: 'flex size-4 shrink-0 items-center justify-center',
45
+ actionButton: cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'shrink-0'),
46
+ cancelButton: cn(buttonVariants({ variant: 'ghost', size: 'sm' }), 'shrink-0'),
47
+ closeButton:
48
+ 'absolute -top-2 -left-2 flex size-5 items-center justify-center rounded-full border border-border bg-popover text-muted-foreground hover:text-foreground',
49
+ ...toastOptions?.classNames,
50
+ },
51
+ }}
52
+ style={{ '--width': '360px' } as React.CSSProperties}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+
58
+ export { Toaster, toast };
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Switch as SwitchPrimitive } from 'radix-ui';
5
+
6
+ import { cn } from '../lib/utils.js';
7
+
8
+ /** Figma: Components · Base › Switch (2007:47). 32×18 track (`input` off, `primary` on), 14 px thumb. */
9
+ function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
10
+ return (
11
+ <SwitchPrimitive.Root
12
+ data-slot="switch"
13
+ className={cn(
14
+ 'peer inline-flex h-[18px] w-8 shrink-0 items-center rounded-full px-0.5 transition-colors outline-none focus-visible:ring-3 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
15
+ className,
16
+ )}
17
+ {...props}
18
+ >
19
+ <SwitchPrimitive.Thumb
20
+ data-slot="switch-thumb"
21
+ className="pointer-events-none block size-3.5 rounded-full bg-background shadow-xs ring-0 transition-transform data-[state=checked]:translate-x-3.5 data-[state=unchecked]:translate-x-0"
22
+ />
23
+ </SwitchPrimitive.Root>
24
+ );
25
+ }
26
+
27
+ export { Switch };
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Tabs as TabsPrimitive } from 'radix-ui';
5
+
6
+ import { cn } from '../lib/utils.js';
7
+
8
+ /**
9
+ * Figma: Components · Base › Tab (2007:42). Underline tabs: 40 px high, 10 px side
10
+ * padding; active = foreground + Medium + 2 px violet underline; inactive = muted.
11
+ * For segmented controls (List / Board) use ToggleGroup.
12
+ */
13
+ function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
14
+ return (
15
+ <TabsPrimitive.Root
16
+ data-slot="tabs"
17
+ className={cn('flex flex-col gap-4', className)}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
24
+ return (
25
+ <TabsPrimitive.List
26
+ data-slot="tabs-list"
27
+ className={cn('flex items-center border-b border-border', className)}
28
+ {...props}
29
+ />
30
+ );
31
+ }
32
+
33
+ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
34
+ return (
35
+ <TabsPrimitive.Trigger
36
+ data-slot="tabs-trigger"
37
+ className={cn(
38
+ "relative -mb-px inline-flex h-10 items-center justify-center gap-1.5 whitespace-nowrap px-2.5 text-body text-muted-foreground transition-colors outline-none after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:bg-transparent hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:font-medium data-[state=active]:text-foreground data-[state=active]:after:bg-primary [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
39
+ className,
40
+ )}
41
+ {...props}
42
+ />
43
+ );
44
+ }
45
+
46
+ function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
47
+ return (
48
+ <TabsPrimitive.Content
49
+ data-slot="tabs-content"
50
+ className={cn('flex-1 outline-none', className)}
51
+ {...props}
52
+ />
53
+ );
54
+ }
55
+
56
+ export { Tabs, TabsList, TabsTrigger, TabsContent };
@@ -0,0 +1,17 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '../lib/utils.js';
4
+ import { controlClassName } from './input.js';
5
+
6
+ /** Figma: Components · Base › Textarea (2083:56). 80 px minimum, 8/10 px padding. */
7
+ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
8
+ return (
9
+ <textarea
10
+ data-slot="textarea"
11
+ className={cn(controlClassName, 'field-sizing-content flex min-h-20 px-2.5 py-2', className)}
12
+ {...props}
13
+ />
14
+ );
15
+ }
16
+
17
+ export { Textarea };
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { ToggleGroup as ToggleGroupPrimitive } from 'radix-ui';
5
+
6
+ import { cn } from '../lib/utils.js';
7
+
8
+ /**
9
+ * Figma: Components · Base › Toggle Group (2085:233) and Toggle Group Item (2085:232).
10
+ * A segmented control: 28 px muted track, 2 px padding; items 24 px, 5 px radius,
11
+ * Body/Small; "on" = background + Shadow/xs + Medium.
12
+ */
13
+ function ToggleGroup({
14
+ className,
15
+ ...props
16
+ }: React.ComponentProps<typeof ToggleGroupPrimitive.Root>) {
17
+ return (
18
+ <ToggleGroupPrimitive.Root
19
+ data-slot="toggle-group"
20
+ className={cn('inline-flex h-7 w-fit items-center gap-0.5 rounded-md bg-muted p-0.5', className)}
21
+ {...props}
22
+ />
23
+ );
24
+ }
25
+
26
+ function ToggleGroupItem({
27
+ className,
28
+ ...props
29
+ }: React.ComponentProps<typeof ToggleGroupPrimitive.Item>) {
30
+ return (
31
+ <ToggleGroupPrimitive.Item
32
+ data-slot="toggle-group-item"
33
+ className={cn(
34
+ "inline-flex h-6 items-center justify-center gap-1.5 whitespace-nowrap rounded-sm px-2.5 text-body-sm text-muted-foreground transition-[color,background-color,box-shadow] outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-background data-[state=on]:font-medium data-[state=on]:text-foreground data-[state=on]:shadow-xs [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
35
+ className,
36
+ )}
37
+ {...props}
38
+ />
39
+ );
40
+ }
41
+
42
+ export { ToggleGroup, ToggleGroupItem };
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Tooltip as TooltipPrimitive } from 'radix-ui';
5
+
6
+ import { cn } from '../lib/utils.js';
7
+
8
+ /**
9
+ * Figma: Components · Base › Tooltip (2007:61). Inverted (foreground fill, background
10
+ * text), Body/Small, 4/8 px padding, 6 px radius, Shadow/md, no arrow.
11
+ * Put one <TooltipProvider> near the app root; <Tooltip> works without it too.
12
+ */
13
+ function TooltipProvider({
14
+ delayDuration = 300,
15
+ ...props
16
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
17
+ return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />;
18
+ }
19
+
20
+ function Tooltip(props: React.ComponentProps<typeof TooltipPrimitive.Root>) {
21
+ return (
22
+ <TooltipProvider>
23
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
24
+ </TooltipProvider>
25
+ );
26
+ }
27
+
28
+ function TooltipTrigger(props: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
29
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
30
+ }
31
+
32
+ function TooltipContent({
33
+ className,
34
+ sideOffset = 6,
35
+ children,
36
+ ...props
37
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
38
+ return (
39
+ <TooltipPrimitive.Portal>
40
+ <TooltipPrimitive.Content
41
+ data-slot="tooltip-content"
42
+ sideOffset={sideOffset}
43
+ className={cn(
44
+ 'z-50 w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) rounded-md border border-sidebar-border bg-foreground px-2 py-1 text-body-sm text-background shadow-md text-balance animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
45
+ className,
46
+ )}
47
+ {...props}
48
+ >
49
+ {children}
50
+ </TooltipPrimitive.Content>
51
+ </TooltipPrimitive.Portal>
52
+ );
53
+ }
54
+
55
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ // @brokenrubik/ui — every component. Per-component imports also work:
2
+ // import { Button } from '@brokenrubik/ui/components/button';
3
+ export * from './lib/utils.js';
4
+ export * from './components/accordion.js';
5
+ export * from './components/alert.js';
6
+ export * from './components/alert-dialog.js';
7
+ export * from './components/avatar.js';
8
+ export * from './components/badge.js';
9
+ export * from './components/button.js';
10
+ export * from './components/calendar.js';
11
+ export * from './components/card.js';
12
+ export * from './components/checkbox.js';
13
+ export * from './components/command.js';
14
+ export * from './components/date-picker.js';
15
+ export * from './components/dialog.js';
16
+ export * from './components/dropdown-menu.js';
17
+ export * from './components/empty-state.js';
18
+ export * from './components/field.js';
19
+ export * from './components/input.js';
20
+ export * from './components/kbd.js';
21
+ export * from './components/label.js';
22
+ export * from './components/pagination.js';
23
+ export * from './components/popover.js';
24
+ export * from './components/progress.js';
25
+ export * from './components/radio-group.js';
26
+ export * from './components/select.js';
27
+ export * from './components/separator.js';
28
+ export * from './components/sheet.js';
29
+ export * from './components/skeleton.js';
30
+ export * from './components/sonner.js';
31
+ export * from './components/switch.js';
32
+ export * from './components/tabs.js';
33
+ export * from './components/textarea.js';
34
+ export * from './components/toggle-group.js';
35
+ export * from './components/tooltip.js';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `tone` and `variant` are two names for the same prop on Badge, Alert, Progress and
3
+ * AvatarFallback. `tone` is the Figma name; `variant` is the shadcn name, so shadcn code
4
+ * (`<Badge variant="secondary">`) works without edits. When both are set, `tone` wins.
5
+ * `aliases` maps shadcn-only values (`default`, `secondary`, `outline`) to a BR tone.
6
+ */
7
+ export function resolveTone<T extends string>(
8
+ tone: T | null | undefined,
9
+ variant: string | null | undefined,
10
+ aliases: Readonly<Record<string, T>>,
11
+ fallback: T,
12
+ ): T {
13
+ if (tone) return tone;
14
+ if (!variant) return fallback;
15
+ return aliases[variant] ?? (variant as T);
16
+ }
@@ -0,0 +1,46 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { extendTailwindMerge, mergeConfigs, type ConfigExtension } from 'tailwind-merge';
3
+
4
+ /**
5
+ * The BR text styles registered as `text-*` font sizes in theme.css. tailwind-merge must
6
+ * know them: by default it reads an unknown `text-foo` as a color, so `cn('text-body',
7
+ * 'text-muted-foreground')` would drop `text-body`.
8
+ */
9
+ export const brTextSizes = [
10
+ 'display',
11
+ 'page-title',
12
+ 'section',
13
+ 'card-title',
14
+ 'body-lg',
15
+ 'body',
16
+ 'body-sm',
17
+ 'caption',
18
+ 'overline',
19
+ 'nav',
20
+ 'nav-label',
21
+ 'initials',
22
+ 'initials-sm',
23
+ 'mono',
24
+ 'mono-sm',
25
+ 'doc-title',
26
+ 'doc-h2',
27
+ 'doc-body',
28
+ ] as const;
29
+
30
+ type MergeConfig = ConfigExtension<string, string>;
31
+
32
+ /**
33
+ * Builds a `cn` for an app that adds its own scales on top of the BR theme (for example
34
+ * extra `text-*` sizes or `shadow-*` names), so tailwind-merge groups them right.
35
+ *
36
+ * export const cn = createCn({ extend: { theme: { text: ['hero'] } } });
37
+ */
38
+ export function createCn(config?: MergeConfig) {
39
+ const merge = extendTailwindMerge({ extend: { theme: { text: [...brTextSizes] } } }, (base) =>
40
+ config ? mergeConfigs(base, config) : base,
41
+ );
42
+ return (...inputs: ClassValue[]) => merge(clsx(inputs));
43
+ }
44
+
45
+ /** Joins class names and resolves Tailwind conflicts (the last class wins). */
46
+ export const cn = createCn();
@@ -0,0 +1,37 @@
1
+ /*
2
+ * @brokenrubik/ui — base styles (optional). Page defaults from the BR Admin Library:
3
+ * Body/Default (13/20 IBM Plex Sans) on --background, slate borders, balanced headings.
4
+ * Skip this file if the app sets its own base; tokens.css and theme.css work without it.
5
+ */
6
+ @layer base {
7
+ *,
8
+ ::before,
9
+ ::after {
10
+ border-color: var(--color-border);
11
+ outline-color: color-mix(in oklab, var(--color-ring) 50%, transparent);
12
+ }
13
+
14
+ html {
15
+ font-family: var(--font-sans);
16
+ }
17
+
18
+ body {
19
+ background-color: var(--color-background);
20
+ color: var(--color-foreground);
21
+ font-size: var(--text-body);
22
+ line-height: var(--text-body--line-height);
23
+ -webkit-font-smoothing: antialiased;
24
+ -moz-osx-font-smoothing: grayscale;
25
+ text-rendering: optimizeLegibility;
26
+ }
27
+
28
+ h1,
29
+ h2,
30
+ h3 {
31
+ text-wrap: balance;
32
+ }
33
+
34
+ p {
35
+ text-wrap: pretty;
36
+ }
37
+ }
@@ -0,0 +1,16 @@
1
+ /*
2
+ * @brokenrubik/ui — fonts: IBM Plex Sans 400/500/600 and IBM Plex Mono 400/500 (the weights
3
+ * the BR Admin Library uses). styles.css imports this file, so apps get the fonts with no
4
+ * extra setup. The font files come from the @fontsource packages (dependencies of this
5
+ * package); the app's bundler copies them. Each @font-face has a unicode-range, so the
6
+ * browser downloads only the subsets (latin, latin-ext…) that the page uses.
7
+ *
8
+ * The families are "IBM Plex Sans" and "IBM Plex Mono", the default of --font-sans and
9
+ * --font-mono in theme.css. An app that loads the fonts in a different way (next/font) can
10
+ * set --font-plex-sans / --font-plex-mono; these faces are then never downloaded.
11
+ */
12
+ @import "@fontsource/ibm-plex-sans/400.css";
13
+ @import "@fontsource/ibm-plex-sans/500.css";
14
+ @import "@fontsource/ibm-plex-sans/600.css";
15
+ @import "@fontsource/ibm-plex-mono/400.css";
16
+ @import "@fontsource/ibm-plex-mono/500.css";
@@ -0,0 +1,14 @@
1
+ /*
2
+ * @brokenrubik/ui — everything: fonts, tokens, Tailwind theme, animations, base styles.
3
+ *
4
+ * @import "tailwindcss";
5
+ * @import "@brokenrubik/ui/styles.css";
6
+ *
7
+ * Need less? Import the parts: fonts.css (IBM Plex), tokens.css (CSS variables only),
8
+ * theme.css (Tailwind mapping), base.css (page defaults). Components need tokens.css + theme.css + tw-animate-css.
9
+ */
10
+ @import "./fonts.css";
11
+ @import "tw-animate-css";
12
+ @import "./tokens.css";
13
+ @import "./theme.css";
14
+ @import "./base.css";