@schandlergarcia/sf-web-components 2.3.16 → 2.4.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/CLAUDE.md +12 -13
  3. package/README.md +0 -15
  4. package/dist/styles/global.css +46 -48
  5. package/package.json +1 -2
  6. package/scripts/apply-brand.mjs +47 -30
  7. package/scripts/postinstall.mjs +1 -11
  8. package/src/styles/global.css +46 -48
  9. package/brands/engine/PARTNER_HUB_PRD.md +0 -584
  10. package/brands/engine/agentApiConfig.ts +0 -36
  11. package/brands/engine/app/api/graphql-operations-types.ts +0 -11260
  12. package/brands/engine/app/api/graphqlClient.ts +0 -25
  13. package/brands/engine/app/api/partnerQueries.ts +0 -212
  14. package/brands/engine/app/appLayout.tsx +0 -5
  15. package/brands/engine/app/components/AgentPanel.tsx +0 -402
  16. package/brands/engine/app/components/AgentforceConversationClient.tsx +0 -201
  17. package/brands/engine/app/components/__inherit_AgentforceConversationClient.tsx +0 -3
  18. package/brands/engine/app/components/alerts/status-alert.tsx +0 -49
  19. package/brands/engine/app/components/layouts/card-layout.tsx +0 -29
  20. package/brands/engine/app/components/workspace/CommandCenter.tsx +0 -16
  21. package/brands/engine/app/config/agentApi.ts +0 -36
  22. package/brands/engine/app/features/object-search/__examples__/api/accountSearchService.ts +0 -46
  23. package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
  24. package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
  25. package/brands/engine/app/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
  26. package/brands/engine/app/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
  27. package/brands/engine/app/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -357
  28. package/brands/engine/app/features/object-search/__examples__/pages/AccountSearch.tsx +0 -312
  29. package/brands/engine/app/features/object-search/__examples__/pages/Home.tsx +0 -34
  30. package/brands/engine/app/features/object-search/api/objectSearchService.ts +0 -84
  31. package/brands/engine/app/features/object-search/components/ActiveFilters.tsx +0 -89
  32. package/brands/engine/app/features/object-search/components/FilterContext.tsx +0 -83
  33. package/brands/engine/app/features/object-search/components/ObjectBreadcrumb.tsx +0 -66
  34. package/brands/engine/app/features/object-search/components/PaginationControls.tsx +0 -109
  35. package/brands/engine/app/features/object-search/components/SearchBar.tsx +0 -41
  36. package/brands/engine/app/features/object-search/components/SortControl.tsx +0 -143
  37. package/brands/engine/app/features/object-search/components/filters/BooleanFilter.tsx +0 -78
  38. package/brands/engine/app/features/object-search/components/filters/DateFilter.tsx +0 -128
  39. package/brands/engine/app/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
  40. package/brands/engine/app/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -33
  41. package/brands/engine/app/features/object-search/components/filters/MultiSelectFilter.tsx +0 -97
  42. package/brands/engine/app/features/object-search/components/filters/NumericRangeFilter.tsx +0 -163
  43. package/brands/engine/app/features/object-search/components/filters/SearchFilter.tsx +0 -50
  44. package/brands/engine/app/features/object-search/components/filters/SelectFilter.tsx +0 -97
  45. package/brands/engine/app/features/object-search/components/filters/TextFilter.tsx +0 -91
  46. package/brands/engine/app/features/object-search/hooks/useAsyncData.ts +0 -54
  47. package/brands/engine/app/features/object-search/hooks/useCachedAsyncData.ts +0 -184
  48. package/brands/engine/app/features/object-search/hooks/useDebouncedCallback.ts +0 -34
  49. package/brands/engine/app/features/object-search/hooks/useObjectSearchParams.ts +0 -252
  50. package/brands/engine/app/features/object-search/utils/debounce.ts +0 -25
  51. package/brands/engine/app/features/object-search/utils/fieldUtils.ts +0 -29
  52. package/brands/engine/app/features/object-search/utils/filterUtils.ts +0 -404
  53. package/brands/engine/app/features/object-search/utils/sortUtils.ts +0 -38
  54. package/brands/engine/app/hooks/useEngineLiveData.ts +0 -49
  55. package/brands/engine/app/hooks/useEvaAgent.ts +0 -288
  56. package/brands/engine/app/hooks/usePartnerDashboardData.ts +0 -141
  57. package/brands/engine/app/navigationMenu.tsx +0 -80
  58. package/brands/engine/app/pages/AccountObjectDetailPage.tsx +0 -361
  59. package/brands/engine/app/pages/AccountSearch.tsx +0 -305
  60. package/brands/engine/app/pages/BlankDashboard.tsx +0 -15
  61. package/brands/engine/app/pages/DataTest.tsx +0 -78
  62. package/brands/engine/app/pages/Home.tsx +0 -5
  63. package/brands/engine/app/pages/NotFound.tsx +0 -19
  64. package/brands/engine/app/pages/PartnerHubDashboard.tsx +0 -2077
  65. package/brands/engine/app/pages/Search.tsx +0 -13
  66. package/brands/engine/app/router-utils.tsx +0 -35
  67. package/brands/engine/app/routes.tsx +0 -39
  68. package/brands/engine/app/styles/global.css +0 -269
  69. package/brands/engine/brand.css +0 -40
  70. package/brands/engine/engine-command-center-prd.md +0 -575
  71. package/brands/engine/engine-live-data.js +0 -135
  72. package/brands/engine/engine-sample-data.js +0 -378
  73. package/brands/engine/engine_logo.png +0 -0
  74. package/brands/engine/global.css +0 -269
  75. package/brands/engine/partner-hub-sample-data.js +0 -281
  76. package/brands/engine/schema.graphql +0 -292
  77. package/brands/engine/useEngineLiveData.ts +0 -49
  78. package/brands/engine/useEvaAgent.ts +0 -288
@@ -1,34 +0,0 @@
1
- import { useState } from "react";
2
- import { useNavigate } from "react-router";
3
- import { SearchBar } from "../../components/SearchBar";
4
- import { Button } from "../../../../components/ui/button";
5
-
6
- export default function HomePage() {
7
- const navigate = useNavigate();
8
- const [text, setText] = useState("");
9
-
10
- const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
11
- e.preventDefault();
12
- const params = text ? `?q=${encodeURIComponent(text)}` : "";
13
- navigate(`/accounts${params}`);
14
- };
15
-
16
- return (
17
- <div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
18
- <div className="flex items-center gap-6 mb-6">
19
- <h1 className="text-2xl font-bold">Account Search</h1>
20
- <Button variant="outline" size="sm" onClick={() => navigate("/accounts")}>
21
- Browse All Accounts
22
- </Button>
23
- </div>
24
- <form onSubmit={handleSubmit} className="flex gap-2">
25
- <SearchBar
26
- placeholder="Search by name, phone, or industry..."
27
- value={text}
28
- handleChange={setText}
29
- />
30
- <Button type="submit">Search</Button>
31
- </form>
32
- </div>
33
- );
34
- }
@@ -1,84 +0,0 @@
1
- import { createDataSDK } from "@salesforce/sdk-data";
2
-
3
- export interface ObjectSearchOptions<TWhere, TOrderBy> {
4
- where?: TWhere;
5
- orderBy?: TOrderBy;
6
- first?: number;
7
- after?: string;
8
- }
9
-
10
- export type PicklistOption = { value: string; label: string };
11
-
12
- /**
13
- * Executes a GraphQL search query and extracts the result for the given object name
14
- * from the standard `uiapi.query.<ObjectName>` response shape.
15
- */
16
- export async function searchObjects<TResult, TQuery, TVariables>(
17
- query: string,
18
- objectName: string,
19
- options: ObjectSearchOptions<unknown, unknown> = {},
20
- ): Promise<TResult> {
21
- const { where, orderBy, first = 20, after } = options;
22
-
23
- const data = await createDataSDK();
24
- const response = await data.graphql?.<TQuery, TVariables>(query, {
25
- first,
26
- after,
27
- where,
28
- orderBy,
29
- } as TVariables);
30
-
31
- if (response?.errors?.length) {
32
- throw new Error(response.errors.map((e) => e.message).join("; "));
33
- }
34
-
35
- const result = (response?.data as Record<string, unknown> | undefined)?.uiapi as
36
- | Record<string, unknown>
37
- | undefined;
38
- const queryResult = (result?.query as Record<string, unknown> | undefined)?.[objectName] as
39
- | TResult
40
- | undefined;
41
-
42
- if (!queryResult) {
43
- throw new Error(`No ${objectName} data returned`);
44
- }
45
-
46
- return queryResult;
47
- }
48
-
49
- /**
50
- * Executes a GraphQL aggregate/groupBy query and extracts picklist options
51
- * from the standard `uiapi.aggregate.<ObjectName>` response shape.
52
- */
53
- export async function fetchDistinctValues<TQuery>(
54
- query: string,
55
- objectName: string,
56
- fieldName: string,
57
- ): Promise<PicklistOption[]> {
58
- const data = await createDataSDK();
59
- const response = await data.graphql?.<TQuery>(query);
60
- const errors = response?.errors;
61
-
62
- if (errors?.length) {
63
- throw new Error(errors.map((e) => e.message).join("; "));
64
- }
65
-
66
- const result = (response?.data as Record<string, unknown> | undefined)?.uiapi as
67
- | Record<string, unknown>
68
- | undefined;
69
- const aggregate = (result?.aggregate as Record<string, unknown> | undefined)?.[objectName] as
70
- | { edges?: Array<{ node?: { aggregate?: Record<string, unknown> } }> }
71
- | undefined;
72
-
73
- const edges = aggregate?.edges ?? [];
74
- return edges
75
- .map((edge) => {
76
- const field = edge?.node?.aggregate?.[fieldName] as
77
- | { value?: string | null; displayValue?: string | null; label?: string | null }
78
- | undefined;
79
- const value = field?.value;
80
- if (!value) return null;
81
- return { value, label: field.label ?? field.displayValue ?? value };
82
- })
83
- .filter((opt): opt is PicklistOption => opt !== null);
84
- }
@@ -1,89 +0,0 @@
1
- import { X } from "lucide-react";
2
- import { Button } from "../../../components/ui/button";
3
- import { cn } from "../../../lib/utils";
4
- import type { ActiveFilterValue } from "../utils/filterUtils";
5
-
6
- function formatFilterLabel(filter: ActiveFilterValue): string {
7
- const { label, type, value, min, max } = filter;
8
-
9
- switch (type) {
10
- case "search":
11
- return `Search: ${value}`;
12
- case "text":
13
- case "picklist":
14
- return `${label}: ${value}`;
15
- case "multipicklist": {
16
- const values = value ? value.split(",") : [];
17
- if (values.length <= 2) return `${label}: ${values.join(", ")}`;
18
- return `${label}: ${values.length} selected`;
19
- }
20
- case "boolean":
21
- return `${label}: ${value === "true" ? "Yes" : "No"}`;
22
- case "numeric": {
23
- if (min && max) return `${label}: ${min} - ${max}`;
24
- if (min) return `${label}: >= ${min}`;
25
- return `${label}: <= ${max}`;
26
- }
27
- case "date": {
28
- if (min && max) return `${label}: ${min} to ${max}`;
29
- if (min) return `${label}: from ${min}`;
30
- return `${label}: until ${max}`;
31
- }
32
- default:
33
- return label;
34
- }
35
- }
36
-
37
- interface ActiveFiltersProps extends React.ComponentProps<"div"> {
38
- filters: ActiveFilterValue[];
39
- onRemove: (field: string) => void;
40
- buttonProps?: Omit<React.ComponentProps<typeof ActiveFilterButton>, "filter" | "onRemove">;
41
- }
42
-
43
- export function ActiveFilters({
44
- filters,
45
- onRemove,
46
- className,
47
- buttonProps,
48
- ...props
49
- }: ActiveFiltersProps) {
50
- if (filters.length === 0) return null;
51
-
52
- return (
53
- <div className={cn("flex flex-wrap gap-2", className)} {...props}>
54
- {filters.map((filter) => (
55
- <ActiveFilterButton
56
- key={filter.field}
57
- filter={filter}
58
- onRemove={onRemove}
59
- {...buttonProps}
60
- />
61
- ))}
62
- </div>
63
- );
64
- }
65
-
66
- interface ActiveFilterButtonProps extends React.ComponentProps<typeof Button> {
67
- filter: ActiveFilterValue;
68
- onRemove: (field: string) => void;
69
- }
70
-
71
- export function ActiveFilterButton({
72
- filter,
73
- onRemove,
74
- className,
75
- ...props
76
- }: ActiveFilterButtonProps) {
77
- return (
78
- <Button
79
- variant="outline"
80
- size="sm"
81
- className={cn("gap-1 h-7 text-xs", className)}
82
- onClick={() => onRemove(filter.field)}
83
- {...props}
84
- >
85
- {formatFilterLabel(filter)}
86
- <X className="h-3 w-3" />
87
- </Button>
88
- );
89
- }
@@ -1,83 +0,0 @@
1
- import { createContext, useContext, useCallback, type ReactNode } from "react";
2
- import { Button } from "../../../components/ui/button";
3
- import type { ActiveFilterValue } from "../utils/filterUtils";
4
-
5
- interface FilterContextValue {
6
- filters: ActiveFilterValue[];
7
- onFilterChange: (field: string, value: ActiveFilterValue | undefined) => void;
8
- onFilterRemove: (field: string) => void;
9
- onReset: () => void;
10
- }
11
-
12
- const FilterContext = createContext<FilterContextValue | null>(null);
13
-
14
- interface FilterProviderProps {
15
- filters: ActiveFilterValue[];
16
- onFilterChange: (field: string, value: ActiveFilterValue | undefined) => void;
17
- onFilterRemove: (field: string) => void;
18
- onReset: () => void;
19
- children: ReactNode;
20
- }
21
-
22
- export function FilterProvider({
23
- filters,
24
- onFilterChange,
25
- onFilterRemove,
26
- onReset,
27
- children,
28
- }: FilterProviderProps) {
29
- return (
30
- <FilterContext.Provider
31
- value={{
32
- filters,
33
- onFilterChange,
34
- onFilterRemove,
35
- onReset,
36
- }}
37
- >
38
- {children}
39
- </FilterContext.Provider>
40
- );
41
- }
42
-
43
- function useFilterContext() {
44
- const ctx = useContext(FilterContext);
45
- if (!ctx) throw new Error("useFilterField must be used within a FilterProvider");
46
- return ctx;
47
- }
48
-
49
- export function useFilterField(field: string) {
50
- const { filters, onFilterChange, onFilterRemove } = useFilterContext();
51
- const value = filters.find((f) => f.field === field);
52
- const onChange = useCallback(
53
- (next: ActiveFilterValue | undefined) => {
54
- if (next) {
55
- onFilterChange(field, next);
56
- } else {
57
- onFilterRemove(field);
58
- }
59
- },
60
- [field, onFilterChange, onFilterRemove],
61
- );
62
- return { value, onChange };
63
- }
64
-
65
- export function useFilterPanel() {
66
- const { filters, onReset } = useFilterContext();
67
- return {
68
- hasActiveFilters: filters.length > 0,
69
- resetAll: onReset,
70
- };
71
- }
72
-
73
- type FilterResetButtonProps = Omit<React.ComponentProps<typeof Button>, "onClick">;
74
-
75
- export function FilterResetButton({ children, ...props }: FilterResetButtonProps) {
76
- const { hasActiveFilters, resetAll } = useFilterPanel();
77
- if (!hasActiveFilters) return null;
78
- return (
79
- <Button onClick={resetAll} aria-label="Reset filters" variant="destructive" {...props}>
80
- {children ?? "Reset"}
81
- </Button>
82
- );
83
- }
@@ -1,66 +0,0 @@
1
- import { Link } from "react-router";
2
- import {
3
- Breadcrumb,
4
- BreadcrumbList,
5
- BreadcrumbItem,
6
- BreadcrumbLink,
7
- BreadcrumbSeparator,
8
- BreadcrumbPage,
9
- } from "../../../components/ui/breadcrumb";
10
- import { Skeleton } from "../../../components/ui/skeleton";
11
-
12
- interface ObjectBreadcrumbProps {
13
- listPath: string;
14
- listLabel: string;
15
- recordName?: string;
16
- loading?: boolean;
17
- includeHome?: boolean; // default is true
18
- homeLabel?: string; // default is "Home"
19
- }
20
-
21
- export function ObjectBreadcrumb({
22
- listPath,
23
- listLabel,
24
- recordName,
25
- loading,
26
- includeHome = true,
27
- homeLabel = "Home",
28
- }: ObjectBreadcrumbProps) {
29
- const isDetailView = loading || recordName;
30
-
31
- return (
32
- <Breadcrumb className="mb-3">
33
- <BreadcrumbList>
34
- {includeHome && (
35
- <BreadcrumbItem>
36
- <BreadcrumbLink asChild>
37
- <Link to="/">{homeLabel}</Link>
38
- </BreadcrumbLink>
39
- </BreadcrumbItem>
40
- )}
41
- <BreadcrumbSeparator />
42
- {isDetailView ? (
43
- <>
44
- <BreadcrumbItem>
45
- <BreadcrumbLink asChild>
46
- <Link to={listPath}>{listLabel}</Link>
47
- </BreadcrumbLink>
48
- </BreadcrumbItem>
49
- <BreadcrumbSeparator />
50
- <BreadcrumbItem>
51
- {loading && !recordName ? (
52
- <Skeleton className="h-4 w-32" />
53
- ) : (
54
- <BreadcrumbPage>{recordName}</BreadcrumbPage>
55
- )}
56
- </BreadcrumbItem>
57
- </>
58
- ) : (
59
- <BreadcrumbItem>
60
- <BreadcrumbPage>{listLabel}</BreadcrumbPage>
61
- </BreadcrumbItem>
62
- )}
63
- </BreadcrumbList>
64
- </Breadcrumb>
65
- );
66
- }
@@ -1,109 +0,0 @@
1
- import {
2
- Pagination,
3
- PaginationContent,
4
- PaginationItem,
5
- PaginationPrevious,
6
- PaginationNext,
7
- } from "../../../components/ui/pagination";
8
- import {
9
- Select,
10
- SelectContent,
11
- SelectItem,
12
- SelectTrigger,
13
- SelectValue,
14
- } from "../../../components/ui/select";
15
- import { Label } from "../../../components/ui/label";
16
-
17
- interface PaginationControlsProps {
18
- pageIndex: number;
19
- hasNextPage: boolean;
20
- hasPreviousPage: boolean;
21
- pageSize: number;
22
- pageSizeOptions: readonly number[];
23
- onNextPage: () => void;
24
- onPreviousPage: () => void;
25
- onPageSizeChange: (newPageSize: number) => void;
26
- disabled?: boolean;
27
- }
28
-
29
- export default function PaginationControls({
30
- pageIndex,
31
- hasNextPage,
32
- hasPreviousPage,
33
- pageSize,
34
- pageSizeOptions,
35
- onNextPage,
36
- onPreviousPage,
37
- onPageSizeChange,
38
- disabled = false,
39
- }: PaginationControlsProps) {
40
- const handlePageSizeChange = (newValue: string) => {
41
- const newSize = parseInt(newValue, 10);
42
- if (!isNaN(newSize) && newSize !== pageSize) {
43
- onPageSizeChange(newSize);
44
- }
45
- };
46
- const currentPage = pageIndex + 1;
47
- const prevDisabled = disabled || !hasPreviousPage;
48
- const nextDisabled = disabled || !hasNextPage;
49
-
50
- return (
51
- <div className="w-full grid grid-cols-1 sm:grid-cols-2 items-center justify-center gap-4 py-2">
52
- <div
53
- className="flex justify-center sm:justify-start items-center gap-2 shrink-0 row-2 sm:row-1"
54
- role="group"
55
- aria-label="Page size selector"
56
- >
57
- <Label htmlFor="page-size-select" className="text-sm font-normal whitespace-nowrap">
58
- Results per page:
59
- </Label>
60
- <Select
61
- value={pageSize.toString()}
62
- onValueChange={handlePageSizeChange}
63
- disabled={disabled}
64
- >
65
- <SelectTrigger
66
- id="page-size-select"
67
- className="w-16"
68
- aria-label="Select number of results per page"
69
- >
70
- <SelectValue />
71
- </SelectTrigger>
72
- <SelectContent>
73
- {pageSizeOptions.map((size) => (
74
- <SelectItem key={size} value={size.toString()}>
75
- {size}
76
- </SelectItem>
77
- ))}
78
- </SelectContent>
79
- </Select>
80
- </div>
81
- <Pagination className="w-full mx-0 sm:justify-end">
82
- <PaginationContent>
83
- <PaginationItem>
84
- <PaginationPrevious
85
- onClick={prevDisabled ? undefined : onPreviousPage}
86
- aria-disabled={prevDisabled}
87
- className={prevDisabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
88
- />
89
- </PaginationItem>
90
- <PaginationItem>
91
- <span
92
- className="min-w-16 text-center text-sm text-muted-foreground px-2"
93
- aria-current="page"
94
- >
95
- Page {currentPage}
96
- </span>
97
- </PaginationItem>
98
- <PaginationItem>
99
- <PaginationNext
100
- onClick={nextDisabled ? undefined : onNextPage}
101
- aria-disabled={nextDisabled}
102
- className={nextDisabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
103
- />
104
- </PaginationItem>
105
- </PaginationContent>
106
- </Pagination>
107
- </div>
108
- );
109
- }
@@ -1,41 +0,0 @@
1
- import { Search } from "lucide-react";
2
- import { Input } from "../../../components/ui/input";
3
- import { cn } from "../../../lib/utils";
4
-
5
- interface SearchBarProps extends React.ComponentProps<"div"> {
6
- value: string;
7
- handleChange: (value: string) => void;
8
- placeholder?: string;
9
- iconProps?: React.ComponentProps<typeof Search>;
10
- inputProps?: Omit<React.ComponentProps<typeof Input>, "value">;
11
- }
12
-
13
- export function SearchBar({
14
- value,
15
- handleChange,
16
- placeholder,
17
- className,
18
- iconProps,
19
- inputProps,
20
- ...props
21
- }: SearchBarProps) {
22
- return (
23
- <div className={cn("relative flex-1", className)} title={placeholder} {...props}>
24
- <Search
25
- {...iconProps}
26
- className={cn(
27
- "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground",
28
- iconProps?.className,
29
- )}
30
- />
31
- <Input
32
- type="text"
33
- value={value}
34
- onChange={(e) => handleChange(e.target.value)}
35
- placeholder={placeholder}
36
- {...inputProps}
37
- className={cn("pl-9", inputProps?.className)}
38
- />
39
- </div>
40
- );
41
- }
@@ -1,143 +0,0 @@
1
- import { ArrowUp, ArrowDown } from "lucide-react";
2
- import {
3
- Select,
4
- SelectContent,
5
- SelectItem,
6
- SelectTrigger,
7
- SelectValue,
8
- } from "../../../components/ui/select";
9
- import { Button } from "../../../components/ui/button";
10
- import { cn } from "../../../lib/utils";
11
- import type { SortFieldConfig, SortState } from "../utils/sortUtils";
12
-
13
- const NONE_VALUE = "__none__";
14
-
15
- interface SortControlProps extends React.ComponentProps<"div"> {
16
- configs: SortFieldConfig[];
17
- sort: SortState | null;
18
- onSortChange: (sort: SortState | null) => void;
19
- labelProps?: React.ComponentProps<"span">;
20
- selectProps?: Omit<
21
- React.ComponentProps<typeof SortControlSelect>,
22
- "configs" | "sort" | "onSortChange"
23
- >;
24
- directionButtonProps?: Omit<
25
- React.ComponentProps<typeof SortDirectionButton>,
26
- "sort" | "onSortChange"
27
- >;
28
- }
29
-
30
- export function SortControl({
31
- configs,
32
- sort,
33
- onSortChange,
34
- className,
35
- labelProps,
36
- selectProps,
37
- directionButtonProps,
38
- ...props
39
- }: SortControlProps) {
40
- return (
41
- <div className={cn("flex items-center gap-2", className)} {...props}>
42
- <span
43
- {...labelProps}
44
- className={cn("text-sm text-muted-foreground whitespace-nowrap", labelProps?.className)}
45
- >
46
- {labelProps?.children ?? "Sort by"}
47
- </span>
48
- <SortControlSelect
49
- configs={configs}
50
- sort={sort}
51
- onSortChange={onSortChange}
52
- {...selectProps}
53
- />
54
- {sort && (
55
- <SortDirectionButton sort={sort} onSortChange={onSortChange} {...directionButtonProps} />
56
- )}
57
- </div>
58
- );
59
- }
60
-
61
- interface SortControlSelectProps {
62
- configs: SortFieldConfig[];
63
- sort: SortState | null;
64
- onSortChange: (sort: SortState | null) => void;
65
- triggerProps?: React.ComponentProps<typeof SelectTrigger>;
66
- contentProps?: React.ComponentProps<typeof SelectContent>;
67
- selectValueProps?: React.ComponentProps<typeof SelectValue>;
68
- selectItemProps?: Omit<React.ComponentProps<typeof SelectItem>, "value">;
69
- }
70
-
71
- export function SortControlSelect({
72
- configs,
73
- sort,
74
- onSortChange,
75
- triggerProps,
76
- contentProps,
77
- selectValueProps,
78
- selectItemProps,
79
- }: SortControlSelectProps) {
80
- return (
81
- <Select
82
- value={sort?.field ?? NONE_VALUE}
83
- onValueChange={(v) => {
84
- if (v === NONE_VALUE) {
85
- onSortChange(null);
86
- } else {
87
- onSortChange({
88
- field: v,
89
- direction: sort?.direction ?? "ASC",
90
- });
91
- }
92
- }}
93
- >
94
- <SelectTrigger
95
- size="sm"
96
- {...triggerProps}
97
- className={cn("w-[160px]", triggerProps?.className)}
98
- >
99
- <SelectValue placeholder="Default" {...selectValueProps} />
100
- </SelectTrigger>
101
- <SelectContent {...contentProps}>
102
- <SelectItem value={NONE_VALUE} {...selectItemProps}>
103
- Default
104
- </SelectItem>
105
- {configs.map((c) => (
106
- <SelectItem key={c.field} value={c.field} {...selectItemProps}>
107
- {c.label}
108
- </SelectItem>
109
- ))}
110
- </SelectContent>
111
- </Select>
112
- );
113
- }
114
-
115
- interface SortDirectionButtonProps extends React.ComponentProps<typeof Button> {
116
- sort: SortState;
117
- onSortChange: (sort: SortState) => void;
118
- }
119
-
120
- export function SortDirectionButton({
121
- sort,
122
- onSortChange,
123
- className,
124
- ...props
125
- }: SortDirectionButtonProps) {
126
- return (
127
- <Button
128
- variant="ghost"
129
- size="icon-sm"
130
- className={cn(className)}
131
- onClick={() =>
132
- onSortChange({
133
- ...sort,
134
- direction: sort.direction === "ASC" ? "DESC" : "ASC",
135
- })
136
- }
137
- aria-label={`Sort ${sort.direction === "ASC" ? "descending" : "ascending"}`}
138
- {...props}
139
- >
140
- {sort.direction === "ASC" ? <ArrowUp /> : <ArrowDown />}
141
- </Button>
142
- );
143
- }