List components #31

Merged
stne3960 merged 68 commits from list_item into main 2025-12-18 12:41:13 +01:00
8 changed files with 912 additions and 8 deletions

View File

@ -0,0 +1,249 @@
import { useState, useRef, useEffect } from "react";
import clsx from "clsx";
import TextInput from "../TextInput/TextInput";
import { SearchIcon } from "../Icon/Icon";
import SearchResultList from "../SearchResultList/SearchResultList";
import { useClickOutside } from "../../hooks/useClickOutside";
export type ComboboxSize = "sm" | "md" | "lg";
export interface ComboboxProps<T> {
options: T[];
getOptionValue: (option: T) => string;
getOptionLabel: (option: T) => string;
getOptionSubtitle?: (option: T) => string | undefined;
placeholder?: string;
label: string;
hideLabel?: boolean;
size?: ComboboxSize;
fullWidth?: boolean;
customWidth?: string;
dropdownHeight?: number;
noResultsText?: string;
multiple?: boolean;
value?: string | string[];
onChange?: (value: string | string[]) => void;
/** Called when search term changes. When provided, local filtering is disabled (assumes API filtering). */
Review

I think this component would have been better split into two, a single select and a multiple select version. If I use it with multiple={false} I would not want my onChange callback to be called with an array of choices.

I think this component would have been better split into two, a single select and a multiple select version. If I use it with `multiple={false}` I would not want my `onChange` callback to be called with an array of choices.
onSearchChange?: (term: string) => void;
}
const widthClasses: Record<ComboboxSize, string> = {
sm: "w-(--text-input-default-width-md)",
md: "w-(--text-input-default-width-md)",
lg: "w-(--text-input-default-width-lg)",
};
const dropdownWrapperClasses =
"absolute top-full left-0 z-50 w-full mt-(--spacing-sm)";
/**
* Scrolls the focused item into view when navigating with arrow keys.
* Uses "nearest" to minimize scrolling - only scrolls if the item is outside the visible area.
*/
function useScrollIntoView(
index: number,
refs: React.RefObject<(HTMLDivElement | null)[]>,
) {
useEffect(() => {
if (index >= 0 && refs.current[index]) {
refs.current[index]?.scrollIntoView({ block: "nearest" });
}
}, [index, refs]);
}
function getNextIndex(
currentIndex: number,
direction: "up" | "down",
maxIndex: number,
): number {
const next = currentIndex + (direction === "down" ? 1 : -1);
return Math.max(0, Math.min(next, maxIndex));
}
export default function Combobox<T>({
options,
getOptionValue,
getOptionLabel,
getOptionSubtitle,
placeholder = "Search...",
label,
hideLabel = false,
size = "md",
fullWidth = false,
customWidth,
dropdownHeight = 300,
noResultsText = "No results found",
multiple = false,
value,
onChange,
onSearchChange,
}: ComboboxProps<T>) {
// Convert value (undefined | string | string[]) to always be an array
const selectedValues: string[] =
value === undefined ? [] : Array.isArray(value) ? value : [value];
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [focusedIndex, setFocusedIndex] = useState(-1);
const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
/**
* Filters options by matching search term against label and subtitle.
* Skipped when onSearchChange is provided (API handles filtering).
*/
const filterOptions = (options: T[], searchTerm: string): T[] => {
const term = searchTerm.toLowerCase();
return options.filter((opt) => {
const label = getOptionLabel(opt).toLowerCase();
const subtitle = getOptionSubtitle?.(opt)?.toLowerCase() || "";
return label.includes(term) || subtitle.includes(term);
});
};
/**
* Determines what text to display in the input field.
* - If user is typing, show the search term
* - If multi-select mode, show nothing (selections appear as ListCards below)
* - If single-select with a selection, show the selected option's label
*/
const getDisplayValue = (): string => {
if (searchTerm) return searchTerm;
if (multiple) return "";
if (selectedValues.length === 0) return "";
const selected = options.find(
(opt) => getOptionValue(opt) === selectedValues[0],
);
return selected ? getOptionLabel(selected) : "";
};
// Derived state - skip local filtering when onSearchChange is provided (API handles filtering)
const filteredOptions = onSearchChange
? options
: filterOptions(options, searchTerm);
const displayValue = getDisplayValue();
const closeDropdown = () => setIsOpen(false);
useClickOutside(containerRef, closeDropdown);
useScrollIntoView(focusedIndex, itemRefs);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setSearchTerm(value);
onSearchChange?.(value);
if (value === "") {
// Clear selection when user empties the field in single-select mode
if (!multiple && selectedValues.length > 0) {
onChange?.("");
}
closeDropdown();
} else {
setIsOpen(true);
setFocusedIndex(-1);
}
};
const handleSelect = (option: T) => {
const optionValue = getOptionValue(option);
if (multiple) {
// Toggle selection in multi-select mode
const newValues = selectedValues.includes(optionValue)
? selectedValues.filter((v) => v !== optionValue)
: [...selectedValues, optionValue];
onChange?.(newValues);
} else {
// Replace selection in single-select mode
onChange?.(optionValue);
}
setSearchTerm(""); // Clear search so selected label shows
closeDropdown();
};
const handleKeyDown = (e: React.KeyboardEvent) => {
// Open dropdown on arrow keys when closed (only if we have options to show)
if (!isOpen) {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
if (filteredOptions.length > 0) {
setIsOpen(true);
}
e.preventDefault();
}
return;
}
// Handle navigation when open
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setFocusedIndex((prev) =>
getNextIndex(prev, "down", filteredOptions.length - 1),
);
break;
case "ArrowUp":
e.preventDefault();
setFocusedIndex((prev) =>
getNextIndex(prev, "up", filteredOptions.length - 1),
);
break;
case "Enter":
e.preventDefault();
if (focusedIndex >= 0 && focusedIndex < filteredOptions.length) {
handleSelect(filteredOptions[focusedIndex]);
}
break;
case "Escape":
case "Tab":
closeDropdown();
break;
}
};
const containerClasses = clsx(
"relative",
fullWidth && "w-full",
!fullWidth && !customWidth && widthClasses[size],
);
const widthStyle = customWidth ? { width: customWidth } : undefined;
return (
<div ref={containerRef} className={containerClasses} style={widthStyle}>
<TextInput
value={displayValue}
onChange={handleInputChange}
onBlur={closeDropdown}
onKeyDown={handleKeyDown}
placeholder={placeholder}
label={label}
hideLabel={hideLabel}
size={size}
fullWidth={fullWidth || !!customWidth}
customWidth={customWidth}
Icon={SearchIcon}
/>
{isOpen && (
<div className={dropdownWrapperClasses}>
<SearchResultList
options={filteredOptions}
getOptionValue={getOptionValue}
getOptionLabel={getOptionLabel}
getOptionSubtitle={getOptionSubtitle}
selectedValues={selectedValues}
focusedIndex={focusedIndex}
maxHeight={dropdownHeight}
noResultsText={noResultsText}
onSelect={handleSelect}
itemRefs={itemRefs}
/>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,70 @@
import type { HTMLAttributes } from "react";
import clsx from "clsx";
import { RemoveIcon } from "../Icon/Icon";
export interface ListCardProps
extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
title: string;
subtitle?: string;
onRemove?: () => void;
}
const baseClasses = clsx(
"px-(--padding-md) py-(--padding-md)",
"bg-sky-35 border border-sky-100 rounded-(--border-radius-md)",
"flex items-center justify-between",
"group cursor-pointer",
);
const stateClasses = clsx(
"text-base-ink-strong",
"hover:bg-sky-70 hover:text-base-ink-max",
"focus-visible:text-base-ink-max",
"focus-visible:border-primary focus-visible:border-[length:var(--border-width-sm)]",
"focus-visible:outline focus-visible:outline-sky-100 focus-visible:outline-[length:var(--border-width-lg)]",
);
const removeButtonClasses = clsx(
"shrink-0 ml-(--spacing-sm) cursor-pointer",
"text-base-ink-placeholder",
"group-hover:text-base-ink-max group-focus-visible:text-base-ink-max",
"focus-visible:outline-none",
);
export default function ListCard({
title,
subtitle,
onRemove,
className,
...props
}: ListCardProps) {
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && onRemove) {
onRemove();
}
};
return (
<div
className={clsx(baseClasses, stateClasses, className)}
tabIndex={0}
onKeyDown={handleKeyDown}
{...props}
>
<div>
<div className="body-light-sm">{title}</div>
{subtitle && <div className="body-light-sm">{subtitle}</div>}
</div>
{onRemove && (
<button
type="button"
tabIndex={-1}
onClick={onRemove}
className={removeButtonClasses}
>
<RemoveIcon />
</button>
)}
</div>
);
}

View File

@ -0,0 +1,68 @@
import type { HTMLAttributes } from "react";
import clsx from "clsx";
import { CheckmarkIcon } from "../Icon/Icon";
export interface ListItemProps
extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
title: string;
subtitle?: string;
selected?: boolean;
focused?: boolean;
}
const baseClasses =
"w-full px-(--padding-md) py-(--padding-md) cursor-pointer flex items-center justify-between";
const defaultStateClasses = clsx(
"bg-base-canvas text-base-ink-strong",
"hover:bg-sky-100 hover:text-base-ink-max",
"focus-visible:bg-sky-100 focus-visible:text-primary focus-visible:outline-none",
);
const selectedStateClasses = clsx(
"bg-base-canvas text-base-ink-placeholder",
"hover:bg-sky-100 hover:text-base-ink-max",
"focus-visible:bg-sky-100 focus-visible:text-primary focus-visible:outline-none",
);
const focusedStateClasses = "bg-sky-100 text-base-ink-max";
function getStateClasses(selected: boolean, focused: boolean): string {
if (selected && focused) return focusedStateClasses;
if (selected) return selectedStateClasses;
if (focused) return focusedStateClasses;
return defaultStateClasses;
}
export default function ListItem({
title,
subtitle,
selected = false,
focused = false,
className,
...props
}: ListItemProps) {
return (
<div
className={clsx(
baseClasses,
getStateClasses(selected, focused),
className,
)}
tabIndex={-1}
role="option"
aria-selected={selected}
{...props}
>
<div>
<div className="body-normal-md">{title}</div>
{subtitle && <div className="body-light-sm">{subtitle}</div>}
</div>
{selected && (
<div className="shrink-0 ml-(--spacing-sm)">
<CheckmarkIcon />
</div>
)}
</div>
);
}

View File

@ -0,0 +1,98 @@
import type { HTMLAttributes } from "react";
import clsx from "clsx";
import Combobox, { type ComboboxSize } from "../Combobox/Combobox";
import ListCard from "../ListCard/ListCard";
export interface ParticipantPickerProps<T>
extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
options: T[];
getOptionValue: (option: T) => string;
getOptionLabel: (option: T) => string;
getOptionSubtitle?: (option: T) => string | undefined;
value: string[];
onChange: (value: string[]) => void;
placeholder?: string;
label: string;
hideLabel?: boolean;
noResultsText?: string;
size?: ComboboxSize;
fullWidth?: boolean;
customWidth?: string;
/** Called when search term changes. When provided, local filtering is disabled (assumes API filtering). */
onSearchChange?: (term: string) => void;
}
const widthClasses: Record<ComboboxSize, string> = {
sm: "w-(--text-input-default-width-md)",
md: "w-(--text-input-default-width-md)",
lg: "w-(--text-input-default-width-lg)",
};
export default function ParticipantPicker<T>({
options,
getOptionValue,
getOptionLabel,
getOptionSubtitle,
value,
onChange,
placeholder = "Sök...",
label,
hideLabel = false,
noResultsText = "Inga resultat",
size = "md",
fullWidth = false,
customWidth,
onSearchChange,
className,
style,
...props
}: ParticipantPickerProps<T>) {
const handleRemove = (valueToRemove: string) => {
onChange(value.filter((v) => v !== valueToRemove));
};
const selectedOptions = options.filter((opt) =>
value.includes(getOptionValue(opt)),
);
const containerClasses = clsx(
"flex flex-col gap-(--spacing-sm)",
fullWidth && "w-full",
!fullWidth && !customWidth && widthClasses[size],
className,
);
const widthStyle = customWidth ? { width: customWidth, ...style } : style;
return (
<div className={containerClasses} style={widthStyle} {...props}>
<Combobox
options={options}
getOptionValue={getOptionValue}
getOptionLabel={getOptionLabel}
getOptionSubtitle={getOptionSubtitle}
value={value}
onChange={(v) => onChange(v as string[])}
placeholder={placeholder}
label={label}
hideLabel={hideLabel}
noResultsText={noResultsText}
size={size}
fullWidth
multiple
onSearchChange={onSearchChange}
/>
{selectedOptions.length > 0 && (
<div className="flex flex-col gap-(--spacing-sm)">
{selectedOptions.map((option) => (
<ListCard
key={getOptionValue(option)}
title={getOptionLabel(option)}
subtitle={getOptionSubtitle?.(option)}
onRemove={() => handleRemove(getOptionValue(option))}
/>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,81 @@
import type { HTMLAttributes } from "react";
import clsx from "clsx";
import ListItem from "../ListItem/ListItem";
export interface SearchResultListProps<T>
extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
ansv7779 marked this conversation as resolved
Review

This is limiting. Often times you want to select some more complex object as you do in your examples in ComponentLibrary. Forcing all users of the component to do their own lookup when it should be handled by the Combobox/this component.

This is limiting. Often times you want to select some more complex object as you do in your examples in `ComponentLibrary`. Forcing all users of the component to do their own lookup when it should be handled by the `Combobox`/this component.
options: T[];
getOptionValue: (option: T) => string;
getOptionLabel: (option: T) => string;
getOptionSubtitle?: (option: T) => string | undefined;
selectedValues?: string[];
focusedIndex?: number;
maxHeight?: number;
noResultsText?: string;
onSelect?: (option: T) => void;
itemRefs?: React.RefObject<(HTMLDivElement | null)[]>;
}
const baseClasses = clsx(
"w-full bg-base-canvas",
"border border-base-ink-medium rounded-(--border-radius-md)",
"overflow-y-auto",
);
const noResultsClasses =
"px-(--padding-md) py-(--padding-md) body-normal-md text-base-ink-placeholder text-center";
const dividerClasses =
"border-t border-base-ink-soft [border-top-width:var(--border-width-sm)]";
export default function SearchResultList<T>({
options,
getOptionValue,
getOptionLabel,
getOptionSubtitle,
selectedValues = [],
focusedIndex = -1,
maxHeight = 300,
noResultsText = "No results found",
onSelect,
itemRefs,
className,
style,
...props
}: SearchResultListProps<T>) {
const isSelected = (option: T) =>
selectedValues.includes(getOptionValue(option));
return (
<div
className={clsx(baseClasses, className)}
style={{ maxHeight, ...style }}
onMouseDown={(e) => e.preventDefault()}
{...props}
>
{options.length > 0 ? (
options.map((option, index) => (
<div
key={getOptionValue(option)}
ref={(el) => {
if (itemRefs?.current) {
itemRefs.current[index] = el;
}
}}
className={index > 0 ? dividerClasses : undefined}
>
<ListItem
title={getOptionLabel(option)}
subtitle={getOptionSubtitle?.(option)}
selected={isSelected(option)}
focused={index === focusedIndex}
onClick={() => onSelect?.(option)}
/>
</div>
))
) : (
<div className={noResultsClasses}>{noResultsText}</div>
)}
</div>
);
}

View File

@ -0,0 +1,17 @@
import { useEffect } from "react";
export function useClickOutside(
ref: React.RefObject<HTMLElement | null>,
onClickOutside: () => void,
) {
useEffect(() => {
const handleClick = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
onClickOutside();
}
};
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, [ref, onClickOutside]);
}

View File

@ -2,11 +2,38 @@ import { useState, useEffect } from "react";
import Button from "../components/Button/Button"; import Button from "../components/Button/Button";
import TextInput from "../components/TextInput/TextInput"; import TextInput from "../components/TextInput/TextInput";
import { SearchIcon } from "../components/Icon/Icon"; import { SearchIcon } from "../components/Icon/Icon";
import ListItem from "../components/ListItem/ListItem";
import SearchResultList from "../components/SearchResultList/SearchResultList";
import Combobox from "../components/Combobox/Combobox";
import ListCard from "../components/ListCard/ListCard";
import ParticipantPicker from "../components/ParticipantPicker/ParticipantPicker";
interface Person {
value: string;
label: string;
subtitle: string;
}
const peopleOptions: Person[] = [
{ value: "1", label: "Lennart Johansson", subtitle: "lejo1891" },
{ value: "2", label: "Mats Rubarth", subtitle: "matsrub1891" },
{ value: "3", label: "Daniel Tjernström", subtitle: "datj1891" },
{ value: "4", label: "Johan Mjällby", subtitle: "jomj1891" },
{ value: "5", label: "Krister Nordin", subtitle: "krno1891" },
{ value: "6", label: "Kurre Hamrin", subtitle: "kuha1891" },
];
const getPersonValue = (person: Person) => person.value;
const getPersonLabel = (person: Person) => person.label;
const getPersonSubtitle = (person: Person) => person.subtitle;
export default function ComponentLibrary() { export default function ComponentLibrary() {
const [darkMode, setDarkMode] = useState(() => { const [darkMode, setDarkMode] = useState(() => {
return document.documentElement.classList.contains("dark"); return document.documentElement.classList.contains("dark");
}); });
const [selectedPerson, setSelectedPerson] = useState<string>("");
const [selectedPeople, setSelectedPeople] = useState<string[]>([]);
const [participants, setParticipants] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
if (darkMode) { if (darkMode) {
@ -45,13 +72,27 @@ export default function ComponentLibrary() {
<Button size="lg">Large</Button> <Button size="lg">Large</Button>
</div> </div>
</section> </section>
<section className="mt-lg"> <section className="mt-lg">
<h2 className="mb-md">Text Input Sizes</h2> <h2 className="mb-md">Text Input Sizes</h2>
<div className="flex flex-wrap items-center gap-md"> <div className="flex flex-wrap items-center gap-md">
<TextInput size="sm" placeholder="Small" label="Small input" hideLabel /> <TextInput
<TextInput size="md" placeholder="Medium" label="Medium input" hideLabel /> size="sm"
<TextInput size="lg" placeholder="Large" label="Large input" hideLabel /> placeholder="Small"
label="Small input"
hideLabel
/>
<TextInput
size="md"
placeholder="Medium"
label="Medium input"
hideLabel
/>
<TextInput
size="lg"
placeholder="Large"
label="Large input"
hideLabel
/>
</div> </div>
</section> </section>
@ -86,14 +127,23 @@ export default function ComponentLibrary() {
<h2 className="mb-md">Text Input States</h2> <h2 className="mb-md">Text Input States</h2>
<div className="flex flex-wrap items-center gap-md"> <div className="flex flex-wrap items-center gap-md">
<TextInput placeholder="Default" label="Default state" hideLabel /> <TextInput placeholder="Default" label="Default state" hideLabel />
<TextInput placeholder="Error state" error label="Error state" hideLabel /> <TextInput
placeholder="Error state"
error
label="Error state"
hideLabel
/>
</div> </div>
</section> </section>
<section className="mt-lg"> <section className="mt-lg">
<h2 className="mb-md">Text Input With/Without Placeholder</h2> <h2 className="mb-md">Text Input With/Without Placeholder</h2>
<div className="flex flex-wrap items-center gap-md"> <div className="flex flex-wrap items-center gap-md">
<TextInput placeholder="Placeholder" label="With placeholder" hideLabel /> <TextInput
placeholder="Placeholder"
label="With placeholder"
hideLabel
/>
<TextInput label="Without placeholder" hideLabel /> <TextInput label="Without placeholder" hideLabel />
</div> </div>
</section> </section>
@ -101,8 +151,18 @@ export default function ComponentLibrary() {
<section className="mt-lg"> <section className="mt-lg">
<h2 className="mb-md">Text Input Width Options</h2> <h2 className="mb-md">Text Input Width Options</h2>
<div className="flex flex-col gap-md"> <div className="flex flex-col gap-md">
<TextInput placeholder="Full width" fullWidth label="Full width input" hideLabel /> <TextInput
<TextInput placeholder="Custom width" customWidth="300px" label="Custom width input" hideLabel /> placeholder="Full width"
fullWidth
label="Full width input"
hideLabel
/>
<TextInput
placeholder="Custom width"
customWidth="300px"
label="Custom width input"
hideLabel
/>
</div> </div>
</section> </section>
@ -131,6 +191,266 @@ export default function ComponentLibrary() {
/> />
</div> </div>
</section> </section>
<section className="mt-lg">
<h2 className="mb-md">List Item</h2>
<div className="max-w-96 border border-base-ink-soft rounded-(--border-radius-md) overflow-hidden">
<ListItem title="Lennart Johansson" subtitle="lejo1891" />
<ListItem title="Mats Rubarth" subtitle="matsrub1891" />
<ListItem title="Daniel Tjernström" subtitle="datj1891" selected />
<ListItem title="Johan Mjällby" subtitle="jomj1891" />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">List Item - Title Only</h2>
<div className="max-w-96 border border-base-ink-soft rounded-(--border-radius-md) overflow-hidden">
<ListItem title="Krister Nordin" />
<ListItem title="Kurre Hamrin" selected />
<ListItem title="Per Karlsson" />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">SearchResultList</h2>
<div className="max-w-96">
<SearchResultList
options={peopleOptions}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
getOptionSubtitle={getPersonSubtitle}
selectedValues={["3"]}
focusedIndex={1}
noResultsText="Inga resultat"
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">SearchResultList - Empty</h2>
<div className="max-w-96">
<SearchResultList
options={[]}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
noResultsText="Inga resultat"
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Combobox - Single Select</h2>
<div className="flex flex-col gap-md">
<Combobox
options={peopleOptions}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
getOptionSubtitle={getPersonSubtitle}
value={selectedPerson}
onChange={(v) => setSelectedPerson(v as string)}
placeholder="Sök..."
label="Välj person"
/>
<p className="body-light-sm text-base-ink-placeholder">
Selected:{" "}
{selectedPerson
? peopleOptions.find((p) => p.value === selectedPerson)?.label
: "None"}
</p>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Combobox - Multi Select</h2>
<div className="flex flex-col gap-md">
<Combobox
options={peopleOptions}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
getOptionSubtitle={getPersonSubtitle}
value={selectedPeople}
onChange={(v) => setSelectedPeople(v as string[])}
placeholder="Sök..."
label="Välj personer"
multiple
/>
<p className="body-light-sm text-base-ink-placeholder">
Selected:{" "}
{selectedPeople.length > 0
? selectedPeople
.map((v) => peopleOptions.find((p) => p.value === v)?.label)
.join(", ")
: "None"}
</p>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Combobox - Sizes</h2>
<div className="flex flex-wrap items-start gap-md">
<Combobox
options={peopleOptions}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
getOptionSubtitle={getPersonSubtitle}
placeholder="Small"
size="sm"
label="Small"
hideLabel
/>
<Combobox
options={peopleOptions}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
getOptionSubtitle={getPersonSubtitle}
placeholder="Medium"
size="md"
label="Medium"
hideLabel
/>
<Combobox
options={peopleOptions}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
getOptionSubtitle={getPersonSubtitle}
placeholder="Large"
size="lg"
label="Large"
hideLabel
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">Combobox - Custom Width</h2>
<div className="flex flex-col gap-md">
<Combobox
options={peopleOptions}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
getOptionSubtitle={getPersonSubtitle}
placeholder="Sök..."
customWidth="350px"
label="Custom width"
hideLabel
/>
<Combobox
options={peopleOptions}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
getOptionSubtitle={getPersonSubtitle}
placeholder="Sök..."
label="Full width"
fullWidth
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">ListCard</h2>
<div className="flex flex-col gap-md max-w-96">
<ListCard title="Lennart Johansson" onRemove={() => {}} />
<ListCard title="Mats Rubarth" onRemove={() => {}} />
<ListCard title="Daniel Tjernström" />
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">ParticipantPicker</h2>
<ParticipantPicker
options={peopleOptions}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
getOptionSubtitle={getPersonSubtitle}
value={participants}
onChange={setParticipants}
placeholder="Sök deltagare..."
label="Välj deltagare"
/>
</section>
<section className="mt-lg">
<h2 className="mb-md">ParticipantPicker - Sizes</h2>
<div className="flex flex-wrap items-start gap-md">
<ParticipantPicker
options={peopleOptions}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
getOptionSubtitle={getPersonSubtitle}
value={participants}
onChange={setParticipants}
placeholder="Small"
size="sm"
label="Small"
hideLabel
/>
<ParticipantPicker
options={peopleOptions}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
getOptionSubtitle={getPersonSubtitle}
value={participants}
onChange={setParticipants}
placeholder="Medium"
size="md"
label="Medium"
hideLabel
/>
<ParticipantPicker
options={peopleOptions}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
getOptionSubtitle={getPersonSubtitle}
value={participants}
onChange={setParticipants}
placeholder="Large"
size="lg"
label="Large"
hideLabel
/>
</div>
</section>
<section className="mt-lg">
<h2 className="mb-md">ParticipantPicker - Custom Width</h2>
<div className="flex flex-col gap-md">
<ParticipantPicker
options={peopleOptions}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
getOptionSubtitle={getPersonSubtitle}
value={participants}
onChange={setParticipants}
placeholder="Sök..."
customWidth="350px"
label="Custom width"
hideLabel
/>
<ParticipantPicker
options={peopleOptions}
getOptionValue={getPersonValue}
getOptionLabel={getPersonLabel}
getOptionSubtitle={getPersonSubtitle}
value={participants}
onChange={setParticipants}
placeholder="Sök..."
label="Full width"
fullWidth
/>
</div>
</section>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br /> <br />
<br /> <br />
<br /> <br />

View File

@ -7,6 +7,7 @@ menu.main {
left: 0; left: 0;
right: 0; right: 0;
display: flex; display: flex;
z-index: 40;
li { li {
list-style: none; list-style: none;
margin: 0; margin: 0;