@brightweblabs/ui 1.1.1 → 1.2.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@brightweblabs/ui",
3
3
  "private": false,
4
- "version": "1.1.1",
4
+ "version": "1.2.0",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
7
7
  "files": [
@@ -56,6 +56,7 @@
56
56
  "./table": "./src/components/table.tsx",
57
57
  "./table-pagination": "./src/components/table-pagination.tsx",
58
58
  "./theme-provider": "./src/components/theme-provider.tsx",
59
+ "./theme-controller": "./src/theme/theme-controller.ts",
59
60
  "./tooltip": "./src/components/tooltip.tsx"
60
61
  },
61
62
  "dependencies": {
@@ -75,7 +76,7 @@
75
76
  "recharts": "^2.15.4",
76
77
  "sonner": "^2.0.7",
77
78
  "tailwind-merge": "^3.6.0",
78
- "@brightweblabs/theme": "0.5.0"
79
+ "@brightweblabs/theme": "0.5.1"
79
80
  },
80
81
  "peerDependencies": {
81
82
  "lucide-react": "^1.8.0",
@@ -4,7 +4,7 @@ import { buttonVariants } from "./button-variants";
4
4
  import { cn } from "../lib/utils";
5
5
 
6
6
  export const actionClassName =
7
- buttonVariants({ variant: "brand", size: "sm", className: "gap-1.5 rounded-full px-3.5 text-ui-meta [&_svg]:size-3.5" });
7
+ buttonVariants({ variant: "brand", size: "sm", className: "gap-1.5 rounded-full px-3.5 text-meta [&_svg]:size-3.5" });
8
8
 
9
9
  export type ActionButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
10
10
 
@@ -84,7 +84,7 @@ function AlertDialogTitle({
84
84
  return (
85
85
  <AlertDialogPrimitive.Title
86
86
  data-slot="alert-dialog-title"
87
- className={cn("text-base font-semibold text-foreground", className)}
87
+ className={cn("text-body-lg font-semibold text-foreground", className)}
88
88
  {...props}
89
89
  />
90
90
  )
@@ -97,7 +97,7 @@ function AlertDialogDescription({
97
97
  return (
98
98
  <AlertDialogPrimitive.Description
99
99
  data-slot="alert-dialog-description"
100
- className={cn("text-sm text-foreground/70", className)}
100
+ className={cn("text-body text-foreground/70", className)}
101
101
  {...props}
102
102
  />
103
103
  )
@@ -30,7 +30,7 @@ function AvatarFallback({ className, ...props }: React.ComponentProps<typeof Ava
30
30
  <AvatarPrimitive.Fallback
31
31
  data-slot="avatar-fallback"
32
32
  className={cn(
33
- "flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
33
+ "flex size-full items-center justify-center rounded-full bg-muted text-body text-muted-foreground group-data-[size=sm]/avatar:text-meta",
34
34
  className,
35
35
  )}
36
36
  {...props}
@@ -69,7 +69,7 @@ function AvatarGroupCount({ className, ...props }: React.ComponentProps<"div">)
69
69
  <div
70
70
  data-slot="avatar-group-count"
71
71
  className={cn(
72
- "relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
72
+ "relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-body text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
73
73
  className,
74
74
  )}
75
75
  {...props}
@@ -21,8 +21,8 @@ const badgeVariants = cva(
21
21
  tint: "text-current [background-color:var(--surface-badge-tint)]",
22
22
  },
23
23
  size: {
24
- default: "gap-1 px-2 py-0.5 text-xs font-semibold",
25
- sm: "gap-1.5 px-xs py-0.5 text-[length:var(--text-ui-micro)] font-semibold leading-none",
24
+ default: "gap-1 px-2 py-0.5 text-meta font-semibold",
25
+ sm: "gap-1.5 px-xs py-0.5 text-micro font-semibold leading-none",
26
26
  },
27
27
  },
28
28
  defaultVariants: {
@@ -15,7 +15,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
15
15
  <ol
16
16
  data-slot="breadcrumb-list"
17
17
  className={cn(
18
- "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
18
+ "text-muted-foreground flex flex-wrap items-center gap-1.5 text-body break-words sm:gap-2.5",
19
19
  className
20
20
  )}
21
21
  {...props}
@@ -1,15 +1,15 @@
1
1
  import { cva, type VariantProps } from "class-variance-authority";
2
2
 
3
3
  const buttonVariants = cva(
4
- "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius-control)] text-ui-body !font-semibold transition-[color,background-color,border-color,box-shadow,transform,filter,text-decoration-color] motion-reduce:transition-none outline-none disabled:pointer-events-none disabled:opacity-50 focus-visible:border-current focus-visible:ring-[3px] focus-visible:ring-current aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
4
+ "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius-control)] text-body !font-semibold transition-[color,background-color,border-color,box-shadow,transform,filter,text-decoration-color] motion-reduce:transition-none outline-none disabled:pointer-events-none disabled:opacity-50 focus-visible:border-current focus-visible:ring-[3px] focus-visible:ring-current aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
5
5
  {
6
6
  variants: {
7
7
  variant: {
8
8
  default: "bg-primary text-primary-foreground hover:bg-primary/90",
9
9
  accent:
10
- "relative isolate h-auto overflow-hidden !rounded-none px-0 py-0 font-display !font-bold text-[length:var(--text-ui-body)] leading-[var(--type-leading-160)] text-foreground [padding:var(--space-button-y)_var(--space-button-x)] hover:text-foreground focus-visible:border-transparent focus-visible:ring-2 focus-visible:ring-current focus-visible:ring-offset-2",
10
+ "relative isolate h-auto overflow-hidden !rounded-none px-0 py-0 font-display !font-bold text-body leading-[var(--type-leading-160)] text-foreground [padding:var(--space-button-y)_var(--space-button-x)] hover:text-foreground focus-visible:border-transparent focus-visible:ring-2 focus-visible:ring-current focus-visible:ring-offset-2",
11
11
  accentLink:
12
- "h-auto min-h-0 gap-[var(--space-sm)] !rounded-none px-0 py-0 font-display !font-bold text-[length:var(--text-ui-subhead)] leading-[var(--type-leading-166)] text-[color:var(--foreground-accent-link)] no-underline hover:underline",
12
+ "h-auto min-h-0 gap-[var(--space-sm)] !rounded-none px-0 py-0 font-display !font-bold text-heading-4 leading-[var(--type-leading-166)] text-[color:var(--foreground-accent-link)] no-underline hover:underline",
13
13
  destructive:
14
14
  "bg-destructive text-destructive-foreground hover:brightness-[1.05]",
15
15
  brand:
@@ -24,7 +24,7 @@ const buttonVariants = cva(
24
24
  },
25
25
  size: {
26
26
  default: "h-9 px-4 py-2 has-[>svg]:px-3",
27
- xs: "h-6 gap-1 rounded-[var(--radius-control)] px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
27
+ xs: "h-6 gap-1 rounded-[var(--radius-control)] px-2 text-meta has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
28
28
  sm: "h-8 gap-1.5 rounded-[var(--radius-control)] px-3 has-[>svg]:px-2.5",
29
29
  lg: "h-10 rounded-[var(--radius-control)] px-6 has-[>svg]:px-4",
30
30
  icon: "size-9",
@@ -71,7 +71,7 @@ function Calendar({
71
71
  defaultClassNames.month_caption
72
72
  ),
73
73
  dropdowns: cn(
74
- "w-full flex items-center text-sm font-semibold justify-center h-(--cell-size) gap-1.5",
74
+ "w-full flex items-center text-body font-semibold justify-center h-(--cell-size) gap-1.5",
75
75
  defaultClassNames.dropdowns
76
76
  ),
77
77
  dropdown_root: cn(
@@ -85,14 +85,14 @@ function Calendar({
85
85
  caption_label: cn(
86
86
  "select-none font-semibold",
87
87
  captionLayout === "label"
88
- ? "text-sm"
89
- : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
88
+ ? "text-body"
89
+ : "rounded-md pl-2 pr-1 flex items-center gap-1 text-body h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
90
90
  defaultClassNames.caption_label
91
91
  ),
92
92
  table: "w-full border-collapse",
93
93
  weekdays: cn("flex", defaultClassNames.weekdays),
94
94
  weekday: cn(
95
- "text-muted-foreground rounded-md flex-1 font-normal text-[length:var(--text-ui-calendar)] select-none",
95
+ "text-muted-foreground rounded-md flex-1 font-normal text-meta text-[length:var(--text-ui-calendar)] select-none",
96
96
  defaultClassNames.weekday
97
97
  ),
98
98
  week: cn("flex w-full mt-2", defaultClassNames.week),
@@ -101,7 +101,7 @@ function Calendar({
101
101
  defaultClassNames.week_number_header
102
102
  ),
103
103
  week_number: cn(
104
- "text-[length:var(--text-ui-calendar)] select-none text-muted-foreground",
104
+ "text-meta text-[length:var(--text-ui-calendar)] select-none text-muted-foreground",
105
105
  defaultClassNames.week_number
106
106
  ),
107
107
  day: cn(
@@ -209,7 +209,7 @@ function CalendarDayButton({
209
209
  data-range-end={modifiers.range_end}
210
210
  data-range-middle={modifiers.range_middle}
211
211
  className={cn(
212
- "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
212
+ "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-meta [&>span]:opacity-70",
213
213
  defaultClassNames.day,
214
214
  className
215
215
  )}
@@ -44,7 +44,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
44
44
  }
45
45
 
46
46
  function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
47
- return <div data-slot="card-description" className={cn("text-muted-foreground text-sm", className)} {...props} />;
47
+ return <div data-slot="card-description" className={cn("text-muted-foreground text-body", className)} {...props} />;
48
48
  }
49
49
 
50
50
  function CardAction({ className, ...props }: React.ComponentProps<"div">) {
@@ -79,7 +79,7 @@ function ChartContainer({
79
79
  data-slot="chart"
80
80
  data-chart={chartId}
81
81
  className={cn(
82
- "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='var(--border)']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='var(--border)']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='var(--border)']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='var(--card)']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='var(--card)']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
82
+ "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='var(--border)']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='var(--border)']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='var(--border)']]:stroke-border flex aspect-video justify-center text-meta [&_.recharts-dot[stroke='var(--card)']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='var(--card)']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
83
83
  className
84
84
  )}
85
85
  {...props}
@@ -197,7 +197,7 @@ function ChartTooltipContent({
197
197
  return (
198
198
  <div
199
199
  className={cn(
200
- "border-border/50 bg-background grid min-w-[var(--chart-tooltip-min-width)] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
200
+ "border-border/50 bg-background grid min-w-[var(--chart-tooltip-min-width)] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-meta shadow-xl",
201
201
  className
202
202
  )}
203
203
  >
@@ -74,7 +74,7 @@ function DropdownMenuItem({
74
74
  data-inset={inset}
75
75
  data-variant={variant}
76
76
  className={cn(
77
- "focus:bg-accent/15 focus:text-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-foreground/70 focus:[&_svg:not([class*='text-'])]:text-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
77
+ "focus:bg-accent/15 focus:text-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-foreground/70 focus:[&_svg:not([class*='text-'])]:text-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-body outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
78
78
  className
79
79
  )}
80
80
  {...props}
@@ -92,7 +92,7 @@ function DropdownMenuCheckboxItem({
92
92
  <DropdownMenuPrimitive.CheckboxItem
93
93
  data-slot="dropdown-menu-checkbox-item"
94
94
  className={cn(
95
- "focus:bg-accent/15 focus:text-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
95
+ "focus:bg-accent/15 focus:text-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-body outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
96
96
  className
97
97
  )}
98
98
  checked={checked}
@@ -128,7 +128,7 @@ function DropdownMenuRadioItem({
128
128
  <DropdownMenuPrimitive.RadioItem
129
129
  data-slot="dropdown-menu-radio-item"
130
130
  className={cn(
131
- "focus:bg-accent/15 focus:text-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
131
+ "focus:bg-accent/15 focus:text-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-body outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
132
132
  className
133
133
  )}
134
134
  {...props}
@@ -155,7 +155,7 @@ function DropdownMenuLabel({
155
155
  data-slot="dropdown-menu-label"
156
156
  data-inset={inset}
157
157
  className={cn(
158
- "px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8",
158
+ "px-2 py-1.5 text-body font-semibold data-[inset]:pl-8",
159
159
  className
160
160
  )}
161
161
  {...props}
@@ -184,7 +184,7 @@ function DropdownMenuShortcut({
184
184
  <span
185
185
  data-slot="dropdown-menu-shortcut"
186
186
  className={cn(
187
- "text-muted-foreground ml-auto text-xs tracking-widest",
187
+ "text-muted-foreground ml-auto text-meta tracking-widest",
188
188
  className
189
189
  )}
190
190
  {...props}
@@ -211,7 +211,7 @@ function DropdownMenuSubTrigger({
211
211
  data-slot="dropdown-menu-sub-trigger"
212
212
  data-inset={inset}
213
213
  className={cn(
214
- "focus:bg-accent/15 focus:text-foreground data-[state=open]:bg-accent/15 data-[state=open]:text-foreground [&_svg:not([class*='text-'])]:text-foreground/70 focus:[&_svg:not([class*='text-'])]:text-foreground data-[state=open]:[&_svg:not([class*='text-'])]:text-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
214
+ "focus:bg-accent/15 focus:text-foreground data-[state=open]:bg-accent/15 data-[state=open]:text-foreground [&_svg:not([class*='text-'])]:text-foreground/70 focus:[&_svg:not([class*='text-'])]:text-foreground data-[state=open]:[&_svg:not([class*='text-'])]:text-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-body outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
215
215
  className
216
216
  )}
217
217
  {...props}
@@ -17,8 +17,8 @@ export function EmptyState({ icon: Icon, title, hint, action, className, ...prop
17
17
  <Icon className="size-5 text-muted-foreground" aria-hidden="true" />
18
18
  </div>
19
19
  <div>
20
- <p className="text-ui-body font-semibold text-foreground">{title}</p>
21
- {hint ? <p className="mt-1 text-ui-meta text-muted-foreground">{hint}</p> : null}
20
+ <p className="text-body font-semibold text-foreground">{title}</p>
21
+ {hint ? <p className="mt-1 text-meta text-muted-foreground">{hint}</p> : null}
22
22
  </div>
23
23
  {action ? <div className="mt-1">{action}</div> : null}
24
24
  </div>
@@ -32,8 +32,8 @@ function FieldLegend({
32
32
  data-variant={variant}
33
33
  className={cn(
34
34
  "mb-3 font-semibold",
35
- "data-[variant=legend]:text-base",
36
- "data-[variant=label]:text-sm",
35
+ "data-[variant=legend]:text-body-lg",
36
+ "data-[variant=label]:text-body",
37
37
  className
38
38
  )}
39
39
  {...props}
@@ -130,7 +130,7 @@ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
130
130
  <div
131
131
  data-slot="field-label"
132
132
  className={cn(
133
- "flex w-fit items-center gap-2 text-sm leading-snug font-semibold group-data-[disabled=true]/field:opacity-50",
133
+ "flex w-fit items-center gap-2 text-body leading-snug font-semibold group-data-[disabled=true]/field:opacity-50",
134
134
  className
135
135
  )}
136
136
  {...props}
@@ -143,7 +143,7 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
143
143
  <p
144
144
  data-slot="field-description"
145
145
  className={cn(
146
- "text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
146
+ "text-muted-foreground text-body leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
147
147
  "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
148
148
  "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
149
149
  className
@@ -165,7 +165,7 @@ function FieldSeparator({
165
165
  data-slot="field-separator"
166
166
  data-content={!!children}
167
167
  className={cn(
168
- "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
168
+ "relative -my-2 h-5 text-body group-data-[variant=outline]/field-group:-mb-2",
169
169
  className
170
170
  )}
171
171
  {...props}
@@ -226,7 +226,7 @@ function FieldError({
226
226
  <div
227
227
  role="alert"
228
228
  data-slot="field-error"
229
- className={cn("text-destructive text-sm font-normal", className)}
229
+ className={cn("text-destructive text-body font-normal", className)}
230
230
  {...props}
231
231
  >
232
232
  {content}
@@ -20,7 +20,7 @@ const toneClasses: Record<InitialsAvatarTone, string> = {
20
20
  export function InitialsAvatar({ label, fallback, tone = "active", className, ...props }: InitialsAvatarProps) {
21
21
  return (
22
22
  <Avatar className={cn("shrink-0", toneClasses[tone], className)} {...props}>
23
- <AvatarFallback className="bg-transparent text-ui-meta font-bold uppercase text-current">
23
+ <AvatarFallback className="bg-transparent text-meta font-bold uppercase text-current">
24
24
  {getInitials(label, fallback)}
25
25
  </AvatarFallback>
26
26
  </Avatar>
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
8
8
  type={type}
9
9
  data-slot="input"
10
10
  className={cn(
11
- "file:text-foreground placeholder:text-foreground-muted-accessible selection:bg-primary selection:text-primary-foreground h-11 w-full min-w-0 rounded-xl border border-foreground/15 bg-foreground/5 px-3 py-1 text-base text-foreground shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-input file:text-sm file:font-semibold disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
11
+ "file:text-foreground placeholder:text-foreground-muted-accessible selection:bg-primary selection:text-primary-foreground h-11 w-full min-w-0 rounded-xl border border-foreground/15 bg-foreground/5 px-3 py-1 text-body-lg text-foreground shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-input file:text-body file:font-semibold disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-body",
12
12
  "focus:border-primary focus:ring-primary/40 focus:ring-[3px]",
13
13
  "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14
14
  className,
@@ -56,7 +56,7 @@ export function KpiBreakdownBar({ items, toneTokens, className }: KpiBreakdownBa
56
56
  {items.map((item) => {
57
57
  const token = toneTokens[item.tone];
58
58
  return (
59
- <div key={`${item.tone}:${item.label}`} className="flex items-center justify-between gap-2 text-ui-meta">
59
+ <div key={`${item.tone}:${item.label}`} className="flex items-center justify-between gap-2 text-meta">
60
60
  <span className="flex min-w-0 items-center gap-2 text-muted-foreground">
61
61
  <span className="size-1.5 shrink-0 rounded-[var(--radius-swatch)]" style={token ? tokenStyle(token) : undefined} />
62
62
  <span className="truncate">{item.label}</span>
@@ -13,7 +13,7 @@ function Label({
13
13
  <LabelPrimitive.Root
14
14
  data-slot="label"
15
15
  className={cn(
16
- "flex items-center gap-2 text-sm leading-none font-semibold select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
16
+ "flex items-center gap-2 text-body leading-none font-semibold select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17
17
  className
18
18
  )}
19
19
  {...props}
@@ -79,7 +79,7 @@ export function PasswordStrength({ password, className }: PasswordStrengthProps)
79
79
  />
80
80
  </div>
81
81
 
82
- <p className={cn("text-ui-meta !font-semibold transition-colors", config.textColor)}>
82
+ <p className={cn("text-meta !font-semibold transition-colors", config.textColor)}>
83
83
  Força: {config.label}
84
84
  </p>
85
85
  </div>
@@ -119,13 +119,13 @@ export function PhoneInput({ value, onChange, defaultCountry = "pt", disabled =
119
119
  closePicker(true);
120
120
  }
121
121
  }}
122
- className="flex h-7 items-center gap-1 border-0 bg-transparent pr-1.5 text-sm disabled:pointer-events-none disabled:opacity-100"
122
+ className="flex h-7 items-center gap-1 border-0 bg-transparent pr-1.5 text-body disabled:pointer-events-none disabled:opacity-100"
123
123
  aria-controls={countryListId}
124
124
  aria-expanded={open}
125
125
  aria-haspopup="listbox"
126
126
  aria-label="Choose country code"
127
127
  >
128
- <span className="text-base leading-none">{flagEmoji(country.iso2)}</span>
128
+ <span className="text-body-lg leading-none">{flagEmoji(country.iso2)}</span>
129
129
  <svg className="size-2.5 text-foreground/40" viewBox="0 0 10 6" fill="none" aria-hidden>
130
130
  <path d="M1 1l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
131
131
  </svg>
@@ -149,7 +149,7 @@ export function PhoneInput({ value, onChange, defaultCountry = "pt", disabled =
149
149
  }}
150
150
  onKeyDown={handlePickerKeyDown}
151
151
  placeholder="Search…"
152
- className="w-full border-0 bg-transparent text-ui-meta text-foreground outline-none placeholder:text-foreground-muted-accessible"
152
+ className="w-full border-0 bg-transparent text-meta text-foreground outline-none placeholder:text-foreground-muted-accessible"
153
153
  />
154
154
  </div>
155
155
  <div id={countryListId} role="listbox" aria-label="Country calling codes" className="max-h-48 overflow-y-auto rounded-b-[var(--radius-card)] p-1">
@@ -170,18 +170,18 @@ export function PhoneInput({ value, onChange, defaultCountry = "pt", disabled =
170
170
  (country.iso2 === candidate.iso2 || activeIndex === index) && "bg-[color:var(--elevate-3)] text-[color:var(--brand-accent)]",
171
171
  )}
172
172
  >
173
- <span className="text-sm leading-none">{flagEmoji(candidate.iso2)}</span>
174
- <span className="flex-1 truncate text-ui-label normal-case tracking-normal text-foreground">{candidate.name}</span>
175
- <span className="shrink-0 text-ui-micro text-foreground-muted-accessible">+{candidate.dialCode}</span>
173
+ <span className="text-body leading-none">{flagEmoji(candidate.iso2)}</span>
174
+ <span className="flex-1 truncate text-label normal-case tracking-normal text-foreground">{candidate.name}</span>
175
+ <span className="shrink-0 text-micro text-foreground-muted-accessible">+{candidate.dialCode}</span>
176
176
  </button>
177
177
  ))}
178
- {filteredCountries.length === 0 ? <p className="px-2 py-2 text-ui-meta text-foreground-muted-accessible">No results</p> : null}
178
+ {filteredCountries.length === 0 ? <p className="px-2 py-2 text-meta text-foreground-muted-accessible">No results</p> : null}
179
179
  </div>
180
180
  </div>
181
181
  ) : null}
182
182
 
183
183
  <span className="h-4 w-px shrink-0 bg-foreground/15" />
184
- <input {...inputProps} ref={inputRef} value={inputValue} onChange={handlePhoneValueChange} disabled={disabled} placeholder={placeholder} type="tel" className="h-7 flex-1 border-0 bg-transparent pl-2 text-sm text-foreground outline-none placeholder:text-foreground-muted-accessible disabled:pointer-events-none disabled:opacity-100" />
184
+ <input {...inputProps} ref={inputRef} value={inputValue} onChange={handlePhoneValueChange} disabled={disabled} placeholder={placeholder} type="tel" className="h-7 flex-1 border-0 bg-transparent pl-2 text-body text-foreground outline-none placeholder:text-foreground-muted-accessible disabled:pointer-events-none disabled:opacity-100" />
185
185
  </div>
186
186
  );
187
187
  }
@@ -49,7 +49,7 @@ function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
49
49
  return (
50
50
  <div
51
51
  data-slot="popover-header"
52
- className={cn("flex flex-col gap-1 text-sm", className)}
52
+ className={cn("flex flex-col gap-1 text-body", className)}
53
53
  {...props}
54
54
  />
55
55
  )
@@ -41,7 +41,7 @@ export function SearchField({
41
41
  aria-label={ariaLabel ?? placeholder}
42
42
  className={cn(
43
43
  size === "sm" ? "h-8" : "h-9",
44
- "rounded-[var(--radius-card)] border-border bg-card pl-8 text-ui-meta text-foreground",
44
+ "rounded-[var(--radius-card)] border-border bg-card pl-8 text-meta text-foreground",
45
45
  showClear ? "pr-8" : null,
46
46
  inputClassName,
47
47
  )}
@@ -18,8 +18,8 @@ export function SectionHeading({ icon: Icon, title, subtitle, action, className,
18
18
  <Icon className="size-3.5" />
19
19
  </span>
20
20
  <div className="min-w-0">
21
- <h2 className="text-ui-panel-title">{title}</h2>
22
- {subtitle ? <p className="mt-1 text-ui-body text-muted-foreground">{subtitle}</p> : null}
21
+ <h2 className="text-heading-3">{title}</h2>
22
+ {subtitle ? <p className="mt-1 text-body text-muted-foreground">{subtitle}</p> : null}
23
23
  </div>
24
24
  </div>
25
25
  {action ? <div className="shrink-0">{action}</div> : null}
@@ -154,7 +154,7 @@ function SheetDescription({
154
154
  return (
155
155
  <SheetPrimitive.Description
156
156
  data-slot="sheet-description"
157
- className={cn("text-muted-foreground text-sm", className)}
157
+ className={cn("text-muted-foreground text-body", className)}
158
158
  {...props}
159
159
  />
160
160
  )
@@ -9,7 +9,7 @@ export type StatTileProps = HTMLAttributes<HTMLDivElement> & {
9
9
  export function StatTile({ label, children, className, ...props }: StatTileProps) {
10
10
  return (
11
11
  <div className={cn("stat-cell", className)} {...props}>
12
- <p className="text-ui-label text-muted-foreground">{label}</p>
12
+ <p className="text-label text-muted-foreground">{label}</p>
13
13
  <div className="mt-3">{children}</div>
14
14
  </div>
15
15
  );
@@ -20,9 +20,9 @@ export type StatValueProps = HTMLAttributes<HTMLParagraphElement> & {
20
20
  };
21
21
 
22
22
  const statValueSizeClasses: Record<NonNullable<StatValueProps["size"]>, string> = {
23
- normal: "text-ui-metric",
24
- large: "text-ui-metric-xl",
25
- display: "text-ui-metric-display",
23
+ normal: "text-metric",
24
+ large: "text-metric-lg",
25
+ display: "text-metric-display",
26
26
  };
27
27
 
28
28
  export function StatValue({ size = "normal", className, ...props }: StatValueProps) {
@@ -12,8 +12,8 @@ export const STATUS_PILL_BASE_CLASS =
12
12
  "inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-full border font-semibold leading-none";
13
13
 
14
14
  export const STATUS_PILL_SIZE_CLASSES: Record<StatusPillSize, string> = {
15
- small: "h-5 px-2 text-ui-micro",
16
- normal: "h-7 px-3 text-ui-meta",
15
+ small: "h-5 px-2 text-micro",
16
+ normal: "h-7 px-3 text-meta",
17
17
  };
18
18
 
19
19
  export type StatusPillProps = Omit<ComponentProps<"span">, "children"> & {
@@ -39,7 +39,7 @@ export function TablePagination({
39
39
  return (
40
40
  <div className={cn("flex min-w-0 flex-col gap-2 border-t border-[color:var(--border)] px-4 py-2 md:flex-row md:items-center md:justify-between", className)}>
41
41
  {summary ? (
42
- <p className="min-w-0 truncate portal-meta leading-tight text-[color:var(--muted-foreground)]">
42
+ <p className="min-w-0 truncate text-meta text-muted-foreground leading-tight text-[color:var(--muted-foreground)]">
43
43
  <span className="font-semibold text-[color:var(--foreground)]">{pageLabel(safePage, safeTotalPages)}</span>
44
44
  <span className="px-1.5 text-[color:var(--border)]">·</span>
45
45
  {summary}
@@ -73,7 +73,7 @@ export function TablePagination({
73
73
  changePage(item);
74
74
  }}
75
75
  className={cn(
76
- "size-8 rounded-full text-[length:var(--text-ui-label)] font-semibold",
76
+ "size-8 rounded-full text-label font-semibold",
77
77
  item === safePage
78
78
  ? "border-[color:var(--border-pagination-active)] bg-[color:var(--surface-pagination-active)] text-[color:var(--foreground)]"
79
79
  : "text-[color:var(--muted-foreground)] hover:bg-[color:var(--muted)] hover:text-[color:var(--foreground)]",
@@ -8,7 +8,7 @@ const Table = React.forwardRef<
8
8
  >(
9
9
  ({ className, containerClassName, ...props }, ref) => (
10
10
  <div className={cn("w-full flex-1 overflow-auto", containerClassName)}>
11
- <table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
11
+ <table ref={ref} className={cn("w-full caption-bottom text-body", className)} {...props} />
12
12
  </div>
13
13
  ),
14
14
  );
@@ -67,7 +67,7 @@ TableCell.displayName = "TableCell";
67
67
 
68
68
  const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
69
69
  ({ className, ...props }, ref) => (
70
- <caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
70
+ <caption ref={ref} className={cn("mt-4 text-body text-muted-foreground", className)} {...props} />
71
71
  ),
72
72
  );
73
73
  TableCaption.displayName = "TableCaption";
@@ -1,130 +1,127 @@
1
1
  "use client";
2
2
 
3
- import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useState } from "react";
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useLayoutEffect,
9
+ useMemo,
10
+ useState,
11
+ } from "react";
4
12
  import type { ReactNode } from "react";
5
-
6
- type Theme = "light" | "dark" | "system";
7
-
8
- type ThemeProviderProps = {
13
+ import {
14
+ applyResolvedTheme,
15
+ BW_THEME_MEDIA_QUERY,
16
+ BW_THEME_STORAGE_KEY,
17
+ getSystemTheme,
18
+ persistTheme,
19
+ readStoredTheme,
20
+ resolveTheme,
21
+ subscribeToSystemTheme,
22
+ THEME_MODES,
23
+ type ResolvedTheme,
24
+ type ThemeMode,
25
+ } from "../theme/theme-controller";
26
+ export type { Theme, ThemeMode, ResolvedTheme } from "../theme/theme-controller";
27
+
28
+ export type ThemeProviderProps = {
9
29
  children: ReactNode;
10
- defaultTheme?: Theme;
30
+ defaultTheme?: ThemeMode;
11
31
  enableSystem?: boolean;
12
32
  disableTransitionOnChange?: boolean;
13
33
  };
14
34
 
15
- type ThemeContextValue = {
16
- theme: Theme;
17
- setTheme: (theme: string) => void;
18
- resolvedTheme: "light" | "dark";
19
- systemTheme: "light" | "dark";
20
- themes: Theme[];
35
+ export type ThemeContextValue = {
36
+ theme: ThemeMode;
37
+ setTheme: (theme: ThemeMode) => void;
38
+ resolvedTheme: ResolvedTheme;
39
+ systemTheme: ResolvedTheme;
40
+ themes: readonly ThemeMode[];
41
+ isThemeProviderMounted: boolean;
21
42
  };
22
43
 
23
- const STORAGE_KEY = "theme";
24
- const THEME_VALUES: Theme[] = ["light", "dark", "system"];
25
- const ThemeContext = createContext<ThemeContextValue | null>(null);
26
-
27
- function getSystemTheme(): "light" | "dark" {
28
- if (typeof window === "undefined") return "light";
29
- return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
30
- }
31
-
32
- function readStoredTheme(defaultTheme: Theme): Theme {
33
- if (typeof window === "undefined") return defaultTheme;
34
- try {
35
- const stored = window.localStorage.getItem(STORAGE_KEY);
36
- return stored === "light" || stored === "dark" || stored === "system" ? stored : defaultTheme;
37
- } catch {
38
- return defaultTheme;
39
- }
40
- }
41
-
42
- function applyResolvedTheme(resolvedTheme: "light" | "dark", disableTransitionOnChange: boolean) {
43
- const root = document.documentElement;
44
- let cleanup: (() => void) | null = null;
44
+ const FALLBACK_CONTEXT: ThemeContextValue = {
45
+ theme: "system",
46
+ setTheme: () => undefined,
47
+ resolvedTheme: "light",
48
+ systemTheme: "light",
49
+ themes: THEME_MODES,
50
+ isThemeProviderMounted: false,
51
+ };
45
52
 
46
- if (disableTransitionOnChange) {
47
- const style = document.createElement("style");
48
- style.appendChild(document.createTextNode("*,*::before,*::after{transition:none!important;animation-duration:0s!important}"));
49
- document.head.appendChild(style);
50
- cleanup = () => {
51
- window.getComputedStyle(document.body);
52
- window.setTimeout(() => style.remove(), 1);
53
- };
54
- }
53
+ const ThemeContext = createContext<ThemeContextValue | null>(null);
55
54
 
56
- root.classList.remove("light", "dark");
57
- root.classList.add(resolvedTheme);
58
- root.dataset.theme = resolvedTheme;
59
- root.style.colorScheme = resolvedTheme;
60
- cleanup?.();
55
+ function disableTransitionsTemporarily() {
56
+ const style = document.createElement("style");
57
+ style.appendChild(
58
+ document.createTextNode(
59
+ "*,*::before,*::after{transition:none!important;animation-duration:0s!important}",
60
+ ),
61
+ );
62
+ document.head.appendChild(style);
63
+ window.getComputedStyle(document.body);
64
+ window.setTimeout(() => style.remove(), 1);
61
65
  }
62
66
 
63
67
  export function ThemeProvider({
64
68
  children,
65
- defaultTheme = "light",
69
+ defaultTheme = "system",
66
70
  enableSystem = true,
67
71
  disableTransitionOnChange = false,
68
72
  }: ThemeProviderProps) {
69
- const [theme, setThemeState] = useState<Theme>(defaultTheme);
70
- const [systemTheme, setSystemTheme] = useState<"light" | "dark">("light");
71
- const resolvedTheme = theme === "system" && enableSystem ? systemTheme : theme === "dark" ? "dark" : "light";
73
+ const [theme, setThemeState] = useState<ThemeMode>(defaultTheme);
74
+ const [systemTheme, setSystemTheme] = useState<ResolvedTheme>("light");
75
+ const [isReady, setIsReady] = useState(false);
76
+ const resolvedTheme = resolveTheme(theme, systemTheme, enableSystem);
72
77
 
73
78
  useLayoutEffect(() => {
74
- applyResolvedTheme(resolvedTheme, disableTransitionOnChange);
75
- }, [disableTransitionOnChange, resolvedTheme]);
76
-
77
- useEffect(() => {
78
- setThemeState(readStoredTheme(defaultTheme));
79
- setSystemTheme(getSystemTheme());
79
+ const mediaQuery = window.matchMedia(BW_THEME_MEDIA_QUERY);
80
+ setThemeState(readStoredTheme(window.localStorage, defaultTheme));
81
+ setSystemTheme(getSystemTheme(mediaQuery));
82
+ setIsReady(true);
80
83
  }, [defaultTheme]);
81
84
 
85
+ useLayoutEffect(() => {
86
+ if (!isReady) return;
87
+ if (disableTransitionOnChange) disableTransitionsTemporarily();
88
+ applyResolvedTheme(document.documentElement, resolvedTheme);
89
+ }, [disableTransitionOnChange, isReady, resolvedTheme]);
90
+
82
91
  useEffect(() => {
83
92
  if (!enableSystem) return;
84
- const media = window.matchMedia("(prefers-color-scheme: dark)");
85
- const handleChange = () => setSystemTheme(media.matches ? "dark" : "light");
86
- handleChange();
87
- media.addEventListener("change", handleChange);
88
- return () => media.removeEventListener("change", handleChange);
93
+ return subscribeToSystemTheme(window.matchMedia(BW_THEME_MEDIA_QUERY), setSystemTheme);
89
94
  }, [enableSystem]);
90
95
 
91
96
  useEffect(() => {
92
97
  const handleStorage = (event: StorageEvent) => {
93
- if (event.key === STORAGE_KEY) setThemeState(readStoredTheme(defaultTheme));
98
+ if (event.key !== BW_THEME_STORAGE_KEY) return;
99
+ setThemeState(readStoredTheme(window.localStorage, defaultTheme));
94
100
  };
95
101
  window.addEventListener("storage", handleStorage);
96
102
  return () => window.removeEventListener("storage", handleStorage);
97
103
  }, [defaultTheme]);
98
104
 
99
- const setTheme = useCallback((nextTheme: string) => {
100
- const normalizedTheme: Theme = nextTheme === "dark" || nextTheme === "light" || nextTheme === "system" ? nextTheme : defaultTheme;
101
- setThemeState(normalizedTheme);
102
- try {
103
- window.localStorage.setItem(STORAGE_KEY, normalizedTheme);
104
- } catch {
105
- // In-memory theme state remains usable when storage is unavailable.
106
- }
107
- }, [defaultTheme]);
108
-
109
- const value = useMemo<ThemeContextValue>(() => ({
110
- theme,
111
- setTheme,
112
- resolvedTheme,
113
- systemTheme,
114
- themes: enableSystem ? THEME_VALUES : ["light", "dark"],
115
- }), [enableSystem, resolvedTheme, setTheme, systemTheme, theme]);
105
+ const setTheme = useCallback((nextTheme: ThemeMode) => {
106
+ setThemeState(nextTheme);
107
+ persistTheme(window.localStorage, nextTheme);
108
+ }, []);
109
+
110
+ const value = useMemo<ThemeContextValue>(
111
+ () => ({
112
+ theme,
113
+ setTheme,
114
+ resolvedTheme,
115
+ systemTheme,
116
+ themes: enableSystem ? THEME_MODES : ["light", "dark"],
117
+ isThemeProviderMounted: true,
118
+ }),
119
+ [enableSystem, resolvedTheme, setTheme, systemTheme, theme],
120
+ );
116
121
 
117
122
  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
118
123
  }
119
124
 
120
125
  export function useTheme(): ThemeContextValue {
121
- return useContext(ThemeContext) ?? {
122
- theme: "light",
123
- setTheme: () => undefined,
124
- resolvedTheme: "light",
125
- systemTheme: "light",
126
- themes: THEME_VALUES,
127
- };
126
+ return useContext(ThemeContext) ?? FALLBACK_CONTEXT;
128
127
  }
129
-
130
- export type { Theme, ThemeContextValue, ThemeProviderProps };
@@ -41,7 +41,7 @@ function TooltipContent({
41
41
  data-slot="tooltip-content"
42
42
  sideOffset={sideOffset}
43
43
  className={cn(
44
- "z-[1300] overflow-hidden rounded-[var(--radius-card)] border border-[color:var(--hairline-strong)] bg-[color:var(--surface-tooltip)] px-2.5 py-1.5 text-ui-meta !font-semibold text-popover-foreground shadow-[var(--shadow-tooltip)] backdrop-blur-xl data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
44
+ "z-[1300] overflow-hidden rounded-[var(--radius-card)] border border-[color:var(--hairline-strong)] bg-[color:var(--surface-tooltip)] px-2.5 py-1.5 text-meta !font-semibold text-popover-foreground shadow-[var(--shadow-tooltip)] backdrop-blur-xl data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
45
45
  className
46
46
  )}
47
47
  {...props}
package/src/index.ts CHANGED
@@ -37,8 +37,29 @@ export * from "./components/popover";
37
37
  export * from "./components/sheet";
38
38
  export * from "./components/table";
39
39
  export { Toaster } from "./components/sonner";
40
- export { ThemeProvider, useTheme } from "./components/theme-provider";
41
- export type { Theme, ThemeContextValue, ThemeProviderProps } from "./components/theme-provider";
40
+ export {
41
+ ThemeProvider,
42
+ useTheme,
43
+ } from "./components/theme-provider";
44
+ export {
45
+ applyResolvedTheme,
46
+ BW_THEME_MEDIA_QUERY,
47
+ BW_THEME_STORAGE_KEY,
48
+ getSystemTheme,
49
+ getThemeBootstrapScript,
50
+ isThemeMode,
51
+ persistTheme,
52
+ readStoredTheme,
53
+ resolveTheme,
54
+ subscribeToSystemTheme,
55
+ THEME_MODES,
56
+ } from "./theme/theme-controller";
57
+ export type {
58
+ Theme,
59
+ ThemeContextValue,
60
+ ThemeProviderProps,
61
+ } from "./components/theme-provider";
62
+ export type { ResolvedTheme, ThemeMode } from "./theme/theme-controller";
42
63
  export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./components/tooltip";
43
64
  export { ActivityMessage } from "./components/activity-message";
44
65
  export * from "./components/action";
@@ -0,0 +1,70 @@
1
+ export const BW_THEME_STORAGE_KEY = "bw-theme";
2
+ export const BW_THEME_MEDIA_QUERY = "(prefers-color-scheme: dark)";
3
+ export const THEME_MODES = ["light", "dark", "system"] as const;
4
+
5
+ export type ThemeMode = (typeof THEME_MODES)[number];
6
+ export type Theme = ThemeMode;
7
+ export type ResolvedTheme = Exclude<ThemeMode, "system">;
8
+
9
+ type ThemeStorage = Pick<Storage, "getItem" | "setItem">;
10
+ type ThemeMediaQuery = Pick<MediaQueryList, "matches" | "addEventListener" | "removeEventListener">;
11
+
12
+ export function isThemeMode(value: unknown): value is ThemeMode {
13
+ return typeof value === "string" && THEME_MODES.includes(value as ThemeMode);
14
+ }
15
+
16
+ export function resolveTheme(mode: ThemeMode, systemTheme: ResolvedTheme, enableSystem = true): ResolvedTheme {
17
+ if (mode === "system") return enableSystem ? systemTheme : "light";
18
+ return mode;
19
+ }
20
+
21
+ export function readStoredTheme(
22
+ storage: Pick<ThemeStorage, "getItem"> | null | undefined,
23
+ defaultTheme: ThemeMode = "system",
24
+ ): ThemeMode {
25
+ if (!storage) return defaultTheme;
26
+ try {
27
+ const storedTheme = storage.getItem(BW_THEME_STORAGE_KEY);
28
+ return isThemeMode(storedTheme) ? storedTheme : defaultTheme;
29
+ } catch {
30
+ return defaultTheme;
31
+ }
32
+ }
33
+
34
+ export function persistTheme(
35
+ storage: Pick<ThemeStorage, "setItem"> | null | undefined,
36
+ theme: ThemeMode,
37
+ ) {
38
+ if (!storage) return;
39
+ try {
40
+ storage.setItem(BW_THEME_STORAGE_KEY, theme);
41
+ } catch {
42
+ // Theme changes remain usable in memory when storage is unavailable.
43
+ }
44
+ }
45
+
46
+ export function getSystemTheme(mediaQuery: Pick<ThemeMediaQuery, "matches">): ResolvedTheme {
47
+ return mediaQuery.matches ? "dark" : "light";
48
+ }
49
+
50
+ export function subscribeToSystemTheme(
51
+ mediaQuery: ThemeMediaQuery,
52
+ onChange: (theme: ResolvedTheme) => void,
53
+ ) {
54
+ const handleChange = () => onChange(getSystemTheme(mediaQuery));
55
+ handleChange();
56
+ mediaQuery.addEventListener("change", handleChange);
57
+ return () => mediaQuery.removeEventListener("change", handleChange);
58
+ }
59
+
60
+ export function applyResolvedTheme(root: HTMLElement, theme: ResolvedTheme) {
61
+ root.classList.remove("light", "dark");
62
+ root.classList.add(theme);
63
+ root.dataset.theme = theme;
64
+ root.style.colorScheme = theme;
65
+ }
66
+
67
+ export function getThemeBootstrapScript(defaultTheme: ThemeMode = "system") {
68
+ const safeDefaultTheme = isThemeMode(defaultTheme) ? defaultTheme : "system";
69
+ return `!function(){var r=document.documentElement,m="${safeDefaultTheme}";try{var s=localStorage.getItem("${BW_THEME_STORAGE_KEY}");m=s==="light"||s==="dark"||s==="system"?s:m}catch(e){}var t=m==="system"?(matchMedia("${BW_THEME_MEDIA_QUERY}").matches?"dark":"light"):m;r.classList.remove("light","dark");r.classList.add(t);r.setAttribute("data-theme",t);r.style.colorScheme=t}();`;
70
+ }