@wealthx/shadcn 0.0.2 → 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 (188) hide show
  1. package/.turbo/turbo-build.log +135 -11
  2. package/CHANGELOG.md +6 -0
  3. package/CHANGES.md +345 -0
  4. package/dist/chunk-2WZVSBAY.mjs +232 -0
  5. package/dist/chunk-2Y7YJKPE.mjs +47 -0
  6. package/dist/chunk-3U7SD3MS.mjs +55 -0
  7. package/dist/chunk-3VQNJ235.mjs +114 -0
  8. package/dist/chunk-55CEW76V.mjs +35 -0
  9. package/dist/chunk-6AFMNC42.mjs +146 -0
  10. package/dist/chunk-6OJF6XRN.mjs +117 -0
  11. package/dist/chunk-7LDIMXGM.mjs +181 -0
  12. package/dist/chunk-AMJ23O53.mjs +122 -0
  13. package/dist/chunk-BBJBJSXQ.mjs +44 -0
  14. package/dist/chunk-BGP2N52Z.mjs +126 -0
  15. package/dist/chunk-BMFN37JH.mjs +41 -0
  16. package/dist/chunk-CGOKTPXU.mjs +79 -0
  17. package/dist/chunk-CZ3BW5GL.mjs +81 -0
  18. package/dist/chunk-DBHJ5KC3.mjs +55 -0
  19. package/dist/chunk-DDPA2XXS.mjs +97 -0
  20. package/dist/chunk-DS2AMHN2.mjs +30 -0
  21. package/dist/chunk-E3K6O4FZ.mjs +57 -0
  22. package/dist/chunk-FWCSY2DS.mjs +37 -0
  23. package/dist/chunk-GPRJQ24C.mjs +28 -0
  24. package/dist/chunk-HS7TFG7V.mjs +24 -0
  25. package/dist/chunk-HUVTPUV2.mjs +256 -0
  26. package/dist/chunk-IAOOZCUY.mjs +90 -0
  27. package/dist/chunk-JF4PHPD5.mjs +111 -0
  28. package/dist/chunk-JU2RUWHF.mjs +123 -0
  29. package/dist/chunk-KKHTJNMM.mjs +86 -0
  30. package/dist/chunk-MJIEMGRD.mjs +266 -0
  31. package/dist/chunk-MKFL5MNH.mjs +372 -0
  32. package/dist/chunk-MQ72DIBH.mjs +105 -0
  33. package/dist/chunk-NGYG2EA6.mjs +148 -0
  34. package/dist/chunk-NWZ46DJL.mjs +213 -0
  35. package/dist/chunk-OXQQNQZI.mjs +75 -0
  36. package/dist/chunk-PMKODV6M.mjs +161 -0
  37. package/dist/chunk-QOJ2DQD6.mjs +57 -0
  38. package/dist/chunk-RL772EH7.mjs +126 -0
  39. package/dist/chunk-SLWCCURD.mjs +99 -0
  40. package/dist/chunk-V7CNWJT3.mjs +10 -0
  41. package/dist/chunk-VG6UF6UT.mjs +68 -0
  42. package/dist/chunk-VYMHBV6D.mjs +123 -0
  43. package/dist/chunk-VZ2NR7L3.mjs +195 -0
  44. package/dist/chunk-YN5SYTOO.mjs +117 -0
  45. package/dist/chunk-Z3MK2KKZ.mjs +83 -0
  46. package/dist/chunk-ZN2QKLF6.mjs +187 -0
  47. package/dist/chunk-ZZV5JVNW.mjs +34 -0
  48. package/dist/components/ui/accordion.js +142 -0
  49. package/dist/components/ui/accordion.mjs +14 -0
  50. package/dist/components/ui/alert-dialog.js +413 -0
  51. package/dist/components/ui/alert-dialog.mjs +34 -0
  52. package/dist/components/ui/alert.js +134 -0
  53. package/dist/components/ui/alert.mjs +12 -0
  54. package/dist/components/ui/avatar.js +173 -0
  55. package/dist/components/ui/avatar.mjs +18 -0
  56. package/dist/components/ui/badge.js +163 -0
  57. package/dist/components/ui/badge.mjs +11 -0
  58. package/dist/components/ui/button.js +198 -0
  59. package/dist/components/ui/button.mjs +11 -0
  60. package/dist/components/ui/calendar.js +408 -0
  61. package/dist/components/ui/calendar.mjs +12 -0
  62. package/dist/components/ui/card.js +156 -0
  63. package/dist/components/ui/card.mjs +20 -0
  64. package/dist/components/ui/checkbox.js +166 -0
  65. package/dist/components/ui/checkbox.mjs +11 -0
  66. package/dist/components/ui/chip.js +199 -0
  67. package/dist/components/ui/chip.mjs +10 -0
  68. package/dist/components/ui/data-table.js +925 -0
  69. package/dist/components/ui/data-table.mjs +29 -0
  70. package/dist/components/ui/date-picker.js +561 -0
  71. package/dist/components/ui/date-picker.mjs +15 -0
  72. package/dist/components/ui/dialog.js +378 -0
  73. package/dist/components/ui/dialog.mjs +30 -0
  74. package/dist/components/ui/drawer.js +213 -0
  75. package/dist/components/ui/drawer.mjs +28 -0
  76. package/dist/components/ui/dropdown-menu.js +338 -0
  77. package/dist/components/ui/dropdown-menu.mjs +38 -0
  78. package/dist/components/ui/empty.js +173 -0
  79. package/dist/components/ui/empty.mjs +18 -0
  80. package/dist/components/ui/field.js +359 -0
  81. package/dist/components/ui/field.mjs +28 -0
  82. package/dist/components/ui/input-group.js +406 -0
  83. package/dist/components/ui/input-group.mjs +22 -0
  84. package/dist/components/ui/input-otp.js +149 -0
  85. package/dist/components/ui/input-otp.mjs +14 -0
  86. package/dist/components/ui/input.js +81 -0
  87. package/dist/components/ui/input.mjs +8 -0
  88. package/dist/components/ui/label.js +85 -0
  89. package/dist/components/ui/label.mjs +8 -0
  90. package/dist/components/ui/pagination.js +333 -0
  91. package/dist/components/ui/pagination.mjs +22 -0
  92. package/dist/components/ui/popover.js +167 -0
  93. package/dist/components/ui/popover.mjs +22 -0
  94. package/dist/components/ui/progress.js +97 -0
  95. package/dist/components/ui/progress.mjs +8 -0
  96. package/dist/components/ui/radio-group.js +178 -0
  97. package/dist/components/ui/radio-group.mjs +12 -0
  98. package/dist/components/ui/select.js +262 -0
  99. package/dist/components/ui/select.mjs +28 -0
  100. package/dist/components/ui/separator.js +86 -0
  101. package/dist/components/ui/separator.mjs +8 -0
  102. package/dist/components/ui/sheet.js +227 -0
  103. package/dist/components/ui/sheet.mjs +26 -0
  104. package/dist/components/ui/skeleton.js +75 -0
  105. package/dist/components/ui/skeleton.mjs +8 -0
  106. package/dist/components/ui/sonner.js +86 -0
  107. package/dist/components/ui/sonner.mjs +7 -0
  108. package/dist/components/ui/spinner.js +93 -0
  109. package/dist/components/ui/spinner.mjs +10 -0
  110. package/dist/components/ui/switch.js +178 -0
  111. package/dist/components/ui/switch.mjs +11 -0
  112. package/dist/components/ui/table.js +184 -0
  113. package/dist/components/ui/table.mjs +22 -0
  114. package/dist/components/ui/tabs.js +181 -0
  115. package/dist/components/ui/tabs.mjs +16 -0
  116. package/dist/components/ui/textarea.js +79 -0
  117. package/dist/components/ui/textarea.mjs +8 -0
  118. package/dist/components/ui/toggle-group.js +184 -0
  119. package/dist/components/ui/toggle-group.mjs +12 -0
  120. package/dist/components/ui/toggle.js +108 -0
  121. package/dist/components/ui/toggle.mjs +11 -0
  122. package/dist/components/ui/tooltip.js +140 -0
  123. package/dist/components/ui/tooltip.mjs +16 -0
  124. package/dist/index.js +4312 -90
  125. package/dist/index.mjs +459 -158
  126. package/dist/lib/colors.js +84 -0
  127. package/dist/lib/colors.mjs +13 -0
  128. package/dist/lib/theme-provider.js +150 -0
  129. package/dist/lib/theme-provider.mjs +13 -0
  130. package/dist/lib/typography.js +157 -0
  131. package/dist/lib/typography.mjs +25 -0
  132. package/dist/lib/utils.js +34 -0
  133. package/dist/lib/utils.mjs +7 -0
  134. package/dist/styles.css +1 -1
  135. package/package.json +228 -11
  136. package/scripts/build-css.ts +15 -9
  137. package/src/components/index.tsx +443 -0
  138. package/src/components/ui/accordion.tsx +99 -0
  139. package/src/components/ui/alert-dialog.tsx +239 -0
  140. package/src/components/ui/alert.tsx +81 -0
  141. package/src/components/ui/avatar.tsx +130 -0
  142. package/src/components/ui/badge.tsx +57 -0
  143. package/src/components/ui/button.tsx +69 -37
  144. package/src/components/ui/calendar.tsx +252 -0
  145. package/src/components/ui/card.tsx +106 -0
  146. package/src/components/ui/checkbox.tsx +111 -0
  147. package/src/components/ui/chip.tsx +65 -0
  148. package/src/components/ui/data-table.tsx +490 -0
  149. package/src/components/ui/date-picker.tsx +133 -0
  150. package/src/components/ui/dialog.tsx +195 -0
  151. package/src/components/ui/drawer.tsx +169 -0
  152. package/src/components/ui/dropdown-menu.tsx +315 -0
  153. package/src/components/ui/empty.tsx +128 -0
  154. package/src/components/ui/field.tsx +273 -0
  155. package/src/components/ui/input-group.tsx +190 -0
  156. package/src/components/ui/input-otp.tsx +90 -0
  157. package/src/components/ui/input.tsx +28 -0
  158. package/src/components/ui/label.tsx +24 -0
  159. package/src/components/ui/pagination.tsx +148 -0
  160. package/src/components/ui/popover.tsx +112 -0
  161. package/src/components/ui/progress.tsx +40 -0
  162. package/src/components/ui/radio-group.tsx +129 -0
  163. package/src/components/ui/select.tsx +201 -0
  164. package/src/components/ui/separator.tsx +26 -0
  165. package/src/components/ui/sheet.tsx +182 -0
  166. package/src/components/ui/skeleton.tsx +22 -0
  167. package/src/components/ui/sonner.tsx +48 -0
  168. package/src/components/ui/spinner.tsx +41 -0
  169. package/src/components/ui/switch.tsx +126 -0
  170. package/src/components/ui/table.tsx +143 -0
  171. package/src/components/ui/tabs.tsx +119 -0
  172. package/src/components/ui/textarea.tsx +28 -0
  173. package/src/components/ui/toggle-group.tsx +94 -0
  174. package/src/components/ui/toggle.tsx +59 -0
  175. package/src/components/ui/tooltip.tsx +80 -0
  176. package/src/index.ts +15 -3
  177. package/src/lib/colors.ts +74 -0
  178. package/src/lib/slot.tsx +68 -0
  179. package/src/lib/theme-provider.tsx +134 -0
  180. package/src/lib/typography.ts +153 -0
  181. package/src/lib/utils.ts +1 -1
  182. package/src/styles/globals.css +377 -107
  183. package/src/styles/styles-css.ts +1 -1
  184. package/tsup.config.ts +48 -2
  185. package/dist/index.d.mts +0 -27
  186. package/dist/index.d.ts +0 -27
  187. package/src/provider/ShadcnProvider.tsx +0 -89
  188. package/src/provider/index.ts +0 -2
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Textarea — WealthX DS overrides (shadcn base)
3
+ *
4
+ * Changes from shadcn default:
5
+ * - `shadow-xs` removed — flat panels per Figma
6
+ * - `font-sans` added — consistent typography token
7
+ */
8
+ import { type ReactElement } from "react"
9
+ import * as React from "react"
10
+ import { cn } from "@/lib/utils"
11
+
12
+ export type TextareaProps = React.ComponentProps<"textarea">
13
+
14
+ function Textarea({ className, ...props }: TextareaProps): ReactElement {
15
+ return (
16
+ <textarea
17
+ className={cn(
18
+ // WealthX: removed shadow-xs (flat panels), added font-sans
19
+ "flex field-sizing-content min-h-16 w-full border border-input bg-transparent px-3 py-2 text-base font-sans transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-[3px] focus-visible:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
20
+ className
21
+ )}
22
+ data-slot="textarea"
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ export { Textarea }
@@ -0,0 +1,94 @@
1
+ "use client"
2
+
3
+ import { type ReactElement } from "react"
4
+ import * as React from "react"
5
+ import { type VariantProps } from "class-variance-authority"
6
+ import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group"
7
+ import { Toggle as ToggleComponent } from "@base-ui/react/toggle"
8
+ import { cn } from "@/lib/utils"
9
+ import { toggleVariants } from "@/components/ui/toggle"
10
+
11
+ const ToggleGroupContext = React.createContext<
12
+ VariantProps<typeof toggleVariants> & {
13
+ spacing?: number
14
+ }
15
+ >({
16
+ size: "default",
17
+ variant: "default",
18
+ spacing: 0,
19
+ })
20
+
21
+ export type ToggleGroupProps = React.ComponentProps<typeof ToggleGroupPrimitive> &
22
+ VariantProps<typeof toggleVariants> & {
23
+ spacing?: number
24
+ /** shadcn-compatible alias: "single" -- multiple=false, "multiple" -- multiple=true */
25
+ type?: "single" | "multiple"
26
+ }
27
+
28
+ function ToggleGroup({
29
+ className,
30
+ variant,
31
+ size,
32
+ spacing = 0,
33
+ type,
34
+ children,
35
+ ...props
36
+ }: ToggleGroupProps): ReactElement {
37
+ return (
38
+ <ToggleGroupPrimitive
39
+ className={cn(
40
+ // WealthX: removed rounded-md (sharp corners) and shadow-xs (flat panels)
41
+ "group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))]",
42
+ className
43
+ )}
44
+ data-size={size}
45
+ data-slot="toggle-group"
46
+ data-spacing={spacing}
47
+ data-variant={variant}
48
+ multiple={type === "multiple"}
49
+ style={{ "--gap": spacing } as React.CSSProperties}
50
+ {...props}
51
+ >
52
+ <ToggleGroupContext.Provider value={{ variant, size, spacing }}>
53
+ {children}
54
+ </ToggleGroupContext.Provider>
55
+ </ToggleGroupPrimitive>
56
+ )
57
+ }
58
+
59
+ export type ToggleGroupItemProps = React.ComponentProps<typeof ToggleComponent> &
60
+ VariantProps<typeof toggleVariants>
61
+
62
+ function ToggleGroupItem({
63
+ className,
64
+ children,
65
+ variant,
66
+ size,
67
+ ...props
68
+ }: ToggleGroupItemProps): ReactElement {
69
+ const context = React.useContext(ToggleGroupContext)
70
+
71
+ return (
72
+ <ToggleComponent
73
+ className={cn(
74
+ toggleVariants({
75
+ variant: context.variant || variant,
76
+ size: context.size || size,
77
+ }),
78
+ "w-auto min-w-0 shrink-0 focus:z-10 focus-visible:z-10",
79
+ // WealthX: no rounded corners on grouped items
80
+ "data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
81
+ className
82
+ )}
83
+ data-size={context.size || size}
84
+ data-slot="toggle-group-item"
85
+ data-spacing={context.spacing}
86
+ data-variant={context.variant || variant}
87
+ {...props}
88
+ >
89
+ {children}
90
+ </ToggleComponent>
91
+ )
92
+ }
93
+
94
+ export { ToggleGroup, ToggleGroupItem }
@@ -0,0 +1,59 @@
1
+ "use client"
2
+
3
+ import { type ReactElement } from "react"
4
+ import * as React from "react"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+ import { Toggle as ToggleComponent } from "@base-ui/react/toggle"
7
+ import { cn } from "@/lib/utils"
8
+
9
+ /**
10
+ * Toggle — WealthX DS overrides (shadcn base)
11
+ *
12
+ * Changes from shadcn default:
13
+ * - `rounded-md` removed — sharp corners per WealthX DS
14
+ * - `shadow-xs` removed from outline variant — flat panels
15
+ * - Pressed/on state uses `bg-primary/10` + `inset-ring-primary` via Figma tokens
16
+ * (inset-ring instead of border-primary so indicator is independent of border-width,
17
+ * works correctly inside ToggleGroup where border-l is collapsed for connected items)
18
+ */
19
+ const toggleVariants = cva(
20
+ "inline-flex items-center justify-center gap-2 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-pressed:bg-primary/10 data-pressed:inset-ring data-pressed:inset-ring-primary data-pressed:text-foreground data-pressed:hover:bg-primary/10 data-pressed:hover:text-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
21
+ {
22
+ variants: {
23
+ variant: {
24
+ default: "bg-transparent hover:bg-muted hover:text-muted-foreground",
25
+ outline:
26
+ "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
27
+ },
28
+ size: {
29
+ default: "h-9 min-w-9 px-2",
30
+ sm: "h-8 min-w-8 px-1.5",
31
+ lg: "h-10 min-w-10 px-2.5",
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ variant: "default",
36
+ size: "default",
37
+ },
38
+ }
39
+ )
40
+
41
+ export type ToggleProps = React.ComponentProps<typeof ToggleComponent> &
42
+ VariantProps<typeof toggleVariants>
43
+
44
+ function Toggle({
45
+ className,
46
+ variant,
47
+ size,
48
+ ...props
49
+ }: ToggleProps): ReactElement {
50
+ return (
51
+ <ToggleComponent
52
+ className={cn(toggleVariants({ variant, size, className }))}
53
+ data-slot="toggle"
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ export { Toggle, toggleVariants }
@@ -0,0 +1,80 @@
1
+ import { type ReactElement } from "react"
2
+ import * as React from "react"
3
+ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
4
+ import { cn } from "@/lib/utils"
5
+ import { useThemeVars } from "@/lib/theme-provider"
6
+
7
+ /**
8
+ * Tooltip — WealthX Design System
9
+ * Figma: https://www.figma.com/design/9V9F0NGVsif8LGmEhVjOcT/Design-System---shadcn?node-id=73-2904
10
+ *
11
+ * Base: official shadcn tooltip (npx shadcn\@latest add tooltip)
12
+ * WealthX overrides: bg-brand-secondary, text-brand-secondary-foreground, sharp corners (no rounded-md)
13
+ */
14
+
15
+ export type TooltipProviderProps = React.ComponentProps<typeof TooltipPrimitive.Provider>
16
+
17
+ function TooltipProvider({
18
+ delay = 0,
19
+ ...props
20
+ }: TooltipProviderProps): ReactElement {
21
+ return (
22
+ <TooltipPrimitive.Provider
23
+ data-slot="tooltip-provider"
24
+ delay={delay}
25
+ {...props}
26
+ />
27
+ )
28
+ }
29
+
30
+ export type TooltipProps = React.ComponentProps<typeof TooltipPrimitive.Root>
31
+
32
+ function Tooltip({
33
+ ...props
34
+ }: TooltipProps): ReactElement {
35
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
36
+ }
37
+
38
+ export type TooltipTriggerProps = React.ComponentProps<typeof TooltipPrimitive.Trigger>
39
+
40
+ function TooltipTrigger({
41
+ ...props
42
+ }: TooltipTriggerProps): ReactElement {
43
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
44
+ }
45
+
46
+ export type TooltipContentProps = React.ComponentProps<typeof TooltipPrimitive.Popup> & {
47
+ sideOffset?: number
48
+ }
49
+
50
+ function TooltipContent({
51
+ className,
52
+ sideOffset = 8,
53
+ children,
54
+ style,
55
+ ...props
56
+ }: TooltipContentProps): ReactElement {
57
+ const themeVars = useThemeVars();
58
+ return (
59
+ <TooltipPrimitive.Portal>
60
+ <TooltipPrimitive.Positioner sideOffset={sideOffset}>
61
+ <TooltipPrimitive.Popup
62
+ className={cn(
63
+ "z-50 w-fit animate-in bg-brand-secondary px-3 py-1.5 text-xs text-balance text-brand-secondary-foreground fade-in-0 zoom-in-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 data-ending-style:animate-out data-ending-style:fade-out-0 data-ending-style:zoom-out-95 data-ending-style:fill-mode-forwards",
64
+ className
65
+ )}
66
+ data-slot="tooltip-content"
67
+ style={{ ...themeVars, ...style } as React.CSSProperties}
68
+ {...props}
69
+ >
70
+ {children}
71
+ </TooltipPrimitive.Popup>
72
+ <TooltipPrimitive.Arrow
73
+ className="z-50 size-2.5 rotate-45 bg-brand-secondary"
74
+ />
75
+ </TooltipPrimitive.Positioner>
76
+ </TooltipPrimitive.Portal>
77
+ )
78
+ }
79
+
80
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
package/src/index.ts CHANGED
@@ -1,3 +1,15 @@
1
- export { ShadcnProvider } from "./provider";
2
- export type { ShadcnProviderProps } from "./provider";
3
- export { Button, buttonVariants } from "./components/ui/button";
1
+ export { cn } from "./lib/utils";
2
+ export {
3
+ TYPOGRAPHY,
4
+ TYPOGRAPHY_DISPLAY,
5
+ TYPOGRAPHY_HEADING,
6
+ TYPOGRAPHY_BODY,
7
+ TYPOGRAPHY_LABEL,
8
+ TYPOGRAPHY_UTILITY,
9
+ TYPOGRAPHY_RESPONSIVE,
10
+ FONT_FAMILY_SANS,
11
+ getTypographyCssVars,
12
+ getResponsiveTypographyCssVars,
13
+ } from "./lib/typography";
14
+ export type { TypographyStyle } from "./lib/typography";
15
+ export * from "./components";
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Color utilities for WealthX white-label theming.
3
+ * Ported from \@wealthx/ui theme/utils.ts — zero MUI dependencies.
4
+ */
5
+
6
+ // WCAG contrast text colors
7
+ const CONTRAST_DARK = "#040D13";
8
+ const CONTRAST_LIGHT = "#FFFFFF";
9
+
10
+ export function hexToRgb(hex: string): [number, number, number] {
11
+ const normalized = hex.replace(/^#/, "");
12
+ if (normalized.length === 3) {
13
+ const r = parseInt(normalized[0] + normalized[0], 16);
14
+ const g = parseInt(normalized[1] + normalized[1], 16);
15
+ const b = parseInt(normalized[2] + normalized[2], 16);
16
+ return [r, g, b];
17
+ }
18
+ if (normalized.length === 6 || normalized.length === 8) {
19
+ const r = parseInt(normalized.slice(0, 2), 16);
20
+ const g = parseInt(normalized.slice(2, 4), 16);
21
+ const b = parseInt(normalized.slice(4, 6), 16);
22
+ return [r, g, b];
23
+ }
24
+ throw new Error(`Invalid hex color: ${hex}`);
25
+ }
26
+
27
+ function linearizeSrgb(c: number): number {
28
+ const n = c / 255;
29
+ return n <= 0.03928 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
30
+ }
31
+
32
+ /** WCAG relative luminance (0 = black, 1 = white) */
33
+ export function getLuminance(hex: string): number {
34
+ const [r, g, b] = hexToRgb(hex);
35
+ return (
36
+ 0.2126 * linearizeSrgb(r) +
37
+ 0.7152 * linearizeSrgb(g) +
38
+ 0.0722 * linearizeSrgb(b)
39
+ );
40
+ }
41
+
42
+ /** Returns dark or light text color based on WCAG contrast ratio */
43
+ export function getContrastText(backgroundColor: string): string {
44
+ const luminance = getLuminance(backgroundColor);
45
+ return luminance > 0.179 ? CONTRAST_DARK : CONTRAST_LIGHT;
46
+ }
47
+
48
+ /** Convert hex to oklch() CSS string */
49
+ export function hexToOklch(hex: string): string {
50
+ const [r, g, b] = hexToRgb(hex);
51
+ const rl = linearizeSrgb(r);
52
+ const gl = linearizeSrgb(g);
53
+ const bl = linearizeSrgb(b);
54
+
55
+ // sRGB linear → OKLab
56
+ const l = 0.4122214708 * rl + 0.5363325363 * gl + 0.0514459929 * bl;
57
+ const m = 0.2119034982 * rl + 0.6806995451 * gl + 0.1073969566 * bl;
58
+ const s = 0.0883024619 * rl + 0.2817188376 * gl + 0.6299787005 * bl;
59
+
60
+ const l_ = l > 0 ? Math.cbrt(l) : 0;
61
+ const m_ = m > 0 ? Math.cbrt(m) : 0;
62
+ const s_ = s > 0 ? Math.cbrt(s) : 0;
63
+
64
+ const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
65
+ const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
66
+ const bv = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
67
+
68
+ // OKLab → OKLch
69
+ const C = Math.sqrt(a * a + bv * bv);
70
+ const h = ((Math.atan2(bv, a) * 180) / Math.PI + 360) % 360;
71
+
72
+ if (C < 0.001) return `oklch(${L.toFixed(3)} 0 0)`;
73
+ return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${h.toFixed(1)})`;
74
+ }
@@ -0,0 +1,68 @@
1
+ import * as React from "react";
2
+
3
+ function mergeRefs<T>(
4
+ ...refs: (React.Ref<T> | undefined | null)[]
5
+ ): (value: T | null) => void {
6
+ return (value: T | null): void => {
7
+ for (const ref of refs) {
8
+ if (typeof ref === "function") ref(value);
9
+ else if (ref !== null)
10
+ (ref as React.MutableRefObject<T | null>).current = value;
11
+ }
12
+ };
13
+ }
14
+
15
+ interface SlotProps extends React.HTMLAttributes<HTMLElement> {
16
+ children?: React.ReactNode;
17
+ }
18
+
19
+ const Slot = React.forwardRef<HTMLElement, SlotProps>(
20
+ ({ children, ...props }, forwardedRef) => {
21
+ const child = React.Children.only(children);
22
+ if (!React.isValidElement(child)) return null;
23
+
24
+ const childProps = child.props as Record<string, unknown>;
25
+ const merged: Record<string, unknown> = { ...props };
26
+
27
+ for (const key of Object.keys(childProps)) {
28
+ if (key === "className") {
29
+ merged.className = [props.className, childProps.className]
30
+ .filter(Boolean)
31
+ .join(" ");
32
+ } else if (key === "style") {
33
+ merged.style = {
34
+ ...(props.style as object),
35
+ ...(childProps.style as object),
36
+ };
37
+ } else if (
38
+ key.startsWith("on") &&
39
+ typeof childProps[key] === "function"
40
+ ) {
41
+ const parentHandler = (props as Record<string, unknown>)[key];
42
+ if (typeof parentHandler === "function") {
43
+ merged[key] = (...args: unknown[]) => {
44
+ (childProps[key] as (...a: unknown[]) => unknown)(...args);
45
+ (parentHandler as (...a: unknown[]) => unknown)(...args);
46
+ };
47
+ } else {
48
+ merged[key] = childProps[key];
49
+ }
50
+ } else {
51
+ merged[key] = childProps[key];
52
+ }
53
+ }
54
+
55
+ const childRef = (child as unknown as { ref?: React.Ref<HTMLElement> }).ref;
56
+ merged.ref = forwardedRef
57
+ ? mergeRefs(forwardedRef, childRef)
58
+ : childRef;
59
+
60
+ return React.cloneElement(
61
+ child,
62
+ merged as React.Attributes & Record<string, unknown>,
63
+ );
64
+ },
65
+ );
66
+ Slot.displayName = "Slot";
67
+
68
+ export { Slot };
@@ -0,0 +1,134 @@
1
+ "use client";
2
+
3
+ import React, { createContext, useContext, useMemo } from "react";
4
+ import { hexToOklch, getContrastText } from "./colors";
5
+
6
+ export interface ThemeProviderProps {
7
+ children: React.ReactNode;
8
+ /** Tenant primary brand color (hex). Default: WealthX green */
9
+ primary?: string;
10
+ /** Tenant secondary brand color (hex). Default: WealthX dark navy */
11
+ secondary?: string;
12
+ /** Tenant font family override. Default: Figtree */
13
+ fontFamily?: string;
14
+ /** Disable CSS variable injection (e.g. for SSR with static CSS) */
15
+ injectCssVariables?: boolean;
16
+ }
17
+
18
+ /**
19
+ * React context that stores the computed theme CSS variables.
20
+ *
21
+ * Portal-rendered components (Dialog, AlertDialog, Popover, Tooltip, etc.)
22
+ * escape the DOM subtree of the ThemeProvider wrapper \<div\>, so they lose
23
+ * the scoped CSS custom properties set via inline styles. Components that
24
+ * render inside a portal consume this context and re-apply the variables
25
+ * on their outermost portal element, restoring the theme cascade.
26
+ */
27
+ const ThemeVarsContext = createContext<Record<string, string>>({});
28
+
29
+ /**
30
+ * Returns the theme CSS variables from the nearest ThemeProvider.
31
+ * Portal-based components use this to re-apply scoped theme vars
32
+ * on content rendered outside the ThemeProvider's DOM subtree.
33
+ */
34
+ export function useThemeVars(): Record<string, string> {
35
+ return useContext(ThemeVarsContext);
36
+ }
37
+
38
+ const DEFAULT_PRIMARY = "#33FF99";
39
+ const DEFAULT_SECONDARY = "#162029";
40
+ const DEFAULT_FONT =
41
+ '"Figtree", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
42
+
43
+ /**
44
+ * WealthX ThemeProvider — injects tenant brand colors as scoped CSS variables.
45
+ *
46
+ * How it works:
47
+ * 1. Takes primary/secondary hex colors from tenant config
48
+ * 2. Converts to oklch() (matching globals.css format)
49
+ * 3. Auto-computes WCAG contrast text for each via WCAG luminance
50
+ * 4. Sets CSS custom properties as inline styles on a wrapper \<div\>,
51
+ * scoped to this subtree (inherited by children via CSS cascade).
52
+ * This allows multiple ThemeProviders on the same page with different colors.
53
+ *
54
+ * Usage:
55
+ * \<ThemeProvider primary="#FF6600" secondary="#1A1A2E"\>
56
+ * \<App /\>
57
+ * \</ThemeProvider\>
58
+ */
59
+ export function ThemeProvider({
60
+ children,
61
+ primary = DEFAULT_PRIMARY,
62
+ secondary = DEFAULT_SECONDARY,
63
+ fontFamily = DEFAULT_FONT,
64
+ injectCssVariables = true,
65
+ }: ThemeProviderProps): React.ReactElement {
66
+ const vars = useMemo(() => {
67
+ if (!injectCssVariables) return {} as Record<string, string>;
68
+
69
+ const primaryOklch = hexToOklch(primary);
70
+ const primaryFgOklch = hexToOklch(getContrastText(primary));
71
+ const secondaryOklch = hexToOklch(secondary);
72
+ const secondaryFgOklch = hexToOklch(getContrastText(secondary));
73
+
74
+ return {
75
+ // Brand primary — used by buttons, links, focus rings, active states
76
+ "--primary": primaryOklch,
77
+ "--primary-foreground": primaryFgOklch,
78
+ "--ring": primaryOklch,
79
+
80
+ // Sidebar uses primary for active navigation
81
+ "--sidebar-primary": primaryOklch,
82
+ "--sidebar-primary-foreground": primaryFgOklch,
83
+
84
+ // Brand secondary — tenant navy used by Secondary button variant
85
+ // NOTE: --secondary is reserved for paper/surface (#F5F8FA), so we use --brand-secondary
86
+ "--brand-secondary": secondaryOklch,
87
+ "--brand-secondary-foreground": secondaryFgOklch,
88
+
89
+ // Font family — applies to all components + typography utility classes
90
+ "--font-sans": fontFamily,
91
+ "--font-family-sans": fontFamily,
92
+
93
+ // Legacy compat (used by existing WealthX apps)
94
+ "--theme-primary": primary,
95
+ "--theme-secondary": secondary,
96
+ };
97
+ }, [primary, secondary, fontFamily, injectCssVariables]);
98
+
99
+ return (
100
+ <ThemeVarsContext.Provider value={vars}>
101
+ <div data-wealthx-theme style={vars as React.CSSProperties}>
102
+ {children}
103
+ </div>
104
+ </ThemeVarsContext.Provider>
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Build CSS variables object for SSR or static contexts.
110
+ * Returns a flat Record\<string, string\> that can be serialized to a \<style\> tag.
111
+ */
112
+ export function buildCssVariables(options: {
113
+ primary?: string;
114
+ secondary?: string;
115
+ fontFamily?: string;
116
+ }): Record<string, string> {
117
+ const primary = options.primary ?? DEFAULT_PRIMARY;
118
+ const secondary = options.secondary ?? DEFAULT_SECONDARY;
119
+ const fontFamily = options.fontFamily ?? DEFAULT_FONT;
120
+
121
+ return {
122
+ "--primary": hexToOklch(primary),
123
+ "--primary-foreground": hexToOklch(getContrastText(primary)),
124
+ "--ring": hexToOklch(primary),
125
+ "--sidebar-primary": hexToOklch(primary),
126
+ "--sidebar-primary-foreground": hexToOklch(getContrastText(primary)),
127
+ "--brand-secondary": hexToOklch(secondary),
128
+ "--brand-secondary-foreground": hexToOklch(getContrastText(secondary)),
129
+ "--font-sans": fontFamily,
130
+ "--font-family-sans": fontFamily,
131
+ "--theme-primary": primary,
132
+ "--theme-secondary": secondary,
133
+ };
134
+ }