@brightweblabs/ui 1.4.5 → 1.4.7
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 +4 -2
- package/src/components/select.tsx +216 -0
- package/src/index.ts +1 -0
- package/src/lib/utils.test.ts +41 -0
- package/src/lib/utils.ts +23 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brightweblabs/ui",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.7",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"files": [
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"./popover": "./src/components/popover.tsx",
|
|
45
45
|
"./role-badge": "./src/components/role-badge.tsx",
|
|
46
46
|
"./search-field": "./src/components/search-field.tsx",
|
|
47
|
+
"./select": "./src/components/select.tsx",
|
|
47
48
|
"./section-heading": "./src/components/section-heading.tsx",
|
|
48
49
|
"./separator": "./src/components/separator.tsx",
|
|
49
50
|
"./sheet": "./src/components/sheet.tsx",
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
"@radix-ui/react-dropdown-menu": "^2.1.22",
|
|
67
68
|
"@radix-ui/react-label": "^2.1.13",
|
|
68
69
|
"@radix-ui/react-popover": "^1.1.21",
|
|
70
|
+
"@radix-ui/react-select": "^2.3.7",
|
|
69
71
|
"@radix-ui/react-separator": "^1.1.13",
|
|
70
72
|
"@radix-ui/react-slot": "^1.3.1",
|
|
71
73
|
"@radix-ui/react-tooltip": "^1.2.14",
|
|
@@ -76,7 +78,7 @@
|
|
|
76
78
|
"recharts": "^2.15.4",
|
|
77
79
|
"sonner": "^2.0.7",
|
|
78
80
|
"tailwind-merge": "^3.6.0",
|
|
79
|
-
"@brightweblabs/theme": "0.7.
|
|
81
|
+
"@brightweblabs/theme": "0.7.4"
|
|
80
82
|
},
|
|
81
83
|
"peerDependencies": {
|
|
82
84
|
"lucide-react": "^1.8.0",
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
5
|
+
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "../lib/utils";
|
|
8
|
+
|
|
9
|
+
const Select = SelectPrimitive.Root;
|
|
10
|
+
const SelectGroup = SelectPrimitive.Group;
|
|
11
|
+
const SelectValue = SelectPrimitive.Value;
|
|
12
|
+
const EMPTY_SELECT_VALUE = "__brightweb_empty_select_value__";
|
|
13
|
+
|
|
14
|
+
export type SelectControlOption = {
|
|
15
|
+
value: string;
|
|
16
|
+
label: React.ReactNode;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SelectControlProps = {
|
|
21
|
+
id?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
value: string;
|
|
24
|
+
options: readonly SelectControlOption[];
|
|
25
|
+
onValueChange: (value: string) => void;
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
required?: boolean;
|
|
29
|
+
className?: string;
|
|
30
|
+
contentClassName?: string;
|
|
31
|
+
"aria-label"?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type StyledSelectChangeEvent = {
|
|
35
|
+
target: { value: string };
|
|
36
|
+
currentTarget: { value: string };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type StyledSelectProps = Omit<SelectControlProps, "options" | "onValueChange" | "placeholder"> & {
|
|
40
|
+
children: React.ReactNode;
|
|
41
|
+
onChange?: (event: StyledSelectChangeEvent) => void;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function SelectTrigger({
|
|
45
|
+
className,
|
|
46
|
+
children,
|
|
47
|
+
...props
|
|
48
|
+
}: React.ComponentProps<typeof SelectPrimitive.Trigger>) {
|
|
49
|
+
return (
|
|
50
|
+
<SelectPrimitive.Trigger
|
|
51
|
+
data-slot="select-trigger"
|
|
52
|
+
className={cn(
|
|
53
|
+
"flex h-9 w-full items-center justify-between gap-2 rounded-lg border border-[color:var(--sheet-edit-control-border,var(--border))] bg-[color:var(--card)] px-2.5 text-left text-body text-foreground shadow-none outline-none transition-[border-color,box-shadow,background-color] placeholder:text-foreground/45 focus:border-[color:var(--accent)] focus:ring-[3px] focus:ring-[color:var(--project-ui-color-10,var(--ring))] disabled:cursor-not-allowed disabled:opacity-55 [&>span]:min-w-0 [&>span]:truncate",
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
<SelectPrimitive.Icon asChild>
|
|
60
|
+
<ChevronDown className="size-4 shrink-0 text-foreground/55 transition-transform duration-150 data-[state=open]:rotate-180" />
|
|
61
|
+
</SelectPrimitive.Icon>
|
|
62
|
+
</SelectPrimitive.Trigger>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function SelectContent({
|
|
67
|
+
className,
|
|
68
|
+
children,
|
|
69
|
+
position = "popper",
|
|
70
|
+
sideOffset = 6,
|
|
71
|
+
...props
|
|
72
|
+
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
|
73
|
+
return (
|
|
74
|
+
<SelectPrimitive.Portal>
|
|
75
|
+
<SelectPrimitive.Content
|
|
76
|
+
data-slot="select-content"
|
|
77
|
+
position={position}
|
|
78
|
+
sideOffset={sideOffset}
|
|
79
|
+
className={cn(
|
|
80
|
+
"relative z-[1400] max-h-[min(20rem,var(--radix-select-content-available-height))] min-w-[var(--radix-select-trigger-width)] overflow-hidden rounded-xl border border-hairline bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
{...props}
|
|
84
|
+
>
|
|
85
|
+
<SelectPrimitive.ScrollUpButton className="flex h-7 cursor-default items-center justify-center bg-popover text-foreground/60">
|
|
86
|
+
<ChevronUp className="size-4" />
|
|
87
|
+
</SelectPrimitive.ScrollUpButton>
|
|
88
|
+
<SelectPrimitive.Viewport className="p-1.5">{children}</SelectPrimitive.Viewport>
|
|
89
|
+
<SelectPrimitive.ScrollDownButton className="flex h-7 cursor-default items-center justify-center bg-popover text-foreground/60">
|
|
90
|
+
<ChevronDown className="size-4" />
|
|
91
|
+
</SelectPrimitive.ScrollDownButton>
|
|
92
|
+
</SelectPrimitive.Content>
|
|
93
|
+
</SelectPrimitive.Portal>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function SelectLabel({
|
|
98
|
+
className,
|
|
99
|
+
...props
|
|
100
|
+
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
|
101
|
+
return (
|
|
102
|
+
<SelectPrimitive.Label
|
|
103
|
+
className={cn("px-2.5 py-1.5 text-label font-semibold text-muted-foreground", className)}
|
|
104
|
+
{...props}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function SelectItem({
|
|
110
|
+
className,
|
|
111
|
+
children,
|
|
112
|
+
...props
|
|
113
|
+
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
|
114
|
+
return (
|
|
115
|
+
<SelectPrimitive.Item
|
|
116
|
+
data-slot="select-item"
|
|
117
|
+
className={cn(
|
|
118
|
+
"relative flex min-h-9 w-full cursor-default select-none items-center rounded-lg py-2 pl-2.5 pr-9 text-body text-foreground outline-none transition-colors focus:bg-accent/12 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 data-[state=checked]:bg-accent/10 data-[state=checked]:font-semibold",
|
|
119
|
+
className,
|
|
120
|
+
)}
|
|
121
|
+
{...props}
|
|
122
|
+
>
|
|
123
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
124
|
+
<span className="absolute right-2.5 flex size-4 items-center justify-center text-accent">
|
|
125
|
+
<SelectPrimitive.ItemIndicator>
|
|
126
|
+
<Check className="size-4 stroke-[2.5]" />
|
|
127
|
+
</SelectPrimitive.ItemIndicator>
|
|
128
|
+
</span>
|
|
129
|
+
</SelectPrimitive.Item>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function SelectSeparator({
|
|
134
|
+
className,
|
|
135
|
+
...props
|
|
136
|
+
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
|
137
|
+
return <SelectPrimitive.Separator className={cn("my-1 h-px bg-border", className)} {...props} />;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function SelectControl({
|
|
141
|
+
id,
|
|
142
|
+
name,
|
|
143
|
+
value,
|
|
144
|
+
options,
|
|
145
|
+
onValueChange,
|
|
146
|
+
placeholder,
|
|
147
|
+
disabled,
|
|
148
|
+
required,
|
|
149
|
+
className,
|
|
150
|
+
contentClassName,
|
|
151
|
+
"aria-label": ariaLabel,
|
|
152
|
+
}: SelectControlProps) {
|
|
153
|
+
const normalizedValue = value || EMPTY_SELECT_VALUE;
|
|
154
|
+
const normalizedOptions = options.some((option) => option.value === "")
|
|
155
|
+
? options
|
|
156
|
+
: placeholder
|
|
157
|
+
? [{ value: "", label: placeholder }, ...options]
|
|
158
|
+
: options;
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<Select
|
|
162
|
+
name={name}
|
|
163
|
+
value={normalizedValue}
|
|
164
|
+
onValueChange={(nextValue) => onValueChange(nextValue === EMPTY_SELECT_VALUE ? "" : nextValue)}
|
|
165
|
+
disabled={disabled}
|
|
166
|
+
required={required}
|
|
167
|
+
>
|
|
168
|
+
<SelectTrigger id={id} aria-label={ariaLabel} className={className}>
|
|
169
|
+
<SelectValue />
|
|
170
|
+
</SelectTrigger>
|
|
171
|
+
<SelectContent align="start" className={contentClassName}>
|
|
172
|
+
{normalizedOptions.map((option) => (
|
|
173
|
+
<SelectItem
|
|
174
|
+
key={option.value || EMPTY_SELECT_VALUE}
|
|
175
|
+
value={option.value || EMPTY_SELECT_VALUE}
|
|
176
|
+
disabled={option.disabled}
|
|
177
|
+
>
|
|
178
|
+
{option.label}
|
|
179
|
+
</SelectItem>
|
|
180
|
+
))}
|
|
181
|
+
</SelectContent>
|
|
182
|
+
</Select>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function StyledSelect({ children, onChange, ...props }: StyledSelectProps) {
|
|
187
|
+
const options = React.Children.toArray(children).flatMap((child) => {
|
|
188
|
+
if (!React.isValidElement<{ value?: string | number; children?: React.ReactNode; disabled?: boolean }>(child) || child.type !== "option") return [];
|
|
189
|
+
return [{
|
|
190
|
+
value: child.props.value == null ? "" : String(child.props.value),
|
|
191
|
+
label: child.props.children,
|
|
192
|
+
disabled: child.props.disabled,
|
|
193
|
+
}];
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<SelectControl
|
|
198
|
+
{...props}
|
|
199
|
+
options={options}
|
|
200
|
+
onValueChange={(value) => onChange?.({ target: { value }, currentTarget: { value } })}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export {
|
|
206
|
+
Select,
|
|
207
|
+
SelectControl,
|
|
208
|
+
SelectContent,
|
|
209
|
+
SelectGroup,
|
|
210
|
+
SelectItem,
|
|
211
|
+
SelectLabel,
|
|
212
|
+
SelectSeparator,
|
|
213
|
+
SelectTrigger,
|
|
214
|
+
SelectValue,
|
|
215
|
+
StyledSelect,
|
|
216
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ export { PhoneInput } from "./components/phone-input";
|
|
|
26
26
|
export type { PhoneInputProps } from "./components/phone-input";
|
|
27
27
|
export { SearchField } from "./components/search-field";
|
|
28
28
|
export type { SearchFieldProps } from "./components/search-field";
|
|
29
|
+
export * from "./components/select";
|
|
29
30
|
export * from "./components/skeleton";
|
|
30
31
|
export { TableRowsSkeleton } from "./components/skeleton-table";
|
|
31
32
|
export { Separator } from "./components/separator";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import test from "node:test"
|
|
3
|
+
|
|
4
|
+
import { cn } from "./utils.ts"
|
|
5
|
+
|
|
6
|
+
test("canonical typography utilities do not remove semantic foreground colors", () => {
|
|
7
|
+
const typographyUtilities = [
|
|
8
|
+
"text-heading-1",
|
|
9
|
+
"text-heading-2",
|
|
10
|
+
"text-heading-3",
|
|
11
|
+
"text-heading-4",
|
|
12
|
+
"text-title",
|
|
13
|
+
"text-body-lg",
|
|
14
|
+
"text-body",
|
|
15
|
+
"text-meta",
|
|
16
|
+
"text-label",
|
|
17
|
+
"text-micro",
|
|
18
|
+
"text-kpi",
|
|
19
|
+
"text-kpi-lg",
|
|
20
|
+
"text-data-sm",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
for (const typography of typographyUtilities) {
|
|
24
|
+
const result = cn("text-destructive-foreground", typography).split(" ")
|
|
25
|
+
|
|
26
|
+
assert.ok(result.includes(typography), `${typography} must remain a font-size utility`)
|
|
27
|
+
assert.ok(
|
|
28
|
+
result.includes("text-destructive-foreground"),
|
|
29
|
+
`${typography} must not remove semantic foreground colors`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test("canonical typography utilities preserve intentional consumer colors", () => {
|
|
35
|
+
const result = cn("text-label text-muted-foreground", "text-body text-destructive").split(" ")
|
|
36
|
+
|
|
37
|
+
assert.ok(result.includes("text-body"))
|
|
38
|
+
assert.ok(result.includes("text-destructive"))
|
|
39
|
+
assert.ok(!result.includes("text-label"))
|
|
40
|
+
assert.ok(!result.includes("text-muted-foreground"))
|
|
41
|
+
})
|
package/src/lib/utils.ts
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { clsx, type ClassValue } from "clsx"
|
|
2
|
-
import {
|
|
2
|
+
import { extendTailwindMerge } from "tailwind-merge"
|
|
3
|
+
|
|
4
|
+
const twMerge = extendTailwindMerge<"canonical-typography">({
|
|
5
|
+
extend: {
|
|
6
|
+
classGroups: {
|
|
7
|
+
"canonical-typography": [
|
|
8
|
+
"text-heading-1",
|
|
9
|
+
"text-heading-2",
|
|
10
|
+
"text-heading-3",
|
|
11
|
+
"text-heading-4",
|
|
12
|
+
"text-title",
|
|
13
|
+
"text-body-lg",
|
|
14
|
+
"text-body",
|
|
15
|
+
"text-meta",
|
|
16
|
+
"text-label",
|
|
17
|
+
"text-micro",
|
|
18
|
+
"text-kpi",
|
|
19
|
+
"text-kpi-lg",
|
|
20
|
+
"text-data-sm",
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
})
|
|
3
25
|
|
|
4
26
|
export function cn(...inputs: ClassValue[]) {
|
|
5
27
|
return twMerge(clsx(inputs))
|