Skip to content

Commit c9caeb2

Browse files
committed
Update Nebula context selection UI
1 parent 9877176 commit c9caeb2

File tree

58 files changed

+1034
-631
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1034
-631
lines changed

apps/dashboard/src/@/components/blocks/MultiNetworkSelector.stories.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { Meta, StoryObj } from "@storybook/react";
22
import { useState } from "react";
3-
import { BadgeContainer } from "../../../stories/utils";
3+
import {
4+
BadgeContainer,
5+
storybookThirdwebClient,
6+
} from "../../../stories/utils";
47
import { MultiNetworkSelector } from "./NetworkSelectors";
58

69
const meta = {
@@ -41,6 +44,7 @@ function Variant(props: {
4144
<BadgeContainer label={props.label}>
4245
<MultiNetworkSelector
4346
selectedChainIds={chainIds}
47+
client={storybookThirdwebClient}
4448
onChange={setChainIds}
4549
/>
4650
</BadgeContainer>

apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx

+19-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { MultiSelect } from "@/components/blocks/multi-select";
44
import { SelectWithSearch } from "@/components/blocks/select-with-search";
55
import { Badge } from "@/components/ui/badge";
66
import { useCallback, useMemo } from "react";
7+
import type { ThirdwebClient } from "thirdweb";
78
import { ChainIconClient } from "../../../components/icons/ChainIcon";
89
import { useAllChainsData } from "../../../hooks/chains/allChains";
910

@@ -20,6 +21,12 @@ export function MultiNetworkSelector(props: {
2021
className?: string;
2122
priorityChains?: number[];
2223
hideTestnets?: boolean;
24+
popoverContentClassName?: string;
25+
customTrigger?: React.ReactNode;
26+
align?: "center" | "start" | "end";
27+
side?: "left" | "right" | "top" | "bottom";
28+
showSelectedValuesInModal?: boolean;
29+
client: ThirdwebClient;
2330
}) {
2431
const { allChains, idToChain } = useAllChainsData();
2532

@@ -84,8 +91,9 @@ export function MultiNetworkSelector(props: {
8491
<span className="flex grow gap-2 truncate text-left">
8592
<ChainIconClient
8693
className="size-5"
87-
ipfsSrc={chain.icon?.url}
8894
loading="lazy"
95+
src={chain.icon?.url}
96+
client={props.client}
8997
/>
9098
{cleanChainName(chain.name)}
9199
</span>
@@ -99,14 +107,19 @@ export function MultiNetworkSelector(props: {
99107
</div>
100108
);
101109
},
102-
[idToChain, props.disableChainId],
110+
[idToChain, props.disableChainId, props.client],
103111
);
104112

105113
return (
106114
<MultiSelect
107115
searchPlaceholder="Search by Name or Chain Id"
108116
selectedValues={props.selectedChainIds.map(String)}
117+
popoverContentClassName={props.popoverContentClassName}
118+
customTrigger={props.customTrigger}
109119
options={options}
120+
align={props.align}
121+
side={props.side}
122+
showSelectedValuesInModal={props.showSelectedValuesInModal}
110123
onSelectedValuesChange={(chainIds) => {
111124
props.onChange(chainIds.map(Number));
112125
}}
@@ -132,6 +145,7 @@ export function SingleNetworkSelector(props: {
132145
disableChainId?: boolean;
133146
align?: "center" | "start" | "end";
134147
placeholder?: string;
148+
client: ThirdwebClient;
135149
}) {
136150
const { allChains, idToChain } = useAllChainsData();
137151

@@ -179,7 +193,8 @@ export function SingleNetworkSelector(props: {
179193
<span className="flex grow gap-2 truncate text-left">
180194
<ChainIconClient
181195
className="size-5"
182-
ipfsSrc={chain.icon?.url}
196+
src={chain.icon?.url}
197+
client={props.client}
183198
loading="lazy"
184199
/>
185200
{cleanChainName(chain.name)}
@@ -194,7 +209,7 @@ export function SingleNetworkSelector(props: {
194209
</div>
195210
);
196211
},
197-
[idToChain, props.disableChainId],
212+
[idToChain, props.disableChainId, props.client],
198213
);
199214

200215
const isLoadingChains = allChains.length === 0;

apps/dashboard/src/@/components/blocks/SingleNetworkSelector.stories.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { Meta, StoryObj } from "@storybook/react";
22
import { useState } from "react";
3-
import { BadgeContainer } from "../../../stories/utils";
3+
import {
4+
BadgeContainer,
5+
storybookThirdwebClient,
6+
} from "../../../stories/utils";
47
import { SingleNetworkSelector } from "./NetworkSelectors";
58

69
const meta = {
@@ -43,6 +46,7 @@ function Variant(props: {
4346
return (
4447
<BadgeContainer label={props.label}>
4548
<SingleNetworkSelector
49+
client={storybookThirdwebClient}
4650
chainId={chainId}
4751
onChange={setChainId}
4852
chainIds={props.chainIds}

apps/dashboard/src/@/components/blocks/multi-select.tsx

+136-86
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,12 @@ interface MultiSelectProps
4747
searchTerm: string,
4848
) => boolean;
4949

50+
popoverContentClassName?: string;
51+
customTrigger?: React.ReactNode;
5052
renderOption?: (option: { value: string; label: string }) => React.ReactNode;
53+
align?: "center" | "start" | "end";
54+
side?: "left" | "right" | "top" | "bottom";
55+
showSelectedValuesInModal?: boolean;
5156
}
5257

5358
export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
@@ -62,6 +67,8 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
6267
overrideSearchFn,
6368
renderOption,
6469
searchPlaceholder,
70+
popoverContentClassName,
71+
showSelectedValuesInModal = false,
6572
...props
6673
},
6774
ref,
@@ -155,111 +162,100 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
155162
return (
156163
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal>
157164
<PopoverTrigger asChild>
158-
<Button
159-
ref={ref}
160-
{...props}
161-
onClick={handleTogglePopover}
162-
className={cn(
163-
"flex h-auto min-h-10 w-full items-center justify-between rounded-md border border-border bg-inherit p-3 hover:bg-inherit",
164-
className,
165-
)}
166-
>
167-
{selectedValues.length > 0 ? (
168-
<div className="flex w-full justify-between">
169-
{/* badges */}
170-
<div className="flex flex-wrap items-center gap-1.5">
171-
{selectedValues.slice(0, maxCount).map((value) => {
172-
const option = options.find((o) => o.value === value);
173-
if (!option) {
174-
return null;
175-
}
176-
177-
return (
178-
<ClosableBadge
179-
key={value}
180-
label={option.label}
181-
onClose={() => toggleOption(value)}
182-
/>
183-
);
184-
})}
165+
{props.customTrigger || (
166+
<Button
167+
ref={ref}
168+
{...props}
169+
onClick={handleTogglePopover}
170+
className={cn(
171+
"flex h-auto min-h-10 w-full items-center justify-between rounded-md border border-border bg-inherit p-3 hover:bg-inherit",
172+
className,
173+
)}
174+
>
175+
{selectedValues.length > 0 ? (
176+
<div className="flex w-full items-center justify-between">
177+
<SelectedChainsBadges
178+
selectedValues={selectedValues}
179+
options={options}
180+
maxCount={maxCount}
181+
onClose={handleClear}
182+
toggleOption={toggleOption}
183+
clearExtraOptions={clearExtraOptions}
184+
/>
185+
<div className="flex items-center justify-between gap-2">
186+
{/* Clear All */}
187+
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
188+
{/* biome-ignore lint/a11y/useFocusableInteractive: <explanation> */}
189+
<div
190+
role="button"
191+
onClick={(event) => {
192+
event.stopPropagation();
193+
handleClear();
194+
}}
195+
className="rounded p-1 hover:bg-accent"
196+
>
197+
<XIcon className="h-4 cursor-pointer text-muted-foreground" />
198+
</div>
185199

186-
{/* +X more */}
187-
{selectedValues.length > maxCount && (
188-
<ClosableBadge
189-
label={`+ ${selectedValues.length - maxCount} more`}
190-
onClose={clearExtraOptions}
200+
<Separator
201+
orientation="vertical"
202+
className="flex h-full min-h-6"
191203
/>
192-
)}
193-
</div>
194-
195-
<div className="flex items-center justify-between gap-2">
196-
{/* Clear All */}
197-
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
198-
{/* biome-ignore lint/a11y/useFocusableInteractive: <explanation> */}
199-
<div
200-
role="button"
201-
onClick={(event) => {
202-
event.stopPropagation();
203-
handleClear();
204-
}}
205-
className="rounded p-1 hover:bg-accent"
206-
>
207-
<XIcon className="h-4 cursor-pointer text-muted-foreground" />
204+
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
208205
</div>
209-
210-
<Separator
211-
orientation="vertical"
212-
className="flex h-full min-h-6"
213-
/>
206+
</div>
207+
) : (
208+
<div className="flex w-full items-center justify-between">
209+
<span className="text-muted-foreground text-sm">
210+
{placeholder}
211+
</span>
214212
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
215213
</div>
216-
</div>
217-
) : (
218-
<div className="flex w-full items-center justify-between">
219-
<span className="text-muted-foreground text-sm">
220-
{placeholder}
221-
</span>
222-
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
223-
</div>
224-
)}
225-
</Button>
214+
)}
215+
</Button>
216+
)}
226217
</PopoverTrigger>
227218
<PopoverContent
228-
className="p-0"
229-
align="center"
219+
className={cn(
220+
"flex max-h-[60vh] flex-col p-0",
221+
popoverContentClassName,
222+
)}
223+
align={props.align}
224+
side={props.side}
230225
sideOffset={10}
231226
onEscapeKeyDown={() => setIsPopoverOpen(false)}
232227
style={{
233228
width: "var(--radix-popover-trigger-width)",
234-
maxHeight: "var(--radix-popover-content-available-height)",
229+
height:
230+
"calc(var(--radix-popover-content-available-height) - 40px)",
235231
}}
236232
ref={popoverElRef}
237233
>
238-
<div>
239-
{/* Search */}
240-
<div className="relative">
241-
<Input
242-
placeholder={searchPlaceholder || "Search"}
243-
value={searchValue}
244-
onChange={(e) => setSearchValue(e.target.value)}
245-
className="!h-auto rounded-b-none border-0 border-border border-b py-4 pl-10 focus-visible:ring-0 focus-visible:ring-offset-0"
246-
onKeyDown={handleInputKeyDown}
247-
/>
248-
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-4 size-4 text-muted-foreground" />
234+
{/* Search */}
235+
<div className="relative">
236+
<Input
237+
placeholder={searchPlaceholder || "Search"}
238+
value={searchValue}
239+
onChange={(e) => setSearchValue(e.target.value)}
240+
className="!h-auto rounded-b-none border-0 border-border border-b py-4 pl-10 focus-visible:ring-0 focus-visible:ring-offset-0"
241+
onKeyDown={handleInputKeyDown}
242+
/>
243+
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-4 size-4 text-muted-foreground" />
244+
</div>
245+
246+
{optionsToShow.length === 0 && (
247+
<div className="flex flex-1 flex-col items-center justify-center py-10 text-sm">
248+
No results found
249249
</div>
250+
)}
250251

252+
{optionsToShow.length > 0 && (
251253
<ScrollShadow
252-
scrollableClassName="max-h-[min(calc(var(--radix-popover-content-available-height)-60px),350px)] p-1"
253-
className="rounded"
254+
scrollableClassName="p-1 h-full"
255+
className="flex-1 rounded"
254256
>
255257
{/* List */}
256258
<div>
257-
{optionsToShow.length === 0 && (
258-
<div className="flex justify-center py-10">
259-
No results found
260-
</div>
261-
)}
262-
263259
{optionsToShow.map((option, i) => {
264260
const isSelected = selectedValues.includes(option.value);
265261
return (
@@ -293,7 +289,20 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
293289
})}
294290
</div>
295291
</ScrollShadow>
296-
</div>
292+
)}
293+
294+
{showSelectedValuesInModal && selectedValues.length > 0 && (
295+
<div className="border-t px-3 py-3">
296+
<SelectedChainsBadges
297+
selectedValues={selectedValues}
298+
options={options}
299+
maxCount={maxCount}
300+
onClose={handleClear}
301+
toggleOption={toggleOption}
302+
clearExtraOptions={clearExtraOptions}
303+
/>
304+
</div>
305+
)}
297306
</PopoverContent>
298307
</Popover>
299308
);
@@ -319,3 +328,44 @@ function ClosableBadge(props: {
319328
}
320329

321330
MultiSelect.displayName = "MultiSelect";
331+
332+
function SelectedChainsBadges(props: {
333+
selectedValues: string[];
334+
options: {
335+
label: string;
336+
value: string;
337+
}[];
338+
maxCount: number;
339+
onClose: () => void;
340+
toggleOption: (value: string) => void;
341+
clearExtraOptions: () => void;
342+
}) {
343+
const { selectedValues, options, maxCount, toggleOption, clearExtraOptions } =
344+
props;
345+
return (
346+
<div className="flex flex-wrap items-center gap-1.5">
347+
{selectedValues.slice(0, maxCount).map((value) => {
348+
const option = options.find((o) => o.value === value);
349+
if (!option) {
350+
return null;
351+
}
352+
353+
return (
354+
<ClosableBadge
355+
key={value}
356+
label={option.label}
357+
onClose={() => toggleOption(value)}
358+
/>
359+
);
360+
})}
361+
362+
{/* +X more */}
363+
{selectedValues.length > maxCount && (
364+
<ClosableBadge
365+
label={`+ ${selectedValues.length - maxCount} more`}
366+
onClose={clearExtraOptions}
367+
/>
368+
)}
369+
</div>
370+
);
371+
}

0 commit comments

Comments
 (0)