@brightweblabs/ui 1.4.6 → 1.4.8

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.4.6",
4
+ "version": "1.4.8",
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",
@@ -16,6 +16,8 @@ import { cn } from "../lib/utils"
16
16
  import { Button } from "./button"
17
17
  import { buttonVariants, type ButtonVariantProps } from "./button-variants"
18
18
 
19
+ const DEFAULT_FUTURE_YEAR_RANGE = 25
20
+
19
21
  function Calendar({
20
22
  className,
21
23
  classNames,
@@ -29,6 +31,21 @@ function Calendar({
29
31
  buttonVariant?: ButtonVariantProps["variant"]
30
32
  }) {
31
33
  const defaultClassNames = getDefaultClassNames()
34
+ const hasYearDropdown =
35
+ captionLayout === "dropdown" || captionLayout === "dropdown-years"
36
+ const hasExplicitEndMonth =
37
+ props.endMonth !== undefined ||
38
+ props.toMonth !== undefined ||
39
+ props.toYear !== undefined
40
+ const defaultEndMonth =
41
+ hasYearDropdown && !hasExplicitEndMonth
42
+ ? new Date(
43
+ (props.today ?? new Date()).getFullYear() +
44
+ DEFAULT_FUTURE_YEAR_RANGE,
45
+ 11,
46
+ 31
47
+ )
48
+ : undefined
32
49
 
33
50
  return (
34
51
  <DayPicker
@@ -176,6 +193,7 @@ function Calendar({
176
193
  ...components,
177
194
  }}
178
195
  {...props}
196
+ endMonth={props.endMonth ?? defaultEndMonth}
179
197
  />
180
198
  )
181
199
  }
@@ -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";