torch-glare 2.3.0 → 2.4.1
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/README.md +8 -0
- package/apps/lib/components/ActionButton.tsx +7 -12
- package/apps/lib/components/ActionsGroup.tsx +4 -15
- package/apps/lib/components/AlertDialog.tsx +173 -191
- package/apps/lib/components/Avatar.tsx +13 -16
- package/apps/lib/components/Badge.tsx +7 -3
- package/apps/lib/components/BadgeField.tsx +76 -71
- package/apps/lib/components/Breadcrumb.tsx +49 -85
- package/apps/lib/components/Button.tsx +20 -19
- package/apps/lib/components/ButtonGroup.tsx +31 -41
- package/apps/lib/components/Calendar.tsx +51 -29
- package/apps/lib/components/Card.tsx +2 -13
- package/apps/lib/components/ChartBlockTool.ts +17 -52
- package/apps/lib/components/Checkbox.tsx +9 -16
- package/apps/lib/components/ContextMenu.tsx +110 -106
- package/apps/lib/components/CountBadge.tsx +4 -6
- package/apps/lib/components/DataTable.tsx +196 -216
- package/apps/lib/components/DataViews/DataViewRadio.tsx +10 -8
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +21 -55
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +1 -3
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +5 -12
- package/apps/lib/components/DataViews/FilterPanel.tsx +173 -150
- package/apps/lib/components/DataViews/HeaderSearch.tsx +1 -5
- package/apps/lib/components/DataViews/InboxView.tsx +22 -68
- package/apps/lib/components/DataViews/InboxViewCard.tsx +68 -77
- package/apps/lib/components/DataViews/KanbanView.tsx +8 -25
- package/apps/lib/components/DataViews/PanelControls.tsx +3 -13
- package/apps/lib/components/DataViews/SettingsPanel.tsx +83 -89
- package/apps/lib/components/DataViews/TableView.tsx +8 -28
- package/apps/lib/components/DataViews/TreeView.tsx +12 -40
- package/apps/lib/components/DataViews/badgeAdapter.ts +32 -28
- package/apps/lib/components/DataViews/fieldRenderers.tsx +106 -141
- package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +87 -0
- package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +39 -32
- package/apps/lib/components/DataViews/filters/PresetChips.tsx +15 -15
- package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +69 -58
- package/apps/lib/components/DataViews/index.ts +26 -26
- package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +12 -16
- package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +28 -31
- package/apps/lib/components/DataViews/types.ts +111 -114
- package/apps/lib/components/DatePicker.tsx +198 -191
- package/apps/lib/components/Dialog.tsx +92 -105
- package/apps/lib/components/Divider.tsx +21 -24
- package/apps/lib/components/Drawer.tsx +79 -139
- package/apps/lib/components/DropdownMenu.tsx +144 -181
- package/apps/lib/components/FieldHint.tsx +4 -11
- package/apps/lib/components/Form.tsx +146 -155
- package/apps/lib/components/FormStepper.tsx +9 -39
- package/apps/lib/components/HeaderBar.tsx +119 -0
- package/apps/lib/components/ImageAttachment.tsx +111 -73
- package/apps/lib/components/InnerLabelField.tsx +20 -16
- package/apps/lib/components/Input.tsx +171 -172
- package/apps/lib/components/InputField.tsx +44 -45
- package/apps/lib/components/InputOTP.tsx +68 -69
- package/apps/lib/components/Label.tsx +31 -35
- package/apps/lib/components/LabelField.tsx +4 -5
- package/apps/lib/components/LabeledCheckBox.tsx +8 -12
- package/apps/lib/components/LabeledRadio.tsx +7 -5
- package/apps/lib/components/LinkButton.tsx +2 -9
- package/apps/lib/components/LoginButton.tsx +4 -9
- package/apps/lib/components/PasswordLevel.tsx +5 -5
- package/apps/lib/components/Popover.tsx +15 -33
- package/apps/lib/components/ProfileMenu.tsx +67 -72
- package/apps/lib/components/Radio.tsx +1 -5
- package/apps/lib/components/RadioCard.tsx +9 -28
- package/apps/lib/components/ScrollArea.tsx +45 -43
- package/apps/lib/components/SearchField.tsx +49 -39
- package/apps/lib/components/SearchableSelect.tsx +46 -52
- package/apps/lib/components/SearchableTable.tsx +199 -219
- package/apps/lib/components/SearchableTree.tsx +469 -0
- package/apps/lib/components/SearchableTreeDialog.tsx +493 -0
- package/apps/lib/components/SectionBlock.tsx +4 -16
- package/apps/lib/components/Select.tsx +53 -80
- package/apps/lib/components/SimpleSelect.tsx +196 -187
- package/apps/lib/components/Skeleton.tsx +9 -12
- package/apps/lib/components/SlideDatePicker.tsx +215 -166
- package/apps/lib/components/SpinLoading.tsx +33 -49
- package/apps/lib/components/Stepper.tsx +13 -56
- package/apps/lib/components/Switch.tsx +8 -10
- package/apps/lib/components/TabFormItem.tsx +3 -12
- package/apps/lib/components/TabSwitch.tsx +7 -34
- package/apps/lib/components/Table.tsx +60 -75
- package/apps/lib/components/TableDnDWrapper.ts +15 -48
- package/apps/lib/components/TextEditor.tsx +93 -104
- package/apps/lib/components/Textarea.tsx +8 -12
- package/apps/lib/components/Timeline.tsx +21 -41
- package/apps/lib/components/Toast.tsx +83 -83
- package/apps/lib/components/Toggle.tsx +112 -114
- package/apps/lib/components/ToggleButton.tsx +1 -1
- package/apps/lib/components/Tooltip.tsx +24 -19
- package/apps/lib/components/TransparentLabel.tsx +61 -62
- package/apps/lib/components/TreeDropDown.tsx +86 -52
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +15 -48
- package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +16 -21
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +9 -20
- package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +3 -3
- package/apps/lib/components/TreeFolder/icons.tsx +14 -17
- package/apps/lib/components/TreeFolder/index.ts +15 -8
- package/apps/lib/components/TreeFolder/treeFolderUtils.ts +47 -52
- package/apps/lib/components/TreeFolder/types.ts +32 -32
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +117 -123
- package/apps/lib/hooks/useActiveTreeItem.ts +52 -55
- package/apps/lib/hooks/useClickOutside.ts +21 -20
- package/apps/lib/hooks/useDataViewsState.ts +86 -80
- package/apps/lib/hooks/useIsMobile.ts +12 -12
- package/apps/lib/hooks/useResize.ts +54 -65
- package/apps/lib/hooks/useTagSelection.ts +201 -201
- package/apps/lib/layouts/CNLayout.tsx +181 -140
- package/apps/lib/layouts/DataViewCard.tsx +4 -18
- package/apps/lib/layouts/FieldSection.tsx +2 -6
- package/apps/lib/layouts/TreeSubLayout.tsx +24 -38
- package/apps/lib/providers/ThemeProvider.tsx +8 -23
- package/apps/lib/registry.json +1004 -0
- package/apps/lib/tsconfig.json +9 -0
- package/apps/lib/utils/cn.ts +1 -1
- package/apps/lib/utils/dataViews/columnUtils.ts +46 -44
- package/apps/lib/utils/dataViews/fieldUtils.ts +70 -71
- package/apps/lib/utils/dataViews/nestedDataUtils.tsx +129 -122
- package/apps/lib/utils/dataViews/pathUtils.ts +74 -67
- package/apps/lib/utils/dataViews/rangeUtils.ts +121 -112
- package/apps/lib/utils/dataViews/treeUtils.ts +204 -211
- package/apps/lib/utils/dateFormat.ts +64 -54
- package/apps/lib/utils/markdownParser.ts +2 -7
- package/apps/lib/utils/resize.ts +21 -25
- package/apps/lib/utils/types.ts +13 -13
- package/dist/bin/index.js +8 -2
- package/dist/bin/index.js.map +1 -1
- package/dist/src/shared/getAvailableFiles.d.ts.map +1 -1
- package/dist/src/shared/getAvailableFiles.js +6 -1
- package/dist/src/shared/getAvailableFiles.js.map +1 -1
- package/dist/src/shared/loadRegistry.d.ts +7 -0
- package/dist/src/shared/loadRegistry.d.ts.map +1 -0
- package/dist/src/shared/loadRegistry.js +31 -0
- package/dist/src/shared/loadRegistry.js.map +1 -0
- package/dist/src/shared/resolveInstallPlan.d.ts +12 -0
- package/dist/src/shared/resolveInstallPlan.d.ts.map +1 -0
- package/dist/src/shared/resolveInstallPlan.js +39 -0
- package/dist/src/shared/resolveInstallPlan.js.map +1 -0
- package/dist/src/types/main.d.ts +24 -0
- package/dist/src/types/main.d.ts.map +1 -1
- package/docs/README.md +10 -2
- package/docs/components/action-button.md +30 -24
- package/docs/components/actions-group.md +26 -20
- package/docs/components/alert-dialog.md +45 -45
- package/docs/components/avatar.md +6 -2
- package/docs/components/badge-field.md +6 -2
- package/docs/components/badge.md +7 -1
- package/docs/components/breadcrumb.md +15 -40
- package/docs/components/button-group.md +13 -10
- package/docs/components/button.md +58 -33
- package/docs/components/calendar.md +26 -19
- package/docs/components/card.md +19 -16
- package/docs/components/chart-block-tool.md +128 -0
- package/docs/components/checkbox.md +16 -8
- package/docs/components/cn-layout.md +7 -1
- package/docs/components/context-menu.md +47 -24
- package/docs/components/count-badge.md +7 -1
- package/docs/components/data-table.md +45 -33
- package/docs/components/data-views-config-panel.md +18 -14
- package/docs/components/data-views-layout.md +20 -17
- package/docs/components/date-picker.md +30 -23
- package/docs/components/dialog.md +43 -42
- package/docs/components/divider.md +10 -4
- package/docs/components/drawer.md +8 -33
- package/docs/components/dropdown-menu.md +51 -32
- package/docs/components/field-hint.md +37 -25
- package/docs/components/field-section.md +9 -3
- package/docs/components/form-stepper.md +6 -0
- package/docs/components/form.md +17 -20
- package/docs/components/header-bar.md +187 -0
- package/docs/components/image-attachment.md +33 -41
- package/docs/components/inbox-view.md +16 -5
- package/docs/components/inner-label-field.md +34 -24
- package/docs/components/input-field.md +18 -10
- package/docs/components/input-otp.md +14 -11
- package/docs/components/input.md +22 -16
- package/docs/components/kanban-view.md +15 -5
- package/docs/components/label-field.md +32 -22
- package/docs/components/label.md +43 -32
- package/docs/components/labeled-check-box.md +16 -8
- package/docs/components/labeled-radio.md +18 -11
- package/docs/components/link-button.md +7 -1
- package/docs/components/login-button.md +8 -2
- package/docs/components/password-level.md +37 -24
- package/docs/components/popover.md +26 -23
- package/docs/components/profile-menu.md +38 -27
- package/docs/components/radio-card.md +16 -9
- package/docs/components/radio.md +16 -8
- package/docs/components/scroll-area.md +6 -2
- package/docs/components/search-field.md +14 -6
- package/docs/components/searchable-select.md +19 -14
- package/docs/components/searchable-table.md +60 -41
- package/docs/components/searchable-tree-dialog.md +190 -0
- package/docs/components/searchable-tree.md +200 -0
- package/docs/components/section-block.md +130 -4
- package/docs/components/select.md +23 -18
- package/docs/components/simple-select.md +14 -10
- package/docs/components/skeleton.md +43 -33
- package/docs/components/slide-date-picker.md +37 -29
- package/docs/components/spin-loading.md +32 -23
- package/docs/components/stepper.md +6 -0
- package/docs/components/switch.md +14 -6
- package/docs/components/tab-form-item.md +7 -1
- package/docs/components/tab-switch.md +13 -6
- package/docs/components/table-dnd-wrapper.md +105 -0
- package/docs/components/table-view.md +15 -5
- package/docs/components/table.md +28 -28
- package/docs/components/text-editor.md +19 -13
- package/docs/components/textarea.md +15 -7
- package/docs/components/timeline.md +6 -0
- package/docs/components/toast.md +35 -26
- package/docs/components/toggle-button.md +24 -21
- package/docs/components/toggle.md +23 -15
- package/docs/components/tooltip.md +42 -30
- package/docs/components/transparent-label.md +29 -20
- package/docs/components/tree-drop-down.md +31 -23
- package/docs/components/tree-sub-layout.md +6 -2
- package/docs/components/tree-view.md +16 -5
- package/docs/explanation/architecture.md +54 -0
- package/docs/explanation/design-system.md +52 -0
- package/docs/how-to/data-views-from-backend-response.md +7 -4
- package/docs/how-to/guides.md +24 -18
- package/docs/migration/changelog.md +38 -0
- package/docs/reference/cli.md +87 -0
- package/docs/reference/components.md +11 -12
- package/docs/reference/hooks.md +8 -8
- package/docs/reference/providers.md +22 -22
- package/docs/reference/tailwind-plugins.md +2 -2
- package/docs/reference/theme.md +85 -0
- package/docs/reference/types.md +4 -4
- package/docs/reference/utilities.md +19 -26
- package/docs/tutorials/building-first-form.md +20 -13
- package/docs/tutorials/component-composition.md +34 -50
- package/docs/tutorials/getting-started.md +2 -2
- package/docs/tutorials/theming-basics.md +13 -10
- package/package.json +31 -3
- package/dist/src/commands/block.d.ts +0 -5
- package/dist/src/commands/block.d.ts.map +0 -1
- package/dist/src/commands/block.js +0 -255
- package/dist/src/commands/block.js.map +0 -1
- package/dist/src/shared/configureFonts.d.ts +0 -6
- package/dist/src/shared/configureFonts.d.ts.map +0 -1
- package/dist/src/shared/configureFonts.js +0 -106
- package/dist/src/shared/configureFonts.js.map +0 -1
- package/dist/src/shared/configureGlobalCss.d.ts +0 -6
- package/dist/src/shared/configureGlobalCss.d.ts.map +0 -1
- package/dist/src/shared/configureGlobalCss.js +0 -154
- package/dist/src/shared/configureGlobalCss.js.map +0 -1
- package/dist/src/shared/configureTailwind.d.ts +0 -7
- package/dist/src/shared/configureTailwind.d.ts.map +0 -1
- package/dist/src/shared/configureTailwind.js +0 -163
- package/dist/src/shared/configureTailwind.js.map +0 -1
- package/dist/src/shared/detectFramework.d.ts +0 -23
- package/dist/src/shared/detectFramework.d.ts.map +0 -1
- package/dist/src/shared/detectFramework.js +0 -119
- package/dist/src/shared/detectFramework.js.map +0 -1
- package/dist/src/shared/installBaseUtils.d.ts +0 -5
- package/dist/src/shared/installBaseUtils.d.ts.map +0 -1
- package/dist/src/shared/installBaseUtils.js +0 -79
- package/dist/src/shared/installBaseUtils.js.map +0 -1
- package/dist/src/shared/resolveAliases.d.ts +0 -24
- package/dist/src/shared/resolveAliases.d.ts.map +0 -1
- package/dist/src/shared/resolveAliases.js +0 -98
- package/dist/src/shared/resolveAliases.js.map +0 -1
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
forwardRef,
|
|
5
|
+
InputHTMLAttributes,
|
|
6
|
+
ReactNode,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
5
12
|
import { cn } from "../utils/cn";
|
|
6
13
|
import { Themes } from "../utils/types";
|
|
7
|
-
import {
|
|
14
|
+
import { Dialog, DialogTrigger, DialogContent, DialogTitle } from "./Dialog";
|
|
8
15
|
import { Icon, Input, Group } from "./Input";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
Table,
|
|
13
|
-
TableHeader,
|
|
14
|
-
TableBody,
|
|
15
|
-
TableHead,
|
|
16
|
-
TableRow,
|
|
17
|
-
TableCell,
|
|
18
|
-
} from "./Table";
|
|
16
|
+
import { LoadingIcon } from "./Button";
|
|
17
|
+
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from "./Table";
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
|
-
* SearchableTable — a
|
|
20
|
+
* SearchableTable — a field that opens a modal Dialog to pick a row from a Table.
|
|
22
21
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* the
|
|
26
|
-
*
|
|
22
|
+
* The trigger shows a placeholder until something is selected, then shows the
|
|
23
|
+
* selected row's label. Clicking it opens a dialog containing a search input and
|
|
24
|
+
* the data Table; clicking a row selects it, sets the value, and closes the
|
|
25
|
+
* dialog. Supports client- or server-side search and infinite-scroll pagination.
|
|
27
26
|
*/
|
|
28
27
|
|
|
29
28
|
export interface SearchableTableColumn<T> {
|
|
@@ -40,13 +39,18 @@ interface Props<T> {
|
|
|
40
39
|
/** Controlled selected row (optional). */
|
|
41
40
|
value?: T | null;
|
|
42
41
|
onSelect?: (row: T) => void;
|
|
43
|
-
/** Text shown
|
|
42
|
+
/** Text shown on the trigger after selection; defaults to the first column's value. */
|
|
44
43
|
getLabel?: (row: T) => string;
|
|
45
44
|
/** Stable id/key per row; defaults to JSON of the row. */
|
|
46
45
|
getRowId?: (row: T) => string;
|
|
47
46
|
/** Which fields the search matches; defaults to every column key. */
|
|
48
47
|
searchKeys?: (keyof T & string)[];
|
|
48
|
+
/** Trigger placeholder shown until a row is selected. */
|
|
49
49
|
placeholder?: string;
|
|
50
|
+
/** Placeholder for the search input inside the dialog. */
|
|
51
|
+
searchPlaceholder?: string;
|
|
52
|
+
/** Label shown on the dialog's search field. */
|
|
53
|
+
title?: string;
|
|
50
54
|
size?: "XS" | "S" | "M";
|
|
51
55
|
variant?: "SystemStyle" | "PresentationStyle";
|
|
52
56
|
icon?: ReactNode;
|
|
@@ -71,44 +75,8 @@ interface Props<T> {
|
|
|
71
75
|
loading?: boolean;
|
|
72
76
|
/** Called when the list nears the bottom and `hasMore && !loading`. */
|
|
73
77
|
onLoadMore?: () => void;
|
|
74
|
-
/** Max rows visible before the list scrolls (default 6). */
|
|
75
|
-
maxVisibleRows?: number;
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
// One M-size table row ≈ 40px + the 44px sticky header.
|
|
79
|
-
const ROW_HEIGHT = 40;
|
|
80
|
-
const HEADER_HEIGHT = 44;
|
|
81
|
-
|
|
82
|
-
// Popover surface styled like the DropdownMenu (kept local — self-contained).
|
|
83
|
-
const tableDropdownStyles = cva(
|
|
84
|
-
[
|
|
85
|
-
"p-1",
|
|
86
|
-
"rounded-[14px]",
|
|
87
|
-
"border-0", // neutralize PopoverContent's base border (the card provides its own)
|
|
88
|
-
"outline-none",
|
|
89
|
-
"overflow-y-auto",
|
|
90
|
-
"scrollbar-hide",
|
|
91
|
-
"backdrop-blur-[21px]",
|
|
92
|
-
"data-[state=open]:animate-in",
|
|
93
|
-
"data-[state=open]:fade-in-0",
|
|
94
|
-
],
|
|
95
|
-
{
|
|
96
|
-
variants: {
|
|
97
|
-
variant: {
|
|
98
|
-
PresentationStyle: [
|
|
99
|
-
"bg-[rgba(61,64,69,0.72)]",
|
|
100
|
-
"shadow-[0_0_32px_2px_rgba(0,0,0,0.20),0_0_48px_2px_rgba(0,0,0,0.05)]",
|
|
101
|
-
],
|
|
102
|
-
SystemStyle: [
|
|
103
|
-
"bg-background-system-body-primary",
|
|
104
|
-
"shadow-[0px_0px_18px_0px_rgba(0,0,0,0.75)]",
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
defaultVariants: { variant: "PresentationStyle" },
|
|
109
|
-
}
|
|
110
|
-
);
|
|
111
|
-
|
|
112
80
|
export function SearchableTable<T extends Record<string, unknown>>({
|
|
113
81
|
columns,
|
|
114
82
|
rows,
|
|
@@ -117,7 +85,9 @@ export function SearchableTable<T extends Record<string, unknown>>({
|
|
|
117
85
|
getLabel,
|
|
118
86
|
getRowId,
|
|
119
87
|
searchKeys,
|
|
120
|
-
placeholder = "
|
|
88
|
+
placeholder = "Select…",
|
|
89
|
+
searchPlaceholder = "Search…",
|
|
90
|
+
title = "Select an item",
|
|
121
91
|
size = "M",
|
|
122
92
|
variant = "PresentationStyle",
|
|
123
93
|
icon,
|
|
@@ -130,14 +100,17 @@ export function SearchableTable<T extends Record<string, unknown>>({
|
|
|
130
100
|
hasMore = false,
|
|
131
101
|
loading = false,
|
|
132
102
|
onLoadMore,
|
|
133
|
-
maxVisibleRows = 6,
|
|
134
103
|
}: Props<T>) {
|
|
135
104
|
const [open, setOpen] = useState(false);
|
|
136
105
|
const [search, setSearch] = useState("");
|
|
137
|
-
const
|
|
138
|
-
const popoverContentRef = useRef<HTMLDivElement>(null);
|
|
106
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
139
107
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
140
108
|
|
|
109
|
+
const rowId = (row: T) => getRowId?.(row) ?? JSON.stringify(row);
|
|
110
|
+
const label = (row: T) => getLabel?.(row) ?? String(row[columns[0]?.key] ?? "");
|
|
111
|
+
|
|
112
|
+
const keysToSearch = searchKeys ?? columns.map((c) => c.key);
|
|
113
|
+
|
|
141
114
|
// Debounced server search: notify the consumer to refetch when the query
|
|
142
115
|
// settles. Skipped entirely if no onSearchChange is provided (static mode).
|
|
143
116
|
useEffect(() => {
|
|
@@ -146,8 +119,13 @@ export function SearchableTable<T extends Record<string, unknown>>({
|
|
|
146
119
|
return () => clearTimeout(id);
|
|
147
120
|
}, [search, onSearchChange, searchDebounceMs]);
|
|
148
121
|
|
|
149
|
-
//
|
|
150
|
-
|
|
122
|
+
// Reset the query whenever the dialog closes so it reopens clean.
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!open) setSearch("");
|
|
125
|
+
}, [open]);
|
|
126
|
+
|
|
127
|
+
// Infinite scroll: when the scroll viewport nears the bottom, ask the consumer
|
|
128
|
+
// to load the next page.
|
|
151
129
|
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
|
152
130
|
if (!onLoadMore || !hasMore || loading) return;
|
|
153
131
|
const el = e.currentTarget;
|
|
@@ -156,22 +134,6 @@ export function SearchableTable<T extends Record<string, unknown>>({
|
|
|
156
134
|
}
|
|
157
135
|
};
|
|
158
136
|
|
|
159
|
-
const groupRef = useClickOutside<HTMLDivElement>((e) => {
|
|
160
|
-
const target = e?.target as Node;
|
|
161
|
-
if (
|
|
162
|
-
!groupRef.current?.contains(target) &&
|
|
163
|
-
!popoverContentRef.current?.contains(target)
|
|
164
|
-
) {
|
|
165
|
-
setOpen(false);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const rowId = (row: T) => getRowId?.(row) ?? JSON.stringify(row);
|
|
170
|
-
const label = (row: T) =>
|
|
171
|
-
getLabel?.(row) ?? String(row[columns[0]?.key] ?? "");
|
|
172
|
-
|
|
173
|
-
const keysToSearch = searchKeys ?? columns.map((c) => c.key);
|
|
174
|
-
|
|
175
137
|
const filteredRows = useMemo(() => {
|
|
176
138
|
// Server-side search mode: render rows as-is (already filtered upstream).
|
|
177
139
|
if (!filterClientSide) return rows;
|
|
@@ -179,185 +141,203 @@ export function SearchableTable<T extends Record<string, unknown>>({
|
|
|
179
141
|
if (!q) return rows;
|
|
180
142
|
return rows.filter((row) =>
|
|
181
143
|
keysToSearch.some((k) =>
|
|
182
|
-
String(row[k] ?? "")
|
|
183
|
-
|
|
144
|
+
String(row[k] ?? "")
|
|
145
|
+
.toLowerCase()
|
|
146
|
+
.includes(q),
|
|
147
|
+
),
|
|
184
148
|
);
|
|
185
149
|
}, [rows, search, keysToSearch, filterClientSide]);
|
|
186
150
|
|
|
187
151
|
const selectedId = value ? rowId(value) : undefined;
|
|
188
|
-
// True while the user is actively typing a query. When false, the input shows
|
|
189
|
-
// the selected row's label as its real value (solid text, not placeholder).
|
|
190
|
-
const [searching, setSearching] = useState(false);
|
|
191
152
|
|
|
192
153
|
const handleSelect = (row: T) => {
|
|
193
154
|
onSelect?.(row);
|
|
194
|
-
setSearch("");
|
|
195
|
-
setSearching(false);
|
|
196
155
|
setOpen(false);
|
|
197
156
|
};
|
|
198
157
|
|
|
199
|
-
// Solid value: search text while typing, otherwise the selected label.
|
|
200
|
-
const displayValue = searching ? search : value ? label(value) : "";
|
|
201
|
-
|
|
202
158
|
return (
|
|
203
|
-
<
|
|
204
|
-
<
|
|
159
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
160
|
+
<DialogTrigger asChild>
|
|
161
|
+
{/* Trigger: shows placeholder until a row is selected, then its label. */}
|
|
205
162
|
<Group
|
|
206
163
|
dir={dir}
|
|
207
164
|
data-theme={theme}
|
|
208
165
|
variant={variant}
|
|
209
166
|
size={size === "XS" ? "S" : size}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
className={cn("flex w-full items-center gap-1 p-1", className)}
|
|
167
|
+
role="button"
|
|
168
|
+
tabIndex={0}
|
|
169
|
+
className={cn("flex w-full items-center gap-1 p-1 cursor-pointer", className)}
|
|
215
170
|
>
|
|
216
171
|
{icon && <Icon>{icon}</Icon>}
|
|
217
|
-
<
|
|
218
|
-
ref={inputRef}
|
|
219
|
-
placeholder={placeholder}
|
|
220
|
-
value={displayValue}
|
|
221
|
-
onChange={(e) => {
|
|
222
|
-
setSearch(e.target.value);
|
|
223
|
-
setSearching(true);
|
|
224
|
-
}}
|
|
225
|
-
onFocus={() => {
|
|
226
|
-
// Start a fresh search; the selected label clears so the user can type.
|
|
227
|
-
setSearching(true);
|
|
228
|
-
setSearch("");
|
|
229
|
-
setOpen(true);
|
|
230
|
-
}}
|
|
231
|
-
onBlur={() => setSearching(false)}
|
|
232
|
-
className={cn("min-w-[100px] flex-1", {
|
|
233
|
-
"!h-[18px]": size === "XS",
|
|
234
|
-
"!h-[22px]": size === "S",
|
|
235
|
-
"!h-[24px]": size === "M",
|
|
236
|
-
})}
|
|
237
|
-
/>
|
|
238
|
-
{/* Chevron toggle — boxed icon button matching the Select component. */}
|
|
239
|
-
<Button
|
|
240
|
-
as="span"
|
|
241
|
-
buttonType="icon"
|
|
242
|
-
tabIndex={-1}
|
|
243
|
-
aria-label={open ? "Close" : "Open"}
|
|
244
|
-
onMouseDown={(e: React.MouseEvent) => {
|
|
245
|
-
// Prevent the input's blur/focus race; toggle the dropdown.
|
|
246
|
-
e.preventDefault();
|
|
247
|
-
if (open) {
|
|
248
|
-
setOpen(false);
|
|
249
|
-
} else {
|
|
250
|
-
inputRef.current?.focus();
|
|
251
|
-
setOpen(true);
|
|
252
|
-
}
|
|
253
|
-
}}
|
|
172
|
+
<span
|
|
254
173
|
className={cn(
|
|
255
|
-
"
|
|
256
|
-
|
|
174
|
+
"flex-1 px-1 truncate typography-body-medium-regular",
|
|
175
|
+
value
|
|
176
|
+
? "text-content-presentation-action-light-primary"
|
|
177
|
+
: "text-content-presentation-action-light-secondary",
|
|
257
178
|
)}
|
|
258
179
|
>
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
open && "rotate-180"
|
|
263
|
-
)}
|
|
264
|
-
/>
|
|
265
|
-
</Button>
|
|
180
|
+
{value ? label(value) : placeholder}
|
|
181
|
+
</span>
|
|
182
|
+
<i className="ri-arrow-down-s-line text-[20px] shrink-0 text-content-presentation-action-light-primary" />
|
|
266
183
|
</Group>
|
|
267
|
-
</
|
|
184
|
+
</DialogTrigger>
|
|
268
185
|
|
|
269
|
-
<
|
|
186
|
+
<DialogContent
|
|
270
187
|
dir={dir}
|
|
271
188
|
data-theme={theme}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// inside it. Cap height to ~maxVisibleRows (+ header) so the rest scrolls.
|
|
277
|
-
style={{
|
|
278
|
-
width: dropdownWidth || undefined,
|
|
279
|
-
maxHeight: HEADER_HEIGHT + maxVisibleRows * ROW_HEIGHT + 8,
|
|
189
|
+
onOpenAutoFocus={(e) => {
|
|
190
|
+
// Focus the search input instead of the first row.
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
inputRef.current?.focus();
|
|
280
193
|
}}
|
|
281
|
-
|
|
282
|
-
|
|
194
|
+
className={cn(
|
|
195
|
+
"w-[min(640px,90vw)] bg-transparent !items-stretch rounded-[14px] shadow-none",
|
|
196
|
+
)}
|
|
283
197
|
>
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
198
|
+
{/* Visually-hidden title for accessibility (Radix requires a DialogTitle). */}
|
|
199
|
+
<DialogTitle className="sr-only">{title ?? "Select an item"}</DialogTitle>
|
|
200
|
+
|
|
201
|
+
{/* Search input — local inline-label field (full control over colors). */}
|
|
202
|
+
<SearchInput
|
|
203
|
+
ref={inputRef}
|
|
204
|
+
variant={variant}
|
|
205
|
+
theme={theme}
|
|
206
|
+
dir={dir}
|
|
207
|
+
label={title}
|
|
208
|
+
value={search}
|
|
209
|
+
placeholder={searchPlaceholder}
|
|
210
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
211
|
+
/>
|
|
212
|
+
|
|
213
|
+
{/* Scrollable table area */}
|
|
214
|
+
<div className=" w-full flex justify-center">
|
|
215
|
+
<div className="bg-[#696a6f] pt-[8px] rounded-b-[14px] pr-[4px] pb-[4px] pl-[4px] w-[95%] shadow-[0_0_32px_2px_rgba(0,0,0,0.20),0_0_48px_2px_rgba(0,0,0,0.05)]">
|
|
216
|
+
<div
|
|
217
|
+
ref={scrollRef}
|
|
218
|
+
onScroll={handleScroll}
|
|
219
|
+
className="max-h-[55vh] overflow-auto scrollbar-hide rounded-[10px] border border-white-alpha-40 bg-[rgba(184,192,204,0.5)]"
|
|
296
220
|
>
|
|
297
|
-
{
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// White semibold header text from the design.
|
|
306
|
-
className="cursor-default typography-body-medium-medium text-white"
|
|
307
|
-
>
|
|
308
|
-
{col.header}
|
|
309
|
-
</TableHead>
|
|
310
|
-
))}
|
|
311
|
-
</TableRow>
|
|
312
|
-
</TableHeader>
|
|
313
|
-
<TableBody>
|
|
314
|
-
{filteredRows.map((row) => {
|
|
315
|
-
const id = rowId(row);
|
|
316
|
-
return (
|
|
317
|
-
<TableRow
|
|
318
|
-
key={id}
|
|
319
|
-
state={selectedId === id ? "selected" : undefined}
|
|
320
|
-
onClick={() => handleSelect(row)}
|
|
321
|
-
// h-[40px] rows with a translucent white bottom rule.
|
|
322
|
-
className={cn("cursor-pointer h-[40px] ", selectedId === id && "bg-white-alpha-50")}
|
|
323
|
-
>
|
|
221
|
+
{filteredRows.length > 0 && (
|
|
222
|
+
<Table
|
|
223
|
+
theme={theme as never}
|
|
224
|
+
data-theme="dark"
|
|
225
|
+
className="w-full rounded-[10px] overflow-hidden"
|
|
226
|
+
>
|
|
227
|
+
<TableHeader className="bg-black-alpha-20 shadow-[0px_4px_8px_0px_rgba(0,0,0,0.15)]">
|
|
228
|
+
<TableRow className="h-[44px] [&_button]:bg-white-alpha-40 [&>th]:border-white-alpha-20">
|
|
324
229
|
{columns.map((col) => (
|
|
325
|
-
<
|
|
230
|
+
<TableHead
|
|
326
231
|
key={col.key}
|
|
327
|
-
|
|
328
|
-
// component's break-all) keeps columns wide so the
|
|
329
|
-
// table overflows horizontally instead of wrapping.
|
|
330
|
-
className="border-b border-white-alpha-20 text-white px-[8px] !whitespace-nowrap !break-normal"
|
|
232
|
+
className="cursor-default typography-body-medium-medium text-white"
|
|
331
233
|
>
|
|
332
|
-
{col.
|
|
333
|
-
|
|
334
|
-
: String(row[col.key] ?? "")}
|
|
335
|
-
</TableCell>
|
|
234
|
+
{col.header}
|
|
235
|
+
</TableHead>
|
|
336
236
|
))}
|
|
337
237
|
</TableRow>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
238
|
+
</TableHeader>
|
|
239
|
+
<TableBody>
|
|
240
|
+
{filteredRows.map((row) => {
|
|
241
|
+
const id = rowId(row);
|
|
242
|
+
return (
|
|
243
|
+
<TableRow
|
|
244
|
+
key={id}
|
|
245
|
+
state={selectedId === id ? "selected" : undefined}
|
|
246
|
+
onClick={() => handleSelect(row)}
|
|
247
|
+
className={cn(
|
|
248
|
+
"cursor-pointer h-[40px]",
|
|
249
|
+
selectedId === id && "bg-white-alpha-50",
|
|
250
|
+
)}
|
|
251
|
+
>
|
|
252
|
+
{columns.map((col) => (
|
|
253
|
+
<TableCell
|
|
254
|
+
key={col.key}
|
|
255
|
+
className="border-b border-white-alpha-20 text-white px-[8px] !whitespace-nowrap !break-normal"
|
|
256
|
+
>
|
|
257
|
+
{col.render ? col.render(row) : String(row[col.key] ?? "")}
|
|
258
|
+
</TableCell>
|
|
259
|
+
))}
|
|
260
|
+
</TableRow>
|
|
261
|
+
);
|
|
262
|
+
})}
|
|
263
|
+
</TableBody>
|
|
264
|
+
</Table>
|
|
265
|
+
)}
|
|
344
266
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
267
|
+
{/* Empty state — only when nothing is loading. */}
|
|
268
|
+
{filteredRows.length === 0 && !loading && (
|
|
269
|
+
<div className="px-3 py-6 text-center typography-body-small-regular text-white-alpha-75">
|
|
270
|
+
No results found
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
351
273
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
274
|
+
{/* Loading row (initial load or fetching the next page). */}
|
|
275
|
+
{loading && (
|
|
276
|
+
<div className="flex items-center justify-center py-3">
|
|
277
|
+
<LoadingIcon size="M" />
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
356
281
|
</div>
|
|
357
|
-
|
|
358
|
-
</
|
|
359
|
-
</
|
|
282
|
+
</div>
|
|
283
|
+
</DialogContent>
|
|
284
|
+
</Dialog>
|
|
360
285
|
);
|
|
361
286
|
}
|
|
362
287
|
|
|
363
288
|
SearchableTable.displayName = "SearchableTable";
|
|
289
|
+
|
|
290
|
+
/* -------------------------------------------------------------------------- */
|
|
291
|
+
/* SearchInput — local inline-label search field (replaces InnerLabelField). */
|
|
292
|
+
/* The whole field + its text turn white on hover/focus, fully controlled here. */
|
|
293
|
+
/* -------------------------------------------------------------------------- */
|
|
294
|
+
|
|
295
|
+
interface SearchInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
|
|
296
|
+
variant?: "SystemStyle" | "PresentationStyle";
|
|
297
|
+
theme?: Themes;
|
|
298
|
+
label?: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
|
302
|
+
({ variant = "PresentationStyle", theme, label, className, ...props }, ref) => (
|
|
303
|
+
<div className="flex p-[3px] flex-col gap-[10px] rounded-[14px] bg-[rgba(61,64,69,0.65)] shadow-[0_0_18px_0_rgba(0,0,0,0.40)] backdrop-blur-[21px]">
|
|
304
|
+
<Group
|
|
305
|
+
data-theme={theme}
|
|
306
|
+
variant={variant}
|
|
307
|
+
size="M"
|
|
308
|
+
className={cn(
|
|
309
|
+
"group flex w-full items-center gap-2 p-1 rounded-[10px] transition-colors",
|
|
310
|
+
"border border-white-alpha-40 bg-[rgba(184,192,204,0.5)]",
|
|
311
|
+
"hover:bg-background-presentation-table-row-hover",
|
|
312
|
+
"focus-within:bg-background-presentation-table-row-hover rounded-[11px] border border-white-alpha-40 bg-[rgba(37,44,57,0.30)]",
|
|
313
|
+
className,
|
|
314
|
+
)}
|
|
315
|
+
>
|
|
316
|
+
<Icon className="shrink-0">
|
|
317
|
+
<i className="ri-search-line text-content-presentation-global-secondary transition-colors group-hover:text-white group-focus-within:text-white" />
|
|
318
|
+
</Icon>
|
|
319
|
+
|
|
320
|
+
{label && (
|
|
321
|
+
<>
|
|
322
|
+
<span className="shrink-0 px-1 typography-labels-small-regular text-content-presentation-global-secondary text-[14px] text-white transition-colors group-hover:text-white group-focus-within:text-white">
|
|
323
|
+
{label}
|
|
324
|
+
</span>
|
|
325
|
+
<span className="h-[14px] w-px shrink-0 rounded-full bg-border-presentation-action-labelless-divider transition-colors group-hover:bg-white-alpha-40" />
|
|
326
|
+
</>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
<Input
|
|
330
|
+
ref={ref}
|
|
331
|
+
{...props}
|
|
332
|
+
className={cn(
|
|
333
|
+
"min-w-[100px] flex-1 !h-[24px] bg-transparent",
|
|
334
|
+
"text-content-presentation-action-light-primary",
|
|
335
|
+
"placeholder:hover:text-white-alpha-75",
|
|
336
|
+
"transition-colors group-hover:!text-white group-focus-within:!text-white",
|
|
337
|
+
)}
|
|
338
|
+
/>
|
|
339
|
+
</Group>
|
|
340
|
+
</div>
|
|
341
|
+
),
|
|
342
|
+
);
|
|
343
|
+
SearchInput.displayName = "SearchInput";
|