@hilum/ui 3.2.3 → 3.2.4
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/dist/index.d.ts +348 -32
- package/dist/index.js +2511 -676
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/alert-dialog.tsx +19 -6
- package/src/components/callout.tsx +99 -0
- package/src/components/card.tsx +13 -0
- package/src/components/chart.tsx +398 -29
- package/src/components/color-picker.tsx +39 -45
- package/src/components/combobox.tsx +83 -64
- package/src/components/command.tsx +35 -0
- package/src/components/context-menu.tsx +49 -27
- package/src/components/data-transfer-controls.tsx +156 -0
- package/src/components/dialog.tsx +19 -6
- package/src/components/dropdown-menu.tsx +50 -28
- package/src/components/file-dropzone.tsx +171 -0
- package/src/components/help-tooltip.tsx +90 -0
- package/src/components/media-asset-card.tsx +118 -0
- package/src/components/menubar.tsx +52 -30
- package/src/components/popover.tsx +31 -17
- package/src/components/rich-text-editor.tsx +448 -0
- package/src/components/searchable-table.tsx +412 -0
- package/src/components/select.tsx +44 -28
- package/src/components/sidebar.tsx +105 -14
- package/src/components/stat-card.tsx +122 -8
- package/src/components/status-badge.tsx +144 -0
- package/src/components/titled-card.tsx +102 -0
- package/src/components/url-redirect-prompt.tsx +85 -0
- package/src/index.ts +10 -0
- package/src/lib/mobile-popper-sheet.ts +78 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ArrowDown, ArrowUp, ChevronsUpDown, Filter, Search } from "lucide-react";
|
|
5
|
+
import { Button } from "./button";
|
|
6
|
+
import { EmptyState } from "./empty-state";
|
|
7
|
+
import { InputGroup } from "./input-group";
|
|
8
|
+
import {
|
|
9
|
+
Pagination,
|
|
10
|
+
PaginationContent,
|
|
11
|
+
PaginationEllipsis,
|
|
12
|
+
PaginationItem,
|
|
13
|
+
PaginationLink,
|
|
14
|
+
PaginationNext,
|
|
15
|
+
PaginationPrevious,
|
|
16
|
+
} from "./pagination";
|
|
17
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
|
|
18
|
+
import { StackedList, StackedListItem } from "./stacked-list";
|
|
19
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./table";
|
|
20
|
+
import { cn } from "../lib/utils";
|
|
21
|
+
|
|
22
|
+
interface SearchableTableFilterOption {
|
|
23
|
+
value: string;
|
|
24
|
+
label: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SearchableTableColumn<T = unknown> {
|
|
28
|
+
key: string;
|
|
29
|
+
label: string;
|
|
30
|
+
render?: (item: T) => React.ReactNode;
|
|
31
|
+
sortable?: boolean;
|
|
32
|
+
sortAccessor?: (item: T) => string | number | Date | null | undefined;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface SearchableTableProps<T extends { id: string | number }> {
|
|
37
|
+
data: T[];
|
|
38
|
+
columns: SearchableTableColumn<T>[];
|
|
39
|
+
searchPlaceholder?: string;
|
|
40
|
+
searchTerm: string;
|
|
41
|
+
onSearchChange: (value: string) => void;
|
|
42
|
+
filters?: Record<
|
|
43
|
+
string,
|
|
44
|
+
{
|
|
45
|
+
value: string;
|
|
46
|
+
onChange: (value: string) => void;
|
|
47
|
+
options: SearchableTableFilterOption[];
|
|
48
|
+
placeholder: string;
|
|
49
|
+
}
|
|
50
|
+
>;
|
|
51
|
+
emptyState?: {
|
|
52
|
+
icon?: React.ReactNode;
|
|
53
|
+
title: string;
|
|
54
|
+
description: string;
|
|
55
|
+
action?: {
|
|
56
|
+
label: string;
|
|
57
|
+
href?: string;
|
|
58
|
+
onClick?: () => void;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
actions?: (item: T) => React.ReactNode;
|
|
62
|
+
mobileCard?: (item: T) => React.ReactNode;
|
|
63
|
+
tableClassName?: string;
|
|
64
|
+
pagination?: {
|
|
65
|
+
pageSize?: number;
|
|
66
|
+
currentPage: number;
|
|
67
|
+
onPageChange: (page: number) => void;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function appendSearchableValue(value: unknown, parts: string[], depth = 0) {
|
|
72
|
+
if (value == null || depth > 2) return;
|
|
73
|
+
|
|
74
|
+
if (value instanceof Date) {
|
|
75
|
+
parts.push(value.toISOString());
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (["string", "number", "boolean"].includes(typeof value)) {
|
|
80
|
+
parts.push(String(value));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
value.forEach((entry) => appendSearchableValue(entry, parts, depth + 1));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof value === "object") {
|
|
90
|
+
Object.values(value as Record<string, unknown>).forEach((entry) =>
|
|
91
|
+
appendSearchableValue(entry, parts, depth + 1),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getSearchableText(item: unknown) {
|
|
97
|
+
const parts: string[] = [];
|
|
98
|
+
appendSearchableValue(item, parts);
|
|
99
|
+
return parts.join(" ").toLowerCase();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizeEmptyStateIcon(icon: React.ReactNode) {
|
|
103
|
+
return icon ? <span className="flex [&_svg]:size-5">{icon}</span> : undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function SearchableTable<T extends { id: string | number }>({
|
|
107
|
+
data,
|
|
108
|
+
columns,
|
|
109
|
+
searchPlaceholder = "Search...",
|
|
110
|
+
searchTerm,
|
|
111
|
+
onSearchChange,
|
|
112
|
+
filters = {},
|
|
113
|
+
emptyState,
|
|
114
|
+
actions,
|
|
115
|
+
mobileCard,
|
|
116
|
+
tableClassName,
|
|
117
|
+
pagination,
|
|
118
|
+
}: SearchableTableProps<T>) {
|
|
119
|
+
const [sort, setSort] = React.useState<{ key: string; direction: "asc" | "desc" } | null>(null);
|
|
120
|
+
const rows = React.useMemo(() => (Array.isArray(data) ? data : []), [data]);
|
|
121
|
+
const normalizedSearchTerm = searchTerm.trim().toLowerCase();
|
|
122
|
+
|
|
123
|
+
const filteredRows = React.useMemo(() => {
|
|
124
|
+
if (!normalizedSearchTerm) return rows;
|
|
125
|
+
return rows.filter((item) => getSearchableText(item).includes(normalizedSearchTerm));
|
|
126
|
+
}, [normalizedSearchTerm, rows]);
|
|
127
|
+
|
|
128
|
+
const sortedData = React.useMemo(() => {
|
|
129
|
+
if (!sort) return filteredRows;
|
|
130
|
+
|
|
131
|
+
const column = columns.find((candidate) => candidate.key === sort.key);
|
|
132
|
+
if (!column || (!column.sortable && !column.sortAccessor)) return filteredRows;
|
|
133
|
+
|
|
134
|
+
const directionMultiplier = sort.direction === "asc" ? 1 : -1;
|
|
135
|
+
const getSortValue = (item: T) =>
|
|
136
|
+
column.sortAccessor?.(item) ?? (item as Record<string, unknown>)[column.key];
|
|
137
|
+
|
|
138
|
+
return [...filteredRows].sort((left, right) => {
|
|
139
|
+
const leftValue = getSortValue(left);
|
|
140
|
+
const rightValue = getSortValue(right);
|
|
141
|
+
|
|
142
|
+
if (leftValue == null && rightValue == null) return 0;
|
|
143
|
+
if (leftValue == null) return 1;
|
|
144
|
+
if (rightValue == null) return -1;
|
|
145
|
+
|
|
146
|
+
const leftComparable = leftValue instanceof Date ? leftValue.getTime() : leftValue;
|
|
147
|
+
const rightComparable = rightValue instanceof Date ? rightValue.getTime() : rightValue;
|
|
148
|
+
|
|
149
|
+
if (typeof leftComparable === "number" && typeof rightComparable === "number") {
|
|
150
|
+
return (leftComparable - rightComparable) * directionMultiplier;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
String(leftComparable).localeCompare(String(rightComparable), undefined, {
|
|
155
|
+
numeric: true,
|
|
156
|
+
sensitivity: "base",
|
|
157
|
+
}) * directionMultiplier
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
}, [columns, filteredRows, sort]);
|
|
161
|
+
|
|
162
|
+
const pageSize = pagination?.pageSize ?? 10;
|
|
163
|
+
const currentPage = pagination?.currentPage ?? 1;
|
|
164
|
+
const totalPages = Math.max(1, Math.ceil(sortedData.length / pageSize));
|
|
165
|
+
const startIndex = (currentPage - 1) * pageSize;
|
|
166
|
+
const displayData = pagination ? sortedData.slice(startIndex, startIndex + pageSize) : sortedData;
|
|
167
|
+
|
|
168
|
+
const toggleSort = (column: SearchableTableColumn<T>) => {
|
|
169
|
+
if (!column.sortable && !column.sortAccessor) return;
|
|
170
|
+
|
|
171
|
+
setSort((current) =>
|
|
172
|
+
current?.key === column.key
|
|
173
|
+
? { key: column.key, direction: current.direction === "asc" ? "desc" : "asc" }
|
|
174
|
+
: { key: column.key, direction: "asc" },
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const renderEmptyState = (className?: string) => (
|
|
179
|
+
<EmptyState
|
|
180
|
+
title={emptyState?.title ?? "No results found"}
|
|
181
|
+
description={emptyState?.description ?? "Try changing the search or filters."}
|
|
182
|
+
{...(className ? { className } : {})}
|
|
183
|
+
{...(emptyState?.icon ? { icon: normalizeEmptyStateIcon(emptyState.icon) } : {})}
|
|
184
|
+
{...(emptyState?.action ? { action: emptyState.action } : {})}
|
|
185
|
+
/>
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const renderPagination = () => {
|
|
189
|
+
if (!pagination || totalPages <= 1) return null;
|
|
190
|
+
|
|
191
|
+
const pages: Array<number | "ellipsis-start" | "ellipsis-end"> = [];
|
|
192
|
+
const showEllipsis = totalPages > 7;
|
|
193
|
+
|
|
194
|
+
if (showEllipsis) {
|
|
195
|
+
pages.push(1);
|
|
196
|
+
if (currentPage > 4) pages.push("ellipsis-start");
|
|
197
|
+
|
|
198
|
+
for (let page = Math.max(2, currentPage - 1); page <= Math.min(totalPages - 1, currentPage + 1); page++) {
|
|
199
|
+
pages.push(page);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (currentPage < totalPages - 3) pages.push("ellipsis-end");
|
|
203
|
+
pages.push(totalPages);
|
|
204
|
+
} else {
|
|
205
|
+
for (let page = 1; page <= totalPages; page++) pages.push(page);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div className="mt-6 flex justify-center">
|
|
210
|
+
<Pagination>
|
|
211
|
+
<PaginationContent>
|
|
212
|
+
<PaginationItem>
|
|
213
|
+
<PaginationPrevious
|
|
214
|
+
onClick={() => pagination.onPageChange(Math.max(1, currentPage - 1))}
|
|
215
|
+
className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
|
216
|
+
/>
|
|
217
|
+
</PaginationItem>
|
|
218
|
+
{pages.map((page, index) => (
|
|
219
|
+
<PaginationItem key={`${page}-${index}`}>
|
|
220
|
+
{page === "ellipsis-start" || page === "ellipsis-end" ? (
|
|
221
|
+
<PaginationEllipsis />
|
|
222
|
+
) : (
|
|
223
|
+
<PaginationLink
|
|
224
|
+
onClick={() => pagination.onPageChange(page)}
|
|
225
|
+
isActive={currentPage === page}
|
|
226
|
+
className="cursor-pointer"
|
|
227
|
+
>
|
|
228
|
+
{page}
|
|
229
|
+
</PaginationLink>
|
|
230
|
+
)}
|
|
231
|
+
</PaginationItem>
|
|
232
|
+
))}
|
|
233
|
+
<PaginationItem>
|
|
234
|
+
<PaginationNext
|
|
235
|
+
onClick={() => pagination.onPageChange(Math.min(totalPages, currentPage + 1))}
|
|
236
|
+
className={
|
|
237
|
+
currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"
|
|
238
|
+
}
|
|
239
|
+
/>
|
|
240
|
+
</PaginationItem>
|
|
241
|
+
</PaginationContent>
|
|
242
|
+
</Pagination>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div className="space-y-4">
|
|
249
|
+
<div className="md:rounded-lg md:border md:border-border md:bg-muted/30 md:p-3">
|
|
250
|
+
<div className="flex flex-col gap-2.5 md:flex-row md:flex-wrap md:items-center md:justify-start md:gap-3">
|
|
251
|
+
<InputGroup
|
|
252
|
+
placeholder={searchPlaceholder}
|
|
253
|
+
value={searchTerm}
|
|
254
|
+
onChange={(event) => onSearchChange(event.target.value)}
|
|
255
|
+
leadingIcon={<Search className="size-4" />}
|
|
256
|
+
wrapperClassName="w-full min-w-0 md:min-w-[280px] md:max-w-[520px] md:flex-1 md:basis-[340px]"
|
|
257
|
+
className="h-9 bg-background"
|
|
258
|
+
/>
|
|
259
|
+
<div className="flex w-full min-w-0 flex-col gap-2 sm:flex-row sm:flex-wrap md:w-auto md:justify-end">
|
|
260
|
+
{Object.entries(filters).map(([key, filter]) => (
|
|
261
|
+
<Select key={key} value={filter.value} onValueChange={filter.onChange}>
|
|
262
|
+
<SelectTrigger className="h-9 w-full bg-background sm:w-[180px]">
|
|
263
|
+
<Filter className="mr-2 size-4" />
|
|
264
|
+
<SelectValue placeholder={filter.placeholder} />
|
|
265
|
+
</SelectTrigger>
|
|
266
|
+
<SelectContent>
|
|
267
|
+
{filter.options.map((option) => (
|
|
268
|
+
<SelectItem key={option.value} value={option.value}>
|
|
269
|
+
{option.label}
|
|
270
|
+
</SelectItem>
|
|
271
|
+
))}
|
|
272
|
+
</SelectContent>
|
|
273
|
+
</Select>
|
|
274
|
+
))}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<StackedList bordered={false} className="rounded-none bg-transparent md:hidden">
|
|
280
|
+
{displayData.length === 0
|
|
281
|
+
? renderEmptyState("px-0 py-9")
|
|
282
|
+
: displayData.map((item) => {
|
|
283
|
+
if (mobileCard) {
|
|
284
|
+
return (
|
|
285
|
+
<StackedListItem key={item.id} className="px-0 py-0">
|
|
286
|
+
{mobileCard(item)}
|
|
287
|
+
</StackedListItem>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const primaryColumn = columns.find((column) => column.label.trim()) ?? columns[0];
|
|
292
|
+
const secondaryColumns = columns
|
|
293
|
+
.filter((column) => column.key !== primaryColumn?.key && column.label)
|
|
294
|
+
.slice(0, 5);
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<StackedListItem key={item.id} className="px-0 py-2.5">
|
|
298
|
+
<div className="flex items-start justify-between gap-3">
|
|
299
|
+
<div className="min-w-0 flex-1">
|
|
300
|
+
{primaryColumn && (
|
|
301
|
+
<div className="min-w-0 text-sm font-semibold text-foreground [&_*]:min-w-0">
|
|
302
|
+
{primaryColumn.render
|
|
303
|
+
? primaryColumn.render(item)
|
|
304
|
+
: (item as Record<string, React.ReactNode>)[primaryColumn.key]}
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
307
|
+
</div>
|
|
308
|
+
{actions && <div className="shrink-0">{actions(item)}</div>}
|
|
309
|
+
</div>
|
|
310
|
+
{secondaryColumns.length > 0 && (
|
|
311
|
+
<dl className="mt-2 divide-y divide-border/70 border-y border-border/70 text-xs">
|
|
312
|
+
{secondaryColumns.map((column) => (
|
|
313
|
+
<div
|
|
314
|
+
key={column.key}
|
|
315
|
+
className="grid min-w-0 grid-cols-[6.25rem_minmax(0,1fr)] gap-2 py-1.5"
|
|
316
|
+
>
|
|
317
|
+
<dt className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
318
|
+
{column.label}
|
|
319
|
+
</dt>
|
|
320
|
+
<dd className="min-w-0 break-words text-foreground">
|
|
321
|
+
{column.render
|
|
322
|
+
? column.render(item)
|
|
323
|
+
: (item as Record<string, React.ReactNode>)[column.key]}
|
|
324
|
+
</dd>
|
|
325
|
+
</div>
|
|
326
|
+
))}
|
|
327
|
+
</dl>
|
|
328
|
+
)}
|
|
329
|
+
</StackedListItem>
|
|
330
|
+
);
|
|
331
|
+
})}
|
|
332
|
+
</StackedList>
|
|
333
|
+
|
|
334
|
+
{displayData.length === 0 ? (
|
|
335
|
+
<div className="hidden min-h-[18rem] items-center justify-center border-t border-border/60 md:flex">
|
|
336
|
+
{renderEmptyState("px-6 py-12")}
|
|
337
|
+
</div>
|
|
338
|
+
) : (
|
|
339
|
+
<Table containerClassName="hidden overflow-x-auto md:block" className={cn("min-w-full", tableClassName)}>
|
|
340
|
+
<TableHeader>
|
|
341
|
+
<TableRow className="relative border-b-0 bg-muted/80 transition-colors hover:bg-muted/80">
|
|
342
|
+
{columns.map((column) => {
|
|
343
|
+
const isSortable = Boolean(column.sortable || column.sortAccessor);
|
|
344
|
+
const isSorted = sort?.key === column.key;
|
|
345
|
+
const SortIcon = !isSorted ? ChevronsUpDown : sort.direction === "asc" ? ArrowUp : ArrowDown;
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
<TableHead
|
|
349
|
+
key={column.key}
|
|
350
|
+
className={cn(
|
|
351
|
+
"cursor-default select-none whitespace-nowrap px-4 py-3 text-xs font-medium text-muted-foreground transition-colors",
|
|
352
|
+
column.className,
|
|
353
|
+
)}
|
|
354
|
+
aria-sort={
|
|
355
|
+
isSortable
|
|
356
|
+
? isSorted
|
|
357
|
+
? sort.direction === "asc"
|
|
358
|
+
? "ascending"
|
|
359
|
+
: "descending"
|
|
360
|
+
: "none"
|
|
361
|
+
: undefined
|
|
362
|
+
}
|
|
363
|
+
>
|
|
364
|
+
{isSortable ? (
|
|
365
|
+
<Button
|
|
366
|
+
type="button"
|
|
367
|
+
variant="ghost"
|
|
368
|
+
size="sm"
|
|
369
|
+
className="-ml-2 h-auto px-2 py-1 text-xs font-medium"
|
|
370
|
+
onClick={() => toggleSort(column)}
|
|
371
|
+
aria-label={`Sort by ${column.label}`}
|
|
372
|
+
>
|
|
373
|
+
<span>{column.label}</span>
|
|
374
|
+
<SortIcon className="size-3.5" />
|
|
375
|
+
</Button>
|
|
376
|
+
) : (
|
|
377
|
+
column.label
|
|
378
|
+
)}
|
|
379
|
+
</TableHead>
|
|
380
|
+
);
|
|
381
|
+
})}
|
|
382
|
+
{actions && (
|
|
383
|
+
<TableHead className="w-32 cursor-default select-none whitespace-nowrap px-4 py-3 text-xs font-medium text-muted-foreground">
|
|
384
|
+
Actions
|
|
385
|
+
</TableHead>
|
|
386
|
+
)}
|
|
387
|
+
</TableRow>
|
|
388
|
+
</TableHeader>
|
|
389
|
+
<TableBody>
|
|
390
|
+
{displayData.map((item, index) => (
|
|
391
|
+
<TableRow key={item.id} className={cn(index === 0 && "border-t-0")}>
|
|
392
|
+
{columns.map((column) => (
|
|
393
|
+
<TableCell key={column.key} className={cn("py-3", column.className)}>
|
|
394
|
+
{column.render
|
|
395
|
+
? column.render(item)
|
|
396
|
+
: (item as Record<string, React.ReactNode>)[column.key]}
|
|
397
|
+
</TableCell>
|
|
398
|
+
))}
|
|
399
|
+
{actions && <TableCell className="w-32 whitespace-nowrap py-3">{actions(item)}</TableCell>}
|
|
400
|
+
</TableRow>
|
|
401
|
+
))}
|
|
402
|
+
</TableBody>
|
|
403
|
+
</Table>
|
|
404
|
+
)}
|
|
405
|
+
|
|
406
|
+
{renderPagination()}
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export { SearchableTable };
|
|
412
|
+
export type { SearchableTableColumn, SearchableTableFilterOption, SearchableTableProps };
|
|
@@ -4,6 +4,12 @@ import * as React from "react";
|
|
|
4
4
|
import { Select } from "radix-ui";
|
|
5
5
|
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
|
6
6
|
import { cn } from "../lib/utils";
|
|
7
|
+
import {
|
|
8
|
+
mobilePopperSheetMotionClassName,
|
|
9
|
+
mobilePopperSheetPositionClassName,
|
|
10
|
+
mobilePopperSheetStyle,
|
|
11
|
+
mobilePopperSheetSurfaceClassName,
|
|
12
|
+
} from "../lib/mobile-popper-sheet";
|
|
7
13
|
|
|
8
14
|
const SelectRoot = Select.Root;
|
|
9
15
|
const SelectGroup = Select.Group;
|
|
@@ -38,38 +44,48 @@ const SelectContent = React.forwardRef<
|
|
|
38
44
|
React.ComponentRef<typeof Select.Content>,
|
|
39
45
|
React.ComponentPropsWithoutRef<typeof Select.Content>
|
|
40
46
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
48
|
-
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
49
|
-
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
50
|
-
"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
|
|
51
|
-
position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1",
|
|
52
|
-
className,
|
|
53
|
-
)}
|
|
54
|
-
position={position}
|
|
55
|
-
{...props}
|
|
56
|
-
>
|
|
57
|
-
<Select.ScrollUpButton className="flex cursor-default items-center justify-center py-1">
|
|
58
|
-
<ChevronUp size={14} className="text-muted-foreground" />
|
|
59
|
-
</Select.ScrollUpButton>
|
|
60
|
-
<Select.Viewport
|
|
47
|
+
<>
|
|
48
|
+
<style>{mobilePopperSheetStyle}</style>
|
|
49
|
+
<Select.Portal>
|
|
50
|
+
<Select.Content
|
|
51
|
+
ref={ref}
|
|
52
|
+
data-hilum-mobile-sheet="true"
|
|
61
53
|
className={cn(
|
|
54
|
+
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden",
|
|
55
|
+
"bg-card rounded-xl shadow-natural p-1",
|
|
56
|
+
mobilePopperSheetPositionClassName,
|
|
57
|
+
mobilePopperSheetSurfaceClassName,
|
|
58
|
+
"max-sm:px-2 max-sm:pb-2 max-sm:pt-5",
|
|
59
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
60
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
61
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
62
|
+
mobilePopperSheetMotionClassName,
|
|
63
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
|
|
62
64
|
position === "popper" &&
|
|
63
|
-
"
|
|
65
|
+
"data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1",
|
|
66
|
+
className,
|
|
64
67
|
)}
|
|
68
|
+
position={position}
|
|
69
|
+
{...props}
|
|
65
70
|
>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
<Select.ScrollUpButton className="flex cursor-default items-center justify-center py-1">
|
|
72
|
+
<ChevronUp size={14} className="text-muted-foreground" />
|
|
73
|
+
</Select.ScrollUpButton>
|
|
74
|
+
<Select.Viewport
|
|
75
|
+
className={cn(
|
|
76
|
+
position === "popper" &&
|
|
77
|
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
|
78
|
+
"max-sm:!h-auto max-sm:max-h-[calc(min(70dvh,28rem)-3rem)] max-sm:!w-full max-sm:!min-w-0",
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
{children}
|
|
82
|
+
</Select.Viewport>
|
|
83
|
+
<Select.ScrollDownButton className="flex cursor-default items-center justify-center py-1">
|
|
84
|
+
<ChevronDown size={14} className="text-muted-foreground" />
|
|
85
|
+
</Select.ScrollDownButton>
|
|
86
|
+
</Select.Content>
|
|
87
|
+
</Select.Portal>
|
|
88
|
+
</>
|
|
73
89
|
));
|
|
74
90
|
SelectContent.displayName = "SelectContent";
|
|
75
91
|
|