@brightweblabs/ui 1.1.0 → 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 +3 -2
- package/src/components/action.tsx +1 -1
- package/src/components/alert-dialog.tsx +2 -2
- package/src/components/avatar.tsx +2 -2
- package/src/components/badge.tsx +2 -2
- package/src/components/breadcrumb.tsx +1 -1
- package/src/components/button-variants.ts +4 -4
- package/src/components/calendar.tsx +6 -6
- package/src/components/card.tsx +1 -1
- package/src/components/chart.tsx +2 -2
- package/src/components/dropdown-menu.tsx +6 -6
- package/src/components/empty-state.tsx +2 -2
- package/src/components/field.tsx +6 -6
- package/src/components/initials-avatar.tsx +1 -1
- package/src/components/input.tsx +1 -1
- package/src/components/kpi-breakdown-bar.tsx +1 -1
- package/src/components/label.tsx +1 -1
- package/src/components/password-strength.tsx +1 -1
- package/src/components/phone-input.tsx +8 -8
- package/src/components/popover.tsx +1 -1
- package/src/components/search-field.tsx +1 -1
- package/src/components/section-heading.tsx +2 -2
- package/src/components/sheet.tsx +1 -1
- package/src/components/stat-tile.tsx +4 -4
- package/src/components/status-pill.tsx +2 -2
- package/src/components/table-pagination.tsx +2 -2
- package/src/components/table.tsx +2 -2
- package/src/components/theme-provider.tsx +87 -90
- package/src/components/tooltip.tsx +1 -1
- package/src/index.ts +23 -2
- package/src/theme/theme-controller.ts +70 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brightweblabs/ui",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "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.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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}
|
package/src/components/badge.tsx
CHANGED
|
@@ -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-
|
|
25
|
-
sm: "gap-1.5 px-xs py-0.5 text-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
89
|
-
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-
|
|
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-
|
|
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
|
)}
|
package/src/components/card.tsx
CHANGED
|
@@ -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-
|
|
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">) {
|
package/src/components/chart.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
21
|
-
{hint ? <p className="mt-1 text-
|
|
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>
|
package/src/components/field.tsx
CHANGED
|
@@ -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-
|
|
36
|
-
"data-[variant=label]:text-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
23
|
+
<AvatarFallback className="bg-transparent text-meta font-bold uppercase text-current">
|
|
24
24
|
{getInitials(label, fallback)}
|
|
25
25
|
</AvatarFallback>
|
|
26
26
|
</Avatar>
|
package/src/components/input.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
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>
|
package/src/components/label.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
174
|
-
<span className="flex-1 truncate text-
|
|
175
|
-
<span className="shrink-0 text-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
22
|
-
{subtitle ? <p className="mt-1 text-
|
|
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}
|
package/src/components/sheet.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
24
|
-
large: "text-
|
|
25
|
-
display: "text-
|
|
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-
|
|
16
|
-
normal: "h-7 px-3 text-
|
|
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
|
|
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-
|
|
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)]",
|
package/src/components/table.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
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 {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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?:
|
|
30
|
+
defaultTheme?: ThemeMode;
|
|
11
31
|
enableSystem?: boolean;
|
|
12
32
|
disableTransitionOnChange?: boolean;
|
|
13
33
|
};
|
|
14
34
|
|
|
15
|
-
type ThemeContextValue = {
|
|
16
|
-
theme:
|
|
17
|
-
setTheme: (theme:
|
|
18
|
-
resolvedTheme:
|
|
19
|
-
systemTheme:
|
|
20
|
-
themes:
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 = "
|
|
69
|
+
defaultTheme = "system",
|
|
66
70
|
enableSystem = true,
|
|
67
71
|
disableTransitionOnChange = false,
|
|
68
72
|
}: ThemeProviderProps) {
|
|
69
|
-
const [theme, setThemeState] = useState<
|
|
70
|
-
const [systemTheme, setSystemTheme] = useState<
|
|
71
|
-
const
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
systemTheme,
|
|
114
|
-
|
|
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-
|
|
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 {
|
|
41
|
-
|
|
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
|
+
}
|